From 3acc4ebdaa78e834260cfa5c9ca082e7a40f46c0 Mon Sep 17 00:00:00 2001
From: Andrey Borodin <amborodin@acm.org>
Date: Tue, 7 Apr 2026 15:27:43 +0500
Subject: [PATCH v2 1/5] Add regression test for SETOF composite invalidation

Add an alter_table regression test for ALTER TYPE ADD ATTRIBUTE on a named
composite returned by a SECURITY DEFINER SQL SRF used from PL/pgSQL
RETURN QUERY.

Before the fix, a warmed plan can stay stale and raise a column-count
mismatch.  The test captures that behavior.
---
 src/test/regress/expected/alter_table.out | 47 +++++++++++++++++++++++
 src/test/regress/sql/alter_table.sql      | 38 ++++++++++++++++++
 2 files changed, 85 insertions(+)

diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out
index e167a41ce79..618706cb8b6 100644
--- a/src/test/regress/expected/alter_table.out
+++ b/src/test/regress/expected/alter_table.out
@@ -3349,6 +3349,53 @@ ALTER TYPE test_type3 DROP ATTRIBUTE a, ADD ATTRIBUTE b int;
 CREATE TYPE test_type_empty AS ();
 DROP TYPE test_type_empty;
 --
+-- Cached plans for queries that scan a set-returning function returning a
+-- named composite type must be invalidated when ALTER TYPE ADD ATTRIBUTE
+-- widens that type.  The fix records the composite's underlying relation OID
+-- (pg_type.typrelid) in the plan's invalItems so the relcache invalidation
+-- broadcast by ALTER TYPE reaches the cached plan.
+--
+-- Without the fix the plan is never marked stale.  A PL/pgSQL function whose
+-- RETURN QUERY calls such an SRF raises "structure of query does not match
+-- function result type" because its SPI plan still expects the old column
+-- count while the SRF (independently replanned via table relcache) already
+-- returns the new one.
+--
+-- SECURITY DEFINER prevents inlining; without inlining the outer plan holds
+-- only the proc OID in invalItems, so only the typrelid dependency added by
+-- the fix triggers its invalidation.
+--
+CREATE TYPE planinv_ct AS (a int, b int);
+CREATE TABLE planinv_tbl (a int, b int);
+INSERT INTO planinv_tbl VALUES (1, 2);
+CREATE FUNCTION planinv_srf() RETURNS SETOF planinv_ct
+  LANGUAGE sql STABLE SECURITY DEFINER AS $$ SELECT * FROM planinv_tbl $$;
+CREATE FUNCTION planinv_caller() RETURNS SETOF planinv_ct LANGUAGE plpgsql AS $$
+BEGIN
+  RETURN QUERY SELECT r.* FROM planinv_srf() r;
+END; $$;
+-- Warm up the plan cache.
+SELECT * FROM planinv_caller();
+ a | b 
+---+---
+ 1 | 2
+(1 row)
+
+ALTER TYPE planinv_ct ADD ATTRIBUTE c int;
+ALTER TABLE planinv_tbl ADD COLUMN c int DEFAULT 99;
+-- Without the fix: ERROR: structure of query does not match function result type
+-- With the fix: the plan is invalidated and the function returns all columns.
+SELECT * FROM planinv_caller();
+ a | b | c  
+---+---+----
+ 1 | 2 | 99
+(1 row)
+
+DROP FUNCTION planinv_caller();
+DROP FUNCTION planinv_srf();
+DROP TABLE planinv_tbl;
+DROP TYPE planinv_ct;
+--
 -- typed tables: OF / NOT OF
 --
 CREATE TYPE tt_t0 AS (z inet, x int, y numeric(8,2));
diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql
index f5f13bbd3e7..f4216ceb2e1 100644
--- a/src/test/regress/sql/alter_table.sql
+++ b/src/test/regress/sql/alter_table.sql
@@ -2055,6 +2055,44 @@ ALTER TYPE test_type3 DROP ATTRIBUTE a, ADD ATTRIBUTE b int;
 CREATE TYPE test_type_empty AS ();
 DROP TYPE test_type_empty;
 
+--
+-- Cached plans for queries that scan a set-returning function returning a
+-- named composite type must be invalidated when ALTER TYPE ADD ATTRIBUTE
+-- widens that type.  The fix records the composite's underlying relation OID
+-- (pg_type.typrelid) in the plan's invalItems so the relcache invalidation
+-- broadcast by ALTER TYPE reaches the cached plan.
+--
+-- Without the fix the plan is never marked stale.  A PL/pgSQL function whose
+-- RETURN QUERY calls such an SRF raises "structure of query does not match
+-- function result type" because its SPI plan still expects the old column
+-- count while the SRF (independently replanned via table relcache) already
+-- returns the new one.
+--
+-- SECURITY DEFINER prevents inlining; without inlining the outer plan holds
+-- only the proc OID in invalItems, so only the typrelid dependency added by
+-- the fix triggers its invalidation.
+--
+CREATE TYPE planinv_ct AS (a int, b int);
+CREATE TABLE planinv_tbl (a int, b int);
+INSERT INTO planinv_tbl VALUES (1, 2);
+CREATE FUNCTION planinv_srf() RETURNS SETOF planinv_ct
+  LANGUAGE sql STABLE SECURITY DEFINER AS $$ SELECT * FROM planinv_tbl $$;
+CREATE FUNCTION planinv_caller() RETURNS SETOF planinv_ct LANGUAGE plpgsql AS $$
+BEGIN
+  RETURN QUERY SELECT r.* FROM planinv_srf() r;
+END; $$;
+-- Warm up the plan cache.
+SELECT * FROM planinv_caller();
+ALTER TYPE planinv_ct ADD ATTRIBUTE c int;
+ALTER TABLE planinv_tbl ADD COLUMN c int DEFAULT 99;
+-- Without the fix: ERROR: structure of query does not match function result type
+-- With the fix: the plan is invalidated and the function returns all columns.
+SELECT * FROM planinv_caller();
+DROP FUNCTION planinv_caller();
+DROP FUNCTION planinv_srf();
+DROP TABLE planinv_tbl;
+DROP TYPE planinv_ct;
+
 --
 -- typed tables: OF / NOT OF
 --
-- 
2.50.1 (Apple Git-155)

