hi.

The previous discussion mentioned using built-in typed tables for the
error saving table.
It's doable.

first create an build-in composite type in system_functions.sql:
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"
);

then we can use it to create a table like:
CREATE TABLE error_saving_table OF copy_error_saving;

The downside of this:
If the pg_catalog composite (type copy_error_saving) were to change,
it could lead to potential compatibility issues.
We need to be confident that copy_error_saving definitions are
unlikely to occur in the future.

For the above type copy_error_saving, I am wondering, do we aslo need
add a timestamptz field like "starttime" to indicate COPY beginning time.

anyway, please take a look at the attached patch.
It introduces a built-in composite type, allowing users to simply use
CREATE TABLE x OF copy_error_saving
to create a table for storing COPY FROM error-related information.
From 0593ef9fee095ed437ec7a5db44e359ee729ca40 Mon Sep 17 00:00:00 2001
From: jian he <[email protected]>
Date: Wed, 22 Oct 2025 13:09:03 +0800
Subject: [PATCH v7 1/1] COPY FROM (on_error table)

The syntax is {on_error table, table error_saving_tbl}.
The error_saving_tbl must be a typed table, based on internal type
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.
---
 doc/src/sgml/datatype.sgml               | 100 +++++++++++++++
 doc/src/sgml/ref/copy.sgml               |  32 ++++-
 src/backend/catalog/system_functions.sql |  13 ++
 src/backend/commands/copy.c              |  37 +++++-
 src/backend/commands/copyfrom.c          | 147 +++++++++++++++++++++--
 src/backend/commands/copyfromparse.c     |  51 +++++++-
 src/backend/parser/gram.y                |   1 +
 src/include/commands/copy.h              |   2 +
 src/include/commands/copyfrom_internal.h |   1 +
 src/test/regress/expected/copy2.out      |  91 ++++++++++++++
 src/test/regress/sql/copy2.sql           |  77 ++++++++++++
 11 files changed, 537 insertions(+), 15 deletions(-)

