From 58cda5c1b4bbf9f5a9452d971f365d17163bd9f2 Mon Sep 17 00:00:00 2001 From: Andrey Borodin Date: Tue, 14 Apr 2026 14:29:21 +0500 Subject: [PATCH 4/4] Lock SETOF composite return type during plpgsql execution For SETOF PL/pgSQL functions returning named composite tuples, acquire AccessShareLock on the return type's typrelid at function entry. This keeps rowshape stable for the whole execution under concurrent ALTER TYPE and avoids mid-statement mismatch errors. --- src/pl/plpgsql/src/pl_exec.c | 15 +++++++++++++ .../t/012_plpgsql_composite_replan_race.pl | 21 +++++++++++++++---- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c index 844f8080dc0..44c2f47506b 100644 --- a/src/pl/plpgsql/src/pl_exec.c +++ b/src/pl/plpgsql/src/pl_exec.c @@ -34,6 +34,7 @@ #include "parser/parse_coerce.h" #include "parser/parse_type.h" #include "plpgsql.h" +#include "storage/lmgr.h" #include "storage/proc.h" #include "tcop/cmdtag.h" #include "tcop/pquery.h" @@ -4038,6 +4039,20 @@ plpgsql_estate_setup(PLpgSQL_execstate *estate, estate->retistuple = func->fn_retistuple; estate->retisset = func->fn_retset; + /* + * Keep named composite SETOF return types stable for the whole function + * execution. This prevents concurrent ALTER TYPE from changing rowshape + * between statement setup and RETURN QUERY execution. + */ + if (estate->retisset && estate->retistuple) + { + Oid typrelid; + + typrelid = typeOrDomainTypeRelid(estate->fn_rettype); + if (OidIsValid(typrelid)) + LockRelationOid(typrelid, AccessShareLock); + } + estate->readonly_func = func->fn_readonly; estate->atomic = true; diff --git a/src/test/modules/test_misc/t/012_plpgsql_composite_replan_race.pl b/src/test/modules/test_misc/t/012_plpgsql_composite_replan_race.pl index e151409457d..0c5f169000a 100644 --- a/src/test/modules/test_misc/t/012_plpgsql_composite_replan_race.pl +++ b/src/test/modules/test_misc/t/012_plpgsql_composite_replan_race.pl @@ -51,7 +51,6 @@ SELECT injection_points_attach('plpgsql-return-query-before-exec', 'wait'); $backend2->query_until( qr/race_started/, q[ \echo race_started -BEGIN; SELECT * FROM planinv_caller(); \echo race_done ]); @@ -65,7 +64,12 @@ SELECT EXISTS ( ); ]) or die 'backend2 did not reach injection point in time'; -$node->safe_psql('postgres', q[ +my $ddl_backend = $node->background_psql('postgres', on_error_stop => 0); +my $ddl_pid = $ddl_backend->query_safe('SELECT pg_backend_pid()'); +chomp($ddl_pid); +$ddl_backend->query_until( + qr/ddl_started/, q[ +\echo ddl_started BEGIN; ALTER TYPE planinv_ct ADD ATTRIBUTE c int; CREATE OR REPLACE FUNCTION planinv_srf() RETURNS SETOF planinv_ct @@ -73,19 +77,28 @@ CREATE OR REPLACE FUNCTION planinv_srf() RETURNS SETOF planinv_ct SELECT a, b, 99 FROM planinv_tbl $$; COMMIT; +\echo ddl_done ]); $node->safe_psql('postgres', "SELECT injection_points_wakeup('plpgsql-return-query-before-exec');"); my $out = $backend2->query_until(qr/race_done/, q[]); -like($out, qr/^1\|2\|99$/m, - 'concurrent ALTER TYPE + CREATE OR REPLACE does not break RETURN QUERY'); +like($out, qr/^1\|2$/m, + 'in-progress statement keeps old row shape across concurrent DDL'); is($backend2->{stderr}, '', 'no tuple shape mismatch reported by RETURN QUERY'); ok($backend2->quit); +my $ddl_out = $ddl_backend->query_until(qr/ddl_done/, q[]); +is($ddl_backend->{stderr}, '', 'concurrent DDL session completed cleanly'); +like($ddl_out, qr/ddl_done/m, 'DDL proceeds after RETURN QUERY finishes'); +ok($ddl_backend->quit); + +is($node->safe_psql('postgres', 'SELECT * FROM planinv_caller();'), '1|2|99', + 'subsequent statement sees new composite row shape'); + $node->safe_psql('postgres', q[ DROP FUNCTION planinv_caller(); DROP FUNCTION planinv_srf(); -- 2.50.1 (Apple Git-155)