agora inbox for pgsql-hackers@postgresql.org  
help / color / mirror / Atom feed
alert clients when prepared statements are deallocated
13+ messages / 5 participants
[nested] [flat]

* alert clients when prepared statements are deallocated
@ 2026-05-29 16:33 Nathan Bossart <nathandbossart@gmail.com>
  2026-05-29 17:07 ` Re: alert clients when prepared statements are deallocated Jacob Champion <jacob.champion@enterprisedb.com>
  2026-05-29 17:09 ` Re: alert clients when prepared statements are deallocated Tom Lane <tgl@sss.pgh.pa.us>
  0 siblings, 2 replies; 13+ messages in thread

From: Nathan Bossart @ 2026-05-29 16:33 UTC (permalink / raw)
  To: Jacob Champion <jacob.champion@enterprisedb.com>; +Cc: pgsql-hackers

(Moving to a new thread since this seems like an independent feature.
Original discussion can be found here:
https://postgr.es/m/ahXE28klgxIJXBLq%40nathan)

When trying to take our own advice and teach the frontend LO interface to
use prepared statements instead of PQfn(), I discovered a couple of
problems.  The biggest problem is that clients aren't alerted when a
prepared statement is deallocated with DISCARD or DEALLOCATE.  Since this
seems like a general problem that affects more than just libpq's LO
functions, I'm seeing whether it makes sense to add some sort of
notification mechanism so that clients can re-prepare as needed.  Some
initial discussion about the work-in-progress patch (which I've attached
again here) follows:

On Fri, May 29, 2026 at 11:10:58AM -0500, Nathan Bossart wrote:
> On Fri, May 29, 2026 at 08:43:07AM -0700, Jacob Champion wrote:
>> On Fri, May 29, 2026 at 8:14 AM Nathan Bossart <nathandbossart@gmail.com> wrote:
>>> Here is a work-in-progress patch set that goes this direction.
>> 
>> At a high level, I think advertising support for a single new message
>> needs to be done in a protocol extension rather than a minor version
>> bump.
> 
> WFM
> 
>>> This
>>> introduces a callback mechanism in libpq that is used to handle statement
>>> deallocation notifications.  Older servers/clients fall back to
>>> PQexecParams(), which is slower, but the alternative is to leave PQnfn()
>>> and related code around indefinitely.
>> 
>> IMO there's no hurry in getting rid of that path. If we decide to go
>> this direction, a fallback to PQnfn() seems like it'd fine for a few
>> releases; we could eventually swap to a PQexecParams() fallback and
>> get rid of the extra code once the older servers have aged out.
> 
> That's fine with me, too.
> 
>>> I'm wondering whether this new message type is general enough.  For
>>> example, perhaps we could make an extensible message type for tracking
>>> various things.  And I want to ensure this is useful for other clients,
>>> too.
>> 
>> If it's just a general notification message, what does negotiating
>> "support" mean? Is best-effort notification okay, if the client has no
>> idea what a future message type means, or if the server doesn't send
>> the specific type of message the client is hoping for?
> 
> That's what I had in mind.  But if we don't have anything specific in mind
> that this mechanism could be extended to support, maybe we shouldn't
> bother.  Especially if we can just add protocol extensions as necessary.
> 
>> (In general, I'm kind of down on the "notify the client that X
>> happened" method of working around architectural issues. Maybe that's
>> what we need to move this specific part forward, but it doesn't feel
>> like a long-term solution and I don't know that we need to genericize
>> it without a solid set of use cases.)
> 
> I'm certainly open to other ideas, but I'm afraid this is the best I've
> come up with in my admittedly limited time thinking about the problem.

-- 
nathan
From ed962c55faf3b6ef28af6672591f460ad2289273 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Thu, 28 May 2026 15:42:20 -0500
Subject: [PATCH v2 1/3] tell client when prep stmts are deallocated

---
 src/backend/commands/prepare.c                | 30 +++++++++++++
 src/include/libpq/pqcomm.h                    |  2 +-
 src/include/libpq/protocol.h                  |  1 +
 src/interfaces/libpq/exports.txt              |  1 +
 src/interfaces/libpq/fe-connect.c             | 30 +++++++++++++
 src/interfaces/libpq/fe-protocol3.c           | 43 +++++++++++++++++++
 src/interfaces/libpq/fe-trace.c               | 10 +++++
 src/interfaces/libpq/libpq-fe.h               |  6 +++
 src/interfaces/libpq/libpq-int.h              |  2 +
 .../modules/libpq_pipeline/libpq_pipeline.c   | 12 +++---
 .../libpq_pipeline/traces/prepared.trace      |  1 +
 11 files changed, 131 insertions(+), 7 deletions(-)

diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 876aad2100a..4ca85b10f9e 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -26,6 +26,9 @@
 #include "commands/explain_state.h"
 #include "commands/prepare.h"
 #include "funcapi.h"
+#include "libpq/libpq-be.h"
+#include "libpq/pqformat.h"
+#include "miscadmin.h"
 #include "nodes/nodeFuncs.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
@@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt)
 		DropAllPreparedStatements();
 }
 
+/*
+ * Tell the client that a prepared statement has been deallocated.  Pass an
+ * empty string to indicate that all statements were deallocated.
+ *
+ * This is only sent to clients that are using protocol version 3.3 or later.
+ */
+static void
+SendStmtDeallocMsg(const char *name)
+{
+	StringInfoData buf;
+
+	if (whereToSendOutput != DestRemote)
+		return;
+	if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3))
+		return;
+
+	pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated);
+	pq_sendstring(&buf, name);
+	pq_endmessage(&buf);
+}
+
 /*
  * Internal version of DEALLOCATE
  *
@@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError)
 		/* Release the plancache entry */
 		DropCachedPlan(entry->plansource);
 
+		/* Alert the client */
+		SendStmtDeallocMsg(entry->stmt_name);
+
 		/* Now we can remove the hash table entry */
 		hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL);
 	}
@@ -548,6 +575,9 @@ DropAllPreparedStatements(void)
 	if (!prepared_queries)
 		return;
 
+	/* Alert the client */
+	SendStmtDeallocMsg("");
+
 	/* walk over cache */
 	hash_seq_init(&seq, prepared_queries);
 	while ((entry = hash_seq_search(&seq)) != NULL)
diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h
index a29c9c94d79..28e7944cdf4 100644
--- a/src/include/libpq/pqcomm.h
+++ b/src/include/libpq/pqcomm.h
@@ -92,7 +92,7 @@ is_unixsock_path(const char *path)
  * The earliest and latest frontend/backend protocol version supported.
  */
 #define PG_PROTOCOL_EARLIEST	PG_PROTOCOL(3,0)
-#define PG_PROTOCOL_LATEST		PG_PROTOCOL(3,2)
+#define PG_PROTOCOL_LATEST		PG_PROTOCOL(3,3)
 
 /*
  * Reserved protocol numbers, which have special semantics:
diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h
index eae8f0e7238..7ea331f7210 100644
--- a/src/include/libpq/protocol.h
+++ b/src/include/libpq/protocol.h
@@ -53,6 +53,7 @@
 #define PqMsg_FunctionCallResponse	'V'
 #define PqMsg_CopyBothResponse		'W'
 #define PqMsg_ReadyForQuery			'Z'
+#define PqMsg_PrepStmtDeallocated	'i'
 #define PqMsg_NoData				'n'
 #define PqMsg_PortalSuspended		's'
 #define PqMsg_ParameterDescription	't'
diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt
index 1e3d5bd5867..effd73ca3e6 100644
--- a/src/interfaces/libpq/exports.txt
+++ b/src/interfaces/libpq/exports.txt
@@ -211,3 +211,4 @@ PQdefaultAuthDataHook     208
 PQfullProtocolVersion     209
 appendPQExpBufferVA       210
 PQgetThreadLock           211
+PQaddPrepStmtDeallocCallback 212
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 4272d386e64..5e41c21c6f6 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn,
 		*result = PG_PROTOCOL(3, 2);
 		return true;
 	}
+	if (strcmp(value, "3.3") == 0)
+	{
+		*result = PG_PROTOCOL(3, 3);
+		return true;
+	}
 
 	libpq_append_conn_error(conn, "invalid %s value: \"%s\"",
 							context, value);
@@ -8426,3 +8431,28 @@ PQgetThreadLock(void)
 	Assert(pg_g_threadlock);
 	return pg_g_threadlock;
 }
+
+/*
+ * Adds a prepared statement deallocation callback to the connection's list of
+ * callbacks.  These are invoked when the server sends us
+ * PqMsg_PrepStmtDeallocated messages.
+ */
+bool
+PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb)
+{
+	if (!conn)
+		return false;
+
+	/* Add to end to preserve registration order */
+	for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++)
+	{
+		if (conn->prepStmtDeallocCallbacks[i])
+			continue;
+
+		conn->prepStmtDeallocCallbacks[i] = cb;
+		return true;
+	}
+
+	libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered");
+	return false;
+}
diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c
index 840e018cd18..0407d10362d 100644
--- a/src/interfaces/libpq/fe-protocol3.c
+++ b/src/interfaces/libpq/fe-protocol3.c
@@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet,
 								   const PQEnvironmentOption *options);
 
 
+/*
+ * Attempt to read a PrepStmtDeallocated message and invoke the connection's
+ * registered callbacks.  This is possible in several places, so we break it
+ * out as a subroutine.
+ *
+ * Entry: 'i' message type and length have already been consumed.
+ * Exit: returns 0 if successfully consumed message and invoked callbacks, or
+ *       EOF if not enough data.
+ */
+static int
+getPrepStmtDeallocated(PGconn *conn)
+{
+	if (pqGets(&conn->workBuffer, conn))
+		return EOF;
+
+	for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++)
+	{
+		if (!conn->prepStmtDeallocCallbacks[i])
+			break;
+
+		(conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data);
+	}
+
+	return 0;
+}
+
 /*
  * parseInput: if appropriate, parse input data from backend
  * until input is exhausted or a stopping state is reached.
@@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn)
 				if (getParameterStatus(conn))
 					return;
 			}
+			else if (id == PqMsg_PrepStmtDeallocated)
+			{
+				if (getPrepStmtDeallocated(conn))
+					return;
+			}
 			else
 			{
 				/* Any other case is unexpected and we summarily skip it */
@@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn)
 					if (getParameterStatus(conn))
 						return;
 					break;
