From 0132dddab33a6eae2d3d63eda9f053e745fedb06 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Tue, 7 Oct 2025 12:09:30 -0400
Subject: [PATCH] Allow private state in certain planner data structures.

Extension that make extensive use of planner hooks may want to
coordinate their efforts, for example to avoid duplicate computation,
but that's currently difficult because there's no really good way to
pass data between different hooks.

To make that easier, allow for storage of extension-managed private
state in PlannerGlobal, PlannerInfo, and RelOptInfo, along very
similar lines to what we have permitted for ExplainState since commit
c65bc2e1d14a2d4daed7c1921ac518f2c5ac3d17.

Reviewed-by: Andrei Lepikhov <lepihov@gmail.com>
Reviewed-by: Melanie Plageman <melanieplageman@gmail.com>
Reviewed-by: Tom Lane <tgl@sss.pgh.pa.us>
Discussion: http://postgr.es/m/CA+TgmoYWKHU2hKr62Toyzh-kTDEnMDeLw7gkOOnjL-TnOUq0kQ@mail.gmail.com
---
 src/backend/optimizer/util/Makefile     |   1 +
 src/backend/optimizer/util/extendplan.c | 183 ++++++++++++++++++++++++
 src/backend/optimizer/util/meson.build  |   1 +
 src/include/nodes/pathnodes.h           |  12 ++
 src/include/optimizer/extendplan.h      |  72 ++++++++++
 5 files changed, 269 insertions(+)
 create mode 100644 src/backend/optimizer/util/extendplan.c
 create mode 100644 src/include/optimizer/extendplan.h

diff --git a/src/backend/optimizer/util/Makefile b/src/backend/optimizer/util/Makefile
index 4fb115cb11..87b4c3c086 100644
--- a/src/backend/optimizer/util/Makefile
+++ b/src/backend/optimizer/util/Makefile
@@ -15,6 +15,7 @@ include $(top_builddir)/src/Makefile.global
 OBJS = \
 	appendinfo.o \
 	clauses.o \
+	extendplan.o \
 	inherit.o \
 	joininfo.o \
 	orclauses.o \
