diff --git a/doc/src/sgml/ref/merge.sgml b/doc/src/sgml/ref/merge.sgml new file mode 100644 index 0995fe0..bedb2c8 --- a/doc/src/sgml/ref/merge.sgml +++ b/doc/src/sgml/ref/merge.sgml @@ -25,6 +25,7 @@ PostgreSQL documentation MERGE INTO [ ONLY ] target_table_name [ * ] [ [ AS ] target_alias ] USING data_source ON join_condition when_clause [...] +[ RETURNING * | output_expression [ [ AS ] output_name ] [, ...] ] where data_source is: diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c new file mode 100644 index e34f583..aa3cca0 --- a/src/backend/commands/copy.c +++ b/src/backend/commands/copy.c @@ -274,12 +274,6 @@ DoCopy(ParseState *pstate, const CopyStm { Assert(stmt->query); - /* MERGE is allowed by parser, but unimplemented. Reject for now */ - if (IsA(stmt->query, MergeStmt)) - ereport(ERROR, - errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("MERGE not supported in COPY")); - query = makeNode(RawStmt); query->stmt = stmt->query; query->stmt_location = stmt_location; diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c new file mode 100644 index 8043b4e..e02d7d0 --- a/src/backend/commands/copyto.c +++ b/src/backend/commands/copyto.c @@ -510,7 +510,8 @@ BeginCopyTo(ParseState *pstate, { Assert(query->commandType == CMD_INSERT || query->commandType == CMD_UPDATE || - query->commandType == CMD_DELETE); + query->commandType == CMD_DELETE || + query->commandType == CMD_MERGE); ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c new file mode 100644 index 812ead9..8572b01 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -48,6 +48,7 @@ #include "utils/array.h" #include "utils/builtins.h" #include "utils/datum.h" +#include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/typcache.h" @@ -2485,6 +2486,22 @@ ExecInitFunc(ExprEvalStep *scratch, Expr InitFunctionCallInfoData(*fcinfo, flinfo, nargs, inputcollid, NULL, NULL); + /* + * Merge support functions should only be called directly from a MERGE + * command, and need access to the parent ModifyTableState. The parser + * should have checked that such functions only appear in the RETURNING + * list of a MERGE, so this should never fail. + */ + if (IsMergeSupportFunction(funcid)) + { + if (!state->parent || + !IsA(state->parent, ModifyTableState) || + ((ModifyTableState *) state->parent)->operation != CMD_MERGE) + elog(ERROR, "merge support function called in non-merge context"); + + fcinfo->context = (Node *) state->parent; + } + /* Keep extra copies of this info to save an indirection at runtime */ scratch->d.func.fn_addr = flinfo->fn_addr; scratch->d.func.nargs = nargs; diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c new file mode 100644 index 651ad24..3391269 --- a/src/backend/executor/execPartition.c +++ b/src/backend/executor/execPartition.c @@ -612,6 +612,9 @@ ExecInitPartitionInfo(ModifyTableState * * case or in the case of UPDATE tuple routing where we didn't find a * result rel to reuse. */ + + /* XXX: What about the MERGE case ??? */ + if (node && node->returningLists != NIL) { TupleTableSlot *slot; @@ -877,6 +880,7 @@ ExecInitPartitionInfo(ModifyTableState * List *firstMergeActionList = linitial(node->mergeActionLists); ListCell *lc; ExprContext *econtext = mtstate->ps.ps_ExprContext; + int action_idx = 1; if (part_attmap == NULL) part_attmap = @@ -897,6 +901,7 @@ ExecInitPartitionInfo(ModifyTableState * /* Generate the action's state for this relation */ action_state = makeNode(MergeActionState); action_state->mas_action = action; + action_state->mas_action_idx = action_idx++; /* And put the action in the appropriate list */ if (action->matched) diff --git a/src/backend/executor/functions.c b/src/backend/executor/functions.c new file mode 100644 index 50e06ec..7e4717a --- a/src/backend/executor/functions.c +++ b/src/backend/executor/functions.c @@ -1665,8 +1665,8 @@ check_sql_fn_retval(List *queryTreeLists /* * If it's a plain SELECT, it returns whatever the targetlist says. - * Otherwise, if it's INSERT/UPDATE/DELETE with RETURNING, it returns - * that. Otherwise, the function return type must be VOID. + * Otherwise, if it's INSERT/UPDATE/DELETE/MERGE with RETURNING, it + * returns that. Otherwise, the function return type must be VOID. * * Note: eventually replace this test with QueryReturnsTuples? We'd need * a more general method of determining the output type, though. Also, it @@ -1684,7 +1684,8 @@ check_sql_fn_retval(List *queryTreeLists else if (parse && (parse->commandType == CMD_INSERT || parse->commandType == CMD_UPDATE || - parse->commandType == CMD_DELETE) && + parse->commandType == CMD_DELETE || + parse->commandType == CMD_MERGE) && parse->returningList) { tlist = parse->returningList; diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c new file mode 100644 index f419c47..87ae37f --- a/src/backend/executor/nodeModifyTable.c +++ b/src/backend/executor/nodeModifyTable.c @@ -36,8 +36,7 @@ * RETURNING tuple after completing each row insert, update, or delete. * It must be called again to continue the operation. Without RETURNING, * we just loop within the node until all the work is done, then - * return NULL. This avoids useless call/return overhead. (MERGE does - * not support RETURNING.) + * return NULL. This avoids useless call/return overhead. */ #include "postgres.h" @@ -97,9 +96,6 @@ typedef struct ModifyTableContext TupleTableSlot *oldSlot, MergeActionState *relaction); - /* MERGE specific */ - MergeActionState *relaction; /* MERGE action in progress */ - /* * Information about the changes that were made concurrently to a tuple * being updated or deleted @@ -172,13 +168,14 @@ static TupleTableSlot *ExecMerge(ModifyT ItemPointer tupleid, bool canSetTag); static void ExecInitMerge(ModifyTableState *mtstate, EState *estate); -static bool ExecMergeMatched(ModifyTableContext *context, - ResultRelInfo *resultRelInfo, - ItemPointer tupleid, - bool canSetTag); -static void ExecMergeNotMatched(ModifyTableContext *context, - ResultRelInfo *resultRelInfo, - bool canSetTag); +static TupleTableSlot *ExecMergeMatched(ModifyTableContext *context, + ResultRelInfo *resultRelInfo, + ItemPointer tupleid, + bool canSetTag, + bool *matched); +static TupleTableSlot *ExecMergeNotMatched(ModifyTableContext *context, + ResultRelInfo *resultRelInfo, + bool canSetTag); static TupleTableSlot *mergeGetUpdateNewTuple(ResultRelInfo *relinfo, TupleTableSlot *planSlot, TupleTableSlot *oldSlot, @@ -987,7 +984,7 @@ ExecInsert(ModifyTableContext *context, if (mtstate->operation == CMD_UPDATE) wco_kind = WCO_RLS_UPDATE_CHECK; else if (mtstate->operation == CMD_MERGE) - wco_kind = (context->relaction->mas_action->commandType == CMD_UPDATE) ? + wco_kind = (mtstate->mt_merge_action->mas_action->commandType == CMD_UPDATE) ? WCO_RLS_UPDATE_CHECK : WCO_RLS_INSERT_CHECK; else wco_kind = WCO_RLS_INSERT_CHECK; @@ -1838,7 +1835,7 @@ ExecCrossPartitionUpdate(ModifyTableCont /* and project the new tuple to retry the UPDATE with */ context->cpUpdateRetrySlot = context->GetUpdateNewTuple(resultRelInfo, epqslot, oldSlot, - context->relaction); + mtstate->mt_merge_action); return false; } } @@ -2054,7 +2051,7 @@ lreplace: * No luck, a retry is needed. If running MERGE, we do not do so * here; instead let it handle that on its own rules. */ - if (context->relaction != NULL) + if (context->mtstate->mt_merge_action != NULL) return TM_Updated; /* @@ -2692,6 +2689,7 @@ static TupleTableSlot * ExecMerge(ModifyTableContext *context, ResultRelInfo *resultRelInfo, ItemPointer tupleid, bool canSetTag) { + TupleTableSlot *rslot = NULL; bool matched; /*----- @@ -2739,7 +2737,8 @@ ExecMerge(ModifyTableContext *context, R */ matched = tupleid != NULL; if (matched) - matched = ExecMergeMatched(context, resultRelInfo, tupleid, canSetTag); + rslot = ExecMergeMatched(context, resultRelInfo, tupleid, canSetTag, + &matched); /* * Either we were dealing with a NOT MATCHED tuple or ExecMergeMatched() @@ -2747,10 +2746,9 @@ ExecMerge(ModifyTableContext *context, R * matches. */ if (!matched) - ExecMergeNotMatched(context, resultRelInfo, canSetTag); + rslot = ExecMergeNotMatched(context, resultRelInfo, canSetTag); - /* No RETURNING support yet */ - return NULL; + return rslot; } /* @@ -2760,8 +2758,8 @@ ExecMerge(ModifyTableContext *context, R * We start from the first WHEN MATCHED action and check if the WHEN quals * pass, if any. If the WHEN quals for the first action do not pass, we * check the second, then the third and so on. If we reach to the end, no - * action is taken and we return true, indicating that no further action is - * required for this tuple. + * action is taken and "matched" is set to true, indicating that no further + * action is required for this tuple. * * If we do find a qualifying action, then we attempt to execute the action. * @@ -2770,15 +2768,16 @@ ExecMerge(ModifyTableContext *context, R * with individual actions are evaluated by this routine via ExecQual, while * EvalPlanQual checks for the join quals. If EvalPlanQual tells us that the * updated tuple still passes the join quals, then we restart from the first - * action to look for a qualifying action. Otherwise, we return false -- - * meaning that a NOT MATCHED action must now be executed for the current - * source tuple. + * action to look for a qualifying action. Otherwise, "matched" is set to + * false -- meaning that a NOT MATCHED action must now be executed for the + * current source tuple. */ -static bool +static TupleTableSlot * ExecMergeMatched(ModifyTableContext *context, ResultRelInfo *resultRelInfo, - ItemPointer tupleid, bool canSetTag) + ItemPointer tupleid, bool canSetTag, bool *matched) { ModifyTableState *mtstate = context->mtstate; + TupleTableSlot *rslot = NULL; TupleTableSlot *newslot; EState *estate = context->estate; ExprContext *econtext = mtstate->ps.ps_ExprContext; @@ -2790,7 +2789,10 @@ ExecMergeMatched(ModifyTableContext *con * If there are no WHEN MATCHED actions, we are done. */ if (resultRelInfo->ri_matchedMergeAction == NIL) - return true; + { + *matched = true; + return NULL; + } /* * Make tuple and any needed join variables available to ExecQual and @@ -2869,7 +2871,7 @@ lmerge_matched: */ newslot = ExecProject(relaction->mas_proj); - context->relaction = relaction; + mtstate->mt_merge_action = relaction; context->GetUpdateNewTuple = mergeGetUpdateNewTuple; context->cpUpdateRetrySlot = NULL; @@ -2892,7 +2894,7 @@ lmerge_matched: break; case CMD_DELETE: - context->relaction = relaction; + mtstate->mt_merge_action = relaction; if (!ExecDeletePrologue(context, resultRelInfo, tupleid, NULL, NULL)) { @@ -2952,7 +2954,8 @@ lmerge_matched: * If the tuple was already deleted, return to let caller * handle it under NOT MATCHED clauses. */ - return false; + *matched = false; + return NULL; case TM_Updated: { @@ -3020,13 +3023,19 @@ lmerge_matched: * NOT MATCHED actions. */ if (TupIsNull(epqslot)) - return false; + { + *matched = false; + return NULL; + } (void) ExecGetJunkAttribute(epqslot, resultRelInfo->ri_RowIdAttNo, &isNull); if (isNull) - return false; + { + *matched = false; + return NULL; + } /* * When a tuple was updated and migrated to @@ -3061,7 +3070,8 @@ lmerge_matched: * tuple already deleted; tell caller to run NOT * MATCHED actions */ - return false; + *matched = false; + return NULL; case TM_SelfModified: @@ -3081,13 +3091,14 @@ lmerge_matched: (errcode(ERRCODE_TRIGGERED_DATA_CHANGE_VIOLATION), errmsg("tuple to be updated or deleted 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."))); - return false; + *matched = false; + return NULL; default: /* see table_tuple_lock call in ExecDelete() */ elog(ERROR, "unexpected table_tuple_lock status: %u", result); - return false; + return NULL; } } @@ -3099,6 +3110,30 @@ lmerge_matched: break; } + /* Process RETURNING if present */ + if (resultRelInfo->ri_projectReturning) + { + switch (commandType) + { + case CMD_UPDATE: + rslot = ExecProcessReturning(resultRelInfo, newslot, + context->planSlot); + break; + + case CMD_DELETE: + rslot = ExecProcessReturning(resultRelInfo, + resultRelInfo->ri_oldTupleSlot, + context->planSlot); + break; + + case CMD_NOTHING: + break; + + default: + elog(ERROR, "unknown action in MERGE WHEN MATCHED clause"); + } + } + /* * We've activated one of the WHEN clauses, so we don't search * further. This is required behaviour, not an optimization. @@ -3109,19 +3144,22 @@ lmerge_matched: /* * Successfully executed an action or no qualifying action was found. */ - return true; + *matched = true; + + return rslot; } /* * Execute the first qualifying NOT MATCHED action. */ -static void +static TupleTableSlot * ExecMergeNotMatched(ModifyTableContext *context, ResultRelInfo *resultRelInfo, bool canSetTag) { ModifyTableState *mtstate = context->mtstate; ExprContext *econtext = mtstate->ps.ps_ExprContext; List *actionStates = NIL; + TupleTableSlot *rslot = NULL; ListCell *l; /* @@ -3171,10 +3209,10 @@ ExecMergeNotMatched(ModifyTableContext * * so we don't need to map the tuple here. */ newslot = ExecProject(action->mas_proj); - context->relaction = action; + mtstate->mt_merge_action = action; - (void) ExecInsert(context, mtstate->rootResultRelInfo, newslot, - canSetTag, NULL, NULL); + rslot = ExecInsert(context, mtstate->rootResultRelInfo, + newslot, canSetTag, NULL, NULL); mtstate->mt_merge_inserted += 1; break; case CMD_NOTHING: @@ -3190,6 +3228,8 @@ ExecMergeNotMatched(ModifyTableContext * */ break; } + + return rslot; } /* @@ -3227,6 +3267,7 @@ ExecInitMerge(ModifyTableState *mtstate, List *mergeActionList = lfirst(lc); TupleDesc relationDesc; ListCell *l; + int action_idx; resultRelInfo = mtstate->resultRelInfo + i; i++; @@ -3236,6 +3277,7 @@ ExecInitMerge(ModifyTableState *mtstate, if (unlikely(!resultRelInfo->ri_projectNewInfoValid)) ExecInitMergeTupleSlots(mtstate, resultRelInfo); + action_idx = 1; foreach(l, mergeActionList) { MergeAction *action = (MergeAction *) lfirst(l); @@ -3250,6 +3292,7 @@ ExecInitMerge(ModifyTableState *mtstate, */ action_state = makeNode(MergeActionState); action_state->mas_action = action; + action_state->mas_action_idx = action_idx++; action_state->mas_whenqual = ExecInitQual((List *) action->qual, &mtstate->ps); @@ -3386,6 +3429,64 @@ mergeGetUpdateNewTuple(ResultRelInfo *re } /* + * pg_merge_action() - + * SQL merge support function to retrieve the currently executing merge + * action command string ("INSERT", "UPDATE", or "DELETE"). + */ +Datum +pg_merge_action(PG_FUNCTION_ARGS) +{ + ModifyTableState *mtstate = (ModifyTableState *) fcinfo->context; + MergeActionState *relaction; + + if (!mtstate || mtstate->operation != CMD_MERGE) + elog(ERROR, "merge support function called in non-merge context"); + + relaction = mtstate->mt_merge_action; + if (relaction) + { + CmdType commandType = relaction->mas_action->commandType; + + switch (commandType) + { + case CMD_INSERT: + PG_RETURN_TEXT_P(cstring_to_text("INSERT")); + case CMD_UPDATE: + PG_RETURN_TEXT_P(cstring_to_text("UPDATE")); + case CMD_DELETE: + PG_RETURN_TEXT_P(cstring_to_text("DELETE")); + case CMD_NOTHING: + PG_RETURN_NULL(); + default: + elog(ERROR, "unrecognized commandType: %d", (int) commandType); + } + } + + PG_RETURN_NULL(); +} + +/* + * pg_merge_when_clause() - + * SQL merge support function to retrieve the 1-based index of the + * currently executing merge WHEN clause. + */ +Datum +pg_merge_when_clause(PG_FUNCTION_ARGS) +{ + ModifyTableState *mtstate = (ModifyTableState *) fcinfo->context; + MergeActionState *relaction; + + if (!mtstate || mtstate->operation != CMD_MERGE) + elog(ERROR, "merge support function called in non-merge context"); + + relaction = mtstate->mt_merge_action; + if (relaction) + PG_RETURN_INT32((int32) relaction->mas_action_idx); + + PG_RETURN_NULL(); +} + +/* * Process BEFORE EACH STATEMENT triggers */ static void @@ -3671,8 +3772,17 @@ ExecModifyTable(PlanState *pstate) { EvalPlanQualSetSlot(&node->mt_epqstate, context.planSlot); - ExecMerge(&context, node->resultRelInfo, NULL, node->canSetTag); - continue; /* no RETURNING support yet */ + slot = ExecMerge(&context, node->resultRelInfo, NULL, + node->canSetTag); + + /* + * If we got a RETURNING result, return it to the caller. + * We'll continue the work on next call. + */ + if (slot) + return slot; + + continue; /* continue with the next tuple */ } elog(ERROR, "tableoid is NULL"); @@ -3749,8 +3859,17 @@ ExecModifyTable(PlanState *pstate) { EvalPlanQualSetSlot(&node->mt_epqstate, context.planSlot); - ExecMerge(&context, node->resultRelInfo, NULL, node->canSetTag); - continue; /* no RETURNING support yet */ + slot = ExecMerge(&context, node->resultRelInfo, NULL, + node->canSetTag); + + /* + * If we got a RETURNING result, return it to the + * caller. We'll continue the work on next call. + */ + if (slot) + return slot; + + continue; /* continue with the next tuple */ } elog(ERROR, "ctid is NULL"); @@ -3843,7 +3962,7 @@ ExecModifyTable(PlanState *pstate) slot = internalGetUpdateNewTuple(resultRelInfo, context.planSlot, oldSlot, NULL); context.GetUpdateNewTuple = internalGetUpdateNewTuple; - context.relaction = NULL; + node->mt_merge_action = NULL; /* Now apply the update. */ slot = ExecUpdate(&context, resultRelInfo, tupleid, oldtuple, diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c new file mode 100644 index 4a817b7..c13c866 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -73,7 +73,6 @@ static void determineRecursiveColTypes(P Node *larg, List *nrtargetlist); static Query *transformReturnStmt(ParseState *pstate, ReturnStmt *stmt); static Query *transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt); -static List *transformReturningList(ParseState *pstate, List *returningList); static Query *transformPLAssignStmt(ParseState *pstate, PLAssignStmt *stmt); static Query *transformDeclareCursorStmt(ParseState *pstate, @@ -514,7 +513,8 @@ transformDeleteStmt(ParseState *pstate, qual = transformWhereClause(pstate, stmt->whereClause, EXPR_KIND_WHERE, "WHERE"); - qry->returningList = transformReturningList(pstate, stmt->returningList); + qry->returningList = transformReturningList(pstate, stmt->returningList, + EXPR_KIND_RETURNING); /* done building the range table and jointree */ qry->rtable = pstate->p_rtable; @@ -940,7 +940,8 @@ transformInsertStmt(ParseState *pstate, /* Process RETURNING, if any. */ if (stmt->returningList) qry->returningList = transformReturningList(pstate, - stmt->returningList); + stmt->returningList, + EXPR_KIND_RETURNING); /* done building the range table and jointree */ qry->rtable = pstate->p_rtable; @@ -2405,7 +2406,8 @@ transformUpdateStmt(ParseState *pstate, qual = transformWhereClause(pstate, stmt->whereClause, EXPR_KIND_WHERE, "WHERE"); - qry->returningList = transformReturningList(pstate, stmt->returningList); + qry->returningList = transformReturningList(pstate, stmt->returningList, + EXPR_KIND_RETURNING); /* * Now we are done with SELECT-like processing, and can get on with @@ -2499,10 +2501,11 @@ transformUpdateTargetList(ParseState *ps /* * transformReturningList - - * handle a RETURNING clause in INSERT/UPDATE/DELETE + * handle a RETURNING clause in INSERT/UPDATE/DELETE/MERGE */ -static List * -transformReturningList(ParseState *pstate, List *returningList) +List * +transformReturningList(ParseState *pstate, List *returningList, + ParseExprKind exprKind) { List *rlist; int save_next_resno; @@ -2519,7 +2522,7 @@ transformReturningList(ParseState *pstat pstate->p_next_resno = 1; /* transform RETURNING identically to a SELECT targetlist */ - rlist = transformTargetList(pstate, returningList, EXPR_KIND_RETURNING); + rlist = transformTargetList(pstate, returningList, exprKind); /* * Complain if the nonempty tlist expanded to nothing (which is possible diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y new file mode 100644 index a013838..4406430 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -12241,6 +12241,7 @@ MergeStmt: USING table_ref ON a_expr merge_when_list + returning_clause { MergeStmt *m = makeNode(MergeStmt); @@ -12249,6 +12250,7 @@ MergeStmt: m->sourceRelation = $6; m->joinCondition = $8; m->mergeWhenClauses = $9; + m->returningList = $10; $$ = (Node *) m; } diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c new file mode 100644 index f7a1046..c66b7cf --- a/src/backend/parser/parse_agg.c +++ b/src/backend/parser/parse_agg.c @@ -455,6 +455,7 @@ check_agglevels_and_constraints(ParseSta errkind = true; break; case EXPR_KIND_RETURNING: + case EXPR_KIND_MERGE_RETURNING: errkind = true; break; case EXPR_KIND_VALUES: @@ -903,6 +904,7 @@ transformWindowFuncCall(ParseState *psta errkind = true; break; case EXPR_KIND_RETURNING: + case EXPR_KIND_MERGE_RETURNING: errkind = true; break; case EXPR_KIND_VALUES: diff --git a/src/backend/parser/parse_cte.c b/src/backend/parser/parse_cte.c new file mode 100644 index c5b1a49..ad3c525 --- a/src/backend/parser/parse_cte.c +++ b/src/backend/parser/parse_cte.c @@ -126,13 +126,6 @@ transformWithClause(ParseState *pstate, CommonTableExpr *cte = (CommonTableExpr *) lfirst(lc); ListCell *rest; - /* MERGE is allowed by parser, but unimplemented. Reject for now */ - if (IsA(cte->ctequery, MergeStmt)) - ereport(ERROR, - errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("MERGE not supported in WITH query"), - parser_errposition(pstate, cte->location)); - for_each_cell(rest, withClause->ctes, lnext(withClause->ctes, lc)) { CommonTableExpr *cte2 = (CommonTableExpr *) lfirst(rest); @@ -153,7 +146,8 @@ transformWithClause(ParseState *pstate, /* must be a data-modifying statement */ Assert(IsA(cte->ctequery, InsertStmt) || IsA(cte->ctequery, UpdateStmt) || - IsA(cte->ctequery, DeleteStmt)); + IsA(cte->ctequery, DeleteStmt) || + IsA(cte->ctequery, MergeStmt)); pstate->p_hasModifyingCTE = true; } diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c new file mode 100644 index 53e904c..2416cd6 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -482,6 +482,7 @@ transformColumnRef(ParseState *pstate, C case EXPR_KIND_LIMIT: case EXPR_KIND_OFFSET: case EXPR_KIND_RETURNING: + case EXPR_KIND_MERGE_RETURNING: case EXPR_KIND_VALUES: case EXPR_KIND_VALUES_SINGLE: case EXPR_KIND_CHECK_CONSTRAINT: @@ -1708,6 +1709,7 @@ transformSubLink(ParseState *pstate, Sub case EXPR_KIND_LIMIT: case EXPR_KIND_OFFSET: case EXPR_KIND_RETURNING: + case EXPR_KIND_MERGE_RETURNING: case EXPR_KIND_VALUES: case EXPR_KIND_VALUES_SINGLE: case EXPR_KIND_CYCLE_MARK: @@ -2997,6 +2999,7 @@ ParseExprKindName(ParseExprKind exprKind case EXPR_KIND_OFFSET: return "OFFSET"; case EXPR_KIND_RETURNING: + case EXPR_KIND_MERGE_RETURNING: return "RETURNING"; case EXPR_KIND_VALUES: case EXPR_KIND_VALUES_SINGLE: diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c new file mode 100644 index ca14f06..914624e --- a/src/backend/parser/parse_func.c +++ b/src/backend/parser/parse_func.c @@ -31,6 +31,7 @@ #include "parser/parse_target.h" #include "parser/parse_type.h" #include "utils/builtins.h" +#include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/syscache.h" @@ -348,6 +349,15 @@ ParseFuncOrColumn(ParseState *pstate, Li parser_errposition(pstate, location))); } + /* Merge support functions are only allowed in MERGE's RETURNING list */ + if (IsMergeSupportFunction(funcid) && + pstate->p_expr_kind != EXPR_KIND_MERGE_RETURNING) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("merge support function %s can only be called from the RETURNING list of a MERGE command", + NameListToString(funcname)), + parser_errposition(pstate, location))); + /* * So far so good, so do some fdresult-type-specific processing. */ @@ -2602,6 +2612,7 @@ check_srf_call_placement(ParseState *pst errkind = true; break; case EXPR_KIND_RETURNING: + case EXPR_KIND_MERGE_RETURNING: errkind = true; break; case EXPR_KIND_VALUES: diff --git a/src/backend/parser/parse_merge.c b/src/backend/parser/parse_merge.c new file mode 100644 index d886637..40afa7c --- a/src/backend/parser/parse_merge.c +++ b/src/backend/parser/parse_merge.c @@ -233,6 +233,10 @@ transformMergeStmt(ParseState *pstate, M */ qry->jointree = makeFromExpr(pstate->p_joinlist, joinExpr); + /* Transform the RETURNING list, if any */ + qry->returningList = transformReturningList(pstate, stmt->returningList, + EXPR_KIND_MERGE_RETURNING); + /* * We now have a good query shape, so now look at the WHEN conditions and * action targetlists. @@ -390,9 +394,6 @@ transformMergeStmt(ParseState *pstate, M qry->mergeActionList = mergeActionList; - /* RETURNING could potentially be added in the future, but not in SQL std */ - qry->returningList = NULL; - qry->hasTargetSRFs = false; qry->hasSubLinks = pstate->p_hasSubLinks; diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c new file mode 100644 index b490541..9fe7bcb --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -2313,9 +2313,10 @@ addRangeTableEntryForCTE(ParseState *pst cte->cterefcount++; /* - * We throw error if the CTE is INSERT/UPDATE/DELETE without RETURNING. - * This won't get checked in case of a self-reference, but that's OK - * because data-modifying CTEs aren't allowed to be recursive anyhow. + * We throw error if the CTE is INSERT/UPDATE/DELETE/MERGE without + * RETURNING. This won't get checked in case of a self-reference, but + * that's OK because data-modifying CTEs aren't allowed to be recursive + * anyhow. */ if (IsA(cte->ctequery, Query)) { diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c new file mode 100644 index c74bac2..939d61e --- a/src/backend/rewrite/rewriteHandler.c +++ b/src/backend/rewrite/rewriteHandler.c @@ -3626,7 +3626,8 @@ RewriteQuery(Query *parsetree, List *rew if (!(ctequery->commandType == CMD_SELECT || ctequery->commandType == CMD_UPDATE || ctequery->commandType == CMD_INSERT || - ctequery->commandType == CMD_DELETE)) + ctequery->commandType == CMD_DELETE || + ctequery->commandType == CMD_MERGE)) { /* * Currently it could only be NOTIFY; this error message will diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c new file mode 100644 index c7d9d96..d393f51 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -2131,11 +2131,10 @@ QueryReturnsTuples(Query *parsetree) case CMD_SELECT: /* returns tuples */ return true; - case CMD_MERGE: - return false; case CMD_INSERT: case CMD_UPDATE: case CMD_DELETE: + case CMD_MERGE: /* the forms with RETURNING return tuples */ if (parsetree->returningList) return true; diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c new file mode 100644 index f907f5d..85eb868 --- a/src/bin/psql/common.c +++ b/src/bin/psql/common.c @@ -962,13 +962,17 @@ PrintQueryResult(PGresult *result, bool else success = true; - /* if it's INSERT/UPDATE/DELETE RETURNING, also print status */ + /* + * If it's INSERT/UPDATE/DELETE/MERGE RETURNING, also print + * status. + */ if (last || pset.show_all_results) { cmdstatus = PQcmdStatus(result); if (strncmp(cmdstatus, "INSERT", 6) == 0 || strncmp(cmdstatus, "UPDATE", 6) == 0 || - strncmp(cmdstatus, "DELETE", 6) == 0) + strncmp(cmdstatus, "DELETE", 6) == 0 || + strncmp(cmdstatus, "MERGE", 5) == 0) PrintQueryStatus(result, printStatusFout); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat new file mode 100644 index 86eb8e8..2c1c3ee --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -11891,4 +11891,14 @@ prorettype => 'bytea', proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_send' }, +# MERGE support functions +{ oid => '9499', descr => 'command type of current MERGE action', + proname => 'pg_merge_action', provolatile => 'v', + prorettype => 'text', proargtypes => '', + prosrc => 'pg_merge_action' }, +{ oid => '9500', descr => 'index of current MERGE WHEN clause', + proname => 'pg_merge_when_clause', provolatile => 'v', + prorettype => 'int4', proargtypes => '', + prosrc => 'pg_merge_when_clause' }, + ] diff --git a/src/include/catalog/pg_proc.h b/src/include/catalog/pg_proc.h new file mode 100644 index e7abe0b..e597826 --- a/src/include/catalog/pg_proc.h +++ b/src/include/catalog/pg_proc.h @@ -182,6 +182,11 @@ DECLARE_UNIQUE_INDEX(pg_proc_proname_arg #define PROARGMODE_VARIADIC 'v' #define PROARGMODE_TABLE 't' +/* Is this a merge support function? (Requires fmgroids.h) */ +#define IsMergeSupportFunction(oid) \ + ((oid) == F_PG_MERGE_ACTION || \ + (oid) == F_PG_MERGE_WHEN_CLAUSE) + #endif /* EXPOSE_TO_CLIENT_CODE */ diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h new file mode 100644 index 20f4c8b..5a0126d --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -415,6 +415,7 @@ typedef struct MergeActionState NodeTag type; MergeAction *mas_action; /* associated MergeAction node */ + int mas_action_idx; /* 1-based index of MergeAction node */ ProjectionInfo *mas_proj; /* projection of the action's targetlist for * this rel */ ExprState *mas_whenqual; /* WHEN [NOT] MATCHED AND conditions */ @@ -1302,6 +1303,9 @@ typedef struct ModifyTableState /* Flags showing which subcommands are present INS/UPD/DEL/DO NOTHING */ int mt_merge_subcommands; + /* For MERGE, the action currently being executed */ + MergeActionState *mt_merge_action; + /* tuple counters for MERGE */ double mt_merge_inserted; double mt_merge_updated; diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h new file mode 100644 index 89335d9..b001d45 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1775,6 +1775,7 @@ typedef struct MergeStmt Node *sourceRelation; /* source relation */ Node *joinCondition; /* join condition between source and target */ List *mergeWhenClauses; /* list of MergeWhenClause(es) */ + List *returningList; /* list of expressions to return */ WithClause *withClause; /* WITH clause */ } MergeStmt; diff --git a/src/include/parser/analyze.h b/src/include/parser/analyze.h new file mode 100644 index 1cef183..78eb55e --- a/src/include/parser/analyze.h +++ b/src/include/parser/analyze.h @@ -44,6 +44,8 @@ extern List *transformInsertRow(ParseSta bool strip_indirection); extern List *transformUpdateTargetList(ParseState *pstate, List *origTlist); +extern List *transformReturningList(ParseState *pstate, List *returningList, + ParseExprKind exprKind); extern Query *transformTopLevelStmt(ParseState *pstate, RawStmt *parseTree); extern Query *transformStmt(ParseState *pstate, Node *parseTree); diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h new file mode 100644 index 1a37922..810a707 --- a/src/include/parser/parse_node.h +++ b/src/include/parser/parse_node.h @@ -61,7 +61,8 @@ typedef enum ParseExprKind EXPR_KIND_DISTINCT_ON, /* DISTINCT ON */ EXPR_KIND_LIMIT, /* LIMIT */ EXPR_KIND_OFFSET, /* OFFSET */ - EXPR_KIND_RETURNING, /* RETURNING */ + EXPR_KIND_RETURNING, /* RETURNING in INSERT/UPDATE/DELETE */ + EXPR_KIND_MERGE_RETURNING, /* RETURNING in MERGE */ EXPR_KIND_VALUES, /* VALUES */ EXPR_KIND_VALUES_SINGLE, /* single-row VALUES (in INSERT only) */ EXPR_KIND_CHECK_CONSTRAINT, /* CHECK constraint for a table */ diff --git a/src/test/regress/expected/merge.out b/src/test/regress/expected/merge.out new file mode 100644 index bc53b21..be7128e --- a/src/test/regress/expected/merge.out +++ b/src/test/regress/expected/merge.out @@ -123,20 +123,20 @@ ON tid = tid WHEN MATCHED THEN DO NOTHING; ERROR: name "target" specified more than once DETAIL: The name is used both as MERGE target table and data source. --- used in a CTE +-- used in a CTE without RETURNING WITH foo AS ( MERGE INTO target USING source ON (true) WHEN MATCHED THEN DELETE ) SELECT * FROM foo; -ERROR: MERGE not supported in WITH query -LINE 1: WITH foo AS ( - ^ --- used in COPY +ERROR: WITH query "foo" does not have a RETURNING clause +LINE 4: ) SELECT * FROM foo; + ^ +-- used in COPY without RETURNING COPY ( MERGE INTO target USING source ON (true) WHEN MATCHED THEN DELETE ) TO stdout; -ERROR: MERGE not supported in COPY +ERROR: COPY query must have a RETURNING clause -- unsupported relation types -- view CREATE VIEW tv AS SELECT * FROM target; @@ -1289,21 +1289,40 @@ WHEN MATCHED AND tid < 2 THEN ROLLBACK; -- RETURNING BEGIN; -INSERT INTO sq_source (sid, balance, delta) VALUES (-1, -1, -10); MERGE INTO sq_target t -USING v +USING sq_source s ON tid = sid -WHEN MATCHED AND tid > 2 THEN +WHEN MATCHED AND tid >= 2 THEN UPDATE SET balance = t.balance + delta WHEN NOT MATCHED THEN INSERT (balance, tid) VALUES (balance + delta, sid) WHEN MATCHED AND tid < 2 THEN DELETE -RETURNING *; -ERROR: syntax error at or near "RETURNING" -LINE 10: RETURNING *; - ^ +RETURNING pg_merge_when_clause() AS when_clause, + pg_merge_action() AS merge_action, + t.*, + CASE pg_merge_action() + WHEN 'INSERT' THEN 'Inserted '||t + WHEN 'UPDATE' THEN 'Added '||delta||' to balance' + WHEN 'DELETE' THEN 'Removed '||t + END AS description; + when_clause | merge_action | tid | balance | description +-------------+--------------+-----+---------+--------------------- + 3 | DELETE | 1 | 100 | Removed (1,100) + 1 | UPDATE | 2 | 220 | Added 20 to balance + 2 | INSERT | 4 | 40 | Inserted (4,40) +(3 rows) + ROLLBACK; +-- error when using MERGE support functions outside MERGE +SELECT pg_merge_action() FROM sq_target; +ERROR: merge support function pg_merge_action can only be called from the RETURNING list of a MERGE command +LINE 1: SELECT pg_merge_action() FROM sq_target; + ^ +UPDATE sq_target SET balance = balance + 1 RETURNING pg_merge_when_clause(); +ERROR: merge support function pg_merge_when_clause can only be called from the RETURNING list of a MERGE command +LINE 1: ...ATE sq_target SET balance = balance + 1 RETURNING pg_merge_w... + ^ -- EXPLAIN CREATE TABLE ex_mtarget (a int, b int) WITH (autovacuum_enabled=off); diff --git a/src/test/regress/sql/merge.sql b/src/test/regress/sql/merge.sql new file mode 100644 index fdbcd70..e434a2b --- a/src/test/regress/sql/merge.sql +++ b/src/test/regress/sql/merge.sql @@ -86,12 +86,12 @@ MERGE INTO target USING target ON tid = tid WHEN MATCHED THEN DO NOTHING; --- used in a CTE +-- used in a CTE without RETURNING WITH foo AS ( MERGE INTO target USING source ON (true) WHEN MATCHED THEN DELETE ) SELECT * FROM foo; --- used in COPY +-- used in COPY without RETURNING COPY ( MERGE INTO target USING source ON (true) WHEN MATCHED THEN DELETE @@ -844,19 +844,29 @@ ROLLBACK; -- RETURNING BEGIN; -INSERT INTO sq_source (sid, balance, delta) VALUES (-1, -1, -10); MERGE INTO sq_target t -USING v +USING sq_source s ON tid = sid -WHEN MATCHED AND tid > 2 THEN +WHEN MATCHED AND tid >= 2 THEN UPDATE SET balance = t.balance + delta WHEN NOT MATCHED THEN INSERT (balance, tid) VALUES (balance + delta, sid) WHEN MATCHED AND tid < 2 THEN DELETE -RETURNING *; +RETURNING pg_merge_when_clause() AS when_clause, + pg_merge_action() AS merge_action, + t.*, + CASE pg_merge_action() + WHEN 'INSERT' THEN 'Inserted '||t + WHEN 'UPDATE' THEN 'Added '||delta||' to balance' + WHEN 'DELETE' THEN 'Removed '||t + END AS description; ROLLBACK; +-- error when using MERGE support functions outside MERGE +SELECT pg_merge_action() FROM sq_target; +UPDATE sq_target SET balance = balance + 1 RETURNING pg_merge_when_clause(); + -- EXPLAIN CREATE TABLE ex_mtarget (a int, b int) WITH (autovacuum_enabled=off);