+				case PqMsg_PrepStmtDeallocated:
+					if (getPrepStmtDeallocated(conn))
+						return;
+					break;
 				case PqMsg_BackendKeyData:
 
 					/*
@@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn)
 				if (getParameterStatus(conn))
 					return 0;
 				break;
+			case PqMsg_PrepStmtDeallocated:
+				if (getPrepStmtDeallocated(conn))
+					return 0;
+				break;
 			case PqMsg_CopyData:
 				return msgLength;
 			case PqMsg_CopyDone:
@@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid,
 				if (getParameterStatus(conn))
 					continue;
 				break;
+			case PqMsg_PrepStmtDeallocated:
+				if (getPrepStmtDeallocated(conn))
+					continue;
+				break;
 			default:
 				/* The backend violates the protocol. */
 				libpq_append_conn_error(conn, "protocol error: id=0x%x", id);
diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c
index c348b08c39b..e9f734187a2 100644
--- a/src/interfaces/libpq/fe-trace.c
+++ b/src/interfaces/libpq/fe-trace.c
@@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor)
 	pqTraceOutputString(f, message, cursor, false);
 }
 
+static void
+pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor)
+{
+	fprintf(f, "PrepStmtDeallocated\t");
+	pqTraceOutputString(f, message, cursor, false);
+}
+
 static void
 pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress)
 {
@@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer)
 			else
 				pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor);
 			break;
+		case PqMsg_PrepStmtDeallocated:
+			pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor);
+			break;
 		case PqMsg_ParameterDescription:
 			pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress);
 			break;
diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h
index 8ecb9b4a4c7..c57bb8806cf 100644
--- a/src/interfaces/libpq/libpq-fe.h
+++ b/src/interfaces/libpq/libpq-fe.h
@@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire);
 extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler);
 extern pgthreadlock_t PQgetThreadLock(void);
 
+/* callbacks for prepared statement deallocation notifications */
+typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name);
+
+extern bool PQaddPrepStmtDeallocCallback(PGconn *conn,
+										 PQprepStmtDeallocCallback cb);
+
 /* === in fe-trace.c === */
 extern void PQtrace(PGconn *conn, FILE *debug_port);
 extern void PQuntrace(PGconn *conn);
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 461b39620c3..7eca941ddcc 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -532,6 +532,8 @@ struct pg_conn
 	void		(*cleanup_async_auth) (PGconn *conn);
 	pgsocket	altsock;		/* alternative socket for client to poll */
 
+	/* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */
+	PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16];
 
 	/* Transient state needed while establishing connection */
 	PGTargetServerType target_server_type;	/* desired session properties */
diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c
index ee3e2ec7570..b61f33e7cd9 100644
--- a/src/test/modules/libpq_pipeline/libpq_pipeline.c
+++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c
@@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn)
 	Assert(max_protocol_version_index >= 0);
 
 	/*
-	 * Test default protocol_version (GREASE - should negotiate down to 3.2)
+	 * Test default protocol_version (GREASE - should negotiate down to 3.3)
 	 */
 	vals[max_protocol_version_index] = "";
 	conn = PQconnectdbParams(keywords, vals, false);
@@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn)
 				 PQerrorMessage(conn));
 
 	protocol_version = PQfullProtocolVersion(conn);
-	if (protocol_version != 30002)
-		pg_fatal("expected 30002, got %d", protocol_version);
+	if (protocol_version != 30003)
+		pg_fatal("expected 30003, got %d", protocol_version);
 
 	PQfinish(conn);
 
@@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn)
 	PQfinish(conn);
 
 	/*
-	 * Test max_protocol_version=latest. 'latest' currently means '3.2'.
+	 * Test max_protocol_version=latest. 'latest' currently means '3.3'.
 	 */
 	vals[max_protocol_version_index] = "latest";
 	conn = PQconnectdbParams(keywords, vals, false);
@@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn)
 				 PQerrorMessage(conn));
 
 	protocol_version = PQfullProtocolVersion(conn);
-	if (protocol_version != 30002)
-		pg_fatal("expected 30002, got %d", protocol_version);
+	if (protocol_version != 30003)
+		pg_fatal("expected 30003, got %d", protocol_version);
 
 	PQfinish(conn);
 
diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace
index aeb5de109e0..5d36fb0056d 100644
--- a/src/test/modules/libpq_pipeline/traces/prepared.trace
+++ b/src/test/modules/libpq_pipeline/traces/prepared.trace
@@ -7,6 +7,7 @@ B	113	RowDescription	 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655
 B	5	ReadyForQuery	 I
 F	16	Close	 S "select_one"
 F	4	Sync
+B	15	PrepStmtDeallocated	 "select_one"
 B	4	CloseComplete
 B	5	ReadyForQuery	 I
 F	16	Describe	 S "select_one"
-- 
2.50.1 (Apple Git-155)

Attachments:

  [text/plain] v2-0001-tell-client-when-prep-stmts-are-deallocated.patch (12.1K, ../../ahm_4eOKkkKJ3Gds@nathan/2-v2-0001-tell-client-when-prep-stmts-are-deallocated.patch)
  download | inline diff:
From ed962c55faf3b6ef28af6672591f460ad2289273 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Thu, 28 May 2026 15:42:20 -0500
Subject: [PATCH v2 1/3] tell client when prep stmts are deallocated

---
 src/backend/commands/prepare.c                | 30 +++++++++++++
 src/include/libpq/pqcomm.h                    |  2 +-
 src/include/libpq/protocol.h                  |  1 +
 src/interfaces/libpq/exports.txt              |  1 +
 src/interfaces/libpq/fe-connect.c             | 30 +++++++++++++
 src/interfaces/libpq/fe-protocol3.c           | 43 +++++++++++++++++++
 src/interfaces/libpq/fe-trace.c               | 10 +++++
 src/interfaces/libpq/libpq-fe.h               |  6 +++
 src/interfaces/libpq/libpq-int.h              |  2 +
 .../modules/libpq_pipeline/libpq_pipeline.c   | 12 +++---
 .../libpq_pipeline/traces/prepared.trace      |  1 +
 11 files changed, 131 insertions(+), 7 deletions(-)

diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 876aad2100a..4ca85b10f9e 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -26,6 +26,9 @@
 #include "commands/explain_state.h"
 #include "commands/prepare.h"
 #include "funcapi.h"
+#include "libpq/libpq-be.h"
+#include "libpq/pqformat.h"
+#include "miscadmin.h"
 #include "nodes/nodeFuncs.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_collate.h"
@@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt)
 		DropAllPreparedStatements();
 }
 
+/*
+ * Tell the client that a prepared statement has been deallocated.  Pass an
+ * empty string to indicate that all statements were deallocated.
+ *
+ * This is only sent to clients that are using protocol version 3.3 or later.
+ */
+static void
+SendStmtDeallocMsg(const char *name)
+{
+	StringInfoData buf;
+
+	if (whereToSendOutput != DestRemote)
+		return;
+	if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3))
+		return;
+
+	pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated);
+	pq_sendstring(&buf, name);
+	pq_endmessage(&buf);
+}
+
 /*
  * Internal version of DEALLOCATE
  *
@@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError)
 		/* Release the plancache entry */
 		DropCachedPlan(entry->plansource);
 
+		/* Alert the client */
+		SendStmtDeallocMsg(entry->stmt_name);
+
 		/* Now we can remove the hash table entry */
 		hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL);
 	}
@@ -548,6 +575,9 @@ DropAllPreparedStatements(void)
 	if (!prepared_queries)
 		return;
 
+	/* Alert the client */
+	SendStmtDeallocMsg("");
+
 	/* walk over cache */
 	hash_seq_init(&seq, prepared_queries);
 	while ((entry = hash_seq_search(&seq)) != NULL)
diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h
index a29c9c94d79..28e7944cdf4 100644
--- a/src/include/libpq/pqcomm.h
+++ b/src/include/libpq/pqcomm.h
@@ -92,7 +92,7 @@ is_unixsock_path(const char *path)
  * The earliest and latest frontend/backend protocol version supported.
  */
 #define PG_PROTOCOL_EARLIEST	PG_PROTOCOL(3,0)
-#define PG_PROTOCOL_LATEST		PG_PROTOCOL(3,2)
+#define PG_PROTOCOL_LATEST		PG_PROTOCOL(3,3)
 
 /*
  * Reserved protocol numbers, which have special semantics:
diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h
index eae8f0e7238..7ea331f7210 100644
--- a/src/include/libpq/protocol.h
+++ b/src/include/libpq/protocol.h
@@ -53,6 +53,7 @@
 #define PqMsg_FunctionCallResponse	'V'
 #define PqMsg_CopyBothResponse		'W'
 #define PqMsg_ReadyForQuery			'Z'
+#define PqMsg_PrepStmtDeallocated	'i'
 #define PqMsg_NoData				'n'
 #define PqMsg_PortalSuspended		's'
 #define PqMsg_ParameterDescription	't'
diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt
index 1e3d5bd5867..effd73ca3e6 100644
--- a/src/interfaces/libpq/exports.txt
+++ b/src/interfaces/libpq/exports.txt
@@ -211,3 +211,4 @@ PQdefaultAuthDataHook     208
 PQfullProtocolVersion     209
 appendPQExpBufferVA       210
 PQgetThreadLock           211
+PQaddPrepStmtDeallocCallback 212
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 4272d386e64..5e41c21c6f6 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn,
 		*result = PG_PROTOCOL(3, 2);
 		return true;
 	}
+	if (strcmp(value, "3.3") == 0)
+	{
+		*result = PG_PROTOCOL(3, 3);
+		return true;
+	}
 
 	libpq_append_conn_error(conn, "invalid %s value: \"%s\"",
 							context, value);
@@ -8426,3 +8431,28 @@ PQgetThreadLock(void)
 	Assert(pg_g_threadlock);
 	return pg_g_threadlock;
 }
+
+/*
+ * Adds a prepared statement deallocation callback to the connection's list of
+ * callbacks.  These are invoked when the server sends us
+ * PqMsg_PrepStmtDeallocated messages.
+ */
+bool
+PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb)
+{
+	if (!conn)
+		return false;
+
+	/* Add to end to preserve registration order */
+	for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++)
+	{
+		if (conn->prepStmtDeallocCallbacks[i])
+			continue;
+
+		conn->prepStmtDeallocCallbacks[i] = cb;
+		return true;
+	}
+
+	libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered");
+	return false;
+}
diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c
index 840e018cd18..0407d10362d 100644
--- a/src/interfaces/libpq/fe-protocol3.c
+++ b/src/interfaces/libpq/fe-protocol3.c
@@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet,
 								   const PQEnvironmentOption *options);
 
 
