From 534f072b6c9aa3280a2394231a5f40a073659f3c Mon Sep 17 00:00:00 2001 From: Muzzammil Sarwar Date: Thu, 15 Oct 2026 03:41:23 +0500 Subject: [PATCH v1] Detoast a returned record before keeping it in a non-atomic context. Since 2efc924180f, PL/pgSQL detoasts the values it stores into variables in a non-atomic context (a DO block or a procedure that can COMMIT or ROLLBACK), so that a later transaction-control command can't leave a variable holding TOAST pointers into rows that may be gone. But exec_move_row_from_datum() simply took over a read/write expanded record of the right type, which is what a PL/pgSQL function hands back when it returns a record variable. That record was filled inside the function, which always runs atomically, so its fields may still be out-of-line values. After a ROLLBACK, or after a COMMIT once the row had been deleted and vacuumed, reading them failed with "missing chunk number 0 for toast value ...". A procedure's composite argument computed by a function call, as in CALL p(f()), had the same problem. Fix by inlining the out-of-line fields of such a record before taking it over in a non-atomic context; EOH_get_flat_size() does that in place for an expanded record. Oversight in commit 2efc924180f. Back-patch to all supported branches. --- src/pl/plpgsql/src/pl_exec.c | 12 +++++ src/test/isolation/expected/plpgsql-toast.out | 46 +++++++++++++++++++ src/test/isolation/specs/plpgsql-toast.spec | 20 ++++++++ 3 files changed, 78 insertions(+) diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c index bc1667f7a13..4a50341d59e 100644 --- a/src/pl/plpgsql/src/pl_exec.c +++ b/src/pl/plpgsql/src/pl_exec.c @@ -7727,6 +7727,18 @@ exec_move_row_from_datum(PLpgSQL_execstate *estate, (rec->rectypeid == RECORDOID && !ExpandedRecordIsDomain(erh)))) { + /* + * In a non-atomic context, though, the record must not keep + * any out-of-line field values, since such TOAST pointers + * might become stale after a COMMIT or ROLLBACK. Computing + * the record's flat size inlines them in place (see + * ER_get_flat_size). + */ + if (!estate->atomic && (erh->flags & ER_FLAG_HAVE_EXTERNAL)) + { + (void) EOH_get_flat_size(&erh->hdr); + Assert(!(erh->flags & ER_FLAG_HAVE_EXTERNAL)); + } assign_record_var(estate, rec, erh); return; } diff --git a/src/test/isolation/expected/plpgsql-toast.out b/src/test/isolation/expected/plpgsql-toast.out index 0fee7953acc..e8073ffe5bd 100644 --- a/src/test/isolation/expected/plpgsql-toast.out +++ b/src/test/isolation/expected/plpgsql-toast.out @@ -284,6 +284,52 @@ s1: NOTICE: length(r) = 9002 s1: NOTICE: length(r) = 12002 step assign6: <... completed> +starting permutation: lock assign7 vacuum unlock +pg_advisory_unlock_all +---------------------- + +(1 row) + +pg_advisory_unlock_all +---------------------- + +(1 row) + +step lock: + SELECT pg_advisory_lock(1); + +pg_advisory_lock +---------------- + +(1 row) + +step assign7: +do $$ + declare + r test1; + begin + r := get_test1(); + delete from test1; + commit; + perform pg_advisory_lock(1); + raise notice 'length(r) = %', length(r::text); + end; +$$; + +step vacuum: + VACUUM test1; + +step unlock: + SELECT pg_advisory_unlock(1); + +pg_advisory_unlock +------------------ +t +(1 row) + +s1: NOTICE: length(r) = 6004 +step assign7: <... completed> + starting permutation: fetch-after-commit pg_advisory_unlock_all ---------------------- diff --git a/src/test/isolation/specs/plpgsql-toast.spec b/src/test/isolation/specs/plpgsql-toast.spec index bb444fc9ded..5a7ce1d88d3 100644 --- a/src/test/isolation/specs/plpgsql-toast.spec +++ b/src/test/isolation/specs/plpgsql-toast.spec @@ -14,10 +14,13 @@ setup ALTER TABLE test1 ALTER COLUMN b SET STORAGE EXTERNAL; INSERT INTO test1 VALUES (1, repeat('foo', 2000)); CREATE TYPE test2 AS (a bigint, b text); + CREATE FUNCTION get_test1() RETURNS test1 LANGUAGE plpgsql + AS $$ declare r test1; begin select * into r from test1; return r; end $$; } teardown { + DROP FUNCTION get_test1(); DROP TABLE test1; DROP TYPE test2; } @@ -131,6 +134,22 @@ do $$ $$; } +# exec_move_row_from_datum(), given a function's read/write expanded record +step assign7 +{ +do $$ + declare + r test1; + begin + r := get_test1(); + delete from test1; + commit; + perform pg_advisory_lock(1); + raise notice 'length(r) = %', length(r::text); + end; +$$; +} + # Check that the results of a query can be detoasted just after committing # (there's no interaction with VACUUM here) step "fetch-after-commit" @@ -175,4 +194,5 @@ permutation lock assign3 vacuum unlock permutation lock assign4 vacuum unlock permutation lock assign5 vacuum unlock permutation lock assign6 vacuum unlock +permutation lock assign7 vacuum unlock permutation "fetch-after-commit" -- 2.47.3