agora inbox for [email protected]
help / color / mirror / Atom feedFrom: Nathan Bossart <[email protected]>
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(-)
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
</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.sgml
index 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.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)
--569rkL4UxgNPPO44--
view thread (1000+ messages)
reply
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: [email protected]
Cc: [email protected]
Subject: Re: [PATCH v3 1/1] tell client when prepared statements are deallocated
In-Reply-To: <no-message-id-1836164@localhost>
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox