From 63a188921074a0a5cf359687e14f8b46b98d2264 Mon Sep 17 00:00:00 2001 From: Andrey Borodin Date: Tue, 14 Apr 2026 14:29:14 +0500 Subject: [PATCH 3/4] Add TAP test for plpgsql RETURN QUERY DDL race Add an injection-point TAP reproducer for concurrent ALTER TYPE + CREATE OR REPLACE FUNCTION during an in-progress RETURN QUERY call. The test captures the mid-statement rowshape mismatch scenario. --- src/pl/plpgsql/src/pl_exec.c | 6 ++ src/test/modules/test_misc/meson.build | 1 + .../t/012_plpgsql_composite_replan_race.pl | 96 +++++++++++++++++++ 3 files changed, 103 insertions(+) create mode 100644 src/test/modules/test_misc/t/012_plpgsql_composite_replan_race.pl diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c index 65b0fd0790f..844f8080dc0 100644 --- a/src/pl/plpgsql/src/pl_exec.c +++ b/src/pl/plpgsql/src/pl_exec.c @@ -41,6 +41,7 @@ #include "utils/builtins.h" #include "utils/datum.h" #include "utils/fmgroids.h" +#include "utils/injection_point.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/rel.h" @@ -3594,6 +3595,11 @@ exec_stmt_return_query(PLpgSQL_execstate *estate, /* There might be some tuples in the tuplestore already */ tcount = tuplestore_tuple_count(estate->tuple_store); + /* + * Test-only pause point for RETURN QUERY race conditions. + */ + INJECTION_POINT("plpgsql-return-query-before-exec", NULL); + /* * Set up DestReceiver to transfer results directly to tuplestore, * converting rowtype if necessary. DestReceiver lives in mcontext. diff --git a/src/test/modules/test_misc/meson.build b/src/test/modules/test_misc/meson.build index 1b25d98f7f3..a4f4f57134b 100644 --- a/src/test/modules/test_misc/meson.build +++ b/src/test/modules/test_misc/meson.build @@ -20,6 +20,7 @@ tests += { 't/009_log_temp_files.pl', 't/010_index_concurrently_upsert.pl', 't/011_lock_stats.pl', + 't/012_plpgsql_composite_replan_race.pl', ], # The injection points are cluster-wide, so disable installcheck 'runningcheck': false, 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 new file mode 100644 index 00000000000..e151409457d --- /dev/null +++ b/src/test/modules/test_misc/t/012_plpgsql_composite_replan_race.pl @@ -0,0 +1,96 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +if ($ENV{enable_injection_points} ne 'yes') +{ + plan skip_all => 'Injection points not supported by this build'; +} + +my $node = PostgreSQL::Test::Cluster->new('plpgsql_composite_replan_race'); +$node->init; +$node->start; + +if (!$node->check_extension('injection_points')) +{ + plan skip_all => 'Extension injection_points not installed'; +} + +$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;'); + +$node->safe_psql('postgres', q[ +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 a, b 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 expression/plan caches first. +is($node->safe_psql('postgres', 'SELECT * FROM planinv_caller();'), '1|2', + 'warmup call returns initial row shape'); + +my $backend2 = $node->background_psql('postgres', on_error_stop => 0); +$backend2->query_safe(q[ +SELECT injection_points_set_local(); +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 +]); + +$node->poll_query_until('postgres', q[ +SELECT EXISTS ( + SELECT 1 + FROM pg_stat_activity + WHERE wait_event_type = 'InjectionPoint' + AND wait_event = 'plpgsql-return-query-before-exec' +); +]) or die 'backend2 did not reach injection point in time'; + +$node->safe_psql('postgres', q[ +BEGIN; +ALTER TYPE planinv_ct ADD ATTRIBUTE c int; +CREATE OR REPLACE FUNCTION planinv_srf() RETURNS SETOF planinv_ct + LANGUAGE sql STABLE SECURITY DEFINER AS $$ + SELECT a, b, 99 FROM planinv_tbl + $$; +COMMIT; +]); + +$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'); +is($backend2->{stderr}, '', + 'no tuple shape mismatch reported by RETURN QUERY'); + +ok($backend2->quit); + +$node->safe_psql('postgres', q[ +DROP FUNCTION planinv_caller(); +DROP FUNCTION planinv_srf(); +DROP TABLE planinv_tbl; +DROP TYPE planinv_ct; +]); + +done_testing(); -- 2.50.1 (Apple Git-155)