diff --git a/doc/src/sgml/ref/copy.sgml b/doc/src/sgml/ref/copy.sgml index 4706c9a4410..dd18bc1bc05 100644 --- a/doc/src/sgml/ref/copy.sgml +++ b/doc/src/sgml/ref/copy.sgml @@ -48,6 +48,7 @@ COPY { table_name [ ( maxerror ENCODING 'encoding_name' LOG_VERBOSITY verbosity + DO ON CONFLICT { DO NOTHING | DO UPDATE } @@ -479,6 +480,56 @@ COPY (SELECT j FROM (VALUES ('null'::json), (NULL::json)) v(j)) + + ON CONFLICT + + + Specifies how to behave when an input row conflicts with a unique or + exclusion constraint on the target table, instead of aborting the whole + COPY command. This clause is compatible with + COPY ON CONFLICT in Alibaba Cloud AnalyticDB for + PostgreSQL and is only applicable to COPY FROM. + + + DO ON CONFLICT DO NOTHING discards the conflicting + input row and continues with the next one. The number of skipped rows + is reported in a NOTICE message at the end of the + command. + + + DO ON CONFLICT DO UPDATE overwrites the conflicting + row with the input row (a full-row overwrite, matching the AnalyticDB + semantics where no SET targets are allowed). Note + that for a column subset COPY (column [, ...]) FROM ..., + columns not listed in the input are filled with their default values, + so a volatile default expression (e.g. nextval) will + produce a new value for each imported row and overwrite the stored one. + + + The number of updated rows is reported in a NOTICE + message at the end of the command. + + + The target table must have at least one unique or exclusion constraint + (primary key, unique index, or EXCLUDE constraint) + for the conflict check to be meaningful; otherwise the + COPY command fails at startup. For partitioned + tables, uniqueness is enforced per leaf partition. + + + Conflict detection is atomic: after an initial constraint check, rows + are inserted speculatively so that a concurrent conflicting insert is + detected by the index insert itself, and the conflicting row is then + handled (or the insertion retried) without a check-to-insert race. For + DO ON CONFLICT DO UPDATE, a row that is concurrently + modified is locked and the update is retried once the concurrent + transaction completes. Duplicate constrained values within a single + COPY raise the same error as + INSERT ... ON CONFLICT. + + + + REJECT_LIMIT diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c index cc226da0615..cecceaeae91 100644 --- a/src/backend/commands/copy.c +++ b/src/backend/commands/copy.c @@ -503,6 +503,35 @@ defGetCopyOnErrorChoice(DefElem *def, ParseState *pstate, bool is_from) return COPY_ON_ERROR_STOP; /* keep compiler quiet */ } +/* + * Extract a CopyOnConflictChoice value from a DefElem. + */ +static CopyOnConflictChoice +defGetCopyOnConflictChoice(DefElem *def, ParseState *pstate, bool is_from) +{ + char *sval = defGetString(def); + + if (!is_from) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + /*- translator: first %s is the name of a COPY option, e.g. ON_CONFLICT, + second %s is a COPY with direction, e.g. COPY TO */ + errmsg("COPY %s cannot be used with %s", "ON_CONFLICT", "COPY TO"), + parser_errposition(pstate, def->location))); + + if (pg_strcasecmp(sval, "nothing") == 0) + return COPY_ON_CONFLICT_NOTHING; + if (pg_strcasecmp(sval, "update") == 0) + return COPY_ON_CONFLICT_UPDATE; + + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + /*- translator: first %s is the name of a COPY option, e.g. ON_CONFLICT */ + errmsg("COPY %s \"%s\" not recognized", "ON_CONFLICT", sval), + parser_errposition(pstate, def->location))); + return COPY_ON_CONFLICT_NONE; /* keep compiler quiet */ +} + /* * Extract REJECT_LIMIT value from a DefElem. * @@ -587,6 +616,7 @@ ProcessCopyOptions(ParseState *pstate, bool freeze_specified = false; bool header_specified = false; bool on_error_specified = false; + bool on_conflict_specified = false; bool log_verbosity_specified = false; bool reject_limit_specified = false; bool force_array_specified = false; @@ -760,6 +790,13 @@ ProcessCopyOptions(ParseState *pstate, on_error_specified = true; opts_out->on_error = defGetCopyOnErrorChoice(defel, pstate, is_from); } + else if (strcmp(defel->defname, "on_conflict") == 0) + { + if (on_conflict_specified) + errorConflictingDefElem(defel, pstate); + on_conflict_specified = true; + opts_out->on_conflict = defGetCopyOnConflictChoice(defel, pstate, is_from); + } else if (strcmp(defel->defname, "log_verbosity") == 0) { if (log_verbosity_specified) diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c index 64ac3063c61..4de2da864ba 100644 --- a/src/backend/commands/copyfrom.c +++ b/src/backend/commands/copyfrom.c @@ -26,6 +26,7 @@ #include "access/heapam.h" #include "access/tableam.h" +#include "catalog/index.h" #include "access/tupconvert.h" #include "access/xact.h" #include "catalog/namespace.h" @@ -774,6 +775,156 @@ CopyMultiInsertInfoStore(CopyMultiInsertInfo *miinfo, ResultRelInfo *rri, miinfo->bufferedBytes += tuplen; } +/* + * CopyOnConflictUpdate -- subroutine for COPY ON CONFLICT DO UPDATE + * + * Lock the conflicting tuple (waiting for any concurrent transaction) and + * overwrite it with the input row via the ModifyTable update machinery + * (BEFORE/AFTER ROW UPDATE triggers, index maintenance, transition + * capture). Returns: + * 0 - the conflicting tuple was concurrently modified or deleted; the + * caller must re-run the conflict check from scratch + * 1 - the row was updated + * 2 - a BEFORE ROW UPDATE trigger suppressed the update (no row change) + */ +static int +CopyOnConflictUpdate(IvyModifyTableContext *context, + ResultRelInfo *resultRelInfo, + ItemPointer conflictTid, + TupleTableSlot *excludedSlot, + IvyUpdateContext *updateCxt) +{ + EState *estate = context->estate; + Relation relation = resultRelInfo->ri_RelationDesc; + TupleTableSlot *existing; + TM_FailureData tmfd; + LockTupleMode lockmode; + TM_Result test; + TM_Result result; + Datum xminDatum; + TransactionId xmin; + bool isnull; + + /* + * Slot for the existing (conflicting) tuple; used only by the lock + * machinery and by error paths. + */ + existing = table_slot_create(relation, &estate->es_tupleTable); + + /* Determine lock mode to use */ + lockmode = LockTupleExclusive; + + /* + * Lock the conflicting tuple. A row locking conflict here means our + * previous conclusion that the tuple is conclusively committed is not + * true anymore (TM_Updated/TM_Deleted) and the caller should retry. + */ + test = table_tuple_lock(relation, conflictTid, estate->es_snapshot, + existing, estate->es_output_cid, + lockmode, LockWaitBlock, 0, &tmfd); + switch (test) + { + case TM_Ok: + /* success! */ + break; + + case TM_Invisible: + + /* + * This can occur when a just inserted row is updated again + * in the same command, e.g. because multiple rows with the + * same conflicting key values are inserted in one COPY. + */ + xminDatum = slot_getsysattr(existing, + MinTransactionIdAttributeNumber, + &isnull); + Assert(!isnull); + xmin = DatumGetTransactionId(xminDatum); + + if (TransactionIdIsCurrentTransactionId(xmin)) + ereport(ERROR, + (errcode(ERRCODE_CARDINALITY_VIOLATION), + errmsg("ON CONFLICT DO UPDATE command cannot affect row a second time"), + errhint("Ensure that no rows proposed for insertion within the same command have duplicate constrained values."))); + /* This shouldn't happen */ + elog(ERROR, "attempted to lock invisible tuple"); + break; + + case TM_SelfModified: + + /* + * This state should never be reached. As a dirty snapshot is + * used to find conflicting tuples, speculative insertion + * wouldn't have seen this row to conflict with. + */ + elog(ERROR, "unexpected self-updated tuple"); + break; + + case TM_Updated: + if (IsolationUsesXactSnapshot()) + ereport(ERROR, + (errcode(ERRCODE_T_R_SERIALIZATION_FAILURE), + errmsg("could not serialize access due to concurrent update"))); + + /* Tell caller to try again from the very start. */ + ExecClearTuple(existing); + return 0; + + case TM_Deleted: + if (IsolationUsesXactSnapshot()) + ereport(ERROR, + (errcode(ERRCODE_T_R_SERIALIZATION_FAILURE), + errmsg("could not serialize access due to concurrent delete"))); + + /* see TM_Updated case */ + ExecClearTuple(existing); + return 0; + + default: + elog(ERROR, "unrecognized table_tuple_lock status: %u", test); + } + + /* Success, the tuple is locked. */ + + /* + * Run BEFORE ROW UPDATE triggers (if any) and perform the update. + * oldtuple is left NULL: the trigger machinery fetches the old row + * itself from conflictTid. The tuple is already locked, so + * table_tuple_update inside ExecUpdateAct does not block. + */ + if (ExecUpdatePrologue(context, resultRelInfo, + conflictTid, NULL, excludedSlot, + &result)) + { + updateCxt->crossPartUpdate = false; + updateCxt->updateIndexes = TU_None; + updateCxt->lockmode = LockTupleExclusive; + + result = ExecUpdateAct(context, resultRelInfo, + conflictTid, NULL, excludedSlot, + true, updateCxt); + + if (result == TM_Ok) + ExecUpdateEpilogue(context, updateCxt, + resultRelInfo, + conflictTid, NULL, + excludedSlot); + else if (result == TM_Updated) + ereport(ERROR, + (errcode(ERRCODE_T_R_SERIALIZATION_FAILURE), + errmsg("concurrent update of row during COPY ON CONFLICT DO UPDATE"))); + else + ereport(ERROR, + (errcode(ERRCODE_T_R_SERIALIZATION_FAILURE), + errmsg("could not update row during COPY ON CONFLICT DO UPDATE: %d", (int) result))); + + return 1; + } + + /* BEFORE ROW UPDATE trigger suppressed the update */ + return 2; +} + /* * Copy FROM file to relation. */ @@ -792,12 +943,15 @@ CopyFrom(CopyFromState cstate) PartitionTupleRouting *proute = NULL; ErrorContextCallback errcallback; CommandId mycid = GetCurrentCommandId(true); + uint32 ti_options = 0; /* start with default options for insert */ BulkInsertState bistate = NULL; CopyInsertMethod insertMethod; CopyMultiInsertInfo multiInsertInfo = {0}; /* pacify compiler */ int64 processed = 0; int64 excluded = 0; + uint64 skipped = 0; /* rows skipped by ON CONFLICT DO NOTHING */ + uint64 updated = 0; /* rows updated by ON CONFLICT DO UPDATE */ bool has_before_insert_row_trig; bool has_instead_insert_row_trig; bool leafpart_use_multi_insert = false; @@ -805,6 +959,20 @@ CopyFrom(CopyFromState cstate) Assert(cstate->rel); Assert(list_length(cstate->range_table) == 1); + /* + * Make sure any UPDATE performed by the ON CONFLICT DO UPDATE clause + * uses the current command id, so that rows inserted by earlier + * commands of this transaction are visible to the update. + */ + estate->es_output_cid = mycid; + + /* + * COPY ON CONFLICT locks conflicting tuples (CopyOnConflictUpdate); + * that needs a snapshot in the executor state. + */ + if (cstate->opts.on_conflict != COPY_ON_CONFLICT_NONE) + estate->es_snapshot = GetActiveSnapshot(); + if (cstate->opts.on_error != COPY_ON_ERROR_STOP) Assert(cstate->escontext); @@ -923,7 +1091,14 @@ CopyFrom(CopyFromState cstate) /* Verify the named relation is a valid target for INSERT */ CheckValidResultRel(resultRelInfo, CMD_INSERT, ONCONFLICT_NONE, NIL); - ExecOpenIndices(resultRelInfo, false); + /* + * With ON CONFLICT we need the per-index unique operator info that is + * built only in speculative mode (BuildSpeculativeIndexInfo), since + * ExecCheckIndexConstraints() looks up existing conflicting tuples via + * index scans keyed on those operators. + */ + ExecOpenIndices(resultRelInfo, + cstate->opts.on_conflict != COPY_ON_CONFLICT_NONE); /* * Set up a ModifyTableState so we can let FDW(s) init themselves for @@ -986,6 +1161,42 @@ CopyFrom(CopyFromState cstate) cstate->qualexpr = ExecInitQual(castNode(List, cstate->whereClause), &mtstate->ps); + /* + * COPY ON CONFLICT needs a unique or exclusion constraint to detect + * conflicts. For partitioned tables the check is done per leaf + * partition at runtime (each partition enforces its own uniqueness), + * so skip the startup check there. + */ + if (cstate->opts.on_conflict != COPY_ON_CONFLICT_NONE && + cstate->rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE) + { + List *indexoidlist; + ListCell *lc; + bool has_unique = false; + + indexoidlist = RelationGetIndexList(resultRelInfo->ri_RelationDesc); + foreach(lc, indexoidlist) + { + Oid indexoid = lfirst_oid(lc); + Relation indexRel; + + indexRel = index_open(indexoid, AccessShareLock); + if (indexRel->rd_index->indisunique || + indexRel->rd_index->indisexclusion) + has_unique = true; + index_close(indexRel, AccessShareLock); + if (has_unique) + break; + } + list_free(indexoidlist); + + if (!has_unique) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("COPY ON CONFLICT requires a unique or exclusion constraint on table \"%s\"", + RelationGetRelationName(cstate->rel)))); + } + /* * It's generally more efficient to prepare a bunch of tuples for * insertion, and insert them in one @@ -994,7 +1205,16 @@ CopyFrom(CopyFromState cstate) * However, there are a number of reasons why we might not be able to do * this. These are explained below. */ - if (resultRelInfo->ri_TrigDesc != NULL && + if (cstate->opts.on_conflict != COPY_ON_CONFLICT_NONE) + { + /* + * COPY ON CONFLICT performs per-tuple conflict checks, which are + * incompatible with multi-insert batching; force single-tuple + * insertion. + */ + insertMethod = CIM_SINGLE; + } + else if (resultRelInfo->ri_TrigDesc != NULL && (resultRelInfo->ri_TrigDesc->trig_insert_before_row || resultRelInfo->ri_TrigDesc->trig_insert_instead_row)) { @@ -1369,6 +1589,220 @@ CopyFrom(CopyFromState cstate) (proute == NULL || has_before_insert_row_trig)) ExecPartitionCheck(resultRelInfo, myslot, estate, true); + /* + * With DO ON CONFLICT DO NOTHING, check the tuple against + * the unique/exclusion constraints of the target table (or + * leaf partition) and skip it on conflict instead of + * aborting the whole COPY. The check runs after BEFORE + * INSERT triggers and constraint checks, matching the + * ordering of INSERT ... ON CONFLICT. Note that this is a + * pre-check: under concurrent insertion a unique violation + * may still be raised by the index insert itself. + */ + if (cstate->opts.on_conflict != COPY_ON_CONFLICT_NONE) + { + ItemPointerData conflictTid; + ItemPointerData invalidItemPtr; + bool specConflict = false; + uint32 specToken = 0; + int update_rc; + int ii; + + ItemPointerSetInvalid(&invalidItemPtr); + + /* + * Leaf partitions reached via tuple routing had their + * indexes opened in ExecInitPartitionInfo() without the + * speculative flag (COPY's ModifyTable plan node is + * NULL), so the per-index unique operator info needed + * by ExecCheckIndexConstraints() is missing. Fill it + * in on first use. + */ + if (resultRelInfo != target_resultRelInfo && + resultRelInfo->ri_IndexRelationInfo != NULL) + { + for (ii = 0; ii < resultRelInfo->ri_NumIndices; ii++) + { + IndexInfo *idxinfo = resultRelInfo->ri_IndexRelationInfo[ii]; + Relation idxrel = resultRelInfo->ri_IndexRelationDescs[ii]; + + if (idxinfo->ii_Unique && idxinfo->ii_UniqueProcs == NULL && + idxrel != NULL) + BuildSpeculativeIndexInfo(idxrel, idxinfo); + } + } + + retry: + if (!ExecCheckIndexConstraints(resultRelInfo, myslot, + estate, &conflictTid, + &invalidItemPtr, NIL)) + { + if (cstate->opts.on_conflict == COPY_ON_CONFLICT_NOTHING) + { + skipped++; + + pgstat_progress_update_param(PROGRESS_COPY_TUPLES_SKIPPED, + skipped); + + continue; /* skip this tuple */ + } + + /* ===== DO ON CONFLICT DO UPDATE: overwrite the conflicting row ===== */ + { + IvyModifyTableContext context; + IvyUpdateContext updateCxt; + EPQState epqstate; + + /* + * Like INSERT ... ON CONFLICT DO UPDATE, moving + * the row to a different partition is not + * supported; reject it explicitly instead of + * crashing in ExecCrossPartitionUpdate (which + * dereferences the ModifyTable plan node). + */ + if (resultRelInfo->ri_RelationDesc->rd_rel->relispartition && + !ExecPartitionCheck(resultRelInfo, myslot, + estate, false)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("COPY ON CONFLICT DO UPDATE cannot move the row to a different partition"), + errdetail("The result tuple would appear in a different partition than the original tuple."))); + + /* Set up the ModifyTable execution context. */ + memset(&context, 0, sizeof(context)); + context.mtstate = mtstate; + context.estate = estate; + context.planSlot = NULL; + + /* + * EvalPlanQualInit() and + * MakeTransitionCaptureState() allocate in the + * current memory context, which here is the + * caller's context living for the whole COPY. + * Allocate them in the per-tuple context so + * per-row state is freed on the next iteration + * (the transition capture pointer is reset + * before we return to the loop). + */ + MemoryContextSwitchTo(GetPerTupleMemoryContext(estate)); + EvalPlanQualInit(&epqstate, estate, NULL, NIL, + 0, NIL); + context.epqstate = &epqstate; + + /* + * Temporarily switch the ModifyTable operation + * to UPDATE so that the Prologue/Epilogue use + * UPDATE semantics (BEFORE/AFTER ROW UPDATE + * triggers, transition capture, etc.). + */ + mtstate->operation = CMD_UPDATE; + if (resultRelInfo->ri_TrigDesc && + (resultRelInfo->ri_TrigDesc->trig_update_new_table || + resultRelInfo->ri_TrigDesc->trig_update_old_table)) + mtstate->mt_transition_capture = + MakeTransitionCaptureState(resultRelInfo->ri_TrigDesc, + RelationGetRelid(resultRelInfo->ri_RelationDesc), + CMD_UPDATE); + else + mtstate->mt_transition_capture = NULL; + MemoryContextSwitchTo(oldcontext); + + /* + * Lock the conflicting row (waiting out any + * concurrent transaction) and overwrite it. + * If the row was concurrently modified or + * deleted, re-run the whole conflict check. + */ + update_rc = CopyOnConflictUpdate(&context, resultRelInfo, + &conflictTid, myslot, + &updateCxt); + if (update_rc == 0) + { + /* Restore INSERT semantics, then retry. */ + mtstate->operation = CMD_INSERT; + mtstate->mt_transition_capture = cstate->transition_capture; + goto retry; + } + + /* Restore INSERT semantics for the rest of COPY. */ + mtstate->operation = CMD_INSERT; + mtstate->mt_transition_capture = cstate->transition_capture; + + if (update_rc == 1) + updated++; + + processed++; + pgstat_progress_update_param(PROGRESS_COPY_TUPLES_PROCESSED, + processed); + + continue; /* row handled as UPDATE (or suppressed) */ + } + } + else + { + /* + * Pre-check passed. Insert speculatively so that a + * concurrent conflicting insert is detected + * atomically by the index insert (no TOCTOU + * window); on conflict, back the tuple out and + * re-run the conflict check, which will now take + * the DO NOTHING / DO UPDATE path above. + */ + List *recheckIndexes = NIL; + + if (specToken == 0) + specToken = SpeculativeInsertionLockAcquire(GetCurrentTransactionId()); + + table_tuple_insert_speculative(resultRelInfo->ri_RelationDesc, + myslot, + estate->es_output_cid, + ti_options, NULL, specToken); + + recheckIndexes = ExecInsertIndexTuples(resultRelInfo, + estate, + EIIT_NO_DUPE_ERROR, + myslot, NIL, + &specConflict); + + if (specConflict) + { + list_free(recheckIndexes); + recheckIndexes = NIL; + /* + * A concurrent transaction inserted a + * conflicting row: back out the speculative + * insert and retry the conflict check. + */ + table_tuple_complete_speculative(resultRelInfo->ri_RelationDesc, + myslot, specToken, + false); + SpeculativeInsertionLockRelease(GetCurrentTransactionId()); + specToken = 0; + specConflict = false; + goto retry; + } + + /* No conflict: confirm the speculative insert. */ + table_tuple_complete_speculative(resultRelInfo->ri_RelationDesc, + myslot, specToken, + true); + SpeculativeInsertionLockRelease(GetCurrentTransactionId()); + specToken = 0; + + /* AFTER ROW INSERT triggers */ + ExecARInsertTriggers(estate, resultRelInfo, myslot, + recheckIndexes, cstate->transition_capture); + list_free(recheckIndexes); + recheckIndexes = NIL; + + processed++; + pgstat_progress_update_param(PROGRESS_COPY_TUPLES_PROCESSED, + processed); + + continue; /* row inserted via speculative path */ + } + } + /* Store the slot in the multi-insert buffer, when enabled. */ if (insertMethod == CIM_MULTI || leafpart_use_multi_insert) { @@ -1482,6 +1916,18 @@ CopyFrom(CopyFromState cstate) cstate->num_errors)); } + if (cstate->opts.on_conflict == COPY_ON_CONFLICT_NOTHING && skipped > 0) + ereport(NOTICE, + errmsg_plural("%" PRIu64 " row was skipped due to ON CONFLICT DO NOTHING", + "%" PRIu64 " rows were skipped due to ON CONFLICT DO NOTHING", + skipped, skipped)); + + if (cstate->opts.on_conflict == COPY_ON_CONFLICT_UPDATE && updated > 0) + ereport(NOTICE, + errmsg_plural("%" PRIu64 " row was updated due to ON CONFLICT DO UPDATE", + "%" PRIu64 " rows were updated due to ON CONFLICT DO UPDATE", + updated, updated)); + if (bistate != NULL) FreeBulkInsertState(bistate); diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c index 35737f93726..c46d5750018 100644 --- a/src/backend/executor/nodeModifyTable.c +++ b/src/backend/executor/nodeModifyTable.c @@ -2168,7 +2168,8 @@ ExecCrossPartitionUpdate(IvyModifyTableContext *context, * to migrate to a different partition. Maybe this can be implemented * some day, but it seems a fringe feature with little redeeming value. */ - if (((ModifyTable *) mtstate->ps.plan)->onConflictAction == ONCONFLICT_UPDATE) + if (mtstate->ps.plan != NULL && + ((ModifyTable *) mtstate->ps.plan)->onConflictAction == ONCONFLICT_UPDATE) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("invalid ON UPDATE specification"), diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 0382929a639..17cdfa32f58 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -501,7 +501,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type opt_instead %type opt_unique opt_verbose opt_full %type opt_freeze opt_analyze opt_default -%type opt_binary copy_delimiter +%type opt_binary copy_delimiter copy_on_conflict %type copy_from opt_program @@ -3553,7 +3553,7 @@ ClosePortalStmt: CopyStmt: COPY opt_binary qualified_name opt_column_list copy_from opt_program copy_file_name copy_delimiter opt_with - copy_options where_clause + copy_options copy_on_conflict where_clause { CopyStmt *n = makeNode(CopyStmt); @@ -3563,7 +3563,7 @@ CopyStmt: COPY opt_binary qualified_name opt_column_list n->is_from = $5; n->is_program = $6; n->filename = $7; - n->whereClause = $11; + n->whereClause = $12; if (n->is_program && n->filename == NULL) ereport(ERROR, @@ -3576,7 +3576,7 @@ CopyStmt: COPY opt_binary qualified_name opt_column_list (errcode(ERRCODE_SYNTAX_ERROR), errmsg("WHERE clause not allowed with COPY TO"), errhint("Try the COPY (SELECT ... WHERE ...) TO variant."), - parser_errposition(@11))); + parser_errposition(@12))); n->options = NIL; /* Concatenate user-supplied flags */ @@ -3586,6 +3586,8 @@ CopyStmt: COPY opt_binary qualified_name opt_column_list n->options = lappend(n->options, $8); if ($10) n->options = list_concat(n->options, $10); + if ($11) + n->options = lappend(n->options, $11); $$ = (Node *) n; } | COPY '(' PreparableStmt ')' TO opt_program copy_file_name opt_with copy_options @@ -3635,6 +3637,23 @@ copy_options: copy_opt_list { $$ = $1; } | '(' copy_generic_opt_list ')' { $$ = $2; } ; +/* COPY ON CONFLICT (AnalyticDB-compatible), a standalone clause that may + * follow either the old-style or the new parenthesized option syntax. */ +copy_on_conflict: + DO ON CONFLICT DO UPDATE + { + $$ = makeDefElem("on_conflict", (Node *) makeString("update"), @1); + } + | DO ON CONFLICT DO NOTHING + { + $$ = makeDefElem("on_conflict", (Node *) makeString("nothing"), @1); + } + | /* EMPTY */ + { + $$ = NULL; + } + ; + /* old COPY option syntax */ copy_opt_list: copy_opt_list copy_opt_item { $$ = lappend($1, $2); } diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h index abecfe51098..33b7216346b 100644 --- a/src/include/commands/copy.h +++ b/src/include/commands/copy.h @@ -28,8 +28,18 @@ #define COPY_HEADER_TRUE 1 /* - * Represents where to save input processing errors. More values to be added - * in the future. + * What to do when a row conflicts with a unique constraint during COPY + * FROM. DO ON CONFLICT DO UPDATE is parsed but not yet implemented. + */ +typedef enum CopyOnConflictChoice +{ + COPY_ON_CONFLICT_NONE, /* no ON CONFLICT clause */ + COPY_ON_CONFLICT_NOTHING, /* DO ON CONFLICT DO NOTHING */ + COPY_ON_CONFLICT_UPDATE /* DO ON CONFLICT DO UPDATE (not yet implemented) */ +} CopyOnConflictChoice; + +/* + * Represents the type of an error to be handled by COPY FROM. */ typedef enum CopyOnErrorChoice { @@ -93,6 +103,7 @@ typedef struct CopyFormatOptions bool force_null_all; /* FORCE_NULL *? */ bool *force_null_flags; /* per-column CSV FN flags */ bool convert_selectively; /* do selective binary conversion? */ + CopyOnConflictChoice on_conflict; /* behavior when unique constraint conflict happens */ CopyOnErrorChoice on_error; /* what to do when error happened */ CopyLogVerbosityChoice log_verbosity; /* verbosity of logged messages */ int64 reject_limit; /* maximum tolerable number of errors */ diff --git a/src/test/regress/expected/copy_on_conflict.out b/src/test/regress/expected/copy_on_conflict.out new file mode 100644 index 00000000000..b088c242235 --- /dev/null +++ b/src/test/regress/expected/copy_on_conflict.out @@ -0,0 +1,240 @@ +-- COPY ON CONFLICT (AnalyticDB-compatible DO ON CONFLICT DO NOTHING) +DROP TABLE IF EXISTS copy_on_conflict_t1, copy_on_conflict_t2, copy_on_conflict_t3, + copy_on_conflict_tp, copy_on_conflict_tp_lo, copy_on_conflict_tp_hi, + copy_on_conflict_t4 CASCADE; +NOTICE: table "copy_on_conflict_t1" does not exist, skipping +NOTICE: table "copy_on_conflict_t2" does not exist, skipping +NOTICE: table "copy_on_conflict_t3" does not exist, skipping +NOTICE: table "copy_on_conflict_tp" does not exist, skipping +NOTICE: table "copy_on_conflict_tp_lo" does not exist, skipping +NOTICE: table "copy_on_conflict_tp_hi" does not exist, skipping +NOTICE: table "copy_on_conflict_t4" does not exist, skipping +CREATE TABLE copy_on_conflict_t1 (id int PRIMARY KEY, name text); +INSERT INTO copy_on_conflict_t1 VALUES (1, 'old-1'), (2, 'old-2'); +-- 1. DO NOTHING skips conflicting rows, inserts the rest +COPY copy_on_conflict_t1 FROM STDIN DO ON CONFLICT DO NOTHING; +NOTICE: 2 rows were skipped due to ON CONFLICT DO NOTHING +SELECT * FROM copy_on_conflict_t1 ORDER BY id; + id | name +----+------- + 1 | old-1 + 2 | old-2 + 3 | new-3 +(3 rows) + +-- 2. no-conflict data behaves like plain COPY +COPY copy_on_conflict_t1 FROM STDIN DO ON CONFLICT DO NOTHING; +SELECT count(*) FROM copy_on_conflict_t1; + count +------- + 4 +(1 row) + +-- 3. table without unique constraint is rejected at startup +CREATE TABLE copy_on_conflict_t2 (a int, b text); +COPY copy_on_conflict_t2 FROM '/dev/null' DO ON CONFLICT DO NOTHING; +ERROR: COPY ON CONFLICT requires a unique or exclusion constraint on table "copy_on_conflict_t2" +-- 4. COPY TO does not accept ON CONFLICT +COPY copy_on_conflict_t1 TO STDOUT DO ON CONFLICT DO NOTHING; +ERROR: COPY ON_CONFLICT cannot be used with COPY TO +LINE 1: COPY copy_on_conflict_t1 TO STDOUT DO ON CONFLICT DO NOTHING... + ^ +-- 5. DO UPDATE with empty input: no conflicts, no-op (see section below for behavior) +COPY copy_on_conflict_t1 FROM '/dev/null' DO ON CONFLICT DO UPDATE; +-- 6. multi-column unique index +CREATE TABLE copy_on_conflict_t3 (a int, b int, c text, UNIQUE (a, b)); +INSERT INTO copy_on_conflict_t3 VALUES (1, 1, 'keep'); +COPY copy_on_conflict_t3 FROM STDIN DO ON CONFLICT DO NOTHING; +NOTICE: 1 row was skipped due to ON CONFLICT DO NOTHING +SELECT * FROM copy_on_conflict_t3 ORDER BY a, b; + a | b | c +---+---+------ + 1 | 1 | keep + 1 | 2 | ok +(2 rows) + +-- 7. partitioned table: uniqueness enforced per leaf partition +CREATE TABLE copy_on_conflict_tp (id int PRIMARY KEY, v text) PARTITION BY RANGE (id); +CREATE TABLE copy_on_conflict_tp_lo PARTITION OF copy_on_conflict_tp FOR VALUES FROM (0) TO (10); +CREATE TABLE copy_on_conflict_tp_hi PARTITION OF copy_on_conflict_tp FOR VALUES FROM (10) TO (20); +INSERT INTO copy_on_conflict_tp VALUES (1, 'p-old'), (11, 'h-old'); +COPY copy_on_conflict_tp FROM STDIN DO ON CONFLICT DO NOTHING; +NOTICE: 2 rows were skipped due to ON CONFLICT DO NOTHING +SELECT * FROM copy_on_conflict_tp ORDER BY id; + id | v +----+------- + 1 | p-old + 5 | p-new + 11 | h-old + 15 | h-new +(4 rows) + +-- 8. combined with ON_ERROR ignore: type errors and conflicts handled independently +CREATE TABLE copy_on_conflict_t4 (id int PRIMARY KEY, v int); +INSERT INTO copy_on_conflict_t4 VALUES (1, 100); +COPY copy_on_conflict_t4 FROM STDIN WITH (ON_ERROR ignore) DO ON CONFLICT DO NOTHING; +NOTICE: skipping row due to data type incompatibility at line 1 for column "v": "bad-dup" +NOTICE: skipping row due to data type incompatibility at line 2 for column "v": "notanumber" +NOTICE: 2 rows were skipped due to data type incompatibility +SELECT * FROM copy_on_conflict_t4 ORDER BY id; + id | v +----+----- + 1 | 100 + 2 | 200 +(2 rows) + +-- 9. parenthesized option syntax also accepts on_conflict +COPY copy_on_conflict_t1 FROM STDIN WITH (on_conflict 'nothing'); +SELECT * FROM copy_on_conflict_t1 WHERE id = 5; + id | name +----+------ + 5 | five +(1 row) + +-- 10. unknown on_conflict value is rejected +COPY copy_on_conflict_t1 FROM '/dev/null' WITH (on_conflict 'bogus'); +ERROR: COPY ON_CONFLICT "bogus" not recognized +LINE 1: COPY copy_on_conflict_t1 FROM '/dev/null' WITH (on_conflict ... + ^ +-- ===== DO ON CONFLICT DO UPDATE ===== +-- 11. DO UPDATE overwrites conflicting rows (full-row overwrite) +DROP TABLE IF EXISTS copy_on_conflict_u1 CASCADE; +NOTICE: table "copy_on_conflict_u1" does not exist, skipping +CREATE TABLE copy_on_conflict_u1 (id int PRIMARY KEY, name text); +INSERT INTO copy_on_conflict_u1 VALUES (1, 'old-1'), (2, 'old-2'); +COPY copy_on_conflict_u1 FROM STDIN DO ON CONFLICT DO UPDATE; +NOTICE: 2 rows were updated due to ON CONFLICT DO UPDATE +SELECT * FROM copy_on_conflict_u1 ORDER BY id; + id | name +----+------- + 1 | new-1 + 2 | new-2 + 3 | new-3 +(3 rows) + +-- 12. DO UPDATE without conflicts behaves like plain COPY +COPY copy_on_conflict_u1 FROM STDIN DO ON CONFLICT DO UPDATE; +SELECT count(*) FROM copy_on_conflict_u1; + count +------- + 4 +(1 row) + +-- 13. multi-column unique index with DO UPDATE +DROP TABLE IF EXISTS copy_on_conflict_u3 CASCADE; +NOTICE: table "copy_on_conflict_u3" does not exist, skipping +CREATE TABLE copy_on_conflict_u3 (a int, b int, c text, UNIQUE (a, b)); +INSERT INTO copy_on_conflict_u3 VALUES (1, 1, 'keep'); +COPY copy_on_conflict_u3 FROM STDIN DO ON CONFLICT DO UPDATE; +NOTICE: 1 row was updated due to ON CONFLICT DO UPDATE +SELECT * FROM copy_on_conflict_u3 ORDER BY a, b; + a | b | c +---+---+--------- + 1 | 1 | changed + 1 | 2 | ok +(2 rows) + +-- 14. partitioned table: in-partition DO UPDATE +DROP TABLE IF EXISTS copy_on_conflict_up CASCADE; +NOTICE: table "copy_on_conflict_up" does not exist, skipping +CREATE TABLE copy_on_conflict_up (id int PRIMARY KEY, v text) PARTITION BY RANGE (id); +CREATE TABLE copy_on_conflict_up_lo PARTITION OF copy_on_conflict_up FOR VALUES FROM (0) TO (10); +CREATE TABLE copy_on_conflict_up_hi PARTITION OF copy_on_conflict_up FOR VALUES FROM (10) TO (20); +INSERT INTO copy_on_conflict_up VALUES (1, 'p-old'), (11, 'h-old'); +COPY copy_on_conflict_up FROM STDIN DO ON CONFLICT DO UPDATE; +NOTICE: 2 rows were updated due to ON CONFLICT DO UPDATE +SELECT * FROM copy_on_conflict_up ORDER BY id; + id | v +----+------- + 1 | p-new + 5 | ins + 11 | h-new +(3 rows) + +-- 15. stored generated columns are recomputed on DO UPDATE +DROP TABLE IF EXISTS copy_on_conflict_u6 CASCADE; +NOTICE: table "copy_on_conflict_u6" does not exist, skipping +CREATE TABLE copy_on_conflict_u6 (id int PRIMARY KEY, a int, + b int GENERATED ALWAYS AS (a * 2) STORED); +INSERT INTO copy_on_conflict_u6 VALUES (1, 5, DEFAULT); +COPY copy_on_conflict_u6 (id, a) FROM STDIN DO ON CONFLICT DO UPDATE; +NOTICE: 1 row was updated due to ON CONFLICT DO UPDATE +SELECT * FROM copy_on_conflict_u6; + id | a | b +----+----+---- + 1 | 10 | 20 +(1 row) + +-- 16. AFTER ROW UPDATE triggers fire on DO UPDATE +DROP TABLE IF EXISTS copy_on_conflict_u7, copy_on_conflict_u7_log CASCADE; +NOTICE: table "copy_on_conflict_u7" does not exist, skipping +NOTICE: table "copy_on_conflict_u7_log" does not exist, skipping +CREATE TABLE copy_on_conflict_u7 (id int PRIMARY KEY, v text); +CREATE TABLE copy_on_conflict_u7_log (id int, old_v text, new_v text); +CREATE FUNCTION copy_on_conflict_u7_trg() RETURNS trigger AS $$ +BEGIN + INSERT INTO copy_on_conflict_u7_log VALUES (OLD.id, OLD.v, NEW.v); + RETURN NEW; +END $$ LANGUAGE plpgsql; +CREATE TRIGGER copy_on_conflict_u7_trg AFTER UPDATE ON copy_on_conflict_u7 + FOR EACH ROW EXECUTE FUNCTION copy_on_conflict_u7_trg(); +INSERT INTO copy_on_conflict_u7 VALUES (1, 'old'); +COPY copy_on_conflict_u7 FROM STDIN DO ON CONFLICT DO UPDATE; +NOTICE: 1 row was updated due to ON CONFLICT DO UPDATE +SELECT * FROM copy_on_conflict_u7; + id | v +----+----- + 1 | new +(1 row) + +SELECT * FROM copy_on_conflict_u7_log; + id | old_v | new_v +----+-------+------- + 1 | old | new +(1 row) + +-- 17. partitioned table unique constraints must include partition columns +-- (PG rule: cross-partition updates are therefore unreachable for ON CONFLICT) +DROP TABLE IF EXISTS copy_on_conflict_u5 CASCADE; +NOTICE: table "copy_on_conflict_u5" does not exist, skipping +CREATE TABLE copy_on_conflict_u5 (id int, uid int UNIQUE, v text) PARTITION BY RANGE (id); +ERROR: UNIQUE constraint on partitioned table must include all partitioning columns +DETAIL: UNIQUE constraint on table "copy_on_conflict_u5" lacks column "id" which is part of the partition key. +-- 18. duplicate constrained values within one COPY with DO UPDATE raises +-- the standard INSERT ON CONFLICT error ('cannot affect row a second time') +DROP TABLE IF EXISTS copy_on_conflict_u8 CASCADE; +NOTICE: table "copy_on_conflict_u8" does not exist, skipping +CREATE TABLE copy_on_conflict_u8 (id int PRIMARY KEY, v text); +INSERT INTO copy_on_conflict_u8 VALUES (1, 'existing'); +COPY copy_on_conflict_u8 FROM STDIN DO ON CONFLICT DO UPDATE; +ERROR: ON CONFLICT DO UPDATE command cannot affect row a second time +HINT: Ensure that no rows proposed for insertion within the same command have duplicate constrained values. +CONTEXT: COPY copy_on_conflict_u8, line 2: "1 second" +SELECT * FROM copy_on_conflict_u8; + id | v +----+---------- + 1 | existing +(1 row) + +-- 19. same for DO NOTHING: the second duplicate is also skipped, no error +DROP TABLE IF EXISTS copy_on_conflict_u9 CASCADE; +NOTICE: table "copy_on_conflict_u9" does not exist, skipping +CREATE TABLE copy_on_conflict_u9 (id int PRIMARY KEY, v text); +INSERT INTO copy_on_conflict_u9 VALUES (1, 'existing'); +COPY copy_on_conflict_u9 FROM STDIN DO ON CONFLICT DO NOTHING; +NOTICE: 2 rows were skipped due to ON CONFLICT DO NOTHING +SELECT * FROM copy_on_conflict_u9 ORDER BY id; + id | v +----+---------- + 1 | existing + 2 | ok +(2 rows) + +-- cleanup +DROP TABLE copy_on_conflict_t1, copy_on_conflict_t2, copy_on_conflict_t3, + copy_on_conflict_tp, copy_on_conflict_tp_lo, copy_on_conflict_tp_hi, + copy_on_conflict_t4, copy_on_conflict_u1, copy_on_conflict_u3, + copy_on_conflict_up, copy_on_conflict_up_lo, copy_on_conflict_up_hi, + copy_on_conflict_u6, copy_on_conflict_u7, copy_on_conflict_u7_log, + copy_on_conflict_u8, copy_on_conflict_u9 CASCADE; +DROP FUNCTION IF EXISTS copy_on_conflict_u7_trg(); diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index a3894d87d8c..79ce06f8c19 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -150,3 +150,6 @@ test: tablespace # Test that Oracle keywords (ROWNUM, ROWID, etc.) have no special meaning in PostgreSQL mode test: ora_keywords_pg + +# COPY ON CONFLICT (AnalyticDB-compatible) +test: copy_on_conflict diff --git a/src/test/regress/sql/copy_on_conflict.sql b/src/test/regress/sql/copy_on_conflict.sql new file mode 100644 index 00000000000..0c4a427be31 --- /dev/null +++ b/src/test/regress/sql/copy_on_conflict.sql @@ -0,0 +1,176 @@ +-- COPY ON CONFLICT (AnalyticDB-compatible DO ON CONFLICT DO NOTHING) +DROP TABLE IF EXISTS copy_on_conflict_t1, copy_on_conflict_t2, copy_on_conflict_t3, + copy_on_conflict_tp, copy_on_conflict_tp_lo, copy_on_conflict_tp_hi, + copy_on_conflict_t4 CASCADE; +CREATE TABLE copy_on_conflict_t1 (id int PRIMARY KEY, name text); +INSERT INTO copy_on_conflict_t1 VALUES (1, 'old-1'), (2, 'old-2'); + +-- 1. DO NOTHING skips conflicting rows, inserts the rest +COPY copy_on_conflict_t1 FROM STDIN DO ON CONFLICT DO NOTHING; +1 new-1 +2 new-2 +3 new-3 +\. +SELECT * FROM copy_on_conflict_t1 ORDER BY id; + +-- 2. no-conflict data behaves like plain COPY +COPY copy_on_conflict_t1 FROM STDIN DO ON CONFLICT DO NOTHING; +4 four +\. +SELECT count(*) FROM copy_on_conflict_t1; + +-- 3. table without unique constraint is rejected at startup +CREATE TABLE copy_on_conflict_t2 (a int, b text); +COPY copy_on_conflict_t2 FROM '/dev/null' DO ON CONFLICT DO NOTHING; + +-- 4. COPY TO does not accept ON CONFLICT +COPY copy_on_conflict_t1 TO STDOUT DO ON CONFLICT DO NOTHING; + +-- 5. DO UPDATE with empty input: no conflicts, no-op (see section below for behavior) +COPY copy_on_conflict_t1 FROM '/dev/null' DO ON CONFLICT DO UPDATE; + +-- 6. multi-column unique index +CREATE TABLE copy_on_conflict_t3 (a int, b int, c text, UNIQUE (a, b)); +INSERT INTO copy_on_conflict_t3 VALUES (1, 1, 'keep'); +COPY copy_on_conflict_t3 FROM STDIN DO ON CONFLICT DO NOTHING; +1 1 dup +1 2 ok +\. +SELECT * FROM copy_on_conflict_t3 ORDER BY a, b; + +-- 7. partitioned table: uniqueness enforced per leaf partition +CREATE TABLE copy_on_conflict_tp (id int PRIMARY KEY, v text) PARTITION BY RANGE (id); +CREATE TABLE copy_on_conflict_tp_lo PARTITION OF copy_on_conflict_tp FOR VALUES FROM (0) TO (10); +CREATE TABLE copy_on_conflict_tp_hi PARTITION OF copy_on_conflict_tp FOR VALUES FROM (10) TO (20); +INSERT INTO copy_on_conflict_tp VALUES (1, 'p-old'), (11, 'h-old'); +COPY copy_on_conflict_tp FROM STDIN DO ON CONFLICT DO NOTHING; +1 p-dup +5 p-new +11 h-dup +15 h-new +\. +SELECT * FROM copy_on_conflict_tp ORDER BY id; + +-- 8. combined with ON_ERROR ignore: type errors and conflicts handled independently +CREATE TABLE copy_on_conflict_t4 (id int PRIMARY KEY, v int); +INSERT INTO copy_on_conflict_t4 VALUES (1, 100); +COPY copy_on_conflict_t4 FROM STDIN WITH (ON_ERROR ignore) DO ON CONFLICT DO NOTHING; +1 bad-dup +2 notanumber +2 200 +\. +SELECT * FROM copy_on_conflict_t4 ORDER BY id; + +-- 9. parenthesized option syntax also accepts on_conflict +COPY copy_on_conflict_t1 FROM STDIN WITH (on_conflict 'nothing'); +5 five +\. +SELECT * FROM copy_on_conflict_t1 WHERE id = 5; + +-- 10. unknown on_conflict value is rejected +COPY copy_on_conflict_t1 FROM '/dev/null' WITH (on_conflict 'bogus'); + +-- ===== DO ON CONFLICT DO UPDATE ===== +-- 11. DO UPDATE overwrites conflicting rows (full-row overwrite) +DROP TABLE IF EXISTS copy_on_conflict_u1 CASCADE; +CREATE TABLE copy_on_conflict_u1 (id int PRIMARY KEY, name text); +INSERT INTO copy_on_conflict_u1 VALUES (1, 'old-1'), (2, 'old-2'); +COPY copy_on_conflict_u1 FROM STDIN DO ON CONFLICT DO UPDATE; +1 new-1 +2 new-2 +3 new-3 +\. +SELECT * FROM copy_on_conflict_u1 ORDER BY id; + +-- 12. DO UPDATE without conflicts behaves like plain COPY +COPY copy_on_conflict_u1 FROM STDIN DO ON CONFLICT DO UPDATE; +4 four +\. +SELECT count(*) FROM copy_on_conflict_u1; + +-- 13. multi-column unique index with DO UPDATE +DROP TABLE IF EXISTS copy_on_conflict_u3 CASCADE; +CREATE TABLE copy_on_conflict_u3 (a int, b int, c text, UNIQUE (a, b)); +INSERT INTO copy_on_conflict_u3 VALUES (1, 1, 'keep'); +COPY copy_on_conflict_u3 FROM STDIN DO ON CONFLICT DO UPDATE; +1 1 changed +1 2 ok +\. +SELECT * FROM copy_on_conflict_u3 ORDER BY a, b; + +-- 14. partitioned table: in-partition DO UPDATE +DROP TABLE IF EXISTS copy_on_conflict_up CASCADE; +CREATE TABLE copy_on_conflict_up (id int PRIMARY KEY, v text) PARTITION BY RANGE (id); +CREATE TABLE copy_on_conflict_up_lo PARTITION OF copy_on_conflict_up FOR VALUES FROM (0) TO (10); +CREATE TABLE copy_on_conflict_up_hi PARTITION OF copy_on_conflict_up FOR VALUES FROM (10) TO (20); +INSERT INTO copy_on_conflict_up VALUES (1, 'p-old'), (11, 'h-old'); +COPY copy_on_conflict_up FROM STDIN DO ON CONFLICT DO UPDATE; +1 p-new +5 ins +11 h-new +\. +SELECT * FROM copy_on_conflict_up ORDER BY id; + +-- 15. stored generated columns are recomputed on DO UPDATE +DROP TABLE IF EXISTS copy_on_conflict_u6 CASCADE; +CREATE TABLE copy_on_conflict_u6 (id int PRIMARY KEY, a int, + b int GENERATED ALWAYS AS (a * 2) STORED); +INSERT INTO copy_on_conflict_u6 VALUES (1, 5, DEFAULT); +COPY copy_on_conflict_u6 (id, a) FROM STDIN DO ON CONFLICT DO UPDATE; +1 10 +\. +SELECT * FROM copy_on_conflict_u6; + +-- 16. AFTER ROW UPDATE triggers fire on DO UPDATE +DROP TABLE IF EXISTS copy_on_conflict_u7, copy_on_conflict_u7_log CASCADE; +CREATE TABLE copy_on_conflict_u7 (id int PRIMARY KEY, v text); +CREATE TABLE copy_on_conflict_u7_log (id int, old_v text, new_v text); +CREATE FUNCTION copy_on_conflict_u7_trg() RETURNS trigger AS $$ +BEGIN + INSERT INTO copy_on_conflict_u7_log VALUES (OLD.id, OLD.v, NEW.v); + RETURN NEW; +END $$ LANGUAGE plpgsql; +CREATE TRIGGER copy_on_conflict_u7_trg AFTER UPDATE ON copy_on_conflict_u7 + FOR EACH ROW EXECUTE FUNCTION copy_on_conflict_u7_trg(); +INSERT INTO copy_on_conflict_u7 VALUES (1, 'old'); +COPY copy_on_conflict_u7 FROM STDIN DO ON CONFLICT DO UPDATE; +1 new +\. +SELECT * FROM copy_on_conflict_u7; +SELECT * FROM copy_on_conflict_u7_log; + +-- 17. partitioned table unique constraints must include partition columns +-- (PG rule: cross-partition updates are therefore unreachable for ON CONFLICT) +DROP TABLE IF EXISTS copy_on_conflict_u5 CASCADE; +CREATE TABLE copy_on_conflict_u5 (id int, uid int UNIQUE, v text) PARTITION BY RANGE (id); + +-- 18. duplicate constrained values within one COPY with DO UPDATE raises +-- the standard INSERT ON CONFLICT error ('cannot affect row a second time') +DROP TABLE IF EXISTS copy_on_conflict_u8 CASCADE; +CREATE TABLE copy_on_conflict_u8 (id int PRIMARY KEY, v text); +INSERT INTO copy_on_conflict_u8 VALUES (1, 'existing'); +COPY copy_on_conflict_u8 FROM STDIN DO ON CONFLICT DO UPDATE; +1 first +1 second +\. +SELECT * FROM copy_on_conflict_u8; + +-- 19. same for DO NOTHING: the second duplicate is also skipped, no error +DROP TABLE IF EXISTS copy_on_conflict_u9 CASCADE; +CREATE TABLE copy_on_conflict_u9 (id int PRIMARY KEY, v text); +INSERT INTO copy_on_conflict_u9 VALUES (1, 'existing'); +COPY copy_on_conflict_u9 FROM STDIN DO ON CONFLICT DO NOTHING; +1 first +1 second +2 ok +\. +SELECT * FROM copy_on_conflict_u9 ORDER BY id; + +-- cleanup +DROP TABLE copy_on_conflict_t1, copy_on_conflict_t2, copy_on_conflict_t3, + copy_on_conflict_tp, copy_on_conflict_tp_lo, copy_on_conflict_tp_hi, + copy_on_conflict_t4, copy_on_conflict_u1, copy_on_conflict_u3, + copy_on_conflict_up, copy_on_conflict_up_lo, copy_on_conflict_up_hi, + copy_on_conflict_u6, copy_on_conflict_u7, copy_on_conflict_u7_log, + copy_on_conflict_u8, copy_on_conflict_u9 CASCADE; +DROP FUNCTION IF EXISTS copy_on_conflict_u7_trg();