COPY ON_CONFLICT TABLE — May 2026 Monthly Summary
Overview
This thread proposes extending PostgreSQL's COPY FROM with the ability to redirect rows that violate unique/primary key constraints into a separate "conflict table" rather than aborting the entire load. The feature is the bulk-load analog of INSERT ... ON CONFLICT DO NOTHING, targeting ETL workloads that ingest semi-clean external data.
The month saw rapid iteration from v1 (labeled v20 in the commitfest) through v3, driven by two external reviewers who uncovered crashes, data loss, and design gaps. A major architectural pivot occurred mid-month when the author abandoned hand-rolled conflict detection in favor of delegating to the executor's existing ExecInsert function.
Proposed Syntax
COPY target FROM ... (ON_CONFLICT TABLE, CONFLICT_TABLE conflict_tbl)
The conflict table must have exactly four columns: OID (target table), TEXT (source filename), INT8 (line number), TEXT (raw input line). Validation is by type, not column name.
Key Architectural Evolution
v1/v20: Hand-rolled conflict detection in copyfrom.c
- Reimplemented speculative-insertion logic directly in the COPY code path
- Led to three critical bugs: NULL-pointer crash on partitioned targets, crash under REPEATABLE READ, and silent data loss on tables with no indexes but with BEFORE ROW triggers
- Root cause: incomplete reimplementation of subtle executor paths
v2: Delegation to ExecInsert
- Constructs a
ModifyTableContextinsideCopyFromStateDataand delegates to the executor'sExecInsert - Same pattern used by logical replication's apply worker
- Immediately inherits correct handling for partitioning, isolation levels, and (unexpectedly) exclusion constraints
- Tradeoff: increased coupling between COPY and executor internals
v3: Bug fixes and refinements
- Rejects
ON_CONFLICT TABLEin binary mode (line_buf is NULL in binary format) - Fixes trigger semantics: statement-level triggers fire unconditionally, row-level per-conflict
- Adds permission regression tests for conflict table access
- Introduces a use-after-close bug (conflict relation closed too early in
BeginCopyFrom)
Critical Bugs Found and Status
| Bug | Severity | Found By | Status in v3 |
|---|---|---|---|
| Crash on partitioned targets | Server crash | Zsolt Parragi | Likely fixed by ExecInsert delegation (not explicitly confirmed) |
| Crash under REPEATABLE READ | Server crash | Zsolt Parragi | Likely fixed by ExecInsert delegation (not explicitly confirmed) |
| Silent data loss (no indexes + trigger) | Data corruption | Zsolt Parragi | Likely fixed by ExecInsert delegation |
| COPY TO accepts conflict options | Wrong behavior | Jim Jones, Zsolt Parragi | Fixed |
| Binary mode crash | Server crash | Jim Jones, Zsolt Parragi | Fixed in v3 |
| Use-after-close of conflict relation | Crash/corruption | Jim Jones, Zsolt Parragi | Open — needs move to EndCopyFrom() |
Unconditional ExecOpenIndices(..., true) |
Performance/correctness | Jim Jones | Fixed in v2 architecture |
Surprise Finding: Exclusion Constraints Work
Despite documentation stating otherwise, Zsolt confirmed that EXCLUDE USING gist(...) constraints are correctly handled by the v3 patch — the ExecInsert delegation picks up this capability for free. Only the error message text is wrong (says "unique constraint" for exclusion violations). This validates the architectural pivot.
Unresolved Issues
- Unification with ON_ERROR table (CF 4817): Both features need a user-named sink table with similar validation and dispatch. Pursuing them independently risks divergent syntax. Not discussed this month.
- No committer engagement: The design has not been validated by any committer.
- BEFORE triggers on conflict table silently dropping rows: Standard PostgreSQL behavior but operationally surprising for a "save my rejected rows" feature. Documentation concern shared with ON_ERROR work.
- Performance under heavy conflicts: Per-row routing to conflict table defeats multi-insert buffering. No batching strategy proposed.
- Syntax aesthetics:
ON_CONFLICT TABLE, CONFLICT_TABLE xis verbose; cleaner alternatives suggested but not adopted.
Month-End State
The patch is at v3 with one known open bug (use-after-close) and several documentation corrections needed. The ExecInsert delegation architecture is a clear improvement over v1 and handles more cases than the author initially expected. The patch remains in early review with no committer signoff.