+/*
+ * Attempt to read a PrepStmtDeallocated message and invoke the connection's
+ * registered callbacks.  This is possible in several places, so we break it
+ * out as a subroutine.
+ *
+ * Entry: 'i' message type and length have already been consumed.
+ * Exit: returns 0 if successfully consumed message and invoked callbacks, or
+ *       EOF if not enough data.
+ */
+static int
+getPrepStmtDeallocated(PGconn *conn)
+{
+	if (pqGets(&conn->workBuffer, conn))
+		return EOF;
+
+	for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++)
+	{
+		if (!conn->prepStmtDeallocCallbacks[i])
+			break;
+
+		(conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data);
+	}
+
+	return 0;
+}
+
 /*
  * parseInput: if appropriate, parse input data from backend
  * until input is exhausted or a stopping state is reached.
@@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn)
 				if (getParameterStatus(conn))
 					return;
 			}
+			else if (id == PqMsg_PrepStmtDeallocated)
+			{
+				if (getPrepStmtDeallocated(conn))
+					return;
+			}
 			else
 			{
 				/* Any other case is unexpected and we summarily skip it */
@@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn)
 					if (getParameterStatus(conn))
 						return;
 					break;
+				case PqMsg_PrepStmtDeallocated:
+					if (getPrepStmtDeallocated(conn))
+						return;
+					break;
 				case PqMsg_BackendKeyData:
 
 					/*
@@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn)
 				if (getParameterStatus(conn))
 					return 0;
 				break;
+			case PqMsg_PrepStmtDeallocated:
+				if (getPrepStmtDeallocated(conn))
+					return 0;
+				break;
 			case PqMsg_CopyData:
 				return msgLength;
 			case PqMsg_CopyDone:
@@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid,
 				if (getParameterStatus(conn))
 					continue;
 				break;
+			case PqMsg_PrepStmtDeallocated:
+				if (getPrepStmtDeallocated(conn))
+					continue;
+				break;
 			default:
 				/* The backend violates the protocol. */
 				libpq_append_conn_error(conn, "protocol error: id=0x%x", id);
diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c
index c348b08c39b..e9f734187a2 100644
--- a/src/interfaces/libpq/fe-trace.c
+++ b/src/interfaces/libpq/fe-trace.c
@@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor)
 	pqTraceOutputString(f, message, cursor, false);
 }
 
+static void
+pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor)
+{
+	fprintf(f, "PrepStmtDeallocated\t");
+	pqTraceOutputString(f, message, cursor, false);
+}
+
 static void
 pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress)
 {
@@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer)
 			else
 				pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor);
 			break;
+		case PqMsg_PrepStmtDeallocated:
+			pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor);
+			break;
 		case PqMsg_ParameterDescription:
 			pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress);
 			break;
diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h
index 8ecb9b4a4c7..c57bb8806cf 100644
--- a/src/interfaces/libpq/libpq-fe.h
+++ b/src/interfaces/libpq/libpq-fe.h
@@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire);
 extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler);
 extern pgthreadlock_t PQgetThreadLock(void);
 
+/* callbacks for prepared statement deallocation notifications */
+typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name);
+
+extern bool PQaddPrepStmtDeallocCallback(PGconn *conn,
+										 PQprepStmtDeallocCallback cb);
+
 /* === in fe-trace.c === */
 extern void PQtrace(PGconn *conn, FILE *debug_port);
 extern void PQuntrace(PGconn *conn);
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 461b39620c3..7eca941ddcc 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -532,6 +532,8 @@ struct pg_conn
 	void		(*cleanup_async_auth) (PGconn *conn);
 	pgsocket	altsock;		/* alternative socket for client to poll */
 
+	/* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */
+	PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16];
 
 	/* Transient state needed while establishing connection */
 	PGTargetServerType target_server_type;	/* desired session properties */
diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c
index ee3e2ec7570..b61f33e7cd9 100644
--- a/src/test/modules/libpq_pipeline/libpq_pipeline.c
+++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c
@@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn)
 	Assert(max_protocol_version_index >= 0);
 
 	/*
-	 * Test default protocol_version (GREASE - should negotiate down to 3.2)
+	 * Test default protocol_version (GREASE - should negotiate down to 3.3)
 	 */
 	vals[max_protocol_version_index] = "";
 	conn = PQconnectdbParams(keywords, vals, false);
@@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn)
 				 PQerrorMessage(conn));
 
 	protocol_version = PQfullProtocolVersion(conn);
-	if (protocol_version != 30002)
-		pg_fatal("expected 30002, got %d", protocol_version);
+	if (protocol_version != 30003)
+		pg_fatal("expected 30003, got %d", protocol_version);
 
 	PQfinish(conn);
 
@@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn)
 	PQfinish(conn);
 
 	/*
-	 * Test max_protocol_version=latest. 'latest' currently means '3.2'.
+	 * Test max_protocol_version=latest. 'latest' currently means '3.3'.
 	 */
 	vals[max_protocol_version_index] = "latest";
 	conn = PQconnectdbParams(keywords, vals, false);
@@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn)
 				 PQerrorMessage(conn));
 
 	protocol_version = PQfullProtocolVersion(conn);
-	if (protocol_version != 30002)
-		pg_fatal("expected 30002, got %d", protocol_version);
+	if (protocol_version != 30003)
+		pg_fatal("expected 30003, got %d", protocol_version);
 
 	PQfinish(conn);
 
diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace
index aeb5de109e0..5d36fb0056d 100644
--- a/src/test/modules/libpq_pipeline/traces/prepared.trace
+++ b/src/test/modules/libpq_pipeline/traces/prepared.trace
@@ -7,6 +7,7 @@ B	113	RowDescription	 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655
 B	5	ReadyForQuery	 I
 F	16	Close	 S "select_one"
 F	4	Sync
+B	15	PrepStmtDeallocated	 "select_one"
 B	4	CloseComplete
 B	5	ReadyForQuery	 I
 F	16	Describe	 S "select_one"
-- 
2.50.1 (Apple Git-155)

^ permalink  raw  reply  [nested|flat] 13+ messages in thread

* Re: alert clients when prepared statements are deallocated
  2026-05-29 16:33 alert clients when prepared statements are deallocated Nathan Bossart <nathandbossart@gmail.com>
@ 2026-05-29 17:07 ` Jacob Champion <jacob.champion@enterprisedb.com>
  2026-06-05 09:34   ` Re: alert clients when prepared statements are deallocated Jelte Fennema-Nio <postgres@jeltef.nl>
  1 sibling, 1 reply; 13+ messages in thread

From: Jacob Champion @ 2026-05-29 17:07 UTC (permalink / raw)
  To: Nathan Bossart <nathandbossart@gmail.com>; +Cc: pgsql-hackers

On Fri, May 29, 2026 at 9:33 AM Nathan Bossart <nathandbossart@gmail.com> wrote:
> > I'm certainly open to other ideas, but I'm afraid this is the best I've
> > come up with in my admittedly limited time thinking about the problem.

Grab bag of alternatives from the other thread:

- let drivers pin protocol-level prepared statements so that
application-level DISCARD doesn't touch them
- explicitly separate client and middleware contexts or streams from each other
- rebuild discarded prepared statements opportunistically on a failure

--Jacob





^ permalink  raw  reply  [nested|flat] 13+ messages in thread

* Re: alert clients when prepared statements are deallocated
  2026-05-29 16:33 alert clients when prepared statements are deallocated Nathan Bossart <nathandbossart@gmail.com>
  2026-05-29 17:07 ` Re: alert clients when prepared statements are deallocated Jacob Champion <jacob.champion@enterprisedb.com>
@ 2026-06-05 09:34   ` Jelte Fennema-Nio <postgres@jeltef.nl>
  2026-06-05 22:03     ` Re: alert clients when prepared statements are deallocated Nathan Bossart <nathandbossart@gmail.com>
  0 siblings, 1 reply; 13+ messages in thread

From: Jelte Fennema-Nio @ 2026-06-05 09:34 UTC (permalink / raw)
  To: Jacob Champion <jacob.champion@enterprisedb.com>; +Cc: Nathan Bossart <nathandbossart@gmail.com>; pgsql-hackers

On Fri, 29 May 2026 at 19:07, Jacob Champion
<jacob.champion@enterprisedb.com> wrote:
> - let drivers pin protocol-level prepared statements so that
> application-level DISCARD doesn't touch them
> - explicitly separate client and middleware contexts or streams from each other
> - rebuild discarded prepared statements opportunistically on a failure

I think I like option 2 best (and after that 1). I'm often annoyed
that our application layer and protocol layer is so intertwined, so
any attempt to separate them is a welcome addition in my opinion.

One approach would be to:
1. add an optional additional text field to the Parse message as a
kind of "namespace" for prepared statements. Leaving this field out or
set to the empty string, would create an "application-level" prepared
statement. Setting it to anything else would create a protocol-level
prepared statement within that namespace. This would allow libpq to
create its own prepared statements, without conflicting with
protocol-level statements created by client libraries like psycopopg.
2. Application-level DISCARD/DEALLOCATE would not clean up these
namespaced protocol level prepared statements
3. Add a protocol-level version of DEALLOCATE ALL and DISCARD ALL
which also clean up all the protocol-level prepared statements
4. Add a protocol-level message to deallocate all prepared statements
in a certain schema.





^ permalink  raw  reply  [nested|flat] 13+ messages in thread

* Re: alert clients when prepared statements are deallocated
  2026-05-29 16:33 alert clients when prepared statements are deallocated Nathan Bossart <nathandbossart@gmail.com>
  2026-05-29 17:07 ` Re: alert clients when prepared statements are deallocated Jacob Champion <jacob.champion@enterprisedb.com>
  2026-06-05 09:34   ` Re: alert clients when prepared statements are deallocated Jelte Fennema-Nio <postgres@jeltef.nl>
@ 2026-06-05 22:03     ` Nathan Bossart <nathandbossart@gmail.com>
  2026-06-05 22:24       ` Re: alert clients when prepared statements are deallocated Jelte Fennema-Nio <postgres@jeltef.nl>
  0 siblings, 1 reply; 13+ messages in thread

From: Nathan Bossart @ 2026-06-05 22:03 UTC (permalink / raw)
  To: Jelte Fennema-Nio <postgres@jeltef.nl>; +Cc: Jacob Champion <jacob.champion@enterprisedb.com>; pgsql-hackers

On Fri, Jun 05, 2026 at 11:34:14AM +0200, Jelte Fennema-Nio wrote:
> I think I like option 2 best (and after that 1). I'm often annoyed
> that our application layer and protocol layer is so intertwined, so
> any attempt to separate them is a welcome addition in my opinion.
> 
> One approach would be to:
> 1. add an optional additional text field to the Parse message as a
> kind of "namespace" for prepared statements. Leaving this field out or
> set to the empty string, would create an "application-level" prepared
> statement. Setting it to anything else would create a protocol-level
> prepared statement within that namespace. This would allow libpq to
> create its own prepared statements, without conflicting with
> protocol-level statements created by client libraries like psycopopg.
> 2. Application-level DISCARD/DEALLOCATE would not clean up these
> namespaced protocol level prepared statements
> 3. Add a protocol-level version of DEALLOCATE ALL and DISCARD ALL
> which also clean up all the protocol-level prepared statements
> 4. Add a protocol-level message to deallocate all prepared statements
> in a certain schema.

Do we need to guard who can create protocol-level statements?  And if so,
how would we do that?

-- 
nathan





^ permalink  raw  reply  [nested|flat] 13+ messages in thread

* Re: alert clients when prepared statements are deallocated
  2026-05-29 16:33 alert clients when prepared statements are deallocated Nathan Bossart <nathandbossart@gmail.com>
  2026-05-29 17:07 ` Re: alert clients when prepared statements are deallocated Jacob Champion <jacob.champion@enterprisedb.com>
  2026-06-05 09:34   ` Re: alert clients when prepared statements are deallocated Jelte Fennema-Nio <postgres@jeltef.nl>
  2026-06-05 22:03     ` Re: alert clients when prepared statements are deallocated Nathan Bossart <nathandbossart@gmail.com>