diff --git a/src/backend/optimizer/util/extendplan.c b/src/backend/optimizer/util/extendplan.c
new file mode 100644
index 0000000000..03d32277ba
--- /dev/null
+++ b/src/backend/optimizer/util/extendplan.c
@@ -0,0 +1,183 @@
+/*-------------------------------------------------------------------------
+ *
+ * extendplan.c
+ *	  Extend core planner objects with additional private state
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994-5, Regents of the University of California
+ *
+ * The interfaces defined in this file make it possible for loadable
+ * modules to store their own private state inside of key planner data
+ * structures -- specifically, the PlannerGlobal, PlannerInfo, and
+ * RelOptInfo structures. This can make it much easier to write
+ * reasonably efficient planner extensions; for instance, code that
+ * uses set_join_pathlist_hook can arrange to compute a key intermediate
+ * result once per joinrel rather than on every call.
+ *
+ * IDENTIFICATION
+ *	  src/backend/optimizer/util/extendplan.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "optimizer/extendplan.h"
+#include "port/pg_bitutils.h"
+#include "utils/memutils.h"
+
+static const char **PlannerExtensionNameArray = NULL;
+static int	PlannerExtensionNamesAssigned = 0;
+static int	PlannerExtensionNamesAllocated = 0;
+
+/*
+ * Map the name of a planner 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. Use the ID
+ * that you get from this function to call the remaining functions in this
+ * file.
+ */
+int
+GetPlannerExtensionId(const char *extension_name)
+{
+	/* Search for an existing extension by this name; if found, return ID. */
+	for (int i = 0; i < PlannerExtensionNamesAssigned; ++i)
+		if (strcmp(PlannerExtensionNameArray[i], extension_name) == 0)
+			return i;
+
+	/* If there is no array yet, create one. */
+	if (PlannerExtensionNameArray == NULL)
+	{
+		PlannerExtensionNamesAllocated = 16;
+		PlannerExtensionNameArray = (const char **)
+			MemoryContextAlloc(TopMemoryContext,
+							   PlannerExtensionNamesAllocated
+							   * sizeof(char *));
+	}
+
+	/* If there's an array but it's currently full, expand it. */
+	if (PlannerExtensionNamesAssigned >= PlannerExtensionNamesAllocated)
+	{
+		int			i = pg_nextpower2_32(PlannerExtensionNamesAssigned + 1);
+
+		PlannerExtensionNameArray = (const char **)
+			repalloc(PlannerExtensionNameArray, i * sizeof(char *));
+		PlannerExtensionNamesAllocated = i;
+	}
+
+	/* Assign and return new ID. */
+	PlannerExtensionNameArray[PlannerExtensionNamesAssigned] = extension_name;
+	return PlannerExtensionNamesAssigned++;
+}
+
+/*
+ * Store extension-specific state into a PlannerGlobal.
+ */
+void
+SetPlannerGlobalExtensionState(PlannerGlobal *glob, int extension_id,
+							   void *opaque)
+{
+	Assert(extension_id >= 0);
+
+	/* If there is no array yet, create one. */
+	if (glob->extension_state == NULL)
+	{
+		MemoryContext planner_cxt;
+		Size		sz;
+
+		planner_cxt = GetMemoryChunkContext(glob);
+		glob->extension_state_allocated =
+			Max(4, pg_nextpower2_32(extension_id + 1));
+		sz = glob->extension_state_allocated * sizeof(void *);
+		glob->extension_state = MemoryContextAllocZero(planner_cxt, sz);
+	}
+
+	/* If there's an array but it's currently full, expand it. */
+	if (extension_id >= glob->extension_state_allocated)
+	{
+		int			i;
+
+		i = pg_nextpower2_32(extension_id + 1);
+		glob->extension_state = (void **)
+			repalloc0(glob->extension_state,
+					  glob->extension_state_allocated * sizeof(void *),
+					  i * sizeof(void *));
+		glob->extension_state_allocated = i;
+	}
+
+	glob->extension_state[extension_id] = opaque;
+}
+
+/*
+ * Store extension-specific state into a PlannerInfo.
+ */
+void
+SetPlannerInfoExtensionState(PlannerInfo *root, int extension_id,
+							 void *opaque)
+{
+	Assert(extension_id >= 0);
+
+	/* If there is no array yet, create one. */
+	if (root->extension_state == NULL)
+	{
+		Size		sz;
+
+		root->extension_state_allocated =
+			Max(4, pg_nextpower2_32(extension_id + 1));
+		sz = root->extension_state_allocated * sizeof(void *);
+		root->extension_state = MemoryContextAllocZero(root->planner_cxt, sz);
+	}
+
+	/* If there's an array but it's currently full, expand it. */
+	if (extension_id >= root->extension_state_allocated)
+	{
+		int			i;
+
+		i = pg_nextpower2_32(extension_id + 1);
+		root->extension_state = (void **)
+			repalloc0(root->extension_state,
+					  root->extension_state_allocated * sizeof(void *),
+					  i * sizeof(void *));
+		root->extension_state_allocated = i;
+	}
+
+	root->extension_state[extension_id] = opaque;
+}
+
+/*
+ * Store extension-specific state into a RelOptInfo.
+ */
+void
+SetRelOptInfoExtensionState(RelOptInfo *rel, int extension_id,
+							void *opaque)
+{
+	Assert(extension_id >= 0);
+
+	/* If there is no array yet, create one. */
+	if (rel->extension_state == NULL)
+	{
+		MemoryContext planner_cxt;
+		Size		sz;
+
+		planner_cxt = GetMemoryChunkContext(rel);
+		rel->extension_state_allocated =
+			Max(4, pg_nextpower2_32(extension_id + 1));
+		sz = rel->extension_state_allocated * sizeof(void *);
+		rel->extension_state = MemoryContextAllocZero(planner_cxt, sz);
+	}
+
+	/* If there's an array but it's currently full, expand it. */
+	if (extension_id >= rel->extension_state_allocated)
+	{
+		int			i;
+
+		i = pg_nextpower2_32(extension_id + 1);
+		rel->extension_state = (void **)
+			repalloc0(rel->extension_state,
+					  rel->extension_state_allocated * sizeof(void *),
+					  i * sizeof(void *));
+		rel->extension_state_allocated = i;
+	}
+
+	rel->extension_state[extension_id] = opaque;
+}
diff --git a/src/backend/optimizer/util/meson.build b/src/backend/optimizer/util/meson.build
index b3bf913d09..f71f56e37a 100644
--- a/src/backend/optimizer/util/meson.build
+++ b/src/backend/optimizer/util/meson.build
@@ -3,6 +3,7 @@
 backend_sources += files(
   'appendinfo.c',
   'clauses.c',
+  'extendplan.c',
   'inherit.c',
   'joininfo.c',
   'orclauses.c',
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index 7ee9a7a68d..554d7c3ef6 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -185,6 +185,10 @@ typedef struct PlannerGlobal
 
 	/* hash table for NOT NULL attnums of relations */
 	struct HTAB *rel_notnullatts_hash pg_node_attr(read_write_ignore);
+
+	/* extension state */
+	void	  **extension_state pg_node_attr(read_write_ignore);
+	int			extension_state_allocated;
 } PlannerGlobal;
 
 /* macro for fetching the Plan associated with a SubPlan node */
