On Sun, 30 Aug 2026 at 20:43, Dilip Kumar <[email protected]> wrote:
>
> On Wed, Aug 26, 2026 at 8:01 PM Dilip Kumar <[email protected]> wrote:
> >
> Updated version of 0002, based on offlist testing by Nisha, revealed
> that the assumption that a column's maximum size could become 6x its
> original size during JSON conversion is incorrect. One edge case is a
> column of type array (int[]). A huge array but mostly empty(all NULL),
> can have a small storage size but produce a much larger JSON since
> each element is serialized. For example
>  - A 4096 byte text column becomes ~24KB json (6x), which stays under
> the budget even accumulated across all columns (24,554 × 1600 × 3 ≈
> 118 MB).
>  - But a 4096 byte all-NULL int[] column can produce ~480KB(120x) of
> json in a edge case.
> Based on this, Amit suggested a offlist POC of the patch which Nisha
> and I further modified. It still needs more review, testing, and logic
> validation, but I am sharing it here so we can review and provide
> feedback.

Thanks for the updated patch.
Few comments:
1) The omitted length shown in some cases is not correct.
This can be reproduced by the following:
-- Create type and table in both publisher and subscriber
CREATE TYPE bigenum AS ENUM ('x');
CREATE TABLE conf_marker (a int PRIMARY KEY, arr int[], e bigenum);

-- Subscriber
-- Only the subscriber serializes to JSON, so only it needs the cast.  This is
-- what puts the enum column on datum_to_json_internal()'s JSONTYPE_CAST path
-- with a rendering far larger than the value.
CREATE FUNCTION bigenum_to_json(bigenum) RETURNS json
    AS $$ SELECT to_json(repeat('a', 30000)) $$
    LANGUAGE sql IMMUTABLE;
CREATE CAST (bigenum AS json) WITH FUNCTION bigenum_to_json(bigenum);

-- Have logical replication setup with above using
track_commit_timestamp as true in subscriber.

--  Publisher
INSERT INTO conf_marker VALUES (1, NULL, NULL);

-- Subscriber
-- Delete the record in subscriber to simulate update_missing conflict
DELETE FROM conf_marker;

-- Publisher
UPDATE conf_marker
       SET arr = array_fill(NULL::int, ARRAY[4000]), e = 'x'
     WHERE a = 1;

-- The above update generates update_missing conflict in the subscriber
postgres=# select relname, conflict_type, remote_tuple,
has_omitted_values from pg_conflict.pg_conflict_log_16395;
   relname   | conflict_type  |
remote_tuple                                 | has_omitted_values
-------------+----------------+-----------------------------------------------------------------------------+--------------------
 conf_marker | update_missing |
{"a":1,"arr":{"omitted":true,"length":524},"e":{"omitted":true,"length":0}}
| t
(1 row)

The omitted length shown as 0 for column e is not correct.

Also the 524 shown for column arr seems incorrect, should it be 20001:
 SELECT length(to_json(array_fill(NULL::int, ARRAY[4000]))::text);
 length
--------
  20001
(1 row)

2) The 16kB mentioned is not correct:
+    <para>
+     A column value is either recorded exactly or not at all; it is never
+     truncated. Any value larger than 16kB is replaced in the
+     JSON columns by an object recording that it was omitted
+     together with its length in bytes, for example:
+<programlisting>
+{"a" : 1, "b" : {"omitted":true,"length":190000000}}

We can see omitted column having lengths 3004, 5004, 10004, 15004
which are less than 16kB:
postgres=# select relname, conflict_type, remote_tuple from
pg_conflict.pg_conflict_log_16395 where has_omitted_values = true;
 relname |     conflict_type     |                   remote_tuple
---------+-----------------------+--------------------------------------------------
 tab     | update_origin_differs |
{"a":10,"b":{"omitted":true,"length":190000004}}
 tab     | update_origin_differs | {"a":10,"b":{"omitted":true,"length":15004}}
 tab     | update_origin_differs | {"a":10,"b":{"omitted":true,"length":10004}}
 tab     | update_origin_differs | {"a":10,"b":{"omitted":true,"length":5004}}
 tab     | update_origin_differs | {"a":10,"b":{"omitted":true,"length":3004}}
(5 rows)

This needs to be updated accordingly to mention based on the new code
changes done.

3) The actual output from clt does not have the spaces around "a" and "b":
<programlisting>
{"a" : 1, "b" : {"omitted":true,"length":190000000}}
</programlisting>

I felt it should be:
{"a":1,"b":{"omitted":true,"length":190000000}}

Regards,
Vignesh


Reply via email to