On Thu, Oct 22, 2026 at 5:23 PM Nisha Moond <[email protected]> wrote:
>
> On Fri, Oct 16, 2026 at 8:03 PM Nisha Moond <[email protected]> wrote:
> >
> > Attached is v82, which addresses the comments on v81:
> >
>
> We have not yet documented that, in rare cases, the subscriber can
> still fail with the 1GB error when recording a conflict.
>
> The 64kB cap on replica identity values is checked after the key
> value's output function has produced its text, so it limits what we
> store, not what we render. Normally that's fine: the publisher already
> rendered the same value to send it, which keeps the text well within
> the limit. That doesn't hold in a few rare cases:
> - the key column has a different data type on the subscriber (e.g.
> text published into a numeric[] column);
> - binary = true, where the publisher never produced the text;
> - the apply worker's bytea_output or quote_all_identifiers differs
> from the publisher's.
>
> In these cases a small stored value can render to more than 1GB, and
> the apply worker errors out.
>
The attached file has standalone steps to reproduce each of the above
cases where recording a conflict in CLT can still hit the 1GB limit on
the subscriber.
These remaining cases are not specific to the CLT. The same problem
already exists wherever we call a type's output function to put a
value into a message.
On PG17 and older branches too, the subscriber already hits OOM on a
key violation. The apply worker fails while building the
unique-violation message: _bt_check_unique() ->
BuildIndexValueDescription() -> record_out()
Below is a test case for binary = true that reproduces this on PG17.
All four cases could fail the same way on PG17 when the subscriber
hits a key violation.
[PG17 binary=true testcase]
---------------------------------
-- [PUB] and [SUB]
DO $$ BEGIN
CREATE TYPE ri_n0 AS (f text);
FOR i IN 1..24 LOOP
EXECUTE format('CREATE TYPE ri_n%s AS (f ri_n%s)', i, i - 1);
END LOOP;
END $$;
-- [PUB]
CREATE TABLE kv1 (k ri_n24);
CREATE PUBLICATION pub_kv1 FOR TABLE kv1;
-- [SUB] the key exists locally
CREATE TABLE kv1 (k ri_n24 PRIMARY KEY);
DO $$ DECLARE r text := format('ROW(%L)::ri_n0', repeat('"', 60));
BEGIN
FOR i IN 1..24 LOOP r := format('ROW(%s)::ri_n%s', r, i); END LOOP;
EXECUTE format('INSERT INTO kv1 VALUES (%s)', r);
END $$;
CREATE SUBSCRIPTION sub_kv1 CONNECTION 'port=9943 dbname=postgres'
PUBLICATION pub_kv1 WITH (copy_data = false, binary = true);
-- [PUB] same key, sent in binary
DO $$ DECLARE r text := format('ROW(%L)::ri_n0', repeat('"', 60));
BEGIN
FOR i IN 1..24 LOOP r := format('ROW(%s)::ri_n%s', r, i); END LOOP;
EXECUTE format('INSERT INTO kv1 VALUES (%s)', r);
END $$;
-- [SUB] expect: ERROR: out of memory, instead of the unique violation:
ERROR: out of memory
DETAIL: Cannot enlarge string buffer containing 1073741822 bytes by 1
more bytes.
CONTEXT: processing remote data for replication origin "pg_16614"
during message type "INSERT" for replication target relation
"public.kv1" in transaction 761, finished at 0/1590948
~~~~
--
Thanks,
Nisha
Case 1: different column type on publisher and subscriber
-----------------------------------------------------------
The publisher's text value is about 590kB. The subscriber reads it as numeric[]
(each 1e-16383 prints 16,386 characters), about 1.07GB.
-- [PUB] insert before subscribing, so the row never exists on the subscriber
CREATE TABLE ttyp (k text);
ALTER TABLE ttyp REPLICA IDENTITY FULL;
INSERT INTO ttyp SELECT '{' || repeat('1e-16383,', 65599) || '1e-16383}';
CREATE PUBLICATION pub_typ FOR TABLE ttyp;
-- [SUB]
CREATE TABLE ttyp (k numeric[] PRIMARY KEY);
CREATE SUBSCRIPTION sub_typ CONNECTION 'port=9933 dbname=postgres'
PUBLICATION pub_typ
WITH (copy_data = false,
conflict_log_destination = 'table');
-- [PUB] triggers delete_missing on the sub
DELETE FROM ttyp;
-- [SUB] expect in sub.log: ERROR: invalid memory alloc request size 1074921602
LOG: conflict detected on relation "public.ttyp": conflict=delete_missing
DETAIL: Conflict details are logged to the conflict log table:
pg_conflict.pg_conflict_log_16410
CONTEXT: processing remote data for replication origin "pg_16410" during
message type "DELETE" for replication target relation "public.ttyp" in
transaction 675, finished at 0/017CCD60
ERROR: invalid memory alloc request size 1074921602
-- Cleanup
-- [SUB] DROP SUBSCRIPTION sub_typ; DROP TABLE ttyp;
-- [PUB] DROP PUBLICATION pub_typ; DROP TABLE ttyp;
Case 2: binary = true (same types on both sides)
---------------------------------------------------
The publisher sends numeric[] in binary form, about 115kB. The subscriber
renders it as text (each 1e131071 prints 131,073 digits), about 1.07GB.
-- [PUB] insert before subscribing, so the row never exists on the subscriber
CREATE TABLE tbin (k numeric[]);
ALTER TABLE tbin REPLICA IDENTITY FULL;
INSERT INTO tbin SELECT ARRAY(SELECT 1e131071::numeric FROM generate_series(1,
8200));
CREATE PUBLICATION pub_bin FOR TABLE tbin;
-- [SUB]
CREATE TABLE tbin (k numeric[] PRIMARY KEY);
CREATE SUBSCRIPTION sub_bin CONNECTION 'port=9933 dbname=postgres'
PUBLICATION pub_bin
WITH (copy_data = false, binary = true,
conflict_log_destination = 'table');
-- [PUB] triggers delete_missing on the subscriber
DELETE FROM tbin;
-- [SUB] expect in sub.log: ERROR: invalid memory alloc request size ...
LOG: conflict detected on relation "public.tbin": conflict=delete_missing
DETAIL: Conflict details are logged to the conflict log table:
pg_conflict.pg_conflict_log_16392
CONTEXT: processing remote data for replication origin "pg_16392" during
message type "DELETE" for replication target relation "public.tbin" in
transaction 668, finished at 0/017C1548
ERROR: invalid memory alloc request size 1074798602
-- Cleanup
-- [SUB] DROP SUBSCRIPTION sub_bin; DROP TABLE tbin;
-- [PUB] DROP PUBLICATION pub_bin; DROP TABLE tbin;
Case 3: bytea_output differs on the subscriber
---------------------------------------------------
The key is a bytea nested in 16 composite levels, about 250 bytes stored. In
hex (the publisher's form) it renders to about 413kB. In escape, every byte
gets a backslash, and each nesting level doubles the backslashes, so the text
goes past 1GB.
-- [PUB] and [SUB] same types and table on both nodes
DO $$ BEGIN
CREATE TYPE b0 AS (f bytea);
FOR i IN 1..16 LOOP
EXECUTE format('CREATE TYPE b%s AS (f b%s)', i, i - 1);
END LOOP;
END $$;
CREATE TABLE tbyt (k b16 PRIMARY KEY);
-- [PUB]
CREATE PUBLICATION pub_byt FOR TABLE tbyt;
-- [SUB]
CREATE SUBSCRIPTION sub_byt CONNECTION 'port=9933 dbname=postgres'
PUBLICATION pub_byt
WITH (copy_data = false,
conflict_log_destination = 'table');
-- [PUB] replicates normally
DO $$ DECLARE s text := 'ROW(decode(repeat(''80'', 10000), ''hex''))::b0';
BEGIN
FOR i IN 1..16 LOOP s := format('ROW(%s)::b%s', s, i); END LOOP;
EXECUTE 'INSERT INTO tbyt VALUES (' || s || ')';
END $$;
-- [SUB] row arrived; switch the apply worker to escape and remove the row
locally
SELECT count(*) FROM tbyt; -- 1 (don't SELECT
k)
ALTER SYSTEM SET bytea_output = 'escape';
SELECT pg_reload_conf();
DELETE FROM tbyt;
-- [PUB] triggers delete_missing on the subscriber
DELETE FROM tbyt;
-- [SUB] expect in sub.log: ERROR: string buffer exceeds maximum allowed length
LOG: parameter "bytea_output" changed to "escape"
LOG: conflict detected on relation "public.tbyt": conflict=delete_missing
DETAIL: Conflict details are logged to the conflict log table:
pg_conflict.pg_conflict_log_16475
CONTEXT: processing remote data for replication origin "pg_16475" during
message type "DELETE" for replication target relation "public.tbyt" in
transaction 682, finished at 0/017F28C8
ERROR: string buffer exceeds maximum allowed length (1073741823 bytes)
DETAIL: Cannot enlarge string buffer containing 1073741822 bytes by 1 more
bytes.
-- [SUB] control: back to hex, retry; the row is recorded with an omitted
marker
ALTER SYSTEM RESET bytea_output;
SELECT pg_reload_conf();
SELECT conflict_type, has_omitted_values FROM :clt; -- delete_missing
| t
-- Cleanup
-- [SUB] DROP SUBSCRIPTION sub_byt; DROP TABLE tbyt; DROP TYPE b0 CASCADE;
-- [PUB] DROP PUBLICATION pub_byt; DROP TABLE tbyt; DROP TYPE b0 CASCADE;
Case 4: quote_all_identifiers differs on the subscriber
----------------------------------------------------------
The key is a regclass[] of 2,100 × pg_class, nested in 16 composite levels,
about 230 bytes stored. With quotes off (the publisher's form) it renders to
about 281kB. With quote_all_identifiers = on, every name gets quotes, each
nesting level doubles them, and the text goes past 1GB.
-- The array names a catalog table on purpose. The apply worker uses
search_path = '', so an unqualified user table name would fail to resolve on
the subscriber.
-- [PUB] and [SUB] same types and table on both nodes
DO $$ BEGIN
CREATE TYPE r0 AS (f regclass[]);
FOR i IN 1..16 LOOP
EXECUTE format('CREATE TYPE r%s AS (f r%s)', i, i - 1);
END LOOP;
END $$;
CREATE TABLE tqai (k r16 PRIMARY KEY);
-- [PUB]
CREATE PUBLICATION pub_qai FOR TABLE tqai;
-- [SUB]
CREATE SUBSCRIPTION sub_qai CONNECTION 'port=9933 dbname=postgres'
PUBLICATION pub_qai
WITH (copy_data = false,
conflict_log_destination = 'table');
-- [PUB] replicates normally
DO $$ DECLARE s text := 'ROW(ARRAY(SELECT ''pg_class''::regclass FROM
generate_series(1, 2100)))::r0';
BEGIN
FOR i IN 1..16 LOOP s := format('ROW(%s)::r%s', s, i); END LOOP;
EXECUTE 'INSERT INTO tqai VALUES (' || s || ')';
END $$;
-- [SUB] row arrived; switch the apply worker to quoting and remove the row
locally
SELECT count(*) FROM tqai; -- 1
ALTER SYSTEM SET quote_all_identifiers = on;
SELECT pg_reload_conf();
DELETE FROM tqai;
-- [PUB] triggers delete_missing on the subscriber
DELETE FROM tqai;
-- [SUB] expect in sub.log: ERROR: string buffer exceeds maximum allowed length
LOG: parameter "quote_all_identifiers" changed to "on"
LOG: conflict detected on relation "public.tqai": conflict=delete_missing
DETAIL: Conflict details are logged to the conflict log table:
pg_conflict.pg_conflict_log_16540
CONTEXT: processing remote data for replication origin "pg_16540" during
message type "DELETE" for replication target relation "public.tqai" in
transaction 690, finished at 0/01817028
ERROR: string buffer exceeds maximum allowed length (1073741823 bytes)
DETAIL: Cannot enlarge string buffer containing 1073741822 bytes by 1 more
bytes.
-- [SUB] control: back to the default, retry; the row is recorded with an
omitted marker
ALTER SYSTEM RESET quote_all_identifiers;
SELECT pg_reload_conf();
SELECT conflict_type, has_omitted_values FROM :clt; -- delete_missing
| t
-- Cleanup
-- [SUB]
DROP SUBSCRIPTION sub_qai; DROP TABLE tqai; DROP TYPE r0 CASCADE;
-- [PUB]
DROP PUBLICATION pub_qai; DROP TABLE tqai; DROP TYPE r0 CASCADE;