From 0418b9a6b1fe998fac8180522d14395b08a2a364 Mon Sep 17 00:00:00 2001 From: Bharath Rupireddy Date: Mon, 17 Aug 2026 22:27:25 +0000 Subject: [PATCH v5] 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 renders every tuple, so INSERT, UPDATE and DELETE output are all covered. Stored generated columns continue to be emitted 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 | 30 ++++++++++++++++++++++++++ contrib/test_decoding/sql/ddl.sql | 17 +++++++++++++++ contrib/test_decoding/test_decoding.c | 11 ++++++++++ 3 files changed, 58 insertions(+) diff --git a/contrib/test_decoding/expected/ddl.out b/contrib/test_decoding/expected/ddl.out index 6819812e806..3a953a34552 100644 --- a/contrib/test_decoding/expected/ddl.out +++ b/contrib/test_decoding/expected/ddl.out @@ -895,6 +895,36 @@ SELECT data FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'inc data (0 rows) \pset format aligned +-- Virtual generated columns are always stored as null in the tuple, so they +-- are not printed at all; a printed null would not be distinguishable from a +-- column that really contains a null. Stored generated columns are printed +-- as usual. +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), (2, 20); +UPDATE gtest1 SET b = 99 WHERE a = 1; +DELETE FROM gtest1 WHERE a = 2; +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 + table public.gtest1: INSERT: a[integer]:2 b[integer]:20 d[integer]:4 e[integer]:null + COMMIT + BEGIN + table public.gtest1: UPDATE: a[integer]:1 b[integer]:99 d[integer]:2 e[integer]:null + COMMIT + BEGIN + table public.gtest1: DELETE: a[integer]:2 + COMMIT +(10 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..5975e55d5ef 100644 --- a/contrib/test_decoding/sql/ddl.sql +++ b/contrib/test_decoding/sql/ddl.sql @@ -467,6 +467,23 @@ 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 +-- Virtual generated columns are always stored as null in the tuple, so they +-- are not printed at all; a printed null would not be distinguishable from a +-- column that really contains a null. Stored generated columns are printed +-- as usual. +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), (2, 20); +UPDATE gtest1 SET b = 99 WHERE a = 1; +DELETE FROM gtest1 WHERE a = 2; +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..61a311ab825 100644 --- a/contrib/test_decoding/test_decoding.c +++ b/contrib/test_decoding/test_decoding.c @@ -554,6 +554,17 @@ 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. pgoutput + * likewise never publishes virtual generated columns (see + * logicalrep_should_publish_column()). 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