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;

Reply via email to