@ 2026-06-05 22:24       ` Jelte Fennema-Nio <postgres@jeltef.nl>
  0 siblings, 0 replies; 13+ messages in thread

From: Jelte Fennema-Nio @ 2026-06-05 22:24 UTC (permalink / raw)
  To: Nathan Bossart <nathandbossart@gmail.com>; +Cc: Jacob Champion <jacob.champion@enterprisedb.com>; pgsql-hackers

On Sat, Jun 6, 2026, 00:03 Nathan Bossart <nathandbossart@gmail.com> wrote:

> Do we need to guard who can create protocol-level statements?  And if so,
> how would we do that?
>

I think the only thing we'd need to guard against is a libpq user messing
with libpq its own internal protocol-level namespace, let's call that
"pqnamespace" for now. We'd add a new version of PQsendPrepare that would
take a namespace, but we'd make that return an error if a user of libpq
passed pqnamespace as the namespace argument. Similarly for the new
PQsendClosePrepared variant that takes a namespace. That way we'd reserve
that namespace.

>

^ permalink  raw  reply  [nested|flat] 13+ messages in thread

* Re: alert clients when prepared statements are deallocated
  2026-05-29 16:33 alert clients when prepared statements are deallocated Nathan Bossart <nathandbossart@gmail.com>
@ 2026-05-29 17:09 ` Tom Lane <tgl@sss.pgh.pa.us>
  2026-05-29 18:29   ` Re: alert clients when prepared statements are deallocated Nathan Bossart <nathandbossart@gmail.com>
  1 sibling, 1 reply; 13+ messages in thread

From: Tom Lane @ 2026-05-29 17:09 UTC (permalink / raw)
  To: Nathan Bossart <nathandbossart@gmail.com>; +Cc: Jacob Champion <jacob.champion@enterprisedb.com>; pgsql-hackers

Nathan Bossart <nathandbossart@gmail.com> writes:
> When trying to take our own advice and teach the frontend LO interface to
> use prepared statements instead of PQfn(), I discovered a couple of
> problems.  The biggest problem is that clients aren't alerted when a
> prepared statement is deallocated with DISCARD or DEALLOCATE.

Of course the first question about that is "why doesn't the client
know that already ... didn't it issue the deallocate itself?".
The core answer to that question is that there might be multiple
levels of client code involved, so that while some level of the client
stack probably knows it in some way, other levels might have created
prepared statements and not be aware that they're gone.

Therefore, having the server report this is only a partial answer
to the problem: it will only directly provide a fix to the bottom
client code level.  To go further you'd need some inside-the-client
mechanism for propagating the notification up the client stack.
We can't really create that in general, but we can at least make
libpq be a responsible citizen in that chain.  In short, a proposed
fix for this must also provide a way for the calling application to
hear about these reports, and a way for it to fall back if they're
not available.

			regards, tom lane





^ permalink  raw  reply  [nested|flat] 13+ messages in thread

* Re: alert clients when prepared statements are deallocated
  2026-05-29 16:33 alert clients when prepared statements are deallocated Nathan Bossart <nathandbossart@gmail.com>
  2026-05-29 17:09 ` Re: alert clients when prepared statements are deallocated Tom Lane <tgl@sss.pgh.pa.us>
@ 2026-05-29 18:29   ` Nathan Bossart <nathandbossart@gmail.com>
  2026-06-02 22:11     ` Re: alert clients when prepared statements are deallocated Nathan Bossart <nathandbossart@gmail.com>
  0 siblings, 1 reply; 13+ messages in thread

From: Nathan Bossart @ 2026-05-29 18:29 UTC (permalink / raw)
  To: Tom Lane <tgl@sss.pgh.pa.us>; +Cc: Jacob Champion <jacob.champion@enterprisedb.com>; pgsql-hackers

On Fri, May 29, 2026 at 01:09:03PM -0400, Tom Lane wrote:
> Nathan Bossart <nathandbossart@gmail.com> writes:
>> When trying to take our own advice and teach the frontend LO interface to
>> use prepared statements instead of PQfn(), I discovered a couple of
>> problems.  The biggest problem is that clients aren't alerted when a
>> prepared statement is deallocated with DISCARD or DEALLOCATE.
> 
> Of course the first question about that is "why doesn't the client
> know that already ... didn't it issue the deallocate itself?".
> The core answer to that question is that there might be multiple
> levels of client code involved, so that while some level of the client
> stack probably knows it in some way, other levels might have created
> prepared statements and not be aware that they're gone.

Right.

> Therefore, having the server report this is only a partial answer
> to the problem: it will only directly provide a fix to the bottom
> client code level.  To go further you'd need some inside-the-client
> mechanism for propagating the notification up the client stack.
> We can't really create that in general, but we can at least make
> libpq be a responsible citizen in that chain.  In short, a proposed
> fix for this must also provide a way for the calling application to
> hear about these reports, and a way for it to fall back if they're
> not available.

This is the intent of the callback mechanism.  In short, a libpq user could
register a callback that runs as soon as a deallocation notification is
received.  We could also add a default callback that stores a list of
deallocated prepared statements (or a subset that a caller has indicated
interest in).  Callers could then call libpq-provided functions to retrieve
and reset that list.  My hunch is that might be more convenient for
projects that use language bindings.

-- 
nathan





^ permalink  raw  reply  [nested|flat] 13+ messages in thread

* Re: alert clients when prepared statements are deallocated
  2026-05-29 16:33 alert clients when prepared statements are deallocated Nathan Bossart <nathandbossart@gmail.com>
  2026-05-29 17:09 ` Re: alert clients when prepared statements are deallocated Tom Lane <tgl@sss.pgh.pa.us>
  2026-05-29 18:29   ` Re: alert clients when prepared statements are deallocated Nathan Bossart <nathandbossart@gmail.com>
@ 2026-06-02 22:11     ` Nathan Bossart <nathandbossart@gmail.com>
  2026-06-03 14:14       ` Re: alert clients when prepared statements are deallocated Zsolt Parragi <zsolt.parragi@percona.com>
  2026-06-03 18:59       ` Re: alert clients when prepared statements are deallocated Nathan Bossart <nathandbossart@gmail.com>
  2026-06-03 21:17       ` Re: alert clients when prepared statements are deallocated Nathan Bossart <nathandbossart@gmail.com>
  0 siblings, 3 replies; 13+ messages in thread

From: Nathan Bossart @ 2026-06-02 22:11 UTC (permalink / raw)
  To: Tom Lane <tgl@sss.pgh.pa.us>; +Cc: Jacob Champion <jacob.champion@enterprisedb.com>; pgsql-hackers

I've spent quite a bit of time on this one.  Attached is a new version of
the patch.  A few notes:

* This version uses a protocol extension instead of bumping the protocol
version.  It looks like this is the first such extension, so it's entirely
possible I'm missing something.  Note that this should work for any version
of PostgreSQL released since 2018 (see commit ae65f6066d), but IIUC older
versions will error on the protocol extension.  I'm a little concerned that
this could break pg_upgrade from early versions of v10 (which will be the
minimum supported source version in v20), so we might need to provide a way
to disable it in libpq.

* I sketched out an alternative design that would allow client applications
to retrieve the notifications at their leisure, but I stopped when I
realized this would actually add quite a bit of complexity.  We have to
think about duplicate reports, specifying which statements to collect,
clearing reports, and other subtle behavior.  I'm hopeful that the callback
mechanism is good enough for now.  If feedback indicates it is not, we can
certainly re-evaluate as a follow-up effort.

* I didn't add notifications for unnamed prepared statements.  I'm not
seeing a real use-case for that, but I admittedly haven't thought about it
too hard.

* I haven't added any tests in this patch.  My thinking is that it will get
tested as part of the libpq-LO-interface revamp in the other thread.

* I'm a little worried about race conditions involving a client trying to
use a statement while a deallocation message is in flight, but I haven't
identified anything concrete so far.  This is something I'd like to
investigate some more, though.

-- 
nathan
From 93667104994fa0393208fa95b76d18296debc0bd Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
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
 
  </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)

Attachments:

  [text/plain] v3-0001-tell-client-when-prepared-statements-are-dealloca.patch (21.0K, ../../ah9VKGwwlgqF-hi7@nathan/2-v3-0001-tell-client-when-prepared-statements-are-dealloca.patch)
  download | inline diff:
From 93667104994fa0393208fa95b76d18296debc0bd Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
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
 
  </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)

^ permalink  raw  reply  [nested|flat] 13+ messages in thread

