From d613f4246f74df67e242dde93f985e5d8e1470e2 Mon Sep 17 00:00:00 2001 From: Bharath Rupireddy 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 Co-authored-by: Bharath Rupireddy Reviewed-by: Euler Taveira Reviewed-by: Fujii Masao Reviewed-by: Masahiko Sawada 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