[PATCH v1] Fix initial sync of zero-column published tables

Started by mingjiegao1 day ago2 messageshackers
Beta feature

Hackorum builds and tests every patch posted to the lists, not only commitfest submissions. This is Hackorum's own CI rather than the PostgreSQL project's, and it is still under testing - please report anything that looks wrong.

appliessuccessCI history

You can run a PostgreSQL built from this patch straight from Docker, with no checkout and no build:

docker run --rm -p 5432:5432 ghcr.io/hackorum-dev/postgres-patch:t254504
psql -h localhost -U postgres

Built from patchset v1 (message #1), November 02, 2026 at 04:33 AM.

Every patchset is also pushed to a branch of our PostgreSQL fork, so you can check out the same tree CI built. Without a PostgreSQL checkout:

git clone --branch t254504_1 https://github.com/hackorum-dev/postgres.git

In a checkout you already have, add the fork once:

git remote add hackorum https://github.com/hackorum-dev/postgres.git

then, for this patchset and every later one:

git fetch hackorum t254504_1 && git checkout t254504_1

Patchset v1 (message #1) is on t254504_1

Jump to latest
#1mingjiegao
mingjie.gmj@gmail.com

Hi,

Initial table synchronization mishandles a zero-column published table
when the subscriber has additional columns. In text mode, COPY can put
an empty string into a subscriber-only column instead of its default.
Subsequent inserts through the apply worker use the default correctly.
In binary mode, the initial copy fails with a field-count mismatch.

I reproduced this on master (20devel) at:
77705d1e5a616339c3e92f586bbfa7b6bb562beb

For example, use two fresh instances with logical replication enabled
on the publisher:

-- Publisher:
CREATE TABLE lr02_t ();
INSERT INTO lr02_t DEFAULT VALUES;
CREATE PUBLICATION lr02_pub FOR TABLE lr02_t WITH (publish = 'insert');

-- Subscriber (replace the connection string):
CREATE TABLE lr02_t (v text DEFAULT 'subscriber-default');
CREATE SUBSCRIPTION lr02_sub
CONNECTION '<publisher connection string>'
PUBLICATION lr02_pub WITH (copy_data = true, binary = false);

On the subscriber, wait for this query to return 'r':

SELECT srsubstate FROM pg_subscription_rel
WHERE srsubid = (SELECT oid FROM pg_subscription
WHERE subname = 'lr02_sub');

Then run:

SELECT quote_nullable(v) FROM lr02_t;

The result is '' instead of 'subscriber-default'. Inserting another row
with DEFAULT VALUES on the publisher produces a correctly defaulted row
on the subscriber once replication catches up. Thus initial and
incremental replication disagree for the same source data.

Repeating the example on fresh instances with binary = true instead
makes initial synchronization fail with:

ERROR: row field count is 0, expected 1

make_copy_attnamelist() returns NIL for the zero-column source, but
BeginCopyFrom() interprets NIL as an unspecified column list and selects
all target non-dropped, non-generated columns. Those columns are then
treated as input columns, so their default expressions are not prepared.

The attached patch adds BeginCopyFromExtended() with a flag indicating
that the column list is explicit. Tablesync uses that entry point, so
an empty list remains empty. This is resolved before default expressions
and parser state are initialized. The existing BeginCopyFrom() interface
retains its behavior for SQL COPY, file_fdw and other callers.

The change keeps row parsing, default evaluation, constraints, triggers
and partition routing in the existing COPY path. There are no SQL syntax
or replication protocol changes. Its scope is the handling of an empty
input column list after remote column discovery; it does not change
which publisher columns are selected for replication.

The patch includes text and binary regression coverage in
008_diff_schema.pl. With only the test changes applied to the baseline,
three assertions fail and two dependent assertions are skipped. With
the fix, all 11 assertions pass.

To reproduce the failure, apply just the test portion to a clean
checkout of the baseline:

git apply --include=src/test/subscription/t/008_diff_schema.pl \
/path/to/v1-0001-Fix-initial-sync-of-zero-column-published-tables.patch

From a configured and built tree with TAP tests enabled, run:

make -j8 -C src/test/subscription check \
PROVE_TESTS=t/008_diff_schema.pl

For an out-of-tree build, run the make command from the build directory.
Applying the remaining source/header changes and rebuilding makes the
test pass.

Testing used Linux x86_64, GCC 15.2, -O2 -g, with assertions disabled.
The targeted subscription tests 004_sync, 008_diff_schema, 014_binary
and 031_column_list pass (64 assertions). A separate 24-assertion test
also passes, covering local defaults, identity values, NULLs, subscriber
stored generated columns, replica triggers, partition routing, dropped
publisher columns and NOT NULL checks in text and binary modes.

The test_setup, copy, copyselect, copydml and copyencoding regression
tests, and the file_fdw regression test, also pass. I have not tested
older major versions or run the full test suite.

Comments on the internal COPY interface change are welcome.

Best regards,
mingjiegao

Attachments:

t254504_1
v1-0001-Fix-initial-sync-of-zero-column-published-tables.patchtext/x-patch; charset=US-ASCII; name=v1-0001-Fix-initial-sync-of-zero-column-published-tables.patchDownload+104-3
#2mingjiegao
mingjie.gmj@gmail.com
In reply to: mingjiegao (#1)
Re: [PATCH v1] Fix initial sync of zero-column published tables

Hi,

I've attached lr02.sql, a minimal psql reproducer for this issue in
text mode (binary = false). It uses separate publisher and subscriber
databases in the same instance.

On an affected build, the initial COPY row gets an empty string
instead of the subscriber's default, while a subsequent incremental
INSERT uses the default correctly.

Please run it on a test instance with logical replication enabled and
authentication configured for the subscription connection.

Best regards,
mingjiegao

Attachments:

lr02.sqltext/plain; charset=US-ASCII; name=lr02.sqlDownload