From 36451d306ea25e97b431c9496744813bdaca9859 Mon Sep 17 00:00:00 2001
From: Andrey Borodin <amborodin@acm.org>
Date: Tue, 7 Apr 2026 15:28:06 +0500
Subject: [PATCH v2 2/5] Track RTE_FUNCTION composite rowtype dependencies

Record typrelid dependencies for named composite outputs of RTE_FUNCTION
nodes, so relcache invalidation from ALTER TYPE marks cached plans stale.

This keeps prepared and SPI plans in sync with composite rowshape changes.
---
 doc/src/sgml/ref/alter_type.sgml     |  9 +++++++
 src/backend/optimizer/plan/setrefs.c | 36 ++++++++++++++++++++++++++++
 2 files changed, 45 insertions(+)

diff --git a/doc/src/sgml/ref/alter_type.sgml b/doc/src/sgml/ref/alter_type.sgml
index 025a3ee48f5..72b046afd98 100644
--- a/doc/src/sgml/ref/alter_type.sgml
+++ b/doc/src/sgml/ref/alter_type.sgml
@@ -242,6 +242,15 @@ ALTER TYPE <replaceable class="parameter">name</replaceable> SET ( <replaceable
    and/or alter the type of several attributes in a single command.
   </para>
 
+  <para>
+   Changing a composite type's attributes also invalidates cached plans for
+   queries that scan <literal>SETOF</literal> functions returning that
+   composite type (including through a domain over the composite), even when
+   no <command>CREATE OR REPLACE FUNCTION</command> is run.  This matches the
+   relcache invalidation already sent for the composite type's cataloged
+   rowtype.
+  </para>
+
   <para>
    You must own the type to use <command>ALTER TYPE</command>.
    To change the schema of a type, you must also have
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index e1182255055..3f29c270c2e 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -26,6 +26,7 @@
 #include "optimizer/subselect.h"
 #include "optimizer/tlist.h"
 #include "parser/parse_relation.h"
+#include "parser/parse_type.h"
 #include "rewrite/rewriteManip.h"
 #include "tcop/utility.h"
 #include "utils/syscache.h"
@@ -497,6 +498,34 @@ add_rtes_to_flat_rtable(PlannerInfo *root, bool recursing)
 	}
 }
 
+/*
+ * Record relcache dependencies for RTE_FUNCTION entries whose declared result
+ * is a named composite type (or a domain over one).  ALTER TYPE {ADD | DROP |
+ * ALTER} ATTRIBUTE updates the composite type's pg_class row without changing
+ * OIDs of dependent functions, so plans must be rebuilt when the composite
+ * rowtype changes.  (Compare extract_query_dependencies_walker, which must
+ * stay in sync.)
+ */
+static void
+add_function_rte_relation_deps(PlannerGlobal *glob, const RangeTblEntry *rte)
+{
+	ListCell   *lc;
+
+	Assert(rte->rtekind == RTE_FUNCTION);
+
+	foreach(lc, rte->functions)
+	{
+		RangeTblFunction *rtfunc = (RangeTblFunction *) lfirst(lc);
+		Oid			typid;
+		Oid			typrelid;
+
+		typid = exprType(rtfunc->funcexpr);
+		typrelid = typeOrDomainTypeRelid(typid);
+		if (OidIsValid(typrelid))
+			glob->relationOids = lappend_oid(glob->relationOids, typrelid);
+	}
+}
+
 /*
  * Extract RangeTblEntries from a subquery that was never planned at all
  */
@@ -526,6 +555,8 @@ flatten_rtes_walker(Node *node, flatten_rtes_walker_context *cxt)
 		if (rte->rtekind == RTE_RELATION ||
 			(rte->rtekind == RTE_SUBQUERY && OidIsValid(rte->relid)))
 			add_rte_to_flat_rtable(cxt->glob, cxt->query->rteperminfos, rte);
+		else if (rte->rtekind == RTE_FUNCTION)
+			add_function_rte_relation_deps(cxt->glob, rte);
 		return false;
 	}
 	if (IsA(node, Query))
@@ -564,6 +595,9 @@ add_rte_to_flat_rtable(PlannerGlobal *glob, List *rteperminfos,
 {
 	RangeTblEntry *newrte;
 
+	if (rte->rtekind == RTE_FUNCTION)
+		add_function_rte_relation_deps(glob, rte);
+
 	/* flat copy to duplicate all the scalar fields */
 	newrte = palloc_object(RangeTblEntry);
 	memcpy(newrte, rte, sizeof(RangeTblEntry));
@@ -3767,6 +3801,8 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context)
 				(rte->rtekind == RTE_NAMEDTUPLESTORE && OidIsValid(rte->relid)))
 				context->glob->relationOids =
 					lappend_oid(context->glob->relationOids, rte->relid);
+			else if (rte->rtekind == RTE_FUNCTION)
+				add_function_rte_relation_deps(context->glob, rte);
 		}
 
 		/* And recurse into the query's subexpressions */
-- 
2.50.1 (Apple Git-155)