* Re: alert clients when prepared statements are deallocated
  2026-05-29 16:33 alert clients when prepared statements are deallocated Nathan Bossart <nathandbossart@gmail.com>
  2026-05-29 17:09 ` Re: alert clients when prepared statements are deallocated Tom Lane <tgl@sss.pgh.pa.us>
  2026-05-29 18:29   ` Re: alert clients when prepared statements are deallocated Nathan Bossart <nathandbossart@gmail.com>
  2026-06-02 22:11     ` Re: alert clients when prepared statements are deallocated Nathan Bossart <nathandbossart@gmail.com>
@ 2026-06-03 14:14       ` Zsolt Parragi <zsolt.parragi@percona.com>
  2026-06-03 14:37         ` Re: alert clients when prepared statements are deallocated Nathan Bossart <nathandbossart@gmail.com>
  2 siblings, 1 reply; 13+ messages in thread

From: Zsolt Parragi @ 2026-06-03 14:14 UTC (permalink / raw)
  To: pgsql-hackers@lists.postgresql.org

Hello

@@ -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("");
 }

DropAllPreparedStatements has an early return if prepared_queries is
uninitialized, so we don't send anything in that case - but it calls
SendStmtDeallocMsg if it is initialized even when no nothing was
removed. This seems inconsistent to me, is it intentional?





^ permalink  raw  reply  [nested|flat] 13+ messages in thread

* Re: alert clients when prepared statements are deallocated
  2026-05-29 16:33 alert clients when prepared statements are deallocated Nathan Bossart <nathandbossart@gmail.com>
  2026-05-29 17:09 ` Re: alert clients when prepared statements are deallocated Tom Lane <tgl@sss.pgh.pa.us>
  2026-05-29 18:29   ` Re: alert clients when prepared statements are deallocated Nathan Bossart <nathandbossart@gmail.com>
  2026-06-02 22:11     ` Re: alert clients when prepared statements are deallocated Nathan Bossart <nathandbossart@gmail.com>
  2026-06-03 14:14       ` Re: alert clients when prepared statements are deallocated Zsolt Parragi <zsolt.parragi@percona.com>
@ 2026-06-03 14:37         ` Nathan Bossart <nathandbossart@gmail.com>
  0 siblings, 0 replies; 13+ messages in thread

From: Nathan Bossart @ 2026-06-03 14:37 UTC (permalink / raw)
  To: Zsolt Parragi <zsolt.parragi@percona.com>; +Cc: pgsql-hackers@lists.postgresql.org

On Wed, Jun 03, 2026 at 07:14:53AM -0700, Zsolt Parragi wrote:
> DropAllPreparedStatements has an early return if prepared_queries is
> uninitialized, so we don't send anything in that case - but it calls
> SendStmtDeallocMsg if it is initialized even when no nothing was
> removed. This seems inconsistent to me, is it intentional?

Nope, will fix.

-- 
nathan





^ permalink  raw  reply  [nested|flat] 13+ messages in thread

* Re: alert clients when prepared statements are deallocated
  2026-05-29 16:33 alert clients when prepared statements are deallocated Nathan Bossart <nathandbossart@gmail.com>
  2026-05-29 17:09 ` Re: alert clients when prepared statements are deallocated Tom Lane <tgl@sss.pgh.pa.us>
  2026-05-29 18:29   ` Re: alert clients when prepared statements are deallocated Nathan Bossart <nathandbossart@gmail.com>
  2026-06-02 22:11     ` Re: alert clients when prepared statements are deallocated Nathan Bossart <nathandbossart@gmail.com>
@ 2026-06-03 18:59       ` Nathan Bossart <nathandbossart@gmail.com>
  2 siblings, 0 replies; 13+ messages in thread

From: Nathan Bossart @ 2026-06-03 18:59 UTC (permalink / raw)
  To: Tom Lane <tgl@sss.pgh.pa.us>; +Cc: Jacob Champion <jacob.champion@enterprisedb.com>; pgsql-hackers

Here is a new version of the patch that adds a connection parameter for
disabling it in libpq.  I've also done a round of cleanup and added a first
draft of a real commit message.

-- 
nathan
From 8bfb65df7bf2c24a12d0c38e5a91015b3f1066b1 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 3 Jun 2026 13:26:05 -0500
Subject: [PATCH v4 1/1] Alert client when prepared statements are deallocated.

While trying to take our own advice and teach libpq's large object
interface to use prepared statements instead of fast-path function
calls, I discovered an interesting problem: a user could deallocate
prepared statements (e.g., via DISCARD ALL) without libpq knowing,
causing the large object functions to subsequently fail.  Of the
various options to deal with this problem, the cleanest one seems
to be to solve the general problem and notify clients when a
prepared statement is deallocated.  This allows different layers of
the client stack to notice when their statements are deallocated
and to preemptively re-prepare them before reuse.

This commit adds this behavior via a new protocol extension that
libpq enables by default, but that can be disabled via a connection
parameter.  libpq offers the ability to register callback functions
that are executed immediately upon receiving a deallocation
notification, as well as a function that returns whether the server
is configured to send these notifications.

This is primarily intended for use in a follow-up commit that
teaches the libpq large object interface to use prepared
statements, but the new message type and libpq interface may be
useful elsewhere.

Reviewed-by: Jacob Champion <jacob.champion@enterprisedb.com>
Reviewed-by: Zsolt Parragi <zsolt.parragi@percona.com>
Discussion: https://postgr.es/m/ahm_4eOKkkKJ3Gds%40nathan
---
 doc/src/sgml/libpq.sgml                       | 91 +++++++++++++++++++
 doc/src/sgml/protocol.sgml                    | 62 ++++++++++++-
 src/backend/commands/prepare.c                | 33 +++++++
 src/backend/tcop/backend_startup.c            | 13 ++-
 src/bin/pg_upgrade/dump.c                     |  6 +-
 src/bin/pg_upgrade/server.c                   |  3 +
 src/bin/pg_upgrade/task.c                     |  3 +
 src/bin/pg_upgrade/version.c                  |  3 +-
 src/include/libpq/libpq-be.h                  |  1 +
 src/include/libpq/protocol.h                  |  1 +
 src/interfaces/libpq/exports.txt              |  2 +
 src/interfaces/libpq/fe-connect.c             | 63 +++++++++++++
 src/interfaces/libpq/fe-protocol3.c           | 56 ++++++++++++
 src/interfaces/libpq/fe-trace.c               | 10 ++
 src/interfaces/libpq/libpq-fe.h               |  9 ++
 src/interfaces/libpq/libpq-int.h              |  6 ++
 .../libpq_pipeline/traces/prepared.trace      |  1 +
 17 files changed, 354 insertions(+), 9 deletions(-)

diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 7d3c3bb66d8..f76b31264f5 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -2297,6 +2297,20 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-report-prep-stmt-dealloc" xreflabel="report_prep_stmt_dealloc">
+      <term><literal>report_prep_stmt_dealloc</literal></term>
+      <listitem>
+       <para>
+        If set to <literal>1</literal> (the default),
+        <application>libpq</application> requests the
+        <link linkend="protocol-extensions"><literal>_pq_.report_prep_stmt_dealloc</literal></link>
+        protocol extension, so that the server reports when prepared statements
+        are deallocated.  Set it to <literal>0</literal> to suppress the
+        request.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-scram-client-key" xreflabel="scram_client_key">
       <term><literal>scram_client_key</literal></term>
       <listitem>
@@ -7253,6 +7267,83 @@ 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_prep_stmt_dealloc</literal></link>
+   protocol extension, which is only available on servers running
+   <productname>PostgreSQL</productname> 20 and later.
+   By default, <application>libpq</application> requests this protocol
+   extension; this can be disabled with the
+   <xref linkend="libpq-connect-report-prep-stmt-dealloc"/> connection
+   parameter.
+  </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-PQprepStmtDeallocReporting">
+     <term><function>PQprepStmtDeallocReporting</function><indexterm><primary>PQprepStmtDeallocReporting</primary></indexterm></term>
+     <listitem>
+      <para>
+       Returns <literal>1</literal> if the server accepted the
+       <literal>_pq_.report_prep_stmt_dealloc</literal> protocol extension.
+       Otherwise, returns <literal>0</literal> to indicate that no prepared
+       statement deallocation notifications will be sent.
+
+<synopsis>
+int PQprepStmtDeallocReporting(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..70ecc451efd 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_prep_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-PrepStmtDealloc">PrepStmtDealloc</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_prep_stmt_dealloc</literal></link>
+    protocol extension, the backend sends a PrepStmtDealloc 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-PrepStmtDealloc">
+    <term>PrepStmtDealloc (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_prep_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..4e7af002d6d 100644
--- a/src/backend/commands/prepare.c
+++ b/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_prep_stmt_dealloc protocol extension.
+ */
+static void
+SendStmtDeallocMsg(const char *name)
+{
+	StringInfoData buf;
+
+	if (whereToSendOutput != DestRemote)
+		return;
+	if (!MyProcPort || !MyProcPort->report_prep_stmt_dealloc)
+		return;
+
+	pq_beginmessage(&buf, PqMsg_PrepStmtDealloc);
+	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);
 	}
 }
 
@@ -543,6 +569,7 @@ DropAllPreparedStatements(void)
 {
 	HASH_SEQ_STATUS seq;
 	PreparedStatement *entry;
+	bool		found = false;
 
 	/* nothing cached */
 	if (!prepared_queries)
@@ -552,12 +579,18 @@ DropAllPreparedStatements(void)
 	hash_seq_init(&seq, prepared_queries);
 	while ((entry = hash_seq_search(&seq)) != NULL)
 	{
+		found = true;
+
 		/* Release the plancache entry */
 		DropCachedPlan(entry->plansource);
 
 		/* Now we can remove the hash table entry */
 		hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL);
 	}
