Hi, V8 is attached. I've refactored a significant portion of the code. Now the new syntax is:
COPY FROM (ON_ERROR TABLE, ERROR_TABLE err_tbl); We now produce a ModifyTableState and ResultRelInfo node, form the slot, and then use table_tuple_insert() + ExecInsertIndexTuples() to insert the error metadata into the ERROR_TABLE. This is very similar to the normal ExecInsert() path. Since the ERROR_TABLE is a user-defined table, we enforce the a lot of restriction, in CopyFromErrorTableCheck, we have comments like: + +/* + * The error saving table must be a plain table and is subject to the following + * restrictions: it cannot have foreign key constraints; nor can it have column + * DEFAULT values, triggers, rules, or row-level security policies. + * + * These restrictions are necessary to allow the use of table_tuple_insert(); + * otherwise, the executor would need to perform additional validation (e.g. + * foreign key constraint check) for each inserted error row. + */ +static void +CopyFromErrorTableCheck(Relation relation) I also add some dummy tests like: +-- Direct modification of system catalog composite type 'copy_error_saving' is +-- not permitted. +ALTER TYPE copy_error_saving ADD ATTRIBUTE b text; +ALTER TYPE copy_error_saving DROP ATTRIBUTE userid; +ALTER TYPE copy_error_saving RENAME ATTRIBUTE userid to userid1; +ALTER TYPE copy_error_saving ALTER ATTRIBUTE userid SET DATA TYPE OID8; To demonstrate that the system's composite copy_error_saving attribute is fixed. Similarly, ERROR_TABLE data definition is fixed. -- jian https://www.enterprisedb.com/
From 3786554d79c05e351cb79bdb7f13ebff25eb6441 Mon Sep 17 00:00:00 2001 From: jian he <[email protected]> Date: Wed, 22 Apr 2026 10:08:58 +0800 Subject: [PATCH v8 1/1] COPY FROM (on_error table, error_table errtbl) The syntax is {on_error table, error_table error_saving_tbl}. The error_saving_tbl must be a typed table, based on internal type pg_catalog.copy_error_saving. A preliminary lock check is also performed on the error-saving table to ensure that inserts into it will not block. When an error occurs, we record the error metadata and insert it into the error_saving_tbl, then proceed to the next row. Although the error_saving_tbl may not capture information for every invalid column in each row, it retains the raw_field_value, which can be used for further investigation. the build-in type: pg_catalog.copy_error_saving definition is 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" ); It's declared in src/backend/catalog/system_functions.sql. If it's going to change, which will cause potential issue. we need be sure that this will unlikely to change in the future. TODO: Should we also add field (starttime timestamptz) to copy_error_saving to indicate the time when this error record was inserted. reference: https://postgr.es/m/CACJufxHi53OpGYPAe6SdCb4m=-+H8L+7LDbUWvTiJp=v4yy...@mail.gmail.com reference: https://postgr.es/m/752672.1699474336%40sss.pgh.pa.us discussion: https://postgr.es/m/CACJufxH_OJpVra=0c4ow8fbxhj7hemcvatnepa5vaursena...@mail.gmail.com commitfest entry: https://commitfest.postgresql.org/patch/4817 --- doc/src/sgml/datatype.sgml | 100 ++++++++ doc/src/sgml/ref/copy.sgml | 39 ++- src/backend/catalog/system_functions.sql | 13 + src/backend/commands/copy.c | 28 +++ src/backend/commands/copyfrom.c | 295 ++++++++++++++++++++++- src/backend/commands/copyfromparse.c | 100 ++++++++ src/backend/parser/gram.y | 1 + src/include/commands/copy.h | 2 + src/include/commands/copyfrom_internal.h | 8 + src/test/regress/expected/copy2.out | 151 ++++++++++++ src/test/regress/sql/copy2.sql | 123 ++++++++++ 11 files changed, 849 insertions(+), 11 deletions(-) diff --git a/doc/src/sgml/datatype.sgml b/doc/src/sgml/datatype.sgml index d8d91678e86..370e99c72ea 100644 --- a/doc/src/sgml/datatype.sgml +++ b/doc/src/sgml/datatype.sgml @@ -5116,6 +5116,106 @@ WHERE ... </para> </sect1> + <sect1 id="datatype-copy_error_saving"> + <title><type>copy_error_saving</type> Type</title> + <indexterm zone="datatype-copy_error_saving"> + <primary>copy_error_saving</primary> + </indexterm> + + <para> + The built-in composite type <type>copy_error_saving</type> is used by the + <link linkend="sql-copy"><command>COPY FROM</command></link> command. + It contains the following fields, which are used to store information when <command>COPY FROM</command> + encounters an error converting a column’s input value to its data type. + </para> + + <para> +<informaltable> + <tgroup cols="3"> + <thead> + <row> + <entry>Column name</entry> + <entry>Data type</entry> + <entry>Description</entry> + </row> + </thead> + + <tbody> + <row> + <entry> <literal>userid</literal> </entry> + <entry><type>oid</type></entry> + <entry>The <command>COPY FROM</command> operation user. + Reference <link linkend="catalog-pg-authid"><structname>pg_authid</structname></link>.<structfield>oid</structfield>, + however there is no hard dependency with catalog <literal>pg_authid</literal>. + If the corresponding row on <literal>pg_authid</literal> is deleted, this value becomes stale. + </entry> + </row> + + <row> + <entry> <literal>copy_tbl</literal> </entry> + <entry><type>oid</type></entry> + <entry>The <command>COPY FROM</command> operation destination table. + Reference <link linkend="catalog-pg-class"><structname>pg_class</structname></link>.<structfield>oid</structfield>, + however there is no hard dependency with catalog <literal>pg_class</literal>. + If the corresponding row on <literal>pg_class</literal> is deleted, this value becomes stale. + </entry> + </row> + + <row> + <entry> <literal>filename</literal> </entry> + <entry><type>text</type></entry> + <entry>The path name of the <command>COPY FROM</command> input</entry> + </row> + + <row> + <entry> <literal>lineno</literal> </entry> + <entry><type>bigint</type></entry> + <entry>Line number where the error occurred, counting from 1</entry> + </row> + + <row> + <entry> <literal>line</literal> </entry> + <entry><type>text</type></entry> + <entry>Raw content of the error occurred line</entry> + </row> + + <row> + <entry> <literal>colname</literal> </entry> + <entry><type>text</type></entry> + <entry>Field where the error occurred</entry> + </row> + + <row> + <entry> <literal>raw_field_value</literal> </entry> + <entry><type>text</type></entry> + <entry>Raw content of the error occurred field</entry> + </row> + + <row> + <entry> <literal>err_message </literal> </entry> + <entry><type>text</type></entry> + <entry>The primary error message, see <link linkend="error-message-reporting">ereport</link></entry> + </row> + + <row> + <entry> <literal>err_detail</literal> </entry> + <entry><type>text</type></entry> + <entry>The optionally detailed error message</entry> + </row> + + <row> + <entry> <literal>errorcode </literal> </entry> + <entry><type>text</type></entry> + <entry>The SQLSTATE error identifier code for the error condition</entry> + </row> + + </tbody> + </tgroup> + </informaltable> + </para> + </sect1> + + <sect1 id="datatype-pg-lsn"> <title><type>pg_lsn</type> Type</title> diff --git a/doc/src/sgml/ref/copy.sgml b/doc/src/sgml/ref/copy.sgml index 4706c9a4410..73c3583dd7c 100644 --- a/doc/src/sgml/ref/copy.sgml +++ b/doc/src/sgml/ref/copy.sgml @@ -46,6 +46,7 @@ COPY { <replaceable class="parameter">table_name</replaceable> [ ( <replaceable FORCE_NULL { ( <replaceable class="parameter">column_name</replaceable> [, ...] ) | * } ON_ERROR <replaceable class="parameter">error_action</replaceable> REJECT_LIMIT <replaceable class="parameter">maxerror</replaceable> + ERROR_TABLE <replaceable class="parameter">error_saving_table</replaceable> ENCODING '<replaceable class="parameter">encoding_name</replaceable>' LOG_VERBOSITY <replaceable class="parameter">verbosity</replaceable> </synopsis> @@ -450,11 +451,13 @@ COPY (SELECT j FROM (VALUES ('null'::json), (NULL::json)) v(j)) <literal>stop</literal> means fail the command, while <literal>ignore</literal> means discard the input row and continue with the next one, and <literal>set_null</literal> means replace the field containing the invalid - input value with a null value and continue to the next field. + input value with a null value and continue to the next field, + and <literal>table</literal> means save error information to <replaceable class="parameter">error_saving_table</replaceable> + and continue with the next one. The default is <literal>stop</literal>. </para> <para> - The <literal>ignore</literal> and <literal>set_null</literal> + The <literal>ignore</literal>, <literal>set_null</literal> and <literal>table</literal> options are applicable only for <command>COPY FROM</command> when the <literal>FORMAT</literal> is <literal>text</literal> or <literal>csv</literal>. </para> @@ -463,16 +466,24 @@ COPY (SELECT j FROM (VALUES ('null'::json), (NULL::json)) v(j)) <literal>set_null</literal>, a <literal>NOTICE</literal> message is emitted at the end of the <command>COPY FROM</command> command containing the count of rows that were ignored or changed, if at least one row was affected. + If <literal>ON_ERROR</literal> option is set to <literal>table</literal>, + a <literal>NOTICE</literal> message containing the row count inserted to + <replaceable class="parameter">error_saving_table</replaceable> is + emitted at the end of the <command>COPY FROM</command>. </para> <para> When <literal>LOG_VERBOSITY</literal> option is set to <literal>verbose</literal>, - for <literal>ignore</literal> option, a <literal>NOTICE</literal> message + for <literal>ignore</literal> or <literal>table</literal> option, a <literal>NOTICE</literal> message containing the line of the input file and the column name whose input conversion has failed is emitted for each discarded row; for <literal>set_null</literal> option, a <literal>NOTICE</literal> message containing the line of the input file and the column name where value was replaced with <literal>NULL</literal> for each input conversion - failure. + failure, + for <literal>table</literal> option, a <literal>NOTICE</literal> + message containing the line of the input file and the column name whose input + conversion has failed is emitted for each row that inserted to + <replaceable class="parameter">error_saving_table</replaceable>. When it is set to <literal>silent</literal>, no message is emitted regarding input conversion failed rows. </para> @@ -497,6 +508,22 @@ COPY (SELECT j FROM (VALUES ('null'::json), (NULL::json)) v(j)) </listitem> </varlistentry> + <varlistentry id="sql-copy-params-error-table"> + <term><literal>ERROR_TABLE</literal></term> + <listitem> + <para> + Save error context details to table <replaceable class="parameter">error_saving_table</replaceable>. + This option is allowed only in <command>COPY FROM</command> and + <literal>ON_ERROR</literal> is specified as <literal>TABLE</literal>. + The current <command>COPY FROM</command> operation user requires have <literal>INSERT</literal> privileges on all columns + of the <replaceable class="parameter">error_saving_table</replaceable>. + The <replaceable class="parameter">error_saving_table</replaceable> must + be a typed table derived from system catalog composite type <link + linkend="datatype-copy_error_saving"><command>copy_error_saving</command></link>. + </para> + </listitem> + </varlistentry> + <varlistentry id="sql-copy-params-encoding"> <term><literal>ENCODING</literal></term> <listitem> @@ -522,8 +549,8 @@ COPY (SELECT j FROM (VALUES ('null'::json), (NULL::json)) v(j)) </para> <para> This is currently used in <command>COPY FROM</command> command when - <literal>ON_ERROR</literal> option is set to <literal>ignore</literal> - or <literal>set_null</literal>. + <literal>ON_ERROR</literal> option is set to <literal>ignore</literal>, + <literal>set_null</literal> or <literal>table</literal>. </para> </listitem> </varlistentry> diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql index c3c0a6e84ed..62a4a6d98aa 100644 --- a/src/backend/catalog/system_functions.sql +++ b/src/backend/catalog/system_functions.sql @@ -366,3 +366,16 @@ CREATE OR REPLACE FUNCTION ts_debug(document text, BEGIN ATOMIC SELECT * FROM ts_debug(get_current_ts_config(), $1); END; + +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" +); diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c index 003b70852bb..1d97064a801 100644 --- a/src/backend/commands/copy.c +++ b/src/backend/commands/copy.c @@ -494,6 +494,8 @@ defGetCopyOnErrorChoice(DefElem *def, ParseState *pstate, bool is_from) return COPY_ON_ERROR_IGNORE; if (pg_strcasecmp(sval, "set_null") == 0) return COPY_ON_ERROR_SET_NULL; + if (pg_strcasecmp(sval, "table") == 0) + return COPY_ON_ERROR_TABLE; ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), @@ -587,6 +589,7 @@ ProcessCopyOptions(ParseState *pstate, bool freeze_specified = false; bool header_specified = false; bool on_error_specified = false; + bool error_table_specified = false; bool log_verbosity_specified = false; bool reject_limit_specified = false; bool force_array_specified = false; @@ -774,6 +777,13 @@ ProcessCopyOptions(ParseState *pstate, reject_limit_specified = true; opts_out->reject_limit = defGetCopyRejectLimitOption(defel); } + else if (strcmp(defel->defname, "error_table") == 0) + { + if (error_table_specified) + errorConflictingDefElem(defel, pstate); + error_table_specified = true; + opts_out->error_table = defGetString(defel); + } else ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), @@ -782,6 +792,24 @@ ProcessCopyOptions(ParseState *pstate, parser_errposition(pstate, defel->location))); } + if (opts_out->on_error == COPY_ON_ERROR_TABLE) + { + if (opts_out->error_table == NULL) + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot set option %s to \"%s\" when \"%s\" is not specified", "ON_ERROR", "TABLE", "ERROR_TABLE"), + errhint("\"%s\" option is required", "ERROR_TABLE")); + + if (opts_out->reject_limit) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot set option %s when %s is specified as \"%s\"", "REJECT_LIMIT", "ON_ERROR", "TABLE")); + } + else if (opts_out->error_table != NULL) + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("COPY %s can only be used when option %s is set to \"%s\"", "ERROR_TABLE", "ON_ERROR", "TABLE")); + /* * Check for incompatible options (must do these three before inserting * defaults) diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c index 64ac3063c61..2efe931ee65 100644 --- a/src/backend/commands/copyfrom.c +++ b/src/backend/commands/copyfrom.c @@ -29,6 +29,7 @@ #include "access/tupconvert.h" #include "access/xact.h" #include "catalog/namespace.h" +#include "catalog/pg_namespace.h" #include "commands/copyapi.h" #include "commands/copyfrom_internal.h" #include "commands/progress.h" @@ -42,16 +43,20 @@ #include "miscadmin.h" #include "nodes/miscnodes.h" #include "optimizer/optimizer.h" +#include "parser/parse_relation.h" #include "pgstat.h" #include "rewrite/rewriteHandler.h" #include "storage/fd.h" #include "tcop/tcopprot.h" +#include "utils/builtins.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/portal.h" #include "utils/rel.h" +#include "utils/regproc.h" #include "utils/snapmgr.h" #include "utils/typcache.h" +#include "utils/syscache.h" /* * No more than this many tuples per CopyMultiInsertBuffer @@ -120,6 +125,11 @@ static void CopyFromBinaryInFunc(CopyFromState cstate, Oid atttypid, FmgrInfo *finfo, Oid *typioparam); static void CopyFromBinaryStart(CopyFromState cstate, TupleDesc tupDesc); static void CopyFromBinaryEnd(CopyFromState cstate); +static void CopyFromErrorTableCheck(Relation relation); +static void RangeVarCallbackForCopyErrorTable(const RangeVar *rv, Oid relid, Oid oldrelid, + void *arg); +static void CopyFromErrorTableInit(CopyFromState cstate); +static void CopyFromErrorTablePermissionCheck(ParseState *pstate, Relation rel); /* @@ -1151,22 +1161,33 @@ CopyFrom(CopyFromState cstate) if (!NextCopyFrom(cstate, econtext, myslot->tts_values, myslot->tts_isnull)) break; - if (cstate->opts.on_error == COPY_ON_ERROR_IGNORE && + if ((cstate->opts.on_error == COPY_ON_ERROR_IGNORE || + cstate->opts.on_error == COPY_ON_ERROR_TABLE) && cstate->escontext->error_occurred) { /* * Soft error occurred, skip this tuple and just make - * ErrorSaveContext ready for the next NextCopyFrom. Since we - * don't set details_wanted and error_data is not to be filled, - * just resetting error_occurred is enough. + * ErrorSaveContext ready for the next NextCopyFrom. */ cstate->escontext->error_occurred = false; + /* + * For ON_ERROR = TABLE, we must set details_wanted to true. This + * ensures that ErrorData is populated when the next error occurs, + * allowing us to capture error metadata. + */ + if (cstate->opts.on_error == COPY_ON_ERROR_TABLE) + { + memset(cstate->escontext->error_data, 0, sizeof(ErrorData)); + cstate->escontext->details_wanted = true; + } + /* Report that this tuple was skipped by the ON_ERROR clause */ pgstat_progress_update_param(PROGRESS_COPY_TUPLES_SKIPPED, cstate->num_errors); - if (cstate->opts.reject_limit > 0 && + if (cstate->opts.on_error == COPY_ON_ERROR_IGNORE && + cstate->opts.reject_limit > 0 && cstate->num_errors > cstate->opts.reject_limit) ereport(ERROR, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), @@ -1480,6 +1501,13 @@ CopyFrom(CopyFromState cstate) "in %" PRIu64 " rows, columns were set to null due to data type incompatibility", cstate->num_errors, cstate->num_errors)); + else if (cstate->opts.on_error == COPY_ON_ERROR_TABLE) + ereport(NOTICE, + errmsg_plural("%" PRIu64 " row was saved to table \"%s\" due to data type incompatibility", + "%" PRIu64 " rows were saved to table \"%s\" due to data type incompatibility", + cstate->num_errors, + cstate->num_errors, + RelationGetRelationName(cstate->error_saving_rel))); } if (bistate != NULL) @@ -1515,6 +1543,17 @@ CopyFrom(CopyFromState cstate) FreeExecutorState(estate); + /* Release resouces associated with error_table */ + if (cstate->error_saving_rel) + { + ExecResetTupleTable(cstate->on_error_mstate->ps.state->es_tupleTable, false); + + ExecCloseResultRelations(cstate->on_error_mstate->ps.state); + ExecCloseRangeTableRelations(cstate->on_error_mstate->ps.state); + + FreeExecutorState(cstate->on_error_mstate->ps.state); + } + return processed; } @@ -1630,10 +1669,50 @@ BeginCopyFrom(ParseState *pstate, if (cstate->opts.on_error == COPY_ON_ERROR_IGNORE || cstate->opts.on_error == COPY_ON_ERROR_SET_NULL) cstate->escontext->details_wanted = false; + else if (cstate->opts.on_error == COPY_ON_ERROR_TABLE) + cstate->escontext->details_wanted = true; } else cstate->escontext = NULL; + cstate->on_error_mstate = NULL; + + /* Set up COPY FROM (ON_ERROR TABLE) */ + if (cstate->opts.on_error == COPY_ON_ERROR_TABLE) + { + Oid err_tbl_oid; + RangeVar *relvar; + List *relname_list; + + Assert(cstate->opts.error_table != NULL); + + relname_list = stringToQualifiedNameList(cstate->opts.error_table, NULL); + relvar = makeRangeVarFromNameList(relname_list); + + /* + * We may need to insert tuples into the error-saving table later. To + * avoid a deadlock or long hang during COPY, verify that the table is + * not already locked; otherwise, report a lock conflict error. + */ + err_tbl_oid = RangeVarGetRelidExtended(relvar, + RowExclusiveLock, + RVR_NOWAIT, + RangeVarCallbackForCopyErrorTable, + NULL); + + if (RelationGetRelid(cstate->rel) == err_tbl_oid) + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot use relation \"%s\" for COPY error saving while copying data to it", + cstate->opts.error_table)); + + cstate->error_saving_rel = table_open(err_tbl_oid, NoLock); + + CopyFromErrorTableInit(cstate); + + table_close(cstate->error_saving_rel, NoLock); + } + if (cstate->opts.on_error == COPY_ON_ERROR_SET_NULL) { int attr_count = list_length(cstate->attnumlist); @@ -1998,3 +2077,209 @@ ClosePipeFromProgram(CopyFromState cstate) errdetail_internal("%s", wait_result_to_str(pclose_rc)))); } } + +/* + * The error saving table must be a plain table and is subject to the following + * restrictions: it cannot have foreign key constraints; nor can it have column + * DEFAULT values, triggers, rules, or row-level security policies. + * + * These restrictions are necessary to allow the use of table_tuple_insert(); + * otherwise, the executor would need to perform additional validation (e.g. + * foreign key constraint check) for each inserted error row. + */ +static void +CopyFromErrorTableCheck(Relation relation) +{ + TupleDesc tupDesc = RelationGetDescr(relation); + char *errdetail_msg = NULL; + + if (tupDesc->constr) + { + if (tupDesc->constr->has_generated_stored || tupDesc->constr->has_generated_virtual) + errdetail_msg = _("The error saving table cannot have generated columns."); + else if (tupDesc->constr->num_defval > 0) + errdetail_msg = _("The error saving table columns cannot have DEFAULT values."); + } + + if (!errdetail_msg) + { + if (list_length(RelationGetFKeyList(relation)) > 0) + errdetail_msg = _("The error saving table cannot have foreign keys."); + else if (relation->rd_rules) + errdetail_msg = _("The error saving table cannot have rules."); + else if (relation->trigdesc) + errdetail_msg = _("The error saving table cannot have triggers."); + else if (relation->rd_rel->relrowsecurity) + errdetail_msg = _("The error saving table cannot have row-level security policies."); + } + + if (errdetail_msg) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot use relation \"%s\" for COPY error saving", + RelationGetRelationName(relation)), + errdetail_internal("%s", errdetail_msg)); +} + +static void +CopyFromErrorTableInit(CopyFromState cstate) +{ + ParseState *pstate; + ResultRelInfo *resultRelInfo; + EState *estate = CreateExecutorState(); + ModifyTableState *mtstate; + Relation relation; + HeapTuple tp; + Oid reloftype = InvalidOid; + Oid typoid; + + Assert(cstate->opts.on_error == COPY_ON_ERROR_TABLE); + + relation = cstate->error_saving_rel; + pstate = make_parsestate(NULL); + + CopyFromErrorTableCheck(relation); + + CopyFromErrorTablePermissionCheck(pstate, relation); + + typoid = GetSysCacheOid2(TYPENAMENSP, Anum_pg_type_oid, + PointerGetDatum("copy_error_saving"), + ObjectIdGetDatum(PG_CATALOG_NAMESPACE)); + + tp = SearchSysCache1(RELOID, ObjectIdGetDatum(RelationGetRelid(relation))); + if (!HeapTupleIsValid(tp)) + elog(ERROR, "cache lookup failed for relation %u", RelationGetRelid(relation)); + else + { + Form_pg_class reltup = (Form_pg_class) GETSTRUCT(tp); + + reloftype = reltup->reloftype; + + if (reloftype != typoid) + ereport(ERROR, + errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot use relation \"%s\" for COPY error saving", + RelationGetRelationName(relation)), + OidIsValid(reloftype) + ? errdetail("Relation \"%s\" is a typed table based on type \"%s\".", + RelationGetRelationName(relation), + format_type_be_qualified(reloftype)) + : 0, + errhint("The COPY error saving table must be a typed table based on type \"%s\".", + format_type_be_qualified(typoid))); + + ReleaseSysCache(tp); + } + + /* + * We need a ResultRelInfo so we can use the regular executor's + * index-entry-making machinery. + */ + ExecInitRangeTable(estate, pstate->p_rtable, pstate->p_rteperminfos, + bms_make_singleton(1)); + + resultRelInfo = makeNode(ResultRelInfo); + ExecInitResultRelation(estate, resultRelInfo, 1); + + /* Verify the named relation is a valid target for INSERT */ + CheckValidResultRel(resultRelInfo, CMD_INSERT, ONCONFLICT_NONE, NIL); + + ExecOpenIndices(resultRelInfo, false); + + /* + * Set up a ModifyTableState for inserting record to error saving table. + */ + mtstate = makeNode(ModifyTableState); + mtstate->ps.plan = NULL; + mtstate->ps.state = estate; + mtstate->operation = CMD_INSERT; + mtstate->mt_nrels = 1; + mtstate->resultRelInfo = resultRelInfo; + mtstate->rootResultRelInfo = resultRelInfo; + + cstate->on_error_mstate = mtstate; +} + +/* + * Callback to RangeVarGetRelidExtended(). + * + * Checks the following: + * - the relation specified is a table. + * - current user must have INSERT priviledge on the table. + * - the table is not a system table. + * + * If any of these checks fails then an error is raised. + */ +static void +RangeVarCallbackForCopyErrorTable(const RangeVar *rv, Oid relid, Oid oldrelid, + void *arg) +{ + HeapTuple tuple; + Form_pg_class classform; + char relkind; + AclResult aclresult; + + tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relid)); + if (!HeapTupleIsValid(tuple)) + return; + + classform = (Form_pg_class) GETSTRUCT(tuple); + relkind = classform->relkind; + + /* Must have INSERT privilege */ + aclresult = pg_class_aclcheck(relid, GetUserId(), ACL_INSERT); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, get_relkind_objtype(get_rel_relkind(relid)), + rv->relname); + + /* No system table modifications unless explicitly allowed */ + if (!allowSystemTableMods && IsSystemClass(relid, classform)) + ereport(ERROR, + errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("permission denied: \"%s\" is a system catalog", + rv->relname)); + + /* The error saving table must be a regular realtion */ + if (relkind != RELKIND_RELATION) + ereport(ERROR, + errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot use relation \"%s\" for COPY error saving", + rv->relname), + errdetail_relkind_not_supported(relkind)); + + ReleaseSysCache(tuple); +} + +static void +CopyFromErrorTablePermissionCheck(ParseState *pstate, Relation rel) +{ + LOCKMODE lockmode = RowExclusiveLock; + ParseNamespaceItem *nsitem; + RTEPermissionInfo *perminfo; + TupleDesc tupDesc; + + nsitem = addRangeTableEntryForRelation(pstate, rel, lockmode, + NULL, false, false); + perminfo = nsitem->p_perminfo; + perminfo->requiredPerms = ACL_INSERT; + + tupDesc = RelationGetDescr(rel); + + for (int i = 0; i < tupDesc->natts; i++) + { + Bitmapset **bms; + int attno; + + CompactAttribute *attr = TupleDescCompactAttr(tupDesc, i); + + if (attr->attisdropped) + continue; + + attno = i + 1 - FirstLowInvalidHeapAttributeNumber; + bms = &perminfo->insertedCols; + + *bms = bms_add_member(*bms, attno); + + } + ExecCheckPermissions(pstate->p_rtable, list_make1(perminfo), true); +} diff --git a/src/backend/commands/copyfromparse.c b/src/backend/commands/copyfromparse.c index 65fd5a0ab4f..bf09323197b 100644 --- a/src/backend/commands/copyfromparse.c +++ b/src/backend/commands/copyfromparse.c @@ -62,6 +62,7 @@ #include <unistd.h> #include <sys/stat.h> +#include "access/heapam.h" #include "commands/copyapi.h" #include "commands/copyfrom_internal.h" #include "commands/progress.h" @@ -1101,6 +1102,90 @@ CopyFromTextLikeOneRow(CopyFromState cstate, ExprContext *econtext, cstate->num_errors++; } } + else if (cstate->opts.on_error == COPY_ON_ERROR_TABLE) + { + char *err_code; + Datum *newvalues; + bool *newnulls; + TupleTableSlot *myslot; + List *recheckIndexes = NIL; + ModifyTableState *mstate = cstate->on_error_mstate; + EState *estate = mstate->ps.state; + + /* Prepare to build the result tuple */ + myslot = ExecGetReturningSlot(estate, mstate->resultRelInfo); + ExecClearTuple(myslot); + + newvalues = myslot->tts_values; + newnulls = myslot->tts_isnull; + + Assert(RelationGetDescr(cstate->error_saving_rel)->natts == 10); + + for (int i = 0; i < RelationGetDescr(cstate->error_saving_rel)->natts; i++) + newnulls[i] = false; + + newvalues[0] = ObjectIdGetDatum(GetUserId()); + newvalues[1] = ObjectIdGetDatum(cstate->rel->rd_rel->oid); + newvalues[2] = CStringGetTextDatum(cstate->filename ? cstate->filename : "STDIN"); + newvalues[3] = Int64GetDatum((int64) cstate->cur_lineno); + newvalues[4] = CStringGetTextDatum(cstate->line_buf.data); + newvalues[5] = CStringGetTextDatum(cstate->cur_attname); + + if (string) + newvalues[6] = CStringGetTextDatum(string); + else + { + newvalues[6] = (Datum) 0; + newnulls[6] = true; + } + + if (cstate->escontext->error_data->message) + { + newvalues[7] = + CStringGetTextDatum(cstate->escontext->error_data->message); + } + else + { + newvalues[7] = (Datum) 0; + newnulls[7] = true; + } + + if (cstate->escontext->error_data->detail) + { + newvalues[8] = + CStringGetTextDatum(cstate->escontext->error_data->detail); + } + else + { + newvalues[8] = (Datum) 0; + newnulls[8] = true; + } + + err_code = unpack_sql_state(cstate->escontext->error_data->sqlerrcode); + newvalues[9] = CStringGetTextDatum(err_code); + + /* Build the virtual tuple. */ + ExecStoreVirtualTuple(myslot); + + /* Check constraint and not-null constraint check */ + if (mstate->resultRelInfo->ri_RelationDesc->rd_att->constr) + ExecConstraints(mstate->resultRelInfo, myslot, estate); + + /* insert the tuple normally */ + table_tuple_insert(cstate->error_saving_rel, myslot, + estate->es_output_cid, + 0, NULL); + + /* insert index entries for tuple */ + if (mstate->resultRelInfo->ri_NumIndices > 0) + recheckIndexes = ExecInsertIndexTuples(mstate->resultRelInfo, + estate, + 0, myslot, NIL, + NULL); + list_free(recheckIndexes); + + cstate->num_errors++; + } if (cstate->opts.log_verbosity == COPY_LOG_VERBOSITY_VERBOSE) { @@ -1130,6 +1215,13 @@ CopyFromTextLikeOneRow(CopyFromState cstate, ExprContext *econtext, cstate->cur_lineno, cstate->cur_attname, attval)); + else if (cstate->opts.on_error == COPY_ON_ERROR_TABLE) + ereport(NOTICE, + errmsg("saving error information to table \"%s\" row due to data type incompatibility at line %" PRIu64 " for column \"%s\": \"%s\"", + RelationGetRelationName(cstate->error_saving_rel), + cstate->cur_lineno, + cstate->cur_attname, + attval)); pfree(attval); } else @@ -1139,6 +1231,12 @@ CopyFromTextLikeOneRow(CopyFromState cstate, ExprContext *econtext, errmsg("skipping row due to data type incompatibility at line %" PRIu64 " for column \"%s\": null input", cstate->cur_lineno, cstate->cur_attname)); + else if (cstate->opts.on_error == COPY_ON_ERROR_TABLE) + ereport(NOTICE, + errmsg("saving error information to table \"%s\" row due to data type incompatibility at line %" PRIu64 " for column \"%s\": null input", + RelationGetRelationName(cstate->error_saving_rel), + cstate->cur_lineno, + cstate->cur_attname)); } /* reset relname_only */ cstate->relname_only = false; @@ -1148,6 +1246,8 @@ CopyFromTextLikeOneRow(CopyFromState cstate, ExprContext *econtext, return true; else if (cstate->opts.on_error == COPY_ON_ERROR_SET_NULL) continue; + else if (cstate->opts.on_error == COPY_ON_ERROR_TABLE) + return true; } cstate->cur_attname = NULL; diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 2d1b19d1f53..923f1acccc7 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -3755,6 +3755,7 @@ copy_generic_opt_arg: | NumericOnly { $$ = (Node *) $1; } | '*' { $$ = (Node *) makeNode(A_Star); } | DEFAULT { $$ = (Node *) makeString("default"); } + | TABLE { $$ = (Node *) makeString("table"); } | '(' copy_generic_opt_arg_list ')' { $$ = (Node *) $2; } | /* EMPTY */ { $$ = NULL; } ; diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h index abecfe51098..f00c3d8db83 100644 --- a/src/include/commands/copy.h +++ b/src/include/commands/copy.h @@ -36,6 +36,7 @@ typedef enum CopyOnErrorChoice COPY_ON_ERROR_STOP = 0, /* immediately throw errors, default */ COPY_ON_ERROR_IGNORE, /* ignore errors */ COPY_ON_ERROR_SET_NULL, /* set error field to null */ + COPY_ON_ERROR_TABLE, /* saving errors info to table */ } CopyOnErrorChoice; /* @@ -96,6 +97,7 @@ typedef struct CopyFormatOptions CopyOnErrorChoice on_error; /* what to do when error happened */ CopyLogVerbosityChoice log_verbosity; /* verbosity of logged messages */ int64 reject_limit; /* maximum tolerable number of errors */ + char *error_table; /* on error, save error info to the table, table name */ List *convert_select; /* list of column names (can be NIL) */ } CopyFormatOptions; diff --git a/src/include/commands/copyfrom_internal.h b/src/include/commands/copyfrom_internal.h index 9d3e244ee55..464c8f5e587 100644 --- a/src/include/commands/copyfrom_internal.h +++ b/src/include/commands/copyfrom_internal.h @@ -73,6 +73,7 @@ typedef struct CopyFromStateData /* parameters from the COPY command */ Relation rel; /* relation to copy from */ + Relation error_saving_rel; /* relation for copy from error saving */ List *attnumlist; /* integer list of attnums to copy */ char *filename; /* filename, or NULL for STDIN */ bool is_program; /* is 'filename' a program to popen? */ @@ -189,6 +190,13 @@ typedef struct CopyFromStateData #define RAW_BUF_BYTES(cstate) ((cstate)->raw_buf_len - (cstate)->raw_buf_index) uint64 bytes_processed; /* number of bytes processed so far */ + + /* + * INSERT operation state for inserting row-level error information to + * error_table. This is used only when ON_ERROR TABLE clause is specified, + * otherwise NULL. + */ + ModifyTableState *on_error_mstate; } CopyFromStateData; extern void ReceiveCopyBegin(CopyFromState cstate); diff --git a/src/test/regress/expected/copy2.out b/src/test/regress/expected/copy2.out index 7600e5239d2..2039d860ee7 100644 --- a/src/test/regress/expected/copy2.out +++ b/src/test/regress/expected/copy2.out @@ -884,7 +884,158 @@ ERROR: skipped more than REJECT_LIMIT (3) rows due to data type incompatibility CONTEXT: COPY check_ign_err, line 5, column n: "" COPY check_ign_err FROM STDIN WITH (on_error ignore, reject_limit 4); NOTICE: 4 rows were skipped due to data type incompatibility +-- Tests for ON_ERROR TABLE, saving error information to a table +-- Direct modification of system catalog composite type 'copy_error_saving' is +-- not permitted. +ALTER TYPE copy_error_saving ADD ATTRIBUTE b text; +ERROR: permission denied: "copy_error_saving" is a system catalog +ALTER TYPE copy_error_saving DROP ATTRIBUTE userid; +ERROR: permission denied: "copy_error_saving" is a system catalog +ALTER TYPE copy_error_saving RENAME ATTRIBUTE userid to userid1; +ERROR: permission denied: "copy_error_saving" is a system catalog +ALTER TYPE copy_error_saving ALTER ATTRIBUTE userid SET DATA TYPE OID8; +ERROR: permission denied: "copy_error_saving" is a system catalog +CREATE TABLE save_error(a int, b int, c int, d dcheck_ign_err2); +CREATE TABLE err_tbl1 OF copy_error_saving; +CREATE TABLE err_tbl OF copy_error_saving; +CREATE UNIQUE INDEX err_tbl_idx ON err_tbl(colname); +CREATE TEMP TABLE err_tbl_1 AS SELECT * FROM err_tbl; +CREATE TYPE t_copy_typ AS (a int, b int, c int); +CREATE TABLE t_copy_tbl1 OF t_copy_typ; +CREATE TEMP VIEW t_copy_v1 AS SELECT * FROM save_error; +-- The error saving table must be a plain table and is subject to the following +-- restrictions: it cannot contain foreign key constraints; it must not have +-- column DEFAULT values, triggers, rules, or row-level security policies. +ALTER TABLE err_tbl1 ADD CONSTRAINT con1 FOREIGN KEY (colname) REFERENCES err_tbl(colname); +ALTER TABLE err_tbl1 ALTER COLUMN errorcode SET DEFAULT 'TEST'; +CREATE TRIGGER trg_x_after AFTER INSERT ON err_tbl1 FOR EACH ROW EXECUTE PROCEDURE fn_x_after(); +CREATE POLICY p1 ON err_tbl1 FOR SELECT USING (true); +ALTER TABLE err_tbl1 ENABLE ROW LEVEL SECURITY; +ALTER TABLE err_tbl1 FORCE ROW LEVEL SECURITY; +COPY save_error(a,b) FROM STDIN WITH (DELIMITER ',', ON_ERROR TABLE, ERROR_TABLE err_tbl1); +ERROR: cannot use relation "err_tbl1" for COPY error saving +DETAIL: The error saving table columns cannot have DEFAULT values. +ALTER TABLE err_tbl1 ALTER COLUMN errorcode DROP DEFAULT; +COPY save_error(a,b) FROM STDIN WITH (DELIMITER ',', ON_ERROR TABLE, ERROR_TABLE err_tbl1); +ERROR: cannot use relation "err_tbl1" for COPY error saving +DETAIL: The error saving table cannot have foreign keys. +ALTER TABLE err_tbl1 DROP CONSTRAINT con1; +COPY save_error(a,b) FROM STDIN WITH (DELIMITER ',', ON_ERROR TABLE, ERROR_TABLE err_tbl1); +ERROR: cannot use relation "err_tbl1" for COPY error saving +DETAIL: The error saving table cannot have triggers. +DROP TRIGGER IF EXISTS trg_x_after ON err_tbl1; +COPY save_error(a,b) FROM STDIN WITH (DELIMITER ',', ON_ERROR TABLE, ERROR_TABLE err_tbl1); +ERROR: cannot use relation "err_tbl1" for COPY error saving +DETAIL: The error saving table cannot have row-level security policies. +DROP POLICY IF EXISTS p1 ON err_tbl1; +ALTER TABLE err_tbl1 DISABLE ROW LEVEL SECURITY; +COPY save_error(a,b) FROM STDIN WITH (DELIMITER ',', ON_ERROR TABLE, ERROR_TABLE err_tbl1); +DROP TABLE err_tbl1; +-- all of the following should fail +COPY save_error FROM STDIN WITH (FORMAT BINARY, ON_ERROR TABLE, ERROR_TABLE err_tbl); +ERROR: only ON_ERROR STOP is allowed in BINARY mode +COPY save_error FROM STDIN WITH (ON_ERROR TABLE, ERROR_TABLE save_error); +ERROR: cannot use relation "save_error" for COPY error saving while copying data to it +COPY save_error(a) FROM STDIN WITH (ON_ERROR TABLE, ERROR_TABLE t_copy_tbl1); +ERROR: cannot use relation "t_copy_tbl1" for COPY error saving +DETAIL: Relation "t_copy_tbl1" is a typed table based on type "public.t_copy_typ". +HINT: The COPY error saving table must be a typed table based on type "pg_catalog.copy_error_saving". +COPY save_error(a) FROM STDIN WITH (ON_ERROR TABLE, ERROR_TABLE t_copy_v1); +ERROR: cannot use relation "t_copy_v1" for COPY error saving +DETAIL: This operation is not supported for views. +COPY save_error(a,b) FROM STDIN WITH (ON_ERROR TABLE, reject_limit 10, ERROR_TABLE err_tbl); +ERROR: cannot set option REJECT_LIMIT when ON_ERROR is specified as "TABLE" +COPY save_error(a,b) FROM STDIN WITH (ON_ERROR TABLE, ERROR_TABLE not_exists); +ERROR: relation "not_exists" does not exist +COPY save_error FROM STDIN WITH (ON_ERROR TABLE); +ERROR: cannot set option ON_ERROR to "TABLE" when "ERROR_TABLE" is not specified +HINT: "ERROR_TABLE" option is required +COPY save_error FROM STDIN WITH (ERROR_TABLE err_tbl); +ERROR: COPY ERROR_TABLE can only be used when option ON_ERROR is set to "TABLE" +COPY save_error TO STDIN WITH (ON_ERROR TABLE); +ERROR: COPY ON_ERROR cannot be used with COPY TO +LINE 1: COPY save_error TO STDIN WITH (ON_ERROR TABLE); + ^ +-- all of the above should fail +-- fail, copied data have extra columns +COPY save_error(a,b) FROM STDIN WITH (DELIMITER ',', ON_ERROR TABLE, ERROR_TABLE err_tbl); +ERROR: extra data after last expected column +CONTEXT: COPY save_error, line 1: "1,2,3,4,5" +-- fail, copied data have less columns +COPY save_error(a,b) FROM STDIN WITH (DELIMITER ',', ON_ERROR TABLE, ERROR_TABLE err_tbl); +ERROR: extra data after last expected column +CONTEXT: COPY save_error, line 1: "1,2,3" +-- fail, constraint violation +ALTER TABLE err_tbl ADD CONSTRAINT cc2 CHECK(lineno > 2); +COPY save_error FROM STDIN WITH (DELIMITER ',', ON_ERROR TABLE, ERROR_TABLE err_tbl); +ERROR: new row for relation "err_tbl" violates check constraint "cc2" +DETAIL: Failing row contains (10, 16848, STDIN, 1, a,b,3,4, a, a, invalid input syntax for type integer: "a", null, 22P02). +CONTEXT: COPY save_error, line 1, column a: "a" +ALTER TABLE err_tbl DROP CONSTRAINT cc2; +ALTER TABLE err_tbl ADD CONSTRAINT cc3 NOT NULL err_detail; +COPY save_error FROM STDIN WITH (DELIMITER ',', ON_ERROR TABLE, ERROR_TABLE err_tbl); +ERROR: null value in column "err_detail" of relation "err_tbl" violates not-null constraint +DETAIL: Failing row contains (10, 16848, STDIN, 1, a,b,3,4, a, a, invalid input syntax for type integer: "a", null, 22P02). +CONTEXT: COPY save_error, line 1, column a: "a" +ALTER TABLE err_tbl DROP CONSTRAINT cc3; +BEGIN; +CREATE USER regress_user30; +GRANT INSERT(userid, copy_tbl, filename, lineno, line, colname, raw_field_value, err_message, err_detail) + ON TABLE err_tbl TO regress_user30; +GRANT INSERT ON TABLE save_error TO regress_user30; +GRANT SELECT ON TABLE err_tbl TO regress_user30; +SAVEPOINT s1; +SET ROLE regress_user30; +COPY save_error FROM STDIN WITH (DELIMITER ',', ON_ERROR TABLE, ERROR_TABLE err_tbl); -- should fail +ERROR: permission denied for table err_tbl +ROLLBACK TO SAVEPOINT s1; +RESET ROLE; +GRANT INSERT on TABLE err_tbl to regress_user30; +GRANT INSERT(errorcode) ON TABLE err_tbl TO regress_user30; +SET ROLE regress_user30; +COPY save_error FROM STDIN WITH (DELIMITER ',', ON_ERROR TABLE, ERROR_TABLE err_tbl); -- ok +NOTICE: 1 row was saved to table "err_tbl" due to data type incompatibility +SELECT copy_tbl::regclass, filename, lineno, line, colname, raw_field_value, + err_message, err_detail, errorcode +FROM err_tbl; + copy_tbl | filename | lineno | line | colname | raw_field_value | err_message | err_detail | errorcode +------------+----------+--------+---------+---------+-----------------+--------------------------------------------+------------+----------- + save_error | STDIN | 1 | a,b,3,4 | a | a | invalid input syntax for type integer: "a" | | 22P02 +(1 row) + +-- error, because of table err_tbl unique constraint violation +COPY save_error FROM STDIN WITH (DELIMITER ',', ON_ERROR TABLE, ERROR_TABLE err_tbl); +ERROR: duplicate key value violates unique constraint "err_tbl_idx" +DETAIL: Key (colname)=(a) already exists. +CONTEXT: COPY save_error, line 1, column a: "a" +ROLLBACK; +DROP INDEX err_tbl_idx; +COPY save_error FROM STDIN WITH (DELIMITER ',', ON_ERROR TABLE, ERROR_TABLE err_tbl, LOG_VERBOSITY verbose); +NOTICE: saving error information to table "err_tbl" row due to data type incompatibility at line 1 for column "c": "a" +NOTICE: saving error information to table "err_tbl" row due to data type incompatibility at line 2 for column "d": null input +NOTICE: saving error information to table "err_tbl" row due to data type incompatibility at line 3 for column "b": "_junk" +NOTICE: saving error information to table "err_tbl" row due to data type incompatibility at line 4 for column "a": "cola" +NOTICE: saving error information to table "err_tbl" row due to data type incompatibility at line 6 for column "c": "4238679732489879879" +NOTICE: 5 rows were saved to table "err_tbl" due to data type incompatibility +SELECT copy_tbl::regclass, filename, lineno, line, colname + ,raw_field_value, err_message, err_detail, errorcode +FROM err_tbl; + copy_tbl | filename | lineno | line | colname | raw_field_value | err_message | err_detail | errorcode +------------+----------+--------+----------------------------+---------+---------------------+--------------------------------------------------------------+------------+----------- + save_error | STDIN | 1 | 1,2,a,1 | c | a | invalid input syntax for type integer: "a" | | 22P02 + save_error | STDIN | 2 | 1,2,3,\N | d | | domain dcheck_ign_err2 does not allow null values | | 23502 + save_error | STDIN | 3 | 1,_junk,test,11 | b | _junk | invalid input syntax for type integer: "_junk" | | 22P02 + save_error | STDIN | 4 | cola,colb,colc,12 | a | cola | invalid input syntax for type integer: "cola" | | 22P02 + save_error | STDIN | 6 | 1,11,4238679732489879879,2 | c | 4238679732489879879 | value "4238679732489879879" is out of range for type integer | | 22003 +(5 rows) + -- clean up +DROP TABLE err_tbl; +DROP TABLE err_tbl_1; +DROP VIEW t_copy_v1; +DROP TABLE save_error; +DROP TABLE t_copy_tbl1; +DROP TYPE t_copy_typ; DROP TABLE forcetest; DROP TABLE vistest; DROP FUNCTION truncate_in_subxact(); diff --git a/src/test/regress/sql/copy2.sql b/src/test/regress/sql/copy2.sql index e0810109473..4c7b12a253b 100644 --- a/src/test/regress/sql/copy2.sql +++ b/src/test/regress/sql/copy2.sql @@ -636,7 +636,130 @@ a {7} 7 10 {10} 10 \. +-- Tests for ON_ERROR TABLE, saving error information to a table + +-- Direct modification of system catalog composite type 'copy_error_saving' is +-- not permitted. +ALTER TYPE copy_error_saving ADD ATTRIBUTE b text; +ALTER TYPE copy_error_saving DROP ATTRIBUTE userid; +ALTER TYPE copy_error_saving RENAME ATTRIBUTE userid to userid1; +ALTER TYPE copy_error_saving ALTER ATTRIBUTE userid SET DATA TYPE OID8; + +CREATE TABLE save_error(a int, b int, c int, d dcheck_ign_err2); +CREATE TABLE err_tbl1 OF copy_error_saving; +CREATE TABLE err_tbl OF copy_error_saving; +CREATE UNIQUE INDEX err_tbl_idx ON err_tbl(colname); +CREATE TEMP TABLE err_tbl_1 AS SELECT * FROM err_tbl; +CREATE TYPE t_copy_typ AS (a int, b int, c int); +CREATE TABLE t_copy_tbl1 OF t_copy_typ; +CREATE TEMP VIEW t_copy_v1 AS SELECT * FROM save_error; + +-- The error saving table must be a plain table and is subject to the following +-- restrictions: it cannot contain foreign key constraints; it must not have +-- column DEFAULT values, triggers, rules, or row-level security policies. +ALTER TABLE err_tbl1 ADD CONSTRAINT con1 FOREIGN KEY (colname) REFERENCES err_tbl(colname); +ALTER TABLE err_tbl1 ALTER COLUMN errorcode SET DEFAULT 'TEST'; +CREATE TRIGGER trg_x_after AFTER INSERT ON err_tbl1 FOR EACH ROW EXECUTE PROCEDURE fn_x_after(); +CREATE POLICY p1 ON err_tbl1 FOR SELECT USING (true); +ALTER TABLE err_tbl1 ENABLE ROW LEVEL SECURITY; +ALTER TABLE err_tbl1 FORCE ROW LEVEL SECURITY; +COPY save_error(a,b) FROM STDIN WITH (DELIMITER ',', ON_ERROR TABLE, ERROR_TABLE err_tbl1); +ALTER TABLE err_tbl1 ALTER COLUMN errorcode DROP DEFAULT; +COPY save_error(a,b) FROM STDIN WITH (DELIMITER ',', ON_ERROR TABLE, ERROR_TABLE err_tbl1); +ALTER TABLE err_tbl1 DROP CONSTRAINT con1; +COPY save_error(a,b) FROM STDIN WITH (DELIMITER ',', ON_ERROR TABLE, ERROR_TABLE err_tbl1); +DROP TRIGGER IF EXISTS trg_x_after ON err_tbl1; +COPY save_error(a,b) FROM STDIN WITH (DELIMITER ',', ON_ERROR TABLE, ERROR_TABLE err_tbl1); +DROP POLICY IF EXISTS p1 ON err_tbl1; +ALTER TABLE err_tbl1 DISABLE ROW LEVEL SECURITY; +COPY save_error(a,b) FROM STDIN WITH (DELIMITER ',', ON_ERROR TABLE, ERROR_TABLE err_tbl1); +\. +DROP TABLE err_tbl1; + +-- all of the following should fail +COPY save_error FROM STDIN WITH (FORMAT BINARY, ON_ERROR TABLE, ERROR_TABLE err_tbl); +COPY save_error FROM STDIN WITH (ON_ERROR TABLE, ERROR_TABLE save_error); +COPY save_error(a) FROM STDIN WITH (ON_ERROR TABLE, ERROR_TABLE t_copy_tbl1); +COPY save_error(a) FROM STDIN WITH (ON_ERROR TABLE, ERROR_TABLE t_copy_v1); +COPY save_error(a,b) FROM STDIN WITH (ON_ERROR TABLE, reject_limit 10, ERROR_TABLE err_tbl); +COPY save_error(a,b) FROM STDIN WITH (ON_ERROR TABLE, ERROR_TABLE not_exists); +COPY save_error FROM STDIN WITH (ON_ERROR TABLE); +COPY save_error FROM STDIN WITH (ERROR_TABLE err_tbl); +COPY save_error TO STDIN WITH (ON_ERROR TABLE); +-- all of the above should fail + +-- fail, copied data have extra columns +COPY save_error(a,b) FROM STDIN WITH (DELIMITER ',', ON_ERROR TABLE, ERROR_TABLE err_tbl); +1,2,3,4,5 +\. + +-- fail, copied data have less columns +COPY save_error(a,b) FROM STDIN WITH (DELIMITER ',', ON_ERROR TABLE, ERROR_TABLE err_tbl); +1,2,3 +\. + +-- fail, constraint violation +ALTER TABLE err_tbl ADD CONSTRAINT cc2 CHECK(lineno > 2); +COPY save_error FROM STDIN WITH (DELIMITER ',', ON_ERROR TABLE, ERROR_TABLE err_tbl); +a,b,3,4 +\. +ALTER TABLE err_tbl DROP CONSTRAINT cc2; + +ALTER TABLE err_tbl ADD CONSTRAINT cc3 NOT NULL err_detail; +COPY save_error FROM STDIN WITH (DELIMITER ',', ON_ERROR TABLE, ERROR_TABLE err_tbl); +a,b,3,4 +\. +ALTER TABLE err_tbl DROP CONSTRAINT cc3; + +BEGIN; +CREATE USER regress_user30; +GRANT INSERT(userid, copy_tbl, filename, lineno, line, colname, raw_field_value, err_message, err_detail) + ON TABLE err_tbl TO regress_user30; +GRANT INSERT ON TABLE save_error TO regress_user30; +GRANT SELECT ON TABLE err_tbl TO regress_user30; +SAVEPOINT s1; + +SET ROLE regress_user30; +COPY save_error FROM STDIN WITH (DELIMITER ',', ON_ERROR TABLE, ERROR_TABLE err_tbl); -- should fail +ROLLBACK TO SAVEPOINT s1; + +RESET ROLE; +GRANT INSERT on TABLE err_tbl to regress_user30; +GRANT INSERT(errorcode) ON TABLE err_tbl TO regress_user30; +SET ROLE regress_user30; +COPY save_error FROM STDIN WITH (DELIMITER ',', ON_ERROR TABLE, ERROR_TABLE err_tbl); -- ok +a,b,3,4 +\. +SELECT copy_tbl::regclass, filename, lineno, line, colname, raw_field_value, + err_message, err_detail, errorcode +FROM err_tbl; + +-- error, because of table err_tbl unique constraint violation +COPY save_error FROM STDIN WITH (DELIMITER ',', ON_ERROR TABLE, ERROR_TABLE err_tbl); +a,b,3,4 +\. +ROLLBACK; + +DROP INDEX err_tbl_idx; +COPY save_error FROM STDIN WITH (DELIMITER ',', ON_ERROR TABLE, ERROR_TABLE err_tbl, LOG_VERBOSITY verbose); +1,2,a,1 +1,2,3,\N +1,_junk,test,11 +cola,colb,colc,12 +4,5,6,1111 +1,11,4238679732489879879,2 +\. +SELECT copy_tbl::regclass, filename, lineno, line, colname + ,raw_field_value, err_message, err_detail, errorcode +FROM err_tbl; + -- clean up +DROP TABLE err_tbl; +DROP TABLE err_tbl_1; +DROP VIEW t_copy_v1; +DROP TABLE save_error; +DROP TABLE t_copy_tbl1; +DROP TYPE t_copy_typ; DROP TABLE forcetest; DROP TABLE vistest; DROP FUNCTION truncate_in_subxact(); -- 2.34.1
