From 3b4e48aa2068c00118b4b0a83f57109190ed7a58 Mon Sep 17 00:00:00 2001 From: Surya Poondla Date: Mon, 13 Jul 2026 13:22:54 -0700 Subject: [PATCH v11 2/4] Refresh composite-type snapshot on in-place record reassignment (bug #19382) The previous commit added a snapshot of reachable composite types on whole-record assignment (via assign_record_var). However, when the LHS record variable already has an ExpandedRecord and the RHS has a matching rowtype, exec_move_row_from_datum takes a fast path that overwrites the tuple bytes in place via expanded_record_set_tuple and returns without going through assign_record_var. On that path the snapshot was not refreshed, leaving stale nested-type versions behind. The visible symptom, reported by Zsolt Parragi: r out2; r := ROW(1, ROW(10, 20)::inn2)::out2; ALTER TYPE inn2 ALTER ATTRIBUTE y TYPE text; r := ROW(1, ROW(10, 'hello')::inn2)::out2; -- fresh valid data RETURN r; -- master returns it; -- previous patch errored At RETURN the check compared the stale inn2 snapshot against the current type-cache version, saw drift, and raised a false-positive error. Add snapshot_record_composite_types() calls at the two in-place set_tuple paths in exec_move_row_from_datum so any whole-record write refreshes the snapshot. Reassignment with fresh data after an ALTER now succeeds; reassignment without a follow-up write still errors. --- .../plpgsql/src/expected/plpgsql_record.out | 23 +++++++++++++++++++ src/pl/plpgsql/src/pl_exec.c | 9 ++++++++ src/pl/plpgsql/src/sql/plpgsql_record.sql | 19 +++++++++++++++ 3 files changed, 51 insertions(+) diff --git a/src/pl/plpgsql/src/expected/plpgsql_record.out b/src/pl/plpgsql/src/expected/plpgsql_record.out index 821aa4df90b..e6b5827ef91 100644 --- a/src/pl/plpgsql/src/expected/plpgsql_record.out +++ b/src/pl/plpgsql/src/expected/plpgsql_record.out @@ -1017,3 +1017,26 @@ CONTEXT: PL/pgSQL function bug19382_test_var_copy() line 6 at assignment drop function bug19382_test_var_copy(); drop type bug19382_outer4 cascade; drop type bug19382_inner4 cascade; +-- Case 9: Whole-record reassignment after inner-type alter. +-- The second assignment builds fresh data matching the post-ALTER type; +-- the snapshot must refresh so the reassignment succeeds. +create type bug19382_inner3 as (x int, y int); +create type bug19382_outer3 as (a int, b bug19382_inner3); +create function bug19382_test_reassign() returns bug19382_outer3 as $$ +declare r bug19382_outer3; +begin + r := row(1, row(10, 20)::bug19382_inner3)::bug19382_outer3; + alter type bug19382_inner3 alter attribute y type text; + r := row(1, row(10, 'hello')::bug19382_inner3)::bug19382_outer3; + return r; +end; +$$ language plpgsql; +select bug19382_test_reassign(); + bug19382_test_reassign +------------------------ + (1,"(10,hello)") +(1 row) + +drop function bug19382_test_reassign(); +drop type bug19382_outer3 cascade; +drop type bug19382_inner3 cascade; diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c index 70578b87c7b..4fa32238330 100644 --- a/src/pl/plpgsql/src/pl_exec.c +++ b/src/pl/plpgsql/src/pl_exec.c @@ -7799,6 +7799,13 @@ exec_move_row_from_datum(PLpgSQL_execstate *estate, { expanded_record_set_tuple(rec->erh, erh->fvalue, true, !estate->atomic); + /* + * The tuple bytes were replaced in place. Refresh the + * composite-type snapshot so a later ALTER TYPE on any + * reachable type is detected, and stale snapshot entries + * from a prior assignment are not carried forward. + */ + snapshot_record_composite_types(estate, rec); return; } @@ -7913,6 +7920,8 @@ exec_move_row_from_datum(PLpgSQL_execstate *estate, { expanded_record_set_tuple(rec->erh, &tmptup, true, !estate->atomic); + /* See comment in the analogous branch above. */ + snapshot_record_composite_types(estate, rec); return; } diff --git a/src/pl/plpgsql/src/sql/plpgsql_record.sql b/src/pl/plpgsql/src/sql/plpgsql_record.sql index 0ee7a131eea..9f33e49118a 100644 --- a/src/pl/plpgsql/src/sql/plpgsql_record.sql +++ b/src/pl/plpgsql/src/sql/plpgsql_record.sql @@ -698,3 +698,22 @@ select bug19382_test_var_copy(); drop function bug19382_test_var_copy(); drop type bug19382_outer4 cascade; drop type bug19382_inner4 cascade; + +-- Case 9: Whole-record reassignment after inner-type alter. +-- The second assignment builds fresh data matching the post-ALTER type; +-- the snapshot must refresh so the reassignment succeeds. +create type bug19382_inner3 as (x int, y int); +create type bug19382_outer3 as (a int, b bug19382_inner3); +create function bug19382_test_reassign() returns bug19382_outer3 as $$ +declare r bug19382_outer3; +begin + r := row(1, row(10, 20)::bug19382_inner3)::bug19382_outer3; + alter type bug19382_inner3 alter attribute y type text; + r := row(1, row(10, 'hello')::bug19382_inner3)::bug19382_outer3; + return r; +end; +$$ language plpgsql; +select bug19382_test_reassign(); +drop function bug19382_test_reassign(); +drop type bug19382_outer3 cascade; +drop type bug19382_inner3 cascade; -- 2.39.5 (Apple Git-154)