From 93667104994fa0393208fa95b76d18296debc0bd Mon Sep 17 00:00:00 2001 From: Nathan Bossart Date: Tue, 2 Jun 2026 17:01:12 -0500 Subject: [PATCH v3 1/1] tell client when prepared statements are deallocated Add a PrepStmtDeallocated protocol message, negotiated via the _pq_.report_stmt_dealloc extension, that tells clients when a named prepared statement is dropped by DEALLOCATE, DISCARD, or a Close message. libpq dispatches the message to callbacks registered with PQaddPrepStmtDeallocCallback, and PQprepStmtDeallocSupported reports whether the server accepted the extension. --- doc/src/sgml/libpq.sgml | 74 +++++++++++++++++++ doc/src/sgml/protocol.sgml | 62 +++++++++++++++- src/backend/commands/prepare.c | 29 ++++++++ src/backend/tcop/backend_startup.c | 13 ++-- src/include/libpq/libpq-be.h | 3 + src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 2 + src/interfaces/libpq/fe-connect.c | 58 +++++++++++++++ src/interfaces/libpq/fe-protocol3.c | 46 ++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++ src/interfaces/libpq/libpq-fe.h | 11 +++ src/interfaces/libpq/libpq-int.h | 6 ++ .../libpq_pipeline/traces/prepared.trace | 1 + 13 files changed, 309 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml index 7d3c3bb66d8..8bc51fffad0 100644 --- a/doc/src/sgml/libpq.sgml +++ b/doc/src/sgml/libpq.sgml @@ -7253,6 +7253,80 @@ typedef struct pgNotify + + Prepared Statement Deallocation Notifications + + + prepared statement deallocation + in libpq + + + + The server can notify the client whenever a named prepared statement is + deallocated by + DEALLOCATE, + DISCARD, + , or + . This is useful when multiple + layers of client code share a connection and one drops a statement another + prepared. This behavior is negotiated through the + _pq_.report_stmt_dealloc + protocol extension, which is only available on servers running + PostgreSQL 20 and later. + libpq always requests this protocol extension. + + + + Notifications are delivered to callbacks registered with + PQaddPrepStmtDeallocCallback. + + + + PQaddPrepStmtDeallocCallbackPQaddPrepStmtDeallocCallback + + + Registers a callback to be invoked immediately upon receiving a prepared + statement deallocation notification. The value of + arg is passed unaltered to the callback. The + name argument will contain the name of the + deallocated prepared statement, or an empty string if all were + deallocated. Callbacks run while libpq + processes incoming data, so they must not call any + libpq functions on the same + conn, and they must not assume that + name survives after returning (copy it if it is + needed later). Returns 1 on success or + 0 if the callback could not be registered (e.g., due + to running out of memory). + + +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, void *arg, const char *name); + +int PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb, void *arg); + + + + + + + PQprepStmtDeallocSupportedPQprepStmtDeallocSupported + + + Returns 1 if the server accepted the + _pq_.report_stmt_dealloc protocol extension. + Otherwise, returns 0 to indicate that no prepared + statement deallocation notifications will be sent. + + +int PQprepStmtDeallocSupported(PGconn *conn); + + + + + + + + Functions Associated with the <command>COPY</command> Command diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml index 49f81676712..c881486d707 100644 --- a/doc/src/sgml/protocol.sgml +++ b/doc/src/sgml/protocol.sgml @@ -346,8 +346,15 @@ - - (No supported protocol extensions are currently defined.) + _pq_.report_stmt_dealloc + none + PostgreSQL 20 and later + When negotiated, the server sends a + PrepStmtDeallocated + message whenever a named prepared statement is deallocated by + DEALLOCATE, + DISCARD, or a + Close message. @@ -1587,6 +1594,20 @@ SELCT 1/0; point in the protocol. + + + If the client requested the + _pq_.report_stmt_dealloc + protocol extension, the backend sends a PrepStmtDeallocated message + whenever a named prepared statement is deallocated by + DEALLOCATE, + DISCARD, or a + Close message. This + alerts the client that a statement it prepared is gone (e.g., if another + layer of the client stack dropped it) and that it must be re-prepared + before reuse. The message carries the deallocated statement's name, or an + empty string to mean that all prepared statements were deallocated. + @@ -5873,6 +5894,43 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;" + + PrepStmtDeallocated (B) + + + + Byte1('i') + + + Identifies the message as a prepared statement deallocation + notification. This is sent only if the client requested the + _pq_.report_stmt_dealloc protocol extension. + + + + + + Int32 + + + Length of message contents in bytes, including self. + + + + + + String + + + The name of the deallocated prepared statement. An empty string + indicates that all prepared statements were deallocated. + + + + + + + Query (F) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..2c3e881cf42 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,26 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated (an empty + * string means all of them). Only sent to clients that requested the + * _pq_.report_stmt_dealloc protocol extension. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || !MyProcPort->report_stmt_dealloc) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -532,6 +555,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); + + /* Alert the client */ + SendStmtDeallocMsg(stmt_name); } } @@ -558,6 +584,9 @@ DropAllPreparedStatements(void) /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } + + /* Alert the client */ + SendStmtDeallocMsg(""); } /* diff --git a/src/backend/tcop/backend_startup.c b/src/backend/tcop/backend_startup.c index 25205cee0fa..7e5a2d08310 100644 --- a/src/backend/tcop/backend_startup.c +++ b/src/backend/tcop/backend_startup.c @@ -806,12 +806,15 @@ retry: else if (strncmp(nameptr, "_pq_.", 5) == 0) { /* - * Any option beginning with _pq_. is reserved for use as a - * protocol-level option, but at present no such options are - * defined. + * Options beginning with _pq_. are protocol extensions. + * Recognized ones are handled here; report the rest as + * unsupported. */ - unrecognized_protocol_options = - lappend(unrecognized_protocol_options, pstrdup(nameptr)); + if (strcmp(nameptr, "_pq_.report_stmt_dealloc") == 0) + port->report_stmt_dealloc = true; + else + unrecognized_protocol_options = + lappend(unrecognized_protocol_options, pstrdup(nameptr)); } else { diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h index 921b2daa4ff..82296a61aac 100644 --- a/src/include/libpq/libpq-be.h +++ b/src/include/libpq/libpq-be.h @@ -152,6 +152,9 @@ typedef struct Port char *cmdline_options; List *guc_options; + /* did client request prepared statement deallocation notifications? */ + bool report_stmt_dealloc; + /* * The startup packet application name, only used here for the "connection * authorized" log message. We shouldn't use this post-startup, instead diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..1cf4d4b4980 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,5 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 +PQprepStmtDeallocSupported 213 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..c55f7ec9942 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -700,6 +700,7 @@ pqDropServerData(PGconn *conn) /* Reset assorted other per-connection state */ conn->last_sqlstate[0] = '\0'; conn->pversion_negotiated = false; + conn->report_stmt_dealloc = true; /* unset if needed at conn start */ conn->auth_req_received = false; conn->client_finished_auth = false; conn->password_needed = false; @@ -5178,6 +5179,10 @@ freePGconn(PGconn *conn) free(conn->rowBuf); termPQExpBuffer(&conn->errorMessage); termPQExpBuffer(&conn->workBuffer); + if (conn->prepStmtDeallocCallbacks) + free(conn->prepStmtDeallocCallbacks); + if (conn->prepStmtDeallocCallbackArgs) + free(conn->prepStmtDeallocCallbackArgs); free(conn); } @@ -8426,3 +8431,56 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Registers a callback to be invoked whenever the server reports a prepared + * statement deallocation. arg is passed through to the callback unaltered. + */ +int +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb, + void *arg) +{ + int i; + PQprepStmtDeallocCallback *new_cbs; + void **new_args; + + if (!conn) + return 0; + + i = conn->nPrepStmtDeallocCallbacks; + + new_cbs = realloc(conn->prepStmtDeallocCallbacks, + (i + 1) * sizeof(PQprepStmtDeallocCallback)); + if (!new_cbs) + { + libpq_append_conn_error(conn, "out of memory"); + return 0; + } + conn->prepStmtDeallocCallbacks = new_cbs; + + new_args = realloc(conn->prepStmtDeallocCallbackArgs, + (i + 1) * sizeof(void *)); + if (!new_args) + { + libpq_append_conn_error(conn, "out of memory"); + return 0; + } + conn->prepStmtDeallocCallbackArgs = new_args; + + new_cbs[i] = cb; + new_args[i] = arg; + conn->nPrepStmtDeallocCallbacks = i + 1; + + return 1; +} + +/* + * Returns true if the server accepted the _pq_.report_stmt_dealloc extension. + * If false, no notifications will arrive and the caller must re-prepare on + * error. + */ +int +PQprepStmtDeallocSupported(PGconn *conn) +{ + return conn && conn->report_stmt_dealloc; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 78ffb1025d0..9da883f2ada 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,30 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Read a PrepStmtDeallocated message and invoke the registered callbacks. + * Broken out as a subroutine since it can occur in several places. + * + * Entry: 'i' message type and length already consumed. + * Exit: 0 on success, EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < conn->nPrepStmtDeallocCallbacks; i++) + { + PQprepStmtDeallocCallback cb = conn->prepStmtDeallocCallbacks[i]; + void *arg = conn->prepStmtDeallocCallbackArgs[i]; + + cb(conn, arg, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +208,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +334,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1545,6 +1578,8 @@ pqGetNegotiateProtocolVersion3(PGconn *conn) { found_test_protocol_negotiation = true; } + else if (strcmp(conn->workBuffer.data, "_pq_.report_stmt_dealloc") == 0) + conn->report_stmt_dealloc = false; else { libpq_append_conn_error(conn, "received invalid protocol negotiation message: server reported an unsupported parameter that was not requested (\"%s\")", @@ -1906,6 +1941,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2410,6 +2449,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); @@ -2525,6 +2568,9 @@ build_startup_packet(const PGconn *conn, char *packet, if (conn->client_encoding_initial && conn->client_encoding_initial[0]) ADD_STARTUP_OPTION("client_encoding", conn->client_encoding_initial); + /* Ask the server to report prepared statement deallocations. */ + ADD_STARTUP_OPTION("_pq_.report_stmt_dealloc", ""); + /* * Add the test_protocol_negotiation option when greasing, to test that * servers properly report unsupported protocol options in addition to diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..d7432ce75ce 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,17 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, void *arg, + const char *name); + +extern int PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb, + void *arg); + +/* whether the server will report prepared statement deallocations */ +extern int PQprepStmtDeallocSupported(PGconn *conn); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..0631535b58d 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -507,6 +507,8 @@ struct pg_conn int sversion; /* server version, e.g. 70401 for 7.4.1 */ bool pversion_negotiated; /* true if NegotiateProtocolVersion * was received */ + bool report_stmt_dealloc; /* true if the server accepted the + * _pq_.report_stmt_dealloc extension */ bool auth_req_received; /* true if any type of auth req received */ bool password_needed; /* true if server demanded a password */ bool gssapi_used; /* true if authenticated via gssapi */ @@ -532,6 +534,10 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks and pass-through args for prepared statement deallocations */ + PQprepStmtDeallocCallback *prepStmtDeallocCallbacks; + void **prepStmtDeallocCallbackArgs; + int nPrepStmtDeallocCallbacks; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155)