v1-0001-COPY-ON_CONFLICT-TABLE.patch
text/x-patch
Filename: v1-0001-COPY-ON_CONFLICT-TABLE.patch
Type: text/x-patch
Part: 0
Patch
Same data as JSON:
GET /api/v1/attachments/:id/patch
the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes.
API reference →
Format: format-patch
Series: patch v1-0001
Subject: COPY ON_CONFLICT TABLE
| File | + | − |
|---|---|---|
| doc/src/sgml/monitoring.sgml | 3 | 3 |
| doc/src/sgml/ref/copy.sgml | 90 | 0 |
| src/backend/commands/copy.c | 50 | 0 |
| src/backend/commands/copyfrom.c | 511 | 9 |
| src/backend/commands/explain.c | 1 | 2 |
| src/backend/executor/nodeModifyTable.c | 1 | 1 |
| src/backend/parser/gram.y | 1 | 0 |
| src/include/commands/copyfrom_internal.h | 4 | 0 |
| src/include/commands/copy.h | 5 | 0 |
| src/include/executor/nodeModifyTable.h | 3 | 0 |
| src/include/nodes/nodes.h | 1 | 0 |
| src/test/regress/expected/copy2.out | 88 | 0 |
| src/test/regress/expected/copy.out | 8 | 0 |
| src/test/regress/sql/copy2.sql | 86 | 0 |
| src/test/regress/sql/copy.sql | 11 | 0 |
From 590c839d9d3f73f6bdaea7746e4bb4730d756590 Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Sat, 25 Apr 2026 12:11:20 +0800
Subject: [PATCH v1 1/1] COPY ON_CONFLICT TABLE
not sure how to deal with excludsion constraint
reference: https://web.archive.org/web/20240328094030/https://riggs.business/blog/f/postgresql-todo-2023
discussion: https://postgr.es/m/
commitfest entry: https://commitfest.postgresql.org/patch/
---
doc/src/sgml/monitoring.sgml | 6 +-
doc/src/sgml/ref/copy.sgml | 90 ++++
src/backend/commands/copy.c | 50 +++
src/backend/commands/copyfrom.c | 520 ++++++++++++++++++++++-
src/backend/commands/explain.c | 3 +-
src/backend/executor/nodeModifyTable.c | 2 +-
src/backend/parser/gram.y | 1 +
src/include/commands/copy.h | 5 +
src/include/commands/copyfrom_internal.h | 4 +
src/include/executor/nodeModifyTable.h | 3 +
src/include/nodes/nodes.h | 1 +
src/test/regress/expected/copy.out | 8 +
src/test/regress/expected/copy2.out | 88 ++++
src/test/regress/sql/copy.sql | 11 +
src/test/regress/sql/copy2.sql | 86 ++++
15 files changed, 863 insertions(+), 15 deletions(-)
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 08d5b824552..0860da3d23b 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -6745,9 +6745,9 @@ FROM pg_stat_get_backend_idset() AS backendid;
</para>
<para>
Number of tuples skipped because they contain malformed data.
- This counter only advances when
- <literal>ignore</literal> is specified to the <literal>ON_ERROR</literal>
- option.
+ This counter advances when
+ <literal>ignore</literal> is specified to the <literal>ON_ERROR</literal> option
+ or <literal>table</literal> is specified to the <literal>ON_CONFLICT</literal> option.
</para></entry>
</row>
</tbody>
diff --git a/doc/src/sgml/ref/copy.sgml b/doc/src/sgml/ref/copy.sgml
index 4706c9a4410..7410248c0b4 100644
--- a/doc/src/sgml/ref/copy.sgml
+++ b/doc/src/sgml/ref/copy.sgml
@@ -44,6 +44,8 @@ COPY { <replaceable class="parameter">table_name</replaceable> [ ( <replaceable
FORCE_QUOTE { ( <replaceable class="parameter">column_name</replaceable> [, ...] ) | * }
FORCE_NOT_NULL { ( <replaceable class="parameter">column_name</replaceable> [, ...] ) | * }
FORCE_NULL { ( <replaceable class="parameter">column_name</replaceable> [, ...] ) | * }
+ ON_CONFLICT <replaceable class="parameter">conflict_action</replaceable>
+ CONFLICT_TABLE <replaceable class="parameter">conflict_table</replaceable>
ON_ERROR <replaceable class="parameter">error_action</replaceable>
REJECT_LIMIT <replaceable class="parameter">maxerror</replaceable>
ENCODING '<replaceable class="parameter">encoding_name</replaceable>'
@@ -440,6 +442,92 @@ COPY (SELECT j FROM (VALUES ('null'::json), (NULL::json)) v(j))
</listitem>
</varlistentry>
+ <varlistentry id="sql-copy-params-on-conflict">
+ <term><literal>ON_CONFLICT</literal></term>
+ <listitem>
+ <para>
+ Specifies the behavior when a row violates a unique constraint.
+ An <replaceable class="parameter">conflict_action</replaceable> value of
+ <literal>stop</literal> means fail the command, while
+ <literal>table</literal> means save the conflicting input row to table
+ <replaceable class="parameter">conflict_table</replaceable>
+ specified by <literal>CONFLICT_TABLE</literal> and continue with the next one.
+ The default is <literal>stop</literal>.
+ </para>
+ <para>
+ The <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>
+ <para>
+ If <literal>ON_CONFLICT</literal> is set to <literal>table</literal>, a
+ <literal>NOTICE</literal> message is emitted at the end of the command
+ reporting the number of rows that were inserted to table <replaceable class="parameter">conflict_table</replaceable>
+ due to unique constraint violation, provided that at least one row was affected.
+ </para>
+ <para>
+ When the <literal>LOG_VERBOSITY</literal> option is set to
+ <literal>verbose</literal>, a <literal>NOTICE</literal> message is emitted
+ for each row insert by <literal>ON_CONFLICT</literal>, containing the
+ input line that violated the unique constraint. When set to
+ <literal>silent</literal>, no messages are emitted regarding discarded rows.
+ </para>
+ <para>
+ This uses the same mechanism as <link linkend="sql-on-conflict"><command>INSERT ... ON CONFLICT</command></link>.
+ However, exclusion constraints are not supported; only <literal>NOT DEFERRABLE</literal>
+ unique constraints are checked for violations.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry id="sql-copy-params-conflict-table">
+ <term><literal>CONFLICT_TABLE</literal></term>
+ <listitem>
+ <para>
+ Specifies a destination table (<replaceable class="parameter">conflict_table</replaceable>)
+ to store details regarding unique constraint violations encountered during
+ the <command>COPY FROM</command> operation. The target table must define
+ exactly four columns, though the specific column names are not restricted.
+ The required column order and data types are:
+
+ <informaltable>
+ <tgroup cols="2">
+ <thead>
+ <row>
+ <entry>Data Type</entry>
+ <entry>Description</entry>
+ </row>
+ </thead>
+ <tbody>
+ <row>
+ <entry><type>oid</type></entry>
+ <entry>
+ The OID of the destination table for the <command>COPY FROM</command> command.
+ This corresponds to <link linkend="catalog-pg-class"><structname>pg_class</structname></link>.<structfield>oid</structfield>.
+ Note that no formal dependency is maintained; if the referenced table is dropped, this value will persist as a stale reference.
+ </entry>
+ </row>
+ <row>
+ <entry><type>text</type></entry>
+ <entry>The file path of the <command>COPY FROM</command> input.</entry>
+ </row>
+ <row>
+ <entry><type>bigint</type></entry>
+ <entry>The line number within the input source where the unique constraint violation occurred (starting at 1).</entry>
+ </row>
+ <row>
+ <entry><type>text</type></entry>
+ <entry>The raw line text content of the record that caused the violation.</entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </informaltable>
+
+ </para>
+ </listitem>
+ </varlistentry>
+
+
<varlistentry id="sql-copy-params-on-error">
<term><literal>ON_ERROR</literal></term>
<listitem>
@@ -493,6 +581,8 @@ COPY (SELECT j FROM (VALUES ('null'::json), (NULL::json)) v(j))
If not specified, <literal>ON_ERROR</literal>=<literal>ignore</literal>
allows an unlimited number of errors, meaning <command>COPY</command> will
skip all erroneous data.
+ Note: Rows ignored due to unique constraint violations via the
+ <literal>ON_CONFLICT</literal> option do not count toward this limit.
</para>
</listitem>
</varlistentry>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 003b70852bb..6101ebd500f 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -561,6 +561,29 @@ defGetCopyLogVerbosityChoice(DefElem *def, ParseState *pstate)
return COPY_LOG_VERBOSITY_DEFAULT; /* keep compiler quiet */
}
+/*
+ * Extract a OnConflictAction value from a DefElem.
+ */
+static OnConflictAction
+defGetdefGetCopyOnConflictChoice(DefElem *def, ParseState *pstate)
+{
+ char *sval;
+
+ sval = defGetString(def);
+ if (pg_strcasecmp(sval, "stop") == 0)
+ return ONCONFLICT_NONE;
+ else if (pg_strcasecmp(sval, "table") == 0)
+ return ONCONFLICT_TABLE;
+
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ /*- translator: first %s is the name of a COPY option, e.g. ON_ERROR */
+ errmsg("COPY %s \"%s\" not recognized", "ON_CONFLICT", sval),
+ parser_errposition(pstate, def->location));
+
+ return ONCONFLICT_NONE; /* keep compiler quiet */
+}
+
/*
* Process the statement option list for COPY.
*
@@ -587,9 +610,11 @@ ProcessCopyOptions(ParseState *pstate,
bool freeze_specified = false;
bool header_specified = false;
bool on_error_specified = false;
+ bool conflict_tbl_specified = false;
bool log_verbosity_specified = false;
bool reject_limit_specified = false;
bool force_array_specified = false;
+ bool on_conflict_specified = false;
ListCell *option;
/* Support external use for option sanity checking */
@@ -774,6 +799,21 @@ ProcessCopyOptions(ParseState *pstate,
reject_limit_specified = true;
opts_out->reject_limit = defGetCopyRejectLimitOption(defel);
}
+ else if (strcmp(defel->defname, "on_conflict") == 0)
+ {
+ if (on_conflict_specified)
+ errorConflictingDefElem(defel, pstate);
+ on_conflict_specified = true;
+ opts_out->on_conflict = defGetdefGetCopyOnConflictChoice(defel, pstate);
+ }
+ else if (strcmp(defel->defname, "conflict_table") == 0)
+ {
+ if (conflict_tbl_specified)
+ errorConflictingDefElem(defel, pstate);
+ conflict_tbl_specified = true;
+
+ opts_out->on_conflict_tbl = defGetString(defel);
+ }
else
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
@@ -782,6 +822,16 @@ ProcessCopyOptions(ParseState *pstate,
parser_errposition(pstate, defel->location)));
}
+ if (!(opts_out->on_conflict == ONCONFLICT_TABLE) && conflict_tbl_specified)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("COPY %s requires %s option", "CONFLICT_TABLE", "ON_CONFLICT"));
+
+ if ((opts_out->on_conflict == ONCONFLICT_TABLE) && !conflict_tbl_specified)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("COPY %s requires %s option", "CONFLICT_TABLE", "ON_CONFLICT"));
+
/*
* 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..8017f0b236e 100644
--- a/src/backend/commands/copyfrom.c
+++ b/src/backend/commands/copyfrom.c
@@ -42,16 +42,23 @@
#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 "storage/lmgr.h"
#include "tcop/tcopprot.h"
+#include "utils/acl.h"
+#include "utils/builtins.h"
+#include "utils/injection_point.h"
#include "utils/lsyscache.h"
#include "utils/memutils.h"
#include "utils/portal.h"
+#include "utils/regproc.h"
#include "utils/rel.h"
#include "utils/snapmgr.h"
#include "utils/typcache.h"
+#include "utils/syscache.h"
/*
* No more than this many tuples per CopyMultiInsertBuffer
@@ -120,6 +127,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 CopyFromConflictTableCheck(Relation relation);
+static void RangeVarCallbackForCopyConflictTable(const RangeVar *rv, Oid relid, Oid oldrelid,
+ void *arg);
+static void CopyFromConflictTableInit(CopyFromState cstate);
+static void CopyConflictTablePermissionCheck(ParseState *pstate, Relation rel);
/*
@@ -801,6 +813,7 @@ CopyFrom(CopyFromState cstate)
bool has_before_insert_row_trig;
bool has_instead_insert_row_trig;
bool leafpart_use_multi_insert = false;
+ TupleTableSlot *conflictslot = NULL;
Assert(cstate->rel);
Assert(list_length(cstate->range_table) == 1);
@@ -808,6 +821,13 @@ CopyFrom(CopyFromState cstate)
if (cstate->opts.on_error != COPY_ON_ERROR_STOP)
Assert(cstate->escontext);
+ if (cstate->opts.on_conflict == ONCONFLICT_TABLE)
+ {
+ conflictslot = ExecInitExtraTupleSlot(estate,
+ RelationGetDescr(cstate->conflictRel),
+ &TTSOpsVirtual);
+ }
+
/*
* The target must be a plain, foreign, or partitioned relation, or have
* an INSTEAD OF INSERT row trigger. (Currently, such triggers are only
@@ -923,7 +943,7 @@ CopyFrom(CopyFromState cstate)
/* Verify the named relation is a valid target for INSERT */
CheckValidResultRel(resultRelInfo, CMD_INSERT, ONCONFLICT_NONE, NIL);
- ExecOpenIndices(resultRelInfo, false);
+ ExecOpenIndices(resultRelInfo, true);
/*
* Set up a ModifyTableState so we can let FDW(s) init themselves for
@@ -1052,6 +1072,11 @@ CopyFrom(CopyFromState cstate)
*/
insertMethod = CIM_SINGLE;
}
+ else if (cstate->opts.on_conflict != ONCONFLICT_NONE &&
+ resultRelInfo->ri_NumIndices > 0)
+ {
+ insertMethod = CIM_SINGLE;
+ }
else
{
/*
@@ -1164,7 +1189,7 @@ CopyFrom(CopyFromState cstate)
/* Report that this tuple was skipped by the ON_ERROR clause */
pgstat_progress_update_param(PROGRESS_COPY_TUPLES_SKIPPED,
- cstate->num_errors);
+ (cstate->num_conflicts + cstate->num_errors));
if (cstate->opts.reject_limit > 0 &&
cstate->num_errors > cstate->opts.reject_limit)
@@ -1425,15 +1450,218 @@ CopyFrom(CopyFromState cstate)
}
else
{
- /* OK, store the tuple and create index entries for it */
- table_tuple_insert(resultRelInfo->ri_RelationDesc,
- myslot, mycid, ti_options, bistate);
+ if (cstate->opts.on_conflict == ONCONFLICT_NONE)
+ {
+ /*
+ * OK, store the tuple and create index entries
+ * for it
+ */
+ table_tuple_insert(resultRelInfo->ri_RelationDesc,
+ myslot, mycid, ti_options, bistate);
- if (resultRelInfo->ri_NumIndices > 0)
+ if (resultRelInfo->ri_NumIndices > 0)
+ recheckIndexes = ExecInsertIndexTuples(resultRelInfo,
+ estate, 0,
+ myslot, NIL,
+ NULL);
+ }
+ else if (resultRelInfo->ri_NumIndices > 0 &&
+ cstate->opts.on_conflict != ONCONFLICT_NONE)
+ {
+ /* Perform a speculative insertion. */
+ uint32 specToken;
+ ItemPointerData conflictTid;
+ ItemPointerData invalidItemPtr;
+ bool specConflict;
+ List *arbiterIndexes;
+
+ ItemPointerSetInvalid(&invalidItemPtr);
+ arbiterIndexes = resultRelInfo->ri_onConflictArbiterIndexes;
+
+ /*
+ * Do a non-conclusive check for conflicts first.
+ *
+ * We're not holding any locks yet, so this
+ * doesn't guarantee that the later insert won't
+ * conflict. But it avoids leaving behind a lot
+ * of canceled speculative insertions, if you run
+ * a lot of INSERT ON CONFLICT statements that do
+ * conflict.
+ *
+ * We loop back here if we find a conflict below,
+ * either during the pre-check, or when we
+ * re-check after inserting the tuple
+ * speculatively. Better allow interrupts in case
+ * some bug makes this an infinite loop.
+ */
+ vlock:
+ CHECK_FOR_INTERRUPTS();
+ specConflict = false;
+ if (!ExecCheckIndexConstraints(resultRelInfo, myslot, estate,
+ &conflictTid, &invalidItemPtr,
+ arbiterIndexes))
+ {
+ /*
+ * This is equivalent to ON CONFLICT DO
+ * SELECT. We need verify that the tuple is
+ * visible to the executor's MVCC snapshot at
+ * higher isolation levels. See comments in
+ * ExecInsert->ExecCheckIndexConstraints also.
+ */
+ if (cstate->opts.on_conflict == ONCONFLICT_TABLE)
+ {
+ int j = 0;
+ Datum *newvalues;
+ bool *nulls;
+ ModifyTableState *mstate = cstate->conflict_mstate;
+ EState *conflict_estate = mstate->ps.state;
+ TupleDesc tupdesc = RelationGetDescr(cstate->conflictRel);
+
+ if (!table_tuple_fetch_row_version(resultRelInfo->ri_RelationDesc,
+ &conflictTid,
+ SnapshotAny,
+ ExecGetReturningSlot(conflict_estate, resultRelInfo)))
+ elog(ERROR, "failed to fetch conflicting tuple for ON CONFLICT");
+
+ ExecCheckTupleVisible(conflict_estate,
+ resultRelInfo->ri_RelationDesc,
+ ExecGetReturningSlot(conflict_estate, resultRelInfo));
+
+ ExecClearTuple(conflictslot);
+
+ newvalues = conflictslot->tts_values;
+ nulls = conflictslot->tts_isnull;
+
+ /* Prepare to build the result tuple */
+ for (int i = 0; i < tupdesc->natts; i++)
+ {
+ Form_pg_attribute att = TupleDescAttr(tupdesc, i);
+
+ if (att->attisdropped)
+ {
+ newvalues[i] = (Datum) 0;
+ nulls[i] = true;
+ continue;
+ }
+
+ j++;
+ nulls[i] = false;
+ switch (j)
+ {
+ case 1:
+ newvalues[i] = ObjectIdGetDatum(RelationGetRelid(cstate->rel));
+ break;
+
+ case 2:
+ newvalues[i] = CStringGetTextDatum(cstate->filename ? cstate->filename : "STDIN");
+ break;
+
+ case 3:
+ newvalues[i] = Int64GetDatum((int64) cstate->cur_lineno);
+ break;
+
+ case 4:
+ newvalues[i] = CStringGetTextDatum(pnstrdup(cstate->line_buf.data,
+ cstate->line_buf.len));
+ break;
+
+ default:
+ elog(ERROR, "COPY ON CONFLICT table must have 4 attributes");
+ break;
+ }
+ }
+
+ /* Build the virtual tuple. */
+ ExecStoreVirtualTuple(conflictslot);
+
+ /*
+ * Check constraint and not-null
+ * constraint vertification
+ */
+ if (tupdesc->constr)
+ ExecConstraints(mstate->resultRelInfo, conflictslot, conflict_estate);
+
+ /* insert the tuple normally */
+ table_tuple_insert(cstate->conflictRel, conflictslot,
+ conflict_estate->es_output_cid,
+ 0, NULL);
+
+ /* insert index entries for tuple */
+ if (mstate->resultRelInfo->ri_NumIndices > 0)
+ recheckIndexes = ExecInsertIndexTuples(mstate->resultRelInfo,
+ conflict_estate,
+ 0, conflictslot, NIL,
+ NULL);
+ list_free(recheckIndexes);
+
+ cstate->num_conflicts++;
+
+ /*
+ * Report that this tuple was skipped by
+ * the ON_ERROR or ON_CONFLICT clause
+ */
+ pgstat_progress_update_param(PROGRESS_COPY_TUPLES_SKIPPED,
+ (cstate->num_conflicts + cstate->num_errors));
+
+ continue;
+ }
+ }
+
+ /*
+ * Before we start insertion proper, acquire our
+ * "speculative insertion lock". Others can use
+ * that to wait for us to decide if we're going to
+ * go ahead with the insertion, instead of waiting
+ * for the whole transaction to complete.
+ */
+ INJECTION_POINT("exec-copy-insert-before-insert-speculative", NULL);
+ specToken = SpeculativeInsertionLockAcquire(GetCurrentTransactionId());
+
+ /* insert the tuple, with the speculative token */
+ table_tuple_insert_speculative(resultRelInfo->ri_RelationDesc, myslot,
+ estate->es_output_cid,
+ 0,
+ NULL,
+ specToken);
+
+ /* insert index entries for tuple */
recheckIndexes = ExecInsertIndexTuples(resultRelInfo,
- estate, 0,
- myslot, NIL,
- NULL);
+ estate, EIIT_NO_DUPE_ERROR,
+ myslot, arbiterIndexes,
+ &specConflict);
+
+ /* adjust the tuple's state accordingly */
+ table_tuple_complete_speculative(resultRelInfo->ri_RelationDesc, myslot,
+ specToken, !specConflict);
+
+ /*
+ * Wake up anyone waiting for our decision. They
+ * will re-check the tuple, see that it's no
+ * longer speculative, and wait on our XID as if
+ * this was a regularly inserted tuple all along.
+ * Or if we killed the tuple, they will see it's
+ * dead, and proceed as if the tuple never
+ * existed.
+ */
+ SpeculativeInsertionLockRelease(GetCurrentTransactionId());
+
+ /*
+ * If there was a conflict, start from the
+ * beginning. We'll do the pre-check again, which
+ * will now find the conflicting tuple (unless it
+ * aborts before we get there).
+ */
+ if (specConflict)
+ {
+ list_free(recheckIndexes);
+ goto vlock;
+ }
+
+ /*
+ * Since there was no insertion conflict, we're
+ * done
+ */
+ }
}
/* AFTER ROW INSERT Triggers */
@@ -1482,6 +1710,18 @@ CopyFrom(CopyFromState cstate)
cstate->num_errors));
}
+ if (cstate->num_conflicts > 0 &&
+ cstate->opts.log_verbosity >= COPY_LOG_VERBOSITY_DEFAULT)
+ {
+ if (cstate->opts.on_conflict == ONCONFLICT_TABLE)
+ ereport(NOTICE,
+ errmsg_plural("%" PRIu64 " row was saved to conflict table \"%s\" due to unique constraint violation",
+ "%" PRIu64 " rows were saved to conflict table \"%s\" due to unique constraint violation",
+ cstate->num_conflicts,
+ cstate->num_conflicts,
+ RelationGetRelationName(cstate->conflictRel)));
+ }
+
if (bistate != NULL)
FreeBulkInsertState(bistate);
@@ -1515,6 +1755,17 @@ CopyFrom(CopyFromState cstate)
FreeExecutorState(estate);
+ /* Close/release resouces associated with copy error saving */
+ if (cstate->conflictRel)
+ {
+ ExecResetTupleTable(cstate->conflict_mstate->ps.state->es_tupleTable, false);
+
+ ExecCloseResultRelations(cstate->conflict_mstate->ps.state);
+ ExecCloseRangeTableRelations(cstate->conflict_mstate->ps.state);
+
+ FreeExecutorState(cstate->conflict_mstate->ps.state);
+ }
+
return processed;
}
@@ -1634,6 +1885,44 @@ BeginCopyFrom(ParseState *pstate,
else
cstate->escontext = NULL;
+ cstate->conflict_mstate = NULL;
+ if (cstate->opts.on_conflict == ONCONFLICT_TABLE)
+ {
+ Oid conflictRelid;
+ RangeVar *relvar;
+ List *relname_list;
+
+ Assert(cstate->opts.on_conflict_tbl != NULL);
+
+ relname_list = stringToQualifiedNameList(cstate->opts.on_conflict_tbl, NULL);
+ relvar = makeRangeVarFromNameList(relname_list);
+
+ /*
+ * We might insert tuples into the conflict 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 conflict
+ * error-saving table is under heavy lock.
+ */
+ conflictRelid = RangeVarGetRelidExtended(relvar,
+ RowExclusiveLock,
+ RVR_NOWAIT,
+ RangeVarCallbackForCopyConflictTable,
+ NULL);
+
+ if (RelationGetRelid(cstate->rel) == conflictRelid)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot use relation \"%s\" for COPY on_conflict error saving while copying data to it",
+ cstate->opts.on_conflict_tbl));
+
+ cstate->conflictRel = table_open(conflictRelid, NoLock);
+
+ CopyFromConflictTableInit(cstate);
+
+ table_close(cstate->conflictRel, NoLock);
+ }
+
if (cstate->opts.on_error == COPY_ON_ERROR_SET_NULL)
{
int attr_count = list_length(cstate->attnumlist);
@@ -1998,3 +2287,216 @@ ClosePipeFromProgram(CopyFromState cstate)
errdetail_internal("%s", wait_result_to_str(pclose_rc))));
}
}
+
+/*
+ * The conflict_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 extensive additional checks and
+ * setup for each inserted error row.
+ */
+static void
+CopyFromConflictTableCheck(Relation relation)
+{
+ int valid_col_count = 0;
+ 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 conflict_table cannot have generated columns.");
+ }
+
+ if (!errdetail_msg)
+ {
+ if (list_length(RelationGetFKeyList(relation)) > 0)
+ errdetail_msg = _("The conflict_table cannot have foreign keys.");
+ else if (relation->rd_rules)
+ errdetail_msg = _("The conflict_table cannot have rules.");
+ else if (relation->trigdesc)
+ errdetail_msg = _("The conflict_table cannot have triggers.");
+ else if (relation->rd_rel->relrowsecurity)
+ errdetail_msg = _("The conflict_table cannot have row-level security policies.");
+ }
+
+ if (errdetail_msg)
+ ereport(ERROR,
+ errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot use relation \"%s\" for COPY on_conflict error saving",
+ RelationGetRelationName(relation)),
+ errdetail_internal("%s", errdetail_msg));
+
+ for (int i = 0; i < tupDesc->natts; i++)
+ {
+ Form_pg_attribute attr = TupleDescAttr(tupDesc, i);
+
+ /* Skip columns marked as dropped */
+ if (attr->attisdropped)
+ continue;
+
+ valid_col_count++;
+
+ /* Check types based on the effective column position */
+ switch (valid_col_count)
+ {
+ case 1:
+ if (attr->atttypid != OIDOID)
+ errdetail_msg = _("The first column of the conflict_table must be type OID.");
+ break;
+ case 2:
+ if (attr->atttypid != TEXTOID)
+ errdetail_msg = _("The second column of the conflict_table must be type TEXT.");
+ break;
+ case 3:
+ if (attr->atttypid != INT8OID)
+ errdetail_msg = _("The third column of the conflict_table must be type BIGINT.");
+ break;
+ case 4:
+ if (attr->atttypid != TEXTOID)
+ errdetail_msg = _("The fourth column of the conflict_table must be type TEXT.");
+ break;
+ default:
+ errdetail_msg = _("The conflict_table must have exactly four columns.");
+ break;
+ }
+ }
+
+ if (valid_col_count != 4)
+ errdetail_msg = _("The conflict_table is incomplete; exactly four columns are required.");
+
+ if (errdetail_msg)
+ ereport(ERROR,
+ errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot use relation \"%s\" for COPY on_conflict error saving",
+ RelationGetRelationName(relation)),
+ errdetail_internal("%s", errdetail_msg));
+}
+
+static void
+CopyFromConflictTableInit(CopyFromState cstate)
+{
+ ParseState *pstate;
+ ResultRelInfo *resultRelInfo;
+ EState *estate = CreateExecutorState();
+ ModifyTableState *mtstate;
+ Relation relation;
+
+ relation = cstate->conflictRel;
+ pstate = make_parsestate(NULL);
+
+ CopyConflictTablePermissionCheck(pstate, relation);
+
+ CopyFromConflictTableCheck(relation);
+
+ /*
+ * 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 CONFLICT_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->conflict_mstate = mtstate;
+}
+
+static void
+CopyConflictTablePermissionCheck(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);
+}
+
+/*
+ * 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
+RangeVarCallbackForCopyConflictTable(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 conflict 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 on_conflict error saving",
+ rv->relname),
+ errdetail_relkind_not_supported(relkind));
+
+ ReleaseSysCache(tuple);
+}
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 112c17b0d64..acefcb20498 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -4857,9 +4857,8 @@ show_modifytable_info(ModifyTableState *mtstate, List *ancestors,
resolution = "NOTHING";
else if (node->onConflictAction == ONCONFLICT_UPDATE)
resolution = "UPDATE";
- else
+ else if (node->onConflictAction == ONCONFLICT_SELECT)
{
- Assert(node->onConflictAction == ONCONFLICT_SELECT);
switch (node->onConflictLockStrength)
{
case LCS_NONE:
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 4cb057ca4f9..4f7a3451bc2 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -380,7 +380,7 @@ ExecProcessReturning(ModifyTableContext *context,
* path) on the basis of another tuple that is not visible to MVCC snapshot.
* Check for the need to raise a serialization failure, and do so as necessary.
*/
-static void
+extern void
ExecCheckTupleVisible(EState *estate,
Relation rel,
TupleTableSlot *slot)
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index ff4e1388c55..2854f2a884f 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..3aeebf24b67 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;
/*
@@ -94,9 +95,13 @@ typedef struct CopyFormatOptions
bool *force_null_flags; /* per-column CSV FN flags */
bool convert_selectively; /* do selective binary conversion? */
CopyOnErrorChoice on_error; /* what to do when error happened */
+ OnConflictAction on_conflict; /* what to do when unique conflict
+ * happened */
CopyLogVerbosityChoice log_verbosity; /* verbosity of logged messages */
int64 reject_limit; /* maximum tolerable number of errors */
List *convert_select; /* list of column names (can be NIL) */
+ char *on_conflict_tbl; /* on error, save error info to the table,
+ * table name */
} CopyFormatOptions;
/* These are private in commands/copy[from|to].c */
diff --git a/src/include/commands/copyfrom_internal.h b/src/include/commands/copyfrom_internal.h
index 9d3e244ee55..ec8f6981fae 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 conflictRel; /* relation for copy from conflict 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? */
@@ -102,6 +103,8 @@ typedef struct CopyFromStateData
* execution */
uint64 num_errors; /* total number of rows which contained soft
* errors */
+ uint64 num_conflicts; /* total number of rows skipped due to unique
+ * constraint conflict */
int *defmap; /* array of default att numbers related to
* missing att */
ExprState **defexprs; /* array of default att expressions for all
@@ -189,6 +192,7 @@ 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 */
+ ModifyTableState *conflict_mstate;
} CopyFromStateData;
extern void ReceiveCopyBegin(CopyFromState cstate);
diff --git a/src/include/executor/nodeModifyTable.h b/src/include/executor/nodeModifyTable.h
index f6070e1cdf3..6d6eba159a5 100644
--- a/src/include/executor/nodeModifyTable.h
+++ b/src/include/executor/nodeModifyTable.h
@@ -29,5 +29,8 @@ extern void ExecReScanModifyTable(ModifyTableState *node);
extern void ExecInitMergeTupleSlots(ModifyTableState *mtstate,
ResultRelInfo *resultRelInfo);
+extern void ExecCheckTupleVisible(EState *estate,
+ Relation rel,
+ TupleTableSlot *slot);
#endif /* NODEMODIFYTABLE_H */
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index a2925ae4946..22a329cb810 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -429,6 +429,7 @@ typedef enum OnConflictAction
ONCONFLICT_NOTHING, /* ON CONFLICT ... DO NOTHING */
ONCONFLICT_UPDATE, /* ON CONFLICT ... DO UPDATE */
ONCONFLICT_SELECT, /* ON CONFLICT ... DO SELECT */
+ ONCONFLICT_TABLE, /* COPY (ON_CONFLICT TABLE) */
} OnConflictAction;
/*
diff --git a/src/test/regress/expected/copy.out b/src/test/regress/expected/copy.out
index 1714faab39c..36fe3c00765 100644
--- a/src/test/regress/expected/copy.out
+++ b/src/test/regress/expected/copy.out
@@ -430,6 +430,14 @@ copy tab_progress_reporting from :'filename'
where (salary < 2000);
INFO: progress: {"type": "FILE", "command": "COPY FROM", "relname": "tab_progress_reporting", "tuples_skipped": 0, "has_bytes_total": true, "tuples_excluded": 1, "tuples_processed": 2, "has_bytes_processed": true}
-- Generate COPY FROM report with PIPE, with some skipped tuples.
+create unique index tab_progress_reporting_idx1 on tab_progress_reporting(name);
+create temp table conflict_tbl(copy_tbl oid, filename text, lineno bigint, line text);
+copy tab_progress_reporting from stdin(on_conflict table, conflict_table 'conflict_tbl');
+NOTICE: 3 rows were saved to conflict table "conflict_tbl" due to unique constraint violation
+INFO: progress: {"type": "PIPE", "command": "COPY FROM", "relname": "tab_progress_reporting", "tuples_skipped": 3, "has_bytes_total": false, "tuples_excluded": 0, "tuples_processed": 0, "has_bytes_processed": true}
+drop index tab_progress_reporting_idx1;
+drop table conflict_tbl;
+-- Generate COPY FROM report with PIPE, with some skipped tuples.
copy tab_progress_reporting from stdin(on_error ignore);
NOTICE: 2 rows were skipped due to data type incompatibility
INFO: progress: {"type": "PIPE", "command": "COPY FROM", "relname": "tab_progress_reporting", "tuples_skipped": 2, "has_bytes_total": false, "tuples_excluded": 0, "tuples_processed": 1, "has_bytes_processed": true}
diff --git a/src/test/regress/expected/copy2.out b/src/test/regress/expected/copy2.out
index 7600e5239d2..7c45400c8dd 100644
--- a/src/test/regress/expected/copy2.out
+++ b/src/test/regress/expected/copy2.out
@@ -884,7 +884,95 @@ 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 DOMAIN d_text as TEXT;
+CREATE TABLE t_copy_tbl(a int, b int, c text);
+CREATE TABLE err_tbl0(copy_tbl oid primary key);
+CREATE TABLE err_tbl1(copy_tbl oid, filename text, lineno bigint, line text generated always as ('hh') stored);
+ALTER TABLE err_tbl1 ADD CONSTRAINT con1 FOREIGN KEY (copy_tbl) REFERENCES err_tbl0(copy_tbl);
+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;
+-- The conflict 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 triggers, rules, or row-level security policies.
+COPY t_copy_tbl FROM STDIN WITH (DELIMITER ',', ON_CONFLICT TABLE, CONFLICT_TABLE err_tbl1);
+ERROR: cannot use relation "err_tbl1" for COPY on_conflict error saving
+DETAIL: The conflict_table cannot have generated columns.
+ALTER TABLE err_tbl1 ALTER COLUMN line DROP EXPRESSION;
+COPY t_copy_tbl FROM STDIN WITH (DELIMITER ',', ON_CONFLICT TABLE, CONFLICT_TABLE err_tbl1);
+ERROR: cannot use relation "err_tbl1" for COPY on_conflict error saving
+DETAIL: The conflict_table cannot have foreign keys.
+ALTER TABLE err_tbl1 DROP CONSTRAINT con1;
+COPY t_copy_tbl FROM STDIN WITH (DELIMITER ',', ON_CONFLICT TABLE, CONFLICT_TABLE err_tbl1);
+ERROR: cannot use relation "err_tbl1" for COPY on_conflict error saving
+DETAIL: The conflict_table cannot have triggers.
+DROP TRIGGER IF EXISTS trg_x_after ON err_tbl1;
+COPY t_copy_tbl FROM STDIN WITH (DELIMITER ',', ON_CONFLICT TABLE, CONFLICT_TABLE err_tbl1);
+ERROR: cannot use relation "err_tbl1" for COPY on_conflict error saving
+DETAIL: The conflict_table cannot have row-level security policies.
+DROP POLICY IF EXISTS p1 ON err_tbl1;
+ALTER TABLE err_tbl1 DISABLE ROW LEVEL SECURITY;
+ALTER TABLE err_tbl1 ALTER COLUMN line SET DATA TYPE d_text;
+COPY t_copy_tbl FROM STDIN WITH (DELIMITER ',', ON_CONFLICT TABLE, CONFLICT_TABLE err_tbl1); -- error
+ERROR: cannot use relation "err_tbl1" for COPY on_conflict error saving
+DETAIL: The fourth column of the conflict_table must be type TEXT.
+ALTER TABLE err_tbl1 DROP COLUMN line;
+COPY t_copy_tbl FROM STDIN WITH (DELIMITER ',', ON_CONFLICT TABLE, CONFLICT_TABLE err_tbl1); -- error
+ERROR: cannot use relation "err_tbl1" for COPY on_conflict error saving
+DETAIL: The conflict_table is incomplete; exactly four columns are required.
+ALTER TABLE err_tbl1 ADD COLUMN line text, ADD column extra int;
+COPY t_copy_tbl FROM STDIN WITH (DELIMITER ',', ON_CONFLICT TABLE, CONFLICT_TABLE err_tbl1); -- error
+ERROR: cannot use relation "err_tbl1" for COPY on_conflict error saving
+DETAIL: The conflict_table is incomplete; exactly four columns are required.
+ALTER TABLE err_tbl1 DROP COLUMN extra;
+CREATE VIEW err_tbl4 AS SELECT * FROM err_tbl1;
+COPY t_copy_tbl FROM STDIN WITH (DELIMITER ',', ON_CONFLICT TABLE, CONFLICT_TABLE err_tbl4); -- error
+ERROR: cannot use relation "err_tbl4" for COPY on_conflict error saving
+DETAIL: This operation is not supported for views.
+COPY t_copy_tbl(c, b) FROM STDIN (DELIMITER ',', ON_CONFLICT TABLE, CONFLICT_TABLE err_tbl1, log_verbosity verbose); -- ok
+-- COPY ON_CONFLICT TABLE cannot apply to deferred unqiue constraint
+ALTER TABLE t_copy_tbl ADD CONSTRAINT t_copy_tbl_unq1 UNIQUE (a) DEFERRABLE INITIALLY DEFERRED;
+BEGIN;
+COPY t_copy_tbl FROM STDIN (DELIMITER ',', ON_CONFLICT TABLE, CONFLICT_TABLE err_tbl1);
+ERROR: ON CONFLICT does not support deferrable unique constraints/exclusion constraints as arbiters
+CONTEXT: COPY t_copy_tbl, line 1: "1,2,3"
+ROLLBACK;
+ALTER TABLE t_copy_tbl DROP CONSTRAINT t_copy_tbl_unq1;
+ALTER TABLE err_tbl1 ADD CONSTRAINT cc CHECK (lineno > 0);
+ALTER TABLE err_tbl1 ADD CONSTRAINT nn NOT NULL copy_tbl;
+CREATE UNIQUE INDEX ON t_copy_tbl (b) WHERE a = 1;
+CREATE UNIQUE INDEX ON t_copy_tbl ((b+1));
+CREATE UNIQUE INDEX ON t_copy_tbl (c);
+COPY t_copy_tbl(b,a,c) FROM STDIN (DELIMITER ',', ON_CONFLICT TABLE,CONFLICT_TABLE err_tbl1, log_verbosity verbose); -- ok
+NOTICE: 2 rows were saved to conflict table "err_tbl1" due to unique constraint violation
+COPY t_copy_tbl(b,a, c) FROM STDIN (DELIMITER ',', ON_CONFLICT TABLE, CONFLICT_TABLE err_tbl1, log_verbosity verbose);
+NOTICE: 3 rows were saved to conflict table "err_tbl1" due to unique constraint violation
+CREATE TABLE err_tbl6 (
+ id1 int4range,
+ valid_at int4range,
+ CONSTRAINT err_tbl6_uq UNIQUE (id1, valid_at WITHOUT OVERLAPS)
+);
+COPY err_tbl6 FROM STDIN (ON_CONFLICT TABLE, CONFLICT_TABLE err_tbl1); -- error
+ERROR: empty WITHOUT OVERLAPS value found in column "valid_at" in relation "err_tbl6"
+CONTEXT: COPY err_tbl6, line 1: "[11,12) empty"
+COPY err_tbl6 FROM STDIN (ON_CONFLICT TABLE, CONFLICT_TABLE err_tbl1);
+NOTICE: 1 row was saved to conflict table "err_tbl1" due to unique constraint violation
+SELECT copy_tbl::regclass, filename, lineno, line FROM err_tbl1;
+ copy_tbl | filename | lineno | line
+------------+----------+--------+----------------------------------------------------------------------------------
+ t_copy_tbl | STDIN | 1 | 2,1,aaa
+ t_copy_tbl | STDIN | 2 | 2,1,XXX
+ t_copy_tbl | STDIN | 2 | 6,11,aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ t_copy_tbl | STDIN | 4 | 12,1,xxxxxxxx
+ t_copy_tbl | STDIN | 5 | 13,1,xxxxxxxx
+ err_tbl6 | STDIN | 2 | [1,10) [1,12)
+(6 rows)
+
-- clean up
+DROP TABLE err_tbl0, err_tbl1 CASCADE;
+NOTICE: drop cascades to view err_tbl4
+DROP DOMAIN d_text;
DROP TABLE forcetest;
DROP TABLE vistest;
DROP FUNCTION truncate_in_subxact();
diff --git a/src/test/regress/sql/copy.sql b/src/test/regress/sql/copy.sql
index eaad290b257..045cb17666f 100644
--- a/src/test/regress/sql/copy.sql
+++ b/src/test/regress/sql/copy.sql
@@ -369,6 +369,17 @@ truncate tab_progress_reporting;
copy tab_progress_reporting from :'filename'
where (salary < 2000);
+-- Generate COPY FROM report with PIPE, with some skipped tuples.
+create unique index tab_progress_reporting_idx1 on tab_progress_reporting(name);
+create temp table conflict_tbl(copy_tbl oid, filename text, lineno bigint, line text);
+copy tab_progress_reporting from stdin(on_conflict table, conflict_table 'conflict_tbl');
+sharon 25 (115,12) 1000 sam
+bill 20 (111,10) 1000 sharon
+bill 20 (111,10) 1000 sharon
+\.
+drop index tab_progress_reporting_idx1;
+drop table conflict_tbl;
+
-- Generate COPY FROM report with PIPE, with some skipped tuples.
copy tab_progress_reporting from stdin(on_error ignore);
sharon x (15,12) x sam
diff --git a/src/test/regress/sql/copy2.sql b/src/test/regress/sql/copy2.sql
index e0810109473..c939c8602c2 100644
--- a/src/test/regress/sql/copy2.sql
+++ b/src/test/regress/sql/copy2.sql
@@ -636,7 +636,93 @@ a {7} 7
10 {10} 10
\.
+CREATE DOMAIN d_text as TEXT;
+CREATE TABLE t_copy_tbl(a int, b int, c text);
+CREATE TABLE err_tbl0(copy_tbl oid primary key);
+
+CREATE TABLE err_tbl1(copy_tbl oid, filename text, lineno bigint, line text generated always as ('hh') stored);
+ALTER TABLE err_tbl1 ADD CONSTRAINT con1 FOREIGN KEY (copy_tbl) REFERENCES err_tbl0(copy_tbl);
+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;
+
+-- The conflict 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 triggers, rules, or row-level security policies.
+COPY t_copy_tbl FROM STDIN WITH (DELIMITER ',', ON_CONFLICT TABLE, CONFLICT_TABLE err_tbl1);
+ALTER TABLE err_tbl1 ALTER COLUMN line DROP EXPRESSION;
+COPY t_copy_tbl FROM STDIN WITH (DELIMITER ',', ON_CONFLICT TABLE, CONFLICT_TABLE err_tbl1);
+ALTER TABLE err_tbl1 DROP CONSTRAINT con1;
+COPY t_copy_tbl FROM STDIN WITH (DELIMITER ',', ON_CONFLICT TABLE, CONFLICT_TABLE err_tbl1);
+DROP TRIGGER IF EXISTS trg_x_after ON err_tbl1;
+COPY t_copy_tbl FROM STDIN WITH (DELIMITER ',', ON_CONFLICT TABLE, CONFLICT_TABLE err_tbl1);
+DROP POLICY IF EXISTS p1 ON err_tbl1;
+ALTER TABLE err_tbl1 DISABLE ROW LEVEL SECURITY;
+
+ALTER TABLE err_tbl1 ALTER COLUMN line SET DATA TYPE d_text;
+COPY t_copy_tbl FROM STDIN WITH (DELIMITER ',', ON_CONFLICT TABLE, CONFLICT_TABLE err_tbl1); -- error
+ALTER TABLE err_tbl1 DROP COLUMN line;
+COPY t_copy_tbl FROM STDIN WITH (DELIMITER ',', ON_CONFLICT TABLE, CONFLICT_TABLE err_tbl1); -- error
+
+ALTER TABLE err_tbl1 ADD COLUMN line text, ADD column extra int;
+COPY t_copy_tbl FROM STDIN WITH (DELIMITER ',', ON_CONFLICT TABLE, CONFLICT_TABLE err_tbl1); -- error
+ALTER TABLE err_tbl1 DROP COLUMN extra;
+
+CREATE VIEW err_tbl4 AS SELECT * FROM err_tbl1;
+COPY t_copy_tbl FROM STDIN WITH (DELIMITER ',', ON_CONFLICT TABLE, CONFLICT_TABLE err_tbl4); -- error
+COPY t_copy_tbl(c, b) FROM STDIN (DELIMITER ',', ON_CONFLICT TABLE, CONFLICT_TABLE err_tbl1, log_verbosity verbose); -- ok
+3,2
+\.
+
+-- COPY ON_CONFLICT TABLE cannot apply to deferred unqiue constraint
+ALTER TABLE t_copy_tbl ADD CONSTRAINT t_copy_tbl_unq1 UNIQUE (a) DEFERRABLE INITIALLY DEFERRED;
+BEGIN;
+COPY t_copy_tbl FROM STDIN (DELIMITER ',', ON_CONFLICT TABLE, CONFLICT_TABLE err_tbl1);
+1,2,3
+\.
+ROLLBACK;
+ALTER TABLE t_copy_tbl DROP CONSTRAINT t_copy_tbl_unq1;
+
+ALTER TABLE err_tbl1 ADD CONSTRAINT cc CHECK (lineno > 0);
+ALTER TABLE err_tbl1 ADD CONSTRAINT nn NOT NULL copy_tbl;
+CREATE UNIQUE INDEX ON t_copy_tbl (b) WHERE a = 1;
+CREATE UNIQUE INDEX ON t_copy_tbl ((b+1));
+CREATE UNIQUE INDEX ON t_copy_tbl (c);
+
+COPY t_copy_tbl(b,a,c) FROM STDIN (DELIMITER ',', ON_CONFLICT TABLE,CONFLICT_TABLE err_tbl1, log_verbosity verbose); -- ok
+2,1,aaa
+2,1,XXX
+\.
+
+COPY t_copy_tbl(b,a, c) FROM STDIN (DELIMITER ',', ON_CONFLICT TABLE, CONFLICT_TABLE err_tbl1, log_verbosity verbose);
+4,17,aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+6,11,aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+11,1,xxxxxxxx
+12,1,xxxxxxxx
+13,1,xxxxxxxx
+\.
+
+CREATE TABLE err_tbl6 (
+ id1 int4range,
+ valid_at int4range,
+ CONSTRAINT err_tbl6_uq UNIQUE (id1, valid_at WITHOUT OVERLAPS)
+);
+
+COPY err_tbl6 FROM STDIN (ON_CONFLICT TABLE, CONFLICT_TABLE err_tbl1); -- error
+[11,12) empty
+\.
+
+COPY err_tbl6 FROM STDIN (ON_CONFLICT TABLE, CONFLICT_TABLE err_tbl1);
+[1,10) [1,2)
+[1,10) [1,12)
+\.
+
+SELECT copy_tbl::regclass, filename, lineno, line FROM err_tbl1;
+
-- clean up
+DROP TABLE err_tbl0, err_tbl1 CASCADE;
+DROP DOMAIN d_text;
DROP TABLE forcetest;
DROP TABLE vistest;
DROP FUNCTION truncate_in_subxact();
--
2.34.1