From fd227526fe7ea6f766120451fad0f634b6b17e78 Mon Sep 17 00:00:00 2001 From: Bug Hunt Date: Tue, 5 May 2026 00:51:03 +0000 Subject: [PATCH] Omit virtual generated columns from test_decoding output Virtual generated columns are not stored on disk, so heap_getattr() in tuple_to_stringinfo() always returned NULL for them, producing misleading output such as table public.t: INSERT: a[integer]:1 b[integer]:10 c[integer]:null even though the user could observe a non-null value via SELECT. Stored generated columns continue to be emitted as before because their values do live in the heap tuple. This matches the policy in pgoutput's logicalrep_should_publish_column() which never publishes virtual generated columns. Also add a regression test (sql/generated.sql, expected/generated.out). --- contrib/test_decoding/Makefile | 3 +- contrib/test_decoding/expected/generated.out | 63 ++++++++++++++++++++ contrib/test_decoding/meson.build | 1 + contrib/test_decoding/sql/generated.sql | 39 ++++++++++++ contrib/test_decoding/test_decoding.c | 11 ++++ 5 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 contrib/test_decoding/expected/generated.out create mode 100644 contrib/test_decoding/sql/generated.sql diff --git a/contrib/test_decoding/Makefile b/contrib/test_decoding/Makefile index 01111243..6d050765 100644 --- a/contrib/test_decoding/Makefile +++ b/contrib/test_decoding/Makefile @@ -5,7 +5,8 @@ PGFILEDESC = "test_decoding - example of a logical decoding output plugin" REGRESS = ddl xact rewrite toast permissions decoding_in_xact \ decoding_into_rel binary prepared replorigin time messages \ - repack spill slot truncate stream stats twophase twophase_stream + repack spill slot truncate stream stats twophase twophase_stream \ + generated ISOLATION = mxact delayed_startup ondisk_startup concurrent_ddl_dml \ oldest_xmin snapshot_transfer subxact_without_top concurrent_stream \ twophase_snapshot slot_creation_error catalog_change_snapshot \ diff --git a/contrib/test_decoding/expected/generated.out b/contrib/test_decoding/expected/generated.out new file mode 100644 index 00000000..5aedb6b9 --- /dev/null +++ b/contrib/test_decoding/expected/generated.out @@ -0,0 +1,63 @@ +-- predictability +SET synchronous_commit = on; +SELECT 'init' FROM pg_create_logical_replication_slot('regression_slot', 'test_decoding'); + ?column? +---------- + init +(1 row) + +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 +); +INSERT INTO gtest1 (a, b) VALUES (1, 10), (2, 20); +UPDATE gtest1 SET b = 99 WHERE a = 1; +DELETE FROM gtest1 WHERE a = 2; +-- Virtual generated column "c" must be omitted (its value is not stored on +-- disk so heap_getattr() would otherwise emit a misleading NULL). Stored +-- generated column "d" is emitted normally because its value is on disk. +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 + table public.gtest1: INSERT: a[integer]:2 b[integer]:20 d[integer]:4 + COMMIT + BEGIN + table public.gtest1: UPDATE: a[integer]:1 b[integer]:99 d[integer]:2 + COMMIT + BEGIN + table public.gtest1: DELETE: a[integer]:2 + COMMIT +(10 rows) + +-- Table with only virtual generated columns alongside the key +CREATE TABLE gtest2 ( + a int PRIMARY KEY, + b int GENERATED ALWAYS AS (a + 1) VIRTUAL, + c text GENERATED ALWAYS AS ('row-' || a::text) VIRTUAL +); +INSERT INTO gtest2 (a) VALUES (10), (20); +SELECT data FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, + 'include-xids', '0', + 'skip-empty-xacts', '1'); + data +-------------------------------------------- + BEGIN + table public.gtest2: INSERT: a[integer]:10 + table public.gtest2: INSERT: a[integer]:20 + COMMIT +(4 rows) + +DROP TABLE gtest1; +DROP TABLE gtest2; +SELECT 'stop' FROM pg_drop_replication_slot('regression_slot'); + ?column? +---------- + stop +(1 row) + diff --git a/contrib/test_decoding/meson.build b/contrib/test_decoding/meson.build index ac655853..24de6543 100644 --- a/contrib/test_decoding/meson.build +++ b/contrib/test_decoding/meson.build @@ -42,6 +42,7 @@ tests += { 'stats', 'twophase', 'twophase_stream', + 'generated', ], 'regress_args': [ '--temp-config', files('logical.conf'), diff --git a/contrib/test_decoding/sql/generated.sql b/contrib/test_decoding/sql/generated.sql new file mode 100644 index 00000000..c57ee3a1 --- /dev/null +++ b/contrib/test_decoding/sql/generated.sql @@ -0,0 +1,39 @@ +-- predictability +SET synchronous_commit = on; + +SELECT 'init' FROM pg_create_logical_replication_slot('regression_slot', 'test_decoding'); + +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 +); + +INSERT INTO gtest1 (a, b) VALUES (1, 10), (2, 20); +UPDATE gtest1 SET b = 99 WHERE a = 1; +DELETE FROM gtest1 WHERE a = 2; + +-- Virtual generated column "c" must be omitted (its value is not stored on +-- disk so heap_getattr() would otherwise emit a misleading NULL). Stored +-- generated column "d" is emitted normally because its value is on disk. +SELECT data FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, + 'include-xids', '0', + 'skip-empty-xacts', '1'); + +-- Table with only virtual generated columns alongside the key +CREATE TABLE gtest2 ( + a int PRIMARY KEY, + b int GENERATED ALWAYS AS (a + 1) VIRTUAL, + c text GENERATED ALWAYS AS ('row-' || a::text) VIRTUAL +); + +INSERT INTO gtest2 (a) VALUES (10), (20); + +SELECT data FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, + 'include-xids', '0', + 'skip-empty-xacts', '1'); + +DROP TABLE gtest1; +DROP TABLE gtest2; +SELECT 'stop' FROM pg_drop_replication_slot('regression_slot'); diff --git a/contrib/test_decoding/test_decoding.c b/contrib/test_decoding/test_decoding.c index d5cf0fa0..83ce9e75 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; + /* + * Don't print virtual generated columns. Their values are not + * stored in the heap tuple, so heap_getattr() would always return + * NULL, which is misleading. This matches pgoutput's policy of + * never publishing virtual generated columns (see + * logicalrep_should_publish_column()). Stored generated columns + * are emitted as usual since their values are actually on disk. + */ + if (attr->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL) + continue; + typid = attr->atttypid; /* get Datum from tuple */ -- 2.43.0