From: Nathan Bossart <nathan@postgresql.org>
Subject: [PATCH v3 1/1] tell client when prepared statements are deallocated
Date: Tue, 2 Jun 2026 17:01:12 -0500
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(-)
inline patch 1: doc/src/sgml/libpq.sgml, doc/src/sgml/protocol.sgml, src/backend/commands/prepare.c and 10 more (+309/-7 lines) download inline-1.patchdiff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgmlindex 7d3c3bb66d8..8bc51fffad0 100644--- a/doc/src/sgml/libpq.sgml+++ b/doc/src/sgml/libpq.sgml@@ -7253,6 +7253,80 @@ typedef struct pgNotify
</sect1>
+ <sect1 id="libpq-prepstmt-dealloc">+ <title>Prepared Statement Deallocation Notifications</title>++ <indexterm zone="libpq-prepstmt-dealloc">+ <primary>prepared statement deallocation</primary>+ <secondary>in libpq</secondary>+ </indexterm>++ <para>+ The server can notify the client whenever a named prepared statement is+ deallocated by+ <link linkend="sql-deallocate"><command>DEALLOCATE</command></link>,+ <link linkend="sql-discard"><command>DISCARD</command></link>,+ <xref linkend="libpq-PQclosePrepared"/>, or+ <xref linkend="libpq-PQsendClosePrepared"/>. 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+ <link linkend="protocol-extensions"><literal>_pq_.report_stmt_dealloc</literal></link>+ protocol extension, which is only available on servers running+ <productname>PostgreSQL</productname> 20 and later.+ <application>libpq</application> always requests this protocol extension.+ </para>++ <para>+ Notifications are delivered to callbacks registered with+ <function>PQaddPrepStmtDeallocCallback</function>.++ <variablelist>+ <varlistentry id="libpq-PQaddPrepStmtDeallocCallback">+ <term><function>PQaddPrepStmtDeallocCallback</function><indexterm><primary>PQaddPrepStmtDeallocCallback</primary></indexterm></term>+ <listitem>+ <para>+ Registers a callback to be invoked immediately upon receiving a prepared+ statement deallocation notification. The value of+ <parameter>arg</parameter> is passed unaltered to the callback. The+ <parameter>name</parameter> argument will contain the name of the+ deallocated prepared statement, or an empty string if all were+ deallocated. Callbacks run while <application>libpq</application>+ processes incoming data, so they must not call any+ <application>libpq</application> functions on the same+ <parameter>conn</parameter>, and they must not assume that+ <parameter>name</parameter> survives after returning (copy it if it is+ needed later). Returns <literal>1</literal> on success or+ <literal>0</literal> if the callback could not be registered (e.g., due+ to running out of memory).++<synopsis>+typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, void *arg, const char *name);++int PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb, void *arg);+</synopsis>+ </para>+ </listitem>+ </varlistentry>++ <varlistentry id="libpq-PQprepStmtDeallocSupported">+ <term><function>PQprepStmtDeallocSupported</function><indexterm><primary>PQprepStmtDeallocSupported</primary></indexterm></term>+ <listitem>+ <para>+ Returns <literal>1</literal> if the server accepted the+ <literal>_pq_.report_stmt_dealloc</literal> protocol extension.+ Otherwise, returns <literal>0</literal> to indicate that no prepared+ statement deallocation notifications will be sent.++<synopsis>+int PQprepStmtDeallocSupported(PGconn *conn);+</synopsis>+ </para>+ </listitem>+ </varlistentry>+ </variablelist>+ </para>+ </sect1>+
<sect1 id="libpq-copy">
<title>Functions Associated with the <command>COPY</command> Command</title>
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgmlindex 49f81676712..c881486d707 100644--- a/doc/src/sgml/protocol.sgml+++ b/doc/src/sgml/protocol.sgml@@ -346,8 +346,15 @@
<tbody>
<row>
- <entry namest="last" align="center" valign="middle">- <emphasis>(No supported protocol extensions are currently defined.)</emphasis>+ <entry><literal>_pq_.report_stmt_dealloc</literal></entry>+ <entry><emphasis>none</emphasis></entry>+ <entry>PostgreSQL 20 and later</entry>+ <entry>When negotiated, the server sends a+ <link linkend="protocol-message-formats-PrepStmtDeallocated">PrepStmtDeallocated</link>+ message whenever a named prepared statement is deallocated by+ <link linkend="sql-deallocate"><command>DEALLOCATE</command></link>,+ <link linkend="sql-discard"><command>DISCARD</command></link>, or a+ <link linkend="protocol-message-formats-Close">Close</link> message.
</entry>
</row>
</tbody>
@@ -1587,6 +1594,20 @@ SELCT 1/0;<!-- this typo is intentional -->
point in the protocol.
</para>
</note>
++ <para>+ If the client requested the+ <link linkend="protocol-extensions"><literal>_pq_.report_stmt_dealloc</literal></link>+ protocol extension, the backend sends a PrepStmtDeallocated message+ whenever a named prepared statement is deallocated by+ <link linkend="sql-deallocate"><command>DEALLOCATE</command></link>,+ <link linkend="sql-discard"><command>DISCARD</command></link>, or a+ <link linkend="protocol-message-formats-Close">Close</link> 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.+ </para>
</sect2>
<sect2 id="protocol-flow-canceling-requests">
@@ -5873,6 +5894,43 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;"
</listitem>
</varlistentry>
+ <varlistentry id="protocol-message-formats-PrepStmtDeallocated">+ <term>PrepStmtDeallocated (B)</term>+ <listitem>+ <variablelist>+ <varlistentry>+ <term>Byte1('i')</term>+ <listitem>+ <para>+ Identifies the message as a prepared statement deallocation+ notification. This is sent only if the client requested the+ <literal>_pq_.report_stmt_dealloc</literal> protocol extension.+ </para>+ </listitem>+ </varlistentry>++ <varlistentry>+ <term>Int32</term>+ <listitem>+ <para>+ Length of message contents in bytes, including self.+ </para>+ </listitem>+ </varlistentry>++ <varlistentry>+ <term>String</term>+ <listitem>+ <para>+ The name of the deallocated prepared statement. An empty string+ indicates that all prepared statements were deallocated.+ </para>+ </listitem>+ </varlistentry>+ </variablelist>+ </listitem>+ </varlistentry>+
<varlistentry id="protocol-message-formats-Query">
<term>Query (F)</term>
<listitem>
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.cindex 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.cindex 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.hindex 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.hindex eae8f0e7238..7ea331f7210 100644--- a/src/include/libpq/protocol.h+++ b/src/include/libpq/protocol.h@@ -53,6 +53,7 @@
#define PqMsg_FunctionCallResponse 'V'
#define PqMsg_CopyBothResponse 'W'
#define PqMsg_ReadyForQuery 'Z'
+#define PqMsg_PrepStmtDeallocated 'i'
#define PqMsg_NoData 'n'
#define PqMsg_PortalSuspended 's'
#define PqMsg_ParameterDescription 't'
diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txtindex 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 213diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.cindex 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.cindex 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.cindex c348b08c39b..e9f734187a2 100644--- a/src/interfaces/libpq/fe-trace.c+++ b/src/interfaces/libpq/fe-trace.c@@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor)
pqTraceOutputString(f, message, cursor, false);
}
+static void+pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor)+{+ fprintf(f, "PrepStmtDeallocated\t");+ pqTraceOutputString(f, message, cursor, false);+}+
static void
pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress)
{
@@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer)
else
pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor);
break;
+ case PqMsg_PrepStmtDeallocated:+ pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor);+ break;
case PqMsg_ParameterDescription:
pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress);
break;
diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.hindex 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.hindex 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.traceindex aeb5de109e0..5d36fb0056d 100644--- a/src/test/modules/libpq_pipeline/traces/prepared.trace+++ b/src/test/modules/libpq_pipeline/traces/prepared.trace@@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655
B 5 ReadyForQuery I
F 16 Close S "select_one"
F 4 Sync
+B 15 PrepStmtDeallocated "select_one"
B 4 CloseComplete
B 5 ReadyForQuery I
F 16 Describe S "select_one"
--
2.50.1 (Apple Git-155)
--569rkL4UxgNPPO44--
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Reply to all the recipients using the --to and --cc options:
reply via email
To: pgsql-hackers@postgresql.org
Cc: nathan@postgresql.org
Subject: Re: [PATCH v3 1/1] tell client when prepared statements are deallocated
In-Reply-To: <no-message-id-472088@localhost>
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
This inbox is served by DDX for PostgreSQL; see mirroring instructions
for how to clone and mirror all data and code used for this inbox