Thread

  1. [PATCH v2 1/3] tell client when prep stmts are deallocated

    Nathan Bossart <nathan@postgresql.org> — 2026-05-28T20:42:20Z

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