diff --git a/doc/src/sgml/datatype.sgml b/doc/src/sgml/datatype.sgml
index b81d89e2608..d5aaafbd2ea 100644
--- a/doc/src/sgml/datatype.sgml
+++ b/doc/src/sgml/datatype.sgml
@@ -5092,6 +5092,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 fdc24b36bb8..0215aac0771 100644
--- a/doc/src/sgml/ref/copy.sgml
+++ b/doc/src/sgml/ref/copy.sgml
@@ -44,6 +44,7 @@ COPY { <replaceable class="parameter">table_name</replaceable> [ ( <replaceable
     FORCE_NOT_NULL { ( <replaceable class="parameter">column_name</replaceable> [, ...] ) | * }
     FORCE_NULL { ( <replaceable class="parameter">column_name</replaceable> [, ...] ) | * }
     ON_ERROR <replaceable class="parameter">error_action</replaceable>
+    TABLE <replaceable class="parameter">error_saving_table</replaceable>
     REJECT_LIMIT <replaceable class="parameter">maxerror</replaceable>
     ENCODING '<replaceable class="parameter">encoding_name</replaceable>'
     LOG_VERBOSITY <replaceable class="parameter">verbosity</replaceable>
@@ -413,15 +414,24 @@ COPY { <replaceable class="parameter">table_name</replaceable> [ ( <replaceable
       input value into its data type.
       An <replaceable class="parameter">error_action</replaceable> value of
       <literal>stop</literal> means fail the command, while
-      <literal>ignore</literal> means discard the input row and continue with the next one.
+      <literal>ignore</literal> means discard the input row and continue with the next one,
+      <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> option is applicable only for <command>COPY FROM</command>
+      The <literal>ignore</literal> and <literal>table</literal> option are applicable only for <command>COPY FROM</command>
       when the <literal>FORMAT</literal> is <literal>text</literal> or <literal>csv</literal>.
      </para>
      <para>
-      A <literal>NOTICE</literal> message containing the ignored row count is
+      If <literal>ON_ERROR</literal> option is set to <literal>table</literal>,
+      a <literal>NOTICE</literal> message containing the row count saved to
+      <replaceable class="parameter">error_saving_table</replaceable> is
+      emitted at the end of the <command>COPY FROM</command>.
+     </para>
+
+     <para>
+      If <literal>ON_ERROR</literal> option is set to <literal>ignore</literal>, a <literal>NOTICE</literal> message containing the ignored row count is
       emitted at the end of the <command>COPY FROM</command> if at least one
       row was discarded. When <literal>LOG_VERBOSITY</literal> option is set to
       <literal>verbose</literal>, a <literal>NOTICE</literal> message
@@ -481,6 +491,22 @@ COPY { <replaceable class="parameter">table_name</replaceable> [ ( <replaceable
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>TABLE</literal></term>
+    <listitem>
+      <para>
+        Save error context details to the specified 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 with <literal>TABLE</literal>.
+        It also require the current <command>COPY FROM</command> user 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 composite type <link
+        linkend="datatype-copy_error_saving"><command>copy_error_saving</command></link>.
+      </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>WHERE</literal></term>
     <listitem>
diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index 2d946d6d9e9..dc58125915d 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -813,3 +813,16 @@ GRANT pg_read_all_settings TO pg_monitor;
 GRANT pg_read_all_stats TO pg_monitor;
 
 GRANT pg_stat_scan_tables TO pg_monitor;
+
+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 44020d0ae80..c896fc67529 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -417,13 +417,16 @@ defGetCopyOnErrorChoice(DefElem *def, ParseState *pstate, bool is_from)
 				 parser_errposition(pstate, def->location)));
 
 	/*
-	 * Allow "stop", or "ignore" values.
+	 * Allow "stop", "ignore" or "table" values.
 	 */
 	if (pg_strcasecmp(sval, "stop") == 0)
 		return COPY_ON_ERROR_STOP;
 	if (pg_strcasecmp(sval, "ignore") == 0)
 		return COPY_ON_ERROR_IGNORE;
 
+	if (pg_strcasecmp(sval, "table") == 0)
+		return COPY_ON_ERROR_TABLE;
+
 	ereport(ERROR,
 			(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 	/*- translator: first %s is the name of a COPY option, e.g. ON_ERROR */
@@ -516,6 +519,7 @@ ProcessCopyOptions(ParseState *pstate,
 	bool		freeze_specified = false;
 	bool		header_specified = false;
 	bool		on_error_specified = false;
+	bool		on_error_tbl_specified = false;
 	bool		log_verbosity_specified = false;
 	bool		reject_limit_specified = false;
 	ListCell   *option;
@@ -691,6 +695,13 @@ ProcessCopyOptions(ParseState *pstate,
 			reject_limit_specified = true;
 			opts_out->reject_limit = defGetCopyRejectLimitOption(defel);
 		}
+		else if (strcmp(defel->defname, "table") == 0)
+		{
+			if (on_error_tbl_specified)
+				errorConflictingDefElem(defel, pstate);
+			on_error_tbl_specified = true;
+			opts_out->on_error_tbl = defGetString(defel);
+		}
 		else
 			ereport(ERROR,
 					(errcode(ERRCODE_SYNTAX_ERROR),
@@ -699,6 +710,30 @@ ProcessCopyOptions(ParseState *pstate,
 					 parser_errposition(pstate, defel->location)));
 	}
 
+	if (opts_out->on_error == COPY_ON_ERROR_TABLE)
+	{
+		if (opts_out->on_error_tbl == NULL)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("cannot set option %s to \"%s\" when \"%s\" is not specified", "ON_ERROR", "TABLE", "TABLE"),
+					errhint("You may need also specify \"%s\" option.", "TABLE"));
+
+		/* TODO: these two options can be supported */
+		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"));
+
+		if (opts_out->log_verbosity == COPY_LOG_VERBOSITY_VERBOSE)
+			ereport(ERROR,
+					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					errmsg("cannot set option %s to \"%s\" when %s is specified as \"%s\"", "LOG_VERBOSITY", "VERBOSE", "ON_ERROR", "TABLE"));
+	}
+	else if (opts_out->on_error_tbl != NULL)
+		ereport(ERROR,
+				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("COPY %s can only be used when option %s is set to \"%s\"", "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 12781963b4f..629bdfb4b38 100644
--- a/src/backend/commands/copyfrom.c
+++ b/src/backend/commands/copyfrom.c
@@ -28,6 +28,7 @@
 #include "access/tableam.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"
@@ -45,11 +46,14 @@
 #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/syscache.h"
 
 /*
  * No more than this many tuples per CopyMultiInsertBuffer
@@ -1174,6 +1178,25 @@ CopyFrom(CopyFromState cstate)
 			/* Repeat NextCopyFrom() until no soft error occurs */
 			continue;
 		}
+		else if (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.
+			 * We also need to set details_wanted to true.
+			 */
+			cstate->escontext->error_occurred = false;
+			cstate->escontext->details_wanted = true;
+			memset(cstate->escontext->error_data, 0, sizeof(ErrorData));
+
+			/* Report that this tuple was skipped by the ON_ERROR clause */
+			pgstat_progress_update_param(PROGRESS_COPY_TUPLES_SKIPPED,
+										 cstate->num_errors);
+
+			/* Repeat NextCopyFrom() until no soft error occurs */
+			continue;
+		}
 
 		ExecStoreVirtualTuple(myslot);
 
@@ -1467,14 +1490,23 @@ CopyFrom(CopyFromState cstate)
 	/* Done, clean up */
 	error_context_stack = errcallback.previous;
 
-	if (cstate->opts.on_error != COPY_ON_ERROR_STOP &&
-		cstate->num_errors > 0 &&
+	if (cstate->num_errors > 0 &&
 		cstate->opts.log_verbosity >= COPY_LOG_VERBOSITY_DEFAULT)
-		ereport(NOTICE,
-				errmsg_plural("%" PRIu64 " row was skipped due to data type incompatibility",
-							  "%" PRIu64 " rows were skipped due to data type incompatibility",
-							  cstate->num_errors,
-							  cstate->num_errors));
+	{
+		if (cstate->opts.on_error == COPY_ON_ERROR_IGNORE)
+			ereport(NOTICE,
+					errmsg_plural("%" PRIu64 " row was skipped due to data type incompatibility",
+								  "%" PRIu64 " rows were skipped 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)));
+	}
 
 	if (bistate != NULL)
 		FreeBulkInsertState(bistate);
@@ -1509,6 +1541,9 @@ CopyFrom(CopyFromState cstate)
 
 	FreeExecutorState(estate);
 
+	if (cstate->error_saving)
+		table_close(cstate->error_saving, NoLock);
+
 	return processed;
 }
 
@@ -1622,15 +1657,109 @@ BeginCopyFrom(ParseState *pstate,
 		cstate->escontext->error_occurred = false;
 
 		/*
-		 * Currently we only support COPY_ON_ERROR_IGNORE. We'll add other
-		 * options later
+		 * Currently we only support COPY_ON_ERROR_IGNORE, COPY_ON_ERROR_TABLE.
+		 * We'll add other options later.
 		 */
 		if (cstate->opts.on_error == COPY_ON_ERROR_IGNORE)
 			cstate->escontext->details_wanted = false;
+		else if (cstate->opts.on_error == COPY_ON_ERROR_TABLE)
+			cstate->escontext->details_wanted = true;
 	}
 	else
 		cstate->escontext = NULL;
 
+	if (cstate->opts.on_error == COPY_ON_ERROR_TABLE)
+	{
+		Oid			reloftype;
+		Oid			typoid;
+		Oid			err_tbl_oid;
+		Datum		value;
+		RangeVar   *relvar;
+		List	   *relname_list;
+		HeapTuple	tp;
+
+		Assert(cstate->opts.on_error_tbl != NULL);
+
+		relname_list = stringToQualifiedNameList(cstate->opts.on_error_tbl, NULL);
+		relvar = makeRangeVarFromNameList(relname_list);
+
+		/*
+		 * We might insert tuples into the error-saving table later, so we first
+		 * need to check its lock status. If it is already heavily locked, our
+		 * subsequent COPY FROM may stuck. Instead of letting COPY FROM hang,
+		 * report an error indicating that the error-saving table is under heavy
+		 * lock.
+		 */
+		err_tbl_oid = RangeVarGetRelidExtended(relvar,
+											   RowExclusiveLock,
+											   RVR_NOWAIT,
+											   NULL,
+											   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.on_error_tbl));
+
+		/* error saving table must be a regular realtion */
+		if (get_rel_relkind(err_tbl_oid) != RELKIND_RELATION)
+			ereport(ERROR,
+					errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					errmsg("cannot use relation \"%s\" for COPY error saving",
+						   cstate->opts.on_error_tbl),
+					errdetail_relkind_not_supported(get_rel_relkind(err_tbl_oid)));
+
+		cstate->error_saving = table_open(err_tbl_oid, NoLock);
+
+		/* The user should have INSERT privilege on error_saving table */
+		value = DirectFunctionCall3(has_table_privilege_id_id,
+									ObjectIdGetDatum(GetUserId()),
+									ObjectIdGetDatum(err_tbl_oid),
+									CStringGetTextDatum("INSERT"));
+		if (!DatumGetBool(value))
+			ereport(ERROR,
+					errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+					errmsg("permission denied to use table \"%s\" for COPY FROM error saving",
+						   RelationGetRelationName(cstate->error_saving)),
+					errhint("Ensure the current user has INSERT privilege on table \"%s\" to use it for COPY FROM error saving.",
+							RelationGetRelationName(cstate->error_saving)));
+
+		tp = SearchSysCache1(RELOID, ObjectIdGetDatum(err_tbl_oid));
+		if (HeapTupleIsValid(tp))
+		{
+			Form_pg_class reltup = (Form_pg_class) GETSTRUCT(tp);
+
+			reloftype = reltup->reloftype;
+			ReleaseSysCache(tp);
+		}
+		else
+			elog(ERROR, "cache lookup failed for relation %u", err_tbl_oid);
+
+		if (!OidIsValid(reloftype))
+			ereport(ERROR,
+					errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					errmsg("cannot use relation \"%s\" for COPY error saving",
+						   cstate->opts.on_error_tbl),
+					errhint("The COPY error saving table must be a typed table base on type \"%s\".",
+							"copy_error_saving"));
+
+		typoid = GetSysCacheOid2(TYPENAMENSP, Anum_pg_type_oid,
+								 PointerGetDatum("copy_error_saving"),
+								 ObjectIdGetDatum(PG_CATALOG_NAMESPACE));
+
+		if (reloftype != typoid)
+			ereport(ERROR,
+					errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					errmsg("cannot use relation \"%s\" for COPY error saving",
+						   cstate->opts.on_error_tbl),
+					errdetail("relation \"%s\" is a typed table based on type \"%s\"",
+							  cstate->opts.on_error_tbl,
+							  format_type_be(reloftype)),
+					errhint("The COPY error saving table must be a typed table base on type \"%s\".",
+							format_type_be(typoid)));
+	}
+
 	/* Convert FORCE_NULL name list to per-column flags, check validity */
 	cstate->opts.force_null_flags = (bool *) palloc0(num_phys_attrs * sizeof(bool));
 	if (cstate->opts.force_null_all)
diff --git a/src/backend/commands/copyfromparse.c b/src/backend/commands/copyfromparse.c
index b1ae97b833d..41bb179a9d9 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"
@@ -1033,7 +1034,7 @@ CopyFromTextLikeOneRow(CopyFromState cstate, ExprContext *econtext,
 		}
 
 		/*
-		 * If ON_ERROR is specified with IGNORE, skip rows with soft errors
+		 * If ON_ERROR is specified with IGNORE or TABLE, skip rows with soft errors
 		 */
 		else if (!InputFunctionCallSafe(&in_functions[m],
 										string,
@@ -1044,9 +1045,55 @@ CopyFromTextLikeOneRow(CopyFromState cstate, ExprContext *econtext,
 		{
 			Assert(cstate->opts.on_error != COPY_ON_ERROR_STOP);
 
+			if (cstate->opts.on_error == COPY_ON_ERROR_TABLE)
+			{
+				/*
+				 * We use ErrorSaveContext stored information to form a tuple
+				 * and insert it to the specified error saving table.
+				 */
+				HeapTuple	tuple;
+				TupleDesc	tupdesc;
+				char		*err_detail;
+				char		*err_code;
+				Datum		values[10] = {0};
+				bool		isnull[10] = {0};
+				int			j = 0;
+
+				Assert(cstate->rel != NULL);
+				Assert(cstate->escontext->error_occurred);
+
+				values[j++] = ObjectIdGetDatum(GetUserId());
+				values[j++] = ObjectIdGetDatum(cstate->rel->rd_rel->oid);
+				values[j++] = CStringGetTextDatum(cstate->filename ? cstate->filename : "STDIN");
+				values[j++] = Int64GetDatum((long long) cstate->cur_lineno);
+				values[j++] = CStringGetTextDatum(cstate->line_buf.data);
+				values[j++] = CStringGetTextDatum(cstate->cur_attname);
+				values[j++] = CStringGetTextDatum(string);
+				values[j++] = CStringGetTextDatum(cstate->escontext->error_data->message);
+
+				if (cstate->escontext->error_data->detail == NULL)
+					err_detail = NULL;
+				else
+					err_detail = cstate->escontext->error_data->detail;
+
+				values[j]   = err_detail ? CStringGetTextDatum(err_detail) : (Datum) 0;
+				isnull[j++] = err_detail ? false : true;
+
+				err_code = unpack_sql_state(cstate->escontext->error_data->sqlerrcode);
+				values[j++] = CStringGetTextDatum(err_code);
+
+				tupdesc =  RelationGetDescr(cstate->error_saving);
+				tuple = heap_form_tuple(tupdesc, values, isnull);
+
+				simple_heap_insert(cstate->error_saving, tuple);
+
+				heap_freetuple(tuple);
+			}
+
 			cstate->num_errors++;
 
-			if (cstate->opts.log_verbosity == COPY_LOG_VERBOSITY_VERBOSE)
+			if (cstate->opts.log_verbosity == COPY_LOG_VERBOSITY_VERBOSE &&
+				cstate->opts.on_error == COPY_ON_ERROR_IGNORE)
 			{
 				/*
 				 * Since we emit line number and column info in the below
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index dc0c2886674..50855d781c6 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -3625,6 +3625,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 541176e1980..80f12af600a 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -35,6 +35,7 @@ typedef enum CopyOnErrorChoice
 {
 	COPY_ON_ERROR_STOP = 0,		/* immediately throw errors, default */
 	COPY_ON_ERROR_IGNORE,		/* ignore errors */
+	COPY_ON_ERROR_TABLE,		/* saving errors info to table */
 } CopyOnErrorChoice;
 
 /*
@@ -84,6 +85,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		*on_error_tbl; 	/* 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 c8b22af22d8..aa4f984d1fa 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; 	/* 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? */
diff --git a/src/test/regress/expected/copy2.out b/src/test/regress/expected/copy2.out
index f3fdce23459..97160a4d02b 100644
--- a/src/test/regress/expected/copy2.out
+++ b/src/test/regress/expected/copy2.out
@@ -820,6 +820,91 @@ 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
+CREATE TABLE err_tbl OF copy_error_saving;
+--cannot use for error saving.
+CREATE TEMP TABLE err_tbl_1 AS SELECT * FROM err_tbl;
+CREATE TEMP TABLE t_copy_tbl(a int, b int, c int);
+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 t_copy_tbl;
+----invalid options, the below all should  fails
+COPY t_copy_tbl FROM STDIN WITH (ON_ERROR TABLE, TABLE err_tbl, LOG_VERBOSITY VERBOSE); --error
+ERROR:  cannot set option LOG_VERBOSITY to "VERBOSE" when ON_ERROR is specified as "TABLE"
+COPY t_copy_tbl FROM STDIN WITH (ON_ERROR TABLE, TABLE t_copy_tbl); --error
+ERROR:  cannot use relation "t_copy_tbl" for COPY error saving while copying data to it
+COPY t_copy_tbl FROM STDIN WITH (ON_ERROR TABLE);
+ERROR:  cannot set option ON_ERROR to "TABLE" when "TABLE" is not specified
+HINT:  You may need also specify "TABLE" option.
+COPY t_copy_tbl FROM STDIN WITH (TABLE err_tbl);
+ERROR:  COPY TABLE can only be used when option ON_ERROR is set to "TABLE"
+COPY t_copy_tbl TO STDIN WITH (ON_ERROR TABLE);
+ERROR:  COPY ON_ERROR cannot be used with COPY TO
+LINE 1: COPY t_copy_tbl TO STDIN WITH (ON_ERROR TABLE);
+                                       ^
+COPY t_copy_tbl(a,b) FROM STDIN WITH (ON_ERROR TABLE, reject_limit 10, TABLE err_tbl);
+ERROR:  cannot set option REJECT_LIMIT when ON_ERROR is specified as "TABLE"
+COPY t_copy_tbl(a,b) FROM STDIN WITH (ON_ERROR TABLE, TABLE not_exists);
+ERROR:  relation "not_exists" does not exist
+COPY t_copy_tbl(a) FROM STDIN WITH (ON_ERROR TABLE, TABLE t_copy_v1);
+ERROR:  cannot use relation "t_copy_v1" for COPY error saving
+DETAIL:  This operation is not supported for views.
+COPY t_copy_tbl(a) FROM STDIN WITH (ON_ERROR TABLE, 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 "t_copy_typ"
+HINT:  The COPY error saving table must be a typed table base on type "copy_error_saving".
+COPY t_copy_tbl(a,b) FROM STDIN WITH (ON_ERROR TABLE, TABLE err_tbl_1);
+ERROR:  cannot use relation "err_tbl_1" for COPY error saving
+HINT:  The COPY error saving table must be a typed table base on type "copy_error_saving".
+----invalid options, the above all should fails
+--should fail, copied data have extra columns
+COPY t_copy_tbl(a,b) FROM STDIN WITH (DELIMITER ',', ON_ERROR TABLE, TABLE err_tbl);
+ERROR:  extra data after last expected column
+CONTEXT:  COPY t_copy_tbl, line 1: "1,2,3,4"
+--should fail, copied data have less columns
+COPY t_copy_tbl(a,b) FROM STDIN WITH (DELIMITER ',', ON_ERROR TABLE, TABLE err_tbl);
+ERROR:  extra data after last expected column
+CONTEXT:  COPY t_copy_tbl, line 1: "1,2,"
+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 t_copy_tbl TO regress_user30;
+GRANT SELECT ON TABLE err_tbl TO regress_user30;
+SAVEPOINT s1;
+SET ROLE regress_user30;
+COPY t_copy_tbl FROM STDIN WITH (delimiter ',', ON_ERROR TABLE, TABLE err_tbl); --should fail
+ERROR:  permission denied to use table "err_tbl" for COPY FROM error saving
+HINT:  Ensure the current user has INSERT privilege on table "err_tbl" to use it for COPY FROM error saving.
+ROLLBACK TO SAVEPOINT s1;
+RESET ROLE;
+GRANT INSERT on TABLE err_tbl to regress_user30;
+SET ROLE regress_user30;
+COPY t_copy_tbl FROM STDIN WITH (delimiter ',', ON_ERROR TABLE, 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 
+------------+----------+--------+------+---------+-----------------+--------------------------------------------+------------+-----------
+ t_copy_tbl | STDIN    |      1 | a,b  | a       | a               | invalid input syntax for type integer: "a" |            | 22P02
+(1 row)
+
+ROLLBACK;
+--ok cases
+COPY t_copy_tbl FROM STDIN WITH (DELIMITER ',', ON_ERROR TABLE, TABLE err_tbl);
+NOTICE:  4 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 
+------------+----------+--------+--------------------------+---------+---------------------+--------------------------------------------------------------+------------+-----------
+ t_copy_tbl | STDIN    |      1 | 1,2,a                    | c       | a                   | invalid input syntax for type integer: "a"                   |            | 22P02
+ t_copy_tbl | STDIN    |      3 | 1,_junk,test             | b       | _junk               | invalid input syntax for type integer: "_junk"               |            | 22P02
+ t_copy_tbl | STDIN    |      4 | cola,colb,colc           | a       | cola                | invalid input syntax for type integer: "cola"                |            | 22P02
+ t_copy_tbl | STDIN    |      6 | 1,11,4238679732489879879 | c       | 4238679732489879879 | value "4238679732489879879" is out of range for type integer |            | 22003
+(4 rows)
+
 -- clean up
 DROP TABLE forcetest;
 DROP TABLE vistest;
@@ -838,6 +923,12 @@ DROP TABLE check_ign_err;
 DROP TABLE check_ign_err2;
 DROP DOMAIN dcheck_ign_err2;
 DROP TABLE hard_err;
+DROP TABLE err_tbl;
+DROP TABLE err_tbl_1;
+DROP VIEW t_copy_v1;
+DROP TABLE t_copy_tbl;
+DROP TABLE t_copy_tbl1;
+DROP TYPE t_copy_typ;
 --
 -- COPY FROM ... DEFAULT
 --
diff --git a/src/test/regress/sql/copy2.sql b/src/test/regress/sql/copy2.sql
index cef45868db5..2e384cc544c 100644
--- a/src/test/regress/sql/copy2.sql
+++ b/src/test/regress/sql/copy2.sql
@@ -591,6 +591,77 @@ a	{7}	7
 10	{10}	10
 \.
 
+CREATE TABLE err_tbl OF copy_error_saving;
+--cannot use for error saving.
+CREATE TEMP TABLE err_tbl_1 AS SELECT * FROM err_tbl;
+CREATE TEMP TABLE t_copy_tbl(a int, b int, c int);
+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 t_copy_tbl;
+
+----invalid options, the below all should  fails
+COPY t_copy_tbl FROM STDIN WITH (ON_ERROR TABLE, TABLE err_tbl, LOG_VERBOSITY VERBOSE); --error
+COPY t_copy_tbl FROM STDIN WITH (ON_ERROR TABLE, TABLE t_copy_tbl); --error
+COPY t_copy_tbl FROM STDIN WITH (ON_ERROR TABLE);
+COPY t_copy_tbl FROM STDIN WITH (TABLE err_tbl);
+COPY t_copy_tbl TO STDIN WITH (ON_ERROR TABLE);
+COPY t_copy_tbl(a,b) FROM STDIN WITH (ON_ERROR TABLE, reject_limit 10, TABLE err_tbl);
+COPY t_copy_tbl(a,b) FROM STDIN WITH (ON_ERROR TABLE, TABLE not_exists);
+COPY t_copy_tbl(a) FROM STDIN WITH (ON_ERROR TABLE, TABLE t_copy_v1);
+COPY t_copy_tbl(a) FROM STDIN WITH (ON_ERROR TABLE, TABLE t_copy_tbl1);
+COPY t_copy_tbl(a,b) FROM STDIN WITH (ON_ERROR TABLE, TABLE err_tbl_1);
+----invalid options, the above all should fails
+
+--should fail, copied data have extra columns
+COPY t_copy_tbl(a,b) FROM STDIN WITH (DELIMITER ',', ON_ERROR TABLE, TABLE err_tbl);
+1,2,3,4
+\.
+
+--should fail, copied data have less columns
+COPY t_copy_tbl(a,b) FROM STDIN WITH (DELIMITER ',', ON_ERROR TABLE, TABLE err_tbl);
+1,2,
+\.
+
+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 t_copy_tbl TO regress_user30;
+GRANT SELECT ON TABLE err_tbl TO regress_user30;
+SAVEPOINT s1;
+
+SET ROLE regress_user30;
+COPY t_copy_tbl FROM STDIN WITH (delimiter ',', ON_ERROR TABLE, TABLE err_tbl); --should fail
+ROLLBACK TO SAVEPOINT s1;
+
+RESET ROLE;
+GRANT INSERT on TABLE err_tbl to regress_user30;
+SET ROLE regress_user30;
+COPY t_copy_tbl FROM STDIN WITH (delimiter ',', ON_ERROR TABLE, TABLE err_tbl); --ok
+a,b
+\.
+
+SELECT  copy_tbl::regclass, filename, lineno, line, colname, raw_field_value,
+        err_message, err_detail, errorcode
+FROM    err_tbl;
+ROLLBACK;
+
+--ok cases
+COPY t_copy_tbl FROM STDIN WITH (DELIMITER ',', ON_ERROR TABLE, TABLE err_tbl);
+1,2,a
+1,2,3
+1,_junk,test
+cola,colb,colc
+4,5,6
+1,11,4238679732489879879
+\.
+SELECT  copy_tbl::regclass, filename, lineno, line, colname, raw_field_value,
+        err_message, err_detail, errorcode
+FROM    err_tbl;
+
+
 -- clean up
 DROP TABLE forcetest;
 DROP TABLE vistest;
@@ -609,6 +680,12 @@ DROP TABLE check_ign_err;
 DROP TABLE check_ign_err2;
 DROP DOMAIN dcheck_ign_err2;
 DROP TABLE hard_err;
+DROP TABLE err_tbl;
+DROP TABLE err_tbl_1;
+DROP VIEW t_copy_v1;
+DROP TABLE t_copy_tbl;
+DROP TABLE t_copy_tbl1;
+DROP TYPE t_copy_typ;
 
 --
 -- COPY FROM ... DEFAULT
-- 
2.34.1

Reply via email to