From d613f4246f74df67e242dde93f985e5d8e1470e2 Mon Sep 17 00:00:00 2001
From: Bharath Rupireddy <bharath.rupireddyforpostgres@gmail.com>
Date: Tue, 18 Aug 2026 16:46:30 +0000
Subject: [PATCH v6] Omit virtual generated columns from test_decoding output.

Virtual generated columns are not stored on disk, so
heap_getattr() in tuple_to_stringinfo() always returns NULL for
them. test_decoding therefore emitted a NULL value for such a
column even though the user can observe a computed, possibly
non-null value via SELECT. This makes the output ambiguous: a
virtual generated column shown as NULL cannot be told apart from
a column that genuinely holds NULL, even though the two mean very
different things.

The logical replication pgoutput plugin already skips virtual
generated columns in logicalrep_should_publish_column().

Fix this by skipping virtual generated columns in
tuple_to_stringinfo(), the single place that prints every tuple.
Stored generated columns continue to be printed as before because
their values do live in the heap tuple.

No back-patch, as this changes test_decoding's output.

Author: Satya Narlapuram <satyanarlapuram@gmail.com>
Co-authored-by: Bharath Rupireddy <bharath.rupireddyforpostgres@gmail.com>
Reviewed-by: Euler Taveira <euler@eulerto.com>
Reviewed-by: Fujii Masao <masao.fujii@gmail.com>
Reviewed-by: Masahiko Sawada <sawada.mshk@gmail.com>
Discussion: https://postgr.es/m/CAHg%2BQDfTh3UbB-Ed--o2Bd%3DSBDJoEiG-qp3C0%2BETDibF63y%3Ddw%40mail.gmail.com
---
 contrib/test_decoding/expected/ddl.out | 18 ++++++++++++++++++
 contrib/test_decoding/sql/ddl.sql      | 12 ++++++++++++
 contrib/test_decoding/test_decoding.c  | 10 ++++++++++
 3 files changed, 40 insertions(+)

diff --git a/contrib/test_decoding/expected/ddl.out b/contrib/test_decoding/expected/ddl.out
index 6819812e806..a129c016d2b 100644
--- a/contrib/test_decoding/expected/ddl.out
+++ b/contrib/test_decoding/expected/ddl.out
@@ -895,6 +895,24 @@ SELECT data FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'inc
 data
 (0 rows)
 \pset format aligned
+-- check that virtual generated columns are not printed
+CREATE TABLE gtest1 (
+    a int PRIMARY KEY,
+    b int,
+    c int GENERATED ALWAYS AS (a + b) VIRTUAL,
+    d int GENERATED ALWAYS AS (a * 2) STORED,
+    e int
+);
+INSERT INTO gtest1 (a, b) VALUES (1, 10);
+SELECT data FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'include-xids', '0', 'skip-empty-xacts', '1');
+                                         data                                         
+--------------------------------------------------------------------------------------
+ BEGIN
+ table public.gtest1: INSERT: a[integer]:1 b[integer]:10 d[integer]:2 e[integer]:null
+ COMMIT
+(3 rows)
+
+DROP TABLE gtest1;
 SELECT pg_drop_replication_slot('regression_slot');
  pg_drop_replication_slot 
 --------------------------
diff --git a/contrib/test_decoding/sql/ddl.sql b/contrib/test_decoding/sql/ddl.sql
index 6d0b7d77778..de8a7cdc0d2 100644
--- a/contrib/test_decoding/sql/ddl.sql
+++ b/contrib/test_decoding/sql/ddl.sql
@@ -467,6 +467,18 @@ SELECT data FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'inc
 SELECT data FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'include-xids', '0', 'skip-empty-xacts', '1');
 \pset format aligned
 
+-- check that virtual generated columns are not printed
+CREATE TABLE gtest1 (
+    a int PRIMARY KEY,
+    b int,
+    c int GENERATED ALWAYS AS (a + b) VIRTUAL,
+    d int GENERATED ALWAYS AS (a * 2) STORED,
+    e int
+);
+INSERT INTO gtest1 (a, b) VALUES (1, 10);
+SELECT data FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'include-xids', '0', 'skip-empty-xacts', '1');
+DROP TABLE gtest1;
+
 SELECT pg_drop_replication_slot('regression_slot');
 
 /* check that the slot is gone */
diff --git a/contrib/test_decoding/test_decoding.c b/contrib/test_decoding/test_decoding.c
index d5cf0fa02b0..78185837aef 100644
--- a/contrib/test_decoding/test_decoding.c
+++ b/contrib/test_decoding/test_decoding.c
@@ -554,6 +554,16 @@ tuple_to_stringinfo(StringInfo s, TupleDesc tupdesc, HeapTuple tuple, bool skip_
 		if (attr->attnum < 0)
 			continue;
 
+		/*
+		 * Virtual generated columns are always stored as null in the tuple,
+		 * so don't print them at all. A printed null would not be
+		 * distinguishable from a column that really contains a null. Stored
+		 * generated columns are printed as usual since their values are
+		 * actually on disk.
+		 */
+		if (attr->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+			continue;
+
 		typid = attr->atttypid;
 
 		/* get Datum from tuple */
-- 
2.47.3

