Re: on_error table, saving error info to a table

jian he <jian.universality@gmail.com>

From: jian he <jian.universality@gmail.com>
To: Zsolt Parragi <zsolt.parragi@percona.com>
Cc: pgsql-hackers@lists.postgresql.org
Date: 2026-05-15T09:05:47Z
Lists: pgsql-hackers

Attachments

On Fri, May 15, 2026 at 1:14 AM Zsolt Parragi <zsolt.parragi@percona.com> wrote:
>
> SET debug_discard_caches = 1;
> CREATE TABLE src_tbl (a int, b int);
> CREATE TABLE err_tbl OF copy_error_saving;
> CREATE INDEX src_idx ON src_tbl(a);
> COPY src_tbl FROM STDIN (FORMAT csv, ON_ERROR table, ERROR_TABLE err_tbl);
> 1,2
> xx,3
> 3,4
> \.
> -- ERROR: relation with OID 0 does not exist
>
Yech. We need do this in EndCopyFrom
+    if (cstate->error_rel)
+        table_close(cstate->error_rel, NoLock);

>
> +               /* Handle queued AFTER triggers */
> +               AfterTriggerEndQuery(cstate->mtcontext->estate);
>
> Is the order of this correct? See the following snippet that crashes the server:
>
> CREATE TABLE target_tbl (id int, val int);
> CREATE TABLE err_tbl OF copy_error_saving;
>
> CREATE OR REPLACE FUNCTION err_stmt_trans_fn() RETURNS trigger AS $$
> BEGIN
> END;
> $$ LANGUAGE plpgsql;
>
> CREATE TRIGGER err_stmt_trans
> AFTER INSERT ON err_tbl
> REFERENCING NEW TABLE AS new_rows
> FOR EACH STATEMENT EXECUTE FUNCTION err_stmt_trans_fn();
>
> \echo === COPY ===
> COPY target_tbl FROM stdin WITH (on_error 'table', error_table 'err_tbl');
> 1       100
> bad     200
> 3       notanumber
> 4       400
> \.
>

We need do AfterTriggerEndQuery(cstate->mtcontext->estate);
first then
AfterTriggerEndQuery(estate);

ExecForPortionOfLeftovers() handles this in the same way.

``static AfterTriggersData afterTriggers;``
and ExecASInsertTriggers, AfterTriggerEndQuery will change the value
of afterTriggers constantly,
therefore, the position of these functions is important!

Therefore in copyfrom.c the order should be:
ExecBSInsertTriggers(estate, resultRelInfo);
ExecBSInsertTriggers(cstate->mtcontext->mtstate->ps.state,
                      cstate->mtcontext->mtstate->rootResultRelInfo);
ExecASInsertTriggers(cstate->mtcontext->estate,
            on_error_mtstate->rootResultRelInfo,
            on_error_mtstate->mt_transition_capture);
ExecASInsertTriggers(estate, target_resultRelInfo, cstate->transition_capture);

> +
> +                               cstate->num_errors = cstate->num_errors + estate->es_processed;
>
> Counting seems to miss if a before trigger returns null:
>
> \set ON_ERROR_STOP 0
> CREATE TABLE t2 (a int, b int, c int);
> CREATE TABLE err_tbl2 OF copy_error_saving;
> CREATE FUNCTION drop_all() RETURNS TRIGGER LANGUAGE plpgsql AS $$
> BEGIN
>   RETURN NULL;
> END;
> $$;
> CREATE TRIGGER drop_all_t BEFORE INSERT ON err_tbl2 FOR EACH ROW
>   EXECUTE FUNCTION drop_all();
> COPY t2 FROM STDIN WITH (FORMAT csv, ON_ERROR table, ERROR_TABLE err_tbl2);
> 1,2,a
> 3,4,b
> 5,6,c
> 7,8,d
> 9,10,e
> \.
>
> SELECT count(*) AS n FROM t2;
> SELECT count(*) AS n FROM err_tbl2;
>
I am not sure.
Should we produce the NOTICE message below for the above test case?
+NOTICE:  5 rows were saved to table "err_tbl2" due to data type incompatibility
but because of the trigger, err_tbl2 has zero rows.

>
> +               typoid = GetSysCacheOid2(TYPENAMENSP, Anum_pg_type_oid,
> +                                                                PointerGetDatum("copy_error_saving"),
> +                                                                ObjectIdGetDatum(PG_CATALOG_NAMESPACE));
> ...
> +                       if (reloftype != typoid)
> +                               ereport(ERROR,
> ...
> +                   errhint("The COPY error saving table must be a
> typed table based on type \"%s\".",
> +                                                               format_type_be_qualified(typoid)));
>
> Isn't an if (!OidIsValid(typoid)) check missing between the two?

Sure, I have added a
+        if (!OidIsValid(typoid))
+            elog(ERROR, "cache lookup failed for catalog type %s",
"copy_error_saving");



--
jian
https://www.enterprisedb.com/