From e8798ed747c6dc9e2930af5dce10de262d1e929a Mon Sep 17 00:00:00 2001
From: "Chao Li (Evan)" <lic@highgo.com>
Date: Thu, 4 Jun 2026 13:51:24 +0800
Subject: [PATCH v2] Fix tuple deformation with virtual generated NOT NULL
 columns

TupleDescFinalize() computes firstNonGuaranteedAttr for the slot
deformation fast path. Virtual generated columns can have valid NOT NULL
constraints, but they are not physically stored in heap tuples. Treating
such columns as part of the guaranteed physical prefix can make tuple
deformation advance the data offset as if the virtual column were stored,
causing following attributes to be read from the wrong location.

Exclude virtual generated columns from the guaranteed prefix while still
allowing stored generated columns to use the optimization.

Add a regression test with a virtual generated NOT NULL column followed by
another fixed-width NOT NULL column, which previously exposed the wrong
offset calculation.

Author: Chao Li <lic@highgo.com>
Reviewed-by: ChangAo Chen <cca5507@qq.com>
Discussion: https://postgr.es/m/A4BC563C-0CA3-4EF3-952A-EA41F9E5BF1E@gmail.com
---
 src/backend/access/common/tupdesc.c             | 11 ++++++++---
 src/test/regress/expected/generated_virtual.out |  9 +++++++++
 src/test/regress/sql/generated_virtual.sql      |  5 +++++
 3 files changed, 22 insertions(+), 3 deletions(-)

diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c
index 196472c05d0..25267f7a9e5 100644
--- a/src/backend/access/common/tupdesc.c
+++ b/src/backend/access/common/tupdesc.c
@@ -517,18 +517,23 @@ TupleDescFinalize(TupleDesc tupdesc)
 	for (int i = 0; i < tupdesc->natts; i++)
 	{
 		CompactAttribute *cattr = TupleDescCompactAttr(tupdesc, i);
+		bool		isVirtualGenerated = cattr->attgenerated &&
+			TupleDescAttr(tupdesc, i)->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL;
 
 		/*
 		 * Find the highest attnum which is guaranteed to exist in all tuples
 		 * in the table.  We currently only pay attention to byval attributes
-		 * to allow additional optimizations during tuple deformation.
+		 * to allow additional optimizations during tuple deformation. Virtual
+		 * generated columns are excluded, since they are computed at read
+		 * time and are not physically stored in tuples.
 		 */
 		if (firstNonGuaranteedAttr == tupdesc->natts &&
 			(cattr->attnullability != ATTNULLABLE_VALID || !cattr->attbyval ||
-			 cattr->atthasmissing || cattr->attisdropped || cattr->attlen <= 0))
+			 cattr->atthasmissing || cattr->attisdropped || cattr->attlen <= 0 ||
+			 isVirtualGenerated))
 			firstNonGuaranteedAttr = i;
 
-		if (cattr->attlen <= 0)
+		if (isVirtualGenerated || cattr->attlen <= 0)
 			break;
 
 		off = att_nominal_alignby(off, cattr->attalignby);
diff --git a/src/test/regress/expected/generated_virtual.out b/src/test/regress/expected/generated_virtual.out
index 24d5dbf46ca..7a5788146f5 100644
--- a/src/test/regress/expected/generated_virtual.out
+++ b/src/test/regress/expected/generated_virtual.out
@@ -727,6 +727,15 @@ ERROR:  null value in column "b" of relation "gtest21b" violates not-null constr
 DETAIL:  Failing row contains (null, virtual).
 ALTER TABLE gtest21b ALTER COLUMN b DROP NOT NULL;
 INSERT INTO gtest21b (a) VALUES (0);  -- ok now
+-- virtual generated columns are not physically stored, even when not null
+CREATE TABLE gtest21c (a int NOT NULL, b int GENERATED ALWAYS AS (a * 2) VIRTUAL NOT NULL, c int NOT NULL);
+INSERT INTO gtest21c (a, c) VALUES (10, 42);
+SELECT a, b, c FROM gtest21c;
+ a  | b  | c  
+----+----+----
+ 10 | 20 | 42
+(1 row)
+
 -- not-null constraint with partitioned table
 CREATE TABLE gtestnn_parent (
     f1 int,
diff --git a/src/test/regress/sql/generated_virtual.sql b/src/test/regress/sql/generated_virtual.sql
index 9c2bb6590b3..126ae3ecda9 100644
--- a/src/test/regress/sql/generated_virtual.sql
+++ b/src/test/regress/sql/generated_virtual.sql
@@ -374,6 +374,11 @@ INSERT INTO gtest21b (a) VALUES (NULL);  -- error
 ALTER TABLE gtest21b ALTER COLUMN b DROP NOT NULL;
 INSERT INTO gtest21b (a) VALUES (0);  -- ok now
 
+-- virtual generated columns are not physically stored, even when not null
+CREATE TABLE gtest21c (a int NOT NULL, b int GENERATED ALWAYS AS (a * 2) VIRTUAL NOT NULL, c int NOT NULL);
+INSERT INTO gtest21c (a, c) VALUES (10, 42);
+SELECT a, b, c FROM gtest21c;
+
 -- not-null constraint with partitioned table
 CREATE TABLE gtestnn_parent (
     f1 int,
-- 
2.50.1 (Apple Git-155)

