From 4a0ba62859d170806c54c87bbb79e933defe99c1 Mon Sep 17 00:00:00 2001 From: mingjiegao Date: Thu, 5 Nov 2026 10:15:40 +0800 Subject: [PATCH v2 1/2] Fix initial sync of zero-column published tables Initial table sync treats an empty publisher column list as an unspecified COPY column list. When the subscriber has additional columns, this can insert an empty string instead of a local default in text mode, or fail with a field-count error in binary mode. Allow COPY initialization to distinguish an explicitly empty column list from an unspecified one, and use this in tablesync. --- src/backend/commands/copyfrom.c | 49 ++++++++++++++++----- src/backend/replication/logical/tablesync.c | 3 +- src/include/commands/copy.h | 5 +++ src/test/subscription/t/008_diff_schema.pl | 47 ++++++++++++++++++++ 4 files changed, 93 insertions(+), 11 deletions(-) diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c index 746b4fdf6c..d8d09a543f 100644 --- a/src/backend/commands/copyfrom.c +++ b/src/backend/commands/copyfrom.c @@ -1518,6 +1518,27 @@ CopyFrom(CopyFromState cstate) return processed; } +/* + * Wrapper for BeginCopyFromExtended() with attnamelist_is_explicit = false. + * + * A NIL attnamelist selects all target columns that are neither dropped nor + * generated. + */ +CopyFromState +BeginCopyFrom(ParseState *pstate, + Relation rel, + Node *whereClause, + const char *filename, + bool is_program, + copy_data_source_cb data_source_cb, + List *attnamelist, + List *options) +{ + return BeginCopyFromExtended(pstate, rel, whereClause, filename, + is_program, data_source_cb, attnamelist, + options, false); +} + /* * Setup to read tuples from a file for COPY FROM. * @@ -1526,20 +1547,25 @@ CopyFrom(CopyFromState cstate) * 'filename': Name of server-local file to read, NULL for STDIN * 'is_program': true if 'filename' is program to execute * 'data_source_cb': callback that provides the input data - * 'attnamelist': List of char *, columns to include. NIL selects all cols. + * 'attnamelist': List of char *, columns to include. * 'options': List of DefElem. See copy_opt_item in gram.y for selections. + * 'attnamelist_is_explicit': If true, a NIL attnamelist means zero input + * columns. Otherwise, NIL selects all target columns that are neither + * dropped nor generated. This is needed when a logical replication source + * has no columns. * * Returns a CopyFromState, to be passed to NextCopyFrom and related functions. */ CopyFromState -BeginCopyFrom(ParseState *pstate, - Relation rel, - Node *whereClause, - const char *filename, - bool is_program, - copy_data_source_cb data_source_cb, - List *attnamelist, - List *options) +BeginCopyFromExtended(ParseState *pstate, + Relation rel, + Node *whereClause, + const char *filename, + bool is_program, + copy_data_source_cb data_source_cb, + List *attnamelist, + List *options, + bool attnamelist_is_explicit) { CopyFromState cstate; bool pipe = (filename == NULL); @@ -1590,7 +1616,10 @@ BeginCopyFrom(ParseState *pstate, /* process common options or initialization */ /* Generate or convert list of attributes to process */ - cstate->attnumlist = CopyGetAttnums(tupDesc, cstate->rel, attnamelist); + if (attnamelist_is_explicit && attnamelist == NIL) + cstate->attnumlist = NIL; + else + cstate->attnumlist = CopyGetAttnums(tupDesc, cstate->rel, attnamelist); num_phys_attrs = tupDesc->natts; diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c index 4015e861a6..723012fb0c 100644 --- a/src/backend/replication/logical/tablesync.c +++ b/src/backend/replication/logical/tablesync.c @@ -1211,7 +1211,8 @@ copy_table(Relation rel) NULL, false, false); attnamelist = make_copy_attnamelist(relmapentry); - cstate = BeginCopyFrom(pstate, rel, NULL, NULL, false, copy_read_data, attnamelist, options); + cstate = BeginCopyFromExtended(pstate, rel, NULL, NULL, false, + copy_read_data, attnamelist, options, true); /* Do the copy */ (void) CopyFrom(cstate); diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h index abecfe5109..74203f3547 100644 --- a/src/include/commands/copy.h +++ b/src/include/commands/copy.h @@ -114,6 +114,11 @@ extern void ProcessCopyOptions(ParseState *pstate, CopyFormatOptions *opts_out, extern CopyFromState BeginCopyFrom(ParseState *pstate, Relation rel, Node *whereClause, const char *filename, bool is_program, copy_data_source_cb data_source_cb, List *attnamelist, List *options); +extern CopyFromState BeginCopyFromExtended(ParseState *pstate, Relation rel, + Node *whereClause, const char *filename, + bool is_program, copy_data_source_cb data_source_cb, + List *attnamelist, List *options, + bool attnamelist_is_explicit); extern void EndCopyFrom(CopyFromState cstate); extern bool NextCopyFrom(CopyFromState cstate, ExprContext *econtext, Datum *values, bool *nulls); diff --git a/src/test/subscription/t/008_diff_schema.pl b/src/test/subscription/t/008_diff_schema.pl index 96e3d6f588..2bf1fa79ab 100644 --- a/src/test/subscription/t/008_diff_schema.pl +++ b/src/test/subscription/t/008_diff_schema.pl @@ -31,6 +31,53 @@ $node_subscriber->safe_psql('postgres', # Setup logical replication my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres'; + +# A zero-column publisher still sends rows. Subscriber-only columns must +# get their defaults during initial COPY as well as incremental replication. +foreach my $binary ('false', 'true') +{ + $node_publisher->safe_psql( + 'postgres', q{ + CREATE TABLE tab_nocols (); + INSERT INTO tab_nocols DEFAULT VALUES; + INSERT INTO tab_nocols DEFAULT VALUES; + CREATE PUBLICATION pub_nocols FOR TABLE tab_nocols + WITH (publish = 'insert'); + }); + $node_subscriber->safe_psql('postgres', + "CREATE TABLE tab_nocols (local_value text DEFAULT 'subscriber-default')" + ); + $node_subscriber->safe_psql('postgres', + "CREATE SUBSCRIPTION sub_nocols CONNECTION '$publisher_connstr' PUBLICATION pub_nocols WITH (binary = $binary)" + ); + + $node_subscriber->wait_for_subscription_sync($node_publisher, 'sub_nocols'); + + is( $node_subscriber->safe_psql( + 'postgres', q{ + SELECT count(*) FROM tab_nocols + WHERE local_value = 'subscriber-default' + }), + '2', + "initial zero-column rows use local defaults (binary = $binary)"); + + $node_publisher->safe_psql('postgres', + "INSERT INTO tab_nocols DEFAULT VALUES"); + $node_publisher->wait_for_catchup('sub_nocols'); + is( $node_subscriber->safe_psql( + 'postgres', q{ + SELECT count(*) FROM tab_nocols + WHERE local_value = 'subscriber-default' + }), + '3', + "incremental zero-column rows use local defaults (binary = $binary)"); + + $node_subscriber->safe_psql('postgres', 'DROP SUBSCRIPTION sub_nocols'); + $node_publisher->safe_psql('postgres', + 'DROP PUBLICATION pub_nocols; DROP TABLE tab_nocols'); + $node_subscriber->safe_psql('postgres', 'DROP TABLE tab_nocols'); +} + $node_publisher->safe_psql('postgres', "CREATE PUBLICATION tap_pub FOR ALL TABLES"); -- 2.43.0