From d1c7aba9079a0871321a06ec7f6e4b96f8fafea8 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Fri, 7 Mar 2025 09:46:52 -0500
Subject: [PATCH v6 1/3] Make it possible for loadable modules to add EXPLAIN
 options.

Modules can use RegisterExtensionExplainOption to register new
EXPLAIN options, and can use SetExplainExtensionState
and GetExplainExtensionState to store related state inside the
ExplainState object and later retrieve it.

Since this substantially increases the amount of code that needs
to handle ExplainState-related tasks, move a few bits of existing
code to a new file explain_state.c and add the rest of this
infrastructure there.

See the comments at the top of explain_state.c for further
explanation of how this mechanism works.
---
 contrib/auto_explain/auto_explain.c   |   1 +
 contrib/file_fdw/file_fdw.c           |   3 +-
 contrib/postgres_fdw/postgres_fdw.c   |   2 +-
 src/backend/commands/Makefile         |   1 +
 src/backend/commands/createas.c       |   2 +
 src/backend/commands/explain.c        | 143 +---------
 src/backend/commands/explain_dr.c     |   1 +
 src/backend/commands/explain_format.c |   1 +
 src/backend/commands/explain_state.c  | 371 ++++++++++++++++++++++++++
 src/backend/commands/meson.build      |   1 +
 src/backend/commands/prepare.c        |   2 +
 src/backend/executor/execAmi.c        |   1 +
 src/backend/tcop/pquery.c             |   1 +
 src/include/commands/explain.h        |  80 +-----
 src/include/commands/explain_state.h  |  96 +++++++
 src/include/commands/prepare.h        |   3 +-
 src/include/nodes/extensible.h        |   2 +-
 src/tools/pgindent/typedefs.list      |   2 +
 18 files changed, 504 insertions(+), 209 deletions(-)
 create mode 100644 src/backend/commands/explain_state.c
 create mode 100644 src/include/commands/explain_state.h

diff --git a/contrib/auto_explain/auto_explain.c b/contrib/auto_explain/auto_explain.c
index 7007a226c08..3b73bd19107 100644
--- a/contrib/auto_explain/auto_explain.c
+++ b/contrib/auto_explain/auto_explain.c
@@ -17,6 +17,7 @@
 #include "access/parallel.h"
 #include "commands/explain.h"
 #include "commands/explain_format.h"
+#include "commands/explain_state.h"
 #include "common/pg_prng.h"
 #include "executor/instrument.h"
 #include "utils/guc.h"
diff --git a/contrib/file_fdw/file_fdw.c b/contrib/file_fdw/file_fdw.c
index bf707c812ed..56ececac70b 100644
--- a/contrib/file_fdw/file_fdw.c
+++ b/contrib/file_fdw/file_fdw.c
@@ -24,9 +24,10 @@
 #include "commands/copy.h"
 #include "commands/copyfrom_internal.h"
 #include "commands/defrem.h"
-#include "commands/explain.h"
 #include "commands/explain_format.h"
+#include "commands/explain_state.h"
 #include "commands/vacuum.h"
+#include "executor/executor.h"
 #include "foreign/fdwapi.h"
 #include "foreign/foreign.h"
 #include "miscadmin.h"
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 1131a8bf77e..263c879026e 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -19,8 +19,8 @@
 #include "access/table.h"
 #include "catalog/pg_opfamily.h"
 #include "commands/defrem.h"
-#include "commands/explain.h"
 #include "commands/explain_format.h"
+#include "commands/explain_state.h"
 #include "executor/execAsync.h"
 #include "foreign/fdwapi.h"
 #include "funcapi.h"
diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile
index 85cfea6fd71..cb2fbdc7c60 100644
--- a/src/backend/commands/Makefile
+++ b/src/backend/commands/Makefile
@@ -36,6 +36,7 @@ OBJS = \
 	explain.o \
 	explain_dr.o \
 	explain_format.o \
+	explain_state.o \
 	extension.o \
 	foreigncmds.o \
 	functioncmds.o \
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index 44b4665ccd3..0a4155773eb 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -35,6 +35,8 @@
 #include "commands/prepare.h"
 #include "commands/tablecmds.h"
 #include "commands/view.h"
+#include "executor/execdesc.h"
+#include "executor/executor.h"
 #include "nodes/makefuncs.h"
 #include "nodes/nodeFuncs.h"
 #include "nodes/queryjumble.h"
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index d8a7232cedb..ac395bf1fdd 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -17,8 +17,10 @@
 #include "catalog/pg_type.h"
 #include "commands/createas.h"
 #include "commands/defrem.h"
+#include "commands/explain.h"
 #include "commands/explain_dr.h"
 #include "commands/explain_format.h"
+#include "commands/explain_state.h"
 #include "commands/prepare.h"
 #include "foreign/fdwapi.h"
 #include "jit/jit.h"
