Thread

  1. Re: on_error table, saving error info to a table

    jian he <jian.universality@gmail.com> — 2026-05-15T09:05:47Z

    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/