Monthly Summary: ON_ERROR TABLE — Saving COPY FROM Errors to a Table (May 2026)
Overview
May 2026 saw the most active development period for this long-running patch (originally proposed Feb 2024), advancing from v9 through v13. The month brought the first independent testing, the first serious code review exposing real bugs, a significant policy reversal on trigger support, and multiple correctness fixes. Despite this progress, no committer has signed off on the patch.
Key Developments
Rebase and Snapshot Fix (v10, May 11)
jian he identified that the hand-built EState for error-table insertion never registered the active snapshot, creating a dangling pointer risk under concurrent activity. v10 adds proper RegisterSnapshot(GetActiveSnapshot()) calls, aligning with standard ExecutorStart/ExecutorEnd lifecycle management.
Major Policy Reversal: Triggers Now Allowed (v11, May 12)
The most consequential change this month was reversing the v8 blanket prohibition on triggers for the error table:
- Statement-level triggers (
BEFORE/AFTER INSERT STATEMENT) now fire unconditionally — even when no errors occur - Row-level triggers (
BEFORE/AFTER INSERT FOR EACH ROW) fire for each error row inserted
This re-opens the security surface flagged by Kirill Reshke in October 2025 (trigger-based privilege escalation). The motivation appears to be real-world use cases like audit triggers and error notification.
First Serious Code Review (Zsolt Parragi, mid-May)
Zsolt Parragi (Percona) provided the first in-depth code review, identifying multiple bugs:
- Use-after-close crash:
error_relwas closed inBeginCopyFrombut referenced throughout COPY. Manifests as crash withdebug_discard_caches=1. Fix: movetable_close()toEndCopyFrom. - Transition table crash:
AFTER INSERT FOR EACH STATEMENTwith transition tables crashed due to incorrectAfterTriggerEndQueryordering between the twoEStates. Fix: fire error-table'sAfterTriggerEndQuerybefore target table's (followingExecForPortionOfLeftoversprecedent). - Incorrect
num_errorscount: When BEFORE ROW triggers return NULL (suppressing insert), the NOTICE still reports rows as "saved." Unresolved. - Missing
OidIsValid()guard on type lookup. Fixed.
Additional issues: redundant ACL check, redundant REJECT_LIMIT check, dead TODO comment, typos.
Documentation and Cleanup (v13, May 25)
v13 addressed Zsolt's review feedback without architectural changes:
- Documented trigger semantics explicitly (fires, NOTICE may not reflect actual inserts)
- Justified
ExecGetReturningSlotreuse (safe becauseri_projectReturningis NULL) - Confirmed trigger ordering model
Remaining Open Issues
- Security with triggers re-enabled: No mitigation proposed for trigger-based privilege escalation
num_errorsaccuracy: Count doesn't account for trigger-suppressed rows- No committer sign-off after 2+ years of iteration
- Built-in type compatibility risk: Changing
copy_error_savingin future versions would break existing tables - Rebase needed: v9 already had conflicts in
nodeModifyTable.c; ongoing HEAD changes will require maintenance
Current Architecture (as of v13)
-- Built-in composite type (created during initdb)
CREATE TYPE copy_error_saving AS (
userid oid, copy_tbl oid, filename text COLLATE "C",
lineno bigint, line text COLLATE "C", colname text COLLATE "C",
raw_field_value text COLLATE "C", err_message text COLLATE "C",
err_detail text COLLATE "C", errorcode text COLLATE "C"
);
-- User creates typed table, runs COPY
CREATE TABLE my_errs OF copy_error_saving;
COPY t FROM '...' WITH (ON_ERROR table, ERROR_TABLE my_errs);
Error insertion reuses the executor's ExecInsert() via exported internals from nodeModifyTable.c. The error table must be a plain heap table (no partitions, no foreign tables, no FK constraints, no RLS, no rules, no column defaults). Triggers are now permitted.