@@ -586,6 +590,10 @@ struct PlannerInfo
 
 	/* PartitionPruneInfos added in this query's plan. */
 	List	   *partPruneInfos;
+
+	/* extension state */
+	void	  **extension_state pg_node_attr(read_write_ignore);
+	int			extension_state_allocated;
 };
 
 
@@ -1097,6 +1105,10 @@ typedef struct RelOptInfo
 	List	  **partexprs pg_node_attr(read_write_ignore);
 	/* Nullable partition key expressions */
 	List	  **nullable_partexprs pg_node_attr(read_write_ignore);
+
+	/* extension state */
+	void	  **extension_state pg_node_attr(read_write_ignore);
+	int			extension_state_allocated;
 } RelOptInfo;
 
 /*
diff --git a/src/include/optimizer/extendplan.h b/src/include/optimizer/extendplan.h
new file mode 100644
index 0000000000..de9618761d
--- /dev/null
+++ b/src/include/optimizer/extendplan.h
@@ -0,0 +1,72 @@
+/*-------------------------------------------------------------------------
+ *
+ * extendplan.h
+ *	  Extend core planner objects with additional private state
+ *
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/optimizer/extendplan.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef EXTENDPLAN_H
+#define EXTENDPLAN_H
+
+#include "nodes/pathnodes.h"
+
+extern int	GetPlannerExtensionId(const char *extension_name);
+
+/*
+ * Get extension-specific state from a PlannerGlobal.
+ */
+static inline void *
+GetPlannerGlobalExtensionState(PlannerGlobal *glob, int extension_id)
+{
+	Assert(extension_id >= 0);
+
+	if (extension_id >= glob->extension_state_allocated)
+		return NULL;
+
+	return glob->extension_state[extension_id];
+}
+
+/*
+ * Get extension-specific state from a PlannerInfo.
+ */
+static inline void *
+GetPlannerInfoExtensionState(PlannerInfo *root, int extension_id)
+{
+	Assert(extension_id >= 0);
+
+	if (extension_id >= root->extension_state_allocated)
+		return NULL;
+
+	return root->extension_state[extension_id];
+}
+
+/*
+ * Get extension-specific state from a PlannerInfo.
+ */
+static inline void *
+GetRelOptInfoExtensionState(RelOptInfo *rel, int extension_id)
+{
+	Assert(extension_id >= 0);
+
+	if (extension_id >= rel->extension_state_allocated)
+		return NULL;
+
+	return rel->extension_state[extension_id];
+}
+
+/* Functions to store private state into various planner objects */
+extern void SetPlannerGlobalExtensionState(PlannerGlobal *glob,
+										   int extension_id,
+										   void *opaque);
+extern void SetPlannerInfoExtensionState(PlannerInfo *root, int extension_id,
+										 void *opaque);
+extern void SetRelOptInfoExtensionState(RelOptInfo *rel, int extension_id,
+										void *opaque);
+
+#endif
-- 
2.39.5

