Thread
Commits
GET /api/v1/messages/:b64id/commits
the thread's linked commits as JSON, with link sources.
API reference →
-
Fix RI fast-path for domain-typed FK columns
- 68ace967c16b 19 (unreleased) cited
-
pg_walsummary: Improve stability of test checking statistics
- a27893df45ec 19 (unreleased) cited
-
[PATCH] Fix replica identity mismatch for partitioned tables with publish_via_partition_root
Mikhail Kharitonov <mikhail.kharitonov.dev@gmail.com> — 2025-05-05T08:18:18Z
Hi hackers, An inconsistency was observed when using logical replication on partitioned tables with the option `publish_via_partition_root = true`: if REPLICA IDENTITY FULL is set only on the parent table, but not on all partitions, logical decoding emits UPDATE and DELETE messages with tag 'O' (old tuple) even for partitions that do not have full replica identity. In those cases, only the primary key columns are included in the message, which contradicts the expected meaning of 'O' and violates the logical replication message protocol: https://www.postgresql.org/docs/current/protocol-logicalrep-message-formats.html This can cause issues in downstream consumers, which interpret the 'O' tag as implying that a full tuple is present. The attached patch resolves the inconsistency by selecting the correct tuple type ('O' vs 'K') based on the replica identity of the actual leaf relation being published, rather than using the setting of the root relation alone. As a result, the format of logical replication messages aligns with the semantics defined by the protocol. Steps to reproduce: 1. Create a partitioned table with REPLICA IDENTITY FULL on the parent and only one of the partitions. 2. Create a publication with `publish_via_partition_root = true`. 3. Perform INSERT, UPDATE, DELETE operations through the root table. 4. Observe via `pg_recvlogical` that for a partition without full replica identity, the logical replication stream contains 'O' records with only key fields. After applying the patch, 'O' is used only when the full row is available, and 'K' is used otherwise - as expected. This patch is based on the current `master` branch as of commit: b3754dcc9ff Best regards, Mikhail Kharitonov -
Re: [PATCH] Fix replica identity mismatch for partitioned tables with publish_via_partition_root
Maxim Orlov <orlovmg@gmail.com> — 2025-05-12T14:25:00Z
Hi! This is probably not the most familiar part of Postgres to me, but does it break anything? Or is it just inconsistency in the replication protocol? A test for the described scenario would be a great addition. And, if it is feasible, provide an example of what would be broken with the way partitioned tables are replicated now. There is a chance that the replication protocol for partitioned tables needs to be rewritten, and I sincerely hope that I am wrong about this. It seems Alvaro Herrera tried this here [0]. [0] https://www.postgresql.org/message-id/201902041630.gpadougzab7v@alvherre.pgsql -- Best regards, Maxim Orlov.
-
Re: [PATCH] Fix replica identity mismatch for partitioned tables with publish_via_partition_root
Mikhail Kharitonov <mikhail.kharitonov.dev@gmail.com> — 2025-05-29T06:30:31Z
Hi, Thank you for the feedback. I would like to clarify that the current behavior does not break replication between PostgreSQL instances. The logical replication stream is still accepted by the subscriber, and the data is applied correctly. However, the protocol semantics are violated, which may cause issues for external systems that rely on interpreting this stream. When using publish_via_partition_root = true and setting REPLICA IDENTITY FULL only on the parent table (but not on all partitions), logical replication generates messages with the tag 'O' (old tuple) for updates and deletes even for partitions that do not have full identity configured. In those cases, only key columns are sent, and the rest of the tuple is omitted. This contradicts the meaning of tag 'O', which, according to the documentation [1], indicates that the full old tuple is included. This behavior is safe for the standard PostgreSQL subscriber, which does not rely on the tag when applying changes. However, third-party tools that consume the logical replication stream and follow the protocol strictly can be misled. For example, one of our clients uses a custom CDC mechanism that extracts changes and sends them to Oracle. Their handler interprets the 'O' tag as a signal that the full old row is available. When it is not - the data is processed incorrectly. The attached patch changes the behavior so that the 'O' or 'K' tag is chosen based on the REPLICA IDENTITY setting of the actual partition where the row ends up not only the parent. - If the partition has REPLICA IDENTITY FULL, the full tuple is sent and tagged 'O'. - Otherwise, only the key columns are sent, and the tag 'K' is used. This aligns the behavior with the protocol documentation. I have also included a TAP test: 036_partition_replica_identity.pl, located in src/test/subscription/t/ It demonstrates two cases: - An update/delete on a partition with REPLICA IDENTITY FULL correctly emits an 'O' tag with the full old row. - An update/delete on a partition without REPLICA IDENTITY FULL currently also emits an 'O' tag, but only with key fields - this is the problem. After applying the patch, the second case correctly uses the 'K' tag. This patch is a minimal change it does not alter protocol structure or introduce new behavior. It only ensures the implementation matches the documentation. In the future, we might consider a broader redesign of logical replication for partitioned tables (see [2]), but this is a narrow fix that solves a real inconsistency. Looking forward to your comments. Best regards, Mikhail Kharitonov [1] https://www.postgresql.org/docs/current/protocol-logicalrep-message-formats.html [2] https://www.postgresql.org/message-id/201902041630.gpadougzab7v@alvherre.pgsql On Mon, May 12, 2025 at 5:25 PM Maxim Orlov <orlovmg@gmail.com> wrote: > > Hi! > > This is probably not the most familiar part of Postgres to me, but does it break anything? Or is it just inconsistency in the replication protocol? > > A test for the described scenario would be a great addition. And, if it is feasible, provide an example of what would be broken with the way partitioned tables are replicated now. > > There is a chance that the replication protocol for partitioned tables needs to be rewritten, and I sincerely hope that I am wrong about this. It seems Alvaro Herrera tried this here [0]. > > > [0] https://www.postgresql.org/message-id/201902041630.gpadougzab7v@alvherre.pgsql > > > -- > Best regards, > Maxim Orlov. -
Re: [PATCH] Fix replica identity mismatch for partitioned tables with publish_via_partition_root
Mikhail Kharitonov <mikhail.kharitonov.dev@gmail.com> — 2025-07-08T08:53:43Z
Hi all, I’m sending v2 of the patch. This is a clean rebase onto current master (commit a27893df45e) and a squash of the fix together with the TAP test into a single patch file. I would appreciate your thoughts and comments on the current problem. Thank you! -- Best regards, Mikhail Kharitonov On Thu, May 29, 2025 at 9:30 AM Mikhail Kharitonov <mikhail.kharitonov.dev@gmail.com> wrote: > > Hi, > > Thank you for the feedback. > > I would like to clarify that the current behavior does not break replication > between PostgreSQL instances. The logical replication stream is still accepted > by the subscriber, and the data is applied correctly. However, the protocol > semantics are violated, which may cause issues for external systems that rely > on interpreting this stream. > > When using publish_via_partition_root = true and setting REPLICA IDENTITY FULL > only on the parent table (but not on all partitions), logical replication > generates messages with the tag 'O' (old tuple) for updates and deletes even > for partitions that do not have full identity configured. > > In those cases, only key columns are sent, and the rest of the tuple is omitted. > This contradicts the meaning of tag 'O', which, according > to the documentation [1], indicates that the full old tuple is included. > > This behavior is safe for the standard PostgreSQL subscriber, which does not > rely on the tag when applying changes. However, third-party tools that consume > the logical replication stream and follow the protocol strictly can be misled. > For example, one of our clients uses a custom CDC mechanism that extracts > changes and sends them to Oracle. Their handler interprets the 'O' tag as a > signal that the full old row is available. When it is not - the data is > processed incorrectly. > > The attached patch changes the behavior so that the 'O' or 'K' tag is chosen > based on the REPLICA IDENTITY setting of the actual partition where the row > ends up not only the parent. > - If the partition has REPLICA IDENTITY FULL, the full tuple is > sent and tagged 'O'. > - Otherwise, only the key columns are sent, and the tag 'K' is used. > > This aligns the behavior with the protocol documentation. > I have also included a TAP test: 036_partition_replica_identity.pl, > located in src/test/subscription/t/ > > It demonstrates two cases: > - An update/delete on a partition with REPLICA IDENTITY FULL correctly > emits an 'O' tag with the full old row. > - An update/delete on a partition without REPLICA IDENTITY FULL currently > also emits an 'O' tag, but only with key fields - this is the problem. > > After applying the patch, the second case correctly uses the 'K' tag. > > This patch is a minimal change it does not alter protocol structure > or introduce new behavior. It only ensures the implementation matches > the documentation. In the future, we might consider a broader redesign > of logical replication for partitioned tables (see [2]), but this is > a narrow fix that solves a real inconsistency. > > Looking forward to your comments. > > Best regards, > Mikhail Kharitonov > > [1] https://www.postgresql.org/docs/current/protocol-logicalrep-message-formats.html > [2] https://www.postgresql.org/message-id/201902041630.gpadougzab7v@alvherre.pgsql > > On Mon, May 12, 2025 at 5:25 PM Maxim Orlov <orlovmg@gmail.com> wrote: > > > > Hi! > > > > This is probably not the most familiar part of Postgres to me, but does it break anything? Or is it just inconsistency in the replication protocol? > > > > A test for the described scenario would be a great addition. And, if it is feasible, provide an example of what would be broken with the way partitioned tables are replicated now. > > > > There is a chance that the replication protocol for partitioned tables needs to be rewritten, and I sincerely hope that I am wrong about this. It seems Alvaro Herrera tried this here [0]. > > > > > > [0] https://www.postgresql.org/message-id/201902041630.gpadougzab7v@alvherre.pgsql > > > > > > -- > > Best regards, > > Maxim Orlov.
-
Re: [PATCH] Fix replica identity mismatch for partitioned tables with publish_via_partition_root
Mikhail Kharitonov <mikhail.kharitonov.dev@gmail.com> — 2025-07-08T09:41:40Z
On Tue, Jul 8, 2025 at 11:53 AM Mikhail Kharitonov <mikhail.kharitonov.dev@gmail.com> wrote: > > Hi all, > > I’m sending v2 of the patch. This is a clean rebase onto current master > (commit a27893df45e) and a squash of the fix together with the TAP > test into a single patch file. > > I would appreciate your thoughts and comments on the current problem. > > Thank you! > > -- > Best regards, > Mikhail Kharitonov > Sorry, I forgot the attachment in my previous message. Please find the v2 patch attached.
-
Re: [PATCH] Fix replica identity mismatch for partitioned tables with publish_via_partition_root
Mikhail Kharitonov <mikhail.kharitonov.dev@gmail.com> — 2025-08-12T12:02:33Z
Hi all, I've rebased this series onto the latest master. Changes in v3: Patch 1/2 adds two new functions: logicalrep_write_update_extended, logicalrep_write_delete_extended to logicalproto. These are now used in pgoutput and allow correct old-tuple flag handling when publish_via_partition_root = true. The old functions remain as wrappers to preserve compatibility. A short documentation note was added to explain the new behaviour. Patch 2/2 moves the TAP test into a separate commit, so the code change and test are isolated. -- Best regards, Mikhail Kharitonov -
Re: [PATCH] Fix replica identity mismatch for partitioned tables with publish_via_partition_root
Masahiko Sawada <sawada.mshk@gmail.com> — 2026-01-07T02:14:44Z
On Mon, May 5, 2025 at 1:56 AM Mikhail Kharitonov <mikhail.kharitonov.dev@gmail.com> wrote: > > Hi hackers, > > An inconsistency was observed when using logical replication on partitioned > tables with the option `publish_via_partition_root = true`: if REPLICA IDENTITY > FULL is set only on the parent table, but not on all partitions, logical > decoding emits UPDATE and DELETE messages with tag 'O' (old tuple) even for > partitions that do not have full replica identity. In those cases, only the > primary key columns are included in the message, which contradicts the expected > meaning of 'O' and violates the logical replication message protocol: > > https://www.postgresql.org/docs/current/protocol-logicalrep-message-formats.html > > This can cause issues in downstream consumers, which interpret > the 'O' tag as implying that a full tuple is present. > > The attached patch resolves the inconsistency by selecting the correct tuple > type ('O' vs 'K') based on the replica identity of the actual leaf relation > being published, rather than using the setting of the root relation alone. > As a result, the format of logical replication messages aligns with > the semantics > defined by the protocol. > > Steps to reproduce: > > 1. Create a partitioned table with REPLICA IDENTITY FULL on the parent > and only one of the partitions. > > 2. Create a publication with `publish_via_partition_root = true`. > > 3. Perform INSERT, UPDATE, DELETE operations through the root table. > > 4. Observe via `pg_recvlogical` that for a partition without full replica > identity, the logical replication stream contains 'O' records with > only key fields. I tested this scenario but what I've seen in my env is somewhat different from the above analysis; pgoutput plugin writes 'O' records as you mentioned, but it doesn't omit non-key fields, but writes NULL as non-key fields. Here are my reproducible steps: create table p (a int not null, b int) partition by list (a); create table c1 partition of p for values in (1); create table c2 partition of p for values in (2); create unique index on c2 (a); alter table p replica identity full; alter table c1 replica identity full; alter table c2 replica identity using INDEX c2_a_idx ; insert into p values (1, 10), (2, 20); create publication pub for all tables with (publish_via_partition_root = 'true'); select pg_create_logical_replication_slot('sub', 'pgoutput'); delete from p where a = 1; delete from p where a = 2; select encode(data, 'escape') from pg_logical_slot_peek_binary_changes('sub', null, null, 'proto_version', '1', 'publication_names', 'pub'); The last pg_logical_slot_peek_binary_changes() writes the two 'D' (delete) messages: 1. D\000\000@\000O\000\x02t\000\000\000\x011t\000\000\000\x0210 2. D\000\000@\000O\000\x02t\000\000\000\x012n What we can know from these messages are: - Both messages have 'O'. - Both messages have two columns ('\000\x02'). - The first message has: the first column '1' (length is 1 ('\000\000\000\x01')), and the second column '10' (length is 2 ('\000\000\000\x02')). - The second message has: the first column '2', and the second column NULL ('n'). From these facts, I guess there could be problematic cases even in the native logical replication. Here are reproducible steps: -- Publisher create table p (a int not null, b int) partition by list (a); create table c1 partition of p for values in (1); create table c2 partition of p for values in (2); create unique index on c2 (a); alter table p replica identity full; alter table c1 replica identity full; alter table c2 replica identity using INDEX c2_a_idx ; insert into p values (1, 10), (2, 20); create publication pub for all tables with (publish_via_partition_root = 'true'); -- Subscriber create table p (a int, b int, c int); create subscription sub connection 'dbname=postgres port=5551' publication pub; -- Publisher delete from p where a = 1; -- generate a message 'DELETE (1, 10)' delete from p where a = 2; -- generate a message 'DELETE (2, NULL)' The second delete message cannot find the tuple on the subscriber, so the table contents are now inconsistent between the publisher and the subscriber. I need more investigation to verify that it's a problem, but this behavior doesn't change even with the proposed change. Regards, -- Masahiko Sawada Amazon Web Services: https://aws.amazon.com -
Re: [PATCH] Fix replica identity mismatch for partitioned tables with publish_via_partition_root
Alexander Korotkov <aekorotkov@gmail.com> — 2026-06-12T05:46:29Z
Hi, Mikhail! On Tue, Aug 12, 2025 at 3:02 PM Mikhail Kharitonov < mikhail.kharitonov.dev@gmail.com> wrote: > I've rebased this series onto the latest master. > > Changes in v3: > > Patch 1/2 adds two new functions: logicalrep_write_update_extended, > logicalrep_write_delete_extended to logicalproto. > These are now used in pgoutput and allow correct old-tuple flag handling > when publish_via_partition_root = true. > The old functions remain as wrappers to preserve compatibility. > A short documentation note was added to explain the new behaviour. > > Patch 2/2 moves the TAP test into a separate commit, > so the code change and test are isolated. Thank you for catching this. I've some notes about your patches. Assert(leafrel->rd_rel->relreplident == REPLICA_IDENTITY_DEFAULT || leafrel->rd_rel->relreplident == REPLICA_IDENTITY_FULL || leafrel->rd_rel->relreplident == REPLICA_IDENTITY_INDEX); This assert was in the beginning of the function. You've moved it into the middle of message forming. If that's an intentional change, it must be motivated. # 2: second partition has REPLICA IDENTITY DEFAULT - only keys expected. if ($wal =~ /U.*K.*second/s) { pass("Tag K correctly used for partition with REPLICA IDENTITY DEFAULT"); } elsif ($wal =~ /(U.*O.*second)/s) { my $blk = $1; my $count = () = $blk =~ /second/g; is($count, 2, "Tag O used but this partition with REPLICA IDENTITY DEFAULT"); } This check is very lossy. I think it must be explicit on what count of which records it expects. Currently, the test silently passes if no branches of above are taken. Also, regexes don't look reliable, they could easily match cross-record. You might either write more reliable regexes, or even better parse individual records before matching them. Also, please note that in your test subscriber does nothing expect waiting for sync. If you're introducing subscriber, it worths to at least check its final state. ------ Regards, Alexander Korotkov Supabase -
Re: [PATCH] Fix replica identity mismatch for partitioned tables with publish_via_partition_root
jihyun bahn <rring0727@gmail.com> — 2026-06-17T05:15:55Z
The following review has been posted through the commitfest application: make installcheck-world: tested, passed Implements feature: tested, passed Spec compliant: tested, passed Documentation: not tested Hi Mikhail, and Masahiko and Alexander, I tested v3 of this patch on current master and looked into the subscriber-side divergence Masahiko raised in January, since it seemed to be the open question holding the thread up. Sharing what I found, in case the data is useful. Environment: - PostgreSQL master (commit 68ace967c16) - macOS 14 (Darwin 24.2) on Apple Silicon, Apple clang 16 - meson build with -Dcassert=true v3-0001 applies cleanly and builds without new warnings. What v3 changes, on the wire ---------------------------- Using Masahiko's setup -- a list-partitioned root p with REPLICA IDENTITY FULL, leaf c1 FULL and leaf c2 USING INDEX, published with publish_via_partition_root = true -- I dumped the DELETE of the c2 row with pg_logical_slot_peek_binary_changes (pgoutput, proto_version 4): before v3: D ... O t "2" n -- tag 'O', column b sent as NULL after v3: D ... K t "2" n -- tag 'K', column b sent as NULL So v3 correctly changes the tag from 'O' to 'K' for the leaf whose replica identity is index-based, which is what it documents. The old-tuple payload itself is unchanged (b is still NULL). What does not change, on the subscriber --------------------------------------- The apply worker reads the 'O'/'K' tag only to decide whether an old tuple follows; it does not branch on the tag afterwards. So native PostgreSQL-to-PostgreSQL apply behaves identically with and without v3. I confirmed this by varying only whether the subscriber table has a key index: subscriber p(a, b), no PK/index: DELETE FROM p WHERE a = 2; -- not applied; (2,20) remains LOG: conflict detected on relation "public.p": conflict=delete_missing DETAIL: Could not find the row to be deleted: replica identity full (2, null). subscriber p(a int primary key, b): DELETE FROM p WHERE a = 2; -- applied correctly, row removed This holds both before and after v3. So the divergence Masahiko saw reproduces only when the subscriber lacks a replica-identity/PK index on the key column; with such an index the key-only old tuple matches via the index path and applies fine. The divergence is governed by the subscriber's index, not by the 'O'/'K' tag, which is why v3 (correctly) does not change it. That suggests v3's effect is precisely scoped to protocol conformance for consumers that trust the tag (external CDC), which is what it claims. It might be worth saying so explicitly in the commit message, to keep it separate from the native-divergence question, which it does not address. On the deeper issue ------------------- The native divergence comes from the old tuple carrying a NULL for a column that the subscriber -- told the relation is REPLICA IDENTITY FULL, from the root -- then compares as a real value. I tried a small experiment: a new per-column wire marker meaning "this column is not part of the replica identity and was not sent", emitted under a new protocol version for the columns outside the leaf's replica identity. The subscriber excludes those columns from old-tuple matching, reusing the same per-column exclusion that RelationFindDeletedTupleInfoSeq() already does for conflict detection. With that, the no-index subscriber above matches the c2 DELETE by key and the divergence disappears, while older subscribers fall back to today's behavior. It does not touch the Relation message. I am not proposing that here -- it is a rough prototype and larger than this patch's scope. But I wanted to ask: is a direction like that of interest as a separate patch, or is a root/leaf replica-identity mismatch under publish_via_partition_root considered a misconfiguration that should instead be warned about at publication time? I could not find the heterogeneous-RI case settled either way in the archives -- a 2019 thread from Alvaro, "propagating replica identity to partitions", explored making it propagate, but as far as I can tell that did not end in a committed change (replica identity still does not cascade to partitions today). So I am unsure which way the project leans. Separately, on Alexander's point that the test only creates a subscriber and waits for sync: I would be happy to help extend the TAP test to assert the subscriber's final table state, if that is useful. Regards, Jihyun Bahn -
Re: [PATCH] Fix replica identity mismatch for partitioned tables with publish_via_partition_root
Henson Choi <assam258@gmail.com> — 2026-06-27T01:11:34Z
Hi Mikhail, One small design question on the v3 API: > The old functions remain as wrappers to preserve compatibility. After the patch, though, the base functions have no callers. The only call site (the switch in pgoutput_change()) calls the _extended forms directly, leaving logicalrep_write_update()/logicalrep_write_delete() as wrappers that just forward the same relation twice; a tree-wide grep finds no caller, and no contrib output plugin uses them either. Is there a basis for treating these as an external interface? They look like pgoutput's internal write-side helpers, paired with the apply worker's logicalrep_read_* side -- not a documented extension API, and signatures here change across major versions anyway. Unless they are commonly called from extensions, formally or informally, I don't see what the _extended wrapping preserves. Since there is exactly one caller, with both the leaf and the publication relation already in scope, it seems simpler to give the base functions the two-relation signature directly and update that one switch -- dropping the _extended variants and the wrappers. No behavior change. Not a blocker; happy to drop this if there's a consumer I'm missing. Regards, Henson