+
+	/* Alert the client */
+	if (found)
+		SendStmtDeallocMsg("");
 }
 
 /*
diff --git a/src/backend/tcop/backend_startup.c b/src/backend/tcop/backend_startup.c
index 25205cee0fa..a9158e86ea1 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_prep_stmt_dealloc") == 0)
+					port->report_prep_stmt_dealloc = true;
+				else
+					unrecognized_protocol_options =
+						lappend(unrecognized_protocol_options, pstrdup(nameptr));
 			}
 			else
 			{
diff --git a/src/bin/pg_upgrade/dump.c b/src/bin/pg_upgrade/dump.c
index f47c8d06211..8d542903cb6 100644
--- a/src/bin/pg_upgrade/dump.c
+++ b/src/bin/pg_upgrade/dump.c
@@ -24,7 +24,8 @@ generate_old_dump(void)
 			  "\"%s/pg_dumpall\" %s%s --globals-only --quote-all-identifiers "
 			  "--binary-upgrade %s --no-sync -f \"%s/%s\"",
 			  new_cluster.bindir, cluster_conn_opts(&old_cluster),
-			  protocol_negotiation_supported(&old_cluster) ? "" : " -d \"max_protocol_version=3.0\"",
+			  protocol_negotiation_supported(&old_cluster) ?
+			  "" : " -d \"max_protocol_version=3.0 report_prep_stmt_dealloc=0\"",
 			  log_opts.verbose ? "--verbose" : "",
 			  log_opts.dumpdir,
 			  GLOBALS_DUMP_FILE);
@@ -45,7 +46,10 @@ generate_old_dump(void)
 		appendPQExpBufferStr(&connstr, "dbname=");
 		appendConnStrVal(&connstr, old_db->db_name);
 		if (!protocol_negotiation_supported(&old_cluster))
+		{
 			appendPQExpBufferStr(&connstr, " max_protocol_version=3.0");
+			appendPQExpBufferStr(&connstr, " report_prep_stmt_dealloc=0");
+		}
 
 		initPQExpBuffer(&escaped_connstr);
 		appendShellString(&escaped_connstr, connstr.data);
diff --git a/src/bin/pg_upgrade/server.c b/src/bin/pg_upgrade/server.c
index 5d81e4e95b8..841802b2b14 100644
--- a/src/bin/pg_upgrade/server.c
+++ b/src/bin/pg_upgrade/server.c
@@ -72,7 +72,10 @@ get_db_conn(ClusterInfo *cluster, const char *db_name)
 		appendConnStrVal(&conn_opts, cluster->sockdir);
 	}
 	if (!protocol_negotiation_supported(cluster))
+	{
 		appendPQExpBufferStr(&conn_opts, " max_protocol_version=3.0");
+		appendPQExpBufferStr(&conn_opts, " report_prep_stmt_dealloc=0");
+	}
 
 	conn = PQconnectdb(conn_opts.data);
 	termPQExpBuffer(&conn_opts);
diff --git a/src/bin/pg_upgrade/task.c b/src/bin/pg_upgrade/task.c
index b6eb29e1f3a..4c64bbabdcf 100644
--- a/src/bin/pg_upgrade/task.c
+++ b/src/bin/pg_upgrade/task.c
@@ -189,7 +189,10 @@ start_conn(const ClusterInfo *cluster, UpgradeTaskSlot *slot)
 		appendConnStrVal(&conn_opts, cluster->sockdir);
 	}
 	if (!protocol_negotiation_supported(cluster))
+	{
 		appendPQExpBufferStr(&conn_opts, " max_protocol_version=3.0");
+		appendPQExpBufferStr(&conn_opts, " report_prep_stmt_dealloc=0");
+	}
 
 	slot->conn = PQconnectStart(conn_opts.data);
 
diff --git a/src/bin/pg_upgrade/version.c b/src/bin/pg_upgrade/version.c
index 047670d4acb..9a86b9a8a55 100644
--- a/src/bin/pg_upgrade/version.c
+++ b/src/bin/pg_upgrade/version.c
@@ -30,7 +30,8 @@ jsonb_9_4_check_applicable(ClusterInfo *cluster)
 
 /*
  * Older servers can't support newer protocol versions, so their connection
- * strings will need to lock max_protocol_version to 3.0.
+ * strings will need to lock max_protocol_version to 3.0 and disable prepared
+ * statement deallocation reports.
  */
 bool
 protocol_negotiation_supported(const ClusterInfo *cluster)
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 921b2daa4ff..1fbf08fc420 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -151,6 +151,7 @@ typedef struct Port
 	char	   *user_name;
 	char	   *cmdline_options;
 	List	   *guc_options;
+	bool		report_prep_stmt_dealloc;
 
 	/*
 	 * The startup packet application name, only used here for the "connection
diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h
index eae8f0e7238..aee0675594d 100644
--- a/src/include/libpq/protocol.h
+++ b/src/include/libpq/protocol.h
@@ -53,6 +53,7 @@
 #define PqMsg_FunctionCallResponse	'V'
 #define PqMsg_CopyBothResponse		'W'
 #define PqMsg_ReadyForQuery			'Z'
+#define PqMsg_PrepStmtDealloc		'i'
 #define PqMsg_NoData				'n'
 #define PqMsg_PortalSuspended		's'
 #define PqMsg_ParameterDescription	't'
diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt
index 1e3d5bd5867..9413412e65d 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
+PQprepStmtDeallocReporting 213
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 4272d386e64..1f0219d09a3 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -421,6 +421,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"SSL-Key-Log-File", "D", 64,
 	offsetof(struct pg_conn, sslkeylogfile)},
 
+	{"report_prep_stmt_dealloc", NULL, "1", NULL,
+		"Report-Prepared-Statement-Deallocations", "", 1,
+	offsetof(struct pg_conn, report_prep_stmt_dealloc)},
+
 	/* Terminating entry --- MUST BE LAST */
 	{NULL, NULL, NULL, NULL,
 	NULL, NULL, 0}
@@ -708,6 +712,7 @@ pqDropServerData(PGconn *conn)
 	free(conn->write_err_msg);
 	conn->write_err_msg = NULL;
 	conn->oauth_want_retry = false;
