diff options
Diffstat (limited to 'src')
72 files changed, 2465 insertions, 159 deletions
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c index d7279248e70..f96567f5d51 100644 --- a/src/backend/access/heap/heapam.c +++ b/src/backend/access/heap/heapam.c @@ -3245,6 +3245,7 @@ l1: result == HeapTupleUpdated || result == HeapTupleBeingUpdated); Assert(!(tp.t_data->t_infomask & HEAP_XMAX_INVALID)); + hufd->result = result; hufd->ctid = tp.t_data->t_ctid; hufd->xmax = HeapTupleHeaderGetUpdateXid(tp.t_data); if (result == HeapTupleSelfUpdated) @@ -3507,7 +3508,7 @@ simple_heap_delete(Relation relation, ItemPointer tid) HTSU_Result heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, CommandId cid, Snapshot crosscheck, bool wait, - HeapUpdateFailureData *hufd, LockTupleMode *lockmode) + HeapUpdateFailureData *hufd) { HTSU_Result result; TransactionId xid = GetCurrentTransactionId(); @@ -3547,8 +3548,10 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, infomask2_old_tuple, infomask_new_tuple, infomask2_new_tuple; + LockTupleMode lockmode; Assert(ItemPointerIsValid(otid)); + Assert(hufd != NULL); /* * Forbid this during a parallel operation, lest it allocate a combocid. @@ -3664,7 +3667,7 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, */ if (!bms_overlap(modified_attrs, key_attrs)) { - *lockmode = LockTupleNoKeyExclusive; + lockmode = hufd->lockmode = LockTupleNoKeyExclusive; mxact_status = MultiXactStatusNoKeyUpdate; key_intact = true; @@ -3681,7 +3684,7 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, } else { - *lockmode = LockTupleExclusive; + lockmode = hufd->lockmode = LockTupleExclusive; mxact_status = MultiXactStatusUpdate; key_intact = false; } @@ -3759,12 +3762,12 @@ l2: int remain; if (DoesMultiXactIdConflict((MultiXactId) xwait, infomask, - *lockmode)) + lockmode)) { LockBuffer(buffer, BUFFER_LOCK_UNLOCK); /* acquire tuple lock, if necessary */ - heap_acquire_tuplock(relation, &(oldtup.t_self), *lockmode, + heap_acquire_tuplock(relation, &(oldtup.t_self), lockmode, LockWaitBlock, &have_tuple_lock); /* wait for multixact */ @@ -3848,7 +3851,7 @@ l2: * lock. */ LockBuffer(buffer, BUFFER_LOCK_UNLOCK); - heap_acquire_tuplock(relation, &(oldtup.t_self), *lockmode, + heap_acquire_tuplock(relation, &(oldtup.t_self), lockmode, LockWaitBlock, &have_tuple_lock); XactLockTableWait(xwait, relation, &oldtup.t_self, XLTW_Update); @@ -3887,6 +3890,7 @@ l2: result == HeapTupleUpdated || result == HeapTupleBeingUpdated); Assert(!(oldtup.t_data->t_infomask & HEAP_XMAX_INVALID)); + hufd->result = result; hufd->ctid = oldtup.t_data->t_ctid; hufd->xmax = HeapTupleHeaderGetUpdateXid(oldtup.t_data); if (result == HeapTupleSelfUpdated) @@ -3895,7 +3899,7 @@ l2: hufd->cmax = InvalidCommandId; UnlockReleaseBuffer(buffer); if (have_tuple_lock) - UnlockTupleTuplock(relation, &(oldtup.t_self), *lockmode); + UnlockTupleTuplock(relation, &(oldtup.t_self), lockmode); if (vmbuffer != InvalidBuffer) ReleaseBuffer(vmbuffer); bms_free(hot_attrs); @@ -3933,7 +3937,7 @@ l2: compute_new_xmax_infomask(HeapTupleHeaderGetRawXmax(oldtup.t_data), oldtup.t_data->t_infomask, oldtup.t_data->t_infomask2, - xid, *lockmode, true, + xid, lockmode, true, &xmax_old_tuple, &infomask_old_tuple, &infomask2_old_tuple); @@ -4050,7 +4054,7 @@ l2: compute_new_xmax_infomask(HeapTupleHeaderGetRawXmax(oldtup.t_data), oldtup.t_data->t_infomask, oldtup.t_data->t_infomask2, - xid, *lockmode, false, + xid, lockmode, false, &xmax_lock_old_tuple, &infomask_lock_old_tuple, &infomask2_lock_old_tuple); @@ -4362,7 +4366,7 @@ l2: * Release the lmgr tuple lock, if we had it. */ if (have_tuple_lock) - UnlockTupleTuplock(relation, &(oldtup.t_self), *lockmode); + UnlockTupleTuplock(relation, &(oldtup.t_self), lockmode); pgstat_count_heap_update(relation, use_hot_update); @@ -4586,12 +4590,11 @@ simple_heap_update(Relation relation, ItemPointer otid, HeapTuple tup) { HTSU_Result result; HeapUpdateFailureData hufd; - LockTupleMode lockmode; result = heap_update(relation, otid, tup, GetCurrentCommandId(true), InvalidSnapshot, true /* wait for commit */ , - &hufd, &lockmode); + &hufd); switch (result) { case HeapTupleSelfUpdated: @@ -5177,6 +5180,7 @@ failed: Assert(result == HeapTupleSelfUpdated || result == HeapTupleUpdated || result == HeapTupleWouldBlock); Assert(!(tuple->t_data->t_infomask & HEAP_XMAX_INVALID)); + hufd->result = result; hufd->ctid = tuple->t_data->t_ctid; hufd->xmax = HeapTupleHeaderGetUpdateXid(tuple->t_data); if (result == HeapTupleSelfUpdated) diff --git a/src/backend/catalog/sql_features.txt b/src/backend/catalog/sql_features.txt index 20d61f37803..ca0409b83ea 100644 --- a/src/backend/catalog/sql_features.txt +++ b/src/backend/catalog/sql_features.txt @@ -229,9 +229,9 @@ F311 Schema definition statement 02 CREATE TABLE for persistent base tables YES F311 Schema definition statement 03 CREATE VIEW YES F311 Schema definition statement 04 CREATE VIEW: WITH CHECK OPTION YES F311 Schema definition statement 05 GRANT statement YES -F312 MERGE statement NO consider INSERT ... ON CONFLICT DO UPDATE -F313 Enhanced MERGE statement NO -F314 MERGE statement with DELETE branch NO +F312 MERGE statement YES also consider INSERT ... ON CONFLICT DO UPDATE +F313 Enhanced MERGE statement YES +F314 MERGE statement with DELETE branch YES F321 User authorization YES F341 Usage tables NO no ROUTINE_*_USAGE tables F361 Subprogram support YES diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c index 8a58672a94e..79f639d5e27 100644 --- a/src/backend/commands/explain.c +++ b/src/backend/commands/explain.c @@ -946,6 +946,9 @@ ExplainNode(PlanState *planstate, List *ancestors, case CMD_DELETE: pname = operation = "Delete"; break; + case CMD_MERGE: + pname = operation = "Merge"; + break; default: pname = "???"; break; @@ -3007,6 +3010,10 @@ show_modifytable_info(ModifyTableState *mtstate, List *ancestors, operation = "Delete"; foperation = "Foreign Delete"; break; + case CMD_MERGE: + operation = "Merge"; + foperation = "Foreign Merge"; + break; default: operation = "???"; foperation = "Foreign ???"; @@ -3129,6 +3136,32 @@ show_modifytable_info(ModifyTableState *mtstate, List *ancestors, other_path, 0, es); } } + else if (node->operation == CMD_MERGE) + { + /* EXPLAIN ANALYZE display of actual outcome for each tuple proposed */ + if (es->analyze && mtstate->ps.instrument) + { + double total; + double insert_path; + double update_path; + double delete_path; + double skipped_path; + + InstrEndLoop(mtstate->mt_plans[0]->instrument); + + /* count the number of source rows */ + total = mtstate->mt_plans[0]->instrument->ntuples; + insert_path = mtstate->ps.instrument->nfiltered1; + update_path = mtstate->ps.instrument->nfiltered2; + delete_path = mtstate->ps.instrument->nfiltered3; + skipped_path = total - insert_path - update_path - delete_path; + + ExplainPropertyFloat("Tuples Inserted", NULL, insert_path, 0, es); + ExplainPropertyFloat("Tuples Updated", NULL, update_path, 0, es); + ExplainPropertyFloat("Tuples Deleted", NULL, delete_path, 0, es); + ExplainPropertyFloat("Tuples Skipped", NULL, skipped_path, 0, es); + } + } if (labeltargets) ExplainCloseGroup("Target Tables", "Target Tables", false, es); diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index b945b1556a8..c3610b18741 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -151,6 +151,7 @@ PrepareQuery(PrepareStmt *stmt, const char *queryString, case CMD_INSERT: case CMD_UPDATE: case CMD_DELETE: + case CMD_MERGE: /* OK */ break; default: diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c index a6593f939ca..e71f921fda1 100644 --- a/src/backend/commands/trigger.c +++ b/src/backend/commands/trigger.c @@ -85,7 +85,8 @@ static HeapTuple GetTupleForTrigger(EState *estate, ResultRelInfo *relinfo, ItemPointer tid, LockTupleMode lockmode, - TupleTableSlot **newSlot); + TupleTableSlot **newSlot, + HeapUpdateFailureData *hufdp); static bool TriggerEnabled(EState *estate, ResultRelInfo *relinfo, Trigger *trigger, TriggerEvent event, Bitmapset *modifiedCols, @@ -2729,7 +2730,8 @@ bool ExecBRDeleteTriggers(EState *estate, EPQState *epqstate, ResultRelInfo *relinfo, ItemPointer tupleid, - HeapTuple fdw_trigtuple) + HeapTuple fdw_trigtuple, + HeapUpdateFailureData *hufdp) { TriggerDesc *trigdesc = relinfo->ri_TrigDesc; bool result = true; @@ -2743,7 +2745,7 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate, if (fdw_trigtuple == NULL) { trigtuple = GetTupleForTrigger(estate, epqstate, relinfo, tupleid, - LockTupleExclusive, &newSlot); + LockTupleExclusive, &newSlot, hufdp); if (trigtuple == NULL) return false; } @@ -2814,6 +2816,7 @@ ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo, relinfo, tupleid, LockTupleExclusive, + NULL, NULL); else trigtuple = fdw_trigtuple; @@ -2951,7 +2954,8 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate, ResultRelInfo *relinfo, ItemPointer tupleid, HeapTuple fdw_trigtuple, - TupleTableSlot *slot) + TupleTableSlot *slot, + HeapUpdateFailureData *hufdp) { TriggerDesc *trigdesc = relinfo->ri_TrigDesc; HeapTuple slottuple = ExecMaterializeSlot(slot); @@ -2972,7 +2976,7 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate, { /* get a copy of the on-disk tuple we are planning to update */ trigtuple = GetTupleForTrigger(estate, epqstate, relinfo, tupleid, - lockmode, &newSlot); + lockmode, &newSlot, hufdp); if (trigtuple == NULL) return NULL; /* cancel the update action */ } @@ -3092,6 +3096,7 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo, relinfo, tupleid, LockTupleExclusive, + NULL, NULL); else trigtuple = fdw_trigtuple; @@ -3240,7 +3245,8 @@ GetTupleForTrigger(EState *estate, ResultRelInfo *relinfo, ItemPointer tid, LockTupleMode lockmode, - TupleTableSlot **newSlot) + TupleTableSlot **newSlot, + HeapUpdateFailureData *hufdp) { Relation relation = relinfo->ri_RelationDesc; HeapTupleData tuple; @@ -3266,6 +3272,11 @@ ltrmark:; estate->es_output_cid, lockmode, LockWaitBlock, false, &buffer, &hufd); + + /* Let the caller know about failure reason, if any. */ + if (hufdp) + *hufdp = hufd; + switch (test) { case HeapTupleSelfUpdated: @@ -3302,10 +3313,17 @@ ltrmark:; /* it was updated, so look at the updated version */ TupleTableSlot *epqslot; + /* + * If we're running MERGE then we must install the + * new tuple in the slot of the underlying join query and + * not the result relation itself. If the join does not + * yield any tuple, the caller will take the necessary + * action. + */ epqslot = EvalPlanQual(estate, epqstate, relation, - relinfo->ri_RangeTableIndex, + GetEPQRangeTableIndex(relinfo), lockmode, &hufd.ctid, hufd.xmax); @@ -3828,8 +3846,14 @@ struct AfterTriggersTableData bool before_trig_done; /* did we already queue BS triggers? */ bool after_trig_done; /* did we already queue AS triggers? */ AfterTriggerEventList after_trig_events; /* if so, saved list pointer */ - Tuplestorestate *old_tuplestore; /* "old" transition table, if any */ - Tuplestorestate *new_tuplestore; /* "new" transition table, if any */ + /* "old" transition table for UPDATE, if any */ + Tuplestorestate *old_upd_tuplestore; + /* "new" transition table for UPDATE, if any */ + Tuplestorestate *new_upd_tuplestore; + /* "old" transition table for DELETE, if any */ + Tuplestorestate *old_del_tuplestore; + /* "new" transition table INSERT, if any */ + Tuplestorestate *new_ins_tuplestore; }; static AfterTriggersData afterTriggers; @@ -4296,13 +4320,19 @@ AfterTriggerExecute(AfterTriggerEvent event, { if (LocTriggerData.tg_trigger->tgoldtable) { - LocTriggerData.tg_oldtable = evtshared->ats_table->old_tuplestore; + if (TRIGGER_FIRED_BY_UPDATE(evtshared->ats_event)) + LocTriggerData.tg_oldtable = evtshared->ats_table->old_upd_tuplestore; + else + LocTriggerData.tg_oldtable = evtshared->ats_table->old_del_tuplestore; evtshared->ats_table->closed = true; } if (LocTriggerData.tg_trigger->tgnewtable) { - LocTriggerData.tg_newtable = evtshared->ats_table->new_tuplestore; + if (TRIGGER_FIRED_BY_INSERT(evtshared->ats_event)) + LocTriggerData.tg_newtable = evtshared->ats_table->new_ins_tuplestore; + else + LocTriggerData.tg_newtable = evtshared->ats_table->new_upd_tuplestore; evtshared->ats_table->closed = true; } } @@ -4637,8 +4667,10 @@ TransitionCaptureState * MakeTransitionCaptureState(TriggerDesc *trigdesc, Oid relid, CmdType cmdType) { TransitionCaptureState *state; - bool need_old, - need_new; + bool need_old_upd, + need_new_upd, + need_old_del, + need_new_ins; AfterTriggersTableData *table; MemoryContext oldcxt; ResourceOwner saveResourceOwner; @@ -4650,23 +4682,31 @@ MakeTransitionCaptureState(TriggerDesc *trigdesc, Oid relid, CmdType cmdType) switch (cmdType) { case CMD_INSERT: - need_old = false; - need_new = trigdesc->trig_insert_new_table; + need_old_upd = need_old_del = need_new_upd = false; + need_new_ins = trigdesc->trig_insert_new_table; break; case CMD_UPDATE: - need_old = trigdesc->trig_update_old_table; - need_new = trigdesc->trig_update_new_table; + need_old_upd = trigdesc->trig_update_old_table; + need_new_upd = trigdesc->trig_update_new_table; + need_old_del = need_new_ins = false; break; case CMD_DELETE: - need_old = trigdesc->trig_delete_old_table; - need_new = false; + need_old_del = trigdesc->trig_delete_old_table; + need_old_upd = need_new_upd = need_new_ins = false; + break; + case CMD_MERGE: + need_old_upd = trigdesc->trig_update_old_table; + need_new_upd = trigdesc->trig_update_new_table; + need_old_del = trigdesc->trig_delete_old_table; + need_new_ins = trigdesc->trig_insert_new_table; break; default: elog(ERROR, "unexpected CmdType: %d", (int) cmdType); - need_old = need_new = false; /* keep compiler quiet */ + /* keep compiler quiet */ + need_old_upd = need_new_upd = need_old_del = need_new_ins = false; break; } - if (!need_old && !need_new) + if (!need_old_upd && !need_new_upd && !need_new_ins && !need_old_del) return NULL; /* Check state, like AfterTriggerSaveEvent. */ @@ -4696,10 +4736,14 @@ MakeTransitionCaptureState(TriggerDesc *trigdesc, Oid relid, CmdType cmdType) saveResourceOwner = CurrentResourceOwner; CurrentResourceOwner = CurTransactionResourceOwner; - if (need_old && table->old_tuplestore == NULL) - table->old_tuplestore = tuplestore_begin_heap(false, false, work_mem); - if (need_new && table->new_tuplestore == NULL) - table->new_tuplestore = tuplestore_begin_heap(false, false, work_mem); + if (need_old_upd && table->old_upd_tuplestore == NULL) + table->old_upd_tuplestore = tuplestore_begin_heap(false, false, work_mem); + if (need_new_upd && table->new_upd_tuplestore == NULL) + table->new_upd_tuplestore = tuplestore_begin_heap(false, false, work_mem); + if (need_old_del && table->old_del_tuplestore == NULL) + table->old_del_tuplestore = tuplestore_begin_heap(false, false, work_mem); + if (need_new_ins && table->new_ins_tuplestore == NULL) + table->new_ins_tuplestore = tuplestore_begin_heap(false, false, work_mem); CurrentResourceOwner = saveResourceOwner; MemoryContextSwitchTo(oldcxt); @@ -4888,12 +4932,20 @@ AfterTriggerFreeQuery(AfterTriggersQueryData *qs) { AfterTriggersTableData *table = (AfterTriggersTableData *) lfirst(lc); - ts = table->old_tuplestore; - table->old_tuplestore = NULL; + ts = table->old_upd_tuplestore; + table->old_upd_tuplestore = NULL; if (ts) tuplestore_end(ts); - ts = table->new_tuplestore; - table->new_tuplestore = NULL; + ts = table->new_upd_tuplestore; + table->new_upd_tuplestore = NULL; + if (ts) + tuplestore_end(ts); + ts = table->old_del_tuplestore; + table->old_del_tuplestore = NULL; + if (ts) + tuplestore_end(ts); + ts = table->new_ins_tuplestore; + table->new_ins_tuplestore = NULL; if (ts) tuplestore_end(ts); } @@ -5744,12 +5796,11 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo, newtup == NULL)); if (oldtup != NULL && - ((event == TRIGGER_EVENT_DELETE && delete_old_table) || - (event == TRIGGER_EVENT_UPDATE && update_old_table))) + (event == TRIGGER_EVENT_DELETE && delete_old_table)) { Tuplestorestate *old_tuplestore; - old_tuplestore = transition_capture->tcs_private->old_tuplestore; + old_tuplestore = transition_capture->tcs_private->old_del_tuplestore; if (map != NULL) { @@ -5761,13 +5812,48 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo, else tuplestore_puttuple(old_tuplestore, oldtup); } + if (oldtup != NULL && + (event == TRIGGER_EVENT_UPDATE && update_old_table)) + { + Tuplestorestate *old_tuplestore; + + old_tuplestore = transition_capture->tcs_private->old_upd_tuplestore; + + if (map != NULL) + { + HeapTuple converted = do_convert_tuple(oldtup, map); + + tuplestore_puttuple(old_tuplestore, converted); + pfree(converted); + } + else + tuplestore_puttuple(old_tuplestore, oldtup); + } + if (newtup != NULL && + (event == TRIGGER_EVENT_INSERT && insert_new_table)) + { + Tuplestorestate *new_tuplestore; + + new_tuplestore = transition_capture->tcs_private->new_ins_tuplestore; + + if (original_insert_tuple != NULL) + tuplestore_puttuple(new_tuplestore, original_insert_tuple); + else if (map != NULL) + { + HeapTuple converted = do_convert_tuple(newtup, map); + + tuplestore_puttuple(new_tuplestore, converted); + pfree(converted); + } + else + tuplestore_puttuple(new_tuplestore, newtup); + } if (newtup != NULL && - ((event == TRIGGER_EVENT_INSERT && insert_new_table) || - (event == TRIGGER_EVENT_UPDATE && update_new_table))) + (event == TRIGGER_EVENT_UPDATE && update_new_table)) { Tuplestorestate *new_tuplestore; - new_tuplestore = transition_capture->tcs_private->new_tuplestore; + new_tuplestore = transition_capture->tcs_private->new_upd_tuplestore; if (original_insert_tuple != NULL) tuplestore_puttuple(new_tuplestore, original_insert_tuple); diff --git a/src/backend/executor/Makefile b/src/backend/executor/Makefile index cc09895fa5c..68675f97966 100644 --- a/src/backend/executor/Makefile +++ b/src/backend/executor/Makefile @@ -22,7 +22,7 @@ OBJS = execAmi.o execCurrent.o execExpr.o execExprInterp.o \ nodeCustom.o nodeFunctionscan.o nodeGather.o \ nodeHash.o nodeHashjoin.o nodeIndexscan.o nodeIndexonlyscan.o \ nodeLimit.o nodeLockRows.o nodeGatherMerge.o \ - nodeMaterial.o nodeMergeAppend.o nodeMergejoin.o nodeModifyTable.o \ + nodeMaterial.o nodeMergeAppend.o nodeMergejoin.o nodeMerge.o nodeModifyTable.o \ nodeNestloop.o nodeProjectSet.o nodeRecursiveunion.o nodeResult.o \ nodeSamplescan.o nodeSeqscan.o nodeSetOp.o nodeSort.o nodeUnique.o \ nodeValuesscan.o \ diff --git a/src/backend/executor/README b/src/backend/executor/README index 0d7cd552eb6..05769772b77 100644 --- a/src/backend/executor/README +++ b/src/backend/executor/README @@ -37,6 +37,16 @@ the plan tree returns the computed tuples to be updated, plus a "junk" one. For DELETE, the plan tree need only deliver a CTID column, and the ModifyTable node visits each of those rows and marks the row deleted. +MERGE runs one generic plan that returns candidate target rows. Each row +consists of a super-row that contains all the columns needed by any of the +individual actions, plus a CTID and a TABLEOID junk columns. The CTID column is +required to know if a matching target row was found or not and the TABLEOID +column is needed to find the underlying target partition, in case when the +target table is a partition table. If the CTID column is set we attempt to +activate WHEN MATCHED actions, or if it is NULL then we will attempt to +activate WHEN NOT MATCHED actions. Once we know which action is activated we +form the final result row and apply only those changes. + XXX a great deal more documentation needs to be written here... diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index 9a107aba561..e4d9b0b3f88 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -233,6 +233,7 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags) case CMD_INSERT: case CMD_DELETE: case CMD_UPDATE: + case CMD_MERGE: estate->es_output_cid = GetCurrentCommandId(true); break; @@ -1357,6 +1358,9 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo, resultRelInfo->ri_onConflictArbiterIndexes = NIL; resultRelInfo->ri_onConflict = NULL; + resultRelInfo->ri_mergeTargetRTI = 0; + resultRelInfo->ri_mergeState = (MergeState *) palloc0(sizeof (MergeState)); + /* * Partition constraint, which also includes the partition constraint of * all the ancestors that are partitions. Note that it will be checked @@ -2205,6 +2209,19 @@ ExecWithCheckOptions(WCOKind kind, ResultRelInfo *resultRelInfo, errmsg("new row violates row-level security policy for table \"%s\"", wco->relname))); break; + case WCO_RLS_MERGE_UPDATE_CHECK: + case WCO_RLS_MERGE_DELETE_CHECK: + if (wco->polname != NULL) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("target row violates row-level security policy \"%s\" (USING expression) for table \"%s\"", + wco->polname, wco->relname))); + else + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("target row violates row-level security policy (USING expression) for table \"%s\"", + wco->relname))); + break; case WCO_RLS_CONFLICT_CHECK: if (wco->polname != NULL) ereport(ERROR, diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c index 9a131886491..a6a7885abd1 100644 --- a/src/backend/executor/execPartition.c +++ b/src/backend/executor/execPartition.c @@ -67,6 +67,8 @@ ExecSetupPartitionTupleRouting(ModifyTableState *mtstate, Relation rel) ResultRelInfo *update_rri = NULL; int num_update_rri = 0, update_rri_index = 0; + bool is_update = false; + bool is_merge = false; PartitionTupleRouting *proute; int nparts; ModifyTable *node = mtstate ? (ModifyTable *) mtstate->ps.plan : NULL; @@ -89,13 +91,22 @@ ExecSetupPartitionTupleRouting(ModifyTableState *mtstate, Relation rel) /* Set up details specific to the type of tuple routing we are doing. */ if (node && node->operation == CMD_UPDATE) + is_update = true; + else if (node && node->operation == CMD_MERGE) + is_merge = true; + + if (is_update) { update_rri = mtstate->resultRelInfo; num_update_rri = list_length(node->plans); proute->subplan_partition_offsets = palloc(num_update_rri * sizeof(int)); proute->num_subplan_partition_offsets = num_update_rri; + } + + if (is_update || is_merge) + { /* * We need an additional tuple slot for storing transient tuples that * are converted to the root table descriptor. @@ -300,6 +311,25 @@ ExecFindPartition(ResultRelInfo *resultRelInfo, PartitionDispatch *pd, } /* + * Given OID of the partition leaf, return the index of the leaf in the + * partition hierarchy. + */ +int +ExecFindPartitionByOid(PartitionTupleRouting *proute, Oid partoid) +{ + int i; + + for (i = 0; i < proute->num_partitions; i++) + { + if (proute->partition_oids[i] == partoid) + break; + } + + Assert(i < proute->num_partitions); + return i; +} + +/* * ExecInitPartitionInfo * Initialize ResultRelInfo and other information for a partition if not * already done @@ -337,6 +367,8 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, rootrel, estate->es_instrument); + leaf_part_rri->ri_PartitionLeafIndex = partidx; + /* * Verify result relation is a valid target for an INSERT. An UPDATE of a * partition-key becomes a DELETE+INSERT operation, so this check is still @@ -625,6 +657,90 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, Assert(proute->partitions[partidx] == NULL); proute->partitions[partidx] = leaf_part_rri; + /* + * Initialize information about this partition that's needed to handle + * MERGE. + */ + if (node && node->operation == CMD_MERGE) + { + TupleDesc partrelDesc = RelationGetDescr(partrel); + TupleConversionMap *map = proute->parent_child_tupconv_maps[partidx]; + int firstVarno = mtstate->resultRelInfo[0].ri_RangeTableIndex; + Relation firstResultRel = mtstate->resultRelInfo[0].ri_RelationDesc; + + /* + * If the root parent and partition have the same tuple + * descriptor, just reuse the original MERGE state for partition. + */ + if (map == NULL) + { + leaf_part_rri->ri_mergeState = resultRelInfo->ri_mergeState; + } + else + { + /* Convert expressions contain partition's attnos. */ + List *conv_tl, *conv_qual; + ListCell *l; + List *matchedActionStates = NIL; + List *notMatchedActionStates = NIL; + + foreach (l, node->mergeActionList) + { + MergeAction *action = lfirst_node(MergeAction, l); + MergeActionState *action_state = makeNode(MergeActionState); + TupleDesc tupDesc; + ExprContext *econtext; + + action_state->matched = action->matched; + action_state->commandType = action->commandType; + + conv_qual = (List *) action->qual; + conv_qual = map_partition_varattnos(conv_qual, + firstVarno, partrel, + firstResultRel, NULL); + + action_state->whenqual = ExecInitQual(conv_qual, &mtstate->ps); + + conv_tl = (List *) action->targetList; + conv_tl = map_partition_varattnos(conv_tl, + firstVarno, partrel, + firstResultRel, NULL); + + conv_tl = adjust_partition_tlist( conv_tl, map); + + tupDesc = ExecTypeFromTL(conv_tl, partrelDesc->tdhasoid); + action_state->tupDesc = tupDesc; + + /* build action projection state */ + econtext = mtstate->ps.ps_ExprContext; + action_state->proj = + ExecBuildProjectionInfo(conv_tl, econtext, + mtstate->mt_mergeproj, + &mtstate->ps, + partrelDesc); + + if (action_state->matched) + matchedActionStates = + lappend(matchedActionStates, action_state); + else + notMatchedActionStates = + lappend(notMatchedActionStates, action_state); + } + leaf_part_rri->ri_mergeState->matchedActionStates = + matchedActionStates; + leaf_part_rri->ri_mergeState->notMatchedActionStates = + notMatchedActionStates; + } + + /* + * get_partition_dispatch_recurse() and expand_partitioned_rtentry() + * fetch the leaf OIDs in the same order. So we can safely derive the + * index of the merge target relation corresponding to this partition + * by simply adding partidx + 1 to the root's merge target relation. + */ + leaf_part_rri->ri_mergeTargetRTI = node->mergeTargetRelation + + partidx + 1; + } MemoryContextSwitchTo(oldContext); return leaf_part_rri; diff --git a/src/backend/executor/execReplication.c b/src/backend/executor/execReplication.c index 32891abbdf5..971f92a938a 100644 --- a/src/backend/executor/execReplication.c +++ b/src/backend/executor/execReplication.c @@ -454,7 +454,7 @@ ExecSimpleRelationUpdate(EState *estate, EPQState *epqstate, { slot = ExecBRUpdateTriggers(estate, epqstate, resultRelInfo, &searchslot->tts_tuple->t_self, - NULL, slot); + NULL, slot, NULL); if (slot == NULL) /* "do nothing" */ skip_tuple = true; @@ -515,7 +515,7 @@ ExecSimpleRelationDelete(EState *estate, EPQState *epqstate, { skip_tuple = !ExecBRDeleteTriggers(estate, epqstate, resultRelInfo, &searchslot->tts_tuple->t_self, - NULL); + NULL, NULL); } if (!skip_tuple) diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c index 1b09868ff8e..b03db64e8e1 100644 --- a/src/backend/executor/nodeModifyTable.c +++ b/src/backend/executor/nodeModifyTable.c @@ -42,6 +42,7 @@ #include "commands/trigger.h" #include "executor/execPartition.h" #include "executor/executor.h" +#include "executor/nodeMerge.h" #include "executor/nodeModifyTable.h" #include "foreign/fdwapi.h" #include "miscadmin.h" @@ -62,17 +63,17 @@ static bool ExecOnConflictUpdate(ModifyTableState *mtstate, EState *estate, bool canSetTag, TupleTableSlot **returning); -static TupleTableSlot *ExecPrepareTupleRouting(ModifyTableState *mtstate, - EState *estate, - PartitionTupleRouting *proute, - ResultRelInfo *targetRelInfo, - TupleTableSlot *slot); static ResultRelInfo *getTargetResultRelInfo(ModifyTableState *node); static void ExecSetupChildParentMapForTcs(ModifyTableState *mtstate); static void ExecSetupChildParentMapForSubplan(ModifyTableState *mtstate); static TupleConversionMap *tupconv_map_for_subplan(ModifyTableState *node, int whichplan); +/* flags for mt_merge_subcommands */ +#define MERGE_INSERT 0x01 +#define MERGE_UPDATE 0x02 +#define MERGE_DELETE 0x04 + /* * Verify that the tuples to be produced by INSERT or UPDATE match the * target relation's rowtype @@ -259,11 +260,12 @@ ExecCheckTIDVisible(EState *estate, * Returns RETURNING result if any, otherwise NULL. * ---------------------------------------------------------------- */ -static TupleTableSlot * +extern TupleTableSlot * ExecInsert(ModifyTableState *mtstate, TupleTableSlot *slot, TupleTableSlot *planSlot, EState *estate, + MergeActionState *actionState, bool canSetTag) { HeapTuple tuple; @@ -390,9 +392,17 @@ ExecInsert(ModifyTableState *mtstate, * partition, we should instead check UPDATE policies, because we are * executing policies defined on the target table, and not those * defined on the child partitions. + * + * If we're running MERGE, we refer to the action that we're executing + * to know if we're doing an INSERT or UPDATE to a partition table. */ - wco_kind = (mtstate->operation == CMD_UPDATE) ? - WCO_RLS_UPDATE_CHECK : WCO_RLS_INSERT_CHECK; + if (mtstate->operation == CMD_UPDATE) + wco_kind = WCO_RLS_UPDATE_CHECK; + else if (mtstate->operation == CMD_MERGE) + wco_kind = (actionState->commandType == CMD_UPDATE) ? + WCO_RLS_UPDATE_CHECK : WCO_RLS_INSERT_CHECK; + else + wco_kind = WCO_RLS_INSERT_CHECK; /* * ExecWithCheckOptions() will skip any WCOs which are not of the kind @@ -617,10 +627,19 @@ ExecInsert(ModifyTableState *mtstate, * passed to foreign table triggers; it is NULL when the foreign * table has no relevant triggers. * + * MERGE passes actionState of the action it's currently executing; + * regular DELETE passes NULL. This is used by ExecDelete to know if it's + * being called from MERGE or regular DELETE operation. + * + * If the DELETE fails because the tuple is concurrently updated/deleted + * by this or some other transaction, hufdp is filled with the reason as + * well as other important information. Currently only MERGE needs this + * information. + * * Returns RETURNING result if any, otherwise NULL. * ---------------------------------------------------------------- */ -static TupleTableSlot * +TupleTableSlot * ExecDelete(ModifyTableState *mtstate, ItemPointer tupleid, HeapTuple oldtuple, @@ -629,6 +648,8 @@ ExecDelete(ModifyTableState *mtstate, EState *estate, bool *tupleDeleted, bool processReturning, + HeapUpdateFailureData *hufdp, + MergeActionState *actionState, bool canSetTag) { ResultRelInfo *resultRelInfo; @@ -642,6 +663,14 @@ ExecDelete(ModifyTableState *mtstate, *tupleDeleted = false; /* + * Initialize hufdp. Since the caller is only interested in the failure + * status, initialize with the state that is used to indicate successful + * operation. + */ + if (hufdp) + hufdp->result = HeapTupleMayBeUpdated; + + /* * get information on the (current) result relation */ resultRelInfo = estate->es_result_relation_info; @@ -654,7 +683,7 @@ ExecDelete(ModifyTableState *mtstate, bool dodelete; dodelete = ExecBRDeleteTriggers(estate, epqstate, resultRelInfo, - tupleid, oldtuple); + tupleid, oldtuple, hufdp); if (!dodelete) /* "do nothing" */ return NULL; @@ -721,6 +750,15 @@ ldelete:; estate->es_crosscheck_snapshot, true /* wait for commit */ , &hufd); + + /* + * Copy the necessary information, if the caller has asked for it. We + * must do this irrespective of whether the tuple was updated or + * deleted. + */ + if (hufdp) + *hufdp = hufd; + switch (result) { case HeapTupleSelfUpdated: @@ -755,7 +793,11 @@ ldelete:; errmsg("tuple to be updated was already modified by an operation triggered by the current command"), errhint("Consider using an AFTER trigger instead of a BEFORE trigger to propagate changes to other rows."))); - /* Else, already deleted by self; nothing to do */ + /* + * Else, already deleted by self; nothing to do but inform + * MERGE about it anyways so that it can take necessary + * action. + */ return NULL; case HeapTupleMayBeUpdated: @@ -766,14 +808,24 @@ ldelete:; ereport(ERROR, (errcode(ERRCODE_T_R_SERIALIZATION_FAILURE), errmsg("could not serialize access due to concurrent update"))); + if (!ItemPointerEquals(tupleid, &hufd.ctid)) { TupleTableSlot *epqslot; + /* + * If we're executing MERGE, then the onus of running + * EvalPlanQual() and handling its outcome lies with the + * caller. + */ + if (actionState != NULL) + return NULL; + + /* Normal DELETE path. */ epqslot = EvalPlanQual(estate, epqstate, resultRelationDesc, - resultRelInfo->ri_RangeTableIndex, + GetEPQRangeTableIndex(resultRelInfo), LockTupleExclusive, &hufd.ctid, hufd.xmax); @@ -783,7 +835,12 @@ ldelete:; goto ldelete; } } - /* tuple already deleted; nothing to do */ + + /* + * tuple already deleted; nothing to do. But MERGE might want + * to handle it differently. We've already filled-in hufdp + * with sufficient information for MERGE to look at. + */ return NULL; default: @@ -911,10 +968,21 @@ ldelete:; * foreign table triggers; it is NULL when the foreign table has * no relevant triggers. * + * MERGE passes actionState of the action it's currently executing; + * regular UPDATE passes NULL. This is used by ExecUpdate to know if it's + * being called from MERGE or regular UPDATE operation. ExecUpdate may + * pass this information to ExecInsert if it ends up running DELETE+INSERT + * for partition key updates. + * + * If the UPDATE fails because the tuple is concurrently updated/deleted + * by this or some other transaction, hufdp is filled with the reason as + * well as other important information. Currently only MERGE needs this + * information. + * * Returns RETURNING result if any, otherwise NULL. * ---------------------------------------------------------------- */ -static TupleTableSlot * +extern TupleTableSlot * ExecUpdate(ModifyTableState *mtstate, ItemPointer tupleid, HeapTuple oldtuple, @@ -922,6 +990,9 @@ ExecUpdate(ModifyTableState *mtstate, TupleTableSlot *planSlot, EPQState *epqstate, EState *estate, + bool *tuple_updated, + HeapUpdateFailureData *hufdp, + MergeActionState *actionState, bool canSetTag) { HeapTuple tuple; @@ -938,6 +1009,17 @@ ExecUpdate(ModifyTableState *mtstate, if (IsBootstrapProcessingMode()) elog(ERROR, "cannot UPDATE during bootstrap"); + if (tuple_updated) + *tuple_updated = false; + + /* + * Initialize hufdp. Since the caller is only interested in the failure + * status, initialize with the state that is used to indicate successful + * operation. + */ + if (hufdp) + hufdp->result = HeapTupleMayBeUpdated; + /* * get the heap tuple out of the tuple table slot, making sure we have a * writable copy @@ -955,7 +1037,7 @@ ExecUpdate(ModifyTableState *mtstate, resultRelInfo->ri_TrigDesc->trig_update_before_row) { slot = ExecBRUpdateTriggers(estate, epqstate, resultRelInfo, - tupleid, oldtuple, slot); + tupleid, oldtuple, slot, hufdp); if (slot == NULL) /* "do nothing" */ return NULL; @@ -1001,7 +1083,6 @@ ExecUpdate(ModifyTableState *mtstate, } else { - LockTupleMode lockmode; bool partition_constraint_failed; /* @@ -1079,8 +1160,9 @@ lreplace:; * Row movement, part 1. Delete the tuple, but skip RETURNING * processing. We want to return rows from INSERT. */ - ExecDelete(mtstate, tupleid, oldtuple, planSlot, epqstate, estate, - &tuple_deleted, false, false); + ExecDelete(mtstate, tupleid, oldtuple, planSlot, epqstate, + estate, &tuple_deleted, false, hufdp, NULL, + false); /* * For some reason if DELETE didn't happen (e.g. trigger prevented @@ -1116,16 +1198,36 @@ lreplace:; saved_tcs_map = mtstate->mt_transition_capture->tcs_map; /* - * resultRelInfo is one of the per-subplan resultRelInfos. So we - * should convert the tuple into root's tuple descriptor, since - * ExecInsert() starts the search from root. The tuple conversion - * map list is in the order of mtstate->resultRelInfo[], so to - * retrieve the one for this resultRel, we need to know the - * position of the resultRel in mtstate->resultRelInfo[]. + * We should convert the tuple into root's tuple descriptor, since + * ExecInsert() starts the search from root. To do that, we need to + * retrieve the tuple conversion map for this resultRelInfo. + * + * If we're running MERGE then resultRelInfo is per-partition + * resultRelInfo as initialized in ExecInitPartitionInfo(). Note + * that we don't expand inheritance for the resultRelation in case + * of MERGE and hence there is just one subplan. Whereas for + * regular UPDATE, resultRelInfo is one of the per-subplan + * resultRelInfos. In either case the position of this partition in + * tracked in ri_PartitionLeafIndex; + * + * Retrieve the map either by looking at the resultRelInfo's + * position in mtstate->resultRelInfo[] (for UPDATE) or by simply + * using the ri_PartitionLeafIndex value (for MERGE). */ - map_index = resultRelInfo - mtstate->resultRelInfo; - Assert(map_index >= 0 && map_index < mtstate->mt_nplans); - tupconv_map = tupconv_map_for_subplan(mtstate, map_index); + if (mtstate->operation == CMD_MERGE) + { + map_index = resultRelInfo->ri_PartitionLeafIndex; + Assert(mtstate->rootResultRelInfo == NULL); + tupconv_map = TupConvMapForLeaf(proute, + mtstate->resultRelInfo, + map_index); + } + else + { + map_index = resultRelInfo - mtstate->resultRelInfo; + Assert(map_index >= 0 && map_index < mtstate->mt_nplans); + tupconv_map = tupconv_map_for_subplan(mtstate, map_index); + } tuple = ConvertPartitionTupleSlot(tupconv_map, tuple, proute->root_tuple_slot, @@ -1135,12 +1237,16 @@ lreplace:; * Prepare for tuple routing, making it look like we're inserting * into the root. */ - Assert(mtstate->rootResultRelInfo != NULL); slot = ExecPrepareTupleRouting(mtstate, estate, proute, - mtstate->rootResultRelInfo, slot); + getTargetResultRelInfo(mtstate), + slot); ret_slot = ExecInsert(mtstate, slot, planSlot, - estate, canSetTag); + estate, actionState, canSetTag); + + /* Update is successful. */ + if (tuple_updated) + *tuple_updated = true; /* Revert ExecPrepareTupleRouting's node change. */ estate->es_result_relation_info = resultRelInfo; @@ -1178,7 +1284,16 @@ lreplace:; estate->es_output_cid, estate->es_crosscheck_snapshot, true /* wait for commit */ , - &hufd, &lockmode); + &hufd); + + /* + * Copy the necessary information, if the caller has asked for it. We + * must do this irrespective of whether the tuple was updated or + * deleted. + */ + if (hufdp) + *hufdp = hufd; + switch (result) { case HeapTupleSelfUpdated: @@ -1223,26 +1338,42 @@ lreplace:; ereport(ERROR, (errcode(ERRCODE_T_R_SERIALIZATION_FAILURE), errmsg("could not serialize access due to concurrent update"))); + if (!ItemPointerEquals(tupleid, &hufd.ctid)) { TupleTableSlot *epqslot; + /* + * If we're executing MERGE, then the onus of running + * EvalPlanQual() and handling its outcome lies with the + * caller. + */ + if (actionState != NULL) + return NULL; + + /* Regular UPDATE path. */ epqslot = EvalPlanQual(estate, epqstate, resultRelationDesc, - resultRelInfo->ri_RangeTableIndex, - lockmode, + GetEPQRangeTableIndex(resultRelInfo), + hufd.lockmode, &hufd.ctid, hufd.xmax); if (!TupIsNull(epqslot)) { *tupleid = hufd.ctid; + /* Normal UPDATE path */ slot = ExecFilterJunk(resultRelInfo->ri_junkFilter, epqslot); tuple = ExecMaterializeSlot(slot); goto lreplace; } } - /* tuple already deleted; nothing to do */ + + /* + * tuple already deleted; nothing to do. But MERGE might want + * to handle it differently. We've already filled-in hufdp + * with sufficient information for MERGE to look at. + */ return NULL; default: @@ -1271,6 +1402,9 @@ lreplace:; estate, false, NULL, NIL); } + if (tuple_updated) + *tuple_updated = true; + if (canSetTag) (estate->es_processed)++; @@ -1365,9 +1499,9 @@ ExecOnConflictUpdate(ModifyTableState *mtstate, * there's no historical behavior to break. * * It is the user's responsibility to prevent this situation from - * occurring. These problems are why SQL-2003 similarly specifies - * that for SQL MERGE, an exception must be raised in the event of - * an attempt to update the same row twice. + * occurring. These problems are why SQL Standard similarly + * specifies that for SQL MERGE, an exception must be raised in + * the event of an attempt to update the same row twice. */ if (TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetXmin(tuple.t_data))) ereport(ERROR, @@ -1489,7 +1623,7 @@ ExecOnConflictUpdate(ModifyTableState *mtstate, *returning = ExecUpdate(mtstate, &tuple.t_self, NULL, mtstate->mt_conflproj, planSlot, &mtstate->mt_epqstate, mtstate->ps.state, - canSetTag); + NULL, NULL, NULL, canSetTag); ReleaseBuffer(buffer); return true; @@ -1527,6 +1661,14 @@ fireBSTriggers(ModifyTableState *node) case CMD_DELETE: ExecBSDeleteTriggers(node->ps.state, resultRelInfo); break; + case CMD_MERGE: + if (node->mt_merge_subcommands & MERGE_INSERT) + ExecBSInsertTriggers(node->ps.state, resultRelInfo); + if (node->mt_merge_subcommands & MERGE_UPDATE) + ExecBSUpdateTriggers(node->ps.state, resultRelInfo); + if (node->mt_merge_subcommands & MERGE_DELETE) + ExecBSDeleteTriggers(node->ps.state, resultRelInfo); + break; default: elog(ERROR, "unknown operation"); break; @@ -1582,6 +1724,17 @@ fireASTriggers(ModifyTableState *node) ExecASDeleteTriggers(node->ps.state, resultRelInfo, node->mt_transition_capture); break; + case CMD_MERGE: + if (node->mt_merge_subcommands & MERGE_DELETE) + ExecASDeleteTriggers(node->ps.state, resultRelInfo, + node->mt_transition_capture); + if (node->mt_merge_subcommands & MERGE_UPDATE) + ExecASUpdateTriggers(node->ps.state, resultRelInfo, + node->mt_transition_capture); + if (node->mt_merge_subcommands & MERGE_INSERT) + ExecASInsertTriggers(node->ps.state, resultRelInfo, + node->mt_transition_capture); + break; default: elog(ERROR, "unknown operation"); break; @@ -1644,7 +1797,7 @@ ExecSetupTransitionCaptureState(ModifyTableState *mtstate, EState *estate) * * Returns a slot holding the tuple of the partition rowtype. */ -static TupleTableSlot * +TupleTableSlot * ExecPrepareTupleRouting(ModifyTableState *mtstate, EState *estate, PartitionTupleRouting *proute, @@ -1967,6 +2120,7 @@ ExecModifyTable(PlanState *pstate) { /* advance to next subplan if any */ node->mt_whichplan++; + if (node->mt_whichplan < node->mt_nplans) { resultRelInfo++; @@ -2015,6 +2169,12 @@ ExecModifyTable(PlanState *pstate) EvalPlanQualSetSlot(&node->mt_epqstate, planSlot); slot = planSlot; + if (operation == CMD_MERGE) + { + ExecMerge(node, estate, slot, junkfilter, resultRelInfo); + continue; + } + tupleid = NULL; oldtuple = NULL; if (junkfilter != NULL) @@ -2096,19 +2256,20 @@ ExecModifyTable(PlanState *pstate) slot = ExecPrepareTupleRouting(node, estate, proute, resultRelInfo, slot); slot = ExecInsert(node, slot, planSlot, - estate, node->canSetTag); + estate, NULL, node->canSetTag); /* Revert ExecPrepareTupleRouting's state change. */ if (proute) estate->es_result_relation_info = resultRelInfo; break; case CMD_UPDATE: slot = ExecUpdate(node, tupleid, oldtuple, slot, planSlot, - &node->mt_epqstate, estate, node->canSetTag); + &node->mt_epqstate, estate, + NULL, NULL, NULL, node->canSetTag); break; case CMD_DELETE: slot = ExecDelete(node, tupleid, oldtuple, planSlot, &node->mt_epqstate, estate, - NULL, true, node->canSetTag); + NULL, true, NULL, NULL, node->canSetTag); break; default: elog(ERROR, "unknown operation"); @@ -2198,6 +2359,16 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags) saved_resultRelInfo = estate->es_result_relation_info; resultRelInfo = mtstate->resultRelInfo; + + /* + * mergeTargetRelation must be set if we're running MERGE and mustn't be + * set if we're not. + */ + Assert(operation != CMD_MERGE || node->mergeTargetRelation > 0); + Assert(operation == CMD_MERGE || node->mergeTargetRelation == 0); + + resultRelInfo->ri_mergeTargetRTI = node->mergeTargetRelation; + i = 0; foreach(l, node->plans) { @@ -2276,7 +2447,8 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags) * partition key. */ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE && - (operation == CMD_INSERT || update_tuple_routing_needed)) + (operation == CMD_INSERT || operation == CMD_MERGE || + update_tuple_routing_needed)) mtstate->mt_partition_tuple_routing = ExecSetupPartitionTupleRouting(mtstate, rel); @@ -2288,6 +2460,15 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags) ExecSetupTransitionCaptureState(mtstate, estate); /* + * If we are doing MERGE then setup child-parent mapping. This will be + * required in case we end up doing a partition-key update, triggering a + * tuple routing. + */ + if (mtstate->operation == CMD_MERGE && + mtstate->mt_partition_tuple_routing != NULL) + ExecSetupChildParentMapForLeaf(mtstate->mt_partition_tuple_routing); + + /* * Construct mapping from each of the per-subplan partition attnos to the * root attno. This is required when during update row movement the tuple * descriptor of a source partition does not match the root partitioned @@ -2478,6 +2659,106 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags) } } + resultRelInfo = mtstate->resultRelInfo; + + if (node->mergeActionList) + { + ListCell *l; + ExprContext *econtext; + List *mergeMatchedActionStates = NIL; + List *mergeNotMatchedActionStates = NIL; + TupleDesc relationDesc = resultRelInfo->ri_RelationDesc->rd_att; + + mtstate->mt_merge_subcommands = 0; + + if (mtstate->ps.ps_ExprContext == NULL) + ExecAssignExprContext(estate, &mtstate->ps); + + econtext = mtstate->ps.ps_ExprContext; + + /* initialize slot for the existing tuple */ + Assert(mtstate->mt_existing == NULL); + mtstate->mt_existing = + ExecInitExtraTupleSlot(mtstate->ps.state, + mtstate->mt_partition_tuple_routing ? + NULL : relationDesc); + + /* initialize slot for merge actions */ + Assert(mtstate->mt_mergeproj == NULL); + mtstate->mt_mergeproj = + ExecInitExtraTupleSlot(mtstate->ps.state, + mtstate->mt_partition_tuple_routing ? + NULL : relationDesc); + + /* + * Create a MergeActionState for each action on the mergeActionList + * and add it to either a list of matched actions or not-matched + * actions. + */ + foreach(l, node->mergeActionList) + { + MergeAction *action = (MergeAction *) lfirst(l); + MergeActionState *action_state = makeNode(MergeActionState); + TupleDesc tupDesc; + + action_state->matched = action->matched; + action_state->commandType = action->commandType; + action_state->whenqual = ExecInitQual((List *) action->qual, + &mtstate->ps); + + /* create target slot for this action's projection */ + tupDesc = ExecTypeFromTL((List *) action->targetList, + resultRelInfo->ri_RelationDesc->rd_rel->relhasoids); + action_state->tupDesc = tupDesc; + + /* build action projection state */ + action_state->proj = + ExecBuildProjectionInfo(action->targetList, econtext, + mtstate->mt_mergeproj, &mtstate->ps, + resultRelInfo->ri_RelationDesc->rd_att); + + /* + * We create two lists - one for WHEN MATCHED actions and one + * for WHEN NOT MATCHED actions - and stick the + * MergeActionState into the appropriate list. + */ + if (action_state->matched) + mergeMatchedActionStates = + lappend(mergeMatchedActionStates, action_state); + else + mergeNotMatchedActionStates = + lappend(mergeNotMatchedActionStates, action_state); + + switch (action->commandType) + { + case CMD_INSERT: + ExecCheckPlanOutput(resultRelInfo->ri_RelationDesc, + action->targetList); + mtstate->mt_merge_subcommands |= MERGE_INSERT; + break; + case CMD_UPDATE: + ExecCheckPlanOutput(resultRelInfo->ri_RelationDesc, + action->targetList); + mtstate->mt_merge_subcommands |= MERGE_UPDATE; + break; + case CMD_DELETE: + mtstate->mt_merge_subcommands |= MERGE_DELETE; + break; + case CMD_NOTHING: + break; + default: + elog(ERROR, "unknown operation"); + break; + } + + resultRelInfo->ri_mergeState->matchedActionStates = + mergeMatchedActionStates; + resultRelInfo->ri_mergeState->notMatchedActionStates = + mergeNotMatchedActionStates; + + } + } + /* select first subplan */ mtstate->mt_whichplan = 0; subplan = (Plan *) linitial(node->plans); @@ -2491,7 +2772,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags) * --- no need to look first. Typically, this will be a 'ctid' or * 'wholerow' attribute, but in the case of a foreign data wrapper it * might be a set of junk attributes sufficient to identify the remote - * row. + * row. We follow this logic for MERGE, so it always has a junk attributes. * * If there are multiple result relations, each one needs its own junk * filter. Note multiple rels are only possible for UPDATE/DELETE, so we @@ -2519,6 +2800,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags) break; case CMD_UPDATE: case CMD_DELETE: + case CMD_MERGE: junk_filter_needed = true; break; default: @@ -2534,6 +2816,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags) JunkFilter *j; subplan = mtstate->mt_plans[i]->plan; + if (operation == CMD_INSERT || operation == CMD_UPDATE) ExecCheckPlanOutput(resultRelInfo->ri_RelationDesc, subplan->targetlist); @@ -2542,7 +2825,9 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags) resultRelInfo->ri_RelationDesc->rd_att->tdhasoid, ExecInitExtraTupleSlot(estate, NULL)); - if (operation == CMD_UPDATE || operation == CMD_DELETE) + if (operation == CMD_UPDATE || + operation == CMD_DELETE || + operation == CMD_MERGE) { /* For UPDATE/DELETE, find the appropriate junk attr now */ char relkind; @@ -2555,6 +2840,15 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags) j->jf_junkAttNo = ExecFindJunkAttribute(j, "ctid"); if (!AttributeNumberIsValid(j->jf_junkAttNo)) elog(ERROR, "could not find junk ctid column"); + + if (operation == CMD_MERGE && + relkind == RELKIND_PARTITIONED_TABLE) + { + j->jf_otherJunkAttNo = ExecFindJunkAttribute(j, "tableoid"); + if (!AttributeNumberIsValid(j->jf_otherJunkAttNo)) + elog(ERROR, "could not find junk tableoid column"); + + } } else if (relkind == RELKIND_FOREIGN_TABLE) { diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c index 08f6f67a15c..a49015e7cbc 100644 --- a/src/backend/executor/spi.c +++ b/src/backend/executor/spi.c @@ -2420,6 +2420,9 @@ _SPI_pquery(QueryDesc *queryDesc, bool fire_triggers, uint64 tcount) else res = SPI_OK_UPDATE; break; + case CMD_MERGE: + res = SPI_OK_MERGE; + break; default: return SPI_ERROR_OPUNKNOWN; } diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c index c7293a60d78..770ed3b1a88 100644 --- a/src/backend/nodes/copyfuncs.c +++ b/src/backend/nodes/copyfuncs.c @@ -207,6 +207,7 @@ _copyModifyTable(const ModifyTable *from) COPY_NODE_FIELD(partitioned_rels); COPY_SCALAR_FIELD(partColsUpdated); COPY_NODE_FIELD(resultRelations); + COPY_SCALAR_FIELD(mergeTargetRelation); COPY_SCALAR_FIELD(resultRelIndex); COPY_SCALAR_FIELD(rootResultRelIndex); COPY_NODE_FIELD(plans); @@ -222,6 +223,8 @@ _copyModifyTable(const ModifyTable *from) COPY_NODE_FIELD(onConflictWhere); COPY_SCALAR_FIELD(exclRelRTI); COPY_NODE_FIELD(exclRelTlist); + COPY_NODE_FIELD(mergeSourceTargetList); + COPY_NODE_FIELD(mergeActionList); return newnode; } @@ -2977,6 +2980,9 @@ _copyQuery(const Query *from) COPY_NODE_FIELD(setOperations); COPY_NODE_FIELD(constraintDeps); COPY_NODE_FIELD(withCheckOptions); + COPY_SCALAR_FIELD(mergeTarget_relation); + COPY_NODE_FIELD(mergeSourceTargetList); + COPY_NODE_FIELD(mergeActionList); COPY_LOCATION_FIELD(stmt_location); COPY_LOCATION_FIELD(stmt_len); @@ -3040,6 +3046,34 @@ _copyUpdateStmt(const UpdateStmt *from) return newnode; } +static MergeStmt * +_copyMergeStmt(const MergeStmt *from) +{ + MergeStmt *newnode = makeNode(MergeStmt); + + COPY_NODE_FIELD(relation); + COPY_NODE_FIELD(source_relation); + COPY_NODE_FIELD(join_condition); + COPY_NODE_FIELD(mergeActionList); + + return newnode; +} + +static MergeAction * +_copyMergeAction(const MergeAction *from) +{ + MergeAction *newnode = makeNode(MergeAction); + + COPY_SCALAR_FIELD(matched); + COPY_SCALAR_FIELD(commandType); + COPY_NODE_FIELD(condition); + COPY_NODE_FIELD(qual); + COPY_NODE_FIELD(stmt); + COPY_NODE_FIELD(targetList); + + return newnode; +} + static SelectStmt * _copySelectStmt(const SelectStmt *from) { @@ -5102,6 +5136,12 @@ copyObjectImpl(const void *from) case T_UpdateStmt: retval = _copyUpdateStmt(from); break; + case T_MergeStmt: + retval = _copyMergeStmt(from); + break; + case T_MergeAction: + retval = _copyMergeAction(from); + break; case T_SelectStmt: retval = _copySelectStmt(from); break; diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c index 765b1be74b3..5a0151eece5 100644 --- a/src/backend/nodes/equalfuncs.c +++ b/src/backend/nodes/equalfuncs.c @@ -987,6 +987,8 @@ _equalQuery(const Query *a, const Query *b) COMPARE_NODE_FIELD(setOperations); COMPARE_NODE_FIELD(constraintDeps); COMPARE_NODE_FIELD(withCheckOptions); + COMPARE_NODE_FIELD(mergeSourceTargetList); + COMPARE_NODE_FIELD(mergeActionList); COMPARE_LOCATION_FIELD(stmt_location); COMPARE_LOCATION_FIELD(stmt_len); @@ -1043,6 +1045,30 @@ _equalUpdateStmt(const UpdateStmt *a, const UpdateStmt *b) } static bool +_equalMergeStmt(const MergeStmt *a, const MergeStmt *b) +{ + COMPARE_NODE_FIELD(relation); + COMPARE_NODE_FIELD(source_relation); + COMPARE_NODE_FIELD(join_condition); + COMPARE_NODE_FIELD(mergeActionList); + + return true; +} + +static bool +_equalMergeAction(const MergeAction *a, const MergeAction *b) +{ + COMPARE_SCALAR_FIELD(matched); + COMPARE_SCALAR_FIELD(commandType); + COMPARE_NODE_FIELD(condition); + COMPARE_NODE_FIELD(qual); + COMPARE_NODE_FIELD(stmt); + COMPARE_NODE_FIELD(targetList); + + return true; +} + +static bool _equalSelectStmt(const SelectStmt *a, const SelectStmt *b) { COMPARE_NODE_FIELD(distinctClause); @@ -3233,6 +3259,12 @@ equal(const void *a, const void *b) case T_UpdateStmt: retval = _equalUpdateStmt(a, b); break; + case T_MergeStmt: + retval = _equalMergeStmt(a, b); + break; + case T_MergeAction: + retval = _equalMergeAction(a, b); + break; case T_SelectStmt: retval = _equalSelectStmt(a, b); break; diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index 6c76c41ebea..3c302db0575 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -2146,6 +2146,16 @@ expression_tree_walker(Node *node, return true; } break; + case T_MergeAction: + { + MergeAction *action = (MergeAction *) node; + + if (walker(action->targetList, context)) + return true; + if (walker(action->qual, context)) + return true; + } + break; case T_JoinExpr: { JoinExpr *join = (JoinExpr *) node; @@ -2255,6 +2265,10 @@ query_tree_walker(Query *query, return true; if (walker((Node *) query->onConflict, context)) return true; + if (walker((Node *) query->mergeSourceTargetList, context)) + return true; + if (walker((Node *) query->mergeActionList, context)) + return true; if (walker((Node *) query->returningList, context)) return true; if (walker((Node *) query->jointree, context)) @@ -2932,6 +2946,18 @@ expression_tree_mutator(Node *node, return (Node *) newnode; } break; + case T_MergeAction: + { + MergeAction *action = (MergeAction *) node; + MergeAction *newnode; + + FLATCOPY(newnode, action, MergeAction); + MUTATE(newnode->qual, action->qual, Node *); + MUTATE(newnode->targetList, action->targetList, List *); + + return (Node *) newnode; + } + break; case T_JoinExpr: { JoinExpr *join = (JoinExpr *) node; @@ -3083,6 +3109,8 @@ query_tree_mutator(Query *query, MUTATE(query->targetList, query->targetList, List *); MUTATE(query->withCheckOptions, query->withCheckOptions, List *); MUTATE(query->onConflict, query->onConflict, OnConflictExpr *); + MUTATE(query->mergeSourceTargetList, query->mergeSourceTargetList, List *); + MUTATE(query->mergeActionList, query->mergeActionList, List *); MUTATE(query->returningList, query->returningList, List *); MUTATE(query->jointree, query->jointree, FromExpr *); MUTATE(query->setOperations, query->setOperations, Node *); @@ -3224,9 +3252,9 @@ query_or_expression_tree_mutator(Node *node, * boundaries: we descend to everything that's possibly interesting. * * Currently, the node type coverage here extends only to DML statements - * (SELECT/INSERT/UPDATE/DELETE) and nodes that can appear in them, because - * this is used mainly during analysis of CTEs, and only DML statements can - * appear in CTEs. + * (SELECT/INSERT/UPDATE/DELETE/MERGE) and nodes that can appear in them, + * because this is used mainly during analysis of CTEs, and only DML + * statements can appear in CTEs. */ bool raw_expression_tree_walker(Node *node, @@ -3406,6 +3434,30 @@ raw_expression_tree_walker(Node *node, return true; } break; + case T_MergeStmt: + { + MergeStmt *stmt = (MergeStmt *) node; + + if (walker(stmt->relation, context)) + return true; + if (walker(stmt->source_relation, context)) + return true; + if (walker(stmt->join_condition, context)) + return true; + if (walker(stmt->mergeActionList, context)) + return true; + } + break; + case T_MergeAction: + { + MergeAction *action = (MergeAction *) node; + + if (walker(action->targetList, context)) + return true; + if (walker(action->qual, context)) + return true; + } + break; case T_SelectStmt: { SelectStmt *stmt = (SelectStmt *) node; diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index f61ae03ac50..c8d962670e2 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -375,6 +375,7 @@ _outModifyTable(StringInfo str, const ModifyTable *node) WRITE_NODE_FIELD(partitioned_rels); WRITE_BOOL_FIELD(partColsUpdated); WRITE_NODE_FIELD(resultRelations); + WRITE_INT_FIELD(mergeTargetRelation); WRITE_INT_FIELD(resultRelIndex); WRITE_INT_FIELD(rootResultRelIndex); WRITE_NODE_FIELD(plans); @@ -390,6 +391,21 @@ _outModifyTable(StringInfo str, const ModifyTable *node) WRITE_NODE_FIELD(onConflictWhere); WRITE_UINT_FIELD(exclRelRTI); WRITE_NODE_FIELD(exclRelTlist); + WRITE_NODE_FIELD(mergeSourceTargetList); + WRITE_NODE_FIELD(mergeActionList); +} + +static void +_outMergeAction(StringInfo str, const MergeAction *node) +{ + WRITE_NODE_TYPE("MERGEACTION"); + + WRITE_BOOL_FIELD(matched); + WRITE_ENUM_FIELD(commandType, CmdType); + WRITE_NODE_FIELD(condition); + WRITE_NODE_FIELD(qual); + WRITE_NODE_FIELD(stmt); + WRITE_NODE_FIELD(targetList); } static void @@ -2114,6 +2130,7 @@ _outModifyTablePath(StringInfo str, const ModifyTablePath *node) WRITE_NODE_FIELD(partitioned_rels); WRITE_BOOL_FIELD(partColsUpdated); WRITE_NODE_FIELD(resultRelations); + WRITE_INT_FIELD(mergeTargetRelation); WRITE_NODE_FIELD(subpaths); WRITE_NODE_FIELD(subroots); WRITE_NODE_FIELD(withCheckOptionLists); @@ -2121,6 +2138,8 @@ _outModifyTablePath(StringInfo str, const ModifyTablePath *node) WRITE_NODE_FIELD(rowMarks); WRITE_NODE_FIELD(onconflict); WRITE_INT_FIELD(epqParam); + WRITE_NODE_FIELD(mergeSourceTargetList); + WRITE_NODE_FIELD(mergeActionList); } static void @@ -2942,6 +2961,9 @@ _outQuery(StringInfo str, const Query *node) WRITE_NODE_FIELD(setOperations); WRITE_NODE_FIELD(constraintDeps); /* withCheckOptions intentionally omitted, see comment in parsenodes.h */ + WRITE_INT_FIELD(mergeTarget_relation); + WRITE_NODE_FIELD(mergeSourceTargetList); + WRITE_NODE_FIELD(mergeActionList); WRITE_LOCATION_FIELD(stmt_location); WRITE_LOCATION_FIELD(stmt_len); } @@ -3657,6 +3679,9 @@ outNode(StringInfo str, const void *obj) case T_ModifyTable: _outModifyTable(str, obj); break; + case T_MergeAction: + _outMergeAction(str, obj); + break; case T_Append: _outAppend(str, obj); break; diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index d02d4ec5b7c..4518fa0cdb7 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -270,6 +270,9 @@ _readQuery(void) READ_NODE_FIELD(setOperations); READ_NODE_FIELD(constraintDeps); /* withCheckOptions intentionally omitted, see comment in parsenodes.h */ + READ_INT_FIELD(mergeTarget_relation); + READ_NODE_FIELD(mergeSourceTargetList); + READ_NODE_FIELD(mergeActionList); READ_LOCATION_FIELD(stmt_location); READ_LOCATION_FIELD(stmt_len); @@ -1576,6 +1579,7 @@ _readModifyTable(void) READ_NODE_FIELD(partitioned_rels); READ_BOOL_FIELD(partColsUpdated); READ_NODE_FIELD(resultRelations); + READ_INT_FIELD(mergeTargetRelation); READ_INT_FIELD(resultRelIndex); READ_INT_FIELD(rootResultRelIndex); READ_NODE_FIELD(plans); @@ -1591,6 +1595,26 @@ _readModifyTable(void) READ_NODE_FIELD(onConflictWhere); READ_UINT_FIELD(exclRelRTI); READ_NODE_FIELD(exclRelTlist); + READ_NODE_FIELD(mergeSourceTargetList); + READ_NODE_FIELD(mergeActionList); + + READ_DONE(); +} + +/* + * _readMergeAction + */ +static MergeAction * +_readMergeAction(void) +{ + READ_LOCALS(MergeAction); + + READ_BOOL_FIELD(matched); + READ_ENUM_FIELD(commandType, CmdType); + READ_NODE_FIELD(condition); + READ_NODE_FIELD(qual); + READ_NODE_FIELD(stmt); + READ_NODE_FIELD(targetList); READ_DONE(); } @@ -2594,6 +2618,8 @@ parseNodeString(void) return_value = _readProjectSet(); else if (MATCH("MODIFYTABLE", 11)) return_value = _readModifyTable(); + else if (MATCH("MERGEACTION", 11)) + return_value = _readMergeAction(); else if (MATCH("APPEND", 6)) return_value = _readAppend(); else if (MATCH("MERGEAPPEND", 11)) diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c index ccdd5cdaba2..99d07360293 100644 --- a/src/backend/optimizer/plan/createplan.c +++ b/src/backend/optimizer/plan/createplan.c @@ -288,9 +288,13 @@ static ModifyTable *make_modifytable(PlannerInfo *root, CmdType operation, bool canSetTag, Index nominalRelation, List *partitioned_rels, bool partColsUpdated, - List *resultRelations, List *subplans, + List *resultRelations, + Index mergeTargetRelation, + List *subplans, List *withCheckOptionLists, List *returningLists, - List *rowMarks, OnConflictExpr *onconflict, int epqParam); + List *rowMarks, OnConflictExpr *onconflict, + List *mergeSourceTargetList, + List *mergeActionList, int epqParam); static GatherMerge *create_gather_merge_plan(PlannerInfo *root, GatherMergePath *best_path); @@ -2446,11 +2450,14 @@ create_modifytable_plan(PlannerInfo *root, ModifyTablePath *best_path) best_path->partitioned_rels, best_path->partColsUpdated, best_path->resultRelations, + best_path->mergeTargetRelation, subplans, best_path->withCheckOptionLists, best_path->returningLists, best_path->rowMarks, best_path->onconflict, + best_path->mergeSourceTargetList, + best_path->mergeActionList, best_path->epqParam); copy_generic_path_info(&plan->plan, &best_path->path); @@ -6517,9 +6524,13 @@ make_modifytable(PlannerInfo *root, CmdType operation, bool canSetTag, Index nominalRelation, List *partitioned_rels, bool partColsUpdated, - List *resultRelations, List *subplans, + List *resultRelations, + Index mergeTargetRelation, + List *subplans, List *withCheckOptionLists, List *returningLists, - List *rowMarks, OnConflictExpr *onconflict, int epqParam) + List *rowMarks, OnConflictExpr *onconflict, + List *mergeSourceTargetList, + List *mergeActionList, int epqParam) { ModifyTable *node = makeNode(ModifyTable); List *fdw_private_list; @@ -6545,6 +6556,7 @@ make_modifytable(PlannerInfo *root, node->partitioned_rels = partitioned_rels; node->partColsUpdated = partColsUpdated; node->resultRelations = resultRelations; + node->mergeTargetRelation = mergeTargetRelation; node->resultRelIndex = -1; /* will be set correctly in setrefs.c */ node->rootResultRelIndex = -1; /* will be set correctly in setrefs.c */ node->plans = subplans; @@ -6577,6 +6589,8 @@ make_modifytable(PlannerInfo *root, node->withCheckOptionLists = withCheckOptionLists; node->returningLists = returningLists; node->rowMarks = rowMarks; + node->mergeSourceTargetList = mergeSourceTargetList; + node->mergeActionList = mergeActionList; node->epqParam = epqParam; /* diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index 53ed6f8a17f..15c8d34c704 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -794,6 +794,24 @@ subquery_planner(PlannerGlobal *glob, Query *parse, /* exclRelTlist contains only Vars, so no preprocessing needed */ } + foreach(l, parse->mergeActionList) + { + MergeAction *action = (MergeAction *) lfirst(l); + + action->targetList = (List *) + preprocess_expression(root, + (Node *) action->targetList, + EXPRKIND_TARGET); + action->qual = + preprocess_expression(root, + (Node *) action->qual, + EXPRKIND_QUAL); + } + + parse->mergeSourceTargetList = (List *) + preprocess_expression(root, (Node *) parse->mergeSourceTargetList, + EXPRKIND_TARGET); + root->append_rel_list = (List *) preprocess_expression(root, (Node *) root->append_rel_list, EXPRKIND_APPINFO); @@ -1535,6 +1553,7 @@ inheritance_planner(PlannerInfo *root) subroot->parse->returningList); Assert(!parse->onConflict); + Assert(parse->mergeActionList == NIL); } /* Result path must go into outer query's FINAL upperrel */ @@ -1593,12 +1612,15 @@ inheritance_planner(PlannerInfo *root) partitioned_rels, partColsUpdated, resultRelations, + 0, subpaths, subroots, withCheckOptionLists, returningLists, rowMarks, NULL, + NULL, + NULL, SS_assign_special_param(root))); } @@ -2129,8 +2151,8 @@ grouping_planner(PlannerInfo *root, bool inheritance_update, } /* - * If this is an INSERT/UPDATE/DELETE, and we're not being called from - * inheritance_planner, add the ModifyTable node. + * If this is an INSERT/UPDATE/DELETE/MERGE, and we're not being + * called from inheritance_planner, add the ModifyTable node. */ if (parse->commandType != CMD_SELECT && !inheritance_update) { @@ -2170,12 +2192,15 @@ grouping_planner(PlannerInfo *root, bool inheritance_update, NIL, false, list_make1_int(parse->resultRelation), + parse->mergeTarget_relation, list_make1(path), list_make1(root), withCheckOptionLists, returningLists, rowMarks, parse->onConflict, + parse->mergeSourceTargetList, + parse->mergeActionList, SS_assign_special_param(root)); } diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c index 69dd327f0c9..cd540a0df5b 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -851,6 +851,60 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset) fix_scan_list(root, splan->exclRelTlist, rtoffset); } + /* + * The MERGE produces the target rows by performing a right + * join between the target relation and the source relation + * (which could be a plain relation or a subquery). The INSERT + * and UPDATE actions of the MERGE requires access to the + * columns from the source relation. We arrange things so that + * the source relation attributes are available as INNER_VAR + * and the target relation attributes are available from the + * scan tuple. + */ + if (splan->mergeActionList != NIL) + { + /* + * mergeSourceTargetList is already setup correctly to + * include all Vars coming from the source relation. So we + * fix the targetList of individual action nodes by + * ensuring that the source relation Vars are referenced + * as INNER_VAR. Note that for this to work correctly, + * during execution, the ecxt_innertuple must be set to + * the tuple obtained from the source relation. + * + * We leave the Vars from the result relation (i.e. the + * target relation) unchanged i.e. those Vars would be + * picked from the scan slot. So during execution, we must + * ensure that ecxt_scantuple is setup correctly to refer + * to the tuple from the target relation. + */ + + indexed_tlist *itlist; + + itlist = build_tlist_index(splan->mergeSourceTargetList); + + splan->mergeTargetRelation += rtoffset; + + foreach(l, splan->mergeActionList) + { + MergeAction *action = (MergeAction *) lfirst(l); + + /* Fix targetList of each action. */ + action->targetList = fix_join_expr(root, + action->targetList, + NULL, itlist, + linitial_int(splan->resultRelations), + rtoffset); + + /* Fix quals too. */ + action->qual = (Node *) fix_join_expr(root, + (List *) action->qual, + NULL, itlist, + linitial_int(splan->resultRelations), + rtoffset); + } + } + splan->nominalRelation += rtoffset; splan->exclRelRTI += rtoffset; diff --git a/src/backend/optimizer/prep/preptlist.c b/src/backend/optimizer/prep/preptlist.c index 8603feef2b8..8a87cfd14ae 100644 --- a/src/backend/optimizer/prep/preptlist.c +++ b/src/backend/optimizer/prep/preptlist.c @@ -118,6 +118,46 @@ preprocess_targetlist(PlannerInfo *root) tlist = expand_targetlist(tlist, command_type, result_relation, target_relation); + if (command_type == CMD_MERGE) + { + ListCell *l; + + /* + * For MERGE, add any junk column(s) needed to allow the executor to + * identify the rows to be updated or deleted, with different + * handling for partitioned tables. + */ + rewriteTargetListMerge(parse, target_relation); + + /* + * For MERGE command, handle targetlist of each MergeAction separately. + * Give the same treatment to MergeAction->targetList as we would have + * given to a regular INSERT/UPDATE/DELETE. + */ + foreach(l, parse->mergeActionList) + { + MergeAction *action = (MergeAction *) lfirst(l); + + switch (action->commandType) + { + case CMD_INSERT: + case CMD_UPDATE: + action->targetList = expand_targetlist(action->targetList, + action->commandType, + result_relation, + target_relation); + break; + case CMD_DELETE: + break; + case CMD_NOTHING: + break; + default: + elog(ERROR, "unknown action in MERGE WHEN clause"); + + } + } + } + /* * Add necessary junk columns for rowmarked rels. These values are needed * for locking of rels selected FOR UPDATE/SHARE, and to do EvalPlanQual @@ -348,6 +388,7 @@ expand_targetlist(List *tlist, int command_type, true /* byval */ ); } break; + case CMD_MERGE: case CMD_UPDATE: if (!att_tup->attisdropped) { diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c index 22133fcf120..416b3f95786 100644 --- a/src/backend/optimizer/util/pathnode.c +++ b/src/backend/optimizer/util/pathnode.c @@ -3284,17 +3284,21 @@ create_lockrows_path(PlannerInfo *root, RelOptInfo *rel, * 'rowMarks' is a list of PlanRowMarks (non-locking only) * 'onconflict' is the ON CONFLICT clause, or NULL * 'epqParam' is the ID of Param for EvalPlanQual re-eval + * 'mergeActionList' is a list of MERGE actions */ ModifyTablePath * create_modifytable_path(PlannerInfo *root, RelOptInfo *rel, CmdType operation, bool canSetTag, Index nominalRelation, List *partitioned_rels, bool partColsUpdated, - List *resultRelations, List *subpaths, + List *resultRelations, + Index mergeTargetRelation, + List *subpaths, List *subroots, List *withCheckOptionLists, List *returningLists, List *rowMarks, OnConflictExpr *onconflict, - int epqParam) + List *mergeSourceTargetList, + List *mergeActionList, int epqParam) { ModifyTablePath *pathnode = makeNode(ModifyTablePath); double total_size; @@ -3359,6 +3363,7 @@ create_modifytable_path(PlannerInfo *root, RelOptInfo *rel, pathnode->partitioned_rels = list_copy(partitioned_rels); pathnode->partColsUpdated = partColsUpdated; pathnode->resultRelations = resultRelations; + pathnode->mergeTargetRelation = mergeTargetRelation; pathnode->subpaths = subpaths; pathnode->subroots = subroots; pathnode->withCheckOptionLists = withCheckOptionLists; @@ -3366,6 +3371,8 @@ create_modifytable_path(PlannerInfo *root, RelOptInfo *rel, pathnode->rowMarks = rowMarks; pathnode->onconflict = onconflict; pathnode->epqParam = epqParam; + pathnode->mergeSourceTargetList = mergeSourceTargetList; + pathnode->mergeActionList = mergeActionList; return pathnode; } diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c index 0231f8bf7c6..8a6baa7beae 100644 --- a/src/backend/optimizer/util/plancat.c +++ b/src/backend/optimizer/util/plancat.c @@ -1835,6 +1835,10 @@ has_row_triggers(PlannerInfo *root, Index rti, CmdType event) trigDesc->trig_delete_before_row)) result = true; break; + /* There is no separate event for MERGE, only INSERT/UPDATE/DELETE */ + case CMD_MERGE: + result = false; + break; default: elog(ERROR, "unrecognized CmdType: %d", (int) event); break; diff --git a/src/backend/parser/Makefile b/src/backend/parser/Makefile index f14febdbda0..95fdf0b9732 100644 --- a/src/backend/parser/Makefile +++ b/src/backend/parser/Makefile @@ -14,7 +14,7 @@ override CPPFLAGS := -I. -I$(srcdir) $(CPPFLAGS) OBJS= analyze.o gram.o scan.o parser.o \ parse_agg.o parse_clause.o parse_coerce.o parse_collate.o parse_cte.o \ - parse_enr.o parse_expr.o parse_func.o parse_node.o parse_oper.o \ + parse_enr.o parse_expr.o parse_func.o parse_merge.o parse_node.o parse_oper.o \ parse_param.o parse_relation.o parse_target.o parse_type.o \ parse_utilcmd.o scansup.o diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index a4b5aaef44f..7eb9544efee 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -38,6 +38,7 @@ #include "parser/parse_cte.h" #include "parser/parse_expr.h" #include "parser/parse_func.h" +#include "parser/parse_merge.h" #include "parser/parse_oper.h" #include "parser/parse_param.h" #include "parser/parse_relation.h" @@ -53,9 +54,6 @@ post_parse_analyze_hook_type post_parse_analyze_hook = NULL; static Query *transformOptionalSelectInto(ParseState *pstate, Node *parseTree); static Query *transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt); static Query *transformInsertStmt(ParseState *pstate, InsertStmt *stmt); -static List *transformInsertRow(ParseState *pstate, List *exprlist, - List *stmtcols, List *icolumns, List *attrnos, - bool strip_indirection); static OnConflictExpr *transformOnConflictClause(ParseState *pstate, OnConflictClause *onConflictClause); static int count_rowexpr_columns(ParseState *pstate, Node *expr); @@ -68,8 +66,6 @@ static void determineRecursiveColTypes(ParseState *pstate, Node *larg, List *nrtargetlist); static Query *transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt); static List *transformReturningList(ParseState *pstate, List *returningList); -static List *transformUpdateTargetList(ParseState *pstate, - List *targetList); static Query *transformDeclareCursorStmt(ParseState *pstate, DeclareCursorStmt *stmt); static Query *transformExplainStmt(ParseState *pstate, @@ -267,6 +263,7 @@ transformStmt(ParseState *pstate, Node *parseTree) case T_InsertStmt: case T_UpdateStmt: case T_DeleteStmt: + case T_MergeStmt: (void) test_raw_expression_coverage(parseTree, NULL); break; default: @@ -291,6 +288,10 @@ transformStmt(ParseState *pstate, Node *parseTree) result = transformUpdateStmt(pstate, (UpdateStmt *) parseTree); break; + case T_MergeStmt: + result = transformMergeStmt(pstate, (MergeStmt *) parseTree); + break; + case T_SelectStmt: { SelectStmt *n = (SelectStmt *) parseTree; @@ -366,6 +367,7 @@ analyze_requires_snapshot(RawStmt *parseTree) case T_InsertStmt: case T_DeleteStmt: case T_UpdateStmt: + case T_MergeStmt: case T_SelectStmt: result = true; break; @@ -896,7 +898,7 @@ transformInsertStmt(ParseState *pstate, InsertStmt *stmt) * attrnos: integer column numbers (must be same length as icolumns) * strip_indirection: if true, remove any field/array assignment nodes */ -static List * +List * transformInsertRow(ParseState *pstate, List *exprlist, List *stmtcols, List *icolumns, List *attrnos, bool strip_indirection) @@ -2260,9 +2262,9 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt) /* * transformUpdateTargetList - - * handle SET clause in UPDATE/INSERT ... ON CONFLICT UPDATE + * handle SET clause in UPDATE/MERGE/INSERT ... ON CONFLICT UPDATE */ -static List * +List * transformUpdateTargetList(ParseState *pstate, List *origTlist) { List *tlist = NIL; diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index cd5ba2d4d8d..583ee321e1d 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -282,6 +282,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); CreateMatViewStmt RefreshMatViewStmt CreateAmStmt CreatePublicationStmt AlterPublicationStmt CreateSubscriptionStmt AlterSubscriptionStmt DropSubscriptionStmt + MergeStmt %type <node> select_no_parens select_with_parens select_clause simple_select values_clause @@ -584,6 +585,10 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type <list> hash_partbound partbound_datum_list range_datum_list %type <defelt> hash_partbound_elem +%type <node> merge_when_clause opt_and_condition +%type <list> merge_when_list +%type <node> merge_update merge_delete merge_insert + /* * Non-keyword token types. These are hard-wired into the "flex" lexer. * They must be listed first so that their numeric codes do not depend on @@ -651,7 +656,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOGGED - MAPPING MATCH MATERIALIZED MAXVALUE METHOD MINUTE_P MINVALUE MODE MONTH_P MOVE + MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE METHOD + MINUTE_P MINVALUE MODE MONTH_P MOVE NAME_P NAMES NATIONAL NATURAL NCHAR NEW NEXT NO NONE NOT NOTHING NOTIFY NOTNULL NOWAIT NULL_P NULLIF @@ -920,6 +926,7 @@ stmt : | RefreshMatViewStmt | LoadStmt | LockStmt + | MergeStmt | NotifyStmt | PrepareStmt | ReassignOwnedStmt @@ -10660,6 +10667,7 @@ ExplainableStmt: | InsertStmt | UpdateStmt | DeleteStmt + | MergeStmt | DeclareCursorStmt | CreateAsStmt | CreateMatViewStmt @@ -10722,6 +10730,7 @@ PreparableStmt: | InsertStmt | UpdateStmt | DeleteStmt /* by default all are $$=$1 */ + | MergeStmt ; /***************************************************************************** @@ -11091,6 +11100,151 @@ set_target_list: /***************************************************************************** * * QUERY: + * MERGE STATEMENTS + * + *****************************************************************************/ + +MergeStmt: + MERGE INTO relation_expr_opt_alias + USING table_ref + ON a_expr + merge_when_list + { + MergeStmt *m = makeNode(MergeStmt); + + m->relation = $3; + m->source_relation = $5; + m->join_condition = $7; + m->mergeActionList = $8; + + $$ = (Node *)m; + } + ; + + +merge_when_list: + merge_when_clause { $$ = list_make1($1); } + | merge_when_list merge_when_clause { $$ = lappend($1,$2); } + ; + +merge_when_clause: + WHEN MATCHED opt_and_condition THEN merge_update + { + MergeAction *m = makeNode(MergeAction); + + m->matched = true; + m->commandType = CMD_UPDATE; + m->condition = $3; + m->stmt = $5; + + $$ = (Node *)m; + } + | WHEN MATCHED opt_and_condition THEN merge_delete + { + MergeAction *m = makeNode(MergeAction); + + m->matched = true; + m->commandType = CMD_DELETE; + m->condition = $3; + m->stmt = $5; + + $$ = (Node *)m; + } + | WHEN NOT MATCHED opt_and_condition THEN merge_insert + { + MergeAction *m = makeNode(MergeAction); + + m->matched = false; + m->commandType = CMD_INSERT; + m->condition = $4; + m->stmt = $6; + + $$ = (Node *)m; + } + | WHEN NOT MATCHED opt_and_condition THEN DO NOTHING + { + MergeAction *m = makeNode(MergeAction); + + m->matched = false; + m->commandType = CMD_NOTHING; + m->condition = $4; + m->stmt = NULL; + + $$ = (Node *)m; + } + ; + +opt_and_condition: + AND a_expr { $$ = $2; } + | { $$ = NULL; } + ; + +merge_delete: + DELETE_P + { + DeleteStmt *n = makeNode(DeleteStmt); + $$ = (Node *)n; + } + ; + +merge_update: + UPDATE SET set_clause_list + { + UpdateStmt *n = makeNode(UpdateStmt); + n->targetList = $3; + + $$ = (Node *)n; + } + ; + +merge_insert: + INSERT values_clause + { + InsertStmt *n = makeNode(InsertStmt); + n->cols = NIL; + n->selectStmt = $2; + + $$ = (Node *)n; + } + | INSERT OVERRIDING override_kind VALUE_P values_clause + { + InsertStmt *n = makeNode(InsertStmt); + n->cols = NIL; + n->override = $3; + n->selectStmt = $5; + + $$ = (Node *)n; + } + | INSERT '(' insert_column_list ')' values_clause + { + InsertStmt *n = makeNode(InsertStmt); + n->cols = $3; + n->selectStmt = $5; + + $$ = (Node *)n; + } + | INSERT '(' insert_column_list ')' OVERRIDING override_kind VALUE_P values_clause + { + InsertStmt *n = makeNode(InsertStmt); + n->cols = $3; + n->override = $6; + n->selectStmt = $8; + + $$ = (Node *)n; + } + | INSERT DEFAULT VALUES + { + InsertStmt *n = makeNode(InsertStmt); + n->cols = NIL; + n->selectStmt = NULL; + + $$ = (Node *)n; + } + ; + +/***************************************************************************** + * + * QUERY: * CURSOR STATEMENTS * *****************************************************************************/ @@ -15088,8 +15242,10 @@ unreserved_keyword: | LOGGED | MAPPING | MATCH + | MATCHED | MATERIALIZED | MAXVALUE + | MERGE | METHOD | MINUTE_P | MINVALUE diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c index 377a7ed6d0a..544e7300b89 100644 --- a/src/backend/parser/parse_agg.c +++ b/src/backend/parser/parse_agg.c @@ -455,6 +455,13 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr) case EXPR_KIND_VALUES_SINGLE: errkind = true; break; + case EXPR_KIND_MERGE_WHEN_AND: + if (isAgg) + err = _("aggregate functions are not allowed in WHEN AND conditions"); + else + err = _("grouping operations are not allowed in WHEN AND conditions"); + + break; case EXPR_KIND_CHECK_CONSTRAINT: case EXPR_KIND_DOMAIN_CHECK: if (isAgg) @@ -873,6 +880,9 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc, case EXPR_KIND_VALUES_SINGLE: errkind = true; break; + case EXPR_KIND_MERGE_WHEN_AND: + err = _("window functions are not allowed in WHEN AND conditions"); + break; case EXPR_KIND_CHECK_CONSTRAINT: case EXPR_KIND_DOMAIN_CHECK: err = _("window functions are not allowed in check constraints"); diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c index 3a02307bd99..3cb761b4ed0 100644 --- a/src/backend/parser/parse_clause.c +++ b/src/backend/parser/parse_clause.c @@ -76,9 +76,6 @@ static RangeTblEntry *transformRangeTableFunc(ParseState *pstate, RangeTableFunc *t); static TableSampleClause *transformRangeTableSample(ParseState *pstate, RangeTableSample *rts); -static Node *transformFromClauseItem(ParseState *pstate, Node *n, - RangeTblEntry **top_rte, int *top_rti, - List **namespace); static Node *buildMergedJoinVar(ParseState *pstate, JoinType jointype, Var *l_colvar, Var *r_colvar); static ParseNamespaceItem *makeNamespaceItem(RangeTblEntry *rte, @@ -139,6 +136,7 @@ transformFromClause(ParseState *pstate, List *frmList) n = transformFromClauseItem(pstate, n, &rte, &rtindex, + NULL, NULL, &namespace); checkNameSpaceConflicts(pstate, pstate->p_namespace, namespace); @@ -1096,13 +1094,20 @@ getRTEForSpecialRelationTypes(ParseState *pstate, RangeVar *rv) * * *top_rti: receives the rangetable index of top_rte. (Ditto.) * + * *right_rte: receives the RTE corresponding to the right side of the + * jointree. Only MERGE really needs to know about this and only MERGE passes a + * non-NULL pointer. + * + * *right_rti: receives the rangetable index of the right_rte. + * * *namespace: receives a List of ParseNamespaceItems for the RTEs exposed * as table/column names by this item. (The lateral_only flags in these items * are indeterminate and should be explicitly set by the caller before use.) */ -static Node * +Node * transformFromClauseItem(ParseState *pstate, Node *n, RangeTblEntry **top_rte, int *top_rti, + RangeTblEntry **right_rte, int *right_rti, List **namespace) { if (IsA(n, RangeVar)) @@ -1194,7 +1199,7 @@ transformFromClauseItem(ParseState *pstate, Node *n, /* Recursively transform the contained relation */ rel = transformFromClauseItem(pstate, rts->relation, - top_rte, top_rti, namespace); + top_rte, top_rti, NULL, NULL, namespace); /* Currently, grammar could only return a RangeVar as contained rel */ rtr = castNode(RangeTblRef, rel); rte = rt_fetch(rtr->rtindex, pstate->p_rtable); @@ -1222,6 +1227,7 @@ transformFromClauseItem(ParseState *pstate, Node *n, List *l_namespace, *r_namespace, *my_namespace, + *save_namespace, *l_colnames, *r_colnames, *res_colnames, @@ -1240,6 +1246,7 @@ transformFromClauseItem(ParseState *pstate, Node *n, j->larg = transformFromClauseItem(pstate, j->larg, &l_rte, &l_rtindex, + NULL, NULL, &l_namespace); /* @@ -1263,12 +1270,34 @@ transformFromClauseItem(ParseState *pstate, Node *n, sv_namespace_length = list_length(pstate->p_namespace); pstate->p_namespace = list_concat(pstate->p_namespace, l_namespace); + /* + * If we are running MERGE, don't make the other RTEs visible while + * parsing the source relation. It mustn't see them. + * + * Currently, only MERGE passes non-NULL value for right_rte, so we + * can safely deduce if we're running MERGE or not by just looking at + * the right_rte. If that ever changes, we should look at other means + * to find that. + */ + if (right_rte) + { + save_namespace = pstate->p_namespace; + pstate->p_namespace = NIL; + } + /* And now we can process the RHS */ j->rarg = transformFromClauseItem(pstate, j->rarg, &r_rte, &r_rtindex, + NULL, NULL, &r_namespace); + /* + * And now restore the namespace again so that join-quals can see it. + */ + if (right_rte) + pstate->p_namespace = save_namespace; + /* Remove the left-side RTEs from the namespace list again */ pstate->p_namespace = list_truncate(pstate->p_namespace, sv_namespace_length); @@ -1295,6 +1324,12 @@ transformFromClauseItem(ParseState *pstate, Node *n, expandRTE(r_rte, r_rtindex, 0, -1, false, &r_colnames, &r_colvars); + if (right_rte) + *right_rte = r_rte; + + if (right_rti) + *right_rti = r_rtindex; + /* * Natural join does not explicitly specify columns; must generate * columns to join. Need to run through the list of columns from each diff --git a/src/backend/parser/parse_collate.c b/src/backend/parser/parse_collate.c index 6d34245083e..51c73c4018a 100644 --- a/src/backend/parser/parse_collate.c +++ b/src/backend/parser/parse_collate.c @@ -485,6 +485,7 @@ assign_collations_walker(Node *node, assign_collations_context *context) case T_FromExpr: case T_OnConflictExpr: case T_SortGroupClause: + case T_MergeAction: (void) expression_tree_walker(node, assign_collations_walker, (void *) &loccontext); diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 385e54a9b69..38fbe3366fc 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -1818,6 +1818,7 @@ transformSubLink(ParseState *pstate, SubLink *sublink) case EXPR_KIND_RETURNING: case EXPR_KIND_VALUES: case EXPR_KIND_VALUES_SINGLE: + case EXPR_KIND_MERGE_WHEN_AND: /* okay */ break; case EXPR_KIND_CHECK_CONSTRAINT: @@ -3475,6 +3476,8 @@ ParseExprKindName(ParseExprKind exprKind) return "PARTITION BY"; case EXPR_KIND_CALL_ARGUMENT: return "CALL"; + case EXPR_KIND_MERGE_WHEN_AND: + return "MERGE WHEN AND"; /* * There is intentionally no default: case here, so that the diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c index ea5d5212b4c..615aee6d15f 100644 --- a/src/backend/parser/parse_func.c +++ b/src/backend/parser/parse_func.c @@ -2277,6 +2277,9 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location) /* okay, since we process this like a SELECT tlist */ pstate->p_hasTargetSRFs = true; break; + case EXPR_KIND_MERGE_WHEN_AND: + err = _("set-returning functions are not allowed in WHEN AND conditions"); + break; case EXPR_KIND_CHECK_CONSTRAINT: case EXPR_KIND_DOMAIN_CHECK: err = _("set-returning functions are not allowed in check constraints"); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 053ae02c9fe..f7e11f969c0 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -728,6 +728,16 @@ scanRTEForColumn(ParseState *pstate, RangeTblEntry *rte, const char *colname, colname), parser_errposition(pstate, location))); + /* In MERGE WHEN AND condition, no system column is allowed except tableOid or OID */ + if (pstate->p_expr_kind == EXPR_KIND_MERGE_WHEN_AND && + attnum < InvalidAttrNumber && + !(attnum == TableOidAttributeNumber || attnum == ObjectIdAttributeNumber)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_COLUMN_REFERENCE), + errmsg("system column \"%s\" reference in WHEN AND condition is invalid", + colname), + parser_errposition(pstate, location))); + if (attnum != InvalidAttrNumber) { /* now check to see if column actually is defined */ diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c index 88140bc6877..98239f569ae 100644 --- a/src/backend/rewrite/rewriteHandler.c +++ b/src/backend/rewrite/rewriteHandler.c @@ -1377,6 +1377,57 @@ rewriteTargetListUD(Query *parsetree, RangeTblEntry *target_rte, } } +void +rewriteTargetListMerge(Query *parsetree, Relation target_relation) +{ + Var *var = NULL; + const char *attrname; + TargetEntry *tle; + + Assert(target_relation->rd_rel->relkind == RELKIND_RELATION || + target_relation->rd_rel->relkind == RELKIND_MATVIEW || + target_relation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE); + + /* + * Emit CTID so that executor can find the row to update or delete. + */ + var = makeVar(parsetree->mergeTarget_relation, + SelfItemPointerAttributeNumber, + TIDOID, + -1, + InvalidOid, + 0); + + attrname = "ctid"; + tle = makeTargetEntry((Expr *) var, + list_length(parsetree->targetList) + 1, + pstrdup(attrname), + true); + + parsetree->targetList = lappend(parsetree->targetList, tle); + + /* + * If we are dealing with partitioned table, then emit TABLEOID so that + * executor can find the partition the row belongs to. + */ + if (target_relation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + { + var = makeVar(parsetree->mergeTarget_relation, + TableOidAttributeNumber, + OIDOID, + -1, + InvalidOid, + 0); + + attrname = "tableoid"; + tle = makeTargetEntry((Expr *) var, + list_length(parsetree->targetList) + 1, + pstrdup(attrname), + true); + + parsetree->targetList = lappend(parsetree->targetList, tle); + } +} /* * matchLocks - @@ -3331,6 +3382,7 @@ RewriteQuery(Query *parsetree, List *rewrite_events) } else if (event == CMD_UPDATE) { + Assert(parsetree->override == OVERRIDING_NOT_SET); parsetree->targetList = rewriteTargetListIU(parsetree->targetList, parsetree->commandType, @@ -3338,6 +3390,50 @@ RewriteQuery(Query *parsetree, List *rewrite_events) rt_entry_relation, parsetree->resultRelation, NULL); } + else if (event == CMD_MERGE) + { + Assert(parsetree->override == OVERRIDING_NOT_SET); + + /* + * Rewrite each action targetlist separately + */ + foreach(lc1, parsetree->mergeActionList) + { + MergeAction *action = (MergeAction *) lfirst(lc1); + + switch (action->commandType) + { + case CMD_NOTHING: + case CMD_DELETE: /* Nothing to do here */ + break; + case CMD_UPDATE: + action->targetList = + rewriteTargetListIU(action->targetList, + action->commandType, + parsetree->override, + rt_entry_relation, + parsetree->resultRelation, + NULL); + break; + case CMD_INSERT: + { + InsertStmt *istmt = (InsertStmt *) action->stmt; + + action->targetList = + rewriteTargetListIU(action->targetList, + action->commandType, + istmt->override, + rt_entry_relation, + parsetree->resultRelation, + NULL); + } + break; + default: + elog(ERROR, "unrecognized commandType: %d", action->commandType); + break; + } + } + } else if (event == CMD_DELETE) { /* Nothing to do here */ @@ -3351,13 +3447,20 @@ RewriteQuery(Query *parsetree, List *rewrite_events) locks = matchLocks(event, rt_entry_relation->rd_rules, result_relation, parsetree, &hasUpdate); - product_queries = fireRules(parsetree, - result_relation, - event, - locks, - &instead, - &returning, - &qual_product); + /* + * XXX MERGE doesn't support write rules because they would violate + * the SQL Standard spec and would be unclear how they should work. + */ + if (event == CMD_MERGE) + product_queries = NIL; + else + product_queries = fireRules(parsetree, + result_relation, + event, + locks, + &instead, + &returning, + &qual_product); /* * If there were no INSTEAD rules, and the target relation is a view diff --git a/src/backend/rewrite/rowsecurity.c b/src/backend/rewrite/rowsecurity.c index ce77a18bc96..6e85886e646 100644 --- a/src/backend/rewrite/rowsecurity.c +++ b/src/backend/rewrite/rowsecurity.c @@ -379,6 +379,95 @@ get_row_security_policies(Query *root, RangeTblEntry *rte, int rt_index, } } + /* + * FOR MERGE, we fetch policies for UPDATE, DELETE and INSERT (and ALL) + * and set them up so that we can enforce the appropriate policy depending + * on the final action we take. + * + * We don't fetch the SELECT policies since they are correctly applied to + * the root->mergeTarget_relation. The target rows are selected after + * joining the mergeTarget_relation and the source relation and hence it's + * enough to apply SELECT policies to the mergeTarget_relation. + * + * We don't push the UPDATE/DELETE USING quals to the RTE because we don't + * really want to apply them while scanning the relation since we don't + * know whether we will be doing a UPDATE or a DELETE at the end. We apply + * the respective policy once we decide the final action on the target + * tuple. + * + * XXX We are setting up USING quals as WITH CHECK. If RLS prohibits + * UPDATE/DELETE on the target row, we shall throw an error instead of + * silently ignoring the row. This is different than how normal + * UPDATE/DELETE works and more in line with INSERT ON CONFLICT DO UPDATE + * handling. + */ + if (commandType == CMD_MERGE) + { + List *merge_permissive_policies; + List *merge_restrictive_policies; + + /* + * Fetch the UPDATE policies and set them up to execute on the + * existing target row before doing UPDATE. + */ + get_policies_for_relation(rel, CMD_UPDATE, user_id, + &merge_permissive_policies, + &merge_restrictive_policies); + + /* + * WCO_RLS_MERGE_UPDATE_CHECK is used to check UPDATE USING quals on + * the existing target row. + */ + add_with_check_options(rel, rt_index, + WCO_RLS_MERGE_UPDATE_CHECK, + merge_permissive_policies, + merge_restrictive_policies, + withCheckOptions, + hasSubLinks, + true); + + /* + * Same with DELETE policies. + */ + get_policies_for_relation(rel, CMD_DELETE, user_id, + &merge_permissive_policies, + &merge_restrictive_policies); + + add_with_check_options(rel, rt_index, + WCO_RLS_MERGE_DELETE_CHECK, + merge_permissive_policies, + merge_restrictive_policies, + withCheckOptions, + hasSubLinks, + true); + + /* + * No special handling is required for INSERT policies. They will be + * checked and enforced during ExecInsert(). But we must add them to + * withCheckOptions. + */ + get_policies_for_relation(rel, CMD_INSERT, user_id, + &merge_permissive_policies, + &merge_restrictive_policies); + + add_with_check_options(rel, rt_index, + WCO_RLS_INSERT_CHECK, + merge_permissive_policies, + merge_restrictive_policies, + withCheckOptions, + hasSubLinks, + false); + + /* Enforce the WITH CHECK clauses of the UPDATE policies */ + add_with_check_options(rel, rt_index, + WCO_RLS_UPDATE_CHECK, + merge_permissive_policies, + merge_restrictive_policies, + withCheckOptions, + hasSubLinks, + false); + } + heap_close(rel, NoLock); /* @@ -438,6 +527,14 @@ get_policies_for_relation(Relation relation, CmdType cmd, Oid user_id, if (policy->polcmd == ACL_DELETE_CHR) cmd_matches = true; break; + case CMD_MERGE: + + /* + * We do not support a separate policy for MERGE command. + * Instead it derives from the policies defined for other + * commands. + */ + break; default: elog(ERROR, "unrecognized policy command type %d", (int) cmd); diff --git a/src/backend/tcop/pquery.c b/src/backend/tcop/pquery.c index 66cc5c35c68..50f852a4aa7 100644 --- a/src/backend/tcop/pquery.c +++ b/src/backend/tcop/pquery.c @@ -193,6 +193,11 @@ ProcessQuery(PlannedStmt *plan, "DELETE " UINT64_FORMAT, queryDesc->estate->es_processed); break; + case CMD_MERGE: + snprintf(completionTag, COMPLETION_TAG_BUFSIZE, + "MERGE " UINT64_FORMAT, + queryDesc->estate->es_processed); + break; default: strcpy(completionTag, "???"); break; diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index b2dc9d18eac..84f25917368 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -110,6 +110,7 @@ CommandIsReadOnly(PlannedStmt *pstmt) case CMD_UPDATE: case CMD_INSERT: case CMD_DELETE: + case CMD_MERGE: return false; case CMD_UTILITY: /* For now, treat all utility commands as read/write */ @@ -1832,6 +1833,8 @@ QueryReturnsTuples(Query *parsetree) case CMD_SELECT: /* returns tuples */ return true; + case CMD_MERGE: + return false; case CMD_INSERT: case CMD_UPDATE: case CMD_DELETE: @@ -2076,6 +2079,10 @@ CreateCommandTag(Node *parsetree) tag = "UPDATE"; break; + case T_MergeStmt: + tag = "MERGE"; + break; + case T_SelectStmt: tag = "SELECT"; break; @@ -2819,6 +2826,9 @@ CreateCommandTag(Node *parsetree) case CMD_DELETE: tag = "DELETE"; break; + case CMD_MERGE: + tag = "MERGE"; + break; case CMD_UTILITY: tag = CreateCommandTag(stmt->utilityStmt); break; @@ -2879,6 +2889,9 @@ CreateCommandTag(Node *parsetree) case CMD_DELETE: tag = "DELETE"; break; + case CMD_MERGE: + tag = "MERGE"; + break; case CMD_UTILITY: tag = CreateCommandTag(stmt->utilityStmt); break; @@ -2927,6 +2940,7 @@ GetCommandLogLevel(Node *parsetree) case T_InsertStmt: case T_DeleteStmt: case T_UpdateStmt: + case T_MergeStmt: lev = LOGSTMT_MOD; break; @@ -3366,6 +3380,7 @@ GetCommandLogLevel(Node *parsetree) case CMD_UPDATE: case CMD_INSERT: case CMD_DELETE: + case CMD_MERGE: lev = LOGSTMT_MOD; break; @@ -3396,6 +3411,7 @@ GetCommandLogLevel(Node *parsetree) case CMD_UPDATE: case CMD_INSERT: case CMD_DELETE: + case CMD_MERGE: lev = LOGSTMT_MOD; break; diff --git a/src/include/access/heapam.h b/src/include/access/heapam.h index 4c0256b18a4..608f50b0616 100644 --- a/src/include/access/heapam.h +++ b/src/include/access/heapam.h @@ -53,23 +53,34 @@ typedef enum LockTupleMode * When heap_update, heap_delete, or heap_lock_tuple fail because the target * tuple is already outdated, they fill in this struct to provide information * to the caller about what happened. + * + * result is the result of HeapTupleSatisfiesUpdate, leading to the failure. + * It's set to HeapTupleMayBeUpdated when there is no failure. + * * ctid is the target's ctid link: it is the same as the target's TID if the * target was deleted, or the location of the replacement tuple if the target * was updated. + * * xmax is the outdating transaction's XID. If the caller wants to visit the * replacement tuple, it must check that this matches before believing the * replacement is really a match. + * * cmax is the outdating command's CID, but only when the failure code is * HeapTupleSelfUpdated (i.e., something in the current transaction outdated * the tuple); otherwise cmax is zero. (We make this restriction because * HeapTupleHeaderGetCmax doesn't work for tuples outdated in other * transactions.) + * + * lockmode is only relevant for callers of heap_update() and is the mode which + * the caller should use in case it needs to lock the updated tuple. */ typedef struct HeapUpdateFailureData { + HTSU_Result result; ItemPointerData ctid; TransactionId xmax; CommandId cmax; + LockTupleMode lockmode; } HeapUpdateFailureData; @@ -162,7 +173,7 @@ extern void heap_abort_speculative(Relation relation, HeapTuple tuple); extern HTSU_Result heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, CommandId cid, Snapshot crosscheck, bool wait, - HeapUpdateFailureData *hufd, LockTupleMode *lockmode); + HeapUpdateFailureData *hufd); extern HTSU_Result heap_lock_tuple(Relation relation, HeapTuple tuple, CommandId cid, LockTupleMode mode, LockWaitPolicy wait_policy, bool follow_update, diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h index a5b8610fa22..1b79a803103 100644 --- a/src/include/commands/trigger.h +++ b/src/include/commands/trigger.h @@ -206,7 +206,8 @@ extern bool ExecBRDeleteTriggers(EState *estate, EPQState *epqstate, ResultRelInfo *relinfo, ItemPointer tupleid, - HeapTuple fdw_trigtuple); + HeapTuple fdw_trigtuple, + HeapUpdateFailureData *hufdp); extern void ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo, ItemPointer tupleid, @@ -225,7 +226,8 @@ extern TupleTableSlot *ExecBRUpdateTriggers(EState *estate, ResultRelInfo *relinfo, ItemPointer tupleid, HeapTuple fdw_trigtuple, - TupleTableSlot *slot); + TupleTableSlot *slot, + HeapUpdateFailureData *hufdp); extern void ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo, ItemPointer tupleid, diff --git a/src/include/executor/execPartition.h b/src/include/executor/execPartition.h index 03a599ad57a..9f55f6409ef 100644 --- a/src/include/executor/execPartition.h +++ b/src/include/executor/execPartition.h @@ -114,6 +114,7 @@ extern int ExecFindPartition(ResultRelInfo *resultRelInfo, PartitionDispatch *pd, TupleTableSlot *slot, EState *estate); +extern int ExecFindPartitionByOid(PartitionTupleRouting *proute, Oid partoid); extern ResultRelInfo *ExecInitPartitionInfo(ModifyTableState *mtstate, ResultRelInfo *resultRelInfo, PartitionTupleRouting *proute, diff --git a/src/include/executor/instrument.h b/src/include/executor/instrument.h index b72f91898ad..28eb0093d47 100644 --- a/src/include/executor/instrument.h +++ b/src/include/executor/instrument.h @@ -58,8 +58,11 @@ typedef struct Instrumentation double total; /* Total total time (in seconds) */ double ntuples; /* Total tuples produced */ double nloops; /* # of run cycles for this node */ - double nfiltered1; /* # tuples removed by scanqual or joinqual */ - double nfiltered2; /* # tuples removed by "other" quals */ + double nfiltered1; /* # tuples removed by scanqual or joinqual OR + * # tuples inserted by MERGE */ + double nfiltered2; /* # tuples removed by "other" quals OR + * # tuples updated by MERGE */ + double nfiltered3; /* # tuples deleted by MERGE */ BufferUsage bufusage; /* Total buffer usage */ } Instrumentation; diff --git a/src/include/executor/nodeModifyTable.h b/src/include/executor/nodeModifyTable.h index 0d7e579e1cb..686cfa61710 100644 --- a/src/include/executor/nodeModifyTable.h +++ b/src/include/executor/nodeModifyTable.h @@ -18,5 +18,26 @@ extern ModifyTableState *ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags); extern void ExecEndModifyTable(ModifyTableState *node); extern void ExecReScanModifyTable(ModifyTableState *node); +extern TupleTableSlot *ExecPrepareTupleRouting(ModifyTableState *mtstate, + EState *estate, + struct PartitionTupleRouting *proute, + ResultRelInfo *targetRelInfo, + TupleTableSlot *slot); +extern TupleTableSlot *ExecDelete(ModifyTableState *mtstate, + ItemPointer tupleid, HeapTuple oldtuple, TupleTableSlot *planSlot, + EPQState *epqstate, EState *estate, bool *tupleDeleted, + bool processReturning, HeapUpdateFailureData *hufdp, + MergeActionState *actionState, bool canSetTag); +extern TupleTableSlot *ExecUpdate(ModifyTableState *mtstate, + ItemPointer tupleid, HeapTuple oldtuple, TupleTableSlot *slot, + TupleTableSlot *planSlot, EPQState *epqstate, EState *estate, + bool *tuple_updated, HeapUpdateFailureData *hufdp, + MergeActionState *actionState, bool canSetTag); +extern TupleTableSlot *ExecInsert(ModifyTableState *mtstate, + TupleTableSlot *slot, + TupleTableSlot *planSlot, + EState *estate, + MergeActionState *actionState, + bool canSetTag); #endif /* NODEMODIFYTABLE_H */ diff --git a/src/include/executor/spi.h b/src/include/executor/spi.h index e5bdaecc4e3..78410b9f772 100644 --- a/src/include/executor/spi.h +++ b/src/include/executor/spi.h @@ -64,6 +64,7 @@ typedef struct _SPI_plan *SPIPlanPtr; #define SPI_OK_REL_REGISTER 15 #define SPI_OK_REL_UNREGISTER 16 #define SPI_OK_TD_REGISTER 17 +#define SPI_OK_MERGE 18 #define SPI_OPT_NONATOMIC (1 << 0) diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index 6070a42b6fe..ff63d179b2a 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -360,8 +360,17 @@ typedef struct JunkFilter AttrNumber *jf_cleanMap; TupleTableSlot *jf_resultSlot; AttrNumber jf_junkAttNo; + AttrNumber jf_otherJunkAttNo; } JunkFilter; +typedef struct MergeState +{ + /* List of MERGE MATCHED action states */ + List *matchedActionStates; + /* List of MERGE NOT MATCHED action states */ + List *notMatchedActionStates; +} MergeState; + /* * OnConflictSetState * @@ -452,8 +461,38 @@ typedef struct ResultRelInfo /* relation descriptor for root partitioned table */ Relation ri_PartitionRoot; + + int ri_PartitionLeafIndex; + /* for running MERGE on this result relation */ + MergeState *ri_mergeState; + + /* + * While executing MERGE, the target relation is processed twice; once + * as a target relation and once to run a join between the target and the + * source. We generate two different RTEs for these two purposes, one with + * rte->inh set to false and other with rte->inh set to true. + * + * Since the plan re-evaluated by EvalPlanQual uses the join RTE, we must + * install the updated tuple in the scan corresponding to that RTE. The + * following member tracks the index of the second RTE for EvalPlanQual + * purposes. ri_mergeTargetRTI is non-zero only when MERGE is in-progress. + * We use ri_mergeTargetRTI to run EvalPlanQual for MERGE and + * ri_RangeTableIndex elsewhere. + */ + Index ri_mergeTargetRTI; } ResultRelInfo; +/* + * Get the Range table index for EvalPlanQual. + * + * We use the ri_mergeTargetRTI if set, otherwise use ri_RangeTableIndex. + * ri_mergeTargetRTI should really be ever set iff we're running MERGE. + */ +#define GetEPQRangeTableIndex(r) \ + (((r)->ri_mergeTargetRTI > 0) \ + ? (r)->ri_mergeTargetRTI \ + : (r)->ri_RangeTableIndex) + /* ---------------- * EState information * @@ -966,6 +1005,11 @@ typedef struct PlanState if (((PlanState *)(node))->instrument) \ ((PlanState *)(node))->instrument->nfiltered2 += (delta); \ } while(0) +#define InstrCountFiltered3(node, delta) \ + do { \ + if (((PlanState *)(node))->instrument) \ + ((PlanState *)(node))->instrument->nfiltered3 += (delta); \ + } while(0) /* * EPQState is state for executing an EvalPlanQual recheck on a candidate @@ -1013,13 +1057,27 @@ typedef struct ProjectSetState } ProjectSetState; /* ---------------- + * MergeActionState information + * ---------------- + */ +typedef struct MergeActionState +{ + NodeTag type; + bool matched; /* true=MATCHED, false=NOT MATCHED */ + ExprState *whenqual; /* WHEN AND conditions */ + CmdType commandType; /* INSERT/UPDATE/DELETE/DO NOTHING */ + ProjectionInfo *proj; /* tuple projection info */ + TupleDesc tupDesc; /* tuple descriptor for projection */ +} MergeActionState; + +/* ---------------- * ModifyTableState information * ---------------- */ typedef struct ModifyTableState { PlanState ps; /* its first field is NodeTag */ - CmdType operation; /* INSERT, UPDATE, or DELETE */ + CmdType operation; /* INSERT, UPDATE, DELETE or MERGE */ bool canSetTag; /* do we set the command tag/es_processed? */ bool mt_done; /* are we done? */ PlanState **mt_plans; /* subplans (one per target rel) */ @@ -1035,6 +1093,8 @@ typedef struct ModifyTableState List *mt_excludedtlist; /* the excluded pseudo relation's tlist */ TupleTableSlot *mt_conflproj; /* CONFLICT ... SET ... projection target */ + TupleTableSlot *mt_mergeproj; /* MERGE action projection target */ + /* Tuple-routing support info */ struct PartitionTupleRouting *mt_partition_tuple_routing; @@ -1046,6 +1106,9 @@ typedef struct ModifyTableState /* Per plan map for tuple conversion from child to root */ TupleConversionMap **mt_per_subplan_tupconv_maps; + + /* Flags showing which subcommands are present INS/UPD/DEL/DO NOTHING */ + int mt_merge_subcommands; } ModifyTableState; /* ---------------- diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h index 443de227041..fce48026b6d 100644 --- a/src/include/nodes/nodes.h +++ b/src/include/nodes/nodes.h @@ -97,6 +97,7 @@ typedef enum NodeTag T_PlanState, T_ResultState, T_ProjectSetState, + T_MergeActionState, T_ModifyTableState, T_AppendState, T_MergeAppendState, @@ -308,6 +309,8 @@ typedef enum NodeTag T_InsertStmt, T_DeleteStmt, T_UpdateStmt, + T_MergeStmt, + T_MergeAction, T_SelectStmt, T_AlterTableStmt, T_AlterTableCmd, @@ -657,7 +660,8 @@ typedef enum CmdType CMD_SELECT, /* select stmt */ CMD_UPDATE, /* update stmt */ CMD_INSERT, /* insert stmt */ - CMD_DELETE, + CMD_DELETE, /* delete stmt */ + CMD_MERGE, /* merge stmt */ CMD_UTILITY, /* cmds like create, destroy, copy, vacuum, * etc. */ CMD_NOTHING /* dummy command for instead nothing rules diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 92082b3a7a2..d005beeba82 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -38,7 +38,7 @@ typedef enum OverridingKind typedef enum QuerySource { QSRC_ORIGINAL, /* original parsetree (explicit query) */ - QSRC_PARSER, /* added by parse analysis (now unused) */ + QSRC_PARSER, /* added by parse analysis in MERGE */ QSRC_INSTEAD_RULE, /* added by unconditional INSTEAD rule */ QSRC_QUAL_INSTEAD_RULE, /* added by conditional INSTEAD rule */ QSRC_NON_INSTEAD_RULE /* added by non-INSTEAD rule */ @@ -107,7 +107,7 @@ typedef struct Query { NodeTag type; - CmdType commandType; /* select|insert|update|delete|utility */ + CmdType commandType; /* select|insert|update|delete|merge|utility */ QuerySource querySource; /* where did I come from? */ @@ -118,7 +118,7 @@ typedef struct Query Node *utilityStmt; /* non-null if commandType == CMD_UTILITY */ int resultRelation; /* rtable index of target relation for - * INSERT/UPDATE/DELETE; 0 for SELECT */ + * INSERT/UPDATE/DELETE/MERGE; 0 for SELECT */ bool hasAggs; /* has aggregates in tlist or havingQual */ bool hasWindowFuncs; /* has window functions in tlist */ @@ -169,6 +169,9 @@ typedef struct Query List *withCheckOptions; /* a list of WithCheckOption's, which are * only added during rewrite and therefore * are not written out as part of Query. */ + int mergeTarget_relation; + List *mergeSourceTargetList; + List *mergeActionList; /* list of actions for MERGE (only) */ /* * The following two fields identify the portion of the source text string @@ -1128,7 +1131,9 @@ typedef enum WCOKind WCO_VIEW_CHECK, /* WCO on an auto-updatable view */ WCO_RLS_INSERT_CHECK, /* RLS INSERT WITH CHECK policy */ WCO_RLS_UPDATE_CHECK, /* RLS UPDATE WITH CHECK policy */ - WCO_RLS_CONFLICT_CHECK /* RLS ON CONFLICT DO UPDATE USING policy */ + WCO_RLS_CONFLICT_CHECK, /* RLS ON CONFLICT DO UPDATE USING policy */ + WCO_RLS_MERGE_UPDATE_CHECK, /* RLS MERGE UPDATE USING policy */ + WCO_RLS_MERGE_DELETE_CHECK /* RLS MERGE DELETE USING policy */ } WCOKind; typedef struct WithCheckOption @@ -1504,6 +1509,30 @@ typedef struct UpdateStmt } UpdateStmt; /* ---------------------- + * Merge Statement + * ---------------------- + */ +typedef struct MergeStmt +{ + NodeTag type; + RangeVar *relation; /* target relation to merge into */ + Node *source_relation; /* source relation */ + Node *join_condition; /* join condition between source and target */ + List *mergeActionList; /* list of MergeAction(s) */ +} MergeStmt; + +typedef struct MergeAction +{ + NodeTag type; + bool matched; /* true=MATCHED, false=NOT MATCHED */ + Node *condition; /* WHEN AND conditions (raw parser) */ + Node *qual; /* transformed WHEN AND conditions */ + CmdType commandType; /* INSERT/UPDATE/DELETE/DO NOTHING */ + Node *stmt; /* T_UpdateStmt etc */ + List *targetList; /* the target list (of ResTarget) */ +} MergeAction; + +/* ---------------------- * Select Statement * * A "simple" SELECT is represented in the output of gram.y by a single diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index c922216b7d1..0a797f0a052 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -18,6 +18,7 @@ #include "lib/stringinfo.h" #include "nodes/bitmapset.h" #include "nodes/lockoptions.h" +#include "nodes/parsenodes.h" #include "nodes/primnodes.h" @@ -42,7 +43,7 @@ typedef struct PlannedStmt { NodeTag type; - CmdType commandType; /* select|insert|update|delete|utility */ + CmdType commandType; /* select|insert|update|delete|merge|utility */ uint64 queryId; /* query identifier (copied from Query) */ @@ -216,13 +217,14 @@ typedef struct ProjectSet typedef struct ModifyTable { Plan plan; - CmdType operation; /* INSERT, UPDATE, or DELETE */ + CmdType operation; /* INSERT, UPDATE, DELETE or MERGE */ bool canSetTag; /* do we set the command tag/es_processed? */ Index nominalRelation; /* Parent RT index for use of EXPLAIN */ /* RT indexes of non-leaf tables in a partition tree */ List *partitioned_rels; bool partColsUpdated; /* some part key in hierarchy updated */ List *resultRelations; /* integer list of RT indexes */ + Index mergeTargetRelation; /* RT index of the merge target */ int resultRelIndex; /* index of first resultRel in plan's list */ int rootResultRelIndex; /* index of the partitioned table root */ List *plans; /* plan(s) producing source data */ @@ -238,6 +240,8 @@ typedef struct ModifyTable Node *onConflictWhere; /* WHERE for ON CONFLICT UPDATE */ Index exclRelRTI; /* RTI of the EXCLUDED pseudo relation */ List *exclRelTlist; /* tlist of the EXCLUDED pseudo relation */ + List *mergeSourceTargetList; + List *mergeActionList; /* actions for MERGE */ } ModifyTable; /* ---------------- diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h index ea5251c6be2..a2dde70de58 100644 --- a/src/include/nodes/relation.h +++ b/src/include/nodes/relation.h @@ -1670,7 +1670,7 @@ typedef struct LockRowsPath } LockRowsPath; /* - * ModifyTablePath represents performing INSERT/UPDATE/DELETE modifications + * ModifyTablePath represents performing INSERT/UPDATE/DELETE/MERGE * * We represent most things that will be in the ModifyTable plan node * literally, except we have child Path(s) not Plan(s). But analysis of the @@ -1679,13 +1679,14 @@ typedef struct LockRowsPath typedef struct ModifyTablePath { Path path; - CmdType operation; /* INSERT, UPDATE, or DELETE */ + CmdType operation; /* INSERT, UPDATE, DELETE or MERGE */ bool canSetTag; /* do we set the command tag/es_processed? */ Index nominalRelation; /* Parent RT index for use of EXPLAIN */ /* RT indexes of non-leaf tables in a partition tree */ List *partitioned_rels; bool partColsUpdated; /* some part key in hierarchy updated */ List *resultRelations; /* integer list of RT indexes */ + Index mergeTargetRelation;/* RT index of merge target relation */ List *subpaths; /* Path(s) producing source data */ List *subroots; /* per-target-table PlannerInfos */ List *withCheckOptionLists; /* per-target-table WCO lists */ @@ -1693,6 +1694,8 @@ typedef struct ModifyTablePath List *rowMarks; /* PlanRowMarks (non-locking only) */ OnConflictExpr *onconflict; /* ON CONFLICT clause, or NULL */ int epqParam; /* ID of Param for EvalPlanQual re-eval */ + List *mergeSourceTargetList; + List *mergeActionList; /* actions for MERGE */ } ModifyTablePath; /* diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h index 381bc30813c..895bf6959da 100644 --- a/src/include/optimizer/pathnode.h +++ b/src/include/optimizer/pathnode.h @@ -241,11 +241,14 @@ extern ModifyTablePath *create_modifytable_path(PlannerInfo *root, CmdType operation, bool canSetTag, Index nominalRelation, List *partitioned_rels, bool partColsUpdated, - List *resultRelations, List *subpaths, + List *resultRelations, + Index mergeTargetRelation, + List *subpaths, List *subroots, List *withCheckOptionLists, List *returningLists, List *rowMarks, OnConflictExpr *onconflict, - int epqParam); + List *mergeSourceTargetList, + List *mergeActionList, int epqParam); extern LimitPath *create_limit_path(PlannerInfo *root, RelOptInfo *rel, Path *subpath, Node *limitOffset, Node *limitCount, diff --git a/src/include/parser/analyze.h b/src/include/parser/analyze.h index 687ae1b5b7c..41fb10666e5 100644 --- a/src/include/parser/analyze.h +++ b/src/include/parser/analyze.h @@ -32,6 +32,11 @@ extern Query *parse_sub_analyze(Node *parseTree, ParseState *parentParseState, bool locked_from_parent, bool resolve_unknowns); +extern List *transformInsertRow(ParseState *pstate, List *exprlist, + List *stmtcols, List *icolumns, List *attrnos, + bool strip_indirection); +extern List *transformUpdateTargetList(ParseState *pstate, + List *targetList); extern Query *transformTopLevelStmt(ParseState *pstate, RawStmt *parseTree); extern Query *transformStmt(ParseState *pstate, Node *parseTree); diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index cf32197bc3e..4dff55a8e99 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -244,8 +244,10 @@ PG_KEYWORD("locked", LOCKED, UNRESERVED_KEYWORD) PG_KEYWORD("logged", LOGGED, UNRESERVED_KEYWORD) PG_KEYWORD("mapping", MAPPING, UNRESERVED_KEYWORD) PG_KEYWORD("match", MATCH, UNRESERVED_KEYWORD) +PG_KEYWORD("matched", MATCHED, UNRESERVED_KEYWORD) PG_KEYWORD("materialized", MATERIALIZED, UNRESERVED_KEYWORD) PG_KEYWORD("maxvalue", MAXVALUE, UNRESERVED_KEYWORD) +PG_KEYWORD("merge", MERGE, UNRESERVED_KEYWORD) PG_KEYWORD("method", METHOD, UNRESERVED_KEYWORD) PG_KEYWORD("minute", MINUTE_P, UNRESERVED_KEYWORD) PG_KEYWORD("minvalue", MINVALUE, UNRESERVED_KEYWORD) diff --git a/src/include/parser/parse_clause.h b/src/include/parser/parse_clause.h index 2c0e0928628..30121c98ed8 100644 --- a/src/include/parser/parse_clause.h +++ b/src/include/parser/parse_clause.h @@ -20,7 +20,10 @@ extern void transformFromClause(ParseState *pstate, List *frmList); extern int setTargetTable(ParseState *pstate, RangeVar *relation, bool inh, bool alsoSource, AclMode requiredPerms); extern bool interpretOidsOption(List *defList, bool allowOids); - +extern Node *transformFromClauseItem(ParseState *pstate, Node *n, + RangeTblEntry **top_rte, int *top_rti, + RangeTblEntry **right_rte, int *right_rti, + List **namespace); extern Node *transformWhereClause(ParseState *pstate, Node *clause, ParseExprKind exprKind, const char *constructName); extern Node *transformLimitClause(ParseState *pstate, Node *clause, diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h index 0230543810f..3fd2151ccbe 100644 --- a/src/include/parser/parse_node.h +++ b/src/include/parser/parse_node.h @@ -50,6 +50,7 @@ typedef enum ParseExprKind EXPR_KIND_INSERT_TARGET, /* INSERT target list item */ EXPR_KIND_UPDATE_SOURCE, /* UPDATE assignment source item */ EXPR_KIND_UPDATE_TARGET, /* UPDATE assignment target item */ + EXPR_KIND_MERGE_WHEN_AND, /* MERGE WHEN ... AND condition */ EXPR_KIND_GROUP_BY, /* GROUP BY */ EXPR_KIND_ORDER_BY, /* ORDER BY */ EXPR_KIND_DISTINCT_ON, /* DISTINCT ON */ @@ -127,7 +128,7 @@ typedef Node *(*CoerceParamHook) (ParseState *pstate, Param *param, * p_parent_cte: CommonTableExpr that immediately contains the current query, * if any. * - * p_target_relation: target relation, if query is INSERT, UPDATE, or DELETE. + * p_target_relation: target relation, if query is INSERT/UPDATE/DELETE/MERGE * * p_target_rangetblentry: target relation's entry in the rtable list. * @@ -181,7 +182,7 @@ struct ParseState List *p_ctenamespace; /* current namespace for common table exprs */ List *p_future_ctes; /* common table exprs not yet in namespace */ CommonTableExpr *p_parent_cte; /* this query's containing CTE */ - Relation p_target_relation; /* INSERT/UPDATE/DELETE target rel */ + Relation p_target_relation; /* INSERT/UPDATE/DELETE/MERGE target rel */ RangeTblEntry *p_target_rangetblentry; /* target rel's RTE */ bool p_is_insert; /* process assignment like INSERT not UPDATE */ List *p_windowdefs; /* raw representations of window clauses */ diff --git a/src/include/rewrite/rewriteHandler.h b/src/include/rewrite/rewriteHandler.h index 8128199fc31..1ab5de39422 100644 --- a/src/include/rewrite/rewriteHandler.h +++ b/src/include/rewrite/rewriteHandler.h @@ -25,6 +25,7 @@ extern void AcquireRewriteLocks(Query *parsetree, extern Node *build_column_default(Relation rel, int attrno); extern void rewriteTargetListUD(Query *parsetree, RangeTblEntry *target_rte, Relation target_relation); +extern void rewriteTargetListMerge(Query *parsetree, Relation target_relation); extern Query *get_view_query(Relation view); extern const char *view_query_is_auto_updatable(Query *viewquery, diff --git a/src/interfaces/libpq/fe-exec.c b/src/interfaces/libpq/fe-exec.c index 4c0114c514d..a432636322b 100644 --- a/src/interfaces/libpq/fe-exec.c +++ b/src/interfaces/libpq/fe-exec.c @@ -3055,9 +3055,9 @@ PQoidValue(const PGresult *res) /* * PQcmdTuples - - * If the last command was INSERT/UPDATE/DELETE/MOVE/FETCH/COPY, return - * a string containing the number of inserted/affected tuples. If not, - * return "". + * If the last command was INSERT/UPDATE/DELETE/MERGE/MOVE/FETCH/COPY, + * return a string containing the number of inserted/affected tuples. + * If not, return "". * * XXX: this should probably return an int */ @@ -3084,7 +3084,8 @@ PQcmdTuples(PGresult *res) strncmp(res->cmdStatus, "DELETE ", 7) == 0 || strncmp(res->cmdStatus, "UPDATE ", 7) == 0) p = res->cmdStatus + 7; - else if (strncmp(res->cmdStatus, "FETCH ", 6) == 0) + else if (strncmp(res->cmdStatus, "FETCH ", 6) == 0 || + strncmp(res->cmdStatus, "MERGE ", 6) == 0) p = res->cmdStatus + 6; else if (strncmp(res->cmdStatus, "MOVE ", 5) == 0 || strncmp(res->cmdStatus, "COPY ", 5) == 0) diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c index f574aa77f0c..67123f85c95 100644 --- a/src/pl/plpgsql/src/pl_exec.c +++ b/src/pl/plpgsql/src/pl_exec.c @@ -3967,7 +3967,7 @@ exec_stmt_execsql(PLpgSQL_execstate *estate, /* * On the first call for this statement generate the plan, and detect - * whether the statement is INSERT/UPDATE/DELETE + * whether the statement is INSERT/UPDATE/DELETE/MERGE */ if (expr->plan == NULL) { @@ -3988,6 +3988,7 @@ exec_stmt_execsql(PLpgSQL_execstate *estate, { if (q->commandType == CMD_INSERT || q->commandType == CMD_UPDATE || + q->commandType == CMD_MERGE || q->commandType == CMD_DELETE) stmt->mod_stmt = true; } @@ -4045,6 +4046,7 @@ exec_stmt_execsql(PLpgSQL_execstate *estate, case SPI_OK_INSERT_RETURNING: case SPI_OK_UPDATE_RETURNING: case SPI_OK_DELETE_RETURNING: + case SPI_OK_MERGE: Assert(stmt->mod_stmt); exec_set_found(estate, (SPI_processed != 0)); break; @@ -4222,6 +4224,7 @@ exec_stmt_dynexecute(PLpgSQL_execstate *estate, case SPI_OK_INSERT_RETURNING: case SPI_OK_UPDATE_RETURNING: case SPI_OK_DELETE_RETURNING: + case SPI_OK_MERGE: case SPI_OK_UTILITY: case SPI_OK_REWRITTEN: break; diff --git a/src/pl/plpgsql/src/pl_gram.y b/src/pl/plpgsql/src/pl_gram.y index b8562ca8b4e..d928efa13ea 100644 --- a/src/pl/plpgsql/src/pl_gram.y +++ b/src/pl/plpgsql/src/pl_gram.y @@ -304,6 +304,7 @@ static void check_raise_parameters(PLpgSQL_stmt_raise *stmt); %token <keyword> K_LAST %token <keyword> K_LOG %token <keyword> K_LOOP +%token <keyword> K_MERGE %token <keyword> K_MESSAGE %token <keyword> K_MESSAGE_TEXT %token <keyword> K_MOVE @@ -1947,6 +1948,10 @@ stmt_execsql : K_IMPORT { $$ = make_execsql_stmt(K_INSERT, @1); } + | K_MERGE + { + $$ = make_execsql_stmt(K_MERGE, @1); + } | T_WORD { int tok; @@ -2469,6 +2474,7 @@ unreserved_keyword : | K_IS | K_LAST | K_LOG + | K_MERGE | K_MESSAGE | K_MESSAGE_TEXT | K_MOVE @@ -2930,6 +2936,8 @@ make_execsql_stmt(int firsttoken, int location) { if (prev_tok == K_INSERT) continue; /* INSERT INTO is not an INTO-target */ + if (prev_tok == K_MERGE) + continue; /* MERGE INTO is not an INTO-target */ if (firsttoken == K_IMPORT) continue; /* IMPORT ... INTO is not an INTO-target */ if (have_into) diff --git a/src/pl/plpgsql/src/pl_scanner.c b/src/pl/plpgsql/src/pl_scanner.c index 08614a89a87..81b5bd1fe03 100644 --- a/src/pl/plpgsql/src/pl_scanner.c +++ b/src/pl/plpgsql/src/pl_scanner.c @@ -138,6 +138,7 @@ static const ScanKeyword unreserved_keywords[] = { PG_KEYWORD("is", K_IS, UNRESERVED_KEYWORD) PG_KEYWORD("last", K_LAST, UNRESERVED_KEYWORD) PG_KEYWORD("log", K_LOG, UNRESERVED_KEYWORD) + PG_KEYWORD("merge", K_MERGE, UNRESERVED_KEYWORD) PG_KEYWORD("message", K_MESSAGE, UNRESERVED_KEYWORD) PG_KEYWORD("message_text", K_MESSAGE_TEXT, UNRESERVED_KEYWORD) PG_KEYWORD("move", K_MOVE, UNRESERVED_KEYWORD) diff --git a/src/pl/plpgsql/src/plpgsql.h b/src/pl/plpgsql/src/plpgsql.h index dc90fe532fd..3f617201d67 100644 --- a/src/pl/plpgsql/src/plpgsql.h +++ b/src/pl/plpgsql/src/plpgsql.h @@ -846,8 +846,8 @@ typedef struct PLpgSQL_stmt_execsql PLpgSQL_stmt_type cmd_type; int lineno; PLpgSQL_expr *sqlstmt; - bool mod_stmt; /* is the stmt INSERT/UPDATE/DELETE? Note: - * mod_stmt is set when we plan the query */ + bool mod_stmt; /* is the stmt INSERT/UPDATE/DELETE/MERGE? + * Note mod_stmt is set when we plan the query */ bool into; /* INTO supplied? */ bool strict; /* INTO STRICT flag */ PLpgSQL_variable *target; /* INTO target (record or row) */ diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule index d3965fe73f7..99dd7c6bdbf 100644 --- a/src/test/isolation/isolation_schedule +++ b/src/test/isolation/isolation_schedule @@ -33,6 +33,10 @@ test: insert-conflict-do-update test: insert-conflict-do-update-2 test: insert-conflict-do-update-3 test: insert-conflict-toast +test: merge-insert-update +test: merge-delete +test: merge-update +test: merge-match-recheck test: delete-abort-savept test: delete-abort-savept-2 test: aborted-keyrevoke diff --git a/src/test/regress/expected/identity.out b/src/test/regress/expected/identity.out index d7d5178f5d8..3a6016c80a6 100644 --- a/src/test/regress/expected/identity.out +++ b/src/test/regress/expected/identity.out @@ -386,3 +386,58 @@ CREATE TABLE itest_child PARTITION OF itest_parent ( ) FOR VALUES FROM ('2016-07-01') TO ('2016-08-01'); -- error ERROR: identity columns are not supported on partitions DROP TABLE itest_parent; +-- MERGE tests +CREATE TABLE itest14 (a int GENERATED ALWAYS AS IDENTITY, b text); +CREATE TABLE itest15 (a int GENERATED BY DEFAULT AS IDENTITY, b text); +MERGE INTO itest14 t +USING (SELECT 10 AS s_a, 'inserted by merge' AS s_b) s +ON t.a = s.s_a +WHEN NOT MATCHED THEN + INSERT (a, b) VALUES (s.s_a, s.s_b); +ERROR: cannot insert into column "a" +DETAIL: Column "a" is an identity column defined as GENERATED ALWAYS. +HINT: Use OVERRIDING SYSTEM VALUE to override. +MERGE INTO itest14 t +USING (SELECT 20 AS s_a, 'inserted by merge' AS s_b) s +ON t.a = s.s_a +WHEN NOT MATCHED THEN + INSERT (a, b) OVERRIDING USER VALUE VALUES (s.s_a, s.s_b); +ERROR: cannot insert into column "a" +DETAIL: Column "a" is an identity column defined as GENERATED ALWAYS. +HINT: Use OVERRIDING SYSTEM VALUE to override. +MERGE INTO itest14 t +USING (SELECT 30 AS s_a, 'inserted by merge' AS s_b) s +ON t.a = s.s_a +WHEN NOT MATCHED THEN + INSERT (a, b) OVERRIDING SYSTEM VALUE VALUES (s.s_a, s.s_b); +MERGE INTO itest15 t +USING (SELECT 10 AS s_a, 'inserted by merge' AS s_b) s +ON t.a = s.s_a +WHEN NOT MATCHED THEN + INSERT (a, b) VALUES (s.s_a, s.s_b); +MERGE INTO itest15 t +USING (SELECT 20 AS s_a, 'inserted by merge' AS s_b) s +ON t.a = s.s_a +WHEN NOT MATCHED THEN + INSERT (a, b) OVERRIDING USER VALUE VALUES (s.s_a, s.s_b); +MERGE INTO itest15 t +USING (SELECT 30 AS s_a, 'inserted by merge' AS s_b) s +ON t.a = s.s_a +WHEN NOT MATCHED THEN + INSERT (a, b) OVERRIDING SYSTEM VALUE VALUES (s.s_a, s.s_b); +SELECT * FROM itest14; + a | b +----+------------------- + 30 | inserted by merge +(1 row) + +SELECT * FROM itest15; + a | b +----+------------------- + 10 | inserted by merge + 1 | inserted by merge + 30 | inserted by merge +(3 rows) + +DROP TABLE itest14; +DROP TABLE itest15; diff --git a/src/test/regress/expected/privileges.out b/src/test/regress/expected/privileges.out index ac8968d24f5..864f2c13457 100644 --- a/src/test/regress/expected/privileges.out +++ b/src/test/regress/expected/privileges.out @@ -517,6 +517,104 @@ SELECT atest6 FROM atest6; -- ok (0 rows) COPY atest6 TO stdout; -- ok +-- test column privileges with MERGE +SET SESSION AUTHORIZATION regress_priv_user1; +CREATE TABLE mtarget (a int, b text); +CREATE TABLE msource (a int, b text); +INSERT INTO mtarget VALUES (1, 'init1'), (2, 'init2'); +INSERT INTO msource VALUES (1, 'source1'), (2, 'source2'), (3, 'source3'); +GRANT SELECT (a) ON msource TO regress_priv_user4; +GRANT SELECT (a) ON mtarget TO regress_priv_user4; +GRANT INSERT (a,b) ON mtarget TO regress_priv_user4; +GRANT UPDATE (b) ON mtarget TO regress_priv_user4; +SET SESSION AUTHORIZATION regress_priv_user4; +-- +-- test source privileges +-- +-- fail (no SELECT priv on s.b) +MERGE INTO mtarget t USING msource s ON t.a = s.a +WHEN MATCHED THEN + UPDATE SET b = s.b +WHEN NOT MATCHED THEN + INSERT VALUES (a, NULL); +ERROR: permission denied for table msource +-- fail (s.b used in the INSERTed values) +MERGE INTO mtarget t USING msource s ON t.a = s.a +WHEN MATCHED THEN + UPDATE SET b = 'x' +WHEN NOT MATCHED THEN + INSERT VALUES (a, b); +ERROR: permission denied for table msource +-- fail (s.b used in the WHEN quals) +MERGE INTO mtarget t USING msource s ON t.a = s.a +WHEN MATCHED AND s.b = 'x' THEN + UPDATE SET b = 'x' +WHEN NOT MATCHED THEN + INSERT VALUES (a, NULL); +ERROR: permission denied for table msource +-- this should be ok since only s.a is accessed +BEGIN; +MERGE INTO mtarget t USING msource s ON t.a = s.a +WHEN MATCHED THEN + UPDATE SET b = 'ok' +WHEN NOT MATCHED THEN + INSERT VALUES (a, NULL); +ROLLBACK; +SET SESSION AUTHORIZATION regress_priv_user1; +GRANT SELECT (b) ON msource TO regress_priv_user4; +SET SESSION AUTHORIZATION regress_priv_user4; +-- should now be ok +BEGIN; +MERGE INTO mtarget t USING msource s ON t.a = s.a +WHEN MATCHED THEN + UPDATE SET b = s.b +WHEN NOT MATCHED THEN + INSERT VALUES (a, b); +ROLLBACK; +-- +-- test target privileges +-- +-- fail (no SELECT priv on t.b) +MERGE INTO mtarget t USING msource s ON t.a = s.a +WHEN MATCHED THEN + UPDATE SET b = t.b +WHEN NOT MATCHED THEN + INSERT VALUES (a, NULL); +ERROR: permission denied for table mtarget +-- fail (no UPDATE on t.a) +MERGE INTO mtarget t USING msource s ON t.a = s.a +WHEN MATCHED THEN + UPDATE SET b = s.b, a = t.a + 1 +WHEN NOT MATCHED THEN + INSERT VALUES (a, b); +ERROR: permission denied for table mtarget +-- fail (no SELECT on t.b) +MERGE INTO mtarget t USING msource s ON t.a = s.a +WHEN MATCHED AND t.b IS NOT NULL THEN + UPDATE SET b = s.b +WHEN NOT MATCHED THEN + INSERT VALUES (a, b); +ERROR: permission denied for table mtarget +-- ok +BEGIN; +MERGE INTO mtarget t USING msource s ON t.a = s.a +WHEN MATCHED THEN + UPDATE SET b = s.b; +ROLLBACK; +-- fail (no DELETE) +MERGE INTO mtarget t USING msource s ON t.a = s.a +WHEN MATCHED AND t.b IS NOT NULL THEN + DELETE; +ERROR: permission denied for table mtarget +-- grant delete privileges +SET SESSION AUTHORIZATION regress_priv_user1; +GRANT DELETE ON mtarget TO regress_priv_user4; +-- should be ok now +BEGIN; +MERGE INTO mtarget t USING msource s ON t.a = s.a +WHEN MATCHED AND t.b IS NOT NULL THEN + DELETE; +ROLLBACK; -- check error reporting with column privs SET SESSION AUTHORIZATION regress_priv_user1; CREATE TABLE t1 (c1 int, c2 int, c3 int check (c3 < 5), primary key (c1, c2)); diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out index f1ae40df61c..bf7af3ba826 100644 --- a/src/test/regress/expected/rowsecurity.out +++ b/src/test/regress/expected/rowsecurity.out @@ -2139,6 +2139,188 @@ INSERT INTO document VALUES (1, (SELECT cid from category WHERE cname = 'novel') ON CONFLICT (did) DO UPDATE SET dauthor = 'regress_rls_carol'; ERROR: new row violates row-level security policy for table "document" -- +-- MERGE +-- +RESET SESSION AUTHORIZATION; +DROP POLICY p3_with_all ON document; +ALTER TABLE document ADD COLUMN dnotes text DEFAULT ''; +-- all documents are readable +CREATE POLICY p1 ON document FOR SELECT USING (true); +-- one may insert documents only authored by them +CREATE POLICY p2 ON document FOR INSERT WITH CHECK (dauthor = current_user); +-- one may only update documents in 'novel' category +CREATE POLICY p3 ON document FOR UPDATE + USING (cid = (SELECT cid from category WHERE cname = 'novel')) + WITH CHECK (dauthor = current_user); +-- one may only delete documents in 'manga' category +CREATE POLICY p4 ON document FOR DELETE + USING (cid = (SELECT cid from category WHERE cname = 'manga')); +SELECT * FROM document; + did | cid | dlevel | dauthor | dtitle | dnotes +-----+-----+--------+-------------------+----------------------------------+-------- + 1 | 11 | 1 | regress_rls_bob | my first novel | + 3 | 22 | 2 | regress_rls_bob | my science fiction | + 4 | 44 | 1 | regress_rls_bob | my first manga | + 5 | 44 | 2 | regress_rls_bob | my second manga | + 6 | 22 | 1 | regress_rls_carol | great science fiction | + 7 | 33 | 2 | regress_rls_carol | great technology book | + 8 | 44 | 1 | regress_rls_carol | great manga | + 9 | 22 | 1 | regress_rls_dave | awesome science fiction | + 10 | 33 | 2 | regress_rls_dave | awesome technology book | + 11 | 33 | 1 | regress_rls_carol | hoge | + 33 | 22 | 1 | regress_rls_bob | okay science fiction | + 2 | 11 | 2 | regress_rls_bob | my first novel | + 78 | 33 | 1 | regress_rls_bob | some technology novel | + 79 | 33 | 1 | regress_rls_bob | technology book, can only insert | +(14 rows) + +SET SESSION AUTHORIZATION regress_rls_bob; +-- Fails, since update violates WITH CHECK qual on dauthor +MERGE INTO document d +USING (SELECT 1 as sdid) s +ON did = s.sdid +WHEN MATCHED THEN + UPDATE SET dnotes = dnotes || ' notes added by merge1 ', dauthor = 'regress_rls_alice'; +ERROR: new row violates row-level security policy for table "document" +-- Should be OK since USING and WITH CHECK quals pass +MERGE INTO document d +USING (SELECT 1 as sdid) s +ON did = s.sdid +WHEN MATCHED THEN + UPDATE SET dnotes = dnotes || ' notes added by merge2 '; +-- Even when dauthor is updated explicitly, but to the existing value +MERGE INTO document d +USING (SELECT 1 as sdid) s +ON did = s.sdid +WHEN MATCHED THEN + UPDATE SET dnotes = dnotes || ' notes added by merge3 ', dauthor = 'regress_rls_bob'; +-- There is a MATCH for did = 3, but UPDATE's USING qual does not allow +-- updating an item in category 'science fiction' +MERGE INTO document d +USING (SELECT 3 as sdid) s +ON did = s.sdid +WHEN MATCHED THEN + UPDATE SET dnotes = dnotes || ' notes added by merge '; +ERROR: target row violates row-level security policy (USING expression) for table "document" +-- The same thing with DELETE action, but fails again because no permissions +-- to delete items in 'science fiction' category that did 3 belongs to. +MERGE INTO document d +USING (SELECT 3 as sdid) s +ON did = s.sdid +WHEN MATCHED THEN + DELETE; +ERROR: target row violates row-level security policy (USING expression) for table "document" +-- Document with did 4 belongs to 'manga' category which is allowed for +-- deletion. But this fails because the UPDATE action is matched first and +-- UPDATE policy does not allow updation in the category. +MERGE INTO document d +USING (SELECT 4 as sdid) s +ON did = s.sdid +WHEN MATCHED AND dnotes = '' THEN + UPDATE SET dnotes = dnotes || ' notes added by merge ' +WHEN MATCHED THEN + DELETE; +ERROR: target row violates row-level security policy (USING expression) for table "document" +-- UPDATE action is not matched this time because of the WHEN AND qual. +-- DELETE still fails because role regress_rls_bob does not have SELECT +-- privileges on 'manga' category row in the category table. +MERGE INTO document d +USING (SELECT 4 as sdid) s +ON did = s.sdid +WHEN MATCHED AND dnotes <> '' THEN + UPDATE SET dnotes = dnotes || ' notes added by merge ' +WHEN MATCHED THEN + DELETE; +ERROR: target row violates row-level security policy (USING expression) for table "document" +SELECT * FROM document WHERE did = 4; + did | cid | dlevel | dauthor | dtitle | dnotes +-----+-----+--------+-----------------+----------------+-------- + 4 | 44 | 1 | regress_rls_bob | my first manga | +(1 row) + +-- Switch to regress_rls_carol role and try the DELETE again. It should succeed +-- this time +RESET SESSION AUTHORIZATION; +SET SESSION AUTHORIZATION regress_rls_carol; +MERGE INTO document d +USING (SELECT 4 as sdid) s +ON did = s.sdid +WHEN MATCHED AND dnotes <> '' THEN + UPDATE SET dnotes = dnotes || ' notes added by merge ' +WHEN MATCHED THEN + DELETE; +-- Switch back to regress_rls_bob role +RESET SESSION AUTHORIZATION; +SET SESSION AUTHORIZATION regress_rls_bob; +-- Try INSERT action. This fails because we are trying to insert +-- dauthor = regress_rls_dave and INSERT's WITH CHECK does not allow +-- that +MERGE INTO document d +USING (SELECT 12 as sdid) s +ON did = s.sdid +WHEN MATCHED THEN + DELETE +WHEN NOT MATCHED THEN + INSERT VALUES (12, 11, 1, 'regress_rls_dave', 'another novel'); +ERROR: new row violates row-level security policy for table "document" +-- This should be fine +MERGE INTO document d +USING (SELECT 12 as sdid) s +ON did = s.sdid +WHEN MATCHED THEN + DELETE +WHEN NOT MATCHED THEN + INSERT VALUES (12, 11, 1, 'regress_rls_bob', 'another novel'); +-- ok +MERGE INTO document d +USING (SELECT 1 as sdid) s +ON did = s.sdid +WHEN MATCHED THEN + UPDATE SET dnotes = dnotes || ' notes added by merge4 ' +WHEN NOT MATCHED THEN + INSERT VALUES (12, 11, 1, 'regress_rls_bob', 'another novel'); +-- drop and create a new SELECT policy which prevents us from reading +-- any document except with category 'magna' +RESET SESSION AUTHORIZATION; +DROP POLICY p1 ON document; +CREATE POLICY p1 ON document FOR SELECT + USING (cid = (SELECT cid from category WHERE cname = 'manga')); +SET SESSION AUTHORIZATION regress_rls_bob; +-- MERGE can no longer see the matching row and hence attempts the +-- NOT MATCHED action, which results in unique key violation +MERGE INTO document d +USING (SELECT 1 as sdid) s +ON did = s.sdid +WHEN MATCHED THEN + UPDATE SET dnotes = dnotes || ' notes added by merge5 ' +WHEN NOT MATCHED THEN + INSERT VALUES (12, 11, 1, 'regress_rls_bob', 'another novel'); +ERROR: duplicate key value violates unique constraint "document_pkey" +RESET SESSION AUTHORIZATION; +-- drop the restrictive SELECT policy so that we can look at the +-- final state of the table +DROP POLICY p1 ON document; +-- Just check everything went per plan +SELECT * FROM document; + did | cid | dlevel | dauthor | dtitle | dnotes +-----+-----+--------+-------------------+----------------------------------+----------------------------------------------------------------------- + 3 | 22 | 2 | regress_rls_bob | my science fiction | + 5 | 44 | 2 | regress_rls_bob | my second manga | + 6 | 22 | 1 | regress_rls_carol | great science fiction | + 7 | 33 | 2 | regress_rls_carol | great technology book | + 8 | 44 | 1 | regress_rls_carol | great manga | + 9 | 22 | 1 | regress_rls_dave | awesome science fiction | + 10 | 33 | 2 | regress_rls_dave | awesome technology book | + 11 | 33 | 1 | regress_rls_carol | hoge | + 33 | 22 | 1 | regress_rls_bob | okay science fiction | + 2 | 11 | 2 | regress_rls_bob | my first novel | + 78 | 33 | 1 | regress_rls_bob | some technology novel | + 79 | 33 | 1 | regress_rls_bob | technology book, can only insert | + 12 | 11 | 1 | regress_rls_bob | another novel | + 1 | 11 | 1 | regress_rls_bob | my first novel | notes added by merge2 notes added by merge3 notes added by merge4 +(14 rows) + +-- -- ROLE/GROUP -- SET SESSION AUTHORIZATION regress_rls_alice; diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out index ae0cd253d5f..a4ec5690aa2 100644 --- a/src/test/regress/expected/rules.out +++ b/src/test/regress/expected/rules.out @@ -3265,6 +3265,37 @@ CREATE RULE rules_parted_table_insert AS ON INSERT to rules_parted_table ALTER RULE rules_parted_table_insert ON rules_parted_table RENAME TO rules_parted_table_insert_redirect; DROP TABLE rules_parted_table; -- +-- test MERGE +-- +CREATE TABLE rule_merge1 (a int, b text); +CREATE TABLE rule_merge2 (a int, b text); +CREATE RULE rule1 AS ON INSERT TO rule_merge1 + DO INSTEAD INSERT INTO rule_merge2 VALUES (NEW.*); +CREATE RULE rule2 AS ON UPDATE TO rule_merge1 + DO INSTEAD UPDATE rule_merge2 SET a = NEW.a, b = NEW.b + WHERE a = OLD.a; +CREATE RULE rule3 AS ON DELETE TO rule_merge1 + DO INSTEAD DELETE FROM rule_merge2 WHERE a = OLD.a; +-- MERGE not supported for table with rules +MERGE INTO rule_merge1 t USING (SELECT 1 AS a) s + ON t.a = s.a + WHEN MATCHED AND t.a < 2 THEN + UPDATE SET b = b || ' updated by merge' + WHEN MATCHED AND t.a > 2 THEN + DELETE + WHEN NOT MATCHED THEN + INSERT VALUES (s.a, ''); +ERROR: MERGE is not supported for relations with rules +-- should be ok with the other table though +MERGE INTO rule_merge2 t USING (SELECT 1 AS a) s + ON t.a = s.a + WHEN MATCHED AND t.a < 2 THEN + UPDATE SET b = b || ' updated by merge' + WHEN MATCHED AND t.a > 2 THEN + DELETE + WHEN NOT MATCHED THEN + INSERT VALUES (s.a, ''); +-- -- Test enabling/disabling -- CREATE TABLE ruletest1 (a int); diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out index 387e40d67d4..7f4a94ef7dc 100644 --- a/src/test/regress/expected/triggers.out +++ b/src/test/regress/expected/triggers.out @@ -2761,6 +2761,54 @@ delete from self_ref where a = 1; NOTICE: trigger_func(self_ref) called: action = DELETE, when = BEFORE, level = STATEMENT NOTICE: trigger = self_ref_s_trig, old table = (1,), (2,1), (3,2), (4,3) drop table self_ref; +-- +-- test transition tables with MERGE +-- +create table merge_target_table (a int primary key, b text); +create trigger merge_target_table_insert_trig + after insert on merge_target_table referencing new table as new_table + for each statement execute procedure dump_insert(); +create trigger merge_target_table_update_trig + after update on merge_target_table referencing old table as old_table new table as new_table + for each statement execute procedure dump_update(); +create trigger merge_target_table_delete_trig + after delete on merge_target_table referencing old table as old_table + for each statement execute procedure dump_delete(); +create table merge_source_table (a int, b text); +insert into merge_source_table + values (1, 'initial1'), (2, 'initial2'), + (3, 'initial3'), (4, 'initial4'); +merge into merge_target_table t +using merge_source_table s +on t.a = s.a +when not matched then + insert values (a, b); +NOTICE: trigger = merge_target_table_insert_trig, new table = (1,initial1), (2,initial2), (3,initial3), (4,initial4) +merge into merge_target_table t +using merge_source_table s +on t.a = s.a +when matched and s.a <= 2 then + update set b = t.b || ' updated by merge' +when matched and s.a > 2 then + delete +when not matched then + insert values (a, b); +NOTICE: trigger = merge_target_table_delete_trig, old table = (3,initial3), (4,initial4) +NOTICE: trigger = merge_target_table_update_trig, old table = (1,initial1), (2,initial2), new table = (1,"initial1 updated by merge"), (2,"initial2 updated by merge") +NOTICE: trigger = merge_target_table_insert_trig, new table = <NULL> +merge into merge_target_table t +using merge_source_table s +on t.a = s.a +when matched and s.a <= 2 then + update set b = t.b || ' updated again by merge' +when matched and s.a > 2 then + delete +when not matched then + insert values (a, b); +NOTICE: trigger = merge_target_table_delete_trig, old table = <NULL> +NOTICE: trigger = merge_target_table_update_trig, old table = (1,"initial1 updated by merge"), (2,"initial2 updated by merge"), new table = (1,"initial1 updated by merge updated again by merge"), (2,"initial2 updated by merge updated again by merge") +NOTICE: trigger = merge_target_table_insert_trig, new table = (3,initial3), (4,initial4) +drop table merge_source_table, merge_target_table; -- cleanup drop function dump_insert(); drop function dump_update(); diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index d858a0e7db4..20d67457308 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -84,7 +84,7 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi # ---------- # Another group of parallel tests # ---------- -test: brin gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password func_index +test: brin gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password func_index merge # ---------- # Another group of parallel tests diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule index 99f8ca37ba9..a08169f256a 100644 --- a/src/test/regress/serial_schedule +++ b/src/test/regress/serial_schedule @@ -123,6 +123,7 @@ test: tablesample test: groupingsets test: drop_operator test: password +test: merge test: alter_generic test: alter_operator test: misc diff --git a/src/test/regress/sql/identity.sql b/src/test/regress/sql/identity.sql index a35f331f4e0..f8f34eaf185 100644 --- a/src/test/regress/sql/identity.sql +++ b/src/test/regress/sql/identity.sql @@ -246,3 +246,48 @@ CREATE TABLE itest_child PARTITION OF itest_parent ( f3 WITH OPTIONS GENERATED ALWAYS AS IDENTITY ) FOR VALUES FROM ('2016-07-01') TO ('2016-08-01'); -- error DROP TABLE itest_parent; + +-- MERGE tests +CREATE TABLE itest14 (a int GENERATED ALWAYS AS IDENTITY, b text); +CREATE TABLE itest15 (a int GENERATED BY DEFAULT AS IDENTITY, b text); + +MERGE INTO itest14 t +USING (SELECT 10 AS s_a, 'inserted by merge' AS s_b) s +ON t.a = s.s_a +WHEN NOT MATCHED THEN + INSERT (a, b) VALUES (s.s_a, s.s_b); + +MERGE INTO itest14 t +USING (SELECT 20 AS s_a, 'inserted by merge' AS s_b) s +ON t.a = s.s_a +WHEN NOT MATCHED THEN + INSERT (a, b) OVERRIDING USER VALUE VALUES (s.s_a, s.s_b); + +MERGE INTO itest14 t +USING (SELECT 30 AS s_a, 'inserted by merge' AS s_b) s +ON t.a = s.s_a +WHEN NOT MATCHED THEN + INSERT (a, b) OVERRIDING SYSTEM VALUE VALUES (s.s_a, s.s_b); + +MERGE INTO itest15 t +USING (SELECT 10 AS s_a, 'inserted by merge' AS s_b) s +ON t.a = s.s_a +WHEN NOT MATCHED THEN + INSERT (a, b) VALUES (s.s_a, s.s_b); + +MERGE INTO itest15 t +USING (SELECT 20 AS s_a, 'inserted by merge' AS s_b) s +ON t.a = s.s_a +WHEN NOT MATCHED THEN + INSERT (a, b) OVERRIDING USER VALUE VALUES (s.s_a, s.s_b); + +MERGE INTO itest15 t +USING (SELECT 30 AS s_a, 'inserted by merge' AS s_b) s +ON t.a = s.s_a +WHEN NOT MATCHED THEN + INSERT (a, b) OVERRIDING SYSTEM VALUE VALUES (s.s_a, s.s_b); + +SELECT * FROM itest14; +SELECT * FROM itest15; +DROP TABLE itest14; +DROP TABLE itest15; diff --git a/src/test/regress/sql/privileges.sql b/src/test/regress/sql/privileges.sql index f7f3bbbeeb6..0a8abf20769 100644 --- a/src/test/regress/sql/privileges.sql +++ b/src/test/regress/sql/privileges.sql @@ -349,6 +349,114 @@ UPDATE atest5 SET one = 1; -- fail SELECT atest6 FROM atest6; -- ok COPY atest6 TO stdout; -- ok +-- test column privileges with MERGE +SET SESSION AUTHORIZATION regress_priv_user1; +CREATE TABLE mtarget (a int, b text); +CREATE TABLE msource (a int, b text); +INSERT INTO mtarget VALUES (1, 'init1'), (2, 'init2'); +INSERT INTO msource VALUES (1, 'source1'), (2, 'source2'), (3, 'source3'); + +GRANT SELECT (a) ON msource TO regress_priv_user4; +GRANT SELECT (a) ON mtarget TO regress_priv_user4; +GRANT INSERT (a,b) ON mtarget TO regress_priv_user4; +GRANT UPDATE (b) ON mtarget TO regress_priv_user4; + +SET SESSION AUTHORIZATION regress_priv_user4; + +-- +-- test source privileges +-- + +-- fail (no SELECT priv on s.b) +MERGE INTO mtarget t USING msource s ON t.a = s.a +WHEN MATCHED THEN + UPDATE SET b = s.b +WHEN NOT MATCHED THEN + INSERT VALUES (a, NULL); + +-- fail (s.b used in the INSERTed values) +MERGE INTO mtarget t USING msource s ON t.a = s.a +WHEN MATCHED THEN + UPDATE SET b = 'x' +WHEN NOT MATCHED THEN + INSERT VALUES (a, b); + +-- fail (s.b used in the WHEN quals) +MERGE INTO mtarget t USING msource s ON t.a = s.a +WHEN MATCHED AND s.b = 'x' THEN + UPDATE SET b = 'x' +WHEN NOT MATCHED THEN + INSERT VALUES (a, NULL); + +-- this should be ok since only s.a is accessed +BEGIN; +MERGE INTO mtarget t USING msource s ON t.a = s.a +WHEN MATCHED THEN + UPDATE SET b = 'ok' +WHEN NOT MATCHED THEN + INSERT VALUES (a, NULL); +ROLLBACK; + +SET SESSION AUTHORIZATION regress_priv_user1; +GRANT SELECT (b) ON msource TO regress_priv_user4; +SET SESSION AUTHORIZATION regress_priv_user4; + +-- should now be ok +BEGIN; +MERGE INTO mtarget t USING msource s ON t.a = s.a +WHEN MATCHED THEN + UPDATE SET b = s.b +WHEN NOT MATCHED THEN + INSERT VALUES (a, b); +ROLLBACK; + +-- +-- test target privileges +-- + +-- fail (no SELECT priv on t.b) +MERGE INTO mtarget t USING msource s ON t.a = s.a +WHEN MATCHED THEN + UPDATE SET b = t.b +WHEN NOT MATCHED THEN + INSERT VALUES (a, NULL); + +-- fail (no UPDATE on t.a) +MERGE INTO mtarget t USING msource s ON t.a = s.a +WHEN MATCHED THEN + UPDATE SET b = s.b, a = t.a + 1 +WHEN NOT MATCHED THEN + INSERT VALUES (a, b); + +-- fail (no SELECT on t.b) +MERGE INTO mtarget t USING msource s ON t.a = s.a +WHEN MATCHED AND t.b IS NOT NULL THEN + UPDATE SET b = s.b +WHEN NOT MATCHED THEN + INSERT VALUES (a, b); + +-- ok +BEGIN; +MERGE INTO mtarget t USING msource s ON t.a = s.a +WHEN MATCHED THEN + UPDATE SET b = s.b; +ROLLBACK; + +-- fail (no DELETE) +MERGE INTO mtarget t USING msource s ON t.a = s.a +WHEN MATCHED AND t.b IS NOT NULL THEN + DELETE; + +-- grant delete privileges +SET SESSION AUTHORIZATION regress_priv_user1; +GRANT DELETE ON mtarget TO regress_priv_user4; +-- should be ok now +BEGIN; +MERGE INTO mtarget t USING msource s ON t.a = s.a +WHEN MATCHED AND t.b IS NOT NULL THEN + DELETE; +ROLLBACK; + -- check error reporting with column privs SET SESSION AUTHORIZATION regress_priv_user1; CREATE TABLE t1 (c1 int, c2 int, c3 int check (c3 < 5), primary key (c1, c2)); diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql index f3a31dbee03..6c752089980 100644 --- a/src/test/regress/sql/rowsecurity.sql +++ b/src/test/regress/sql/rowsecurity.sql @@ -813,6 +813,162 @@ INSERT INTO document VALUES (1, (SELECT cid from category WHERE cname = 'novel') ON CONFLICT (did) DO UPDATE SET dauthor = 'regress_rls_carol'; -- +-- MERGE +-- +RESET SESSION AUTHORIZATION; +DROP POLICY p3_with_all ON document; + +ALTER TABLE document ADD COLUMN dnotes text DEFAULT ''; +-- all documents are readable +CREATE POLICY p1 ON document FOR SELECT USING (true); +-- one may insert documents only authored by them +CREATE POLICY p2 ON document FOR INSERT WITH CHECK (dauthor = current_user); +-- one may only update documents in 'novel' category +CREATE POLICY p3 ON document FOR UPDATE + USING (cid = (SELECT cid from category WHERE cname = 'novel')) + WITH CHECK (dauthor = current_user); +-- one may only delete documents in 'manga' category +CREATE POLICY p4 ON document FOR DELETE + USING (cid = (SELECT cid from category WHERE cname = 'manga')); + +SELECT * FROM document; + +SET SESSION AUTHORIZATION regress_rls_bob; + +-- Fails, since update violates WITH CHECK qual on dauthor +MERGE INTO document d +USING (SELECT 1 as sdid) s +ON did = s.sdid +WHEN MATCHED THEN + UPDATE SET dnotes = dnotes || ' notes added by merge1 ', dauthor = 'regress_rls_alice'; + +-- Should be OK since USING and WITH CHECK quals pass +MERGE INTO document d +USING (SELECT 1 as sdid) s +ON did = s.sdid +WHEN MATCHED THEN + UPDATE SET dnotes = dnotes || ' notes added by merge2 '; + +-- Even when dauthor is updated explicitly, but to the existing value +MERGE INTO document d +USING (SELECT 1 as sdid) s +ON did = s.sdid +WHEN MATCHED THEN + UPDATE SET dnotes = dnotes || ' notes added by merge3 ', dauthor = 'regress_rls_bob'; + +-- There is a MATCH for did = 3, but UPDATE's USING qual does not allow +-- updating an item in category 'science fiction' +MERGE INTO document d +USING (SELECT 3 as sdid) s +ON did = s.sdid +WHEN MATCHED THEN + UPDATE SET dnotes = dnotes || ' notes added by merge '; + +-- The same thing with DELETE action, but fails again because no permissions +-- to delete items in 'science fiction' category that did 3 belongs to. +MERGE INTO document d +USING (SELECT 3 as sdid) s +ON did = s.sdid +WHEN MATCHED THEN + DELETE; + +-- Document with did 4 belongs to 'manga' category which is allowed for +-- deletion. But this fails because the UPDATE action is matched first and +-- UPDATE policy does not allow updation in the category. +MERGE INTO document d +USING (SELECT 4 as sdid) s +ON did = s.sdid +WHEN MATCHED AND dnotes = '' THEN + UPDATE SET dnotes = dnotes || ' notes added by merge ' +WHEN MATCHED THEN + DELETE; + +-- UPDATE action is not matched this time because of the WHEN AND qual. +-- DELETE still fails because role regress_rls_bob does not have SELECT +-- privileges on 'manga' category row in the category table. +MERGE INTO document d +USING (SELECT 4 as sdid) s +ON did = s.sdid +WHEN MATCHED AND dnotes <> '' THEN + UPDATE SET dnotes = dnotes || ' notes added by merge ' +WHEN MATCHED THEN + DELETE; + +SELECT * FROM document WHERE did = 4; + +-- Switch to regress_rls_carol role and try the DELETE again. It should succeed +-- this time +RESET SESSION AUTHORIZATION; +SET SESSION AUTHORIZATION regress_rls_carol; + +MERGE INTO document d +USING (SELECT 4 as sdid) s +ON did = s.sdid +WHEN MATCHED AND dnotes <> '' THEN + UPDATE SET dnotes = dnotes || ' notes added by merge ' +WHEN MATCHED THEN + DELETE; + +-- Switch back to regress_rls_bob role +RESET SESSION AUTHORIZATION; +SET SESSION AUTHORIZATION regress_rls_bob; + +-- Try INSERT action. This fails because we are trying to insert +-- dauthor = regress_rls_dave and INSERT's WITH CHECK does not allow +-- that +MERGE INTO document d +USING (SELECT 12 as sdid) s +ON did = s.sdid +WHEN MATCHED THEN + DELETE +WHEN NOT MATCHED THEN + INSERT VALUES (12, 11, 1, 'regress_rls_dave', 'another novel'); + +-- This should be fine +MERGE INTO document d +USING (SELECT 12 as sdid) s +ON did = s.sdid +WHEN MATCHED THEN + DELETE +WHEN NOT MATCHED THEN + INSERT VALUES (12, 11, 1, 'regress_rls_bob', 'another novel'); + +-- ok +MERGE INTO document d +USING (SELECT 1 as sdid) s +ON did = s.sdid +WHEN MATCHED THEN + UPDATE SET dnotes = dnotes || ' notes added by merge4 ' +WHEN NOT MATCHED THEN + INSERT VALUES (12, 11, 1, 'regress_rls_bob', 'another novel'); + +-- drop and create a new SELECT policy which prevents us from reading +-- any document except with category 'magna' +RESET SESSION AUTHORIZATION; +DROP POLICY p1 ON document; +CREATE POLICY p1 ON document FOR SELECT + USING (cid = (SELECT cid from category WHERE cname = 'manga')); + +SET SESSION AUTHORIZATION regress_rls_bob; + +-- MERGE can no longer see the matching row and hence attempts the +-- NOT MATCHED action, which results in unique key violation +MERGE INTO document d +USING (SELECT 1 as sdid) s +ON did = s.sdid +WHEN MATCHED THEN + UPDATE SET dnotes = dnotes || ' notes added by merge5 ' +WHEN NOT MATCHED THEN + INSERT VALUES (12, 11, 1, 'regress_rls_bob', 'another novel'); + +RESET SESSION AUTHORIZATION; +-- drop the restrictive SELECT policy so that we can look at the +-- final state of the table +DROP POLICY p1 ON document; +-- Just check everything went per plan +SELECT * FROM document; + +-- -- ROLE/GROUP -- SET SESSION AUTHORIZATION regress_rls_alice; diff --git a/src/test/regress/sql/rules.sql b/src/test/regress/sql/rules.sql index a82f52d1548..b866268892f 100644 --- a/src/test/regress/sql/rules.sql +++ b/src/test/regress/sql/rules.sql @@ -1192,6 +1192,39 @@ ALTER RULE rules_parted_table_insert ON rules_parted_table RENAME TO rules_parte DROP TABLE rules_parted_table; -- +-- test MERGE +-- +CREATE TABLE rule_merge1 (a int, b text); +CREATE TABLE rule_merge2 (a int, b text); +CREATE RULE rule1 AS ON INSERT TO rule_merge1 + DO INSTEAD INSERT INTO rule_merge2 VALUES (NEW.*); +CREATE RULE rule2 AS ON UPDATE TO rule_merge1 + DO INSTEAD UPDATE rule_merge2 SET a = NEW.a, b = NEW.b + WHERE a = OLD.a; +CREATE RULE rule3 AS ON DELETE TO rule_merge1 + DO INSTEAD DELETE FROM rule_merge2 WHERE a = OLD.a; + +-- MERGE not supported for table with rules +MERGE INTO rule_merge1 t USING (SELECT 1 AS a) s + ON t.a = s.a + WHEN MATCHED AND t.a < 2 THEN + UPDATE SET b = b || ' updated by merge' + WHEN MATCHED AND t.a > 2 THEN + DELETE + WHEN NOT MATCHED THEN + INSERT VALUES (s.a, ''); + +-- should be ok with the other table though +MERGE INTO rule_merge2 t USING (SELECT 1 AS a) s + ON t.a = s.a + WHEN MATCHED AND t.a < 2 THEN + UPDATE SET b = b || ' updated by merge' + WHEN MATCHED AND t.a > 2 THEN + DELETE + WHEN NOT MATCHED THEN + INSERT VALUES (s.a, ''); + +-- -- Test enabling/disabling -- CREATE TABLE ruletest1 (a int); diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sql index c6f31dd8c8f..b51c884eee4 100644 --- a/src/test/regress/sql/triggers.sql +++ b/src/test/regress/sql/triggers.sql @@ -2110,6 +2110,53 @@ delete from self_ref where a = 1; drop table self_ref; +-- +-- test transition tables with MERGE +-- +create table merge_target_table (a int primary key, b text); +create trigger merge_target_table_insert_trig + after insert on merge_target_table referencing new table as new_table + for each statement execute procedure dump_insert(); +create trigger merge_target_table_update_trig + after update on merge_target_table referencing old table as old_table new table as new_table + for each statement execute procedure dump_update(); +create trigger merge_target_table_delete_trig + after delete on merge_target_table referencing old table as old_table + for each statement execute procedure dump_delete(); + +create table merge_source_table (a int, b text); +insert into merge_source_table + values (1, 'initial1'), (2, 'initial2'), + (3, 'initial3'), (4, 'initial4'); + +merge into merge_target_table t +using merge_source_table s +on t.a = s.a +when not matched then + insert values (a, b); + +merge into merge_target_table t +using merge_source_table s +on t.a = s.a +when matched and s.a <= 2 then + update set b = t.b || ' updated by merge' +when matched and s.a > 2 then + delete +when not matched then + insert values (a, b); + +merge into merge_target_table t +using merge_source_table s +on t.a = s.a +when matched and s.a <= 2 then + update set b = t.b || ' updated again by merge' +when matched and s.a > 2 then + delete +when not matched then + insert values (a, b); + +drop table merge_source_table, merge_target_table; + -- cleanup drop function dump_insert(); drop function dump_update(); diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index abc10a8ffd4..6d8a44cd9e8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1228,6 +1228,8 @@ MemoryContextCallbackFunction MemoryContextCounters MemoryContextData MemoryContextMethods +MergeAction +MergeActionState MergeAppend MergeAppendPath MergeAppendState @@ -1235,6 +1237,7 @@ MergeJoin MergeJoinClause MergeJoinState MergePath +MergeStmt MergeScanSelCache MetaCommand MinMaxAggInfo |