@@ -170,130 +172,11 @@ ExplainQuery(ParseState *pstate, ExplainStmt *stmt,
 	JumbleState *jstate = NULL;
 	Query	   *query;
 	List	   *rewritten;
-	ListCell   *lc;
-	bool		timing_set = false;
-	bool		buffers_set = false;
-	bool		summary_set = false;
-
-	/* Parse options list. */
-	foreach(lc, stmt->options)
-	{
-		DefElem    *opt = (DefElem *) lfirst(lc);
 
-		if (strcmp(opt->defname, "analyze") == 0)
-			es->analyze = defGetBoolean(opt);
-		else if (strcmp(opt->defname, "verbose") == 0)
-			es->verbose = defGetBoolean(opt);
-		else if (strcmp(opt->defname, "costs") == 0)
-			es->costs = defGetBoolean(opt);
-		else if (strcmp(opt->defname, "buffers") == 0)
-		{
-			buffers_set = true;
-			es->buffers = defGetBoolean(opt);
-		}
-		else if (strcmp(opt->defname, "wal") == 0)
-			es->wal = defGetBoolean(opt);
-		else if (strcmp(opt->defname, "settings") == 0)
-			es->settings = defGetBoolean(opt);
-		else if (strcmp(opt->defname, "generic_plan") == 0)
-			es->generic = defGetBoolean(opt);
-		else if (strcmp(opt->defname, "timing") == 0)
-		{
-			timing_set = true;
-			es->timing = defGetBoolean(opt);
-		}
-		else if (strcmp(opt->defname, "summary") == 0)
-		{
-			summary_set = true;
-			es->summary = defGetBoolean(opt);
-		}
-		else if (strcmp(opt->defname, "memory") == 0)
-			es->memory = defGetBoolean(opt);
-		else if (strcmp(opt->defname, "serialize") == 0)
-		{
-			if (opt->arg)
-			{
-				char	   *p = defGetString(opt);
-
-				if (strcmp(p, "off") == 0 || strcmp(p, "none") == 0)
-					es->serialize = EXPLAIN_SERIALIZE_NONE;
-				else if (strcmp(p, "text") == 0)
-					es->serialize = EXPLAIN_SERIALIZE_TEXT;
-				else if (strcmp(p, "binary") == 0)
-					es->serialize = EXPLAIN_SERIALIZE_BINARY;
-				else
-					ereport(ERROR,
-							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-							 errmsg("unrecognized value for EXPLAIN option \"%s\": \"%s\"",
-									opt->defname, p),
-							 parser_errposition(pstate, opt->location)));
-			}
-			else
-			{
-				/* SERIALIZE without an argument is taken as 'text' */
-				es->serialize = EXPLAIN_SERIALIZE_TEXT;
-			}
-		}
-		else if (strcmp(opt->defname, "format") == 0)
-		{
-			char	   *p = defGetString(opt);
-
-			if (strcmp(p, "text") == 0)
-				es->format = EXPLAIN_FORMAT_TEXT;
-			else if (strcmp(p, "xml") == 0)
-				es->format = EXPLAIN_FORMAT_XML;
-			else if (strcmp(p, "json") == 0)
-				es->format = EXPLAIN_FORMAT_JSON;
-			else if (strcmp(p, "yaml") == 0)
-				es->format = EXPLAIN_FORMAT_YAML;
-			else
-				ereport(ERROR,
-						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-						 errmsg("unrecognized value for EXPLAIN option \"%s\": \"%s\"",
-								opt->defname, p),
-						 parser_errposition(pstate, opt->location)));
-		}
-		else
-			ereport(ERROR,
-					(errcode(ERRCODE_SYNTAX_ERROR),
-					 errmsg("unrecognized EXPLAIN option \"%s\"",
-							opt->defname),
-					 parser_errposition(pstate, opt->location)));
-	}
-
-	/* check that WAL is used with EXPLAIN ANALYZE */
-	if (es->wal && !es->analyze)
-		ereport(ERROR,
-				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				 errmsg("EXPLAIN option %s requires ANALYZE", "WAL")));
-
-	/* if the timing was not set explicitly, set default value */
-	es->timing = (timing_set) ? es->timing : es->analyze;
-
-	/* if the buffers was not set explicitly, set default value */
-	es->buffers = (buffers_set) ? es->buffers : es->analyze;
-
-	/* check that timing is used with EXPLAIN ANALYZE */
-	if (es->timing && !es->analyze)
-		ereport(ERROR,
-				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				 errmsg("EXPLAIN option %s requires ANALYZE", "TIMING")));
-
-	/* check that serialize is used with EXPLAIN ANALYZE */
-	if (es->serialize != EXPLAIN_SERIALIZE_NONE && !es->analyze)
-		ereport(ERROR,
-				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				 errmsg("EXPLAIN option %s requires ANALYZE", "SERIALIZE")));
-
-	/* check that GENERIC_PLAN is not used with EXPLAIN ANALYZE */
-	if (es->generic && es->analyze)
-		ereport(ERROR,
-				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				 errmsg("EXPLAIN options ANALYZE and GENERIC_PLAN cannot be used together")));
-
-	/* if the summary was not set explicitly, set default value */
-	es->summary = (summary_set) ? es->summary : es->analyze;
+	/* Configure the ExplainState based on the provided options */
+	ParseExplainOptionList(es, stmt->options, pstate);
 