+	conn->prepStmtDeallocReporting = false;
 
 	/*
 	 * Cancel connections need to retain their be_pid and be_cancel_key across
@@ -5178,6 +5183,11 @@ freePGconn(PGconn *conn)
 	free(conn->rowBuf);
 	termPQExpBuffer(&conn->errorMessage);
 	termPQExpBuffer(&conn->workBuffer);
+	free(conn->report_prep_stmt_dealloc);
+	if (conn->prepStmtDeallocCallbacks)
+		free(conn->prepStmtDeallocCallbacks);
+	if (conn->prepStmtDeallocCallbackArgs)
+		free(conn->prepStmtDeallocCallbackArgs);
 
 	free(conn);
 }
@@ -8426,3 +8436,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_prep_stmt_dealloc
+ * extension.  If false, no notifications will arrive and the caller must
+ * re-prepare on error.
+ */
+int
+PQprepStmtDeallocReporting(PGconn *conn)
+{
+	return conn && conn->prepStmtDeallocReporting;
+}
diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c
index 78ffb1025d0..f5cc361fb73 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 PrepStmtDealloc 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
+getPrepStmtDealloc(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_PrepStmtDealloc)
+			{
+				if (getPrepStmtDealloc(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_PrepStmtDealloc:
+					if (getPrepStmtDealloc(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_prep_stmt_dealloc") == 0)
+			conn->prepStmtDeallocReporting = 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_PrepStmtDealloc:
+				if (getPrepStmtDealloc(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_PrepStmtDealloc:
+				if (getPrepStmtDealloc(conn))
+					continue;
+				break;
 			default:
 				/* The backend violates the protocol. */
 				libpq_append_conn_error(conn, "protocol error: id=0x%x", id);
@@ -2451,6 +2494,15 @@ pqBuildStartupPacket3(PGconn *conn, int *packetlen,
 	char	   *startpacket;
 	size_t		len;
 
+	/*
+	 * Initialize prepStmtDeallocReporting based on whether the client
+	 * requested the protocol extension.  pqGetNegotiateProtocolVersion3()
+	 * clears this if the server rejects it.
+	 */
+	if (conn->report_prep_stmt_dealloc &&
+		strcmp(conn->report_prep_stmt_dealloc, "1") == 0)
+		conn->prepStmtDeallocReporting = true;
+
 	len = build_startup_packet(conn, NULL, options);
 	if (len == 0 || len > INT_MAX)
 		return NULL;
@@ -2525,6 +2577,10 @@ 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. */
+	if (conn->prepStmtDeallocReporting)
+		ADD_STARTUP_OPTION("_pq_.report_prep_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..401e83d95e8 100644
--- a/src/interfaces/libpq/fe-trace.c
+++ b/src/interfaces/libpq/fe-trace.c
@@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor)
 	pqTraceOutputString(f, message, cursor, false);
 }
 
+static void
+pqTraceOutput_PrepStmtDealloc(FILE *f, const char *message, int *cursor)
+{
+	fprintf(f, "PrepStmtDealloc\t");
+	pqTraceOutputString(f, message, cursor, false);
+}
+
 static void
 pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress)
 {
@@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer)
 			else
 				pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor);
 			break;
+		case PqMsg_PrepStmtDealloc:
+			pqTraceOutput_PrepStmtDealloc(conn->Pfdebug, message, &logCursor);
+			break;
 		case PqMsg_ParameterDescription:
 			pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress);
 			break;
diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h
index 8ecb9b4a4c7..6cec59c939c 100644
--- a/src/interfaces/libpq/libpq-fe.h
+++ b/src/interfaces/libpq/libpq-fe.h
@@ -486,6 +486,15 @@ typedef void (*pgthreadlock_t) (int 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);
+extern int	PQprepStmtDeallocReporting(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..c4f264cf603 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -432,6 +432,7 @@ struct pg_conn
 	char	   *scram_client_key;	/* base64-encoded SCRAM client key */
 	char	   *scram_server_key;	/* base64-encoded SCRAM server key */
 	char	   *sslkeylogfile;	/* where should the client write ssl keylogs */
+	char	   *report_prep_stmt_dealloc;	/* request pstmt dealloc reports */
 
 	bool		cancelRequest;	/* true if this connection is used to send a
 								 * cancel request, instead of being a normal
@@ -532,6 +533,11 @@ struct pg_conn
 	void		(*cleanup_async_auth) (PGconn *conn);
 	pgsocket	altsock;		/* alternative socket for client to poll */
 
+	/* prepared statement deallocation notifications */
+	bool		prepStmtDeallocReporting;
+	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..ef383f20697 100644
--- a/src/test/modules/libpq_pipeline/traces/prepared.trace
+++ b/src/test/modules/libpq_pipeline/traces/prepared.trace
@@ -7,6 +7,7 @@ B	113	RowDescription	 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655
 B	5	ReadyForQuery	 I
 F	16	Close	 S "select_one"
 F	4	Sync
+B	15	PrepStmtDealloc	 "select_one"
 B	4	CloseComplete
 B	5	ReadyForQuery	 I
 F	16	Describe	 S "select_one"
-- 
2.50.1 (Apple Git-155)

Attachments:

  [text/plain] v4-0001-Alert-client-when-prepared-statements-are-dealloc.patch (27.1K, ../../aiB5mM0d-jfbmNkP@nathan/2-v4-0001-Alert-client-when-prepared-statements-are-dealloc.patch)
  download | inline diff:
From 8bfb65df7bf2c24a12d0c38e5a91015b3f1066b1 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 3 Jun 2026 13:26:05 -0500
Subject: [PATCH v4 1/1] Alert client when prepared statements are deallocated.

While trying to take our own advice and teach libpq's large object
interface to use prepared statements instead of fast-path function
calls, I discovered an interesting problem: a user could deallocate
prepared statements (e.g., via DISCARD ALL) without libpq knowing,
causing the large object functions to subsequently fail.  Of the
various options to deal with this problem, the cleanest one seems
to be to solve the general problem and notify clients when a
prepared statement is deallocated.  This allows different layers of
the client stack to notice when their statements are deallocated
and to preemptively re-prepare them before reuse.

This commit adds this behavior via a new protocol extension that
libpq enables by default, but that can be disabled via a connection
parameter.  libpq offers the ability to register callback functions
that are executed immediately upon receiving a deallocation
notification, as well as a function that returns whether the server
is configured to send these notifications.

This is primarily intended for use in a follow-up commit that
teaches the libpq large object interface to use prepared
statements, but the new message type and libpq interface may be
useful elsewhere.

Reviewed-by: Jacob Champion <jacob.champion@enterprisedb.com>
Reviewed-by: Zsolt Parragi <zsolt.parragi@percona.com>
Discussion: https://postgr.es/m/ahm_4eOKkkKJ3Gds%40nathan
---
 doc/src/sgml/libpq.sgml                       | 91 +++++++++++++++++++
 doc/src/sgml/protocol.sgml                    | 62 ++++++++++++-
 src/backend/commands/prepare.c                | 33 +++++++
 src/backend/tcop/backend_startup.c            | 13 ++-
 src/bin/pg_upgrade/dump.c                     |  6 +-
 src/bin/pg_upgrade/server.c                   |  3 +
 src/bin/pg_upgrade/task.c                     |  3 +
 src/bin/pg_upgrade/version.c                  |  3 +-
 src/include/libpq/libpq-be.h                  |  1 +
 src/include/libpq/protocol.h                  |  1 +
 src/interfaces/libpq/exports.txt              |  2 +
 src/interfaces/libpq/fe-connect.c             | 63 +++++++++++++
 src/interfaces/libpq/fe-protocol3.c           | 56 ++++++++++++
 src/interfaces/libpq/fe-trace.c               | 10 ++
 src/interfaces/libpq/libpq-fe.h               |  9 ++
 src/interfaces/libpq/libpq-int.h              |  6 ++
 .../libpq_pipeline/traces/prepared.trace      |  1 +
 17 files changed, 354 insertions(+), 9 deletions(-)

diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 7d3c3bb66d8..f76b31264f5 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -2297,6 +2297,20 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-report-prep-stmt-dealloc" xreflabel="report_prep_stmt_dealloc">
+      <term><literal>report_prep_stmt_dealloc</literal></term>
+      <listitem>
+       <para>
+        If set to <literal>1</literal> (the default),
+        <application>libpq</application> requests the
+        <link linkend="protocol-extensions"><literal>_pq_.report_prep_stmt_dealloc</literal></link>
+        protocol extension, so that the server reports when prepared statements
+        are deallocated.  Set it to <literal>0</literal> to suppress the
+        request.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-scram-client-key" xreflabel="scram_client_key">
       <term><literal>scram_client_key</literal></term>
       <listitem>
@@ -7253,6 +7267,83 @@ 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_prep_stmt_dealloc</literal></link>
+   protocol extension, which is only available on servers running
+   <productname>PostgreSQL</productname> 20 and later.
+   By default, <application>libpq</application> requests this protocol
+   extension; this can be disabled with the
+   <xref linkend="libpq-connect-report-prep-stmt-dealloc"/> connection
+   parameter.
+  </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-PQprepStmtDeallocReporting">
+     <term><function>PQprepStmtDeallocReporting</function><indexterm><primary>PQprepStmtDeallocReporting</primary></indexterm></term>
+     <listitem>
+      <para>
+       Returns <literal>1</literal> if the server accepted the
+       <literal>_pq_.report_prep_stmt_dealloc</literal> protocol extension.
+       Otherwise, returns <literal>0</literal> to indicate that no prepared
+       statement deallocation notifications will be sent.
+
+<synopsis>
+int PQprepStmtDeallocReporting(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..70ecc451efd 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_prep_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-PrepStmtDealloc">PrepStmtDealloc</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_prep_stmt_dealloc</literal></link>
+    protocol extension, the backend sends a PrepStmtDealloc 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-PrepStmtDealloc">
+    <term>PrepStmtDealloc (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_prep_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..4e7af002d6d 100644
--- a/src/backend/commands/prepare.c
+++ b/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_prep_stmt_dealloc protocol extension.
+ */
+static void
+SendStmtDeallocMsg(const char *name)
+{
+	StringInfoData buf;
+
+	if (whereToSendOutput != DestRemote)
+		return;
+	if (!MyProcPort || !MyProcPort->report_prep_stmt_dealloc)
+		return;
+
+	pq_beginmessage(&buf, PqMsg_PrepStmtDealloc);
+	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);
 	}
 }
 
@@ -543,6 +569,7 @@ DropAllPreparedStatements(void)
 {
 	HASH_SEQ_STATUS seq;
 	PreparedStatement *entry;
+	bool		found = false;
 
 	/* nothing cached */
 	if (!prepared_queries)
@@ -552,12 +579,18 @@ DropAllPreparedStatements(void)
 	hash_seq_init(&seq, prepared_queries);
 	while ((entry = hash_seq_search(&seq)) != NULL)
 	{
+		found = true;
+
 		/* Release the plancache entry */
 		DropCachedPlan(entry->plansource);
 
 		/* Now we can remove the hash table entry */
 		hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL);
 	}
+
+	/* Alert the client */
+	if (found)
+		SendStmtDeallocMsg("");
 }
 
 /*
diff --git a/src/backend/tcop/backend_startup.c b/src/backend/tcop/backend_startup.c
index 25205cee0fa..a9158e86ea1 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_prep_stmt_dealloc") == 0)
+					port->report_prep_stmt_dealloc = true;
+				else
+					unrecognized_protocol_options =
+						lappend(unrecognized_protocol_options, pstrdup(nameptr));
 			}
 			else
 			{
diff --git a/src/bin/pg_upgrade/dump.c b/src/bin/pg_upgrade/dump.c
index f47c8d06211..8d542903cb6 100644
--- a/src/bin/pg_upgrade/dump.c
+++ b/src/bin/pg_upgrade/dump.c
@@ -24,7 +24,8 @@ generate_old_dump(void)
 			  "\"%s/pg_dumpall\" %s%s --globals-only --quote-all-identifiers "
 			  "--binary-upgrade %s --no-sync -f \"%s/%s\"",
 			  new_cluster.bindir, cluster_conn_opts(&old_cluster),
-			  protocol_negotiation_supported(&old_cluster) ? "" : " -d \"max_protocol_version=3.0\"",
+			  protocol_negotiation_supported(&old_cluster) ?
+			  "" : " -d \"max_protocol_version=3.0 report_prep_stmt_dealloc=0\"",
 			  log_opts.verbose ? "--verbose" : "",
 			  log_opts.dumpdir,
 			  GLOBALS_DUMP_FILE);
@@ -45,7 +46,10 @@ generate_old_dump(void)
 		appendPQExpBufferStr(&connstr, "dbname=");
 		appendConnStrVal(&connstr, old_db->db_name);
 		if (!protocol_negotiation_supported(&old_cluster))
+		{
 			appendPQExpBufferStr(&connstr, " max_protocol_version=3.0");
+			appendPQExpBufferStr(&connstr, " report_prep_stmt_dealloc=0");
+		}
 
 		initPQExpBuffer(&escaped_connstr);
 		appendShellString(&escaped_connstr, connstr.data);
diff --git a/src/bin/pg_upgrade/server.c b/src/bin/pg_upgrade/server.c
index 5d81e4e95b8..841802b2b14 100644
--- a/src/bin/pg_upgrade/server.c
+++ b/src/bin/pg_upgrade/server.c
@@ -72,7 +72,10 @@ get_db_conn(ClusterInfo *cluster, const char *db_name)
 		appendConnStrVal(&conn_opts, cluster->sockdir);
 	}
 	if (!protocol_negotiation_supported(cluster))
+	{
 		appendPQExpBufferStr(&conn_opts, " max_protocol_version=3.0");
+		appendPQExpBufferStr(&conn_opts, " report_prep_stmt_dealloc=0");
+	}
 
 	conn = PQconnectdb(conn_opts.data);
 	termPQExpBuffer(&conn_opts);
diff --git a/src/bin/pg_upgrade/task.c b/src/bin/pg_upgrade/task.c
index b6eb29e1f3a..4c64bbabdcf 100644
--- a/src/bin/pg_upgrade/task.c
+++ b/src/bin/pg_upgrade/task.c
@@ -189,7 +189,10 @@ start_conn(const ClusterInfo *cluster, UpgradeTaskSlot *slot)
 		appendConnStrVal(&conn_opts, cluster->sockdir);
 	}
 	if (!protocol_negotiation_supported(cluster))
+	{
 		appendPQExpBufferStr(&conn_opts, " max_protocol_version=3.0");
+		appendPQExpBufferStr(&conn_opts, " report_prep_stmt_dealloc=0");
+	}
 
 	slot->conn = PQconnectStart(conn_opts.data);
 
diff --git a/src/bin/pg_upgrade/version.c b/src/bin/pg_upgrade/version.c
index 047670d4acb..9a86b9a8a55 100644
--- a/src/bin/pg_upgrade/version.c
+++ b/src/bin/pg_upgrade/version.c
@@ -30,7 +30,8 @@ jsonb_9_4_check_applicable(ClusterInfo *cluster)
 
 /*
  * Older servers can't support newer protocol versions, so their connection
- * strings will need to lock max_protocol_version to 3.0.
+ * strings will need to lock max_protocol_version to 3.0 and disable prepared
+ * statement deallocation reports.
  */
 bool
 protocol_negotiation_supported(const ClusterInfo *cluster)
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 921b2daa4ff..1fbf08fc420 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -151,6 +151,7 @@ typedef struct Port
 	char	   *user_name;
 	char	   *cmdline_options;
 	List	   *guc_options;
+	bool		report_prep_stmt_dealloc;
 
 	/*
 	 * The startup packet application name, only used here for the "connection
diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h
index eae8f0e7238..aee0675594d 100644
--- a/src/include/libpq/protocol.h
+++ b/src/include/libpq/protocol.h
@@ -53,6 +53,7 @@
 #define PqMsg_FunctionCallResponse	'V'
 #define PqMsg_CopyBothResponse		'W'
 #define PqMsg_ReadyForQuery			'Z'
+#define PqMsg_PrepStmtDealloc		'i'
 #define PqMsg_NoData				'n'
 #define PqMsg_PortalSuspended		's'
 #define PqMsg_ParameterDescription	't'
diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt
index 1e3d5bd5867..9413412e65d 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
+PQprepStmtDeallocReporting 213
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 4272d386e64..1f0219d09a3 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -421,6 +421,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"SSL-Key-Log-File", "D", 64,
 	offsetof(struct pg_conn, sslkeylogfile)},
 
+	{"report_prep_stmt_dealloc", NULL, "1", NULL,
+		"Report-Prepared-Statement-Deallocations", "", 1,
+	offsetof(struct pg_conn, report_prep_stmt_dealloc)},
+
 	/* Terminating entry --- MUST BE LAST */
 	{NULL, NULL, NULL, NULL,
 	NULL, NULL, 0}
@@ -708,6 +712,7 @@ pqDropServerData(PGconn *conn)
 	free(conn->write_err_msg);
 	conn->write_err_msg = NULL;
 	conn->oauth_want_retry = false;
+	conn->prepStmtDeallocReporting = false;
 
 	/*
 	 * Cancel connections need to retain their be_pid and be_cancel_key across
@@ -5178,6 +5183,11 @@ freePGconn(PGconn *conn)
 	free(conn->rowBuf);
 	termPQExpBuffer(&conn->errorMessage);
 	termPQExpBuffer(&conn->workBuffer);
+	free(conn->report_prep_stmt_dealloc);
+	if (conn->prepStmtDeallocCallbacks)
+		free(conn->prepStmtDeallocCallbacks);
+	if (conn->prepStmtDeallocCallbackArgs)
+		free(conn->prepStmtDeallocCallbackArgs);
 
 	free(conn);
 }
@@ -8426,3 +8436,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_prep_stmt_dealloc
+ * extension.  If false, no notifications will arrive and the caller must
+ * re-prepare on error.
+ */
+int
+PQprepStmtDeallocReporting(PGconn *conn)
+{
+	return conn && conn->prepStmtDeallocReporting;
+}
diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c
index 78ffb1025d0..f5cc361fb73 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 PrepStmtDealloc 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
+getPrepStmtDealloc(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_PrepStmtDealloc)
+			{
+				if (getPrepStmtDealloc(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_PrepStmtDealloc:
+					if (getPrepStmtDealloc(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_prep_stmt_dealloc") == 0)
+			conn->prepStmtDeallocReporting = 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_PrepStmtDealloc:
+				if (getPrepStmtDealloc(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_PrepStmtDealloc:
+				if (getPrepStmtDealloc(conn))
+					continue;
+				break;
 			default:
 				/* The backend violates the protocol. */
 				libpq_append_conn_error(conn, "protocol error: id=0x%x", id);
@@ -2451,6 +2494,15 @@ pqBuildStartupPacket3(PGconn *conn, int *packetlen,
 	char	   *startpacket;
 	size_t		len;
 
+	/*
+	 * Initialize prepStmtDeallocReporting based on whether the client
+	 * requested the protocol extension.  pqGetNegotiateProtocolVersion3()
+	 * clears this if the server rejects it.
+	 */
+	if (conn->report_prep_stmt_dealloc &&
+		strcmp(conn->report_prep_stmt_dealloc, "1") == 0)
+		conn->prepStmtDeallocReporting = true;
+
 	len = build_startup_packet(conn, NULL, options);
 	if (len == 0 || len > INT_MAX)
 		return NULL;
@@ -2525,6 +2577,10 @@ 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. */
+	if (conn->prepStmtDeallocReporting)
+		ADD_STARTUP_OPTION("_pq_.report_prep_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..401e83d95e8 100644
--- a/src/interfaces/libpq/fe-trace.c
+++ b/src/interfaces/libpq/fe-trace.c
@@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor)
 	pqTraceOutputString(f, message, cursor, false);
 }
 
+static void
+pqTraceOutput_PrepStmtDealloc(FILE *f, const char *message, int *cursor)
+{
+	fprintf(f, "PrepStmtDealloc\t");
+	pqTraceOutputString(f, message, cursor, false);
+}
+
 static void
 pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress)
 {
@@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer)
 			else
 				pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor);
 			break;
+		case PqMsg_PrepStmtDealloc:
+			pqTraceOutput_PrepStmtDealloc(conn->Pfdebug, message, &logCursor);
+			break;
 		case PqMsg_ParameterDescription:
 			pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress);
 			break;
diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h
index 8ecb9b4a4c7..6cec59c939c 100644
--- a/src/interfaces/libpq/libpq-fe.h
+++ b/src/interfaces/libpq/libpq-fe.h
@@ -486,6 +486,15 @@ typedef void (*pgthreadlock_t) (int 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);
+extern int	PQprepStmtDeallocReporting(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..c4f264cf603 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -432,6 +432,7 @@ struct pg_conn
 	char	   *scram_client_key;	/* base64-encoded SCRAM client key */
 	char	   *scram_server_key;	/* base64-encoded SCRAM server key */
 	char	   *sslkeylogfile;	/* where should the client write ssl keylogs */
+	char	   *report_prep_stmt_dealloc;	/* request pstmt dealloc reports */
 
 	bool		cancelRequest;	/* true if this connection is used to send a
 								 * cancel request, instead of being a normal
@@ -532,6 +533,11 @@ struct pg_conn
 	void		(*cleanup_async_auth) (PGconn *conn);
 	pgsocket	altsock;		/* alternative socket for client to poll */
 
+	/* prepared statement deallocation notifications */
+	bool		prepStmtDeallocReporting;
+	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..ef383f20697 100644
--- a/src/test/modules/libpq_pipeline/traces/prepared.trace
+++ b/src/test/modules/libpq_pipeline/traces/prepared.trace
@@ -7,6 +7,7 @@ B	113	RowDescription	 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655
 B	5	ReadyForQuery	 I
 F	16	Close	 S "select_one"
 F	4	Sync
+B	15	PrepStmtDealloc	 "select_one"
 B	4	CloseComplete
 B	5	ReadyForQuery	 I
 F	16	Describe	 S "select_one"
-- 
2.50.1 (Apple Git-155)

^ permalink  raw  reply  [nested|flat] 13+ messages in thread

* Re: alert clients when prepared statements are deallocated
  2026-05-29 16:33 alert clients when prepared statements are deallocated Nathan Bossart <nathandbossart@gmail.com>
  2026-05-29 17:09 ` Re: alert clients when prepared statements are deallocated Tom Lane <tgl@sss.pgh.pa.us>
  2026-05-29 18:29   ` Re: alert clients when prepared statements are deallocated Nathan Bossart <nathandbossart@gmail.com>
  2026-06-02 22:11     ` Re: alert clients when prepared statements are deallocated Nathan Bossart <nathandbossart@gmail.com>
@ 2026-06-03 21:17       ` Nathan Bossart <nathandbossart@gmail.com>
  2026-06-04 23:56         ` Re: alert clients when prepared statements are deallocated Jacob Champion <jacob.champion@enterprisedb.com>
  2 siblings, 1 reply; 13+ messages in thread

From: Nathan Bossart @ 2026-06-03 21:17 UTC (permalink / raw)
  To: Tom Lane <tgl@sss.pgh.pa.us>; +Cc: Jacob Champion <jacob.champion@enterprisedb.com>; pgsql-hackers

On Tue, Jun 02, 2026 at 05:11:52PM -0500, Nathan Bossart wrote:
> * I'm a little worried about race conditions involving a client trying to
> use a statement while a deallocation message is in flight, but I haven't
> identified anything concrete so far.  This is something I'd like to
> investigate some more, though.

Hm.  So there's actually a pretty obvious problem here.  Say a user
executes something like PQsendQuery(conn, "DISCARD ALL") and then tries to
execute an lo_* function (modified to use prepared statements) prior to
consuming the result.  In that case, the callback won't be called in time
and the LO function will fail.

My first instinct is that this is a showstopper for $subject, but perhaps
it is a rare enough scenario that we could live with documenting it.  My
suspicion is that it's uncommon for folks to asynchronously deallocate all
prepared statements, and I don't know why you'd use PQsendClosePrepared()
on statements named libpq_internal_*.  Nevertheless, this seems like a
rather large hole.

I think this calls into question whether moving the libpq interface to
prepared statements makes sense.  If we can't do that, I think we're pretty
much forced to keep the fast-path around forever or to accept a larger
performance hit.  In any case, I find it a little strange that there's not
a great way to use prepared statements internally in libpq, which is why
I'm chasing this a little more than perhaps I should.

-- 
nathan





^ permalink  raw  reply  [nested|flat] 13+ messages in thread

* Re: alert clients when prepared statements are deallocated
  2026-05-29 16:33 alert clients when prepared statements are deallocated Nathan Bossart <nathandbossart@gmail.com>
  2026-05-29 17:09 ` Re: alert clients when prepared statements are deallocated Tom Lane <tgl@sss.pgh.pa.us>
  2026-05-29 18:29   ` Re: alert clients when prepared statements are deallocated Nathan Bossart <nathandbossart@gmail.com>
  2026-06-02 22:11     ` Re: alert clients when prepared statements are deallocated Nathan Bossart <nathandbossart@gmail.com>
  2026-06-03 21:17       ` Re: alert clients when prepared statements are deallocated Nathan Bossart <nathandbossart@gmail.com>
@ 2026-06-04 23:56         ` Jacob Champion <jacob.champion@enterprisedb.com>
  0 siblings, 0 replies; 13+ messages in thread

From: Jacob Champion @ 2026-06-04 23:56 UTC (permalink / raw)
  To: Nathan Bossart <nathandbossart@gmail.com>; +Cc: Tom Lane <tgl@sss.pgh.pa.us>; pgsql-hackers

On Wed, Jun 3, 2026 at 2:17 PM Nathan Bossart <nathandbossart@gmail.com> wrote:
> Hm.  So there's actually a pretty obvious problem here.  Say a user
> executes something like PQsendQuery(conn, "DISCARD ALL") and then tries to
> execute an lo_* function (modified to use prepared statements) prior to
> consuming the result.  In that case, the callback won't be called in time
> and the LO function will fail.

Are you allowed to call lo_*/PQfn() in the middle of an operation like
that? I.e. does it work today?

--Jacob





^ permalink  raw  reply  [nested|flat] 13+ messages in thread


end of thread, other threads:[~2026-06-05 22:24 UTC | newest]

Thread overview: 13+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2026-05-29 16:33 alert clients when prepared statements are deallocated Nathan Bossart <nathandbossart@gmail.com>
2026-05-29 17:07 ` Jacob Champion <jacob.champion@enterprisedb.com>
2026-06-05 09:34   ` Jelte Fennema-Nio <postgres@jeltef.nl>
2026-06-05 22:03     ` Nathan Bossart <nathandbossart@gmail.com>
2026-06-05 22:24       ` Jelte Fennema-Nio <postgres@jeltef.nl>
2026-05-29 17:09 ` Tom Lane <tgl@sss.pgh.pa.us>
2026-05-29 18:29   ` Nathan Bossart <nathandbossart@gmail.com>
2026-06-02 22:11     ` Nathan Bossart <nathandbossart@gmail.com>
2026-06-03 14:14       ` Zsolt Parragi <zsolt.parragi@percona.com>
2026-06-03 14:37         ` Nathan Bossart <nathandbossart@gmail.com>
2026-06-03 18:59       ` Nathan Bossart <nathandbossart@gmail.com>
2026-06-03 21:17       ` Nathan Bossart <nathandbossart@gmail.com>
2026-06-04 23:56         ` Jacob Champion <jacob.champion@enterprisedb.com>

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox