From 287c3aacc201787521525165e9a19566cda86a43 Mon Sep 17 00:00:00 2001 From: Andrey Borodin Date: Tue, 7 Apr 2026 15:27:43 +0500 Subject: [PATCH 1/4] 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 dad9d36937e..b8136666f05 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -3348,6 +3348,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)