+	/* Extract the query and, if enabled, jumble it */
 	query = castNode(Query, stmt->query);
 	if (IsQueryIdEnabled())
 		jstate = JumbleQuery(query);
@@ -354,22 +237,6 @@ ExplainQuery(ParseState *pstate, ExplainStmt *stmt,
 	pfree(es->str->data);
 }
 
-/*
- * Create a new ExplainState struct initialized with default options.
- */
-ExplainState *
-NewExplainState(void)
-{
-	ExplainState *es = (ExplainState *) palloc0(sizeof(ExplainState));
-
-	/* Set default options (most fields can be left as zeroes). */
-	es->costs = true;
-	/* Prepare output buffer. */
-	es->str = makeStringInfo();
-
-	return es;
-}
-
 /*
  * ExplainResultDesc -
  *	  construct the result tupledesc for an EXPLAIN
diff --git a/src/backend/commands/explain_dr.c b/src/backend/commands/explain_dr.c
index fb42bee6e72..5715546cf43 100644
--- a/src/backend/commands/explain_dr.c
+++ b/src/backend/commands/explain_dr.c
@@ -15,6 +15,7 @@
 
 #include "commands/explain.h"
 #include "commands/explain_dr.h"
+#include "commands/explain_state.h"
 #include "libpq/pqformat.h"
 #include "libpq/protocol.h"
 #include "utils/lsyscache.h"
diff --git a/src/backend/commands/explain_format.c b/src/backend/commands/explain_format.c
index bccdd76a874..752691d56db 100644
--- a/src/backend/commands/explain_format.c
+++ b/src/backend/commands/explain_format.c
@@ -15,6 +15,7 @@
 
 #include "commands/explain.h"
 #include "commands/explain_format.h"
+#include "commands/explain_state.h"
 #include "utils/json.h"
 #include "utils/xml.h"
 
diff --git a/src/backend/commands/explain_state.c b/src/backend/commands/explain_state.c
new file mode 100644
index 00000000000..8e6eea20ba8
--- /dev/null
+++ b/src/backend/commands/explain_state.c
@@ -0,0 +1,371 @@
+/*-------------------------------------------------------------------------
+ *
+ * explain_state.c
+ *	  Code for initializing and accessing ExplainState objects
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994-5, Regents of the University of California
+ *
+ * In-core options have hard-coded fields inside ExplainState; e.g. if
+ * the user writes EXPLAIN (BUFFERS) then ExplainState's "buffers" member
+ * will be set to true. Extensions can also register options using
+ * RegisterExtensionExplainOption; so that e.g. EXPLAIN (BICYCLE 'red')
+ * will invoke a designated handler that knows what the legal values are
+ * for the BICYCLE option. However, it's not enough for an extension to be
+ * able to parse new options: it also needs a place to store the results
+ * of that parsing, and an ExplainState has no 'bicycle' field.
+ *
+ * To solve this problem, an ExplainState can contain an array of opaque
+ * pointers, one per extension. GetExplainExtensionId generates integer
+ * IDs that are used as offests into this array. Extensions don't call this
+ * function direcrtly; instead, they call GetExplainExtensionState and
+ * SetExplainExtensionState which first map the provided name to an integer
+ * and then access the appropriate array offset.
+ *
+ * Note that there is no requirement that the name of the option match
+ * the name of the extension; e.g. a pg_explain_conveyance extension could
+ * implement options for BICYCLE, MONORAIL, etc.
+ *
+ * IDENTIFICATION
+ *	  src/backend/commands/explain_state.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "commands/defrem.h"
+#include "commands/explain.h"
+#include "commands/explain_state.h"
+
+typedef struct
+{
+	const char *option_name;
+	ExplainOptionHandler option_handler;
+} ExplainExtensionOption;
+
+static int GetExplainExtensionId(const char *extension_name);
+
+static const char **ExplainExtensionNameArray = NULL;
+static int	ExplainExtensionNamesAssigned = 0;
+static int	ExplainExtensionNamesAllocated = 0;
+
+static ExplainExtensionOption *ExplainExtensionOptionArray = NULL;
+static int	ExplainExtensionOptionsAssigned = 0;
+static int	ExplainExtensionOptionsAllocated = 0;
+
+/*
+ * Create a new ExplainState struct initialized with default options.
+ */
+ExplainState *
+NewExplainState(void)
+{
+	ExplainState *es = (ExplainState *) palloc0(sizeof(ExplainState));
+
+	/* Set default options (most fields can be left as zeroes). */
+	es->costs = true;
+	/* Prepare output buffer. */
+	es->str = makeStringInfo();
+
+	return es;
+}
+
+/*
+ * Parse a list of EXPLAIN options and update an ExplainState accordingly.
+ */
+void
+ParseExplainOptionList(ExplainState *es, List *options, ParseState *pstate)
+{
+	ListCell   *lc;
+	bool		timing_set = false;
+	bool		buffers_set = false;
+	bool		summary_set = false;
+
+	/* Parse options list. */
+	foreach(lc, options)
+	{
+		DefElem    *opt = (DefElem *) lfirst(lc);
+
+		if (strcmp(opt->defname, "analyze") == 0)
+			es->analyze = defGetBoolean(opt);
+		else if (strcmp(opt->defname, "verbose") == 0)
+			es->verbose = defGetBoolean(opt);
+		else if (strcmp(opt->defname, "costs") == 0)
+			es->costs = defGetBoolean(opt);
+		else if (strcmp(opt->defname, "buffers") == 0)
+		{
+			buffers_set = true;
+			es->buffers = defGetBoolean(opt);
+		}
+		else if (strcmp(opt->defname, "wal") == 0)
+			es->wal = defGetBoolean(opt);
+		else if (strcmp(opt->defname, "settings") == 0)
+			es->settings = defGetBoolean(opt);
+		else if (strcmp(opt->defname, "generic_plan") == 0)
+			es->generic = defGetBoolean(opt);
+		else if (strcmp(opt->defname, "timing") == 0)
+		{
+			timing_set = true;
+			es->timing = defGetBoolean(opt);
+		}
+		else if (strcmp(opt->defname, "summary") == 0)
+		{
+			summary_set = true;
+			es->summary = defGetBoolean(opt);
+		}
+		else if (strcmp(opt->defname, "memory") == 0)
+			es->memory = defGetBoolean(opt);
+		else if (strcmp(opt->defname, "serialize") == 0)
+		{
+			if (opt->arg)
+			{
+				char	   *p = defGetString(opt);
+
+				if (strcmp(p, "off") == 0 || strcmp(p, "none") == 0)
+					es->serialize = EXPLAIN_SERIALIZE_NONE;
+				else if (strcmp(p, "text") == 0)
+					es->serialize = EXPLAIN_SERIALIZE_TEXT;
+				else if (strcmp(p, "binary") == 0)
+					es->serialize = EXPLAIN_SERIALIZE_BINARY;
+				else
+					ereport(ERROR,
+							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							 errmsg("unrecognized value for EXPLAIN option \"%s\": \"%s\"",
+									opt->defname, p),
+							 parser_errposition(pstate, opt->location)));
+			}
+			else
+			{
+				/* SERIALIZE without an argument is taken as 'text' */
+				es->serialize = EXPLAIN_SERIALIZE_TEXT;
+			}
+		}
+		else if (strcmp(opt->defname, "format") == 0)
+		{
+			char	   *p = defGetString(opt);
+
+			if (strcmp(p, "text") == 0)
+				es->format = EXPLAIN_FORMAT_TEXT;
+			else if (strcmp(p, "xml") == 0)
+				es->format = EXPLAIN_FORMAT_XML;
+			else if (strcmp(p, "json") == 0)
+				es->format = EXPLAIN_FORMAT_JSON;
+			else if (strcmp(p, "yaml") == 0)
+				es->format = EXPLAIN_FORMAT_YAML;
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("unrecognized value for EXPLAIN option \"%s\": \"%s\"",
+								opt->defname, p),
+						 parser_errposition(pstate, opt->location)));
+		}
+		else if (!ApplyExtensionExplainOption(es, opt, pstate))
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("unrecognized EXPLAIN option \"%s\"",
+							opt->defname),
+					 parser_errposition(pstate, opt->location)));
+	}
+
+	/* check that WAL is used with EXPLAIN ANALYZE */
+	if (es->wal && !es->analyze)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("EXPLAIN option %s requires ANALYZE", "WAL")));
+
+	/* if the timing was not set explicitly, set default value */
+	es->timing = (timing_set) ? es->timing : es->analyze;
+
+	/* if the buffers was not set explicitly, set default value */
+	es->buffers = (buffers_set) ? es->buffers : es->analyze;
+
+	/* check that timing is used with EXPLAIN ANALYZE */
+	if (es->timing && !es->analyze)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("EXPLAIN option %s requires ANALYZE", "TIMING")));
+
+	/* check that serialize is used with EXPLAIN ANALYZE */
+	if (es->serialize != EXPLAIN_SERIALIZE_NONE && !es->analyze)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("EXPLAIN option %s requires ANALYZE", "SERIALIZE")));
+
+	/* check that GENERIC_PLAN is not used with EXPLAIN ANALYZE */
+	if (es->generic && es->analyze)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("EXPLAIN options ANALYZE and GENERIC_PLAN cannot be used together")));
+
+	/* if the summary was not set explicitly, set default value */
+	es->summary = (summary_set) ? es->summary : es->analyze;
+}
+
+/*
+ * Get extension-specific state from an ExplainState.
+ *
+ * See comments for SetExplainExtensionState, below.
+ */
+void *
+GetExplainExtensionState(ExplainState *es, const char *extension_name)
+{
+	int		extension_id = GetExplainExtensionId(extension_name);
+
+	if (extension_id >= es->extension_state_allocated)
+		return NULL;
+
+	return es->extension_state[extension_id];
+}
+
+/*
+ * Store extension-specific state into an ExplainState.
+ *
+ * extension_name is assumed to be a constant string or allocated in storage
+ * that will never be freed. You can retrieve the opaque pointer that you
+ * store with this extension later via GetExplainExtensionState.
+ */
+void
+SetExplainExtensionState(ExplainState *es, const char *extension_name,
+						 void *opaque)
+{
+	int		extension_id = GetExplainExtensionId(extension_name);
+
+	/* If there is no array yet, create one. */
+	if (es->extension_state == NULL)
+	{
+		es->extension_state_allocated = 16;
+		es->extension_state =
+			palloc0(es->extension_state_allocated * sizeof(void *));
+	}
+
+	/* If there's an array but it's currently full, expand it. */
+	if (extension_id >= es->extension_state_allocated)
+	{
+		int			i;
+
+		i = pg_nextpower2_32(es->extension_state_allocated + 1);
+		es->extension_state = (void **)
+			repalloc0(es->extension_state,
+					  es->extension_state_allocated * sizeof(void *),
+					  i * sizeof(void *));
+		es->extension_state_allocated = i;
+	}
+
+	es->extension_state[extension_id] = opaque;
+}
+
+/*
+ * Register a new EXPLAIN option.
+ *
+ * When option_name is used as an EXPLAIN option, handler will be called and
+ * should update the ExplainState passed to it. See comments at top of file
+ * for a more detailed explanation.
+ *
+ * option_name is assumed to be a constant string or allocated in storage
+ * that will never be freed.
+ */
+void
+RegisterExtensionExplainOption(const char *option_name,
+							   ExplainOptionHandler handler)
+{
+	ExplainExtensionOption *exopt;
+
+	/* Search for an existing option by this name; if found, update handler. */
+	for (int i = 0; i < ExplainExtensionOptionsAssigned; ++i)
+	{
+		if (strcmp(ExplainExtensionOptionArray[i].option_name,
+				   option_name) == 0)
+		{
+			ExplainExtensionOptionArray[i].option_handler = handler;
+			return;
+		}
+	}
+
+	/* If there is no array yet, create one. */
+	if (ExplainExtensionOptionArray == NULL)
+	{
+		ExplainExtensionOptionsAllocated = 16;
+		ExplainExtensionOptionArray = (ExplainExtensionOption *)
+			MemoryContextAlloc(TopMemoryContext,
+							   ExplainExtensionOptionsAllocated
+							   * sizeof(char *));
+	}
+
+	/* If there's an array but it's currently full, expand it. */
+	if (ExplainExtensionOptionsAssigned >= ExplainExtensionOptionsAllocated)
+	{
+		int			i = pg_nextpower2_32(ExplainExtensionOptionsAssigned + 1);
+
+		ExplainExtensionOptionArray = (ExplainExtensionOption *)
+			repalloc(ExplainExtensionOptionArray, i * sizeof(char *));
+		ExplainExtensionOptionsAllocated = i;
+	}
+
+	/* Assign and return new ID. */
+	exopt = &ExplainExtensionOptionArray[ExplainExtensionOptionsAssigned++];
+	exopt->option_name = option_name;
+	exopt->option_handler = handler;
+}
+
+/*
+ * Apply an EXPLAIN option registered by an extension.
+ *
+ * If no extension has registered the named option, returns false. Otherwise,
+ * calls the appropriate handler function and then returns true.
+ */
+bool
+ApplyExtensionExplainOption(ExplainState *es, DefElem *opt, ParseState *pstate)
+{
+	for (int i = 0; i < ExplainExtensionOptionsAssigned; ++i)
+	{
+		if (strcmp(ExplainExtensionOptionArray[i].option_name,
+				   opt->defname) == 0)
+		{
+			ExplainExtensionOptionArray[i].option_handler(es, opt, pstate);
+			return true;
+		}
+	}
+
+	return false;
+}
+
+/*
+ * Map the name of an EXPLAIN extension to an integer ID.
+ *
+ * Within the lifetime of a particular backend, the same name will be mapped
+ * to the same ID every time. IDs are not stable across backends.
+ *
+ * extension_name is assumed to be a constant string or allocated in storage
+ * that will never be freed.
+ */
+static int
+GetExplainExtensionId(const char *extension_name)
+{
+	/* Search for an existing extension by this name; if found, return ID. */
+	for (int i = 0; i < ExplainExtensionNamesAssigned; ++i)
+		if (strcmp(ExplainExtensionNameArray[i], extension_name) == 0)
+			return i;
+
+	/* If there is no array yet, create one. */
+	if (ExplainExtensionNameArray == NULL)
+	{
+		ExplainExtensionNamesAllocated = 16;
+		ExplainExtensionNameArray = (const char **)
+			MemoryContextAlloc(TopMemoryContext,
+							   ExplainExtensionNamesAllocated
+							   * sizeof(char *));
+	}
+
+	/* If there's an array but it's currently full, expand it. */
+	if (ExplainExtensionNamesAssigned >= ExplainExtensionNamesAllocated)
+	{
+		int			i = pg_nextpower2_32(ExplainExtensionNamesAssigned + 1);
+
+		ExplainExtensionNameArray = (const char **)
+			repalloc(ExplainExtensionNameArray, i * sizeof(char *));
+		ExplainExtensionNamesAllocated = i;
+	}
+
+	/* Assign and return new ID. */
+	ExplainExtensionNameArray[ExplainExtensionNamesAssigned] = extension_name;
+	return ExplainExtensionNamesAssigned++;
+}
diff --git a/src/backend/commands/meson.build b/src/backend/commands/meson.build
index ce8d1ab8bac..dd4cde41d32 100644
--- a/src/backend/commands/meson.build
+++ b/src/backend/commands/meson.build
@@ -24,6 +24,7 @@ backend_sources += files(
   'explain.c',
   'explain_dr.c',
   'explain_format.c',
+  'explain_state.c',
   'extension.c',
   'foreigncmds.c',
   'functioncmds.c',
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 4d68d4d25c7..bf7d2b2309f 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -21,7 +21,9 @@
 #include "access/xact.h"
 #include "catalog/pg_type.h"
 #include "commands/createas.h"
+#include "commands/explain.h"
 #include "commands/explain_format.h"
+#include "commands/explain_state.h"
 #include "commands/prepare.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
diff --git a/src/backend/executor/execAmi.c b/src/backend/executor/execAmi.c
index db525194554..1d0e8ad57b4 100644
--- a/src/backend/executor/execAmi.c
+++ b/src/backend/executor/execAmi.c
@@ -15,6 +15,7 @@
 #include "access/amapi.h"
 #include "access/htup_details.h"
 #include "catalog/pg_class.h"
+#include "executor/executor.h"
 #include "executor/nodeAgg.h"
 #include "executor/nodeAppend.h"
 #include "executor/nodeBitmapAnd.h"
diff --git a/src/backend/tcop/pquery.c b/src/backend/tcop/pquery.c
index dea24453a6c..8164d0fbb4f 100644
--- a/src/backend/tcop/pquery.c
+++ b/src/backend/tcop/pquery.c
@@ -20,6 +20,7 @@
 #include "access/xact.h"
 #include "commands/prepare.h"
 #include "executor/execdesc.h"
+#include "executor/executor.h"
 #include "executor/tstoreReceiver.h"
 #include "miscadmin.h"
 #include "pg_trace.h"
diff --git a/src/include/commands/explain.h b/src/include/commands/explain.h
index 64547bd9b9c..783f67b468a 100644
--- a/src/include/commands/explain.h
+++ b/src/include/commands/explain.h
@@ -14,70 +14,15 @@
 #define EXPLAIN_H
 
 #include "executor/executor.h"
-#include "lib/stringinfo.h"
 #include "parser/parse_node.h"
 
-typedef enum ExplainSerializeOption
-{
-	EXPLAIN_SERIALIZE_NONE,
-	EXPLAIN_SERIALIZE_TEXT,
-	EXPLAIN_SERIALIZE_BINARY,
-} ExplainSerializeOption;
-
-typedef enum ExplainFormat
-{
-	EXPLAIN_FORMAT_TEXT,
-	EXPLAIN_FORMAT_XML,
-	EXPLAIN_FORMAT_JSON,
-	EXPLAIN_FORMAT_YAML,
-} ExplainFormat;
-
-typedef struct ExplainWorkersState
-{
-	int			num_workers;	/* # of worker processes the plan used */
-	bool	   *worker_inited;	/* per-worker state-initialized flags */
-	StringInfoData *worker_str; /* per-worker transient output buffers */
-	int		   *worker_state_save;	/* per-worker grouping state save areas */
-	StringInfo	prev_str;		/* saved output buffer while redirecting */
-} ExplainWorkersState;
-
-typedef struct ExplainState
-{
-	StringInfo	str;			/* output buffer */
-	/* options */
-	bool		verbose;		/* be verbose */
-	bool		analyze;		/* print actual times */
-	bool		costs;			/* print estimated costs */
-	bool		buffers;		/* print buffer usage */
-	bool		wal;			/* print WAL usage */
-	bool		timing;			/* print detailed node timing */
-	bool		summary;		/* print total planning and execution timing */
-	bool		memory;			/* print planner's memory usage information */
-	bool		settings;		/* print modified settings */
-	bool		generic;		/* generate a generic plan */
-	ExplainSerializeOption serialize;	/* serialize the query's output? */
-	ExplainFormat format;		/* output format */
-	/* state for output formatting --- not reset for each new plan tree */
-	int			indent;			/* current indentation level */
-	List	   *grouping_stack; /* format-specific grouping state */
-	/* state related to the current plan tree (filled by ExplainPrintPlan) */
-	PlannedStmt *pstmt;			/* top of plan */
-	List	   *rtable;			/* range table */
-	List	   *rtable_names;	/* alias names for RTEs */
-	List	   *deparse_cxt;	/* context list for deparsing expressions */
-	Bitmapset  *printed_subplans;	/* ids of SubPlans we've printed */
-	bool		hide_workers;	/* set if we find an invisible Gather */
-	int			rtable_size;	/* length of rtable excluding the RTE_GROUP
-								 * entry */
-	/* state related to the current plan node */
-	ExplainWorkersState *workers_state; /* needed if parallel plan */
-} ExplainState;
+struct ExplainState;	/* defined in explain_state.h */
 
 /* Hook for plugins to get control in ExplainOneQuery() */
 typedef void (*ExplainOneQuery_hook_type) (Query *query,
 										   int cursorOptions,
 										   IntoClause *into,
-										   ExplainState *es,
+										   struct ExplainState *es,
 										   const char *queryString,
 										   ParamListInfo params,
 										   QueryEnvironment *queryEnv);
@@ -91,33 +36,34 @@ extern PGDLLIMPORT explain_get_index_name_hook_type explain_get_index_name_hook;
 extern void ExplainQuery(ParseState *pstate, ExplainStmt *stmt,
 						 ParamListInfo params, DestReceiver *dest);
 extern void standard_ExplainOneQuery(Query *query, int cursorOptions,
-									 IntoClause *into, ExplainState *es,
+									 IntoClause *into, struct ExplainState *es,
 									 const char *queryString, ParamListInfo params,
 									 QueryEnvironment *queryEnv);
 
-extern ExplainState *NewExplainState(void);
-
 extern TupleDesc ExplainResultDesc(ExplainStmt *stmt);
 
 extern void ExplainOneUtility(Node *utilityStmt, IntoClause *into,
-							  ExplainState *es, ParseState *pstate,
+							  struct ExplainState *es, ParseState *pstate,
 							  ParamListInfo params);
 
 extern void ExplainOnePlan(PlannedStmt *plannedstmt, CachedPlan *cplan,
 						   CachedPlanSource *plansource, int plan_index,
-						   IntoClause *into, ExplainState *es,
+						   IntoClause *into, struct ExplainState *es,
 						   const char *queryString,
 						   ParamListInfo params, QueryEnvironment *queryEnv,
 						   const instr_time *planduration,
 						   const BufferUsage *bufusage,
 						   const MemoryContextCounters *mem_counters);
 
-extern void ExplainPrintPlan(ExplainState *es, QueryDesc *queryDesc);
-extern void ExplainPrintTriggers(ExplainState *es, QueryDesc *queryDesc);
+extern void ExplainPrintPlan(struct ExplainState *es, QueryDesc *queryDesc);
+extern void ExplainPrintTriggers(struct ExplainState *es,
+								 QueryDesc *queryDesc);
 
-extern void ExplainPrintJITSummary(ExplainState *es, QueryDesc *queryDesc);
+extern void ExplainPrintJITSummary(struct ExplainState *es,
+								   QueryDesc *queryDesc);
 
-extern void ExplainQueryText(ExplainState *es, QueryDesc *queryDesc);
-extern void ExplainQueryParameters(ExplainState *es, ParamListInfo params, int maxlen);
+extern void ExplainQueryText(struct ExplainState *es, QueryDesc *queryDesc);
+extern void ExplainQueryParameters(struct ExplainState *es,
+								   ParamListInfo params, int maxlen);
 
 #endif							/* EXPLAIN_H */
diff --git a/src/include/commands/explain_state.h b/src/include/commands/explain_state.h
new file mode 100644
index 00000000000..6c161bfb615
--- /dev/null
+++ b/src/include/commands/explain_state.h
@@ -0,0 +1,96 @@
+/*-------------------------------------------------------------------------
+ *
+ * explain_state.h
+ *	  prototypes for explain_state.c
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994-5, Regents of the University of California
+ *
+ * src/include/commands/explain_state.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef EXPLAIN_STATE_H
+#define EXPLAIN_STATE_H
+
+#include "nodes/parsenodes.h"
+#include "nodes/plannodes.h"
+#include "parser/parse_node.h"
+
+typedef enum ExplainSerializeOption
+{
+	EXPLAIN_SERIALIZE_NONE,
+	EXPLAIN_SERIALIZE_TEXT,
+	EXPLAIN_SERIALIZE_BINARY,
+} ExplainSerializeOption;
+
+typedef enum ExplainFormat
+{
+	EXPLAIN_FORMAT_TEXT,
+	EXPLAIN_FORMAT_XML,
+	EXPLAIN_FORMAT_JSON,
+	EXPLAIN_FORMAT_YAML,
+} ExplainFormat;
+
+typedef struct ExplainWorkersState
+{
+	int			num_workers;	/* # of worker processes the plan used */
+	bool	   *worker_inited;	/* per-worker state-initialized flags */
+	StringInfoData *worker_str; /* per-worker transient output buffers */
+	int		   *worker_state_save;	/* per-worker grouping state save areas */
+	StringInfo	prev_str;		/* saved output buffer while redirecting */
+} ExplainWorkersState;
+
+typedef struct ExplainState
+{
+	StringInfo	str;			/* output buffer */
+	/* options */
+	bool		verbose;		/* be verbose */
+	bool		analyze;		/* print actual times */
+	bool		costs;			/* print estimated costs */
+	bool		buffers;		/* print buffer usage */
+	bool		wal;			/* print WAL usage */
+	bool		timing;			/* print detailed node timing */
+	bool		summary;		/* print total planning and execution timing */
+	bool		memory;			/* print planner's memory usage information */
+	bool		settings;		/* print modified settings */
+	bool		generic;		/* generate a generic plan */
+	ExplainSerializeOption serialize;	/* serialize the query's output? */
+	ExplainFormat format;		/* output format */
+	/* state for output formatting --- not reset for each new plan tree */
+	int			indent;			/* current indentation level */
+	List	   *grouping_stack; /* format-specific grouping state */
+	/* state related to the current plan tree (filled by ExplainPrintPlan) */
+	PlannedStmt *pstmt;			/* top of plan */
+	List	   *rtable;			/* range table */
+	List	   *rtable_names;	/* alias names for RTEs */
+	List	   *deparse_cxt;	/* context list for deparsing expressions */
+	Bitmapset  *printed_subplans;	/* ids of SubPlans we've printed */
+	bool		hide_workers;	/* set if we find an invisible Gather */
+	int			rtable_size;	/* length of rtable excluding the RTE_GROUP
+								 * entry */
+	/* state related to the current plan node */
+	ExplainWorkersState *workers_state; /* needed if parallel plan */
+	/* extensions */
+	void	  **extension_state;
+	int			extension_state_allocated;
+} ExplainState;
+
+typedef void (*ExplainOptionHandler) (ExplainState *, DefElem *, ParseState *);
+
+extern ExplainState *NewExplainState(void);
+extern void ParseExplainOptionList(ExplainState *es, List *options,
+								   ParseState *pstate);
+
+extern void *GetExplainExtensionState(ExplainState *es,
+									  const char *extension_name);
+extern void SetExplainExtensionState(ExplainState *es,
+									 const char *extension_name,
+									 void *opaque);
+
+extern void RegisterExtensionExplainOption(const char *option_name,
+										   ExplainOptionHandler handler);
+extern bool ApplyExtensionExplainOption(ExplainState *es, DefElem *opt,
+										ParseState *pstate);
+
+#endif							/* EXPLAIN_STATE_H */
diff --git a/src/include/commands/prepare.h b/src/include/commands/prepare.h
index b9533f1af84..08daac8c926 100644
--- a/src/include/commands/prepare.h
+++ b/src/include/commands/prepare.h
@@ -13,8 +13,9 @@
 #ifndef PREPARE_H
 #define PREPARE_H
 
-#include "commands/explain.h"
+#include "commands/explain_state.h"
 #include "datatype/timestamp.h"
+#include "tcop/dest.h"
 #include "utils/plancache.h"
 
 /*
diff --git a/src/include/nodes/extensible.h b/src/include/nodes/extensible.h
index 552ce19bdd5..1129c4ba4b1 100644
--- a/src/include/nodes/extensible.h
+++ b/src/include/nodes/extensible.h
@@ -15,7 +15,7 @@
 #define EXTENSIBLE_H
 
 #include "access/parallel.h"
-#include "commands/explain.h"
+#include "commands/explain_state.h"
 #include "nodes/execnodes.h"
 #include "nodes/pathnodes.h"
 #include "nodes/plannodes.h"
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9840060997f..e70c9368d94 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -4244,3 +4244,5 @@ yyscan_t
 z_stream
 z_streamp
 zic_t
+ExplainExtensionOption
+ExplainOptionHandler
-- 
2.39.3 (Apple Git-145)

