Technical Analysis: Make the Logical Replication Conflict Messages More Like Each Other
Core Problem
The logical replication subsystem in PostgreSQL generates conflict messages (via conflict.c, specifically the errdetail_apply_conflict function) when the apply worker encounters situations where incoming remote changes cannot be cleanly applied to the subscriber. These conflicts are categorized into distinct types (CT_INSERT_EXISTS, CT_UPDATE_EXISTS, CT_UPDATE_DELETED, CT_UPDATE_MISSING, CT_DELETE_ORIGIN_DIFFERS, etc.), each with their own error detail messages.
The problem is one of message consistency and developer ergonomics: the current messages across these conflict types use inconsistent structure, wording, and formatting patterns. This matters because:
- Operational clarity: DBAs diagnosing replication conflicts need to quickly parse and understand these messages. Inconsistent formatting forces cognitive overhead when switching between different conflict types.
- Log parsing/monitoring: Automated tooling that parses PostgreSQL logs for replication conflicts benefits from predictable message structure.
- Documentation alignment: The messages should ideally mirror the documentation descriptions at
logical-replication-conflicts.html.
Specific Inconsistencies Identified
Structural inconsistency
- Some conflict types (e.g.,
CT_UPDATE_DELETED,CT_INSERT_EXISTS) use a two-level pattern: a general reason line followed by detail lines providing origin/transaction specifics. - Other conflict types (e.g.,
CT_UPDATE_ORIGIN_DIFFERS,CT_DELETE_ORIGIN_DIFFERS) embed all information into a single message without a separating general reason line. CT_UPDATE_MISSINGandCT_DELETE_MISSINGuse only a top-level message with no detail sub-lines.
Wording inconsistency
- "Could not apply remote change" vs. "Could not find the row to be updated" vs. "Updating the row that was modified" — these use different grammatical constructions (imperative failure vs. gerund description).
- "The row to be updated was deleted" (passive infinitive) vs. "Updating the row that was modified" (active gerund) — mixing verb forms for essentially parallel operations.
Punctuation inconsistency
- Some detail messages end with periods, others do not (notably
CT_UPDATE_DELETEDdetail lines lack terminal periods).
Proposed Solution
The proposal advocates for a uniform two-level message pattern across ALL conflict types:
-
Level 1 (general): A consistent "The row to {operation} {what happened}" summary, e.g.:
- "The row to insert violates a constraint: %s."
- "The row to update was already deleted: %s."
- "The row to delete was modified by another origin."
-
Level 2 (detail): Origin/transaction specifics using "It was..." pronoun reference to avoid repetition, e.g.:
- "It was modified locally in transaction %u at %s: %s."
- "It was deleted by a different origin "%s" in transaction %u at %s."
Key Design Decisions in the Proposal
- Consistent verb form: "The row to update" / "The row to delete" / "The row to insert" (infinitive without "be")
- Pronoun deduplication: Using "It was..." in detail lines rather than repeating "The row to be updated was..."
- Universal two-level structure: Even
CT_UPDATE_MISSINGandCT_DELETE_MISSINGwould get the pattern (though they have no sub-details about origin) - Terminal periods everywhere: All messages end with periods
- Documentation alignment: Wording chosen to match the official docs descriptions
Implications and Tradeoffs
Positive
- Better log parsability with consistent structure
- Reduced cognitive load for operators
- Clearer separation between "what happened" and "who/when caused it"
- Alignment with documentation
Concerns
- Translators burden: Changing
errdetailstrings means all translation teams need to update their.pofiles - Log format changes break tooling: Any existing monitoring/alerting that regex-matches on current message formats will break
- Backpatch considerations: This is purely cosmetic, so it would only go into the next major version, but users upgrading need awareness
- No functional change: This is entirely a UX/messaging change with zero behavioral impact on conflict resolution logic
Relationship to Broader Work
This proposal was originally part of another thread (referenced as [1]) working on logical replication conflict handling improvements. It was split out because the message reformatting was considered orthogonal to functional changes. This is a common pattern in PostgreSQL development — separating cosmetic/refactoring work from behavioral changes to keep patches reviewable and committable independently.
The conflict detection infrastructure in conflict.c is relatively recent (added as part of the logical replication conflict detection/resolution work in PostgreSQL 17), so cleaning up message consistency early — before more conflict types are potentially added — is strategically sound.
Current Status
This is at the RFC (Request for Comments) stage. The author explicitly states no patch will be written until there is community agreement that the direction is worthwhile. This is a sensible approach for cosmetic changes that touch many translatable strings — getting buy-in first avoids wasted effort if the community prefers the current wording or has different structural preferences.