From 3b4e48aa2068c00118b4b0a83f57109190ed7a58 Mon Sep 17 00:00:00 2001
From: Surya Poondla <suryapoondla4@gmail.com>
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)

