MERGE ... RETURNING
I've been thinking about adding RETURNING support to MERGE in order to
let the user see what changed.
I considered allowing a separate RETURNING list at the end of each
action, but rapidly dismissed that idea. Firstly, it introduces
shift/reduce conflicts to the grammar. These can be resolved by making
the "AS" before column aliases non-optional, but that's pretty ugly,
and there may be a better way. More serious drawbacks are that this
syntax is much more cumbersome for the end user, having to repeat the
RETURNING clause several times, and the implementation is likely to be
pretty complex, so I didn't pursue it.
A much simpler approach is to just have a single RETURNING list at the
end of the command. That's much easier to implement, and easier for
the end user. The main drawback is that it's impossible for the user
to work out from the values returned which action was actually taken,
and I think that's a pretty essential piece of information (at least
it seems pretty limiting to me, not being able to work that out).
So playing around with it (and inspired by the WITH ORDINALITY syntax
for SRFs), I had the idea of allowing "WITH WHEN CLAUSE" at the end of
the returning list, which adds an integer column to the list, whose
value is set to the index of the when clause executed, as in the
attached very rough patch.
So, quoting an example from the tests, this allows things like:
WITH t AS (
MERGE INTO sq_target t USING v ON tid = sid
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 t.* WITH WHEN CLAUSE
)
SELECT CASE when_clause
WHEN 1 THEN 'UPDATE'
WHEN 2 THEN 'INSERT'
WHEN 3 THEN 'DELETE'
END, *
FROM t;
case | tid | balance | when_clause
--------+-----+---------+-------------
INSERT | -1 | -11 | 2
DELETE | 1 | 100 | 3
(2 rows)
1 row is returned for each merge action executed (other than DO
NOTHING actions), and as usual, the values represent old target values
for DELETE actions, and new target values for INSERT/UPDATE actions.
It's also possible to return the source values, and a bare "*" in the
returning list expands to all the source columns, followed by all the
target columns.
The name of the added column, if included, can be changed by
specifying "WITH WHEN CLAUSE [AS] col_alias". I chose the syntax "WHEN
CLAUSE" and "when_clause" as the default column name because those
match the existing terminology used in the docs.
Anyway, this feels like a good point to stop playing around and get
feedback on whether this seems useful, or if anyone has other ideas.
Regards,
Dean
Attachments:
POC-support-MERGE-RETURNING.patchtext/x-patch; charset=US-ASCII; name=POC-support-MERGE-RETURNING.patchDownload
diff --git a/doc/src/sgml/ref/merge.sgml b/doc/src/sgml/ref/merge.sgml
new file mode 100644
index 0995fe0..4fc0a65
--- a/doc/src/sgml/ref/merge.sgml
+++ b/doc/src/sgml/ref/merge.sgml
@@ -25,6 +25,8 @@ PostgreSQL documentation
MERGE INTO [ ONLY ] <replaceable class="parameter">target_table_name</replaceable> [ * ] [ [ AS ] <replaceable class="parameter">target_alias</replaceable> ]
USING <replaceable class="parameter">data_source</replaceable> ON <replaceable class="parameter">join_condition</replaceable>
<replaceable class="parameter">when_clause</replaceable> [...]
+[ RETURNING * | <replaceable class="parameter">output_expression</replaceable> [ [ AS ] <replaceable class="parameter">output_name</replaceable> ] [, ...]
+ [ WITH WHEN CLAUSE [ [ AS ] <replaceable class="parameter">col_alias</replaceable> ] ] ]
<phrase>where <replaceable class="parameter">data_source</replaceable> is:</phrase>
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/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 687a542..64f6531
--- 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"
@@ -172,13 +171,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,
@@ -2692,6 +2692,7 @@ static TupleTableSlot *
ExecMerge(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
ItemPointer tupleid, bool canSetTag)
{
+ TupleTableSlot *rslot = NULL;
bool matched;
/*-----
@@ -2739,7 +2740,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 +2749,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 +2761,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 +2771,17 @@ 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;
+ ModifyTable *node = (ModifyTable *) mtstate->ps.plan;
+ TupleTableSlot *rslot = NULL;
TupleTableSlot *newslot;
EState *estate = context->estate;
ExprContext *econtext = mtstate->ps.ps_ExprContext;
@@ -2790,7 +2793,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
@@ -2952,7 +2958,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 +3027,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 +3074,8 @@ lmerge_matched:
* tuple already deleted; tell caller to run NOT
* MATCHED actions
*/
- return false;
+ *matched = false;
+ return NULL;
case TM_SelfModified:
@@ -3081,13 +3095,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 +3114,39 @@ 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");
+ }
+ }
+
+ /* Set RETURNING WITH WHEN CLAUSE result, if required */
+ if (rslot && node->hasReturningWhenClause)
+ {
+ int attnum = rslot->tts_tupleDescriptor->natts - 1;
+
+ rslot->tts_values[attnum] = Int32GetDatum(relaction->mas_action_idx);
+ rslot->tts_isnull[attnum] = false;
+ }
+
/*
* We've activated one of the WHEN clauses, so we don't search
* further. This is required behaviour, not an optimization.
@@ -3109,19 +3157,23 @@ 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;
+ ModifyTable *node = (ModifyTable *) mtstate->ps.plan;
ExprContext *econtext = mtstate->ps.ps_ExprContext;
List *actionStates = NIL;
+ TupleTableSlot *rslot = NULL;
ListCell *l;
/*
@@ -3173,9 +3225,18 @@ ExecMergeNotMatched(ModifyTableContext *
newslot = ExecProject(action->mas_proj);
context->relaction = action;
- (void) ExecInsert(context, mtstate->rootResultRelInfo, newslot,
- canSetTag, NULL, NULL);
+ rslot = ExecInsert(context, mtstate->rootResultRelInfo,
+ newslot, canSetTag, NULL, NULL);
mtstate->mt_merge_inserted += 1;
+
+ /* Set RETURNING WITH WHEN CLAUSE result, if requested */
+ if (rslot && node->hasReturningWhenClause)
+ {
+ int attnum = rslot->tts_tupleDescriptor->natts - 1;
+
+ rslot->tts_values[attnum] = Int32GetDatum(action->mas_action_idx);
+ rslot->tts_isnull[attnum] = false;
+ }
break;
case CMD_NOTHING:
/* Do nothing */
@@ -3190,6 +3251,8 @@ ExecMergeNotMatched(ModifyTableContext *
*/
break;
}
+
+ return rslot;
}
/*
@@ -3227,6 +3290,7 @@ ExecInitMerge(ModifyTableState *mtstate,
List *mergeActionList = lfirst(lc);
TupleDesc relationDesc;
ListCell *l;
+ int action_idx;
resultRelInfo = mtstate->resultRelInfo + i;
i++;
@@ -3236,6 +3300,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 +3315,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);
@@ -3671,8 +3737,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 +3824,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");
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
new file mode 100644
index aa24958..90b7fdc
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -7056,6 +7056,7 @@ make_modifytable(PlannerInfo *root, Plan
node->rowMarks = rowMarks;
node->mergeActionLists = mergeActionLists;
node->epqParam = epqParam;
+ node->hasReturningWhenClause = root->parse->hasReturningWhenClause;
/*
* For each result relation that is a foreign table, allow the FDW to
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
new file mode 100644
index 5b90974..50285a2
--- 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,
@@ -2501,7 +2500,7 @@ transformUpdateTargetList(ParseState *ps
* transformReturningList -
* handle a RETURNING clause in INSERT/UPDATE/DELETE
*/
-static List *
+List *
transformReturningList(ParseState *pstate, List *returningList)
{
List *rlist;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
new file mode 100644
index a013838..cf8599a
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -151,6 +151,13 @@ typedef struct KeyActions
KeyAction *deleteAction;
} KeyActions;
+/* Private struct for the result of merge_returning_clause */
+typedef struct MergeReturningClause
+{
+ List *returningList;
+ char *returningWhenClause;
+} MergeReturningClause;
+
/* ConstraintAttributeSpec yields an integer bitmask of these flags: */
#define CAS_NOT_DEFERRABLE 0x01
#define CAS_DEFERRABLE 0x02
@@ -274,6 +281,7 @@ static Node *makeRecursiveViewSelect(cha
SetQuantifier setquantifier;
struct GroupClause *groupclause;
MergeWhenClause *mergewhen;
+ struct MergeReturningClause *mergereturning;
struct KeyActions *keyactions;
struct KeyAction *keyaction;
}
@@ -511,6 +519,7 @@ static Node *makeRecursiveViewSelect(cha
%type <infer> opt_conf_expr
%type <onconflict> opt_on_conflict
%type <mergewhen> merge_insert merge_update merge_delete
+%type <mergereturning> merge_returning_clause
%type <node> merge_when_clause opt_merge_when_condition
%type <list> merge_when_list
@@ -677,7 +686,7 @@ static Node *makeRecursiveViewSelect(cha
BOOLEAN_P BOTH BREADTH BY
CACHE CALL CALLED CASCADE CASCADED CASE CAST CATALOG_P CHAIN CHAR_P
- CHARACTER CHARACTERISTICS CHECK CHECKPOINT CLASS CLOSE
+ CHARACTER CHARACTERISTICS CHECK CHECKPOINT CLASS CLAUSE CLOSE
CLUSTER COALESCE COLLATE COLLATION COLUMN COLUMNS COMMENT COMMENTS COMMIT
COMMITTED COMPRESSION CONCURRENTLY CONFIGURATION CONFLICT
CONNECTION CONSTRAINT CONSTRAINTS CONTENT_P CONTINUE_P CONVERSION_P COPY
@@ -12241,6 +12250,7 @@ MergeStmt:
USING table_ref
ON a_expr
merge_when_list
+ merge_returning_clause
{
MergeStmt *m = makeNode(MergeStmt);
@@ -12249,6 +12259,8 @@ MergeStmt:
m->sourceRelation = $6;
m->joinCondition = $8;
m->mergeWhenClauses = $9;
+ m->returningList = $10 ? $10->returningList : NIL;
+ m->returningWhenClause = $10 ? $10->returningWhenClause : NULL;
$$ = (Node *) m;
}
@@ -12389,6 +12401,45 @@ merge_values_clause:
}
;
+merge_returning_clause:
+ RETURNING target_list WITH WHEN CLAUSE AS ColLabel
+ {
+ MergeReturningClause *n = palloc(sizeof(MergeReturningClause));
+
+ n->returningList = $2;
+ n->returningWhenClause = $7;
+ $$ = n;
+ }
+ | RETURNING target_list WITH WHEN CLAUSE ColLabel
+ {
+ MergeReturningClause *n = palloc(sizeof(MergeReturningClause));
+
+ n->returningList = $2;
+ n->returningWhenClause = $6;
+ $$ = n;
+ }
+ | RETURNING target_list WITH WHEN CLAUSE
+ {
+ MergeReturningClause *n = palloc(sizeof(MergeReturningClause));
+
+ n->returningList = $2;
+ n->returningWhenClause = pstrdup("when_clause");
+ $$ = n;
+ }
+ | RETURNING target_list
+ {
+ MergeReturningClause *n = palloc(sizeof(MergeReturningClause));
+
+ n->returningList = $2;
+ n->returningWhenClause = NULL;
+ $$ = n;
+ }
+ | /* EMPTY */
+ {
+ $$ = NULL;
+ }
+ ;
+
/*****************************************************************************
*
* QUERY:
@@ -16744,6 +16795,7 @@ unreserved_keyword:
| CHARACTERISTICS
| CHECKPOINT
| CLASS
+ | CLAUSE
| CLOSE
| CLUSTER
| COLUMNS
@@ -17269,6 +17321,7 @@ bare_label_keyword:
| CHECK
| CHECKPOINT
| CLASS
+ | CLAUSE
| CLOSE
| CLUSTER
| COALESCE
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_merge.c b/src/backend/parser/parse_merge.c
new file mode 100644
index 611dfce..0ba114c
--- a/src/backend/parser/parse_merge.c
+++ b/src/backend/parser/parse_merge.c
@@ -233,6 +233,34 @@ transformMergeStmt(ParseState *pstate, M
*/
qry->jointree = makeFromExpr(pstate->p_joinlist, joinExpr);
+ /* Transform the RETURNING list, if any */
+ qry->returningList = transformReturningList(pstate, stmt->returningList);
+
+ /* Add integer column for RETURNING ... WITH WHEN CLAUSE, if specified */
+ if (stmt->returningWhenClause)
+ {
+ Expr *null_expr;
+ TargetEntry *tle;
+
+ null_expr = (Expr *) makeConst(INT4OID,
+ -1,
+ InvalidOid,
+ sizeof(int32),
+ (Datum) 0,
+ true,
+ true);
+
+ tle = makeTargetEntry(null_expr,
+ list_length(qry->returningList) + 1,
+ stmt->returningWhenClause,
+ false);
+
+ qry->returningList = lappend(qry->returningList, tle);
+ qry->hasReturningWhenClause = true;
+ }
+ else
+ qry->hasReturningWhenClause = false;
+
/*
* We now have a good query shape, so now look at the WHEN conditions and
* action targetlists.
@@ -390,9 +418,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 5389a0e..162a305
--- 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 1960dad..ac5a0c7
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -3640,7 +3640,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 38f9b10..024e75a
--- 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/nodes/execnodes.h b/src/include/nodes/execnodes.h
new file mode 100644
index 20f4c8b..3a25c93
--- 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 */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
new file mode 100644
index cfeca96..c8fc76c
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -160,6 +160,7 @@ typedef struct Query
List *mergeActionList; /* list of actions for MERGE (only) */
bool mergeUseOuterJoin; /* whether to use outer join */
+ bool hasReturningWhenClause; /* has RETURNING WITH WHEN CLAUSE */
List *targetList; /* target list (of TargetEntry) */
@@ -1713,6 +1714,9 @@ 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 */
+ char *returningWhenClause; /* RETURNING ... WITH WHEN CLAUSE
+ * column alias */
WithClause *withClause; /* WITH clause */
} MergeStmt;
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
new file mode 100644
index c1234fc..c8c8b97
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -254,6 +254,7 @@ typedef struct ModifyTable
List *exclRelTlist; /* tlist of the EXCLUDED pseudo relation */
List *mergeActionLists; /* per-target-table lists of actions for
* MERGE */
+ bool hasReturningWhenClause; /* has RETURNING WITH WHEN CLAUSE */
} ModifyTable;
struct PartitionPruneInfo; /* forward reference to struct below */
diff --git a/src/include/parser/analyze.h b/src/include/parser/analyze.h
new file mode 100644
index c97be6e..34cc1ec
--- a/src/include/parser/analyze.h
+++ b/src/include/parser/analyze.h
@@ -44,6 +44,7 @@ extern List *transformInsertRow(ParseSta
bool strip_indirection);
extern List *transformUpdateTargetList(ParseState *pstate,
List *origTlist);
+extern List *transformReturningList(ParseState *pstate, List *returningList);
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
new file mode 100644
index bb36213..e3860fa
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -79,6 +79,7 @@ PG_KEYWORD("characteristics", CHARACTERI
PG_KEYWORD("check", CHECK, RESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("checkpoint", CHECKPOINT, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("class", CLASS, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("clause", CLAUSE, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("close", CLOSE, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("cluster", CLUSTER, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("coalesce", COALESCE, COL_NAME_KEYWORD, BARE_LABEL)
diff --git a/src/test/regress/expected/merge.out b/src/test/regress/expected/merge.out
new file mode 100644
index 6c8a18f..0adf7c1
--- 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;
@@ -1299,11 +1299,69 @@ 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 t.*;
+ tid | balance
+-----+---------
+ -1 | -11
+ 1 | 100
+(2 rows)
+
+ROLLBACK;
+-- RETURNING ... WITH WHEN CLAUSE
+BEGIN;
+INSERT INTO sq_source (sid, balance, delta) VALUES (-1, -1, -10);
+WITH t AS (
+ MERGE INTO sq_target t
+ USING v
+ ON tid = sid
+ 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 t.* WITH WHEN CLAUSE
+)
+SELECT CASE when_clause
+ WHEN 1 THEN 'UPDATE'
+ WHEN 2 THEN 'INSERT'
+ WHEN 3 THEN 'DELETE'
+ END, *
+FROM t;
+ case | tid | balance | when_clause
+--------+-----+---------+-------------
+ INSERT | -1 | -11 | 2
+ DELETE | 1 | 100 | 3
+(2 rows)
+
ROLLBACK;
+-- RETURNING ... WITH WHEN CLAUSE [AS] alias
+MERGE INTO sq_target t
+USING sq_source s
+ON tid = sid
+WHEN MATCHED AND tid = 1 THEN
+ DO NOTHING
+WHEN MATCHED THEN
+ UPDATE SET balance = t.balance + delta
+RETURNING t.* WITH WHEN CLAUSE AS wc;
+ tid | balance | wc
+-----+---------+----
+ 2 | 220 | 2
+(1 row)
+
+MERGE INTO sq_target t
+USING sq_source s
+ON tid = sid
+WHEN MATCHED AND tid = 2 THEN
+ UPDATE SET balance = t.balance - delta
+WHEN MATCHED THEN
+ DO NOTHING
+RETURNING t.* WITH WHEN CLAUSE wc;
+ tid | balance | wc
+-----+---------+----
+ 2 | 200 | 1
+(1 row)
+
-- 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 98fe104..42f2483
--- 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
@@ -854,9 +854,51 @@ WHEN NOT MATCHED THEN
INSERT (balance, tid) VALUES (balance + delta, sid)
WHEN MATCHED AND tid < 2 THEN
DELETE
-RETURNING *;
+RETURNING t.*;
+ROLLBACK;
+
+-- RETURNING ... WITH WHEN CLAUSE
+BEGIN;
+INSERT INTO sq_source (sid, balance, delta) VALUES (-1, -1, -10);
+WITH t AS (
+ MERGE INTO sq_target t
+ USING v
+ ON tid = sid
+ 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 t.* WITH WHEN CLAUSE
+)
+SELECT CASE when_clause
+ WHEN 1 THEN 'UPDATE'
+ WHEN 2 THEN 'INSERT'
+ WHEN 3 THEN 'DELETE'
+ END, *
+FROM t;
ROLLBACK;
+-- RETURNING ... WITH WHEN CLAUSE [AS] alias
+MERGE INTO sq_target t
+USING sq_source s
+ON tid = sid
+WHEN MATCHED AND tid = 1 THEN
+ DO NOTHING
+WHEN MATCHED THEN
+ UPDATE SET balance = t.balance + delta
+RETURNING t.* WITH WHEN CLAUSE AS wc;
+
+MERGE INTO sq_target t
+USING sq_source s
+ON tid = sid
+WHEN MATCHED AND tid = 2 THEN
+ UPDATE SET balance = t.balance - delta
+WHEN MATCHED THEN
+ DO NOTHING
+RETURNING t.* WITH WHEN CLAUSE wc;
+
-- EXPLAIN
CREATE TABLE ex_mtarget (a int, b int)
WITH (autovacuum_enabled=off);
On Sun, 8 Jan 2023 at 07:28, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
So playing around with it (and inspired by the WITH ORDINALITY syntax
for SRFs), I had the idea of allowing "WITH WHEN CLAUSE" at the end of
the returning list, which adds an integer column to the list, whose
value is set to the index of the when clause executed, as in the
attached very rough patch.
Would it be useful to have just the action? Perhaps "WITH ACTION"? My idea
is that this would return an enum of INSERT, UPDATE, DELETE (so is "action"
the right word?). It seems to me in many situations I would be more likely
to care about which of these 3 happened rather than the exact clause that
applied. This isn't necessarily meant to be instead of your suggestion
because I can imagine wanting to know the exact clause, just an alternative
that might suffice in many situations. Using it would also avoid problems
arising from editing the query in a way which changes the numbers of the
clauses.
So, quoting an example from the tests, this allows things like:
WITH t AS (
MERGE INTO sq_target t USING v ON tid = sid
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 t.* WITH WHEN CLAUSE
)
SELECT CASE when_clause
WHEN 1 THEN 'UPDATE'
WHEN 2 THEN 'INSERT'
WHEN 3 THEN 'DELETE'
END, *
FROM t;case | tid | balance | when_clause
--------+-----+---------+-------------
INSERT | -1 | -11 | 2
DELETE | 1 | 100 | 3
(2 rows)1 row is returned for each merge action executed (other than DO
NOTHING actions), and as usual, the values represent old target values
for DELETE actions, and new target values for INSERT/UPDATE actions.
Would it be feasible to allow specifying old.column or new.column? These
would always be NULL for INSERT and DELETE respectively but more useful
with UPDATE. Actually I've been meaning to ask this question about UPDATE …
RETURNING.
Actually, even with DELETE/INSERT, I can imagine wanting, for example, to
get only the new values associated with INSERT or UPDATE and not the values
removed by a DELETE. So I can imagine specifying new.column to get NULLs
for any row that was deleted but still get the new values for other rows.
It's also possible to return the source values, and a bare "*" in the
returning list expands to all the source columns, followed by all the
target columns.
Does this lead to a problem in the event there are same-named columns
between source and target?
The name of the added column, if included, can be changed by
Show quoted text
specifying "WITH WHEN CLAUSE [AS] col_alias". I chose the syntax "WHEN
CLAUSE" and "when_clause" as the default column name because those
match the existing terminology used in the docs.
On Sun, 8 Jan 2023 at 20:09, Isaac Morland <isaac.morland@gmail.com> wrote:
Would it be useful to have just the action? Perhaps "WITH ACTION"? My idea is that this would return an enum of INSERT, UPDATE, DELETE (so is "action" the right word?). It seems to me in many situations I would be more likely to care about which of these 3 happened rather than the exact clause that applied. This isn't necessarily meant to be instead of your suggestion because I can imagine wanting to know the exact clause, just an alternative that might suffice in many situations. Using it would also avoid problems arising from editing the query in a way which changes the numbers of the clauses.
Hmm, perhaps that's something that can be added as well. Both use
cases seem useful.
1 row is returned for each merge action executed (other than DO
NOTHING actions), and as usual, the values represent old target values
for DELETE actions, and new target values for INSERT/UPDATE actions.Would it be feasible to allow specifying old.column or new.column? These would always be NULL for INSERT and DELETE respectively but more useful with UPDATE. Actually I've been meaning to ask this question about UPDATE … RETURNING.
I too have wished for the ability to do that with UPDATE ...
RETURNING, though I'm not sure how feasible it is.
I think it's something best considered separately though. I haven't
given any thought as to how to make it work, so there might be
technical difficulties. But if it could be made to work for UPDATE, it
shouldn't be much more effort to make it work for MERGE.
It's also possible to return the source values, and a bare "*" in the
returning list expands to all the source columns, followed by all the
target columns.Does this lead to a problem in the event there are same-named columns between source and target?
Not really. It's exactly the same as doing "SELECT * FROM src JOIN tgt
ON ...". That may lead to duplicate column names in the result, but
that's not necessarily a problem.
Regards,
Dean
On 1/9/23 13:29, Dean Rasheed wrote:
On Sun, 8 Jan 2023 at 20:09, Isaac Morland <isaac.morland@gmail.com> wrote:
Would it be useful to have just the action? Perhaps "WITH ACTION"? My idea is that this would return an enum of INSERT, UPDATE, DELETE (so is "action" the right word?). It seems to me in many situations I would be more likely to care about which of these 3 happened rather than the exact clause that applied. This isn't necessarily meant to be instead of your suggestion because I can imagine wanting to know the exact clause, just an alternative that might suffice in many situations. Using it would also avoid problems arising from editing the query in a way which changes the numbers of the clauses.
Hmm, perhaps that's something that can be added as well. Both use
cases seem useful.
Bikeshedding here. Instead of Yet Another WITH Clause, could we perhaps
make a MERGING() function analogous to the GROUPING() function that goes
with grouping sets?
MERGE ...
RETURNING *, MERGING('clause'), MERGING('action');
Or something.
--
Vik Fearing
On Mon, 9 Jan 2023 at 16:23, Vik Fearing <vik@postgresfriends.org> wrote:
Bikeshedding here. Instead of Yet Another WITH Clause, could we perhaps
make a MERGING() function analogous to the GROUPING() function that goes
with grouping sets?MERGE ...
RETURNING *, MERGING('clause'), MERGING('action');
Hmm, possibly, but I think that would complicate the implementation quite a bit.
GROUPING() is not really a function (in the sense that there is no
pg_proc entry for it, you can't do "\df grouping", and it isn't
executed with its arguments like a normal function). Rather, it
requires special-case handling in the parser, through to the executor,
and I think MERGING() would be similar.
Also, it masks any user function with the same name, and would
probably require MERGING to be some level of reserved keyword.
I'm not sure that's worth it, just to have a more standard-looking
RETURNING list, without a WITH clause.
Regards,
Dean
On Mon, 9 Jan 2023 at 17:44, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
On Mon, 9 Jan 2023 at 16:23, Vik Fearing <vik@postgresfriends.org> wrote:
Bikeshedding here. Instead of Yet Another WITH Clause, could we perhaps
make a MERGING() function analogous to the GROUPING() function that goes
with grouping sets?MERGE ...
RETURNING *, MERGING('clause'), MERGING('action');Hmm, possibly, but I think that would complicate the implementation quite a bit.
GROUPING() is not really a function (in the sense that there is no
pg_proc entry for it, you can't do "\df grouping", and it isn't
executed with its arguments like a normal function). Rather, it
requires special-case handling in the parser, through to the executor,
and I think MERGING() would be similar.Also, it masks any user function with the same name, and would
probably require MERGING to be some level of reserved keyword.
I thought about this some more, and I think functions do make more
sense here, rather than inventing a special WITH syntax. However,
rather than using a special MERGING() function like GROUPING(), which
isn't really a function at all, I think it's better (and much simpler
to implement) to have a pair of normal functions (one returning int,
and one text).
The example from the tests shows the sort of thing this allows:
MERGE INTO sq_target t USING sq_source s ON tid = sid
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 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)
I think this is easier to use than the WITH syntax, and more flexible,
since the new functions can be used anywhere in the RETURNING list,
including in expressions.
There is one limitation though. Due to the way these functions need
access to the originating query, they need to appear directly in
MERGE's RETURNING list, not in subqueries, plpgsql function bodies, or
anything else that amounts to a different query. Maybe there's a way
round that, but it looks tricky. In practice though, it's easy to work
around, if necessary (e.g., by wrapping the MERGE in a CTE).
Regards,
Dean
Attachments:
v2-POC-support-MERGE-RETURNING.patchtext/x-patch; charset=US-ASCII; name=v2-POC-support-MERGE-RETURNING.patchDownload
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 ] <replaceable class="parameter">target_table_name</replaceable> [ * ] [ [ AS ] <replaceable class="parameter">target_alias</replaceable> ]
USING <replaceable class="parameter">data_source</replaceable> ON <replaceable class="parameter">join_condition</replaceable>
<replaceable class="parameter">when_clause</replaceable> [...]
+[ RETURNING * | <replaceable class="parameter">output_expression</replaceable> [ [ AS ] <replaceable class="parameter">output_name</replaceable> ] [, ...] ]
<phrase>where <replaceable class="parameter">data_source</replaceable> is:</phrase>
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);
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"));
Does this COPY stuff come from another branch where you're adding
support for MERGE in COPY? I see that you add a test that MERGE without
RETURNING fails, but you didn't add any tests that it works with
RETURNING. Anyway, I suspect these small changes shouldn't be here.
Overall, the idea of using Postgres-specific functions for extracting
context in the RETURNING clause looks acceptable to me. We can change
that to add support to whatever clauses the SQL committee offers, when
they get around to offering something. (We do have to keep our fingers
crossed that they will decide to use the same RETURNING syntax as we do
in this patch, of course.)
Regarding mas_action_idx, I would have thought that it belongs in
MergeAction rather than MergeActionState. After all, you determine it
once at parse time, and it is a constant from there onwards, right?
--
Álvaro Herrera PostgreSQL Developer — https://www.EnterpriseDB.com/
On Sun, 22 Jan 2023 at 19:08, Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
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"));Does this COPY stuff come from another branch where you're adding
support for MERGE in COPY? I see that you add a test that MERGE without
RETURNING fails, but you didn't add any tests that it works with
RETURNING. Anyway, I suspect these small changes shouldn't be here.
A few of the code changes I made were entirely untested (this was
after all just a proof-of-concept, intended to gather opinions and get
consensus about the overall shape of the feature). They serve as
useful reminders of things to test. In fact, since then, I've been
doing more testing, and so far everything I have tried has just
worked, including COPY (MERGE ... RETURNING ...) TO ... Thinking about
it, I can't see any reason why it wouldn't.
Still, there's a lot more testing to do. Just going through the docs
looking for references to RETURNING gave me a lot more ideas of things
to test.
Overall, the idea of using Postgres-specific functions for extracting
context in the RETURNING clause looks acceptable to me.
Cool.
We can change
that to add support to whatever clauses the SQL committee offers, when
they get around to offering something. (We do have to keep our fingers
crossed that they will decide to use the same RETURNING syntax as we do
in this patch, of course.)
Yes indeed. At least, done this way, the only non-SQL-standard syntax
is the RETURNING keyword itself, which we've already settled on for
INSERT/UPDATE/DELETE. Let's just hope they don't decide to use
RETURNING in an incompatible way in the future.
Regarding mas_action_idx, I would have thought that it belongs in
MergeAction rather than MergeActionState. After all, you determine it
once at parse time, and it is a constant from there onwards, right?
Oh, yes that makes sense (and removes the need for a couple of the
executor changes). Thanks for looking.
Regards,
Dean
On Mon, 23 Jan 2023 at 16:54, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
On Sun, 22 Jan 2023 at 19:08, Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
Regarding mas_action_idx, I would have thought that it belongs in
MergeAction rather than MergeActionState. After all, you determine it
once at parse time, and it is a constant from there onwards, right?Oh, yes that makes sense (and removes the need for a couple of the
executor changes). Thanks for looking.
Attached is a more complete patch, with that change and more doc and
test updates.
A couple of latest changes from this patch look like they represent
pre-existing issues with MERGE that should really be extracted from
this patch and applied to HEAD+v15. I'll take a closer look at that,
and start new threads for those.
Regards,
Dean
Attachments:
support-merge-returning-v3.patchtext/x-patch; charset=US-ASCII; name=support-merge-returning-v3.patchDownload
diff --git a/doc/src/sgml/dml.sgml b/doc/src/sgml/dml.sgml
new file mode 100644
index cbbc5e2..ff2a827
--- a/doc/src/sgml/dml.sgml
+++ b/doc/src/sgml/dml.sgml
@@ -283,10 +283,15 @@ DELETE FROM products;
<secondary>RETURNING</secondary>
</indexterm>
+ <indexterm zone="dml-returning">
+ <primary>MERGE</primary>
+ <secondary>RETURNING</secondary>
+ </indexterm>
+
<para>
Sometimes it is useful to obtain data from modified rows while they are
being manipulated. The <command>INSERT</command>, <command>UPDATE</command>,
- and <command>DELETE</command> commands all have an
+ <command>DELETE</command>, and <command>MERGE</command> commands all have an
optional <literal>RETURNING</literal> clause that supports this. Use
of <literal>RETURNING</literal> avoids performing an extra database query to
collect the data, and is especially valuable when it would otherwise be
@@ -339,6 +344,24 @@ DELETE FROM products
</programlisting>
</para>
+ <para>
+ In a <command>MERGE</command>, the data available to <literal>RETURNING</literal> is
+ the content of the source row plus the content of the inserted, updated, or
+ deleted target row. In addition, the merge support functions
+ <xref linkend="pg-merge-action"/> and <xref linkend="pg-merge-when-clause"/>
+ may be used to return information about which merge action was executed for
+ each row. Since it is quite common for the source and target to have many
+ of the same columns, specifying <literal>RETURNING *</literal> can lead to a
+ lot of duplicated columns, so it is often more useful to just return the
+ target row. For example:
+<programlisting>
+MERGE INTO products p USING new_products n ON p.product_no = n.product_no
+ WHEN NOT MATCHED THEN INSERT VALUES (n.product_no, n.name, n.price)
+ WHEN MATCHED THEN UPDATE SET name = n.name, price = n.price
+ RETURNING pg_merge_action(), p.*;
+</programlisting>
+ </para>
+
<para>
If there are triggers (<xref linkend="triggers"/>) on the target table,
the data available to <literal>RETURNING</literal> is the row as modified by
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
new file mode 100644
index e09e289..6927228
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -21202,6 +21202,99 @@ SELECT count(*) FROM sometable;
</sect1>
+ <sect1 id="functions-merge-support">
+ <title>Merge Support Functions</title>
+
+ <indexterm>
+ <primary>MERGE</primary>
+ <secondary>RETURNING</secondary>
+ </indexterm>
+
+ <para>
+ The merge support functions shown in
+ <xref linkend="functions-merge-support-table"/> may be used in the
+ <literal>RETURNING</literal> list of a <xref linkend="sql-merge"/> command
+ to return additional information about the action taken for each row.
+ </para>
+
+ <table id="functions-merge-support-table">
+ <title>Merge Support Functions</title>
+
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ Function
+ </para>
+ <para>
+ Description
+ </para></entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm id="pg-merge-action">
+ <primary>pg_merge_action</primary>
+ </indexterm>
+ <function>pg_merge_action</function> ( )
+ <returnvalue>text</returnvalue>
+ </para>
+ <para>
+ Returns the action performed on the current row (<literal>'INSERT'</literal>,
+ <literal>'UPDATE'</literal>, or <literal>'DELETE'</literal>).
+ </para></entry>
+ </row>
+
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm id="pg-merge-when-clause">
+ <primary>pg_merge_when_clause</primary>
+ </indexterm>
+ <function>pg_merge_when_clause</function> ( )
+ <returnvalue>integer</returnvalue>
+ </para>
+ <para>
+ Returns the 1-based index of the <literal>WHEN</literal> clause executed
+ for the current row.
+ </para></entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+
+ <para>
+ Example:
+<screen><![CDATA[
+MERGE INTO products p
+ USING stock s ON p.product_id = s.product_id
+ WHEN MATCHED AND s.quantity > 0 THEN
+ UPDATE SET in_stock = true, quantity = s.quantity
+ WHEN MATCHED THEN
+ UPDATE SET in_stock = false, quantity = 0
+ WHEN NOT MATCHED THEN
+ INSERT (product_id, in_stock, quantity)
+ VALUES (s.product_id, true, s.quantity)
+ RETURNING pg_merge_action() AS action,
+ pg_merge_when_clause() AS when_clause,
+ p.*;
+
+ action | when_clause | product_id | in_stock | quantity
+--------+-------------+------------+----------+----------
+ UPDATE | 1 | 1001 | t | 50
+ UPDATE | 2 | 1002 | f | 0
+ INSERT | 3 | 1003 | t | 10
+]]></screen>
+ </para>
+
+ <para>
+ Note that it is an error to use these functions anywhere other than the
+ <literal>RETURNING</literal> list of a <command>MERGE</command> command.
+ </para>
+
+ </sect1>
+
<sect1 id="functions-subquery">
<title>Subquery Expressions</title>
diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml
new file mode 100644
index 7c01a54..4317cfc
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -1323,9 +1323,9 @@
to a <glossterm linkend="glossary-client">client</glossterm> upon the
completion of an <acronym>SQL</acronym> command, usually a
<command>SELECT</command> but it can be an
- <command>INSERT</command>, <command>UPDATE</command>, or
- <command>DELETE</command> command if the <literal>RETURNING</literal>
- clause is specified.
+ <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command> command if the
+ <literal>RETURNING</literal> clause is specified.
</para>
<para>
The fact that a result set is a relation means that a query can be used
diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml
new file mode 100644
index 8897a54..3249e9c
--- a/doc/src/sgml/plpgsql.sgml
+++ b/doc/src/sgml/plpgsql.sgml
@@ -1020,8 +1020,8 @@ INSERT INTO mytable VALUES (1,'one'), (2
</para>
<para>
- If the command does return rows (for example <command>SELECT</command>,
- or <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>
+ If the command does return rows (for example <command>SELECT</command>, or
+ <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command>
with <literal>RETURNING</literal>), there are two ways to proceed.
When the command will return at most one row, or you only care about
the first row of output, write the command as usual but add
@@ -1044,7 +1044,8 @@ INSERT INTO mytable VALUES (1,'one'), (2
<application>PL/pgSQL</application> variable values can be
automatically inserted into optimizable SQL commands, which
are <command>SELECT</command>, <command>INSERT</command>,
- <command>UPDATE</command>, <command>DELETE</command>, and certain
+ <command>UPDATE</command>, <command>DELETE</command>,
+ <command>MERGE</command> and certain
utility commands that incorporate one of these, such
as <command>EXPLAIN</command> and <command>CREATE TABLE ... AS
SELECT</command>. In these commands,
@@ -1148,6 +1149,7 @@ SELECT <replaceable>select_expressions</
INSERT ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
UPDATE ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
DELETE ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
+MERGE ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
</synopsis>
where <replaceable>target</replaceable> can be a record variable, a row
@@ -1158,8 +1160,8 @@ DELETE ... RETURNING <replaceable>expres
<literal>INTO</literal> clause) just as described above,
and the plan is cached in the same way.
This works for <command>SELECT</command>,
- <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command> with
- <literal>RETURNING</literal>, and certain utility commands
+ <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command>
+ with <literal>RETURNING</literal>, and certain utility commands
that return row sets, such as <command>EXPLAIN</command>.
Except for the <literal>INTO</literal> clause, the SQL command is the same
as it would be written outside <application>PL/pgSQL</application>.
@@ -1235,7 +1237,7 @@ END;
</para>
<para>
- For <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command> with
+ For <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command> with
<literal>RETURNING</literal>, <application>PL/pgSQL</application> reports
an error for more than one returned row, even when
<literal>STRICT</literal> is not specified. This is because there
diff --git a/doc/src/sgml/queries.sgml b/doc/src/sgml/queries.sgml
new file mode 100644
index 45741e7..0ca593b
--- a/doc/src/sgml/queries.sgml
+++ b/doc/src/sgml/queries.sgml
@@ -2062,10 +2062,11 @@ SELECT <replaceable>select_list</replace
Table Expressions or <acronym>CTE</acronym>s, can be thought of as defining
temporary tables that exist just for one query. Each auxiliary statement
in a <literal>WITH</literal> clause can be a <command>SELECT</command>,
- <command>INSERT</command>, <command>UPDATE</command>, or <command>DELETE</command>; and the
+ <command>INSERT</command>, <command>UPDATE</command>, <command>DELETE</command>,
+ or <command>MERGE</command>; and the
<literal>WITH</literal> clause itself is attached to a primary statement that can
- also be a <command>SELECT</command>, <command>INSERT</command>, <command>UPDATE</command>, or
- <command>DELETE</command>.
+ also be a <command>SELECT</command>, <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command>.
</para>
<sect2 id="queries-with-select">
@@ -2587,7 +2588,8 @@ SELECT * FROM w AS w1 JOIN w AS w2 ON w1
<para>
The examples above only show <literal>WITH</literal> being used with
<command>SELECT</command>, but it can be attached in the same way to
- <command>INSERT</command>, <command>UPDATE</command>, or <command>DELETE</command>.
+ <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command> or <command>MERGE</command>.
In each case it effectively provides temporary table(s) that can
be referred to in the main command.
</para>
@@ -2598,7 +2600,8 @@ SELECT * FROM w AS w1 JOIN w AS w2 ON w1
<para>
You can use data-modifying statements (<command>INSERT</command>,
- <command>UPDATE</command>, or <command>DELETE</command>) in <literal>WITH</literal>. This
+ <command>UPDATE</command>, <command>DELETE</command>, or
+ <command>MERGE</command>) in <literal>WITH</literal>. This
allows you to perform several different operations in the same query.
An example is:
diff --git a/doc/src/sgml/ref/copy.sgml b/doc/src/sgml/ref/copy.sgml
new file mode 100644
index c25b52d..d822470
--- a/doc/src/sgml/ref/copy.sgml
+++ b/doc/src/sgml/ref/copy.sgml
@@ -121,13 +121,15 @@ COPY { <replaceable class="parameter">ta
A <link linkend="sql-select"><command>SELECT</command></link>,
<link linkend="sql-values"><command>VALUES</command></link>,
<link linkend="sql-insert"><command>INSERT</command></link>,
- <link linkend="sql-update"><command>UPDATE</command></link>, or
- <link linkend="sql-delete"><command>DELETE</command></link> command whose results are to be
+ <link linkend="sql-update"><command>UPDATE</command></link>,
+ <link linkend="sql-update"><command>DELETE</command></link>, or
+ <link linkend="sql-delete"><command>MERGE</command></link> command whose results are to be
copied. Note that parentheses are required around the query.
</para>
<para>
- For <command>INSERT</command>, <command>UPDATE</command> and
- <command>DELETE</command> queries a RETURNING clause must be provided,
+ For <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, and <command>MERGE</command> queries a
+ RETURNING clause must be provided,
and the target relation must not have a conditional rule, nor
an <literal>ALSO</literal> rule, nor an <literal>INSTEAD</literal> rule
that expands to multiple statements.
diff --git a/doc/src/sgml/ref/merge.sgml b/doc/src/sgml/ref/merge.sgml
new file mode 100644
index 0995fe0..7eb641a
--- a/doc/src/sgml/ref/merge.sgml
+++ b/doc/src/sgml/ref/merge.sgml
@@ -25,6 +25,7 @@ PostgreSQL documentation
MERGE INTO [ ONLY ] <replaceable class="parameter">target_table_name</replaceable> [ * ] [ [ AS ] <replaceable class="parameter">target_alias</replaceable> ]
USING <replaceable class="parameter">data_source</replaceable> ON <replaceable class="parameter">join_condition</replaceable>
<replaceable class="parameter">when_clause</replaceable> [...]
+[ RETURNING * | <replaceable class="parameter">output_expression</replaceable> [ [ AS ] <replaceable class="parameter">output_name</replaceable> ] [, ...] ]
<phrase>where <replaceable class="parameter">data_source</replaceable> is:</phrase>
@@ -95,6 +96,20 @@ DELETE
</para>
<para>
+ The optional <literal>RETURNING</literal> clause causes <command>MERGE</command>
+ to compute and return value(s) based on each row inserted, updated, or
+ deleted. Any expression using the source or target table's columns, or the
+ merge support functions <xref linkend="pg-merge-action"/> and
+ <xref linkend="pg-merge-when-clause"/> can be computed. When an
+ <command>INSERT</command> or <command>UPDATE</command> action is performed,
+ the new values of the target table's columns are used. When a
+ <command>DELETE</command> is performed, the old values of the target table's
+ columns are used. The syntax of the <literal>RETURNING</literal> list is
+ identical to that of the output list of <command>SELECT</command>.
+
+ </para>
+
+ <para>
There is no separate <literal>MERGE</literal> privilege.
If you specify an update action, you must have the
<literal>UPDATE</literal> privilege on the column(s)
@@ -125,6 +140,18 @@ DELETE
<variablelist>
<varlistentry>
+ <term><replaceable class="parameter">with_query</replaceable></term>
+ <listitem>
+ <para>
+ The <literal>WITH</literal> clause allows you to specify one or more
+ subqueries that can be referenced by name in the <command>MERGE</command>
+ query. See <xref linkend="queries-with"/> and <xref linkend="sql-select"/>
+ for details.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
<term><replaceable class="parameter">target_table_name</replaceable></term>
<listitem>
<para>
@@ -399,13 +426,30 @@ DELETE
</varlistentry>
<varlistentry>
- <term><replaceable class="parameter">with_query</replaceable></term>
+ <term><replaceable class="parameter">output_expression</replaceable></term>
<listitem>
<para>
- The <literal>WITH</literal> clause allows you to specify one or more
- subqueries that can be referenced by name in the <command>MERGE</command>
- query. See <xref linkend="queries-with"/> and <xref linkend="sql-select"/>
- for details.
+ An expression to be computed and returned by the <command>MERGE</command>
+ command after each row is merged. The expression can use any columns of
+ the source or target tables, or the merge support functions
+ <xref linkend="pg-merge-action"/> and <xref linkend="pg-merge-when-clause"/>.
+ </para>
+ <para>
+ Writing <literal>*</literal> will return all columns from the source
+ table, followed by all columns from the target table. Often this will
+ lead to a lot of duplication, since it is common for the source and
+ target tables to have a lot of the same columns. This can be avoided by
+ qualifying the <literal>*</literal> with the name or alias of the source
+ or target table.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><replaceable class="parameter">output_name</replaceable></term>
+ <listitem>
+ <para>
+ A name to use for a returned column.
</para>
</listitem>
</varlistentry>
@@ -428,6 +472,13 @@ MERGE <replaceable class="parameter">tot
were changed in any way.
</para>
+ <para>
+ If the <command>MERGE</command> command contains a <literal>RETURNING</literal>
+ clause, the result will be similar to that of a <command>SELECT</command>
+ statement containing the columns and values defined in the
+ <literal>RETURNING</literal> list, computed over the row(s) inserted, updated,
+ or deleted by the command.
+ </para>
</refsect1>
<refsect1>
@@ -544,13 +595,6 @@ MERGE <replaceable class="parameter">tot
</para>
<para>
- There is no <literal>RETURNING</literal> clause with
- <command>MERGE</command>. Actions of <command>INSERT</command>,
- <command>UPDATE</command> and <command>DELETE</command> cannot contain
- <literal>RETURNING</literal> or <literal>WITH</literal> clauses.
- </para>
-
- <para>
When <command>MERGE</command> is run concurrently with other commands
that modify the target table, the usual transaction isolation rules
apply; see <xref linkend="transaction-iso"/> for an explanation
@@ -602,7 +646,8 @@ WHEN NOT MATCHED THEN
<para>
Attempt to insert a new stock item along with the quantity of stock. If
the item already exists, instead update the stock count of the existing
- item. Don't allow entries that have zero stock.
+ item. Don't allow entries that have zero stock. Return details of all
+ changes made.
<programlisting>
MERGE INTO wines w
USING wine_stock_changes s
@@ -612,7 +657,8 @@ WHEN NOT MATCHED AND s.stock_delta > 0 T
WHEN MATCHED AND w.stock + s.stock_delta > 0 THEN
UPDATE SET stock = w.stock + s.stock_delta
WHEN MATCHED THEN
- DELETE;
+ DELETE
+RETURNING pg_merge_action(), w.*;
</programlisting>
The <literal>wine_stock_changes</literal> table might be, for example, a
@@ -627,7 +673,8 @@ WHEN MATCHED THEN
This command conforms to the <acronym>SQL</acronym> standard.
</para>
<para>
- The WITH clause and <literal>DO NOTHING</literal> action are extensions to
+ The WITH clause, <literal>DO NOTHING</literal> action, and
+ <literal>RETURNING</literal> clause are extensions to
the <acronym>SQL</acronym> standard.
</para>
</refsect1>
diff --git a/doc/src/sgml/ref/select.sgml b/doc/src/sgml/ref/select.sgml
new file mode 100644
index 1f9538f..5840ddc
--- a/doc/src/sgml/ref/select.sgml
+++ b/doc/src/sgml/ref/select.sgml
@@ -74,7 +74,7 @@ SELECT [ ALL | DISTINCT [ ON ( <replacea
<phrase>and <replaceable class="parameter">with_query</replaceable> is:</phrase>
- <replaceable class="parameter">with_query_name</replaceable> [ ( <replaceable class="parameter">column_name</replaceable> [, ...] ) ] AS [ [ NOT ] MATERIALIZED ] ( <replaceable class="parameter">select</replaceable> | <replaceable class="parameter">values</replaceable> | <replaceable class="parameter">insert</replaceable> | <replaceable class="parameter">update</replaceable> | <replaceable class="parameter">delete</replaceable> )
+ <replaceable class="parameter">with_query_name</replaceable> [ ( <replaceable class="parameter">column_name</replaceable> [, ...] ) ] AS [ [ NOT ] MATERIALIZED ] ( <replaceable class="parameter">select</replaceable> | <replaceable class="parameter">values</replaceable> | <replaceable class="parameter">insert</replaceable> | <replaceable class="parameter">update</replaceable> | <replaceable class="parameter">delete</replaceable> | <replaceable class="parameter">merge</replaceable> )
[ SEARCH { BREADTH | DEPTH } FIRST BY <replaceable>column_name</replaceable> [, ...] SET <replaceable>search_seq_col_name</replaceable> ]
[ CYCLE <replaceable>column_name</replaceable> [, ...] SET <replaceable>cycle_mark_col_name</replaceable> [ TO <replaceable>cycle_mark_value</replaceable> DEFAULT <replaceable>cycle_mark_default</replaceable> ] USING <replaceable>cycle_path_col_name</replaceable> ]
@@ -227,10 +227,10 @@ TABLE [ ONLY ] <replaceable class="param
The subqueries effectively act as temporary tables or views
for the duration of the primary query.
Each subquery can be a <command>SELECT</command>, <command>TABLE</command>, <command>VALUES</command>,
- <command>INSERT</command>, <command>UPDATE</command> or
- <command>DELETE</command> statement.
+ <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command> statement.
When writing a data-modifying statement (<command>INSERT</command>,
- <command>UPDATE</command> or <command>DELETE</command>) in
+ <command>UPDATE</command>, <command>DELETE</command>, or <command>MERGE</command>) in
<literal>WITH</literal>, it is usual to include a <literal>RETURNING</literal> clause.
It is the output of <literal>RETURNING</literal>, <emphasis>not</emphasis> the underlying
table that the statement modifies, that forms the temporary table that is
@@ -2179,7 +2179,8 @@ SELECT 2+2;
<para>
<productname>PostgreSQL</productname> allows <command>INSERT</command>,
- <command>UPDATE</command>, and <command>DELETE</command> to be used as <literal>WITH</literal>
+ <command>UPDATE</command>, <command>DELETE</command>, and
+ <command>MERGE</command> to be used as <literal>WITH</literal>
queries. This is not found in the SQL standard.
</para>
</refsect2>
diff --git a/doc/src/sgml/rowtypes.sgml b/doc/src/sgml/rowtypes.sgml
new file mode 100644
index 4d86f97..bbeac84
--- a/doc/src/sgml/rowtypes.sgml
+++ b/doc/src/sgml/rowtypes.sgml
@@ -348,7 +348,7 @@ SELECT m.* FROM some_table, LATERAL myfu
column expansion of this kind when it appears at the top level of
a <link linkend="queries-select-lists"><command>SELECT</command> output
list</link>, a <link linkend="dml-returning"><literal>RETURNING</literal>
- list</link> in <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>,
+ list</link> in <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command>,
a <link linkend="queries-values"><literal>VALUES</literal> clause</link>, or
a <link linkend="sql-syntax-row-constructors">row constructor</link>.
In all other contexts (including when nested inside one of those
diff --git a/doc/src/sgml/spi.sgml b/doc/src/sgml/spi.sgml
new file mode 100644
index 7581661..ad0a9b1
--- a/doc/src/sgml/spi.sgml
+++ b/doc/src/sgml/spi.sgml
@@ -301,8 +301,9 @@ SPI_execute("INSERT INTO foo SELECT * FR
is returned in the global variable <varname>SPI_processed</varname>.
If the return value of the function is <symbol>SPI_OK_SELECT</symbol>,
<symbol>SPI_OK_INSERT_RETURNING</symbol>,
- <symbol>SPI_OK_DELETE_RETURNING</symbol>, or
- <symbol>SPI_OK_UPDATE_RETURNING</symbol>,
+ <symbol>SPI_OK_DELETE_RETURNING</symbol>,
+ <symbol>SPI_OK_UPDATE_RETURNING</symbol>, or
+ <symbol>SPI_OK_MERGE_RETURNING</symbol>
then you can use the
global pointer <literal>SPITupleTable *SPI_tuptable</literal> to
access the result rows. Some utility commands (such as
@@ -438,6 +439,15 @@ typedef struct SPITupleTable
</varlistentry>
<varlistentry>
+ <term><symbol>SPI_OK_MERGE</symbol></term>
+ <listitem>
+ <para>
+ if a <command>MERGE</command> was executed
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
<term><symbol>SPI_OK_INSERT_RETURNING</symbol></term>
<listitem>
<para>
@@ -463,6 +473,15 @@ typedef struct SPITupleTable
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><symbol>SPI_OK_MERGE_RETURNING</symbol></term>
+ <listitem>
+ <para>
+ if a <command>MERGE RETURNING</command> was executed
+ </para>
+ </listitem>
+ </varlistentry>
<varlistentry>
<term><symbol>SPI_OK_UTILITY</symbol></term>
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
new file mode 100644
index e2a5496..114c3d3
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -177,7 +177,8 @@
statements separated by semicolons. A semicolon after the last
statement is optional. Unless the function is declared to return
<type>void</type>, the last statement must be a <command>SELECT</command>,
- or an <command>INSERT</command>, <command>UPDATE</command>, or <command>DELETE</command>
+ or an <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command>
that has a <literal>RETURNING</literal> clause.
</para>
@@ -186,7 +187,8 @@
language can be packaged together and defined as a function.
Besides <command>SELECT</command> queries, the commands can include data
modification queries (<command>INSERT</command>,
- <command>UPDATE</command>, and <command>DELETE</command>), as well as
+ <command>UPDATE</command>, <command>DELETE</command> and
+ <command>MERGE</command>), as well as
other SQL commands. (You cannot use transaction control commands, e.g.,
<command>COMMIT</command>, <command>SAVEPOINT</command>, and some utility
commands, e.g., <literal>VACUUM</literal>, in <acronym>SQL</acronym> functions.)
@@ -1235,8 +1237,9 @@ SELECT x, CASE WHEN x > 0 THEN genera
<note>
<para>
- If a function's last command is <command>INSERT</command>, <command>UPDATE</command>,
- or <command>DELETE</command> with <literal>RETURNING</literal>, that command will
+ If a function's last command is <command>INSERT</command>,
+ <command>UPDATE</command>, <command>DELETE</command>, or
+ <command>MERGE</command> with <literal>RETURNING</literal>, that command will
always be executed to completion, even if the function is not declared
with <literal>SETOF</literal> or the calling query does not fetch all the
result rows. Any extra rows produced by the <literal>RETURNING</literal>
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..95bbf67
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -545,8 +545,8 @@ ExecInitPartitionInfo(ModifyTableState *
* Build WITH CHECK OPTION constraints for the partition. Note that we
* didn't build the withCheckOptionList for partitions within the planner,
* but simple translation of varattnos will suffice. This only occurs for
- * the INSERT case or in the case of UPDATE tuple routing where we didn't
- * find a result rel to reuse.
+ * the INSERT case or in the case of UPDATE/MERGE tuple routing where we
+ * didn't find a result rel to reuse.
*/
if (node && node->withCheckOptionLists != NIL)
{
@@ -557,13 +557,16 @@ ExecInitPartitionInfo(ModifyTableState *
/*
* In the case of INSERT on a partitioned table, there is only one
* plan. Likewise, there is only one WCO list, not one per partition.
- * For UPDATE, there are as many WCO lists as there are plans.
+ * For UPDATE/MERGE, there are as many WCO lists as there are plans.
*/
Assert((node->operation == CMD_INSERT &&
list_length(node->withCheckOptionLists) == 1 &&
list_length(node->resultRelations) == 1) ||
(node->operation == CMD_UPDATE &&
list_length(node->withCheckOptionLists) ==
+ list_length(node->resultRelations)) ||
+ (node->operation == CMD_MERGE &&
+ list_length(node->withCheckOptionLists) ==
list_length(node->resultRelations)));
/*
@@ -609,8 +612,8 @@ ExecInitPartitionInfo(ModifyTableState *
* Build the RETURNING projection for the partition. Note that we didn't
* build the returningList for partitions within the planner, but simple
* translation of varattnos will suffice. This only occurs for the INSERT
- * case or in the case of UPDATE tuple routing where we didn't find a
- * result rel to reuse.
+ * case or in the case of UPDATE/MERGE tuple routing where we didn't find
+ * a result rel to reuse.
*/
if (node && node->returningLists != NIL)
{
@@ -624,6 +627,9 @@ ExecInitPartitionInfo(ModifyTableState *
list_length(node->resultRelations) == 1) ||
(node->operation == CMD_UPDATE &&
list_length(node->returningLists) ==
+ list_length(node->resultRelations)) ||
+ (node->operation == CMD_MERGE &&
+ list_length(node->returningLists) ==
list_length(node->resultRelations)));
/*
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 1ac6517..0bf94dc
--- 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;
}
/*
@@ -3386,6 +3426,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->index);
+
+ PG_RETURN_NULL();
+}
+
+/*
* Process BEFORE EACH STATEMENT triggers
*/
static void
@@ -3671,8 +3769,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 +3856,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 +3959,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/executor/spi.c b/src/backend/executor/spi.c
new file mode 100644
index 61f03e3..f7f40ae
--- a/src/backend/executor/spi.c
+++ b/src/backend/executor/spi.c
@@ -2015,6 +2015,8 @@ SPI_result_code_string(int code)
return "SPI_OK_DELETE";
case SPI_OK_UPDATE:
return "SPI_OK_UPDATE";
+ case SPI_OK_MERGE:
+ return "SPI_OK_MERGE";
case SPI_OK_CURSOR:
return "SPI_OK_CURSOR";
case SPI_OK_INSERT_RETURNING:
@@ -2023,6 +2025,8 @@ SPI_result_code_string(int code)
return "SPI_OK_DELETE_RETURNING";
case SPI_OK_UPDATE_RETURNING:
return "SPI_OK_UPDATE_RETURNING";
+ case SPI_OK_MERGE_RETURNING:
+ return "SPI_OK_MERGE_RETURNING";
case SPI_OK_REWRITTEN:
return "SPI_OK_REWRITTEN";
case SPI_OK_REL_REGISTER:
@@ -2882,7 +2886,10 @@ _SPI_pquery(QueryDesc *queryDesc, bool f
res = SPI_OK_UPDATE;
break;
case CMD_MERGE:
- res = SPI_OK_MERGE;
+ if (queryDesc->plannedstmt->hasReturning)
+ res = SPI_OK_MERGE_RETURNING;
+ else
+ res = SPI_OK_MERGE;
break;
default:
return SPI_ERROR_OPUNKNOWN;
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
new file mode 100644
index e892df9..fe0f5cb
--- 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;
@@ -941,7 +941,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;
@@ -2406,7 +2407,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
@@ -2500,10 +2502,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;
@@ -2520,7 +2523,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 4fbf80c..b8d655a
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -456,6 +456,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:
@@ -904,6 +905,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 7ff41ac..92cd8c1
--- 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:
@@ -3002,6 +3004,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..2e50eb0
--- 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.
@@ -254,6 +258,7 @@ transformMergeStmt(ParseState *pstate, M
MergeAction *action;
action = makeNode(MergeAction);
+ action->index = foreach_current_index(l) + 1;
action->commandType = mergeWhenClause->commandType;
action->matched = mergeWhenClause->matched;
@@ -390,9 +395,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 52b4a6e..20e0c05
--- a/src/backend/parser/parse_relation.c
+++ b/src/backend/parser/parse_relation.c
@@ -2345,9 +2345,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 c0f2a8a..792060d
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -11918,4 +11918,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/executor/spi.h b/src/include/executor/spi.h
new file mode 100644
index d1de139..ed8787d
--- a/src/include/executor/spi.h
+++ b/src/include/executor/spi.h
@@ -97,6 +97,7 @@ typedef struct _SPI_plan *SPIPlanPtr;
#define SPI_OK_REL_UNREGISTER 16
#define SPI_OK_TD_REGISTER 17
#define SPI_OK_MERGE 18
+#define SPI_OK_MERGE_RETURNING 19
#define SPI_OPT_NONATOMIC (1 << 0)
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
new file mode 100644
index 20f4c8b..3d62fb9
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1302,6 +1302,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 855da99..a35ab54
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -1687,6 +1687,7 @@ typedef struct MergeWhenClause
typedef struct MergeAction
{
NodeTag type;
+ int index; /* 1-based index of the clause */
bool matched; /* true=MATCHED, false=NOT MATCHED */
CmdType commandType; /* INSERT/UPDATE/DELETE/DO NOTHING */
/* OVERRIDING clause */
@@ -1801,6 +1802,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 f589112..836819d
--- 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/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
new file mode 100644
index 70a002a..143fb29
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -4246,9 +4246,9 @@ exec_stmt_execsql(PLpgSQL_execstate *est
/*
* If we have INTO, then we only need one row back ... but if we have INTO
* STRICT or extra check too_many_rows, ask for two rows, so that we can
- * verify the statement returns only one. INSERT/UPDATE/DELETE are always
- * treated strictly. Without INTO, just run the statement to completion
- * (tcount = 0).
+ * verify the statement returns only one. INSERT/UPDATE/DELETE/MERGE are
+ * always treated strictly. Without INTO, just run the statement to
+ * completion (tcount = 0).
*
* We could just ask for two rows always when using INTO, but there are
* some cases where demanding the extra row costs significant time, eg by
@@ -4286,10 +4286,11 @@ exec_stmt_execsql(PLpgSQL_execstate *est
case SPI_OK_INSERT:
case SPI_OK_UPDATE:
case SPI_OK_DELETE:
+ case SPI_OK_MERGE:
case SPI_OK_INSERT_RETURNING:
case SPI_OK_UPDATE_RETURNING:
case SPI_OK_DELETE_RETURNING:
- case SPI_OK_MERGE:
+ case SPI_OK_MERGE_RETURNING:
Assert(stmt->mod_stmt);
exec_set_found(estate, (SPI_processed != 0));
break;
@@ -4468,10 +4469,11 @@ exec_stmt_dynexecute(PLpgSQL_execstate *
case SPI_OK_INSERT:
case SPI_OK_UPDATE:
case SPI_OK_DELETE:
+ case SPI_OK_MERGE:
case SPI_OK_INSERT_RETURNING:
case SPI_OK_UPDATE_RETURNING:
case SPI_OK_DELETE_RETURNING:
- case SPI_OK_MERGE:
+ case SPI_OK_MERGE_RETURNING:
case SPI_OK_UTILITY:
case SPI_OK_REWRITTEN:
break;
diff --git a/src/pl/tcl/pltcl.c b/src/pl/tcl/pltcl.c
new file mode 100644
index 185d5be..58c4f50
--- a/src/pl/tcl/pltcl.c
+++ b/src/pl/tcl/pltcl.c
@@ -2441,6 +2441,7 @@ pltcl_process_SPI_result(Tcl_Interp *int
case SPI_OK_INSERT:
case SPI_OK_DELETE:
case SPI_OK_UPDATE:
+ case SPI_OK_MERGE:
Tcl_SetObjResult(interp, Tcl_NewWideIntObj(ntuples));
break;
@@ -2458,6 +2459,7 @@ pltcl_process_SPI_result(Tcl_Interp *int
case SPI_OK_INSERT_RETURNING:
case SPI_OK_DELETE_RETURNING:
case SPI_OK_UPDATE_RETURNING:
+ case SPI_OK_MERGE_RETURNING:
/*
* Process the tuples we got
diff --git a/src/test/regress/expected/merge.out b/src/test/regress/expected/merge.out
new file mode 100644
index bc53b21..b150726
--- 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,20 +1289,187 @@ 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)
+ 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 *;
- ^
+ DELETE
+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...
+ ^
+-- RETURNING in CTEs
+CREATE TABLE sq_target_merge_log (tid integer NOT NULL, last_change text);
+INSERT INTO sq_target_merge_log VALUES (1, 'Original value');
+BEGIN;
+WITH m AS (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 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
+), m2 AS (
+ MERGE INTO sq_target_merge_log l
+ USING m
+ ON l.tid = m.tid
+ WHEN MATCHED THEN
+ UPDATE SET last_change = description
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (m.tid, description)
+ RETURNING merge_action, pg_merge_action() AS merge_log_action, l.*
+)
+SELECT * FROM m2;
+ merge_action | merge_log_action | tid | last_change
+--------------+------------------+-----+---------------------
+ DELETE | UPDATE | 1 | Removed (1,100)
+ UPDATE | INSERT | 2 | Added 20 to balance
+ INSERT | INSERT | 4 | Inserted (4,40)
+(3 rows)
+
+SELECT * FROM sq_target_merge_log ORDER BY tid;
+ tid | last_change
+-----+---------------------
+ 1 | Removed (1,100)
+ 2 | Added 20 to balance
+ 4 | Inserted (4,40)
+(3 rows)
+
+ROLLBACK;
+-- COPY (MERGE ... RETURNING) TO ...
+BEGIN;
+COPY (
+ MERGE INTO sq_target t
+ USING v
+ ON tid = sid
+ 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 pg_merge_action(), t.*
+) TO stdout;
+DELETE 1 100
+ROLLBACK;
+-- SQL function with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT action text, OUT tid int, OUT new_balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid > 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING pg_merge_action(), t.*;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT action, tid, new_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ action | tid | new_balance
+--------+-----+-------------
+ DELETE | 1 | 100
+ UPDATE | 3 | 320
+ INSERT | 4 | 110
+(3 rows)
+
+ROLLBACK;
+-- SQL SRF with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_sq_source_into_sq_target()
+RETURNS TABLE (action text, tid int, balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 pg_merge_action(), t.*;
+$$;
+SELECT * FROM merge_sq_source_into_sq_target();
+ action | tid | balance
+--------+-----+---------
+ DELETE | 1 | 100
+ INSERT | 4 | 40
+(2 rows)
+
+ROLLBACK;
+-- PL/pgSQL function with MERGE ... RETURNING ... INTO
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT r_action text, OUT r_tid int, OUT r_balance int)
+LANGUAGE plpgsql AS
+$$
+BEGIN
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid > 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING pg_merge_action(), t.* INTO r_action, r_tid, r_balance;
+END;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT r_action, r_tid, r_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ r_action | r_tid | r_balance
+----------+-------+-----------
+ DELETE | 1 | 100
+ UPDATE | 3 | 320
+ INSERT | 4 | 110
+(3 rows)
+
ROLLBACK;
-- EXPLAIN
CREATE TABLE ex_mtarget (a int, b int)
@@ -1499,7 +1666,7 @@ SELECT * FROM sq_target WHERE tid = 1;
(1 row)
ROLLBACK;
-DROP TABLE sq_target, sq_source CASCADE;
+DROP TABLE sq_target, sq_target_merge_log, sq_source CASCADE;
NOTICE: drop cascades to view v
CREATE TABLE pa_target (tid integer, balance float, val text)
PARTITION BY LIST (tid);
@@ -1588,7 +1755,26 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (sid, delta, 'inserted by merge');
+ INSERT VALUES (sid, delta, 'inserted by merge')
+ RETURNING pg_merge_action(), pg_merge_when_clause(), t.*;
+ pg_merge_action | pg_merge_when_clause | tid | balance | val
+-----------------+----------------------+-----+---------+--------------------------
+ UPDATE | 1 | 2 | 110 | initial updated by merge
+ INSERT | 2 | 2 | 20 | inserted by merge
+ UPDATE | 1 | 4 | 330 | initial updated by merge
+ INSERT | 2 | 4 | 40 | inserted by merge
+ UPDATE | 1 | 6 | 550 | initial updated by merge
+ INSERT | 2 | 6 | 60 | inserted by merge
+ UPDATE | 1 | 8 | 770 | initial updated by merge
+ INSERT | 2 | 8 | 80 | inserted by merge
+ UPDATE | 1 | 10 | 990 | initial updated by merge
+ INSERT | 2 | 10 | 100 | inserted by merge
+ UPDATE | 1 | 12 | 1210 | initial updated by merge
+ INSERT | 2 | 12 | 120 | inserted by merge
+ UPDATE | 1 | 14 | 1430 | initial updated by merge
+ INSERT | 2 | 14 | 140 | inserted by merge
+(14 rows)
+
SELECT * FROM pa_target ORDER BY tid;
tid | balance | val
-----+---------+--------------------------
@@ -1609,6 +1795,32 @@ SELECT * FROM pa_target ORDER BY tid;
(14 rows)
ROLLBACK;
+-- update partition key to partition not initially scanned
+BEGIN;
+MERGE INTO pa_target t
+ USING pa_source s
+ ON t.tid = s.sid AND t.tid = 1
+ WHEN MATCHED THEN
+ UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
+ RETURNING pg_merge_action(), pg_merge_when_clause(), t.*;
+ pg_merge_action | pg_merge_when_clause | tid | balance | val
+-----------------+----------------------+-----+---------+--------------------------
+ UPDATE | 1 | 2 | 110 | initial updated by merge
+(1 row)
+
+SELECT * FROM pa_target ORDER BY tid;
+ tid | balance | val
+-----+---------+--------------------------
+ 2 | 110 | initial updated by merge
+ 3 | 300 | initial
+ 5 | 500 | initial
+ 7 | 700 | initial
+ 9 | 900 | initial
+ 11 | 1100 | initial
+ 13 | 1300 | initial
+(7 rows)
+
+ROLLBACK;
DROP TABLE pa_target CASCADE;
-- The target table is partitioned in the same way, but this time by attaching
-- partitions which have columns in different order, dropped columns etc.
@@ -1700,7 +1912,26 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (sid, delta, 'inserted by merge');
+ INSERT VALUES (sid, delta, 'inserted by merge')
+ RETURNING pg_merge_action(), pg_merge_when_clause(), t.*;
+ pg_merge_action | pg_merge_when_clause | tid | balance | val
+-----------------+----------------------+-----+---------+--------------------------
+ UPDATE | 1 | 2 | 110 | initial updated by merge
+ INSERT | 2 | 2 | 20 | inserted by merge
+ UPDATE | 1 | 4 | 330 | initial updated by merge
+ INSERT | 2 | 4 | 40 | inserted by merge
+ UPDATE | 1 | 6 | 550 | initial updated by merge
+ INSERT | 2 | 6 | 60 | inserted by merge
+ UPDATE | 1 | 8 | 770 | initial updated by merge
+ INSERT | 2 | 8 | 80 | inserted by merge
+ UPDATE | 1 | 10 | 990 | initial updated by merge
+ INSERT | 2 | 10 | 100 | inserted by merge
+ UPDATE | 1 | 12 | 1210 | initial updated by merge
+ INSERT | 2 | 12 | 120 | inserted by merge
+ UPDATE | 1 | 14 | 1430 | initial updated by merge
+ INSERT | 2 | 14 | 140 | inserted by merge
+(14 rows)
+
SELECT * FROM pa_target ORDER BY tid;
tid | balance | val
-----+---------+--------------------------
@@ -1755,7 +1986,21 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge');
+ INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge')
+ RETURNING pg_merge_action(), pg_merge_when_clause(), t.*;
+ pg_merge_action | pg_merge_when_clause | logts | tid | balance | val
+-----------------+----------------------+--------------------------+-----+---------+--------------------------
+ UPDATE | 1 | Tue Jan 31 00:00:00 2017 | 1 | 110 | initial updated by merge
+ UPDATE | 1 | Tue Feb 28 00:00:00 2017 | 2 | 220 | initial updated by merge
+ INSERT | 2 | Sun Jan 15 00:00:00 2017 | 3 | 30 | inserted by merge
+ UPDATE | 1 | Tue Jan 31 00:00:00 2017 | 4 | 440 | initial updated by merge
+ UPDATE | 1 | Tue Feb 28 00:00:00 2017 | 5 | 550 | initial updated by merge
+ INSERT | 2 | Sun Jan 15 00:00:00 2017 | 6 | 60 | inserted by merge
+ UPDATE | 1 | Tue Jan 31 00:00:00 2017 | 7 | 770 | initial updated by merge
+ UPDATE | 1 | Tue Feb 28 00:00:00 2017 | 8 | 880 | initial updated by merge
+ INSERT | 2 | Sun Jan 15 00:00:00 2017 | 9 | 90 | inserted by merge
+(9 rows)
+
SELECT * FROM pa_target ORDER BY tid;
logts | tid | balance | val
--------------------------+-----+---------+--------------------------
diff --git a/src/test/regress/sql/merge.sql b/src/test/regress/sql/merge.sql
new file mode 100644
index fdbcd70..3bffb29
--- 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,17 +844,145 @@ 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)
+ INSERT (balance, tid) VALUES (balance + delta, sid)
WHEN MATCHED AND tid < 2 THEN
- DELETE
-RETURNING *;
+ DELETE
+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();
+
+-- RETURNING in CTEs
+CREATE TABLE sq_target_merge_log (tid integer NOT NULL, last_change text);
+INSERT INTO sq_target_merge_log VALUES (1, 'Original value');
+BEGIN;
+WITH m AS (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 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
+), m2 AS (
+ MERGE INTO sq_target_merge_log l
+ USING m
+ ON l.tid = m.tid
+ WHEN MATCHED THEN
+ UPDATE SET last_change = description
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (m.tid, description)
+ RETURNING merge_action, pg_merge_action() AS merge_log_action, l.*
+)
+SELECT * FROM m2;
+SELECT * FROM sq_target_merge_log ORDER BY tid;
+ROLLBACK;
+
+-- COPY (MERGE ... RETURNING) TO ...
+BEGIN;
+COPY (
+ MERGE INTO sq_target t
+ USING v
+ ON tid = sid
+ 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 pg_merge_action(), t.*
+) TO stdout;
+ROLLBACK;
+
+-- SQL function with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT action text, OUT tid int, OUT new_balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid > 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING pg_merge_action(), t.*;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT action, tid, new_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ROLLBACK;
+
+-- SQL SRF with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_sq_source_into_sq_target()
+RETURNS TABLE (action text, tid int, balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 pg_merge_action(), t.*;
+$$;
+SELECT * FROM merge_sq_source_into_sq_target();
+ROLLBACK;
+
+-- PL/pgSQL function with MERGE ... RETURNING ... INTO
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT r_action text, OUT r_tid int, OUT r_balance int)
+LANGUAGE plpgsql AS
+$$
+BEGIN
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid > 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING pg_merge_action(), t.* INTO r_action, r_tid, r_balance;
+END;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT r_action, r_tid, r_balance FROM merge_into_sq_target(sid, balance, delta)) m;
ROLLBACK;
-- EXPLAIN
@@ -953,7 +1081,7 @@ WHEN MATCHED THEN
SELECT * FROM sq_target WHERE tid = 1;
ROLLBACK;
-DROP TABLE sq_target, sq_source CASCADE;
+DROP TABLE sq_target, sq_target_merge_log, sq_source CASCADE;
CREATE TABLE pa_target (tid integer, balance float, val text)
PARTITION BY LIST (tid);
@@ -1005,7 +1133,19 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (sid, delta, 'inserted by merge');
+ INSERT VALUES (sid, delta, 'inserted by merge')
+ RETURNING pg_merge_action(), pg_merge_when_clause(), t.*;
+SELECT * FROM pa_target ORDER BY tid;
+ROLLBACK;
+
+-- update partition key to partition not initially scanned
+BEGIN;
+MERGE INTO pa_target t
+ USING pa_source s
+ ON t.tid = s.sid AND t.tid = 1
+ WHEN MATCHED THEN
+ UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
+ RETURNING pg_merge_action(), pg_merge_when_clause(), t.*;
SELECT * FROM pa_target ORDER BY tid;
ROLLBACK;
@@ -1067,7 +1207,8 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (sid, delta, 'inserted by merge');
+ INSERT VALUES (sid, delta, 'inserted by merge')
+ RETURNING pg_merge_action(), pg_merge_when_clause(), t.*;
SELECT * FROM pa_target ORDER BY tid;
ROLLBACK;
@@ -1109,7 +1250,8 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge');
+ INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge')
+ RETURNING pg_merge_action(), pg_merge_when_clause(), t.*;
SELECT * FROM pa_target ORDER BY tid;
ROLLBACK;
On Tue, 7 Feb 2023 at 10:56, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
Attached is a more complete patch
Rebased version attached.
Regards,
Dean
Attachments:
support-merge-returning-v4.patchtext/x-patch; charset=US-ASCII; name=support-merge-returning-v4.patchDownload
diff --git a/doc/src/sgml/dml.sgml b/doc/src/sgml/dml.sgml
new file mode 100644
index cbbc5e2..ff2a827
--- a/doc/src/sgml/dml.sgml
+++ b/doc/src/sgml/dml.sgml
@@ -283,10 +283,15 @@ DELETE FROM products;
<secondary>RETURNING</secondary>
</indexterm>
+ <indexterm zone="dml-returning">
+ <primary>MERGE</primary>
+ <secondary>RETURNING</secondary>
+ </indexterm>
+
<para>
Sometimes it is useful to obtain data from modified rows while they are
being manipulated. The <command>INSERT</command>, <command>UPDATE</command>,
- and <command>DELETE</command> commands all have an
+ <command>DELETE</command>, and <command>MERGE</command> commands all have an
optional <literal>RETURNING</literal> clause that supports this. Use
of <literal>RETURNING</literal> avoids performing an extra database query to
collect the data, and is especially valuable when it would otherwise be
@@ -339,6 +344,24 @@ DELETE FROM products
</programlisting>
</para>
+ <para>
+ In a <command>MERGE</command>, the data available to <literal>RETURNING</literal> is
+ the content of the source row plus the content of the inserted, updated, or
+ deleted target row. In addition, the merge support functions
+ <xref linkend="pg-merge-action"/> and <xref linkend="pg-merge-when-clause"/>
+ may be used to return information about which merge action was executed for
+ each row. Since it is quite common for the source and target to have many
+ of the same columns, specifying <literal>RETURNING *</literal> can lead to a
+ lot of duplicated columns, so it is often more useful to just return the
+ target row. For example:
+<programlisting>
+MERGE INTO products p USING new_products n ON p.product_no = n.product_no
+ WHEN NOT MATCHED THEN INSERT VALUES (n.product_no, n.name, n.price)
+ WHEN MATCHED THEN UPDATE SET name = n.name, price = n.price
+ RETURNING pg_merge_action(), p.*;
+</programlisting>
+ </para>
+
<para>
If there are triggers (<xref linkend="triggers"/>) on the target table,
the data available to <literal>RETURNING</literal> is the row as modified by
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
new file mode 100644
index 0cbdf63..224e9b8
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -21216,6 +21216,99 @@ SELECT count(*) FROM sometable;
</sect1>
+ <sect1 id="functions-merge-support">
+ <title>Merge Support Functions</title>
+
+ <indexterm>
+ <primary>MERGE</primary>
+ <secondary>RETURNING</secondary>
+ </indexterm>
+
+ <para>
+ The merge support functions shown in
+ <xref linkend="functions-merge-support-table"/> may be used in the
+ <literal>RETURNING</literal> list of a <xref linkend="sql-merge"/> command
+ to return additional information about the action taken for each row.
+ </para>
+
+ <table id="functions-merge-support-table">
+ <title>Merge Support Functions</title>
+
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ Function
+ </para>
+ <para>
+ Description
+ </para></entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm id="pg-merge-action">
+ <primary>pg_merge_action</primary>
+ </indexterm>
+ <function>pg_merge_action</function> ( )
+ <returnvalue>text</returnvalue>
+ </para>
+ <para>
+ Returns the action performed on the current row (<literal>'INSERT'</literal>,
+ <literal>'UPDATE'</literal>, or <literal>'DELETE'</literal>).
+ </para></entry>
+ </row>
+
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm id="pg-merge-when-clause">
+ <primary>pg_merge_when_clause</primary>
+ </indexterm>
+ <function>pg_merge_when_clause</function> ( )
+ <returnvalue>integer</returnvalue>
+ </para>
+ <para>
+ Returns the 1-based index of the <literal>WHEN</literal> clause executed
+ for the current row.
+ </para></entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+
+ <para>
+ Example:
+<screen><![CDATA[
+MERGE INTO products p
+ USING stock s ON p.product_id = s.product_id
+ WHEN MATCHED AND s.quantity > 0 THEN
+ UPDATE SET in_stock = true, quantity = s.quantity
+ WHEN MATCHED THEN
+ UPDATE SET in_stock = false, quantity = 0
+ WHEN NOT MATCHED THEN
+ INSERT (product_id, in_stock, quantity)
+ VALUES (s.product_id, true, s.quantity)
+ RETURNING pg_merge_action() AS action,
+ pg_merge_when_clause() AS when_clause,
+ p.*;
+
+ action | when_clause | product_id | in_stock | quantity
+--------+-------------+------------+----------+----------
+ UPDATE | 1 | 1001 | t | 50
+ UPDATE | 2 | 1002 | f | 0
+ INSERT | 3 | 1003 | t | 10
+]]></screen>
+ </para>
+
+ <para>
+ Note that it is an error to use these functions anywhere other than the
+ <literal>RETURNING</literal> list of a <command>MERGE</command> command.
+ </para>
+
+ </sect1>
+
<sect1 id="functions-subquery">
<title>Subquery Expressions</title>
diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml
new file mode 100644
index 7c01a54..4317cfc
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -1323,9 +1323,9 @@
to a <glossterm linkend="glossary-client">client</glossterm> upon the
completion of an <acronym>SQL</acronym> command, usually a
<command>SELECT</command> but it can be an
- <command>INSERT</command>, <command>UPDATE</command>, or
- <command>DELETE</command> command if the <literal>RETURNING</literal>
- clause is specified.
+ <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command> command if the
+ <literal>RETURNING</literal> clause is specified.
</para>
<para>
The fact that a result set is a relation means that a query can be used
diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml
new file mode 100644
index 8897a54..3249e9c
--- a/doc/src/sgml/plpgsql.sgml
+++ b/doc/src/sgml/plpgsql.sgml
@@ -1020,8 +1020,8 @@ INSERT INTO mytable VALUES (1,'one'), (2
</para>
<para>
- If the command does return rows (for example <command>SELECT</command>,
- or <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>
+ If the command does return rows (for example <command>SELECT</command>, or
+ <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command>
with <literal>RETURNING</literal>), there are two ways to proceed.
When the command will return at most one row, or you only care about
the first row of output, write the command as usual but add
@@ -1044,7 +1044,8 @@ INSERT INTO mytable VALUES (1,'one'), (2
<application>PL/pgSQL</application> variable values can be
automatically inserted into optimizable SQL commands, which
are <command>SELECT</command>, <command>INSERT</command>,
- <command>UPDATE</command>, <command>DELETE</command>, and certain
+ <command>UPDATE</command>, <command>DELETE</command>,
+ <command>MERGE</command> and certain
utility commands that incorporate one of these, such
as <command>EXPLAIN</command> and <command>CREATE TABLE ... AS
SELECT</command>. In these commands,
@@ -1148,6 +1149,7 @@ SELECT <replaceable>select_expressions</
INSERT ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
UPDATE ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
DELETE ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
+MERGE ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
</synopsis>
where <replaceable>target</replaceable> can be a record variable, a row
@@ -1158,8 +1160,8 @@ DELETE ... RETURNING <replaceable>expres
<literal>INTO</literal> clause) just as described above,
and the plan is cached in the same way.
This works for <command>SELECT</command>,
- <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command> with
- <literal>RETURNING</literal>, and certain utility commands
+ <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command>
+ with <literal>RETURNING</literal>, and certain utility commands
that return row sets, such as <command>EXPLAIN</command>.
Except for the <literal>INTO</literal> clause, the SQL command is the same
as it would be written outside <application>PL/pgSQL</application>.
@@ -1235,7 +1237,7 @@ END;
</para>
<para>
- For <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command> with
+ For <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command> with
<literal>RETURNING</literal>, <application>PL/pgSQL</application> reports
an error for more than one returned row, even when
<literal>STRICT</literal> is not specified. This is because there
diff --git a/doc/src/sgml/queries.sgml b/doc/src/sgml/queries.sgml
new file mode 100644
index 45741e7..0ca593b
--- a/doc/src/sgml/queries.sgml
+++ b/doc/src/sgml/queries.sgml
@@ -2062,10 +2062,11 @@ SELECT <replaceable>select_list</replace
Table Expressions or <acronym>CTE</acronym>s, can be thought of as defining
temporary tables that exist just for one query. Each auxiliary statement
in a <literal>WITH</literal> clause can be a <command>SELECT</command>,
- <command>INSERT</command>, <command>UPDATE</command>, or <command>DELETE</command>; and the
+ <command>INSERT</command>, <command>UPDATE</command>, <command>DELETE</command>,
+ or <command>MERGE</command>; and the
<literal>WITH</literal> clause itself is attached to a primary statement that can
- also be a <command>SELECT</command>, <command>INSERT</command>, <command>UPDATE</command>, or
- <command>DELETE</command>.
+ also be a <command>SELECT</command>, <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command>.
</para>
<sect2 id="queries-with-select">
@@ -2587,7 +2588,8 @@ SELECT * FROM w AS w1 JOIN w AS w2 ON w1
<para>
The examples above only show <literal>WITH</literal> being used with
<command>SELECT</command>, but it can be attached in the same way to
- <command>INSERT</command>, <command>UPDATE</command>, or <command>DELETE</command>.
+ <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command> or <command>MERGE</command>.
In each case it effectively provides temporary table(s) that can
be referred to in the main command.
</para>
@@ -2598,7 +2600,8 @@ SELECT * FROM w AS w1 JOIN w AS w2 ON w1
<para>
You can use data-modifying statements (<command>INSERT</command>,
- <command>UPDATE</command>, or <command>DELETE</command>) in <literal>WITH</literal>. This
+ <command>UPDATE</command>, <command>DELETE</command>, or
+ <command>MERGE</command>) in <literal>WITH</literal>. This
allows you to perform several different operations in the same query.
An example is:
diff --git a/doc/src/sgml/ref/copy.sgml b/doc/src/sgml/ref/copy.sgml
new file mode 100644
index c25b52d..d822470
--- a/doc/src/sgml/ref/copy.sgml
+++ b/doc/src/sgml/ref/copy.sgml
@@ -121,13 +121,15 @@ COPY { <replaceable class="parameter">ta
A <link linkend="sql-select"><command>SELECT</command></link>,
<link linkend="sql-values"><command>VALUES</command></link>,
<link linkend="sql-insert"><command>INSERT</command></link>,
- <link linkend="sql-update"><command>UPDATE</command></link>, or
- <link linkend="sql-delete"><command>DELETE</command></link> command whose results are to be
+ <link linkend="sql-update"><command>UPDATE</command></link>,
+ <link linkend="sql-update"><command>DELETE</command></link>, or
+ <link linkend="sql-delete"><command>MERGE</command></link> command whose results are to be
copied. Note that parentheses are required around the query.
</para>
<para>
- For <command>INSERT</command>, <command>UPDATE</command> and
- <command>DELETE</command> queries a RETURNING clause must be provided,
+ For <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, and <command>MERGE</command> queries a
+ RETURNING clause must be provided,
and the target relation must not have a conditional rule, nor
an <literal>ALSO</literal> rule, nor an <literal>INSTEAD</literal> rule
that expands to multiple statements.
diff --git a/doc/src/sgml/ref/merge.sgml b/doc/src/sgml/ref/merge.sgml
new file mode 100644
index 0995fe0..7eb641a
--- a/doc/src/sgml/ref/merge.sgml
+++ b/doc/src/sgml/ref/merge.sgml
@@ -25,6 +25,7 @@ PostgreSQL documentation
MERGE INTO [ ONLY ] <replaceable class="parameter">target_table_name</replaceable> [ * ] [ [ AS ] <replaceable class="parameter">target_alias</replaceable> ]
USING <replaceable class="parameter">data_source</replaceable> ON <replaceable class="parameter">join_condition</replaceable>
<replaceable class="parameter">when_clause</replaceable> [...]
+[ RETURNING * | <replaceable class="parameter">output_expression</replaceable> [ [ AS ] <replaceable class="parameter">output_name</replaceable> ] [, ...] ]
<phrase>where <replaceable class="parameter">data_source</replaceable> is:</phrase>
@@ -95,6 +96,20 @@ DELETE
</para>
<para>
+ The optional <literal>RETURNING</literal> clause causes <command>MERGE</command>
+ to compute and return value(s) based on each row inserted, updated, or
+ deleted. Any expression using the source or target table's columns, or the
+ merge support functions <xref linkend="pg-merge-action"/> and
+ <xref linkend="pg-merge-when-clause"/> can be computed. When an
+ <command>INSERT</command> or <command>UPDATE</command> action is performed,
+ the new values of the target table's columns are used. When a
+ <command>DELETE</command> is performed, the old values of the target table's
+ columns are used. The syntax of the <literal>RETURNING</literal> list is
+ identical to that of the output list of <command>SELECT</command>.
+
+ </para>
+
+ <para>
There is no separate <literal>MERGE</literal> privilege.
If you specify an update action, you must have the
<literal>UPDATE</literal> privilege on the column(s)
@@ -125,6 +140,18 @@ DELETE
<variablelist>
<varlistentry>
+ <term><replaceable class="parameter">with_query</replaceable></term>
+ <listitem>
+ <para>
+ The <literal>WITH</literal> clause allows you to specify one or more
+ subqueries that can be referenced by name in the <command>MERGE</command>
+ query. See <xref linkend="queries-with"/> and <xref linkend="sql-select"/>
+ for details.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
<term><replaceable class="parameter">target_table_name</replaceable></term>
<listitem>
<para>
@@ -399,13 +426,30 @@ DELETE
</varlistentry>
<varlistentry>
- <term><replaceable class="parameter">with_query</replaceable></term>
+ <term><replaceable class="parameter">output_expression</replaceable></term>
<listitem>
<para>
- The <literal>WITH</literal> clause allows you to specify one or more
- subqueries that can be referenced by name in the <command>MERGE</command>
- query. See <xref linkend="queries-with"/> and <xref linkend="sql-select"/>
- for details.
+ An expression to be computed and returned by the <command>MERGE</command>
+ command after each row is merged. The expression can use any columns of
+ the source or target tables, or the merge support functions
+ <xref linkend="pg-merge-action"/> and <xref linkend="pg-merge-when-clause"/>.
+ </para>
+ <para>
+ Writing <literal>*</literal> will return all columns from the source
+ table, followed by all columns from the target table. Often this will
+ lead to a lot of duplication, since it is common for the source and
+ target tables to have a lot of the same columns. This can be avoided by
+ qualifying the <literal>*</literal> with the name or alias of the source
+ or target table.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><replaceable class="parameter">output_name</replaceable></term>
+ <listitem>
+ <para>
+ A name to use for a returned column.
</para>
</listitem>
</varlistentry>
@@ -428,6 +472,13 @@ MERGE <replaceable class="parameter">tot
were changed in any way.
</para>
+ <para>
+ If the <command>MERGE</command> command contains a <literal>RETURNING</literal>
+ clause, the result will be similar to that of a <command>SELECT</command>
+ statement containing the columns and values defined in the
+ <literal>RETURNING</literal> list, computed over the row(s) inserted, updated,
+ or deleted by the command.
+ </para>
</refsect1>
<refsect1>
@@ -544,13 +595,6 @@ MERGE <replaceable class="parameter">tot
</para>
<para>
- There is no <literal>RETURNING</literal> clause with
- <command>MERGE</command>. Actions of <command>INSERT</command>,
- <command>UPDATE</command> and <command>DELETE</command> cannot contain
- <literal>RETURNING</literal> or <literal>WITH</literal> clauses.
- </para>
-
- <para>
When <command>MERGE</command> is run concurrently with other commands
that modify the target table, the usual transaction isolation rules
apply; see <xref linkend="transaction-iso"/> for an explanation
@@ -602,7 +646,8 @@ WHEN NOT MATCHED THEN
<para>
Attempt to insert a new stock item along with the quantity of stock. If
the item already exists, instead update the stock count of the existing
- item. Don't allow entries that have zero stock.
+ item. Don't allow entries that have zero stock. Return details of all
+ changes made.
<programlisting>
MERGE INTO wines w
USING wine_stock_changes s
@@ -612,7 +657,8 @@ WHEN NOT MATCHED AND s.stock_delta > 0 T
WHEN MATCHED AND w.stock + s.stock_delta > 0 THEN
UPDATE SET stock = w.stock + s.stock_delta
WHEN MATCHED THEN
- DELETE;
+ DELETE
+RETURNING pg_merge_action(), w.*;
</programlisting>
The <literal>wine_stock_changes</literal> table might be, for example, a
@@ -627,7 +673,8 @@ WHEN MATCHED THEN
This command conforms to the <acronym>SQL</acronym> standard.
</para>
<para>
- The WITH clause and <literal>DO NOTHING</literal> action are extensions to
+ The WITH clause, <literal>DO NOTHING</literal> action, and
+ <literal>RETURNING</literal> clause are extensions to
the <acronym>SQL</acronym> standard.
</para>
</refsect1>
diff --git a/doc/src/sgml/ref/select.sgml b/doc/src/sgml/ref/select.sgml
new file mode 100644
index 1f9538f..5840ddc
--- a/doc/src/sgml/ref/select.sgml
+++ b/doc/src/sgml/ref/select.sgml
@@ -74,7 +74,7 @@ SELECT [ ALL | DISTINCT [ ON ( <replacea
<phrase>and <replaceable class="parameter">with_query</replaceable> is:</phrase>
- <replaceable class="parameter">with_query_name</replaceable> [ ( <replaceable class="parameter">column_name</replaceable> [, ...] ) ] AS [ [ NOT ] MATERIALIZED ] ( <replaceable class="parameter">select</replaceable> | <replaceable class="parameter">values</replaceable> | <replaceable class="parameter">insert</replaceable> | <replaceable class="parameter">update</replaceable> | <replaceable class="parameter">delete</replaceable> )
+ <replaceable class="parameter">with_query_name</replaceable> [ ( <replaceable class="parameter">column_name</replaceable> [, ...] ) ] AS [ [ NOT ] MATERIALIZED ] ( <replaceable class="parameter">select</replaceable> | <replaceable class="parameter">values</replaceable> | <replaceable class="parameter">insert</replaceable> | <replaceable class="parameter">update</replaceable> | <replaceable class="parameter">delete</replaceable> | <replaceable class="parameter">merge</replaceable> )
[ SEARCH { BREADTH | DEPTH } FIRST BY <replaceable>column_name</replaceable> [, ...] SET <replaceable>search_seq_col_name</replaceable> ]
[ CYCLE <replaceable>column_name</replaceable> [, ...] SET <replaceable>cycle_mark_col_name</replaceable> [ TO <replaceable>cycle_mark_value</replaceable> DEFAULT <replaceable>cycle_mark_default</replaceable> ] USING <replaceable>cycle_path_col_name</replaceable> ]
@@ -227,10 +227,10 @@ TABLE [ ONLY ] <replaceable class="param
The subqueries effectively act as temporary tables or views
for the duration of the primary query.
Each subquery can be a <command>SELECT</command>, <command>TABLE</command>, <command>VALUES</command>,
- <command>INSERT</command>, <command>UPDATE</command> or
- <command>DELETE</command> statement.
+ <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command> statement.
When writing a data-modifying statement (<command>INSERT</command>,
- <command>UPDATE</command> or <command>DELETE</command>) in
+ <command>UPDATE</command>, <command>DELETE</command>, or <command>MERGE</command>) in
<literal>WITH</literal>, it is usual to include a <literal>RETURNING</literal> clause.
It is the output of <literal>RETURNING</literal>, <emphasis>not</emphasis> the underlying
table that the statement modifies, that forms the temporary table that is
@@ -2179,7 +2179,8 @@ SELECT 2+2;
<para>
<productname>PostgreSQL</productname> allows <command>INSERT</command>,
- <command>UPDATE</command>, and <command>DELETE</command> to be used as <literal>WITH</literal>
+ <command>UPDATE</command>, <command>DELETE</command>, and
+ <command>MERGE</command> to be used as <literal>WITH</literal>
queries. This is not found in the SQL standard.
</para>
</refsect2>
diff --git a/doc/src/sgml/rowtypes.sgml b/doc/src/sgml/rowtypes.sgml
new file mode 100644
index 4d86f97..bbeac84
--- a/doc/src/sgml/rowtypes.sgml
+++ b/doc/src/sgml/rowtypes.sgml
@@ -348,7 +348,7 @@ SELECT m.* FROM some_table, LATERAL myfu
column expansion of this kind when it appears at the top level of
a <link linkend="queries-select-lists"><command>SELECT</command> output
list</link>, a <link linkend="dml-returning"><literal>RETURNING</literal>
- list</link> in <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>,
+ list</link> in <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command>,
a <link linkend="queries-values"><literal>VALUES</literal> clause</link>, or
a <link linkend="sql-syntax-row-constructors">row constructor</link>.
In all other contexts (including when nested inside one of those
diff --git a/doc/src/sgml/spi.sgml b/doc/src/sgml/spi.sgml
new file mode 100644
index 651930a..ad0a9b1
--- a/doc/src/sgml/spi.sgml
+++ b/doc/src/sgml/spi.sgml
@@ -301,8 +301,9 @@ SPI_execute("INSERT INTO foo SELECT * FR
is returned in the global variable <varname>SPI_processed</varname>.
If the return value of the function is <symbol>SPI_OK_SELECT</symbol>,
<symbol>SPI_OK_INSERT_RETURNING</symbol>,
- <symbol>SPI_OK_DELETE_RETURNING</symbol>, or
- <symbol>SPI_OK_UPDATE_RETURNING</symbol>,
+ <symbol>SPI_OK_DELETE_RETURNING</symbol>,
+ <symbol>SPI_OK_UPDATE_RETURNING</symbol>, or
+ <symbol>SPI_OK_MERGE_RETURNING</symbol>
then you can use the
global pointer <literal>SPITupleTable *SPI_tuptable</literal> to
access the result rows. Some utility commands (such as
@@ -472,6 +473,15 @@ typedef struct SPITupleTable
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><symbol>SPI_OK_MERGE_RETURNING</symbol></term>
+ <listitem>
+ <para>
+ if a <command>MERGE RETURNING</command> was executed
+ </para>
+ </listitem>
+ </varlistentry>
<varlistentry>
<term><symbol>SPI_OK_UTILITY</symbol></term>
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
new file mode 100644
index e2a5496..114c3d3
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -177,7 +177,8 @@
statements separated by semicolons. A semicolon after the last
statement is optional. Unless the function is declared to return
<type>void</type>, the last statement must be a <command>SELECT</command>,
- or an <command>INSERT</command>, <command>UPDATE</command>, or <command>DELETE</command>
+ or an <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command>
that has a <literal>RETURNING</literal> clause.
</para>
@@ -186,7 +187,8 @@
language can be packaged together and defined as a function.
Besides <command>SELECT</command> queries, the commands can include data
modification queries (<command>INSERT</command>,
- <command>UPDATE</command>, and <command>DELETE</command>), as well as
+ <command>UPDATE</command>, <command>DELETE</command> and
+ <command>MERGE</command>), as well as
other SQL commands. (You cannot use transaction control commands, e.g.,
<command>COMMIT</command>, <command>SAVEPOINT</command>, and some utility
commands, e.g., <literal>VACUUM</literal>, in <acronym>SQL</acronym> functions.)
@@ -1235,8 +1237,9 @@ SELECT x, CASE WHEN x > 0 THEN genera
<note>
<para>
- If a function's last command is <command>INSERT</command>, <command>UPDATE</command>,
- or <command>DELETE</command> with <literal>RETURNING</literal>, that command will
+ If a function's last command is <command>INSERT</command>,
+ <command>UPDATE</command>, <command>DELETE</command>, or
+ <command>MERGE</command> with <literal>RETURNING</literal>, that command will
always be executed to completion, even if the function is not declared
with <literal>SETOF</literal> or the calling query does not fetch all the
result rows. Any extra rows produced by the <literal>RETURNING</literal>
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 fd6ca8a..95bbf67
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -612,8 +612,8 @@ ExecInitPartitionInfo(ModifyTableState *
* Build the RETURNING projection for the partition. Note that we didn't
* build the returningList for partitions within the planner, but simple
* translation of varattnos will suffice. This only occurs for the INSERT
- * case or in the case of UPDATE tuple routing where we didn't find a
- * result rel to reuse.
+ * case or in the case of UPDATE/MERGE tuple routing where we didn't find
+ * a result rel to reuse.
*/
if (node && node->returningLists != NIL)
{
@@ -622,12 +622,14 @@ ExecInitPartitionInfo(ModifyTableState *
List *returningList;
/* See the comment above for WCO lists. */
- /* (except no RETURNING support for MERGE yet) */
Assert((node->operation == CMD_INSERT &&
list_length(node->returningLists) == 1 &&
list_length(node->resultRelations) == 1) ||
(node->operation == CMD_UPDATE &&
list_length(node->returningLists) ==
+ list_length(node->resultRelations)) ||
+ (node->operation == CMD_MERGE &&
+ list_length(node->returningLists) ==
list_length(node->resultRelations)));
/*
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 6f0543a..7e98890
--- 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;
}
}
@@ -2051,7 +2048,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;
/*
@@ -2689,6 +2686,7 @@ static TupleTableSlot *
ExecMerge(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
ItemPointer tupleid, bool canSetTag)
{
+ TupleTableSlot *rslot = NULL;
bool matched;
/*-----
@@ -2736,7 +2734,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()
@@ -2744,10 +2743,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;
}
/*
@@ -2757,8 +2755,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.
*
@@ -2767,15 +2765,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;
@@ -2787,7 +2786,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
@@ -2866,7 +2868,7 @@ lmerge_matched:
*/
newslot = ExecProject(relaction->mas_proj);
- context->relaction = relaction;
+ mtstate->mt_merge_action = relaction;
context->GetUpdateNewTuple = mergeGetUpdateNewTuple;
context->cpUpdateRetrySlot = NULL;
@@ -2889,7 +2891,7 @@ lmerge_matched:
break;
case CMD_DELETE:
- context->relaction = relaction;
+ mtstate->mt_merge_action = relaction;
if (!ExecDeletePrologue(context, resultRelInfo, tupleid,
NULL, NULL))
{
@@ -2949,7 +2951,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:
{
@@ -3016,13 +3019,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
@@ -3057,7 +3066,8 @@ lmerge_matched:
* tuple already deleted; tell caller to run NOT
* MATCHED actions
*/
- return false;
+ *matched = false;
+ return NULL;
case TM_SelfModified:
@@ -3077,13 +3087,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;
}
}
@@ -3095,6 +3106,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.
@@ -3105,19 +3140,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;
/*
@@ -3167,10 +3205,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:
@@ -3186,6 +3224,8 @@ ExecMergeNotMatched(ModifyTableContext *
*/
break;
}
+
+ return rslot;
}
/*
@@ -3382,6 +3422,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->index);
+
+ PG_RETURN_NULL();
+}
+
+/*
* Process BEFORE EACH STATEMENT triggers
*/
static void
@@ -3667,8 +3765,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");
@@ -3745,8 +3852,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");
@@ -3839,7 +3955,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/executor/spi.c b/src/backend/executor/spi.c
new file mode 100644
index e3a170c..be88056
--- a/src/backend/executor/spi.c
+++ b/src/backend/executor/spi.c
@@ -2033,6 +2033,8 @@ SPI_result_code_string(int code)
return "SPI_OK_TD_REGISTER";
case SPI_OK_MERGE:
return "SPI_OK_MERGE";
+ case SPI_OK_MERGE_RETURNING:
+ return "SPI_OK_MERGE_RETURNING";
}
/* Unrecognized code ... return something useful ... */
sprintf(buf, "Unrecognized SPI code %d", code);
@@ -2886,7 +2888,10 @@ _SPI_pquery(QueryDesc *queryDesc, bool f
res = SPI_OK_UPDATE;
break;
case CMD_MERGE:
- res = SPI_OK_MERGE;
+ if (queryDesc->plannedstmt->hasReturning)
+ res = SPI_OK_MERGE_RETURNING;
+ else
+ res = SPI_OK_MERGE;
break;
default:
return SPI_ERROR_OPUNKNOWN;
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
new file mode 100644
index e892df9..fe0f5cb
--- 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;
@@ -941,7 +941,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;
@@ -2406,7 +2407,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
@@ -2500,10 +2502,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;
@@ -2520,7 +2523,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 4fbf80c..b8d655a
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -456,6 +456,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:
@@ -904,6 +905,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 7ff41ac..92cd8c1
--- 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:
@@ -3002,6 +3004,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..2e50eb0
--- 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.
@@ -254,6 +258,7 @@ transformMergeStmt(ParseState *pstate, M
MergeAction *action;
action = makeNode(MergeAction);
+ action->index = foreach_current_index(l) + 1;
action->commandType = mergeWhenClause->commandType;
action->matched = mergeWhenClause->matched;
@@ -390,9 +395,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 de355dd..97ca651
--- a/src/backend/parser/parse_relation.c
+++ b/src/backend/parser/parse_relation.c
@@ -2345,9 +2345,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 b6452e9..e753620
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -3625,7 +3625,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 e2a7642..50c9b59
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -11935,4 +11935,14 @@
proname => 'any_value_transfn', prorettype => 'anyelement',
proargtypes => 'anyelement anyelement', prosrc => 'any_value_transfn' },
+# 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/executor/spi.h b/src/include/executor/spi.h
new file mode 100644
index d1de139..ed8787d
--- a/src/include/executor/spi.h
+++ b/src/include/executor/spi.h
@@ -97,6 +97,7 @@ typedef struct _SPI_plan *SPIPlanPtr;
#define SPI_OK_REL_UNREGISTER 16
#define SPI_OK_TD_REGISTER 17
#define SPI_OK_MERGE 18
+#define SPI_OK_MERGE_RETURNING 19
#define SPI_OPT_NONATOMIC (1 << 0)
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
new file mode 100644
index 20f4c8b..3d62fb9
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1302,6 +1302,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 f7d7f10..a959ac7
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -1687,6 +1687,7 @@ typedef struct MergeWhenClause
typedef struct MergeAction
{
NodeTag type;
+ int index; /* 1-based index of the clause */
bool matched; /* true=MATCHED, false=NOT MATCHED */
CmdType commandType; /* INSERT/UPDATE/DELETE/DO NOTHING */
/* OVERRIDING clause */
@@ -1806,6 +1807,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 f589112..836819d
--- 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/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
new file mode 100644
index ffd6d2e..502056d
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -4246,9 +4246,9 @@ exec_stmt_execsql(PLpgSQL_execstate *est
/*
* If we have INTO, then we only need one row back ... but if we have INTO
* STRICT or extra check too_many_rows, ask for two rows, so that we can
- * verify the statement returns only one. INSERT/UPDATE/DELETE are always
- * treated strictly. Without INTO, just run the statement to completion
- * (tcount = 0).
+ * verify the statement returns only one. INSERT/UPDATE/DELETE/MERGE are
+ * always treated strictly. Without INTO, just run the statement to
+ * completion (tcount = 0).
*
* We could just ask for two rows always when using INTO, but there are
* some cases where demanding the extra row costs significant time, eg by
@@ -4286,10 +4286,11 @@ exec_stmt_execsql(PLpgSQL_execstate *est
case SPI_OK_INSERT:
case SPI_OK_UPDATE:
case SPI_OK_DELETE:
+ case SPI_OK_MERGE:
case SPI_OK_INSERT_RETURNING:
case SPI_OK_UPDATE_RETURNING:
case SPI_OK_DELETE_RETURNING:
- case SPI_OK_MERGE:
+ case SPI_OK_MERGE_RETURNING:
Assert(stmt->mod_stmt);
exec_set_found(estate, (SPI_processed != 0));
break;
@@ -4468,10 +4469,11 @@ exec_stmt_dynexecute(PLpgSQL_execstate *
case SPI_OK_INSERT:
case SPI_OK_UPDATE:
case SPI_OK_DELETE:
+ case SPI_OK_MERGE:
case SPI_OK_INSERT_RETURNING:
case SPI_OK_UPDATE_RETURNING:
case SPI_OK_DELETE_RETURNING:
- case SPI_OK_MERGE:
+ case SPI_OK_MERGE_RETURNING:
case SPI_OK_UTILITY:
case SPI_OK_REWRITTEN:
break;
diff --git a/src/pl/tcl/pltcl.c b/src/pl/tcl/pltcl.c
new file mode 100644
index 499a9ea..58c4f50
--- a/src/pl/tcl/pltcl.c
+++ b/src/pl/tcl/pltcl.c
@@ -2459,6 +2459,7 @@ pltcl_process_SPI_result(Tcl_Interp *int
case SPI_OK_INSERT_RETURNING:
case SPI_OK_DELETE_RETURNING:
case SPI_OK_UPDATE_RETURNING:
+ case SPI_OK_MERGE_RETURNING:
/*
* Process the tuples we got
diff --git a/src/test/regress/expected/merge.out b/src/test/regress/expected/merge.out
new file mode 100644
index e1d3639..ef6dd55
--- 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,20 +1289,187 @@ 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)
+ 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 *;
- ^
+ DELETE
+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...
+ ^
+-- RETURNING in CTEs
+CREATE TABLE sq_target_merge_log (tid integer NOT NULL, last_change text);
+INSERT INTO sq_target_merge_log VALUES (1, 'Original value');
+BEGIN;
+WITH m AS (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 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
+), m2 AS (
+ MERGE INTO sq_target_merge_log l
+ USING m
+ ON l.tid = m.tid
+ WHEN MATCHED THEN
+ UPDATE SET last_change = description
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (m.tid, description)
+ RETURNING merge_action, pg_merge_action() AS merge_log_action, l.*
+)
+SELECT * FROM m2;
+ merge_action | merge_log_action | tid | last_change
+--------------+------------------+-----+---------------------
+ DELETE | UPDATE | 1 | Removed (1,100)
+ UPDATE | INSERT | 2 | Added 20 to balance
+ INSERT | INSERT | 4 | Inserted (4,40)
+(3 rows)
+
+SELECT * FROM sq_target_merge_log ORDER BY tid;
+ tid | last_change
+-----+---------------------
+ 1 | Removed (1,100)
+ 2 | Added 20 to balance
+ 4 | Inserted (4,40)
+(3 rows)
+
+ROLLBACK;
+-- COPY (MERGE ... RETURNING) TO ...
+BEGIN;
+COPY (
+ MERGE INTO sq_target t
+ USING v
+ ON tid = sid
+ 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 pg_merge_action(), t.*
+) TO stdout;
+DELETE 1 100
+ROLLBACK;
+-- SQL function with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT action text, OUT tid int, OUT new_balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid > 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING pg_merge_action(), t.*;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT action, tid, new_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ action | tid | new_balance
+--------+-----+-------------
+ DELETE | 1 | 100
+ UPDATE | 3 | 320
+ INSERT | 4 | 110
+(3 rows)
+
+ROLLBACK;
+-- SQL SRF with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_sq_source_into_sq_target()
+RETURNS TABLE (action text, tid int, balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 pg_merge_action(), t.*;
+$$;
+SELECT * FROM merge_sq_source_into_sq_target();
+ action | tid | balance
+--------+-----+---------
+ DELETE | 1 | 100
+ INSERT | 4 | 40
+(2 rows)
+
+ROLLBACK;
+-- PL/pgSQL function with MERGE ... RETURNING ... INTO
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT r_action text, OUT r_tid int, OUT r_balance int)
+LANGUAGE plpgsql AS
+$$
+BEGIN
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid > 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING pg_merge_action(), t.* INTO r_action, r_tid, r_balance;
+END;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT r_action, r_tid, r_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ r_action | r_tid | r_balance
+----------+-------+-----------
+ DELETE | 1 | 100
+ UPDATE | 3 | 320
+ INSERT | 4 | 110
+(3 rows)
+
ROLLBACK;
-- EXPLAIN
CREATE TABLE ex_mtarget (a int, b int)
@@ -1499,7 +1666,7 @@ SELECT * FROM sq_target WHERE tid = 1;
(1 row)
ROLLBACK;
-DROP TABLE sq_target, sq_source CASCADE;
+DROP TABLE sq_target, sq_target_merge_log, sq_source CASCADE;
NOTICE: drop cascades to view v
CREATE TABLE pa_target (tid integer, balance float, val text)
PARTITION BY LIST (tid);
@@ -1625,6 +1792,32 @@ SELECT * FROM pa_target ORDER BY tid;
(14 rows)
ROLLBACK;
+-- update partition key to partition not initially scanned
+BEGIN;
+MERGE INTO pa_target t
+ USING pa_source s
+ ON t.tid = s.sid AND t.tid = 1
+ WHEN MATCHED THEN
+ UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
+ RETURNING pg_merge_action(), pg_merge_when_clause(), t.*;
+ pg_merge_action | pg_merge_when_clause | tid | balance | val
+-----------------+----------------------+-----+---------+--------------------------
+ UPDATE | 1 | 2 | 110 | initial updated by merge
+(1 row)
+
+SELECT * FROM pa_target ORDER BY tid;
+ tid | balance | val
+-----+---------+--------------------------
+ 2 | 110 | initial updated by merge
+ 3 | 300 | initial
+ 5 | 500 | initial
+ 7 | 700 | initial
+ 9 | 900 | initial
+ 11 | 1100 | initial
+ 13 | 1300 | initial
+(7 rows)
+
+ROLLBACK;
DROP TABLE pa_target CASCADE;
-- The target table is partitioned in the same way, but this time by attaching
-- partitions which have columns in different order, dropped columns etc.
@@ -1716,7 +1909,26 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (sid, delta, 'inserted by merge');
+ INSERT VALUES (sid, delta, 'inserted by merge')
+ RETURNING pg_merge_action(), pg_merge_when_clause(), t.*;
+ pg_merge_action | pg_merge_when_clause | tid | balance | val
+-----------------+----------------------+-----+---------+--------------------------
+ UPDATE | 1 | 2 | 110 | initial updated by merge
+ INSERT | 2 | 2 | 20 | inserted by merge
+ UPDATE | 1 | 4 | 330 | initial updated by merge
+ INSERT | 2 | 4 | 40 | inserted by merge
+ UPDATE | 1 | 6 | 550 | initial updated by merge
+ INSERT | 2 | 6 | 60 | inserted by merge
+ UPDATE | 1 | 8 | 770 | initial updated by merge
+ INSERT | 2 | 8 | 80 | inserted by merge
+ UPDATE | 1 | 10 | 990 | initial updated by merge
+ INSERT | 2 | 10 | 100 | inserted by merge
+ UPDATE | 1 | 12 | 1210 | initial updated by merge
+ INSERT | 2 | 12 | 120 | inserted by merge
+ UPDATE | 1 | 14 | 1430 | initial updated by merge
+ INSERT | 2 | 14 | 140 | inserted by merge
+(14 rows)
+
SELECT * FROM pa_target ORDER BY tid;
tid | balance | val
-----+---------+--------------------------
@@ -1783,7 +1995,21 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge');
+ INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge')
+ RETURNING pg_merge_action(), pg_merge_when_clause(), t.*;
+ pg_merge_action | pg_merge_when_clause | logts | tid | balance | val
+-----------------+----------------------+--------------------------+-----+---------+--------------------------
+ UPDATE | 1 | Tue Jan 31 00:00:00 2017 | 1 | 110 | initial updated by merge
+ UPDATE | 1 | Tue Feb 28 00:00:00 2017 | 2 | 220 | initial updated by merge
+ INSERT | 2 | Sun Jan 15 00:00:00 2017 | 3 | 30 | inserted by merge
+ UPDATE | 1 | Tue Jan 31 00:00:00 2017 | 4 | 440 | initial updated by merge
+ UPDATE | 1 | Tue Feb 28 00:00:00 2017 | 5 | 550 | initial updated by merge
+ INSERT | 2 | Sun Jan 15 00:00:00 2017 | 6 | 60 | inserted by merge
+ UPDATE | 1 | Tue Jan 31 00:00:00 2017 | 7 | 770 | initial updated by merge
+ UPDATE | 1 | Tue Feb 28 00:00:00 2017 | 8 | 880 | initial updated by merge
+ INSERT | 2 | Sun Jan 15 00:00:00 2017 | 9 | 90 | inserted by merge
+(9 rows)
+
SELECT * FROM pa_target ORDER BY tid;
logts | tid | balance | val
--------------------------+-----+---------+--------------------------
diff --git a/src/test/regress/sql/merge.sql b/src/test/regress/sql/merge.sql
new file mode 100644
index f085416..dba6a19
--- 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,17 +844,145 @@ 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)
+ INSERT (balance, tid) VALUES (balance + delta, sid)
WHEN MATCHED AND tid < 2 THEN
- DELETE
-RETURNING *;
+ DELETE
+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();
+
+-- RETURNING in CTEs
+CREATE TABLE sq_target_merge_log (tid integer NOT NULL, last_change text);
+INSERT INTO sq_target_merge_log VALUES (1, 'Original value');
+BEGIN;
+WITH m AS (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 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
+), m2 AS (
+ MERGE INTO sq_target_merge_log l
+ USING m
+ ON l.tid = m.tid
+ WHEN MATCHED THEN
+ UPDATE SET last_change = description
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (m.tid, description)
+ RETURNING merge_action, pg_merge_action() AS merge_log_action, l.*
+)
+SELECT * FROM m2;
+SELECT * FROM sq_target_merge_log ORDER BY tid;
+ROLLBACK;
+
+-- COPY (MERGE ... RETURNING) TO ...
+BEGIN;
+COPY (
+ MERGE INTO sq_target t
+ USING v
+ ON tid = sid
+ 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 pg_merge_action(), t.*
+) TO stdout;
+ROLLBACK;
+
+-- SQL function with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT action text, OUT tid int, OUT new_balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid > 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING pg_merge_action(), t.*;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT action, tid, new_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ROLLBACK;
+
+-- SQL SRF with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_sq_source_into_sq_target()
+RETURNS TABLE (action text, tid int, balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 pg_merge_action(), t.*;
+$$;
+SELECT * FROM merge_sq_source_into_sq_target();
+ROLLBACK;
+
+-- PL/pgSQL function with MERGE ... RETURNING ... INTO
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT r_action text, OUT r_tid int, OUT r_balance int)
+LANGUAGE plpgsql AS
+$$
+BEGIN
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid > 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING pg_merge_action(), t.* INTO r_action, r_tid, r_balance;
+END;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT r_action, r_tid, r_balance FROM merge_into_sq_target(sid, balance, delta)) m;
ROLLBACK;
-- EXPLAIN
@@ -953,7 +1081,7 @@ WHEN MATCHED THEN
SELECT * FROM sq_target WHERE tid = 1;
ROLLBACK;
-DROP TABLE sq_target, sq_source CASCADE;
+DROP TABLE sq_target, sq_target_merge_log, sq_source CASCADE;
CREATE TABLE pa_target (tid integer, balance float, val text)
PARTITION BY LIST (tid);
@@ -1020,6 +1148,17 @@ SELECT merge_func();
SELECT * FROM pa_target ORDER BY tid;
ROLLBACK;
+-- update partition key to partition not initially scanned
+BEGIN;
+MERGE INTO pa_target t
+ USING pa_source s
+ ON t.tid = s.sid AND t.tid = 1
+ WHEN MATCHED THEN
+ UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
+ RETURNING pg_merge_action(), pg_merge_when_clause(), t.*;
+SELECT * FROM pa_target ORDER BY tid;
+ROLLBACK;
+
DROP TABLE pa_target CASCADE;
-- The target table is partitioned in the same way, but this time by attaching
@@ -1078,7 +1217,8 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (sid, delta, 'inserted by merge');
+ INSERT VALUES (sid, delta, 'inserted by merge')
+ RETURNING pg_merge_action(), pg_merge_when_clause(), t.*;
SELECT * FROM pa_target ORDER BY tid;
ROLLBACK;
@@ -1132,7 +1272,8 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge');
+ INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge')
+ RETURNING pg_merge_action(), pg_merge_when_clause(), t.*;
SELECT * FROM pa_target ORDER BY tid;
ROLLBACK;
On Fri, 24 Feb 2023 at 05:46, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
Rebased version attached.
Another rebase.
Regards,
Dean
Attachments:
support-merge-returning-v5.patchtext/x-patch; charset=US-ASCII; name=support-merge-returning-v5.patchDownload
diff --git a/doc/src/sgml/dml.sgml b/doc/src/sgml/dml.sgml
new file mode 100644
index cbbc5e2..ff2a827
--- a/doc/src/sgml/dml.sgml
+++ b/doc/src/sgml/dml.sgml
@@ -283,10 +283,15 @@ DELETE FROM products;
<secondary>RETURNING</secondary>
</indexterm>
+ <indexterm zone="dml-returning">
+ <primary>MERGE</primary>
+ <secondary>RETURNING</secondary>
+ </indexterm>
+
<para>
Sometimes it is useful to obtain data from modified rows while they are
being manipulated. The <command>INSERT</command>, <command>UPDATE</command>,
- and <command>DELETE</command> commands all have an
+ <command>DELETE</command>, and <command>MERGE</command> commands all have an
optional <literal>RETURNING</literal> clause that supports this. Use
of <literal>RETURNING</literal> avoids performing an extra database query to
collect the data, and is especially valuable when it would otherwise be
@@ -339,6 +344,24 @@ DELETE FROM products
</programlisting>
</para>
+ <para>
+ In a <command>MERGE</command>, the data available to <literal>RETURNING</literal> is
+ the content of the source row plus the content of the inserted, updated, or
+ deleted target row. In addition, the merge support functions
+ <xref linkend="pg-merge-action"/> and <xref linkend="pg-merge-when-clause"/>
+ may be used to return information about which merge action was executed for
+ each row. Since it is quite common for the source and target to have many
+ of the same columns, specifying <literal>RETURNING *</literal> can lead to a
+ lot of duplicated columns, so it is often more useful to just return the
+ target row. For example:
+<programlisting>
+MERGE INTO products p USING new_products n ON p.product_no = n.product_no
+ WHEN NOT MATCHED THEN INSERT VALUES (n.product_no, n.name, n.price)
+ WHEN MATCHED THEN UPDATE SET name = n.name, price = n.price
+ RETURNING pg_merge_action(), p.*;
+</programlisting>
+ </para>
+
<para>
If there are triggers (<xref linkend="triggers"/>) on the target table,
the data available to <literal>RETURNING</literal> is the row as modified by
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
new file mode 100644
index 0cbdf63..224e9b8
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -21216,6 +21216,99 @@ SELECT count(*) FROM sometable;
</sect1>
+ <sect1 id="functions-merge-support">
+ <title>Merge Support Functions</title>
+
+ <indexterm>
+ <primary>MERGE</primary>
+ <secondary>RETURNING</secondary>
+ </indexterm>
+
+ <para>
+ The merge support functions shown in
+ <xref linkend="functions-merge-support-table"/> may be used in the
+ <literal>RETURNING</literal> list of a <xref linkend="sql-merge"/> command
+ to return additional information about the action taken for each row.
+ </para>
+
+ <table id="functions-merge-support-table">
+ <title>Merge Support Functions</title>
+
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ Function
+ </para>
+ <para>
+ Description
+ </para></entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm id="pg-merge-action">
+ <primary>pg_merge_action</primary>
+ </indexterm>
+ <function>pg_merge_action</function> ( )
+ <returnvalue>text</returnvalue>
+ </para>
+ <para>
+ Returns the action performed on the current row (<literal>'INSERT'</literal>,
+ <literal>'UPDATE'</literal>, or <literal>'DELETE'</literal>).
+ </para></entry>
+ </row>
+
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm id="pg-merge-when-clause">
+ <primary>pg_merge_when_clause</primary>
+ </indexterm>
+ <function>pg_merge_when_clause</function> ( )
+ <returnvalue>integer</returnvalue>
+ </para>
+ <para>
+ Returns the 1-based index of the <literal>WHEN</literal> clause executed
+ for the current row.
+ </para></entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+
+ <para>
+ Example:
+<screen><![CDATA[
+MERGE INTO products p
+ USING stock s ON p.product_id = s.product_id
+ WHEN MATCHED AND s.quantity > 0 THEN
+ UPDATE SET in_stock = true, quantity = s.quantity
+ WHEN MATCHED THEN
+ UPDATE SET in_stock = false, quantity = 0
+ WHEN NOT MATCHED THEN
+ INSERT (product_id, in_stock, quantity)
+ VALUES (s.product_id, true, s.quantity)
+ RETURNING pg_merge_action() AS action,
+ pg_merge_when_clause() AS when_clause,
+ p.*;
+
+ action | when_clause | product_id | in_stock | quantity
+--------+-------------+------------+----------+----------
+ UPDATE | 1 | 1001 | t | 50
+ UPDATE | 2 | 1002 | f | 0
+ INSERT | 3 | 1003 | t | 10
+]]></screen>
+ </para>
+
+ <para>
+ Note that it is an error to use these functions anywhere other than the
+ <literal>RETURNING</literal> list of a <command>MERGE</command> command.
+ </para>
+
+ </sect1>
+
<sect1 id="functions-subquery">
<title>Subquery Expressions</title>
diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml
new file mode 100644
index 7c01a54..4317cfc
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -1323,9 +1323,9 @@
to a <glossterm linkend="glossary-client">client</glossterm> upon the
completion of an <acronym>SQL</acronym> command, usually a
<command>SELECT</command> but it can be an
- <command>INSERT</command>, <command>UPDATE</command>, or
- <command>DELETE</command> command if the <literal>RETURNING</literal>
- clause is specified.
+ <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command> command if the
+ <literal>RETURNING</literal> clause is specified.
</para>
<para>
The fact that a result set is a relation means that a query can be used
diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml
new file mode 100644
index 7c8a49f..b839695
--- a/doc/src/sgml/plpgsql.sgml
+++ b/doc/src/sgml/plpgsql.sgml
@@ -1020,8 +1020,8 @@ INSERT INTO mytable VALUES (1,'one'), (2
</para>
<para>
- If the command does return rows (for example <command>SELECT</command>,
- or <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>
+ If the command does return rows (for example <command>SELECT</command>, or
+ <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command>
with <literal>RETURNING</literal>), there are two ways to proceed.
When the command will return at most one row, or you only care about
the first row of output, write the command as usual but add
@@ -1149,6 +1149,7 @@ SELECT <replaceable>select_expressions</
INSERT ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
UPDATE ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
DELETE ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
+MERGE ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
</synopsis>
where <replaceable>target</replaceable> can be a record variable, a row
@@ -1159,8 +1160,8 @@ DELETE ... RETURNING <replaceable>expres
<literal>INTO</literal> clause) just as described above,
and the plan is cached in the same way.
This works for <command>SELECT</command>,
- <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command> with
- <literal>RETURNING</literal>, and certain utility commands
+ <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command>
+ with <literal>RETURNING</literal>, and certain utility commands
that return row sets, such as <command>EXPLAIN</command>.
Except for the <literal>INTO</literal> clause, the SQL command is the same
as it would be written outside <application>PL/pgSQL</application>.
@@ -1236,7 +1237,7 @@ END;
</para>
<para>
- For <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command> with
+ For <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command> with
<literal>RETURNING</literal>, <application>PL/pgSQL</application> reports
an error for more than one returned row, even when
<literal>STRICT</literal> is not specified. This is because there
diff --git a/doc/src/sgml/queries.sgml b/doc/src/sgml/queries.sgml
new file mode 100644
index 6986ec5..9e9687b
--- a/doc/src/sgml/queries.sgml
+++ b/doc/src/sgml/queries.sgml
@@ -2062,9 +2062,10 @@ SELECT <replaceable>select_list</replace
Table Expressions or <acronym>CTE</acronym>s, can be thought of as defining
temporary tables that exist just for one query. Each auxiliary statement
in a <literal>WITH</literal> clause can be a <command>SELECT</command>,
- <command>INSERT</command>, <command>UPDATE</command>, or <command>DELETE</command>; and the
+ <command>INSERT</command>, <command>UPDATE</command>, <command>DELETE</command>,
+ or <command>MERGE</command>; and the
<literal>WITH</literal> clause itself is attached to a primary statement that can
- be a <command>SELECT</command>, <command>INSERT</command>, <command>UPDATE</command>,
+ also be a <command>SELECT</command>, <command>INSERT</command>, <command>UPDATE</command>,
<command>DELETE</command>, or <command>MERGE</command>.
</para>
@@ -2598,8 +2599,8 @@ SELECT * FROM w AS w1 JOIN w AS w2 ON w1
<title>Data-Modifying Statements in <literal>WITH</literal></title>
<para>
- You can use most data-modifying statements (<command>INSERT</command>,
- <command>UPDATE</command>, or <command>DELETE</command>, but not
+ You can use data-modifying statements (<command>INSERT</command>,
+ <command>UPDATE</command>, <command>DELETE</command>, or
<command>MERGE</command>) in <literal>WITH</literal>. This
allows you to perform several different operations in the same query.
An example is:
diff --git a/doc/src/sgml/ref/copy.sgml b/doc/src/sgml/ref/copy.sgml
new file mode 100644
index c25b52d..d822470
--- a/doc/src/sgml/ref/copy.sgml
+++ b/doc/src/sgml/ref/copy.sgml
@@ -121,13 +121,15 @@ COPY { <replaceable class="parameter">ta
A <link linkend="sql-select"><command>SELECT</command></link>,
<link linkend="sql-values"><command>VALUES</command></link>,
<link linkend="sql-insert"><command>INSERT</command></link>,
- <link linkend="sql-update"><command>UPDATE</command></link>, or
- <link linkend="sql-delete"><command>DELETE</command></link> command whose results are to be
+ <link linkend="sql-update"><command>UPDATE</command></link>,
+ <link linkend="sql-update"><command>DELETE</command></link>, or
+ <link linkend="sql-delete"><command>MERGE</command></link> command whose results are to be
copied. Note that parentheses are required around the query.
</para>
<para>
- For <command>INSERT</command>, <command>UPDATE</command> and
- <command>DELETE</command> queries a RETURNING clause must be provided,
+ For <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, and <command>MERGE</command> queries a
+ RETURNING clause must be provided,
and the target relation must not have a conditional rule, nor
an <literal>ALSO</literal> rule, nor an <literal>INSTEAD</literal> rule
that expands to multiple statements.
diff --git a/doc/src/sgml/ref/merge.sgml b/doc/src/sgml/ref/merge.sgml
new file mode 100644
index 0995fe0..7eb641a
--- a/doc/src/sgml/ref/merge.sgml
+++ b/doc/src/sgml/ref/merge.sgml
@@ -25,6 +25,7 @@ PostgreSQL documentation
MERGE INTO [ ONLY ] <replaceable class="parameter">target_table_name</replaceable> [ * ] [ [ AS ] <replaceable class="parameter">target_alias</replaceable> ]
USING <replaceable class="parameter">data_source</replaceable> ON <replaceable class="parameter">join_condition</replaceable>
<replaceable class="parameter">when_clause</replaceable> [...]
+[ RETURNING * | <replaceable class="parameter">output_expression</replaceable> [ [ AS ] <replaceable class="parameter">output_name</replaceable> ] [, ...] ]
<phrase>where <replaceable class="parameter">data_source</replaceable> is:</phrase>
@@ -95,6 +96,20 @@ DELETE
</para>
<para>
+ The optional <literal>RETURNING</literal> clause causes <command>MERGE</command>
+ to compute and return value(s) based on each row inserted, updated, or
+ deleted. Any expression using the source or target table's columns, or the
+ merge support functions <xref linkend="pg-merge-action"/> and
+ <xref linkend="pg-merge-when-clause"/> can be computed. When an
+ <command>INSERT</command> or <command>UPDATE</command> action is performed,
+ the new values of the target table's columns are used. When a
+ <command>DELETE</command> is performed, the old values of the target table's
+ columns are used. The syntax of the <literal>RETURNING</literal> list is
+ identical to that of the output list of <command>SELECT</command>.
+
+ </para>
+
+ <para>
There is no separate <literal>MERGE</literal> privilege.
If you specify an update action, you must have the
<literal>UPDATE</literal> privilege on the column(s)
@@ -125,6 +140,18 @@ DELETE
<variablelist>
<varlistentry>
+ <term><replaceable class="parameter">with_query</replaceable></term>
+ <listitem>
+ <para>
+ The <literal>WITH</literal> clause allows you to specify one or more
+ subqueries that can be referenced by name in the <command>MERGE</command>
+ query. See <xref linkend="queries-with"/> and <xref linkend="sql-select"/>
+ for details.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
<term><replaceable class="parameter">target_table_name</replaceable></term>
<listitem>
<para>
@@ -399,13 +426,30 @@ DELETE
</varlistentry>
<varlistentry>
- <term><replaceable class="parameter">with_query</replaceable></term>
+ <term><replaceable class="parameter">output_expression</replaceable></term>
<listitem>
<para>
- The <literal>WITH</literal> clause allows you to specify one or more
- subqueries that can be referenced by name in the <command>MERGE</command>
- query. See <xref linkend="queries-with"/> and <xref linkend="sql-select"/>
- for details.
+ An expression to be computed and returned by the <command>MERGE</command>
+ command after each row is merged. The expression can use any columns of
+ the source or target tables, or the merge support functions
+ <xref linkend="pg-merge-action"/> and <xref linkend="pg-merge-when-clause"/>.
+ </para>
+ <para>
+ Writing <literal>*</literal> will return all columns from the source
+ table, followed by all columns from the target table. Often this will
+ lead to a lot of duplication, since it is common for the source and
+ target tables to have a lot of the same columns. This can be avoided by
+ qualifying the <literal>*</literal> with the name or alias of the source
+ or target table.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><replaceable class="parameter">output_name</replaceable></term>
+ <listitem>
+ <para>
+ A name to use for a returned column.
</para>
</listitem>
</varlistentry>
@@ -428,6 +472,13 @@ MERGE <replaceable class="parameter">tot
were changed in any way.
</para>
+ <para>
+ If the <command>MERGE</command> command contains a <literal>RETURNING</literal>
+ clause, the result will be similar to that of a <command>SELECT</command>
+ statement containing the columns and values defined in the
+ <literal>RETURNING</literal> list, computed over the row(s) inserted, updated,
+ or deleted by the command.
+ </para>
</refsect1>
<refsect1>
@@ -544,13 +595,6 @@ MERGE <replaceable class="parameter">tot
</para>
<para>
- There is no <literal>RETURNING</literal> clause with
- <command>MERGE</command>. Actions of <command>INSERT</command>,
- <command>UPDATE</command> and <command>DELETE</command> cannot contain
- <literal>RETURNING</literal> or <literal>WITH</literal> clauses.
- </para>
-
- <para>
When <command>MERGE</command> is run concurrently with other commands
that modify the target table, the usual transaction isolation rules
apply; see <xref linkend="transaction-iso"/> for an explanation
@@ -602,7 +646,8 @@ WHEN NOT MATCHED THEN
<para>
Attempt to insert a new stock item along with the quantity of stock. If
the item already exists, instead update the stock count of the existing
- item. Don't allow entries that have zero stock.
+ item. Don't allow entries that have zero stock. Return details of all
+ changes made.
<programlisting>
MERGE INTO wines w
USING wine_stock_changes s
@@ -612,7 +657,8 @@ WHEN NOT MATCHED AND s.stock_delta > 0 T
WHEN MATCHED AND w.stock + s.stock_delta > 0 THEN
UPDATE SET stock = w.stock + s.stock_delta
WHEN MATCHED THEN
- DELETE;
+ DELETE
+RETURNING pg_merge_action(), w.*;
</programlisting>
The <literal>wine_stock_changes</literal> table might be, for example, a
@@ -627,7 +673,8 @@ WHEN MATCHED THEN
This command conforms to the <acronym>SQL</acronym> standard.
</para>
<para>
- The WITH clause and <literal>DO NOTHING</literal> action are extensions to
+ The WITH clause, <literal>DO NOTHING</literal> action, and
+ <literal>RETURNING</literal> clause are extensions to
the <acronym>SQL</acronym> standard.
</para>
</refsect1>
diff --git a/doc/src/sgml/ref/select.sgml b/doc/src/sgml/ref/select.sgml
new file mode 100644
index 1f9538f..5840ddc
--- a/doc/src/sgml/ref/select.sgml
+++ b/doc/src/sgml/ref/select.sgml
@@ -74,7 +74,7 @@ SELECT [ ALL | DISTINCT [ ON ( <replacea
<phrase>and <replaceable class="parameter">with_query</replaceable> is:</phrase>
- <replaceable class="parameter">with_query_name</replaceable> [ ( <replaceable class="parameter">column_name</replaceable> [, ...] ) ] AS [ [ NOT ] MATERIALIZED ] ( <replaceable class="parameter">select</replaceable> | <replaceable class="parameter">values</replaceable> | <replaceable class="parameter">insert</replaceable> | <replaceable class="parameter">update</replaceable> | <replaceable class="parameter">delete</replaceable> )
+ <replaceable class="parameter">with_query_name</replaceable> [ ( <replaceable class="parameter">column_name</replaceable> [, ...] ) ] AS [ [ NOT ] MATERIALIZED ] ( <replaceable class="parameter">select</replaceable> | <replaceable class="parameter">values</replaceable> | <replaceable class="parameter">insert</replaceable> | <replaceable class="parameter">update</replaceable> | <replaceable class="parameter">delete</replaceable> | <replaceable class="parameter">merge</replaceable> )
[ SEARCH { BREADTH | DEPTH } FIRST BY <replaceable>column_name</replaceable> [, ...] SET <replaceable>search_seq_col_name</replaceable> ]
[ CYCLE <replaceable>column_name</replaceable> [, ...] SET <replaceable>cycle_mark_col_name</replaceable> [ TO <replaceable>cycle_mark_value</replaceable> DEFAULT <replaceable>cycle_mark_default</replaceable> ] USING <replaceable>cycle_path_col_name</replaceable> ]
@@ -227,10 +227,10 @@ TABLE [ ONLY ] <replaceable class="param
The subqueries effectively act as temporary tables or views
for the duration of the primary query.
Each subquery can be a <command>SELECT</command>, <command>TABLE</command>, <command>VALUES</command>,
- <command>INSERT</command>, <command>UPDATE</command> or
- <command>DELETE</command> statement.
+ <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command> statement.
When writing a data-modifying statement (<command>INSERT</command>,
- <command>UPDATE</command> or <command>DELETE</command>) in
+ <command>UPDATE</command>, <command>DELETE</command>, or <command>MERGE</command>) in
<literal>WITH</literal>, it is usual to include a <literal>RETURNING</literal> clause.
It is the output of <literal>RETURNING</literal>, <emphasis>not</emphasis> the underlying
table that the statement modifies, that forms the temporary table that is
@@ -2179,7 +2179,8 @@ SELECT 2+2;
<para>
<productname>PostgreSQL</productname> allows <command>INSERT</command>,
- <command>UPDATE</command>, and <command>DELETE</command> to be used as <literal>WITH</literal>
+ <command>UPDATE</command>, <command>DELETE</command>, and
+ <command>MERGE</command> to be used as <literal>WITH</literal>
queries. This is not found in the SQL standard.
</para>
</refsect2>
diff --git a/doc/src/sgml/rowtypes.sgml b/doc/src/sgml/rowtypes.sgml
new file mode 100644
index 4d86f97..bbeac84
--- a/doc/src/sgml/rowtypes.sgml
+++ b/doc/src/sgml/rowtypes.sgml
@@ -348,7 +348,7 @@ SELECT m.* FROM some_table, LATERAL myfu
column expansion of this kind when it appears at the top level of
a <link linkend="queries-select-lists"><command>SELECT</command> output
list</link>, a <link linkend="dml-returning"><literal>RETURNING</literal>
- list</link> in <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>,
+ list</link> in <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command>,
a <link linkend="queries-values"><literal>VALUES</literal> clause</link>, or
a <link linkend="sql-syntax-row-constructors">row constructor</link>.
In all other contexts (including when nested inside one of those
diff --git a/doc/src/sgml/spi.sgml b/doc/src/sgml/spi.sgml
new file mode 100644
index 651930a..ad0a9b1
--- a/doc/src/sgml/spi.sgml
+++ b/doc/src/sgml/spi.sgml
@@ -301,8 +301,9 @@ SPI_execute("INSERT INTO foo SELECT * FR
is returned in the global variable <varname>SPI_processed</varname>.
If the return value of the function is <symbol>SPI_OK_SELECT</symbol>,
<symbol>SPI_OK_INSERT_RETURNING</symbol>,
- <symbol>SPI_OK_DELETE_RETURNING</symbol>, or
- <symbol>SPI_OK_UPDATE_RETURNING</symbol>,
+ <symbol>SPI_OK_DELETE_RETURNING</symbol>,
+ <symbol>SPI_OK_UPDATE_RETURNING</symbol>, or
+ <symbol>SPI_OK_MERGE_RETURNING</symbol>
then you can use the
global pointer <literal>SPITupleTable *SPI_tuptable</literal> to
access the result rows. Some utility commands (such as
@@ -472,6 +473,15 @@ typedef struct SPITupleTable
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><symbol>SPI_OK_MERGE_RETURNING</symbol></term>
+ <listitem>
+ <para>
+ if a <command>MERGE RETURNING</command> was executed
+ </para>
+ </listitem>
+ </varlistentry>
<varlistentry>
<term><symbol>SPI_OK_UTILITY</symbol></term>
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
new file mode 100644
index 9620ea9..4f68efb
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -177,7 +177,8 @@
statements separated by semicolons. A semicolon after the last
statement is optional. Unless the function is declared to return
<type>void</type>, the last statement must be a <command>SELECT</command>,
- or an <command>INSERT</command>, <command>UPDATE</command>, or <command>DELETE</command>
+ or an <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command>
that has a <literal>RETURNING</literal> clause.
</para>
@@ -1236,8 +1237,9 @@ SELECT x, CASE WHEN x > 0 THEN genera
<note>
<para>
- If a function's last command is <command>INSERT</command>, <command>UPDATE</command>,
- or <command>DELETE</command> with <literal>RETURNING</literal>, that command will
+ If a function's last command is <command>INSERT</command>,
+ <command>UPDATE</command>, <command>DELETE</command>, or
+ <command>MERGE</command> with <literal>RETURNING</literal>, that command will
always be executed to completion, even if the function is not declared
with <literal>SETOF</literal> or the calling query does not fetch all the
result rows. Any extra rows produced by the <literal>RETURNING</literal>
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 c61f23c..8ce6a4f
--- 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"
@@ -2488,6 +2489,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 fd6ca8a..95bbf67
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -612,8 +612,8 @@ ExecInitPartitionInfo(ModifyTableState *
* Build the RETURNING projection for the partition. Note that we didn't
* build the returningList for partitions within the planner, but simple
* translation of varattnos will suffice. This only occurs for the INSERT
- * case or in the case of UPDATE tuple routing where we didn't find a
- * result rel to reuse.
+ * case or in the case of UPDATE/MERGE tuple routing where we didn't find
+ * a result rel to reuse.
*/
if (node && node->returningLists != NIL)
{
@@ -622,12 +622,14 @@ ExecInitPartitionInfo(ModifyTableState *
List *returningList;
/* See the comment above for WCO lists. */
- /* (except no RETURNING support for MERGE yet) */
Assert((node->operation == CMD_INSERT &&
list_length(node->returningLists) == 1 &&
list_length(node->resultRelations) == 1) ||
(node->operation == CMD_UPDATE &&
list_length(node->returningLists) ==
+ list_length(node->resultRelations)) ||
+ (node->operation == CMD_MERGE &&
+ list_length(node->returningLists) ==
list_length(node->resultRelations)));
/*
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 6f0543a..7e98890
--- 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;
}
}
@@ -2051,7 +2048,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;
/*
@@ -2689,6 +2686,7 @@ static TupleTableSlot *
ExecMerge(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
ItemPointer tupleid, bool canSetTag)
{
+ TupleTableSlot *rslot = NULL;
bool matched;
/*-----
@@ -2736,7 +2734,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()
@@ -2744,10 +2743,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;
}
/*
@@ -2757,8 +2755,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.
*
@@ -2767,15 +2765,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;
@@ -2787,7 +2786,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
@@ -2866,7 +2868,7 @@ lmerge_matched:
*/
newslot = ExecProject(relaction->mas_proj);
- context->relaction = relaction;
+ mtstate->mt_merge_action = relaction;
context->GetUpdateNewTuple = mergeGetUpdateNewTuple;
context->cpUpdateRetrySlot = NULL;
@@ -2889,7 +2891,7 @@ lmerge_matched:
break;
case CMD_DELETE:
- context->relaction = relaction;
+ mtstate->mt_merge_action = relaction;
if (!ExecDeletePrologue(context, resultRelInfo, tupleid,
NULL, NULL))
{
@@ -2949,7 +2951,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:
{
@@ -3016,13 +3019,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
@@ -3057,7 +3066,8 @@ lmerge_matched:
* tuple already deleted; tell caller to run NOT
* MATCHED actions
*/
- return false;
+ *matched = false;
+ return NULL;
case TM_SelfModified:
@@ -3077,13 +3087,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;
}
}
@@ -3095,6 +3106,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.
@@ -3105,19 +3140,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;
/*
@@ -3167,10 +3205,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:
@@ -3186,6 +3224,8 @@ ExecMergeNotMatched(ModifyTableContext *
*/
break;
}
+
+ return rslot;
}
/*
@@ -3382,6 +3422,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->index);
+
+ PG_RETURN_NULL();
+}
+
+/*
* Process BEFORE EACH STATEMENT triggers
*/
static void
@@ -3667,8 +3765,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");
@@ -3745,8 +3852,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");
@@ -3839,7 +3955,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/executor/spi.c b/src/backend/executor/spi.c
new file mode 100644
index e3a170c..be88056
--- a/src/backend/executor/spi.c
+++ b/src/backend/executor/spi.c
@@ -2033,6 +2033,8 @@ SPI_result_code_string(int code)
return "SPI_OK_TD_REGISTER";
case SPI_OK_MERGE:
return "SPI_OK_MERGE";
+ case SPI_OK_MERGE_RETURNING:
+ return "SPI_OK_MERGE_RETURNING";
}
/* Unrecognized code ... return something useful ... */
sprintf(buf, "Unrecognized SPI code %d", code);
@@ -2886,7 +2888,10 @@ _SPI_pquery(QueryDesc *queryDesc, bool f
res = SPI_OK_UPDATE;
break;
case CMD_MERGE:
- res = SPI_OK_MERGE;
+ if (queryDesc->plannedstmt->hasReturning)
+ res = SPI_OK_MERGE_RETURNING;
+ else
+ res = SPI_OK_MERGE;
break;
default:
return SPI_ERROR_OPUNKNOWN;
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
new file mode 100644
index e892df9..fe0f5cb
--- 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;
@@ -941,7 +941,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;
@@ -2406,7 +2407,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
@@ -2500,10 +2502,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;
@@ -2520,7 +2523,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 4fbf80c..b8d655a
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -456,6 +456,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:
@@ -904,6 +905,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 7ff41ac..92cd8c1
--- 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:
@@ -3002,6 +3004,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..2e50eb0
--- 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.
@@ -254,6 +258,7 @@ transformMergeStmt(ParseState *pstate, M
MergeAction *action;
action = makeNode(MergeAction);
+ action->index = foreach_current_index(l) + 1;
action->commandType = mergeWhenClause->commandType;
action->matched = mergeWhenClause->matched;
@@ -390,9 +395,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 de355dd..97ca651
--- a/src/backend/parser/parse_relation.c
+++ b/src/backend/parser/parse_relation.c
@@ -2345,9 +2345,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 a614e3f..b337e15
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -3639,7 +3639,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 e2a7642..50c9b59
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -11935,4 +11935,14 @@
proname => 'any_value_transfn', prorettype => 'anyelement',
proargtypes => 'anyelement anyelement', prosrc => 'any_value_transfn' },
+# 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/executor/spi.h b/src/include/executor/spi.h
new file mode 100644
index d1de139..ed8787d
--- a/src/include/executor/spi.h
+++ b/src/include/executor/spi.h
@@ -97,6 +97,7 @@ typedef struct _SPI_plan *SPIPlanPtr;
#define SPI_OK_REL_UNREGISTER 16
#define SPI_OK_TD_REGISTER 17
#define SPI_OK_MERGE 18
+#define SPI_OK_MERGE_RETURNING 19
#define SPI_OPT_NONATOMIC (1 << 0)
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
new file mode 100644
index 20f4c8b..3d62fb9
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1302,6 +1302,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 f7d7f10..a959ac7
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -1687,6 +1687,7 @@ typedef struct MergeWhenClause
typedef struct MergeAction
{
NodeTag type;
+ int index; /* 1-based index of the clause */
bool matched; /* true=MATCHED, false=NOT MATCHED */
CmdType commandType; /* INSERT/UPDATE/DELETE/DO NOTHING */
/* OVERRIDING clause */
@@ -1806,6 +1807,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 f589112..836819d
--- 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/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
new file mode 100644
index ffd6d2e..502056d
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -4246,9 +4246,9 @@ exec_stmt_execsql(PLpgSQL_execstate *est
/*
* If we have INTO, then we only need one row back ... but if we have INTO
* STRICT or extra check too_many_rows, ask for two rows, so that we can
- * verify the statement returns only one. INSERT/UPDATE/DELETE are always
- * treated strictly. Without INTO, just run the statement to completion
- * (tcount = 0).
+ * verify the statement returns only one. INSERT/UPDATE/DELETE/MERGE are
+ * always treated strictly. Without INTO, just run the statement to
+ * completion (tcount = 0).
*
* We could just ask for two rows always when using INTO, but there are
* some cases where demanding the extra row costs significant time, eg by
@@ -4286,10 +4286,11 @@ exec_stmt_execsql(PLpgSQL_execstate *est
case SPI_OK_INSERT:
case SPI_OK_UPDATE:
case SPI_OK_DELETE:
+ case SPI_OK_MERGE:
case SPI_OK_INSERT_RETURNING:
case SPI_OK_UPDATE_RETURNING:
case SPI_OK_DELETE_RETURNING:
- case SPI_OK_MERGE:
+ case SPI_OK_MERGE_RETURNING:
Assert(stmt->mod_stmt);
exec_set_found(estate, (SPI_processed != 0));
break;
@@ -4468,10 +4469,11 @@ exec_stmt_dynexecute(PLpgSQL_execstate *
case SPI_OK_INSERT:
case SPI_OK_UPDATE:
case SPI_OK_DELETE:
+ case SPI_OK_MERGE:
case SPI_OK_INSERT_RETURNING:
case SPI_OK_UPDATE_RETURNING:
case SPI_OK_DELETE_RETURNING:
- case SPI_OK_MERGE:
+ case SPI_OK_MERGE_RETURNING:
case SPI_OK_UTILITY:
case SPI_OK_REWRITTEN:
break;
diff --git a/src/pl/tcl/pltcl.c b/src/pl/tcl/pltcl.c
new file mode 100644
index 499a9ea..58c4f50
--- a/src/pl/tcl/pltcl.c
+++ b/src/pl/tcl/pltcl.c
@@ -2459,6 +2459,7 @@ pltcl_process_SPI_result(Tcl_Interp *int
case SPI_OK_INSERT_RETURNING:
case SPI_OK_DELETE_RETURNING:
case SPI_OK_UPDATE_RETURNING:
+ case SPI_OK_MERGE_RETURNING:
/*
* Process the tuples we got
diff --git a/src/test/regress/expected/merge.out b/src/test/regress/expected/merge.out
new file mode 100644
index e1d3639..ef6dd55
--- 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,20 +1289,187 @@ 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)
+ 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 *;
- ^
+ DELETE
+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...
+ ^
+-- RETURNING in CTEs
+CREATE TABLE sq_target_merge_log (tid integer NOT NULL, last_change text);
+INSERT INTO sq_target_merge_log VALUES (1, 'Original value');
+BEGIN;
+WITH m AS (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 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
+), m2 AS (
+ MERGE INTO sq_target_merge_log l
+ USING m
+ ON l.tid = m.tid
+ WHEN MATCHED THEN
+ UPDATE SET last_change = description
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (m.tid, description)
+ RETURNING merge_action, pg_merge_action() AS merge_log_action, l.*
+)
+SELECT * FROM m2;
+ merge_action | merge_log_action | tid | last_change
+--------------+------------------+-----+---------------------
+ DELETE | UPDATE | 1 | Removed (1,100)
+ UPDATE | INSERT | 2 | Added 20 to balance
+ INSERT | INSERT | 4 | Inserted (4,40)
+(3 rows)
+
+SELECT * FROM sq_target_merge_log ORDER BY tid;
+ tid | last_change
+-----+---------------------
+ 1 | Removed (1,100)
+ 2 | Added 20 to balance
+ 4 | Inserted (4,40)
+(3 rows)
+
+ROLLBACK;
+-- COPY (MERGE ... RETURNING) TO ...
+BEGIN;
+COPY (
+ MERGE INTO sq_target t
+ USING v
+ ON tid = sid
+ 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 pg_merge_action(), t.*
+) TO stdout;
+DELETE 1 100
+ROLLBACK;
+-- SQL function with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT action text, OUT tid int, OUT new_balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid > 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING pg_merge_action(), t.*;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT action, tid, new_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ action | tid | new_balance
+--------+-----+-------------
+ DELETE | 1 | 100
+ UPDATE | 3 | 320
+ INSERT | 4 | 110
+(3 rows)
+
+ROLLBACK;
+-- SQL SRF with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_sq_source_into_sq_target()
+RETURNS TABLE (action text, tid int, balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 pg_merge_action(), t.*;
+$$;
+SELECT * FROM merge_sq_source_into_sq_target();
+ action | tid | balance
+--------+-----+---------
+ DELETE | 1 | 100
+ INSERT | 4 | 40
+(2 rows)
+
+ROLLBACK;
+-- PL/pgSQL function with MERGE ... RETURNING ... INTO
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT r_action text, OUT r_tid int, OUT r_balance int)
+LANGUAGE plpgsql AS
+$$
+BEGIN
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid > 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING pg_merge_action(), t.* INTO r_action, r_tid, r_balance;
+END;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT r_action, r_tid, r_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ r_action | r_tid | r_balance
+----------+-------+-----------
+ DELETE | 1 | 100
+ UPDATE | 3 | 320
+ INSERT | 4 | 110
+(3 rows)
+
ROLLBACK;
-- EXPLAIN
CREATE TABLE ex_mtarget (a int, b int)
@@ -1499,7 +1666,7 @@ SELECT * FROM sq_target WHERE tid = 1;
(1 row)
ROLLBACK;
-DROP TABLE sq_target, sq_source CASCADE;
+DROP TABLE sq_target, sq_target_merge_log, sq_source CASCADE;
NOTICE: drop cascades to view v
CREATE TABLE pa_target (tid integer, balance float, val text)
PARTITION BY LIST (tid);
@@ -1625,6 +1792,32 @@ SELECT * FROM pa_target ORDER BY tid;
(14 rows)
ROLLBACK;
+-- update partition key to partition not initially scanned
+BEGIN;
+MERGE INTO pa_target t
+ USING pa_source s
+ ON t.tid = s.sid AND t.tid = 1
+ WHEN MATCHED THEN
+ UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
+ RETURNING pg_merge_action(), pg_merge_when_clause(), t.*;
+ pg_merge_action | pg_merge_when_clause | tid | balance | val
+-----------------+----------------------+-----+---------+--------------------------
+ UPDATE | 1 | 2 | 110 | initial updated by merge
+(1 row)
+
+SELECT * FROM pa_target ORDER BY tid;
+ tid | balance | val
+-----+---------+--------------------------
+ 2 | 110 | initial updated by merge
+ 3 | 300 | initial
+ 5 | 500 | initial
+ 7 | 700 | initial
+ 9 | 900 | initial
+ 11 | 1100 | initial
+ 13 | 1300 | initial
+(7 rows)
+
+ROLLBACK;
DROP TABLE pa_target CASCADE;
-- The target table is partitioned in the same way, but this time by attaching
-- partitions which have columns in different order, dropped columns etc.
@@ -1716,7 +1909,26 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (sid, delta, 'inserted by merge');
+ INSERT VALUES (sid, delta, 'inserted by merge')
+ RETURNING pg_merge_action(), pg_merge_when_clause(), t.*;
+ pg_merge_action | pg_merge_when_clause | tid | balance | val
+-----------------+----------------------+-----+---------+--------------------------
+ UPDATE | 1 | 2 | 110 | initial updated by merge
+ INSERT | 2 | 2 | 20 | inserted by merge
+ UPDATE | 1 | 4 | 330 | initial updated by merge
+ INSERT | 2 | 4 | 40 | inserted by merge
+ UPDATE | 1 | 6 | 550 | initial updated by merge
+ INSERT | 2 | 6 | 60 | inserted by merge
+ UPDATE | 1 | 8 | 770 | initial updated by merge
+ INSERT | 2 | 8 | 80 | inserted by merge
+ UPDATE | 1 | 10 | 990 | initial updated by merge
+ INSERT | 2 | 10 | 100 | inserted by merge
+ UPDATE | 1 | 12 | 1210 | initial updated by merge
+ INSERT | 2 | 12 | 120 | inserted by merge
+ UPDATE | 1 | 14 | 1430 | initial updated by merge
+ INSERT | 2 | 14 | 140 | inserted by merge
+(14 rows)
+
SELECT * FROM pa_target ORDER BY tid;
tid | balance | val
-----+---------+--------------------------
@@ -1783,7 +1995,21 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge');
+ INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge')
+ RETURNING pg_merge_action(), pg_merge_when_clause(), t.*;
+ pg_merge_action | pg_merge_when_clause | logts | tid | balance | val
+-----------------+----------------------+--------------------------+-----+---------+--------------------------
+ UPDATE | 1 | Tue Jan 31 00:00:00 2017 | 1 | 110 | initial updated by merge
+ UPDATE | 1 | Tue Feb 28 00:00:00 2017 | 2 | 220 | initial updated by merge
+ INSERT | 2 | Sun Jan 15 00:00:00 2017 | 3 | 30 | inserted by merge
+ UPDATE | 1 | Tue Jan 31 00:00:00 2017 | 4 | 440 | initial updated by merge
+ UPDATE | 1 | Tue Feb 28 00:00:00 2017 | 5 | 550 | initial updated by merge
+ INSERT | 2 | Sun Jan 15 00:00:00 2017 | 6 | 60 | inserted by merge
+ UPDATE | 1 | Tue Jan 31 00:00:00 2017 | 7 | 770 | initial updated by merge
+ UPDATE | 1 | Tue Feb 28 00:00:00 2017 | 8 | 880 | initial updated by merge
+ INSERT | 2 | Sun Jan 15 00:00:00 2017 | 9 | 90 | inserted by merge
+(9 rows)
+
SELECT * FROM pa_target ORDER BY tid;
logts | tid | balance | val
--------------------------+-----+---------+--------------------------
diff --git a/src/test/regress/sql/merge.sql b/src/test/regress/sql/merge.sql
new file mode 100644
index f085416..dba6a19
--- 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,17 +844,145 @@ 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)
+ INSERT (balance, tid) VALUES (balance + delta, sid)
WHEN MATCHED AND tid < 2 THEN
- DELETE
-RETURNING *;
+ DELETE
+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();
+
+-- RETURNING in CTEs
+CREATE TABLE sq_target_merge_log (tid integer NOT NULL, last_change text);
+INSERT INTO sq_target_merge_log VALUES (1, 'Original value');
+BEGIN;
+WITH m AS (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 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
+), m2 AS (
+ MERGE INTO sq_target_merge_log l
+ USING m
+ ON l.tid = m.tid
+ WHEN MATCHED THEN
+ UPDATE SET last_change = description
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (m.tid, description)
+ RETURNING merge_action, pg_merge_action() AS merge_log_action, l.*
+)
+SELECT * FROM m2;
+SELECT * FROM sq_target_merge_log ORDER BY tid;
+ROLLBACK;
+
+-- COPY (MERGE ... RETURNING) TO ...
+BEGIN;
+COPY (
+ MERGE INTO sq_target t
+ USING v
+ ON tid = sid
+ 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 pg_merge_action(), t.*
+) TO stdout;
+ROLLBACK;
+
+-- SQL function with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT action text, OUT tid int, OUT new_balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid > 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING pg_merge_action(), t.*;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT action, tid, new_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ROLLBACK;
+
+-- SQL SRF with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_sq_source_into_sq_target()
+RETURNS TABLE (action text, tid int, balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 pg_merge_action(), t.*;
+$$;
+SELECT * FROM merge_sq_source_into_sq_target();
+ROLLBACK;
+
+-- PL/pgSQL function with MERGE ... RETURNING ... INTO
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT r_action text, OUT r_tid int, OUT r_balance int)
+LANGUAGE plpgsql AS
+$$
+BEGIN
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid > 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING pg_merge_action(), t.* INTO r_action, r_tid, r_balance;
+END;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT r_action, r_tid, r_balance FROM merge_into_sq_target(sid, balance, delta)) m;
ROLLBACK;
-- EXPLAIN
@@ -953,7 +1081,7 @@ WHEN MATCHED THEN
SELECT * FROM sq_target WHERE tid = 1;
ROLLBACK;
-DROP TABLE sq_target, sq_source CASCADE;
+DROP TABLE sq_target, sq_target_merge_log, sq_source CASCADE;
CREATE TABLE pa_target (tid integer, balance float, val text)
PARTITION BY LIST (tid);
@@ -1020,6 +1148,17 @@ SELECT merge_func();
SELECT * FROM pa_target ORDER BY tid;
ROLLBACK;
+-- update partition key to partition not initially scanned
+BEGIN;
+MERGE INTO pa_target t
+ USING pa_source s
+ ON t.tid = s.sid AND t.tid = 1
+ WHEN MATCHED THEN
+ UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
+ RETURNING pg_merge_action(), pg_merge_when_clause(), t.*;
+SELECT * FROM pa_target ORDER BY tid;
+ROLLBACK;
+
DROP TABLE pa_target CASCADE;
-- The target table is partitioned in the same way, but this time by attaching
@@ -1078,7 +1217,8 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (sid, delta, 'inserted by merge');
+ INSERT VALUES (sid, delta, 'inserted by merge')
+ RETURNING pg_merge_action(), pg_merge_when_clause(), t.*;
SELECT * FROM pa_target ORDER BY tid;
ROLLBACK;
@@ -1132,7 +1272,8 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge');
+ INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge')
+ RETURNING pg_merge_action(), pg_merge_when_clause(), t.*;
SELECT * FROM pa_target ORDER BY tid;
ROLLBACK;
On Sun, 26 Feb 2023 at 09:50, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
Another rebase.
And another rebase.
Regards,
Dean
Attachments:
support-merge-returning-v6.patchtext/x-patch; charset=US-ASCII; name=support-merge-returning-v6.patchDownload
diff --git a/doc/src/sgml/dml.sgml b/doc/src/sgml/dml.sgml
new file mode 100644
index cbbc5e2..ff2a827
--- a/doc/src/sgml/dml.sgml
+++ b/doc/src/sgml/dml.sgml
@@ -283,10 +283,15 @@ DELETE FROM products;
<secondary>RETURNING</secondary>
</indexterm>
+ <indexterm zone="dml-returning">
+ <primary>MERGE</primary>
+ <secondary>RETURNING</secondary>
+ </indexterm>
+
<para>
Sometimes it is useful to obtain data from modified rows while they are
being manipulated. The <command>INSERT</command>, <command>UPDATE</command>,
- and <command>DELETE</command> commands all have an
+ <command>DELETE</command>, and <command>MERGE</command> commands all have an
optional <literal>RETURNING</literal> clause that supports this. Use
of <literal>RETURNING</literal> avoids performing an extra database query to
collect the data, and is especially valuable when it would otherwise be
@@ -339,6 +344,24 @@ DELETE FROM products
</programlisting>
</para>
+ <para>
+ In a <command>MERGE</command>, the data available to <literal>RETURNING</literal> is
+ the content of the source row plus the content of the inserted, updated, or
+ deleted target row. In addition, the merge support functions
+ <xref linkend="pg-merge-action"/> and <xref linkend="pg-merge-when-clause"/>
+ may be used to return information about which merge action was executed for
+ each row. Since it is quite common for the source and target to have many
+ of the same columns, specifying <literal>RETURNING *</literal> can lead to a
+ lot of duplicated columns, so it is often more useful to just return the
+ target row. For example:
+<programlisting>
+MERGE INTO products p USING new_products n ON p.product_no = n.product_no
+ WHEN NOT MATCHED THEN INSERT VALUES (n.product_no, n.name, n.price)
+ WHEN MATCHED THEN UPDATE SET name = n.name, price = n.price
+ RETURNING pg_merge_action(), p.*;
+</programlisting>
+ </para>
+
<para>
If there are triggers (<xref linkend="triggers"/>) on the target table,
the data available to <literal>RETURNING</literal> is the row as modified by
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
new file mode 100644
index 9c6107f..d07f90a
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -21216,6 +21216,99 @@ SELECT count(*) FROM sometable;
</sect1>
+ <sect1 id="functions-merge-support">
+ <title>Merge Support Functions</title>
+
+ <indexterm>
+ <primary>MERGE</primary>
+ <secondary>RETURNING</secondary>
+ </indexterm>
+
+ <para>
+ The merge support functions shown in
+ <xref linkend="functions-merge-support-table"/> may be used in the
+ <literal>RETURNING</literal> list of a <xref linkend="sql-merge"/> command
+ to return additional information about the action taken for each row.
+ </para>
+
+ <table id="functions-merge-support-table">
+ <title>Merge Support Functions</title>
+
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ Function
+ </para>
+ <para>
+ Description
+ </para></entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm id="pg-merge-action">
+ <primary>pg_merge_action</primary>
+ </indexterm>
+ <function>pg_merge_action</function> ( )
+ <returnvalue>text</returnvalue>
+ </para>
+ <para>
+ Returns the action performed on the current row (<literal>'INSERT'</literal>,
+ <literal>'UPDATE'</literal>, or <literal>'DELETE'</literal>).
+ </para></entry>
+ </row>
+
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm id="pg-merge-when-clause">
+ <primary>pg_merge_when_clause</primary>
+ </indexterm>
+ <function>pg_merge_when_clause</function> ( )
+ <returnvalue>integer</returnvalue>
+ </para>
+ <para>
+ Returns the 1-based index of the <literal>WHEN</literal> clause executed
+ for the current row.
+ </para></entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+
+ <para>
+ Example:
+<screen><![CDATA[
+MERGE INTO products p
+ USING stock s ON p.product_id = s.product_id
+ WHEN MATCHED AND s.quantity > 0 THEN
+ UPDATE SET in_stock = true, quantity = s.quantity
+ WHEN MATCHED THEN
+ UPDATE SET in_stock = false, quantity = 0
+ WHEN NOT MATCHED THEN
+ INSERT (product_id, in_stock, quantity)
+ VALUES (s.product_id, true, s.quantity)
+ RETURNING pg_merge_action() AS action,
+ pg_merge_when_clause() AS when_clause,
+ p.*;
+
+ action | when_clause | product_id | in_stock | quantity
+--------+-------------+------------+----------+----------
+ UPDATE | 1 | 1001 | t | 50
+ UPDATE | 2 | 1002 | f | 0
+ INSERT | 3 | 1003 | t | 10
+]]></screen>
+ </para>
+
+ <para>
+ Note that it is an error to use these functions anywhere other than the
+ <literal>RETURNING</literal> list of a <command>MERGE</command> command.
+ </para>
+
+ </sect1>
+
<sect1 id="functions-subquery">
<title>Subquery Expressions</title>
diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml
new file mode 100644
index 7c01a54..4317cfc
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -1323,9 +1323,9 @@
to a <glossterm linkend="glossary-client">client</glossterm> upon the
completion of an <acronym>SQL</acronym> command, usually a
<command>SELECT</command> but it can be an
- <command>INSERT</command>, <command>UPDATE</command>, or
- <command>DELETE</command> command if the <literal>RETURNING</literal>
- clause is specified.
+ <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command> command if the
+ <literal>RETURNING</literal> clause is specified.
</para>
<para>
The fact that a result set is a relation means that a query can be used
diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml
new file mode 100644
index 7c8a49f..b839695
--- a/doc/src/sgml/plpgsql.sgml
+++ b/doc/src/sgml/plpgsql.sgml
@@ -1020,8 +1020,8 @@ INSERT INTO mytable VALUES (1,'one'), (2
</para>
<para>
- If the command does return rows (for example <command>SELECT</command>,
- or <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>
+ If the command does return rows (for example <command>SELECT</command>, or
+ <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command>
with <literal>RETURNING</literal>), there are two ways to proceed.
When the command will return at most one row, or you only care about
the first row of output, write the command as usual but add
@@ -1149,6 +1149,7 @@ SELECT <replaceable>select_expressions</
INSERT ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
UPDATE ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
DELETE ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
+MERGE ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
</synopsis>
where <replaceable>target</replaceable> can be a record variable, a row
@@ -1159,8 +1160,8 @@ DELETE ... RETURNING <replaceable>expres
<literal>INTO</literal> clause) just as described above,
and the plan is cached in the same way.
This works for <command>SELECT</command>,
- <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command> with
- <literal>RETURNING</literal>, and certain utility commands
+ <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command>
+ with <literal>RETURNING</literal>, and certain utility commands
that return row sets, such as <command>EXPLAIN</command>.
Except for the <literal>INTO</literal> clause, the SQL command is the same
as it would be written outside <application>PL/pgSQL</application>.
@@ -1236,7 +1237,7 @@ END;
</para>
<para>
- For <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command> with
+ For <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command> with
<literal>RETURNING</literal>, <application>PL/pgSQL</application> reports
an error for more than one returned row, even when
<literal>STRICT</literal> is not specified. This is because there
diff --git a/doc/src/sgml/queries.sgml b/doc/src/sgml/queries.sgml
new file mode 100644
index 6986ec5..9e9687b
--- a/doc/src/sgml/queries.sgml
+++ b/doc/src/sgml/queries.sgml
@@ -2062,9 +2062,10 @@ SELECT <replaceable>select_list</replace
Table Expressions or <acronym>CTE</acronym>s, can be thought of as defining
temporary tables that exist just for one query. Each auxiliary statement
in a <literal>WITH</literal> clause can be a <command>SELECT</command>,
- <command>INSERT</command>, <command>UPDATE</command>, or <command>DELETE</command>; and the
+ <command>INSERT</command>, <command>UPDATE</command>, <command>DELETE</command>,
+ or <command>MERGE</command>; and the
<literal>WITH</literal> clause itself is attached to a primary statement that can
- be a <command>SELECT</command>, <command>INSERT</command>, <command>UPDATE</command>,
+ also be a <command>SELECT</command>, <command>INSERT</command>, <command>UPDATE</command>,
<command>DELETE</command>, or <command>MERGE</command>.
</para>
@@ -2598,8 +2599,8 @@ SELECT * FROM w AS w1 JOIN w AS w2 ON w1
<title>Data-Modifying Statements in <literal>WITH</literal></title>
<para>
- You can use most data-modifying statements (<command>INSERT</command>,
- <command>UPDATE</command>, or <command>DELETE</command>, but not
+ You can use data-modifying statements (<command>INSERT</command>,
+ <command>UPDATE</command>, <command>DELETE</command>, or
<command>MERGE</command>) in <literal>WITH</literal>. This
allows you to perform several different operations in the same query.
An example is:
diff --git a/doc/src/sgml/ref/copy.sgml b/doc/src/sgml/ref/copy.sgml
new file mode 100644
index c25b52d..d822470
--- a/doc/src/sgml/ref/copy.sgml
+++ b/doc/src/sgml/ref/copy.sgml
@@ -121,13 +121,15 @@ COPY { <replaceable class="parameter">ta
A <link linkend="sql-select"><command>SELECT</command></link>,
<link linkend="sql-values"><command>VALUES</command></link>,
<link linkend="sql-insert"><command>INSERT</command></link>,
- <link linkend="sql-update"><command>UPDATE</command></link>, or
- <link linkend="sql-delete"><command>DELETE</command></link> command whose results are to be
+ <link linkend="sql-update"><command>UPDATE</command></link>,
+ <link linkend="sql-update"><command>DELETE</command></link>, or
+ <link linkend="sql-delete"><command>MERGE</command></link> command whose results are to be
copied. Note that parentheses are required around the query.
</para>
<para>
- For <command>INSERT</command>, <command>UPDATE</command> and
- <command>DELETE</command> queries a RETURNING clause must be provided,
+ For <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, and <command>MERGE</command> queries a
+ RETURNING clause must be provided,
and the target relation must not have a conditional rule, nor
an <literal>ALSO</literal> rule, nor an <literal>INSTEAD</literal> rule
that expands to multiple statements.
diff --git a/doc/src/sgml/ref/merge.sgml b/doc/src/sgml/ref/merge.sgml
new file mode 100644
index 0995fe0..7eb641a
--- a/doc/src/sgml/ref/merge.sgml
+++ b/doc/src/sgml/ref/merge.sgml
@@ -25,6 +25,7 @@ PostgreSQL documentation
MERGE INTO [ ONLY ] <replaceable class="parameter">target_table_name</replaceable> [ * ] [ [ AS ] <replaceable class="parameter">target_alias</replaceable> ]
USING <replaceable class="parameter">data_source</replaceable> ON <replaceable class="parameter">join_condition</replaceable>
<replaceable class="parameter">when_clause</replaceable> [...]
+[ RETURNING * | <replaceable class="parameter">output_expression</replaceable> [ [ AS ] <replaceable class="parameter">output_name</replaceable> ] [, ...] ]
<phrase>where <replaceable class="parameter">data_source</replaceable> is:</phrase>
@@ -95,6 +96,20 @@ DELETE
</para>
<para>
+ The optional <literal>RETURNING</literal> clause causes <command>MERGE</command>
+ to compute and return value(s) based on each row inserted, updated, or
+ deleted. Any expression using the source or target table's columns, or the
+ merge support functions <xref linkend="pg-merge-action"/> and
+ <xref linkend="pg-merge-when-clause"/> can be computed. When an
+ <command>INSERT</command> or <command>UPDATE</command> action is performed,
+ the new values of the target table's columns are used. When a
+ <command>DELETE</command> is performed, the old values of the target table's
+ columns are used. The syntax of the <literal>RETURNING</literal> list is
+ identical to that of the output list of <command>SELECT</command>.
+
+ </para>
+
+ <para>
There is no separate <literal>MERGE</literal> privilege.
If you specify an update action, you must have the
<literal>UPDATE</literal> privilege on the column(s)
@@ -125,6 +140,18 @@ DELETE
<variablelist>
<varlistentry>
+ <term><replaceable class="parameter">with_query</replaceable></term>
+ <listitem>
+ <para>
+ The <literal>WITH</literal> clause allows you to specify one or more
+ subqueries that can be referenced by name in the <command>MERGE</command>
+ query. See <xref linkend="queries-with"/> and <xref linkend="sql-select"/>
+ for details.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
<term><replaceable class="parameter">target_table_name</replaceable></term>
<listitem>
<para>
@@ -399,13 +426,30 @@ DELETE
</varlistentry>
<varlistentry>
- <term><replaceable class="parameter">with_query</replaceable></term>
+ <term><replaceable class="parameter">output_expression</replaceable></term>
<listitem>
<para>
- The <literal>WITH</literal> clause allows you to specify one or more
- subqueries that can be referenced by name in the <command>MERGE</command>
- query. See <xref linkend="queries-with"/> and <xref linkend="sql-select"/>
- for details.
+ An expression to be computed and returned by the <command>MERGE</command>
+ command after each row is merged. The expression can use any columns of
+ the source or target tables, or the merge support functions
+ <xref linkend="pg-merge-action"/> and <xref linkend="pg-merge-when-clause"/>.
+ </para>
+ <para>
+ Writing <literal>*</literal> will return all columns from the source
+ table, followed by all columns from the target table. Often this will
+ lead to a lot of duplication, since it is common for the source and
+ target tables to have a lot of the same columns. This can be avoided by
+ qualifying the <literal>*</literal> with the name or alias of the source
+ or target table.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><replaceable class="parameter">output_name</replaceable></term>
+ <listitem>
+ <para>
+ A name to use for a returned column.
</para>
</listitem>
</varlistentry>
@@ -428,6 +472,13 @@ MERGE <replaceable class="parameter">tot
were changed in any way.
</para>
+ <para>
+ If the <command>MERGE</command> command contains a <literal>RETURNING</literal>
+ clause, the result will be similar to that of a <command>SELECT</command>
+ statement containing the columns and values defined in the
+ <literal>RETURNING</literal> list, computed over the row(s) inserted, updated,
+ or deleted by the command.
+ </para>
</refsect1>
<refsect1>
@@ -544,13 +595,6 @@ MERGE <replaceable class="parameter">tot
</para>
<para>
- There is no <literal>RETURNING</literal> clause with
- <command>MERGE</command>. Actions of <command>INSERT</command>,
- <command>UPDATE</command> and <command>DELETE</command> cannot contain
- <literal>RETURNING</literal> or <literal>WITH</literal> clauses.
- </para>
-
- <para>
When <command>MERGE</command> is run concurrently with other commands
that modify the target table, the usual transaction isolation rules
apply; see <xref linkend="transaction-iso"/> for an explanation
@@ -602,7 +646,8 @@ WHEN NOT MATCHED THEN
<para>
Attempt to insert a new stock item along with the quantity of stock. If
the item already exists, instead update the stock count of the existing
- item. Don't allow entries that have zero stock.
+ item. Don't allow entries that have zero stock. Return details of all
+ changes made.
<programlisting>
MERGE INTO wines w
USING wine_stock_changes s
@@ -612,7 +657,8 @@ WHEN NOT MATCHED AND s.stock_delta > 0 T
WHEN MATCHED AND w.stock + s.stock_delta > 0 THEN
UPDATE SET stock = w.stock + s.stock_delta
WHEN MATCHED THEN
- DELETE;
+ DELETE
+RETURNING pg_merge_action(), w.*;
</programlisting>
The <literal>wine_stock_changes</literal> table might be, for example, a
@@ -627,7 +673,8 @@ WHEN MATCHED THEN
This command conforms to the <acronym>SQL</acronym> standard.
</para>
<para>
- The WITH clause and <literal>DO NOTHING</literal> action are extensions to
+ The WITH clause, <literal>DO NOTHING</literal> action, and
+ <literal>RETURNING</literal> clause are extensions to
the <acronym>SQL</acronym> standard.
</para>
</refsect1>
diff --git a/doc/src/sgml/ref/select.sgml b/doc/src/sgml/ref/select.sgml
new file mode 100644
index 1f9538f..5840ddc
--- a/doc/src/sgml/ref/select.sgml
+++ b/doc/src/sgml/ref/select.sgml
@@ -74,7 +74,7 @@ SELECT [ ALL | DISTINCT [ ON ( <replacea
<phrase>and <replaceable class="parameter">with_query</replaceable> is:</phrase>
- <replaceable class="parameter">with_query_name</replaceable> [ ( <replaceable class="parameter">column_name</replaceable> [, ...] ) ] AS [ [ NOT ] MATERIALIZED ] ( <replaceable class="parameter">select</replaceable> | <replaceable class="parameter">values</replaceable> | <replaceable class="parameter">insert</replaceable> | <replaceable class="parameter">update</replaceable> | <replaceable class="parameter">delete</replaceable> )
+ <replaceable class="parameter">with_query_name</replaceable> [ ( <replaceable class="parameter">column_name</replaceable> [, ...] ) ] AS [ [ NOT ] MATERIALIZED ] ( <replaceable class="parameter">select</replaceable> | <replaceable class="parameter">values</replaceable> | <replaceable class="parameter">insert</replaceable> | <replaceable class="parameter">update</replaceable> | <replaceable class="parameter">delete</replaceable> | <replaceable class="parameter">merge</replaceable> )
[ SEARCH { BREADTH | DEPTH } FIRST BY <replaceable>column_name</replaceable> [, ...] SET <replaceable>search_seq_col_name</replaceable> ]
[ CYCLE <replaceable>column_name</replaceable> [, ...] SET <replaceable>cycle_mark_col_name</replaceable> [ TO <replaceable>cycle_mark_value</replaceable> DEFAULT <replaceable>cycle_mark_default</replaceable> ] USING <replaceable>cycle_path_col_name</replaceable> ]
@@ -227,10 +227,10 @@ TABLE [ ONLY ] <replaceable class="param
The subqueries effectively act as temporary tables or views
for the duration of the primary query.
Each subquery can be a <command>SELECT</command>, <command>TABLE</command>, <command>VALUES</command>,
- <command>INSERT</command>, <command>UPDATE</command> or
- <command>DELETE</command> statement.
+ <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command> statement.
When writing a data-modifying statement (<command>INSERT</command>,
- <command>UPDATE</command> or <command>DELETE</command>) in
+ <command>UPDATE</command>, <command>DELETE</command>, or <command>MERGE</command>) in
<literal>WITH</literal>, it is usual to include a <literal>RETURNING</literal> clause.
It is the output of <literal>RETURNING</literal>, <emphasis>not</emphasis> the underlying
table that the statement modifies, that forms the temporary table that is
@@ -2179,7 +2179,8 @@ SELECT 2+2;
<para>
<productname>PostgreSQL</productname> allows <command>INSERT</command>,
- <command>UPDATE</command>, and <command>DELETE</command> to be used as <literal>WITH</literal>
+ <command>UPDATE</command>, <command>DELETE</command>, and
+ <command>MERGE</command> to be used as <literal>WITH</literal>
queries. This is not found in the SQL standard.
</para>
</refsect2>
diff --git a/doc/src/sgml/rowtypes.sgml b/doc/src/sgml/rowtypes.sgml
new file mode 100644
index 4d86f97..bbeac84
--- a/doc/src/sgml/rowtypes.sgml
+++ b/doc/src/sgml/rowtypes.sgml
@@ -348,7 +348,7 @@ SELECT m.* FROM some_table, LATERAL myfu
column expansion of this kind when it appears at the top level of
a <link linkend="queries-select-lists"><command>SELECT</command> output
list</link>, a <link linkend="dml-returning"><literal>RETURNING</literal>
- list</link> in <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>,
+ list</link> in <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command>,
a <link linkend="queries-values"><literal>VALUES</literal> clause</link>, or
a <link linkend="sql-syntax-row-constructors">row constructor</link>.
In all other contexts (including when nested inside one of those
diff --git a/doc/src/sgml/spi.sgml b/doc/src/sgml/spi.sgml
new file mode 100644
index 651930a..ad0a9b1
--- a/doc/src/sgml/spi.sgml
+++ b/doc/src/sgml/spi.sgml
@@ -301,8 +301,9 @@ SPI_execute("INSERT INTO foo SELECT * FR
is returned in the global variable <varname>SPI_processed</varname>.
If the return value of the function is <symbol>SPI_OK_SELECT</symbol>,
<symbol>SPI_OK_INSERT_RETURNING</symbol>,
- <symbol>SPI_OK_DELETE_RETURNING</symbol>, or
- <symbol>SPI_OK_UPDATE_RETURNING</symbol>,
+ <symbol>SPI_OK_DELETE_RETURNING</symbol>,
+ <symbol>SPI_OK_UPDATE_RETURNING</symbol>, or
+ <symbol>SPI_OK_MERGE_RETURNING</symbol>
then you can use the
global pointer <literal>SPITupleTable *SPI_tuptable</literal> to
access the result rows. Some utility commands (such as
@@ -472,6 +473,15 @@ typedef struct SPITupleTable
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><symbol>SPI_OK_MERGE_RETURNING</symbol></term>
+ <listitem>
+ <para>
+ if a <command>MERGE RETURNING</command> was executed
+ </para>
+ </listitem>
+ </varlistentry>
<varlistentry>
<term><symbol>SPI_OK_UTILITY</symbol></term>
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
new file mode 100644
index 9620ea9..4f68efb
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -177,7 +177,8 @@
statements separated by semicolons. A semicolon after the last
statement is optional. Unless the function is declared to return
<type>void</type>, the last statement must be a <command>SELECT</command>,
- or an <command>INSERT</command>, <command>UPDATE</command>, or <command>DELETE</command>
+ or an <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command>
that has a <literal>RETURNING</literal> clause.
</para>
@@ -1236,8 +1237,9 @@ SELECT x, CASE WHEN x > 0 THEN genera
<note>
<para>
- If a function's last command is <command>INSERT</command>, <command>UPDATE</command>,
- or <command>DELETE</command> with <literal>RETURNING</literal>, that command will
+ If a function's last command is <command>INSERT</command>,
+ <command>UPDATE</command>, <command>DELETE</command>, or
+ <command>MERGE</command> with <literal>RETURNING</literal>, that command will
always be executed to completion, even if the function is not declared
with <literal>SETOF</literal> or the calling query does not fetch all the
result rows. Any extra rows produced by the <literal>RETURNING</literal>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
new file mode 100644
index f3bbd91..3ca7d85
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -277,12 +277,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 beea1ac..19b7f9f
--- 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 c61f23c..8ce6a4f
--- 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"
@@ -2488,6 +2489,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 fd6ca8a..95bbf67
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -612,8 +612,8 @@ ExecInitPartitionInfo(ModifyTableState *
* Build the RETURNING projection for the partition. Note that we didn't
* build the returningList for partitions within the planner, but simple
* translation of varattnos will suffice. This only occurs for the INSERT
- * case or in the case of UPDATE tuple routing where we didn't find a
- * result rel to reuse.
+ * case or in the case of UPDATE/MERGE tuple routing where we didn't find
+ * a result rel to reuse.
*/
if (node && node->returningLists != NIL)
{
@@ -622,12 +622,14 @@ ExecInitPartitionInfo(ModifyTableState *
List *returningList;
/* See the comment above for WCO lists. */
- /* (except no RETURNING support for MERGE yet) */
Assert((node->operation == CMD_INSERT &&
list_length(node->returningLists) == 1 &&
list_length(node->resultRelations) == 1) ||
(node->operation == CMD_UPDATE &&
list_length(node->returningLists) ==
+ list_length(node->resultRelations)) ||
+ (node->operation == CMD_MERGE &&
+ list_length(node->returningLists) ==
list_length(node->resultRelations)));
/*
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 3fa2b93..5b08d2e
--- 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"
@@ -88,9 +87,6 @@ typedef struct ModifyTableContext
*/
TupleTableSlot *planSlot;
- /* MERGE specific */
- MergeActionState *relaction; /* MERGE action in progress */
-
/*
* Information about the changes that were made concurrently to a tuple
* being updated or deleted
@@ -153,13 +149,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);
/*
@@ -980,7 +977,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;
@@ -1820,7 +1817,7 @@ ExecCrossPartitionUpdate(ModifyTableCont
* additional rechecking, and might end up executing a different
* action entirely).
*/
- if (context->relaction != NULL)
+ if (mtstate->mt_merge_action != NULL)
return false;
else if (TupIsNull(epqslot))
return true;
@@ -2061,7 +2058,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;
/*
@@ -2703,6 +2700,7 @@ static TupleTableSlot *
ExecMerge(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
ItemPointer tupleid, bool canSetTag)
{
+ TupleTableSlot *rslot = NULL;
bool matched;
/*-----
@@ -2750,7 +2748,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()
@@ -2758,10 +2757,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;
}
/*
@@ -2771,8 +2769,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.
*
@@ -2781,15 +2779,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;
@@ -2801,7 +2800,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
@@ -2879,12 +2881,15 @@ lmerge_matched:
*/
newslot = ExecProject(relaction->mas_proj);
- context->relaction = relaction;
+ mtstate->mt_merge_action = relaction;
if (!ExecUpdatePrologue(context, resultRelInfo,
tupleid, NULL, newslot, &result))
{
if (result == TM_Ok)
- return true; /* "do nothing" */
+ {
+ *matched = true;
+ return NULL; /* "do nothing" */
+ }
break; /* concurrent update/delete */
}
result = ExecUpdateAct(context, resultRelInfo, tupleid, NULL,
@@ -2898,12 +2903,15 @@ lmerge_matched:
break;
case CMD_DELETE:
- context->relaction = relaction;
+ mtstate->mt_merge_action = relaction;
if (!ExecDeletePrologue(context, resultRelInfo, tupleid,
NULL, NULL, &result))
{
if (result == TM_Ok)
- return true; /* "do nothing" */
+ {
+ *matched = true;
+ return NULL; /* "do nothing" */
+ }
break; /* concurrent update/delete */
}
result = ExecDeleteAct(context, resultRelInfo, tupleid, false);
@@ -2959,7 +2967,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:
{
@@ -3005,13 +3014,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
@@ -3046,7 +3061,8 @@ lmerge_matched:
* tuple already deleted; tell caller to run NOT
* MATCHED actions
*/
- return false;
+ *matched = false;
+ return NULL;
case TM_SelfModified:
@@ -3066,13 +3082,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;
}
}
@@ -3084,6 +3101,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.
@@ -3094,19 +3135,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;
/*
@@ -3156,10 +3200,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:
@@ -3175,6 +3219,8 @@ ExecMergeNotMatched(ModifyTableContext *
*/
break;
}
+
+ return rslot;
}
/*
@@ -3352,6 +3398,64 @@ ExecInitMergeTupleSlots(ModifyTableState
}
/*
+ * 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->index);
+
+ PG_RETURN_NULL();
+}
+
+/*
* Process BEFORE EACH STATEMENT triggers
*/
static void
@@ -3637,8 +3741,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");
@@ -3715,8 +3828,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");
@@ -3808,7 +3930,7 @@ ExecModifyTable(PlanState *pstate)
}
slot = ExecGetUpdateNewTuple(resultRelInfo, context.planSlot,
oldSlot);
- context.relaction = NULL;
+ node->mt_merge_action = NULL;
/* Now apply the update. */
slot = ExecUpdate(&context, resultRelInfo, tupleid, oldtuple,
diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c
new file mode 100644
index e3a170c..be88056
--- a/src/backend/executor/spi.c
+++ b/src/backend/executor/spi.c
@@ -2033,6 +2033,8 @@ SPI_result_code_string(int code)
return "SPI_OK_TD_REGISTER";
case SPI_OK_MERGE:
return "SPI_OK_MERGE";
+ case SPI_OK_MERGE_RETURNING:
+ return "SPI_OK_MERGE_RETURNING";
}
/* Unrecognized code ... return something useful ... */
sprintf(buf, "Unrecognized SPI code %d", code);
@@ -2886,7 +2888,10 @@ _SPI_pquery(QueryDesc *queryDesc, bool f
res = SPI_OK_UPDATE;
break;
case CMD_MERGE:
- res = SPI_OK_MERGE;
+ if (queryDesc->plannedstmt->hasReturning)
+ res = SPI_OK_MERGE_RETURNING;
+ else
+ res = SPI_OK_MERGE;
break;
default:
return SPI_ERROR_OPUNKNOWN;
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
new file mode 100644
index e892df9..fe0f5cb
--- 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;
@@ -941,7 +941,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;
@@ -2406,7 +2407,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
@@ -2500,10 +2502,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;
@@ -2520,7 +2523,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 4fbf80c..b8d655a
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -456,6 +456,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:
@@ -904,6 +905,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 78221d2..b4021f6
--- 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:
@@ -3002,6 +3004,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..2e50eb0
--- 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.
@@ -254,6 +258,7 @@ transformMergeStmt(ParseState *pstate, M
MergeAction *action;
action = makeNode(MergeAction);
+ action->index = foreach_current_index(l) + 1;
action->commandType = mergeWhenClause->commandType;
action->matched = mergeWhenClause->matched;
@@ -390,9 +395,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 41d6049..794530b
--- a/src/backend/parser/parse_relation.c
+++ b/src/backend/parser/parse_relation.c
@@ -2345,9 +2345,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 980dc18..3689e54
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -3655,7 +3655,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 5055956..9e86bf9
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -11939,4 +11939,14 @@
proname => 'any_value_transfn', prorettype => 'anyelement',
proargtypes => 'anyelement anyelement', prosrc => 'any_value_transfn' },
+# 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/executor/spi.h b/src/include/executor/spi.h
new file mode 100644
index d1de139..ed8787d
--- a/src/include/executor/spi.h
+++ b/src/include/executor/spi.h
@@ -97,6 +97,7 @@ typedef struct _SPI_plan *SPIPlanPtr;
#define SPI_OK_REL_UNREGISTER 16
#define SPI_OK_TD_REGISTER 17
#define SPI_OK_MERGE 18
+#define SPI_OK_MERGE_RETURNING 19
#define SPI_OPT_NONATOMIC (1 << 0)
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
new file mode 100644
index bc67cb9..cc85e25
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1304,6 +1304,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 371aa0f..3a0e076
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -1686,6 +1686,7 @@ typedef struct MergeWhenClause
typedef struct MergeAction
{
NodeTag type;
+ int index; /* 1-based index of the clause */
bool matched; /* true=MATCHED, false=NOT MATCHED */
CmdType commandType; /* INSERT/UPDATE/DELETE/DO NOTHING */
/* OVERRIDING clause */
@@ -1805,6 +1806,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 f589112..836819d
--- 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/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
new file mode 100644
index b0a2cac..b72e7b9
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -4246,9 +4246,9 @@ exec_stmt_execsql(PLpgSQL_execstate *est
/*
* If we have INTO, then we only need one row back ... but if we have INTO
* STRICT or extra check too_many_rows, ask for two rows, so that we can
- * verify the statement returns only one. INSERT/UPDATE/DELETE are always
- * treated strictly. Without INTO, just run the statement to completion
- * (tcount = 0).
+ * verify the statement returns only one. INSERT/UPDATE/DELETE/MERGE are
+ * always treated strictly. Without INTO, just run the statement to
+ * completion (tcount = 0).
*
* We could just ask for two rows always when using INTO, but there are
* some cases where demanding the extra row costs significant time, eg by
@@ -4286,10 +4286,11 @@ exec_stmt_execsql(PLpgSQL_execstate *est
case SPI_OK_INSERT:
case SPI_OK_UPDATE:
case SPI_OK_DELETE:
+ case SPI_OK_MERGE:
case SPI_OK_INSERT_RETURNING:
case SPI_OK_UPDATE_RETURNING:
case SPI_OK_DELETE_RETURNING:
- case SPI_OK_MERGE:
+ case SPI_OK_MERGE_RETURNING:
Assert(stmt->mod_stmt);
exec_set_found(estate, (SPI_processed != 0));
break;
@@ -4468,10 +4469,11 @@ exec_stmt_dynexecute(PLpgSQL_execstate *
case SPI_OK_INSERT:
case SPI_OK_UPDATE:
case SPI_OK_DELETE:
+ case SPI_OK_MERGE:
case SPI_OK_INSERT_RETURNING:
case SPI_OK_UPDATE_RETURNING:
case SPI_OK_DELETE_RETURNING:
- case SPI_OK_MERGE:
+ case SPI_OK_MERGE_RETURNING:
case SPI_OK_UTILITY:
case SPI_OK_REWRITTEN:
break;
diff --git a/src/pl/tcl/pltcl.c b/src/pl/tcl/pltcl.c
new file mode 100644
index 499a9ea..58c4f50
--- a/src/pl/tcl/pltcl.c
+++ b/src/pl/tcl/pltcl.c
@@ -2459,6 +2459,7 @@ pltcl_process_SPI_result(Tcl_Interp *int
case SPI_OK_INSERT_RETURNING:
case SPI_OK_DELETE_RETURNING:
case SPI_OK_UPDATE_RETURNING:
+ case SPI_OK_MERGE_RETURNING:
/*
* Process the tuples we got
diff --git a/src/test/regress/expected/merge.out b/src/test/regress/expected/merge.out
new file mode 100644
index e32afc3..5b60743
--- 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;
@@ -1304,20 +1304,187 @@ 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)
+ 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 *;
- ^
+ DELETE
+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...
+ ^
+-- RETURNING in CTEs
+CREATE TABLE sq_target_merge_log (tid integer NOT NULL, last_change text);
+INSERT INTO sq_target_merge_log VALUES (1, 'Original value');
+BEGIN;
+WITH m AS (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 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
+), m2 AS (
+ MERGE INTO sq_target_merge_log l
+ USING m
+ ON l.tid = m.tid
+ WHEN MATCHED THEN
+ UPDATE SET last_change = description
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (m.tid, description)
+ RETURNING merge_action, pg_merge_action() AS merge_log_action, l.*
+)
+SELECT * FROM m2;
+ merge_action | merge_log_action | tid | last_change
+--------------+------------------+-----+---------------------
+ DELETE | UPDATE | 1 | Removed (1,100)
+ UPDATE | INSERT | 2 | Added 20 to balance
+ INSERT | INSERT | 4 | Inserted (4,40)
+(3 rows)
+
+SELECT * FROM sq_target_merge_log ORDER BY tid;
+ tid | last_change
+-----+---------------------
+ 1 | Removed (1,100)
+ 2 | Added 20 to balance
+ 4 | Inserted (4,40)
+(3 rows)
+
+ROLLBACK;
+-- COPY (MERGE ... RETURNING) TO ...
+BEGIN;
+COPY (
+ MERGE INTO sq_target t
+ USING v
+ ON tid = sid
+ 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 pg_merge_action(), t.*
+) TO stdout;
+DELETE 1 100
+ROLLBACK;
+-- SQL function with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT action text, OUT tid int, OUT new_balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid > 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING pg_merge_action(), t.*;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT action, tid, new_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ action | tid | new_balance
+--------+-----+-------------
+ DELETE | 1 | 100
+ UPDATE | 3 | 320
+ INSERT | 4 | 110
+(3 rows)
+
+ROLLBACK;
+-- SQL SRF with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_sq_source_into_sq_target()
+RETURNS TABLE (action text, tid int, balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 pg_merge_action(), t.*;
+$$;
+SELECT * FROM merge_sq_source_into_sq_target();
+ action | tid | balance
+--------+-----+---------
+ DELETE | 1 | 100
+ INSERT | 4 | 40
+(2 rows)
+
+ROLLBACK;
+-- PL/pgSQL function with MERGE ... RETURNING ... INTO
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT r_action text, OUT r_tid int, OUT r_balance int)
+LANGUAGE plpgsql AS
+$$
+BEGIN
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid > 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING pg_merge_action(), t.* INTO r_action, r_tid, r_balance;
+END;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT r_action, r_tid, r_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ r_action | r_tid | r_balance
+----------+-------+-----------
+ DELETE | 1 | 100
+ UPDATE | 3 | 320
+ INSERT | 4 | 110
+(3 rows)
+
ROLLBACK;
-- EXPLAIN
CREATE TABLE ex_mtarget (a int, b int)
@@ -1514,7 +1681,7 @@ SELECT * FROM sq_target WHERE tid = 1;
(1 row)
ROLLBACK;
-DROP TABLE sq_target, sq_source CASCADE;
+DROP TABLE sq_target, sq_target_merge_log, sq_source CASCADE;
NOTICE: drop cascades to view v
CREATE TABLE pa_target (tid integer, balance float, val text)
PARTITION BY LIST (tid);
@@ -1640,6 +1807,32 @@ SELECT * FROM pa_target ORDER BY tid;
(14 rows)
ROLLBACK;
+-- update partition key to partition not initially scanned
+BEGIN;
+MERGE INTO pa_target t
+ USING pa_source s
+ ON t.tid = s.sid AND t.tid = 1
+ WHEN MATCHED THEN
+ UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
+ RETURNING pg_merge_action(), pg_merge_when_clause(), t.*;
+ pg_merge_action | pg_merge_when_clause | tid | balance | val
+-----------------+----------------------+-----+---------+--------------------------
+ UPDATE | 1 | 2 | 110 | initial updated by merge
+(1 row)
+
+SELECT * FROM pa_target ORDER BY tid;
+ tid | balance | val
+-----+---------+--------------------------
+ 2 | 110 | initial updated by merge
+ 3 | 300 | initial
+ 5 | 500 | initial
+ 7 | 700 | initial
+ 9 | 900 | initial
+ 11 | 1100 | initial
+ 13 | 1300 | initial
+(7 rows)
+
+ROLLBACK;
DROP TABLE pa_target CASCADE;
-- The target table is partitioned in the same way, but this time by attaching
-- partitions which have columns in different order, dropped columns etc.
@@ -1731,7 +1924,26 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (sid, delta, 'inserted by merge');
+ INSERT VALUES (sid, delta, 'inserted by merge')
+ RETURNING pg_merge_action(), pg_merge_when_clause(), t.*;
+ pg_merge_action | pg_merge_when_clause | tid | balance | val
+-----------------+----------------------+-----+---------+--------------------------
+ UPDATE | 1 | 2 | 110 | initial updated by merge
+ INSERT | 2 | 2 | 20 | inserted by merge
+ UPDATE | 1 | 4 | 330 | initial updated by merge
+ INSERT | 2 | 4 | 40 | inserted by merge
+ UPDATE | 1 | 6 | 550 | initial updated by merge
+ INSERT | 2 | 6 | 60 | inserted by merge
+ UPDATE | 1 | 8 | 770 | initial updated by merge
+ INSERT | 2 | 8 | 80 | inserted by merge
+ UPDATE | 1 | 10 | 990 | initial updated by merge
+ INSERT | 2 | 10 | 100 | inserted by merge
+ UPDATE | 1 | 12 | 1210 | initial updated by merge
+ INSERT | 2 | 12 | 120 | inserted by merge
+ UPDATE | 1 | 14 | 1430 | initial updated by merge
+ INSERT | 2 | 14 | 140 | inserted by merge
+(14 rows)
+
SELECT * FROM pa_target ORDER BY tid;
tid | balance | val
-----+---------+--------------------------
@@ -1798,7 +2010,21 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge');
+ INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge')
+ RETURNING pg_merge_action(), pg_merge_when_clause(), t.*;
+ pg_merge_action | pg_merge_when_clause | logts | tid | balance | val
+-----------------+----------------------+--------------------------+-----+---------+--------------------------
+ UPDATE | 1 | Tue Jan 31 00:00:00 2017 | 1 | 110 | initial updated by merge
+ UPDATE | 1 | Tue Feb 28 00:00:00 2017 | 2 | 220 | initial updated by merge
+ INSERT | 2 | Sun Jan 15 00:00:00 2017 | 3 | 30 | inserted by merge
+ UPDATE | 1 | Tue Jan 31 00:00:00 2017 | 4 | 440 | initial updated by merge
+ UPDATE | 1 | Tue Feb 28 00:00:00 2017 | 5 | 550 | initial updated by merge
+ INSERT | 2 | Sun Jan 15 00:00:00 2017 | 6 | 60 | inserted by merge
+ UPDATE | 1 | Tue Jan 31 00:00:00 2017 | 7 | 770 | initial updated by merge
+ UPDATE | 1 | Tue Feb 28 00:00:00 2017 | 8 | 880 | initial updated by merge
+ INSERT | 2 | Sun Jan 15 00:00:00 2017 | 9 | 90 | inserted by merge
+(9 rows)
+
SELECT * FROM pa_target ORDER BY tid;
logts | tid | balance | val
--------------------------+-----+---------+--------------------------
diff --git a/src/test/regress/sql/merge.sql b/src/test/regress/sql/merge.sql
new file mode 100644
index cae6902..e0f2442
--- 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
@@ -857,17 +857,145 @@ 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)
+ INSERT (balance, tid) VALUES (balance + delta, sid)
WHEN MATCHED AND tid < 2 THEN
- DELETE
-RETURNING *;
+ DELETE
+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();
+
+-- RETURNING in CTEs
+CREATE TABLE sq_target_merge_log (tid integer NOT NULL, last_change text);
+INSERT INTO sq_target_merge_log VALUES (1, 'Original value');
+BEGIN;
+WITH m AS (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 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
+), m2 AS (
+ MERGE INTO sq_target_merge_log l
+ USING m
+ ON l.tid = m.tid
+ WHEN MATCHED THEN
+ UPDATE SET last_change = description
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (m.tid, description)
+ RETURNING merge_action, pg_merge_action() AS merge_log_action, l.*
+)
+SELECT * FROM m2;
+SELECT * FROM sq_target_merge_log ORDER BY tid;
+ROLLBACK;
+
+-- COPY (MERGE ... RETURNING) TO ...
+BEGIN;
+COPY (
+ MERGE INTO sq_target t
+ USING v
+ ON tid = sid
+ 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 pg_merge_action(), t.*
+) TO stdout;
+ROLLBACK;
+
+-- SQL function with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT action text, OUT tid int, OUT new_balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid > 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING pg_merge_action(), t.*;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT action, tid, new_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ROLLBACK;
+
+-- SQL SRF with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_sq_source_into_sq_target()
+RETURNS TABLE (action text, tid int, balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 pg_merge_action(), t.*;
+$$;
+SELECT * FROM merge_sq_source_into_sq_target();
+ROLLBACK;
+
+-- PL/pgSQL function with MERGE ... RETURNING ... INTO
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT r_action text, OUT r_tid int, OUT r_balance int)
+LANGUAGE plpgsql AS
+$$
+BEGIN
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid > 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING pg_merge_action(), t.* INTO r_action, r_tid, r_balance;
+END;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT r_action, r_tid, r_balance FROM merge_into_sq_target(sid, balance, delta)) m;
ROLLBACK;
-- EXPLAIN
@@ -966,7 +1094,7 @@ WHEN MATCHED THEN
SELECT * FROM sq_target WHERE tid = 1;
ROLLBACK;
-DROP TABLE sq_target, sq_source CASCADE;
+DROP TABLE sq_target, sq_target_merge_log, sq_source CASCADE;
CREATE TABLE pa_target (tid integer, balance float, val text)
PARTITION BY LIST (tid);
@@ -1033,6 +1161,17 @@ SELECT merge_func();
SELECT * FROM pa_target ORDER BY tid;
ROLLBACK;
+-- update partition key to partition not initially scanned
+BEGIN;
+MERGE INTO pa_target t
+ USING pa_source s
+ ON t.tid = s.sid AND t.tid = 1
+ WHEN MATCHED THEN
+ UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
+ RETURNING pg_merge_action(), pg_merge_when_clause(), t.*;
+SELECT * FROM pa_target ORDER BY tid;
+ROLLBACK;
+
DROP TABLE pa_target CASCADE;
-- The target table is partitioned in the same way, but this time by attaching
@@ -1091,7 +1230,8 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (sid, delta, 'inserted by merge');
+ INSERT VALUES (sid, delta, 'inserted by merge')
+ RETURNING pg_merge_action(), pg_merge_when_clause(), t.*;
SELECT * FROM pa_target ORDER BY tid;
ROLLBACK;
@@ -1145,7 +1285,8 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge');
+ INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge')
+ RETURNING pg_merge_action(), pg_merge_when_clause(), t.*;
SELECT * FROM pa_target ORDER BY tid;
ROLLBACK;
On Mon, 13 Mar 2023 at 13:36, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
And another rebase.
I ran out of cycles to pursue the MERGE patches in v16, but hopefully
I can make more progress in v17.
Looking at this one with fresh eyes, it looks mostly in good shape. To
recap, this adds support for the RETURNING clause in MERGE, together
with new support functions pg_merge_action() and
pg_merge_when_clause() that can be used in the RETURNING list of MERGE
to retrieve the kind of action (INSERT/UPDATE/DELETE), and the index
of the WHEN clause executed for each row merged. In addition,
RETURNING support allows MERGE to be used as the source query in COPY
TO and WITH queries.
One minor annoyance is that, due to the way that pg_merge_action() and
pg_merge_when_clause() require access to the MergeActionState, they
only work if they appear directly in the RETURNING list. They can't,
for example, appear in a subquery in the RETURNING list, and I don't
see an easy way round that limitation.
Attached is an updated patch with some cosmetic updates, plus updated
ruleutils support.
Regards,
Dean
Attachments:
support-merge-returning-v7.patchtext/x-patch; charset=US-ASCII; name=support-merge-returning-v7.patchDownload
diff --git a/doc/src/sgml/dml.sgml b/doc/src/sgml/dml.sgml
new file mode 100644
index cbbc5e2..ff2a827
--- a/doc/src/sgml/dml.sgml
+++ b/doc/src/sgml/dml.sgml
@@ -283,10 +283,15 @@ DELETE FROM products;
<secondary>RETURNING</secondary>
</indexterm>
+ <indexterm zone="dml-returning">
+ <primary>MERGE</primary>
+ <secondary>RETURNING</secondary>
+ </indexterm>
+
<para>
Sometimes it is useful to obtain data from modified rows while they are
being manipulated. The <command>INSERT</command>, <command>UPDATE</command>,
- and <command>DELETE</command> commands all have an
+ <command>DELETE</command>, and <command>MERGE</command> commands all have an
optional <literal>RETURNING</literal> clause that supports this. Use
of <literal>RETURNING</literal> avoids performing an extra database query to
collect the data, and is especially valuable when it would otherwise be
@@ -339,6 +344,24 @@ DELETE FROM products
</programlisting>
</para>
+ <para>
+ In a <command>MERGE</command>, the data available to <literal>RETURNING</literal> is
+ the content of the source row plus the content of the inserted, updated, or
+ deleted target row. In addition, the merge support functions
+ <xref linkend="pg-merge-action"/> and <xref linkend="pg-merge-when-clause"/>
+ may be used to return information about which merge action was executed for
+ each row. Since it is quite common for the source and target to have many
+ of the same columns, specifying <literal>RETURNING *</literal> can lead to a
+ lot of duplicated columns, so it is often more useful to just return the
+ target row. For example:
+<programlisting>
+MERGE INTO products p USING new_products n ON p.product_no = n.product_no
+ WHEN NOT MATCHED THEN INSERT VALUES (n.product_no, n.name, n.price)
+ WHEN MATCHED THEN UPDATE SET name = n.name, price = n.price
+ RETURNING pg_merge_action(), p.*;
+</programlisting>
+ </para>
+
<para>
If there are triggers (<xref linkend="triggers"/>) on the target table,
the data available to <literal>RETURNING</literal> is the row as modified by
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
new file mode 100644
index 5a47ce4..ca1b3cc
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -21705,6 +21705,100 @@ SELECT count(*) FROM sometable;
</sect1>
+ <sect1 id="functions-merge-support">
+ <title>Merge Support Functions</title>
+
+ <indexterm>
+ <primary>MERGE</primary>
+ <secondary>RETURNING</secondary>
+ </indexterm>
+
+ <para>
+ The merge support functions shown in
+ <xref linkend="functions-merge-support-table"/> may be used in the
+ <literal>RETURNING</literal> list of a <xref linkend="sql-merge"/> command
+ to return additional information about the action taken for each row.
+ </para>
+
+ <table id="functions-merge-support-table">
+ <title>Merge Support Functions</title>
+
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ Function
+ </para>
+ <para>
+ Description
+ </para></entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm id="pg-merge-action">
+ <primary>pg_merge_action</primary>
+ </indexterm>
+ <function>pg_merge_action</function> ( )
+ <returnvalue>text</returnvalue>
+ </para>
+ <para>
+ Returns the action performed on the current row (<literal>'INSERT'</literal>,
+ <literal>'UPDATE'</literal>, or <literal>'DELETE'</literal>).
+ </para></entry>
+ </row>
+
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm id="pg-merge-when-clause">
+ <primary>pg_merge_when_clause</primary>
+ </indexterm>
+ <function>pg_merge_when_clause</function> ( )
+ <returnvalue>integer</returnvalue>
+ </para>
+ <para>
+ Returns the 1-based index of the <literal>WHEN</literal> clause executed
+ for the current row.
+ </para></entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+
+ <para>
+ Example:
+<screen><![CDATA[
+MERGE INTO products p
+ USING stock s ON p.product_id = s.product_id
+ WHEN MATCHED AND s.quantity > 0 THEN
+ UPDATE SET in_stock = true, quantity = s.quantity
+ WHEN MATCHED THEN
+ UPDATE SET in_stock = false, quantity = 0
+ WHEN NOT MATCHED THEN
+ INSERT (product_id, in_stock, quantity)
+ VALUES (s.product_id, true, s.quantity)
+ RETURNING pg_merge_action() AS action,
+ pg_merge_when_clause() AS when_clause,
+ p.*;
+
+ action | when_clause | product_id | in_stock | quantity
+--------+-------------+------------+----------+----------
+ UPDATE | 1 | 1001 | t | 50
+ UPDATE | 2 | 1002 | f | 0
+ INSERT | 3 | 1003 | t | 10
+]]></screen>
+ </para>
+
+ <para>
+ Note that these functions can only be used in the <literal>RETURNING</literal>
+ list of a <command>MERGE</command> command. It is an error to use them in
+ any other part of a query, or in a subquery.
+ </para>
+
+ </sect1>
+
<sect1 id="functions-subquery">
<title>Subquery Expressions</title>
diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml
new file mode 100644
index fe8def4..51a15ca
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -1387,9 +1387,9 @@
to a <glossterm linkend="glossary-client">client</glossterm> upon the
completion of an <acronym>SQL</acronym> command, usually a
<command>SELECT</command> but it can be an
- <command>INSERT</command>, <command>UPDATE</command>, or
- <command>DELETE</command> command if the <literal>RETURNING</literal>
- clause is specified.
+ <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command> command if the
+ <literal>RETURNING</literal> clause is specified.
</para>
<para>
The fact that a result set is a relation means that a query can be used
diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml
new file mode 100644
index f55e901..6803240
--- a/doc/src/sgml/plpgsql.sgml
+++ b/doc/src/sgml/plpgsql.sgml
@@ -1020,8 +1020,8 @@ INSERT INTO mytable VALUES (1,'one'), (2
</para>
<para>
- If the command does return rows (for example <command>SELECT</command>,
- or <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>
+ If the command does return rows (for example <command>SELECT</command>, or
+ <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command>
with <literal>RETURNING</literal>), there are two ways to proceed.
When the command will return at most one row, or you only care about
the first row of output, write the command as usual but add
@@ -1149,6 +1149,7 @@ SELECT <replaceable>select_expressions</
INSERT ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
UPDATE ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
DELETE ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
+MERGE ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
</synopsis>
where <replaceable>target</replaceable> can be a record variable, a row
@@ -1159,8 +1160,8 @@ DELETE ... RETURNING <replaceable>expres
<literal>INTO</literal> clause) just as described above,
and the plan is cached in the same way.
This works for <command>SELECT</command>,
- <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command> with
- <literal>RETURNING</literal>, and certain utility commands
+ <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command>
+ with <literal>RETURNING</literal>, and certain utility commands
that return row sets, such as <command>EXPLAIN</command>.
Except for the <literal>INTO</literal> clause, the SQL command is the same
as it would be written outside <application>PL/pgSQL</application>.
@@ -1236,7 +1237,7 @@ END;
</para>
<para>
- For <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command> with
+ For <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command> with
<literal>RETURNING</literal>, <application>PL/pgSQL</application> reports
an error for more than one returned row, even when
<literal>STRICT</literal> is not specified. This is because there
diff --git a/doc/src/sgml/queries.sgml b/doc/src/sgml/queries.sgml
new file mode 100644
index 3f95849..9928373
--- a/doc/src/sgml/queries.sgml
+++ b/doc/src/sgml/queries.sgml
@@ -2062,9 +2062,10 @@ SELECT <replaceable>select_list</replace
Table Expressions or <acronym>CTE</acronym>s, can be thought of as defining
temporary tables that exist just for one query. Each auxiliary statement
in a <literal>WITH</literal> clause can be a <command>SELECT</command>,
- <command>INSERT</command>, <command>UPDATE</command>, or <command>DELETE</command>; and the
+ <command>INSERT</command>, <command>UPDATE</command>, <command>DELETE</command>,
+ or <command>MERGE</command>; and the
<literal>WITH</literal> clause itself is attached to a primary statement that can
- be a <command>SELECT</command>, <command>INSERT</command>, <command>UPDATE</command>,
+ also be a <command>SELECT</command>, <command>INSERT</command>, <command>UPDATE</command>,
<command>DELETE</command>, or <command>MERGE</command>.
</para>
@@ -2598,8 +2599,8 @@ SELECT * FROM w AS w1 JOIN w AS w2 ON w1
<title>Data-Modifying Statements in <literal>WITH</literal></title>
<para>
- You can use most data-modifying statements (<command>INSERT</command>,
- <command>UPDATE</command>, or <command>DELETE</command>, but not
+ You can use data-modifying statements (<command>INSERT</command>,
+ <command>UPDATE</command>, <command>DELETE</command>, or
<command>MERGE</command>) in <literal>WITH</literal>. This
allows you to perform several different operations in the same query.
An example is:
diff --git a/doc/src/sgml/ref/copy.sgml b/doc/src/sgml/ref/copy.sgml
new file mode 100644
index 5e591ed..dbc76df
--- a/doc/src/sgml/ref/copy.sgml
+++ b/doc/src/sgml/ref/copy.sgml
@@ -122,13 +122,16 @@ COPY { <replaceable class="parameter">ta
A <link linkend="sql-select"><command>SELECT</command></link>,
<link linkend="sql-values"><command>VALUES</command></link>,
<link linkend="sql-insert"><command>INSERT</command></link>,
- <link linkend="sql-update"><command>UPDATE</command></link>, or
- <link linkend="sql-delete"><command>DELETE</command></link> command whose results are to be
- copied. Note that parentheses are required around the query.
+ <link linkend="sql-update"><command>UPDATE</command></link>,
+ <link linkend="sql-delete"><command>DELETE</command></link>, or
+ <link linkend="sql-merge"><command>MERGE</command></link> command
+ whose results are to be copied. Note that parentheses are required
+ around the query.
</para>
<para>
- For <command>INSERT</command>, <command>UPDATE</command> and
- <command>DELETE</command> queries a RETURNING clause must be provided,
+ For <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, and <command>MERGE</command> queries a
+ RETURNING clause must be provided,
and the target relation must not have a conditional rule, nor
an <literal>ALSO</literal> rule, nor an <literal>INSTEAD</literal> rule
that expands to multiple statements.
diff --git a/doc/src/sgml/ref/merge.sgml b/doc/src/sgml/ref/merge.sgml
new file mode 100644
index 0995fe0..7eb641a
--- a/doc/src/sgml/ref/merge.sgml
+++ b/doc/src/sgml/ref/merge.sgml
@@ -25,6 +25,7 @@ PostgreSQL documentation
MERGE INTO [ ONLY ] <replaceable class="parameter">target_table_name</replaceable> [ * ] [ [ AS ] <replaceable class="parameter">target_alias</replaceable> ]
USING <replaceable class="parameter">data_source</replaceable> ON <replaceable class="parameter">join_condition</replaceable>
<replaceable class="parameter">when_clause</replaceable> [...]
+[ RETURNING * | <replaceable class="parameter">output_expression</replaceable> [ [ AS ] <replaceable class="parameter">output_name</replaceable> ] [, ...] ]
<phrase>where <replaceable class="parameter">data_source</replaceable> is:</phrase>
@@ -95,6 +96,20 @@ DELETE
</para>
<para>
+ The optional <literal>RETURNING</literal> clause causes <command>MERGE</command>
+ to compute and return value(s) based on each row inserted, updated, or
+ deleted. Any expression using the source or target table's columns, or the
+ merge support functions <xref linkend="pg-merge-action"/> and
+ <xref linkend="pg-merge-when-clause"/> can be computed. When an
+ <command>INSERT</command> or <command>UPDATE</command> action is performed,
+ the new values of the target table's columns are used. When a
+ <command>DELETE</command> is performed, the old values of the target table's
+ columns are used. The syntax of the <literal>RETURNING</literal> list is
+ identical to that of the output list of <command>SELECT</command>.
+
+ </para>
+
+ <para>
There is no separate <literal>MERGE</literal> privilege.
If you specify an update action, you must have the
<literal>UPDATE</literal> privilege on the column(s)
@@ -125,6 +140,18 @@ DELETE
<variablelist>
<varlistentry>
+ <term><replaceable class="parameter">with_query</replaceable></term>
+ <listitem>
+ <para>
+ The <literal>WITH</literal> clause allows you to specify one or more
+ subqueries that can be referenced by name in the <command>MERGE</command>
+ query. See <xref linkend="queries-with"/> and <xref linkend="sql-select"/>
+ for details.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
<term><replaceable class="parameter">target_table_name</replaceable></term>
<listitem>
<para>
@@ -399,13 +426,30 @@ DELETE
</varlistentry>
<varlistentry>
- <term><replaceable class="parameter">with_query</replaceable></term>
+ <term><replaceable class="parameter">output_expression</replaceable></term>
<listitem>
<para>
- The <literal>WITH</literal> clause allows you to specify one or more
- subqueries that can be referenced by name in the <command>MERGE</command>
- query. See <xref linkend="queries-with"/> and <xref linkend="sql-select"/>
- for details.
+ An expression to be computed and returned by the <command>MERGE</command>
+ command after each row is merged. The expression can use any columns of
+ the source or target tables, or the merge support functions
+ <xref linkend="pg-merge-action"/> and <xref linkend="pg-merge-when-clause"/>.
+ </para>
+ <para>
+ Writing <literal>*</literal> will return all columns from the source
+ table, followed by all columns from the target table. Often this will
+ lead to a lot of duplication, since it is common for the source and
+ target tables to have a lot of the same columns. This can be avoided by
+ qualifying the <literal>*</literal> with the name or alias of the source
+ or target table.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><replaceable class="parameter">output_name</replaceable></term>
+ <listitem>
+ <para>
+ A name to use for a returned column.
</para>
</listitem>
</varlistentry>
@@ -428,6 +472,13 @@ MERGE <replaceable class="parameter">tot
were changed in any way.
</para>
+ <para>
+ If the <command>MERGE</command> command contains a <literal>RETURNING</literal>
+ clause, the result will be similar to that of a <command>SELECT</command>
+ statement containing the columns and values defined in the
+ <literal>RETURNING</literal> list, computed over the row(s) inserted, updated,
+ or deleted by the command.
+ </para>
</refsect1>
<refsect1>
@@ -544,13 +595,6 @@ MERGE <replaceable class="parameter">tot
</para>
<para>
- There is no <literal>RETURNING</literal> clause with
- <command>MERGE</command>. Actions of <command>INSERT</command>,
- <command>UPDATE</command> and <command>DELETE</command> cannot contain
- <literal>RETURNING</literal> or <literal>WITH</literal> clauses.
- </para>
-
- <para>
When <command>MERGE</command> is run concurrently with other commands
that modify the target table, the usual transaction isolation rules
apply; see <xref linkend="transaction-iso"/> for an explanation
@@ -602,7 +646,8 @@ WHEN NOT MATCHED THEN
<para>
Attempt to insert a new stock item along with the quantity of stock. If
the item already exists, instead update the stock count of the existing
- item. Don't allow entries that have zero stock.
+ item. Don't allow entries that have zero stock. Return details of all
+ changes made.
<programlisting>
MERGE INTO wines w
USING wine_stock_changes s
@@ -612,7 +657,8 @@ WHEN NOT MATCHED AND s.stock_delta > 0 T
WHEN MATCHED AND w.stock + s.stock_delta > 0 THEN
UPDATE SET stock = w.stock + s.stock_delta
WHEN MATCHED THEN
- DELETE;
+ DELETE
+RETURNING pg_merge_action(), w.*;
</programlisting>
The <literal>wine_stock_changes</literal> table might be, for example, a
@@ -627,7 +673,8 @@ WHEN MATCHED THEN
This command conforms to the <acronym>SQL</acronym> standard.
</para>
<para>
- The WITH clause and <literal>DO NOTHING</literal> action are extensions to
+ The WITH clause, <literal>DO NOTHING</literal> action, and
+ <literal>RETURNING</literal> clause are extensions to
the <acronym>SQL</acronym> standard.
</para>
</refsect1>
diff --git a/doc/src/sgml/ref/select.sgml b/doc/src/sgml/ref/select.sgml
new file mode 100644
index 0ee0cc7..f81bdea
--- a/doc/src/sgml/ref/select.sgml
+++ b/doc/src/sgml/ref/select.sgml
@@ -74,7 +74,7 @@ SELECT [ ALL | DISTINCT [ ON ( <replacea
<phrase>and <replaceable class="parameter">with_query</replaceable> is:</phrase>
- <replaceable class="parameter">with_query_name</replaceable> [ ( <replaceable class="parameter">column_name</replaceable> [, ...] ) ] AS [ [ NOT ] MATERIALIZED ] ( <replaceable class="parameter">select</replaceable> | <replaceable class="parameter">values</replaceable> | <replaceable class="parameter">insert</replaceable> | <replaceable class="parameter">update</replaceable> | <replaceable class="parameter">delete</replaceable> )
+ <replaceable class="parameter">with_query_name</replaceable> [ ( <replaceable class="parameter">column_name</replaceable> [, ...] ) ] AS [ [ NOT ] MATERIALIZED ] ( <replaceable class="parameter">select</replaceable> | <replaceable class="parameter">values</replaceable> | <replaceable class="parameter">insert</replaceable> | <replaceable class="parameter">update</replaceable> | <replaceable class="parameter">delete</replaceable> | <replaceable class="parameter">merge</replaceable> )
[ SEARCH { BREADTH | DEPTH } FIRST BY <replaceable>column_name</replaceable> [, ...] SET <replaceable>search_seq_col_name</replaceable> ]
[ CYCLE <replaceable>column_name</replaceable> [, ...] SET <replaceable>cycle_mark_col_name</replaceable> [ TO <replaceable>cycle_mark_value</replaceable> DEFAULT <replaceable>cycle_mark_default</replaceable> ] USING <replaceable>cycle_path_col_name</replaceable> ]
@@ -227,10 +227,10 @@ TABLE [ ONLY ] <replaceable class="param
The subqueries effectively act as temporary tables or views
for the duration of the primary query.
Each subquery can be a <command>SELECT</command>, <command>TABLE</command>, <command>VALUES</command>,
- <command>INSERT</command>, <command>UPDATE</command> or
- <command>DELETE</command> statement.
+ <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command> statement.
When writing a data-modifying statement (<command>INSERT</command>,
- <command>UPDATE</command> or <command>DELETE</command>) in
+ <command>UPDATE</command>, <command>DELETE</command>, or <command>MERGE</command>) in
<literal>WITH</literal>, it is usual to include a <literal>RETURNING</literal> clause.
It is the output of <literal>RETURNING</literal>, <emphasis>not</emphasis> the underlying
table that the statement modifies, that forms the temporary table that is
@@ -2179,7 +2179,8 @@ SELECT 2+2;
<para>
<productname>PostgreSQL</productname> allows <command>INSERT</command>,
- <command>UPDATE</command>, and <command>DELETE</command> to be used as <literal>WITH</literal>
+ <command>UPDATE</command>, <command>DELETE</command>, and
+ <command>MERGE</command> to be used as <literal>WITH</literal>
queries. This is not found in the SQL standard.
</para>
</refsect2>
diff --git a/doc/src/sgml/rowtypes.sgml b/doc/src/sgml/rowtypes.sgml
new file mode 100644
index 4d86f97..bbeac84
--- a/doc/src/sgml/rowtypes.sgml
+++ b/doc/src/sgml/rowtypes.sgml
@@ -348,7 +348,7 @@ SELECT m.* FROM some_table, LATERAL myfu
column expansion of this kind when it appears at the top level of
a <link linkend="queries-select-lists"><command>SELECT</command> output
list</link>, a <link linkend="dml-returning"><literal>RETURNING</literal>
- list</link> in <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>,
+ list</link> in <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command>,
a <link linkend="queries-values"><literal>VALUES</literal> clause</link>, or
a <link linkend="sql-syntax-row-constructors">row constructor</link>.
In all other contexts (including when nested inside one of those
diff --git a/doc/src/sgml/spi.sgml b/doc/src/sgml/spi.sgml
new file mode 100644
index 651930a..ad0a9b1
--- a/doc/src/sgml/spi.sgml
+++ b/doc/src/sgml/spi.sgml
@@ -301,8 +301,9 @@ SPI_execute("INSERT INTO foo SELECT * FR
is returned in the global variable <varname>SPI_processed</varname>.
If the return value of the function is <symbol>SPI_OK_SELECT</symbol>,
<symbol>SPI_OK_INSERT_RETURNING</symbol>,
- <symbol>SPI_OK_DELETE_RETURNING</symbol>, or
- <symbol>SPI_OK_UPDATE_RETURNING</symbol>,
+ <symbol>SPI_OK_DELETE_RETURNING</symbol>,
+ <symbol>SPI_OK_UPDATE_RETURNING</symbol>, or
+ <symbol>SPI_OK_MERGE_RETURNING</symbol>
then you can use the
global pointer <literal>SPITupleTable *SPI_tuptable</literal> to
access the result rows. Some utility commands (such as
@@ -472,6 +473,15 @@ typedef struct SPITupleTable
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><symbol>SPI_OK_MERGE_RETURNING</symbol></term>
+ <listitem>
+ <para>
+ if a <command>MERGE RETURNING</command> was executed
+ </para>
+ </listitem>
+ </varlistentry>
<varlistentry>
<term><symbol>SPI_OK_UTILITY</symbol></term>
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
new file mode 100644
index 9620ea9..4f68efb
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -177,7 +177,8 @@
statements separated by semicolons. A semicolon after the last
statement is optional. Unless the function is declared to return
<type>void</type>, the last statement must be a <command>SELECT</command>,
- or an <command>INSERT</command>, <command>UPDATE</command>, or <command>DELETE</command>
+ or an <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command>
that has a <literal>RETURNING</literal> clause.
</para>
@@ -1236,8 +1237,9 @@ SELECT x, CASE WHEN x > 0 THEN genera
<note>
<para>
- If a function's last command is <command>INSERT</command>, <command>UPDATE</command>,
- or <command>DELETE</command> with <literal>RETURNING</literal>, that command will
+ If a function's last command is <command>INSERT</command>,
+ <command>UPDATE</command>, <command>DELETE</command>, or
+ <command>MERGE</command> with <literal>RETURNING</literal>, that command will
always be executed to completion, even if the function is not declared
with <literal>SETOF</literal> or the calling query does not fetch all the
result rows. Any extra rows produced by the <literal>RETURNING</literal>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
new file mode 100644
index f14fae3..05f2e5b
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -283,12 +283,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 9e4b243..cf56f9f
--- 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 e6e6168..5ea6c14
--- 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"
@@ -2603,6 +2604,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 eb8a87f..b9bd03c
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -612,8 +612,8 @@ ExecInitPartitionInfo(ModifyTableState *
* Build the RETURNING projection for the partition. Note that we didn't
* build the returningList for partitions within the planner, but simple
* translation of varattnos will suffice. This only occurs for the INSERT
- * case or in the case of UPDATE tuple routing where we didn't find a
- * result rel to reuse.
+ * case or in the case of UPDATE/MERGE tuple routing where we didn't find
+ * a result rel to reuse.
*/
if (node && node->returningLists != NIL)
{
@@ -622,12 +622,14 @@ ExecInitPartitionInfo(ModifyTableState *
List *returningList;
/* See the comment above for WCO lists. */
- /* (except no RETURNING support for MERGE yet) */
Assert((node->operation == CMD_INSERT &&
list_length(node->returningLists) == 1 &&
list_length(node->resultRelations) == 1) ||
(node->operation == CMD_UPDATE &&
list_length(node->returningLists) ==
+ list_length(node->resultRelations)) ||
+ (node->operation == CMD_MERGE &&
+ list_length(node->returningLists) ==
list_length(node->resultRelations)));
/*
diff --git a/src/backend/executor/functions.c b/src/backend/executor/functions.c
new file mode 100644
index f55424e..f99f15f
--- a/src/backend/executor/functions.c
+++ b/src/backend/executor/functions.c
@@ -1660,8 +1660,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
@@ -1679,7 +1679,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 2a5fec8..b4c61ec
--- 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"
@@ -88,9 +87,6 @@ typedef struct ModifyTableContext
*/
TupleTableSlot *planSlot;
- /* MERGE specific */
- MergeActionState *relaction; /* MERGE action in progress */
-
/*
* Information about the changes that were made concurrently to a tuple
* being updated or deleted
@@ -153,13 +149,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);
/*
@@ -979,7 +976,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;
@@ -1828,7 +1825,7 @@ ExecCrossPartitionUpdate(ModifyTableCont
* additional rechecking, and might end up executing a different
* action entirely).
*/
- if (context->relaction != NULL)
+ if (mtstate->mt_merge_action != NULL)
return false;
else if (TupIsNull(epqslot))
return true;
@@ -2069,7 +2066,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;
/*
@@ -2712,6 +2709,7 @@ static TupleTableSlot *
ExecMerge(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
ItemPointer tupleid, bool canSetTag)
{
+ TupleTableSlot *rslot = NULL;
bool matched;
/*-----
@@ -2759,18 +2757,18 @@ 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()
- * returned "false", indicating the previously MATCHED tuple no longer
- * matches.
+ * Deal with the NOT MATCHED case (either a NOT MATCHED tuple from the
+ * join, or a previously MATCHED tuple for which ExecMergeMatched() set
+ * "matched" to false, indicating that it no longer matches).
*/
if (!matched)
- ExecMergeNotMatched(context, resultRelInfo, canSetTag);
+ rslot = ExecMergeNotMatched(context, resultRelInfo, canSetTag);
- /* No RETURNING support yet */
- return NULL;
+ return rslot;
}
/*
@@ -2780,8 +2778,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.
*
@@ -2790,15 +2788,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;
@@ -2810,7 +2809,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
@@ -2888,12 +2890,15 @@ lmerge_matched:
*/
newslot = ExecProject(relaction->mas_proj);
- context->relaction = relaction;
+ mtstate->mt_merge_action = relaction;
if (!ExecUpdatePrologue(context, resultRelInfo,
tupleid, NULL, newslot, &result))
{
if (result == TM_Ok)
- return true; /* "do nothing" */
+ {
+ *matched = true;
+ return NULL; /* "do nothing" */
+ }
break; /* concurrent update/delete */
}
result = ExecUpdateAct(context, resultRelInfo, tupleid, NULL,
@@ -2907,12 +2912,15 @@ lmerge_matched:
break;
case CMD_DELETE:
- context->relaction = relaction;
+ mtstate->mt_merge_action = relaction;
if (!ExecDeletePrologue(context, resultRelInfo, tupleid,
NULL, NULL, &result))
{
if (result == TM_Ok)
- return true; /* "do nothing" */
+ {
+ *matched = true;
+ return NULL; /* "do nothing" */
+ }
break; /* concurrent update/delete */
}
result = ExecDeleteAct(context, resultRelInfo, tupleid, false);
@@ -2968,7 +2976,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:
{
@@ -3014,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
@@ -3055,7 +3070,8 @@ lmerge_matched:
* tuple already deleted; tell caller to run NOT
* MATCHED actions
*/
- return false;
+ *matched = false;
+ return NULL;
case TM_SelfModified:
@@ -3075,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;
}
}
@@ -3093,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.
@@ -3103,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;
/*
@@ -3165,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:
@@ -3184,6 +3228,8 @@ ExecMergeNotMatched(ModifyTableContext *
*/
break;
}
+
+ return rslot;
}
/*
@@ -3361,6 +3407,64 @@ ExecInitMergeTupleSlots(ModifyTableState
}
/*
+ * 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->index);
+
+ PG_RETURN_NULL();
+}
+
+/*
* Process BEFORE EACH STATEMENT triggers
*/
static void
@@ -3646,8 +3750,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");
@@ -3724,8 +3837,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");
@@ -3817,7 +3939,7 @@ ExecModifyTable(PlanState *pstate)
}
slot = ExecGetUpdateNewTuple(resultRelInfo, context.planSlot,
oldSlot);
- context.relaction = NULL;
+ node->mt_merge_action = NULL;
/* Now apply the update. */
slot = ExecUpdate(&context, resultRelInfo, tupleid, oldtuple,
diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c
new file mode 100644
index 3397568..b9f443c
--- a/src/backend/executor/spi.c
+++ b/src/backend/executor/spi.c
@@ -2033,6 +2033,8 @@ SPI_result_code_string(int code)
return "SPI_OK_TD_REGISTER";
case SPI_OK_MERGE:
return "SPI_OK_MERGE";
+ case SPI_OK_MERGE_RETURNING:
+ return "SPI_OK_MERGE_RETURNING";
}
/* Unrecognized code ... return something useful ... */
sprintf(buf, "Unrecognized SPI code %d", code);
@@ -2886,7 +2888,10 @@ _SPI_pquery(QueryDesc *queryDesc, bool f
res = SPI_OK_UPDATE;
break;
case CMD_MERGE:
- res = SPI_OK_MERGE;
+ if (queryDesc->plannedstmt->hasReturning)
+ res = SPI_OK_MERGE_RETURNING;
+ else
+ res = SPI_OK_MERGE;
break;
default:
return SPI_ERROR_OPUNKNOWN;
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
new file mode 100644
index 4006632..6794511
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -74,7 +74,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,
@@ -515,7 +514,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;
@@ -942,7 +942,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;
@@ -2407,7 +2408,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
@@ -2501,10 +2503,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;
@@ -2521,7 +2524,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 39ab7ea..5455607
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -12249,6 +12249,7 @@ MergeStmt:
USING table_ref
ON a_expr
merge_when_list
+ returning_clause
{
MergeStmt *m = makeNode(MergeStmt);
@@ -12257,6 +12258,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 85cd47b..e72c37b
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -456,6 +456,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 346fd27..38fc8a9
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -525,6 +525,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:
@@ -1751,6 +1752,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:
@@ -3099,6 +3101,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 b3f0b6a..5cc933e
--- 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.
*/
@@ -2599,6 +2609,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 91b1156..33852e9
--- 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.
@@ -254,6 +258,7 @@ transformMergeStmt(ParseState *pstate, M
MergeAction *action;
action = makeNode(MergeAction);
+ action->index = foreach_current_index(l) + 1;
action->commandType = mergeWhenClause->commandType;
action->matched = mergeWhenClause->matched;
@@ -390,9 +395,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 41d6049..794530b
--- a/src/backend/parser/parse_relation.c
+++ b/src/backend/parser/parse_relation.c
@@ -2345,9 +2345,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 b486ab5..1bd0ce2
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -3629,9 +3629,9 @@ RewriteQuery(Query *parsetree, List *rew
ListCell *lc1;
/*
- * First, recursively process any insert/update/delete statements in WITH
- * clauses. (We have to do this first because the WITH clauses may get
- * copied into rule actions below.)
+ * First, recursively process any insert/update/delete/merge statements in
+ * WITH clauses. (We have to do this first because the WITH clauses may
+ * get copied into rule actions below.)
*/
foreach(lc1, parsetree->cteList)
{
@@ -3656,7 +3656,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 30b51bf..0ae3890
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -2141,11 +2141,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/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
new file mode 100644
index d3a973d..f097d98
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -7183,8 +7183,13 @@ get_merge_query_def(Query *query, depars
appendStringInfoString(buf, "DO NOTHING");
}
- /* No RETURNING support in MERGE yet */
- Assert(query->returningList == NIL);
+ /* Add RETURNING if present */
+ if (query->returningList)
+ {
+ appendContextKeyword(context, " RETURNING",
+ -PRETTYINDENT_STD, PRETTYINDENT_STD, 1);
+ get_target_list(query->returningList, context, NULL, colNamesVisible);
+ }
}
diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c
new file mode 100644
index 5973df2..a1380f8
--- a/src/bin/psql/common.c
+++ b/src/bin/psql/common.c
@@ -982,13 +982,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 6996073..ac3e56b
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12043,4 +12043,14 @@
proname => 'any_value_transfn', prorettype => 'anyelement',
proargtypes => 'anyelement anyelement', prosrc => 'any_value_transfn' },
+# 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..463add8
--- 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(funcid) \
+ ((funcid) == F_PG_MERGE_ACTION || \
+ (funcid) == F_PG_MERGE_WHEN_CLAUSE)
+
#endif /* EXPOSE_TO_CLIENT_CODE */
diff --git a/src/include/executor/spi.h b/src/include/executor/spi.h
new file mode 100644
index d1de139..ed8787d
--- a/src/include/executor/spi.h
+++ b/src/include/executor/spi.h
@@ -97,6 +97,7 @@ typedef struct _SPI_plan *SPIPlanPtr;
#define SPI_OK_REL_UNREGISTER 16
#define SPI_OK_TD_REGISTER 17
#define SPI_OK_MERGE 18
+#define SPI_OK_MERGE_RETURNING 19
#define SPI_OPT_NONATOMIC (1 << 0)
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
new file mode 100644
index cb714f4..6708b6b
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1319,6 +1319,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 b3bec90..1e14f7b
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -1687,6 +1687,7 @@ typedef struct MergeWhenClause
typedef struct MergeAction
{
NodeTag type;
+ int index; /* 1-based index of the clause */
bool matched; /* true=MATCHED, false=NOT MATCHED */
CmdType commandType; /* INSERT/UPDATE/DELETE/DO NOTHING */
/* OVERRIDING clause */
@@ -1913,6 +1914,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 f589112..836819d
--- 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/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
new file mode 100644
index 4b76f76..fb6289c
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -4267,9 +4267,9 @@ exec_stmt_execsql(PLpgSQL_execstate *est
/*
* If we have INTO, then we only need one row back ... but if we have INTO
* STRICT or extra check too_many_rows, ask for two rows, so that we can
- * verify the statement returns only one. INSERT/UPDATE/DELETE are always
- * treated strictly. Without INTO, just run the statement to completion
- * (tcount = 0).
+ * verify the statement returns only one. INSERT/UPDATE/DELETE/MERGE are
+ * always treated strictly. Without INTO, just run the statement to
+ * completion (tcount = 0).
*
* We could just ask for two rows always when using INTO, but there are
* some cases where demanding the extra row costs significant time, eg by
@@ -4307,10 +4307,11 @@ exec_stmt_execsql(PLpgSQL_execstate *est
case SPI_OK_INSERT:
case SPI_OK_UPDATE:
case SPI_OK_DELETE:
+ case SPI_OK_MERGE:
case SPI_OK_INSERT_RETURNING:
case SPI_OK_UPDATE_RETURNING:
case SPI_OK_DELETE_RETURNING:
- case SPI_OK_MERGE:
+ case SPI_OK_MERGE_RETURNING:
Assert(stmt->mod_stmt);
exec_set_found(estate, (SPI_processed != 0));
break;
@@ -4489,10 +4490,11 @@ exec_stmt_dynexecute(PLpgSQL_execstate *
case SPI_OK_INSERT:
case SPI_OK_UPDATE:
case SPI_OK_DELETE:
+ case SPI_OK_MERGE:
case SPI_OK_INSERT_RETURNING:
case SPI_OK_UPDATE_RETURNING:
case SPI_OK_DELETE_RETURNING:
- case SPI_OK_MERGE:
+ case SPI_OK_MERGE_RETURNING:
case SPI_OK_UTILITY:
case SPI_OK_REWRITTEN:
break;
diff --git a/src/pl/tcl/pltcl.c b/src/pl/tcl/pltcl.c
new file mode 100644
index e8f9d7b..0c9f81e
--- a/src/pl/tcl/pltcl.c
+++ b/src/pl/tcl/pltcl.c
@@ -2456,6 +2456,7 @@ pltcl_process_SPI_result(Tcl_Interp *int
case SPI_OK_INSERT_RETURNING:
case SPI_OK_DELETE_RETURNING:
case SPI_OK_UPDATE_RETURNING:
+ case SPI_OK_MERGE_RETURNING:
/*
* Process the tuples we got
diff --git a/src/test/regress/expected/merge.out b/src/test/regress/expected/merge.out
new file mode 100644
index 133d421..da4dbc2
--- 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;
@@ -1304,20 +1304,187 @@ 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)
+ 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 *;
- ^
+ DELETE
+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...
+ ^
+-- RETURNING in CTEs
+CREATE TABLE sq_target_merge_log (tid integer NOT NULL, last_change text);
+INSERT INTO sq_target_merge_log VALUES (1, 'Original value');
+BEGIN;
+WITH m AS (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 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
+), m2 AS (
+ MERGE INTO sq_target_merge_log l
+ USING m
+ ON l.tid = m.tid
+ WHEN MATCHED THEN
+ UPDATE SET last_change = description
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (m.tid, description)
+ RETURNING merge_action, pg_merge_action() AS merge_log_action, l.*
+)
+SELECT * FROM m2;
+ merge_action | merge_log_action | tid | last_change
+--------------+------------------+-----+---------------------
+ DELETE | UPDATE | 1 | Removed (1,100)
+ UPDATE | INSERT | 2 | Added 20 to balance
+ INSERT | INSERT | 4 | Inserted (4,40)
+(3 rows)
+
+SELECT * FROM sq_target_merge_log ORDER BY tid;
+ tid | last_change
+-----+---------------------
+ 1 | Removed (1,100)
+ 2 | Added 20 to balance
+ 4 | Inserted (4,40)
+(3 rows)
+
+ROLLBACK;
+-- COPY (MERGE ... RETURNING) TO ...
+BEGIN;
+COPY (
+ MERGE INTO sq_target t
+ USING v
+ ON tid = sid
+ 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 pg_merge_action(), t.*
+) TO stdout;
+DELETE 1 100
+ROLLBACK;
+-- SQL function with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT action text, OUT tid int, OUT new_balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid > 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING pg_merge_action(), t.*;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT action, tid, new_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ action | tid | new_balance
+--------+-----+-------------
+ DELETE | 1 | 100
+ UPDATE | 3 | 320
+ INSERT | 4 | 110
+(3 rows)
+
+ROLLBACK;
+-- SQL SRF with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_sq_source_into_sq_target()
+RETURNS TABLE (action text, tid int, balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 pg_merge_action(), t.*;
+$$;
+SELECT * FROM merge_sq_source_into_sq_target();
+ action | tid | balance
+--------+-----+---------
+ DELETE | 1 | 100
+ INSERT | 4 | 40
+(2 rows)
+
+ROLLBACK;
+-- PL/pgSQL function with MERGE ... RETURNING ... INTO
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT r_action text, OUT r_tid int, OUT r_balance int)
+LANGUAGE plpgsql AS
+$$
+BEGIN
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid > 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING pg_merge_action(), t.* INTO r_action, r_tid, r_balance;
+END;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT r_action, r_tid, r_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ r_action | r_tid | r_balance
+----------+-------+-----------
+ DELETE | 1 | 100
+ UPDATE | 3 | 320
+ INSERT | 4 | 110
+(3 rows)
+
ROLLBACK;
-- EXPLAIN
CREATE TABLE ex_mtarget (a int, b int)
@@ -1514,7 +1681,7 @@ SELECT * FROM sq_target WHERE tid = 1;
(1 row)
ROLLBACK;
-DROP TABLE sq_target, sq_source CASCADE;
+DROP TABLE sq_target, sq_target_merge_log, sq_source CASCADE;
NOTICE: drop cascades to view v
CREATE TABLE pa_target (tid integer, balance float, val text)
PARTITION BY LIST (tid);
@@ -1640,6 +1807,32 @@ SELECT * FROM pa_target ORDER BY tid;
(14 rows)
ROLLBACK;
+-- update partition key to partition not initially scanned
+BEGIN;
+MERGE INTO pa_target t
+ USING pa_source s
+ ON t.tid = s.sid AND t.tid = 1
+ WHEN MATCHED THEN
+ UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
+ RETURNING pg_merge_action(), pg_merge_when_clause(), t.*;
+ pg_merge_action | pg_merge_when_clause | tid | balance | val
+-----------------+----------------------+-----+---------+--------------------------
+ UPDATE | 1 | 2 | 110 | initial updated by merge
+(1 row)
+
+SELECT * FROM pa_target ORDER BY tid;
+ tid | balance | val
+-----+---------+--------------------------
+ 2 | 110 | initial updated by merge
+ 3 | 300 | initial
+ 5 | 500 | initial
+ 7 | 700 | initial
+ 9 | 900 | initial
+ 11 | 1100 | initial
+ 13 | 1300 | initial
+(7 rows)
+
+ROLLBACK;
DROP TABLE pa_target CASCADE;
-- The target table is partitioned in the same way, but this time by attaching
-- partitions which have columns in different order, dropped columns etc.
@@ -1731,7 +1924,26 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (sid, delta, 'inserted by merge');
+ INSERT VALUES (sid, delta, 'inserted by merge')
+ RETURNING pg_merge_action(), pg_merge_when_clause(), t.*;
+ pg_merge_action | pg_merge_when_clause | tid | balance | val
+-----------------+----------------------+-----+---------+--------------------------
+ UPDATE | 1 | 2 | 110 | initial updated by merge
+ INSERT | 2 | 2 | 20 | inserted by merge
+ UPDATE | 1 | 4 | 330 | initial updated by merge
+ INSERT | 2 | 4 | 40 | inserted by merge
+ UPDATE | 1 | 6 | 550 | initial updated by merge
+ INSERT | 2 | 6 | 60 | inserted by merge
+ UPDATE | 1 | 8 | 770 | initial updated by merge
+ INSERT | 2 | 8 | 80 | inserted by merge
+ UPDATE | 1 | 10 | 990 | initial updated by merge
+ INSERT | 2 | 10 | 100 | inserted by merge
+ UPDATE | 1 | 12 | 1210 | initial updated by merge
+ INSERT | 2 | 12 | 120 | inserted by merge
+ UPDATE | 1 | 14 | 1430 | initial updated by merge
+ INSERT | 2 | 14 | 140 | inserted by merge
+(14 rows)
+
SELECT * FROM pa_target ORDER BY tid;
tid | balance | val
-----+---------+--------------------------
@@ -1798,7 +2010,21 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge');
+ INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge')
+ RETURNING pg_merge_action(), pg_merge_when_clause(), t.*;
+ pg_merge_action | pg_merge_when_clause | logts | tid | balance | val
+-----------------+----------------------+--------------------------+-----+---------+--------------------------
+ UPDATE | 1 | Tue Jan 31 00:00:00 2017 | 1 | 110 | initial updated by merge
+ UPDATE | 1 | Tue Feb 28 00:00:00 2017 | 2 | 220 | initial updated by merge
+ INSERT | 2 | Sun Jan 15 00:00:00 2017 | 3 | 30 | inserted by merge
+ UPDATE | 1 | Tue Jan 31 00:00:00 2017 | 4 | 440 | initial updated by merge
+ UPDATE | 1 | Tue Feb 28 00:00:00 2017 | 5 | 550 | initial updated by merge
+ INSERT | 2 | Sun Jan 15 00:00:00 2017 | 6 | 60 | inserted by merge
+ UPDATE | 1 | Tue Jan 31 00:00:00 2017 | 7 | 770 | initial updated by merge
+ UPDATE | 1 | Tue Feb 28 00:00:00 2017 | 8 | 880 | initial updated by merge
+ INSERT | 2 | Sun Jan 15 00:00:00 2017 | 9 | 90 | inserted by merge
+(9 rows)
+
SELECT * FROM pa_target ORDER BY tid;
logts | tid | balance | val
--------------------------+-----+---------+--------------------------
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
new file mode 100644
index 7fd81e6..87a3686
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -3590,7 +3590,8 @@ MERGE INTO rule_merge2 t USING (SELECT 1
-- test deparsing
CREATE TABLE sf_target(id int, data text, filling int[]);
CREATE FUNCTION merge_sf_test()
- RETURNS void
+ RETURNS TABLE(pg_merge_action text, when_clause int,
+ a int, b text, id int, data text, filling int[])
LANGUAGE sql
BEGIN ATOMIC
MERGE INTO sf_target t
@@ -3627,11 +3628,13 @@ WHEN NOT MATCHED
VALUES (s.a, s.b, DEFAULT)
WHEN NOT MATCHED
THEN INSERT (filling[1], id)
- VALUES (s.a, s.a);
+ VALUES (s.a, s.a)
+RETURNING
+ pg_merge_action(), pg_merge_when_clause() AS when_clause, *;
END;
\sf merge_sf_test
CREATE OR REPLACE FUNCTION public.merge_sf_test()
- RETURNS void
+ RETURNS TABLE(pg_merge_action text, when_clause integer, a integer, b text, id integer, data text, filling integer[])
LANGUAGE sql
BEGIN ATOMIC
MERGE INTO sf_target t
@@ -3668,7 +3671,14 @@ BEGIN ATOMIC
VALUES (s.a, s.b, DEFAULT)
WHEN NOT MATCHED
THEN INSERT (filling[1], id)
- VALUES (s.a, s.a);
+ VALUES (s.a, s.a)
+ RETURNING pg_merge_action() AS pg_merge_action,
+ pg_merge_when_clause() AS when_clause,
+ s.a,
+ s.b,
+ t.id,
+ t.data,
+ t.filling;
END
DROP FUNCTION merge_sf_test;
DROP TABLE sf_target;
diff --git a/src/test/regress/sql/merge.sql b/src/test/regress/sql/merge.sql
new file mode 100644
index 4cf6db9..ae5d7c2
--- 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
@@ -857,17 +857,145 @@ 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)
+ INSERT (balance, tid) VALUES (balance + delta, sid)
WHEN MATCHED AND tid < 2 THEN
- DELETE
-RETURNING *;
+ DELETE
+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();
+
+-- RETURNING in CTEs
+CREATE TABLE sq_target_merge_log (tid integer NOT NULL, last_change text);
+INSERT INTO sq_target_merge_log VALUES (1, 'Original value');
+BEGIN;
+WITH m AS (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 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
+), m2 AS (
+ MERGE INTO sq_target_merge_log l
+ USING m
+ ON l.tid = m.tid
+ WHEN MATCHED THEN
+ UPDATE SET last_change = description
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (m.tid, description)
+ RETURNING merge_action, pg_merge_action() AS merge_log_action, l.*
+)
+SELECT * FROM m2;
+SELECT * FROM sq_target_merge_log ORDER BY tid;
+ROLLBACK;
+
+-- COPY (MERGE ... RETURNING) TO ...
+BEGIN;
+COPY (
+ MERGE INTO sq_target t
+ USING v
+ ON tid = sid
+ 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 pg_merge_action(), t.*
+) TO stdout;
+ROLLBACK;
+
+-- SQL function with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT action text, OUT tid int, OUT new_balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid > 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING pg_merge_action(), t.*;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT action, tid, new_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ROLLBACK;
+
+-- SQL SRF with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_sq_source_into_sq_target()
+RETURNS TABLE (action text, tid int, balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 pg_merge_action(), t.*;
+$$;
+SELECT * FROM merge_sq_source_into_sq_target();
+ROLLBACK;
+
+-- PL/pgSQL function with MERGE ... RETURNING ... INTO
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT r_action text, OUT r_tid int, OUT r_balance int)
+LANGUAGE plpgsql AS
+$$
+BEGIN
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid > 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING pg_merge_action(), t.* INTO r_action, r_tid, r_balance;
+END;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT r_action, r_tid, r_balance FROM merge_into_sq_target(sid, balance, delta)) m;
ROLLBACK;
-- EXPLAIN
@@ -966,7 +1094,7 @@ WHEN MATCHED THEN
SELECT * FROM sq_target WHERE tid = 1;
ROLLBACK;
-DROP TABLE sq_target, sq_source CASCADE;
+DROP TABLE sq_target, sq_target_merge_log, sq_source CASCADE;
CREATE TABLE pa_target (tid integer, balance float, val text)
PARTITION BY LIST (tid);
@@ -1033,6 +1161,17 @@ SELECT merge_func();
SELECT * FROM pa_target ORDER BY tid;
ROLLBACK;
+-- update partition key to partition not initially scanned
+BEGIN;
+MERGE INTO pa_target t
+ USING pa_source s
+ ON t.tid = s.sid AND t.tid = 1
+ WHEN MATCHED THEN
+ UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
+ RETURNING pg_merge_action(), pg_merge_when_clause(), t.*;
+SELECT * FROM pa_target ORDER BY tid;
+ROLLBACK;
+
DROP TABLE pa_target CASCADE;
-- The target table is partitioned in the same way, but this time by attaching
@@ -1091,7 +1230,8 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (sid, delta, 'inserted by merge');
+ INSERT VALUES (sid, delta, 'inserted by merge')
+ RETURNING pg_merge_action(), pg_merge_when_clause(), t.*;
SELECT * FROM pa_target ORDER BY tid;
ROLLBACK;
@@ -1145,7 +1285,8 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge');
+ INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge')
+ RETURNING pg_merge_action(), pg_merge_when_clause(), t.*;
SELECT * FROM pa_target ORDER BY tid;
ROLLBACK;
diff --git a/src/test/regress/sql/rules.sql b/src/test/regress/sql/rules.sql
new file mode 100644
index 8b7e255..a33515b
--- a/src/test/regress/sql/rules.sql
+++ b/src/test/regress/sql/rules.sql
@@ -1281,7 +1281,8 @@ MERGE INTO rule_merge2 t USING (SELECT 1
CREATE TABLE sf_target(id int, data text, filling int[]);
CREATE FUNCTION merge_sf_test()
- RETURNS void
+ RETURNS TABLE(pg_merge_action text, when_clause int,
+ a int, b text, id int, data text, filling int[])
LANGUAGE sql
BEGIN ATOMIC
MERGE INTO sf_target t
@@ -1318,7 +1319,9 @@ WHEN NOT MATCHED
VALUES (s.a, s.b, DEFAULT)
WHEN NOT MATCHED
THEN INSERT (filling[1], id)
- VALUES (s.a, s.a);
+ VALUES (s.a, s.a)
+RETURNING
+ pg_merge_action(), pg_merge_when_clause() AS when_clause, *;
END;
\sf merge_sf_test
On Sat, Jul 1, 2023 at 4:08 AM Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
On Mon, 13 Mar 2023 at 13:36, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
And another rebase.
I ran out of cycles to pursue the MERGE patches in v16, but hopefully
I can make more progress in v17.Looking at this one with fresh eyes, it looks mostly in good shape.
+1
Most of the review was done with the v6 of the patch, minus the doc
build step. The additional changes in v7 of the patch were eyeballed,
and tested with `make check`.
To
recap, this adds support for the RETURNING clause in MERGE, together
with new support functions pg_merge_action() and
pg_merge_when_clause() that can be used in the RETURNING list of MERGE
nit: s/can be used in/can be used only in/
to retrieve the kind of action (INSERT/UPDATE/DELETE), and the index
of the WHEN clause executed for each row merged. In addition,
RETURNING support allows MERGE to be used as the source query in COPY
TO and WITH queries.One minor annoyance is that, due to the way that pg_merge_action() and
pg_merge_when_clause() require access to the MergeActionState, they
only work if they appear directly in the RETURNING list. They can't,
for example, appear in a subquery in the RETURNING list, and I don't
see an easy way round that limitation.
I believe that's a serious limitation, and would be a blocker for the feature.
Attached is an updated patch with some cosmetic updates, plus updated
ruleutils support.
With each iteration of your patch, it is becoming increasingly clear
that this will be a documentation-heavy patch :-)
I think the name of function pg_merge_when_clause() can be improved.
How about pg_merge_when_clause_ordinal().
In the doc page of MERGE, you've moved the 'with_query' from the
bottom of Parameters section to the top of that section. Any reason
for this? Since the Parameters section is quite large, for a moment I
thought the patch was _adding_ the description of 'with_query'.
+ /*
+ * 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");
As the comment says, this is an unexpected condition, and should have
been caught and reported by the parser. So I think it'd be better to
use an Assert() here. Moreover, there's ERROR being raised by the
pg_merge_action() and pg_merge_when_clause() functions, when they
detect the function context is not appropriate.
I found it very innovative to allow these functions to be called only
in a certain part of certain SQL command. I don't think there's a
precedence for this in Postgres; I'd be glad to learn if there are
other functions that Postgres exposes this way.
- EXPR_KIND_RETURNING, /* RETURNING */
+ EXPR_KIND_RETURNING, /* RETURNING in INSERT/UPDATE/DELETE */
+ EXPR_KIND_MERGE_RETURNING, /* RETURNING in MERGE */
Having to invent a whole new ParseExprKind enum, feels like a bit of
an overkill. My only objection is that this is exactly like
EXPR_KIND_RETURNING, hence EXPR_KIND_MERGE_RETURNING needs to be dealt
with exactly in as many places. But I don't have any alternative
proposals.
--- a/src/include/catalog/pg_proc.h
+++ b/src/include/catalog/pg_proc.h
+/* Is this a merge support function? (Requires fmgroids.h) */
+#define IsMergeSupportFunction(funcid) \
+ ((funcid) == F_PG_MERGE_ACTION || \
+ (funcid) == F_PG_MERGE_WHEN_CLAUSE)
If it doesn't cause recursion or other complications, I think we
should simply include the fmgroids.h in pg_proc.h. I understand that
not all .c files/consumers that include pg_proc.h may need fmgroids.h,
but #include'ing it will eliminate the need to keep the "Requires..."
note above, and avoid confusion, too.
--- a/src/test/regress/expected/merge.out
+++ b/src/test/regress/expected/merge.out
-WHEN MATCHED AND tid > 2 THEN
+WHEN MATCHED AND tid >= 2 THEN
This change can be treated as a bug fix :-)
+-- COPY (MERGE ... RETURNING) TO ...
+BEGIN;
+COPY (
+ MERGE INTO sq_target t
+ USING v
+ ON tid = sid
+ WHEN MATCHED AND tid > 2 THEN
For consistency, the check should be tid >= 2, like you've fixed in
command referenced above.
+BEGIN;
+COPY (
+ MERGE INTO sq_target t
+ USING v
+ ON tid = sid
+ 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 pg_merge_action(), t.*
+) TO stdout;
+DELETE 1 100
+ROLLBACK;
I expected the .out file to have captured the stdout. I'm gradually,
and gladly, re-learning bits of the test infrastructure.
The DELETE command tag in the output does not feel appropriate for a
COPY command that's using MERGE as the source of the data.
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT action text, OUT tid int,
OUT new_balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid > 2 THEN
Again, for consistency, the comparison operator should be >=. There
are a few more occurrences of this comparison in the rest of the file,
that need the same treatment.
Best regards,
Gurjeet
http://Gurje.et
On 2023-Jul-05, Gurjeet Singh wrote:
+BEGIN; +COPY ( + MERGE INTO sq_target t + USING v + ON tid = sid + 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 pg_merge_action(), t.* +) TO stdout; +DELETE 1 100 +ROLLBACK;I expected the .out file to have captured the stdout. I'm gradually,
and gladly, re-learning bits of the test infrastructure.The DELETE command tag in the output does not feel appropriate for a
COPY command that's using MERGE as the source of the data.
You misread this one :-) The COPY output is there, the tag is not. So
DELETE is the value from pg_merge_action(), and "1 100" correspond to
the columns in the the sq_target row that was deleted. The command tag
is presumably MERGE 1.
--
Álvaro Herrera Breisgau, Deutschland — https://www.EnterpriseDB.com/
On Thu, Jul 6, 2023 at 1:13 PM Gurjeet Singh <gurjeet@singh.im> wrote:
On Sat, Jul 1, 2023 at 4:08 AM Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
On Mon, 13 Mar 2023 at 13:36, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
And another rebase.
I ran out of cycles to pursue the MERGE patches in v16, but hopefully
I can make more progress in v17.Looking at this one with fresh eyes, it looks mostly in good shape.
+1
Most of the review was done with the v6 of the patch, minus the doc
build step. The additional changes in v7 of the patch were eyeballed,
and tested with `make check`.To
recap, this adds support for the RETURNING clause in MERGE, together
with new support functions pg_merge_action() and
pg_merge_when_clause() that can be used in the RETURNING list of MERGEnit: s/can be used in/can be used only in/
to retrieve the kind of action (INSERT/UPDATE/DELETE), and the index
of the WHEN clause executed for each row merged. In addition,
RETURNING support allows MERGE to be used as the source query in COPY
TO and WITH queries.One minor annoyance is that, due to the way that pg_merge_action() and
pg_merge_when_clause() require access to the MergeActionState, they
only work if they appear directly in the RETURNING list. They can't,
for example, appear in a subquery in the RETURNING list, and I don't
see an easy way round that limitation.I believe that's a serious limitation, and would be a blocker for the feature.
Attached is an updated patch with some cosmetic updates, plus updated
ruleutils support.With each iteration of your patch, it is becoming increasingly clear
that this will be a documentation-heavy patch :-)I think the name of function pg_merge_when_clause() can be improved.
How about pg_merge_when_clause_ordinal().
I think the name of function pg_merge_when_clause() can be improved.
How about pg_merge_when_clause_ordinal().
another idea: pg_merge_action_ordinal()
On Thu, Jul 6, 2023 at 3:39 AM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
On 2023-Jul-05, Gurjeet Singh wrote:
I expected the .out file to have captured the stdout. I'm gradually,
and gladly, re-learning bits of the test infrastructure.The DELETE command tag in the output does not feel appropriate for a
COPY command that's using MERGE as the source of the data.You misread this one :-) The COPY output is there, the tag is not. So
DELETE is the value from pg_merge_action(), and "1 100" correspond to
the columns in the the sq_target row that was deleted. The command tag
is presumably MERGE 1.
:-) That makes more sense. It matches my old mental model. Thanks for
clarifying!
Best regards,
Gurjeet
http://Gurje.et
On Thu, Jul 6, 2023 at 4:07 AM jian he <jian.universality@gmail.com> wrote:
On Thu, Jul 6, 2023 at 1:13 PM Gurjeet Singh <gurjeet@singh.im> wrote:
I think the name of function pg_merge_when_clause() can be improved.
How about pg_merge_when_clause_ordinal().another idea: pg_merge_action_ordinal()
Since there can be many occurrences of the same action
(INSERT/UPDATE/DELETE) in a MERGE command associated with different
conditions, I don't think action_ordinal would make sense for this
function name.
e.g.
WHEN MATCHED and src.col1 = val1 THEN UPDATE col2 = someval1
WHEN MATCHED and src.col1 = val2 THEN UPDATE col2 = someval2
...
When looking at the implementation code, as well, we see that the code
in this function tracks and reports the lexical position of the WHEN
clause, irrespective of the action associated with that WHEN clause.
foreach(l, stmt->mergeWhenClauses)
{
...
action->index = foreach_current_index(l) + 1;
Best regards,
Gurjeet
http://Gurje.et
On Thu, 6 Jul 2023 at 06:13, Gurjeet Singh <gurjeet@singh.im> wrote:
Most of the review was done with the v6 of the patch, minus the doc
build step. The additional changes in v7 of the patch were eyeballed,
and tested with `make check`.
Thanks for the review!
One minor annoyance is that, due to the way that pg_merge_action() and
pg_merge_when_clause() require access to the MergeActionState, they
only work if they appear directly in the RETURNING list. They can't,
for example, appear in a subquery in the RETURNING list, and I don't
see an easy way round that limitation.I believe that's a serious limitation, and would be a blocker for the feature.
Yes, that was bugging me for quite a while.
Attached is a new version that removes that restriction, allowing the
merge support functions to appear anywhere. This requires a bit of
planner support, to deal with merge support functions in subqueries,
in a similar way to how aggregates and GROUPING() expressions are
handled. But a number of the changes from the previous patch are no
longer necessary, so overall, this version of the patch is slightly
smaller.
I think the name of function pg_merge_when_clause() can be improved.
How about pg_merge_when_clause_ordinal().
That's a bit of a mouthful, but I don't have a better idea right now.
I've left the names alone for now, in case something better occurs to
anyone.
In the doc page of MERGE, you've moved the 'with_query' from the
bottom of Parameters section to the top of that section. Any reason
for this? Since the Parameters section is quite large, for a moment I
thought the patch was _adding_ the description of 'with_query'.
Ah yes, I was just making the order consistent with the
INSERT/UPDATE/DELETE pages. That could probably be committed
separately.
+ /* + * 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");As the comment says, this is an unexpected condition, and should have
been caught and reported by the parser. So I think it'd be better to
use an Assert() here. Moreover, there's ERROR being raised by the
pg_merge_action() and pg_merge_when_clause() functions, when they
detect the function context is not appropriate.I found it very innovative to allow these functions to be called only
in a certain part of certain SQL command. I don't think there's a
precedence for this in Postgres; I'd be glad to learn if there are
other functions that Postgres exposes this way.- EXPR_KIND_RETURNING, /* RETURNING */ + EXPR_KIND_RETURNING, /* RETURNING in INSERT/UPDATE/DELETE */ + EXPR_KIND_MERGE_RETURNING, /* RETURNING in MERGE */Having to invent a whole new ParseExprKind enum, feels like a bit of
an overkill. My only objection is that this is exactly like
EXPR_KIND_RETURNING, hence EXPR_KIND_MERGE_RETURNING needs to be dealt
with exactly in as many places. But I don't have any alternative
proposals.
That's all gone now from the new patch, since there is no longer any
restriction on where these functions can appear.
--- a/src/include/catalog/pg_proc.h +++ b/src/include/catalog/pg_proc.h +/* Is this a merge support function? (Requires fmgroids.h) */ +#define IsMergeSupportFunction(funcid) \ + ((funcid) == F_PG_MERGE_ACTION || \ + (funcid) == F_PG_MERGE_WHEN_CLAUSE)If it doesn't cause recursion or other complications, I think we
should simply include the fmgroids.h in pg_proc.h. I understand that
not all .c files/consumers that include pg_proc.h may need fmgroids.h,
but #include'ing it will eliminate the need to keep the "Requires..."
note above, and avoid confusion, too.
There's now only one place that uses this macro, whereas there are a
lot of places that include pg_proc.h and not fmgroids.h, so I don't
think forcing them all to include fmgroids.h is a good idea. (BTW,
this approach and comment is borrowed from IsTrueArrayType() in
pg_type.h)
--- a/src/test/regress/expected/merge.out +++ b/src/test/regress/expected/merge.out-WHEN MATCHED AND tid > 2 THEN +WHEN MATCHED AND tid >= 2 THENThis change can be treated as a bug fix :-)
+-- COPY (MERGE ... RETURNING) TO ... +BEGIN; +COPY ( + MERGE INTO sq_target t + USING v + ON tid = sid + WHEN MATCHED AND tid > 2 THENFor consistency, the check should be tid >= 2, like you've fixed in
command referenced above.+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int, + OUT action text, OUT tid int, OUT new_balance int) +LANGUAGE sql AS +$$ + MERGE INTO sq_target t + USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta) + ON tid = v.sid + WHEN MATCHED AND tid > 2 THENAgain, for consistency, the comparison operator should be >=. There
are a few more occurrences of this comparison in the rest of the file,
that need the same treatment.
I changed the new tests to use ">= 2" (and the COPY test now returns 3
rows, with an action of each type, which is easier to read), but I
don't think it's really necessary to change all the existing tests
from "> 2". There's nothing wrong with the "= 2" case having no
action, as long as the tests give decent coverage.
Thanks again for all the feedback.
Regards,
Dean
Attachments:
support-merge-returning-v8.patchtext/x-patch; charset=US-ASCII; name=support-merge-returning-v8.patchDownload
diff --git a/doc/src/sgml/dml.sgml b/doc/src/sgml/dml.sgml
new file mode 100644
index cbbc5e2..ff2a827
--- a/doc/src/sgml/dml.sgml
+++ b/doc/src/sgml/dml.sgml
@@ -283,10 +283,15 @@ DELETE FROM products;
<secondary>RETURNING</secondary>
</indexterm>
+ <indexterm zone="dml-returning">
+ <primary>MERGE</primary>
+ <secondary>RETURNING</secondary>
+ </indexterm>
+
<para>
Sometimes it is useful to obtain data from modified rows while they are
being manipulated. The <command>INSERT</command>, <command>UPDATE</command>,
- and <command>DELETE</command> commands all have an
+ <command>DELETE</command>, and <command>MERGE</command> commands all have an
optional <literal>RETURNING</literal> clause that supports this. Use
of <literal>RETURNING</literal> avoids performing an extra database query to
collect the data, and is especially valuable when it would otherwise be
@@ -339,6 +344,24 @@ DELETE FROM products
</programlisting>
</para>
+ <para>
+ In a <command>MERGE</command>, the data available to <literal>RETURNING</literal> is
+ the content of the source row plus the content of the inserted, updated, or
+ deleted target row. In addition, the merge support functions
+ <xref linkend="pg-merge-action"/> and <xref linkend="pg-merge-when-clause"/>
+ may be used to return information about which merge action was executed for
+ each row. Since it is quite common for the source and target to have many
+ of the same columns, specifying <literal>RETURNING *</literal> can lead to a
+ lot of duplicated columns, so it is often more useful to just return the
+ target row. For example:
+<programlisting>
+MERGE INTO products p USING new_products n ON p.product_no = n.product_no
+ WHEN NOT MATCHED THEN INSERT VALUES (n.product_no, n.name, n.price)
+ WHEN MATCHED THEN UPDATE SET name = n.name, price = n.price
+ RETURNING pg_merge_action(), p.*;
+</programlisting>
+ </para>
+
<para>
If there are triggers (<xref linkend="triggers"/>) on the target table,
the data available to <literal>RETURNING</literal> is the row as modified by
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
new file mode 100644
index 5a47ce4..1406c18
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -21705,6 +21705,94 @@ SELECT count(*) FROM sometable;
</sect1>
+ <sect1 id="functions-merge-support">
+ <title>Merge Support Functions</title>
+
+ <indexterm>
+ <primary>MERGE</primary>
+ <secondary>RETURNING</secondary>
+ </indexterm>
+
+ <para>
+ The merge support functions shown in
+ <xref linkend="functions-merge-support-table"/> may be used in the
+ <literal>RETURNING</literal> list of a <xref linkend="sql-merge"/> command
+ to return additional information about the action taken for each row.
+ </para>
+
+ <table id="functions-merge-support-table">
+ <title>Merge Support Functions</title>
+
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ Function
+ </para>
+ <para>
+ Description
+ </para></entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm id="pg-merge-action">
+ <primary>pg_merge_action</primary>
+ </indexterm>
+ <function>pg_merge_action</function> ( )
+ <returnvalue>text</returnvalue>
+ </para>
+ <para>
+ Returns the action performed on the current row (<literal>'INSERT'</literal>,
+ <literal>'UPDATE'</literal>, or <literal>'DELETE'</literal>).
+ </para></entry>
+ </row>
+
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm id="pg-merge-when-clause">
+ <primary>pg_merge_when_clause</primary>
+ </indexterm>
+ <function>pg_merge_when_clause</function> ( )
+ <returnvalue>integer</returnvalue>
+ </para>
+ <para>
+ Returns the 1-based index of the <literal>WHEN</literal> clause executed
+ for the current row.
+ </para></entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+
+ <para>
+ Example:
+<screen><![CDATA[
+MERGE INTO products p
+ USING stock s ON p.product_id = s.product_id
+ WHEN MATCHED AND s.quantity > 0 THEN
+ UPDATE SET in_stock = true, quantity = s.quantity
+ WHEN MATCHED THEN
+ UPDATE SET in_stock = false, quantity = 0
+ WHEN NOT MATCHED THEN
+ INSERT (product_id, in_stock, quantity)
+ VALUES (s.product_id, true, s.quantity)
+ RETURNING pg_merge_action() AS action,
+ pg_merge_when_clause() AS when_clause,
+ p.*;
+
+ action | when_clause | product_id | in_stock | quantity
+--------+-------------+------------+----------+----------
+ UPDATE | 1 | 1001 | t | 50
+ UPDATE | 2 | 1002 | f | 0
+ INSERT | 3 | 1003 | t | 10
+]]></screen>
+ </para>
+
+ </sect1>
+
<sect1 id="functions-subquery">
<title>Subquery Expressions</title>
diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml
new file mode 100644
index fe8def4..51a15ca
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -1387,9 +1387,9 @@
to a <glossterm linkend="glossary-client">client</glossterm> upon the
completion of an <acronym>SQL</acronym> command, usually a
<command>SELECT</command> but it can be an
- <command>INSERT</command>, <command>UPDATE</command>, or
- <command>DELETE</command> command if the <literal>RETURNING</literal>
- clause is specified.
+ <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command> command if the
+ <literal>RETURNING</literal> clause is specified.
</para>
<para>
The fact that a result set is a relation means that a query can be used
diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml
new file mode 100644
index f55e901..6803240
--- a/doc/src/sgml/plpgsql.sgml
+++ b/doc/src/sgml/plpgsql.sgml
@@ -1020,8 +1020,8 @@ INSERT INTO mytable VALUES (1,'one'), (2
</para>
<para>
- If the command does return rows (for example <command>SELECT</command>,
- or <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>
+ If the command does return rows (for example <command>SELECT</command>, or
+ <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command>
with <literal>RETURNING</literal>), there are two ways to proceed.
When the command will return at most one row, or you only care about
the first row of output, write the command as usual but add
@@ -1149,6 +1149,7 @@ SELECT <replaceable>select_expressions</
INSERT ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
UPDATE ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
DELETE ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
+MERGE ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
</synopsis>
where <replaceable>target</replaceable> can be a record variable, a row
@@ -1159,8 +1160,8 @@ DELETE ... RETURNING <replaceable>expres
<literal>INTO</literal> clause) just as described above,
and the plan is cached in the same way.
This works for <command>SELECT</command>,
- <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command> with
- <literal>RETURNING</literal>, and certain utility commands
+ <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command>
+ with <literal>RETURNING</literal>, and certain utility commands
that return row sets, such as <command>EXPLAIN</command>.
Except for the <literal>INTO</literal> clause, the SQL command is the same
as it would be written outside <application>PL/pgSQL</application>.
@@ -1236,7 +1237,7 @@ END;
</para>
<para>
- For <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command> with
+ For <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command> with
<literal>RETURNING</literal>, <application>PL/pgSQL</application> reports
an error for more than one returned row, even when
<literal>STRICT</literal> is not specified. This is because there
diff --git a/doc/src/sgml/queries.sgml b/doc/src/sgml/queries.sgml
new file mode 100644
index 3f95849..9928373
--- a/doc/src/sgml/queries.sgml
+++ b/doc/src/sgml/queries.sgml
@@ -2062,9 +2062,10 @@ SELECT <replaceable>select_list</replace
Table Expressions or <acronym>CTE</acronym>s, can be thought of as defining
temporary tables that exist just for one query. Each auxiliary statement
in a <literal>WITH</literal> clause can be a <command>SELECT</command>,
- <command>INSERT</command>, <command>UPDATE</command>, or <command>DELETE</command>; and the
+ <command>INSERT</command>, <command>UPDATE</command>, <command>DELETE</command>,
+ or <command>MERGE</command>; and the
<literal>WITH</literal> clause itself is attached to a primary statement that can
- be a <command>SELECT</command>, <command>INSERT</command>, <command>UPDATE</command>,
+ also be a <command>SELECT</command>, <command>INSERT</command>, <command>UPDATE</command>,
<command>DELETE</command>, or <command>MERGE</command>.
</para>
@@ -2598,8 +2599,8 @@ SELECT * FROM w AS w1 JOIN w AS w2 ON w1
<title>Data-Modifying Statements in <literal>WITH</literal></title>
<para>
- You can use most data-modifying statements (<command>INSERT</command>,
- <command>UPDATE</command>, or <command>DELETE</command>, but not
+ You can use data-modifying statements (<command>INSERT</command>,
+ <command>UPDATE</command>, <command>DELETE</command>, or
<command>MERGE</command>) in <literal>WITH</literal>. This
allows you to perform several different operations in the same query.
An example is:
diff --git a/doc/src/sgml/ref/copy.sgml b/doc/src/sgml/ref/copy.sgml
new file mode 100644
index 5e591ed..dbc76df
--- a/doc/src/sgml/ref/copy.sgml
+++ b/doc/src/sgml/ref/copy.sgml
@@ -122,13 +122,16 @@ COPY { <replaceable class="parameter">ta
A <link linkend="sql-select"><command>SELECT</command></link>,
<link linkend="sql-values"><command>VALUES</command></link>,
<link linkend="sql-insert"><command>INSERT</command></link>,
- <link linkend="sql-update"><command>UPDATE</command></link>, or
- <link linkend="sql-delete"><command>DELETE</command></link> command whose results are to be
- copied. Note that parentheses are required around the query.
+ <link linkend="sql-update"><command>UPDATE</command></link>,
+ <link linkend="sql-delete"><command>DELETE</command></link>, or
+ <link linkend="sql-merge"><command>MERGE</command></link> command
+ whose results are to be copied. Note that parentheses are required
+ around the query.
</para>
<para>
- For <command>INSERT</command>, <command>UPDATE</command> and
- <command>DELETE</command> queries a RETURNING clause must be provided,
+ For <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, and <command>MERGE</command> queries a
+ RETURNING clause must be provided,
and the target relation must not have a conditional rule, nor
an <literal>ALSO</literal> rule, nor an <literal>INSTEAD</literal> rule
that expands to multiple statements.
diff --git a/doc/src/sgml/ref/merge.sgml b/doc/src/sgml/ref/merge.sgml
new file mode 100644
index 0995fe0..7eb641a
--- a/doc/src/sgml/ref/merge.sgml
+++ b/doc/src/sgml/ref/merge.sgml
@@ -25,6 +25,7 @@ PostgreSQL documentation
MERGE INTO [ ONLY ] <replaceable class="parameter">target_table_name</replaceable> [ * ] [ [ AS ] <replaceable class="parameter">target_alias</replaceable> ]
USING <replaceable class="parameter">data_source</replaceable> ON <replaceable class="parameter">join_condition</replaceable>
<replaceable class="parameter">when_clause</replaceable> [...]
+[ RETURNING * | <replaceable class="parameter">output_expression</replaceable> [ [ AS ] <replaceable class="parameter">output_name</replaceable> ] [, ...] ]
<phrase>where <replaceable class="parameter">data_source</replaceable> is:</phrase>
@@ -95,6 +96,20 @@ DELETE
</para>
<para>
+ The optional <literal>RETURNING</literal> clause causes <command>MERGE</command>
+ to compute and return value(s) based on each row inserted, updated, or
+ deleted. Any expression using the source or target table's columns, or the
+ merge support functions <xref linkend="pg-merge-action"/> and
+ <xref linkend="pg-merge-when-clause"/> can be computed. When an
+ <command>INSERT</command> or <command>UPDATE</command> action is performed,
+ the new values of the target table's columns are used. When a
+ <command>DELETE</command> is performed, the old values of the target table's
+ columns are used. The syntax of the <literal>RETURNING</literal> list is
+ identical to that of the output list of <command>SELECT</command>.
+
+ </para>
+
+ <para>
There is no separate <literal>MERGE</literal> privilege.
If you specify an update action, you must have the
<literal>UPDATE</literal> privilege on the column(s)
@@ -125,6 +140,18 @@ DELETE
<variablelist>
<varlistentry>
+ <term><replaceable class="parameter">with_query</replaceable></term>
+ <listitem>
+ <para>
+ The <literal>WITH</literal> clause allows you to specify one or more
+ subqueries that can be referenced by name in the <command>MERGE</command>
+ query. See <xref linkend="queries-with"/> and <xref linkend="sql-select"/>
+ for details.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
<term><replaceable class="parameter">target_table_name</replaceable></term>
<listitem>
<para>
@@ -399,13 +426,30 @@ DELETE
</varlistentry>
<varlistentry>
- <term><replaceable class="parameter">with_query</replaceable></term>
+ <term><replaceable class="parameter">output_expression</replaceable></term>
<listitem>
<para>
- The <literal>WITH</literal> clause allows you to specify one or more
- subqueries that can be referenced by name in the <command>MERGE</command>
- query. See <xref linkend="queries-with"/> and <xref linkend="sql-select"/>
- for details.
+ An expression to be computed and returned by the <command>MERGE</command>
+ command after each row is merged. The expression can use any columns of
+ the source or target tables, or the merge support functions
+ <xref linkend="pg-merge-action"/> and <xref linkend="pg-merge-when-clause"/>.
+ </para>
+ <para>
+ Writing <literal>*</literal> will return all columns from the source
+ table, followed by all columns from the target table. Often this will
+ lead to a lot of duplication, since it is common for the source and
+ target tables to have a lot of the same columns. This can be avoided by
+ qualifying the <literal>*</literal> with the name or alias of the source
+ or target table.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><replaceable class="parameter">output_name</replaceable></term>
+ <listitem>
+ <para>
+ A name to use for a returned column.
</para>
</listitem>
</varlistentry>
@@ -428,6 +472,13 @@ MERGE <replaceable class="parameter">tot
were changed in any way.
</para>
+ <para>
+ If the <command>MERGE</command> command contains a <literal>RETURNING</literal>
+ clause, the result will be similar to that of a <command>SELECT</command>
+ statement containing the columns and values defined in the
+ <literal>RETURNING</literal> list, computed over the row(s) inserted, updated,
+ or deleted by the command.
+ </para>
</refsect1>
<refsect1>
@@ -544,13 +595,6 @@ MERGE <replaceable class="parameter">tot
</para>
<para>
- There is no <literal>RETURNING</literal> clause with
- <command>MERGE</command>. Actions of <command>INSERT</command>,
- <command>UPDATE</command> and <command>DELETE</command> cannot contain
- <literal>RETURNING</literal> or <literal>WITH</literal> clauses.
- </para>
-
- <para>
When <command>MERGE</command> is run concurrently with other commands
that modify the target table, the usual transaction isolation rules
apply; see <xref linkend="transaction-iso"/> for an explanation
@@ -602,7 +646,8 @@ WHEN NOT MATCHED THEN
<para>
Attempt to insert a new stock item along with the quantity of stock. If
the item already exists, instead update the stock count of the existing
- item. Don't allow entries that have zero stock.
+ item. Don't allow entries that have zero stock. Return details of all
+ changes made.
<programlisting>
MERGE INTO wines w
USING wine_stock_changes s
@@ -612,7 +657,8 @@ WHEN NOT MATCHED AND s.stock_delta > 0 T
WHEN MATCHED AND w.stock + s.stock_delta > 0 THEN
UPDATE SET stock = w.stock + s.stock_delta
WHEN MATCHED THEN
- DELETE;
+ DELETE
+RETURNING pg_merge_action(), w.*;
</programlisting>
The <literal>wine_stock_changes</literal> table might be, for example, a
@@ -627,7 +673,8 @@ WHEN MATCHED THEN
This command conforms to the <acronym>SQL</acronym> standard.
</para>
<para>
- The WITH clause and <literal>DO NOTHING</literal> action are extensions to
+ The WITH clause, <literal>DO NOTHING</literal> action, and
+ <literal>RETURNING</literal> clause are extensions to
the <acronym>SQL</acronym> standard.
</para>
</refsect1>
diff --git a/doc/src/sgml/ref/select.sgml b/doc/src/sgml/ref/select.sgml
new file mode 100644
index 0ee0cc7..f81bdea
--- a/doc/src/sgml/ref/select.sgml
+++ b/doc/src/sgml/ref/select.sgml
@@ -74,7 +74,7 @@ SELECT [ ALL | DISTINCT [ ON ( <replacea
<phrase>and <replaceable class="parameter">with_query</replaceable> is:</phrase>
- <replaceable class="parameter">with_query_name</replaceable> [ ( <replaceable class="parameter">column_name</replaceable> [, ...] ) ] AS [ [ NOT ] MATERIALIZED ] ( <replaceable class="parameter">select</replaceable> | <replaceable class="parameter">values</replaceable> | <replaceable class="parameter">insert</replaceable> | <replaceable class="parameter">update</replaceable> | <replaceable class="parameter">delete</replaceable> )
+ <replaceable class="parameter">with_query_name</replaceable> [ ( <replaceable class="parameter">column_name</replaceable> [, ...] ) ] AS [ [ NOT ] MATERIALIZED ] ( <replaceable class="parameter">select</replaceable> | <replaceable class="parameter">values</replaceable> | <replaceable class="parameter">insert</replaceable> | <replaceable class="parameter">update</replaceable> | <replaceable class="parameter">delete</replaceable> | <replaceable class="parameter">merge</replaceable> )
[ SEARCH { BREADTH | DEPTH } FIRST BY <replaceable>column_name</replaceable> [, ...] SET <replaceable>search_seq_col_name</replaceable> ]
[ CYCLE <replaceable>column_name</replaceable> [, ...] SET <replaceable>cycle_mark_col_name</replaceable> [ TO <replaceable>cycle_mark_value</replaceable> DEFAULT <replaceable>cycle_mark_default</replaceable> ] USING <replaceable>cycle_path_col_name</replaceable> ]
@@ -227,10 +227,10 @@ TABLE [ ONLY ] <replaceable class="param
The subqueries effectively act as temporary tables or views
for the duration of the primary query.
Each subquery can be a <command>SELECT</command>, <command>TABLE</command>, <command>VALUES</command>,
- <command>INSERT</command>, <command>UPDATE</command> or
- <command>DELETE</command> statement.
+ <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command> statement.
When writing a data-modifying statement (<command>INSERT</command>,
- <command>UPDATE</command> or <command>DELETE</command>) in
+ <command>UPDATE</command>, <command>DELETE</command>, or <command>MERGE</command>) in
<literal>WITH</literal>, it is usual to include a <literal>RETURNING</literal> clause.
It is the output of <literal>RETURNING</literal>, <emphasis>not</emphasis> the underlying
table that the statement modifies, that forms the temporary table that is
@@ -2179,7 +2179,8 @@ SELECT 2+2;
<para>
<productname>PostgreSQL</productname> allows <command>INSERT</command>,
- <command>UPDATE</command>, and <command>DELETE</command> to be used as <literal>WITH</literal>
+ <command>UPDATE</command>, <command>DELETE</command>, and
+ <command>MERGE</command> to be used as <literal>WITH</literal>
queries. This is not found in the SQL standard.
</para>
</refsect2>
diff --git a/doc/src/sgml/rowtypes.sgml b/doc/src/sgml/rowtypes.sgml
new file mode 100644
index 4d86f97..bbeac84
--- a/doc/src/sgml/rowtypes.sgml
+++ b/doc/src/sgml/rowtypes.sgml
@@ -348,7 +348,7 @@ SELECT m.* FROM some_table, LATERAL myfu
column expansion of this kind when it appears at the top level of
a <link linkend="queries-select-lists"><command>SELECT</command> output
list</link>, a <link linkend="dml-returning"><literal>RETURNING</literal>
- list</link> in <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>,
+ list</link> in <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command>,
a <link linkend="queries-values"><literal>VALUES</literal> clause</link>, or
a <link linkend="sql-syntax-row-constructors">row constructor</link>.
In all other contexts (including when nested inside one of those
diff --git a/doc/src/sgml/spi.sgml b/doc/src/sgml/spi.sgml
new file mode 100644
index 651930a..ad0a9b1
--- a/doc/src/sgml/spi.sgml
+++ b/doc/src/sgml/spi.sgml
@@ -301,8 +301,9 @@ SPI_execute("INSERT INTO foo SELECT * FR
is returned in the global variable <varname>SPI_processed</varname>.
If the return value of the function is <symbol>SPI_OK_SELECT</symbol>,
<symbol>SPI_OK_INSERT_RETURNING</symbol>,
- <symbol>SPI_OK_DELETE_RETURNING</symbol>, or
- <symbol>SPI_OK_UPDATE_RETURNING</symbol>,
+ <symbol>SPI_OK_DELETE_RETURNING</symbol>,
+ <symbol>SPI_OK_UPDATE_RETURNING</symbol>, or
+ <symbol>SPI_OK_MERGE_RETURNING</symbol>
then you can use the
global pointer <literal>SPITupleTable *SPI_tuptable</literal> to
access the result rows. Some utility commands (such as
@@ -472,6 +473,15 @@ typedef struct SPITupleTable
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><symbol>SPI_OK_MERGE_RETURNING</symbol></term>
+ <listitem>
+ <para>
+ if a <command>MERGE RETURNING</command> was executed
+ </para>
+ </listitem>
+ </varlistentry>
<varlistentry>
<term><symbol>SPI_OK_UTILITY</symbol></term>
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
new file mode 100644
index 9620ea9..4f68efb
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -177,7 +177,8 @@
statements separated by semicolons. A semicolon after the last
statement is optional. Unless the function is declared to return
<type>void</type>, the last statement must be a <command>SELECT</command>,
- or an <command>INSERT</command>, <command>UPDATE</command>, or <command>DELETE</command>
+ or an <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command>
that has a <literal>RETURNING</literal> clause.
</para>
@@ -1236,8 +1237,9 @@ SELECT x, CASE WHEN x > 0 THEN genera
<note>
<para>
- If a function's last command is <command>INSERT</command>, <command>UPDATE</command>,
- or <command>DELETE</command> with <literal>RETURNING</literal>, that command will
+ If a function's last command is <command>INSERT</command>,
+ <command>UPDATE</command>, <command>DELETE</command>, or
+ <command>MERGE</command> with <literal>RETURNING</literal>, that command will
always be executed to completion, even if the function is not declared
with <literal>SETOF</literal> or the calling query does not fetch all the
result rows. Any extra rows produced by the <literal>RETURNING</literal>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
new file mode 100644
index f14fae3..05f2e5b
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -283,12 +283,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 9e4b243..cf56f9f
--- 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/execPartition.c b/src/backend/executor/execPartition.c
new file mode 100644
index eb8a87f..b9bd03c
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -612,8 +612,8 @@ ExecInitPartitionInfo(ModifyTableState *
* Build the RETURNING projection for the partition. Note that we didn't
* build the returningList for partitions within the planner, but simple
* translation of varattnos will suffice. This only occurs for the INSERT
- * case or in the case of UPDATE tuple routing where we didn't find a
- * result rel to reuse.
+ * case or in the case of UPDATE/MERGE tuple routing where we didn't find
+ * a result rel to reuse.
*/
if (node && node->returningLists != NIL)
{
@@ -622,12 +622,14 @@ ExecInitPartitionInfo(ModifyTableState *
List *returningList;
/* See the comment above for WCO lists. */
- /* (except no RETURNING support for MERGE yet) */
Assert((node->operation == CMD_INSERT &&
list_length(node->returningLists) == 1 &&
list_length(node->resultRelations) == 1) ||
(node->operation == CMD_UPDATE &&
list_length(node->returningLists) ==
+ list_length(node->resultRelations)) ||
+ (node->operation == CMD_MERGE &&
+ list_length(node->returningLists) ==
list_length(node->resultRelations)));
/*
diff --git a/src/backend/executor/functions.c b/src/backend/executor/functions.c
new file mode 100644
index f55424e..f99f15f
--- a/src/backend/executor/functions.c
+++ b/src/backend/executor/functions.c
@@ -1660,8 +1660,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
@@ -1679,7 +1679,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 2a5fec8..3f6f495
--- 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"
@@ -64,6 +63,10 @@
#include "utils/rel.h"
+/* State available for merge support functions */
+static CmdType mergeActionCmdType = CMD_NOTHING;
+static int32 mergeActionIdx = 0;
+
typedef struct MTTargetRelLookup
{
Oid relationOid; /* hash key, must be first */
@@ -153,13 +156,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);
/*
@@ -237,6 +241,7 @@ ExecCheckPlanOutput(Relation resultRel,
/*
* ExecProcessReturning --- evaluate a RETURNING list
*
+ * context: state of current ModifyTable operation
* resultRelInfo: current result rel
* tupleSlot: slot holding tuple actually inserted/updated/deleted
* planSlot: slot holding tuple returned by top subplan node
@@ -247,12 +252,14 @@ ExecCheckPlanOutput(Relation resultRel,
* Returns a slot holding the result tuple
*/
static TupleTableSlot *
-ExecProcessReturning(ResultRelInfo *resultRelInfo,
+ExecProcessReturning(ModifyTableContext *context,
+ ResultRelInfo *resultRelInfo,
TupleTableSlot *tupleSlot,
TupleTableSlot *planSlot)
{
ProjectionInfo *projectReturning = resultRelInfo->ri_projectReturning;
ExprContext *econtext = projectReturning->pi_exprContext;
+ TupleTableSlot *rslot;
/* Make tuple and any needed join variables available to ExecProject */
if (tupleSlot)
@@ -266,8 +273,32 @@ ExecProcessReturning(ResultRelInfo *resu
econtext->ecxt_scantuple->tts_tableOid =
RelationGetRelid(resultRelInfo->ri_RelationDesc);
- /* Compute the RETURNING expressions */
- return ExecProject(projectReturning);
+ /*
+ * Compute the RETURNING expressions. If this is a MERGE, additional state
+ * is made available to any merge support functions in the RETURNING list.
+ */
+ if (context->relaction)
+ {
+ CmdType saved_mergeActionCmdType = mergeActionCmdType;
+ int32 saved_mergeActionIdx = mergeActionIdx;
+
+ mergeActionCmdType = context->relaction->mas_action->commandType;
+ mergeActionIdx = context->relaction->mas_action->index;
+ PG_TRY();
+ {
+ rslot = ExecProject(projectReturning);
+ }
+ PG_FINALLY();
+ {
+ mergeActionCmdType = saved_mergeActionCmdType;
+ mergeActionIdx = saved_mergeActionIdx;
+ }
+ PG_END_TRY();
+ }
+ else
+ rslot = ExecProject(projectReturning);
+
+ return rslot;
}
/*
@@ -1195,7 +1226,7 @@ ExecInsert(ModifyTableContext *context,
/* Process RETURNING if present */
if (resultRelInfo->ri_projectReturning)
- result = ExecProcessReturning(resultRelInfo, slot, planSlot);
+ result = ExecProcessReturning(context, resultRelInfo, slot, planSlot);
if (inserted_tuple)
*inserted_tuple = slot;
@@ -1692,7 +1723,8 @@ ldelete:
}
}
- rslot = ExecProcessReturning(resultRelInfo, slot, context->planSlot);
+ rslot = ExecProcessReturning(context, resultRelInfo, slot,
+ context->planSlot);
/*
* Before releasing the target tuple again, make sure rslot has a
@@ -2480,7 +2512,8 @@ redo_act:
/* Process RETURNING if present */
if (resultRelInfo->ri_projectReturning)
- return ExecProcessReturning(resultRelInfo, slot, context->planSlot);
+ return ExecProcessReturning(context, resultRelInfo, slot,
+ context->planSlot);
return NULL;
}
@@ -2712,6 +2745,7 @@ static TupleTableSlot *
ExecMerge(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
ItemPointer tupleid, bool canSetTag)
{
+ TupleTableSlot *rslot = NULL;
bool matched;
/*-----
@@ -2759,18 +2793,18 @@ 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()
- * returned "false", indicating the previously MATCHED tuple no longer
- * matches.
+ * Deal with the NOT MATCHED case (either a NOT MATCHED tuple from the
+ * join, or a previously MATCHED tuple for which ExecMergeMatched() set
+ * "matched" to false, indicating that it no longer matches).
*/
if (!matched)
- ExecMergeNotMatched(context, resultRelInfo, canSetTag);
+ rslot = ExecMergeNotMatched(context, resultRelInfo, canSetTag);
- /* No RETURNING support yet */
- return NULL;
+ return rslot;
}
/*
@@ -2780,8 +2814,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.
*
@@ -2790,15 +2824,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;
@@ -2810,7 +2845,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
@@ -2893,7 +2931,10 @@ lmerge_matched:
tupleid, NULL, newslot, &result))
{
if (result == TM_Ok)
- return true; /* "do nothing" */
+ {
+ *matched = true;
+ return NULL; /* "do nothing" */
+ }
break; /* concurrent update/delete */
}
result = ExecUpdateAct(context, resultRelInfo, tupleid, NULL,
@@ -2912,7 +2953,10 @@ lmerge_matched:
NULL, NULL, &result))
{
if (result == TM_Ok)
- return true; /* "do nothing" */
+ {
+ *matched = true;
+ return NULL; /* "do nothing" */
+ }
break; /* concurrent update/delete */
}
result = ExecDeleteAct(context, resultRelInfo, tupleid, false);
@@ -2968,7 +3012,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:
{
@@ -3014,13 +3059,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
@@ -3055,7 +3106,8 @@ lmerge_matched:
* tuple already deleted; tell caller to run NOT
* MATCHED actions
*/
- return false;
+ *matched = false;
+ return NULL;
case TM_SelfModified:
@@ -3075,13 +3127,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;
}
}
@@ -3093,6 +3146,32 @@ lmerge_matched:
break;
}
+ /* Process RETURNING if present */
+ if (resultRelInfo->ri_projectReturning)
+ {
+ switch (commandType)
+ {
+ case CMD_UPDATE:
+ rslot = ExecProcessReturning(context,
+ resultRelInfo, newslot,
+ context->planSlot);
+ break;
+
+ case CMD_DELETE:
+ rslot = ExecProcessReturning(context,
+ 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.
@@ -3103,19 +3182,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;
/*
@@ -3167,8 +3249,8 @@ ExecMergeNotMatched(ModifyTableContext *
newslot = ExecProject(action->mas_proj);
context->relaction = 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:
@@ -3184,6 +3266,8 @@ ExecMergeNotMatched(ModifyTableContext *
*/
break;
}
+
+ return rslot;
}
/*
@@ -3361,6 +3445,44 @@ ExecInitMergeTupleSlots(ModifyTableState
}
/*
+ * 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)
+{
+ switch (mergeActionCmdType)
+ {
+ 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) mergeActionCmdType);
+ }
+ 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)
+{
+ if (mergeActionIdx > 0)
+ PG_RETURN_INT32(mergeActionIdx);
+
+ PG_RETURN_NULL();
+}
+
+/*
* Process BEFORE EACH STATEMENT triggers
*/
static void
@@ -3615,6 +3737,9 @@ ExecModifyTable(PlanState *pstate)
context.planSlot = ExecProcNode(subplanstate);
+ /* Reset current MERGE action */
+ context.relaction = NULL;
+
/* No more tuples to process? */
if (TupIsNull(context.planSlot))
break;
@@ -3646,8 +3771,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");
@@ -3674,7 +3808,8 @@ ExecModifyTable(PlanState *pstate)
* ExecProcessReturning by IterateDirectModify, so no need to
* provide it here.
*/
- slot = ExecProcessReturning(resultRelInfo, NULL, context.planSlot);
+ slot = ExecProcessReturning(&context, resultRelInfo, NULL,
+ context.planSlot);
return slot;
}
@@ -3724,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");
@@ -3817,7 +3961,6 @@ ExecModifyTable(PlanState *pstate)
}
slot = ExecGetUpdateNewTuple(resultRelInfo, context.planSlot,
oldSlot);
- context.relaction = NULL;
/* Now apply the update. */
slot = ExecUpdate(&context, resultRelInfo, tupleid, oldtuple,
diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c
new file mode 100644
index 3397568..b9f443c
--- a/src/backend/executor/spi.c
+++ b/src/backend/executor/spi.c
@@ -2033,6 +2033,8 @@ SPI_result_code_string(int code)
return "SPI_OK_TD_REGISTER";
case SPI_OK_MERGE:
return "SPI_OK_MERGE";
+ case SPI_OK_MERGE_RETURNING:
+ return "SPI_OK_MERGE_RETURNING";
}
/* Unrecognized code ... return something useful ... */
sprintf(buf, "Unrecognized SPI code %d", code);
@@ -2886,7 +2888,10 @@ _SPI_pquery(QueryDesc *queryDesc, bool f
res = SPI_OK_UPDATE;
break;
case CMD_MERGE:
- res = SPI_OK_MERGE;
+ if (queryDesc->plannedstmt->hasReturning)
+ res = SPI_OK_MERGE_RETURNING;
+ else
+ res = SPI_OK_MERGE;
break;
default:
return SPI_ERROR_OPUNKNOWN;
diff --git a/src/backend/optimizer/plan/subselect.c b/src/backend/optimizer/plan/subselect.c
new file mode 100644
index da2d8ab..2b898bf
--- a/src/backend/optimizer/plan/subselect.c
+++ b/src/backend/optimizer/plan/subselect.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "catalog/pg_operator.h"
+#include "catalog/pg_proc.h"
#include "catalog/pg_type.h"
#include "executor/executor.h"
#include "miscadmin.h"
@@ -35,6 +36,7 @@
#include "parser/parse_relation.h"
#include "rewrite/rewriteManip.h"
#include "utils/builtins.h"
+#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/syscache.h"
@@ -1847,7 +1849,8 @@ convert_EXISTS_to_ANY(PlannerInfo *root,
/*
* Replace correlation vars (uplevel vars) with Params.
*
- * Uplevel PlaceHolderVars and aggregates are replaced, too.
+ * Uplevel PlaceHolderVars, aggregates, GROUPING() expressions and merge
+ * support functions are replaced, too.
*
* Note: it is critical that this runs immediately after SS_process_sublinks.
* Since we do not recurse into the arguments of uplevel PHVs and aggregates,
@@ -1901,6 +1904,22 @@ replace_correlation_vars_mutator(Node *n
if (((GroupingFunc *) node)->agglevelsup > 0)
return (Node *) replace_outer_grouping(root, (GroupingFunc *) node);
}
+ if (IsA(node, FuncExpr))
+ {
+ if (IsMergeSupportFunction(((FuncExpr *) node)->funcid))
+ {
+ Param *param;
+
+ /*
+ * Replace with a Param, if it belongs to an upper-level MERGE
+ * query. Otherwise, leave it be.
+ */
+ param = replace_outer_merge_support_function(root,
+ (FuncExpr *) node);
+ if (param)
+ return (Node *) param;
+ }
+ }
return expression_tree_mutator(node,
replace_correlation_vars_mutator,
(void *) root);
diff --git a/src/backend/optimizer/util/paramassign.c b/src/backend/optimizer/util/paramassign.c
new file mode 100644
index d6a923b..2b7b8d1
--- a/src/backend/optimizer/util/paramassign.c
+++ b/src/backend/optimizer/util/paramassign.c
@@ -307,6 +307,63 @@ replace_outer_grouping(PlannerInfo *root
}
/*
+ * Generate a Param node to replace the given FuncExpr expression, which is
+ * expected to be a merge support function, if it belongs to an upper-level
+ * MERGE query. Otherwise NULL will be returned.
+ *
+ * A NULL return value indicates either that the merge support function
+ * belongs to a MERGE query at this query level, or that there is no MERGE
+ * query at all. In both cases, the caller is expected to leave the FuncExpr
+ * node unaltered, so that it is executed at this query level (in the latter
+ * case, it is up to the merge support function to decide how to handle being
+ * called outside of a MERGE query).
+ */
+Param *
+replace_outer_merge_support_function(PlannerInfo *root, FuncExpr *func)
+{
+ Param *retval;
+ PlannerParamItem *pitem;
+ Index levelsup;
+ Oid ptype = exprType((Node *) func);
+
+ /* Find the upper-level MERGE query the function belongs to */
+ levelsup = 0;
+ while (root->parse->commandType != CMD_MERGE)
+ {
+ root = root->parent_root;
+ if (root == NULL)
+ return NULL; /* Not in a MERGE query */
+ levelsup++;
+ }
+ if (levelsup == 0)
+ return NULL; /* Local MERGE query at this level */
+
+ /*
+ * It does not seem worthwhile to try to de-duplicate references to outer
+ * merge support functions. Just make a new slot every time.
+ */
+ func = copyObject(func);
+
+ pitem = makeNode(PlannerParamItem);
+ pitem->item = (Node *) func;
+ pitem->paramId = list_length(root->glob->paramExecTypes);
+ root->glob->paramExecTypes = lappend_oid(root->glob->paramExecTypes,
+ ptype);
+
+ root->plan_params = lappend(root->plan_params, pitem);
+
+ retval = makeNode(Param);
+ retval->paramkind = PARAM_EXEC;
+ retval->paramid = pitem->paramId;
+ retval->paramtype = ptype;
+ retval->paramtypmod = -1;
+ retval->paramcollid = InvalidOid;
+ retval->location = func->location;
+
+ return retval;
+}
+
+/*
* Generate a Param node to replace the given Var,
* which is expected to come from some upper NestLoop plan node.
* Record the need for the Var in root->curOuterParams.
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
new file mode 100644
index 4006632..d6eafab
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -74,7 +74,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,
@@ -2501,9 +2500,9 @@ transformUpdateTargetList(ParseState *ps
/*
* transformReturningList -
- * handle a RETURNING clause in INSERT/UPDATE/DELETE
+ * handle a RETURNING clause in INSERT/UPDATE/DELETE/MERGE
*/
-static List *
+List *
transformReturningList(ParseState *pstate, List *returningList)
{
List *rlist;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
new file mode 100644
index 39ab7ea..5455607
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -12249,6 +12249,7 @@ MergeStmt:
USING table_ref
ON a_expr
merge_when_list
+ returning_clause
{
MergeStmt *m = makeNode(MergeStmt);
@@ -12257,6 +12258,7 @@ MergeStmt:
m->sourceRelation = $6;
m->joinCondition = $8;
m->mergeWhenClauses = $9;
+ m->returningList = $10;
$$ = (Node *) m;
}
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_merge.c b/src/backend/parser/parse_merge.c
new file mode 100644
index 91b1156..4427f75
--- a/src/backend/parser/parse_merge.c
+++ b/src/backend/parser/parse_merge.c
@@ -233,6 +233,9 @@ transformMergeStmt(ParseState *pstate, M
*/
qry->jointree = makeFromExpr(pstate->p_joinlist, joinExpr);
+ /* Transform the RETURNING list, if any */
+ qry->returningList = transformReturningList(pstate, stmt->returningList);
+
/*
* We now have a good query shape, so now look at the WHEN conditions and
* action targetlists.
@@ -254,6 +257,7 @@ transformMergeStmt(ParseState *pstate, M
MergeAction *action;
action = makeNode(MergeAction);
+ action->index = foreach_current_index(l) + 1;
action->commandType = mergeWhenClause->commandType;
action->matched = mergeWhenClause->matched;
@@ -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 41d6049..794530b
--- a/src/backend/parser/parse_relation.c
+++ b/src/backend/parser/parse_relation.c
@@ -2345,9 +2345,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 b486ab5..1bd0ce2
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -3629,9 +3629,9 @@ RewriteQuery(Query *parsetree, List *rew
ListCell *lc1;
/*
- * First, recursively process any insert/update/delete statements in WITH
- * clauses. (We have to do this first because the WITH clauses may get
- * copied into rule actions below.)
+ * First, recursively process any insert/update/delete/merge statements in
+ * WITH clauses. (We have to do this first because the WITH clauses may
+ * get copied into rule actions below.)
*/
foreach(lc1, parsetree->cteList)
{
@@ -3656,7 +3656,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 30b51bf..0ae3890
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -2141,11 +2141,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/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
new file mode 100644
index d3a973d..f097d98
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -7183,8 +7183,13 @@ get_merge_query_def(Query *query, depars
appendStringInfoString(buf, "DO NOTHING");
}
- /* No RETURNING support in MERGE yet */
- Assert(query->returningList == NIL);
+ /* Add RETURNING if present */
+ if (query->returningList)
+ {
+ appendContextKeyword(context, " RETURNING",
+ -PRETTYINDENT_STD, PRETTYINDENT_STD, 1);
+ get_target_list(query->returningList, context, NULL, colNamesVisible);
+ }
}
diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c
new file mode 100644
index 5973df2..a1380f8
--- a/src/bin/psql/common.c
+++ b/src/bin/psql/common.c
@@ -982,13 +982,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 6996073..46c36af
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12043,4 +12043,14 @@
proname => 'any_value_transfn', prorettype => 'anyelement',
proargtypes => 'anyelement anyelement', prosrc => 'any_value_transfn' },
+# MERGE support functions
+{ oid => '9499', descr => 'command type of current MERGE action',
+ proname => 'pg_merge_action', provolatile => 'v', proparallel => 'r',
+ prorettype => 'text', proargtypes => '',
+ prosrc => 'pg_merge_action' },
+{ oid => '9500', descr => 'index of current MERGE WHEN clause',
+ proname => 'pg_merge_when_clause', provolatile => 'v', proparallel => 'r',
+ 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..463add8
--- 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(funcid) \
+ ((funcid) == F_PG_MERGE_ACTION || \
+ (funcid) == F_PG_MERGE_WHEN_CLAUSE)
+
#endif /* EXPOSE_TO_CLIENT_CODE */
diff --git a/src/include/executor/spi.h b/src/include/executor/spi.h
new file mode 100644
index d1de139..ed8787d
--- a/src/include/executor/spi.h
+++ b/src/include/executor/spi.h
@@ -97,6 +97,7 @@ typedef struct _SPI_plan *SPIPlanPtr;
#define SPI_OK_REL_UNREGISTER 16
#define SPI_OK_TD_REGISTER 17
#define SPI_OK_MERGE 18
+#define SPI_OK_MERGE_RETURNING 19
#define SPI_OPT_NONATOMIC (1 << 0)
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
new file mode 100644
index 88b03cc..79511e9
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -1689,6 +1689,7 @@ typedef struct MergeWhenClause
typedef struct MergeAction
{
NodeTag type;
+ int index; /* 1-based index of the clause */
bool matched; /* true=MATCHED, false=NOT MATCHED */
CmdType commandType; /* INSERT/UPDATE/DELETE/DO NOTHING */
/* OVERRIDING clause */
@@ -1915,6 +1916,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/optimizer/paramassign.h b/src/include/optimizer/paramassign.h
new file mode 100644
index 55c27a6..6f70c78
--- a/src/include/optimizer/paramassign.h
+++ b/src/include/optimizer/paramassign.h
@@ -20,6 +20,8 @@ extern Param *replace_outer_placeholderv
PlaceHolderVar *phv);
extern Param *replace_outer_agg(PlannerInfo *root, Aggref *agg);
extern Param *replace_outer_grouping(PlannerInfo *root, GroupingFunc *grp);
+extern Param *replace_outer_merge_support_function(PlannerInfo *root,
+ FuncExpr *func);
extern Param *replace_nestloop_param_var(PlannerInfo *root, Var *var);
extern Param *replace_nestloop_param_placeholdervar(PlannerInfo *root,
PlaceHolderVar *phv);
diff --git a/src/include/parser/analyze.h b/src/include/parser/analyze.h
new file mode 100644
index 1cef183..fd5848d
--- a/src/include/parser/analyze.h
+++ b/src/include/parser/analyze.h
@@ -44,6 +44,7 @@ extern List *transformInsertRow(ParseSta
bool strip_indirection);
extern List *transformUpdateTargetList(ParseState *pstate,
List *origTlist);
+extern List *transformReturningList(ParseState *pstate, List *returningList);
extern Query *transformTopLevelStmt(ParseState *pstate, RawStmt *parseTree);
extern Query *transformStmt(ParseState *pstate, Node *parseTree);
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
new file mode 100644
index 4b76f76..fb6289c
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -4267,9 +4267,9 @@ exec_stmt_execsql(PLpgSQL_execstate *est
/*
* If we have INTO, then we only need one row back ... but if we have INTO
* STRICT or extra check too_many_rows, ask for two rows, so that we can
- * verify the statement returns only one. INSERT/UPDATE/DELETE are always
- * treated strictly. Without INTO, just run the statement to completion
- * (tcount = 0).
+ * verify the statement returns only one. INSERT/UPDATE/DELETE/MERGE are
+ * always treated strictly. Without INTO, just run the statement to
+ * completion (tcount = 0).
*
* We could just ask for two rows always when using INTO, but there are
* some cases where demanding the extra row costs significant time, eg by
@@ -4307,10 +4307,11 @@ exec_stmt_execsql(PLpgSQL_execstate *est
case SPI_OK_INSERT:
case SPI_OK_UPDATE:
case SPI_OK_DELETE:
+ case SPI_OK_MERGE:
case SPI_OK_INSERT_RETURNING:
case SPI_OK_UPDATE_RETURNING:
case SPI_OK_DELETE_RETURNING:
- case SPI_OK_MERGE:
+ case SPI_OK_MERGE_RETURNING:
Assert(stmt->mod_stmt);
exec_set_found(estate, (SPI_processed != 0));
break;
@@ -4489,10 +4490,11 @@ exec_stmt_dynexecute(PLpgSQL_execstate *
case SPI_OK_INSERT:
case SPI_OK_UPDATE:
case SPI_OK_DELETE:
+ case SPI_OK_MERGE:
case SPI_OK_INSERT_RETURNING:
case SPI_OK_UPDATE_RETURNING:
case SPI_OK_DELETE_RETURNING:
- case SPI_OK_MERGE:
+ case SPI_OK_MERGE_RETURNING:
case SPI_OK_UTILITY:
case SPI_OK_REWRITTEN:
break;
diff --git a/src/pl/tcl/pltcl.c b/src/pl/tcl/pltcl.c
new file mode 100644
index e8f9d7b..0c9f81e
--- a/src/pl/tcl/pltcl.c
+++ b/src/pl/tcl/pltcl.c
@@ -2456,6 +2456,7 @@ pltcl_process_SPI_result(Tcl_Interp *int
case SPI_OK_INSERT_RETURNING:
case SPI_OK_DELETE_RETURNING:
case SPI_OK_UPDATE_RETURNING:
+ case SPI_OK_MERGE_RETURNING:
/*
* Process the tuples we got
diff --git a/src/test/regress/expected/merge.out b/src/test/regress/expected/merge.out
new file mode 100644
index 133d421..e2cab70
--- 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;
@@ -1304,20 +1304,184 @@ WHEN MATCHED AND tid < 2 THEN
ROLLBACK;
-- RETURNING
BEGIN;
-INSERT INTO sq_source (sid, balance, delta) VALUES (-1, -1, -10);
+CREATE TABLE merge_actions(action text, abbrev text);
+INSERT INTO merge_actions VALUES ('INSERT', 'ins'), ('UPDATE', 'upd'), ('DELETE', 'del');
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)
+ 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 *;
- ^
+ DELETE
+RETURNING pg_merge_when_clause() AS when_clause,
+ (SELECT abbrev FROM merge_actions
+ WHERE action = 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 | del | 1 | 100 | Removed (1,100)
+ 1 | upd | 2 | 220 | Added 20 to balance
+ 2 | ins | 4 | 40 | Inserted (4,40)
+(3 rows)
+
+ROLLBACK;
+-- RETURNING in CTEs
+CREATE TABLE sq_target_merge_log (tid integer NOT NULL, last_change text);
+INSERT INTO sq_target_merge_log VALUES (1, 'Original value');
+BEGIN;
+WITH m AS (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 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
+), m2 AS (
+ MERGE INTO sq_target_merge_log l
+ USING m
+ ON l.tid = m.tid
+ WHEN MATCHED THEN
+ UPDATE SET last_change = description
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (m.tid, description)
+ RETURNING merge_action, pg_merge_action() AS merge_log_action, l.*
+)
+SELECT * FROM m2;
+ merge_action | merge_log_action | tid | last_change
+--------------+------------------+-----+---------------------
+ DELETE | UPDATE | 1 | Removed (1,100)
+ UPDATE | INSERT | 2 | Added 20 to balance
+ INSERT | INSERT | 4 | Inserted (4,40)
+(3 rows)
+
+SELECT * FROM sq_target_merge_log ORDER BY tid;
+ tid | last_change
+-----+---------------------
+ 1 | Removed (1,100)
+ 2 | Added 20 to balance
+ 4 | Inserted (4,40)
+(3 rows)
+
+ROLLBACK;
+-- COPY (MERGE ... RETURNING) TO ...
+BEGIN;
+COPY (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 pg_merge_action(), t.*
+) TO stdout;
+DELETE 1 100
+UPDATE 2 220
+INSERT 4 40
+ROLLBACK;
+-- SQL function with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT action text, OUT tid int, OUT new_balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING pg_merge_action(), t.*;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT action, tid, new_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ action | tid | new_balance
+--------+-----+-------------
+ DELETE | 1 | 100
+ UPDATE | 3 | 320
+ INSERT | 4 | 110
+(3 rows)
+
+ROLLBACK;
+-- SQL SRF with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_sq_source_into_sq_target()
+RETURNS TABLE (action text, tid int, balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 pg_merge_action(), t.*;
+$$;
+SELECT * FROM merge_sq_source_into_sq_target();
+ action | tid | balance
+--------+-----+---------
+ DELETE | 1 | 100
+ UPDATE | 2 | 220
+ INSERT | 4 | 40
+(3 rows)
+
+ROLLBACK;
+-- PL/pgSQL function with MERGE ... RETURNING ... INTO
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT r_action text, OUT r_tid int, OUT r_balance int)
+LANGUAGE plpgsql AS
+$$
+BEGIN
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING pg_merge_action(), t.* INTO r_action, r_tid, r_balance;
+END;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT r_action, r_tid, r_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ r_action | r_tid | r_balance
+----------+-------+-----------
+ DELETE | 1 | 100
+ UPDATE | 3 | 320
+ INSERT | 4 | 110
+(3 rows)
+
ROLLBACK;
-- EXPLAIN
CREATE TABLE ex_mtarget (a int, b int)
@@ -1514,7 +1678,7 @@ SELECT * FROM sq_target WHERE tid = 1;
(1 row)
ROLLBACK;
-DROP TABLE sq_target, sq_source CASCADE;
+DROP TABLE sq_target, sq_target_merge_log, sq_source CASCADE;
NOTICE: drop cascades to view v
CREATE TABLE pa_target (tid integer, balance float, val text)
PARTITION BY LIST (tid);
@@ -1640,6 +1804,32 @@ SELECT * FROM pa_target ORDER BY tid;
(14 rows)
ROLLBACK;
+-- update partition key to partition not initially scanned
+BEGIN;
+MERGE INTO pa_target t
+ USING pa_source s
+ ON t.tid = s.sid AND t.tid = 1
+ WHEN MATCHED THEN
+ UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
+ RETURNING pg_merge_action(), pg_merge_when_clause(), t.*;
+ pg_merge_action | pg_merge_when_clause | tid | balance | val
+-----------------+----------------------+-----+---------+--------------------------
+ UPDATE | 1 | 2 | 110 | initial updated by merge
+(1 row)
+
+SELECT * FROM pa_target ORDER BY tid;
+ tid | balance | val
+-----+---------+--------------------------
+ 2 | 110 | initial updated by merge
+ 3 | 300 | initial
+ 5 | 500 | initial
+ 7 | 700 | initial
+ 9 | 900 | initial
+ 11 | 1100 | initial
+ 13 | 1300 | initial
+(7 rows)
+
+ROLLBACK;
DROP TABLE pa_target CASCADE;
-- The target table is partitioned in the same way, but this time by attaching
-- partitions which have columns in different order, dropped columns etc.
@@ -1731,7 +1921,26 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (sid, delta, 'inserted by merge');
+ INSERT VALUES (sid, delta, 'inserted by merge')
+ RETURNING pg_merge_action(), pg_merge_when_clause(), t.*;
+ pg_merge_action | pg_merge_when_clause | tid | balance | val
+-----------------+----------------------+-----+---------+--------------------------
+ UPDATE | 1 | 2 | 110 | initial updated by merge
+ INSERT | 2 | 2 | 20 | inserted by merge
+ UPDATE | 1 | 4 | 330 | initial updated by merge
+ INSERT | 2 | 4 | 40 | inserted by merge
+ UPDATE | 1 | 6 | 550 | initial updated by merge
+ INSERT | 2 | 6 | 60 | inserted by merge
+ UPDATE | 1 | 8 | 770 | initial updated by merge
+ INSERT | 2 | 8 | 80 | inserted by merge
+ UPDATE | 1 | 10 | 990 | initial updated by merge
+ INSERT | 2 | 10 | 100 | inserted by merge
+ UPDATE | 1 | 12 | 1210 | initial updated by merge
+ INSERT | 2 | 12 | 120 | inserted by merge
+ UPDATE | 1 | 14 | 1430 | initial updated by merge
+ INSERT | 2 | 14 | 140 | inserted by merge
+(14 rows)
+
SELECT * FROM pa_target ORDER BY tid;
tid | balance | val
-----+---------+--------------------------
@@ -1798,7 +2007,21 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge');
+ INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge')
+ RETURNING pg_merge_action(), pg_merge_when_clause(), t.*;
+ pg_merge_action | pg_merge_when_clause | logts | tid | balance | val
+-----------------+----------------------+--------------------------+-----+---------+--------------------------
+ UPDATE | 1 | Tue Jan 31 00:00:00 2017 | 1 | 110 | initial updated by merge
+ UPDATE | 1 | Tue Feb 28 00:00:00 2017 | 2 | 220 | initial updated by merge
+ INSERT | 2 | Sun Jan 15 00:00:00 2017 | 3 | 30 | inserted by merge
+ UPDATE | 1 | Tue Jan 31 00:00:00 2017 | 4 | 440 | initial updated by merge
+ UPDATE | 1 | Tue Feb 28 00:00:00 2017 | 5 | 550 | initial updated by merge
+ INSERT | 2 | Sun Jan 15 00:00:00 2017 | 6 | 60 | inserted by merge
+ UPDATE | 1 | Tue Jan 31 00:00:00 2017 | 7 | 770 | initial updated by merge
+ UPDATE | 1 | Tue Feb 28 00:00:00 2017 | 8 | 880 | initial updated by merge
+ INSERT | 2 | Sun Jan 15 00:00:00 2017 | 9 | 90 | inserted by merge
+(9 rows)
+
SELECT * FROM pa_target ORDER BY tid;
logts | tid | balance | val
--------------------------+-----+---------+--------------------------
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
new file mode 100644
index 7fd81e6..87a3686
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -3590,7 +3590,8 @@ MERGE INTO rule_merge2 t USING (SELECT 1
-- test deparsing
CREATE TABLE sf_target(id int, data text, filling int[]);
CREATE FUNCTION merge_sf_test()
- RETURNS void
+ RETURNS TABLE(pg_merge_action text, when_clause int,
+ a int, b text, id int, data text, filling int[])
LANGUAGE sql
BEGIN ATOMIC
MERGE INTO sf_target t
@@ -3627,11 +3628,13 @@ WHEN NOT MATCHED
VALUES (s.a, s.b, DEFAULT)
WHEN NOT MATCHED
THEN INSERT (filling[1], id)
- VALUES (s.a, s.a);
+ VALUES (s.a, s.a)
+RETURNING
+ pg_merge_action(), pg_merge_when_clause() AS when_clause, *;
END;
\sf merge_sf_test
CREATE OR REPLACE FUNCTION public.merge_sf_test()
- RETURNS void
+ RETURNS TABLE(pg_merge_action text, when_clause integer, a integer, b text, id integer, data text, filling integer[])
LANGUAGE sql
BEGIN ATOMIC
MERGE INTO sf_target t
@@ -3668,7 +3671,14 @@ BEGIN ATOMIC
VALUES (s.a, s.b, DEFAULT)
WHEN NOT MATCHED
THEN INSERT (filling[1], id)
- VALUES (s.a, s.a);
+ VALUES (s.a, s.a)
+ RETURNING pg_merge_action() AS pg_merge_action,
+ pg_merge_when_clause() AS when_clause,
+ s.a,
+ s.b,
+ t.id,
+ t.data,
+ t.filling;
END
DROP FUNCTION merge_sf_test;
DROP TABLE sf_target;
diff --git a/src/test/regress/sql/merge.sql b/src/test/regress/sql/merge.sql
new file mode 100644
index 4cf6db9..6dfe4da
--- 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
@@ -857,17 +857,144 @@ ROLLBACK;
-- RETURNING
BEGIN;
-INSERT INTO sq_source (sid, balance, delta) VALUES (-1, -1, -10);
+CREATE TABLE merge_actions(action text, abbrev text);
+INSERT INTO merge_actions VALUES ('INSERT', 'ins'), ('UPDATE', 'upd'), ('DELETE', 'del');
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)
+ INSERT (balance, tid) VALUES (balance + delta, sid)
WHEN MATCHED AND tid < 2 THEN
- DELETE
-RETURNING *;
+ DELETE
+RETURNING pg_merge_when_clause() AS when_clause,
+ (SELECT abbrev FROM merge_actions
+ WHERE action = 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;
+
+-- RETURNING in CTEs
+CREATE TABLE sq_target_merge_log (tid integer NOT NULL, last_change text);
+INSERT INTO sq_target_merge_log VALUES (1, 'Original value');
+BEGIN;
+WITH m AS (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 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
+), m2 AS (
+ MERGE INTO sq_target_merge_log l
+ USING m
+ ON l.tid = m.tid
+ WHEN MATCHED THEN
+ UPDATE SET last_change = description
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (m.tid, description)
+ RETURNING merge_action, pg_merge_action() AS merge_log_action, l.*
+)
+SELECT * FROM m2;
+SELECT * FROM sq_target_merge_log ORDER BY tid;
+ROLLBACK;
+
+-- COPY (MERGE ... RETURNING) TO ...
+BEGIN;
+COPY (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 pg_merge_action(), t.*
+) TO stdout;
+ROLLBACK;
+
+-- SQL function with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT action text, OUT tid int, OUT new_balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING pg_merge_action(), t.*;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT action, tid, new_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ROLLBACK;
+
+-- SQL SRF with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_sq_source_into_sq_target()
+RETURNS TABLE (action text, tid int, balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 pg_merge_action(), t.*;
+$$;
+SELECT * FROM merge_sq_source_into_sq_target();
+ROLLBACK;
+
+-- PL/pgSQL function with MERGE ... RETURNING ... INTO
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT r_action text, OUT r_tid int, OUT r_balance int)
+LANGUAGE plpgsql AS
+$$
+BEGIN
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING pg_merge_action(), t.* INTO r_action, r_tid, r_balance;
+END;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT r_action, r_tid, r_balance FROM merge_into_sq_target(sid, balance, delta)) m;
ROLLBACK;
-- EXPLAIN
@@ -966,7 +1093,7 @@ WHEN MATCHED THEN
SELECT * FROM sq_target WHERE tid = 1;
ROLLBACK;
-DROP TABLE sq_target, sq_source CASCADE;
+DROP TABLE sq_target, sq_target_merge_log, sq_source CASCADE;
CREATE TABLE pa_target (tid integer, balance float, val text)
PARTITION BY LIST (tid);
@@ -1033,6 +1160,17 @@ SELECT merge_func();
SELECT * FROM pa_target ORDER BY tid;
ROLLBACK;
+-- update partition key to partition not initially scanned
+BEGIN;
+MERGE INTO pa_target t
+ USING pa_source s
+ ON t.tid = s.sid AND t.tid = 1
+ WHEN MATCHED THEN
+ UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
+ RETURNING pg_merge_action(), pg_merge_when_clause(), t.*;
+SELECT * FROM pa_target ORDER BY tid;
+ROLLBACK;
+
DROP TABLE pa_target CASCADE;
-- The target table is partitioned in the same way, but this time by attaching
@@ -1091,7 +1229,8 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (sid, delta, 'inserted by merge');
+ INSERT VALUES (sid, delta, 'inserted by merge')
+ RETURNING pg_merge_action(), pg_merge_when_clause(), t.*;
SELECT * FROM pa_target ORDER BY tid;
ROLLBACK;
@@ -1145,7 +1284,8 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge');
+ INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge')
+ RETURNING pg_merge_action(), pg_merge_when_clause(), t.*;
SELECT * FROM pa_target ORDER BY tid;
ROLLBACK;
diff --git a/src/test/regress/sql/rules.sql b/src/test/regress/sql/rules.sql
new file mode 100644
index 8b7e255..a33515b
--- a/src/test/regress/sql/rules.sql
+++ b/src/test/regress/sql/rules.sql
@@ -1281,7 +1281,8 @@ MERGE INTO rule_merge2 t USING (SELECT 1
CREATE TABLE sf_target(id int, data text, filling int[]);
CREATE FUNCTION merge_sf_test()
- RETURNS void
+ RETURNS TABLE(pg_merge_action text, when_clause int,
+ a int, b text, id int, data text, filling int[])
LANGUAGE sql
BEGIN ATOMIC
MERGE INTO sf_target t
@@ -1318,7 +1319,9 @@ WHEN NOT MATCHED
VALUES (s.a, s.b, DEFAULT)
WHEN NOT MATCHED
THEN INSERT (filling[1], id)
- VALUES (s.a, s.a);
+ VALUES (s.a, s.a)
+RETURNING
+ pg_merge_action(), pg_merge_when_clause() AS when_clause, *;
END;
\sf merge_sf_test
On Fri, Jul 7, 2023 at 3:48 PM Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
On Thu, 6 Jul 2023 at 06:13, Gurjeet Singh <gurjeet@singh.im> wrote:
One minor annoyance is that, due to the way that pg_merge_action() and
pg_merge_when_clause() require access to the MergeActionState, they
only work if they appear directly in the RETURNING list. They can't,
for example, appear in a subquery in the RETURNING list, and I don't
see an easy way round that limitation.I believe that's a serious limitation, and would be a blocker for the feature.
Yes, that was bugging me for quite a while.
Attached is a new version that removes that restriction, allowing the
merge support functions to appear anywhere. This requires a bit of
planner support, to deal with merge support functions in subqueries,
in a similar way to how aggregates and GROUPING() expressions are
handled. But a number of the changes from the previous patch are no
longer necessary, so overall, this version of the patch is slightly
smaller.
+1
I think the name of function pg_merge_when_clause() can be improved.
How about pg_merge_when_clause_ordinal().That's a bit of a mouthful, but I don't have a better idea right now.
I've left the names alone for now, in case something better occurs to
anyone.
+1. How do we make sure we don't forget that it needs to be named
better. Perhaps a TODO item within the patch will help.
In the doc page of MERGE, you've moved the 'with_query' from the
bottom of Parameters section to the top of that section. Any reason
for this? Since the Parameters section is quite large, for a moment I
thought the patch was _adding_ the description of 'with_query'.Ah yes, I was just making the order consistent with the
INSERT/UPDATE/DELETE pages. That could probably be committed
separately.
I don't think that's necessary, if it improves consistency with related docs.
+ /* + * 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");As the comment says, this is an unexpected condition, and should have
been caught and reported by the parser. So I think it'd be better to
use an Assert() here. Moreover, there's ERROR being raised by the
pg_merge_action() and pg_merge_when_clause() functions, when they
detect the function context is not appropriate.I found it very innovative to allow these functions to be called only
in a certain part of certain SQL command. I don't think there's a
precedence for this in Postgres; I'd be glad to learn if there are
other functions that Postgres exposes this way.- EXPR_KIND_RETURNING, /* RETURNING */ + EXPR_KIND_RETURNING, /* RETURNING in INSERT/UPDATE/DELETE */ + EXPR_KIND_MERGE_RETURNING, /* RETURNING in MERGE */Having to invent a whole new ParseExprKind enum, feels like a bit of
an overkill. My only objection is that this is exactly like
EXPR_KIND_RETURNING, hence EXPR_KIND_MERGE_RETURNING needs to be dealt
with exactly in as many places. But I don't have any alternative
proposals.That's all gone now from the new patch, since there is no longer any
restriction on where these functions can appear.
I believe this elog can be safely turned into an Assert.
+ switch (mergeActionCmdType)
{
- CmdType commandType = relaction->mas_action->commandType;
....
+ case CMD_INSERT:
....
+ default:
+ elog(ERROR, "unrecognized commandType: %d", (int)
mergeActionCmdType);
The same treatment can be applied to the elog(ERROR) in pg_merge_action().
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int, + OUT action text, OUT tid int, OUT new_balance int) +LANGUAGE sql AS +$$ + MERGE INTO sq_target t + USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta) + ON tid = v.sid + WHEN MATCHED AND tid > 2 THENAgain, for consistency, the comparison operator should be >=. There
are a few more occurrences of this comparison in the rest of the file,
that need the same treatment.I changed the new tests to use ">= 2" (and the COPY test now returns 3
rows, with an action of each type, which is easier to read), but I
don't think it's really necessary to change all the existing tests
from "> 2". There's nothing wrong with the "= 2" case having no
action, as long as the tests give decent coverage.
I was just trying to drive these tests towards a consistent pattern.
As a reader, if I see these differences, the first and the
conservative thought I have is that these differences must be there
for a reason, and then I have to work to find out what those reasons
might be. And that's a lot of wasted effort, just in case someone
intends to change something in these tests.
I performed this round of review by comparing the diff between the v7
and v8 patches (after applying to commit 4f4d73466d)
-ExecProcessReturning(ResultRelInfo *resultRelInfo,
+ExecProcessReturning(ModifyTableContext *context,
+ ResultRelInfo *resultRelInfo,
...
+ TupleTableSlot *rslot;
...
+ if (context->relaction)
+ {
...
+ PG_TRY();
+ {
+ rslot = ExecProject(projectReturning);
+ }
+ PG_FINALLY();
+ {
+ mergeActionCmdType = saved_mergeActionCmdType;
+ mergeActionIdx = saved_mergeActionIdx;
+ }
+ PG_END_TRY();
+ }
+ else
+ rslot = ExecProject(projectReturning);
+
+ return rslot;
In the above hunk, if there's an exception/ERROR, I believe we should
PG_RE_THROW(). If there's a reason to continue, we should at least set
rslot = NULL, otherwise we may be returning an uninitialized value to
the caller.
{ oid => '9499', descr => 'command type of current MERGE action',
- proname => 'pg_merge_action', provolatile => 'v',
+ proname => 'pg_merge_action', provolatile => 'v', proparallel => 'r',
....
{ oid => '9500', descr => 'index of current MERGE WHEN clause',
- proname => 'pg_merge_when_clause', provolatile => 'v',
+ proname => 'pg_merge_when_clause', provolatile => 'v', proparallel => 'r',
....
I see that you've now set proparallel of these functions to 'r'. I'd
just like to understand how you got to that conclusion.
--- error when using MERGE support functions outside MERGE
-SELECT pg_merge_action() FROM sq_target;
I believe it would be worthwhile to keep a record of the expected
outputs of these invocations in the tests, just in case they change
over time.
Best regards,
Gurjeet
http://Gurje.et
On Tue, Jul 11, 2023 at 1:43 PM Gurjeet Singh <gurjeet@singh.im> wrote:
In the above hunk, if there's an exception/ERROR, I believe we should
PG_RE_THROW(). If there's a reason to continue, we should at least set
rslot = NULL, otherwise we may be returning an uninitialized value to
the caller.
Excuse the brain-fart on my part. There's not need to PG_RE_THROW(),
since there's no PG_CATCH(). Re-learning the code's infrastructure
slowly :-)
Best regards,
Gurjeet
http://Gurje.et
On Sun, 2023-01-22 at 19:58 +0100, Alvaro Herrera wrote:
(We do have to keep our fingers
crossed that they will decide to use the same RETURNING syntax as we
do
in this patch, of course.)
Do we have a reason to think that they will accept something similar?
Regards,
Jeff Davis
On 7/12/23 02:43, Jeff Davis wrote:
On Sun, 2023-01-22 at 19:58 +0100, Alvaro Herrera wrote:
(We do have to keep our fingers
crossed that they will decide to use the same RETURNING syntax as we
do
in this patch, of course.)Do we have a reason to think that they will accept something similar?
We have reason to think that they won't care at all.
There is no RETURNING clause in Standard SQL, and the way they would do
this is:
SELECT ...
FROM OLD TABLE (
MERGE ...
) AS m
The rules for that for MERGE are well defined.
--
Vik Fearing
On Wed, 2023-07-12 at 03:47 +0200, Vik Fearing wrote:
There is no RETURNING clause in Standard SQL, and the way they would
do
this is:SELECT ...
FROM OLD TABLE (
MERGE ...
) AS mThe rules for that for MERGE are well defined.
I only see OLD TABLE referenced as part of a trigger definition. Where
is it defined for MERGE?
In any case, as long as the SQL standard doesn't conflict, then we're
fine. And it looks unlikely to cause a conflict right now that wouldn't
also be a conflict with our existing RETURNING clause elsewhere, so I'm
not seeing a problem here.
Regards,
Jeff Davis
On 7/13/23 01:48, Jeff Davis wrote:
On Wed, 2023-07-12 at 03:47 +0200, Vik Fearing wrote:
There is no RETURNING clause in Standard SQL, and the way they would
do
this is:SELECT ...
FROM OLD TABLE (
MERGE ...
) AS mThe rules for that for MERGE are well defined.
I only see OLD TABLE referenced as part of a trigger definition. Where
is it defined for MERGE?
Look up <data change delta table> for that syntax. For how MERGE
generates those, see 9075-2:2023 Section 14.12 <merge statement> General
Rules 6.b and 6.c.
In any case, as long as the SQL standard doesn't conflict, then we're
fine. And it looks unlikely to cause a conflict right now that wouldn't
also be a conflict with our existing RETURNING clause elsewhere, so I'm
not seeing a problem here.
I do not see a problem either, which was what I was trying to express
(perhaps poorly). At least not with the syntax. I have not yet tested
that the returned rows match the standard.
--
Vik Fearing
On Sun, 2023-01-08 at 12:28 +0000, Dean Rasheed wrote:
I considered allowing a separate RETURNING list at the end of each
action, but rapidly dismissed that idea.
One potential benefit of that approach is that it would be more natural
to specify output specific to the action, e.g.
WHEN MATCHED THEN UPDATE ... RETURNING 'UPDATE', ...
which would be an alternative to the special function pg_merge_action()
or "WITH WHEN".
I agree that it can be awkward to specify multiple RETURNING clauses
and get the columns to match up, but it's hard for me to say whether
it's better or worse without knowing more about the use cases.
Regards,
Jeff Davis
On Tue, 11 Jul 2023 at 21:43, Gurjeet Singh <gurjeet@singh.im> wrote:
I think the name of function pg_merge_when_clause() can be improved.
How about pg_merge_when_clause_ordinal().That's a bit of a mouthful, but I don't have a better idea right now.
I've left the names alone for now, in case something better occurs to
anyone.+1. How do we make sure we don't forget that it needs to be named
better. Perhaps a TODO item within the patch will help.
Thinking about that some more, I think the word "number" is more
familiar to most people than "ordinal". There's the row_number()
function, for example. So perhaps pg_merge_when_clause_number() would
be a better name. It's still quite long, but it's the best I can think
of.
I believe this elog can be safely turned into an Assert.
+ switch (mergeActionCmdType) { - CmdType commandType = relaction->mas_action->commandType; .... + case CMD_INSERT: .... + default: + elog(ERROR, "unrecognized commandType: %d", (int) mergeActionCmdType);The same treatment can be applied to the elog(ERROR) in pg_merge_action().
Hmm, that's a very common code pattern used in dozens, if not hundreds
of places throughout the backend code, so I don't think this should be
different.
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int, + OUT action text, OUT tid int, OUT new_balance int) +LANGUAGE sql AS +$$ + MERGE INTO sq_target t + USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta) + ON tid = v.sid + WHEN MATCHED AND tid > 2 THENAgain, for consistency, the comparison operator should be >=. There
are a few more occurrences of this comparison in the rest of the file,
that need the same treatment.I changed the new tests to use ">= 2" (and the COPY test now returns 3
rows, with an action of each type, which is easier to read), but I
don't think it's really necessary to change all the existing tests
from "> 2". There's nothing wrong with the "= 2" case having no
action, as long as the tests give decent coverage.I was just trying to drive these tests towards a consistent pattern.
As a reader, if I see these differences, the first and the
conservative thought I have is that these differences must be there
for a reason, and then I have to work to find out what those reasons
might be. And that's a lot of wasted effort, just in case someone
intends to change something in these tests.
OK, I see what you're saying. I think it should be a follow-on patch
though, because I don't like the idea of this patch to be making
changes to tests not related to the feature being added.
{ oid => '9499', descr => 'command type of current MERGE action', - proname => 'pg_merge_action', provolatile => 'v', + proname => 'pg_merge_action', provolatile => 'v', proparallel => 'r', .... { oid => '9500', descr => 'index of current MERGE WHEN clause', - proname => 'pg_merge_when_clause', provolatile => 'v', + proname => 'pg_merge_when_clause', provolatile => 'v', proparallel => 'r', ....I see that you've now set proparallel of these functions to 'r'. I'd
just like to understand how you got to that conclusion.
Now that these functions can appear in subqueries in the RETURNING
list, there exists the theoretical possibility that the subquery might
use a parallel plan (actually that can't happen today, for any query
that modifies data, but maybe someday it may become a possibility),
and it's possible to use these functions in a SELECT query inside a
PL/pgSQL function called from the RETURNING list, which might consider
a parallel plan. Since these functions rely on access to executor
state that isn't copied to parallel workers, they must be run on the
leader, hence I think PARALLEL RESTRICTED is the right level to use. A
similar example is pg_trigger_depth().
--- error when using MERGE support functions outside MERGE -SELECT pg_merge_action() FROM sq_target;I believe it would be worthwhile to keep a record of the expected
outputs of these invocations in the tests, just in case they change
over time.
Yeah, that makes sense. I'll post an update soon.
Regards,
Dean
On Mon, 2023-01-09 at 12:29 +0000, Dean Rasheed wrote:
Would it be feasible to allow specifying old.column or new.column?
These would always be NULL for INSERT and DELETE respectively but
more useful with UPDATE. Actually I've been meaning to ask this
question about UPDATE … RETURNING.I too have wished for the ability to do that with UPDATE ...
RETURNING, though I'm not sure how feasible it is.I think it's something best considered separately though. I haven't
given any thought as to how to make it work, so there might be
technical difficulties. But if it could be made to work for UPDATE,
it
shouldn't be much more effort to make it work for MERGE.
MERGE can end up combining old and new values in a way that doesn't
happen with INSERT/UPDATE/DELETE. For instance, a "MERGE ... RETURNING
id" would return a mix of NEW.id (for INSERT/UPDATE actions) and OLD.id
(for DELETE actions).
The pg_merge_action() can differentiate the old and new values, but
it's a bit more awkward.
I'm fine considering that as a separate patch, but it does seem worth
discussing briefly here.
Regards,
Jeff Davis
On Mon, 2023-01-09 at 12:29 +0000, Dean Rasheed wrote:
Would it be useful to have just the action? Perhaps "WITH ACTION"?
My idea is that this would return an enum of INSERT, UPDATE, DELETE
(so is "action" the right word?). It seems to me in many situations
I would be more likely to care about which of these 3 happened
rather than the exact clause that applied. This isn't necessarily
meant to be instead of your suggestion because I can imagine
wanting to know the exact clause, just an alternative that might
suffice in many situations. Using it would also avoid problems
arising from editing the query in a way which changes the numbers
of the clauses.Hmm, perhaps that's something that can be added as well. Both use
cases seem useful.
Can you expand a bit on the use cases for identifying individual WHEN
clauses? I see that it offers a new capability beyond just the action
type, but I'm having trouble thinking of real use cases.
Regards,
Jeff Davis
On Thu, Jul 13, 2023 at 8:38 AM Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
On Tue, 11 Jul 2023 at 21:43, Gurjeet Singh <gurjeet@singh.im> wrote:
{ oid => '9499', descr => 'command type of current MERGE action', - proname => 'pg_merge_action', provolatile => 'v', + proname => 'pg_merge_action', provolatile => 'v', proparallel => 'r', .... { oid => '9500', descr => 'index of current MERGE WHEN clause', - proname => 'pg_merge_when_clause', provolatile => 'v', + proname => 'pg_merge_when_clause', provolatile => 'v', proparallel => 'r', ....I see that you've now set proparallel of these functions to 'r'. I'd
just like to understand how you got to that conclusion.Now that these functions can appear in subqueries in the RETURNING
list, there exists the theoretical possibility that the subquery might
use a parallel plan (actually that can't happen today, for any query
that modifies data, but maybe someday it may become a possibility),
and it's possible to use these functions in a SELECT query inside a
PL/pgSQL function called from the RETURNING list, which might consider
a parallel plan. Since these functions rely on access to executor
state that isn't copied to parallel workers, they must be run on the
leader, hence I think PARALLEL RESTRICTED is the right level to use. A
similar example is pg_trigger_depth().
Thanks for the explanation. That helps.
Best regards,
Gurjeet
http://Gurje.et
On 7/13/23 17:38, Dean Rasheed wrote:
On Tue, 11 Jul 2023 at 21:43, Gurjeet Singh <gurjeet@singh.im> wrote:
I think the name of function pg_merge_when_clause() can be improved.
How about pg_merge_when_clause_ordinal().That's a bit of a mouthful, but I don't have a better idea right now.
I've left the names alone for now, in case something better occurs to
anyone.+1. How do we make sure we don't forget that it needs to be named
better. Perhaps a TODO item within the patch will help.Thinking about that some more, I think the word "number" is more
familiar to most people than "ordinal". There's the row_number()
function, for example.
There is also the WITH ORDINALITY and FOR ORDINALITY examples.
So perhaps pg_merge_when_clause_number() would
be a better name. It's still quite long, but it's the best I can think
of.
How about pg_merge_match_number() or pg_merge_ordinality()?
--
Vik Fearing
On Thu, 13 Jul 2023 at 17:01, Jeff Davis <pgsql@j-davis.com> wrote:
MERGE can end up combining old and new values in a way that doesn't
happen with INSERT/UPDATE/DELETE. For instance, a "MERGE ... RETURNING
id" would return a mix of NEW.id (for INSERT/UPDATE actions) and OLD.id
(for DELETE actions).
Right, but allowing OLD/NEW.colname in the RETURNING list would remove
that complication, and it shouldn't change how a bare colname
reference behaves.
The pg_merge_action() can differentiate the old and new values, but
it's a bit more awkward.
For some use cases, I can imagine allowing OLD/NEW.colname would mean
you wouldn't need pg_merge_action() (if the column was NOT NULL), so I
think the features should work well together.
Regards,
Dean
On Thu, 13 Jul 2023 at 17:43, Vik Fearing <vik@postgresfriends.org> wrote:
There is also the WITH ORDINALITY and FOR ORDINALITY examples.
True. I just think "number" is a friendlier, more familiar word than "ordinal".
So perhaps pg_merge_when_clause_number() would
be a better name. It's still quite long, but it's the best I can think
of.How about pg_merge_match_number() or pg_merge_ordinality()?
I think "match_number" is problematic, because it might be a "matched"
or a "not matched" action. "when_clause" is the term used on the MERGE
doc page.
Regards,
Dean
On Thu, 2023-07-13 at 18:01 +0100, Dean Rasheed wrote:
For some use cases, I can imagine allowing OLD/NEW.colname would mean
you wouldn't need pg_merge_action() (if the column was NOT NULL), so
I
think the features should work well together.
For use cases where a user could do it either way, which would you
expect to be the "typical" way (assuming we supported the new/old)?
MERGE ... RETURNING pg_merge_action(), id, val;
or
MERGE ... RETURNING id, OLD.val, NEW.val;
?
I am still bothered that pg_merge_action() is so context-sensitive.
"SELECT pg_merge_action()" by itself doesn't make any sense, but it's
allowed in the v8 patch. We could make that a runtime error, which
would be better, but it feels like it's structurally wrong. This is not
an objection, but it's just making me think harder about alternatives.
Maybe instead of a function it could be a special table reference like:
MERGE ... RETURNING MERGE.action, MERGE.action_number, id, val?
Regards,
Jeff Davis
On Thu, 13 Jul 2023 at 20:14, Jeff Davis <pgsql@j-davis.com> wrote:
On Thu, 2023-07-13 at 18:01 +0100, Dean Rasheed wrote:
For some use cases, I can imagine allowing OLD/NEW.colname would mean
you wouldn't need pg_merge_action() (if the column was NOT NULL), so
I
think the features should work well together.For use cases where a user could do it either way, which would you
expect to be the "typical" way (assuming we supported the new/old)?MERGE ... RETURNING pg_merge_action(), id, val;
or
MERGE ... RETURNING id, OLD.val, NEW.val;
?
I think it might depend on whether OLD.val and NEW.val were actually
required, but I think I would still probably use pg_merge_action() to
get the action, since it doesn't rely on specific table columns being
NOT NULL. It's a little like writing a trigger function that handles
multiple command types. You could use OLD and NEW to deduce whether it
was an INSERT, UPDATE or DELETE, or you could use TG_OP. I tend to use
TG_OP, but maybe there are situations where using OLD and NEW is more
natural.
I found a 10-year-old thread discussing adding support for OLD/NEW to
RETURNING [1]/messages/by-id/51822C0F.5030807@gmail.com, but it doesn't look like anything close to a
committable solution was developed, or even a design that might lead
to one. That's a shame, because there seemed to be a lot of demand for
the feature, but it's not clear how much effort it would be to
implement.
I am still bothered that pg_merge_action() is so context-sensitive.
"SELECT pg_merge_action()" by itself doesn't make any sense, but it's
allowed in the v8 patch. We could make that a runtime error, which
would be better, but it feels like it's structurally wrong. This is not
an objection, but it's just making me think harder about alternatives.Maybe instead of a function it could be a special table reference like:
MERGE ... RETURNING MERGE.action, MERGE.action_number, id, val?
Well, that's a little more concise, but I'm not sure that it really
buys us that much, to be worth the extra complication. Presumably
something in the planner would turn that into something the executor
could handle, which might just end up being the existing functions
anyway.
Regards,
Dean
On Fri, 2023-07-14 at 09:55 +0100, Dean Rasheed wrote:
I found a 10-year-old thread discussing adding support for OLD/NEW to
RETURNING [1], but it doesn't look like anything close to a
committable solution was developed, or even a design that might lead
to one. That's a shame, because there seemed to be a lot of demand
for
the feature, but it's not clear how much effort it would be to
implement.
It looks like progress was made in the direction of using a table alias
with executor support to bring the right attributes along.
There was some concern about how exactly the table alias should work
such that it doesn't look too much like a join. Not sure how much of a
problem that is.
Maybe instead of a function it could be a special table reference
like:MERGE ... RETURNING MERGE.action, MERGE.action_number, id, val?
Well, that's a little more concise, but I'm not sure that it really
buys us that much, to be worth the extra complication. Presumably
something in the planner would turn that into something the executor
could handle, which might just end up being the existing functions
anyway.
The benefits are:
1. It is naturally constrained to the right context. It doesn't require
global variables and the PG_TRY/PG_FINALLY, and can't be called in the
wrong contexts (like SELECT).
2. More likely to be consistent with eventual support for NEW/OLD
(actually BEFORE/AFTER for reasons the prior thread discussed).
I'm not sure how much extra complication it would cause, though.
Regards,
Jeff Davis
On Fri, Jul 14, 2023 at 1:55 AM Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
On Thu, 13 Jul 2023 at 20:14, Jeff Davis <pgsql@j-davis.com> wrote:
On Thu, 2023-07-13 at 18:01 +0100, Dean Rasheed wrote:
For some use cases, I can imagine allowing OLD/NEW.colname would mean
you wouldn't need pg_merge_action() (if the column was NOT NULL), so
I
think the features should work well together.For use cases where a user could do it either way, which would you
expect to be the "typical" way (assuming we supported the new/old)?MERGE ... RETURNING pg_merge_action(), id, val;
or
MERGE ... RETURNING id, OLD.val, NEW.val;
?
I think it might depend on whether OLD.val and NEW.val were actually
required, but I think I would still probably use pg_merge_action() to
get the action, since it doesn't rely on specific table columns being
NOT NULL.
+1. It would be better to expose the action explicitly, rather than
asking the user to deduce it based on the old and new values of a
column. The server providing that value is better than letting users
rely on error-prone methods.
I found a 10-year-old thread discussing adding support for OLD/NEW to
RETURNING [1],
Thanks for digging up that thread. An important concern brought up in
that thread was how the use of names OLD and NEW will affect plpgsql
(an possibly other PLs) trigger functions, which rely on specific
meaning for those names. The names BEFORE and AFTER, proposed there
are not as intuitive as OLD/NEW for the purpose of identifying old and
new versions of the row, but I don't have a better proposal. Perhaps
PREVIOUS and CURRENT?
but it doesn't look like anything close to a
committable solution was developed, or even a design that might lead
to one. That's a shame, because there seemed to be a lot of demand for
the feature,
+1
I am still bothered that pg_merge_action() is so context-sensitive.
"SELECT pg_merge_action()" by itself doesn't make any sense, but it's
allowed in the v8 patch. We could make that a runtime error, which
would be better, but it feels like it's structurally wrong. This is not
an objection, but it's just making me think harder about alternatives.Maybe instead of a function it could be a special table reference like:
MERGE ... RETURNING MERGE.action, MERGE.action_number, id, val?
I believe Jeff meant s/action_number/when_number/. Not that we've
settled on a name for this virtual column.
Well, that's a little more concise, but I'm not sure that it really
buys us that much, to be worth the extra complication.
After considering the options, and their pros and cons (ease of
implementation, possibility of conflict with SQL spec, intuitiveness
of syntax), I'm now strongly leaning towards the SQL syntax variant.
Exposing the action taken via a context-sensitive function feels
kludgy, when compared to Jeff's proposed SQL syntax. Don't get me
wrong, I still feel it was very clever how you were able to make the
function context sensitive, and make it work in expressions deeper in
the subqueries.
Plus, if we were able to make it work as SQL syntax, it's very likely
we can use the same technique to implement BEFORE and AFTER behaviour
in UPDATE ... RETURNING that the old thread could not accomplish a
decade ago.
Presumably
something in the planner would turn that into something the executor
could handle, which might just end up being the existing functions
anyway.
If the current patch's functions can serve the needs of the SQL syntax
variant, that'd be a neat win!
Best regards,
Gurjeet
http://Gurje.et
On Mon, Jul 17, 2023 at 12:43 PM Jeff Davis <pgsql@j-davis.com> wrote:
On Fri, 2023-07-14 at 09:55 +0100, Dean Rasheed wrote:
I found a 10-year-old thread discussing adding support for OLD/NEW to
RETURNING [1], but it doesn't look like anything close to a
committable solution was developed, or even a design that might lead
to one. That's a shame, because there seemed to be a lot of demand
for
the feature, but it's not clear how much effort it would be to
implement.It looks like progress was made in the direction of using a table alias
with executor support to bring the right attributes along.
That patch introduced RTE_ALIAS to carry that info through to the
executor, and having to special-case that that in many places was seen
as a bad thing.
There was some concern about how exactly the table alias should work
such that it doesn't look too much like a join. Not sure how much of a
problem that is.
My understanding of that thread is that the join example Robert shared
was for illustrative purposes only, to show that executor already has
enough information to produce the desired output, and to show that
it's a matter of tweaking the parser and planner to tell the executor
what output to produce. But later reviewers pointed out that it's not
that simple (example was given of ExecDelete performing
pullups/working hard to get the correct values of the old version of
the row).
The concerns were mainly around use of OLD.* and NEW.*, too much
special-casing of RTE_ALIAS, and then the code quality of the patch
itself.
Maybe instead of a function it could be a special table reference
like:MERGE ... RETURNING MERGE.action, MERGE.action_number, id, val?
Well, that's a little more concise, but I'm not sure that it really
buys us that much, to be worth the extra complication. Presumably
something in the planner would turn that into something the executor
could handle, which might just end up being the existing functions
anyway.The benefits are:
1. It is naturally constrained to the right context.
+1
I'm not sure how much extra complication it would cause, though.
If that attempt with UPDATE RETURNING a decade ago is any indication,
it's probably a tough one.
Best regards,
Gurjeet
http://Gurje.et
On Thu, 2023-07-20 at 23:19 -0700, Gurjeet Singh wrote:
Plus, if we were able to make it work as SQL syntax, it's very likely
we can use the same technique to implement BEFORE and AFTER behaviour
in UPDATE ... RETURNING that the old thread could not accomplish a
decade ago.
To clarify, I don't think having a special table alias will require any
changes in gram.y and I don't consider it a syntactical change.
I haven't looked into the implementation yet.
Regards,
Jeff Davis
On Mon, 17 Jul 2023 at 20:43, Jeff Davis <pgsql@j-davis.com> wrote:
Maybe instead of a function it could be a special table reference
like:MERGE ... RETURNING MERGE.action, MERGE.action_number, id, val?
The benefits are:
1. It is naturally constrained to the right context. It doesn't require
global variables and the PG_TRY/PG_FINALLY, and can't be called in the
wrong contexts (like SELECT).2. More likely to be consistent with eventual support for NEW/OLD
(actually BEFORE/AFTER for reasons the prior thread discussed).
Thinking about this some more, I think that the point about
constraining these functions to the right context is a reasonable one,
and earlier versions of this patch did that better, without needing
global variables or a PG_TRY/PG_FINALLY block.
Here is an updated patch that goes back to doing it that way. This is
more like the way that aggregate functions and GROUPING() work, in
that the parser constrains the location from which the functions can
be used, and at execution time, the functions rely on the relevant
context being passed via the FunctionCallInfo context.
It's still possible to use these functions in subqueries in the
RETURNING list, but attempting to use them anywhere else (like a
SELECT on its own) will raise an error at parse time. If they do
somehow get invoked in a non-MERGE context, they will elog an error
(again, just like aggregate functions), because that's a "shouldn't
happen" error.
This does nothing to be consistent with eventual support for
BEFORE/AFTER, but I think that's really an entirely separate thing,
and likely to work quite differently, internally.
From a user perspective, writing something like "BEFORE.id" is quite
natural, because it's clear that "id" is a column, and "BEFORE" is the
old state of the table. Writing something like "MERGE.action" seems a
lot more counter-intuitive, because "action" isn't a column of
anything (and if it was, I think this syntax would potentially cause
even more confusion).
So really, I think "MERGE.action" is an abuse of the syntax,
inconsistent with any other SQL syntax, and using functions is much
more natural, akin to GROUPING(), for example.
Regards,
Dean
Attachments:
support-merge-returning-v9.patchtext/x-patch; charset=US-ASCII; name=support-merge-returning-v9.patchDownload
diff --git a/doc/src/sgml/dml.sgml b/doc/src/sgml/dml.sgml
new file mode 100644
index cbbc5e2..7f65f6e
--- a/doc/src/sgml/dml.sgml
+++ b/doc/src/sgml/dml.sgml
@@ -283,10 +283,15 @@ DELETE FROM products;
<secondary>RETURNING</secondary>
</indexterm>
+ <indexterm zone="dml-returning">
+ <primary>MERGE</primary>
+ <secondary>RETURNING</secondary>
+ </indexterm>
+
<para>
Sometimes it is useful to obtain data from modified rows while they are
being manipulated. The <command>INSERT</command>, <command>UPDATE</command>,
- and <command>DELETE</command> commands all have an
+ <command>DELETE</command>, and <command>MERGE</command> commands all have an
optional <literal>RETURNING</literal> clause that supports this. Use
of <literal>RETURNING</literal> avoids performing an extra database query to
collect the data, and is especially valuable when it would otherwise be
@@ -339,6 +344,24 @@ DELETE FROM products
</programlisting>
</para>
+ <para>
+ In a <command>MERGE</command>, the data available to <literal>RETURNING</literal> is
+ the content of the source row plus the content of the inserted, updated, or
+ deleted target row. In addition, the merge support functions
+ <xref linkend="pg-merge-action"/> and <xref linkend="pg-merge-when-clause-number"/>
+ may be used to return information about which merge action was executed for
+ each row. Since it is quite common for the source and target to have many
+ of the same columns, specifying <literal>RETURNING *</literal> can lead to a
+ lot of duplicated columns, so it is often more useful to just return the
+ target row. For example:
+<programlisting>
+MERGE INTO products p USING new_products n ON p.product_no = n.product_no
+ WHEN NOT MATCHED THEN INSERT VALUES (n.product_no, n.name, n.price)
+ WHEN MATCHED THEN UPDATE SET name = n.name, price = n.price
+ RETURNING pg_merge_action(), p.*;
+</programlisting>
+ </para>
+
<para>
If there are triggers (<xref linkend="triggers"/>) on the target table,
the data available to <literal>RETURNING</literal> is the row as modified by
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
new file mode 100644
index b948276..b84a95a
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -21705,6 +21705,100 @@ SELECT count(*) FROM sometable;
</sect1>
+ <sect1 id="functions-merge-support">
+ <title>Merge Support Functions</title>
+
+ <indexterm>
+ <primary>MERGE</primary>
+ <secondary>RETURNING</secondary>
+ </indexterm>
+
+ <para>
+ The merge support functions shown in
+ <xref linkend="functions-merge-support-table"/> may be used in the
+ <literal>RETURNING</literal> list of a <xref linkend="sql-merge"/> command
+ to return additional information about the action taken for each row.
+ </para>
+
+ <table id="functions-merge-support-table">
+ <title>Merge Support Functions</title>
+
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ Function
+ </para>
+ <para>
+ Description
+ </para></entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm id="pg-merge-action">
+ <primary>pg_merge_action</primary>
+ </indexterm>
+ <function>pg_merge_action</function> ( )
+ <returnvalue>text</returnvalue>
+ </para>
+ <para>
+ Returns the action performed on the current row (<literal>'INSERT'</literal>,
+ <literal>'UPDATE'</literal>, or <literal>'DELETE'</literal>).
+ </para></entry>
+ </row>
+
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm id="pg-merge-when-clause-number">
+ <primary>pg_merge_when_clause_number</primary>
+ </indexterm>
+ <function>pg_merge_when_clause_number</function> ( )
+ <returnvalue>integer</returnvalue>
+ </para>
+ <para>
+ Returns the 1-based index of the <literal>WHEN</literal> clause executed
+ for the current row.
+ </para></entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+
+ <para>
+ Example:
+<screen><![CDATA[
+MERGE INTO products p
+ USING stock s ON p.product_id = s.product_id
+ WHEN MATCHED AND s.quantity > 0 THEN
+ UPDATE SET in_stock = true, quantity = s.quantity
+ WHEN MATCHED THEN
+ UPDATE SET in_stock = false, quantity = 0
+ WHEN NOT MATCHED THEN
+ INSERT (product_id, in_stock, quantity)
+ VALUES (s.product_id, true, s.quantity)
+ RETURNING pg_merge_action() AS action,
+ pg_merge_when_clause_number() AS clause_number,
+ p.*;
+
+ action | clause_number | product_id | in_stock | quantity
+--------+---------------+------------+----------+----------
+ UPDATE | 1 | 1001 | t | 50
+ UPDATE | 2 | 1002 | f | 0
+ INSERT | 3 | 1003 | t | 10
+]]></screen>
+ </para>
+
+ <para>
+ Note that these functions can only be used in the <literal>RETURNING</literal>
+ list of a <command>MERGE</command> command. It is an error to use them in
+ any other part of a query.
+ </para>
+
+ </sect1>
+
<sect1 id="functions-subquery">
<title>Subquery Expressions</title>
diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml
new file mode 100644
index fe8def4..51a15ca
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -1387,9 +1387,9 @@
to a <glossterm linkend="glossary-client">client</glossterm> upon the
completion of an <acronym>SQL</acronym> command, usually a
<command>SELECT</command> but it can be an
- <command>INSERT</command>, <command>UPDATE</command>, or
- <command>DELETE</command> command if the <literal>RETURNING</literal>
- clause is specified.
+ <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command> command if the
+ <literal>RETURNING</literal> clause is specified.
</para>
<para>
The fact that a result set is a relation means that a query can be used
diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml
new file mode 100644
index f55e901..6803240
--- a/doc/src/sgml/plpgsql.sgml
+++ b/doc/src/sgml/plpgsql.sgml
@@ -1020,8 +1020,8 @@ INSERT INTO mytable VALUES (1,'one'), (2
</para>
<para>
- If the command does return rows (for example <command>SELECT</command>,
- or <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>
+ If the command does return rows (for example <command>SELECT</command>, or
+ <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command>
with <literal>RETURNING</literal>), there are two ways to proceed.
When the command will return at most one row, or you only care about
the first row of output, write the command as usual but add
@@ -1149,6 +1149,7 @@ SELECT <replaceable>select_expressions</
INSERT ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
UPDATE ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
DELETE ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
+MERGE ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
</synopsis>
where <replaceable>target</replaceable> can be a record variable, a row
@@ -1159,8 +1160,8 @@ DELETE ... RETURNING <replaceable>expres
<literal>INTO</literal> clause) just as described above,
and the plan is cached in the same way.
This works for <command>SELECT</command>,
- <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command> with
- <literal>RETURNING</literal>, and certain utility commands
+ <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command>
+ with <literal>RETURNING</literal>, and certain utility commands
that return row sets, such as <command>EXPLAIN</command>.
Except for the <literal>INTO</literal> clause, the SQL command is the same
as it would be written outside <application>PL/pgSQL</application>.
@@ -1236,7 +1237,7 @@ END;
</para>
<para>
- For <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command> with
+ For <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command> with
<literal>RETURNING</literal>, <application>PL/pgSQL</application> reports
an error for more than one returned row, even when
<literal>STRICT</literal> is not specified. This is because there
diff --git a/doc/src/sgml/queries.sgml b/doc/src/sgml/queries.sgml
new file mode 100644
index 3f95849..9928373
--- a/doc/src/sgml/queries.sgml
+++ b/doc/src/sgml/queries.sgml
@@ -2062,9 +2062,10 @@ SELECT <replaceable>select_list</replace
Table Expressions or <acronym>CTE</acronym>s, can be thought of as defining
temporary tables that exist just for one query. Each auxiliary statement
in a <literal>WITH</literal> clause can be a <command>SELECT</command>,
- <command>INSERT</command>, <command>UPDATE</command>, or <command>DELETE</command>; and the
+ <command>INSERT</command>, <command>UPDATE</command>, <command>DELETE</command>,
+ or <command>MERGE</command>; and the
<literal>WITH</literal> clause itself is attached to a primary statement that can
- be a <command>SELECT</command>, <command>INSERT</command>, <command>UPDATE</command>,
+ also be a <command>SELECT</command>, <command>INSERT</command>, <command>UPDATE</command>,
<command>DELETE</command>, or <command>MERGE</command>.
</para>
@@ -2598,8 +2599,8 @@ SELECT * FROM w AS w1 JOIN w AS w2 ON w1
<title>Data-Modifying Statements in <literal>WITH</literal></title>
<para>
- You can use most data-modifying statements (<command>INSERT</command>,
- <command>UPDATE</command>, or <command>DELETE</command>, but not
+ You can use data-modifying statements (<command>INSERT</command>,
+ <command>UPDATE</command>, <command>DELETE</command>, or
<command>MERGE</command>) in <literal>WITH</literal>. This
allows you to perform several different operations in the same query.
An example is:
diff --git a/doc/src/sgml/ref/copy.sgml b/doc/src/sgml/ref/copy.sgml
new file mode 100644
index 4d614a0..efaacb9
--- a/doc/src/sgml/ref/copy.sgml
+++ b/doc/src/sgml/ref/copy.sgml
@@ -122,13 +122,16 @@ COPY { <replaceable class="parameter">ta
A <link linkend="sql-select"><command>SELECT</command></link>,
<link linkend="sql-values"><command>VALUES</command></link>,
<link linkend="sql-insert"><command>INSERT</command></link>,
- <link linkend="sql-update"><command>UPDATE</command></link>, or
- <link linkend="sql-delete"><command>DELETE</command></link> command whose results are to be
- copied. Note that parentheses are required around the query.
+ <link linkend="sql-update"><command>UPDATE</command></link>,
+ <link linkend="sql-delete"><command>DELETE</command></link>, or
+ <link linkend="sql-merge"><command>MERGE</command></link> command
+ whose results are to be copied. Note that parentheses are required
+ around the query.
</para>
<para>
- For <command>INSERT</command>, <command>UPDATE</command> and
- <command>DELETE</command> queries a RETURNING clause must be provided,
+ For <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, and <command>MERGE</command> queries a
+ RETURNING clause must be provided,
and the target relation must not have a conditional rule, nor
an <literal>ALSO</literal> rule, nor an <literal>INSTEAD</literal> rule
that expands to multiple statements.
diff --git a/doc/src/sgml/ref/merge.sgml b/doc/src/sgml/ref/merge.sgml
new file mode 100644
index 0995fe0..01dfc98
--- a/doc/src/sgml/ref/merge.sgml
+++ b/doc/src/sgml/ref/merge.sgml
@@ -25,6 +25,7 @@ PostgreSQL documentation
MERGE INTO [ ONLY ] <replaceable class="parameter">target_table_name</replaceable> [ * ] [ [ AS ] <replaceable class="parameter">target_alias</replaceable> ]
USING <replaceable class="parameter">data_source</replaceable> ON <replaceable class="parameter">join_condition</replaceable>
<replaceable class="parameter">when_clause</replaceable> [...]
+[ RETURNING * | <replaceable class="parameter">output_expression</replaceable> [ [ AS ] <replaceable class="parameter">output_name</replaceable> ] [, ...] ]
<phrase>where <replaceable class="parameter">data_source</replaceable> is:</phrase>
@@ -95,6 +96,20 @@ DELETE
</para>
<para>
+ The optional <literal>RETURNING</literal> clause causes <command>MERGE</command>
+ to compute and return value(s) based on each row inserted, updated, or
+ deleted. Any expression using the source or target table's columns, or the
+ merge support functions <xref linkend="pg-merge-action"/> and
+ <xref linkend="pg-merge-when-clause-number"/> can be computed. When an
+ <command>INSERT</command> or <command>UPDATE</command> action is performed,
+ the new values of the target table's columns are used. When a
+ <command>DELETE</command> is performed, the old values of the target table's
+ columns are used. The syntax of the <literal>RETURNING</literal> list is
+ identical to that of the output list of <command>SELECT</command>.
+
+ </para>
+
+ <para>
There is no separate <literal>MERGE</literal> privilege.
If you specify an update action, you must have the
<literal>UPDATE</literal> privilege on the column(s)
@@ -125,6 +140,18 @@ DELETE
<variablelist>
<varlistentry>
+ <term><replaceable class="parameter">with_query</replaceable></term>
+ <listitem>
+ <para>
+ The <literal>WITH</literal> clause allows you to specify one or more
+ subqueries that can be referenced by name in the <command>MERGE</command>
+ query. See <xref linkend="queries-with"/> and <xref linkend="sql-select"/>
+ for details.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
<term><replaceable class="parameter">target_table_name</replaceable></term>
<listitem>
<para>
@@ -399,13 +426,31 @@ DELETE
</varlistentry>
<varlistentry>
- <term><replaceable class="parameter">with_query</replaceable></term>
+ <term><replaceable class="parameter">output_expression</replaceable></term>
<listitem>
<para>
- The <literal>WITH</literal> clause allows you to specify one or more
- subqueries that can be referenced by name in the <command>MERGE</command>
- query. See <xref linkend="queries-with"/> and <xref linkend="sql-select"/>
- for details.
+ An expression to be computed and returned by the <command>MERGE</command>
+ command after each row is merged. The expression can use any columns of
+ the source or target tables, or the merge support functions
+ <xref linkend="pg-merge-action"/> and
+ <xref linkend="pg-merge-when-clause-number"/>.
+ </para>
+ <para>
+ Writing <literal>*</literal> will return all columns from the source
+ table, followed by all columns from the target table. Often this will
+ lead to a lot of duplication, since it is common for the source and
+ target tables to have a lot of the same columns. This can be avoided by
+ qualifying the <literal>*</literal> with the name or alias of the source
+ or target table.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><replaceable class="parameter">output_name</replaceable></term>
+ <listitem>
+ <para>
+ A name to use for a returned column.
</para>
</listitem>
</varlistentry>
@@ -428,6 +473,13 @@ MERGE <replaceable class="parameter">tot
were changed in any way.
</para>
+ <para>
+ If the <command>MERGE</command> command contains a <literal>RETURNING</literal>
+ clause, the result will be similar to that of a <command>SELECT</command>
+ statement containing the columns and values defined in the
+ <literal>RETURNING</literal> list, computed over the row(s) inserted, updated,
+ or deleted by the command.
+ </para>
</refsect1>
<refsect1>
@@ -544,13 +596,6 @@ MERGE <replaceable class="parameter">tot
</para>
<para>
- There is no <literal>RETURNING</literal> clause with
- <command>MERGE</command>. Actions of <command>INSERT</command>,
- <command>UPDATE</command> and <command>DELETE</command> cannot contain
- <literal>RETURNING</literal> or <literal>WITH</literal> clauses.
- </para>
-
- <para>
When <command>MERGE</command> is run concurrently with other commands
that modify the target table, the usual transaction isolation rules
apply; see <xref linkend="transaction-iso"/> for an explanation
@@ -602,7 +647,8 @@ WHEN NOT MATCHED THEN
<para>
Attempt to insert a new stock item along with the quantity of stock. If
the item already exists, instead update the stock count of the existing
- item. Don't allow entries that have zero stock.
+ item. Don't allow entries that have zero stock. Return details of all
+ changes made.
<programlisting>
MERGE INTO wines w
USING wine_stock_changes s
@@ -612,7 +658,8 @@ WHEN NOT MATCHED AND s.stock_delta > 0 T
WHEN MATCHED AND w.stock + s.stock_delta > 0 THEN
UPDATE SET stock = w.stock + s.stock_delta
WHEN MATCHED THEN
- DELETE;
+ DELETE
+RETURNING pg_merge_action(), w.*;
</programlisting>
The <literal>wine_stock_changes</literal> table might be, for example, a
@@ -627,7 +674,8 @@ WHEN MATCHED THEN
This command conforms to the <acronym>SQL</acronym> standard.
</para>
<para>
- The WITH clause and <literal>DO NOTHING</literal> action are extensions to
+ The WITH clause, <literal>DO NOTHING</literal> action, and
+ <literal>RETURNING</literal> clause are extensions to
the <acronym>SQL</acronym> standard.
</para>
</refsect1>
diff --git a/doc/src/sgml/ref/select.sgml b/doc/src/sgml/ref/select.sgml
new file mode 100644
index 0ee0cc7..f81bdea
--- a/doc/src/sgml/ref/select.sgml
+++ b/doc/src/sgml/ref/select.sgml
@@ -74,7 +74,7 @@ SELECT [ ALL | DISTINCT [ ON ( <replacea
<phrase>and <replaceable class="parameter">with_query</replaceable> is:</phrase>
- <replaceable class="parameter">with_query_name</replaceable> [ ( <replaceable class="parameter">column_name</replaceable> [, ...] ) ] AS [ [ NOT ] MATERIALIZED ] ( <replaceable class="parameter">select</replaceable> | <replaceable class="parameter">values</replaceable> | <replaceable class="parameter">insert</replaceable> | <replaceable class="parameter">update</replaceable> | <replaceable class="parameter">delete</replaceable> )
+ <replaceable class="parameter">with_query_name</replaceable> [ ( <replaceable class="parameter">column_name</replaceable> [, ...] ) ] AS [ [ NOT ] MATERIALIZED ] ( <replaceable class="parameter">select</replaceable> | <replaceable class="parameter">values</replaceable> | <replaceable class="parameter">insert</replaceable> | <replaceable class="parameter">update</replaceable> | <replaceable class="parameter">delete</replaceable> | <replaceable class="parameter">merge</replaceable> )
[ SEARCH { BREADTH | DEPTH } FIRST BY <replaceable>column_name</replaceable> [, ...] SET <replaceable>search_seq_col_name</replaceable> ]
[ CYCLE <replaceable>column_name</replaceable> [, ...] SET <replaceable>cycle_mark_col_name</replaceable> [ TO <replaceable>cycle_mark_value</replaceable> DEFAULT <replaceable>cycle_mark_default</replaceable> ] USING <replaceable>cycle_path_col_name</replaceable> ]
@@ -227,10 +227,10 @@ TABLE [ ONLY ] <replaceable class="param
The subqueries effectively act as temporary tables or views
for the duration of the primary query.
Each subquery can be a <command>SELECT</command>, <command>TABLE</command>, <command>VALUES</command>,
- <command>INSERT</command>, <command>UPDATE</command> or
- <command>DELETE</command> statement.
+ <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command> statement.
When writing a data-modifying statement (<command>INSERT</command>,
- <command>UPDATE</command> or <command>DELETE</command>) in
+ <command>UPDATE</command>, <command>DELETE</command>, or <command>MERGE</command>) in
<literal>WITH</literal>, it is usual to include a <literal>RETURNING</literal> clause.
It is the output of <literal>RETURNING</literal>, <emphasis>not</emphasis> the underlying
table that the statement modifies, that forms the temporary table that is
@@ -2179,7 +2179,8 @@ SELECT 2+2;
<para>
<productname>PostgreSQL</productname> allows <command>INSERT</command>,
- <command>UPDATE</command>, and <command>DELETE</command> to be used as <literal>WITH</literal>
+ <command>UPDATE</command>, <command>DELETE</command>, and
+ <command>MERGE</command> to be used as <literal>WITH</literal>
queries. This is not found in the SQL standard.
</para>
</refsect2>
diff --git a/doc/src/sgml/rowtypes.sgml b/doc/src/sgml/rowtypes.sgml
new file mode 100644
index 4d86f97..bbeac84
--- a/doc/src/sgml/rowtypes.sgml
+++ b/doc/src/sgml/rowtypes.sgml
@@ -348,7 +348,7 @@ SELECT m.* FROM some_table, LATERAL myfu
column expansion of this kind when it appears at the top level of
a <link linkend="queries-select-lists"><command>SELECT</command> output
list</link>, a <link linkend="dml-returning"><literal>RETURNING</literal>
- list</link> in <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>,
+ list</link> in <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command>,
a <link linkend="queries-values"><literal>VALUES</literal> clause</link>, or
a <link linkend="sql-syntax-row-constructors">row constructor</link>.
In all other contexts (including when nested inside one of those
diff --git a/doc/src/sgml/spi.sgml b/doc/src/sgml/spi.sgml
new file mode 100644
index 16c3ab7..51d70c7
--- a/doc/src/sgml/spi.sgml
+++ b/doc/src/sgml/spi.sgml
@@ -301,8 +301,9 @@ SPI_execute("INSERT INTO foo SELECT * FR
is returned in the global variable <varname>SPI_processed</varname>.
If the return value of the function is <symbol>SPI_OK_SELECT</symbol>,
<symbol>SPI_OK_INSERT_RETURNING</symbol>,
- <symbol>SPI_OK_DELETE_RETURNING</symbol>, or
- <symbol>SPI_OK_UPDATE_RETURNING</symbol>,
+ <symbol>SPI_OK_DELETE_RETURNING</symbol>,
+ <symbol>SPI_OK_UPDATE_RETURNING</symbol>, or
+ <symbol>SPI_OK_MERGE_RETURNING</symbol>
then you can use the
global pointer <literal>SPITupleTable *SPI_tuptable</literal> to
access the result rows. Some utility commands (such as
@@ -472,6 +473,15 @@ typedef struct SPITupleTable
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><symbol>SPI_OK_MERGE_RETURNING</symbol></term>
+ <listitem>
+ <para>
+ if a <command>MERGE RETURNING</command> was executed
+ </para>
+ </listitem>
+ </varlistentry>
<varlistentry>
<term><symbol>SPI_OK_UTILITY</symbol></term>
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
new file mode 100644
index 9620ea9..4f68efb
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -177,7 +177,8 @@
statements separated by semicolons. A semicolon after the last
statement is optional. Unless the function is declared to return
<type>void</type>, the last statement must be a <command>SELECT</command>,
- or an <command>INSERT</command>, <command>UPDATE</command>, or <command>DELETE</command>
+ or an <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command>
that has a <literal>RETURNING</literal> clause.
</para>
@@ -1236,8 +1237,9 @@ SELECT x, CASE WHEN x > 0 THEN genera
<note>
<para>
- If a function's last command is <command>INSERT</command>, <command>UPDATE</command>,
- or <command>DELETE</command> with <literal>RETURNING</literal>, that command will
+ If a function's last command is <command>INSERT</command>,
+ <command>UPDATE</command>, <command>DELETE</command>, or
+ <command>MERGE</command> with <literal>RETURNING</literal>, that command will
always be executed to completion, even if the function is not declared
with <literal>SETOF</literal> or the calling query does not fetch all the
result rows. Any extra rows produced by the <literal>RETURNING</literal>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
new file mode 100644
index f14fae3..05f2e5b
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -283,12 +283,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 9e4b243..cf56f9f
--- 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 bf3a08c..7bdaac1
--- 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"
@@ -2590,6 +2591,22 @@ ExecInitFunc(ExprEvalStep *scratch, Expr
InitFunctionCallInfoData(*fcinfo, flinfo,
nargs, inputcollid, NULL, NULL);
+ /*
+ * Merge support functions should only be called 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 eb8a87f..b9bd03c
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -612,8 +612,8 @@ ExecInitPartitionInfo(ModifyTableState *
* Build the RETURNING projection for the partition. Note that we didn't
* build the returningList for partitions within the planner, but simple
* translation of varattnos will suffice. This only occurs for the INSERT
- * case or in the case of UPDATE tuple routing where we didn't find a
- * result rel to reuse.
+ * case or in the case of UPDATE/MERGE tuple routing where we didn't find
+ * a result rel to reuse.
*/
if (node && node->returningLists != NIL)
{
@@ -622,12 +622,14 @@ ExecInitPartitionInfo(ModifyTableState *
List *returningList;
/* See the comment above for WCO lists. */
- /* (except no RETURNING support for MERGE yet) */
Assert((node->operation == CMD_INSERT &&
list_length(node->returningLists) == 1 &&
list_length(node->resultRelations) == 1) ||
(node->operation == CMD_UPDATE &&
list_length(node->returningLists) ==
+ list_length(node->resultRelations)) ||
+ (node->operation == CMD_MERGE &&
+ list_length(node->returningLists) ==
list_length(node->resultRelations)));
/*
diff --git a/src/backend/executor/functions.c b/src/backend/executor/functions.c
new file mode 100644
index f55424e..f99f15f
--- a/src/backend/executor/functions.c
+++ b/src/backend/executor/functions.c
@@ -1660,8 +1660,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
@@ -1679,7 +1679,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 2a5fec8..460127e
--- 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"
@@ -88,9 +87,6 @@ typedef struct ModifyTableContext
*/
TupleTableSlot *planSlot;
- /* MERGE specific */
- MergeActionState *relaction; /* MERGE action in progress */
-
/*
* Information about the changes that were made concurrently to a tuple
* being updated or deleted
@@ -153,13 +149,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);
/*
@@ -979,7 +976,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;
@@ -1828,7 +1825,7 @@ ExecCrossPartitionUpdate(ModifyTableCont
* additional rechecking, and might end up executing a different
* action entirely).
*/
- if (context->relaction != NULL)
+ if (mtstate->mt_merge_action != NULL)
return false;
else if (TupIsNull(epqslot))
return true;
@@ -2069,7 +2066,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;
/*
@@ -2712,6 +2709,7 @@ static TupleTableSlot *
ExecMerge(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
ItemPointer tupleid, bool canSetTag)
{
+ TupleTableSlot *rslot = NULL;
bool matched;
/*-----
@@ -2759,18 +2757,18 @@ 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()
- * returned "false", indicating the previously MATCHED tuple no longer
- * matches.
+ * Deal with the NOT MATCHED case (either a NOT MATCHED tuple from the
+ * join, or a previously MATCHED tuple for which ExecMergeMatched() set
+ * "matched" to false, indicating that it no longer matches).
*/
if (!matched)
- ExecMergeNotMatched(context, resultRelInfo, canSetTag);
+ rslot = ExecMergeNotMatched(context, resultRelInfo, canSetTag);
- /* No RETURNING support yet */
- return NULL;
+ return rslot;
}
/*
@@ -2780,8 +2778,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.
*
@@ -2790,15 +2788,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;
@@ -2810,7 +2809,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
@@ -2888,12 +2890,15 @@ lmerge_matched:
*/
newslot = ExecProject(relaction->mas_proj);
- context->relaction = relaction;
+ mtstate->mt_merge_action = relaction;
if (!ExecUpdatePrologue(context, resultRelInfo,
tupleid, NULL, newslot, &result))
{
if (result == TM_Ok)
- return true; /* "do nothing" */
+ {
+ *matched = true;
+ return NULL; /* "do nothing" */
+ }
break; /* concurrent update/delete */
}
result = ExecUpdateAct(context, resultRelInfo, tupleid, NULL,
@@ -2907,12 +2912,15 @@ lmerge_matched:
break;
case CMD_DELETE:
- context->relaction = relaction;
+ mtstate->mt_merge_action = relaction;
if (!ExecDeletePrologue(context, resultRelInfo, tupleid,
NULL, NULL, &result))
{
if (result == TM_Ok)
- return true; /* "do nothing" */
+ {
+ *matched = true;
+ return NULL; /* "do nothing" */
+ }
break; /* concurrent update/delete */
}
result = ExecDeleteAct(context, resultRelInfo, tupleid, false);
@@ -2968,7 +2976,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:
{
@@ -3014,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
@@ -3055,7 +3070,8 @@ lmerge_matched:
* tuple already deleted; tell caller to run NOT
* MATCHED actions
*/
- return false;
+ *matched = false;
+ return NULL;
case TM_SelfModified:
@@ -3075,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;
}
}
@@ -3093,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.
@@ -3103,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;
/*
@@ -3165,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:
@@ -3184,6 +3228,8 @@ ExecMergeNotMatched(ModifyTableContext *
*/
break;
}
+
+ return rslot;
}
/*
@@ -3361,6 +3407,64 @@ ExecInitMergeTupleSlots(ModifyTableState
}
/*
+ * 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_number() -
+ * SQL merge support function to retrieve the 1-based index of the
+ * currently executing merge WHEN clause.
+ */
+Datum
+pg_merge_when_clause_number(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->index);
+
+ PG_RETURN_NULL();
+}
+
+/*
* Process BEFORE EACH STATEMENT triggers
*/
static void
@@ -3646,8 +3750,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");
@@ -3724,8 +3837,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");
@@ -3817,7 +3939,7 @@ ExecModifyTable(PlanState *pstate)
}
slot = ExecGetUpdateNewTuple(resultRelInfo, context.planSlot,
oldSlot);
- context.relaction = NULL;
+ node->mt_merge_action = NULL;
/* Now apply the update. */
slot = ExecUpdate(&context, resultRelInfo, tupleid, oldtuple,
diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c
new file mode 100644
index 3397568..b9f443c
--- a/src/backend/executor/spi.c
+++ b/src/backend/executor/spi.c
@@ -2033,6 +2033,8 @@ SPI_result_code_string(int code)
return "SPI_OK_TD_REGISTER";
case SPI_OK_MERGE:
return "SPI_OK_MERGE";
+ case SPI_OK_MERGE_RETURNING:
+ return "SPI_OK_MERGE_RETURNING";
}
/* Unrecognized code ... return something useful ... */
sprintf(buf, "Unrecognized SPI code %d", code);
@@ -2886,7 +2888,10 @@ _SPI_pquery(QueryDesc *queryDesc, bool f
res = SPI_OK_UPDATE;
break;
case CMD_MERGE:
- res = SPI_OK_MERGE;
+ if (queryDesc->plannedstmt->hasReturning)
+ res = SPI_OK_MERGE_RETURNING;
+ else
+ res = SPI_OK_MERGE;
break;
default:
return SPI_ERROR_OPUNKNOWN;
diff --git a/src/backend/optimizer/plan/subselect.c b/src/backend/optimizer/plan/subselect.c
new file mode 100644
index 7a9fe88..7851761
--- a/src/backend/optimizer/plan/subselect.c
+++ b/src/backend/optimizer/plan/subselect.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "catalog/pg_operator.h"
+#include "catalog/pg_proc.h"
#include "catalog/pg_type.h"
#include "executor/executor.h"
#include "miscadmin.h"
@@ -35,6 +36,7 @@
#include "parser/parse_relation.h"
#include "rewrite/rewriteManip.h"
#include "utils/builtins.h"
+#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/syscache.h"
@@ -1846,7 +1848,8 @@ convert_EXISTS_to_ANY(PlannerInfo *root,
/*
* Replace correlation vars (uplevel vars) with Params.
*
- * Uplevel PlaceHolderVars and aggregates are replaced, too.
+ * Uplevel PlaceHolderVars, aggregates, GROUPING() expressions and merge
+ * support functions are replaced, too.
*
* Note: it is critical that this runs immediately after SS_process_sublinks.
* Since we do not recurse into the arguments of uplevel PHVs and aggregates,
@@ -1900,6 +1903,22 @@ replace_correlation_vars_mutator(Node *n
if (((GroupingFunc *) node)->agglevelsup > 0)
return (Node *) replace_outer_grouping(root, (GroupingFunc *) node);
}
+ if (IsA(node, FuncExpr))
+ {
+ if (IsMergeSupportFunction(((FuncExpr *) node)->funcid))
+ {
+ Param *param;
+
+ /*
+ * Replace with a Param, if it belongs to an upper-level MERGE
+ * query. Otherwise, leave it be.
+ */
+ param = replace_outer_merge_support_function(root,
+ (FuncExpr *) node);
+ if (param)
+ return (Node *) param;
+ }
+ }
return expression_tree_mutator(node,
replace_correlation_vars_mutator,
(void *) root);
diff --git a/src/backend/optimizer/util/paramassign.c b/src/backend/optimizer/util/paramassign.c
new file mode 100644
index d6a923b..2b7b8d1
--- a/src/backend/optimizer/util/paramassign.c
+++ b/src/backend/optimizer/util/paramassign.c
@@ -307,6 +307,63 @@ replace_outer_grouping(PlannerInfo *root
}
/*
+ * Generate a Param node to replace the given FuncExpr expression, which is
+ * expected to be a merge support function, if it belongs to an upper-level
+ * MERGE query. Otherwise NULL will be returned.
+ *
+ * A NULL return value indicates either that the merge support function
+ * belongs to a MERGE query at this query level, or that there is no MERGE
+ * query at all. In both cases, the caller is expected to leave the FuncExpr
+ * node unaltered, so that it is executed at this query level (in the latter
+ * case, it is up to the merge support function to decide how to handle being
+ * called outside of a MERGE query).
+ */
+Param *
+replace_outer_merge_support_function(PlannerInfo *root, FuncExpr *func)
+{
+ Param *retval;
+ PlannerParamItem *pitem;
+ Index levelsup;
+ Oid ptype = exprType((Node *) func);
+
+ /* Find the upper-level MERGE query the function belongs to */
+ levelsup = 0;
+ while (root->parse->commandType != CMD_MERGE)
+ {
+ root = root->parent_root;
+ if (root == NULL)
+ return NULL; /* Not in a MERGE query */
+ levelsup++;
+ }
+ if (levelsup == 0)
+ return NULL; /* Local MERGE query at this level */
+
+ /*
+ * It does not seem worthwhile to try to de-duplicate references to outer
+ * merge support functions. Just make a new slot every time.
+ */
+ func = copyObject(func);
+
+ pitem = makeNode(PlannerParamItem);
+ pitem->item = (Node *) func;
+ pitem->paramId = list_length(root->glob->paramExecTypes);
+ root->glob->paramExecTypes = lappend_oid(root->glob->paramExecTypes,
+ ptype);
+
+ root->plan_params = lappend(root->plan_params, pitem);
+
+ retval = makeNode(Param);
+ retval->paramkind = PARAM_EXEC;
+ retval->paramid = pitem->paramId;
+ retval->paramtype = ptype;
+ retval->paramtypmod = -1;
+ retval->paramcollid = InvalidOid;
+ retval->location = func->location;
+
+ return retval;
+}
+
+/*
* Generate a Param node to replace the given Var,
* which is expected to come from some upper NestLoop plan node.
* Record the need for the Var in root->curOuterParams.
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
new file mode 100644
index 4006632..6794511
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -74,7 +74,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,
@@ -515,7 +514,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;
@@ -942,7 +942,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;
@@ -2407,7 +2408,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
@@ -2501,10 +2503,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;
@@ -2521,7 +2524,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 60080e8..3dd1063
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -12255,6 +12255,7 @@ MergeStmt:
USING table_ref
ON a_expr
merge_when_list
+ returning_clause
{
MergeStmt *m = makeNode(MergeStmt);
@@ -12263,6 +12264,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 85cd47b..e72c37b
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -456,6 +456,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 c08c063..3228624
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -525,6 +525,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:
@@ -1751,6 +1752,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:
@@ -3099,6 +3101,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 b3f0b6a..cf7c0b9
--- 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,26 @@ 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)
+ {
+ ParseState *parent_pstate = pstate->parentParseState;
+
+ /* May be in a subquery in RETURNING list */
+ while (parent_pstate &&
+ parent_pstate->p_expr_kind != EXPR_KIND_MERGE_RETURNING)
+ parent_pstate = parent_pstate->parentParseState;
+
+ if (!parent_pstate ||
+ parent_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.
*/
@@ -2599,6 +2620,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 91b1156..33852e9
--- 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.
@@ -254,6 +258,7 @@ transformMergeStmt(ParseState *pstate, M
MergeAction *action;
action = makeNode(MergeAction);
+ action->index = foreach_current_index(l) + 1;
action->commandType = mergeWhenClause->commandType;
action->matched = mergeWhenClause->matched;
@@ -390,9 +395,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 864ea9b..cc19041
--- a/src/backend/parser/parse_relation.c
+++ b/src/backend/parser/parse_relation.c
@@ -2346,9 +2346,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 b486ab5..1bd0ce2
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -3629,9 +3629,9 @@ RewriteQuery(Query *parsetree, List *rew
ListCell *lc1;
/*
- * First, recursively process any insert/update/delete statements in WITH
- * clauses. (We have to do this first because the WITH clauses may get
- * copied into rule actions below.)
+ * First, recursively process any insert/update/delete/merge statements in
+ * WITH clauses. (We have to do this first because the WITH clauses may
+ * get copied into rule actions below.)
*/
foreach(lc1, parsetree->cteList)
{
@@ -3656,7 +3656,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 e3ccf6c..6f3dc89
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -2142,11 +2142,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/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
new file mode 100644
index fcb2f45..b47cfc7
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -7183,8 +7183,13 @@ get_merge_query_def(Query *query, depars
appendStringInfoString(buf, "DO NOTHING");
}
- /* No RETURNING support in MERGE yet */
- Assert(query->returningList == NIL);
+ /* Add RETURNING if present */
+ if (query->returningList)
+ {
+ appendContextKeyword(context, " RETURNING",
+ -PRETTYINDENT_STD, PRETTYINDENT_STD, 1);
+ get_target_list(query->returningList, context, NULL, colNamesVisible);
+ }
}
diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c
new file mode 100644
index 5973df2..a1380f8
--- a/src/bin/psql/common.c
+++ b/src/bin/psql/common.c
@@ -982,13 +982,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 6996073..2181eb8
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12043,4 +12043,14 @@
proname => 'any_value_transfn', prorettype => 'anyelement',
proargtypes => 'anyelement anyelement', prosrc => 'any_value_transfn' },
+# MERGE support functions
+{ oid => '9499', descr => 'command type of current MERGE action',
+ proname => 'pg_merge_action', provolatile => 'v', proparallel => 'r',
+ prorettype => 'text', proargtypes => '',
+ prosrc => 'pg_merge_action' },
+{ oid => '9500', descr => 'index of current MERGE WHEN clause',
+ proname => 'pg_merge_when_clause_number', provolatile => 'v',
+ proparallel => 'r', prorettype => 'int4', proargtypes => '',
+ prosrc => 'pg_merge_when_clause_number' },
+
]
diff --git a/src/include/catalog/pg_proc.h b/src/include/catalog/pg_proc.h
new file mode 100644
index e7abe0b..eb6cad9
--- 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(funcid) \
+ ((funcid) == F_PG_MERGE_ACTION || \
+ (funcid) == F_PG_MERGE_WHEN_CLAUSE_NUMBER)
+
#endif /* EXPOSE_TO_CLIENT_CODE */
diff --git a/src/include/executor/spi.h b/src/include/executor/spi.h
new file mode 100644
index d1de139..ed8787d
--- a/src/include/executor/spi.h
+++ b/src/include/executor/spi.h
@@ -97,6 +97,7 @@ typedef struct _SPI_plan *SPIPlanPtr;
#define SPI_OK_REL_UNREGISTER 16
#define SPI_OK_TD_REGISTER 17
#define SPI_OK_MERGE 18
+#define SPI_OK_MERGE_RETURNING 19
#define SPI_OPT_NONATOMIC (1 << 0)
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
new file mode 100644
index cb714f4..6708b6b
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1319,6 +1319,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 efb5c3e..1d5d631
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -1688,6 +1688,7 @@ typedef struct MergeWhenClause
typedef struct MergeAction
{
NodeTag type;
+ int index; /* 1-based index of the clause */
bool matched; /* true=MATCHED, false=NOT MATCHED */
CmdType commandType; /* INSERT/UPDATE/DELETE/DO NOTHING */
/* OVERRIDING clause */
@@ -1914,6 +1915,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/optimizer/paramassign.h b/src/include/optimizer/paramassign.h
new file mode 100644
index 55c27a6..6f70c78
--- a/src/include/optimizer/paramassign.h
+++ b/src/include/optimizer/paramassign.h
@@ -20,6 +20,8 @@ extern Param *replace_outer_placeholderv
PlaceHolderVar *phv);
extern Param *replace_outer_agg(PlannerInfo *root, Aggref *agg);
extern Param *replace_outer_grouping(PlannerInfo *root, GroupingFunc *grp);
+extern Param *replace_outer_merge_support_function(PlannerInfo *root,
+ FuncExpr *func);
extern Param *replace_nestloop_param_var(PlannerInfo *root, Var *var);
extern Param *replace_nestloop_param_placeholdervar(PlannerInfo *root,
PlaceHolderVar *phv);
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 f589112..836819d
--- 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/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
new file mode 100644
index 4b76f76..fb6289c
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -4267,9 +4267,9 @@ exec_stmt_execsql(PLpgSQL_execstate *est
/*
* If we have INTO, then we only need one row back ... but if we have INTO
* STRICT or extra check too_many_rows, ask for two rows, so that we can
- * verify the statement returns only one. INSERT/UPDATE/DELETE are always
- * treated strictly. Without INTO, just run the statement to completion
- * (tcount = 0).
+ * verify the statement returns only one. INSERT/UPDATE/DELETE/MERGE are
+ * always treated strictly. Without INTO, just run the statement to
+ * completion (tcount = 0).
*
* We could just ask for two rows always when using INTO, but there are
* some cases where demanding the extra row costs significant time, eg by
@@ -4307,10 +4307,11 @@ exec_stmt_execsql(PLpgSQL_execstate *est
case SPI_OK_INSERT:
case SPI_OK_UPDATE:
case SPI_OK_DELETE:
+ case SPI_OK_MERGE:
case SPI_OK_INSERT_RETURNING:
case SPI_OK_UPDATE_RETURNING:
case SPI_OK_DELETE_RETURNING:
- case SPI_OK_MERGE:
+ case SPI_OK_MERGE_RETURNING:
Assert(stmt->mod_stmt);
exec_set_found(estate, (SPI_processed != 0));
break;
@@ -4489,10 +4490,11 @@ exec_stmt_dynexecute(PLpgSQL_execstate *
case SPI_OK_INSERT:
case SPI_OK_UPDATE:
case SPI_OK_DELETE:
+ case SPI_OK_MERGE:
case SPI_OK_INSERT_RETURNING:
case SPI_OK_UPDATE_RETURNING:
case SPI_OK_DELETE_RETURNING:
- case SPI_OK_MERGE:
+ case SPI_OK_MERGE_RETURNING:
case SPI_OK_UTILITY:
case SPI_OK_REWRITTEN:
break;
diff --git a/src/pl/tcl/pltcl.c b/src/pl/tcl/pltcl.c
new file mode 100644
index e8f9d7b..0c9f81e
--- a/src/pl/tcl/pltcl.c
+++ b/src/pl/tcl/pltcl.c
@@ -2456,6 +2456,7 @@ pltcl_process_SPI_result(Tcl_Interp *int
case SPI_OK_INSERT_RETURNING:
case SPI_OK_DELETE_RETURNING:
case SPI_OK_UPDATE_RETURNING:
+ case SPI_OK_MERGE_RETURNING:
/*
* Process the tuples we got
diff --git a/src/test/regress/expected/merge.out b/src/test/regress/expected/merge.out
new file mode 100644
index 133d421..5be3d2d
--- 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;
@@ -1256,7 +1256,7 @@ BEGIN;
MERGE INTO sq_target
USING v
ON tid = sid
-WHEN MATCHED AND tid > 2 THEN
+WHEN MATCHED AND tid >= 2 THEN
UPDATE SET balance = balance + delta
WHEN NOT MATCHED THEN
INSERT (balance, tid) VALUES (balance + delta, sid)
@@ -1271,7 +1271,7 @@ INSERT INTO sq_source (sid, balance, del
MERGE INTO sq_target t
USING v
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)
@@ -1295,7 +1295,7 @@ WITH targq AS (
MERGE INTO sq_target t
USING v
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)
@@ -1304,20 +1304,193 @@ WHEN MATCHED AND tid < 2 THEN
ROLLBACK;
-- RETURNING
BEGIN;
-INSERT INTO sq_source (sid, balance, delta) VALUES (-1, -1, -10);
+CREATE TABLE merge_actions(action text, abbrev text);
+INSERT INTO merge_actions VALUES ('INSERT', 'ins'), ('UPDATE', 'upd'), ('DELETE', 'del');
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)
+ 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 *;
- ^
+ DELETE
+RETURNING pg_merge_when_clause_number() AS clause_number,
+ (SELECT abbrev FROM merge_actions
+ WHERE action = 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;
+ clause_number | merge_action | tid | balance | description
+---------------+--------------+-----+---------+---------------------
+ 3 | del | 1 | 100 | Removed (1,100)
+ 1 | upd | 2 | 220 | Added 20 to balance
+ 2 | ins | 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_number();
+ERROR: merge support function pg_merge_when_clause_number 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...
+ ^
+-- RETURNING in CTEs
+CREATE TABLE sq_target_merge_log (tid integer NOT NULL, last_change text);
+INSERT INTO sq_target_merge_log VALUES (1, 'Original value');
+BEGIN;
+WITH m AS (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 pg_merge_when_clause_number() AS clause_number,
+ 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
+), m2 AS (
+ MERGE INTO sq_target_merge_log l
+ USING m
+ ON l.tid = m.tid
+ WHEN MATCHED THEN
+ UPDATE SET last_change = description
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (m.tid, description)
+ RETURNING merge_action, pg_merge_action() AS merge_log_action, l.*
+)
+SELECT * FROM m2;
+ merge_action | merge_log_action | tid | last_change
+--------------+------------------+-----+---------------------
+ DELETE | UPDATE | 1 | Removed (1,100)
+ UPDATE | INSERT | 2 | Added 20 to balance
+ INSERT | INSERT | 4 | Inserted (4,40)
+(3 rows)
+
+SELECT * FROM sq_target_merge_log ORDER BY tid;
+ tid | last_change
+-----+---------------------
+ 1 | Removed (1,100)
+ 2 | Added 20 to balance
+ 4 | Inserted (4,40)
+(3 rows)
+
+ROLLBACK;
+-- COPY (MERGE ... RETURNING) TO ...
+BEGIN;
+COPY (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 pg_merge_action(), t.*
+) TO stdout;
+DELETE 1 100
+UPDATE 2 220
+INSERT 4 40
+ROLLBACK;
+-- SQL function with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT action text, OUT tid int, OUT new_balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING pg_merge_action(), t.*;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT action, tid, new_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ action | tid | new_balance
+--------+-----+-------------
+ DELETE | 1 | 100
+ UPDATE | 3 | 320
+ INSERT | 4 | 110
+(3 rows)
+
+ROLLBACK;
+-- SQL SRF with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_sq_source_into_sq_target()
+RETURNS TABLE (action text, tid int, balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 pg_merge_action(), t.*;
+$$;
+SELECT * FROM merge_sq_source_into_sq_target();
+ action | tid | balance
+--------+-----+---------
+ DELETE | 1 | 100
+ UPDATE | 2 | 220
+ INSERT | 4 | 40
+(3 rows)
+
+ROLLBACK;
+-- PL/pgSQL function with MERGE ... RETURNING ... INTO
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT r_action text, OUT r_tid int, OUT r_balance int)
+LANGUAGE plpgsql AS
+$$
+BEGIN
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING pg_merge_action(), t.* INTO r_action, r_tid, r_balance;
+END;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT r_action, r_tid, r_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ r_action | r_tid | r_balance
+----------+-------+-----------
+ DELETE | 1 | 100
+ UPDATE | 3 | 320
+ INSERT | 4 | 110
+(3 rows)
+
ROLLBACK;
-- EXPLAIN
CREATE TABLE ex_mtarget (a int, b int)
@@ -1514,7 +1687,7 @@ SELECT * FROM sq_target WHERE tid = 1;
(1 row)
ROLLBACK;
-DROP TABLE sq_target, sq_source CASCADE;
+DROP TABLE sq_target, sq_target_merge_log, sq_source CASCADE;
NOTICE: drop cascades to view v
CREATE TABLE pa_target (tid integer, balance float, val text)
PARTITION BY LIST (tid);
@@ -1640,6 +1813,32 @@ SELECT * FROM pa_target ORDER BY tid;
(14 rows)
ROLLBACK;
+-- update partition key to partition not initially scanned
+BEGIN;
+MERGE INTO pa_target t
+ USING pa_source s
+ ON t.tid = s.sid AND t.tid = 1
+ WHEN MATCHED THEN
+ UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
+ RETURNING pg_merge_action(), pg_merge_when_clause_number(), t.*;
+ pg_merge_action | pg_merge_when_clause_number | tid | balance | val
+-----------------+-----------------------------+-----+---------+--------------------------
+ UPDATE | 1 | 2 | 110 | initial updated by merge
+(1 row)
+
+SELECT * FROM pa_target ORDER BY tid;
+ tid | balance | val
+-----+---------+--------------------------
+ 2 | 110 | initial updated by merge
+ 3 | 300 | initial
+ 5 | 500 | initial
+ 7 | 700 | initial
+ 9 | 900 | initial
+ 11 | 1100 | initial
+ 13 | 1300 | initial
+(7 rows)
+
+ROLLBACK;
DROP TABLE pa_target CASCADE;
-- The target table is partitioned in the same way, but this time by attaching
-- partitions which have columns in different order, dropped columns etc.
@@ -1731,7 +1930,26 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (sid, delta, 'inserted by merge');
+ INSERT VALUES (sid, delta, 'inserted by merge')
+ RETURNING pg_merge_action(), pg_merge_when_clause_number(), t.*;
+ pg_merge_action | pg_merge_when_clause_number | tid | balance | val
+-----------------+-----------------------------+-----+---------+--------------------------
+ UPDATE | 1 | 2 | 110 | initial updated by merge
+ INSERT | 2 | 2 | 20 | inserted by merge
+ UPDATE | 1 | 4 | 330 | initial updated by merge
+ INSERT | 2 | 4 | 40 | inserted by merge
+ UPDATE | 1 | 6 | 550 | initial updated by merge
+ INSERT | 2 | 6 | 60 | inserted by merge
+ UPDATE | 1 | 8 | 770 | initial updated by merge
+ INSERT | 2 | 8 | 80 | inserted by merge
+ UPDATE | 1 | 10 | 990 | initial updated by merge
+ INSERT | 2 | 10 | 100 | inserted by merge
+ UPDATE | 1 | 12 | 1210 | initial updated by merge
+ INSERT | 2 | 12 | 120 | inserted by merge
+ UPDATE | 1 | 14 | 1430 | initial updated by merge
+ INSERT | 2 | 14 | 140 | inserted by merge
+(14 rows)
+
SELECT * FROM pa_target ORDER BY tid;
tid | balance | val
-----+---------+--------------------------
@@ -1798,7 +2016,21 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge');
+ INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge')
+ RETURNING pg_merge_action(), pg_merge_when_clause_number(), t.*;
+ pg_merge_action | pg_merge_when_clause_number | logts | tid | balance | val
+-----------------+-----------------------------+--------------------------+-----+---------+--------------------------
+ UPDATE | 1 | Tue Jan 31 00:00:00 2017 | 1 | 110 | initial updated by merge
+ UPDATE | 1 | Tue Feb 28 00:00:00 2017 | 2 | 220 | initial updated by merge
+ INSERT | 2 | Sun Jan 15 00:00:00 2017 | 3 | 30 | inserted by merge
+ UPDATE | 1 | Tue Jan 31 00:00:00 2017 | 4 | 440 | initial updated by merge
+ UPDATE | 1 | Tue Feb 28 00:00:00 2017 | 5 | 550 | initial updated by merge
+ INSERT | 2 | Sun Jan 15 00:00:00 2017 | 6 | 60 | inserted by merge
+ UPDATE | 1 | Tue Jan 31 00:00:00 2017 | 7 | 770 | initial updated by merge
+ UPDATE | 1 | Tue Feb 28 00:00:00 2017 | 8 | 880 | initial updated by merge
+ INSERT | 2 | Sun Jan 15 00:00:00 2017 | 9 | 90 | inserted by merge
+(9 rows)
+
SELECT * FROM pa_target ORDER BY tid;
logts | tid | balance | val
--------------------------+-----+---------+--------------------------
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
new file mode 100644
index e07afcd..219f0ad
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -3592,7 +3592,8 @@ MERGE INTO rule_merge2 t USING (SELECT 1
-- test deparsing
CREATE TABLE sf_target(id int, data text, filling int[]);
CREATE FUNCTION merge_sf_test()
- RETURNS void
+ RETURNS TABLE(pg_merge_action text, when_clause int,
+ a int, b text, id int, data text, filling int[])
LANGUAGE sql
BEGIN ATOMIC
MERGE INTO sf_target t
@@ -3629,11 +3630,13 @@ WHEN NOT MATCHED
VALUES (s.a, s.b, DEFAULT)
WHEN NOT MATCHED
THEN INSERT (filling[1], id)
- VALUES (s.a, s.a);
+ VALUES (s.a, s.a)
+RETURNING
+ pg_merge_action(), pg_merge_when_clause_number() AS clause_number, *;
END;
\sf merge_sf_test
CREATE OR REPLACE FUNCTION public.merge_sf_test()
- RETURNS void
+ RETURNS TABLE(pg_merge_action text, when_clause integer, a integer, b text, id integer, data text, filling integer[])
LANGUAGE sql
BEGIN ATOMIC
MERGE INTO sf_target t
@@ -3670,7 +3673,14 @@ BEGIN ATOMIC
VALUES (s.a, s.b, DEFAULT)
WHEN NOT MATCHED
THEN INSERT (filling[1], id)
- VALUES (s.a, s.a);
+ VALUES (s.a, s.a)
+ RETURNING pg_merge_action() AS pg_merge_action,
+ pg_merge_when_clause_number() AS clause_number,
+ s.a,
+ s.b,
+ t.id,
+ t.data,
+ t.filling;
END
DROP FUNCTION merge_sf_test;
DROP TABLE sf_target;
diff --git a/src/test/regress/sql/merge.sql b/src/test/regress/sql/merge.sql
new file mode 100644
index 4cf6db9..8394d01
--- 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
@@ -816,7 +816,7 @@ BEGIN;
MERGE INTO sq_target
USING v
ON tid = sid
-WHEN MATCHED AND tid > 2 THEN
+WHEN MATCHED AND tid >= 2 THEN
UPDATE SET balance = balance + delta
WHEN NOT MATCHED THEN
INSERT (balance, tid) VALUES (balance + delta, sid)
@@ -829,7 +829,7 @@ INSERT INTO sq_source (sid, balance, del
MERGE INTO sq_target t
USING v
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)
@@ -847,7 +847,7 @@ WITH targq AS (
MERGE INTO sq_target t
USING v
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)
@@ -857,17 +857,148 @@ ROLLBACK;
-- RETURNING
BEGIN;
-INSERT INTO sq_source (sid, balance, delta) VALUES (-1, -1, -10);
+CREATE TABLE merge_actions(action text, abbrev text);
+INSERT INTO merge_actions VALUES ('INSERT', 'ins'), ('UPDATE', 'upd'), ('DELETE', 'del');
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)
+ INSERT (balance, tid) VALUES (balance + delta, sid)
WHEN MATCHED AND tid < 2 THEN
- DELETE
-RETURNING *;
+ DELETE
+RETURNING pg_merge_when_clause_number() AS clause_number,
+ (SELECT abbrev FROM merge_actions
+ WHERE action = 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_number();
+
+-- RETURNING in CTEs
+CREATE TABLE sq_target_merge_log (tid integer NOT NULL, last_change text);
+INSERT INTO sq_target_merge_log VALUES (1, 'Original value');
+BEGIN;
+WITH m AS (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 pg_merge_when_clause_number() AS clause_number,
+ 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
+), m2 AS (
+ MERGE INTO sq_target_merge_log l
+ USING m
+ ON l.tid = m.tid
+ WHEN MATCHED THEN
+ UPDATE SET last_change = description
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (m.tid, description)
+ RETURNING merge_action, pg_merge_action() AS merge_log_action, l.*
+)
+SELECT * FROM m2;
+SELECT * FROM sq_target_merge_log ORDER BY tid;
+ROLLBACK;
+
+-- COPY (MERGE ... RETURNING) TO ...
+BEGIN;
+COPY (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 pg_merge_action(), t.*
+) TO stdout;
+ROLLBACK;
+
+-- SQL function with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT action text, OUT tid int, OUT new_balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING pg_merge_action(), t.*;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT action, tid, new_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ROLLBACK;
+
+-- SQL SRF with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_sq_source_into_sq_target()
+RETURNS TABLE (action text, tid int, balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 pg_merge_action(), t.*;
+$$;
+SELECT * FROM merge_sq_source_into_sq_target();
+ROLLBACK;
+
+-- PL/pgSQL function with MERGE ... RETURNING ... INTO
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT r_action text, OUT r_tid int, OUT r_balance int)
+LANGUAGE plpgsql AS
+$$
+BEGIN
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING pg_merge_action(), t.* INTO r_action, r_tid, r_balance;
+END;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT r_action, r_tid, r_balance FROM merge_into_sq_target(sid, balance, delta)) m;
ROLLBACK;
-- EXPLAIN
@@ -966,7 +1097,7 @@ WHEN MATCHED THEN
SELECT * FROM sq_target WHERE tid = 1;
ROLLBACK;
-DROP TABLE sq_target, sq_source CASCADE;
+DROP TABLE sq_target, sq_target_merge_log, sq_source CASCADE;
CREATE TABLE pa_target (tid integer, balance float, val text)
PARTITION BY LIST (tid);
@@ -1033,6 +1164,17 @@ SELECT merge_func();
SELECT * FROM pa_target ORDER BY tid;
ROLLBACK;
+-- update partition key to partition not initially scanned
+BEGIN;
+MERGE INTO pa_target t
+ USING pa_source s
+ ON t.tid = s.sid AND t.tid = 1
+ WHEN MATCHED THEN
+ UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
+ RETURNING pg_merge_action(), pg_merge_when_clause_number(), t.*;
+SELECT * FROM pa_target ORDER BY tid;
+ROLLBACK;
+
DROP TABLE pa_target CASCADE;
-- The target table is partitioned in the same way, but this time by attaching
@@ -1091,7 +1233,8 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (sid, delta, 'inserted by merge');
+ INSERT VALUES (sid, delta, 'inserted by merge')
+ RETURNING pg_merge_action(), pg_merge_when_clause_number(), t.*;
SELECT * FROM pa_target ORDER BY tid;
ROLLBACK;
@@ -1145,7 +1288,8 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge');
+ INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge')
+ RETURNING pg_merge_action(), pg_merge_when_clause_number(), t.*;
SELECT * FROM pa_target ORDER BY tid;
ROLLBACK;
diff --git a/src/test/regress/sql/rules.sql b/src/test/regress/sql/rules.sql
new file mode 100644
index 8b7e255..5dbeb6b
--- a/src/test/regress/sql/rules.sql
+++ b/src/test/regress/sql/rules.sql
@@ -1281,7 +1281,8 @@ MERGE INTO rule_merge2 t USING (SELECT 1
CREATE TABLE sf_target(id int, data text, filling int[]);
CREATE FUNCTION merge_sf_test()
- RETURNS void
+ RETURNS TABLE(pg_merge_action text, when_clause int,
+ a int, b text, id int, data text, filling int[])
LANGUAGE sql
BEGIN ATOMIC
MERGE INTO sf_target t
@@ -1318,7 +1319,9 @@ WHEN NOT MATCHED
VALUES (s.a, s.b, DEFAULT)
WHEN NOT MATCHED
THEN INSERT (filling[1], id)
- VALUES (s.a, s.a);
+ VALUES (s.a, s.a)
+RETURNING
+ pg_merge_action(), pg_merge_when_clause_number() AS clause_number, *;
END;
\sf merge_sf_test
On Fri, Jul 21, 2023 at 7:17 PM Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
On Mon, 17 Jul 2023 at 20:43, Jeff Davis <pgsql@j-davis.com> wrote:
Maybe instead of a function it could be a special table reference
like:MERGE ... RETURNING MERGE.action, MERGE.action_number, id, val?
The benefits are:
1. It is naturally constrained to the right context. It doesn't require
global variables and the PG_TRY/PG_FINALLY, and can't be called in the
wrong contexts (like SELECT).2. More likely to be consistent with eventual support for NEW/OLD
(actually BEFORE/AFTER for reasons the prior thread discussed).Thinking about this some more, I think that the point about
constraining these functions to the right context is a reasonable one,
and earlier versions of this patch did that better, without needing
global variables or a PG_TRY/PG_FINALLY block.Here is an updated patch that goes back to doing it that way. This is
more like the way that aggregate functions and GROUPING() work, in
that the parser constrains the location from which the functions can
be used, and at execution time, the functions rely on the relevant
context being passed via the FunctionCallInfo context.It's still possible to use these functions in subqueries in the
RETURNING list, but attempting to use them anywhere else (like a
SELECT on its own) will raise an error at parse time. If they do
somehow get invoked in a non-MERGE context, they will elog an error
(again, just like aggregate functions), because that's a "shouldn't
happen" error.This does nothing to be consistent with eventual support for
BEFORE/AFTER, but I think that's really an entirely separate thing,
+1
From a user perspective, writing something like "BEFORE.id" is quite
natural, because it's clear that "id" is a column, and "BEFORE" is the
old state of the table. Writing something like "MERGE.action" seems a
lot more counter-intuitive, because "action" isn't a column of
anything (and if it was, I think this syntax would potentially cause
even more confusion).So really, I think "MERGE.action" is an abuse of the syntax,
inconsistent with any other SQL syntax, and using functions is much
more natural, akin to GROUPING(), for example.
There seems to be other use cases which need us to invent a method to
expose a command-specific alias. See Tatsuo Ishii's call for help in
his patch for Row Pattern Recognition [1]Row pattern recognition /messages/by-id/20230625.210509.1276733411677577841.t-ishii@sranhm.sra.co.jp.
<quote>
I was not able to find a way to implement expressions like START.price
(START is not a table alias). Any suggestion is greatly appreciated.
</quote>
It looks like the SQL standard has started using more of such
context-specific keywords, and I'd expect such keywords to only
increase in future, as the SQL committee tries to introduce more
features into the standard.
So if MERGE.action is not to your taste, perhaps you/someone can
suggest an alternative that doesn't cause a confusion, and yet
implementing it would open up the way for more such context-specific
keywords.
I performed the review vo v9 patch by comparing it to v8 and v7
patches, and then comparing to HEAD.
The v9 patch is more or less a complete revert to v7 patch, plus the
planner support for calling the merge-support functions in subqueries,
parser catching use of merge-support functions outside MERGE command,
and name change for one of the support functions.
But reverting to v7 also means that some of my gripes with that
version also return; e.g. invention of EXPR_KIND_MERGE_RETURNING. And
as noted in v7 review, I don't have a better proposal.
Function name changed from pg_merge_when_clause() to
pg_merge_when_clause_number(). That's better, even though it's a bit
of mouthful.
Doc changes (compared to v7) look good.
The changes made to tests (compared to v7) are for the better.
- * Uplevel PlaceHolderVars and aggregates are replaced, too.
+ * Uplevel PlaceHolderVars, aggregates, GROUPING() expressions and merge
+ * support functions are replaced, too.
Needs Oxford comma: s/GROUPING() expressions and/GROUPING() expressions, and/
+pg_merge_action(PG_FUNCTION_ARGS)
+{
...
+ relaction = mtstate->mt_merge_action;
+ if (relaction)
+ {
..
+ }
+
+ PG_RETURN_NULL();
+}
Under what circumstances would the relaction be null? Is it okay to
return NULL from this function if relaction is null, or is it better
to throw an error? These questions apply to the
pg_merge_when_clause_number() function as well.
[1]: Row pattern recognition /messages/by-id/20230625.210509.1276733411677577841.t-ishii@sranhm.sra.co.jp
/messages/by-id/20230625.210509.1276733411677577841.t-ishii@sranhm.sra.co.jp
Best regards,
Gurjeet
http://Gurje.et
On Tue, 25 Jul 2023 at 21:46, Gurjeet Singh <gurjeet@singh.im> wrote:
There seems to be other use cases which need us to invent a method to
expose a command-specific alias. See Tatsuo Ishii's call for help in
his patch for Row Pattern Recognition [1].
I think that's different though, because in that example "START" is a
row from the table, and "price" is a table column, so using the table
alias syntax "START.price" makes sense, to refer to a value from the
table.
In this case "MERGE" and "action" have nothing to do with table rows
or columns, so saying "MERGE.action" doesn't fit the pattern.
I performed the review vo v9 patch by comparing it to v8 and v7
patches, and then comparing to HEAD.
Many thanks for looking at this.
The v9 patch is more or less a complete revert to v7 patch, plus the
planner support for calling the merge-support functions in subqueries,
parser catching use of merge-support functions outside MERGE command,
and name change for one of the support functions.
Yes, that's a fair summary.
But reverting to v7 also means that some of my gripes with that
version also return; e.g. invention of EXPR_KIND_MERGE_RETURNING. And
as noted in v7 review, I don't have a better proposal.
True, but I think that it's in keeping with the purpose of the
ParseExprKind enumeration:
/*
* Expression kinds distinguished by transformExpr(). Many of these are not
* semantically distinct so far as expression transformation goes; rather,
* we distinguish them so that context-specific error messages can be printed.
*/
which matches what the patch is using EXPR_KIND_MERGE_RETURNING for.
- * Uplevel PlaceHolderVars and aggregates are replaced, too. + * Uplevel PlaceHolderVars, aggregates, GROUPING() expressions and merge + * support functions are replaced, too.Needs Oxford comma: s/GROUPING() expressions and/GROUPING() expressions, and/
Added.
+pg_merge_action(PG_FUNCTION_ARGS) +{ ... + relaction = mtstate->mt_merge_action; + if (relaction) + { .. + } + + PG_RETURN_NULL(); +}Under what circumstances would the relaction be null? Is it okay to
return NULL from this function if relaction is null, or is it better
to throw an error? These questions apply to the
pg_merge_when_clause_number() function as well.
Yes, it's really a "should never happen" situation, so I've converted
it to elog() an error. Similarly, commandType should never be
CMD_NOTHING in pg_merge_action(), so that also now throws an error.
Also, the planner code now throws an error if it sees a merge support
function outside a MERGE. Again, that should never happen, due to the
parser check, but it seems better to be sure, and catch it early.
While at it, I tidied up the planner code a bit, making the merge
support function handling more like the other cases in
replace_correlation_vars_mutator(), and making
replace_outer_merge_support_function() more like its neighbouring
functions, such as replace_outer_grouping(). In particular, it is now
only called if it is a reference to an upper-level MERGE, not for
local references, which matches the pattern used in the neighbouring
functions.
Finally, I have added some new RLS code and tests, to apply SELECT
policies to new rows inserted by MERGE INSERT actions, if a RETURNING
clause is specified, to make it consistent with a plain INSERT ...
RETURNING command (see commit c2e08b04c9).
Regards,
Dean
Attachments:
support-merge-returning-v10.patchtext/x-patch; charset=US-ASCII; name=support-merge-returning-v10.patchDownload
diff --git a/doc/src/sgml/dml.sgml b/doc/src/sgml/dml.sgml
new file mode 100644
index cbbc5e2..7f65f6e
--- a/doc/src/sgml/dml.sgml
+++ b/doc/src/sgml/dml.sgml
@@ -283,10 +283,15 @@ DELETE FROM products;
<secondary>RETURNING</secondary>
</indexterm>
+ <indexterm zone="dml-returning">
+ <primary>MERGE</primary>
+ <secondary>RETURNING</secondary>
+ </indexterm>
+
<para>
Sometimes it is useful to obtain data from modified rows while they are
being manipulated. The <command>INSERT</command>, <command>UPDATE</command>,
- and <command>DELETE</command> commands all have an
+ <command>DELETE</command>, and <command>MERGE</command> commands all have an
optional <literal>RETURNING</literal> clause that supports this. Use
of <literal>RETURNING</literal> avoids performing an extra database query to
collect the data, and is especially valuable when it would otherwise be
@@ -339,6 +344,24 @@ DELETE FROM products
</programlisting>
</para>
+ <para>
+ In a <command>MERGE</command>, the data available to <literal>RETURNING</literal> is
+ the content of the source row plus the content of the inserted, updated, or
+ deleted target row. In addition, the merge support functions
+ <xref linkend="pg-merge-action"/> and <xref linkend="pg-merge-when-clause-number"/>
+ may be used to return information about which merge action was executed for
+ each row. Since it is quite common for the source and target to have many
+ of the same columns, specifying <literal>RETURNING *</literal> can lead to a
+ lot of duplicated columns, so it is often more useful to just return the
+ target row. For example:
+<programlisting>
+MERGE INTO products p USING new_products n ON p.product_no = n.product_no
+ WHEN NOT MATCHED THEN INSERT VALUES (n.product_no, n.name, n.price)
+ WHEN MATCHED THEN UPDATE SET name = n.name, price = n.price
+ RETURNING pg_merge_action(), p.*;
+</programlisting>
+ </para>
+
<para>
If there are triggers (<xref linkend="triggers"/>) on the target table,
the data available to <literal>RETURNING</literal> is the row as modified by
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
new file mode 100644
index be2f54c..e79e2ad
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -21772,6 +21772,100 @@ SELECT count(*) FROM sometable;
</sect1>
+ <sect1 id="functions-merge-support">
+ <title>Merge Support Functions</title>
+
+ <indexterm>
+ <primary>MERGE</primary>
+ <secondary>RETURNING</secondary>
+ </indexterm>
+
+ <para>
+ The merge support functions shown in
+ <xref linkend="functions-merge-support-table"/> may be used in the
+ <literal>RETURNING</literal> list of a <xref linkend="sql-merge"/> command
+ to return additional information about the action taken for each row.
+ </para>
+
+ <table id="functions-merge-support-table">
+ <title>Merge Support Functions</title>
+
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ Function
+ </para>
+ <para>
+ Description
+ </para></entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm id="pg-merge-action">
+ <primary>pg_merge_action</primary>
+ </indexterm>
+ <function>pg_merge_action</function> ( )
+ <returnvalue>text</returnvalue>
+ </para>
+ <para>
+ Returns the action performed on the current row (<literal>'INSERT'</literal>,
+ <literal>'UPDATE'</literal>, or <literal>'DELETE'</literal>).
+ </para></entry>
+ </row>
+
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm id="pg-merge-when-clause-number">
+ <primary>pg_merge_when_clause_number</primary>
+ </indexterm>
+ <function>pg_merge_when_clause_number</function> ( )
+ <returnvalue>integer</returnvalue>
+ </para>
+ <para>
+ Returns the 1-based index of the <literal>WHEN</literal> clause executed
+ for the current row.
+ </para></entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+
+ <para>
+ Example:
+<screen><![CDATA[
+MERGE INTO products p
+ USING stock s ON p.product_id = s.product_id
+ WHEN MATCHED AND s.quantity > 0 THEN
+ UPDATE SET in_stock = true, quantity = s.quantity
+ WHEN MATCHED THEN
+ UPDATE SET in_stock = false, quantity = 0
+ WHEN NOT MATCHED THEN
+ INSERT (product_id, in_stock, quantity)
+ VALUES (s.product_id, true, s.quantity)
+ RETURNING pg_merge_action() AS action,
+ pg_merge_when_clause_number() AS clause_number,
+ p.*;
+
+ action | clause_number | product_id | in_stock | quantity
+--------+---------------+------------+----------+----------
+ UPDATE | 1 | 1001 | t | 50
+ UPDATE | 2 | 1002 | f | 0
+ INSERT | 3 | 1003 | t | 10
+]]></screen>
+ </para>
+
+ <para>
+ Note that these functions can only be used in the <literal>RETURNING</literal>
+ list of a <command>MERGE</command> command. It is an error to use them in
+ any other part of a query.
+ </para>
+
+ </sect1>
+
<sect1 id="functions-subquery">
<title>Subquery Expressions</title>
diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml
new file mode 100644
index fe8def4..51a15ca
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -1387,9 +1387,9 @@
to a <glossterm linkend="glossary-client">client</glossterm> upon the
completion of an <acronym>SQL</acronym> command, usually a
<command>SELECT</command> but it can be an
- <command>INSERT</command>, <command>UPDATE</command>, or
- <command>DELETE</command> command if the <literal>RETURNING</literal>
- clause is specified.
+ <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command> command if the
+ <literal>RETURNING</literal> clause is specified.
</para>
<para>
The fact that a result set is a relation means that a query can be used
diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml
new file mode 100644
index f55e901..6803240
--- a/doc/src/sgml/plpgsql.sgml
+++ b/doc/src/sgml/plpgsql.sgml
@@ -1020,8 +1020,8 @@ INSERT INTO mytable VALUES (1,'one'), (2
</para>
<para>
- If the command does return rows (for example <command>SELECT</command>,
- or <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>
+ If the command does return rows (for example <command>SELECT</command>, or
+ <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command>
with <literal>RETURNING</literal>), there are two ways to proceed.
When the command will return at most one row, or you only care about
the first row of output, write the command as usual but add
@@ -1149,6 +1149,7 @@ SELECT <replaceable>select_expressions</
INSERT ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
UPDATE ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
DELETE ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
+MERGE ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
</synopsis>
where <replaceable>target</replaceable> can be a record variable, a row
@@ -1159,8 +1160,8 @@ DELETE ... RETURNING <replaceable>expres
<literal>INTO</literal> clause) just as described above,
and the plan is cached in the same way.
This works for <command>SELECT</command>,
- <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command> with
- <literal>RETURNING</literal>, and certain utility commands
+ <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command>
+ with <literal>RETURNING</literal>, and certain utility commands
that return row sets, such as <command>EXPLAIN</command>.
Except for the <literal>INTO</literal> clause, the SQL command is the same
as it would be written outside <application>PL/pgSQL</application>.
@@ -1236,7 +1237,7 @@ END;
</para>
<para>
- For <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command> with
+ For <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command> with
<literal>RETURNING</literal>, <application>PL/pgSQL</application> reports
an error for more than one returned row, even when
<literal>STRICT</literal> is not specified. This is because there
diff --git a/doc/src/sgml/queries.sgml b/doc/src/sgml/queries.sgml
new file mode 100644
index 3f95849..9928373
--- a/doc/src/sgml/queries.sgml
+++ b/doc/src/sgml/queries.sgml
@@ -2062,9 +2062,10 @@ SELECT <replaceable>select_list</replace
Table Expressions or <acronym>CTE</acronym>s, can be thought of as defining
temporary tables that exist just for one query. Each auxiliary statement
in a <literal>WITH</literal> clause can be a <command>SELECT</command>,
- <command>INSERT</command>, <command>UPDATE</command>, or <command>DELETE</command>; and the
+ <command>INSERT</command>, <command>UPDATE</command>, <command>DELETE</command>,
+ or <command>MERGE</command>; and the
<literal>WITH</literal> clause itself is attached to a primary statement that can
- be a <command>SELECT</command>, <command>INSERT</command>, <command>UPDATE</command>,
+ also be a <command>SELECT</command>, <command>INSERT</command>, <command>UPDATE</command>,
<command>DELETE</command>, or <command>MERGE</command>.
</para>
@@ -2598,8 +2599,8 @@ SELECT * FROM w AS w1 JOIN w AS w2 ON w1
<title>Data-Modifying Statements in <literal>WITH</literal></title>
<para>
- You can use most data-modifying statements (<command>INSERT</command>,
- <command>UPDATE</command>, or <command>DELETE</command>, but not
+ You can use data-modifying statements (<command>INSERT</command>,
+ <command>UPDATE</command>, <command>DELETE</command>, or
<command>MERGE</command>) in <literal>WITH</literal>. This
allows you to perform several different operations in the same query.
An example is:
diff --git a/doc/src/sgml/ref/copy.sgml b/doc/src/sgml/ref/copy.sgml
new file mode 100644
index 4d614a0..efaacb9
--- a/doc/src/sgml/ref/copy.sgml
+++ b/doc/src/sgml/ref/copy.sgml
@@ -122,13 +122,16 @@ COPY { <replaceable class="parameter">ta
A <link linkend="sql-select"><command>SELECT</command></link>,
<link linkend="sql-values"><command>VALUES</command></link>,
<link linkend="sql-insert"><command>INSERT</command></link>,
- <link linkend="sql-update"><command>UPDATE</command></link>, or
- <link linkend="sql-delete"><command>DELETE</command></link> command whose results are to be
- copied. Note that parentheses are required around the query.
+ <link linkend="sql-update"><command>UPDATE</command></link>,
+ <link linkend="sql-delete"><command>DELETE</command></link>, or
+ <link linkend="sql-merge"><command>MERGE</command></link> command
+ whose results are to be copied. Note that parentheses are required
+ around the query.
</para>
<para>
- For <command>INSERT</command>, <command>UPDATE</command> and
- <command>DELETE</command> queries a RETURNING clause must be provided,
+ For <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, and <command>MERGE</command> queries a
+ RETURNING clause must be provided,
and the target relation must not have a conditional rule, nor
an <literal>ALSO</literal> rule, nor an <literal>INSTEAD</literal> rule
that expands to multiple statements.
diff --git a/doc/src/sgml/ref/merge.sgml b/doc/src/sgml/ref/merge.sgml
new file mode 100644
index 0995fe0..d1aa937
--- a/doc/src/sgml/ref/merge.sgml
+++ b/doc/src/sgml/ref/merge.sgml
@@ -25,6 +25,7 @@ PostgreSQL documentation
MERGE INTO [ ONLY ] <replaceable class="parameter">target_table_name</replaceable> [ * ] [ [ AS ] <replaceable class="parameter">target_alias</replaceable> ]
USING <replaceable class="parameter">data_source</replaceable> ON <replaceable class="parameter">join_condition</replaceable>
<replaceable class="parameter">when_clause</replaceable> [...]
+[ RETURNING * | <replaceable class="parameter">output_expression</replaceable> [ [ AS ] <replaceable class="parameter">output_name</replaceable> ] [, ...] ]
<phrase>where <replaceable class="parameter">data_source</replaceable> is:</phrase>
@@ -95,6 +96,19 @@ DELETE
</para>
<para>
+ The optional <literal>RETURNING</literal> clause causes <command>MERGE</command>
+ to compute and return value(s) based on each row inserted, updated, or
+ deleted. Any expression using the source or target table's columns, or the
+ merge support functions <xref linkend="pg-merge-action"/> and
+ <xref linkend="pg-merge-when-clause-number"/> can be computed. When an
+ <command>INSERT</command> or <command>UPDATE</command> action is performed,
+ the new values of the target table's columns are used. When a
+ <command>DELETE</command> is performed, the old values of the target table's
+ columns are used. The syntax of the <literal>RETURNING</literal> list is
+ identical to that of the output list of <command>SELECT</command>.
+ </para>
+
+ <para>
There is no separate <literal>MERGE</literal> privilege.
If you specify an update action, you must have the
<literal>UPDATE</literal> privilege on the column(s)
@@ -125,6 +139,18 @@ DELETE
<variablelist>
<varlistentry>
+ <term><replaceable class="parameter">with_query</replaceable></term>
+ <listitem>
+ <para>
+ The <literal>WITH</literal> clause allows you to specify one or more
+ subqueries that can be referenced by name in the <command>MERGE</command>
+ query. See <xref linkend="queries-with"/> and <xref linkend="sql-select"/>
+ for details.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
<term><replaceable class="parameter">target_table_name</replaceable></term>
<listitem>
<para>
@@ -399,13 +425,31 @@ DELETE
</varlistentry>
<varlistentry>
- <term><replaceable class="parameter">with_query</replaceable></term>
+ <term><replaceable class="parameter">output_expression</replaceable></term>
<listitem>
<para>
- The <literal>WITH</literal> clause allows you to specify one or more
- subqueries that can be referenced by name in the <command>MERGE</command>
- query. See <xref linkend="queries-with"/> and <xref linkend="sql-select"/>
- for details.
+ An expression to be computed and returned by the <command>MERGE</command>
+ command after each row is merged. The expression can use any columns of
+ the source or target tables, or the merge support functions
+ <xref linkend="pg-merge-action"/> and
+ <xref linkend="pg-merge-when-clause-number"/>.
+ </para>
+ <para>
+ Writing <literal>*</literal> will return all columns from the source
+ table, followed by all columns from the target table. Often this will
+ lead to a lot of duplication, since it is common for the source and
+ target tables to have a lot of the same columns. This can be avoided by
+ qualifying the <literal>*</literal> with the name or alias of the source
+ or target table.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><replaceable class="parameter">output_name</replaceable></term>
+ <listitem>
+ <para>
+ A name to use for a returned column.
</para>
</listitem>
</varlistentry>
@@ -428,6 +472,13 @@ MERGE <replaceable class="parameter">tot
were changed in any way.
</para>
+ <para>
+ If the <command>MERGE</command> command contains a <literal>RETURNING</literal>
+ clause, the result will be similar to that of a <command>SELECT</command>
+ statement containing the columns and values defined in the
+ <literal>RETURNING</literal> list, computed over the row(s) inserted, updated,
+ or deleted by the command.
+ </para>
</refsect1>
<refsect1>
@@ -544,13 +595,6 @@ MERGE <replaceable class="parameter">tot
</para>
<para>
- There is no <literal>RETURNING</literal> clause with
- <command>MERGE</command>. Actions of <command>INSERT</command>,
- <command>UPDATE</command> and <command>DELETE</command> cannot contain
- <literal>RETURNING</literal> or <literal>WITH</literal> clauses.
- </para>
-
- <para>
When <command>MERGE</command> is run concurrently with other commands
that modify the target table, the usual transaction isolation rules
apply; see <xref linkend="transaction-iso"/> for an explanation
@@ -602,7 +646,8 @@ WHEN NOT MATCHED THEN
<para>
Attempt to insert a new stock item along with the quantity of stock. If
the item already exists, instead update the stock count of the existing
- item. Don't allow entries that have zero stock.
+ item. Don't allow entries that have zero stock. Return details of all
+ changes made.
<programlisting>
MERGE INTO wines w
USING wine_stock_changes s
@@ -612,7 +657,8 @@ WHEN NOT MATCHED AND s.stock_delta > 0 T
WHEN MATCHED AND w.stock + s.stock_delta > 0 THEN
UPDATE SET stock = w.stock + s.stock_delta
WHEN MATCHED THEN
- DELETE;
+ DELETE
+RETURNING pg_merge_action(), w.*;
</programlisting>
The <literal>wine_stock_changes</literal> table might be, for example, a
@@ -627,7 +673,8 @@ WHEN MATCHED THEN
This command conforms to the <acronym>SQL</acronym> standard.
</para>
<para>
- The WITH clause and <literal>DO NOTHING</literal> action are extensions to
+ The WITH clause, <literal>DO NOTHING</literal> action, and
+ <literal>RETURNING</literal> clause are extensions to
the <acronym>SQL</acronym> standard.
</para>
</refsect1>
diff --git a/doc/src/sgml/ref/select.sgml b/doc/src/sgml/ref/select.sgml
new file mode 100644
index 0ee0cc7..f81bdea
--- a/doc/src/sgml/ref/select.sgml
+++ b/doc/src/sgml/ref/select.sgml
@@ -74,7 +74,7 @@ SELECT [ ALL | DISTINCT [ ON ( <replacea
<phrase>and <replaceable class="parameter">with_query</replaceable> is:</phrase>
- <replaceable class="parameter">with_query_name</replaceable> [ ( <replaceable class="parameter">column_name</replaceable> [, ...] ) ] AS [ [ NOT ] MATERIALIZED ] ( <replaceable class="parameter">select</replaceable> | <replaceable class="parameter">values</replaceable> | <replaceable class="parameter">insert</replaceable> | <replaceable class="parameter">update</replaceable> | <replaceable class="parameter">delete</replaceable> )
+ <replaceable class="parameter">with_query_name</replaceable> [ ( <replaceable class="parameter">column_name</replaceable> [, ...] ) ] AS [ [ NOT ] MATERIALIZED ] ( <replaceable class="parameter">select</replaceable> | <replaceable class="parameter">values</replaceable> | <replaceable class="parameter">insert</replaceable> | <replaceable class="parameter">update</replaceable> | <replaceable class="parameter">delete</replaceable> | <replaceable class="parameter">merge</replaceable> )
[ SEARCH { BREADTH | DEPTH } FIRST BY <replaceable>column_name</replaceable> [, ...] SET <replaceable>search_seq_col_name</replaceable> ]
[ CYCLE <replaceable>column_name</replaceable> [, ...] SET <replaceable>cycle_mark_col_name</replaceable> [ TO <replaceable>cycle_mark_value</replaceable> DEFAULT <replaceable>cycle_mark_default</replaceable> ] USING <replaceable>cycle_path_col_name</replaceable> ]
@@ -227,10 +227,10 @@ TABLE [ ONLY ] <replaceable class="param
The subqueries effectively act as temporary tables or views
for the duration of the primary query.
Each subquery can be a <command>SELECT</command>, <command>TABLE</command>, <command>VALUES</command>,
- <command>INSERT</command>, <command>UPDATE</command> or
- <command>DELETE</command> statement.
+ <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command> statement.
When writing a data-modifying statement (<command>INSERT</command>,
- <command>UPDATE</command> or <command>DELETE</command>) in
+ <command>UPDATE</command>, <command>DELETE</command>, or <command>MERGE</command>) in
<literal>WITH</literal>, it is usual to include a <literal>RETURNING</literal> clause.
It is the output of <literal>RETURNING</literal>, <emphasis>not</emphasis> the underlying
table that the statement modifies, that forms the temporary table that is
@@ -2179,7 +2179,8 @@ SELECT 2+2;
<para>
<productname>PostgreSQL</productname> allows <command>INSERT</command>,
- <command>UPDATE</command>, and <command>DELETE</command> to be used as <literal>WITH</literal>
+ <command>UPDATE</command>, <command>DELETE</command>, and
+ <command>MERGE</command> to be used as <literal>WITH</literal>
queries. This is not found in the SQL standard.
</para>
</refsect2>
diff --git a/doc/src/sgml/rowtypes.sgml b/doc/src/sgml/rowtypes.sgml
new file mode 100644
index 4d86f97..bbeac84
--- a/doc/src/sgml/rowtypes.sgml
+++ b/doc/src/sgml/rowtypes.sgml
@@ -348,7 +348,7 @@ SELECT m.* FROM some_table, LATERAL myfu
column expansion of this kind when it appears at the top level of
a <link linkend="queries-select-lists"><command>SELECT</command> output
list</link>, a <link linkend="dml-returning"><literal>RETURNING</literal>
- list</link> in <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>,
+ list</link> in <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command>,
a <link linkend="queries-values"><literal>VALUES</literal> clause</link>, or
a <link linkend="sql-syntax-row-constructors">row constructor</link>.
In all other contexts (including when nested inside one of those
diff --git a/doc/src/sgml/spi.sgml b/doc/src/sgml/spi.sgml
new file mode 100644
index 16c3ab7..6410862
--- a/doc/src/sgml/spi.sgml
+++ b/doc/src/sgml/spi.sgml
@@ -301,8 +301,9 @@ SPI_execute("INSERT INTO foo SELECT * FR
is returned in the global variable <varname>SPI_processed</varname>.
If the return value of the function is <symbol>SPI_OK_SELECT</symbol>,
<symbol>SPI_OK_INSERT_RETURNING</symbol>,
- <symbol>SPI_OK_DELETE_RETURNING</symbol>, or
- <symbol>SPI_OK_UPDATE_RETURNING</symbol>,
+ <symbol>SPI_OK_DELETE_RETURNING</symbol>,
+ <symbol>SPI_OK_UPDATE_RETURNING</symbol>, or
+ <symbol>SPI_OK_MERGE_RETURNING</symbol>,
then you can use the
global pointer <literal>SPITupleTable *SPI_tuptable</literal> to
access the result rows. Some utility commands (such as
@@ -472,6 +473,15 @@ typedef struct SPITupleTable
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><symbol>SPI_OK_MERGE_RETURNING</symbol></term>
+ <listitem>
+ <para>
+ if a <command>MERGE RETURNING</command> was executed
+ </para>
+ </listitem>
+ </varlistentry>
<varlistentry>
<term><symbol>SPI_OK_UTILITY</symbol></term>
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
new file mode 100644
index 281c178..3aec197
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -177,7 +177,8 @@
statements separated by semicolons. A semicolon after the last
statement is optional. Unless the function is declared to return
<type>void</type>, the last statement must be a <command>SELECT</command>,
- or an <command>INSERT</command>, <command>UPDATE</command>, or <command>DELETE</command>
+ or an <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command>
that has a <literal>RETURNING</literal> clause.
</para>
@@ -1236,8 +1237,9 @@ SELECT x, CASE WHEN x > 0 THEN genera
<note>
<para>
- If a function's last command is <command>INSERT</command>, <command>UPDATE</command>,
- or <command>DELETE</command> with <literal>RETURNING</literal>, that command will
+ If a function's last command is <command>INSERT</command>,
+ <command>UPDATE</command>, <command>DELETE</command>, or
+ <command>MERGE</command> with <literal>RETURNING</literal>, that command will
always be executed to completion, even if the function is not declared
with <literal>SETOF</literal> or the calling query does not fetch all the
result rows. Any extra rows produced by the <literal>RETURNING</literal>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
new file mode 100644
index f14fae3..05f2e5b
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -283,12 +283,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 eaa3172..a50cd30
--- 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 2c62b0c..f09d512
--- 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/jsonfuncs.h"
#include "utils/lsyscache.h"
#include "utils/typcache.h"
@@ -2620,6 +2621,22 @@ ExecInitFunc(ExprEvalStep *scratch, Expr
InitFunctionCallInfoData(*fcinfo, flinfo,
nargs, inputcollid, NULL, NULL);
+ /*
+ * Merge support functions should only be called 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 eb8a87f..b9bd03c
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -612,8 +612,8 @@ ExecInitPartitionInfo(ModifyTableState *
* Build the RETURNING projection for the partition. Note that we didn't
* build the returningList for partitions within the planner, but simple
* translation of varattnos will suffice. This only occurs for the INSERT
- * case or in the case of UPDATE tuple routing where we didn't find a
- * result rel to reuse.
+ * case or in the case of UPDATE/MERGE tuple routing where we didn't find
+ * a result rel to reuse.
*/
if (node && node->returningLists != NIL)
{
@@ -622,12 +622,14 @@ ExecInitPartitionInfo(ModifyTableState *
List *returningList;
/* See the comment above for WCO lists. */
- /* (except no RETURNING support for MERGE yet) */
Assert((node->operation == CMD_INSERT &&
list_length(node->returningLists) == 1 &&
list_length(node->resultRelations) == 1) ||
(node->operation == CMD_UPDATE &&
list_length(node->returningLists) ==
+ list_length(node->resultRelations)) ||
+ (node->operation == CMD_MERGE &&
+ list_length(node->returningLists) ==
list_length(node->resultRelations)));
/*
diff --git a/src/backend/executor/functions.c b/src/backend/executor/functions.c
new file mode 100644
index f55424e..97874a4
--- a/src/backend/executor/functions.c
+++ b/src/backend/executor/functions.c
@@ -1660,8 +1660,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
@@ -1679,7 +1679,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;
@@ -1693,7 +1694,7 @@ check_sql_fn_retval(List *queryTreeLists
(errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
errmsg("return type mismatch in function declared to return %s",
format_type_be(rettype)),
- errdetail("Function's final statement must be SELECT or INSERT/UPDATE/DELETE RETURNING.")));
+ errdetail("Function's final statement must be SELECT or INSERT/UPDATE/DELETE/MERGE RETURNING.")));
return false; /* keep compiler quiet */
}
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
new file mode 100644
index 5005d8c..a6f6e32
--- 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"
@@ -88,9 +87,6 @@ typedef struct ModifyTableContext
*/
TupleTableSlot *planSlot;
- /* MERGE specific */
- MergeActionState *relaction; /* MERGE action in progress */
-
/*
* Information about the changes that were made concurrently to a tuple
* being updated or deleted
@@ -153,13 +149,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);
/*
@@ -979,7 +976,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;
@@ -1828,7 +1825,7 @@ ExecCrossPartitionUpdate(ModifyTableCont
* additional rechecking, and might end up executing a different
* action entirely).
*/
- if (context->relaction != NULL)
+ if (mtstate->mt_merge_action != NULL)
return false;
else if (TupIsNull(epqslot))
return true;
@@ -2069,7 +2066,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;
/*
@@ -2712,6 +2709,7 @@ static TupleTableSlot *
ExecMerge(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
ItemPointer tupleid, bool canSetTag)
{
+ TupleTableSlot *rslot = NULL;
bool matched;
/*-----
@@ -2759,18 +2757,18 @@ 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()
- * returned "false", indicating the previously MATCHED tuple no longer
- * matches.
+ * Deal with the NOT MATCHED case (either a NOT MATCHED tuple from the
+ * join, or a previously MATCHED tuple for which ExecMergeMatched() set
+ * "matched" to false, indicating that it no longer matches).
*/
if (!matched)
- ExecMergeNotMatched(context, resultRelInfo, canSetTag);
+ rslot = ExecMergeNotMatched(context, resultRelInfo, canSetTag);
- /* No RETURNING support yet */
- return NULL;
+ return rslot;
}
/*
@@ -2780,8 +2778,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.
*
@@ -2790,15 +2788,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;
@@ -2810,7 +2809,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
@@ -2889,12 +2891,15 @@ lmerge_matched:
*/
newslot = ExecProject(relaction->mas_proj);
- context->relaction = relaction;
+ mtstate->mt_merge_action = relaction;
if (!ExecUpdatePrologue(context, resultRelInfo,
tupleid, NULL, newslot, &result))
{
if (result == TM_Ok)
- return true; /* "do nothing" */
+ {
+ *matched = true;
+ return NULL; /* "do nothing" */
+ }
break; /* concurrent update/delete */
}
result = ExecUpdateAct(context, resultRelInfo, tupleid, NULL,
@@ -2908,12 +2913,15 @@ lmerge_matched:
break;
case CMD_DELETE:
- context->relaction = relaction;
+ mtstate->mt_merge_action = relaction;
if (!ExecDeletePrologue(context, resultRelInfo, tupleid,
NULL, NULL, &result))
{
if (result == TM_Ok)
- return true; /* "do nothing" */
+ {
+ *matched = true;
+ return NULL; /* "do nothing" */
+ }
break; /* concurrent update/delete */
}
result = ExecDeleteAct(context, resultRelInfo, tupleid, false);
@@ -2969,7 +2977,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:
{
@@ -3015,13 +3024,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
@@ -3056,7 +3071,8 @@ lmerge_matched:
* tuple already deleted; tell caller to run NOT
* MATCHED actions
*/
- return false;
+ *matched = false;
+ return NULL;
case TM_SelfModified:
@@ -3076,13 +3092,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;
}
}
@@ -3094,6 +3111,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.
@@ -3104,19 +3145,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;
/*
@@ -3166,10 +3210,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:
@@ -3185,6 +3229,8 @@ ExecMergeNotMatched(ModifyTableContext *
*/
break;
}
+
+ return rslot;
}
/*
@@ -3362,6 +3408,65 @@ ExecInitMergeTupleSlots(ModifyTableState
}
/*
+ * 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;
+ CmdType commandType;
+
+ if (!mtstate || mtstate->operation != CMD_MERGE)
+ elog(ERROR, "merge support function called in non-merge context");
+
+ relaction = mtstate->mt_merge_action;
+ if (!relaction)
+ elog(ERROR, "no merge action in progress");
+
+ 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:
+ elog(ERROR, "unexpected merge action: DO NOTHING");
+ break;
+ default:
+ elog(ERROR, "unrecognized commandType: %d", (int) commandType);
+ }
+
+ PG_RETURN_NULL(); /* keep compiler quiet */
+}
+
+/*
+ * pg_merge_when_clause_number() -
+ * SQL merge support function to retrieve the 1-based index of the
+ * currently executing merge WHEN clause.
+ */
+Datum
+pg_merge_when_clause_number(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)
+ elog(ERROR, "no merge action in progress");
+
+ PG_RETURN_INT32((int32) relaction->mas_action->index);
+}
+
+/*
* Process BEFORE EACH STATEMENT triggers
*/
static void
@@ -3647,8 +3752,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");
@@ -3725,8 +3839,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");
@@ -3818,7 +3941,7 @@ ExecModifyTable(PlanState *pstate)
}
slot = ExecGetUpdateNewTuple(resultRelInfo, context.planSlot,
oldSlot);
- context.relaction = NULL;
+ node->mt_merge_action = NULL;
/* Now apply the update. */
slot = ExecUpdate(&context, resultRelInfo, tupleid, oldtuple,
diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c
new file mode 100644
index 3397568..b9f443c
--- a/src/backend/executor/spi.c
+++ b/src/backend/executor/spi.c
@@ -2033,6 +2033,8 @@ SPI_result_code_string(int code)
return "SPI_OK_TD_REGISTER";
case SPI_OK_MERGE:
return "SPI_OK_MERGE";
+ case SPI_OK_MERGE_RETURNING:
+ return "SPI_OK_MERGE_RETURNING";
}
/* Unrecognized code ... return something useful ... */
sprintf(buf, "Unrecognized SPI code %d", code);
@@ -2886,7 +2888,10 @@ _SPI_pquery(QueryDesc *queryDesc, bool f
res = SPI_OK_UPDATE;
break;
case CMD_MERGE:
- res = SPI_OK_MERGE;
+ if (queryDesc->plannedstmt->hasReturning)
+ res = SPI_OK_MERGE_RETURNING;
+ else
+ res = SPI_OK_MERGE;
break;
default:
return SPI_ERROR_OPUNKNOWN;
diff --git a/src/backend/optimizer/plan/subselect.c b/src/backend/optimizer/plan/subselect.c
new file mode 100644
index 7a9fe88..d3ee1a2
--- a/src/backend/optimizer/plan/subselect.c
+++ b/src/backend/optimizer/plan/subselect.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "catalog/pg_operator.h"
+#include "catalog/pg_proc.h"
#include "catalog/pg_type.h"
#include "executor/executor.h"
#include "miscadmin.h"
@@ -35,6 +36,7 @@
#include "parser/parse_relation.h"
#include "rewrite/rewriteManip.h"
#include "utils/builtins.h"
+#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/syscache.h"
@@ -1846,7 +1848,8 @@ convert_EXISTS_to_ANY(PlannerInfo *root,
/*
* Replace correlation vars (uplevel vars) with Params.
*
- * Uplevel PlaceHolderVars and aggregates are replaced, too.
+ * Uplevel PlaceHolderVars, aggregates, GROUPING() expressions, and merge
+ * support functions are replaced, too.
*
* Note: it is critical that this runs immediately after SS_process_sublinks.
* Since we do not recurse into the arguments of uplevel PHVs and aggregates,
@@ -1900,6 +1903,13 @@ replace_correlation_vars_mutator(Node *n
if (((GroupingFunc *) node)->agglevelsup > 0)
return (Node *) replace_outer_grouping(root, (GroupingFunc *) node);
}
+ if (IsA(node, FuncExpr) &&
+ IsMergeSupportFunction(((FuncExpr *) node)->funcid))
+ {
+ if (root->parse->commandType != CMD_MERGE)
+ return (Node *) replace_outer_merge_support_function(root,
+ (FuncExpr *) node);
+ }
return expression_tree_mutator(node,
replace_correlation_vars_mutator,
(void *) root);
diff --git a/src/backend/optimizer/util/paramassign.c b/src/backend/optimizer/util/paramassign.c
new file mode 100644
index d6a923b..5ecf502
--- a/src/backend/optimizer/util/paramassign.c
+++ b/src/backend/optimizer/util/paramassign.c
@@ -307,6 +307,57 @@ replace_outer_grouping(PlannerInfo *root
}
/*
+ * Generate a Param node to replace the given FuncExpr node, which is expected
+ * to be a merge support function in the RETURNING list of an upper-level
+ * MERGE query. Record the need for the FuncExpr in the proper upper-level
+ * root->plan_params.
+ */
+Param *
+replace_outer_merge_support_function(PlannerInfo *root, FuncExpr *func)
+{
+ Param *retval;
+ PlannerParamItem *pitem;
+ Oid ptype = exprType((Node *) func);
+
+ Assert(root->parse->commandType != CMD_MERGE);
+
+ /*
+ * The parser should have ensured that the merge support function is in
+ * the RETURNING list of an upper-level MERGE query, so find that query.
+ */
+ do
+ {
+ root = root->parent_root;
+ if (root == NULL)
+ elog(ERROR, "merge support function used outside MERGE");
+ } while (root->parse->commandType != CMD_MERGE);
+
+ /*
+ * It does not seem worthwhile to try to de-duplicate references to outer
+ * merge support functions. Just make a new slot every time.
+ */
+ func = copyObject(func);
+
+ pitem = makeNode(PlannerParamItem);
+ pitem->item = (Node *) func;
+ pitem->paramId = list_length(root->glob->paramExecTypes);
+ root->glob->paramExecTypes = lappend_oid(root->glob->paramExecTypes,
+ ptype);
+
+ root->plan_params = lappend(root->plan_params, pitem);
+
+ retval = makeNode(Param);
+ retval->paramkind = PARAM_EXEC;
+ retval->paramid = pitem->paramId;
+ retval->paramtype = ptype;
+ retval->paramtypmod = -1;
+ retval->paramcollid = InvalidOid;
+ retval->location = func->location;
+
+ return retval;
+}
+
+/*
* Generate a Param node to replace the given Var,
* which is expected to come from some upper NestLoop plan node.
* Record the need for the Var in root->curOuterParams.
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
new file mode 100644
index 4006632..6794511
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -74,7 +74,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,
@@ -515,7 +514,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;
@@ -942,7 +942,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;
@@ -2407,7 +2408,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
@@ -2501,10 +2503,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;
@@ -2521,7 +2524,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 b3bdf94..dd2a74f
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -12270,6 +12270,7 @@ MergeStmt:
USING table_ref
ON a_expr
merge_when_list
+ returning_clause
{
MergeStmt *m = makeNode(MergeStmt);
@@ -12278,6 +12279,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 85cd47b..e72c37b
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -456,6 +456,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 aa72665..3957d7c
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -541,6 +541,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:
@@ -1767,6 +1768,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:
@@ -3115,6 +3117,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 b3f0b6a..cf7c0b9
--- 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,26 @@ 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)
+ {
+ ParseState *parent_pstate = pstate->parentParseState;
+
+ /* May be in a subquery in RETURNING list */
+ while (parent_pstate &&
+ parent_pstate->p_expr_kind != EXPR_KIND_MERGE_RETURNING)
+ parent_pstate = parent_pstate->parentParseState;
+
+ if (!parent_pstate ||
+ parent_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.
*/
@@ -2599,6 +2620,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 91b1156..33852e9
--- 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.
@@ -254,6 +258,7 @@ transformMergeStmt(ParseState *pstate, M
MergeAction *action;
action = makeNode(MergeAction);
+ action->index = foreach_current_index(l) + 1;
action->commandType = mergeWhenClause->commandType;
action->matched = mergeWhenClause->matched;
@@ -390,9 +395,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 864ea9b..cc19041
--- a/src/backend/parser/parse_relation.c
+++ b/src/backend/parser/parse_relation.c
@@ -2346,9 +2346,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 b486ab5..1bd0ce2
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -3629,9 +3629,9 @@ RewriteQuery(Query *parsetree, List *rew
ListCell *lc1;
/*
- * First, recursively process any insert/update/delete statements in WITH
- * clauses. (We have to do this first because the WITH clauses may get
- * copied into rule actions below.)
+ * First, recursively process any insert/update/delete/merge statements in
+ * WITH clauses. (We have to do this first because the WITH clauses may
+ * get copied into rule actions below.)
*/
foreach(lc1, parsetree->cteList)
{
@@ -3656,7 +3656,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/rewrite/rowsecurity.c b/src/backend/rewrite/rowsecurity.c
new file mode 100644
index b1620e4..966dcf0
--- a/src/backend/rewrite/rowsecurity.c
+++ b/src/backend/rewrite/rowsecurity.c
@@ -395,10 +395,10 @@ get_row_security_policies(Query *root, R
* on the final action we take.
*
* We already fetched the SELECT policies above, to check existing rows,
- * but we must also check that new rows created by UPDATE actions are
- * visible, if SELECT rights are required for this relation. We don't do
- * this for INSERT actions, since an INSERT command would only do this
- * check if it had a RETURNING list, and MERGE does not support RETURNING.
+ * but we must also check that new rows created by INSERT/UPDATE actions
+ * are visible, if SELECT rights are required. For INSERT actions, we only
+ * do this if RETURNING is specified, to be consistent with a plain INSERT
+ * command, which can only require SELECT rights when RETURNING is used.
*
* 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
@@ -420,6 +420,8 @@ get_row_security_policies(Query *root, R
List *merge_delete_restrictive_policies;
List *merge_insert_permissive_policies;
List *merge_insert_restrictive_policies;
+ List *merge_select_permissive_policies = NIL;
+ List *merge_select_restrictive_policies = NIL;
/*
* Fetch the UPDATE policies and set them up to execute on the
@@ -457,9 +459,6 @@ get_row_security_policies(Query *root, R
*/
if (perminfo->requiredPerms & ACL_SELECT)
{
- List *merge_select_permissive_policies;
- List *merge_select_restrictive_policies;
-
get_policies_for_relation(rel, CMD_SELECT, user_id,
&merge_select_permissive_policies,
&merge_select_restrictive_policies);
@@ -508,6 +507,21 @@ get_row_security_policies(Query *root, R
withCheckOptions,
hasSubLinks,
false);
+
+ /*
+ * Add ALL/SELECT policies as WCO_RLS_INSERT_CHECK WCOs, to ensure
+ * that the inserted row is visible when executing an INSERT action,
+ * if RETURNING is specified and SELECT rights are required for this
+ * relation.
+ */
+ if (perminfo->requiredPerms & ACL_SELECT && root->returningList)
+ add_with_check_options(rel, rt_index,
+ WCO_RLS_INSERT_CHECK,
+ merge_select_permissive_policies,
+ merge_select_restrictive_policies,
+ withCheckOptions,
+ hasSubLinks,
+ true);
}
table_close(rel, NoLock);
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
new file mode 100644
index e3ccf6c..6f3dc89
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -2142,11 +2142,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/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
new file mode 100644
index 03f2835..25f49fa
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -7183,8 +7183,13 @@ get_merge_query_def(Query *query, depars
appendStringInfoString(buf, "DO NOTHING");
}
- /* No RETURNING support in MERGE yet */
- Assert(query->returningList == NIL);
+ /* Add RETURNING if present */
+ if (query->returningList)
+ {
+ appendContextKeyword(context, " RETURNING",
+ -PRETTYINDENT_STD, PRETTYINDENT_STD, 1);
+ get_target_list(query->returningList, context, NULL, colNamesVisible);
+ }
}
diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c
new file mode 100644
index 10ad1f2..3f42ca4
--- a/src/bin/psql/common.c
+++ b/src/bin/psql/common.c
@@ -982,13 +982,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 12fac15..88a3b29
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12049,4 +12049,14 @@
proname => 'any_value_transfn', prorettype => 'anyelement',
proargtypes => 'anyelement anyelement', prosrc => 'any_value_transfn' },
+# MERGE support functions
+{ oid => '9499', descr => 'command type of current MERGE action',
+ proname => 'pg_merge_action', provolatile => 'v', proparallel => 'r',
+ prorettype => 'text', proargtypes => '',
+ prosrc => 'pg_merge_action' },
+{ oid => '9500', descr => 'index of current MERGE WHEN clause',
+ proname => 'pg_merge_when_clause_number', provolatile => 'v',
+ proparallel => 'r', prorettype => 'int4', proargtypes => '',
+ prosrc => 'pg_merge_when_clause_number' },
+
]
diff --git a/src/include/catalog/pg_proc.h b/src/include/catalog/pg_proc.h
new file mode 100644
index e7abe0b..eb6cad9
--- 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(funcid) \
+ ((funcid) == F_PG_MERGE_ACTION || \
+ (funcid) == F_PG_MERGE_WHEN_CLAUSE_NUMBER)
+
#endif /* EXPOSE_TO_CLIENT_CODE */
diff --git a/src/include/executor/spi.h b/src/include/executor/spi.h
new file mode 100644
index d1de139..ed8787d
--- a/src/include/executor/spi.h
+++ b/src/include/executor/spi.h
@@ -97,6 +97,7 @@ typedef struct _SPI_plan *SPIPlanPtr;
#define SPI_OK_REL_UNREGISTER 16
#define SPI_OK_TD_REGISTER 17
#define SPI_OK_MERGE 18
+#define SPI_OK_MERGE_RETURNING 19
#define SPI_OPT_NONATOMIC (1 << 0)
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
new file mode 100644
index cb714f4..6708b6b
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1319,6 +1319,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 2565348..ac0a754
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -1688,6 +1688,7 @@ typedef struct MergeWhenClause
typedef struct MergeAction
{
NodeTag type;
+ int index; /* 1-based index of the clause */
bool matched; /* true=MATCHED, false=NOT MATCHED */
CmdType commandType; /* INSERT/UPDATE/DELETE/DO NOTHING */
/* OVERRIDING clause */
@@ -1951,6 +1952,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/optimizer/paramassign.h b/src/include/optimizer/paramassign.h
new file mode 100644
index 55c27a6..6f70c78
--- a/src/include/optimizer/paramassign.h
+++ b/src/include/optimizer/paramassign.h
@@ -20,6 +20,8 @@ extern Param *replace_outer_placeholderv
PlaceHolderVar *phv);
extern Param *replace_outer_agg(PlannerInfo *root, Aggref *agg);
extern Param *replace_outer_grouping(PlannerInfo *root, GroupingFunc *grp);
+extern Param *replace_outer_merge_support_function(PlannerInfo *root,
+ FuncExpr *func);
extern Param *replace_nestloop_param_var(PlannerInfo *root, Var *var);
extern Param *replace_nestloop_param_placeholdervar(PlannerInfo *root,
PlaceHolderVar *phv);
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 f589112..836819d
--- 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/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
new file mode 100644
index 4b76f76..fb6289c
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -4267,9 +4267,9 @@ exec_stmt_execsql(PLpgSQL_execstate *est
/*
* If we have INTO, then we only need one row back ... but if we have INTO
* STRICT or extra check too_many_rows, ask for two rows, so that we can
- * verify the statement returns only one. INSERT/UPDATE/DELETE are always
- * treated strictly. Without INTO, just run the statement to completion
- * (tcount = 0).
+ * verify the statement returns only one. INSERT/UPDATE/DELETE/MERGE are
+ * always treated strictly. Without INTO, just run the statement to
+ * completion (tcount = 0).
*
* We could just ask for two rows always when using INTO, but there are
* some cases where demanding the extra row costs significant time, eg by
@@ -4307,10 +4307,11 @@ exec_stmt_execsql(PLpgSQL_execstate *est
case SPI_OK_INSERT:
case SPI_OK_UPDATE:
case SPI_OK_DELETE:
+ case SPI_OK_MERGE:
case SPI_OK_INSERT_RETURNING:
case SPI_OK_UPDATE_RETURNING:
case SPI_OK_DELETE_RETURNING:
- case SPI_OK_MERGE:
+ case SPI_OK_MERGE_RETURNING:
Assert(stmt->mod_stmt);
exec_set_found(estate, (SPI_processed != 0));
break;
@@ -4489,10 +4490,11 @@ exec_stmt_dynexecute(PLpgSQL_execstate *
case SPI_OK_INSERT:
case SPI_OK_UPDATE:
case SPI_OK_DELETE:
+ case SPI_OK_MERGE:
case SPI_OK_INSERT_RETURNING:
case SPI_OK_UPDATE_RETURNING:
case SPI_OK_DELETE_RETURNING:
- case SPI_OK_MERGE:
+ case SPI_OK_MERGE_RETURNING:
case SPI_OK_UTILITY:
case SPI_OK_REWRITTEN:
break;
diff --git a/src/pl/tcl/pltcl.c b/src/pl/tcl/pltcl.c
new file mode 100644
index e8f9d7b..0c9f81e
--- a/src/pl/tcl/pltcl.c
+++ b/src/pl/tcl/pltcl.c
@@ -2456,6 +2456,7 @@ pltcl_process_SPI_result(Tcl_Interp *int
case SPI_OK_INSERT_RETURNING:
case SPI_OK_DELETE_RETURNING:
case SPI_OK_UPDATE_RETURNING:
+ case SPI_OK_MERGE_RETURNING:
/*
* Process the tuples we got
diff --git a/src/test/regress/expected/merge.out b/src/test/regress/expected/merge.out
new file mode 100644
index 133d421..5be3d2d
--- 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;
@@ -1256,7 +1256,7 @@ BEGIN;
MERGE INTO sq_target
USING v
ON tid = sid
-WHEN MATCHED AND tid > 2 THEN
+WHEN MATCHED AND tid >= 2 THEN
UPDATE SET balance = balance + delta
WHEN NOT MATCHED THEN
INSERT (balance, tid) VALUES (balance + delta, sid)
@@ -1271,7 +1271,7 @@ INSERT INTO sq_source (sid, balance, del
MERGE INTO sq_target t
USING v
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)
@@ -1295,7 +1295,7 @@ WITH targq AS (
MERGE INTO sq_target t
USING v
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)
@@ -1304,20 +1304,193 @@ WHEN MATCHED AND tid < 2 THEN
ROLLBACK;
-- RETURNING
BEGIN;
-INSERT INTO sq_source (sid, balance, delta) VALUES (-1, -1, -10);
+CREATE TABLE merge_actions(action text, abbrev text);
+INSERT INTO merge_actions VALUES ('INSERT', 'ins'), ('UPDATE', 'upd'), ('DELETE', 'del');
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)
+ 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 *;
- ^
+ DELETE
+RETURNING pg_merge_when_clause_number() AS clause_number,
+ (SELECT abbrev FROM merge_actions
+ WHERE action = 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;
+ clause_number | merge_action | tid | balance | description
+---------------+--------------+-----+---------+---------------------
+ 3 | del | 1 | 100 | Removed (1,100)
+ 1 | upd | 2 | 220 | Added 20 to balance
+ 2 | ins | 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_number();
+ERROR: merge support function pg_merge_when_clause_number 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...
+ ^
+-- RETURNING in CTEs
+CREATE TABLE sq_target_merge_log (tid integer NOT NULL, last_change text);
+INSERT INTO sq_target_merge_log VALUES (1, 'Original value');
+BEGIN;
+WITH m AS (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 pg_merge_when_clause_number() AS clause_number,
+ 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
+), m2 AS (
+ MERGE INTO sq_target_merge_log l
+ USING m
+ ON l.tid = m.tid
+ WHEN MATCHED THEN
+ UPDATE SET last_change = description
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (m.tid, description)
+ RETURNING merge_action, pg_merge_action() AS merge_log_action, l.*
+)
+SELECT * FROM m2;
+ merge_action | merge_log_action | tid | last_change
+--------------+------------------+-----+---------------------
+ DELETE | UPDATE | 1 | Removed (1,100)
+ UPDATE | INSERT | 2 | Added 20 to balance
+ INSERT | INSERT | 4 | Inserted (4,40)
+(3 rows)
+
+SELECT * FROM sq_target_merge_log ORDER BY tid;
+ tid | last_change
+-----+---------------------
+ 1 | Removed (1,100)
+ 2 | Added 20 to balance
+ 4 | Inserted (4,40)
+(3 rows)
+
+ROLLBACK;
+-- COPY (MERGE ... RETURNING) TO ...
+BEGIN;
+COPY (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 pg_merge_action(), t.*
+) TO stdout;
+DELETE 1 100
+UPDATE 2 220
+INSERT 4 40
+ROLLBACK;
+-- SQL function with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT action text, OUT tid int, OUT new_balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING pg_merge_action(), t.*;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT action, tid, new_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ action | tid | new_balance
+--------+-----+-------------
+ DELETE | 1 | 100
+ UPDATE | 3 | 320
+ INSERT | 4 | 110
+(3 rows)
+
+ROLLBACK;
+-- SQL SRF with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_sq_source_into_sq_target()
+RETURNS TABLE (action text, tid int, balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 pg_merge_action(), t.*;
+$$;
+SELECT * FROM merge_sq_source_into_sq_target();
+ action | tid | balance
+--------+-----+---------
+ DELETE | 1 | 100
+ UPDATE | 2 | 220
+ INSERT | 4 | 40
+(3 rows)
+
+ROLLBACK;
+-- PL/pgSQL function with MERGE ... RETURNING ... INTO
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT r_action text, OUT r_tid int, OUT r_balance int)
+LANGUAGE plpgsql AS
+$$
+BEGIN
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING pg_merge_action(), t.* INTO r_action, r_tid, r_balance;
+END;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT r_action, r_tid, r_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ r_action | r_tid | r_balance
+----------+-------+-----------
+ DELETE | 1 | 100
+ UPDATE | 3 | 320
+ INSERT | 4 | 110
+(3 rows)
+
ROLLBACK;
-- EXPLAIN
CREATE TABLE ex_mtarget (a int, b int)
@@ -1514,7 +1687,7 @@ SELECT * FROM sq_target WHERE tid = 1;
(1 row)
ROLLBACK;
-DROP TABLE sq_target, sq_source CASCADE;
+DROP TABLE sq_target, sq_target_merge_log, sq_source CASCADE;
NOTICE: drop cascades to view v
CREATE TABLE pa_target (tid integer, balance float, val text)
PARTITION BY LIST (tid);
@@ -1640,6 +1813,32 @@ SELECT * FROM pa_target ORDER BY tid;
(14 rows)
ROLLBACK;
+-- update partition key to partition not initially scanned
+BEGIN;
+MERGE INTO pa_target t
+ USING pa_source s
+ ON t.tid = s.sid AND t.tid = 1
+ WHEN MATCHED THEN
+ UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
+ RETURNING pg_merge_action(), pg_merge_when_clause_number(), t.*;
+ pg_merge_action | pg_merge_when_clause_number | tid | balance | val
+-----------------+-----------------------------+-----+---------+--------------------------
+ UPDATE | 1 | 2 | 110 | initial updated by merge
+(1 row)
+
+SELECT * FROM pa_target ORDER BY tid;
+ tid | balance | val
+-----+---------+--------------------------
+ 2 | 110 | initial updated by merge
+ 3 | 300 | initial
+ 5 | 500 | initial
+ 7 | 700 | initial
+ 9 | 900 | initial
+ 11 | 1100 | initial
+ 13 | 1300 | initial
+(7 rows)
+
+ROLLBACK;
DROP TABLE pa_target CASCADE;
-- The target table is partitioned in the same way, but this time by attaching
-- partitions which have columns in different order, dropped columns etc.
@@ -1731,7 +1930,26 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (sid, delta, 'inserted by merge');
+ INSERT VALUES (sid, delta, 'inserted by merge')
+ RETURNING pg_merge_action(), pg_merge_when_clause_number(), t.*;
+ pg_merge_action | pg_merge_when_clause_number | tid | balance | val
+-----------------+-----------------------------+-----+---------+--------------------------
+ UPDATE | 1 | 2 | 110 | initial updated by merge
+ INSERT | 2 | 2 | 20 | inserted by merge
+ UPDATE | 1 | 4 | 330 | initial updated by merge
+ INSERT | 2 | 4 | 40 | inserted by merge
+ UPDATE | 1 | 6 | 550 | initial updated by merge
+ INSERT | 2 | 6 | 60 | inserted by merge
+ UPDATE | 1 | 8 | 770 | initial updated by merge
+ INSERT | 2 | 8 | 80 | inserted by merge
+ UPDATE | 1 | 10 | 990 | initial updated by merge
+ INSERT | 2 | 10 | 100 | inserted by merge
+ UPDATE | 1 | 12 | 1210 | initial updated by merge
+ INSERT | 2 | 12 | 120 | inserted by merge
+ UPDATE | 1 | 14 | 1430 | initial updated by merge
+ INSERT | 2 | 14 | 140 | inserted by merge
+(14 rows)
+
SELECT * FROM pa_target ORDER BY tid;
tid | balance | val
-----+---------+--------------------------
@@ -1798,7 +2016,21 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge');
+ INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge')
+ RETURNING pg_merge_action(), pg_merge_when_clause_number(), t.*;
+ pg_merge_action | pg_merge_when_clause_number | logts | tid | balance | val
+-----------------+-----------------------------+--------------------------+-----+---------+--------------------------
+ UPDATE | 1 | Tue Jan 31 00:00:00 2017 | 1 | 110 | initial updated by merge
+ UPDATE | 1 | Tue Feb 28 00:00:00 2017 | 2 | 220 | initial updated by merge
+ INSERT | 2 | Sun Jan 15 00:00:00 2017 | 3 | 30 | inserted by merge
+ UPDATE | 1 | Tue Jan 31 00:00:00 2017 | 4 | 440 | initial updated by merge
+ UPDATE | 1 | Tue Feb 28 00:00:00 2017 | 5 | 550 | initial updated by merge
+ INSERT | 2 | Sun Jan 15 00:00:00 2017 | 6 | 60 | inserted by merge
+ UPDATE | 1 | Tue Jan 31 00:00:00 2017 | 7 | 770 | initial updated by merge
+ UPDATE | 1 | Tue Feb 28 00:00:00 2017 | 8 | 880 | initial updated by merge
+ INSERT | 2 | Sun Jan 15 00:00:00 2017 | 9 | 90 | inserted by merge
+(9 rows)
+
SELECT * FROM pa_target ORDER BY tid;
logts | tid | balance | val
--------------------------+-----+---------+--------------------------
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
new file mode 100644
index 97ca9bf..55250b3
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -2300,6 +2300,35 @@ WHEN MATCHED THEN
UPDATE SET dnotes = dnotes || ' notes added by merge8 '
WHEN NOT MATCHED THEN
INSERT VALUES (13, 44, 1, 'regress_rls_bob', 'new manga');
+SELECT * FROM document WHERE did = 13;
+ did | cid | dlevel | dauthor | dtitle | dnotes
+-----+-----+--------+---------+--------+--------
+(0 rows)
+
+-- but not OK if RETURNING is used
+MERGE INTO document d
+USING (SELECT 14 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge9 '
+WHEN NOT MATCHED THEN
+ INSERT VALUES (14, 44, 1, 'regress_rls_bob', 'new manga')
+RETURNING *;
+ERROR: new row violates row-level security policy for table "document"
+-- but OK if new row is visible
+MERGE INTO document d
+USING (SELECT 14 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge10 '
+WHEN NOT MATCHED THEN
+ INSERT VALUES (14, 11, 1, 'regress_rls_bob', 'new novel')
+RETURNING *;
+ sdid | did | cid | dlevel | dauthor | dtitle | dnotes
+------+-----+-----+--------+-----------------+-----------+--------
+ 14 | 14 | 11 | 1 | regress_rls_bob | new novel |
+(1 row)
+
RESET SESSION AUTHORIZATION;
-- drop the restrictive SELECT policy so that we can look at the
-- final state of the table
@@ -2323,7 +2352,8 @@ SELECT * FROM document;
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 notes added by merge7
13 | 44 | 1 | regress_rls_bob | new manga |
-(15 rows)
+ 14 | 11 | 1 | regress_rls_bob | new novel |
+(16 rows)
--
-- ROLE/GROUP
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
new file mode 100644
index 5058be5..2adfad7
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -3596,7 +3596,8 @@ MERGE INTO rule_merge2 t USING (SELECT 1
-- test deparsing
CREATE TABLE sf_target(id int, data text, filling int[]);
CREATE FUNCTION merge_sf_test()
- RETURNS void
+ RETURNS TABLE(pg_merge_action text, when_clause int,
+ a int, b text, id int, data text, filling int[])
LANGUAGE sql
BEGIN ATOMIC
MERGE INTO sf_target t
@@ -3633,11 +3634,13 @@ WHEN NOT MATCHED
VALUES (s.a, s.b, DEFAULT)
WHEN NOT MATCHED
THEN INSERT (filling[1], id)
- VALUES (s.a, s.a);
+ VALUES (s.a, s.a)
+RETURNING
+ pg_merge_action(), pg_merge_when_clause_number() AS clause_number, *;
END;
\sf merge_sf_test
CREATE OR REPLACE FUNCTION public.merge_sf_test()
- RETURNS void
+ RETURNS TABLE(pg_merge_action text, when_clause integer, a integer, b text, id integer, data text, filling integer[])
LANGUAGE sql
BEGIN ATOMIC
MERGE INTO sf_target t
@@ -3674,7 +3677,14 @@ BEGIN ATOMIC
VALUES (s.a, s.b, DEFAULT)
WHEN NOT MATCHED
THEN INSERT (filling[1], id)
- VALUES (s.a, s.a);
+ VALUES (s.a, s.a)
+ RETURNING pg_merge_action() AS pg_merge_action,
+ pg_merge_when_clause_number() AS clause_number,
+ s.a,
+ s.b,
+ t.id,
+ t.data,
+ t.filling;
END
DROP FUNCTION merge_sf_test;
DROP TABLE sf_target;
diff --git a/src/test/regress/sql/merge.sql b/src/test/regress/sql/merge.sql
new file mode 100644
index 4cf6db9..8394d01
--- 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
@@ -816,7 +816,7 @@ BEGIN;
MERGE INTO sq_target
USING v
ON tid = sid
-WHEN MATCHED AND tid > 2 THEN
+WHEN MATCHED AND tid >= 2 THEN
UPDATE SET balance = balance + delta
WHEN NOT MATCHED THEN
INSERT (balance, tid) VALUES (balance + delta, sid)
@@ -829,7 +829,7 @@ INSERT INTO sq_source (sid, balance, del
MERGE INTO sq_target t
USING v
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)
@@ -847,7 +847,7 @@ WITH targq AS (
MERGE INTO sq_target t
USING v
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)
@@ -857,17 +857,148 @@ ROLLBACK;
-- RETURNING
BEGIN;
-INSERT INTO sq_source (sid, balance, delta) VALUES (-1, -1, -10);
+CREATE TABLE merge_actions(action text, abbrev text);
+INSERT INTO merge_actions VALUES ('INSERT', 'ins'), ('UPDATE', 'upd'), ('DELETE', 'del');
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)
+ INSERT (balance, tid) VALUES (balance + delta, sid)
WHEN MATCHED AND tid < 2 THEN
- DELETE
-RETURNING *;
+ DELETE
+RETURNING pg_merge_when_clause_number() AS clause_number,
+ (SELECT abbrev FROM merge_actions
+ WHERE action = 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_number();
+
+-- RETURNING in CTEs
+CREATE TABLE sq_target_merge_log (tid integer NOT NULL, last_change text);
+INSERT INTO sq_target_merge_log VALUES (1, 'Original value');
+BEGIN;
+WITH m AS (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 pg_merge_when_clause_number() AS clause_number,
+ 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
+), m2 AS (
+ MERGE INTO sq_target_merge_log l
+ USING m
+ ON l.tid = m.tid
+ WHEN MATCHED THEN
+ UPDATE SET last_change = description
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (m.tid, description)
+ RETURNING merge_action, pg_merge_action() AS merge_log_action, l.*
+)
+SELECT * FROM m2;
+SELECT * FROM sq_target_merge_log ORDER BY tid;
+ROLLBACK;
+
+-- COPY (MERGE ... RETURNING) TO ...
+BEGIN;
+COPY (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 pg_merge_action(), t.*
+) TO stdout;
+ROLLBACK;
+
+-- SQL function with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT action text, OUT tid int, OUT new_balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING pg_merge_action(), t.*;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT action, tid, new_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ROLLBACK;
+
+-- SQL SRF with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_sq_source_into_sq_target()
+RETURNS TABLE (action text, tid int, balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 pg_merge_action(), t.*;
+$$;
+SELECT * FROM merge_sq_source_into_sq_target();
+ROLLBACK;
+
+-- PL/pgSQL function with MERGE ... RETURNING ... INTO
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT r_action text, OUT r_tid int, OUT r_balance int)
+LANGUAGE plpgsql AS
+$$
+BEGIN
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING pg_merge_action(), t.* INTO r_action, r_tid, r_balance;
+END;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT r_action, r_tid, r_balance FROM merge_into_sq_target(sid, balance, delta)) m;
ROLLBACK;
-- EXPLAIN
@@ -966,7 +1097,7 @@ WHEN MATCHED THEN
SELECT * FROM sq_target WHERE tid = 1;
ROLLBACK;
-DROP TABLE sq_target, sq_source CASCADE;
+DROP TABLE sq_target, sq_target_merge_log, sq_source CASCADE;
CREATE TABLE pa_target (tid integer, balance float, val text)
PARTITION BY LIST (tid);
@@ -1033,6 +1164,17 @@ SELECT merge_func();
SELECT * FROM pa_target ORDER BY tid;
ROLLBACK;
+-- update partition key to partition not initially scanned
+BEGIN;
+MERGE INTO pa_target t
+ USING pa_source s
+ ON t.tid = s.sid AND t.tid = 1
+ WHEN MATCHED THEN
+ UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
+ RETURNING pg_merge_action(), pg_merge_when_clause_number(), t.*;
+SELECT * FROM pa_target ORDER BY tid;
+ROLLBACK;
+
DROP TABLE pa_target CASCADE;
-- The target table is partitioned in the same way, but this time by attaching
@@ -1091,7 +1233,8 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (sid, delta, 'inserted by merge');
+ INSERT VALUES (sid, delta, 'inserted by merge')
+ RETURNING pg_merge_action(), pg_merge_when_clause_number(), t.*;
SELECT * FROM pa_target ORDER BY tid;
ROLLBACK;
@@ -1145,7 +1288,8 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge');
+ INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge')
+ RETURNING pg_merge_action(), pg_merge_when_clause_number(), t.*;
SELECT * FROM pa_target ORDER BY tid;
ROLLBACK;
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
new file mode 100644
index dec7340..1d5ed0a
--- a/src/test/regress/sql/rowsecurity.sql
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -992,6 +992,27 @@ WHEN MATCHED THEN
UPDATE SET dnotes = dnotes || ' notes added by merge8 '
WHEN NOT MATCHED THEN
INSERT VALUES (13, 44, 1, 'regress_rls_bob', 'new manga');
+SELECT * FROM document WHERE did = 13;
+
+-- but not OK if RETURNING is used
+MERGE INTO document d
+USING (SELECT 14 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge9 '
+WHEN NOT MATCHED THEN
+ INSERT VALUES (14, 44, 1, 'regress_rls_bob', 'new manga')
+RETURNING *;
+
+-- but OK if new row is visible
+MERGE INTO document d
+USING (SELECT 14 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge10 '
+WHEN NOT MATCHED THEN
+ INSERT VALUES (14, 11, 1, 'regress_rls_bob', 'new novel')
+RETURNING *;
RESET SESSION AUTHORIZATION;
-- drop the restrictive SELECT policy so that we can look at the
diff --git a/src/test/regress/sql/rules.sql b/src/test/regress/sql/rules.sql
new file mode 100644
index 8b7e255..5dbeb6b
--- a/src/test/regress/sql/rules.sql
+++ b/src/test/regress/sql/rules.sql
@@ -1281,7 +1281,8 @@ MERGE INTO rule_merge2 t USING (SELECT 1
CREATE TABLE sf_target(id int, data text, filling int[]);
CREATE FUNCTION merge_sf_test()
- RETURNS void
+ RETURNS TABLE(pg_merge_action text, when_clause int,
+ a int, b text, id int, data text, filling int[])
LANGUAGE sql
BEGIN ATOMIC
MERGE INTO sf_target t
@@ -1318,7 +1319,9 @@ WHEN NOT MATCHED
VALUES (s.a, s.b, DEFAULT)
WHEN NOT MATCHED
THEN INSERT (filling[1], id)
- VALUES (s.a, s.a);
+ VALUES (s.a, s.a)
+RETURNING
+ pg_merge_action(), pg_merge_when_clause_number() AS clause_number, *;
END;
\sf merge_sf_test
Updated version attached, fixing an uninitialized-variable warning
from the cfbot.
Regards,
Dean
Attachments:
support-merge-returning-v11.patchtext/x-patch; charset=US-ASCII; name=support-merge-returning-v11.patchDownload
diff --git a/doc/src/sgml/dml.sgml b/doc/src/sgml/dml.sgml
new file mode 100644
index cbbc5e2..7f65f6e
--- a/doc/src/sgml/dml.sgml
+++ b/doc/src/sgml/dml.sgml
@@ -283,10 +283,15 @@ DELETE FROM products;
<secondary>RETURNING</secondary>
</indexterm>
+ <indexterm zone="dml-returning">
+ <primary>MERGE</primary>
+ <secondary>RETURNING</secondary>
+ </indexterm>
+
<para>
Sometimes it is useful to obtain data from modified rows while they are
being manipulated. The <command>INSERT</command>, <command>UPDATE</command>,
- and <command>DELETE</command> commands all have an
+ <command>DELETE</command>, and <command>MERGE</command> commands all have an
optional <literal>RETURNING</literal> clause that supports this. Use
of <literal>RETURNING</literal> avoids performing an extra database query to
collect the data, and is especially valuable when it would otherwise be
@@ -339,6 +344,24 @@ DELETE FROM products
</programlisting>
</para>
+ <para>
+ In a <command>MERGE</command>, the data available to <literal>RETURNING</literal> is
+ the content of the source row plus the content of the inserted, updated, or
+ deleted target row. In addition, the merge support functions
+ <xref linkend="pg-merge-action"/> and <xref linkend="pg-merge-when-clause-number"/>
+ may be used to return information about which merge action was executed for
+ each row. Since it is quite common for the source and target to have many
+ of the same columns, specifying <literal>RETURNING *</literal> can lead to a
+ lot of duplicated columns, so it is often more useful to just return the
+ target row. For example:
+<programlisting>
+MERGE INTO products p USING new_products n ON p.product_no = n.product_no
+ WHEN NOT MATCHED THEN INSERT VALUES (n.product_no, n.name, n.price)
+ WHEN MATCHED THEN UPDATE SET name = n.name, price = n.price
+ RETURNING pg_merge_action(), p.*;
+</programlisting>
+ </para>
+
<para>
If there are triggers (<xref linkend="triggers"/>) on the target table,
the data available to <literal>RETURNING</literal> is the row as modified by
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
new file mode 100644
index be2f54c..e79e2ad
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -21772,6 +21772,100 @@ SELECT count(*) FROM sometable;
</sect1>
+ <sect1 id="functions-merge-support">
+ <title>Merge Support Functions</title>
+
+ <indexterm>
+ <primary>MERGE</primary>
+ <secondary>RETURNING</secondary>
+ </indexterm>
+
+ <para>
+ The merge support functions shown in
+ <xref linkend="functions-merge-support-table"/> may be used in the
+ <literal>RETURNING</literal> list of a <xref linkend="sql-merge"/> command
+ to return additional information about the action taken for each row.
+ </para>
+
+ <table id="functions-merge-support-table">
+ <title>Merge Support Functions</title>
+
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ Function
+ </para>
+ <para>
+ Description
+ </para></entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm id="pg-merge-action">
+ <primary>pg_merge_action</primary>
+ </indexterm>
+ <function>pg_merge_action</function> ( )
+ <returnvalue>text</returnvalue>
+ </para>
+ <para>
+ Returns the action performed on the current row (<literal>'INSERT'</literal>,
+ <literal>'UPDATE'</literal>, or <literal>'DELETE'</literal>).
+ </para></entry>
+ </row>
+
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm id="pg-merge-when-clause-number">
+ <primary>pg_merge_when_clause_number</primary>
+ </indexterm>
+ <function>pg_merge_when_clause_number</function> ( )
+ <returnvalue>integer</returnvalue>
+ </para>
+ <para>
+ Returns the 1-based index of the <literal>WHEN</literal> clause executed
+ for the current row.
+ </para></entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+
+ <para>
+ Example:
+<screen><![CDATA[
+MERGE INTO products p
+ USING stock s ON p.product_id = s.product_id
+ WHEN MATCHED AND s.quantity > 0 THEN
+ UPDATE SET in_stock = true, quantity = s.quantity
+ WHEN MATCHED THEN
+ UPDATE SET in_stock = false, quantity = 0
+ WHEN NOT MATCHED THEN
+ INSERT (product_id, in_stock, quantity)
+ VALUES (s.product_id, true, s.quantity)
+ RETURNING pg_merge_action() AS action,
+ pg_merge_when_clause_number() AS clause_number,
+ p.*;
+
+ action | clause_number | product_id | in_stock | quantity
+--------+---------------+------------+----------+----------
+ UPDATE | 1 | 1001 | t | 50
+ UPDATE | 2 | 1002 | f | 0
+ INSERT | 3 | 1003 | t | 10
+]]></screen>
+ </para>
+
+ <para>
+ Note that these functions can only be used in the <literal>RETURNING</literal>
+ list of a <command>MERGE</command> command. It is an error to use them in
+ any other part of a query.
+ </para>
+
+ </sect1>
+
<sect1 id="functions-subquery">
<title>Subquery Expressions</title>
diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml
new file mode 100644
index fe8def4..51a15ca
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -1387,9 +1387,9 @@
to a <glossterm linkend="glossary-client">client</glossterm> upon the
completion of an <acronym>SQL</acronym> command, usually a
<command>SELECT</command> but it can be an
- <command>INSERT</command>, <command>UPDATE</command>, or
- <command>DELETE</command> command if the <literal>RETURNING</literal>
- clause is specified.
+ <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command> command if the
+ <literal>RETURNING</literal> clause is specified.
</para>
<para>
The fact that a result set is a relation means that a query can be used
diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml
new file mode 100644
index f55e901..6803240
--- a/doc/src/sgml/plpgsql.sgml
+++ b/doc/src/sgml/plpgsql.sgml
@@ -1020,8 +1020,8 @@ INSERT INTO mytable VALUES (1,'one'), (2
</para>
<para>
- If the command does return rows (for example <command>SELECT</command>,
- or <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>
+ If the command does return rows (for example <command>SELECT</command>, or
+ <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command>
with <literal>RETURNING</literal>), there are two ways to proceed.
When the command will return at most one row, or you only care about
the first row of output, write the command as usual but add
@@ -1149,6 +1149,7 @@ SELECT <replaceable>select_expressions</
INSERT ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
UPDATE ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
DELETE ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
+MERGE ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
</synopsis>
where <replaceable>target</replaceable> can be a record variable, a row
@@ -1159,8 +1160,8 @@ DELETE ... RETURNING <replaceable>expres
<literal>INTO</literal> clause) just as described above,
and the plan is cached in the same way.
This works for <command>SELECT</command>,
- <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command> with
- <literal>RETURNING</literal>, and certain utility commands
+ <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command>
+ with <literal>RETURNING</literal>, and certain utility commands
that return row sets, such as <command>EXPLAIN</command>.
Except for the <literal>INTO</literal> clause, the SQL command is the same
as it would be written outside <application>PL/pgSQL</application>.
@@ -1236,7 +1237,7 @@ END;
</para>
<para>
- For <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command> with
+ For <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command> with
<literal>RETURNING</literal>, <application>PL/pgSQL</application> reports
an error for more than one returned row, even when
<literal>STRICT</literal> is not specified. This is because there
diff --git a/doc/src/sgml/queries.sgml b/doc/src/sgml/queries.sgml
new file mode 100644
index 3f95849..9928373
--- a/doc/src/sgml/queries.sgml
+++ b/doc/src/sgml/queries.sgml
@@ -2062,9 +2062,10 @@ SELECT <replaceable>select_list</replace
Table Expressions or <acronym>CTE</acronym>s, can be thought of as defining
temporary tables that exist just for one query. Each auxiliary statement
in a <literal>WITH</literal> clause can be a <command>SELECT</command>,
- <command>INSERT</command>, <command>UPDATE</command>, or <command>DELETE</command>; and the
+ <command>INSERT</command>, <command>UPDATE</command>, <command>DELETE</command>,
+ or <command>MERGE</command>; and the
<literal>WITH</literal> clause itself is attached to a primary statement that can
- be a <command>SELECT</command>, <command>INSERT</command>, <command>UPDATE</command>,
+ also be a <command>SELECT</command>, <command>INSERT</command>, <command>UPDATE</command>,
<command>DELETE</command>, or <command>MERGE</command>.
</para>
@@ -2598,8 +2599,8 @@ SELECT * FROM w AS w1 JOIN w AS w2 ON w1
<title>Data-Modifying Statements in <literal>WITH</literal></title>
<para>
- You can use most data-modifying statements (<command>INSERT</command>,
- <command>UPDATE</command>, or <command>DELETE</command>, but not
+ You can use data-modifying statements (<command>INSERT</command>,
+ <command>UPDATE</command>, <command>DELETE</command>, or
<command>MERGE</command>) in <literal>WITH</literal>. This
allows you to perform several different operations in the same query.
An example is:
diff --git a/doc/src/sgml/ref/copy.sgml b/doc/src/sgml/ref/copy.sgml
new file mode 100644
index 4d614a0..efaacb9
--- a/doc/src/sgml/ref/copy.sgml
+++ b/doc/src/sgml/ref/copy.sgml
@@ -122,13 +122,16 @@ COPY { <replaceable class="parameter">ta
A <link linkend="sql-select"><command>SELECT</command></link>,
<link linkend="sql-values"><command>VALUES</command></link>,
<link linkend="sql-insert"><command>INSERT</command></link>,
- <link linkend="sql-update"><command>UPDATE</command></link>, or
- <link linkend="sql-delete"><command>DELETE</command></link> command whose results are to be
- copied. Note that parentheses are required around the query.
+ <link linkend="sql-update"><command>UPDATE</command></link>,
+ <link linkend="sql-delete"><command>DELETE</command></link>, or
+ <link linkend="sql-merge"><command>MERGE</command></link> command
+ whose results are to be copied. Note that parentheses are required
+ around the query.
</para>
<para>
- For <command>INSERT</command>, <command>UPDATE</command> and
- <command>DELETE</command> queries a RETURNING clause must be provided,
+ For <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, and <command>MERGE</command> queries a
+ RETURNING clause must be provided,
and the target relation must not have a conditional rule, nor
an <literal>ALSO</literal> rule, nor an <literal>INSTEAD</literal> rule
that expands to multiple statements.
diff --git a/doc/src/sgml/ref/merge.sgml b/doc/src/sgml/ref/merge.sgml
new file mode 100644
index 0995fe0..d1aa937
--- a/doc/src/sgml/ref/merge.sgml
+++ b/doc/src/sgml/ref/merge.sgml
@@ -25,6 +25,7 @@ PostgreSQL documentation
MERGE INTO [ ONLY ] <replaceable class="parameter">target_table_name</replaceable> [ * ] [ [ AS ] <replaceable class="parameter">target_alias</replaceable> ]
USING <replaceable class="parameter">data_source</replaceable> ON <replaceable class="parameter">join_condition</replaceable>
<replaceable class="parameter">when_clause</replaceable> [...]
+[ RETURNING * | <replaceable class="parameter">output_expression</replaceable> [ [ AS ] <replaceable class="parameter">output_name</replaceable> ] [, ...] ]
<phrase>where <replaceable class="parameter">data_source</replaceable> is:</phrase>
@@ -95,6 +96,19 @@ DELETE
</para>
<para>
+ The optional <literal>RETURNING</literal> clause causes <command>MERGE</command>
+ to compute and return value(s) based on each row inserted, updated, or
+ deleted. Any expression using the source or target table's columns, or the
+ merge support functions <xref linkend="pg-merge-action"/> and
+ <xref linkend="pg-merge-when-clause-number"/> can be computed. When an
+ <command>INSERT</command> or <command>UPDATE</command> action is performed,
+ the new values of the target table's columns are used. When a
+ <command>DELETE</command> is performed, the old values of the target table's
+ columns are used. The syntax of the <literal>RETURNING</literal> list is
+ identical to that of the output list of <command>SELECT</command>.
+ </para>
+
+ <para>
There is no separate <literal>MERGE</literal> privilege.
If you specify an update action, you must have the
<literal>UPDATE</literal> privilege on the column(s)
@@ -125,6 +139,18 @@ DELETE
<variablelist>
<varlistentry>
+ <term><replaceable class="parameter">with_query</replaceable></term>
+ <listitem>
+ <para>
+ The <literal>WITH</literal> clause allows you to specify one or more
+ subqueries that can be referenced by name in the <command>MERGE</command>
+ query. See <xref linkend="queries-with"/> and <xref linkend="sql-select"/>
+ for details.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
<term><replaceable class="parameter">target_table_name</replaceable></term>
<listitem>
<para>
@@ -399,13 +425,31 @@ DELETE
</varlistentry>
<varlistentry>
- <term><replaceable class="parameter">with_query</replaceable></term>
+ <term><replaceable class="parameter">output_expression</replaceable></term>
<listitem>
<para>
- The <literal>WITH</literal> clause allows you to specify one or more
- subqueries that can be referenced by name in the <command>MERGE</command>
- query. See <xref linkend="queries-with"/> and <xref linkend="sql-select"/>
- for details.
+ An expression to be computed and returned by the <command>MERGE</command>
+ command after each row is merged. The expression can use any columns of
+ the source or target tables, or the merge support functions
+ <xref linkend="pg-merge-action"/> and
+ <xref linkend="pg-merge-when-clause-number"/>.
+ </para>
+ <para>
+ Writing <literal>*</literal> will return all columns from the source
+ table, followed by all columns from the target table. Often this will
+ lead to a lot of duplication, since it is common for the source and
+ target tables to have a lot of the same columns. This can be avoided by
+ qualifying the <literal>*</literal> with the name or alias of the source
+ or target table.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><replaceable class="parameter">output_name</replaceable></term>
+ <listitem>
+ <para>
+ A name to use for a returned column.
</para>
</listitem>
</varlistentry>
@@ -428,6 +472,13 @@ MERGE <replaceable class="parameter">tot
were changed in any way.
</para>
+ <para>
+ If the <command>MERGE</command> command contains a <literal>RETURNING</literal>
+ clause, the result will be similar to that of a <command>SELECT</command>
+ statement containing the columns and values defined in the
+ <literal>RETURNING</literal> list, computed over the row(s) inserted, updated,
+ or deleted by the command.
+ </para>
</refsect1>
<refsect1>
@@ -544,13 +595,6 @@ MERGE <replaceable class="parameter">tot
</para>
<para>
- There is no <literal>RETURNING</literal> clause with
- <command>MERGE</command>. Actions of <command>INSERT</command>,
- <command>UPDATE</command> and <command>DELETE</command> cannot contain
- <literal>RETURNING</literal> or <literal>WITH</literal> clauses.
- </para>
-
- <para>
When <command>MERGE</command> is run concurrently with other commands
that modify the target table, the usual transaction isolation rules
apply; see <xref linkend="transaction-iso"/> for an explanation
@@ -602,7 +646,8 @@ WHEN NOT MATCHED THEN
<para>
Attempt to insert a new stock item along with the quantity of stock. If
the item already exists, instead update the stock count of the existing
- item. Don't allow entries that have zero stock.
+ item. Don't allow entries that have zero stock. Return details of all
+ changes made.
<programlisting>
MERGE INTO wines w
USING wine_stock_changes s
@@ -612,7 +657,8 @@ WHEN NOT MATCHED AND s.stock_delta > 0 T
WHEN MATCHED AND w.stock + s.stock_delta > 0 THEN
UPDATE SET stock = w.stock + s.stock_delta
WHEN MATCHED THEN
- DELETE;
+ DELETE
+RETURNING pg_merge_action(), w.*;
</programlisting>
The <literal>wine_stock_changes</literal> table might be, for example, a
@@ -627,7 +673,8 @@ WHEN MATCHED THEN
This command conforms to the <acronym>SQL</acronym> standard.
</para>
<para>
- The WITH clause and <literal>DO NOTHING</literal> action are extensions to
+ The WITH clause, <literal>DO NOTHING</literal> action, and
+ <literal>RETURNING</literal> clause are extensions to
the <acronym>SQL</acronym> standard.
</para>
</refsect1>
diff --git a/doc/src/sgml/ref/select.sgml b/doc/src/sgml/ref/select.sgml
new file mode 100644
index 0ee0cc7..f81bdea
--- a/doc/src/sgml/ref/select.sgml
+++ b/doc/src/sgml/ref/select.sgml
@@ -74,7 +74,7 @@ SELECT [ ALL | DISTINCT [ ON ( <replacea
<phrase>and <replaceable class="parameter">with_query</replaceable> is:</phrase>
- <replaceable class="parameter">with_query_name</replaceable> [ ( <replaceable class="parameter">column_name</replaceable> [, ...] ) ] AS [ [ NOT ] MATERIALIZED ] ( <replaceable class="parameter">select</replaceable> | <replaceable class="parameter">values</replaceable> | <replaceable class="parameter">insert</replaceable> | <replaceable class="parameter">update</replaceable> | <replaceable class="parameter">delete</replaceable> )
+ <replaceable class="parameter">with_query_name</replaceable> [ ( <replaceable class="parameter">column_name</replaceable> [, ...] ) ] AS [ [ NOT ] MATERIALIZED ] ( <replaceable class="parameter">select</replaceable> | <replaceable class="parameter">values</replaceable> | <replaceable class="parameter">insert</replaceable> | <replaceable class="parameter">update</replaceable> | <replaceable class="parameter">delete</replaceable> | <replaceable class="parameter">merge</replaceable> )
[ SEARCH { BREADTH | DEPTH } FIRST BY <replaceable>column_name</replaceable> [, ...] SET <replaceable>search_seq_col_name</replaceable> ]
[ CYCLE <replaceable>column_name</replaceable> [, ...] SET <replaceable>cycle_mark_col_name</replaceable> [ TO <replaceable>cycle_mark_value</replaceable> DEFAULT <replaceable>cycle_mark_default</replaceable> ] USING <replaceable>cycle_path_col_name</replaceable> ]
@@ -227,10 +227,10 @@ TABLE [ ONLY ] <replaceable class="param
The subqueries effectively act as temporary tables or views
for the duration of the primary query.
Each subquery can be a <command>SELECT</command>, <command>TABLE</command>, <command>VALUES</command>,
- <command>INSERT</command>, <command>UPDATE</command> or
- <command>DELETE</command> statement.
+ <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command> statement.
When writing a data-modifying statement (<command>INSERT</command>,
- <command>UPDATE</command> or <command>DELETE</command>) in
+ <command>UPDATE</command>, <command>DELETE</command>, or <command>MERGE</command>) in
<literal>WITH</literal>, it is usual to include a <literal>RETURNING</literal> clause.
It is the output of <literal>RETURNING</literal>, <emphasis>not</emphasis> the underlying
table that the statement modifies, that forms the temporary table that is
@@ -2179,7 +2179,8 @@ SELECT 2+2;
<para>
<productname>PostgreSQL</productname> allows <command>INSERT</command>,
- <command>UPDATE</command>, and <command>DELETE</command> to be used as <literal>WITH</literal>
+ <command>UPDATE</command>, <command>DELETE</command>, and
+ <command>MERGE</command> to be used as <literal>WITH</literal>
queries. This is not found in the SQL standard.
</para>
</refsect2>
diff --git a/doc/src/sgml/rowtypes.sgml b/doc/src/sgml/rowtypes.sgml
new file mode 100644
index 4d86f97..bbeac84
--- a/doc/src/sgml/rowtypes.sgml
+++ b/doc/src/sgml/rowtypes.sgml
@@ -348,7 +348,7 @@ SELECT m.* FROM some_table, LATERAL myfu
column expansion of this kind when it appears at the top level of
a <link linkend="queries-select-lists"><command>SELECT</command> output
list</link>, a <link linkend="dml-returning"><literal>RETURNING</literal>
- list</link> in <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>,
+ list</link> in <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command>,
a <link linkend="queries-values"><literal>VALUES</literal> clause</link>, or
a <link linkend="sql-syntax-row-constructors">row constructor</link>.
In all other contexts (including when nested inside one of those
diff --git a/doc/src/sgml/spi.sgml b/doc/src/sgml/spi.sgml
new file mode 100644
index 16c3ab7..6410862
--- a/doc/src/sgml/spi.sgml
+++ b/doc/src/sgml/spi.sgml
@@ -301,8 +301,9 @@ SPI_execute("INSERT INTO foo SELECT * FR
is returned in the global variable <varname>SPI_processed</varname>.
If the return value of the function is <symbol>SPI_OK_SELECT</symbol>,
<symbol>SPI_OK_INSERT_RETURNING</symbol>,
- <symbol>SPI_OK_DELETE_RETURNING</symbol>, or
- <symbol>SPI_OK_UPDATE_RETURNING</symbol>,
+ <symbol>SPI_OK_DELETE_RETURNING</symbol>,
+ <symbol>SPI_OK_UPDATE_RETURNING</symbol>, or
+ <symbol>SPI_OK_MERGE_RETURNING</symbol>,
then you can use the
global pointer <literal>SPITupleTable *SPI_tuptable</literal> to
access the result rows. Some utility commands (such as
@@ -472,6 +473,15 @@ typedef struct SPITupleTable
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><symbol>SPI_OK_MERGE_RETURNING</symbol></term>
+ <listitem>
+ <para>
+ if a <command>MERGE RETURNING</command> was executed
+ </para>
+ </listitem>
+ </varlistentry>
<varlistentry>
<term><symbol>SPI_OK_UTILITY</symbol></term>
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
new file mode 100644
index 281c178..3aec197
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -177,7 +177,8 @@
statements separated by semicolons. A semicolon after the last
statement is optional. Unless the function is declared to return
<type>void</type>, the last statement must be a <command>SELECT</command>,
- or an <command>INSERT</command>, <command>UPDATE</command>, or <command>DELETE</command>
+ or an <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command>
that has a <literal>RETURNING</literal> clause.
</para>
@@ -1236,8 +1237,9 @@ SELECT x, CASE WHEN x > 0 THEN genera
<note>
<para>
- If a function's last command is <command>INSERT</command>, <command>UPDATE</command>,
- or <command>DELETE</command> with <literal>RETURNING</literal>, that command will
+ If a function's last command is <command>INSERT</command>,
+ <command>UPDATE</command>, <command>DELETE</command>, or
+ <command>MERGE</command> with <literal>RETURNING</literal>, that command will
always be executed to completion, even if the function is not declared
with <literal>SETOF</literal> or the calling query does not fetch all the
result rows. Any extra rows produced by the <literal>RETURNING</literal>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
new file mode 100644
index f14fae3..05f2e5b
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -283,12 +283,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 eaa3172..a50cd30
--- 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 2c62b0c..f09d512
--- 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/jsonfuncs.h"
#include "utils/lsyscache.h"
#include "utils/typcache.h"
@@ -2620,6 +2621,22 @@ ExecInitFunc(ExprEvalStep *scratch, Expr
InitFunctionCallInfoData(*fcinfo, flinfo,
nargs, inputcollid, NULL, NULL);
+ /*
+ * Merge support functions should only be called 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 eb8a87f..b9bd03c
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -612,8 +612,8 @@ ExecInitPartitionInfo(ModifyTableState *
* Build the RETURNING projection for the partition. Note that we didn't
* build the returningList for partitions within the planner, but simple
* translation of varattnos will suffice. This only occurs for the INSERT
- * case or in the case of UPDATE tuple routing where we didn't find a
- * result rel to reuse.
+ * case or in the case of UPDATE/MERGE tuple routing where we didn't find
+ * a result rel to reuse.
*/
if (node && node->returningLists != NIL)
{
@@ -622,12 +622,14 @@ ExecInitPartitionInfo(ModifyTableState *
List *returningList;
/* See the comment above for WCO lists. */
- /* (except no RETURNING support for MERGE yet) */
Assert((node->operation == CMD_INSERT &&
list_length(node->returningLists) == 1 &&
list_length(node->resultRelations) == 1) ||
(node->operation == CMD_UPDATE &&
list_length(node->returningLists) ==
+ list_length(node->resultRelations)) ||
+ (node->operation == CMD_MERGE &&
+ list_length(node->returningLists) ==
list_length(node->resultRelations)));
/*
diff --git a/src/backend/executor/functions.c b/src/backend/executor/functions.c
new file mode 100644
index f55424e..97874a4
--- a/src/backend/executor/functions.c
+++ b/src/backend/executor/functions.c
@@ -1660,8 +1660,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
@@ -1679,7 +1679,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;
@@ -1693,7 +1694,7 @@ check_sql_fn_retval(List *queryTreeLists
(errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
errmsg("return type mismatch in function declared to return %s",
format_type_be(rettype)),
- errdetail("Function's final statement must be SELECT or INSERT/UPDATE/DELETE RETURNING.")));
+ errdetail("Function's final statement must be SELECT or INSERT/UPDATE/DELETE/MERGE RETURNING.")));
return false; /* keep compiler quiet */
}
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
new file mode 100644
index 5005d8c..63a8039
--- 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"
@@ -88,9 +87,6 @@ typedef struct ModifyTableContext
*/
TupleTableSlot *planSlot;
- /* MERGE specific */
- MergeActionState *relaction; /* MERGE action in progress */
-
/*
* Information about the changes that were made concurrently to a tuple
* being updated or deleted
@@ -153,13 +149,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);
/*
@@ -979,7 +976,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;
@@ -1828,7 +1825,7 @@ ExecCrossPartitionUpdate(ModifyTableCont
* additional rechecking, and might end up executing a different
* action entirely).
*/
- if (context->relaction != NULL)
+ if (mtstate->mt_merge_action != NULL)
return false;
else if (TupIsNull(epqslot))
return true;
@@ -2069,7 +2066,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;
/*
@@ -2712,6 +2709,7 @@ static TupleTableSlot *
ExecMerge(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
ItemPointer tupleid, bool canSetTag)
{
+ TupleTableSlot *rslot = NULL;
bool matched;
/*-----
@@ -2759,18 +2757,18 @@ 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()
- * returned "false", indicating the previously MATCHED tuple no longer
- * matches.
+ * Deal with the NOT MATCHED case (either a NOT MATCHED tuple from the
+ * join, or a previously MATCHED tuple for which ExecMergeMatched() set
+ * "matched" to false, indicating that it no longer matches).
*/
if (!matched)
- ExecMergeNotMatched(context, resultRelInfo, canSetTag);
+ rslot = ExecMergeNotMatched(context, resultRelInfo, canSetTag);
- /* No RETURNING support yet */
- return NULL;
+ return rslot;
}
/*
@@ -2780,8 +2778,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.
*
@@ -2790,16 +2788,17 @@ 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 *newslot;
+ TupleTableSlot *rslot = NULL;
+ TupleTableSlot *newslot = NULL;
EState *estate = context->estate;
ExprContext *econtext = mtstate->ps.ps_ExprContext;
bool isNull;
@@ -2810,7 +2809,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
@@ -2889,12 +2891,15 @@ lmerge_matched:
*/
newslot = ExecProject(relaction->mas_proj);
- context->relaction = relaction;
+ mtstate->mt_merge_action = relaction;
if (!ExecUpdatePrologue(context, resultRelInfo,
tupleid, NULL, newslot, &result))
{
if (result == TM_Ok)
- return true; /* "do nothing" */
+ {
+ *matched = true;
+ return NULL; /* "do nothing" */
+ }
break; /* concurrent update/delete */
}
result = ExecUpdateAct(context, resultRelInfo, tupleid, NULL,
@@ -2908,12 +2913,15 @@ lmerge_matched:
break;
case CMD_DELETE:
- context->relaction = relaction;
+ mtstate->mt_merge_action = relaction;
if (!ExecDeletePrologue(context, resultRelInfo, tupleid,
NULL, NULL, &result))
{
if (result == TM_Ok)
- return true; /* "do nothing" */
+ {
+ *matched = true;
+ return NULL; /* "do nothing" */
+ }
break; /* concurrent update/delete */
}
result = ExecDeleteAct(context, resultRelInfo, tupleid, false);
@@ -2969,7 +2977,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:
{
@@ -3015,13 +3024,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
@@ -3056,7 +3071,8 @@ lmerge_matched:
* tuple already deleted; tell caller to run NOT
* MATCHED actions
*/
- return false;
+ *matched = false;
+ return NULL;
case TM_SelfModified:
@@ -3076,13 +3092,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;
}
}
@@ -3094,6 +3111,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.
@@ -3104,19 +3145,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;
/*
@@ -3166,10 +3210,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:
@@ -3185,6 +3229,8 @@ ExecMergeNotMatched(ModifyTableContext *
*/
break;
}
+
+ return rslot;
}
/*
@@ -3362,6 +3408,65 @@ ExecInitMergeTupleSlots(ModifyTableState
}
/*
+ * 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;
+ CmdType commandType;
+
+ if (!mtstate || mtstate->operation != CMD_MERGE)
+ elog(ERROR, "merge support function called in non-merge context");
+
+ relaction = mtstate->mt_merge_action;
+ if (!relaction)
+ elog(ERROR, "no merge action in progress");
+
+ 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:
+ elog(ERROR, "unexpected merge action: DO NOTHING");
+ break;
+ default:
+ elog(ERROR, "unrecognized commandType: %d", (int) commandType);
+ }
+
+ PG_RETURN_NULL(); /* keep compiler quiet */
+}
+
+/*
+ * pg_merge_when_clause_number() -
+ * SQL merge support function to retrieve the 1-based index of the
+ * currently executing merge WHEN clause.
+ */
+Datum
+pg_merge_when_clause_number(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)
+ elog(ERROR, "no merge action in progress");
+
+ PG_RETURN_INT32((int32) relaction->mas_action->index);
+}
+
+/*
* Process BEFORE EACH STATEMENT triggers
*/
static void
@@ -3647,8 +3752,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");
@@ -3725,8 +3839,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");
@@ -3818,7 +3941,7 @@ ExecModifyTable(PlanState *pstate)
}
slot = ExecGetUpdateNewTuple(resultRelInfo, context.planSlot,
oldSlot);
- context.relaction = NULL;
+ node->mt_merge_action = NULL;
/* Now apply the update. */
slot = ExecUpdate(&context, resultRelInfo, tupleid, oldtuple,
diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c
new file mode 100644
index 3397568..b9f443c
--- a/src/backend/executor/spi.c
+++ b/src/backend/executor/spi.c
@@ -2033,6 +2033,8 @@ SPI_result_code_string(int code)
return "SPI_OK_TD_REGISTER";
case SPI_OK_MERGE:
return "SPI_OK_MERGE";
+ case SPI_OK_MERGE_RETURNING:
+ return "SPI_OK_MERGE_RETURNING";
}
/* Unrecognized code ... return something useful ... */
sprintf(buf, "Unrecognized SPI code %d", code);
@@ -2886,7 +2888,10 @@ _SPI_pquery(QueryDesc *queryDesc, bool f
res = SPI_OK_UPDATE;
break;
case CMD_MERGE:
- res = SPI_OK_MERGE;
+ if (queryDesc->plannedstmt->hasReturning)
+ res = SPI_OK_MERGE_RETURNING;
+ else
+ res = SPI_OK_MERGE;
break;
default:
return SPI_ERROR_OPUNKNOWN;
diff --git a/src/backend/optimizer/plan/subselect.c b/src/backend/optimizer/plan/subselect.c
new file mode 100644
index 7a9fe88..d3ee1a2
--- a/src/backend/optimizer/plan/subselect.c
+++ b/src/backend/optimizer/plan/subselect.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "catalog/pg_operator.h"
+#include "catalog/pg_proc.h"
#include "catalog/pg_type.h"
#include "executor/executor.h"
#include "miscadmin.h"
@@ -35,6 +36,7 @@
#include "parser/parse_relation.h"
#include "rewrite/rewriteManip.h"
#include "utils/builtins.h"
+#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/syscache.h"
@@ -1846,7 +1848,8 @@ convert_EXISTS_to_ANY(PlannerInfo *root,
/*
* Replace correlation vars (uplevel vars) with Params.
*
- * Uplevel PlaceHolderVars and aggregates are replaced, too.
+ * Uplevel PlaceHolderVars, aggregates, GROUPING() expressions, and merge
+ * support functions are replaced, too.
*
* Note: it is critical that this runs immediately after SS_process_sublinks.
* Since we do not recurse into the arguments of uplevel PHVs and aggregates,
@@ -1900,6 +1903,13 @@ replace_correlation_vars_mutator(Node *n
if (((GroupingFunc *) node)->agglevelsup > 0)
return (Node *) replace_outer_grouping(root, (GroupingFunc *) node);
}
+ if (IsA(node, FuncExpr) &&
+ IsMergeSupportFunction(((FuncExpr *) node)->funcid))
+ {
+ if (root->parse->commandType != CMD_MERGE)
+ return (Node *) replace_outer_merge_support_function(root,
+ (FuncExpr *) node);
+ }
return expression_tree_mutator(node,
replace_correlation_vars_mutator,
(void *) root);
diff --git a/src/backend/optimizer/util/paramassign.c b/src/backend/optimizer/util/paramassign.c
new file mode 100644
index d6a923b..5ecf502
--- a/src/backend/optimizer/util/paramassign.c
+++ b/src/backend/optimizer/util/paramassign.c
@@ -307,6 +307,57 @@ replace_outer_grouping(PlannerInfo *root
}
/*
+ * Generate a Param node to replace the given FuncExpr node, which is expected
+ * to be a merge support function in the RETURNING list of an upper-level
+ * MERGE query. Record the need for the FuncExpr in the proper upper-level
+ * root->plan_params.
+ */
+Param *
+replace_outer_merge_support_function(PlannerInfo *root, FuncExpr *func)
+{
+ Param *retval;
+ PlannerParamItem *pitem;
+ Oid ptype = exprType((Node *) func);
+
+ Assert(root->parse->commandType != CMD_MERGE);
+
+ /*
+ * The parser should have ensured that the merge support function is in
+ * the RETURNING list of an upper-level MERGE query, so find that query.
+ */
+ do
+ {
+ root = root->parent_root;
+ if (root == NULL)
+ elog(ERROR, "merge support function used outside MERGE");
+ } while (root->parse->commandType != CMD_MERGE);
+
+ /*
+ * It does not seem worthwhile to try to de-duplicate references to outer
+ * merge support functions. Just make a new slot every time.
+ */
+ func = copyObject(func);
+
+ pitem = makeNode(PlannerParamItem);
+ pitem->item = (Node *) func;
+ pitem->paramId = list_length(root->glob->paramExecTypes);
+ root->glob->paramExecTypes = lappend_oid(root->glob->paramExecTypes,
+ ptype);
+
+ root->plan_params = lappend(root->plan_params, pitem);
+
+ retval = makeNode(Param);
+ retval->paramkind = PARAM_EXEC;
+ retval->paramid = pitem->paramId;
+ retval->paramtype = ptype;
+ retval->paramtypmod = -1;
+ retval->paramcollid = InvalidOid;
+ retval->location = func->location;
+
+ return retval;
+}
+
+/*
* Generate a Param node to replace the given Var,
* which is expected to come from some upper NestLoop plan node.
* Record the need for the Var in root->curOuterParams.
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
new file mode 100644
index 4006632..6794511
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -74,7 +74,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,
@@ -515,7 +514,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;
@@ -942,7 +942,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;
@@ -2407,7 +2408,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
@@ -2501,10 +2503,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;
@@ -2521,7 +2524,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 b3bdf94..dd2a74f
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -12270,6 +12270,7 @@ MergeStmt:
USING table_ref
ON a_expr
merge_when_list
+ returning_clause
{
MergeStmt *m = makeNode(MergeStmt);
@@ -12278,6 +12279,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 85cd47b..e72c37b
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -456,6 +456,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 aa72665..3957d7c
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -541,6 +541,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:
@@ -1767,6 +1768,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:
@@ -3115,6 +3117,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 b3f0b6a..cf7c0b9
--- 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,26 @@ 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)
+ {
+ ParseState *parent_pstate = pstate->parentParseState;
+
+ /* May be in a subquery in RETURNING list */
+ while (parent_pstate &&
+ parent_pstate->p_expr_kind != EXPR_KIND_MERGE_RETURNING)
+ parent_pstate = parent_pstate->parentParseState;
+
+ if (!parent_pstate ||
+ parent_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.
*/
@@ -2599,6 +2620,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 91b1156..33852e9
--- 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.
@@ -254,6 +258,7 @@ transformMergeStmt(ParseState *pstate, M
MergeAction *action;
action = makeNode(MergeAction);
+ action->index = foreach_current_index(l) + 1;
action->commandType = mergeWhenClause->commandType;
action->matched = mergeWhenClause->matched;
@@ -390,9 +395,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 864ea9b..cc19041
--- a/src/backend/parser/parse_relation.c
+++ b/src/backend/parser/parse_relation.c
@@ -2346,9 +2346,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 b486ab5..1bd0ce2
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -3629,9 +3629,9 @@ RewriteQuery(Query *parsetree, List *rew
ListCell *lc1;
/*
- * First, recursively process any insert/update/delete statements in WITH
- * clauses. (We have to do this first because the WITH clauses may get
- * copied into rule actions below.)
+ * First, recursively process any insert/update/delete/merge statements in
+ * WITH clauses. (We have to do this first because the WITH clauses may
+ * get copied into rule actions below.)
*/
foreach(lc1, parsetree->cteList)
{
@@ -3656,7 +3656,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/rewrite/rowsecurity.c b/src/backend/rewrite/rowsecurity.c
new file mode 100644
index b1620e4..966dcf0
--- a/src/backend/rewrite/rowsecurity.c
+++ b/src/backend/rewrite/rowsecurity.c
@@ -395,10 +395,10 @@ get_row_security_policies(Query *root, R
* on the final action we take.
*
* We already fetched the SELECT policies above, to check existing rows,
- * but we must also check that new rows created by UPDATE actions are
- * visible, if SELECT rights are required for this relation. We don't do
- * this for INSERT actions, since an INSERT command would only do this
- * check if it had a RETURNING list, and MERGE does not support RETURNING.
+ * but we must also check that new rows created by INSERT/UPDATE actions
+ * are visible, if SELECT rights are required. For INSERT actions, we only
+ * do this if RETURNING is specified, to be consistent with a plain INSERT
+ * command, which can only require SELECT rights when RETURNING is used.
*
* 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
@@ -420,6 +420,8 @@ get_row_security_policies(Query *root, R
List *merge_delete_restrictive_policies;
List *merge_insert_permissive_policies;
List *merge_insert_restrictive_policies;
+ List *merge_select_permissive_policies = NIL;
+ List *merge_select_restrictive_policies = NIL;
/*
* Fetch the UPDATE policies and set them up to execute on the
@@ -457,9 +459,6 @@ get_row_security_policies(Query *root, R
*/
if (perminfo->requiredPerms & ACL_SELECT)
{
- List *merge_select_permissive_policies;
- List *merge_select_restrictive_policies;
-
get_policies_for_relation(rel, CMD_SELECT, user_id,
&merge_select_permissive_policies,
&merge_select_restrictive_policies);
@@ -508,6 +507,21 @@ get_row_security_policies(Query *root, R
withCheckOptions,
hasSubLinks,
false);
+
+ /*
+ * Add ALL/SELECT policies as WCO_RLS_INSERT_CHECK WCOs, to ensure
+ * that the inserted row is visible when executing an INSERT action,
+ * if RETURNING is specified and SELECT rights are required for this
+ * relation.
+ */
+ if (perminfo->requiredPerms & ACL_SELECT && root->returningList)
+ add_with_check_options(rel, rt_index,
+ WCO_RLS_INSERT_CHECK,
+ merge_select_permissive_policies,
+ merge_select_restrictive_policies,
+ withCheckOptions,
+ hasSubLinks,
+ true);
}
table_close(rel, NoLock);
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
new file mode 100644
index e3ccf6c..6f3dc89
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -2142,11 +2142,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/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
new file mode 100644
index 03f2835..25f49fa
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -7183,8 +7183,13 @@ get_merge_query_def(Query *query, depars
appendStringInfoString(buf, "DO NOTHING");
}
- /* No RETURNING support in MERGE yet */
- Assert(query->returningList == NIL);
+ /* Add RETURNING if present */
+ if (query->returningList)
+ {
+ appendContextKeyword(context, " RETURNING",
+ -PRETTYINDENT_STD, PRETTYINDENT_STD, 1);
+ get_target_list(query->returningList, context, NULL, colNamesVisible);
+ }
}
diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c
new file mode 100644
index 10ad1f2..3f42ca4
--- a/src/bin/psql/common.c
+++ b/src/bin/psql/common.c
@@ -982,13 +982,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 12fac15..88a3b29
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12049,4 +12049,14 @@
proname => 'any_value_transfn', prorettype => 'anyelement',
proargtypes => 'anyelement anyelement', prosrc => 'any_value_transfn' },
+# MERGE support functions
+{ oid => '9499', descr => 'command type of current MERGE action',
+ proname => 'pg_merge_action', provolatile => 'v', proparallel => 'r',
+ prorettype => 'text', proargtypes => '',
+ prosrc => 'pg_merge_action' },
+{ oid => '9500', descr => 'index of current MERGE WHEN clause',
+ proname => 'pg_merge_when_clause_number', provolatile => 'v',
+ proparallel => 'r', prorettype => 'int4', proargtypes => '',
+ prosrc => 'pg_merge_when_clause_number' },
+
]
diff --git a/src/include/catalog/pg_proc.h b/src/include/catalog/pg_proc.h
new file mode 100644
index e7abe0b..eb6cad9
--- 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(funcid) \
+ ((funcid) == F_PG_MERGE_ACTION || \
+ (funcid) == F_PG_MERGE_WHEN_CLAUSE_NUMBER)
+
#endif /* EXPOSE_TO_CLIENT_CODE */
diff --git a/src/include/executor/spi.h b/src/include/executor/spi.h
new file mode 100644
index d1de139..ed8787d
--- a/src/include/executor/spi.h
+++ b/src/include/executor/spi.h
@@ -97,6 +97,7 @@ typedef struct _SPI_plan *SPIPlanPtr;
#define SPI_OK_REL_UNREGISTER 16
#define SPI_OK_TD_REGISTER 17
#define SPI_OK_MERGE 18
+#define SPI_OK_MERGE_RETURNING 19
#define SPI_OPT_NONATOMIC (1 << 0)
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
new file mode 100644
index cb714f4..6708b6b
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1319,6 +1319,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 2565348..ac0a754
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -1688,6 +1688,7 @@ typedef struct MergeWhenClause
typedef struct MergeAction
{
NodeTag type;
+ int index; /* 1-based index of the clause */
bool matched; /* true=MATCHED, false=NOT MATCHED */
CmdType commandType; /* INSERT/UPDATE/DELETE/DO NOTHING */
/* OVERRIDING clause */
@@ -1951,6 +1952,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/optimizer/paramassign.h b/src/include/optimizer/paramassign.h
new file mode 100644
index 55c27a6..6f70c78
--- a/src/include/optimizer/paramassign.h
+++ b/src/include/optimizer/paramassign.h
@@ -20,6 +20,8 @@ extern Param *replace_outer_placeholderv
PlaceHolderVar *phv);
extern Param *replace_outer_agg(PlannerInfo *root, Aggref *agg);
extern Param *replace_outer_grouping(PlannerInfo *root, GroupingFunc *grp);
+extern Param *replace_outer_merge_support_function(PlannerInfo *root,
+ FuncExpr *func);
extern Param *replace_nestloop_param_var(PlannerInfo *root, Var *var);
extern Param *replace_nestloop_param_placeholdervar(PlannerInfo *root,
PlaceHolderVar *phv);
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 f589112..836819d
--- 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/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
new file mode 100644
index 4b76f76..fb6289c
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -4267,9 +4267,9 @@ exec_stmt_execsql(PLpgSQL_execstate *est
/*
* If we have INTO, then we only need one row back ... but if we have INTO
* STRICT or extra check too_many_rows, ask for two rows, so that we can
- * verify the statement returns only one. INSERT/UPDATE/DELETE are always
- * treated strictly. Without INTO, just run the statement to completion
- * (tcount = 0).
+ * verify the statement returns only one. INSERT/UPDATE/DELETE/MERGE are
+ * always treated strictly. Without INTO, just run the statement to
+ * completion (tcount = 0).
*
* We could just ask for two rows always when using INTO, but there are
* some cases where demanding the extra row costs significant time, eg by
@@ -4307,10 +4307,11 @@ exec_stmt_execsql(PLpgSQL_execstate *est
case SPI_OK_INSERT:
case SPI_OK_UPDATE:
case SPI_OK_DELETE:
+ case SPI_OK_MERGE:
case SPI_OK_INSERT_RETURNING:
case SPI_OK_UPDATE_RETURNING:
case SPI_OK_DELETE_RETURNING:
- case SPI_OK_MERGE:
+ case SPI_OK_MERGE_RETURNING:
Assert(stmt->mod_stmt);
exec_set_found(estate, (SPI_processed != 0));
break;
@@ -4489,10 +4490,11 @@ exec_stmt_dynexecute(PLpgSQL_execstate *
case SPI_OK_INSERT:
case SPI_OK_UPDATE:
case SPI_OK_DELETE:
+ case SPI_OK_MERGE:
case SPI_OK_INSERT_RETURNING:
case SPI_OK_UPDATE_RETURNING:
case SPI_OK_DELETE_RETURNING:
- case SPI_OK_MERGE:
+ case SPI_OK_MERGE_RETURNING:
case SPI_OK_UTILITY:
case SPI_OK_REWRITTEN:
break;
diff --git a/src/pl/tcl/pltcl.c b/src/pl/tcl/pltcl.c
new file mode 100644
index e8f9d7b..0c9f81e
--- a/src/pl/tcl/pltcl.c
+++ b/src/pl/tcl/pltcl.c
@@ -2456,6 +2456,7 @@ pltcl_process_SPI_result(Tcl_Interp *int
case SPI_OK_INSERT_RETURNING:
case SPI_OK_DELETE_RETURNING:
case SPI_OK_UPDATE_RETURNING:
+ case SPI_OK_MERGE_RETURNING:
/*
* Process the tuples we got
diff --git a/src/test/regress/expected/merge.out b/src/test/regress/expected/merge.out
new file mode 100644
index 133d421..5be3d2d
--- 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;
@@ -1256,7 +1256,7 @@ BEGIN;
MERGE INTO sq_target
USING v
ON tid = sid
-WHEN MATCHED AND tid > 2 THEN
+WHEN MATCHED AND tid >= 2 THEN
UPDATE SET balance = balance + delta
WHEN NOT MATCHED THEN
INSERT (balance, tid) VALUES (balance + delta, sid)
@@ -1271,7 +1271,7 @@ INSERT INTO sq_source (sid, balance, del
MERGE INTO sq_target t
USING v
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)
@@ -1295,7 +1295,7 @@ WITH targq AS (
MERGE INTO sq_target t
USING v
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)
@@ -1304,20 +1304,193 @@ WHEN MATCHED AND tid < 2 THEN
ROLLBACK;
-- RETURNING
BEGIN;
-INSERT INTO sq_source (sid, balance, delta) VALUES (-1, -1, -10);
+CREATE TABLE merge_actions(action text, abbrev text);
+INSERT INTO merge_actions VALUES ('INSERT', 'ins'), ('UPDATE', 'upd'), ('DELETE', 'del');
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)
+ 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 *;
- ^
+ DELETE
+RETURNING pg_merge_when_clause_number() AS clause_number,
+ (SELECT abbrev FROM merge_actions
+ WHERE action = 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;
+ clause_number | merge_action | tid | balance | description
+---------------+--------------+-----+---------+---------------------
+ 3 | del | 1 | 100 | Removed (1,100)
+ 1 | upd | 2 | 220 | Added 20 to balance
+ 2 | ins | 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_number();
+ERROR: merge support function pg_merge_when_clause_number 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...
+ ^
+-- RETURNING in CTEs
+CREATE TABLE sq_target_merge_log (tid integer NOT NULL, last_change text);
+INSERT INTO sq_target_merge_log VALUES (1, 'Original value');
+BEGIN;
+WITH m AS (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 pg_merge_when_clause_number() AS clause_number,
+ 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
+), m2 AS (
+ MERGE INTO sq_target_merge_log l
+ USING m
+ ON l.tid = m.tid
+ WHEN MATCHED THEN
+ UPDATE SET last_change = description
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (m.tid, description)
+ RETURNING merge_action, pg_merge_action() AS merge_log_action, l.*
+)
+SELECT * FROM m2;
+ merge_action | merge_log_action | tid | last_change
+--------------+------------------+-----+---------------------
+ DELETE | UPDATE | 1 | Removed (1,100)
+ UPDATE | INSERT | 2 | Added 20 to balance
+ INSERT | INSERT | 4 | Inserted (4,40)
+(3 rows)
+
+SELECT * FROM sq_target_merge_log ORDER BY tid;
+ tid | last_change
+-----+---------------------
+ 1 | Removed (1,100)
+ 2 | Added 20 to balance
+ 4 | Inserted (4,40)
+(3 rows)
+
+ROLLBACK;
+-- COPY (MERGE ... RETURNING) TO ...
+BEGIN;
+COPY (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 pg_merge_action(), t.*
+) TO stdout;
+DELETE 1 100
+UPDATE 2 220
+INSERT 4 40
+ROLLBACK;
+-- SQL function with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT action text, OUT tid int, OUT new_balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING pg_merge_action(), t.*;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT action, tid, new_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ action | tid | new_balance
+--------+-----+-------------
+ DELETE | 1 | 100
+ UPDATE | 3 | 320
+ INSERT | 4 | 110
+(3 rows)
+
+ROLLBACK;
+-- SQL SRF with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_sq_source_into_sq_target()
+RETURNS TABLE (action text, tid int, balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 pg_merge_action(), t.*;
+$$;
+SELECT * FROM merge_sq_source_into_sq_target();
+ action | tid | balance
+--------+-----+---------
+ DELETE | 1 | 100
+ UPDATE | 2 | 220
+ INSERT | 4 | 40
+(3 rows)
+
+ROLLBACK;
+-- PL/pgSQL function with MERGE ... RETURNING ... INTO
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT r_action text, OUT r_tid int, OUT r_balance int)
+LANGUAGE plpgsql AS
+$$
+BEGIN
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING pg_merge_action(), t.* INTO r_action, r_tid, r_balance;
+END;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT r_action, r_tid, r_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ r_action | r_tid | r_balance
+----------+-------+-----------
+ DELETE | 1 | 100
+ UPDATE | 3 | 320
+ INSERT | 4 | 110
+(3 rows)
+
ROLLBACK;
-- EXPLAIN
CREATE TABLE ex_mtarget (a int, b int)
@@ -1514,7 +1687,7 @@ SELECT * FROM sq_target WHERE tid = 1;
(1 row)
ROLLBACK;
-DROP TABLE sq_target, sq_source CASCADE;
+DROP TABLE sq_target, sq_target_merge_log, sq_source CASCADE;
NOTICE: drop cascades to view v
CREATE TABLE pa_target (tid integer, balance float, val text)
PARTITION BY LIST (tid);
@@ -1640,6 +1813,32 @@ SELECT * FROM pa_target ORDER BY tid;
(14 rows)
ROLLBACK;
+-- update partition key to partition not initially scanned
+BEGIN;
+MERGE INTO pa_target t
+ USING pa_source s
+ ON t.tid = s.sid AND t.tid = 1
+ WHEN MATCHED THEN
+ UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
+ RETURNING pg_merge_action(), pg_merge_when_clause_number(), t.*;
+ pg_merge_action | pg_merge_when_clause_number | tid | balance | val
+-----------------+-----------------------------+-----+---------+--------------------------
+ UPDATE | 1 | 2 | 110 | initial updated by merge
+(1 row)
+
+SELECT * FROM pa_target ORDER BY tid;
+ tid | balance | val
+-----+---------+--------------------------
+ 2 | 110 | initial updated by merge
+ 3 | 300 | initial
+ 5 | 500 | initial
+ 7 | 700 | initial
+ 9 | 900 | initial
+ 11 | 1100 | initial
+ 13 | 1300 | initial
+(7 rows)
+
+ROLLBACK;
DROP TABLE pa_target CASCADE;
-- The target table is partitioned in the same way, but this time by attaching
-- partitions which have columns in different order, dropped columns etc.
@@ -1731,7 +1930,26 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (sid, delta, 'inserted by merge');
+ INSERT VALUES (sid, delta, 'inserted by merge')
+ RETURNING pg_merge_action(), pg_merge_when_clause_number(), t.*;
+ pg_merge_action | pg_merge_when_clause_number | tid | balance | val
+-----------------+-----------------------------+-----+---------+--------------------------
+ UPDATE | 1 | 2 | 110 | initial updated by merge
+ INSERT | 2 | 2 | 20 | inserted by merge
+ UPDATE | 1 | 4 | 330 | initial updated by merge
+ INSERT | 2 | 4 | 40 | inserted by merge
+ UPDATE | 1 | 6 | 550 | initial updated by merge
+ INSERT | 2 | 6 | 60 | inserted by merge
+ UPDATE | 1 | 8 | 770 | initial updated by merge
+ INSERT | 2 | 8 | 80 | inserted by merge
+ UPDATE | 1 | 10 | 990 | initial updated by merge
+ INSERT | 2 | 10 | 100 | inserted by merge
+ UPDATE | 1 | 12 | 1210 | initial updated by merge
+ INSERT | 2 | 12 | 120 | inserted by merge
+ UPDATE | 1 | 14 | 1430 | initial updated by merge
+ INSERT | 2 | 14 | 140 | inserted by merge
+(14 rows)
+
SELECT * FROM pa_target ORDER BY tid;
tid | balance | val
-----+---------+--------------------------
@@ -1798,7 +2016,21 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge');
+ INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge')
+ RETURNING pg_merge_action(), pg_merge_when_clause_number(), t.*;
+ pg_merge_action | pg_merge_when_clause_number | logts | tid | balance | val
+-----------------+-----------------------------+--------------------------+-----+---------+--------------------------
+ UPDATE | 1 | Tue Jan 31 00:00:00 2017 | 1 | 110 | initial updated by merge
+ UPDATE | 1 | Tue Feb 28 00:00:00 2017 | 2 | 220 | initial updated by merge
+ INSERT | 2 | Sun Jan 15 00:00:00 2017 | 3 | 30 | inserted by merge
+ UPDATE | 1 | Tue Jan 31 00:00:00 2017 | 4 | 440 | initial updated by merge
+ UPDATE | 1 | Tue Feb 28 00:00:00 2017 | 5 | 550 | initial updated by merge
+ INSERT | 2 | Sun Jan 15 00:00:00 2017 | 6 | 60 | inserted by merge
+ UPDATE | 1 | Tue Jan 31 00:00:00 2017 | 7 | 770 | initial updated by merge
+ UPDATE | 1 | Tue Feb 28 00:00:00 2017 | 8 | 880 | initial updated by merge
+ INSERT | 2 | Sun Jan 15 00:00:00 2017 | 9 | 90 | inserted by merge
+(9 rows)
+
SELECT * FROM pa_target ORDER BY tid;
logts | tid | balance | val
--------------------------+-----+---------+--------------------------
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
new file mode 100644
index 97ca9bf..55250b3
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -2300,6 +2300,35 @@ WHEN MATCHED THEN
UPDATE SET dnotes = dnotes || ' notes added by merge8 '
WHEN NOT MATCHED THEN
INSERT VALUES (13, 44, 1, 'regress_rls_bob', 'new manga');
+SELECT * FROM document WHERE did = 13;
+ did | cid | dlevel | dauthor | dtitle | dnotes
+-----+-----+--------+---------+--------+--------
+(0 rows)
+
+-- but not OK if RETURNING is used
+MERGE INTO document d
+USING (SELECT 14 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge9 '
+WHEN NOT MATCHED THEN
+ INSERT VALUES (14, 44, 1, 'regress_rls_bob', 'new manga')
+RETURNING *;
+ERROR: new row violates row-level security policy for table "document"
+-- but OK if new row is visible
+MERGE INTO document d
+USING (SELECT 14 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge10 '
+WHEN NOT MATCHED THEN
+ INSERT VALUES (14, 11, 1, 'regress_rls_bob', 'new novel')
+RETURNING *;
+ sdid | did | cid | dlevel | dauthor | dtitle | dnotes
+------+-----+-----+--------+-----------------+-----------+--------
+ 14 | 14 | 11 | 1 | regress_rls_bob | new novel |
+(1 row)
+
RESET SESSION AUTHORIZATION;
-- drop the restrictive SELECT policy so that we can look at the
-- final state of the table
@@ -2323,7 +2352,8 @@ SELECT * FROM document;
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 notes added by merge7
13 | 44 | 1 | regress_rls_bob | new manga |
-(15 rows)
+ 14 | 11 | 1 | regress_rls_bob | new novel |
+(16 rows)
--
-- ROLE/GROUP
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
new file mode 100644
index 5058be5..2adfad7
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -3596,7 +3596,8 @@ MERGE INTO rule_merge2 t USING (SELECT 1
-- test deparsing
CREATE TABLE sf_target(id int, data text, filling int[]);
CREATE FUNCTION merge_sf_test()
- RETURNS void
+ RETURNS TABLE(pg_merge_action text, when_clause int,
+ a int, b text, id int, data text, filling int[])
LANGUAGE sql
BEGIN ATOMIC
MERGE INTO sf_target t
@@ -3633,11 +3634,13 @@ WHEN NOT MATCHED
VALUES (s.a, s.b, DEFAULT)
WHEN NOT MATCHED
THEN INSERT (filling[1], id)
- VALUES (s.a, s.a);
+ VALUES (s.a, s.a)
+RETURNING
+ pg_merge_action(), pg_merge_when_clause_number() AS clause_number, *;
END;
\sf merge_sf_test
CREATE OR REPLACE FUNCTION public.merge_sf_test()
- RETURNS void
+ RETURNS TABLE(pg_merge_action text, when_clause integer, a integer, b text, id integer, data text, filling integer[])
LANGUAGE sql
BEGIN ATOMIC
MERGE INTO sf_target t
@@ -3674,7 +3677,14 @@ BEGIN ATOMIC
VALUES (s.a, s.b, DEFAULT)
WHEN NOT MATCHED
THEN INSERT (filling[1], id)
- VALUES (s.a, s.a);
+ VALUES (s.a, s.a)
+ RETURNING pg_merge_action() AS pg_merge_action,
+ pg_merge_when_clause_number() AS clause_number,
+ s.a,
+ s.b,
+ t.id,
+ t.data,
+ t.filling;
END
DROP FUNCTION merge_sf_test;
DROP TABLE sf_target;
diff --git a/src/test/regress/sql/merge.sql b/src/test/regress/sql/merge.sql
new file mode 100644
index 4cf6db9..8394d01
--- 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
@@ -816,7 +816,7 @@ BEGIN;
MERGE INTO sq_target
USING v
ON tid = sid
-WHEN MATCHED AND tid > 2 THEN
+WHEN MATCHED AND tid >= 2 THEN
UPDATE SET balance = balance + delta
WHEN NOT MATCHED THEN
INSERT (balance, tid) VALUES (balance + delta, sid)
@@ -829,7 +829,7 @@ INSERT INTO sq_source (sid, balance, del
MERGE INTO sq_target t
USING v
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)
@@ -847,7 +847,7 @@ WITH targq AS (
MERGE INTO sq_target t
USING v
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)
@@ -857,17 +857,148 @@ ROLLBACK;
-- RETURNING
BEGIN;
-INSERT INTO sq_source (sid, balance, delta) VALUES (-1, -1, -10);
+CREATE TABLE merge_actions(action text, abbrev text);
+INSERT INTO merge_actions VALUES ('INSERT', 'ins'), ('UPDATE', 'upd'), ('DELETE', 'del');
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)
+ INSERT (balance, tid) VALUES (balance + delta, sid)
WHEN MATCHED AND tid < 2 THEN
- DELETE
-RETURNING *;
+ DELETE
+RETURNING pg_merge_when_clause_number() AS clause_number,
+ (SELECT abbrev FROM merge_actions
+ WHERE action = 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_number();
+
+-- RETURNING in CTEs
+CREATE TABLE sq_target_merge_log (tid integer NOT NULL, last_change text);
+INSERT INTO sq_target_merge_log VALUES (1, 'Original value');
+BEGIN;
+WITH m AS (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 pg_merge_when_clause_number() AS clause_number,
+ 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
+), m2 AS (
+ MERGE INTO sq_target_merge_log l
+ USING m
+ ON l.tid = m.tid
+ WHEN MATCHED THEN
+ UPDATE SET last_change = description
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (m.tid, description)
+ RETURNING merge_action, pg_merge_action() AS merge_log_action, l.*
+)
+SELECT * FROM m2;
+SELECT * FROM sq_target_merge_log ORDER BY tid;
+ROLLBACK;
+
+-- COPY (MERGE ... RETURNING) TO ...
+BEGIN;
+COPY (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 pg_merge_action(), t.*
+) TO stdout;
+ROLLBACK;
+
+-- SQL function with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT action text, OUT tid int, OUT new_balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING pg_merge_action(), t.*;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT action, tid, new_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ROLLBACK;
+
+-- SQL SRF with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_sq_source_into_sq_target()
+RETURNS TABLE (action text, tid int, balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 pg_merge_action(), t.*;
+$$;
+SELECT * FROM merge_sq_source_into_sq_target();
+ROLLBACK;
+
+-- PL/pgSQL function with MERGE ... RETURNING ... INTO
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT r_action text, OUT r_tid int, OUT r_balance int)
+LANGUAGE plpgsql AS
+$$
+BEGIN
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING pg_merge_action(), t.* INTO r_action, r_tid, r_balance;
+END;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT r_action, r_tid, r_balance FROM merge_into_sq_target(sid, balance, delta)) m;
ROLLBACK;
-- EXPLAIN
@@ -966,7 +1097,7 @@ WHEN MATCHED THEN
SELECT * FROM sq_target WHERE tid = 1;
ROLLBACK;
-DROP TABLE sq_target, sq_source CASCADE;
+DROP TABLE sq_target, sq_target_merge_log, sq_source CASCADE;
CREATE TABLE pa_target (tid integer, balance float, val text)
PARTITION BY LIST (tid);
@@ -1033,6 +1164,17 @@ SELECT merge_func();
SELECT * FROM pa_target ORDER BY tid;
ROLLBACK;
+-- update partition key to partition not initially scanned
+BEGIN;
+MERGE INTO pa_target t
+ USING pa_source s
+ ON t.tid = s.sid AND t.tid = 1
+ WHEN MATCHED THEN
+ UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
+ RETURNING pg_merge_action(), pg_merge_when_clause_number(), t.*;
+SELECT * FROM pa_target ORDER BY tid;
+ROLLBACK;
+
DROP TABLE pa_target CASCADE;
-- The target table is partitioned in the same way, but this time by attaching
@@ -1091,7 +1233,8 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (sid, delta, 'inserted by merge');
+ INSERT VALUES (sid, delta, 'inserted by merge')
+ RETURNING pg_merge_action(), pg_merge_when_clause_number(), t.*;
SELECT * FROM pa_target ORDER BY tid;
ROLLBACK;
@@ -1145,7 +1288,8 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge');
+ INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge')
+ RETURNING pg_merge_action(), pg_merge_when_clause_number(), t.*;
SELECT * FROM pa_target ORDER BY tid;
ROLLBACK;
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
new file mode 100644
index dec7340..1d5ed0a
--- a/src/test/regress/sql/rowsecurity.sql
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -992,6 +992,27 @@ WHEN MATCHED THEN
UPDATE SET dnotes = dnotes || ' notes added by merge8 '
WHEN NOT MATCHED THEN
INSERT VALUES (13, 44, 1, 'regress_rls_bob', 'new manga');
+SELECT * FROM document WHERE did = 13;
+
+-- but not OK if RETURNING is used
+MERGE INTO document d
+USING (SELECT 14 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge9 '
+WHEN NOT MATCHED THEN
+ INSERT VALUES (14, 44, 1, 'regress_rls_bob', 'new manga')
+RETURNING *;
+
+-- but OK if new row is visible
+MERGE INTO document d
+USING (SELECT 14 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge10 '
+WHEN NOT MATCHED THEN
+ INSERT VALUES (14, 11, 1, 'regress_rls_bob', 'new novel')
+RETURNING *;
RESET SESSION AUTHORIZATION;
-- drop the restrictive SELECT policy so that we can look at the
diff --git a/src/test/regress/sql/rules.sql b/src/test/regress/sql/rules.sql
new file mode 100644
index 8b7e255..5dbeb6b
--- a/src/test/regress/sql/rules.sql
+++ b/src/test/regress/sql/rules.sql
@@ -1281,7 +1281,8 @@ MERGE INTO rule_merge2 t USING (SELECT 1
CREATE TABLE sf_target(id int, data text, filling int[]);
CREATE FUNCTION merge_sf_test()
- RETURNS void
+ RETURNS TABLE(pg_merge_action text, when_clause int,
+ a int, b text, id int, data text, filling int[])
LANGUAGE sql
BEGIN ATOMIC
MERGE INTO sf_target t
@@ -1318,7 +1319,9 @@ WHEN NOT MATCHED
VALUES (s.a, s.b, DEFAULT)
WHEN NOT MATCHED
THEN INSERT (filling[1], id)
- VALUES (s.a, s.a);
+ VALUES (s.a, s.a)
+RETURNING
+ pg_merge_action(), pg_merge_when_clause_number() AS clause_number, *;
END;
\sf merge_sf_test
On Wed, 2023-08-23 at 11:58 +0100, Dean Rasheed wrote:
Updated version attached, fixing an uninitialized-variable warning
from the cfbot.
I took another look and I'm still not comfortable with the special
IsMergeSupportFunction() functions. I don't object necessarily -- if
someone else wants to commit it, they can -- but I don't plan to commit
it in this form.
Can we revisit the idea of a per-WHEN RETURNING clause? The returning
clauses could be treated kind of like a UNION, which makes sense
because it really is a union of different results (the returned tuples
from an INSERT are different than the returned tuples from a DELETE).
You can just add constants to the target lists to distinguish which
WHEN clause they came from.
I know you rejected that approach early on, but perhaps it's worth
discussing further?
Regards,
Jeff Davis
On Tue, Oct 24, 2023 at 2:11 PM Jeff Davis <pgsql@j-davis.com> wrote:
On Wed, 2023-08-23 at 11:58 +0100, Dean Rasheed wrote:
Updated version attached, fixing an uninitialized-variable warning
from the cfbot.I took another look and I'm still not comfortable with the special
IsMergeSupportFunction() functions. I don't object necessarily -- if
someone else wants to commit it, they can -- but I don't plan to commit
it in this form.Can we revisit the idea of a per-WHEN RETURNING clause? The returning
clauses could be treated kind of like a UNION, which makes sense
because it really is a union of different results (the returned tuples
from an INSERT are different than the returned tuples from a DELETE).
You can just add constants to the target lists to distinguish which
WHEN clause they came from.I know you rejected that approach early on, but perhaps it's worth
discussing further?
Yeah. Side benefit, the 'action_number' felt really out of place, and
that neatly might solve it. It doesn't match tg_op, for example. With the
current approach, return a text, or an enum? Why doesn't it match concepts
that are pretty well established elsewhere? SQL has a pretty good track
record for not inventing weird numbers with no real meaning (sadly, not so
much the developers). Having said that, pg_merge_action() doesn't feel
too bad if the syntax issues can be worked out.
Very supportive of the overall goal.
merlin
On Wed, 25 Oct 2023 at 02:07, Merlin Moncure <mmoncure@gmail.com> wrote:
On Tue, Oct 24, 2023 at 2:11 PM Jeff Davis <pgsql@j-davis.com> wrote:
Can we revisit the idea of a per-WHEN RETURNING clause? The returning
clauses could be treated kind of like a UNION, which makes sense
because it really is a union of different results (the returned tuples
from an INSERT are different than the returned tuples from a DELETE).
You can just add constants to the target lists to distinguish which
WHEN clause they came from.Yeah. Side benefit, the 'action_number' felt really out of place, and that neatly might solve it. It doesn't match tg_op, for example. With the current approach, return a text, or an enum? Why doesn't it match concepts that are pretty well established elsewhere? SQL has a pretty good track record for not inventing weird numbers with no real meaning (sadly, not so much the developers). Having said that, pg_merge_action() doesn't feel too bad if the syntax issues can be worked out.
I've been playing around a little with per-action RETURNING lists, and
attached is a working proof-of-concept (no docs yet).
The implementation is simplified a little by not needing special merge
support functions, but overall this approach introduces a little more
complexity, which is perhaps not surprising.
One fiddly part is resolving the shift/reduce conflicts in the
grammar. Specifically, on seeing "RETURNING expr when ...", there is
ambiguity over whether the "when" is a column alias or the start of
the next merge action. I've resolved that by assigning a slightly
higher precedence to an expression without an alias, so WHEN is
assumed to not be an alias. It seems pretty ugly though (in terms of
having to duplicate so much code), and I'd be interested to know if
there's a neater way to do it.
From a usability perspective, I'm still somewhat sceptical about this
approach. It's a much more verbose syntax, and it gets quite tedious
having to repeat the RETURNING list for every action, and keep them in
sync. I also note that other database vendors seem to have opted for
the single RETURNING list approach (not that we necessarily need to
copy them).
The patch enforces the rule that if any action has a RETURNING list,
they all must have a RETURNING list. Not doing that leads to the
number of rows returned not matching the command tag, or the number of
rows modified, which I think would just lead to confusion. Also, it
would likely be a source of easy-to-overlook mistakes. One such
mistake would be assuming that a RETURNING list at the very end
applied to all actions, though it would also be easy to accidentally
omit a RETURNING list in the middle of the command.
Having said that, I wonder if it would make sense to also support
having a RETURNING list at the very end, if there are no other
RETURNING lists. If we see that, we could automatically apply it to
all actions, which I think would be much more convenient in situations
where you don't care about the action executed, and just want the
results from the table. That would go a long way towards addressing my
usability concerns.
Regards,
Dean
Attachments:
POC-support-merge-returning-per-action.patchtext/x-patch; charset=US-ASCII; name=POC-support-merge-returning-per-action.patchDownload
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
new file mode 100644
index c5d7d78..7977873
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -283,12 +283,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 c66a047..1145aaf
--- 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/execPartition.c b/src/backend/executor/execPartition.c
new file mode 100644
index f6c3432..f0e75c3
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -613,16 +613,16 @@ ExecInitPartitionInfo(ModifyTableState *
* build the returningList for partitions within the planner, but simple
* translation of varattnos will suffice. This only occurs for the INSERT
* case or in the case of UPDATE tuple routing where we didn't find a
- * result rel to reuse.
+ * result rel to reuse. We skip this for MERGE, since it uses per-action
+ * RETURNING lists, which are handled below.
*/
- if (node && node->returningLists != NIL)
+ if (node && node->returningLists != NIL && node->operation != CMD_MERGE)
{
TupleTableSlot *slot;
ExprContext *econtext;
List *returningList;
/* See the comment above for WCO lists. */
- /* (except no RETURNING support for MERGE yet) */
Assert((node->operation == CMD_INSERT &&
list_length(node->returningLists) == 1 &&
list_length(node->resultRelations) == 1) ||
@@ -959,6 +959,25 @@ ExecInitPartitionInfo(ModifyTableState *
&found_whole_row);
action_state->mas_whenqual =
ExecInitQual((List *) action->qual, &mtstate->ps);
+
+ /* Build a projection for the action's RETURNING list, if any */
+ if (action->returningList)
+ {
+ /* found_whole_row intentionally ignored. */
+ action->returningList = (List *)
+ map_variable_attnos((Node *) action->returningList,
+ firstVarno, 0,
+ part_attmap,
+ RelationGetForm(partrel)->reltype,
+ &found_whole_row);
+
+ action_state->mas_proj_returning =
+ ExecBuildProjectionInfo(action->returningList,
+ econtext,
+ mtstate->ps.ps_ResultTupleSlot,
+ &mtstate->ps,
+ RelationGetDescr(partrel));
+ }
}
}
MemoryContextSwitchTo(oldcxt);
diff --git a/src/backend/executor/functions.c b/src/backend/executor/functions.c
new file mode 100644
index bace252..68cb4fa
--- a/src/backend/executor/functions.c
+++ b/src/backend/executor/functions.c
@@ -1660,8 +1660,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
@@ -1679,7 +1679,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;
@@ -1693,7 +1694,7 @@ check_sql_fn_retval(List *queryTreeLists
(errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
errmsg("return type mismatch in function declared to return %s",
format_type_be(rettype)),
- errdetail("Function's final statement must be SELECT or INSERT/UPDATE/DELETE RETURNING.")));
+ errdetail("Function's final statement must be SELECT or INSERT/UPDATE/DELETE/MERGE RETURNING.")));
return false; /* keep compiler quiet */
}
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
new file mode 100644
index 299c2c7..b8a3500
--- 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"
@@ -90,6 +89,7 @@ typedef struct ModifyTableContext
/* MERGE specific */
MergeActionState *relaction; /* MERGE action in progress */
+ int relaction_idx; /* its position in the list */
/*
* Information about the changes that were made concurrently to a tuple
@@ -153,13 +153,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);
/*
@@ -779,6 +780,19 @@ ExecInsert(ModifyTableContext *context,
slot = ExecPrepareTupleRouting(mtstate, estate, proute,
resultRelInfo, slot,
&partRelInfo);
+
+ /*
+ * If we're running MERGE, update the context's merge action to the
+ * corresponding action for this partition, so that we have the
+ * correct projection for RETURNING below.
+ */
+ if (mtstate->operation == CMD_MERGE)
+ context->relaction = (MergeActionState *)
+ list_nth(context->relaction->mas_action->matched ?
+ partRelInfo->ri_matchedMergeAction :
+ partRelInfo->ri_notMatchedMergeAction,
+ context->relaction_idx);
+
resultRelInfo = partRelInfo;
}
@@ -1193,7 +1207,12 @@ ExecInsert(ModifyTableContext *context,
if (resultRelInfo->ri_WithCheckOptions != NIL)
ExecWithCheckOptions(WCO_VIEW_CHECK, resultRelInfo, slot, estate);
- /* Process RETURNING if present */
+ /*
+ * Process RETURNING if present. If we're running MERGE, be sure to use
+ * the projection for the current merge action's RETURNING list, if any.
+ */
+ if (mtstate->operation == CMD_MERGE)
+ resultRelInfo->ri_projectReturning = context->relaction->mas_proj_returning;
if (resultRelInfo->ri_projectReturning)
result = ExecProcessReturning(resultRelInfo, slot, planSlot);
@@ -2712,6 +2731,7 @@ static TupleTableSlot *
ExecMerge(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
ItemPointer tupleid, bool canSetTag)
{
+ TupleTableSlot *rslot = NULL;
bool matched;
/*-----
@@ -2759,18 +2779,18 @@ 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()
- * returned "false", indicating the previously MATCHED tuple no longer
- * matches.
+ * Deal with the NOT MATCHED case (either a NOT MATCHED tuple from the
+ * join, or a previously MATCHED tuple for which ExecMergeMatched() set
+ * "matched" to false, indicating that it no longer matches).
*/
if (!matched)
- ExecMergeNotMatched(context, resultRelInfo, canSetTag);
+ rslot = ExecMergeNotMatched(context, resultRelInfo, canSetTag);
- /* No RETURNING support yet */
- return NULL;
+ return rslot;
}
/*
@@ -2780,8 +2800,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.
*
@@ -2790,16 +2810,17 @@ 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 *newslot;
+ TupleTableSlot *rslot = NULL;
+ TupleTableSlot *newslot = NULL;
EState *estate = context->estate;
ExprContext *econtext = mtstate->ps.ps_ExprContext;
bool isNull;
@@ -2810,7 +2831,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
@@ -2890,15 +2914,38 @@ lmerge_matched:
newslot = ExecProject(relaction->mas_proj);
context->relaction = relaction;
+ context->relaction_idx = foreach_current_index(l);
if (!ExecUpdatePrologue(context, resultRelInfo,
tupleid, NULL, newslot, &result))
{
if (result == TM_Ok)
- return true; /* "do nothing" */
+ {
+ *matched = true;
+ return NULL; /* "do nothing" */
+ }
break; /* concurrent update/delete */
}
result = ExecUpdateAct(context, resultRelInfo, tupleid, NULL,
newslot, false, &updateCxt);
+
+ /*
+ * If ExecUpdateAct reports that a cross-partition update was
+ * done, then the RETURNING tuple (if any) has been projected
+ * and there's nothing else for us to do.
+ *
+ * XXX: Pre-existing bug here? For a cross-partition update,
+ * this should not be calling ExecUpdateEpilogue(). Row update
+ * triggers should not be fired, etc.
+ */
+ if (updateCxt.crossPartUpdate)
+ {
+ *matched = true;
+ mtstate->mt_merge_updated += 1;
+ if (canSetTag)
+ (estate->es_processed)++;
+ return context->cpUpdateReturningSlot;
+ }
+
if (result == TM_Ok && updateCxt.updated)
{
ExecUpdateEpilogue(context, &updateCxt, resultRelInfo,
@@ -2909,11 +2956,15 @@ lmerge_matched:
case CMD_DELETE:
context->relaction = relaction;
+ context->relaction_idx = foreach_current_index(l);
if (!ExecDeletePrologue(context, resultRelInfo, tupleid,
NULL, NULL, &result))
{
if (result == TM_Ok)
- return true; /* "do nothing" */
+ {
+ *matched = true;
+ return NULL; /* "do nothing" */
+ }
break; /* concurrent update/delete */
}
result = ExecDeleteAct(context, resultRelInfo, tupleid, false);
@@ -2969,7 +3020,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:
{
@@ -3015,13 +3067,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
@@ -3056,7 +3114,8 @@ lmerge_matched:
* tuple already deleted; tell caller to run NOT
* MATCHED actions
*/
- return false;
+ *matched = false;
+ return NULL;
case TM_SelfModified:
@@ -3076,13 +3135,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;
}
}
@@ -3095,6 +3155,36 @@ lmerge_matched:
}
/*
+ * Process RETURNING if present, using the action's RETURNING list.
+ * Using the root relation's action is correct here, since we know
+ * that this wasn't a cross-partition update at this point.
+ */
+ if (relaction->mas_proj_returning)
+ {
+ resultRelInfo->ri_projectReturning = relaction->mas_proj_returning;
+
+ 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.
*/
@@ -3104,19 +3194,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;
/*
@@ -3167,9 +3260,10 @@ ExecMergeNotMatched(ModifyTableContext *
*/
newslot = ExecProject(action->mas_proj);
context->relaction = action;
+ context->relaction_idx = foreach_current_index(l);
- (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:
@@ -3185,6 +3279,8 @@ ExecMergeNotMatched(ModifyTableContext *
*/
break;
}
+
+ return rslot;
}
/*
@@ -3334,6 +3430,15 @@ ExecInitMerge(ModifyTableState *mtstate,
elog(ERROR, "unknown operation");
break;
}
+
+ /* Build a projection for the action's RETURNING list, if any */
+ if (action->returningList)
+ action_state->mas_proj_returning =
+ ExecBuildProjectionInfo(action->returningList,
+ econtext,
+ mtstate->ps.ps_ResultTupleSlot,
+ &mtstate->ps,
+ relationDesc);
}
}
}
@@ -3647,8 +3752,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");
@@ -3725,8 +3839,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");
@@ -4192,18 +4315,23 @@ ExecInitModifyTable(ModifyTable *node, E
econtext = mtstate->ps.ps_ExprContext;
/*
- * Build a projection for each result rel.
+ * Build a projection for each result rel. We skip this for MERGE,
+ * since it uses per-action RETURNING lists, which each have their own
+ * projections, built in ExecInitMerge().
*/
- resultRelInfo = mtstate->resultRelInfo;
- foreach(l, node->returningLists)
+ if (operation != CMD_MERGE)
{
- List *rlist = (List *) lfirst(l);
+ resultRelInfo = mtstate->resultRelInfo;
+ foreach(l, node->returningLists)
+ {
+ List *rlist = (List *) lfirst(l);
- resultRelInfo->ri_returningList = rlist;
- resultRelInfo->ri_projectReturning =
- ExecBuildProjectionInfo(rlist, econtext, slot, &mtstate->ps,
- resultRelInfo->ri_RelationDesc->rd_att);
- resultRelInfo++;
+ resultRelInfo->ri_returningList = rlist;
+ resultRelInfo->ri_projectReturning =
+ ExecBuildProjectionInfo(rlist, econtext, slot, &mtstate->ps,
+ resultRelInfo->ri_RelationDesc->rd_att);
+ resultRelInfo++;
+ }
}
}
else
diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c
new file mode 100644
index 3397568..b9f443c
--- a/src/backend/executor/spi.c
+++ b/src/backend/executor/spi.c
@@ -2033,6 +2033,8 @@ SPI_result_code_string(int code)
return "SPI_OK_TD_REGISTER";
case SPI_OK_MERGE:
return "SPI_OK_MERGE";
+ case SPI_OK_MERGE_RETURNING:
+ return "SPI_OK_MERGE_RETURNING";
}
/* Unrecognized code ... return something useful ... */
sprintf(buf, "Unrecognized SPI code %d", code);
@@ -2886,7 +2888,10 @@ _SPI_pquery(QueryDesc *queryDesc, bool f
res = SPI_OK_UPDATE;
break;
case CMD_MERGE:
- res = SPI_OK_MERGE;
+ if (queryDesc->plannedstmt->hasReturning)
+ res = SPI_OK_MERGE_RETURNING;
+ else
+ res = SPI_OK_MERGE;
break;
default:
return SPI_ERROR_OPUNKNOWN;
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
new file mode 100644
index c03f4f2..5ca63ae
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -2440,6 +2440,8 @@ expression_tree_walker_impl(Node *node,
return true;
if (WALK(action->targetList))
return true;
+ if (WALK(action->returningList))
+ return true;
}
break;
case T_PartitionPruneStepOp:
@@ -3421,6 +3423,7 @@ expression_tree_mutator_impl(Node *node,
FLATCOPY(newnode, action, MergeAction);
MUTATE(newnode->qual, action->qual, Node *);
MUTATE(newnode->targetList, action->targetList, List *);
+ MUTATE(newnode->returningList, action->returningList, List *);
return (Node *) newnode;
}
@@ -4061,6 +4064,8 @@ raw_expression_tree_walker_impl(Node *no
return true;
if (WALK(mergeWhenClause->values))
return true;
+ if (WALK(mergeWhenClause->returningList))
+ return true;
}
break;
case T_SelectStmt:
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
new file mode 100644
index a8cea5e..07d4f20
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -908,6 +908,11 @@ subquery_planner(PlannerGlobal *glob, Qu
preprocess_expression(root,
(Node *) action->qual,
EXPRKIND_QUAL);
+
+ action->returningList = (List *)
+ preprocess_expression(root,
+ (Node *) action->returningList,
+ EXPRKIND_TARGET);
}
root->append_rel_list = (List *)
@@ -1884,6 +1889,11 @@ grouping_planner(PlannerInfo *root, doub
(Node *) action->targetList,
this_result_rel,
top_result_rel);
+ leaf_action->returningList = (List *)
+ adjust_appendrel_attrs_multilevel(root,
+ (Node *) action->returningList,
+ this_result_rel,
+ top_result_rel);
if (leaf_action->commandType == CMD_UPDATE)
leaf_action->updateColnos =
adjust_inherited_attnums_multilevel(root,
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
new file mode 100644
index fc37095..000e204
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -1192,6 +1192,15 @@ set_plan_refs(PlannerInfo *root, Plan *p
rtoffset,
NRM_EQUAL,
NUM_EXEC_QUAL(plan));
+
+ /* Fix returningList too. */
+ action->returningList =
+ set_returning_clause_references(root,
+ action->returningList,
+ subplan,
+ resultrel,
+ rtoffset);
+
}
}
}
diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c
new file mode 100644
index 73ff407..ca2b176
--- a/src/backend/optimizer/prep/prepjointree.c
+++ b/src/backend/optimizer/prep/prepjointree.c
@@ -2153,6 +2153,8 @@ perform_pullup_replace_vars(PlannerInfo
action->qual = pullup_replace_vars(action->qual, rvcontext);
action->targetList = (List *)
pullup_replace_vars((Node *) action->targetList, rvcontext);
+ action->returningList = (List *)
+ pullup_replace_vars((Node *) action->returningList, rvcontext);
}
}
replace_vars_in_jointree((Node *) parse->jointree, rvcontext);
diff --git a/src/backend/optimizer/prep/preptlist.c b/src/backend/optimizer/prep/preptlist.c
new file mode 100644
index 9d46488..d209371
--- a/src/backend/optimizer/prep/preptlist.c
+++ b/src/backend/optimizer/prep/preptlist.c
@@ -144,6 +144,7 @@ preprocess_targetlist(PlannerInfo *root)
foreach(l, parse->mergeActionList)
{
MergeAction *action = (MergeAction *) lfirst(l);
+ List *exprs;
List *vars;
ListCell *l2;
@@ -156,14 +157,14 @@ preprocess_targetlist(PlannerInfo *root)
/*
* Add resjunk entries for any Vars and PlaceHolderVars used in
- * each action's targetlist and WHEN condition that belong to
- * relations other than the target. We don't expect to see any
- * aggregates or window functions here.
+ * each action's targetlist, WHEN condition, and returningList
+ * that belong to relations other than the target. We don't
+ * expect to see any aggregates or window functions here.
*/
- vars = pull_var_clause((Node *)
- list_concat_copy((List *) action->qual,
- action->targetList),
- PVC_INCLUDE_PLACEHOLDERS);
+ exprs = list_concat_copy((List *) action->qual, action->targetList);
+ exprs = list_concat(exprs, action->returningList);
+ vars = pull_var_clause((Node *) exprs, PVC_INCLUDE_PLACEHOLDERS);
+
foreach(l2, vars)
{
Var *var = (Var *) lfirst(l2);
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
new file mode 100644
index 7a1dfb6..a7021d2
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -74,7 +74,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,
@@ -2539,9 +2538,9 @@ transformUpdateTargetList(ParseState *ps
/*
* transformReturningList -
- * handle a RETURNING clause in INSERT/UPDATE/DELETE
+ * handle a RETURNING clause in INSERT/UPDATE/DELETE/MERGE
*/
-static List *
+List *
transformReturningList(ParseState *pstate, List *returningList)
{
List *rlist;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
new file mode 100644
index c224df4..b4ae1c5
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -438,7 +438,7 @@ static Node *makeRecursiveViewSelect(cha
any_operator expr_list attrs
distinct_clause opt_distinct_clause
target_list opt_target_list insert_column_list set_target_list
- merge_values_clause
+ merge_values_clause merge_returning_clause merge_target_list
set_clause_list set_clause
def_list operator_def_list indirection opt_indirection
reloption_list TriggerFuncArgs opclass_item_list opclass_drop_list
@@ -550,7 +550,7 @@ static Node *makeRecursiveViewSelect(cha
%type <range> extended_relation_expr
%type <range> relation_expr_opt_alias
%type <node> tablesample_clause opt_repeatable_clause
-%type <target> target_el set_target insert_column_item
+%type <target> target_el set_target insert_column_item merge_target_el
%type <str> generic_option_name
%type <node> generic_option_arg
@@ -835,7 +835,8 @@ static Node *makeRecursiveViewSelect(cha
* the same as non-keywords, reducing the risk of unwanted precedence effects.
*
* We need to do this for PARTITION, RANGE, ROWS, and GROUPS to support
- * opt_existing_window_name (see comment there).
+ * opt_existing_window_name (see comment there), and for WHEN to support
+ * merge_target_el.
*
* The frame_bound productions UNBOUNDED PRECEDING and UNBOUNDED FOLLOWING
* are even messier: since UNBOUNDED is an unreserved keyword (per spec!),
@@ -852,7 +853,7 @@ static Node *makeRecursiveViewSelect(cha
* Using the same precedence as IDENT seems right for the reasons given above.
*/
%nonassoc UNBOUNDED /* ideally would have same precedence as IDENT */
-%nonassoc IDENT PARTITION RANGE ROWS GROUPS PRECEDING FOLLOWING CUBE ROLLUP
+%nonassoc IDENT PARTITION RANGE ROWS GROUPS PRECEDING FOLLOWING CUBE ROLLUP WHEN
%left Op OPERATOR /* multi-character ops and user-defined operators */
%left '+' '-'
%left '*' '/' '%'
@@ -12379,6 +12380,7 @@ merge_when_clause:
m->matched = true;
m->commandType = CMD_NOTHING;
m->condition = $3;
+ m->location = @1;
$$ = (Node *) m;
}
@@ -12389,6 +12391,7 @@ merge_when_clause:
m->matched = false;
m->commandType = CMD_NOTHING;
m->condition = $4;
+ m->location = @1;
$$ = (Node *) m;
}
@@ -12400,75 +12403,89 @@ opt_merge_when_condition:
;
merge_update:
- UPDATE SET set_clause_list
+ UPDATE SET set_clause_list merge_returning_clause
{
MergeWhenClause *n = makeNode(MergeWhenClause);
n->commandType = CMD_UPDATE;
n->override = OVERRIDING_NOT_SET;
n->targetList = $3;
n->values = NIL;
+ n->returningList = $4;
+ n->location = @1;
$$ = n;
}
;
merge_delete:
- DELETE_P
+ DELETE_P merge_returning_clause
{
MergeWhenClause *n = makeNode(MergeWhenClause);
n->commandType = CMD_DELETE;
n->override = OVERRIDING_NOT_SET;
n->targetList = NIL;
n->values = NIL;
+ n->returningList = $2;
+ n->location = @1;
$$ = n;
}
;
merge_insert:
- INSERT merge_values_clause
+ INSERT merge_values_clause merge_returning_clause
{
MergeWhenClause *n = makeNode(MergeWhenClause);
n->commandType = CMD_INSERT;
n->override = OVERRIDING_NOT_SET;
n->targetList = NIL;
n->values = $2;
+ n->returningList = $3;
+ n->location = @1;
$$ = n;
}
- | INSERT OVERRIDING override_kind VALUE_P merge_values_clause
+ | INSERT OVERRIDING override_kind VALUE_P merge_values_clause merge_returning_clause
{
MergeWhenClause *n = makeNode(MergeWhenClause);
n->commandType = CMD_INSERT;
n->override = $3;
n->targetList = NIL;
n->values = $5;
+ n->returningList = $6;
+ n->location = @1;
$$ = n;
}
- | INSERT '(' insert_column_list ')' merge_values_clause
+ | INSERT '(' insert_column_list ')' merge_values_clause merge_returning_clause
{
MergeWhenClause *n = makeNode(MergeWhenClause);
n->commandType = CMD_INSERT;
n->override = OVERRIDING_NOT_SET;
n->targetList = $3;
n->values = $5;
+ n->returningList = $6;
+ n->location = @1;
$$ = n;
}
- | INSERT '(' insert_column_list ')' OVERRIDING override_kind VALUE_P merge_values_clause
+ | INSERT '(' insert_column_list ')' OVERRIDING override_kind VALUE_P merge_values_clause merge_returning_clause
{
MergeWhenClause *n = makeNode(MergeWhenClause);
n->commandType = CMD_INSERT;
n->override = $6;
n->targetList = $3;
n->values = $8;
+ n->returningList = $9;
+ n->location = @1;
$$ = n;
}
- | INSERT DEFAULT VALUES
+ | INSERT DEFAULT VALUES merge_returning_clause
{
MergeWhenClause *n = makeNode(MergeWhenClause);
n->commandType = CMD_INSERT;
n->override = OVERRIDING_NOT_SET;
n->targetList = NIL;
n->values = NIL;
+ n->returningList = $4;
+ n->location = @1;
$$ = n;
}
;
@@ -12480,6 +12497,64 @@ merge_values_clause:
}
;
+merge_returning_clause:
+ RETURNING merge_target_list { $$ = $2; }
+ | /* EMPTY */ { $$ = NIL; }
+ ;
+
+merge_target_list:
+ merge_target_el { $$ = list_make1($1); }
+ | merge_target_list ',' merge_target_el { $$ = lappend($1, $3); }
+ ;
+
+/*
+ * Given "RETURNING expr when ...", we have to decide without looking any
+ * further ahead whether the "when" is an alias or the start of the next WHEN
+ * clause. We resolve the shift/reduce conflict by giving the first
+ * merge_target_el production a slightly higher precedence than the WHEN token
+ * has, causing the parser to prefer to reduce, in effect assuming that the
+ * WHEN is not an alias.
+ */
+merge_target_el:
+ a_expr %prec Op
+ {
+ $$ = makeNode(ResTarget);
+ $$->name = NULL;
+ $$->indirection = NIL;
+ $$->val = (Node *) $1;
+ $$->location = @1;
+ }
+ | a_expr BareColLabel
+ {
+ $$ = makeNode(ResTarget);
+ $$->name = $2;
+ $$->indirection = NIL;
+ $$->val = (Node *) $1;
+ $$->location = @1;
+ }
+ | a_expr AS ColLabel
+ {
+ $$ = makeNode(ResTarget);
+ $$->name = $3;
+ $$->indirection = NIL;
+ $$->val = (Node *) $1;
+ $$->location = @1;
+ }
+ | '*'
+ {
+ ColumnRef *n = makeNode(ColumnRef);
+
+ n->fields = list_make1(makeNode(A_Star));
+ n->location = @1;
+
+ $$ = makeNode(ResTarget);
+ $$->name = NULL;
+ $$->indirection = NIL;
+ $$->val = (Node *) n;
+ $$->location = @1;
+ }
+ ;
+
/*****************************************************************************
*
* QUERY:
diff --git a/src/backend/parser/parse_cte.c b/src/backend/parser/parse_cte.c
new file mode 100644
index 6992a78..5cad6d8
--- 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_merge.c b/src/backend/parser/parse_merge.c
new file mode 100644
index 91b1156..367857a
--- a/src/backend/parser/parse_merge.c
+++ b/src/backend/parser/parse_merge.c
@@ -18,11 +18,13 @@
#include "access/sysattr.h"
#include "miscadmin.h"
#include "nodes/makefuncs.h"
+#include "nodes/nodeFuncs.h"
#include "parser/analyze.h"
#include "parser/parse_collate.h"
#include "parser/parsetree.h"
#include "parser/parser.h"
#include "parser/parse_clause.h"
+#include "parser/parse_coerce.h"
#include "parser/parse_cte.h"
#include "parser/parse_expr.h"
#include "parser/parse_merge.h"
@@ -103,6 +105,8 @@ transformMergeStmt(ParseState *pstate, M
List *mergeActionList;
Node *joinExpr;
ParseNamespaceItem *nsitem;
+ int returning_list_length = -1;
+ List **returning_colexprs = NULL;
/* There can't be any outer WITH to worry about */
Assert(pstate->p_ctenamespace == NIL);
@@ -385,14 +389,146 @@ transformMergeStmt(ParseState *pstate, M
elog(ERROR, "unknown action in MERGE WHEN clause");
}
+ /*
+ * Transform the RETURNING list, if any. All action RETURNING lists
+ * must be the same length, *after* transformation (which might expand
+ * '*' into multiple items). At this stage, we prevent resolving any
+ * unknown-type outputs as TEXT, allowing us to resolve such outputs
+ * using select_common_type() below.
+ */
+ if (mergeWhenClause->returningList)
+ {
+ bool save_resolve_unknowns = pstate->p_resolve_unknowns;
+ int i;
+ ListCell *lc2;
+
+ /* The RETURNING list can see both target and source relations */
+ setNamespaceVisibilityForRTE(pstate->p_namespace,
+ rt_fetch(qry->resultRelation,
+ pstate->p_rtable),
+ true, true);
+
+ setNamespaceVisibilityForRTE(pstate->p_namespace,
+ rt_fetch(sourceRTI,
+ pstate->p_rtable),
+ true, true);
+
+ pstate->p_resolve_unknowns = false;
+
+ action->returningList = transformReturningList(pstate,
+ mergeWhenClause->returningList);
+
+ pstate->p_resolve_unknowns = save_resolve_unknowns;
+
+ if (returning_list_length < 0)
+ {
+ /* Remember post-transformation length of first list */
+ returning_list_length = list_length(action->returningList);
+ /* and allocate array for per-column lists */
+ returning_colexprs = (List **) palloc0(returning_list_length * sizeof(List *));
+ }
+ else if (list_length(action->returningList) != returning_list_length)
+ ereport(ERROR,
+ errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("RETURNING lists must all be the same length"),
+ parser_errposition(pstate,
+ exprLocation((Node *) action->returningList)));
+
+ /* Build per-column RETURNING expression lists */
+ i = 0;
+ foreach(lc2, action->returningList)
+ {
+ TargetEntry *tle = lfirst_node(TargetEntry, lc2);
+
+ returning_colexprs[i] = lappend(returning_colexprs[i], tle->expr);
+ i++;
+ }
+ }
+ else
+ action->returningList = NIL;
+
mergeActionList = lappend(mergeActionList, action);
}
- qry->mergeActionList = mergeActionList;
+ /*
+ * Enforce the rule that if any action has a RETURNING list, all must
+ * have (except for DO NOTHING actions). We do this using a second pass
+ * over the merge actions to generate a more useful error message.
+ */
+ if (returning_list_length > 0)
+ {
+ ListCell *l2;
- /* RETURNING could potentially be added in the future, but not in SQL std */
+ forboth(l, stmt->mergeWhenClauses, l2, mergeActionList)
+ {
+ MergeWhenClause *mergeWhenClause = lfirst_node(MergeWhenClause, l);
+ MergeAction *action = lfirst_node(MergeAction, l2);
+
+ if (action->commandType != CMD_NOTHING && !action->returningList)
+ ereport(ERROR,
+ errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("missing RETURNING list for MERGE action"),
+ errhint("To enable RETURNING for MERGE, add a RETURNING list to every INSERT/UPDATE/DELETE action."),
+ parser_errposition(pstate, mergeWhenClause->location));
+ }
+ }
+
+ /*
+ * Resolve the common types of the columns of any RETURNING lists, and
+ * coerce everything to those types.
+ *
+ * Then identify the common typmod and common collation, if any, of each
+ * column, and build a top-level RETURNING list for the Query node, for
+ * use in outer queries. This RETURNING list is never evaluated, and just
+ * contains dummy expressions, to hold the common type, typmod, and
+ * collation information returned by this query.
+ */
qry->returningList = NULL;
+ for (int i = 0; i < returning_list_length; i++)
+ {
+ TargetEntry *first_tle = NULL;
+ Oid coltype;
+ int32 coltypmod;
+ Oid colcoll;
+ Const *con;
+ TargetEntry *new_tle;
+
+ coltype = select_common_type(pstate, returning_colexprs[i],
+ "RETURNING", NULL);
+
+ foreach(l, mergeActionList)
+ {
+ MergeAction *action = lfirst_node(MergeAction, l);
+
+ if (action->returningList)
+ {
+ TargetEntry *tle;
+
+ tle = list_nth_node(TargetEntry, action->returningList, i);
+ tle->expr = (Expr *) coerce_to_common_type(pstate,
+ (Node *) tle->expr,
+ coltype,
+ "RETURNING");
+ if (first_tle == NULL)
+ first_tle = tle;
+ }
+ }
+
+ coltypmod = select_common_typmod(pstate, returning_colexprs[i], coltype);
+ colcoll = select_common_collation(pstate, returning_colexprs[i], true);
+
+ con = makeNullConst(coltype, coltypmod, colcoll);
+ new_tle = makeTargetEntry((Expr *) con,
+ first_tle->resno,
+ pstrdup(first_tle->resname),
+ first_tle->resjunk);
+
+ qry->returningList = lappend(qry->returningList, new_tle);
+ }
+
+ qry->mergeActionList = mergeActionList;
+
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 864ea9b..cc19041
--- a/src/backend/parser/parse_relation.c
+++ b/src/backend/parser/parse_relation.c
@@ -2346,9 +2346,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 41a3623..3786a7c
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -3615,9 +3615,9 @@ RewriteQuery(Query *parsetree, List *rew
ListCell *lc1;
/*
- * First, recursively process any insert/update/delete statements in WITH
- * clauses. (We have to do this first because the WITH clauses may get
- * copied into rule actions below.)
+ * First, recursively process any insert/update/delete/merge statements in
+ * WITH clauses. (We have to do this first because the WITH clauses may
+ * get copied into rule actions below.)
*/
foreach(lc1, parsetree->cteList)
{
@@ -3642,7 +3642,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/rewrite/rowsecurity.c b/src/backend/rewrite/rowsecurity.c
new file mode 100644
index b1620e4..966dcf0
--- a/src/backend/rewrite/rowsecurity.c
+++ b/src/backend/rewrite/rowsecurity.c
@@ -395,10 +395,10 @@ get_row_security_policies(Query *root, R
* on the final action we take.
*
* We already fetched the SELECT policies above, to check existing rows,
- * but we must also check that new rows created by UPDATE actions are
- * visible, if SELECT rights are required for this relation. We don't do
- * this for INSERT actions, since an INSERT command would only do this
- * check if it had a RETURNING list, and MERGE does not support RETURNING.
+ * but we must also check that new rows created by INSERT/UPDATE actions
+ * are visible, if SELECT rights are required. For INSERT actions, we only
+ * do this if RETURNING is specified, to be consistent with a plain INSERT
+ * command, which can only require SELECT rights when RETURNING is used.
*
* 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
@@ -420,6 +420,8 @@ get_row_security_policies(Query *root, R
List *merge_delete_restrictive_policies;
List *merge_insert_permissive_policies;
List *merge_insert_restrictive_policies;
+ List *merge_select_permissive_policies = NIL;
+ List *merge_select_restrictive_policies = NIL;
/*
* Fetch the UPDATE policies and set them up to execute on the
@@ -457,9 +459,6 @@ get_row_security_policies(Query *root, R
*/
if (perminfo->requiredPerms & ACL_SELECT)
{
- List *merge_select_permissive_policies;
- List *merge_select_restrictive_policies;
-
get_policies_for_relation(rel, CMD_SELECT, user_id,
&merge_select_permissive_policies,
&merge_select_restrictive_policies);
@@ -508,6 +507,21 @@ get_row_security_policies(Query *root, R
withCheckOptions,
hasSubLinks,
false);
+
+ /*
+ * Add ALL/SELECT policies as WCO_RLS_INSERT_CHECK WCOs, to ensure
+ * that the inserted row is visible when executing an INSERT action,
+ * if RETURNING is specified and SELECT rights are required for this
+ * relation.
+ */
+ if (perminfo->requiredPerms & ACL_SELECT && root->returningList)
+ add_with_check_options(rel, rt_index,
+ WCO_RLS_INSERT_CHECK,
+ merge_select_permissive_policies,
+ merge_select_restrictive_policies,
+ withCheckOptions,
+ hasSubLinks,
+ true);
}
table_close(rel, NoLock);
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
new file mode 100644
index e3ccf6c..6f3dc89
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -2142,11 +2142,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/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
new file mode 100644
index ed7f40f..b99118c
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -7195,10 +7195,16 @@ get_merge_query_def(Query *query, depars
appendStringInfoString(buf, "DELETE");
else if (action->commandType == CMD_NOTHING)
appendStringInfoString(buf, "DO NOTHING");
- }
- /* No RETURNING support in MERGE yet */
- Assert(query->returningList == NIL);
+ /* Add RETURNING if present */
+ if (action->returningList)
+ {
+ appendContextKeyword(context, " RETURNING",
+ -PRETTYINDENT_STD, PRETTYINDENT_STD, 4);
+ get_target_list(action->returningList, context, NULL,
+ colNamesVisible);
+ }
+ }
}
diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c
new file mode 100644
index daabf6f..6b63214
--- a/src/bin/psql/common.c
+++ b/src/bin/psql/common.c
@@ -983,13 +983,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/executor/spi.h b/src/include/executor/spi.h
new file mode 100644
index d1de139..ed8787d
--- a/src/include/executor/spi.h
+++ b/src/include/executor/spi.h
@@ -97,6 +97,7 @@ typedef struct _SPI_plan *SPIPlanPtr;
#define SPI_OK_REL_UNREGISTER 16
#define SPI_OK_TD_REGISTER 17
#define SPI_OK_MERGE 18
+#define SPI_OK_MERGE_RETURNING 19
#define SPI_OPT_NONATOMIC (1 << 0)
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
new file mode 100644
index 5d7f17d..6c27fee
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -419,6 +419,8 @@ typedef struct MergeActionState
ProjectionInfo *mas_proj; /* projection of the action's targetlist for
* this rel */
ExprState *mas_whenqual; /* WHEN [NOT] MATCHED AND conditions */
+ /* projection of the action's returningList for this rel */
+ ProjectionInfo *mas_proj_returning;
} MergeActionState;
/*
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
new file mode 100644
index cf7e790..13bc017
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -1679,6 +1679,8 @@ typedef struct MergeWhenClause
List *targetList; /* INSERT/UPDATE targetlist */
/* the following members are only used in INSERT actions */
List *values; /* VALUES to INSERT, or NULL */
+ List *returningList; /* list of expressions to return */
+ int location; /* token location, or -1 if unknown */
} MergeWhenClause;
/*
@@ -1696,6 +1698,7 @@ typedef struct MergeAction
List *targetList; /* the target list (of TargetEntry) */
/* target attribute numbers of an UPDATE */
List *updateColnos pg_node_attr(query_jumble_ignore);
+ List *returningList; /* list of expressions to return */
} MergeAction;
/*
diff --git a/src/include/parser/analyze.h b/src/include/parser/analyze.h
new file mode 100644
index c96483a..14e6877
--- a/src/include/parser/analyze.h
+++ b/src/include/parser/analyze.h
@@ -44,6 +44,7 @@ extern List *transformInsertRow(ParseSta
bool strip_indirection);
extern List *transformUpdateTargetList(ParseState *pstate,
List *origTlist);
+extern List *transformReturningList(ParseState *pstate, List *returningList);
extern Query *transformTopLevelStmt(ParseState *pstate, RawStmt *parseTree);
extern Query *transformStmt(ParseState *pstate, Node *parseTree);
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
new file mode 100644
index 4b76f76..fb6289c
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -4267,9 +4267,9 @@ exec_stmt_execsql(PLpgSQL_execstate *est
/*
* If we have INTO, then we only need one row back ... but if we have INTO
* STRICT or extra check too_many_rows, ask for two rows, so that we can
- * verify the statement returns only one. INSERT/UPDATE/DELETE are always
- * treated strictly. Without INTO, just run the statement to completion
- * (tcount = 0).
+ * verify the statement returns only one. INSERT/UPDATE/DELETE/MERGE are
+ * always treated strictly. Without INTO, just run the statement to
+ * completion (tcount = 0).
*
* We could just ask for two rows always when using INTO, but there are
* some cases where demanding the extra row costs significant time, eg by
@@ -4307,10 +4307,11 @@ exec_stmt_execsql(PLpgSQL_execstate *est
case SPI_OK_INSERT:
case SPI_OK_UPDATE:
case SPI_OK_DELETE:
+ case SPI_OK_MERGE:
case SPI_OK_INSERT_RETURNING:
case SPI_OK_UPDATE_RETURNING:
case SPI_OK_DELETE_RETURNING:
- case SPI_OK_MERGE:
+ case SPI_OK_MERGE_RETURNING:
Assert(stmt->mod_stmt);
exec_set_found(estate, (SPI_processed != 0));
break;
@@ -4489,10 +4490,11 @@ exec_stmt_dynexecute(PLpgSQL_execstate *
case SPI_OK_INSERT:
case SPI_OK_UPDATE:
case SPI_OK_DELETE:
+ case SPI_OK_MERGE:
case SPI_OK_INSERT_RETURNING:
case SPI_OK_UPDATE_RETURNING:
case SPI_OK_DELETE_RETURNING:
- case SPI_OK_MERGE:
+ case SPI_OK_MERGE_RETURNING:
case SPI_OK_UTILITY:
case SPI_OK_REWRITTEN:
break;
diff --git a/src/pl/tcl/pltcl.c b/src/pl/tcl/pltcl.c
new file mode 100644
index e8f9d7b..0c9f81e
--- a/src/pl/tcl/pltcl.c
+++ b/src/pl/tcl/pltcl.c
@@ -2456,6 +2456,7 @@ pltcl_process_SPI_result(Tcl_Interp *int
case SPI_OK_INSERT_RETURNING:
case SPI_OK_DELETE_RETURNING:
case SPI_OK_UPDATE_RETURNING:
+ case SPI_OK_MERGE_RETURNING:
/*
* Process the tuples we got
diff --git a/src/test/regress/expected/merge.out b/src/test/regress/expected/merge.out
new file mode 100644
index 28a6d0b..8dafe10
--- 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;
@@ -1256,7 +1256,7 @@ BEGIN;
MERGE INTO sq_target
USING v
ON tid = sid
-WHEN MATCHED AND tid > 2 THEN
+WHEN MATCHED AND tid >= 2 THEN
UPDATE SET balance = balance + delta
WHEN NOT MATCHED THEN
INSERT (balance, tid) VALUES (balance + delta, sid)
@@ -1271,7 +1271,7 @@ INSERT INTO sq_source (sid, balance, del
MERGE INTO sq_target t
USING v
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)
@@ -1295,7 +1295,7 @@ WITH targq AS (
MERGE INTO sq_target t
USING v
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)
@@ -1304,20 +1304,183 @@ 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
+ RETURNING 1 AS clause_number, 'upd' AS merge_action, t.*,
+ 'Added '||delta||' to balance' AS description
WHEN NOT MATCHED THEN
INSERT (balance, tid) VALUES (balance + delta, sid)
+ RETURNING 2, 'ins', t.*, 'Inserted '||t
WHEN MATCHED AND tid < 2 THEN
DELETE
-RETURNING *;
-ERROR: syntax error at or near "RETURNING"
-LINE 10: RETURNING *;
- ^
+ RETURNING 3, 'del', t.*, 'Removed '||t;
+ clause_number | merge_action | tid | balance | description
+---------------+--------------+-----+---------+---------------------
+ 3 | del | 1 | 100 | Removed (1,100)
+ 1 | upd | 2 | 220 | Added 20 to balance
+ 2 | ins | 4 | 40 | Inserted (4,40)
+(3 rows)
+
+ROLLBACK;
+-- RETURNING in CTEs
+CREATE TABLE sq_target_merge_log (tid integer NOT NULL, last_change text);
+INSERT INTO sq_target_merge_log VALUES (1, 'Original value');
+BEGIN;
+WITH m AS (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + delta
+ RETURNING 1 AS clause_number, 'UPDATE' AS merge_action, t.*,
+ 'Added '||delta||' to balance' AS description
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (balance + delta, sid)
+ RETURNING 2, 'INSERT', t.*, 'Inserted '||t AS colname_ignored
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING 3, 'DELETE', t.*, 'Removed '||t
+), m2 AS (
+ MERGE INTO sq_target_merge_log l
+ USING m
+ ON l.tid = m.tid
+ WHEN MATCHED THEN
+ UPDATE SET last_change = description
+ RETURNING merge_action, 'UPDATE' AS merge_log_action, l.*
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (m.tid, description)
+ RETURNING merge_action, 'INSERT', l.*
+)
+SELECT * FROM m2;
+ merge_action | merge_log_action | tid | last_change
+--------------+------------------+-----+---------------------
+ DELETE | UPDATE | 1 | Removed (1,100)
+ UPDATE | INSERT | 2 | Added 20 to balance
+ INSERT | INSERT | 4 | Inserted (4,40)
+(3 rows)
+
+SELECT * FROM sq_target_merge_log ORDER BY tid;
+ tid | last_change
+-----+---------------------
+ 1 | Removed (1,100)
+ 2 | Added 20 to balance
+ 4 | Inserted (4,40)
+(3 rows)
+
+ROLLBACK;
+-- COPY (MERGE ... RETURNING) TO ...
+BEGIN;
+COPY (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + delta
+ RETURNING 'UPDATE', t.*
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (balance + delta, sid)
+ RETURNING 'INSERT', t.*
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING 'DELETE', t.*
+) TO stdout;
+DELETE 1 100
+UPDATE 2 220
+INSERT 4 40
+ROLLBACK;
+-- SQL function with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT action text, OUT tid int, OUT new_balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ RETURNING 'UPDATE', t.*
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ RETURNING 'INSERT', t.*
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING 'DELETE', t.*;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT action, tid, new_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ action | tid | new_balance
+--------+-----+-------------
+ DELETE | 1 | 100
+ UPDATE | 3 | 320
+ INSERT | 4 | 110
+(3 rows)
+
+ROLLBACK;
+-- SQL SRF with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_sq_source_into_sq_target()
+RETURNS TABLE (action text, tid int, balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + delta
+ RETURNING 'UPDATE', t.*
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (balance + delta, sid)
+ RETURNING 'INSERT', t.*
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING 'DELETE', t.*;
+$$;
+SELECT * FROM merge_sq_source_into_sq_target();
+ action | tid | balance
+--------+-----+---------
+ DELETE | 1 | 100
+ UPDATE | 2 | 220
+ INSERT | 4 | 40
+(3 rows)
+
+ROLLBACK;
+-- PL/pgSQL function with MERGE ... RETURNING ... INTO
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT r_action text, OUT r_tid int, OUT r_balance int)
+LANGUAGE plpgsql AS
+$$
+BEGIN
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ RETURNING 'UPDATE', t.*
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ RETURNING 'INSERT', t.*
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING 'DELETE', t.*
+ INTO r_action, r_tid, r_balance;
+END;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT r_action, r_tid, r_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ r_action | r_tid | r_balance
+----------+-------+-----------
+ DELETE | 1 | 100
+ UPDATE | 3 | 320
+ INSERT | 4 | 110
+(3 rows)
+
ROLLBACK;
-- EXPLAIN
CREATE TABLE ex_mtarget (a int, b int)
@@ -1514,7 +1677,7 @@ SELECT * FROM sq_target WHERE tid = 1;
(1 row)
ROLLBACK;
-DROP TABLE sq_target, sq_source CASCADE;
+DROP TABLE sq_target, sq_target_merge_log, sq_source CASCADE;
NOTICE: drop cascades to view v
CREATE TABLE pa_target (tid integer, balance float, val text)
PARTITION BY LIST (tid);
@@ -1640,6 +1803,32 @@ SELECT * FROM pa_target ORDER BY tid;
(14 rows)
ROLLBACK;
+-- update partition key to partition not initially scanned
+BEGIN;
+MERGE INTO pa_target t
+ USING pa_source s
+ ON t.tid = s.sid AND t.tid = 1
+ WHEN MATCHED THEN
+ UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
+ RETURNING t.*;
+ tid | balance | val
+-----+---------+--------------------------
+ 2 | 110 | initial updated by merge
+(1 row)
+
+SELECT * FROM pa_target ORDER BY tid;
+ tid | balance | val
+-----+---------+--------------------------
+ 2 | 110 | initial updated by merge
+ 3 | 300 | initial
+ 5 | 500 | initial
+ 7 | 700 | initial
+ 9 | 900 | initial
+ 11 | 1100 | initial
+ 13 | 1300 | initial
+(7 rows)
+
+ROLLBACK;
DROP TABLE pa_target CASCADE;
-- The target table is partitioned in the same way, but this time by attaching
-- partitions which have columns in different order, dropped columns etc.
@@ -1730,8 +1919,28 @@ MERGE INTO pa_target t
ON t.tid = s.sid
WHEN MATCHED THEN
UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
+ RETURNING 'UPDATE' AS merge_action, t.*
WHEN NOT MATCHED THEN
- INSERT VALUES (sid, delta, 'inserted by merge');
+ INSERT VALUES (sid, delta, 'inserted by merge')
+ RETURNING 'INSERT', t.*;
+ merge_action | tid | balance | val
+--------------+-----+---------+--------------------------
+ UPDATE | 2 | 110 | initial updated by merge
+ INSERT | 2 | 20 | inserted by merge
+ UPDATE | 4 | 330 | initial updated by merge
+ INSERT | 4 | 40 | inserted by merge
+ UPDATE | 6 | 550 | initial updated by merge
+ INSERT | 6 | 60 | inserted by merge
+ UPDATE | 8 | 770 | initial updated by merge
+ INSERT | 8 | 80 | inserted by merge
+ UPDATE | 10 | 990 | initial updated by merge
+ INSERT | 10 | 100 | inserted by merge
+ UPDATE | 12 | 1210 | initial updated by merge
+ INSERT | 12 | 120 | inserted by merge
+ UPDATE | 14 | 1430 | initial updated by merge
+ INSERT | 14 | 140 | inserted by merge
+(14 rows)
+
SELECT * FROM pa_target ORDER BY tid;
tid | balance | val
-----+---------+--------------------------
@@ -1797,8 +2006,23 @@ MERGE INTO pa_target t
ON t.tid = s.sid
WHEN MATCHED THEN
UPDATE SET balance = balance + delta, val = val || ' updated by merge'
+ RETURNING 'UPDATE' AS merge_action, t.*
WHEN NOT MATCHED THEN
- INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge');
+ INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge')
+ RETURNING 'INSERT' AS merge_action, t.*;
+ merge_action | logts | tid | balance | val
+--------------+--------------------------+-----+---------+--------------------------
+ UPDATE | Tue Jan 31 00:00:00 2017 | 1 | 110 | initial updated by merge
+ UPDATE | Tue Feb 28 00:00:00 2017 | 2 | 220 | initial updated by merge
+ INSERT | Sun Jan 15 00:00:00 2017 | 3 | 30 | inserted by merge
+ UPDATE | Tue Jan 31 00:00:00 2017 | 4 | 440 | initial updated by merge
+ UPDATE | Tue Feb 28 00:00:00 2017 | 5 | 550 | initial updated by merge
+ INSERT | Sun Jan 15 00:00:00 2017 | 6 | 60 | inserted by merge
+ UPDATE | Tue Jan 31 00:00:00 2017 | 7 | 770 | initial updated by merge
+ UPDATE | Tue Feb 28 00:00:00 2017 | 8 | 880 | initial updated by merge
+ INSERT | Sun Jan 15 00:00:00 2017 | 9 | 90 | inserted by merge
+(9 rows)
+
SELECT * FROM pa_target ORDER BY tid;
logts | tid | balance | val
--------------------------+-----+---------+--------------------------
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
new file mode 100644
index 6988128..d0c385d
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -2302,6 +2302,37 @@ WHEN MATCHED THEN
UPDATE SET dnotes = dnotes || ' notes added by merge8 '
WHEN NOT MATCHED THEN
INSERT VALUES (13, 44, 1, 'regress_rls_bob', 'new manga');
+SELECT * FROM document WHERE did = 13;
+ did | cid | dlevel | dauthor | dtitle | dnotes
+-----+-----+--------+---------+--------+--------
+(0 rows)
+
+-- but not OK if RETURNING is used
+MERGE INTO document d
+USING (SELECT 14 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge9 '
+ RETURNING *
+WHEN NOT MATCHED THEN
+ INSERT VALUES (14, 44, 1, 'regress_rls_bob', 'new manga')
+ RETURNING *;
+ERROR: new row violates row-level security policy for table "document"
+-- but OK if new row is visible
+MERGE INTO document d
+USING (SELECT 14 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge10 '
+ RETURNING *
+WHEN NOT MATCHED THEN
+ INSERT VALUES (14, 11, 1, 'regress_rls_bob', 'new novel')
+ RETURNING *;
+ sdid | did | cid | dlevel | dauthor | dtitle | dnotes
+------+-----+-----+--------+-----------------+-----------+--------
+ 14 | 14 | 11 | 1 | regress_rls_bob | new novel |
+(1 row)
+
RESET SESSION AUTHORIZATION;
-- drop the restrictive SELECT policy so that we can look at the
-- final state of the table
@@ -2325,7 +2356,8 @@ SELECT * FROM document;
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 notes added by merge7
13 | 44 | 1 | regress_rls_bob | new manga |
-(15 rows)
+ 14 | 11 | 1 | regress_rls_bob | new novel |
+(16 rows)
--
-- ROLE/GROUP
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
new file mode 100644
index 2c60400..e8b1b0d
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -3597,7 +3597,8 @@ MERGE INTO rule_merge2 t USING (SELECT 1
-- test deparsing
CREATE TABLE sf_target(id int, data text, filling int[]);
CREATE FUNCTION merge_sf_test()
- RETURNS void
+ RETURNS TABLE(merge_action text, when_clause int,
+ a int, b text, id int, data text, filling int[])
LANGUAGE sql
BEGIN ATOMIC
MERGE INTO sf_target t
@@ -3606,39 +3607,48 @@ BEGIN ATOMIC
WHEN MATCHED
AND (s.a + t.id) = 42
THEN UPDATE SET data = repeat(t.data, s.a) || s.b, id = length(s.b)
+ RETURNING 'UPDATE', 1, *
WHEN NOT MATCHED
AND (s.b IS NOT NULL)
THEN INSERT (data, id)
VALUES (s.b, s.a)
+ RETURNING 'INSERT', 2, *
WHEN MATCHED
AND length(s.b || t.data) > 10
THEN UPDATE SET data = s.b
+ RETURNING 'UPDATE', 3, *
WHEN MATCHED
AND s.a > 200
THEN UPDATE SET filling[s.a] = t.id
+ RETURNING 'UPDATE', 4, *
WHEN MATCHED
AND s.a > 100
THEN DELETE
+ RETURNING 'DELETE', 5, *
WHEN MATCHED
THEN DO NOTHING
WHEN NOT MATCHED
AND s.a > 200
THEN INSERT DEFAULT VALUES
+ RETURNING 'INSERT', 6, *
WHEN NOT MATCHED
AND s.a > 100
THEN INSERT (id, data) OVERRIDING USER VALUE
VALUES (s.a, DEFAULT)
+ RETURNING 'INSERT', 7, *
WHEN NOT MATCHED
AND s.a > 0
THEN INSERT
VALUES (s.a, s.b, DEFAULT)
+ RETURNING 'INSERT', 8, *
WHEN NOT MATCHED
THEN INSERT (filling[1], id)
- VALUES (s.a, s.a);
+ VALUES (s.a, s.a)
+ RETURNING 'INSERT', 9, *;
END;
\sf merge_sf_test
CREATE OR REPLACE FUNCTION public.merge_sf_test()
- RETURNS void
+ RETURNS TABLE(merge_action text, when_clause integer, a integer, b text, id integer, data text, filling integer[])
LANGUAGE sql
BEGIN ATOMIC
MERGE INTO sf_target t
@@ -3647,35 +3657,98 @@ BEGIN ATOMIC
WHEN MATCHED
AND ((s.a + t.id) = 42)
THEN UPDATE SET data = (repeat(t.data, s.a) || s.b), id = length(s.b)
+ RETURNING 'UPDATE'::text,
+ 1,
+ s.a,
+ s.b,
+ t.id,
+ t.data,
+ t.filling
WHEN NOT MATCHED
AND (s.b IS NOT NULL)
THEN INSERT (data, id)
VALUES (s.b, s.a)
+ RETURNING 'INSERT'::text,
+ 2,
+ s.a,
+ s.b,
+ t.id,
+ t.data,
+ t.filling
WHEN MATCHED
AND (length((s.b || t.data)) > 10)
THEN UPDATE SET data = s.b
+ RETURNING 'UPDATE'::text,
+ 3,
+ s.a,
+ s.b,
+ t.id,
+ t.data,
+ t.filling
WHEN MATCHED
AND (s.a > 200)
THEN UPDATE SET filling[s.a] = t.id
+ RETURNING 'UPDATE'::text,
+ 4,
+ s.a,
+ s.b,
+ t.id,
+ t.data,
+ t.filling
WHEN MATCHED
AND (s.a > 100)
THEN DELETE
+ RETURNING 'DELETE'::text,
+ 5,
+ s.a,
+ s.b,
+ t.id,
+ t.data,
+ t.filling
WHEN MATCHED
THEN DO NOTHING
WHEN NOT MATCHED
AND (s.a > 200)
THEN INSERT DEFAULT VALUES
+ RETURNING 'INSERT'::text,
+ 6,
+ s.a,
+ s.b,
+ t.id,
+ t.data,
+ t.filling
WHEN NOT MATCHED
AND (s.a > 100)
THEN INSERT (id, data) OVERRIDING USER VALUE
VALUES (s.a, DEFAULT)
+ RETURNING 'INSERT'::text,
+ 7,
+ s.a,
+ s.b,
+ t.id,
+ t.data,
+ t.filling
WHEN NOT MATCHED
AND (s.a > 0)
THEN INSERT (id, data, filling)
VALUES (s.a, s.b, DEFAULT)
+ RETURNING 'INSERT'::text,
+ 8,
+ s.a,
+ s.b,
+ t.id,
+ t.data,
+ t.filling
WHEN NOT MATCHED
THEN INSERT (filling[1], id)
- VALUES (s.a, s.a);
+ VALUES (s.a, s.a)
+ RETURNING 'INSERT'::text,
+ 9,
+ s.a,
+ s.b,
+ t.id,
+ t.data,
+ t.filling;
END
DROP FUNCTION merge_sf_test;
DROP TABLE sf_target;
diff --git a/src/test/regress/sql/merge.sql b/src/test/regress/sql/merge.sql
new file mode 100644
index 4cf6db9..32a70e7
--- 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
@@ -816,7 +816,7 @@ BEGIN;
MERGE INTO sq_target
USING v
ON tid = sid
-WHEN MATCHED AND tid > 2 THEN
+WHEN MATCHED AND tid >= 2 THEN
UPDATE SET balance = balance + delta
WHEN NOT MATCHED THEN
INSERT (balance, tid) VALUES (balance + delta, sid)
@@ -829,7 +829,7 @@ INSERT INTO sq_source (sid, balance, del
MERGE INTO sq_target t
USING v
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)
@@ -847,7 +847,7 @@ WITH targq AS (
MERGE INTO sq_target t
USING v
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)
@@ -857,17 +857,143 @@ 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
+ RETURNING 1 AS clause_number, 'upd' AS merge_action, t.*,
+ 'Added '||delta||' to balance' AS description
WHEN NOT MATCHED THEN
INSERT (balance, tid) VALUES (balance + delta, sid)
+ RETURNING 2, 'ins', t.*, 'Inserted '||t
WHEN MATCHED AND tid < 2 THEN
DELETE
-RETURNING *;
+ RETURNING 3, 'del', t.*, 'Removed '||t;
+ROLLBACK;
+
+-- RETURNING in CTEs
+CREATE TABLE sq_target_merge_log (tid integer NOT NULL, last_change text);
+INSERT INTO sq_target_merge_log VALUES (1, 'Original value');
+BEGIN;
+WITH m AS (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + delta
+ RETURNING 1 AS clause_number, 'UPDATE' AS merge_action, t.*,
+ 'Added '||delta||' to balance' AS description
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (balance + delta, sid)
+ RETURNING 2, 'INSERT', t.*, 'Inserted '||t AS colname_ignored
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING 3, 'DELETE', t.*, 'Removed '||t
+), m2 AS (
+ MERGE INTO sq_target_merge_log l
+ USING m
+ ON l.tid = m.tid
+ WHEN MATCHED THEN
+ UPDATE SET last_change = description
+ RETURNING merge_action, 'UPDATE' AS merge_log_action, l.*
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (m.tid, description)
+ RETURNING merge_action, 'INSERT', l.*
+)
+SELECT * FROM m2;
+SELECT * FROM sq_target_merge_log ORDER BY tid;
+ROLLBACK;
+
+-- COPY (MERGE ... RETURNING) TO ...
+BEGIN;
+COPY (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + delta
+ RETURNING 'UPDATE', t.*
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (balance + delta, sid)
+ RETURNING 'INSERT', t.*
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING 'DELETE', t.*
+) TO stdout;
+ROLLBACK;
+
+-- SQL function with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT action text, OUT tid int, OUT new_balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ RETURNING 'UPDATE', t.*
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ RETURNING 'INSERT', t.*
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING 'DELETE', t.*;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT action, tid, new_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ROLLBACK;
+
+-- SQL SRF with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_sq_source_into_sq_target()
+RETURNS TABLE (action text, tid int, balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + delta
+ RETURNING 'UPDATE', t.*
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (balance + delta, sid)
+ RETURNING 'INSERT', t.*
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING 'DELETE', t.*;
+$$;
+SELECT * FROM merge_sq_source_into_sq_target();
+ROLLBACK;
+
+-- PL/pgSQL function with MERGE ... RETURNING ... INTO
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT r_action text, OUT r_tid int, OUT r_balance int)
+LANGUAGE plpgsql AS
+$$
+BEGIN
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ RETURNING 'UPDATE', t.*
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ RETURNING 'INSERT', t.*
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING 'DELETE', t.*
+ INTO r_action, r_tid, r_balance;
+END;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT r_action, r_tid, r_balance FROM merge_into_sq_target(sid, balance, delta)) m;
ROLLBACK;
-- EXPLAIN
@@ -966,7 +1092,7 @@ WHEN MATCHED THEN
SELECT * FROM sq_target WHERE tid = 1;
ROLLBACK;
-DROP TABLE sq_target, sq_source CASCADE;
+DROP TABLE sq_target, sq_target_merge_log, sq_source CASCADE;
CREATE TABLE pa_target (tid integer, balance float, val text)
PARTITION BY LIST (tid);
@@ -1033,6 +1159,17 @@ SELECT merge_func();
SELECT * FROM pa_target ORDER BY tid;
ROLLBACK;
+-- update partition key to partition not initially scanned
+BEGIN;
+MERGE INTO pa_target t
+ USING pa_source s
+ ON t.tid = s.sid AND t.tid = 1
+ WHEN MATCHED THEN
+ UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
+ RETURNING t.*;
+SELECT * FROM pa_target ORDER BY tid;
+ROLLBACK;
+
DROP TABLE pa_target CASCADE;
-- The target table is partitioned in the same way, but this time by attaching
@@ -1090,8 +1227,10 @@ MERGE INTO pa_target t
ON t.tid = s.sid
WHEN MATCHED THEN
UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
+ RETURNING 'UPDATE' AS merge_action, t.*
WHEN NOT MATCHED THEN
- INSERT VALUES (sid, delta, 'inserted by merge');
+ INSERT VALUES (sid, delta, 'inserted by merge')
+ RETURNING 'INSERT', t.*;
SELECT * FROM pa_target ORDER BY tid;
ROLLBACK;
@@ -1144,8 +1283,10 @@ MERGE INTO pa_target t
ON t.tid = s.sid
WHEN MATCHED THEN
UPDATE SET balance = balance + delta, val = val || ' updated by merge'
+ RETURNING 'UPDATE' AS merge_action, t.*
WHEN NOT MATCHED THEN
- INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge');
+ INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge')
+ RETURNING 'INSERT' AS merge_action, t.*;
SELECT * FROM pa_target ORDER BY tid;
ROLLBACK;
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
new file mode 100644
index dec7340..8c093ee
--- a/src/test/regress/sql/rowsecurity.sql
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -992,6 +992,29 @@ WHEN MATCHED THEN
UPDATE SET dnotes = dnotes || ' notes added by merge8 '
WHEN NOT MATCHED THEN
INSERT VALUES (13, 44, 1, 'regress_rls_bob', 'new manga');
+SELECT * FROM document WHERE did = 13;
+
+-- but not OK if RETURNING is used
+MERGE INTO document d
+USING (SELECT 14 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge9 '
+ RETURNING *
+WHEN NOT MATCHED THEN
+ INSERT VALUES (14, 44, 1, 'regress_rls_bob', 'new manga')
+ RETURNING *;
+
+-- but OK if new row is visible
+MERGE INTO document d
+USING (SELECT 14 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge10 '
+ RETURNING *
+WHEN NOT MATCHED THEN
+ INSERT VALUES (14, 11, 1, 'regress_rls_bob', 'new novel')
+ RETURNING *;
RESET SESSION AUTHORIZATION;
-- drop the restrictive SELECT policy so that we can look at the
diff --git a/src/test/regress/sql/rules.sql b/src/test/regress/sql/rules.sql
new file mode 100644
index 8b7e255..92d2ccd
--- a/src/test/regress/sql/rules.sql
+++ b/src/test/regress/sql/rules.sql
@@ -1281,7 +1281,8 @@ MERGE INTO rule_merge2 t USING (SELECT 1
CREATE TABLE sf_target(id int, data text, filling int[]);
CREATE FUNCTION merge_sf_test()
- RETURNS void
+ RETURNS TABLE(merge_action text, when_clause int,
+ a int, b text, id int, data text, filling int[])
LANGUAGE sql
BEGIN ATOMIC
MERGE INTO sf_target t
@@ -1290,35 +1291,44 @@ BEGIN ATOMIC
WHEN MATCHED
AND (s.a + t.id) = 42
THEN UPDATE SET data = repeat(t.data, s.a) || s.b, id = length(s.b)
+ RETURNING 'UPDATE', 1, *
WHEN NOT MATCHED
AND (s.b IS NOT NULL)
THEN INSERT (data, id)
VALUES (s.b, s.a)
+ RETURNING 'INSERT', 2, *
WHEN MATCHED
AND length(s.b || t.data) > 10
THEN UPDATE SET data = s.b
+ RETURNING 'UPDATE', 3, *
WHEN MATCHED
AND s.a > 200
THEN UPDATE SET filling[s.a] = t.id
+ RETURNING 'UPDATE', 4, *
WHEN MATCHED
AND s.a > 100
THEN DELETE
+ RETURNING 'DELETE', 5, *
WHEN MATCHED
THEN DO NOTHING
WHEN NOT MATCHED
AND s.a > 200
THEN INSERT DEFAULT VALUES
+ RETURNING 'INSERT', 6, *
WHEN NOT MATCHED
AND s.a > 100
THEN INSERT (id, data) OVERRIDING USER VALUE
VALUES (s.a, DEFAULT)
+ RETURNING 'INSERT', 7, *
WHEN NOT MATCHED
AND s.a > 0
THEN INSERT
VALUES (s.a, s.b, DEFAULT)
+ RETURNING 'INSERT', 8, *
WHEN NOT MATCHED
THEN INSERT (filling[1], id)
- VALUES (s.a, s.a);
+ VALUES (s.a, s.a)
+ RETURNING 'INSERT', 9, *;
END;
\sf merge_sf_test
On Fri, 2023-10-27 at 15:46 +0100, Dean Rasheed wrote:
One fiddly part is resolving the shift/reduce conflicts in the
grammar. Specifically, on seeing "RETURNING expr when ...", there is
ambiguity over whether the "when" is a column alias or the start of
the next merge action. I've resolved that by assigning a slightly
higher precedence to an expression without an alias, so WHEN is
assumed to not be an alias. It seems pretty ugly though (in terms of
having to duplicate so much code), and I'd be interested to know if
there's a neater way to do it.
Can someone else comment on whether this is a reasonable solution to
the grammar problem?
From a usability perspective, I'm still somewhat sceptical about this
approach. It's a much more verbose syntax, and it gets quite tedious
having to repeat the RETURNING list for every action, and keep them
in
sync.
If we go with the single RETURNING-clause-at-the-end approach, how
important is it that the action can be a part of an arbitrary
expression?
Perhaps something closer to your original proposal would be a good
compromise (sorry to backtrack yet again...)? It couldn't be used in an
arbitrary expression, but that also means that it couldn't end up in
the wrong kind of expression.
Regards,
Jeff Davis
On 10/24/23 21:10, Jeff Davis wrote:
Can we revisit the idea of a per-WHEN RETURNING clause?
For the record, I dislike this idea.
--
Vik Fearing
On Tue, 2023-10-31 at 12:45 +0100, Vik Fearing wrote:
On 10/24/23 21:10, Jeff Davis wrote:
Can we revisit the idea of a per-WHEN RETURNING clause?
For the record, I dislike this idea.
I agree that it makes things awkward, and if it creates grammatical
problems as well, then it's not very appealing.
There are only so many approaches though, so it would be helpful if you
could say which approach you prefer.
Assuming we have one RETURNING clause at the end, then it creates the
problem of how to communicate which WHEN clause a tuple came from,
whether it's the old or the new version, and/or which action was
performed on that tuple.
How do we communicate any of those things? We need to get that
information into the result table somehow, so it should probably be
some kind of expression that can exist in the RETURNING clause. But
what kind of expression?
(a) It could be a totally new expression kind with a new keyword (or
recycling some existing keywords for the same effect, or something that
looks superficially like a function call but isn't) that's only valid
in the RETURNING clause of a MERGE statement. If you use it in another
expression (say the targetlist of a SELECT statement), then you'd get a
failure at parse analysis time.
(b) It could be a FuncExpr that is passed the information out-of-band
(i.e. not as an argument) and would fail at runtime if called in the
wrong context.
(c) It could be a Var (or perhaps a Param?), but to which table would
it refer? The source or the target table could be used, but we would
also need a special table reference to represent additional context
(WHEN clause number, action, or "after" version of the tuple).
Dean's v11 patch had kind of a combination of (a) and (b). It's raises
an error at parse analysis time like (a), but without any grammar
changes or new expr kind because it's a function. I must admit that
might be a very reasonable compromise and I certainly won't reject it
without a clearly better alternative. It does feel like a hack though
in the sense that it's hard-coding function OIDs into the parse
analysis and I'm not sure that's a great thing to do. I wonder if it
would be worth thinking about a way to make it generic by really making
it into a different kind of function with pg_proc support? That feels
like over-engineering, and I hate to generalize from a single use case,
but it might be a good thought exercise.
The cleanest from a SQL perspective (in my opinion) would be something
more like (c), because the merge action and WHEN clause number would be
passed in tuple data. It also would be good precedent for something
like BEFORE/AFTER aliases, which could be useful for UPDATE actions.
But given the implementation complexities brought up earlier (I haven't
looked into the details, but others have), that might be over-
engineering.
Regards,
Jeff Davis
On 10/31/23 19:28, Jeff Davis wrote:
On Tue, 2023-10-31 at 12:45 +0100, Vik Fearing wrote:
On 10/24/23 21:10, Jeff Davis wrote:
Can we revisit the idea of a per-WHEN RETURNING clause?
For the record, I dislike this idea.
I agree that it makes things awkward, and if it creates grammatical
problems as well, then it's not very appealing.There are only so many approaches though, so it would be helpful if you
could say which approach you prefer.
This isn't as easy to answer for me as it seems because I care deeply
about respecting the standard. The standard does not have RETURNING at
all but instead has <data change delta table> and the results for that
for a MERGE statement are clearly defined.
On the other hand, we don't have that and we do have RETURNING so we
should improve upon that if we can.
One thing I don't like about either solution is that you cannot get at
both the old row versions and the new row versions at the same time. I
don't see how <data change delta table>s can be fixed to support that,
but RETURNING certainly can be with some spelling of OLD/NEW or
BEFORE/AFTER or whatever.
Assuming we have one RETURNING clause at the end, then it creates the
problem of how to communicate which WHEN clause a tuple came from,
whether it's the old or the new version, and/or which action was
performed on that tuple.How do we communicate any of those things? We need to get that
information into the result table somehow, so it should probably be
some kind of expression that can exist in the RETURNING clause. But
what kind of expression?(a) It could be a totally new expression kind with a new keyword (or
recycling some existing keywords for the same effect, or something that
looks superficially like a function call but isn't) that's only valid
in the RETURNING clause of a MERGE statement. If you use it in another
expression (say the targetlist of a SELECT statement), then you'd get a
failure at parse analysis time.
This would be my choice, the same as how the standard GROUPING()
"function" for grouping sets is implemented by GroupingFunc.
--
Vik Fearing
On Tue, 31 Oct 2023 at 23:19, Vik Fearing <vik@postgresfriends.org> wrote:
On 10/31/23 19:28, Jeff Davis wrote:
Assuming we have one RETURNING clause at the end, then it creates the
problem of how to communicate which WHEN clause a tuple came from,
whether it's the old or the new version, and/or which action was
performed on that tuple.How do we communicate any of those things? We need to get that
information into the result table somehow, so it should probably be
some kind of expression that can exist in the RETURNING clause. But
what kind of expression?(a) It could be a totally new expression kind with a new keyword (or
recycling some existing keywords for the same effect, or something that
looks superficially like a function call but isn't) that's only valid
in the RETURNING clause of a MERGE statement. If you use it in another
expression (say the targetlist of a SELECT statement), then you'd get a
failure at parse analysis time.This would be my choice, the same as how the standard GROUPING()
"function" for grouping sets is implemented by GroupingFunc.
Something I'm wondering about is to what extent this discussion is
driven by concerns about aspects of the implementation (specifically,
references to function OIDs in code), versus a desire for a different
user-visible syntax. To a large extent, those are orthogonal
questions.
(As an aside, I would note that there are already around a dozen
references to specific function OIDs in the parse analysis code, and a
lot more if you grep more widely across the whole of the backend
code.)
At one point, as I was writing this patch, I went part-way down the
route of adding a new node type (I think I called it MergeFunc), for
these merge support functions, somewhat inspired by GroupingFunc. In
the end, I backed out of that approach, because it seemed to be
introducing a lot of unnecessary additional complexity, and I decided
that a regular FuncExpr would suffice.
If pg_merge_action() and pg_merge_when_clause_number() were
implemented using a MergeFunc node, it would reduce the number of
places that refer to specific function OIDs. Basically, a MergeFunc
node would be very much like a FuncExpr node, except that it would
have a "levels up" field, set during parse analysis, at the point
where we check that it is being used in a merge returning clause, and
this field would be used during subselect planning. Note, however,
that that doesn't entirely eliminate references to specific function
OIDs -- the parse analysis code would still do that. Also, additional
special-case code in the executor would be required to handle
MergeFunc nodes. Also, code like IncrementVarSublevelsUp() would need
adjusting, and anything else like that.
A separate question is what the syntax should be. We could invent a
new syntax, like GROUPING(). Perhaps:
MERGE(ACTION) instead of pg_merge_action()
MERGE(CLAUSE NUMBER) instead of pg_merge_when_clause_number()
But note that those could equally well generate either FuncExpr nodes
or MergeFunc nodes, so the syntax question remains orthogonal to that
internal implementation question.
If MERGE(...) (or MERGING(...), or whatever) were part of the SQL
standard, then that would be the clear choice. But since it's not, I
don't see any real advantage to inventing special syntax here, rather
than just using a regular function call. In fact, it's worse, because
if this were to work like GROUPING(), it would require MERGE (or
MERGING, or whatever) to be a COL_NAME_KEYWORD, where currently MERGE
is an UNRESERVED_KEYWORD, and that would break any existing
user-defined functions with that name, whereas the "pg_" prefix of my
functions makes that much less likely.
So on the syntax question, in the absence of anything specific from
the SQL standard, I think we should stick to builtin functions,
without inventing special syntax. That doesn't preclude adding special
syntax later, if the SQL standard mandates it, but that might be
harder, if we invent our own syntax now.
On the implementation question, I'm not completely against the idea of
a MergeFunc node, but it does feel a little over-engineered.
Regards,
Dean
On 11/1/23 11:12, Dean Rasheed wrote:
On Tue, 31 Oct 2023 at 23:19, Vik Fearing <vik@postgresfriends.org> wrote:
On 10/31/23 19:28, Jeff Davis wrote:
Assuming we have one RETURNING clause at the end, then it creates the
problem of how to communicate which WHEN clause a tuple came from,
whether it's the old or the new version, and/or which action was
performed on that tuple.How do we communicate any of those things? We need to get that
information into the result table somehow, so it should probably be
some kind of expression that can exist in the RETURNING clause. But
what kind of expression?(a) It could be a totally new expression kind with a new keyword (or
recycling some existing keywords for the same effect, or something that
looks superficially like a function call but isn't) that's only valid
in the RETURNING clause of a MERGE statement. If you use it in another
expression (say the targetlist of a SELECT statement), then you'd get a
failure at parse analysis time.This would be my choice, the same as how the standard GROUPING()
"function" for grouping sets is implemented by GroupingFunc.Something I'm wondering about is to what extent this discussion is
driven by concerns about aspects of the implementation (specifically,
references to function OIDs in code), versus a desire for a different
user-visible syntax. To a large extent, those are orthogonal
questions.
For my part, I am most concerned about the language level. I am
sympathetic to the implementers' issues, but that is not my main focus.
So please do not take my implementation advice into account when I voice
my opinions.
--
Vik Fearing
On Wed, Nov 1, 2023 at 5:12 AM Dean Rasheed <dean.a.rasheed@gmail.com>
wrote:
On Tue, 31 Oct 2023 at 23:19, Vik Fearing <vik@postgresfriends.org> wrote:
On 10/31/23 19:28, Jeff Davis wrote:
Assuming we have one RETURNING clause at the end, then it creates the
problem of how to communicate which WHEN clause a tuple came from,
whether it's the old or the new version, and/or which action was
performed on that tuple.How do we communicate any of those things? We need to get that
information into the result table somehow, so it should probably be
some kind of expression that can exist in the RETURNING clause. But
what kind of expression?(a) It could be a totally new expression kind with a new keyword (or
recycling some existing keywords for the same effect, or something that
looks superficially like a function call but isn't) that's only valid
in the RETURNING clause of a MERGE statement. If you use it in another
expression (say the targetlist of a SELECT statement), then you'd get a
failure at parse analysis time.This would be my choice, the same as how the standard GROUPING()
"function" for grouping sets is implemented by GroupingFunc.Something I'm wondering about is to what extent this discussion is
driven by concerns about aspects of the implementation (specifically,
references to function OIDs in code), versus a desire for a different
user-visible syntax. To a large extent, those are orthogonal
questions.(As an aside, I would note that there are already around a dozen
references to specific function OIDs in the parse analysis code, and a
lot more if you grep more widely across the whole of the backend
code.)At one point, as I was writing this patch, I went part-way down the
route of adding a new node type (I think I called it MergeFunc), for
these merge support functions, somewhat inspired by GroupingFunc. In
the end, I backed out of that approach, because it seemed to be
introducing a lot of unnecessary additional complexity, and I decided
that a regular FuncExpr would suffice.If pg_merge_action() and pg_merge_when_clause_number() were
implemented using a MergeFunc node, it would reduce the number of
places that refer to specific function OIDs. Basically, a MergeFunc
node would be very much like a FuncExpr node, except that it would
have a "levels up" field, set during parse analysis, at the point
where we check that it is being used in a merge returning clause, and
this field would be used during subselect planning. Note, however,
that that doesn't entirely eliminate references to specific function
OIDs -- the parse analysis code would still do that. Also, additional
special-case code in the executor would be required to handle
MergeFunc nodes. Also, code like IncrementVarSublevelsUp() would need
adjusting, and anything else like that.A separate question is what the syntax should be. We could invent a
new syntax, like GROUPING(). Perhaps:MERGE(ACTION) instead of pg_merge_action()
MERGE(CLAUSE NUMBER) instead of pg_merge_when_clause_number()
Hm, still struggling with this merge action and (especially) number stuff.
Currently we have:
WHEN MATCHED [ AND *condition* ] THEN { *merge_update* |
*merge_delete* | DO NOTHING } |
WHEN NOT MATCHED [ AND *condition* ] THEN { *merge_insert* | DO NOTHING } }
What about extending to something like:
WHEN MATCHED [ AND *condition* ] [ AS *merge_clause_name ]*
WHEN MATCHED AND tid > 2 AS giraffes THEN UPDATE SET balance = t.balance +
delta
...and have pg_merge_clause() return 'giraffes' (of name type). If merge
clause is not identified, maybe don't return any data for that clause
through returning,, or return NULL. Maybe 'returning' clause doesn't have
to be extended or molested in any way, it would follow mechanics as per
'update', and could not refer to identified merge_clauses, but would allow
for pg_merge_clause() functioning. You wouldn't need to identify action or
number. Food for thought, -- may have missed some finer details upthread.
for example,
with r as (
merge into x using y on x.a = y.a
when matched and x.c > 0 as good then do nothing
when matched and x.c <= 0 as bad then do nothing
returning pg_merge_clause(), x.*
) ...
yielding
pg_merge_clause a c
good 1 5
good 2 7
bad 3 0
...
...maybe allow pg_merge_clause() take to optionally yield column name:
returning pg_merge_clause('result'), x.*
) ...
yielding
result a c
good 1 5
good 2 7
bad 3 0
...
merlin
On Wed, 2023-11-01 at 10:12 +0000, Dean Rasheed wrote:
Something I'm wondering about is to what extent this discussion is
driven by concerns about aspects of the implementation (specifically,
references to function OIDs in code), versus a desire for a different
user-visible syntax. To a large extent, those are orthogonal
questions.
Most of my concern is that parts of the implementation feel like a
hack, which makes me concerned that we're approaching it the wrong way.
At a language level, I'm also concerned that we don't have a way to
access the before/after versions of the tuple. I won't insist on this
because I'm hoping that could be solved as part of a later patch that
also addresses UPDATE ... RETURNING.
(As an aside, I would note that there are already around a dozen
references to specific function OIDs in the parse analysis code, and
a
lot more if you grep more widely across the whole of the backend
code.)
If you can point to a precedent, then I'm much more inclined to be OK
with the implementation.
Regards,
Jeff Davis
On Wed, 1 Nov 2023 at 17:49, Jeff Davis <pgsql@j-davis.com> wrote:
Most of my concern is that parts of the implementation feel like a
hack, which makes me concerned that we're approaching it the wrong way.
OK, that's a fair point. Attached is a new version, replacing those
parts of the implementation with a new MergingFunc node. It doesn't
add that much more complexity, and I think the new code is much
neater.
Also, I think this makes it easier / more natural to add additional
returning options, like Merlin's suggestion to return a user-defined
label value, though I haven't implemented that.
I have gone with the name originally suggested by Vik -- MERGING(),
which means that that has to be a new col-name keyword. I'm not
especially wedded to that name, but I think that it's not a bad
choice, and I think going with that is preferable to making MERGE a
col-name keyword.
So (quoting the example from the docs), the new syntax looks like this:
MERGE INTO products p
USING stock s ON p.product_id = s.product_id
WHEN MATCHED AND s.quantity > 0 THEN
UPDATE SET in_stock = true, quantity = s.quantity
WHEN MATCHED THEN
UPDATE SET in_stock = false, quantity = 0
WHEN NOT MATCHED THEN
INSERT (product_id, in_stock, quantity)
VALUES (s.product_id, true, s.quantity)
RETURNING MERGING(ACTION), MERGING(CLAUSE_NUMBER), p.*;
action | clause_number | product_id | in_stock | quantity
--------+---------------+------------+----------+----------
UPDATE | 1 | 1001 | t | 50
UPDATE | 2 | 1002 | f | 0
INSERT | 3 | 1003 | t | 10
By default, the returned column names are automatically taken from the
argument to the MERGING() function (which isn't actually a function
anymore).
There's one bug that I know about, to do with cross-partition updates,
but since that's a pre-existing bug, I'll start a new thread for it.
Regards,
Dean
Attachments:
support-merge-returning-v12.patchtext/x-patch; charset=US-ASCII; name=support-merge-returning-v12.patchDownload
diff --git a/doc/src/sgml/dml.sgml b/doc/src/sgml/dml.sgml
new file mode 100644
index cbbc5e2..3d95bdb
--- a/doc/src/sgml/dml.sgml
+++ b/doc/src/sgml/dml.sgml
@@ -283,10 +283,15 @@ DELETE FROM products;
<secondary>RETURNING</secondary>
</indexterm>
+ <indexterm zone="dml-returning">
+ <primary>MERGE</primary>
+ <secondary>RETURNING</secondary>
+ </indexterm>
+
<para>
Sometimes it is useful to obtain data from modified rows while they are
being manipulated. The <command>INSERT</command>, <command>UPDATE</command>,
- and <command>DELETE</command> commands all have an
+ <command>DELETE</command>, and <command>MERGE</command> commands all have an
optional <literal>RETURNING</literal> clause that supports this. Use
of <literal>RETURNING</literal> avoids performing an extra database query to
collect the data, and is especially valuable when it would otherwise be
@@ -339,6 +344,21 @@ DELETE FROM products
</programlisting>
</para>
+ <para>
+ In a <command>MERGE</command>, the data available to <literal>RETURNING</literal> is
+ the content of the source row plus the content of the inserted, updated, or
+ deleted target row. Since it is quite common for the source and target to
+ have many of the same columns, specifying <literal>RETURNING *</literal>
+ can lead to a lot of duplicated columns, so it is often more useful to
+ qualify it so as to return just the source or target row. For example:
+<programlisting>
+MERGE INTO products p USING new_products n ON p.product_no = n.product_no
+ WHEN NOT MATCHED THEN INSERT VALUES (n.product_no, n.name, n.price)
+ WHEN MATCHED THEN UPDATE SET name = n.name, price = n.price
+ RETURNING p.*;
+</programlisting>
+ </para>
+
<para>
If there are triggers (<xref linkend="triggers"/>) on the target table,
the data available to <literal>RETURNING</literal> is the row as modified by
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
new file mode 100644
index a6fcac0..06d5fe8
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -21960,6 +21960,84 @@ SELECT count(*) FROM sometable;
</sect1>
+ <sect1 id="functions-merge-support">
+ <title>Merge Support Functions</title>
+
+ <indexterm>
+ <primary>MERGE</primary>
+ <secondary>RETURNING</secondary>
+ </indexterm>
+
+ <indexterm>
+ <primary>MERGING</primary>
+ </indexterm>
+
+ <para>
+ <productname>PostgreSQL</productname> includes one merge support function
+ that may be used in the <literal>RETURNING</literal> list of a
+ <xref linkend="sql-merge"/> command to return additional information about
+ the action taken for each row:
+<synopsis>
+<function id="function-merging">MERGING</function> ( <replaceable>property</replaceable> )
+</synopsis>
+ The following are valid property values specifying what to return:
+
+ <variablelist>
+ <varlistentry>
+ <term><literal>ACTION</literal></term>
+ <listitem>
+ <para>
+ The merge action command executed for the current row
+ (<literal>'INSERT'</literal>, <literal>'UPDATE'</literal>, or
+ <literal>'DELETE'</literal>).
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>CLAUSE_NUMBER</literal></term>
+ <listitem>
+ <para>
+ The 1-based index of the <literal>WHEN</literal> clause executed for
+ the current row.
+ </para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </para>
+
+ <para>
+ The following example illustrates how this may be used to determine
+ which actions were performed for each row affected by the
+ <command>MERGE</command> command:
+<screen><![CDATA[
+MERGE INTO products p
+ USING stock s ON p.product_id = s.product_id
+ WHEN MATCHED AND s.quantity > 0 THEN
+ UPDATE SET in_stock = true, quantity = s.quantity
+ WHEN MATCHED THEN
+ UPDATE SET in_stock = false, quantity = 0
+ WHEN NOT MATCHED THEN
+ INSERT (product_id, in_stock, quantity)
+ VALUES (s.product_id, true, s.quantity)
+ RETURNING MERGING(ACTION), MERGING(CLAUSE_NUMBER), p.*;
+
+ action | clause_number | product_id | in_stock | quantity
+--------+---------------+------------+----------+----------
+ UPDATE | 1 | 1001 | t | 50
+ UPDATE | 2 | 1002 | f | 0
+ INSERT | 3 | 1003 | t | 10
+]]></screen>
+ </para>
+
+ <para>
+ Note that this function can only be used in the <literal>RETURNING</literal>
+ list of a <command>MERGE</command> command. It is an error to use it in any
+ other part of a query.
+ </para>
+
+ </sect1>
+
<sect1 id="functions-subquery">
<title>Subquery Expressions</title>
diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml
new file mode 100644
index fe8def4..51a15ca
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -1387,9 +1387,9 @@
to a <glossterm linkend="glossary-client">client</glossterm> upon the
completion of an <acronym>SQL</acronym> command, usually a
<command>SELECT</command> but it can be an
- <command>INSERT</command>, <command>UPDATE</command>, or
- <command>DELETE</command> command if the <literal>RETURNING</literal>
- clause is specified.
+ <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command> command if the
+ <literal>RETURNING</literal> clause is specified.
</para>
<para>
The fact that a result set is a relation means that a query can be used
diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml
new file mode 100644
index 5977534..4ad9894
--- a/doc/src/sgml/plpgsql.sgml
+++ b/doc/src/sgml/plpgsql.sgml
@@ -1020,8 +1020,8 @@ INSERT INTO mytable VALUES (1,'one'), (2
</para>
<para>
- If the command does return rows (for example <command>SELECT</command>,
- or <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>
+ If the command does return rows (for example <command>SELECT</command>, or
+ <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command>
with <literal>RETURNING</literal>), there are two ways to proceed.
When the command will return at most one row, or you only care about
the first row of output, write the command as usual but add
@@ -1149,6 +1149,7 @@ SELECT <replaceable>select_expressions</
INSERT ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
UPDATE ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
DELETE ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
+MERGE ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
</synopsis>
where <replaceable>target</replaceable> can be a record variable, a row
@@ -1159,8 +1160,8 @@ DELETE ... RETURNING <replaceable>expres
<literal>INTO</literal> clause) just as described above,
and the plan is cached in the same way.
This works for <command>SELECT</command>,
- <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command> with
- <literal>RETURNING</literal>, and certain utility commands
+ <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command>
+ with <literal>RETURNING</literal>, and certain utility commands
that return row sets, such as <command>EXPLAIN</command>.
Except for the <literal>INTO</literal> clause, the SQL command is the same
as it would be written outside <application>PL/pgSQL</application>.
@@ -1236,7 +1237,7 @@ END;
</para>
<para>
- For <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command> with
+ For <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command> with
<literal>RETURNING</literal>, <application>PL/pgSQL</application> reports
an error for more than one returned row, even when
<literal>STRICT</literal> is not specified. This is because there
diff --git a/doc/src/sgml/queries.sgml b/doc/src/sgml/queries.sgml
new file mode 100644
index 8a4674e..ae1f738
--- a/doc/src/sgml/queries.sgml
+++ b/doc/src/sgml/queries.sgml
@@ -2062,9 +2062,10 @@ SELECT <replaceable>select_list</replace
Table Expressions or <acronym>CTE</acronym>s, can be thought of as defining
temporary tables that exist just for one query. Each auxiliary statement
in a <literal>WITH</literal> clause can be a <command>SELECT</command>,
- <command>INSERT</command>, <command>UPDATE</command>, or <command>DELETE</command>; and the
+ <command>INSERT</command>, <command>UPDATE</command>, <command>DELETE</command>,
+ or <command>MERGE</command>; and the
<literal>WITH</literal> clause itself is attached to a primary statement that can
- be a <command>SELECT</command>, <command>INSERT</command>, <command>UPDATE</command>,
+ also be a <command>SELECT</command>, <command>INSERT</command>, <command>UPDATE</command>,
<command>DELETE</command>, or <command>MERGE</command>.
</para>
@@ -2598,8 +2599,8 @@ SELECT * FROM w AS w1 JOIN w AS w2 ON w1
<title>Data-Modifying Statements in <literal>WITH</literal></title>
<para>
- You can use most data-modifying statements (<command>INSERT</command>,
- <command>UPDATE</command>, or <command>DELETE</command>, but not
+ You can use data-modifying statements (<command>INSERT</command>,
+ <command>UPDATE</command>, <command>DELETE</command>, or
<command>MERGE</command>) in <literal>WITH</literal>. This
allows you to perform several different operations in the same query.
An example is:
diff --git a/doc/src/sgml/ref/copy.sgml b/doc/src/sgml/ref/copy.sgml
new file mode 100644
index d12ba96..9b9536b
--- a/doc/src/sgml/ref/copy.sgml
+++ b/doc/src/sgml/ref/copy.sgml
@@ -122,13 +122,16 @@ COPY { <replaceable class="parameter">ta
A <link linkend="sql-select"><command>SELECT</command></link>,
<link linkend="sql-values"><command>VALUES</command></link>,
<link linkend="sql-insert"><command>INSERT</command></link>,
- <link linkend="sql-update"><command>UPDATE</command></link>, or
- <link linkend="sql-delete"><command>DELETE</command></link> command whose results are to be
- copied. Note that parentheses are required around the query.
+ <link linkend="sql-update"><command>UPDATE</command></link>,
+ <link linkend="sql-delete"><command>DELETE</command></link>, or
+ <link linkend="sql-merge"><command>MERGE</command></link> command
+ whose results are to be copied. Note that parentheses are required
+ around the query.
</para>
<para>
- For <command>INSERT</command>, <command>UPDATE</command> and
- <command>DELETE</command> queries a RETURNING clause must be provided,
+ For <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, and <command>MERGE</command> queries a
+ RETURNING clause must be provided,
and the target relation must not have a conditional rule, nor
an <literal>ALSO</literal> rule, nor an <literal>INSTEAD</literal> rule
that expands to multiple statements.
diff --git a/doc/src/sgml/ref/merge.sgml b/doc/src/sgml/ref/merge.sgml
new file mode 100644
index 0f61d47..1ef29ab
--- a/doc/src/sgml/ref/merge.sgml
+++ b/doc/src/sgml/ref/merge.sgml
@@ -25,6 +25,7 @@ PostgreSQL documentation
MERGE INTO [ ONLY ] <replaceable class="parameter">target_table_name</replaceable> [ * ] [ [ AS ] <replaceable class="parameter">target_alias</replaceable> ]
USING <replaceable class="parameter">data_source</replaceable> ON <replaceable class="parameter">join_condition</replaceable>
<replaceable class="parameter">when_clause</replaceable> [...]
+[ RETURNING * | <replaceable class="parameter">output_expression</replaceable> [ [ AS ] <replaceable class="parameter">output_name</replaceable> ] [, ...] ]
<phrase>where <replaceable class="parameter">data_source</replaceable> is:</phrase>
@@ -95,6 +96,18 @@ DELETE
</para>
<para>
+ The optional <literal>RETURNING</literal> clause causes <command>MERGE</command>
+ to compute and return value(s) based on each row inserted, updated, or
+ deleted. Any expression using the source or target table's columns, or
+ the <xref linkend="function-merging"/> function can be computed. When an
+ <command>INSERT</command> or <command>UPDATE</command> action is performed,
+ the new values of the target table's columns are used. When a
+ <command>DELETE</command> is performed, the old values of the target table's
+ columns are used. The syntax of the <literal>RETURNING</literal> list is
+ identical to that of the output list of <command>SELECT</command>.
+ </para>
+
+ <para>
There is no separate <literal>MERGE</literal> privilege.
If you specify an update action, you must have the
<literal>UPDATE</literal> privilege on the column(s)
@@ -125,6 +138,18 @@ DELETE
<variablelist>
<varlistentry>
+ <term><replaceable class="parameter">with_query</replaceable></term>
+ <listitem>
+ <para>
+ The <literal>WITH</literal> clause allows you to specify one or more
+ subqueries that can be referenced by name in the <command>MERGE</command>
+ query. See <xref linkend="queries-with"/> and <xref linkend="sql-select"/>
+ for details.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
<term><replaceable class="parameter">target_table_name</replaceable></term>
<listitem>
<para>
@@ -399,13 +424,30 @@ DELETE
</varlistentry>
<varlistentry>
- <term><replaceable class="parameter">with_query</replaceable></term>
+ <term><replaceable class="parameter">output_expression</replaceable></term>
<listitem>
<para>
- The <literal>WITH</literal> clause allows you to specify one or more
- subqueries that can be referenced by name in the <command>MERGE</command>
- query. See <xref linkend="queries-with"/> and <xref linkend="sql-select"/>
- for details.
+ An expression to be computed and returned by the <command>MERGE</command>
+ command after each row is merged. The expression can use any columns of
+ the source or target tables, or the <xref linkend="function-merging"/>
+ function to return additional information about the action executed.
+ </para>
+ <para>
+ Writing <literal>*</literal> will return all columns from the source
+ table, followed by all columns from the target table. Often this will
+ lead to a lot of duplication, since it is common for the source and
+ target tables to have a lot of the same columns. This can be avoided by
+ qualifying the <literal>*</literal> with the name or alias of the source
+ or target table.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><replaceable class="parameter">output_name</replaceable></term>
+ <listitem>
+ <para>
+ A name to use for a returned column.
</para>
</listitem>
</varlistentry>
@@ -428,6 +470,13 @@ MERGE <replaceable class="parameter">tot
were changed in any way.
</para>
+ <para>
+ If the <command>MERGE</command> command contains a <literal>RETURNING</literal>
+ clause, the result will be similar to that of a <command>SELECT</command>
+ statement containing the columns and values defined in the
+ <literal>RETURNING</literal> list, computed over the row(s) inserted, updated,
+ or deleted by the command.
+ </para>
</refsect1>
<refsect1>
@@ -544,13 +593,6 @@ MERGE <replaceable class="parameter">tot
</para>
<para>
- There is no <literal>RETURNING</literal> clause with
- <command>MERGE</command>. Actions of <command>INSERT</command>,
- <command>UPDATE</command> and <command>DELETE</command> cannot contain
- <literal>RETURNING</literal> or <literal>WITH</literal> clauses.
- </para>
-
- <para>
When <command>MERGE</command> is run concurrently with other commands
that modify the target table, the usual transaction isolation rules
apply; see <xref linkend="transaction-iso"/> for an explanation
@@ -602,7 +644,8 @@ WHEN NOT MATCHED THEN
<para>
Attempt to insert a new stock item along with the quantity of stock. If
the item already exists, instead update the stock count of the existing
- item. Don't allow entries that have zero stock.
+ item. Don't allow entries that have zero stock. Return details of all
+ changes made.
<programlisting>
MERGE INTO wines w
USING wine_stock_changes s
@@ -612,7 +655,8 @@ WHEN NOT MATCHED AND s.stock_delta > 0 T
WHEN MATCHED AND w.stock + s.stock_delta > 0 THEN
UPDATE SET stock = w.stock + s.stock_delta
WHEN MATCHED THEN
- DELETE;
+ DELETE
+RETURNING MERGING(ACTION), w.*;
</programlisting>
The <literal>wine_stock_changes</literal> table might be, for example, a
@@ -627,7 +671,8 @@ WHEN MATCHED THEN
This command conforms to the <acronym>SQL</acronym> standard.
</para>
<para>
- The WITH clause and <literal>DO NOTHING</literal> action are extensions to
+ The WITH clause, <literal>DO NOTHING</literal> action, and
+ <literal>RETURNING</literal> clause are extensions to
the <acronym>SQL</acronym> standard.
</para>
</refsect1>
diff --git a/doc/src/sgml/ref/select.sgml b/doc/src/sgml/ref/select.sgml
new file mode 100644
index 42d7891..8513ca6
--- a/doc/src/sgml/ref/select.sgml
+++ b/doc/src/sgml/ref/select.sgml
@@ -74,7 +74,7 @@ SELECT [ ALL | DISTINCT [ ON ( <replacea
<phrase>and <replaceable class="parameter">with_query</replaceable> is:</phrase>
- <replaceable class="parameter">with_query_name</replaceable> [ ( <replaceable class="parameter">column_name</replaceable> [, ...] ) ] AS [ [ NOT ] MATERIALIZED ] ( <replaceable class="parameter">select</replaceable> | <replaceable class="parameter">values</replaceable> | <replaceable class="parameter">insert</replaceable> | <replaceable class="parameter">update</replaceable> | <replaceable class="parameter">delete</replaceable> )
+ <replaceable class="parameter">with_query_name</replaceable> [ ( <replaceable class="parameter">column_name</replaceable> [, ...] ) ] AS [ [ NOT ] MATERIALIZED ] ( <replaceable class="parameter">select</replaceable> | <replaceable class="parameter">values</replaceable> | <replaceable class="parameter">insert</replaceable> | <replaceable class="parameter">update</replaceable> | <replaceable class="parameter">delete</replaceable> | <replaceable class="parameter">merge</replaceable> )
[ SEARCH { BREADTH | DEPTH } FIRST BY <replaceable>column_name</replaceable> [, ...] SET <replaceable>search_seq_col_name</replaceable> ]
[ CYCLE <replaceable>column_name</replaceable> [, ...] SET <replaceable>cycle_mark_col_name</replaceable> [ TO <replaceable>cycle_mark_value</replaceable> DEFAULT <replaceable>cycle_mark_default</replaceable> ] USING <replaceable>cycle_path_col_name</replaceable> ]
@@ -230,10 +230,10 @@ TABLE [ ONLY ] <replaceable class="param
The subqueries effectively act as temporary tables or views
for the duration of the primary query.
Each subquery can be a <command>SELECT</command>, <command>TABLE</command>, <command>VALUES</command>,
- <command>INSERT</command>, <command>UPDATE</command> or
- <command>DELETE</command> statement.
+ <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command> statement.
When writing a data-modifying statement (<command>INSERT</command>,
- <command>UPDATE</command> or <command>DELETE</command>) in
+ <command>UPDATE</command>, <command>DELETE</command>, or <command>MERGE</command>) in
<literal>WITH</literal>, it is usual to include a <literal>RETURNING</literal> clause.
It is the output of <literal>RETURNING</literal>, <emphasis>not</emphasis> the underlying
table that the statement modifies, that forms the temporary table that is
@@ -2182,7 +2182,8 @@ SELECT 2+2;
<para>
<productname>PostgreSQL</productname> allows <command>INSERT</command>,
- <command>UPDATE</command>, and <command>DELETE</command> to be used as <literal>WITH</literal>
+ <command>UPDATE</command>, <command>DELETE</command>, and
+ <command>MERGE</command> to be used as <literal>WITH</literal>
queries. This is not found in the SQL standard.
</para>
</refsect2>
diff --git a/doc/src/sgml/rowtypes.sgml b/doc/src/sgml/rowtypes.sgml
new file mode 100644
index 4d86f97..bbeac84
--- a/doc/src/sgml/rowtypes.sgml
+++ b/doc/src/sgml/rowtypes.sgml
@@ -348,7 +348,7 @@ SELECT m.* FROM some_table, LATERAL myfu
column expansion of this kind when it appears at the top level of
a <link linkend="queries-select-lists"><command>SELECT</command> output
list</link>, a <link linkend="dml-returning"><literal>RETURNING</literal>
- list</link> in <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>,
+ list</link> in <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command>,
a <link linkend="queries-values"><literal>VALUES</literal> clause</link>, or
a <link linkend="sql-syntax-row-constructors">row constructor</link>.
In all other contexts (including when nested inside one of those
diff --git a/doc/src/sgml/spi.sgml b/doc/src/sgml/spi.sgml
new file mode 100644
index 47f4b5b..bb37786
--- a/doc/src/sgml/spi.sgml
+++ b/doc/src/sgml/spi.sgml
@@ -301,8 +301,9 @@ SPI_execute("INSERT INTO foo SELECT * FR
is returned in the global variable <varname>SPI_processed</varname>.
If the return value of the function is <symbol>SPI_OK_SELECT</symbol>,
<symbol>SPI_OK_INSERT_RETURNING</symbol>,
- <symbol>SPI_OK_DELETE_RETURNING</symbol>, or
- <symbol>SPI_OK_UPDATE_RETURNING</symbol>,
+ <symbol>SPI_OK_DELETE_RETURNING</symbol>,
+ <symbol>SPI_OK_UPDATE_RETURNING</symbol>, or
+ <symbol>SPI_OK_MERGE_RETURNING</symbol>,
then you can use the
global pointer <literal>SPITupleTable *SPI_tuptable</literal> to
access the result rows. Some utility commands (such as
@@ -472,6 +473,15 @@ typedef struct SPITupleTable
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><symbol>SPI_OK_MERGE_RETURNING</symbol></term>
+ <listitem>
+ <para>
+ if a <command>MERGE RETURNING</command> was executed
+ </para>
+ </listitem>
+ </varlistentry>
<varlistentry>
<term><symbol>SPI_OK_UTILITY</symbol></term>
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
new file mode 100644
index 89116ae..cb0aa55
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -177,7 +177,8 @@
statements separated by semicolons. A semicolon after the last
statement is optional. Unless the function is declared to return
<type>void</type>, the last statement must be a <command>SELECT</command>,
- or an <command>INSERT</command>, <command>UPDATE</command>, or <command>DELETE</command>
+ or an <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command>
that has a <literal>RETURNING</literal> clause.
</para>
@@ -1236,8 +1237,9 @@ SELECT x, CASE WHEN x > 0 THEN genera
<note>
<para>
- If a function's last command is <command>INSERT</command>, <command>UPDATE</command>,
- or <command>DELETE</command> with <literal>RETURNING</literal>, that command will
+ If a function's last command is <command>INSERT</command>,
+ <command>UPDATE</command>, <command>DELETE</command>, or
+ <command>MERGE</command> with <literal>RETURNING</literal>, that command will
always be executed to completion, even if the function is not declared
with <literal>SETOF</literal> or the calling query does not fetch all the
result rows. Any extra rows produced by the <literal>RETURNING</literal>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
new file mode 100644
index c5d7d78..7977873
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -283,12 +283,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 c66a047..1145aaf
--- 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 2c62b0c..6414c0d
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -1111,6 +1111,21 @@ ExecInitExprRec(Expr *node, ExprState *s
break;
}
+ case T_MergingFunc:
+ {
+ MergingFunc *mf_node = (MergingFunc *) node;
+
+ if (!state->parent ||
+ !IsA(state->parent, ModifyTableState) ||
+ ((ModifyTableState *) state->parent)->operation != CMD_MERGE)
+ elog(ERROR, "MergingFunc found in non-merge plan node");
+
+ scratch.opcode = EEOP_MERGING_FUNC;
+ scratch.d.merging_func.mfop = mf_node->mfop;
+ ExprEvalPushStep(state, &scratch);
+ break;
+ }
+
case T_SubscriptingRef:
{
SubscriptingRef *sbsref = (SubscriptingRef *) node;
diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
new file mode 100644
index 24c2b60..b176ab6
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -483,6 +483,7 @@ ExecInterpExpr(ExprState *state, ExprCon
&&CASE_EEOP_AGGREF,
&&CASE_EEOP_GROUPING_FUNC,
&&CASE_EEOP_WINDOW_FUNC,
+ &&CASE_EEOP_MERGING_FUNC,
&&CASE_EEOP_SUBPLAN,
&&CASE_EEOP_AGG_STRICT_DESERIALIZE,
&&CASE_EEOP_AGG_DESERIALIZE,
@@ -1582,6 +1583,14 @@ ExecInterpExpr(ExprState *state, ExprCon
EEO_NEXT();
}
+ EEO_CASE(EEOP_MERGING_FUNC)
+ {
+ /* too complex/uncommon for an inline implementation */
+ ExecEvalMergingFunc(state, op, econtext);
+
+ EEO_NEXT();
+ }
+
EEO_CASE(EEOP_SUBPLAN)
{
/* too complex for an inline implementation */
@@ -4171,6 +4180,59 @@ ExecEvalGroupingFunc(ExprState *state, E
}
/*
+ * ExecEvalMergingFunc
+ *
+ * Returns information about the current MERGE action for its RETURNING list.
+ */
+void
+ExecEvalMergingFunc(ExprState *state, ExprEvalStep *op, ExprContext *econtext)
+{
+ MergingFuncOp mfop = op->d.merging_func.mfop;
+ ModifyTableState *mtstate = castNode(ModifyTableState, state->parent);
+ MergeActionState *relaction = mtstate->mt_merge_action;
+
+ if (!relaction)
+ elog(ERROR, "no merge action in progress");
+
+ switch (mfop)
+ {
+ case MERGING_ACTION:
+ /* Return the MERGE action ("INSERT", "UPDATE", or "DELETE") */
+ switch (relaction->mas_action->commandType)
+ {
+ case CMD_INSERT:
+ *op->resvalue = PointerGetDatum(cstring_to_text_with_len("INSERT", 6));
+ *op->resnull = false;
+ break;
+ case CMD_UPDATE:
+ *op->resvalue = PointerGetDatum(cstring_to_text_with_len("UPDATE", 6));
+ *op->resnull = false;
+ break;
+ case CMD_DELETE:
+ *op->resvalue = PointerGetDatum(cstring_to_text_with_len("DELETE", 6));
+ *op->resnull = false;
+ break;
+ case CMD_NOTHING:
+ elog(ERROR, "unexpected merge action: DO NOTHING");
+ break;
+ default:
+ elog(ERROR, "unrecognized commandType: %d",
+ (int) relaction->mas_action->commandType);
+ }
+ break;
+
+ case MERGING_CLAUSE_NUMBER:
+ /* Return the 1-based index of the MERGE action */
+ *op->resvalue = Int32GetDatum(relaction->mas_action->index);
+ *op->resnull = false;
+ break;
+
+ default:
+ elog(ERROR, "unrecognized MergingFuncOp: %d", (int) mfop);
+ }
+}
+
+/*
* Hand off evaluation of a subplan to nodeSubplan.c
*/
void
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
new file mode 100644
index f6c3432..34d8231
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -612,8 +612,8 @@ ExecInitPartitionInfo(ModifyTableState *
* Build the RETURNING projection for the partition. Note that we didn't
* build the returningList for partitions within the planner, but simple
* translation of varattnos will suffice. This only occurs for the INSERT
- * case or in the case of UPDATE tuple routing where we didn't find a
- * result rel to reuse.
+ * case or in the case of UPDATE/MERGE tuple routing where we didn't find
+ * a result rel to reuse.
*/
if (node && node->returningLists != NIL)
{
@@ -622,12 +622,14 @@ ExecInitPartitionInfo(ModifyTableState *
List *returningList;
/* See the comment above for WCO lists. */
- /* (except no RETURNING support for MERGE yet) */
Assert((node->operation == CMD_INSERT &&
list_length(node->returningLists) == 1 &&
list_length(node->resultRelations) == 1) ||
(node->operation == CMD_UPDATE &&
list_length(node->returningLists) ==
+ list_length(node->resultRelations)) ||
+ (node->operation == CMD_MERGE &&
+ list_length(node->returningLists) ==
list_length(node->resultRelations)));
/*
diff --git a/src/backend/executor/functions.c b/src/backend/executor/functions.c
new file mode 100644
index bace252..68cb4fa
--- a/src/backend/executor/functions.c
+++ b/src/backend/executor/functions.c
@@ -1660,8 +1660,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
@@ -1679,7 +1679,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;
@@ -1693,7 +1694,7 @@ check_sql_fn_retval(List *queryTreeLists
(errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
errmsg("return type mismatch in function declared to return %s",
format_type_be(rettype)),
- errdetail("Function's final statement must be SELECT or INSERT/UPDATE/DELETE RETURNING.")));
+ errdetail("Function's final statement must be SELECT or INSERT/UPDATE/DELETE/MERGE RETURNING.")));
return false; /* keep compiler quiet */
}
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
new file mode 100644
index 299c2c7..7745d86
--- 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"
@@ -88,9 +87,6 @@ typedef struct ModifyTableContext
*/
TupleTableSlot *planSlot;
- /* MERGE specific */
- MergeActionState *relaction; /* MERGE action in progress */
-
/*
* Information about the changes that were made concurrently to a tuple
* being updated or deleted
@@ -153,13 +149,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);
/*
@@ -979,7 +976,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;
@@ -1828,7 +1825,7 @@ ExecCrossPartitionUpdate(ModifyTableCont
* additional rechecking, and might end up executing a different
* action entirely).
*/
- if (context->relaction != NULL)
+ if (mtstate->mt_merge_action != NULL)
return false;
else if (TupIsNull(epqslot))
return true;
@@ -2069,7 +2066,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;
/*
@@ -2712,6 +2709,7 @@ static TupleTableSlot *
ExecMerge(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
ItemPointer tupleid, bool canSetTag)
{
+ TupleTableSlot *rslot = NULL;
bool matched;
/*-----
@@ -2759,18 +2757,18 @@ 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()
- * returned "false", indicating the previously MATCHED tuple no longer
- * matches.
+ * Deal with the NOT MATCHED case (either a NOT MATCHED tuple from the
+ * join, or a previously MATCHED tuple for which ExecMergeMatched() set
+ * "matched" to false, indicating that it no longer matches).
*/
if (!matched)
- ExecMergeNotMatched(context, resultRelInfo, canSetTag);
+ rslot = ExecMergeNotMatched(context, resultRelInfo, canSetTag);
- /* No RETURNING support yet */
- return NULL;
+ return rslot;
}
/*
@@ -2780,8 +2778,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.
*
@@ -2790,16 +2788,17 @@ 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 *newslot;
+ TupleTableSlot *rslot = NULL;
+ TupleTableSlot *newslot = NULL;
EState *estate = context->estate;
ExprContext *econtext = mtstate->ps.ps_ExprContext;
bool isNull;
@@ -2810,7 +2809,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
@@ -2889,12 +2891,15 @@ lmerge_matched:
*/
newslot = ExecProject(relaction->mas_proj);
- context->relaction = relaction;
+ mtstate->mt_merge_action = relaction;
if (!ExecUpdatePrologue(context, resultRelInfo,
tupleid, NULL, newslot, &result))
{
if (result == TM_Ok)
- return true; /* "do nothing" */
+ {
+ *matched = true;
+ return NULL; /* "do nothing" */
+ }
break; /* concurrent update/delete */
}
result = ExecUpdateAct(context, resultRelInfo, tupleid, NULL,
@@ -2908,12 +2913,15 @@ lmerge_matched:
break;
case CMD_DELETE:
- context->relaction = relaction;
+ mtstate->mt_merge_action = relaction;
if (!ExecDeletePrologue(context, resultRelInfo, tupleid,
NULL, NULL, &result))
{
if (result == TM_Ok)
- return true; /* "do nothing" */
+ {
+ *matched = true;
+ return NULL; /* "do nothing" */
+ }
break; /* concurrent update/delete */
}
result = ExecDeleteAct(context, resultRelInfo, tupleid, false);
@@ -2969,7 +2977,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:
{
@@ -3015,13 +3024,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
@@ -3056,7 +3071,8 @@ lmerge_matched:
* tuple already deleted; tell caller to run NOT
* MATCHED actions
*/
- return false;
+ *matched = false;
+ return NULL;
case TM_SelfModified:
@@ -3076,13 +3092,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;
}
}
@@ -3094,6 +3111,31 @@ 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, "unrecognized commandType: %d",
+ (int) commandType);
+ }
+ }
+
/*
* We've activated one of the WHEN clauses, so we don't search
* further. This is required behaviour, not an optimization.
@@ -3104,19 +3146,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;
/*
@@ -3166,10 +3211,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:
@@ -3185,6 +3230,8 @@ ExecMergeNotMatched(ModifyTableContext *
*/
break;
}
+
+ return rslot;
}
/*
@@ -3647,8 +3694,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");
@@ -3725,8 +3781,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");
@@ -3818,7 +3883,7 @@ ExecModifyTable(PlanState *pstate)
}
slot = ExecGetUpdateNewTuple(resultRelInfo, context.planSlot,
oldSlot);
- context.relaction = NULL;
+ node->mt_merge_action = NULL;
/* Now apply the update. */
slot = ExecUpdate(&context, resultRelInfo, tupleid, oldtuple,
diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c
new file mode 100644
index 3397568..b9f443c
--- a/src/backend/executor/spi.c
+++ b/src/backend/executor/spi.c
@@ -2033,6 +2033,8 @@ SPI_result_code_string(int code)
return "SPI_OK_TD_REGISTER";
case SPI_OK_MERGE:
return "SPI_OK_MERGE";
+ case SPI_OK_MERGE_RETURNING:
+ return "SPI_OK_MERGE_RETURNING";
}
/* Unrecognized code ... return something useful ... */
sprintf(buf, "Unrecognized SPI code %d", code);
@@ -2886,7 +2888,10 @@ _SPI_pquery(QueryDesc *queryDesc, bool f
res = SPI_OK_UPDATE;
break;
case CMD_MERGE:
- res = SPI_OK_MERGE;
+ if (queryDesc->plannedstmt->hasReturning)
+ res = SPI_OK_MERGE_RETURNING;
+ else
+ res = SPI_OK_MERGE;
break;
default:
return SPI_ERROR_OPUNKNOWN;
diff --git a/src/backend/jit/llvm/llvmjit_expr.c b/src/backend/jit/llvm/llvmjit_expr.c
new file mode 100644
index a3a0876..e966037
--- a/src/backend/jit/llvm/llvmjit_expr.c
+++ b/src/backend/jit/llvm/llvmjit_expr.c
@@ -1980,6 +1980,12 @@ llvm_compile_expr(ExprState *state)
break;
}
+ case EEOP_MERGING_FUNC:
+ build_EvalXFunc(b, mod, "ExecEvalMergingFunc",
+ v_state, op, v_econtext);
+ LLVMBuildBr(b, opblocks[opno + 1]);
+ break;
+
case EEOP_SUBPLAN:
build_EvalXFunc(b, mod, "ExecEvalSubPlan",
v_state, op, v_econtext);
diff --git a/src/backend/jit/llvm/llvmjit_types.c b/src/backend/jit/llvm/llvmjit_types.c
new file mode 100644
index 791902f..65db8f2
--- a/src/backend/jit/llvm/llvmjit_types.c
+++ b/src/backend/jit/llvm/llvmjit_types.c
@@ -155,6 +155,7 @@ void *referenced_functions[] =
ExecEvalFuncExprFusage,
ExecEvalFuncExprStrictFusage,
ExecEvalGroupingFunc,
+ ExecEvalMergingFunc,
ExecEvalMinMax,
ExecEvalNextValueExpr,
ExecEvalParamExec,
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
new file mode 100644
index c03f4f2..6cf7895
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -67,6 +67,9 @@ exprType(const Node *expr)
case T_WindowFunc:
type = ((const WindowFunc *) expr)->wintype;
break;
+ case T_MergingFunc:
+ type = ((const MergingFunc *) expr)->mftype;
+ break;
case T_SubscriptingRef:
type = ((const SubscriptingRef *) expr)->refrestype;
break;
@@ -810,6 +813,9 @@ exprCollation(const Node *expr)
case T_WindowFunc:
coll = ((const WindowFunc *) expr)->wincollid;
break;
+ case T_MergingFunc:
+ coll = ((const MergingFunc *) expr)->mfcollid;
+ break;
case T_SubscriptingRef:
coll = ((const SubscriptingRef *) expr)->refcollid;
break;
@@ -1085,6 +1091,9 @@ exprSetCollation(Node *expr, Oid collati
case T_WindowFunc:
((WindowFunc *) expr)->wincollid = collation;
break;
+ case T_MergingFunc:
+ ((MergingFunc *) expr)->mfcollid = collation;
+ break;
case T_SubscriptingRef:
((SubscriptingRef *) expr)->refcollid = collation;
break;
@@ -1343,6 +1352,9 @@ exprLocation(const Node *expr)
/* function name should always be the first thing */
loc = ((const WindowFunc *) expr)->location;
break;
+ case T_MergingFunc:
+ loc = ((const MergingFunc *) expr)->location;
+ break;
case T_SubscriptingRef:
/* just use container argument's location */
loc = exprLocation((Node *) ((const SubscriptingRef *) expr)->refexpr);
@@ -2035,6 +2047,7 @@ expression_tree_walker_impl(Node *node,
case T_RangeTblRef:
case T_SortGroupClause:
case T_CTESearchClause:
+ case T_MergingFunc:
/* primitive node types with no expression subnodes */
break;
case T_WithCheckOption:
@@ -2865,6 +2878,7 @@ expression_tree_mutator_impl(Node *node,
case T_RangeTblRef:
case T_SortGroupClause:
case T_CTESearchClause:
+ case T_MergingFunc:
return (Node *) copyObject(node);
case T_WithCheckOption:
{
@@ -3827,6 +3841,7 @@ raw_expression_tree_walker_impl(Node *no
case T_ParamRef:
case T_A_Const:
case T_A_Star:
+ case T_MergingFunc:
/* primitive node types with no subnodes */
break;
case T_Alias:
diff --git a/src/backend/optimizer/plan/subselect.c b/src/backend/optimizer/plan/subselect.c
new file mode 100644
index 7a9fe88..262adf5
--- a/src/backend/optimizer/plan/subselect.c
+++ b/src/backend/optimizer/plan/subselect.c
@@ -1846,7 +1846,8 @@ convert_EXISTS_to_ANY(PlannerInfo *root,
/*
* Replace correlation vars (uplevel vars) with Params.
*
- * Uplevel PlaceHolderVars and aggregates are replaced, too.
+ * Uplevel PlaceHolderVars, aggregates, GROUPING() expressions, and MERGING()
+ * expressions are replaced, too.
*
* Note: it is critical that this runs immediately after SS_process_sublinks.
* Since we do not recurse into the arguments of uplevel PHVs and aggregates,
@@ -1900,6 +1901,11 @@ replace_correlation_vars_mutator(Node *n
if (((GroupingFunc *) node)->agglevelsup > 0)
return (Node *) replace_outer_grouping(root, (GroupingFunc *) node);
}
+ if (IsA(node, MergingFunc))
+ {
+ if (root->parse->commandType != CMD_MERGE)
+ return (Node *) replace_outer_merging(root, (MergingFunc *) node);
+ }
return expression_tree_mutator(node,
replace_correlation_vars_mutator,
(void *) root);
diff --git a/src/backend/optimizer/util/paramassign.c b/src/backend/optimizer/util/paramassign.c
new file mode 100644
index d6a923b..b3b4da6
--- a/src/backend/optimizer/util/paramassign.c
+++ b/src/backend/optimizer/util/paramassign.c
@@ -307,6 +307,56 @@ replace_outer_grouping(PlannerInfo *root
}
/*
+ * Generate a Param node to replace the given MergingFunc expression which is
+ * expected to be in the RETURNING list of an upper-level MERGE query. Record
+ * the need for the MergingFunc in the proper upper-level root->plan_params.
+ */
+Param *
+replace_outer_merging(PlannerInfo *root, MergingFunc *mf)
+{
+ Param *retval;
+ PlannerParamItem *pitem;
+ Oid ptype = exprType((Node *) mf);
+
+ Assert(root->parse->commandType != CMD_MERGE);
+
+ /*
+ * The parser should have ensured that the MergingFunc is in the RETURNING
+ * list of an upper-level MERGE query, so find that query.
+ */
+ do
+ {
+ root = root->parent_root;
+ if (root == NULL)
+ elog(ERROR, "MergingFunc found outside MERGE");
+ } while (root->parse->commandType != CMD_MERGE);
+
+ /*
+ * It does not seem worthwhile to try to de-duplicate references to outer
+ * MergingFunc expressions. Just make a new slot every time.
+ */
+ mf = copyObject(mf);
+
+ pitem = makeNode(PlannerParamItem);
+ pitem->item = (Node *) mf;
+ pitem->paramId = list_length(root->glob->paramExecTypes);
+ root->glob->paramExecTypes = lappend_oid(root->glob->paramExecTypes,
+ ptype);
+
+ root->plan_params = lappend(root->plan_params, pitem);
+
+ retval = makeNode(Param);
+ retval->paramkind = PARAM_EXEC;
+ retval->paramid = pitem->paramId;
+ retval->paramtype = ptype;
+ retval->paramtypmod = -1;
+ retval->paramcollid = InvalidOid;
+ retval->location = mf->location;
+
+ return retval;
+}
+
+/*
* Generate a Param node to replace the given Var,
* which is expected to come from some upper NestLoop plan node.
* Record the need for the Var in root->curOuterParams.
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
new file mode 100644
index 7a1dfb6..5f882b4
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -74,7 +74,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,
@@ -553,7 +552,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;
@@ -980,7 +980,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;
@@ -2445,7 +2446,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
@@ -2539,10 +2541,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;
@@ -2559,7 +2562,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 c224df4..c366c5b
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -693,7 +693,7 @@ static Node *makeRecursiveViewSelect(cha
BOOLEAN_P BOTH BREADTH BY
CACHE CALL CALLED CASCADE CASCADED CASE CAST CATALOG_P CHAIN CHAR_P
- CHARACTER CHARACTERISTICS CHECK CHECKPOINT CLASS CLOSE
+ CHARACTER CHARACTERISTICS CHECK CHECKPOINT CLASS CLAUSE_NUMBER CLOSE
CLUSTER COALESCE COLLATE COLLATION COLUMN COLUMNS COMMENT COMMENTS COMMIT
COMMITTED COMPRESSION CONCURRENTLY CONFIGURATION CONFLICT
CONNECTION CONSTRAINT CONSTRAINTS CONTENT_P CONTINUE_P CONVERSION_P COPY
@@ -731,7 +731,7 @@ static Node *makeRecursiveViewSelect(cha
LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL
LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOGGED
- MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE METHOD
+ MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE MERGING METHOD
MINUTE_P MINVALUE MODE MONTH_P MOVE
NAME_P NAMES NATIONAL NATURAL NCHAR NEW NEXT NFC NFD NFKC NFKD NO NONE
@@ -12332,6 +12332,7 @@ MergeStmt:
USING table_ref
ON a_expr
merge_when_list
+ returning_clause
{
MergeStmt *m = makeNode(MergeStmt);
@@ -12340,6 +12341,7 @@ MergeStmt:
m->sourceRelation = $6;
m->joinCondition = $8;
m->mergeWhenClauses = $9;
+ m->returningList = $10;
$$ = (Node *) m;
}
@@ -15755,6 +15757,24 @@ func_expr_common_subexpr:
n->location = @1;
$$ = (Node *) n;
}
+ | MERGING '(' ACTION ')'
+ {
+ MergingFunc *m = makeNode(MergingFunc);
+
+ m->mfop = MERGING_ACTION;
+ m->mftype = TEXTOID;
+ m->location = @1;
+ $$ = (Node *) m;
+ }
+ | MERGING '(' CLAUSE_NUMBER ')'
+ {
+ MergingFunc *m = makeNode(MergingFunc);
+
+ m->mfop = MERGING_CLAUSE_NUMBER;
+ m->mftype = INT4OID;
+ m->location = @1;
+ $$ = (Node *) m;
+ }
;
@@ -17100,6 +17120,7 @@ unreserved_keyword:
| CHARACTERISTICS
| CHECKPOINT
| CLASS
+ | CLAUSE_NUMBER
| CLOSE
| CLUSTER
| COLUMNS
@@ -17419,6 +17440,7 @@ col_name_keyword:
| JSON_SCALAR
| JSON_SERIALIZE
| LEAST
+ | MERGING
| NATIONAL
| NCHAR
| NONE
@@ -17637,6 +17659,7 @@ bare_label_keyword:
| CHECK
| CHECKPOINT
| CLASS
+ | CLAUSE_NUMBER
| CLOSE
| CLUSTER
| COALESCE
@@ -17808,6 +17831,7 @@ bare_label_keyword:
| MATERIALIZED
| MAXVALUE
| MERGE
+ | MERGING
| METHOD
| MINVALUE
| MODE
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
new file mode 100644
index 85cd47b..e72c37b
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -456,6 +456,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 6992a78..5cad6d8
--- 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 64c582c..ad0717e
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -54,6 +54,7 @@ static Node *transformAExprDistinct(Pars
static Node *transformAExprNullIf(ParseState *pstate, A_Expr *a);
static Node *transformAExprIn(ParseState *pstate, A_Expr *a);
static Node *transformAExprBetween(ParseState *pstate, A_Expr *a);
+static Node *transformMergingFunc(ParseState *pstate, MergingFunc *f);
static Node *transformBoolExpr(ParseState *pstate, BoolExpr *a);
static Node *transformFuncCall(ParseState *pstate, FuncCall *fn);
static Node *transformMultiAssignRef(ParseState *pstate, MultiAssignRef *maref);
@@ -227,6 +228,10 @@ transformExprRecurse(ParseState *pstate,
result = transformGroupingFunc(pstate, (GroupingFunc *) expr);
break;
+ case T_MergingFunc:
+ result = transformMergingFunc(pstate, (MergingFunc *) expr);
+ break;
+
case T_NamedArgExpr:
{
NamedArgExpr *na = (NamedArgExpr *) expr;
@@ -541,6 +546,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:
@@ -1354,6 +1360,31 @@ transformAExprBetween(ParseState *pstate
}
static Node *
+transformMergingFunc(ParseState *pstate, MergingFunc *f)
+{
+ /*
+ * Check that we're in the RETURNING list of a MERGE command.
+ */
+ if (pstate->p_expr_kind != EXPR_KIND_MERGE_RETURNING)
+ {
+ ParseState *parent_pstate = pstate->parentParseState;
+
+ while (parent_pstate &&
+ parent_pstate->p_expr_kind != EXPR_KIND_MERGE_RETURNING)
+ parent_pstate = parent_pstate->parentParseState;
+
+ if (!parent_pstate ||
+ parent_pstate->p_expr_kind != EXPR_KIND_MERGE_RETURNING)
+ ereport(ERROR,
+ errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("MERGING() can only be used in the RETURNING list of a MERGE command"),
+ parser_errposition(pstate, f->location));
+ }
+
+ return (Node *) f;
+}
+
+static Node *
transformBoolExpr(ParseState *pstate, BoolExpr *a)
{
List *args = NIL;
@@ -1767,6 +1798,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:
@@ -3115,6 +3147,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 6c29471..1a0f983
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -2599,6 +2599,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 91b1156..33852e9
--- 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.
@@ -254,6 +258,7 @@ transformMergeStmt(ParseState *pstate, M
MergeAction *action;
action = makeNode(MergeAction);
+ action->index = foreach_current_index(l) + 1;
action->commandType = mergeWhenClause->commandType;
action->matched = mergeWhenClause->matched;
@@ -390,9 +395,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 864ea9b..cc19041
--- a/src/backend/parser/parse_relation.c
+++ b/src/backend/parser/parse_relation.c
@@ -2346,9 +2346,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/parser/parse_target.c b/src/backend/parser/parse_target.c
new file mode 100644
index 3bc62ac..eaa2a2b
--- a/src/backend/parser/parse_target.c
+++ b/src/backend/parser/parse_target.c
@@ -1807,6 +1807,20 @@ FigureColnameInternal(Node *node, char *
/* make GROUPING() act like a regular function */
*name = "grouping";
return 2;
+ case T_MergingFunc:
+ switch (((MergingFunc *) node)->mfop)
+ {
+ case MERGING_ACTION:
+ *name = "action";
+ return 2;
+ case MERGING_CLAUSE_NUMBER:
+ *name = "clause_number";
+ return 2;
+ default:
+ elog(ERROR, "unrecognized MergingFuncOp: %d",
+ (int) ((MergingFunc *) node)->mfop);
+ }
+ break;
case T_SubLink:
switch (((SubLink *) node)->subLinkType)
{
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
new file mode 100644
index 41a3623..3786a7c
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -3615,9 +3615,9 @@ RewriteQuery(Query *parsetree, List *rew
ListCell *lc1;
/*
- * First, recursively process any insert/update/delete statements in WITH
- * clauses. (We have to do this first because the WITH clauses may get
- * copied into rule actions below.)
+ * First, recursively process any insert/update/delete/merge statements in
+ * WITH clauses. (We have to do this first because the WITH clauses may
+ * get copied into rule actions below.)
*/
foreach(lc1, parsetree->cteList)
{
@@ -3642,7 +3642,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/rewrite/rowsecurity.c b/src/backend/rewrite/rowsecurity.c
new file mode 100644
index b1620e4..966dcf0
--- a/src/backend/rewrite/rowsecurity.c
+++ b/src/backend/rewrite/rowsecurity.c
@@ -395,10 +395,10 @@ get_row_security_policies(Query *root, R
* on the final action we take.
*
* We already fetched the SELECT policies above, to check existing rows,
- * but we must also check that new rows created by UPDATE actions are
- * visible, if SELECT rights are required for this relation. We don't do
- * this for INSERT actions, since an INSERT command would only do this
- * check if it had a RETURNING list, and MERGE does not support RETURNING.
+ * but we must also check that new rows created by INSERT/UPDATE actions
+ * are visible, if SELECT rights are required. For INSERT actions, we only
+ * do this if RETURNING is specified, to be consistent with a plain INSERT
+ * command, which can only require SELECT rights when RETURNING is used.
*
* 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
@@ -420,6 +420,8 @@ get_row_security_policies(Query *root, R
List *merge_delete_restrictive_policies;
List *merge_insert_permissive_policies;
List *merge_insert_restrictive_policies;
+ List *merge_select_permissive_policies = NIL;
+ List *merge_select_restrictive_policies = NIL;
/*
* Fetch the UPDATE policies and set them up to execute on the
@@ -457,9 +459,6 @@ get_row_security_policies(Query *root, R
*/
if (perminfo->requiredPerms & ACL_SELECT)
{
- List *merge_select_permissive_policies;
- List *merge_select_restrictive_policies;
-
get_policies_for_relation(rel, CMD_SELECT, user_id,
&merge_select_permissive_policies,
&merge_select_restrictive_policies);
@@ -508,6 +507,21 @@ get_row_security_policies(Query *root, R
withCheckOptions,
hasSubLinks,
false);
+
+ /*
+ * Add ALL/SELECT policies as WCO_RLS_INSERT_CHECK WCOs, to ensure
+ * that the inserted row is visible when executing an INSERT action,
+ * if RETURNING is specified and SELECT rights are required for this
+ * relation.
+ */
+ if (perminfo->requiredPerms & ACL_SELECT && root->returningList)
+ add_with_check_options(rel, rt_index,
+ WCO_RLS_INSERT_CHECK,
+ merge_select_permissive_policies,
+ merge_select_restrictive_policies,
+ withCheckOptions,
+ hasSubLinks,
+ true);
}
table_close(rel, NoLock);
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
new file mode 100644
index e3ccf6c..6f3dc89
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -2142,11 +2142,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/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
new file mode 100644
index ed7f40f..aca2217
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -7197,8 +7197,13 @@ get_merge_query_def(Query *query, depars
appendStringInfoString(buf, "DO NOTHING");
}
- /* No RETURNING support in MERGE yet */
- Assert(query->returningList == NIL);
+ /* Add RETURNING if present */
+ if (query->returningList)
+ {
+ appendContextKeyword(context, " RETURNING",
+ -PRETTYINDENT_STD, PRETTYINDENT_STD, 1);
+ get_target_list(query->returningList, context, NULL, colNamesVisible);
+ }
}
@@ -8298,6 +8303,7 @@ isSimpleNode(Node *node, Node *parentNod
case T_Aggref:
case T_GroupingFunc:
case T_WindowFunc:
+ case T_MergingFunc:
case T_FuncExpr:
case T_JsonConstructorExpr:
/* function-like: name(..) or name[..] */
@@ -8652,6 +8658,27 @@ get_rule_expr(Node *node, deparse_contex
get_windowfunc_expr((WindowFunc *) node, context);
break;
+ case T_MergingFunc:
+ {
+ MergingFunc *mexpr = (MergingFunc *) node;
+
+ appendStringInfoString(buf, "MERGING(");
+ switch (mexpr->mfop)
+ {
+ case MERGING_ACTION:
+ appendStringInfoString(buf, "ACTION");
+ break;
+ case MERGING_CLAUSE_NUMBER:
+ appendStringInfoString(buf, "CLAUSE_NUMBER");
+ break;
+ default:
+ elog(ERROR, "unrecognized MergingFuncOp: %d",
+ (int) mexpr->mfop);
+ }
+ appendStringInfoChar(buf, ')');
+ }
+ break;
+
case T_SubscriptingRef:
{
SubscriptingRef *sbsref = (SubscriptingRef *) node;
diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c
new file mode 100644
index daabf6f..6b63214
--- a/src/bin/psql/common.c
+++ b/src/bin/psql/common.c
@@ -983,13 +983,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/executor/execExpr.h b/src/include/executor/execExpr.h
new file mode 100644
index 048573c..7580ab9
--- a/src/include/executor/execExpr.h
+++ b/src/include/executor/execExpr.h
@@ -241,6 +241,7 @@ typedef enum ExprEvalOp
EEOP_AGGREF,
EEOP_GROUPING_FUNC,
EEOP_WINDOW_FUNC,
+ EEOP_MERGING_FUNC,
EEOP_SUBPLAN,
/* aggregation related nodes */
@@ -623,6 +624,12 @@ typedef struct ExprEvalStep
WindowFuncExprState *wfstate;
} window_func;
+ /* for EEOP_MERGING_FUNC */
+ struct
+ {
+ MergingFuncOp mfop; /* the MERGING() operation to perform */
+ } merging_func;
+
/* for EEOP_SUBPLAN */
struct
{
@@ -806,6 +813,8 @@ extern void ExecEvalJsonConstructor(Expr
ExprContext *econtext);
extern void ExecEvalJsonIsPredicate(ExprState *state, ExprEvalStep *op);
extern void ExecEvalGroupingFunc(ExprState *state, ExprEvalStep *op);
+extern void ExecEvalMergingFunc(ExprState *state, ExprEvalStep *op,
+ ExprContext *econtext);
extern void ExecEvalSubPlan(ExprState *state, ExprEvalStep *op,
ExprContext *econtext);
extern void ExecEvalWholeRowVar(ExprState *state, ExprEvalStep *op,
diff --git a/src/include/executor/spi.h b/src/include/executor/spi.h
new file mode 100644
index d1de139..ed8787d
--- a/src/include/executor/spi.h
+++ b/src/include/executor/spi.h
@@ -97,6 +97,7 @@ typedef struct _SPI_plan *SPIPlanPtr;
#define SPI_OK_REL_UNREGISTER 16
#define SPI_OK_TD_REGISTER 17
#define SPI_OK_MERGE 18
+#define SPI_OK_MERGE_RETURNING 19
#define SPI_OPT_NONATOMIC (1 << 0)
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
new file mode 100644
index 5d7f17d..08bc9ee
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1318,6 +1318,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 cf7e790..0dd872a
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -1688,6 +1688,7 @@ typedef struct MergeWhenClause
typedef struct MergeAction
{
NodeTag type;
+ int index; /* 1-based index of the clause */
bool matched; /* true=MATCHED, false=NOT MATCHED */
CmdType commandType; /* INSERT/UPDATE/DELETE/DO NOTHING */
/* OVERRIDING clause */
@@ -1951,6 +1952,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/nodes/primnodes.h b/src/include/nodes/primnodes.h
new file mode 100644
index ab6d7fd..7a2fe8e
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -564,6 +564,39 @@ typedef struct WindowFunc
} WindowFunc;
/*
+ * MergingFunc
+ *
+ * A MergingFunc is a MERGING(...) expression that can only appear in the
+ * RETURNING list of a MERGE command. It returns information about the
+ * currently executing merge action. Possible operations are:
+ *
+ * MERGING_ACTION:
+ * Return the command string of the current merge action ("INSERT",
+ * "UPDATE" or "DELETE").
+ *
+ * MERGING_CLAUSE_NUMBER:
+ * Return the 1-based index of the current merge WHEN clause.
+ */
+typedef enum MergingFuncOp
+{
+ MERGING_ACTION,
+ MERGING_CLAUSE_NUMBER,
+} MergingFuncOp;
+
+typedef struct MergingFunc
+{
+ Expr xpr;
+ /* operation to perform */
+ MergingFuncOp mfop;
+ /* type Oid of result */
+ Oid mftype pg_node_attr(query_jumble_ignore);
+ /* OID of collation, or InvalidOid if none */
+ Oid mfcollid pg_node_attr(query_jumble_ignore);
+ /* token location, or -1 if unknown */
+ int location;
+} MergingFunc;
+
+/*
* SubscriptingRef: describes a subscripting operation over a container
* (array, etc).
*
diff --git a/src/include/optimizer/paramassign.h b/src/include/optimizer/paramassign.h
new file mode 100644
index 55c27a6..cd0e629
--- a/src/include/optimizer/paramassign.h
+++ b/src/include/optimizer/paramassign.h
@@ -20,6 +20,7 @@ extern Param *replace_outer_placeholderv
PlaceHolderVar *phv);
extern Param *replace_outer_agg(PlannerInfo *root, Aggref *agg);
extern Param *replace_outer_grouping(PlannerInfo *root, GroupingFunc *grp);
+extern Param *replace_outer_merging(PlannerInfo *root, MergingFunc *mf);
extern Param *replace_nestloop_param_var(PlannerInfo *root, Var *var);
extern Param *replace_nestloop_param_placeholdervar(PlannerInfo *root,
PlaceHolderVar *phv);
diff --git a/src/include/parser/analyze.h b/src/include/parser/analyze.h
new file mode 100644
index c96483a..47d988b
--- 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/kwlist.h b/src/include/parser/kwlist.h
new file mode 100644
index 5984dcf..c283a3f
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -80,6 +80,7 @@ PG_KEYWORD("characteristics", CHARACTERI
PG_KEYWORD("check", CHECK, RESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("checkpoint", CHECKPOINT, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("class", CLASS, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("clause_number", CLAUSE_NUMBER, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("close", CLOSE, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("cluster", CLUSTER, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("coalesce", COALESCE, COL_NAME_KEYWORD, BARE_LABEL)
@@ -266,6 +267,7 @@ PG_KEYWORD("matched", MATCHED, UNRESERVE
PG_KEYWORD("materialized", MATERIALIZED, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("maxvalue", MAXVALUE, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("merge", MERGE, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("merging", MERGING, COL_NAME_KEYWORD, BARE_LABEL)
PG_KEYWORD("method", METHOD, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("minute", MINUTE_P, UNRESERVED_KEYWORD, AS_LABEL)
PG_KEYWORD("minvalue", MINVALUE, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
new file mode 100644
index f589112..836819d
--- 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/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
new file mode 100644
index 4b76f76..fb6289c
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -4267,9 +4267,9 @@ exec_stmt_execsql(PLpgSQL_execstate *est
/*
* If we have INTO, then we only need one row back ... but if we have INTO
* STRICT or extra check too_many_rows, ask for two rows, so that we can
- * verify the statement returns only one. INSERT/UPDATE/DELETE are always
- * treated strictly. Without INTO, just run the statement to completion
- * (tcount = 0).
+ * verify the statement returns only one. INSERT/UPDATE/DELETE/MERGE are
+ * always treated strictly. Without INTO, just run the statement to
+ * completion (tcount = 0).
*
* We could just ask for two rows always when using INTO, but there are
* some cases where demanding the extra row costs significant time, eg by
@@ -4307,10 +4307,11 @@ exec_stmt_execsql(PLpgSQL_execstate *est
case SPI_OK_INSERT:
case SPI_OK_UPDATE:
case SPI_OK_DELETE:
+ case SPI_OK_MERGE:
case SPI_OK_INSERT_RETURNING:
case SPI_OK_UPDATE_RETURNING:
case SPI_OK_DELETE_RETURNING:
- case SPI_OK_MERGE:
+ case SPI_OK_MERGE_RETURNING:
Assert(stmt->mod_stmt);
exec_set_found(estate, (SPI_processed != 0));
break;
@@ -4489,10 +4490,11 @@ exec_stmt_dynexecute(PLpgSQL_execstate *
case SPI_OK_INSERT:
case SPI_OK_UPDATE:
case SPI_OK_DELETE:
+ case SPI_OK_MERGE:
case SPI_OK_INSERT_RETURNING:
case SPI_OK_UPDATE_RETURNING:
case SPI_OK_DELETE_RETURNING:
- case SPI_OK_MERGE:
+ case SPI_OK_MERGE_RETURNING:
case SPI_OK_UTILITY:
case SPI_OK_REWRITTEN:
break;
diff --git a/src/pl/tcl/pltcl.c b/src/pl/tcl/pltcl.c
new file mode 100644
index e8f9d7b..0c9f81e
--- a/src/pl/tcl/pltcl.c
+++ b/src/pl/tcl/pltcl.c
@@ -2456,6 +2456,7 @@ pltcl_process_SPI_result(Tcl_Interp *int
case SPI_OK_INSERT_RETURNING:
case SPI_OK_DELETE_RETURNING:
case SPI_OK_UPDATE_RETURNING:
+ case SPI_OK_MERGE_RETURNING:
/*
* Process the tuples we got
diff --git a/src/test/regress/expected/merge.out b/src/test/regress/expected/merge.out
new file mode 100644
index 28a6d0b..183d160
--- 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;
@@ -1256,7 +1256,7 @@ BEGIN;
MERGE INTO sq_target
USING v
ON tid = sid
-WHEN MATCHED AND tid > 2 THEN
+WHEN MATCHED AND tid >= 2 THEN
UPDATE SET balance = balance + delta
WHEN NOT MATCHED THEN
INSERT (balance, tid) VALUES (balance + delta, sid)
@@ -1271,7 +1271,7 @@ INSERT INTO sq_source (sid, balance, del
MERGE INTO sq_target t
USING v
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)
@@ -1295,7 +1295,7 @@ WITH targq AS (
MERGE INTO sq_target t
USING v
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)
@@ -1304,20 +1304,191 @@ WHEN MATCHED AND tid < 2 THEN
ROLLBACK;
-- RETURNING
BEGIN;
-INSERT INTO sq_source (sid, balance, delta) VALUES (-1, -1, -10);
+CREATE TABLE merge_actions(action text, abbrev text);
+INSERT INTO merge_actions VALUES ('INSERT', 'ins'), ('UPDATE', 'upd'), ('DELETE', 'del');
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)
+ 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 *;
- ^
+ DELETE
+RETURNING MERGING(CLAUSE_NUMBER) AS clause,
+ (SELECT abbrev FROM merge_actions
+ WHERE action = MERGING(ACTION)) AS action,
+ t.*,
+ CASE MERGING(ACTION)
+ WHEN 'INSERT' THEN 'Inserted '||t
+ WHEN 'UPDATE' THEN 'Added '||delta||' to balance'
+ WHEN 'DELETE' THEN 'Removed '||t
+ END AS description;
+ clause | action | tid | balance | description
+--------+--------+-----+---------+---------------------
+ 3 | del | 1 | 100 | Removed (1,100)
+ 1 | upd | 2 | 220 | Added 20 to balance
+ 2 | ins | 4 | 40 | Inserted (4,40)
+(3 rows)
+
+ROLLBACK;
+-- error when using MERGING() outside MERGE
+SELECT MERGING(ACTION) FROM sq_target;
+ERROR: MERGING() can only be used in the RETURNING list of a MERGE command
+LINE 1: SELECT MERGING(ACTION) FROM sq_target;
+ ^
+UPDATE sq_target SET balance = balance + 1 RETURNING MERGING(CLAUSE_NUMBER);
+ERROR: MERGING() can only be used in the RETURNING list of a MERGE command
+LINE 1: ...ATE sq_target SET balance = balance + 1 RETURNING MERGING(CL...
+ ^
+-- RETURNING in CTEs
+CREATE TABLE sq_target_merge_log (tid integer NOT NULL, last_change text);
+INSERT INTO sq_target_merge_log VALUES (1, 'Original value');
+BEGIN;
+WITH m AS (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 MERGING(CLAUSE_NUMBER), MERGING(ACTION), t.*,
+ CASE MERGING(ACTION)
+ WHEN 'INSERT' THEN 'Inserted '||t
+ WHEN 'UPDATE' THEN 'Added '||delta||' to balance'
+ WHEN 'DELETE' THEN 'Removed '||t
+ END AS description
+), m2 AS (
+ MERGE INTO sq_target_merge_log l
+ USING m
+ ON l.tid = m.tid
+ WHEN MATCHED THEN
+ UPDATE SET last_change = description
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (m.tid, description)
+ RETURNING action, MERGING(ACTION) AS log_action, l.*
+)
+SELECT * FROM m2;
+ action | log_action | tid | last_change
+--------+------------+-----+---------------------
+ DELETE | UPDATE | 1 | Removed (1,100)
+ UPDATE | INSERT | 2 | Added 20 to balance
+ INSERT | INSERT | 4 | Inserted (4,40)
+(3 rows)
+
+SELECT * FROM sq_target_merge_log ORDER BY tid;
+ tid | last_change
+-----+---------------------
+ 1 | Removed (1,100)
+ 2 | Added 20 to balance
+ 4 | Inserted (4,40)
+(3 rows)
+
+ROLLBACK;
+-- COPY (MERGE ... RETURNING) TO ...
+BEGIN;
+COPY (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 MERGING(ACTION), t.*
+) TO stdout;
+DELETE 1 100
+UPDATE 2 220
+INSERT 4 40
+ROLLBACK;
+-- SQL function with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT action text, OUT tid int, OUT new_balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING MERGING(ACTION), t.*;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT action, tid, new_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ action | tid | new_balance
+--------+-----+-------------
+ DELETE | 1 | 100
+ UPDATE | 3 | 320
+ INSERT | 4 | 110
+(3 rows)
+
+ROLLBACK;
+-- SQL SRF with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_sq_source_into_sq_target()
+RETURNS TABLE (action text, tid int, balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 MERGING(ACTION), t.*;
+$$;
+SELECT * FROM merge_sq_source_into_sq_target();
+ action | tid | balance
+--------+-----+---------
+ DELETE | 1 | 100
+ UPDATE | 2 | 220
+ INSERT | 4 | 40
+(3 rows)
+
+ROLLBACK;
+-- PL/pgSQL function with MERGE ... RETURNING ... INTO
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT r_action text, OUT r_tid int, OUT r_balance int)
+LANGUAGE plpgsql AS
+$$
+BEGIN
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING MERGING(ACTION), t.* INTO r_action, r_tid, r_balance;
+END;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT r_action, r_tid, r_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ r_action | r_tid | r_balance
+----------+-------+-----------
+ DELETE | 1 | 100
+ UPDATE | 3 | 320
+ INSERT | 4 | 110
+(3 rows)
+
ROLLBACK;
-- EXPLAIN
CREATE TABLE ex_mtarget (a int, b int)
@@ -1514,7 +1685,7 @@ SELECT * FROM sq_target WHERE tid = 1;
(1 row)
ROLLBACK;
-DROP TABLE sq_target, sq_source CASCADE;
+DROP TABLE sq_target, sq_target_merge_log, sq_source CASCADE;
NOTICE: drop cascades to view v
CREATE TABLE pa_target (tid integer, balance float, val text)
PARTITION BY LIST (tid);
@@ -1640,6 +1811,32 @@ SELECT * FROM pa_target ORDER BY tid;
(14 rows)
ROLLBACK;
+-- update partition key to partition not initially scanned
+BEGIN;
+MERGE INTO pa_target t
+ USING pa_source s
+ ON t.tid = s.sid AND t.tid = 1
+ WHEN MATCHED THEN
+ UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
+ RETURNING MERGING(ACTION), MERGING(CLAUSE_NUMBER), t.*;
+ action | clause_number | tid | balance | val
+--------+---------------+-----+---------+--------------------------
+ UPDATE | 1 | 2 | 110 | initial updated by merge
+(1 row)
+
+SELECT * FROM pa_target ORDER BY tid;
+ tid | balance | val
+-----+---------+--------------------------
+ 2 | 110 | initial updated by merge
+ 3 | 300 | initial
+ 5 | 500 | initial
+ 7 | 700 | initial
+ 9 | 900 | initial
+ 11 | 1100 | initial
+ 13 | 1300 | initial
+(7 rows)
+
+ROLLBACK;
DROP TABLE pa_target CASCADE;
-- The target table is partitioned in the same way, but this time by attaching
-- partitions which have columns in different order, dropped columns etc.
@@ -1731,7 +1928,26 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (sid, delta, 'inserted by merge');
+ INSERT VALUES (sid, delta, 'inserted by merge')
+ RETURNING MERGING(ACTION), MERGING(CLAUSE_NUMBER), t.*;
+ action | clause_number | tid | balance | val
+--------+---------------+-----+---------+--------------------------
+ UPDATE | 1 | 2 | 110 | initial updated by merge
+ INSERT | 2 | 2 | 20 | inserted by merge
+ UPDATE | 1 | 4 | 330 | initial updated by merge
+ INSERT | 2 | 4 | 40 | inserted by merge
+ UPDATE | 1 | 6 | 550 | initial updated by merge
+ INSERT | 2 | 6 | 60 | inserted by merge
+ UPDATE | 1 | 8 | 770 | initial updated by merge
+ INSERT | 2 | 8 | 80 | inserted by merge
+ UPDATE | 1 | 10 | 990 | initial updated by merge
+ INSERT | 2 | 10 | 100 | inserted by merge
+ UPDATE | 1 | 12 | 1210 | initial updated by merge
+ INSERT | 2 | 12 | 120 | inserted by merge
+ UPDATE | 1 | 14 | 1430 | initial updated by merge
+ INSERT | 2 | 14 | 140 | inserted by merge
+(14 rows)
+
SELECT * FROM pa_target ORDER BY tid;
tid | balance | val
-----+---------+--------------------------
@@ -1798,7 +2014,21 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge');
+ INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge')
+ RETURNING MERGING(ACTION), MERGING(CLAUSE_NUMBER), t.*;
+ action | clause_number | logts | tid | balance | val
+--------+---------------+--------------------------+-----+---------+--------------------------
+ UPDATE | 1 | Tue Jan 31 00:00:00 2017 | 1 | 110 | initial updated by merge
+ UPDATE | 1 | Tue Feb 28 00:00:00 2017 | 2 | 220 | initial updated by merge
+ INSERT | 2 | Sun Jan 15 00:00:00 2017 | 3 | 30 | inserted by merge
+ UPDATE | 1 | Tue Jan 31 00:00:00 2017 | 4 | 440 | initial updated by merge
+ UPDATE | 1 | Tue Feb 28 00:00:00 2017 | 5 | 550 | initial updated by merge
+ INSERT | 2 | Sun Jan 15 00:00:00 2017 | 6 | 60 | inserted by merge
+ UPDATE | 1 | Tue Jan 31 00:00:00 2017 | 7 | 770 | initial updated by merge
+ UPDATE | 1 | Tue Feb 28 00:00:00 2017 | 8 | 880 | initial updated by merge
+ INSERT | 2 | Sun Jan 15 00:00:00 2017 | 9 | 90 | inserted by merge
+(9 rows)
+
SELECT * FROM pa_target ORDER BY tid;
logts | tid | balance | val
--------------------------+-----+---------+--------------------------
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
new file mode 100644
index 6988128..3fd4fab
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -2302,6 +2302,35 @@ WHEN MATCHED THEN
UPDATE SET dnotes = dnotes || ' notes added by merge8 '
WHEN NOT MATCHED THEN
INSERT VALUES (13, 44, 1, 'regress_rls_bob', 'new manga');
+SELECT * FROM document WHERE did = 13;
+ did | cid | dlevel | dauthor | dtitle | dnotes
+-----+-----+--------+---------+--------+--------
+(0 rows)
+
+-- but not OK if RETURNING is used
+MERGE INTO document d
+USING (SELECT 14 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge9 '
+WHEN NOT MATCHED THEN
+ INSERT VALUES (14, 44, 1, 'regress_rls_bob', 'new manga')
+RETURNING *;
+ERROR: new row violates row-level security policy for table "document"
+-- but OK if new row is visible
+MERGE INTO document d
+USING (SELECT 14 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge10 '
+WHEN NOT MATCHED THEN
+ INSERT VALUES (14, 11, 1, 'regress_rls_bob', 'new novel')
+RETURNING *;
+ sdid | did | cid | dlevel | dauthor | dtitle | dnotes
+------+-----+-----+--------+-----------------+-----------+--------
+ 14 | 14 | 11 | 1 | regress_rls_bob | new novel |
+(1 row)
+
RESET SESSION AUTHORIZATION;
-- drop the restrictive SELECT policy so that we can look at the
-- final state of the table
@@ -2325,7 +2354,8 @@ SELECT * FROM document;
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 notes added by merge7
13 | 44 | 1 | regress_rls_bob | new manga |
-(15 rows)
+ 14 | 11 | 1 | regress_rls_bob | new novel |
+(16 rows)
--
-- ROLE/GROUP
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
new file mode 100644
index 1442c43..f187dec
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -3596,7 +3596,8 @@ MERGE INTO rule_merge2 t USING (SELECT 1
-- test deparsing
CREATE TABLE sf_target(id int, data text, filling int[]);
CREATE FUNCTION merge_sf_test()
- RETURNS void
+ RETURNS TABLE(action text, clause_number int,
+ a int, b text, id int, data text, filling int[])
LANGUAGE sql
BEGIN ATOMIC
MERGE INTO sf_target t
@@ -3633,11 +3634,13 @@ WHEN NOT MATCHED
VALUES (s.a, s.b, DEFAULT)
WHEN NOT MATCHED
THEN INSERT (filling[1], id)
- VALUES (s.a, s.a);
+ VALUES (s.a, s.a)
+RETURNING
+ merging(action), merging(clause_number) AS clause_number, *;
END;
\sf merge_sf_test
CREATE OR REPLACE FUNCTION public.merge_sf_test()
- RETURNS void
+ RETURNS TABLE(action text, clause_number integer, a integer, b text, id integer, data text, filling integer[])
LANGUAGE sql
BEGIN ATOMIC
MERGE INTO sf_target t
@@ -3674,7 +3677,14 @@ BEGIN ATOMIC
VALUES (s.a, s.b, DEFAULT)
WHEN NOT MATCHED
THEN INSERT (filling[1], id)
- VALUES (s.a, s.a);
+ VALUES (s.a, s.a)
+ RETURNING MERGING(ACTION) AS action,
+ MERGING(CLAUSE_NUMBER) AS clause_number,
+ s.a,
+ s.b,
+ t.id,
+ t.data,
+ t.filling;
END
DROP FUNCTION merge_sf_test;
DROP TABLE sf_target;
diff --git a/src/test/regress/sql/merge.sql b/src/test/regress/sql/merge.sql
new file mode 100644
index 4cf6db9..0eb2f97
--- 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
@@ -816,7 +816,7 @@ BEGIN;
MERGE INTO sq_target
USING v
ON tid = sid
-WHEN MATCHED AND tid > 2 THEN
+WHEN MATCHED AND tid >= 2 THEN
UPDATE SET balance = balance + delta
WHEN NOT MATCHED THEN
INSERT (balance, tid) VALUES (balance + delta, sid)
@@ -829,7 +829,7 @@ INSERT INTO sq_source (sid, balance, del
MERGE INTO sq_target t
USING v
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)
@@ -847,7 +847,7 @@ WITH targq AS (
MERGE INTO sq_target t
USING v
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)
@@ -857,17 +857,146 @@ ROLLBACK;
-- RETURNING
BEGIN;
-INSERT INTO sq_source (sid, balance, delta) VALUES (-1, -1, -10);
+CREATE TABLE merge_actions(action text, abbrev text);
+INSERT INTO merge_actions VALUES ('INSERT', 'ins'), ('UPDATE', 'upd'), ('DELETE', 'del');
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)
+ INSERT (balance, tid) VALUES (balance + delta, sid)
WHEN MATCHED AND tid < 2 THEN
- DELETE
-RETURNING *;
+ DELETE
+RETURNING MERGING(CLAUSE_NUMBER) AS clause,
+ (SELECT abbrev FROM merge_actions
+ WHERE action = MERGING(ACTION)) AS action,
+ t.*,
+ CASE MERGING(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 MERGING() outside MERGE
+SELECT MERGING(ACTION) FROM sq_target;
+UPDATE sq_target SET balance = balance + 1 RETURNING MERGING(CLAUSE_NUMBER);
+
+-- RETURNING in CTEs
+CREATE TABLE sq_target_merge_log (tid integer NOT NULL, last_change text);
+INSERT INTO sq_target_merge_log VALUES (1, 'Original value');
+BEGIN;
+WITH m AS (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 MERGING(CLAUSE_NUMBER), MERGING(ACTION), t.*,
+ CASE MERGING(ACTION)
+ WHEN 'INSERT' THEN 'Inserted '||t
+ WHEN 'UPDATE' THEN 'Added '||delta||' to balance'
+ WHEN 'DELETE' THEN 'Removed '||t
+ END AS description
+), m2 AS (
+ MERGE INTO sq_target_merge_log l
+ USING m
+ ON l.tid = m.tid
+ WHEN MATCHED THEN
+ UPDATE SET last_change = description
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (m.tid, description)
+ RETURNING action, MERGING(ACTION) AS log_action, l.*
+)
+SELECT * FROM m2;
+SELECT * FROM sq_target_merge_log ORDER BY tid;
+ROLLBACK;
+
+-- COPY (MERGE ... RETURNING) TO ...
+BEGIN;
+COPY (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 MERGING(ACTION), t.*
+) TO stdout;
+ROLLBACK;
+
+-- SQL function with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT action text, OUT tid int, OUT new_balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING MERGING(ACTION), t.*;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT action, tid, new_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ROLLBACK;
+
+-- SQL SRF with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_sq_source_into_sq_target()
+RETURNS TABLE (action text, tid int, balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 MERGING(ACTION), t.*;
+$$;
+SELECT * FROM merge_sq_source_into_sq_target();
+ROLLBACK;
+
+-- PL/pgSQL function with MERGE ... RETURNING ... INTO
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT r_action text, OUT r_tid int, OUT r_balance int)
+LANGUAGE plpgsql AS
+$$
+BEGIN
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING MERGING(ACTION), t.* INTO r_action, r_tid, r_balance;
+END;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT r_action, r_tid, r_balance FROM merge_into_sq_target(sid, balance, delta)) m;
ROLLBACK;
-- EXPLAIN
@@ -966,7 +1095,7 @@ WHEN MATCHED THEN
SELECT * FROM sq_target WHERE tid = 1;
ROLLBACK;
-DROP TABLE sq_target, sq_source CASCADE;
+DROP TABLE sq_target, sq_target_merge_log, sq_source CASCADE;
CREATE TABLE pa_target (tid integer, balance float, val text)
PARTITION BY LIST (tid);
@@ -1033,6 +1162,17 @@ SELECT merge_func();
SELECT * FROM pa_target ORDER BY tid;
ROLLBACK;
+-- update partition key to partition not initially scanned
+BEGIN;
+MERGE INTO pa_target t
+ USING pa_source s
+ ON t.tid = s.sid AND t.tid = 1
+ WHEN MATCHED THEN
+ UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
+ RETURNING MERGING(ACTION), MERGING(CLAUSE_NUMBER), t.*;
+SELECT * FROM pa_target ORDER BY tid;
+ROLLBACK;
+
DROP TABLE pa_target CASCADE;
-- The target table is partitioned in the same way, but this time by attaching
@@ -1091,7 +1231,8 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (sid, delta, 'inserted by merge');
+ INSERT VALUES (sid, delta, 'inserted by merge')
+ RETURNING MERGING(ACTION), MERGING(CLAUSE_NUMBER), t.*;
SELECT * FROM pa_target ORDER BY tid;
ROLLBACK;
@@ -1145,7 +1286,8 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge');
+ INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge')
+ RETURNING MERGING(ACTION), MERGING(CLAUSE_NUMBER), t.*;
SELECT * FROM pa_target ORDER BY tid;
ROLLBACK;
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
new file mode 100644
index dec7340..1d5ed0a
--- a/src/test/regress/sql/rowsecurity.sql
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -992,6 +992,27 @@ WHEN MATCHED THEN
UPDATE SET dnotes = dnotes || ' notes added by merge8 '
WHEN NOT MATCHED THEN
INSERT VALUES (13, 44, 1, 'regress_rls_bob', 'new manga');
+SELECT * FROM document WHERE did = 13;
+
+-- but not OK if RETURNING is used
+MERGE INTO document d
+USING (SELECT 14 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge9 '
+WHEN NOT MATCHED THEN
+ INSERT VALUES (14, 44, 1, 'regress_rls_bob', 'new manga')
+RETURNING *;
+
+-- but OK if new row is visible
+MERGE INTO document d
+USING (SELECT 14 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge10 '
+WHEN NOT MATCHED THEN
+ INSERT VALUES (14, 11, 1, 'regress_rls_bob', 'new novel')
+RETURNING *;
RESET SESSION AUTHORIZATION;
-- drop the restrictive SELECT policy so that we can look at the
diff --git a/src/test/regress/sql/rules.sql b/src/test/regress/sql/rules.sql
new file mode 100644
index 8b7e255..d820b9b
--- a/src/test/regress/sql/rules.sql
+++ b/src/test/regress/sql/rules.sql
@@ -1281,7 +1281,8 @@ MERGE INTO rule_merge2 t USING (SELECT 1
CREATE TABLE sf_target(id int, data text, filling int[]);
CREATE FUNCTION merge_sf_test()
- RETURNS void
+ RETURNS TABLE(action text, clause_number int,
+ a int, b text, id int, data text, filling int[])
LANGUAGE sql
BEGIN ATOMIC
MERGE INTO sf_target t
@@ -1318,7 +1319,9 @@ WHEN NOT MATCHED
VALUES (s.a, s.b, DEFAULT)
WHEN NOT MATCHED
THEN INSERT (filling[1], id)
- VALUES (s.a, s.a);
+ VALUES (s.a, s.a)
+RETURNING
+ merging(action), merging(clause_number) AS clause_number, *;
END;
\sf merge_sf_test
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
new file mode 100644
index 87c1aee..31a552e
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1559,6 +1559,8 @@ MergePath
MergeScanSelCache
MergeStmt
MergeWhenClause
+MergingFunc
+MergingFuncOp
MetaCommand
MinMaxAggInfo
MinMaxAggPath
On Sun, 5 Nov 2023 at 11:52, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
OK, that's a fair point. Attached is a new version, replacing those
parts of the implementation with a new MergingFunc node. It doesn't
add that much more complexity, and I think the new code is much
neater.
Rebased version attached, following the changes made in 615f5f6faa and
a4f7d33a90.
Regards,
Dean
Attachments:
support-merge-returning-v13.patchtext/x-patch; charset=US-ASCII; name=support-merge-returning-v13.patchDownload
diff --git a/doc/src/sgml/dml.sgml b/doc/src/sgml/dml.sgml
new file mode 100644
index cbbc5e2..3d95bdb
--- a/doc/src/sgml/dml.sgml
+++ b/doc/src/sgml/dml.sgml
@@ -283,10 +283,15 @@ DELETE FROM products;
<secondary>RETURNING</secondary>
</indexterm>
+ <indexterm zone="dml-returning">
+ <primary>MERGE</primary>
+ <secondary>RETURNING</secondary>
+ </indexterm>
+
<para>
Sometimes it is useful to obtain data from modified rows while they are
being manipulated. The <command>INSERT</command>, <command>UPDATE</command>,
- and <command>DELETE</command> commands all have an
+ <command>DELETE</command>, and <command>MERGE</command> commands all have an
optional <literal>RETURNING</literal> clause that supports this. Use
of <literal>RETURNING</literal> avoids performing an extra database query to
collect the data, and is especially valuable when it would otherwise be
@@ -339,6 +344,21 @@ DELETE FROM products
</programlisting>
</para>
+ <para>
+ In a <command>MERGE</command>, the data available to <literal>RETURNING</literal> is
+ the content of the source row plus the content of the inserted, updated, or
+ deleted target row. Since it is quite common for the source and target to
+ have many of the same columns, specifying <literal>RETURNING *</literal>
+ can lead to a lot of duplicated columns, so it is often more useful to
+ qualify it so as to return just the source or target row. For example:
+<programlisting>
+MERGE INTO products p USING new_products n ON p.product_no = n.product_no
+ WHEN NOT MATCHED THEN INSERT VALUES (n.product_no, n.name, n.price)
+ WHEN MATCHED THEN UPDATE SET name = n.name, price = n.price
+ RETURNING p.*;
+</programlisting>
+ </para>
+
<para>
If there are triggers (<xref linkend="triggers"/>) on the target table,
the data available to <literal>RETURNING</literal> is the row as modified by
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
new file mode 100644
index d963f0a..b25de53
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -21990,6 +21990,84 @@ SELECT count(*) FROM sometable;
</sect1>
+ <sect1 id="functions-merge-support">
+ <title>Merge Support Functions</title>
+
+ <indexterm>
+ <primary>MERGE</primary>
+ <secondary>RETURNING</secondary>
+ </indexterm>
+
+ <indexterm>
+ <primary>MERGING</primary>
+ </indexterm>
+
+ <para>
+ <productname>PostgreSQL</productname> includes one merge support function
+ that may be used in the <literal>RETURNING</literal> list of a
+ <xref linkend="sql-merge"/> command to return additional information about
+ the action taken for each row:
+<synopsis>
+<function id="function-merging">MERGING</function> ( <replaceable>property</replaceable> )
+</synopsis>
+ The following are valid property values specifying what to return:
+
+ <variablelist>
+ <varlistentry>
+ <term><literal>ACTION</literal></term>
+ <listitem>
+ <para>
+ The merge action command executed for the current row
+ (<literal>'INSERT'</literal>, <literal>'UPDATE'</literal>, or
+ <literal>'DELETE'</literal>).
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>CLAUSE_NUMBER</literal></term>
+ <listitem>
+ <para>
+ The 1-based index of the <literal>WHEN</literal> clause executed for
+ the current row.
+ </para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </para>
+
+ <para>
+ The following example illustrates how this may be used to determine
+ which actions were performed for each row affected by the
+ <command>MERGE</command> command:
+<screen><![CDATA[
+MERGE INTO products p
+ USING stock s ON p.product_id = s.product_id
+ WHEN MATCHED AND s.quantity > 0 THEN
+ UPDATE SET in_stock = true, quantity = s.quantity
+ WHEN MATCHED THEN
+ UPDATE SET in_stock = false, quantity = 0
+ WHEN NOT MATCHED THEN
+ INSERT (product_id, in_stock, quantity)
+ VALUES (s.product_id, true, s.quantity)
+ RETURNING MERGING(ACTION), MERGING(CLAUSE_NUMBER), p.*;
+
+ action | clause_number | product_id | in_stock | quantity
+--------+---------------+------------+----------+----------
+ UPDATE | 1 | 1001 | t | 50
+ UPDATE | 2 | 1002 | f | 0
+ INSERT | 3 | 1003 | t | 10
+]]></screen>
+ </para>
+
+ <para>
+ Note that this function can only be used in the <literal>RETURNING</literal>
+ list of a <command>MERGE</command> command. It is an error to use it in any
+ other part of a query.
+ </para>
+
+ </sect1>
+
<sect1 id="functions-subquery">
<title>Subquery Expressions</title>
diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml
new file mode 100644
index fe8def4..51a15ca
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -1387,9 +1387,9 @@
to a <glossterm linkend="glossary-client">client</glossterm> upon the
completion of an <acronym>SQL</acronym> command, usually a
<command>SELECT</command> but it can be an
- <command>INSERT</command>, <command>UPDATE</command>, or
- <command>DELETE</command> command if the <literal>RETURNING</literal>
- clause is specified.
+ <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command> command if the
+ <literal>RETURNING</literal> clause is specified.
</para>
<para>
The fact that a result set is a relation means that a query can be used
diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml
new file mode 100644
index 5977534..4ad9894
--- a/doc/src/sgml/plpgsql.sgml
+++ b/doc/src/sgml/plpgsql.sgml
@@ -1020,8 +1020,8 @@ INSERT INTO mytable VALUES (1,'one'), (2
</para>
<para>
- If the command does return rows (for example <command>SELECT</command>,
- or <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>
+ If the command does return rows (for example <command>SELECT</command>, or
+ <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command>
with <literal>RETURNING</literal>), there are two ways to proceed.
When the command will return at most one row, or you only care about
the first row of output, write the command as usual but add
@@ -1149,6 +1149,7 @@ SELECT <replaceable>select_expressions</
INSERT ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
UPDATE ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
DELETE ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
+MERGE ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
</synopsis>
where <replaceable>target</replaceable> can be a record variable, a row
@@ -1159,8 +1160,8 @@ DELETE ... RETURNING <replaceable>expres
<literal>INTO</literal> clause) just as described above,
and the plan is cached in the same way.
This works for <command>SELECT</command>,
- <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command> with
- <literal>RETURNING</literal>, and certain utility commands
+ <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command>
+ with <literal>RETURNING</literal>, and certain utility commands
that return row sets, such as <command>EXPLAIN</command>.
Except for the <literal>INTO</literal> clause, the SQL command is the same
as it would be written outside <application>PL/pgSQL</application>.
@@ -1236,7 +1237,7 @@ END;
</para>
<para>
- For <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command> with
+ For <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command> with
<literal>RETURNING</literal>, <application>PL/pgSQL</application> reports
an error for more than one returned row, even when
<literal>STRICT</literal> is not specified. This is because there
diff --git a/doc/src/sgml/queries.sgml b/doc/src/sgml/queries.sgml
new file mode 100644
index 8a4674e..ae1f738
--- a/doc/src/sgml/queries.sgml
+++ b/doc/src/sgml/queries.sgml
@@ -2062,9 +2062,10 @@ SELECT <replaceable>select_list</replace
Table Expressions or <acronym>CTE</acronym>s, can be thought of as defining
temporary tables that exist just for one query. Each auxiliary statement
in a <literal>WITH</literal> clause can be a <command>SELECT</command>,
- <command>INSERT</command>, <command>UPDATE</command>, or <command>DELETE</command>; and the
+ <command>INSERT</command>, <command>UPDATE</command>, <command>DELETE</command>,
+ or <command>MERGE</command>; and the
<literal>WITH</literal> clause itself is attached to a primary statement that can
- be a <command>SELECT</command>, <command>INSERT</command>, <command>UPDATE</command>,
+ also be a <command>SELECT</command>, <command>INSERT</command>, <command>UPDATE</command>,
<command>DELETE</command>, or <command>MERGE</command>.
</para>
@@ -2598,8 +2599,8 @@ SELECT * FROM w AS w1 JOIN w AS w2 ON w1
<title>Data-Modifying Statements in <literal>WITH</literal></title>
<para>
- You can use most data-modifying statements (<command>INSERT</command>,
- <command>UPDATE</command>, or <command>DELETE</command>, but not
+ You can use data-modifying statements (<command>INSERT</command>,
+ <command>UPDATE</command>, <command>DELETE</command>, or
<command>MERGE</command>) in <literal>WITH</literal>. This
allows you to perform several different operations in the same query.
An example is:
diff --git a/doc/src/sgml/ref/copy.sgml b/doc/src/sgml/ref/copy.sgml
new file mode 100644
index d12ba96..9b9536b
--- a/doc/src/sgml/ref/copy.sgml
+++ b/doc/src/sgml/ref/copy.sgml
@@ -122,13 +122,16 @@ COPY { <replaceable class="parameter">ta
A <link linkend="sql-select"><command>SELECT</command></link>,
<link linkend="sql-values"><command>VALUES</command></link>,
<link linkend="sql-insert"><command>INSERT</command></link>,
- <link linkend="sql-update"><command>UPDATE</command></link>, or
- <link linkend="sql-delete"><command>DELETE</command></link> command whose results are to be
- copied. Note that parentheses are required around the query.
+ <link linkend="sql-update"><command>UPDATE</command></link>,
+ <link linkend="sql-delete"><command>DELETE</command></link>, or
+ <link linkend="sql-merge"><command>MERGE</command></link> command
+ whose results are to be copied. Note that parentheses are required
+ around the query.
</para>
<para>
- For <command>INSERT</command>, <command>UPDATE</command> and
- <command>DELETE</command> queries a RETURNING clause must be provided,
+ For <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, and <command>MERGE</command> queries a
+ RETURNING clause must be provided,
and the target relation must not have a conditional rule, nor
an <literal>ALSO</literal> rule, nor an <literal>INSTEAD</literal> rule
that expands to multiple statements.
diff --git a/doc/src/sgml/ref/merge.sgml b/doc/src/sgml/ref/merge.sgml
new file mode 100644
index 0f61d47..1ef29ab
--- a/doc/src/sgml/ref/merge.sgml
+++ b/doc/src/sgml/ref/merge.sgml
@@ -25,6 +25,7 @@ PostgreSQL documentation
MERGE INTO [ ONLY ] <replaceable class="parameter">target_table_name</replaceable> [ * ] [ [ AS ] <replaceable class="parameter">target_alias</replaceable> ]
USING <replaceable class="parameter">data_source</replaceable> ON <replaceable class="parameter">join_condition</replaceable>
<replaceable class="parameter">when_clause</replaceable> [...]
+[ RETURNING * | <replaceable class="parameter">output_expression</replaceable> [ [ AS ] <replaceable class="parameter">output_name</replaceable> ] [, ...] ]
<phrase>where <replaceable class="parameter">data_source</replaceable> is:</phrase>
@@ -95,6 +96,18 @@ DELETE
</para>
<para>
+ The optional <literal>RETURNING</literal> clause causes <command>MERGE</command>
+ to compute and return value(s) based on each row inserted, updated, or
+ deleted. Any expression using the source or target table's columns, or
+ the <xref linkend="function-merging"/> function can be computed. When an
+ <command>INSERT</command> or <command>UPDATE</command> action is performed,
+ the new values of the target table's columns are used. When a
+ <command>DELETE</command> is performed, the old values of the target table's
+ columns are used. The syntax of the <literal>RETURNING</literal> list is
+ identical to that of the output list of <command>SELECT</command>.
+ </para>
+
+ <para>
There is no separate <literal>MERGE</literal> privilege.
If you specify an update action, you must have the
<literal>UPDATE</literal> privilege on the column(s)
@@ -125,6 +138,18 @@ DELETE
<variablelist>
<varlistentry>
+ <term><replaceable class="parameter">with_query</replaceable></term>
+ <listitem>
+ <para>
+ The <literal>WITH</literal> clause allows you to specify one or more
+ subqueries that can be referenced by name in the <command>MERGE</command>
+ query. See <xref linkend="queries-with"/> and <xref linkend="sql-select"/>
+ for details.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
<term><replaceable class="parameter">target_table_name</replaceable></term>
<listitem>
<para>
@@ -399,13 +424,30 @@ DELETE
</varlistentry>
<varlistentry>
- <term><replaceable class="parameter">with_query</replaceable></term>
+ <term><replaceable class="parameter">output_expression</replaceable></term>
<listitem>
<para>
- The <literal>WITH</literal> clause allows you to specify one or more
- subqueries that can be referenced by name in the <command>MERGE</command>
- query. See <xref linkend="queries-with"/> and <xref linkend="sql-select"/>
- for details.
+ An expression to be computed and returned by the <command>MERGE</command>
+ command after each row is merged. The expression can use any columns of
+ the source or target tables, or the <xref linkend="function-merging"/>
+ function to return additional information about the action executed.
+ </para>
+ <para>
+ Writing <literal>*</literal> will return all columns from the source
+ table, followed by all columns from the target table. Often this will
+ lead to a lot of duplication, since it is common for the source and
+ target tables to have a lot of the same columns. This can be avoided by
+ qualifying the <literal>*</literal> with the name or alias of the source
+ or target table.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><replaceable class="parameter">output_name</replaceable></term>
+ <listitem>
+ <para>
+ A name to use for a returned column.
</para>
</listitem>
</varlistentry>
@@ -428,6 +470,13 @@ MERGE <replaceable class="parameter">tot
were changed in any way.
</para>
+ <para>
+ If the <command>MERGE</command> command contains a <literal>RETURNING</literal>
+ clause, the result will be similar to that of a <command>SELECT</command>
+ statement containing the columns and values defined in the
+ <literal>RETURNING</literal> list, computed over the row(s) inserted, updated,
+ or deleted by the command.
+ </para>
</refsect1>
<refsect1>
@@ -544,13 +593,6 @@ MERGE <replaceable class="parameter">tot
</para>
<para>
- There is no <literal>RETURNING</literal> clause with
- <command>MERGE</command>. Actions of <command>INSERT</command>,
- <command>UPDATE</command> and <command>DELETE</command> cannot contain
- <literal>RETURNING</literal> or <literal>WITH</literal> clauses.
- </para>
-
- <para>
When <command>MERGE</command> is run concurrently with other commands
that modify the target table, the usual transaction isolation rules
apply; see <xref linkend="transaction-iso"/> for an explanation
@@ -602,7 +644,8 @@ WHEN NOT MATCHED THEN
<para>
Attempt to insert a new stock item along with the quantity of stock. If
the item already exists, instead update the stock count of the existing
- item. Don't allow entries that have zero stock.
+ item. Don't allow entries that have zero stock. Return details of all
+ changes made.
<programlisting>
MERGE INTO wines w
USING wine_stock_changes s
@@ -612,7 +655,8 @@ WHEN NOT MATCHED AND s.stock_delta > 0 T
WHEN MATCHED AND w.stock + s.stock_delta > 0 THEN
UPDATE SET stock = w.stock + s.stock_delta
WHEN MATCHED THEN
- DELETE;
+ DELETE
+RETURNING MERGING(ACTION), w.*;
</programlisting>
The <literal>wine_stock_changes</literal> table might be, for example, a
@@ -627,7 +671,8 @@ WHEN MATCHED THEN
This command conforms to the <acronym>SQL</acronym> standard.
</para>
<para>
- The WITH clause and <literal>DO NOTHING</literal> action are extensions to
+ The WITH clause, <literal>DO NOTHING</literal> action, and
+ <literal>RETURNING</literal> clause are extensions to
the <acronym>SQL</acronym> standard.
</para>
</refsect1>
diff --git a/doc/src/sgml/ref/select.sgml b/doc/src/sgml/ref/select.sgml
new file mode 100644
index 42d7891..8513ca6
--- a/doc/src/sgml/ref/select.sgml
+++ b/doc/src/sgml/ref/select.sgml
@@ -74,7 +74,7 @@ SELECT [ ALL | DISTINCT [ ON ( <replacea
<phrase>and <replaceable class="parameter">with_query</replaceable> is:</phrase>
- <replaceable class="parameter">with_query_name</replaceable> [ ( <replaceable class="parameter">column_name</replaceable> [, ...] ) ] AS [ [ NOT ] MATERIALIZED ] ( <replaceable class="parameter">select</replaceable> | <replaceable class="parameter">values</replaceable> | <replaceable class="parameter">insert</replaceable> | <replaceable class="parameter">update</replaceable> | <replaceable class="parameter">delete</replaceable> )
+ <replaceable class="parameter">with_query_name</replaceable> [ ( <replaceable class="parameter">column_name</replaceable> [, ...] ) ] AS [ [ NOT ] MATERIALIZED ] ( <replaceable class="parameter">select</replaceable> | <replaceable class="parameter">values</replaceable> | <replaceable class="parameter">insert</replaceable> | <replaceable class="parameter">update</replaceable> | <replaceable class="parameter">delete</replaceable> | <replaceable class="parameter">merge</replaceable> )
[ SEARCH { BREADTH | DEPTH } FIRST BY <replaceable>column_name</replaceable> [, ...] SET <replaceable>search_seq_col_name</replaceable> ]
[ CYCLE <replaceable>column_name</replaceable> [, ...] SET <replaceable>cycle_mark_col_name</replaceable> [ TO <replaceable>cycle_mark_value</replaceable> DEFAULT <replaceable>cycle_mark_default</replaceable> ] USING <replaceable>cycle_path_col_name</replaceable> ]
@@ -230,10 +230,10 @@ TABLE [ ONLY ] <replaceable class="param
The subqueries effectively act as temporary tables or views
for the duration of the primary query.
Each subquery can be a <command>SELECT</command>, <command>TABLE</command>, <command>VALUES</command>,
- <command>INSERT</command>, <command>UPDATE</command> or
- <command>DELETE</command> statement.
+ <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command> statement.
When writing a data-modifying statement (<command>INSERT</command>,
- <command>UPDATE</command> or <command>DELETE</command>) in
+ <command>UPDATE</command>, <command>DELETE</command>, or <command>MERGE</command>) in
<literal>WITH</literal>, it is usual to include a <literal>RETURNING</literal> clause.
It is the output of <literal>RETURNING</literal>, <emphasis>not</emphasis> the underlying
table that the statement modifies, that forms the temporary table that is
@@ -2182,7 +2182,8 @@ SELECT 2+2;
<para>
<productname>PostgreSQL</productname> allows <command>INSERT</command>,
- <command>UPDATE</command>, and <command>DELETE</command> to be used as <literal>WITH</literal>
+ <command>UPDATE</command>, <command>DELETE</command>, and
+ <command>MERGE</command> to be used as <literal>WITH</literal>
queries. This is not found in the SQL standard.
</para>
</refsect2>
diff --git a/doc/src/sgml/rowtypes.sgml b/doc/src/sgml/rowtypes.sgml
new file mode 100644
index 4d86f97..bbeac84
--- a/doc/src/sgml/rowtypes.sgml
+++ b/doc/src/sgml/rowtypes.sgml
@@ -348,7 +348,7 @@ SELECT m.* FROM some_table, LATERAL myfu
column expansion of this kind when it appears at the top level of
a <link linkend="queries-select-lists"><command>SELECT</command> output
list</link>, a <link linkend="dml-returning"><literal>RETURNING</literal>
- list</link> in <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>,
+ list</link> in <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command>,
a <link linkend="queries-values"><literal>VALUES</literal> clause</link>, or
a <link linkend="sql-syntax-row-constructors">row constructor</link>.
In all other contexts (including when nested inside one of those
diff --git a/doc/src/sgml/spi.sgml b/doc/src/sgml/spi.sgml
new file mode 100644
index 47f4b5b..bb37786
--- a/doc/src/sgml/spi.sgml
+++ b/doc/src/sgml/spi.sgml
@@ -301,8 +301,9 @@ SPI_execute("INSERT INTO foo SELECT * FR
is returned in the global variable <varname>SPI_processed</varname>.
If the return value of the function is <symbol>SPI_OK_SELECT</symbol>,
<symbol>SPI_OK_INSERT_RETURNING</symbol>,
- <symbol>SPI_OK_DELETE_RETURNING</symbol>, or
- <symbol>SPI_OK_UPDATE_RETURNING</symbol>,
+ <symbol>SPI_OK_DELETE_RETURNING</symbol>,
+ <symbol>SPI_OK_UPDATE_RETURNING</symbol>, or
+ <symbol>SPI_OK_MERGE_RETURNING</symbol>,
then you can use the
global pointer <literal>SPITupleTable *SPI_tuptable</literal> to
access the result rows. Some utility commands (such as
@@ -472,6 +473,15 @@ typedef struct SPITupleTable
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><symbol>SPI_OK_MERGE_RETURNING</symbol></term>
+ <listitem>
+ <para>
+ if a <command>MERGE RETURNING</command> was executed
+ </para>
+ </listitem>
+ </varlistentry>
<varlistentry>
<term><symbol>SPI_OK_UTILITY</symbol></term>
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
new file mode 100644
index 89116ae..cb0aa55
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -177,7 +177,8 @@
statements separated by semicolons. A semicolon after the last
statement is optional. Unless the function is declared to return
<type>void</type>, the last statement must be a <command>SELECT</command>,
- or an <command>INSERT</command>, <command>UPDATE</command>, or <command>DELETE</command>
+ or an <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command>
that has a <literal>RETURNING</literal> clause.
</para>
@@ -1236,8 +1237,9 @@ SELECT x, CASE WHEN x > 0 THEN genera
<note>
<para>
- If a function's last command is <command>INSERT</command>, <command>UPDATE</command>,
- or <command>DELETE</command> with <literal>RETURNING</literal>, that command will
+ If a function's last command is <command>INSERT</command>,
+ <command>UPDATE</command>, <command>DELETE</command>, or
+ <command>MERGE</command> with <literal>RETURNING</literal>, that command will
always be executed to completion, even if the function is not declared
with <literal>SETOF</literal> or the calling query does not fetch all the
result rows. Any extra rows produced by the <literal>RETURNING</literal>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
new file mode 100644
index c5d7d78..7977873
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -283,12 +283,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 c66a047..1145aaf
--- 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 2c62b0c..6414c0d
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -1111,6 +1111,21 @@ ExecInitExprRec(Expr *node, ExprState *s
break;
}
+ case T_MergingFunc:
+ {
+ MergingFunc *mf_node = (MergingFunc *) node;
+
+ if (!state->parent ||
+ !IsA(state->parent, ModifyTableState) ||
+ ((ModifyTableState *) state->parent)->operation != CMD_MERGE)
+ elog(ERROR, "MergingFunc found in non-merge plan node");
+
+ scratch.opcode = EEOP_MERGING_FUNC;
+ scratch.d.merging_func.mfop = mf_node->mfop;
+ ExprEvalPushStep(state, &scratch);
+ break;
+ }
+
case T_SubscriptingRef:
{
SubscriptingRef *sbsref = (SubscriptingRef *) node;
diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
new file mode 100644
index 24c2b60..b176ab6
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -483,6 +483,7 @@ ExecInterpExpr(ExprState *state, ExprCon
&&CASE_EEOP_AGGREF,
&&CASE_EEOP_GROUPING_FUNC,
&&CASE_EEOP_WINDOW_FUNC,
+ &&CASE_EEOP_MERGING_FUNC,
&&CASE_EEOP_SUBPLAN,
&&CASE_EEOP_AGG_STRICT_DESERIALIZE,
&&CASE_EEOP_AGG_DESERIALIZE,
@@ -1582,6 +1583,14 @@ ExecInterpExpr(ExprState *state, ExprCon
EEO_NEXT();
}
+ EEO_CASE(EEOP_MERGING_FUNC)
+ {
+ /* too complex/uncommon for an inline implementation */
+ ExecEvalMergingFunc(state, op, econtext);
+
+ EEO_NEXT();
+ }
+
EEO_CASE(EEOP_SUBPLAN)
{
/* too complex for an inline implementation */
@@ -4171,6 +4180,59 @@ ExecEvalGroupingFunc(ExprState *state, E
}
/*
+ * ExecEvalMergingFunc
+ *
+ * Returns information about the current MERGE action for its RETURNING list.
+ */
+void
+ExecEvalMergingFunc(ExprState *state, ExprEvalStep *op, ExprContext *econtext)
+{
+ MergingFuncOp mfop = op->d.merging_func.mfop;
+ ModifyTableState *mtstate = castNode(ModifyTableState, state->parent);
+ MergeActionState *relaction = mtstate->mt_merge_action;
+
+ if (!relaction)
+ elog(ERROR, "no merge action in progress");
+
+ switch (mfop)
+ {
+ case MERGING_ACTION:
+ /* Return the MERGE action ("INSERT", "UPDATE", or "DELETE") */
+ switch (relaction->mas_action->commandType)
+ {
+ case CMD_INSERT:
+ *op->resvalue = PointerGetDatum(cstring_to_text_with_len("INSERT", 6));
+ *op->resnull = false;
+ break;
+ case CMD_UPDATE:
+ *op->resvalue = PointerGetDatum(cstring_to_text_with_len("UPDATE", 6));
+ *op->resnull = false;
+ break;
+ case CMD_DELETE:
+ *op->resvalue = PointerGetDatum(cstring_to_text_with_len("DELETE", 6));
+ *op->resnull = false;
+ break;
+ case CMD_NOTHING:
+ elog(ERROR, "unexpected merge action: DO NOTHING");
+ break;
+ default:
+ elog(ERROR, "unrecognized commandType: %d",
+ (int) relaction->mas_action->commandType);
+ }
+ break;
+
+ case MERGING_CLAUSE_NUMBER:
+ /* Return the 1-based index of the MERGE action */
+ *op->resvalue = Int32GetDatum(relaction->mas_action->index);
+ *op->resnull = false;
+ break;
+
+ default:
+ elog(ERROR, "unrecognized MergingFuncOp: %d", (int) mfop);
+ }
+}
+
+/*
* Hand off evaluation of a subplan to nodeSubplan.c
*/
void
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
new file mode 100644
index f6c3432..34d8231
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -612,8 +612,8 @@ ExecInitPartitionInfo(ModifyTableState *
* Build the RETURNING projection for the partition. Note that we didn't
* build the returningList for partitions within the planner, but simple
* translation of varattnos will suffice. This only occurs for the INSERT
- * case or in the case of UPDATE tuple routing where we didn't find a
- * result rel to reuse.
+ * case or in the case of UPDATE/MERGE tuple routing where we didn't find
+ * a result rel to reuse.
*/
if (node && node->returningLists != NIL)
{
@@ -622,12 +622,14 @@ ExecInitPartitionInfo(ModifyTableState *
List *returningList;
/* See the comment above for WCO lists. */
- /* (except no RETURNING support for MERGE yet) */
Assert((node->operation == CMD_INSERT &&
list_length(node->returningLists) == 1 &&
list_length(node->resultRelations) == 1) ||
(node->operation == CMD_UPDATE &&
list_length(node->returningLists) ==
+ list_length(node->resultRelations)) ||
+ (node->operation == CMD_MERGE &&
+ list_length(node->returningLists) ==
list_length(node->resultRelations)));
/*
diff --git a/src/backend/executor/functions.c b/src/backend/executor/functions.c
new file mode 100644
index bace252..68cb4fa
--- a/src/backend/executor/functions.c
+++ b/src/backend/executor/functions.c
@@ -1660,8 +1660,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
@@ -1679,7 +1679,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;
@@ -1693,7 +1694,7 @@ check_sql_fn_retval(List *queryTreeLists
(errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
errmsg("return type mismatch in function declared to return %s",
format_type_be(rettype)),
- errdetail("Function's final statement must be SELECT or INSERT/UPDATE/DELETE RETURNING.")));
+ errdetail("Function's final statement must be SELECT or INSERT/UPDATE/DELETE/MERGE RETURNING.")));
return false; /* keep compiler quiet */
}
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
new file mode 100644
index b16fbe9..6f9c5c9
--- 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"
@@ -88,9 +87,6 @@ typedef struct ModifyTableContext
*/
TupleTableSlot *planSlot;
- /* MERGE specific */
- MergeActionState *relaction; /* MERGE action in progress */
-
/*
* Information about the changes that were made concurrently to a tuple
* being updated or deleted
@@ -153,13 +149,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);
/*
@@ -979,7 +976,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;
@@ -1828,7 +1825,7 @@ ExecCrossPartitionUpdate(ModifyTableCont
* additional rechecking, and might end up executing a different
* action entirely).
*/
- if (context->relaction != NULL)
+ if (mtstate->mt_merge_action != NULL)
return false;
else if (TupIsNull(epqslot))
return true;
@@ -2069,7 +2066,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;
/*
@@ -2712,6 +2709,7 @@ static TupleTableSlot *
ExecMerge(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
ItemPointer tupleid, bool canSetTag)
{
+ TupleTableSlot *rslot = NULL;
bool matched;
/*-----
@@ -2759,18 +2757,18 @@ 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()
- * returned "false", indicating the previously MATCHED tuple no longer
- * matches.
+ * Deal with the NOT MATCHED case (either a NOT MATCHED tuple from the
+ * join, or a previously MATCHED tuple for which ExecMergeMatched() set
+ * "matched" to false, indicating that it no longer matches).
*/
if (!matched)
- ExecMergeNotMatched(context, resultRelInfo, canSetTag);
+ rslot = ExecMergeNotMatched(context, resultRelInfo, canSetTag);
- /* No RETURNING support yet */
- return NULL;
+ return rslot;
}
/*
@@ -2780,8 +2778,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.
*
@@ -2790,16 +2788,17 @@ 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 *newslot;
+ TupleTableSlot *rslot = NULL;
+ TupleTableSlot *newslot = NULL;
EState *estate = context->estate;
ExprContext *econtext = mtstate->ps.ps_ExprContext;
bool isNull;
@@ -2810,7 +2809,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
@@ -2889,12 +2891,15 @@ lmerge_matched:
*/
newslot = ExecProject(relaction->mas_proj);
- context->relaction = relaction;
+ mtstate->mt_merge_action = relaction;
if (!ExecUpdatePrologue(context, resultRelInfo,
tupleid, NULL, newslot, &result))
{
if (result == TM_Ok)
- return true; /* "do nothing" */
+ {
+ *matched = true;
+ return NULL; /* "do nothing" */
+ }
break; /* concurrent update/delete */
}
result = ExecUpdateAct(context, resultRelInfo, tupleid, NULL,
@@ -2905,14 +2910,15 @@ lmerge_matched:
* cross-partition update was done, then there's nothing else
* for us to do --- the UPDATE has been turned into a DELETE
* and an INSERT, and we must not perform any of the usual
- * post-update tasks.
+ * post-update tasks. Also, the RETURNING tuple (if any) has
+ * been projected, so we can just return that.
*/
if (updateCxt.crossPartUpdate)
{
mtstate->mt_merge_updated += 1;
if (canSetTag)
(estate->es_processed)++;
- return true;
+ return context->cpUpdateReturningSlot;
}
if (result == TM_Ok && updateCxt.updated)
@@ -2924,12 +2930,15 @@ lmerge_matched:
break;
case CMD_DELETE:
- context->relaction = relaction;
+ mtstate->mt_merge_action = relaction;
if (!ExecDeletePrologue(context, resultRelInfo, tupleid,
NULL, NULL, &result))
{
if (result == TM_Ok)
- return true; /* "do nothing" */
+ {
+ *matched = true;
+ return NULL; /* "do nothing" */
+ }
break; /* concurrent update/delete */
}
result = ExecDeleteAct(context, resultRelInfo, tupleid, false);
@@ -2985,7 +2994,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:
{
@@ -3031,13 +3041,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
@@ -3072,7 +3088,8 @@ lmerge_matched:
* tuple already deleted; tell caller to run NOT
* MATCHED actions
*/
- return false;
+ *matched = false;
+ return NULL;
case TM_SelfModified:
@@ -3092,13 +3109,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;
}
}
@@ -3110,6 +3128,31 @@ 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, "unrecognized commandType: %d",
+ (int) commandType);
+ }
+ }
+
/*
* We've activated one of the WHEN clauses, so we don't search
* further. This is required behaviour, not an optimization.
@@ -3120,19 +3163,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;
/*
@@ -3182,10 +3228,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:
@@ -3201,6 +3247,8 @@ ExecMergeNotMatched(ModifyTableContext *
*/
break;
}
+
+ return rslot;
}
/*
@@ -3663,8 +3711,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");
@@ -3741,8 +3798,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");
@@ -3834,7 +3900,7 @@ ExecModifyTable(PlanState *pstate)
}
slot = ExecGetUpdateNewTuple(resultRelInfo, context.planSlot,
oldSlot);
- context.relaction = NULL;
+ node->mt_merge_action = NULL;
/* Now apply the update. */
slot = ExecUpdate(&context, resultRelInfo, tupleid, oldtuple,
diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c
new file mode 100644
index 3397568..b9f443c
--- a/src/backend/executor/spi.c
+++ b/src/backend/executor/spi.c
@@ -2033,6 +2033,8 @@ SPI_result_code_string(int code)
return "SPI_OK_TD_REGISTER";
case SPI_OK_MERGE:
return "SPI_OK_MERGE";
+ case SPI_OK_MERGE_RETURNING:
+ return "SPI_OK_MERGE_RETURNING";
}
/* Unrecognized code ... return something useful ... */
sprintf(buf, "Unrecognized SPI code %d", code);
@@ -2886,7 +2888,10 @@ _SPI_pquery(QueryDesc *queryDesc, bool f
res = SPI_OK_UPDATE;
break;
case CMD_MERGE:
- res = SPI_OK_MERGE;
+ if (queryDesc->plannedstmt->hasReturning)
+ res = SPI_OK_MERGE_RETURNING;
+ else
+ res = SPI_OK_MERGE;
break;
default:
return SPI_ERROR_OPUNKNOWN;
diff --git a/src/backend/jit/llvm/llvmjit_expr.c b/src/backend/jit/llvm/llvmjit_expr.c
new file mode 100644
index a3a0876..e966037
--- a/src/backend/jit/llvm/llvmjit_expr.c
+++ b/src/backend/jit/llvm/llvmjit_expr.c
@@ -1980,6 +1980,12 @@ llvm_compile_expr(ExprState *state)
break;
}
+ case EEOP_MERGING_FUNC:
+ build_EvalXFunc(b, mod, "ExecEvalMergingFunc",
+ v_state, op, v_econtext);
+ LLVMBuildBr(b, opblocks[opno + 1]);
+ break;
+
case EEOP_SUBPLAN:
build_EvalXFunc(b, mod, "ExecEvalSubPlan",
v_state, op, v_econtext);
diff --git a/src/backend/jit/llvm/llvmjit_types.c b/src/backend/jit/llvm/llvmjit_types.c
new file mode 100644
index 791902f..65db8f2
--- a/src/backend/jit/llvm/llvmjit_types.c
+++ b/src/backend/jit/llvm/llvmjit_types.c
@@ -155,6 +155,7 @@ void *referenced_functions[] =
ExecEvalFuncExprFusage,
ExecEvalFuncExprStrictFusage,
ExecEvalGroupingFunc,
+ ExecEvalMergingFunc,
ExecEvalMinMax,
ExecEvalNextValueExpr,
ExecEvalParamExec,
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
new file mode 100644
index c03f4f2..6cf7895
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -67,6 +67,9 @@ exprType(const Node *expr)
case T_WindowFunc:
type = ((const WindowFunc *) expr)->wintype;
break;
+ case T_MergingFunc:
+ type = ((const MergingFunc *) expr)->mftype;
+ break;
case T_SubscriptingRef:
type = ((const SubscriptingRef *) expr)->refrestype;
break;
@@ -810,6 +813,9 @@ exprCollation(const Node *expr)
case T_WindowFunc:
coll = ((const WindowFunc *) expr)->wincollid;
break;
+ case T_MergingFunc:
+ coll = ((const MergingFunc *) expr)->mfcollid;
+ break;
case T_SubscriptingRef:
coll = ((const SubscriptingRef *) expr)->refcollid;
break;
@@ -1085,6 +1091,9 @@ exprSetCollation(Node *expr, Oid collati
case T_WindowFunc:
((WindowFunc *) expr)->wincollid = collation;
break;
+ case T_MergingFunc:
+ ((MergingFunc *) expr)->mfcollid = collation;
+ break;
case T_SubscriptingRef:
((SubscriptingRef *) expr)->refcollid = collation;
break;
@@ -1343,6 +1352,9 @@ exprLocation(const Node *expr)
/* function name should always be the first thing */
loc = ((const WindowFunc *) expr)->location;
break;
+ case T_MergingFunc:
+ loc = ((const MergingFunc *) expr)->location;
+ break;
case T_SubscriptingRef:
/* just use container argument's location */
loc = exprLocation((Node *) ((const SubscriptingRef *) expr)->refexpr);
@@ -2035,6 +2047,7 @@ expression_tree_walker_impl(Node *node,
case T_RangeTblRef:
case T_SortGroupClause:
case T_CTESearchClause:
+ case T_MergingFunc:
/* primitive node types with no expression subnodes */
break;
case T_WithCheckOption:
@@ -2865,6 +2878,7 @@ expression_tree_mutator_impl(Node *node,
case T_RangeTblRef:
case T_SortGroupClause:
case T_CTESearchClause:
+ case T_MergingFunc:
return (Node *) copyObject(node);
case T_WithCheckOption:
{
@@ -3827,6 +3841,7 @@ raw_expression_tree_walker_impl(Node *no
case T_ParamRef:
case T_A_Const:
case T_A_Star:
+ case T_MergingFunc:
/* primitive node types with no subnodes */
break;
case T_Alias:
diff --git a/src/backend/optimizer/plan/subselect.c b/src/backend/optimizer/plan/subselect.c
new file mode 100644
index 7a9fe88..262adf5
--- a/src/backend/optimizer/plan/subselect.c
+++ b/src/backend/optimizer/plan/subselect.c
@@ -1846,7 +1846,8 @@ convert_EXISTS_to_ANY(PlannerInfo *root,
/*
* Replace correlation vars (uplevel vars) with Params.
*
- * Uplevel PlaceHolderVars and aggregates are replaced, too.
+ * Uplevel PlaceHolderVars, aggregates, GROUPING() expressions, and MERGING()
+ * expressions are replaced, too.
*
* Note: it is critical that this runs immediately after SS_process_sublinks.
* Since we do not recurse into the arguments of uplevel PHVs and aggregates,
@@ -1900,6 +1901,11 @@ replace_correlation_vars_mutator(Node *n
if (((GroupingFunc *) node)->agglevelsup > 0)
return (Node *) replace_outer_grouping(root, (GroupingFunc *) node);
}
+ if (IsA(node, MergingFunc))
+ {
+ if (root->parse->commandType != CMD_MERGE)
+ return (Node *) replace_outer_merging(root, (MergingFunc *) node);
+ }
return expression_tree_mutator(node,
replace_correlation_vars_mutator,
(void *) root);
diff --git a/src/backend/optimizer/util/paramassign.c b/src/backend/optimizer/util/paramassign.c
new file mode 100644
index d6a923b..b3b4da6
--- a/src/backend/optimizer/util/paramassign.c
+++ b/src/backend/optimizer/util/paramassign.c
@@ -307,6 +307,56 @@ replace_outer_grouping(PlannerInfo *root
}
/*
+ * Generate a Param node to replace the given MergingFunc expression which is
+ * expected to be in the RETURNING list of an upper-level MERGE query. Record
+ * the need for the MergingFunc in the proper upper-level root->plan_params.
+ */
+Param *
+replace_outer_merging(PlannerInfo *root, MergingFunc *mf)
+{
+ Param *retval;
+ PlannerParamItem *pitem;
+ Oid ptype = exprType((Node *) mf);
+
+ Assert(root->parse->commandType != CMD_MERGE);
+
+ /*
+ * The parser should have ensured that the MergingFunc is in the RETURNING
+ * list of an upper-level MERGE query, so find that query.
+ */
+ do
+ {
+ root = root->parent_root;
+ if (root == NULL)
+ elog(ERROR, "MergingFunc found outside MERGE");
+ } while (root->parse->commandType != CMD_MERGE);
+
+ /*
+ * It does not seem worthwhile to try to de-duplicate references to outer
+ * MergingFunc expressions. Just make a new slot every time.
+ */
+ mf = copyObject(mf);
+
+ pitem = makeNode(PlannerParamItem);
+ pitem->item = (Node *) mf;
+ pitem->paramId = list_length(root->glob->paramExecTypes);
+ root->glob->paramExecTypes = lappend_oid(root->glob->paramExecTypes,
+ ptype);
+
+ root->plan_params = lappend(root->plan_params, pitem);
+
+ retval = makeNode(Param);
+ retval->paramkind = PARAM_EXEC;
+ retval->paramid = pitem->paramId;
+ retval->paramtype = ptype;
+ retval->paramtypmod = -1;
+ retval->paramcollid = InvalidOid;
+ retval->location = mf->location;
+
+ return retval;
+}
+
+/*
* Generate a Param node to replace the given Var,
* which is expected to come from some upper NestLoop plan node.
* Record the need for the Var in root->curOuterParams.
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
new file mode 100644
index 7a1dfb6..5f882b4
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -74,7 +74,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,
@@ -553,7 +552,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;
@@ -980,7 +980,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;
@@ -2445,7 +2446,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
@@ -2539,10 +2541,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;
@@ -2559,7 +2562,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 c224df4..c366c5b
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -693,7 +693,7 @@ static Node *makeRecursiveViewSelect(cha
BOOLEAN_P BOTH BREADTH BY
CACHE CALL CALLED CASCADE CASCADED CASE CAST CATALOG_P CHAIN CHAR_P
- CHARACTER CHARACTERISTICS CHECK CHECKPOINT CLASS CLOSE
+ CHARACTER CHARACTERISTICS CHECK CHECKPOINT CLASS CLAUSE_NUMBER CLOSE
CLUSTER COALESCE COLLATE COLLATION COLUMN COLUMNS COMMENT COMMENTS COMMIT
COMMITTED COMPRESSION CONCURRENTLY CONFIGURATION CONFLICT
CONNECTION CONSTRAINT CONSTRAINTS CONTENT_P CONTINUE_P CONVERSION_P COPY
@@ -731,7 +731,7 @@ static Node *makeRecursiveViewSelect(cha
LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL
LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOGGED
- MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE METHOD
+ MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE MERGING METHOD
MINUTE_P MINVALUE MODE MONTH_P MOVE
NAME_P NAMES NATIONAL NATURAL NCHAR NEW NEXT NFC NFD NFKC NFKD NO NONE
@@ -12332,6 +12332,7 @@ MergeStmt:
USING table_ref
ON a_expr
merge_when_list
+ returning_clause
{
MergeStmt *m = makeNode(MergeStmt);
@@ -12340,6 +12341,7 @@ MergeStmt:
m->sourceRelation = $6;
m->joinCondition = $8;
m->mergeWhenClauses = $9;
+ m->returningList = $10;
$$ = (Node *) m;
}
@@ -15755,6 +15757,24 @@ func_expr_common_subexpr:
n->location = @1;
$$ = (Node *) n;
}
+ | MERGING '(' ACTION ')'
+ {
+ MergingFunc *m = makeNode(MergingFunc);
+
+ m->mfop = MERGING_ACTION;
+ m->mftype = TEXTOID;
+ m->location = @1;
+ $$ = (Node *) m;
+ }
+ | MERGING '(' CLAUSE_NUMBER ')'
+ {
+ MergingFunc *m = makeNode(MergingFunc);
+
+ m->mfop = MERGING_CLAUSE_NUMBER;
+ m->mftype = INT4OID;
+ m->location = @1;
+ $$ = (Node *) m;
+ }
;
@@ -17100,6 +17120,7 @@ unreserved_keyword:
| CHARACTERISTICS
| CHECKPOINT
| CLASS
+ | CLAUSE_NUMBER
| CLOSE
| CLUSTER
| COLUMNS
@@ -17419,6 +17440,7 @@ col_name_keyword:
| JSON_SCALAR
| JSON_SERIALIZE
| LEAST
+ | MERGING
| NATIONAL
| NCHAR
| NONE
@@ -17637,6 +17659,7 @@ bare_label_keyword:
| CHECK
| CHECKPOINT
| CLASS
+ | CLAUSE_NUMBER
| CLOSE
| CLUSTER
| COALESCE
@@ -17808,6 +17831,7 @@ bare_label_keyword:
| MATERIALIZED
| MAXVALUE
| MERGE
+ | MERGING
| METHOD
| MINVALUE
| MODE
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
new file mode 100644
index 9bbad33..f57098f
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -467,6 +467,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:
@@ -914,6 +915,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 6992a78..5cad6d8
--- 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 64c582c..ad0717e
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -54,6 +54,7 @@ static Node *transformAExprDistinct(Pars
static Node *transformAExprNullIf(ParseState *pstate, A_Expr *a);
static Node *transformAExprIn(ParseState *pstate, A_Expr *a);
static Node *transformAExprBetween(ParseState *pstate, A_Expr *a);
+static Node *transformMergingFunc(ParseState *pstate, MergingFunc *f);
static Node *transformBoolExpr(ParseState *pstate, BoolExpr *a);
static Node *transformFuncCall(ParseState *pstate, FuncCall *fn);
static Node *transformMultiAssignRef(ParseState *pstate, MultiAssignRef *maref);
@@ -227,6 +228,10 @@ transformExprRecurse(ParseState *pstate,
result = transformGroupingFunc(pstate, (GroupingFunc *) expr);
break;
+ case T_MergingFunc:
+ result = transformMergingFunc(pstate, (MergingFunc *) expr);
+ break;
+
case T_NamedArgExpr:
{
NamedArgExpr *na = (NamedArgExpr *) expr;
@@ -541,6 +546,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:
@@ -1354,6 +1360,31 @@ transformAExprBetween(ParseState *pstate
}
static Node *
+transformMergingFunc(ParseState *pstate, MergingFunc *f)
+{
+ /*
+ * Check that we're in the RETURNING list of a MERGE command.
+ */
+ if (pstate->p_expr_kind != EXPR_KIND_MERGE_RETURNING)
+ {
+ ParseState *parent_pstate = pstate->parentParseState;
+
+ while (parent_pstate &&
+ parent_pstate->p_expr_kind != EXPR_KIND_MERGE_RETURNING)
+ parent_pstate = parent_pstate->parentParseState;
+
+ if (!parent_pstate ||
+ parent_pstate->p_expr_kind != EXPR_KIND_MERGE_RETURNING)
+ ereport(ERROR,
+ errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("MERGING() can only be used in the RETURNING list of a MERGE command"),
+ parser_errposition(pstate, f->location));
+ }
+
+ return (Node *) f;
+}
+
+static Node *
transformBoolExpr(ParseState *pstate, BoolExpr *a)
{
List *args = NIL;
@@ -1767,6 +1798,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:
@@ -3115,6 +3147,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 6c29471..1a0f983
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -2599,6 +2599,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 91b1156..33852e9
--- 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.
@@ -254,6 +258,7 @@ transformMergeStmt(ParseState *pstate, M
MergeAction *action;
action = makeNode(MergeAction);
+ action->index = foreach_current_index(l) + 1;
action->commandType = mergeWhenClause->commandType;
action->matched = mergeWhenClause->matched;
@@ -390,9 +395,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 864ea9b..cc19041
--- a/src/backend/parser/parse_relation.c
+++ b/src/backend/parser/parse_relation.c
@@ -2346,9 +2346,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/parser/parse_target.c b/src/backend/parser/parse_target.c
new file mode 100644
index 3bc62ac..eaa2a2b
--- a/src/backend/parser/parse_target.c
+++ b/src/backend/parser/parse_target.c
@@ -1807,6 +1807,20 @@ FigureColnameInternal(Node *node, char *
/* make GROUPING() act like a regular function */
*name = "grouping";
return 2;
+ case T_MergingFunc:
+ switch (((MergingFunc *) node)->mfop)
+ {
+ case MERGING_ACTION:
+ *name = "action";
+ return 2;
+ case MERGING_CLAUSE_NUMBER:
+ *name = "clause_number";
+ return 2;
+ default:
+ elog(ERROR, "unrecognized MergingFuncOp: %d",
+ (int) ((MergingFunc *) node)->mfop);
+ }
+ break;
case T_SubLink:
switch (((SubLink *) node)->subLinkType)
{
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
new file mode 100644
index 41a3623..3786a7c
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -3615,9 +3615,9 @@ RewriteQuery(Query *parsetree, List *rew
ListCell *lc1;
/*
- * First, recursively process any insert/update/delete statements in WITH
- * clauses. (We have to do this first because the WITH clauses may get
- * copied into rule actions below.)
+ * First, recursively process any insert/update/delete/merge statements in
+ * WITH clauses. (We have to do this first because the WITH clauses may
+ * get copied into rule actions below.)
*/
foreach(lc1, parsetree->cteList)
{
@@ -3642,7 +3642,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/rewrite/rowsecurity.c b/src/backend/rewrite/rowsecurity.c
new file mode 100644
index b1620e4..966dcf0
--- a/src/backend/rewrite/rowsecurity.c
+++ b/src/backend/rewrite/rowsecurity.c
@@ -395,10 +395,10 @@ get_row_security_policies(Query *root, R
* on the final action we take.
*
* We already fetched the SELECT policies above, to check existing rows,
- * but we must also check that new rows created by UPDATE actions are
- * visible, if SELECT rights are required for this relation. We don't do
- * this for INSERT actions, since an INSERT command would only do this
- * check if it had a RETURNING list, and MERGE does not support RETURNING.
+ * but we must also check that new rows created by INSERT/UPDATE actions
+ * are visible, if SELECT rights are required. For INSERT actions, we only
+ * do this if RETURNING is specified, to be consistent with a plain INSERT
+ * command, which can only require SELECT rights when RETURNING is used.
*
* 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
@@ -420,6 +420,8 @@ get_row_security_policies(Query *root, R
List *merge_delete_restrictive_policies;
List *merge_insert_permissive_policies;
List *merge_insert_restrictive_policies;
+ List *merge_select_permissive_policies = NIL;
+ List *merge_select_restrictive_policies = NIL;
/*
* Fetch the UPDATE policies and set them up to execute on the
@@ -457,9 +459,6 @@ get_row_security_policies(Query *root, R
*/
if (perminfo->requiredPerms & ACL_SELECT)
{
- List *merge_select_permissive_policies;
- List *merge_select_restrictive_policies;
-
get_policies_for_relation(rel, CMD_SELECT, user_id,
&merge_select_permissive_policies,
&merge_select_restrictive_policies);
@@ -508,6 +507,21 @@ get_row_security_policies(Query *root, R
withCheckOptions,
hasSubLinks,
false);
+
+ /*
+ * Add ALL/SELECT policies as WCO_RLS_INSERT_CHECK WCOs, to ensure
+ * that the inserted row is visible when executing an INSERT action,
+ * if RETURNING is specified and SELECT rights are required for this
+ * relation.
+ */
+ if (perminfo->requiredPerms & ACL_SELECT && root->returningList)
+ add_with_check_options(rel, rt_index,
+ WCO_RLS_INSERT_CHECK,
+ merge_select_permissive_policies,
+ merge_select_restrictive_policies,
+ withCheckOptions,
+ hasSubLinks,
+ true);
}
table_close(rel, NoLock);
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
new file mode 100644
index e3ccf6c..6f3dc89
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -2142,11 +2142,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/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
new file mode 100644
index ed7f40f..aca2217
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -7197,8 +7197,13 @@ get_merge_query_def(Query *query, depars
appendStringInfoString(buf, "DO NOTHING");
}
- /* No RETURNING support in MERGE yet */
- Assert(query->returningList == NIL);
+ /* Add RETURNING if present */
+ if (query->returningList)
+ {
+ appendContextKeyword(context, " RETURNING",
+ -PRETTYINDENT_STD, PRETTYINDENT_STD, 1);
+ get_target_list(query->returningList, context, NULL, colNamesVisible);
+ }
}
@@ -8298,6 +8303,7 @@ isSimpleNode(Node *node, Node *parentNod
case T_Aggref:
case T_GroupingFunc:
case T_WindowFunc:
+ case T_MergingFunc:
case T_FuncExpr:
case T_JsonConstructorExpr:
/* function-like: name(..) or name[..] */
@@ -8652,6 +8658,27 @@ get_rule_expr(Node *node, deparse_contex
get_windowfunc_expr((WindowFunc *) node, context);
break;
+ case T_MergingFunc:
+ {
+ MergingFunc *mexpr = (MergingFunc *) node;
+
+ appendStringInfoString(buf, "MERGING(");
+ switch (mexpr->mfop)
+ {
+ case MERGING_ACTION:
+ appendStringInfoString(buf, "ACTION");
+ break;
+ case MERGING_CLAUSE_NUMBER:
+ appendStringInfoString(buf, "CLAUSE_NUMBER");
+ break;
+ default:
+ elog(ERROR, "unrecognized MergingFuncOp: %d",
+ (int) mexpr->mfop);
+ }
+ appendStringInfoChar(buf, ')');
+ }
+ break;
+
case T_SubscriptingRef:
{
SubscriptingRef *sbsref = (SubscriptingRef *) node;
diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c
new file mode 100644
index daabf6f..6b63214
--- a/src/bin/psql/common.c
+++ b/src/bin/psql/common.c
@@ -983,13 +983,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/executor/execExpr.h b/src/include/executor/execExpr.h
new file mode 100644
index 048573c..7580ab9
--- a/src/include/executor/execExpr.h
+++ b/src/include/executor/execExpr.h
@@ -241,6 +241,7 @@ typedef enum ExprEvalOp
EEOP_AGGREF,
EEOP_GROUPING_FUNC,
EEOP_WINDOW_FUNC,
+ EEOP_MERGING_FUNC,
EEOP_SUBPLAN,
/* aggregation related nodes */
@@ -623,6 +624,12 @@ typedef struct ExprEvalStep
WindowFuncExprState *wfstate;
} window_func;
+ /* for EEOP_MERGING_FUNC */
+ struct
+ {
+ MergingFuncOp mfop; /* the MERGING() operation to perform */
+ } merging_func;
+
/* for EEOP_SUBPLAN */
struct
{
@@ -806,6 +813,8 @@ extern void ExecEvalJsonConstructor(Expr
ExprContext *econtext);
extern void ExecEvalJsonIsPredicate(ExprState *state, ExprEvalStep *op);
extern void ExecEvalGroupingFunc(ExprState *state, ExprEvalStep *op);
+extern void ExecEvalMergingFunc(ExprState *state, ExprEvalStep *op,
+ ExprContext *econtext);
extern void ExecEvalSubPlan(ExprState *state, ExprEvalStep *op,
ExprContext *econtext);
extern void ExecEvalWholeRowVar(ExprState *state, ExprEvalStep *op,
diff --git a/src/include/executor/spi.h b/src/include/executor/spi.h
new file mode 100644
index d1de139..ed8787d
--- a/src/include/executor/spi.h
+++ b/src/include/executor/spi.h
@@ -97,6 +97,7 @@ typedef struct _SPI_plan *SPIPlanPtr;
#define SPI_OK_REL_UNREGISTER 16
#define SPI_OK_TD_REGISTER 17
#define SPI_OK_MERGE 18
+#define SPI_OK_MERGE_RETURNING 19
#define SPI_OPT_NONATOMIC (1 << 0)
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
new file mode 100644
index 5d7f17d..08bc9ee
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1318,6 +1318,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 e494309..67ec626
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -1927,6 +1927,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/nodes/primnodes.h b/src/include/nodes/primnodes.h
new file mode 100644
index bb930af..00e0992
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -572,6 +572,39 @@ typedef struct WindowFunc
} WindowFunc;
/*
+ * MergingFunc
+ *
+ * A MergingFunc is a MERGING(...) expression that can only appear in the
+ * RETURNING list of a MERGE command. It returns information about the
+ * currently executing merge action. Possible operations are:
+ *
+ * MERGING_ACTION:
+ * Return the command string of the current merge action ("INSERT",
+ * "UPDATE" or "DELETE").
+ *
+ * MERGING_CLAUSE_NUMBER:
+ * Return the 1-based index of the current merge WHEN clause.
+ */
+typedef enum MergingFuncOp
+{
+ MERGING_ACTION,
+ MERGING_CLAUSE_NUMBER,
+} MergingFuncOp;
+
+typedef struct MergingFunc
+{
+ Expr xpr;
+ /* operation to perform */
+ MergingFuncOp mfop;
+ /* type Oid of result */
+ Oid mftype pg_node_attr(query_jumble_ignore);
+ /* OID of collation, or InvalidOid if none */
+ Oid mfcollid pg_node_attr(query_jumble_ignore);
+ /* token location, or -1 if unknown */
+ int location;
+} MergingFunc;
+
+/*
* SubscriptingRef: describes a subscripting operation over a container
* (array, etc).
*
@@ -1735,6 +1768,7 @@ typedef struct BooleanTest
typedef struct MergeAction
{
NodeTag type;
+ int index; /* 1-based index of the clause */
bool matched; /* true=MATCHED, false=NOT MATCHED */
CmdType commandType; /* INSERT/UPDATE/DELETE/DO NOTHING */
/* OVERRIDING clause */
diff --git a/src/include/optimizer/paramassign.h b/src/include/optimizer/paramassign.h
new file mode 100644
index 55c27a6..cd0e629
--- a/src/include/optimizer/paramassign.h
+++ b/src/include/optimizer/paramassign.h
@@ -20,6 +20,7 @@ extern Param *replace_outer_placeholderv
PlaceHolderVar *phv);
extern Param *replace_outer_agg(PlannerInfo *root, Aggref *agg);
extern Param *replace_outer_grouping(PlannerInfo *root, GroupingFunc *grp);
+extern Param *replace_outer_merging(PlannerInfo *root, MergingFunc *mf);
extern Param *replace_nestloop_param_var(PlannerInfo *root, Var *var);
extern Param *replace_nestloop_param_placeholdervar(PlannerInfo *root,
PlaceHolderVar *phv);
diff --git a/src/include/parser/analyze.h b/src/include/parser/analyze.h
new file mode 100644
index c96483a..47d988b
--- 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/kwlist.h b/src/include/parser/kwlist.h
new file mode 100644
index 5984dcf..c283a3f
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -80,6 +80,7 @@ PG_KEYWORD("characteristics", CHARACTERI
PG_KEYWORD("check", CHECK, RESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("checkpoint", CHECKPOINT, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("class", CLASS, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("clause_number", CLAUSE_NUMBER, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("close", CLOSE, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("cluster", CLUSTER, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("coalesce", COALESCE, COL_NAME_KEYWORD, BARE_LABEL)
@@ -266,6 +267,7 @@ PG_KEYWORD("matched", MATCHED, UNRESERVE
PG_KEYWORD("materialized", MATERIALIZED, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("maxvalue", MAXVALUE, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("merge", MERGE, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("merging", MERGING, COL_NAME_KEYWORD, BARE_LABEL)
PG_KEYWORD("method", METHOD, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("minute", MINUTE_P, UNRESERVED_KEYWORD, AS_LABEL)
PG_KEYWORD("minvalue", MINVALUE, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
new file mode 100644
index f589112..836819d
--- 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/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
new file mode 100644
index f8c7f48..e5d92f4
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -4267,9 +4267,9 @@ exec_stmt_execsql(PLpgSQL_execstate *est
/*
* If we have INTO, then we only need one row back ... but if we have INTO
* STRICT or extra check too_many_rows, ask for two rows, so that we can
- * verify the statement returns only one. INSERT/UPDATE/DELETE are always
- * treated strictly. Without INTO, just run the statement to completion
- * (tcount = 0).
+ * verify the statement returns only one. INSERT/UPDATE/DELETE/MERGE are
+ * always treated strictly. Without INTO, just run the statement to
+ * completion (tcount = 0).
*
* We could just ask for two rows always when using INTO, but there are
* some cases where demanding the extra row costs significant time, eg by
@@ -4307,10 +4307,11 @@ exec_stmt_execsql(PLpgSQL_execstate *est
case SPI_OK_INSERT:
case SPI_OK_UPDATE:
case SPI_OK_DELETE:
+ case SPI_OK_MERGE:
case SPI_OK_INSERT_RETURNING:
case SPI_OK_UPDATE_RETURNING:
case SPI_OK_DELETE_RETURNING:
- case SPI_OK_MERGE:
+ case SPI_OK_MERGE_RETURNING:
Assert(stmt->mod_stmt);
exec_set_found(estate, (SPI_processed != 0));
break;
@@ -4489,10 +4490,11 @@ exec_stmt_dynexecute(PLpgSQL_execstate *
case SPI_OK_INSERT:
case SPI_OK_UPDATE:
case SPI_OK_DELETE:
+ case SPI_OK_MERGE:
case SPI_OK_INSERT_RETURNING:
case SPI_OK_UPDATE_RETURNING:
case SPI_OK_DELETE_RETURNING:
- case SPI_OK_MERGE:
+ case SPI_OK_MERGE_RETURNING:
case SPI_OK_UTILITY:
case SPI_OK_REWRITTEN:
break;
diff --git a/src/pl/tcl/pltcl.c b/src/pl/tcl/pltcl.c
new file mode 100644
index e8f9d7b..0c9f81e
--- a/src/pl/tcl/pltcl.c
+++ b/src/pl/tcl/pltcl.c
@@ -2456,6 +2456,7 @@ pltcl_process_SPI_result(Tcl_Interp *int
case SPI_OK_INSERT_RETURNING:
case SPI_OK_DELETE_RETURNING:
case SPI_OK_UPDATE_RETURNING:
+ case SPI_OK_MERGE_RETURNING:
/*
* Process the tuples we got
diff --git a/src/test/regress/expected/merge.out b/src/test/regress/expected/merge.out
new file mode 100644
index 28a6d0b..183d160
--- 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;
@@ -1256,7 +1256,7 @@ BEGIN;
MERGE INTO sq_target
USING v
ON tid = sid
-WHEN MATCHED AND tid > 2 THEN
+WHEN MATCHED AND tid >= 2 THEN
UPDATE SET balance = balance + delta
WHEN NOT MATCHED THEN
INSERT (balance, tid) VALUES (balance + delta, sid)
@@ -1271,7 +1271,7 @@ INSERT INTO sq_source (sid, balance, del
MERGE INTO sq_target t
USING v
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)
@@ -1295,7 +1295,7 @@ WITH targq AS (
MERGE INTO sq_target t
USING v
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)
@@ -1304,20 +1304,191 @@ WHEN MATCHED AND tid < 2 THEN
ROLLBACK;
-- RETURNING
BEGIN;
-INSERT INTO sq_source (sid, balance, delta) VALUES (-1, -1, -10);
+CREATE TABLE merge_actions(action text, abbrev text);
+INSERT INTO merge_actions VALUES ('INSERT', 'ins'), ('UPDATE', 'upd'), ('DELETE', 'del');
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)
+ 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 *;
- ^
+ DELETE
+RETURNING MERGING(CLAUSE_NUMBER) AS clause,
+ (SELECT abbrev FROM merge_actions
+ WHERE action = MERGING(ACTION)) AS action,
+ t.*,
+ CASE MERGING(ACTION)
+ WHEN 'INSERT' THEN 'Inserted '||t
+ WHEN 'UPDATE' THEN 'Added '||delta||' to balance'
+ WHEN 'DELETE' THEN 'Removed '||t
+ END AS description;
+ clause | action | tid | balance | description
+--------+--------+-----+---------+---------------------
+ 3 | del | 1 | 100 | Removed (1,100)
+ 1 | upd | 2 | 220 | Added 20 to balance
+ 2 | ins | 4 | 40 | Inserted (4,40)
+(3 rows)
+
+ROLLBACK;
+-- error when using MERGING() outside MERGE
+SELECT MERGING(ACTION) FROM sq_target;
+ERROR: MERGING() can only be used in the RETURNING list of a MERGE command
+LINE 1: SELECT MERGING(ACTION) FROM sq_target;
+ ^
+UPDATE sq_target SET balance = balance + 1 RETURNING MERGING(CLAUSE_NUMBER);
+ERROR: MERGING() can only be used in the RETURNING list of a MERGE command
+LINE 1: ...ATE sq_target SET balance = balance + 1 RETURNING MERGING(CL...
+ ^
+-- RETURNING in CTEs
+CREATE TABLE sq_target_merge_log (tid integer NOT NULL, last_change text);
+INSERT INTO sq_target_merge_log VALUES (1, 'Original value');
+BEGIN;
+WITH m AS (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 MERGING(CLAUSE_NUMBER), MERGING(ACTION), t.*,
+ CASE MERGING(ACTION)
+ WHEN 'INSERT' THEN 'Inserted '||t
+ WHEN 'UPDATE' THEN 'Added '||delta||' to balance'
+ WHEN 'DELETE' THEN 'Removed '||t
+ END AS description
+), m2 AS (
+ MERGE INTO sq_target_merge_log l
+ USING m
+ ON l.tid = m.tid
+ WHEN MATCHED THEN
+ UPDATE SET last_change = description
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (m.tid, description)
+ RETURNING action, MERGING(ACTION) AS log_action, l.*
+)
+SELECT * FROM m2;
+ action | log_action | tid | last_change
+--------+------------+-----+---------------------
+ DELETE | UPDATE | 1 | Removed (1,100)
+ UPDATE | INSERT | 2 | Added 20 to balance
+ INSERT | INSERT | 4 | Inserted (4,40)
+(3 rows)
+
+SELECT * FROM sq_target_merge_log ORDER BY tid;
+ tid | last_change
+-----+---------------------
+ 1 | Removed (1,100)
+ 2 | Added 20 to balance
+ 4 | Inserted (4,40)
+(3 rows)
+
+ROLLBACK;
+-- COPY (MERGE ... RETURNING) TO ...
+BEGIN;
+COPY (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 MERGING(ACTION), t.*
+) TO stdout;
+DELETE 1 100
+UPDATE 2 220
+INSERT 4 40
+ROLLBACK;
+-- SQL function with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT action text, OUT tid int, OUT new_balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING MERGING(ACTION), t.*;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT action, tid, new_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ action | tid | new_balance
+--------+-----+-------------
+ DELETE | 1 | 100
+ UPDATE | 3 | 320
+ INSERT | 4 | 110
+(3 rows)
+
+ROLLBACK;
+-- SQL SRF with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_sq_source_into_sq_target()
+RETURNS TABLE (action text, tid int, balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 MERGING(ACTION), t.*;
+$$;
+SELECT * FROM merge_sq_source_into_sq_target();
+ action | tid | balance
+--------+-----+---------
+ DELETE | 1 | 100
+ UPDATE | 2 | 220
+ INSERT | 4 | 40
+(3 rows)
+
+ROLLBACK;
+-- PL/pgSQL function with MERGE ... RETURNING ... INTO
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT r_action text, OUT r_tid int, OUT r_balance int)
+LANGUAGE plpgsql AS
+$$
+BEGIN
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING MERGING(ACTION), t.* INTO r_action, r_tid, r_balance;
+END;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT r_action, r_tid, r_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ r_action | r_tid | r_balance
+----------+-------+-----------
+ DELETE | 1 | 100
+ UPDATE | 3 | 320
+ INSERT | 4 | 110
+(3 rows)
+
ROLLBACK;
-- EXPLAIN
CREATE TABLE ex_mtarget (a int, b int)
@@ -1514,7 +1685,7 @@ SELECT * FROM sq_target WHERE tid = 1;
(1 row)
ROLLBACK;
-DROP TABLE sq_target, sq_source CASCADE;
+DROP TABLE sq_target, sq_target_merge_log, sq_source CASCADE;
NOTICE: drop cascades to view v
CREATE TABLE pa_target (tid integer, balance float, val text)
PARTITION BY LIST (tid);
@@ -1640,6 +1811,32 @@ SELECT * FROM pa_target ORDER BY tid;
(14 rows)
ROLLBACK;
+-- update partition key to partition not initially scanned
+BEGIN;
+MERGE INTO pa_target t
+ USING pa_source s
+ ON t.tid = s.sid AND t.tid = 1
+ WHEN MATCHED THEN
+ UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
+ RETURNING MERGING(ACTION), MERGING(CLAUSE_NUMBER), t.*;
+ action | clause_number | tid | balance | val
+--------+---------------+-----+---------+--------------------------
+ UPDATE | 1 | 2 | 110 | initial updated by merge
+(1 row)
+
+SELECT * FROM pa_target ORDER BY tid;
+ tid | balance | val
+-----+---------+--------------------------
+ 2 | 110 | initial updated by merge
+ 3 | 300 | initial
+ 5 | 500 | initial
+ 7 | 700 | initial
+ 9 | 900 | initial
+ 11 | 1100 | initial
+ 13 | 1300 | initial
+(7 rows)
+
+ROLLBACK;
DROP TABLE pa_target CASCADE;
-- The target table is partitioned in the same way, but this time by attaching
-- partitions which have columns in different order, dropped columns etc.
@@ -1731,7 +1928,26 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (sid, delta, 'inserted by merge');
+ INSERT VALUES (sid, delta, 'inserted by merge')
+ RETURNING MERGING(ACTION), MERGING(CLAUSE_NUMBER), t.*;
+ action | clause_number | tid | balance | val
+--------+---------------+-----+---------+--------------------------
+ UPDATE | 1 | 2 | 110 | initial updated by merge
+ INSERT | 2 | 2 | 20 | inserted by merge
+ UPDATE | 1 | 4 | 330 | initial updated by merge
+ INSERT | 2 | 4 | 40 | inserted by merge
+ UPDATE | 1 | 6 | 550 | initial updated by merge
+ INSERT | 2 | 6 | 60 | inserted by merge
+ UPDATE | 1 | 8 | 770 | initial updated by merge
+ INSERT | 2 | 8 | 80 | inserted by merge
+ UPDATE | 1 | 10 | 990 | initial updated by merge
+ INSERT | 2 | 10 | 100 | inserted by merge
+ UPDATE | 1 | 12 | 1210 | initial updated by merge
+ INSERT | 2 | 12 | 120 | inserted by merge
+ UPDATE | 1 | 14 | 1430 | initial updated by merge
+ INSERT | 2 | 14 | 140 | inserted by merge
+(14 rows)
+
SELECT * FROM pa_target ORDER BY tid;
tid | balance | val
-----+---------+--------------------------
@@ -1798,7 +2014,21 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge');
+ INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge')
+ RETURNING MERGING(ACTION), MERGING(CLAUSE_NUMBER), t.*;
+ action | clause_number | logts | tid | balance | val
+--------+---------------+--------------------------+-----+---------+--------------------------
+ UPDATE | 1 | Tue Jan 31 00:00:00 2017 | 1 | 110 | initial updated by merge
+ UPDATE | 1 | Tue Feb 28 00:00:00 2017 | 2 | 220 | initial updated by merge
+ INSERT | 2 | Sun Jan 15 00:00:00 2017 | 3 | 30 | inserted by merge
+ UPDATE | 1 | Tue Jan 31 00:00:00 2017 | 4 | 440 | initial updated by merge
+ UPDATE | 1 | Tue Feb 28 00:00:00 2017 | 5 | 550 | initial updated by merge
+ INSERT | 2 | Sun Jan 15 00:00:00 2017 | 6 | 60 | inserted by merge
+ UPDATE | 1 | Tue Jan 31 00:00:00 2017 | 7 | 770 | initial updated by merge
+ UPDATE | 1 | Tue Feb 28 00:00:00 2017 | 8 | 880 | initial updated by merge
+ INSERT | 2 | Sun Jan 15 00:00:00 2017 | 9 | 90 | inserted by merge
+(9 rows)
+
SELECT * FROM pa_target ORDER BY tid;
logts | tid | balance | val
--------------------------+-----+---------+--------------------------
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
new file mode 100644
index 6988128..3fd4fab
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -2302,6 +2302,35 @@ WHEN MATCHED THEN
UPDATE SET dnotes = dnotes || ' notes added by merge8 '
WHEN NOT MATCHED THEN
INSERT VALUES (13, 44, 1, 'regress_rls_bob', 'new manga');
+SELECT * FROM document WHERE did = 13;
+ did | cid | dlevel | dauthor | dtitle | dnotes
+-----+-----+--------+---------+--------+--------
+(0 rows)
+
+-- but not OK if RETURNING is used
+MERGE INTO document d
+USING (SELECT 14 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge9 '
+WHEN NOT MATCHED THEN
+ INSERT VALUES (14, 44, 1, 'regress_rls_bob', 'new manga')
+RETURNING *;
+ERROR: new row violates row-level security policy for table "document"
+-- but OK if new row is visible
+MERGE INTO document d
+USING (SELECT 14 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge10 '
+WHEN NOT MATCHED THEN
+ INSERT VALUES (14, 11, 1, 'regress_rls_bob', 'new novel')
+RETURNING *;
+ sdid | did | cid | dlevel | dauthor | dtitle | dnotes
+------+-----+-----+--------+-----------------+-----------+--------
+ 14 | 14 | 11 | 1 | regress_rls_bob | new novel |
+(1 row)
+
RESET SESSION AUTHORIZATION;
-- drop the restrictive SELECT policy so that we can look at the
-- final state of the table
@@ -2325,7 +2354,8 @@ SELECT * FROM document;
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 notes added by merge7
13 | 44 | 1 | regress_rls_bob | new manga |
-(15 rows)
+ 14 | 11 | 1 | regress_rls_bob | new novel |
+(16 rows)
--
-- ROLE/GROUP
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
new file mode 100644
index 1442c43..f187dec
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -3596,7 +3596,8 @@ MERGE INTO rule_merge2 t USING (SELECT 1
-- test deparsing
CREATE TABLE sf_target(id int, data text, filling int[]);
CREATE FUNCTION merge_sf_test()
- RETURNS void
+ RETURNS TABLE(action text, clause_number int,
+ a int, b text, id int, data text, filling int[])
LANGUAGE sql
BEGIN ATOMIC
MERGE INTO sf_target t
@@ -3633,11 +3634,13 @@ WHEN NOT MATCHED
VALUES (s.a, s.b, DEFAULT)
WHEN NOT MATCHED
THEN INSERT (filling[1], id)
- VALUES (s.a, s.a);
+ VALUES (s.a, s.a)
+RETURNING
+ merging(action), merging(clause_number) AS clause_number, *;
END;
\sf merge_sf_test
CREATE OR REPLACE FUNCTION public.merge_sf_test()
- RETURNS void
+ RETURNS TABLE(action text, clause_number integer, a integer, b text, id integer, data text, filling integer[])
LANGUAGE sql
BEGIN ATOMIC
MERGE INTO sf_target t
@@ -3674,7 +3677,14 @@ BEGIN ATOMIC
VALUES (s.a, s.b, DEFAULT)
WHEN NOT MATCHED
THEN INSERT (filling[1], id)
- VALUES (s.a, s.a);
+ VALUES (s.a, s.a)
+ RETURNING MERGING(ACTION) AS action,
+ MERGING(CLAUSE_NUMBER) AS clause_number,
+ s.a,
+ s.b,
+ t.id,
+ t.data,
+ t.filling;
END
DROP FUNCTION merge_sf_test;
DROP TABLE sf_target;
diff --git a/src/test/regress/sql/merge.sql b/src/test/regress/sql/merge.sql
new file mode 100644
index 4cf6db9..0eb2f97
--- 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
@@ -816,7 +816,7 @@ BEGIN;
MERGE INTO sq_target
USING v
ON tid = sid
-WHEN MATCHED AND tid > 2 THEN
+WHEN MATCHED AND tid >= 2 THEN
UPDATE SET balance = balance + delta
WHEN NOT MATCHED THEN
INSERT (balance, tid) VALUES (balance + delta, sid)
@@ -829,7 +829,7 @@ INSERT INTO sq_source (sid, balance, del
MERGE INTO sq_target t
USING v
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)
@@ -847,7 +847,7 @@ WITH targq AS (
MERGE INTO sq_target t
USING v
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)
@@ -857,17 +857,146 @@ ROLLBACK;
-- RETURNING
BEGIN;
-INSERT INTO sq_source (sid, balance, delta) VALUES (-1, -1, -10);
+CREATE TABLE merge_actions(action text, abbrev text);
+INSERT INTO merge_actions VALUES ('INSERT', 'ins'), ('UPDATE', 'upd'), ('DELETE', 'del');
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)
+ INSERT (balance, tid) VALUES (balance + delta, sid)
WHEN MATCHED AND tid < 2 THEN
- DELETE
-RETURNING *;
+ DELETE
+RETURNING MERGING(CLAUSE_NUMBER) AS clause,
+ (SELECT abbrev FROM merge_actions
+ WHERE action = MERGING(ACTION)) AS action,
+ t.*,
+ CASE MERGING(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 MERGING() outside MERGE
+SELECT MERGING(ACTION) FROM sq_target;
+UPDATE sq_target SET balance = balance + 1 RETURNING MERGING(CLAUSE_NUMBER);
+
+-- RETURNING in CTEs
+CREATE TABLE sq_target_merge_log (tid integer NOT NULL, last_change text);
+INSERT INTO sq_target_merge_log VALUES (1, 'Original value');
+BEGIN;
+WITH m AS (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 MERGING(CLAUSE_NUMBER), MERGING(ACTION), t.*,
+ CASE MERGING(ACTION)
+ WHEN 'INSERT' THEN 'Inserted '||t
+ WHEN 'UPDATE' THEN 'Added '||delta||' to balance'
+ WHEN 'DELETE' THEN 'Removed '||t
+ END AS description
+), m2 AS (
+ MERGE INTO sq_target_merge_log l
+ USING m
+ ON l.tid = m.tid
+ WHEN MATCHED THEN
+ UPDATE SET last_change = description
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (m.tid, description)
+ RETURNING action, MERGING(ACTION) AS log_action, l.*
+)
+SELECT * FROM m2;
+SELECT * FROM sq_target_merge_log ORDER BY tid;
+ROLLBACK;
+
+-- COPY (MERGE ... RETURNING) TO ...
+BEGIN;
+COPY (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 MERGING(ACTION), t.*
+) TO stdout;
+ROLLBACK;
+
+-- SQL function with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT action text, OUT tid int, OUT new_balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING MERGING(ACTION), t.*;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT action, tid, new_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ROLLBACK;
+
+-- SQL SRF with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_sq_source_into_sq_target()
+RETURNS TABLE (action text, tid int, balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 MERGING(ACTION), t.*;
+$$;
+SELECT * FROM merge_sq_source_into_sq_target();
+ROLLBACK;
+
+-- PL/pgSQL function with MERGE ... RETURNING ... INTO
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT r_action text, OUT r_tid int, OUT r_balance int)
+LANGUAGE plpgsql AS
+$$
+BEGIN
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING MERGING(ACTION), t.* INTO r_action, r_tid, r_balance;
+END;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT r_action, r_tid, r_balance FROM merge_into_sq_target(sid, balance, delta)) m;
ROLLBACK;
-- EXPLAIN
@@ -966,7 +1095,7 @@ WHEN MATCHED THEN
SELECT * FROM sq_target WHERE tid = 1;
ROLLBACK;
-DROP TABLE sq_target, sq_source CASCADE;
+DROP TABLE sq_target, sq_target_merge_log, sq_source CASCADE;
CREATE TABLE pa_target (tid integer, balance float, val text)
PARTITION BY LIST (tid);
@@ -1033,6 +1162,17 @@ SELECT merge_func();
SELECT * FROM pa_target ORDER BY tid;
ROLLBACK;
+-- update partition key to partition not initially scanned
+BEGIN;
+MERGE INTO pa_target t
+ USING pa_source s
+ ON t.tid = s.sid AND t.tid = 1
+ WHEN MATCHED THEN
+ UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
+ RETURNING MERGING(ACTION), MERGING(CLAUSE_NUMBER), t.*;
+SELECT * FROM pa_target ORDER BY tid;
+ROLLBACK;
+
DROP TABLE pa_target CASCADE;
-- The target table is partitioned in the same way, but this time by attaching
@@ -1091,7 +1231,8 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (sid, delta, 'inserted by merge');
+ INSERT VALUES (sid, delta, 'inserted by merge')
+ RETURNING MERGING(ACTION), MERGING(CLAUSE_NUMBER), t.*;
SELECT * FROM pa_target ORDER BY tid;
ROLLBACK;
@@ -1145,7 +1286,8 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge');
+ INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge')
+ RETURNING MERGING(ACTION), MERGING(CLAUSE_NUMBER), t.*;
SELECT * FROM pa_target ORDER BY tid;
ROLLBACK;
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
new file mode 100644
index dec7340..1d5ed0a
--- a/src/test/regress/sql/rowsecurity.sql
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -992,6 +992,27 @@ WHEN MATCHED THEN
UPDATE SET dnotes = dnotes || ' notes added by merge8 '
WHEN NOT MATCHED THEN
INSERT VALUES (13, 44, 1, 'regress_rls_bob', 'new manga');
+SELECT * FROM document WHERE did = 13;
+
+-- but not OK if RETURNING is used
+MERGE INTO document d
+USING (SELECT 14 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge9 '
+WHEN NOT MATCHED THEN
+ INSERT VALUES (14, 44, 1, 'regress_rls_bob', 'new manga')
+RETURNING *;
+
+-- but OK if new row is visible
+MERGE INTO document d
+USING (SELECT 14 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge10 '
+WHEN NOT MATCHED THEN
+ INSERT VALUES (14, 11, 1, 'regress_rls_bob', 'new novel')
+RETURNING *;
RESET SESSION AUTHORIZATION;
-- drop the restrictive SELECT policy so that we can look at the
diff --git a/src/test/regress/sql/rules.sql b/src/test/regress/sql/rules.sql
new file mode 100644
index 8b7e255..d820b9b
--- a/src/test/regress/sql/rules.sql
+++ b/src/test/regress/sql/rules.sql
@@ -1281,7 +1281,8 @@ MERGE INTO rule_merge2 t USING (SELECT 1
CREATE TABLE sf_target(id int, data text, filling int[]);
CREATE FUNCTION merge_sf_test()
- RETURNS void
+ RETURNS TABLE(action text, clause_number int,
+ a int, b text, id int, data text, filling int[])
LANGUAGE sql
BEGIN ATOMIC
MERGE INTO sf_target t
@@ -1318,7 +1319,9 @@ WHEN NOT MATCHED
VALUES (s.a, s.b, DEFAULT)
WHEN NOT MATCHED
THEN INSERT (filling[1], id)
- VALUES (s.a, s.a);
+ VALUES (s.a, s.a)
+RETURNING
+ merging(action), merging(clause_number) AS clause_number, *;
END;
\sf merge_sf_test
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
new file mode 100644
index bf50a32..0ca25c6
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1561,6 +1561,8 @@ MergePath
MergeScanSelCache
MergeStmt
MergeWhenClause
+MergingFunc
+MergingFuncOp
MetaCommand
MinMaxAggInfo
MinMaxAggPath
Hi.
v13 works fine. all tests passed. The code is very intuitive. played
with multi WHEN clauses, even with before/after row triggers, work as
expected.
I don't know when replace_outer_merging will be invoked. even set a
breakpoint on it. coverage shows replace_outer_merging only called
once.
sql-merge.html miss mentioned RETURNING need select columns privilege?
in sql-insert.html, we have:
"Use of the RETURNING clause requires SELECT privilege on all columns
mentioned in RETURNING. If you use the query clause to insert rows
from a query, you of course need to have SELECT privilege on any table
or column used in the query."
I saw the change in src/sgml/glossary.sgml, So i looked around. in the
"Materialized view (relation)" part. "It cannot be modified via
INSERT, UPDATE, or DELETE operations.". Do we need to put "MERGE" into
that sentence?
also there is SELECT, INSERT, UPDATE, DELETE, do we need to add a
MERGE entry in glossary.sgml?
On Mon, 13 Nov 2023 at 05:29, jian he <jian.universality@gmail.com> wrote:
v13 works fine. all tests passed. The code is very intuitive. played
with multi WHEN clauses, even with before/after row triggers, work as
expected.
Thanks for the review and testing!
I don't know when replace_outer_merging will be invoked. even set a
breakpoint on it. coverage shows replace_outer_merging only called
once.
It's used when MERGING() is used in a subquery in the RETURNING list.
The MergingFunc node in the subquery is replaced by a Param node,
referring to the outer MERGE query, so that the result from MERGING()
is available in the SELECT subquery (under any other circumstances,
you're not allowed to use MERGING() in a SELECT). This is similar to
what happens when a subquery contains an aggregate over columns from
an outer query only -- for example, see:
https://github.com/postgres/postgres/commit/e649796f128bd8702ba5744d36f4e8cb81f0b754
A MERGING() expression in a subquery in the RETURNING list is
analogous, in that it belongs to the outer MERGE query, not the SELECT
subquery.
sql-merge.html miss mentioned RETURNING need select columns privilege?
in sql-insert.html, we have:
"Use of the RETURNING clause requires SELECT privilege on all columns
mentioned in RETURNING. If you use the query clause to insert rows
from a query, you of course need to have SELECT privilege on any table
or column used in the query."
Ah, good point. I don't think I looked at the privileges paragraph on
the MERGE page. Currently it says:
You will require the SELECT privilege on the data_source
and any column(s) of the target_table_name referred to in a
condition.
Being pedantic, there are 2 problems with that:
1. It might be taken to imply that you need the SELECT privilege on
every column of the data_source, which isn't the case.
2. It mentions conditions, but not expressions (such as those that can
appear in INSERT and UPDATE actions).
A more accurate statement would be:
You will require the SELECT privilege and any column(s)
of the data_source and target_table_name referred to in any
condition or expression.
which is also consistent with the wording used on the UPDATE manual page.
Done that way, I don't think it would need to be updated to mention
RETURNING, because RETURNING just returns a list of expressions.
Again, that would be consistent with the UPDATE page, which doesn't
mention RETURNING in its discussion of privileges.
I saw the change in src/sgml/glossary.sgml, So i looked around. in the
"Materialized view (relation)" part. "It cannot be modified via
INSERT, UPDATE, or DELETE operations.". Do we need to put "MERGE" into
that sentence?
also there is SELECT, INSERT, UPDATE, DELETE, do we need to add a
MERGE entry in glossary.sgml?
Yes, that makes sense.
Attached is a separate patch with those doc updates, intended to be
applied and back-patched independently of the main RETURNING patch.
Regards,
Dean
Attachments:
merge-docs.patch.no-cfbotapplication/octet-stream; name=merge-docs.patch.no-cfbotDownload
diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml
new file mode 100644
index fe8def4..5815fa4
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -1097,8 +1097,8 @@
(just like a <glossterm linkend="glossary-view">view</glossterm>),
but stores data in the same way that a
<glossterm linkend="glossary-table">table</glossterm> does. It cannot be
- modified via <command>INSERT</command>, <command>UPDATE</command>, or
- <command>DELETE</command> operations.
+ modified via <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command> operations.
</para>
<para>
For more information, see
@@ -1106,6 +1106,23 @@
</para>
</glossdef>
</glossentry>
+
+ <glossentry id="glossary-merge">
+ <glossterm>Merge</glossterm>
+ <glossdef>
+ <para>
+ An <acronym>SQL</acronym> command used to conditionally add, modify,
+ or remove <glossterm linkend="glossary-tuple">rows</glossterm>
+ in a given <glossterm linkend="glossary-table">table</glossterm>,
+ using data from a source
+ <glossterm linkend="glossary-relation">relation</glossterm>.
+ </para>
+ <para>
+ For more information, see
+ <xref linkend="sql-merge"/>.
+ </para>
+ </glossdef>
+ </glossentry>
<glossentry id="glossary-mvcc">
<glossterm>Multi-version concurrency control (MVCC)</glossterm>
diff --git a/doc/src/sgml/ref/merge.sgml b/doc/src/sgml/ref/merge.sgml
new file mode 100644
index 0f61d47..ab78510
--- a/doc/src/sgml/ref/merge.sgml
+++ b/doc/src/sgml/ref/merge.sgml
@@ -102,14 +102,14 @@ DELETE
that are referred to in the <literal>SET</literal> clause.
If you specify an insert action, you must have the <literal>INSERT</literal>
privilege on the <replaceable class="parameter">target_table_name</replaceable>.
- If you specify an delete action, you must have the <literal>DELETE</literal>
+ If you specify a delete action, you must have the <literal>DELETE</literal>
privilege on the <replaceable class="parameter">target_table_name</replaceable>.
Privileges are tested once at statement start and are checked
whether or not particular <literal>WHEN</literal> clauses are executed.
- You will require the <literal>SELECT</literal> privilege on the
- <replaceable class="parameter">data_source</replaceable> and any column(s)
- of the <replaceable class="parameter">target_table_name</replaceable>
- referred to in a <literal>condition</literal>.
+ You will require the <literal>SELECT</literal> privilege and any column(s)
+ of the <replaceable class="parameter">data_source</replaceable> and
+ <replaceable class="parameter">target_table_name</replaceable> referred to
+ in any <literal>condition</literal> or <literal>expression</literal>.
</para>
<para>
Attached is a separate patch with those doc updates, intended to be
applied and back-patched independently of the main RETURNING patch.Regards,
Dean
+ You will require the <literal>SELECT</literal> privilege and any column(s)
+ of the <replaceable class="parameter">data_source</replaceable> and
+ <replaceable class="parameter">target_table_name</replaceable> referred to
+ in any <literal>condition</literal> or <literal>expression</literal>.
I think it should be:
+ You will require the <literal>SELECT</literal> privilege on any column(s)
+ of the <replaceable class="parameter">data_source</replaceable> and
+ <replaceable class="parameter">target_table_name</replaceable> referred to
+ in any <literal>condition</literal> or <literal>expression</literal>.
Other than that, it looks fine.
On Fri, 17 Nov 2023 at 04:30, jian he <jian.universality@gmail.com> wrote:
I think it should be: + You will require the <literal>SELECT</literal> privilege on any column(s) + of the <replaceable class="parameter">data_source</replaceable> and + <replaceable class="parameter">target_table_name</replaceable> referred to + in any <literal>condition</literal> or <literal>expression</literal>.
Ah, of course. As always, I'm blind to grammatical errors in my own
text, no matter how many times I read it. Thanks for checking!
Pushed.
The v13 patch still applies on top of this, so I won't re-post it.
Regards,
Dean
On Sat, Nov 18, 2023 at 8:55 PM Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
The v13 patch still applies on top of this, so I won't re-post it.
Hi.
minor issues based on v13.
+<synopsis>
+<function id="function-merging">MERGING</function> (
<replaceable>property</replaceable> )
+</synopsis>
+ The following are valid property values specifying what to return:
+
+ <variablelist>
+ <varlistentry>
+ <term><literal>ACTION</literal></term>
+ <listitem>
+ <para>
+ The merge action command executed for the current row
+ (<literal>'INSERT'</literal>, <literal>'UPDATE'</literal>, or
+ <literal>'DELETE'</literal>).
+ </para>
+ </listitem>
+ </varlistentry>
do we change to <literal>property</literal>?
Maybe the main para should be two sentences like:
The merge action command executed for the current row. Possible values
are: <literal>'INSERT'</literal>, <literal>'UPDATE'</literal>,
<literal>'DELETE'</literal>.
static Node *
+transformMergingFunc(ParseState *pstate, MergingFunc *f)
+{
+ /*
+ * Check that we're in the RETURNING list of a MERGE command.
+ */
+ if (pstate->p_expr_kind != EXPR_KIND_MERGE_RETURNING)
+ {
+ ParseState *parent_pstate = pstate->parentParseState;
+
+ while (parent_pstate &&
+ parent_pstate->p_expr_kind != EXPR_KIND_MERGE_RETURNING)
+ parent_pstate = parent_pstate->parentParseState;
+
+ if (!parent_pstate ||
+ parent_pstate->p_expr_kind != EXPR_KIND_MERGE_RETURNING)
+ ereport(ERROR,
+ errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("MERGING() can only be used in the RETURNING list of a MERGE command"),
+ parser_errposition(pstate, f->location));
+ }
+
+ return (Node *) f;
+}
+
the object is correct, but not in the right place.
maybe we should change errcode(ERRCODE_WRONG_OBJECT_TYPE) to
errcode(ERRCODE_INVALID_OBJECT_DEFINITION)
also do we need to add some comments explain that why we return it as
is when it's EXPR_KIND_MERGE_RETURNING.
(my understanding is that, if key words not match, then it will fail
at gram.y, like syntax error, else MERGING will based on keywords make
a MergingFunc node and assign mfop, mftype, location to it)
in src/backend/executor/functions.c
/*
* Break from loop if we didn't shut down (implying we got a
* lazily-evaluated row). Otherwise we'll press on till the whole
* function is done, relying on the tuplestore to keep hold of the
* data to eventually be returned. This is necessary since an
* INSERT/UPDATE/DELETE RETURNING that sets the result might be
* followed by additional rule-inserted commands, and we want to
* finish doing all those commands before we return anything.
*/
Does the above comments need to change to INSERT/UPDATE/DELETE/MERGE?
in src/backend/nodes/nodeFuncs.c
case T_UpdateStmt:
{
UpdateStmt *stmt = (UpdateStmt *) node;
if (WALK(stmt->relation))
return true;
if (WALK(stmt->targetList))
return true;
if (WALK(stmt->whereClause))
return true;
if (WALK(stmt->fromClause))
return true;
if (WALK(stmt->returningList))
return true;
if (WALK(stmt->withClause))
return true;
}
break;
case T_MergeStmt:
{
MergeStmt *stmt = (MergeStmt *) node;
if (WALK(stmt->relation))
return true;
if (WALK(stmt->sourceRelation))
return true;
if (WALK(stmt->joinCondition))
return true;
if (WALK(stmt->mergeWhenClauses))
return true;
if (WALK(stmt->withClause))
return true;
}
break;
you add "returningList" to MergeStmt.
do you need to do the following similar to UpdateStmt, even though
it's so abstract, i have no idea what's going on.
`
if (WALK(stmt->returningList))
return true;
`
On Wed, 17 Jan 2024 at 14:43, jian he <jian.universality@gmail.com> wrote:
+<synopsis> +<function id="function-merging">MERGING</function> ( <replaceable>property</replaceable> ) +</synopsis> + The following are valid property values specifying what to return: + + <variablelist> + <varlistentry> + <term><literal>ACTION</literal></term> + <listitem> + <para> + The merge action command executed for the current row + (<literal>'INSERT'</literal>, <literal>'UPDATE'</literal>, or + <literal>'DELETE'</literal>). + </para> + </listitem> + </varlistentry> do we change to <literal>property</literal>? Maybe the main para should be two sentences like: The merge action command executed for the current row. Possible values are: <literal>'INSERT'</literal>, <literal>'UPDATE'</literal>, <literal>'DELETE'</literal>.
OK, though actually it should be <parameter>property</parameter>.
Also, the parameter should be described as a key word, to match
similar existing keyword function parameters (e.g., the normalize()
function's second parameter).
+ if (!parent_pstate || + parent_pstate->p_expr_kind != EXPR_KIND_MERGE_RETURNING) + ereport(ERROR, + errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("MERGING() can only be used in the RETURNING list of a MERGE command"), + parser_errposition(pstate, f->location)); + the object is correct, but not in the right place. maybe we should change errcode(ERRCODE_WRONG_OBJECT_TYPE) to errcode(ERRCODE_INVALID_OBJECT_DEFINITION) also do we need to add some comments explain that why we return it as is when it's EXPR_KIND_MERGE_RETURNING. (my understanding is that, if key words not match, then it will fail at gram.y, like syntax error, else MERGING will based on keywords make a MergingFunc node and assign mfop, mftype, location to it)
Ah yes, that error code dates back to an earlier version of the patch,
when this check was done in ParseFuncOrColumn(), when I think I just
copied the error code from nearby checks. I agree that
ERRCODE_WRONG_OBJECT_TYPE isn't really right, but I'm not convinced
that ERRCODE_INVALID_OBJECT_DEFINITION is right either, since the
object is valid, but it's not allowed in that part of the query. I
think ERRCODE_SYNTAX_ERROR is probably better (similar to when we find
"DEFAULT" where we shouldn't, for example).
in src/backend/executor/functions.c
/*
* Break from loop if we didn't shut down (implying we got a
* lazily-evaluated row). Otherwise we'll press on till the whole
* function is done, relying on the tuplestore to keep hold of the
* data to eventually be returned. This is necessary since an
* INSERT/UPDATE/DELETE RETURNING that sets the result might be
* followed by additional rule-inserted commands, and we want to
* finish doing all those commands before we return anything.
*/
Does the above comments need to change to INSERT/UPDATE/DELETE/MERGE?
No, because MERGE doesn't support tables with rules, so this can't
apply to MERGE. I suppose the comment could be updated to say that,
but I don't think it's worth it, because I think it would distract the
reader from the main point of the comment. I think that function is
complex enough as it is, and since this patch isn't touching it, it
should probably be left alone.
in src/backend/nodes/nodeFuncs.c
you add "returningList" to MergeStmt.
do you need to do the following similar to UpdateStmt, even though
it's so abstract, i have no idea what's going on.
`
if (WALK(stmt->returningList))
return true;
`
Ah yes, good point. This can be triggered using a recursive CTE
containing a MERGE ... RETURNING that returns an expression containing
a subquery with a recursive reference to the outer CTE, which should
be an error. I've added a regression test to ensure that this walker
path gets coverage.
Thanks for reviewing. Updated patch attached.
The wider question is whether people are happy with the overall
approach this patch now takes, and the new MERGING() function and
MergingFunc node.
Regards,
Dean
Attachments:
support-merge-returning-v14.patchtext/x-patch; charset=US-ASCII; name=support-merge-returning-v14.patchDownload
diff --git a/doc/src/sgml/dml.sgml b/doc/src/sgml/dml.sgml
new file mode 100644
index cbbc5e2..3d95bdb
--- a/doc/src/sgml/dml.sgml
+++ b/doc/src/sgml/dml.sgml
@@ -283,10 +283,15 @@ DELETE FROM products;
<secondary>RETURNING</secondary>
</indexterm>
+ <indexterm zone="dml-returning">
+ <primary>MERGE</primary>
+ <secondary>RETURNING</secondary>
+ </indexterm>
+
<para>
Sometimes it is useful to obtain data from modified rows while they are
being manipulated. The <command>INSERT</command>, <command>UPDATE</command>,
- and <command>DELETE</command> commands all have an
+ <command>DELETE</command>, and <command>MERGE</command> commands all have an
optional <literal>RETURNING</literal> clause that supports this. Use
of <literal>RETURNING</literal> avoids performing an extra database query to
collect the data, and is especially valuable when it would otherwise be
@@ -339,6 +344,21 @@ DELETE FROM products
</programlisting>
</para>
+ <para>
+ In a <command>MERGE</command>, the data available to <literal>RETURNING</literal> is
+ the content of the source row plus the content of the inserted, updated, or
+ deleted target row. Since it is quite common for the source and target to
+ have many of the same columns, specifying <literal>RETURNING *</literal>
+ can lead to a lot of duplicated columns, so it is often more useful to
+ qualify it so as to return just the source or target row. For example:
+<programlisting>
+MERGE INTO products p USING new_products n ON p.product_no = n.product_no
+ WHEN NOT MATCHED THEN INSERT VALUES (n.product_no, n.name, n.price)
+ WHEN MATCHED THEN UPDATE SET name = n.name, price = n.price
+ RETURNING p.*;
+</programlisting>
+ </para>
+
<para>
If there are triggers (<xref linkend="triggers"/>) on the target table,
the data available to <literal>RETURNING</literal> is the row as modified by
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
new file mode 100644
index 210c7c0..51db66f
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -21998,6 +21998,84 @@ SELECT count(*) FROM sometable;
</sect1>
+ <sect1 id="functions-merge-support">
+ <title>Merge Support Functions</title>
+
+ <indexterm>
+ <primary>MERGE</primary>
+ <secondary>RETURNING</secondary>
+ </indexterm>
+
+ <indexterm>
+ <primary>MERGING</primary>
+ </indexterm>
+
+ <para>
+ <productname>PostgreSQL</productname> includes one merge support function
+ that may be used in the <literal>RETURNING</literal> list of a
+ <xref linkend="sql-merge"/> command to return additional information about
+ the action taken for each row:
+<synopsis>
+<function id="function-merging">MERGING</function> ( <parameter>property</parameter> )
+</synopsis>
+ The <parameter>property</parameter> key word specifies what to return:
+
+ <variablelist>
+ <varlistentry>
+ <term><literal>ACTION</literal></term>
+ <listitem>
+ <para>
+ The merge action command executed for the current row. This will be
+ <literal>'INSERT'</literal>, <literal>'UPDATE'</literal>, or
+ <literal>'DELETE'</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>CLAUSE_NUMBER</literal></term>
+ <listitem>
+ <para>
+ The 1-based index of the <literal>WHEN</literal> clause executed for
+ the current row.
+ </para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </para>
+
+ <para>
+ The following example illustrates how this may be used to determine
+ which actions were performed for each row affected by the
+ <command>MERGE</command> command:
+<screen><![CDATA[
+MERGE INTO products p
+ USING stock s ON p.product_id = s.product_id
+ WHEN MATCHED AND s.quantity > 0 THEN
+ UPDATE SET in_stock = true, quantity = s.quantity
+ WHEN MATCHED THEN
+ UPDATE SET in_stock = false, quantity = 0
+ WHEN NOT MATCHED THEN
+ INSERT (product_id, in_stock, quantity)
+ VALUES (s.product_id, true, s.quantity)
+ RETURNING MERGING(ACTION), MERGING(CLAUSE_NUMBER), p.*;
+
+ action | clause_number | product_id | in_stock | quantity
+--------+---------------+------------+----------+----------
+ UPDATE | 1 | 1001 | t | 50
+ UPDATE | 2 | 1002 | f | 0
+ INSERT | 3 | 1003 | t | 10
+]]></screen>
+ </para>
+
+ <para>
+ Note that this function can only be used in the <literal>RETURNING</literal>
+ list of a <command>MERGE</command> command. It is an error to use it in any
+ other part of a query.
+ </para>
+
+ </sect1>
+
<sect1 id="functions-subquery">
<title>Subquery Expressions</title>
diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml
new file mode 100644
index 881026d..b609bd3
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -1426,9 +1426,9 @@
to a <glossterm linkend="glossary-client">client</glossterm> upon the
completion of an <acronym>SQL</acronym> command, usually a
<command>SELECT</command> but it can be an
- <command>INSERT</command>, <command>UPDATE</command>, or
- <command>DELETE</command> command if the <literal>RETURNING</literal>
- clause is specified.
+ <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command> command if the
+ <literal>RETURNING</literal> clause is specified.
</para>
<para>
The fact that a result set is a relation means that a query can be used
diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml
new file mode 100644
index c2b9c6a..406834e
--- a/doc/src/sgml/plpgsql.sgml
+++ b/doc/src/sgml/plpgsql.sgml
@@ -1043,8 +1043,8 @@ INSERT INTO mytable VALUES (1,'one'), (2
</para>
<para>
- If the command does return rows (for example <command>SELECT</command>,
- or <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>
+ If the command does return rows (for example <command>SELECT</command>, or
+ <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command>
with <literal>RETURNING</literal>), there are two ways to proceed.
When the command will return at most one row, or you only care about
the first row of output, write the command as usual but add
@@ -1172,6 +1172,7 @@ SELECT <replaceable>select_expressions</
INSERT ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
UPDATE ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
DELETE ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
+MERGE ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
</synopsis>
where <replaceable>target</replaceable> can be a record variable, a row
@@ -1182,8 +1183,8 @@ DELETE ... RETURNING <replaceable>expres
<literal>INTO</literal> clause) just as described above,
and the plan is cached in the same way.
This works for <command>SELECT</command>,
- <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command> with
- <literal>RETURNING</literal>, and certain utility commands
+ <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command>
+ with <literal>RETURNING</literal>, and certain utility commands
that return row sets, such as <command>EXPLAIN</command>.
Except for the <literal>INTO</literal> clause, the SQL command is the same
as it would be written outside <application>PL/pgSQL</application>.
@@ -1259,7 +1260,7 @@ END;
</para>
<para>
- For <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command> with
+ For <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command> with
<literal>RETURNING</literal>, <application>PL/pgSQL</application> reports
an error for more than one returned row, even when
<literal>STRICT</literal> is not specified. This is because there
diff --git a/doc/src/sgml/queries.sgml b/doc/src/sgml/queries.sgml
new file mode 100644
index 648b283..372cce1
--- a/doc/src/sgml/queries.sgml
+++ b/doc/src/sgml/queries.sgml
@@ -2063,9 +2063,10 @@ SELECT <replaceable>select_list</replace
Table Expressions or <acronym>CTE</acronym>s, can be thought of as defining
temporary tables that exist just for one query. Each auxiliary statement
in a <literal>WITH</literal> clause can be a <command>SELECT</command>,
- <command>INSERT</command>, <command>UPDATE</command>, or <command>DELETE</command>; and the
+ <command>INSERT</command>, <command>UPDATE</command>, <command>DELETE</command>,
+ or <command>MERGE</command>; and the
<literal>WITH</literal> clause itself is attached to a primary statement that can
- be a <command>SELECT</command>, <command>INSERT</command>, <command>UPDATE</command>,
+ also be a <command>SELECT</command>, <command>INSERT</command>, <command>UPDATE</command>,
<command>DELETE</command>, or <command>MERGE</command>.
</para>
@@ -2599,8 +2600,8 @@ SELECT * FROM w AS w1 JOIN w AS w2 ON w1
<title>Data-Modifying Statements in <literal>WITH</literal></title>
<para>
- You can use most data-modifying statements (<command>INSERT</command>,
- <command>UPDATE</command>, or <command>DELETE</command>, but not
+ You can use data-modifying statements (<command>INSERT</command>,
+ <command>UPDATE</command>, <command>DELETE</command>, or
<command>MERGE</command>) in <literal>WITH</literal>. This
allows you to perform several different operations in the same query.
An example is:
diff --git a/doc/src/sgml/ref/copy.sgml b/doc/src/sgml/ref/copy.sgml
new file mode 100644
index 85881ca..49ce67c
--- a/doc/src/sgml/ref/copy.sgml
+++ b/doc/src/sgml/ref/copy.sgml
@@ -123,16 +123,19 @@ COPY { <replaceable class="parameter">ta
A <link linkend="sql-select"><command>SELECT</command></link>,
<link linkend="sql-values"><command>VALUES</command></link>,
<link linkend="sql-insert"><command>INSERT</command></link>,
- <link linkend="sql-update"><command>UPDATE</command></link>, or
- <link linkend="sql-delete"><command>DELETE</command></link> command whose results are to be
- copied. Note that parentheses are required around the query.
+ <link linkend="sql-update"><command>UPDATE</command></link>,
+ <link linkend="sql-delete"><command>DELETE</command></link>, or
+ <link linkend="sql-merge"><command>MERGE</command></link> command
+ whose results are to be copied. Note that parentheses are required
+ around the query.
</para>
<para>
- For <command>INSERT</command>, <command>UPDATE</command> and
- <command>DELETE</command> queries a <literal>RETURNING</literal> clause
- must be provided, and the target relation must not have a conditional
- rule, nor an <literal>ALSO</literal> rule, nor an
- <literal>INSTEAD</literal> rule that expands to multiple statements.
+ For <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, and <command>MERGE</command> queries a
+ <literal>RETURNING</literal> clause must be provided, and the target
+ relation must not have a conditional rule, nor an
+ <literal>ALSO</literal> rule, nor an <literal>INSTEAD</literal> rule
+ that expands to multiple statements.
</para>
</listitem>
</varlistentry>
diff --git a/doc/src/sgml/ref/merge.sgml b/doc/src/sgml/ref/merge.sgml
new file mode 100644
index 655f7dc..44b3fa4
--- a/doc/src/sgml/ref/merge.sgml
+++ b/doc/src/sgml/ref/merge.sgml
@@ -25,6 +25,7 @@ PostgreSQL documentation
MERGE INTO [ ONLY ] <replaceable class="parameter">target_table_name</replaceable> [ * ] [ [ AS ] <replaceable class="parameter">target_alias</replaceable> ]
USING <replaceable class="parameter">data_source</replaceable> ON <replaceable class="parameter">join_condition</replaceable>
<replaceable class="parameter">when_clause</replaceable> [...]
+[ RETURNING * | <replaceable class="parameter">output_expression</replaceable> [ [ AS ] <replaceable class="parameter">output_name</replaceable> ] [, ...] ]
<phrase>where <replaceable class="parameter">data_source</replaceable> is:</phrase>
@@ -95,6 +96,18 @@ DELETE
</para>
<para>
+ The optional <literal>RETURNING</literal> clause causes <command>MERGE</command>
+ to compute and return value(s) based on each row inserted, updated, or
+ deleted. Any expression using the source or target table's columns, or
+ the <xref linkend="function-merging"/> function can be computed. When an
+ <command>INSERT</command> or <command>UPDATE</command> action is performed,
+ the new values of the target table's columns are used. When a
+ <command>DELETE</command> is performed, the old values of the target table's
+ columns are used. The syntax of the <literal>RETURNING</literal> list is
+ identical to that of the output list of <command>SELECT</command>.
+ </para>
+
+ <para>
There is no separate <literal>MERGE</literal> privilege.
If you specify an update action, you must have the
<literal>UPDATE</literal> privilege on the column(s)
@@ -125,6 +138,18 @@ DELETE
<variablelist>
<varlistentry>
+ <term><replaceable class="parameter">with_query</replaceable></term>
+ <listitem>
+ <para>
+ The <literal>WITH</literal> clause allows you to specify one or more
+ subqueries that can be referenced by name in the <command>MERGE</command>
+ query. See <xref linkend="queries-with"/> and <xref linkend="sql-select"/>
+ for details.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
<term><replaceable class="parameter">target_table_name</replaceable></term>
<listitem>
<para>
@@ -399,13 +424,30 @@ DELETE
</varlistentry>
<varlistentry>
- <term><replaceable class="parameter">with_query</replaceable></term>
+ <term><replaceable class="parameter">output_expression</replaceable></term>
<listitem>
<para>
- The <literal>WITH</literal> clause allows you to specify one or more
- subqueries that can be referenced by name in the <command>MERGE</command>
- query. See <xref linkend="queries-with"/> and <xref linkend="sql-select"/>
- for details.
+ An expression to be computed and returned by the <command>MERGE</command>
+ command after each row is merged. The expression can use any columns of
+ the source or target tables, or the <xref linkend="function-merging"/>
+ function to return additional information about the action executed.
+ </para>
+ <para>
+ Writing <literal>*</literal> will return all columns from the source
+ table, followed by all columns from the target table. Often this will
+ lead to a lot of duplication, since it is common for the source and
+ target tables to have a lot of the same columns. This can be avoided by
+ qualifying the <literal>*</literal> with the name or alias of the source
+ or target table.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><replaceable class="parameter">output_name</replaceable></term>
+ <listitem>
+ <para>
+ A name to use for a returned column.
</para>
</listitem>
</varlistentry>
@@ -428,6 +470,13 @@ MERGE <replaceable class="parameter">tot
were changed in any way.
</para>
+ <para>
+ If the <command>MERGE</command> command contains a <literal>RETURNING</literal>
+ clause, the result will be similar to that of a <command>SELECT</command>
+ statement containing the columns and values defined in the
+ <literal>RETURNING</literal> list, computed over the row(s) inserted, updated,
+ or deleted by the command.
+ </para>
</refsect1>
<refsect1>
@@ -544,13 +593,6 @@ MERGE <replaceable class="parameter">tot
</para>
<para>
- There is no <literal>RETURNING</literal> clause with
- <command>MERGE</command>. Actions of <command>INSERT</command>,
- <command>UPDATE</command> and <command>DELETE</command> cannot contain
- <literal>RETURNING</literal> or <literal>WITH</literal> clauses.
- </para>
-
- <para>
When <command>MERGE</command> is run concurrently with other commands
that modify the target table, the usual transaction isolation rules
apply; see <xref linkend="transaction-iso"/> for an explanation
@@ -602,7 +644,8 @@ WHEN NOT MATCHED THEN
<para>
Attempt to insert a new stock item along with the quantity of stock. If
the item already exists, instead update the stock count of the existing
- item. Don't allow entries that have zero stock.
+ item. Don't allow entries that have zero stock. Return details of all
+ changes made.
<programlisting>
MERGE INTO wines w
USING wine_stock_changes s
@@ -612,7 +655,8 @@ WHEN NOT MATCHED AND s.stock_delta > 0 T
WHEN MATCHED AND w.stock + s.stock_delta > 0 THEN
UPDATE SET stock = w.stock + s.stock_delta
WHEN MATCHED THEN
- DELETE;
+ DELETE
+RETURNING MERGING(ACTION), w.*;
</programlisting>
The <literal>wine_stock_changes</literal> table might be, for example, a
@@ -627,8 +671,9 @@ WHEN MATCHED THEN
This command conforms to the <acronym>SQL</acronym> standard.
</para>
<para>
- The <literal>WITH</literal> clause and <literal>DO NOTHING</literal>
- action are extensions to the <acronym>SQL</acronym> standard.
+ The <literal>WITH</literal> clause, <literal>DO NOTHING</literal> action,
+ and <literal>RETURNING</literal> clause are extensions to the
+ <acronym>SQL</acronym> standard.
</para>
</refsect1>
</refentry>
diff --git a/doc/src/sgml/ref/select.sgml b/doc/src/sgml/ref/select.sgml
new file mode 100644
index 9917df7..066aed4
--- a/doc/src/sgml/ref/select.sgml
+++ b/doc/src/sgml/ref/select.sgml
@@ -74,7 +74,7 @@ SELECT [ ALL | DISTINCT [ ON ( <replacea
<phrase>and <replaceable class="parameter">with_query</replaceable> is:</phrase>
- <replaceable class="parameter">with_query_name</replaceable> [ ( <replaceable class="parameter">column_name</replaceable> [, ...] ) ] AS [ [ NOT ] MATERIALIZED ] ( <replaceable class="parameter">select</replaceable> | <replaceable class="parameter">values</replaceable> | <replaceable class="parameter">insert</replaceable> | <replaceable class="parameter">update</replaceable> | <replaceable class="parameter">delete</replaceable> )
+ <replaceable class="parameter">with_query_name</replaceable> [ ( <replaceable class="parameter">column_name</replaceable> [, ...] ) ] AS [ [ NOT ] MATERIALIZED ] ( <replaceable class="parameter">select</replaceable> | <replaceable class="parameter">values</replaceable> | <replaceable class="parameter">insert</replaceable> | <replaceable class="parameter">update</replaceable> | <replaceable class="parameter">delete</replaceable> | <replaceable class="parameter">merge</replaceable> )
[ SEARCH { BREADTH | DEPTH } FIRST BY <replaceable>column_name</replaceable> [, ...] SET <replaceable>search_seq_col_name</replaceable> ]
[ CYCLE <replaceable>column_name</replaceable> [, ...] SET <replaceable>cycle_mark_col_name</replaceable> [ TO <replaceable>cycle_mark_value</replaceable> DEFAULT <replaceable>cycle_mark_default</replaceable> ] USING <replaceable>cycle_path_col_name</replaceable> ]
@@ -230,10 +230,10 @@ TABLE [ ONLY ] <replaceable class="param
The subqueries effectively act as temporary tables or views
for the duration of the primary query.
Each subquery can be a <command>SELECT</command>, <command>TABLE</command>, <command>VALUES</command>,
- <command>INSERT</command>, <command>UPDATE</command> or
- <command>DELETE</command> statement.
+ <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command> statement.
When writing a data-modifying statement (<command>INSERT</command>,
- <command>UPDATE</command> or <command>DELETE</command>) in
+ <command>UPDATE</command>, <command>DELETE</command>, or <command>MERGE</command>) in
<literal>WITH</literal>, it is usual to include a <literal>RETURNING</literal> clause.
It is the output of <literal>RETURNING</literal>, <emphasis>not</emphasis> the underlying
table that the statement modifies, that forms the temporary table that is
@@ -2184,7 +2184,8 @@ SELECT 2+2;
<para>
<productname>PostgreSQL</productname> allows <command>INSERT</command>,
- <command>UPDATE</command>, and <command>DELETE</command> to be used as <literal>WITH</literal>
+ <command>UPDATE</command>, <command>DELETE</command>, and
+ <command>MERGE</command> to be used as <literal>WITH</literal>
queries. This is not found in the SQL standard.
</para>
</refsect2>
diff --git a/doc/src/sgml/rowtypes.sgml b/doc/src/sgml/rowtypes.sgml
new file mode 100644
index 4d86f97..bbeac84
--- a/doc/src/sgml/rowtypes.sgml
+++ b/doc/src/sgml/rowtypes.sgml
@@ -348,7 +348,7 @@ SELECT m.* FROM some_table, LATERAL myfu
column expansion of this kind when it appears at the top level of
a <link linkend="queries-select-lists"><command>SELECT</command> output
list</link>, a <link linkend="dml-returning"><literal>RETURNING</literal>
- list</link> in <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>,
+ list</link> in <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command>,
a <link linkend="queries-values"><literal>VALUES</literal> clause</link>, or
a <link linkend="sql-syntax-row-constructors">row constructor</link>.
In all other contexts (including when nested inside one of those
diff --git a/doc/src/sgml/spi.sgml b/doc/src/sgml/spi.sgml
new file mode 100644
index 47f4b5b..bb37786
--- a/doc/src/sgml/spi.sgml
+++ b/doc/src/sgml/spi.sgml
@@ -301,8 +301,9 @@ SPI_execute("INSERT INTO foo SELECT * FR
is returned in the global variable <varname>SPI_processed</varname>.
If the return value of the function is <symbol>SPI_OK_SELECT</symbol>,
<symbol>SPI_OK_INSERT_RETURNING</symbol>,
- <symbol>SPI_OK_DELETE_RETURNING</symbol>, or
- <symbol>SPI_OK_UPDATE_RETURNING</symbol>,
+ <symbol>SPI_OK_DELETE_RETURNING</symbol>,
+ <symbol>SPI_OK_UPDATE_RETURNING</symbol>, or
+ <symbol>SPI_OK_MERGE_RETURNING</symbol>,
then you can use the
global pointer <literal>SPITupleTable *SPI_tuptable</literal> to
access the result rows. Some utility commands (such as
@@ -472,6 +473,15 @@ typedef struct SPITupleTable
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><symbol>SPI_OK_MERGE_RETURNING</symbol></term>
+ <listitem>
+ <para>
+ if a <command>MERGE RETURNING</command> was executed
+ </para>
+ </listitem>
+ </varlistentry>
<varlistentry>
<term><symbol>SPI_OK_UTILITY</symbol></term>
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
new file mode 100644
index 89116ae..cb0aa55
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -177,7 +177,8 @@
statements separated by semicolons. A semicolon after the last
statement is optional. Unless the function is declared to return
<type>void</type>, the last statement must be a <command>SELECT</command>,
- or an <command>INSERT</command>, <command>UPDATE</command>, or <command>DELETE</command>
+ or an <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command>
that has a <literal>RETURNING</literal> clause.
</para>
@@ -1236,8 +1237,9 @@ SELECT x, CASE WHEN x > 0 THEN genera
<note>
<para>
- If a function's last command is <command>INSERT</command>, <command>UPDATE</command>,
- or <command>DELETE</command> with <literal>RETURNING</literal>, that command will
+ If a function's last command is <command>INSERT</command>,
+ <command>UPDATE</command>, <command>DELETE</command>, or
+ <command>MERGE</command> with <literal>RETURNING</literal>, that command will
always be executed to completion, even if the function is not declared
with <literal>SETOF</literal> or the calling query does not fetch all the
result rows. Any extra rows produced by the <literal>RETURNING</literal>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
new file mode 100644
index c36d7f1..7bcf923
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -283,12 +283,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 d3dc3fc..c29fecd
--- 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 91df200..7744be8
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -1108,6 +1108,21 @@ ExecInitExprRec(Expr *node, ExprState *s
break;
}
+ case T_MergingFunc:
+ {
+ MergingFunc *mf_node = (MergingFunc *) node;
+
+ if (!state->parent ||
+ !IsA(state->parent, ModifyTableState) ||
+ ((ModifyTableState *) state->parent)->operation != CMD_MERGE)
+ elog(ERROR, "MergingFunc found in non-merge plan node");
+
+ scratch.opcode = EEOP_MERGING_FUNC;
+ scratch.d.merging_func.mfop = mf_node->mfop;
+ ExprEvalPushStep(state, &scratch);
+ break;
+ }
+
case T_SubscriptingRef:
{
SubscriptingRef *sbsref = (SubscriptingRef *) node;
diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
new file mode 100644
index 3c17cc6..11ceeab
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -483,6 +483,7 @@ ExecInterpExpr(ExprState *state, ExprCon
&&CASE_EEOP_AGGREF,
&&CASE_EEOP_GROUPING_FUNC,
&&CASE_EEOP_WINDOW_FUNC,
+ &&CASE_EEOP_MERGING_FUNC,
&&CASE_EEOP_SUBPLAN,
&&CASE_EEOP_AGG_STRICT_DESERIALIZE,
&&CASE_EEOP_AGG_DESERIALIZE,
@@ -1582,6 +1583,14 @@ ExecInterpExpr(ExprState *state, ExprCon
EEO_NEXT();
}
+ EEO_CASE(EEOP_MERGING_FUNC)
+ {
+ /* too complex/uncommon for an inline implementation */
+ ExecEvalMergingFunc(state, op, econtext);
+
+ EEO_NEXT();
+ }
+
EEO_CASE(EEOP_SUBPLAN)
{
/* too complex for an inline implementation */
@@ -4171,6 +4180,59 @@ ExecEvalGroupingFunc(ExprState *state, E
}
/*
+ * ExecEvalMergingFunc
+ *
+ * Returns information about the current MERGE action for its RETURNING list.
+ */
+void
+ExecEvalMergingFunc(ExprState *state, ExprEvalStep *op, ExprContext *econtext)
+{
+ MergingFuncOp mfop = op->d.merging_func.mfop;
+ ModifyTableState *mtstate = castNode(ModifyTableState, state->parent);
+ MergeActionState *relaction = mtstate->mt_merge_action;
+
+ if (!relaction)
+ elog(ERROR, "no merge action in progress");
+
+ switch (mfop)
+ {
+ case MERGING_ACTION:
+ /* Return the MERGE action ("INSERT", "UPDATE", or "DELETE") */
+ switch (relaction->mas_action->commandType)
+ {
+ case CMD_INSERT:
+ *op->resvalue = PointerGetDatum(cstring_to_text_with_len("INSERT", 6));
+ *op->resnull = false;
+ break;
+ case CMD_UPDATE:
+ *op->resvalue = PointerGetDatum(cstring_to_text_with_len("UPDATE", 6));
+ *op->resnull = false;
+ break;
+ case CMD_DELETE:
+ *op->resvalue = PointerGetDatum(cstring_to_text_with_len("DELETE", 6));
+ *op->resnull = false;
+ break;
+ case CMD_NOTHING:
+ elog(ERROR, "unexpected merge action: DO NOTHING");
+ break;
+ default:
+ elog(ERROR, "unrecognized commandType: %d",
+ (int) relaction->mas_action->commandType);
+ }
+ break;
+
+ case MERGING_CLAUSE_NUMBER:
+ /* Return the 1-based index of the MERGE action */
+ *op->resvalue = Int32GetDatum(relaction->mas_action->index);
+ *op->resnull = false;
+ break;
+
+ default:
+ elog(ERROR, "unrecognized MergingFuncOp: %d", (int) mfop);
+ }
+}
+
+/*
* Hand off evaluation of a subplan to nodeSubplan.c
*/
void
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
new file mode 100644
index b22040a..9c56a10
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -612,8 +612,8 @@ ExecInitPartitionInfo(ModifyTableState *
* Build the RETURNING projection for the partition. Note that we didn't
* build the returningList for partitions within the planner, but simple
* translation of varattnos will suffice. This only occurs for the INSERT
- * case or in the case of UPDATE tuple routing where we didn't find a
- * result rel to reuse.
+ * case or in the case of UPDATE/MERGE tuple routing where we didn't find
+ * a result rel to reuse.
*/
if (node && node->returningLists != NIL)
{
@@ -622,12 +622,14 @@ ExecInitPartitionInfo(ModifyTableState *
List *returningList;
/* See the comment above for WCO lists. */
- /* (except no RETURNING support for MERGE yet) */
Assert((node->operation == CMD_INSERT &&
list_length(node->returningLists) == 1 &&
list_length(node->resultRelations) == 1) ||
(node->operation == CMD_UPDATE &&
list_length(node->returningLists) ==
+ list_length(node->resultRelations)) ||
+ (node->operation == CMD_MERGE &&
+ list_length(node->returningLists) ==
list_length(node->resultRelations)));
/*
diff --git a/src/backend/executor/functions.c b/src/backend/executor/functions.c
new file mode 100644
index 0f811fd..7da02b0
--- a/src/backend/executor/functions.c
+++ b/src/backend/executor/functions.c
@@ -1660,8 +1660,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
@@ -1679,7 +1679,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;
@@ -1693,7 +1694,7 @@ check_sql_fn_retval(List *queryTreeLists
(errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
errmsg("return type mismatch in function declared to return %s",
format_type_be(rettype)),
- errdetail("Function's final statement must be SELECT or INSERT/UPDATE/DELETE RETURNING.")));
+ errdetail("Function's final statement must be SELECT or INSERT/UPDATE/DELETE/MERGE RETURNING.")));
return false; /* keep compiler quiet */
}
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
new file mode 100644
index 9fc5abf..01ef113
--- 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"
@@ -88,9 +87,6 @@ typedef struct ModifyTableContext
*/
TupleTableSlot *planSlot;
- /* MERGE specific */
- MergeActionState *relaction; /* MERGE action in progress */
-
/*
* Information about the changes that were made concurrently to a tuple
* being updated or deleted
@@ -153,13 +149,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);
/*
@@ -979,7 +976,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;
@@ -1833,7 +1830,7 @@ ExecCrossPartitionUpdate(ModifyTableCont
* additional rechecking, and might end up executing a different
* action entirely).
*/
- if (context->relaction != NULL)
+ if (mtstate->mt_merge_action != NULL)
return *tmresult == TM_Ok;
else if (TupIsNull(epqslot))
return true;
@@ -2075,7 +2072,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 result;
/*
@@ -2718,6 +2715,7 @@ static TupleTableSlot *
ExecMerge(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
ItemPointer tupleid, bool canSetTag)
{
+ TupleTableSlot *rslot = NULL;
bool matched;
/*-----
@@ -2765,18 +2763,18 @@ 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()
- * returned "false", indicating the previously MATCHED tuple no longer
- * matches.
+ * Deal with the NOT MATCHED case (either a NOT MATCHED tuple from the
+ * join, or a previously MATCHED tuple for which ExecMergeMatched() set
+ * "matched" to false, indicating that it no longer matches).
*/
if (!matched)
- ExecMergeNotMatched(context, resultRelInfo, canSetTag);
+ rslot = ExecMergeNotMatched(context, resultRelInfo, canSetTag);
- /* No RETURNING support yet */
- return NULL;
+ return rslot;
}
/*
@@ -2786,8 +2784,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.
*
@@ -2796,16 +2794,17 @@ 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 *newslot;
+ TupleTableSlot *rslot = NULL;
+ TupleTableSlot *newslot = NULL;
EState *estate = context->estate;
ExprContext *econtext = mtstate->ps.ps_ExprContext;
bool isNull;
@@ -2816,7 +2815,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
@@ -2895,12 +2897,15 @@ lmerge_matched:
*/
newslot = ExecProject(relaction->mas_proj);
- context->relaction = relaction;
+ mtstate->mt_merge_action = relaction;
if (!ExecUpdatePrologue(context, resultRelInfo,
tupleid, NULL, newslot, &result))
{
if (result == TM_Ok)
- return true; /* "do nothing" */
+ {
+ *matched = true;
+ return NULL; /* "do nothing" */
+ }
break; /* concurrent update/delete */
}
result = ExecUpdateAct(context, resultRelInfo, tupleid, NULL,
@@ -2911,12 +2916,13 @@ lmerge_matched:
* cross-partition update was done, then there's nothing else
* for us to do --- the UPDATE has been turned into a DELETE
* and an INSERT, and we must not perform any of the usual
- * post-update tasks.
+ * post-update tasks. Also, the RETURNING tuple (if any) has
+ * been projected, so we can just return that.
*/
if (updateCxt.crossPartUpdate)
{
mtstate->mt_merge_updated += 1;
- return true;
+ return context->cpUpdateReturningSlot;
}
if (result == TM_Ok && updateCxt.updated)
@@ -2928,12 +2934,15 @@ lmerge_matched:
break;
case CMD_DELETE:
- context->relaction = relaction;
+ mtstate->mt_merge_action = relaction;
if (!ExecDeletePrologue(context, resultRelInfo, tupleid,
NULL, NULL, &result))
{
if (result == TM_Ok)
- return true; /* "do nothing" */
+ {
+ *matched = true;
+ return NULL; /* "do nothing" */
+ }
break; /* concurrent update/delete */
}
result = ExecDeleteAct(context, resultRelInfo, tupleid, false);
@@ -2989,7 +2998,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:
{
@@ -3035,13 +3045,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
@@ -3076,7 +3092,8 @@ lmerge_matched:
* tuple already deleted; tell caller to run NOT
* MATCHED actions
*/
- return false;
+ *matched = false;
+ return NULL;
case TM_SelfModified:
@@ -3096,13 +3113,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;
}
}
@@ -3114,6 +3132,31 @@ 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, "unrecognized commandType: %d",
+ (int) commandType);
+ }
+ }
+
/*
* We've activated one of the WHEN clauses, so we don't search
* further. This is required behaviour, not an optimization.
@@ -3124,19 +3167,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;
/*
@@ -3186,10 +3232,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:
@@ -3205,6 +3251,8 @@ ExecMergeNotMatched(ModifyTableContext *
*/
break;
}
+
+ return rslot;
}
/*
@@ -3667,8 +3715,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");
@@ -3745,8 +3802,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");
@@ -3838,7 +3904,7 @@ ExecModifyTable(PlanState *pstate)
}
slot = ExecGetUpdateNewTuple(resultRelInfo, context.planSlot,
oldSlot);
- context.relaction = NULL;
+ node->mt_merge_action = NULL;
/* Now apply the update. */
slot = ExecUpdate(&context, resultRelInfo, tupleid, oldtuple,
diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c
new file mode 100644
index d71484c..abb8b0d
--- a/src/backend/executor/spi.c
+++ b/src/backend/executor/spi.c
@@ -2033,6 +2033,8 @@ SPI_result_code_string(int code)
return "SPI_OK_TD_REGISTER";
case SPI_OK_MERGE:
return "SPI_OK_MERGE";
+ case SPI_OK_MERGE_RETURNING:
+ return "SPI_OK_MERGE_RETURNING";
}
/* Unrecognized code ... return something useful ... */
sprintf(buf, "Unrecognized SPI code %d", code);
@@ -2886,7 +2888,10 @@ _SPI_pquery(QueryDesc *queryDesc, bool f
res = SPI_OK_UPDATE;
break;
case CMD_MERGE:
- res = SPI_OK_MERGE;
+ if (queryDesc->plannedstmt->hasReturning)
+ res = SPI_OK_MERGE_RETURNING;
+ else
+ res = SPI_OK_MERGE;
break;
default:
return SPI_ERROR_OPUNKNOWN;
diff --git a/src/backend/jit/llvm/llvmjit_expr.c b/src/backend/jit/llvm/llvmjit_expr.c
new file mode 100644
index 33161d8..bbbaa93
--- a/src/backend/jit/llvm/llvmjit_expr.c
+++ b/src/backend/jit/llvm/llvmjit_expr.c
@@ -1980,6 +1980,12 @@ llvm_compile_expr(ExprState *state)
break;
}
+ case EEOP_MERGING_FUNC:
+ build_EvalXFunc(b, mod, "ExecEvalMergingFunc",
+ v_state, op, v_econtext);
+ LLVMBuildBr(b, opblocks[opno + 1]);
+ break;
+
case EEOP_SUBPLAN:
build_EvalXFunc(b, mod, "ExecEvalSubPlan",
v_state, op, v_econtext);
diff --git a/src/backend/jit/llvm/llvmjit_types.c b/src/backend/jit/llvm/llvmjit_types.c
new file mode 100644
index 5212f52..61fa503
--- a/src/backend/jit/llvm/llvmjit_types.c
+++ b/src/backend/jit/llvm/llvmjit_types.c
@@ -155,6 +155,7 @@ void *referenced_functions[] =
ExecEvalFuncExprFusage,
ExecEvalFuncExprStrictFusage,
ExecEvalGroupingFunc,
+ ExecEvalMergingFunc,
ExecEvalMinMax,
ExecEvalNextValueExpr,
ExecEvalParamExec,
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
new file mode 100644
index e1a5bc7..2468063
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -67,6 +67,9 @@ exprType(const Node *expr)
case T_WindowFunc:
type = ((const WindowFunc *) expr)->wintype;
break;
+ case T_MergingFunc:
+ type = ((const MergingFunc *) expr)->mftype;
+ break;
case T_SubscriptingRef:
type = ((const SubscriptingRef *) expr)->refrestype;
break;
@@ -810,6 +813,9 @@ exprCollation(const Node *expr)
case T_WindowFunc:
coll = ((const WindowFunc *) expr)->wincollid;
break;
+ case T_MergingFunc:
+ coll = ((const MergingFunc *) expr)->mfcollid;
+ break;
case T_SubscriptingRef:
coll = ((const SubscriptingRef *) expr)->refcollid;
break;
@@ -1085,6 +1091,9 @@ exprSetCollation(Node *expr, Oid collati
case T_WindowFunc:
((WindowFunc *) expr)->wincollid = collation;
break;
+ case T_MergingFunc:
+ ((MergingFunc *) expr)->mfcollid = collation;
+ break;
case T_SubscriptingRef:
((SubscriptingRef *) expr)->refcollid = collation;
break;
@@ -1343,6 +1352,9 @@ exprLocation(const Node *expr)
/* function name should always be the first thing */
loc = ((const WindowFunc *) expr)->location;
break;
+ case T_MergingFunc:
+ loc = ((const MergingFunc *) expr)->location;
+ break;
case T_SubscriptingRef:
/* just use container argument's location */
loc = exprLocation((Node *) ((const SubscriptingRef *) expr)->refexpr);
@@ -2035,6 +2047,7 @@ expression_tree_walker_impl(Node *node,
case T_RangeTblRef:
case T_SortGroupClause:
case T_CTESearchClause:
+ case T_MergingFunc:
/* primitive node types with no expression subnodes */
break;
case T_WithCheckOption:
@@ -2869,6 +2882,7 @@ expression_tree_mutator_impl(Node *node,
case T_RangeTblRef:
case T_SortGroupClause:
case T_CTESearchClause:
+ case T_MergingFunc:
return (Node *) copyObject(node);
case T_WithCheckOption:
{
@@ -3833,6 +3847,7 @@ raw_expression_tree_walker_impl(Node *no
case T_ParamRef:
case T_A_Const:
case T_A_Star:
+ case T_MergingFunc:
/* primitive node types with no subnodes */
break;
case T_Alias:
@@ -4053,6 +4068,8 @@ raw_expression_tree_walker_impl(Node *no
return true;
if (WALK(stmt->mergeWhenClauses))
return true;
+ if (WALK(stmt->returningList))
+ return true;
if (WALK(stmt->withClause))
return true;
}
diff --git a/src/backend/optimizer/plan/subselect.c b/src/backend/optimizer/plan/subselect.c
new file mode 100644
index 3115d79..a3646c0
--- a/src/backend/optimizer/plan/subselect.c
+++ b/src/backend/optimizer/plan/subselect.c
@@ -1846,7 +1846,8 @@ convert_EXISTS_to_ANY(PlannerInfo *root,
/*
* Replace correlation vars (uplevel vars) with Params.
*
- * Uplevel PlaceHolderVars and aggregates are replaced, too.
+ * Uplevel PlaceHolderVars, aggregates, GROUPING() expressions, and MERGING()
+ * expressions are replaced, too.
*
* Note: it is critical that this runs immediately after SS_process_sublinks.
* Since we do not recurse into the arguments of uplevel PHVs and aggregates,
@@ -1900,6 +1901,11 @@ replace_correlation_vars_mutator(Node *n
if (((GroupingFunc *) node)->agglevelsup > 0)
return (Node *) replace_outer_grouping(root, (GroupingFunc *) node);
}
+ if (IsA(node, MergingFunc))
+ {
+ if (root->parse->commandType != CMD_MERGE)
+ return (Node *) replace_outer_merging(root, (MergingFunc *) node);
+ }
return expression_tree_mutator(node,
replace_correlation_vars_mutator,
(void *) root);
diff --git a/src/backend/optimizer/util/paramassign.c b/src/backend/optimizer/util/paramassign.c
new file mode 100644
index a711003..9d8c5f6
--- a/src/backend/optimizer/util/paramassign.c
+++ b/src/backend/optimizer/util/paramassign.c
@@ -307,6 +307,56 @@ replace_outer_grouping(PlannerInfo *root
}
/*
+ * Generate a Param node to replace the given MergingFunc expression which is
+ * expected to be in the RETURNING list of an upper-level MERGE query. Record
+ * the need for the MergingFunc in the proper upper-level root->plan_params.
+ */
+Param *
+replace_outer_merging(PlannerInfo *root, MergingFunc *mf)
+{
+ Param *retval;
+ PlannerParamItem *pitem;
+ Oid ptype = exprType((Node *) mf);
+
+ Assert(root->parse->commandType != CMD_MERGE);
+
+ /*
+ * The parser should have ensured that the MergingFunc is in the RETURNING
+ * list of an upper-level MERGE query, so find that query.
+ */
+ do
+ {
+ root = root->parent_root;
+ if (root == NULL)
+ elog(ERROR, "MergingFunc found outside MERGE");
+ } while (root->parse->commandType != CMD_MERGE);
+
+ /*
+ * It does not seem worthwhile to try to de-duplicate references to outer
+ * MergingFunc expressions. Just make a new slot every time.
+ */
+ mf = copyObject(mf);
+
+ pitem = makeNode(PlannerParamItem);
+ pitem->item = (Node *) mf;
+ pitem->paramId = list_length(root->glob->paramExecTypes);
+ root->glob->paramExecTypes = lappend_oid(root->glob->paramExecTypes,
+ ptype);
+
+ root->plan_params = lappend(root->plan_params, pitem);
+
+ retval = makeNode(Param);
+ retval->paramkind = PARAM_EXEC;
+ retval->paramid = pitem->paramId;
+ retval->paramtype = ptype;
+ retval->paramtypmod = -1;
+ retval->paramcollid = InvalidOid;
+ retval->location = mf->location;
+
+ return retval;
+}
+
+/*
* Generate a Param node to replace the given Var,
* which is expected to come from some upper NestLoop plan node.
* Record the need for the Var in root->curOuterParams.
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
new file mode 100644
index 06fc8ce..f2b32ba
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -74,7 +74,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,
@@ -553,7 +552,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;
@@ -980,7 +980,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;
@@ -2445,7 +2446,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
@@ -2539,10 +2541,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;
@@ -2559,7 +2562,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 3460fea..2b24b1b
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -694,7 +694,7 @@ static Node *makeRecursiveViewSelect(cha
BOOLEAN_P BOTH BREADTH BY
CACHE CALL CALLED CASCADE CASCADED CASE CAST CATALOG_P CHAIN CHAR_P
- CHARACTER CHARACTERISTICS CHECK CHECKPOINT CLASS CLOSE
+ CHARACTER CHARACTERISTICS CHECK CHECKPOINT CLASS CLAUSE_NUMBER CLOSE
CLUSTER COALESCE COLLATE COLLATION COLUMN COLUMNS COMMENT COMMENTS COMMIT
COMMITTED COMPRESSION CONCURRENTLY CONFIGURATION CONFLICT
CONNECTION CONSTRAINT CONSTRAINTS CONTENT_P CONTINUE_P CONVERSION_P COPY
@@ -732,7 +732,7 @@ static Node *makeRecursiveViewSelect(cha
LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL
LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOGGED
- MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE METHOD
+ MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE MERGING METHOD
MINUTE_P MINVALUE MODE MONTH_P MOVE
NAME_P NAMES NATIONAL NATURAL NCHAR NEW NEXT NFC NFD NFKC NFKD NO NONE
@@ -12361,6 +12361,7 @@ MergeStmt:
USING table_ref
ON a_expr
merge_when_list
+ returning_clause
{
MergeStmt *m = makeNode(MergeStmt);
@@ -12369,6 +12370,7 @@ MergeStmt:
m->sourceRelation = $6;
m->joinCondition = $8;
m->mergeWhenClauses = $9;
+ m->returningList = $10;
$$ = (Node *) m;
}
@@ -15782,6 +15784,24 @@ func_expr_common_subexpr:
n->location = @1;
$$ = (Node *) n;
}
+ | MERGING '(' ACTION ')'
+ {
+ MergingFunc *m = makeNode(MergingFunc);
+
+ m->mfop = MERGING_ACTION;
+ m->mftype = TEXTOID;
+ m->location = @1;
+ $$ = (Node *) m;
+ }
+ | MERGING '(' CLAUSE_NUMBER ')'
+ {
+ MergingFunc *m = makeNode(MergingFunc);
+
+ m->mfop = MERGING_CLAUSE_NUMBER;
+ m->mftype = INT4OID;
+ m->location = @1;
+ $$ = (Node *) m;
+ }
;
@@ -17160,6 +17180,7 @@ unreserved_keyword:
| CHARACTERISTICS
| CHECKPOINT
| CLASS
+ | CLAUSE_NUMBER
| CLOSE
| CLUSTER
| COLUMNS
@@ -17479,6 +17500,7 @@ col_name_keyword:
| JSON_SCALAR
| JSON_SERIALIZE
| LEAST
+ | MERGING
| NATIONAL
| NCHAR
| NONE
@@ -17697,6 +17719,7 @@ bare_label_keyword:
| CHECK
| CHECKPOINT
| CLASS
+ | CLAUSE_NUMBER
| CLOSE
| CLUSTER
| COALESCE
@@ -17868,6 +17891,7 @@ bare_label_keyword:
| MATERIALIZED
| MAXVALUE
| MERGE
+ | MERGING
| METHOD
| MINVALUE
| MODE
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
new file mode 100644
index 7b211a7..f143310
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -467,6 +467,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:
@@ -914,6 +915,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 3c88c9a..6826d4f
--- 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 9300c7b..ab7e564
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -54,6 +54,7 @@ static Node *transformAExprDistinct(Pars
static Node *transformAExprNullIf(ParseState *pstate, A_Expr *a);
static Node *transformAExprIn(ParseState *pstate, A_Expr *a);
static Node *transformAExprBetween(ParseState *pstate, A_Expr *a);
+static Node *transformMergingFunc(ParseState *pstate, MergingFunc *f);
static Node *transformBoolExpr(ParseState *pstate, BoolExpr *a);
static Node *transformFuncCall(ParseState *pstate, FuncCall *fn);
static Node *transformMultiAssignRef(ParseState *pstate, MultiAssignRef *maref);
@@ -227,6 +228,10 @@ transformExprRecurse(ParseState *pstate,
result = transformGroupingFunc(pstate, (GroupingFunc *) expr);
break;
+ case T_MergingFunc:
+ result = transformMergingFunc(pstate, (MergingFunc *) expr);
+ break;
+
case T_NamedArgExpr:
{
NamedArgExpr *na = (NamedArgExpr *) expr;
@@ -541,6 +546,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:
@@ -1354,6 +1360,32 @@ transformAExprBetween(ParseState *pstate
}
static Node *
+transformMergingFunc(ParseState *pstate, MergingFunc *f)
+{
+ /*
+ * All we need to do is check that we're in the RETURNING list of a MERGE
+ * command. If so, we just return the node as-is.
+ */
+ if (pstate->p_expr_kind != EXPR_KIND_MERGE_RETURNING)
+ {
+ ParseState *parent_pstate = pstate->parentParseState;
+
+ while (parent_pstate &&
+ parent_pstate->p_expr_kind != EXPR_KIND_MERGE_RETURNING)
+ parent_pstate = parent_pstate->parentParseState;
+
+ if (!parent_pstate ||
+ parent_pstate->p_expr_kind != EXPR_KIND_MERGE_RETURNING)
+ ereport(ERROR,
+ errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("MERGING() can only be used in the RETURNING list of a MERGE command"),
+ parser_errposition(pstate, f->location));
+ }
+
+ return (Node *) f;
+}
+
+static Node *
transformBoolExpr(ParseState *pstate, BoolExpr *a)
{
List *args = NIL;
@@ -1767,6 +1799,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:
@@ -3115,6 +3148,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 fdb3e6d..0cbc950
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -2599,6 +2599,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 5f6a683..55936bb
--- 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.
@@ -254,6 +258,7 @@ transformMergeStmt(ParseState *pstate, M
MergeAction *action;
action = makeNode(MergeAction);
+ action->index = foreach_current_index(l) + 1;
action->commandType = mergeWhenClause->commandType;
action->matched = mergeWhenClause->matched;
@@ -390,9 +395,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 34a0ec5..60b7e4f
--- a/src/backend/parser/parse_relation.c
+++ b/src/backend/parser/parse_relation.c
@@ -2346,9 +2346,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/parser/parse_target.c b/src/backend/parser/parse_target.c
new file mode 100644
index 0cd904f..eab78f4
--- a/src/backend/parser/parse_target.c
+++ b/src/backend/parser/parse_target.c
@@ -1807,6 +1807,20 @@ FigureColnameInternal(Node *node, char *
/* make GROUPING() act like a regular function */
*name = "grouping";
return 2;
+ case T_MergingFunc:
+ switch (((MergingFunc *) node)->mfop)
+ {
+ case MERGING_ACTION:
+ *name = "action";
+ return 2;
+ case MERGING_CLAUSE_NUMBER:
+ *name = "clause_number";
+ return 2;
+ default:
+ elog(ERROR, "unrecognized MergingFuncOp: %d",
+ (int) ((MergingFunc *) node)->mfop);
+ }
+ break;
case T_SubLink:
switch (((SubLink *) node)->subLinkType)
{
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
new file mode 100644
index f60b34d..d719d07
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -3632,9 +3632,9 @@ RewriteQuery(Query *parsetree, List *rew
ListCell *lc1;
/*
- * First, recursively process any insert/update/delete statements in WITH
- * clauses. (We have to do this first because the WITH clauses may get
- * copied into rule actions below.)
+ * First, recursively process any insert/update/delete/merge statements in
+ * WITH clauses. (We have to do this first because the WITH clauses may
+ * get copied into rule actions below.)
*/
foreach(lc1, parsetree->cteList)
{
@@ -3659,7 +3659,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/rewrite/rowsecurity.c b/src/backend/rewrite/rowsecurity.c
new file mode 100644
index 94268a2..65ce579
--- a/src/backend/rewrite/rowsecurity.c
+++ b/src/backend/rewrite/rowsecurity.c
@@ -395,10 +395,10 @@ get_row_security_policies(Query *root, R
* on the final action we take.
*
* We already fetched the SELECT policies above, to check existing rows,
- * but we must also check that new rows created by UPDATE actions are
- * visible, if SELECT rights are required for this relation. We don't do
- * this for INSERT actions, since an INSERT command would only do this
- * check if it had a RETURNING list, and MERGE does not support RETURNING.
+ * but we must also check that new rows created by INSERT/UPDATE actions
+ * are visible, if SELECT rights are required. For INSERT actions, we only
+ * do this if RETURNING is specified, to be consistent with a plain INSERT
+ * command, which can only require SELECT rights when RETURNING is used.
*
* 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
@@ -420,6 +420,8 @@ get_row_security_policies(Query *root, R
List *merge_delete_restrictive_policies;
List *merge_insert_permissive_policies;
List *merge_insert_restrictive_policies;
+ List *merge_select_permissive_policies = NIL;
+ List *merge_select_restrictive_policies = NIL;
/*
* Fetch the UPDATE policies and set them up to execute on the
@@ -457,9 +459,6 @@ get_row_security_policies(Query *root, R
*/
if (perminfo->requiredPerms & ACL_SELECT)
{
- List *merge_select_permissive_policies;
- List *merge_select_restrictive_policies;
-
get_policies_for_relation(rel, CMD_SELECT, user_id,
&merge_select_permissive_policies,
&merge_select_restrictive_policies);
@@ -508,6 +507,21 @@ get_row_security_policies(Query *root, R
withCheckOptions,
hasSubLinks,
false);
+
+ /*
+ * Add ALL/SELECT policies as WCO_RLS_INSERT_CHECK WCOs, to ensure
+ * that the inserted row is visible when executing an INSERT action,
+ * if RETURNING is specified and SELECT rights are required for this
+ * relation.
+ */
+ if (perminfo->requiredPerms & ACL_SELECT && root->returningList)
+ add_with_check_options(rel, rt_index,
+ WCO_RLS_INSERT_CHECK,
+ merge_select_permissive_policies,
+ merge_select_restrictive_policies,
+ withCheckOptions,
+ hasSubLinks,
+ true);
}
table_close(rel, NoLock);
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
new file mode 100644
index 8de821f..f84a943
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -2145,11 +2145,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/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
new file mode 100644
index 0b2a164..6775f91
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -7197,8 +7197,13 @@ get_merge_query_def(Query *query, depars
appendStringInfoString(buf, "DO NOTHING");
}
- /* No RETURNING support in MERGE yet */
- Assert(query->returningList == NIL);
+ /* Add RETURNING if present */
+ if (query->returningList)
+ {
+ appendContextKeyword(context, " RETURNING",
+ -PRETTYINDENT_STD, PRETTYINDENT_STD, 1);
+ get_target_list(query->returningList, context, NULL, colNamesVisible);
+ }
}
@@ -8298,6 +8303,7 @@ isSimpleNode(Node *node, Node *parentNod
case T_Aggref:
case T_GroupingFunc:
case T_WindowFunc:
+ case T_MergingFunc:
case T_FuncExpr:
case T_JsonConstructorExpr:
/* function-like: name(..) or name[..] */
@@ -8652,6 +8658,27 @@ get_rule_expr(Node *node, deparse_contex
get_windowfunc_expr((WindowFunc *) node, context);
break;
+ case T_MergingFunc:
+ {
+ MergingFunc *mexpr = (MergingFunc *) node;
+
+ appendStringInfoString(buf, "MERGING(");
+ switch (mexpr->mfop)
+ {
+ case MERGING_ACTION:
+ appendStringInfoString(buf, "ACTION");
+ break;
+ case MERGING_CLAUSE_NUMBER:
+ appendStringInfoString(buf, "CLAUSE_NUMBER");
+ break;
+ default:
+ elog(ERROR, "unrecognized MergingFuncOp: %d",
+ (int) mexpr->mfop);
+ }
+ appendStringInfoChar(buf, ')');
+ }
+ break;
+
case T_SubscriptingRef:
{
SubscriptingRef *sbsref = (SubscriptingRef *) node;
diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c
new file mode 100644
index 76e01b0..2830bde
--- a/src/bin/psql/common.c
+++ b/src/bin/psql/common.c
@@ -983,13 +983,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/executor/execExpr.h b/src/include/executor/execExpr.h
new file mode 100644
index a20c539..a24aa65
--- a/src/include/executor/execExpr.h
+++ b/src/include/executor/execExpr.h
@@ -241,6 +241,7 @@ typedef enum ExprEvalOp
EEOP_AGGREF,
EEOP_GROUPING_FUNC,
EEOP_WINDOW_FUNC,
+ EEOP_MERGING_FUNC,
EEOP_SUBPLAN,
/* aggregation related nodes */
@@ -623,6 +624,12 @@ typedef struct ExprEvalStep
WindowFuncExprState *wfstate;
} window_func;
+ /* for EEOP_MERGING_FUNC */
+ struct
+ {
+ MergingFuncOp mfop; /* the MERGING() operation to perform */
+ } merging_func;
+
/* for EEOP_SUBPLAN */
struct
{
@@ -806,6 +813,8 @@ extern void ExecEvalJsonConstructor(Expr
ExprContext *econtext);
extern void ExecEvalJsonIsPredicate(ExprState *state, ExprEvalStep *op);
extern void ExecEvalGroupingFunc(ExprState *state, ExprEvalStep *op);
+extern void ExecEvalMergingFunc(ExprState *state, ExprEvalStep *op,
+ ExprContext *econtext);
extern void ExecEvalSubPlan(ExprState *state, ExprEvalStep *op,
ExprContext *econtext);
extern void ExecEvalWholeRowVar(ExprState *state, ExprEvalStep *op,
diff --git a/src/include/executor/spi.h b/src/include/executor/spi.h
new file mode 100644
index c13e1f6..48b8773
--- a/src/include/executor/spi.h
+++ b/src/include/executor/spi.h
@@ -97,6 +97,7 @@ typedef struct _SPI_plan *SPIPlanPtr;
#define SPI_OK_REL_UNREGISTER 16
#define SPI_OK_TD_REGISTER 17
#define SPI_OK_MERGE 18
+#define SPI_OK_MERGE_RETURNING 19
#define SPI_OPT_NONATOMIC (1 << 0)
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
new file mode 100644
index 561fdd9..75c4fde
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1318,6 +1318,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 b3181f3..89447ca
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -1927,6 +1927,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/nodes/primnodes.h b/src/include/nodes/primnodes.h
new file mode 100644
index 4a15460..97806f5
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -572,6 +572,39 @@ typedef struct WindowFunc
} WindowFunc;
/*
+ * MergingFunc
+ *
+ * A MergingFunc is a MERGING(...) expression that can only appear in the
+ * RETURNING list of a MERGE command. It returns information about the
+ * currently executing merge action. Possible operations are:
+ *
+ * MERGING_ACTION:
+ * Return the command string of the current merge action ("INSERT",
+ * "UPDATE" or "DELETE").
+ *
+ * MERGING_CLAUSE_NUMBER:
+ * Return the 1-based index of the current merge WHEN clause.
+ */
+typedef enum MergingFuncOp
+{
+ MERGING_ACTION,
+ MERGING_CLAUSE_NUMBER,
+} MergingFuncOp;
+
+typedef struct MergingFunc
+{
+ Expr xpr;
+ /* operation to perform */
+ MergingFuncOp mfop;
+ /* type Oid of result */
+ Oid mftype pg_node_attr(query_jumble_ignore);
+ /* OID of collation, or InvalidOid if none */
+ Oid mfcollid pg_node_attr(query_jumble_ignore);
+ /* token location, or -1 if unknown */
+ int location;
+} MergingFunc;
+
+/*
* SubscriptingRef: describes a subscripting operation over a container
* (array, etc).
*
@@ -1735,6 +1768,7 @@ typedef struct BooleanTest
typedef struct MergeAction
{
NodeTag type;
+ int index; /* 1-based index of the clause */
bool matched; /* true=MATCHED, false=NOT MATCHED */
CmdType commandType; /* INSERT/UPDATE/DELETE/DO NOTHING */
/* OVERRIDING clause */
diff --git a/src/include/optimizer/paramassign.h b/src/include/optimizer/paramassign.h
new file mode 100644
index 501f728..1e95c8f
--- a/src/include/optimizer/paramassign.h
+++ b/src/include/optimizer/paramassign.h
@@ -20,6 +20,7 @@ extern Param *replace_outer_placeholderv
PlaceHolderVar *phv);
extern Param *replace_outer_agg(PlannerInfo *root, Aggref *agg);
extern Param *replace_outer_grouping(PlannerInfo *root, GroupingFunc *grp);
+extern Param *replace_outer_merging(PlannerInfo *root, MergingFunc *mf);
extern Param *replace_nestloop_param_var(PlannerInfo *root, Var *var);
extern Param *replace_nestloop_param_placeholdervar(PlannerInfo *root,
PlaceHolderVar *phv);
diff --git a/src/include/parser/analyze.h b/src/include/parser/analyze.h
new file mode 100644
index 5ecdd10..28b66fc
--- 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/kwlist.h b/src/include/parser/kwlist.h
new file mode 100644
index 2331aca..9c5d6ea
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -80,6 +80,7 @@ PG_KEYWORD("characteristics", CHARACTERI
PG_KEYWORD("check", CHECK, RESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("checkpoint", CHECKPOINT, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("class", CLASS, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("clause_number", CLAUSE_NUMBER, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("close", CLOSE, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("cluster", CLUSTER, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("coalesce", COALESCE, COL_NAME_KEYWORD, BARE_LABEL)
@@ -266,6 +267,7 @@ PG_KEYWORD("matched", MATCHED, UNRESERVE
PG_KEYWORD("materialized", MATERIALIZED, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("maxvalue", MAXVALUE, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("merge", MERGE, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("merging", MERGING, COL_NAME_KEYWORD, BARE_LABEL)
PG_KEYWORD("method", METHOD, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("minute", MINUTE_P, UNRESERVED_KEYWORD, AS_LABEL)
PG_KEYWORD("minvalue", MINVALUE, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
new file mode 100644
index 99d6515..5b781d8
--- 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/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
new file mode 100644
index 6d16913..51de094
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -4267,9 +4267,9 @@ exec_stmt_execsql(PLpgSQL_execstate *est
/*
* If we have INTO, then we only need one row back ... but if we have INTO
* STRICT or extra check too_many_rows, ask for two rows, so that we can
- * verify the statement returns only one. INSERT/UPDATE/DELETE are always
- * treated strictly. Without INTO, just run the statement to completion
- * (tcount = 0).
+ * verify the statement returns only one. INSERT/UPDATE/DELETE/MERGE are
+ * always treated strictly. Without INTO, just run the statement to
+ * completion (tcount = 0).
*
* We could just ask for two rows always when using INTO, but there are
* some cases where demanding the extra row costs significant time, eg by
@@ -4307,10 +4307,11 @@ exec_stmt_execsql(PLpgSQL_execstate *est
case SPI_OK_INSERT:
case SPI_OK_UPDATE:
case SPI_OK_DELETE:
+ case SPI_OK_MERGE:
case SPI_OK_INSERT_RETURNING:
case SPI_OK_UPDATE_RETURNING:
case SPI_OK_DELETE_RETURNING:
- case SPI_OK_MERGE:
+ case SPI_OK_MERGE_RETURNING:
Assert(stmt->mod_stmt);
exec_set_found(estate, (SPI_processed != 0));
break;
@@ -4489,10 +4490,11 @@ exec_stmt_dynexecute(PLpgSQL_execstate *
case SPI_OK_INSERT:
case SPI_OK_UPDATE:
case SPI_OK_DELETE:
+ case SPI_OK_MERGE:
case SPI_OK_INSERT_RETURNING:
case SPI_OK_UPDATE_RETURNING:
case SPI_OK_DELETE_RETURNING:
- case SPI_OK_MERGE:
+ case SPI_OK_MERGE_RETURNING:
case SPI_OK_UTILITY:
case SPI_OK_REWRITTEN:
break;
diff --git a/src/pl/tcl/pltcl.c b/src/pl/tcl/pltcl.c
new file mode 100644
index 6187e15..18d14a2
--- a/src/pl/tcl/pltcl.c
+++ b/src/pl/tcl/pltcl.c
@@ -2456,6 +2456,7 @@ pltcl_process_SPI_result(Tcl_Interp *int
case SPI_OK_INSERT_RETURNING:
case SPI_OK_DELETE_RETURNING:
case SPI_OK_UPDATE_RETURNING:
+ case SPI_OK_MERGE_RETURNING:
/*
* Process the tuples we got
diff --git a/src/test/regress/expected/merge.out b/src/test/regress/expected/merge.out
new file mode 100644
index f87905f..8baa0f4
--- 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;
@@ -1256,7 +1256,7 @@ BEGIN;
MERGE INTO sq_target
USING v
ON tid = sid
-WHEN MATCHED AND tid > 2 THEN
+WHEN MATCHED AND tid >= 2 THEN
UPDATE SET balance = balance + delta
WHEN NOT MATCHED THEN
INSERT (balance, tid) VALUES (balance + delta, sid)
@@ -1271,7 +1271,7 @@ INSERT INTO sq_source (sid, balance, del
MERGE INTO sq_target t
USING v
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)
@@ -1295,7 +1295,7 @@ WITH targq AS (
MERGE INTO sq_target t
USING v
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)
@@ -1304,20 +1304,191 @@ WHEN MATCHED AND tid < 2 THEN
ROLLBACK;
-- RETURNING
BEGIN;
-INSERT INTO sq_source (sid, balance, delta) VALUES (-1, -1, -10);
+CREATE TABLE merge_actions(action text, abbrev text);
+INSERT INTO merge_actions VALUES ('INSERT', 'ins'), ('UPDATE', 'upd'), ('DELETE', 'del');
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)
+ 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 *;
- ^
+ DELETE
+RETURNING MERGING(CLAUSE_NUMBER) AS clause,
+ (SELECT abbrev FROM merge_actions
+ WHERE action = MERGING(ACTION)) AS action,
+ t.*,
+ CASE MERGING(ACTION)
+ WHEN 'INSERT' THEN 'Inserted '||t
+ WHEN 'UPDATE' THEN 'Added '||delta||' to balance'
+ WHEN 'DELETE' THEN 'Removed '||t
+ END AS description;
+ clause | action | tid | balance | description
+--------+--------+-----+---------+---------------------
+ 3 | del | 1 | 100 | Removed (1,100)
+ 1 | upd | 2 | 220 | Added 20 to balance
+ 2 | ins | 4 | 40 | Inserted (4,40)
+(3 rows)
+
+ROLLBACK;
+-- error when using MERGING() outside MERGE
+SELECT MERGING(ACTION) FROM sq_target;
+ERROR: MERGING() can only be used in the RETURNING list of a MERGE command
+LINE 1: SELECT MERGING(ACTION) FROM sq_target;
+ ^
+UPDATE sq_target SET balance = balance + 1 RETURNING MERGING(CLAUSE_NUMBER);
+ERROR: MERGING() can only be used in the RETURNING list of a MERGE command
+LINE 1: ...ATE sq_target SET balance = balance + 1 RETURNING MERGING(CL...
+ ^
+-- RETURNING in CTEs
+CREATE TABLE sq_target_merge_log (tid integer NOT NULL, last_change text);
+INSERT INTO sq_target_merge_log VALUES (1, 'Original value');
+BEGIN;
+WITH m AS (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 MERGING(CLAUSE_NUMBER), MERGING(ACTION), t.*,
+ CASE MERGING(ACTION)
+ WHEN 'INSERT' THEN 'Inserted '||t
+ WHEN 'UPDATE' THEN 'Added '||delta||' to balance'
+ WHEN 'DELETE' THEN 'Removed '||t
+ END AS description
+), m2 AS (
+ MERGE INTO sq_target_merge_log l
+ USING m
+ ON l.tid = m.tid
+ WHEN MATCHED THEN
+ UPDATE SET last_change = description
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (m.tid, description)
+ RETURNING action, MERGING(ACTION) AS log_action, l.*
+)
+SELECT * FROM m2;
+ action | log_action | tid | last_change
+--------+------------+-----+---------------------
+ DELETE | UPDATE | 1 | Removed (1,100)
+ UPDATE | INSERT | 2 | Added 20 to balance
+ INSERT | INSERT | 4 | Inserted (4,40)
+(3 rows)
+
+SELECT * FROM sq_target_merge_log ORDER BY tid;
+ tid | last_change
+-----+---------------------
+ 1 | Removed (1,100)
+ 2 | Added 20 to balance
+ 4 | Inserted (4,40)
+(3 rows)
+
+ROLLBACK;
+-- COPY (MERGE ... RETURNING) TO ...
+BEGIN;
+COPY (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 MERGING(ACTION), t.*
+) TO stdout;
+DELETE 1 100
+UPDATE 2 220
+INSERT 4 40
+ROLLBACK;
+-- SQL function with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT action text, OUT tid int, OUT new_balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING MERGING(ACTION), t.*;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT action, tid, new_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ action | tid | new_balance
+--------+-----+-------------
+ DELETE | 1 | 100
+ UPDATE | 3 | 320
+ INSERT | 4 | 110
+(3 rows)
+
+ROLLBACK;
+-- SQL SRF with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_sq_source_into_sq_target()
+RETURNS TABLE (action text, tid int, balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 MERGING(ACTION), t.*;
+$$;
+SELECT * FROM merge_sq_source_into_sq_target();
+ action | tid | balance
+--------+-----+---------
+ DELETE | 1 | 100
+ UPDATE | 2 | 220
+ INSERT | 4 | 40
+(3 rows)
+
+ROLLBACK;
+-- PL/pgSQL function with MERGE ... RETURNING ... INTO
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT r_action text, OUT r_tid int, OUT r_balance int)
+LANGUAGE plpgsql AS
+$$
+BEGIN
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING MERGING(ACTION), t.* INTO r_action, r_tid, r_balance;
+END;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT r_action, r_tid, r_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ r_action | r_tid | r_balance
+----------+-------+-----------
+ DELETE | 1 | 100
+ UPDATE | 3 | 320
+ INSERT | 4 | 110
+(3 rows)
+
ROLLBACK;
-- EXPLAIN
CREATE TABLE ex_mtarget (a int, b int)
@@ -1514,7 +1685,7 @@ SELECT * FROM sq_target WHERE tid = 1;
(1 row)
ROLLBACK;
-DROP TABLE sq_target, sq_source CASCADE;
+DROP TABLE sq_target, sq_target_merge_log, sq_source CASCADE;
NOTICE: drop cascades to view v
CREATE TABLE pa_target (tid integer, balance float, val text)
PARTITION BY LIST (tid);
@@ -1640,6 +1811,32 @@ SELECT * FROM pa_target ORDER BY tid;
(14 rows)
ROLLBACK;
+-- update partition key to partition not initially scanned
+BEGIN;
+MERGE INTO pa_target t
+ USING pa_source s
+ ON t.tid = s.sid AND t.tid = 1
+ WHEN MATCHED THEN
+ UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
+ RETURNING MERGING(ACTION), MERGING(CLAUSE_NUMBER), t.*;
+ action | clause_number | tid | balance | val
+--------+---------------+-----+---------+--------------------------
+ UPDATE | 1 | 2 | 110 | initial updated by merge
+(1 row)
+
+SELECT * FROM pa_target ORDER BY tid;
+ tid | balance | val
+-----+---------+--------------------------
+ 2 | 110 | initial updated by merge
+ 3 | 300 | initial
+ 5 | 500 | initial
+ 7 | 700 | initial
+ 9 | 900 | initial
+ 11 | 1100 | initial
+ 13 | 1300 | initial
+(7 rows)
+
+ROLLBACK;
DROP TABLE pa_target CASCADE;
-- The target table is partitioned in the same way, but this time by attaching
-- partitions which have columns in different order, dropped columns etc.
@@ -1889,7 +2086,21 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge');
+ INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge')
+ RETURNING MERGING(ACTION), MERGING(CLAUSE_NUMBER), t.*;
+ action | clause_number | logts | tid | balance | val
+--------+---------------+--------------------------+-----+---------+--------------------------
+ UPDATE | 1 | Tue Jan 31 00:00:00 2017 | 1 | 110 | initial updated by merge
+ UPDATE | 1 | Tue Feb 28 00:00:00 2017 | 2 | 220 | initial updated by merge
+ INSERT | 2 | Sun Jan 15 00:00:00 2017 | 3 | 30 | inserted by merge
+ UPDATE | 1 | Tue Jan 31 00:00:00 2017 | 4 | 440 | initial updated by merge
+ UPDATE | 1 | Tue Feb 28 00:00:00 2017 | 5 | 550 | initial updated by merge
+ INSERT | 2 | Sun Jan 15 00:00:00 2017 | 6 | 60 | inserted by merge
+ UPDATE | 1 | Tue Jan 31 00:00:00 2017 | 7 | 770 | initial updated by merge
+ UPDATE | 1 | Tue Feb 28 00:00:00 2017 | 8 | 880 | initial updated by merge
+ INSERT | 2 | Sun Jan 15 00:00:00 2017 | 9 | 90 | inserted by merge
+(9 rows)
+
SELECT * FROM pa_target ORDER BY tid;
logts | tid | balance | val
--------------------------+-----+---------+--------------------------
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
new file mode 100644
index 6988128..3fd4fab
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -2302,6 +2302,35 @@ WHEN MATCHED THEN
UPDATE SET dnotes = dnotes || ' notes added by merge8 '
WHEN NOT MATCHED THEN
INSERT VALUES (13, 44, 1, 'regress_rls_bob', 'new manga');
+SELECT * FROM document WHERE did = 13;
+ did | cid | dlevel | dauthor | dtitle | dnotes
+-----+-----+--------+---------+--------+--------
+(0 rows)
+
+-- but not OK if RETURNING is used
+MERGE INTO document d
+USING (SELECT 14 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge9 '
+WHEN NOT MATCHED THEN
+ INSERT VALUES (14, 44, 1, 'regress_rls_bob', 'new manga')
+RETURNING *;
+ERROR: new row violates row-level security policy for table "document"
+-- but OK if new row is visible
+MERGE INTO document d
+USING (SELECT 14 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge10 '
+WHEN NOT MATCHED THEN
+ INSERT VALUES (14, 11, 1, 'regress_rls_bob', 'new novel')
+RETURNING *;
+ sdid | did | cid | dlevel | dauthor | dtitle | dnotes
+------+-----+-----+--------+-----------------+-----------+--------
+ 14 | 14 | 11 | 1 | regress_rls_bob | new novel |
+(1 row)
+
RESET SESSION AUTHORIZATION;
-- drop the restrictive SELECT policy so that we can look at the
-- final state of the table
@@ -2325,7 +2354,8 @@ SELECT * FROM document;
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 notes added by merge7
13 | 44 | 1 | regress_rls_bob | new manga |
-(15 rows)
+ 14 | 11 | 1 | regress_rls_bob | new novel |
+(16 rows)
--
-- ROLE/GROUP
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
new file mode 100644
index 55f2e95..57fb335
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -3623,7 +3623,8 @@ MERGE INTO rule_merge2 t USING (SELECT 1
-- test deparsing
CREATE TABLE sf_target(id int, data text, filling int[]);
CREATE FUNCTION merge_sf_test()
- RETURNS void
+ RETURNS TABLE(action text, clause_number int,
+ a int, b text, id int, data text, filling int[])
LANGUAGE sql
BEGIN ATOMIC
MERGE INTO sf_target t
@@ -3660,11 +3661,13 @@ WHEN NOT MATCHED
VALUES (s.a, s.b, DEFAULT)
WHEN NOT MATCHED
THEN INSERT (filling[1], id)
- VALUES (s.a, s.a);
+ VALUES (s.a, s.a)
+RETURNING
+ merging(action), merging(clause_number) AS clause_number, *;
END;
\sf merge_sf_test
CREATE OR REPLACE FUNCTION public.merge_sf_test()
- RETURNS void
+ RETURNS TABLE(action text, clause_number integer, a integer, b text, id integer, data text, filling integer[])
LANGUAGE sql
BEGIN ATOMIC
MERGE INTO sf_target t
@@ -3701,7 +3704,14 @@ BEGIN ATOMIC
VALUES (s.a, s.b, DEFAULT)
WHEN NOT MATCHED
THEN INSERT (filling[1], id)
- VALUES (s.a, s.a);
+ VALUES (s.a, s.a)
+ RETURNING MERGING(ACTION) AS action,
+ MERGING(CLAUSE_NUMBER) AS clause_number,
+ s.a,
+ s.b,
+ t.id,
+ t.data,
+ t.filling;
END
DROP FUNCTION merge_sf_test;
DROP TABLE sf_target;
diff --git a/src/test/regress/expected/with.out b/src/test/regress/expected/with.out
new file mode 100644
index 7d796ea..4666873
--- a/src/test/regress/expected/with.out
+++ b/src/test/regress/expected/with.out
@@ -3493,6 +3493,16 @@ SELECT * FROM t;
ERROR: WITH query "t" does not have a RETURNING clause
LINE 4: SELECT * FROM t;
^
+-- RETURNING tries to return its own output
+WITH RECURSIVE t(action, a) AS (
+ MERGE INTO y USING (VALUES (11)) v(a) ON y.a = v.a
+ WHEN NOT MATCHED THEN INSERT VALUES (v.a)
+ RETURNING MERGING(ACTION), (SELECT a FROM t)
+)
+SELECT * FROM t;
+ERROR: recursive query "t" must not contain data-modifying statements
+LINE 1: WITH RECURSIVE t(action, a) AS (
+ ^
-- data-modifying WITH allowed only at the top level
SELECT * FROM (
WITH t AS (UPDATE y SET a=a+1 RETURNING *)
diff --git a/src/test/regress/sql/merge.sql b/src/test/regress/sql/merge.sql
new file mode 100644
index 66cb75a..daa9dde
--- 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
@@ -816,7 +816,7 @@ BEGIN;
MERGE INTO sq_target
USING v
ON tid = sid
-WHEN MATCHED AND tid > 2 THEN
+WHEN MATCHED AND tid >= 2 THEN
UPDATE SET balance = balance + delta
WHEN NOT MATCHED THEN
INSERT (balance, tid) VALUES (balance + delta, sid)
@@ -829,7 +829,7 @@ INSERT INTO sq_source (sid, balance, del
MERGE INTO sq_target t
USING v
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)
@@ -847,7 +847,7 @@ WITH targq AS (
MERGE INTO sq_target t
USING v
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)
@@ -857,17 +857,146 @@ ROLLBACK;
-- RETURNING
BEGIN;
-INSERT INTO sq_source (sid, balance, delta) VALUES (-1, -1, -10);
+CREATE TABLE merge_actions(action text, abbrev text);
+INSERT INTO merge_actions VALUES ('INSERT', 'ins'), ('UPDATE', 'upd'), ('DELETE', 'del');
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)
+ INSERT (balance, tid) VALUES (balance + delta, sid)
WHEN MATCHED AND tid < 2 THEN
- DELETE
-RETURNING *;
+ DELETE
+RETURNING MERGING(CLAUSE_NUMBER) AS clause,
+ (SELECT abbrev FROM merge_actions
+ WHERE action = MERGING(ACTION)) AS action,
+ t.*,
+ CASE MERGING(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 MERGING() outside MERGE
+SELECT MERGING(ACTION) FROM sq_target;
+UPDATE sq_target SET balance = balance + 1 RETURNING MERGING(CLAUSE_NUMBER);
+
+-- RETURNING in CTEs
+CREATE TABLE sq_target_merge_log (tid integer NOT NULL, last_change text);
+INSERT INTO sq_target_merge_log VALUES (1, 'Original value');
+BEGIN;
+WITH m AS (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 MERGING(CLAUSE_NUMBER), MERGING(ACTION), t.*,
+ CASE MERGING(ACTION)
+ WHEN 'INSERT' THEN 'Inserted '||t
+ WHEN 'UPDATE' THEN 'Added '||delta||' to balance'
+ WHEN 'DELETE' THEN 'Removed '||t
+ END AS description
+), m2 AS (
+ MERGE INTO sq_target_merge_log l
+ USING m
+ ON l.tid = m.tid
+ WHEN MATCHED THEN
+ UPDATE SET last_change = description
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (m.tid, description)
+ RETURNING action, MERGING(ACTION) AS log_action, l.*
+)
+SELECT * FROM m2;
+SELECT * FROM sq_target_merge_log ORDER BY tid;
+ROLLBACK;
+
+-- COPY (MERGE ... RETURNING) TO ...
+BEGIN;
+COPY (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 MERGING(ACTION), t.*
+) TO stdout;
+ROLLBACK;
+
+-- SQL function with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT action text, OUT tid int, OUT new_balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING MERGING(ACTION), t.*;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT action, tid, new_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ROLLBACK;
+
+-- SQL SRF with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_sq_source_into_sq_target()
+RETURNS TABLE (action text, tid int, balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 MERGING(ACTION), t.*;
+$$;
+SELECT * FROM merge_sq_source_into_sq_target();
+ROLLBACK;
+
+-- PL/pgSQL function with MERGE ... RETURNING ... INTO
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT r_action text, OUT r_tid int, OUT r_balance int)
+LANGUAGE plpgsql AS
+$$
+BEGIN
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING MERGING(ACTION), t.* INTO r_action, r_tid, r_balance;
+END;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT r_action, r_tid, r_balance FROM merge_into_sq_target(sid, balance, delta)) m;
ROLLBACK;
-- EXPLAIN
@@ -966,7 +1095,7 @@ WHEN MATCHED THEN
SELECT * FROM sq_target WHERE tid = 1;
ROLLBACK;
-DROP TABLE sq_target, sq_source CASCADE;
+DROP TABLE sq_target, sq_target_merge_log, sq_source CASCADE;
CREATE TABLE pa_target (tid integer, balance float, val text)
PARTITION BY LIST (tid);
@@ -1033,6 +1162,17 @@ SELECT merge_func();
SELECT * FROM pa_target ORDER BY tid;
ROLLBACK;
+-- update partition key to partition not initially scanned
+BEGIN;
+MERGE INTO pa_target t
+ USING pa_source s
+ ON t.tid = s.sid AND t.tid = 1
+ WHEN MATCHED THEN
+ UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
+ RETURNING MERGING(ACTION), MERGING(CLAUSE_NUMBER), t.*;
+SELECT * FROM pa_target ORDER BY tid;
+ROLLBACK;
+
DROP TABLE pa_target CASCADE;
-- The target table is partitioned in the same way, but this time by attaching
@@ -1209,7 +1349,8 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge');
+ INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge')
+ RETURNING MERGING(ACTION), MERGING(CLAUSE_NUMBER), t.*;
SELECT * FROM pa_target ORDER BY tid;
ROLLBACK;
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
new file mode 100644
index dec7340..1d5ed0a
--- a/src/test/regress/sql/rowsecurity.sql
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -992,6 +992,27 @@ WHEN MATCHED THEN
UPDATE SET dnotes = dnotes || ' notes added by merge8 '
WHEN NOT MATCHED THEN
INSERT VALUES (13, 44, 1, 'regress_rls_bob', 'new manga');
+SELECT * FROM document WHERE did = 13;
+
+-- but not OK if RETURNING is used
+MERGE INTO document d
+USING (SELECT 14 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge9 '
+WHEN NOT MATCHED THEN
+ INSERT VALUES (14, 44, 1, 'regress_rls_bob', 'new manga')
+RETURNING *;
+
+-- but OK if new row is visible
+MERGE INTO document d
+USING (SELECT 14 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge10 '
+WHEN NOT MATCHED THEN
+ INSERT VALUES (14, 11, 1, 'regress_rls_bob', 'new novel')
+RETURNING *;
RESET SESSION AUTHORIZATION;
-- drop the restrictive SELECT policy so that we can look at the
diff --git a/src/test/regress/sql/rules.sql b/src/test/regress/sql/rules.sql
new file mode 100644
index 93aff4e..4b15a13
--- a/src/test/regress/sql/rules.sql
+++ b/src/test/regress/sql/rules.sql
@@ -1281,7 +1281,8 @@ MERGE INTO rule_merge2 t USING (SELECT 1
CREATE TABLE sf_target(id int, data text, filling int[]);
CREATE FUNCTION merge_sf_test()
- RETURNS void
+ RETURNS TABLE(action text, clause_number int,
+ a int, b text, id int, data text, filling int[])
LANGUAGE sql
BEGIN ATOMIC
MERGE INTO sf_target t
@@ -1318,7 +1319,9 @@ WHEN NOT MATCHED
VALUES (s.a, s.b, DEFAULT)
WHEN NOT MATCHED
THEN INSERT (filling[1], id)
- VALUES (s.a, s.a);
+ VALUES (s.a, s.a)
+RETURNING
+ merging(action), merging(clause_number) AS clause_number, *;
END;
\sf merge_sf_test
diff --git a/src/test/regress/sql/with.sql b/src/test/regress/sql/with.sql
new file mode 100644
index f8a213e..1d82d4a
--- a/src/test/regress/sql/with.sql
+++ b/src/test/regress/sql/with.sql
@@ -1591,6 +1591,14 @@ WITH t AS (
)
SELECT * FROM t;
+-- RETURNING tries to return its own output
+WITH RECURSIVE t(action, a) AS (
+ MERGE INTO y USING (VALUES (11)) v(a) ON y.a = v.a
+ WHEN NOT MATCHED THEN INSERT VALUES (v.a)
+ RETURNING MERGING(ACTION), (SELECT a FROM t)
+)
+SELECT * FROM t;
+
-- data-modifying WITH allowed only at the top level
SELECT * FROM (
WITH t AS (UPDATE y SET a=a+1 RETURNING *)
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
new file mode 100644
index 29fd1ca..82e1747
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1570,6 +1570,8 @@ MergePath
MergeScanSelCache
MergeStmt
MergeWhenClause
+MergingFunc
+MergingFuncOp
MetaCommand
MinMaxAggInfo
MinMaxAggPath
On Fri, Jan 19, 2024 at 1:44 AM Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
Thanks for reviewing. Updated patch attached.
The wider question is whether people are happy with the overall
approach this patch now takes, and the new MERGING() function and
MergingFunc node.
one minor white space issue:
git diff --check
doc/src/sgml/func.sgml:22482: trailing whitespace.
+ action | clause_number | product_id | in_stock | quantity
@@ -3838,7 +3904,7 @@ ExecModifyTable(PlanState *pstate)
}
slot = ExecGetUpdateNewTuple(resultRelInfo, context.planSlot,
oldSlot);
- context.relaction = NULL;
+ node->mt_merge_action = NULL;
I wonder what's the purpose of setting node->mt_merge_action to null ?
I add `node->mt_merge_action = NULL;` at the end of each branch in
`switch (operation)`.
All the tests still passed.
Other than this question, this patch is very good.
On Sun, 28 Jan 2024 at 23:50, jian he <jian.universality@gmail.com> wrote:
one minor white space issue:
git diff --check
doc/src/sgml/func.sgml:22482: trailing whitespace.
+ action | clause_number | product_id | in_stock | quantity
Ah, well spotted! I'm not in the habit of running git diff --check.
@@ -3838,7 +3904,7 @@ ExecModifyTable(PlanState *pstate) } slot = ExecGetUpdateNewTuple(resultRelInfo, context.planSlot, oldSlot); - context.relaction = NULL; + node->mt_merge_action = NULL;I wonder what's the purpose of setting node->mt_merge_action to null ?
I add `node->mt_merge_action = NULL;` at the end of each branch in
`switch (operation)`.
All the tests still passed.
Good question. It was necessary to set it to NULL there, because code
under ExecUpdate() reads it, and context.relaction would otherwise be
uninitialised. Now though, mtstate->mt_merge_action is automatically
initialised to NULL when the ModifyTableState node is first built, and
only the MERGE code sets it to non-NULL, so it's no longer necessary
to set it to NULL for other types of operation, because it will never
become non-NULL unless mtstate->operation is CMD_MERGE. So we can
safely remove that line.
Having said that, it seems a bit ugly to be relying on mt_merge_action
in so many places anyway. The places that test if it's non-NULL should
more logically be testing whether mtstate->operation is CMD_MERGE.
Doing that, reduces the number of places in nodeModifyTable.c that
read mt_merge_action down to one, and that one place only reads it
after testing that mtstate->operation is CMD_MERGE, which seems neater
and safer.
Regards,
Dean
Attachments:
support-merge-returning-v15.patchtext/x-patch; charset=US-ASCII; name=support-merge-returning-v15.patchDownload
diff --git a/doc/src/sgml/dml.sgml b/doc/src/sgml/dml.sgml
new file mode 100644
index cbbc5e2..3d95bdb
--- a/doc/src/sgml/dml.sgml
+++ b/doc/src/sgml/dml.sgml
@@ -283,10 +283,15 @@ DELETE FROM products;
<secondary>RETURNING</secondary>
</indexterm>
+ <indexterm zone="dml-returning">
+ <primary>MERGE</primary>
+ <secondary>RETURNING</secondary>
+ </indexterm>
+
<para>
Sometimes it is useful to obtain data from modified rows while they are
being manipulated. The <command>INSERT</command>, <command>UPDATE</command>,
- and <command>DELETE</command> commands all have an
+ <command>DELETE</command>, and <command>MERGE</command> commands all have an
optional <literal>RETURNING</literal> clause that supports this. Use
of <literal>RETURNING</literal> avoids performing an extra database query to
collect the data, and is especially valuable when it would otherwise be
@@ -339,6 +344,21 @@ DELETE FROM products
</programlisting>
</para>
+ <para>
+ In a <command>MERGE</command>, the data available to <literal>RETURNING</literal> is
+ the content of the source row plus the content of the inserted, updated, or
+ deleted target row. Since it is quite common for the source and target to
+ have many of the same columns, specifying <literal>RETURNING *</literal>
+ can lead to a lot of duplicated columns, so it is often more useful to
+ qualify it so as to return just the source or target row. For example:
+<programlisting>
+MERGE INTO products p USING new_products n ON p.product_no = n.product_no
+ WHEN NOT MATCHED THEN INSERT VALUES (n.product_no, n.name, n.price)
+ WHEN MATCHED THEN UPDATE SET name = n.name, price = n.price
+ RETURNING p.*;
+</programlisting>
+ </para>
+
<para>
If there are triggers (<xref linkend="triggers"/>) on the target table,
the data available to <literal>RETURNING</literal> is the row as modified by
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
new file mode 100644
index 6788ba8..a26a06d
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -22417,6 +22417,84 @@ SELECT count(*) FROM sometable;
</sect1>
+ <sect1 id="functions-merge-support">
+ <title>Merge Support Functions</title>
+
+ <indexterm>
+ <primary>MERGE</primary>
+ <secondary>RETURNING</secondary>
+ </indexterm>
+
+ <indexterm>
+ <primary>MERGING</primary>
+ </indexterm>
+
+ <para>
+ <productname>PostgreSQL</productname> includes one merge support function
+ that may be used in the <literal>RETURNING</literal> list of a
+ <xref linkend="sql-merge"/> command to return additional information about
+ the action taken for each row:
+<synopsis>
+<function id="function-merging">MERGING</function> ( <parameter>property</parameter> )
+</synopsis>
+ The <parameter>property</parameter> key word specifies what to return:
+
+ <variablelist>
+ <varlistentry>
+ <term><literal>ACTION</literal></term>
+ <listitem>
+ <para>
+ The merge action command executed for the current row. This will be
+ <literal>'INSERT'</literal>, <literal>'UPDATE'</literal>, or
+ <literal>'DELETE'</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>CLAUSE_NUMBER</literal></term>
+ <listitem>
+ <para>
+ The 1-based index of the <literal>WHEN</literal> clause executed for
+ the current row.
+ </para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </para>
+
+ <para>
+ The following example illustrates how this may be used to determine
+ which actions were performed for each row affected by the
+ <command>MERGE</command> command:
+<screen><![CDATA[
+MERGE INTO products p
+ USING stock s ON p.product_id = s.product_id
+ WHEN MATCHED AND s.quantity > 0 THEN
+ UPDATE SET in_stock = true, quantity = s.quantity
+ WHEN MATCHED THEN
+ UPDATE SET in_stock = false, quantity = 0
+ WHEN NOT MATCHED THEN
+ INSERT (product_id, in_stock, quantity)
+ VALUES (s.product_id, true, s.quantity)
+ RETURNING MERGING(ACTION), MERGING(CLAUSE_NUMBER), p.*;
+
+ action | clause_number | product_id | in_stock | quantity
+--------+---------------+------------+----------+----------
+ UPDATE | 1 | 1001 | t | 50
+ UPDATE | 2 | 1002 | f | 0
+ INSERT | 3 | 1003 | t | 10
+]]></screen>
+ </para>
+
+ <para>
+ Note that this function can only be used in the <literal>RETURNING</literal>
+ list of a <command>MERGE</command> command. It is an error to use it in any
+ other part of a query.
+ </para>
+
+ </sect1>
+
<sect1 id="functions-subquery">
<title>Subquery Expressions</title>
diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml
new file mode 100644
index 8c2f114..a81c17a
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -1442,9 +1442,9 @@
to a <glossterm linkend="glossary-client">client</glossterm> upon the
completion of an <acronym>SQL</acronym> command, usually a
<command>SELECT</command> but it can be an
- <command>INSERT</command>, <command>UPDATE</command>, or
- <command>DELETE</command> command if the <literal>RETURNING</literal>
- clause is specified.
+ <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command> command if the
+ <literal>RETURNING</literal> clause is specified.
</para>
<para>
The fact that a result set is a relation means that a query can be used
diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml
new file mode 100644
index c2b9c6a..406834e
--- a/doc/src/sgml/plpgsql.sgml
+++ b/doc/src/sgml/plpgsql.sgml
@@ -1043,8 +1043,8 @@ INSERT INTO mytable VALUES (1,'one'), (2
</para>
<para>
- If the command does return rows (for example <command>SELECT</command>,
- or <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>
+ If the command does return rows (for example <command>SELECT</command>, or
+ <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command>
with <literal>RETURNING</literal>), there are two ways to proceed.
When the command will return at most one row, or you only care about
the first row of output, write the command as usual but add
@@ -1172,6 +1172,7 @@ SELECT <replaceable>select_expressions</
INSERT ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
UPDATE ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
DELETE ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
+MERGE ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
</synopsis>
where <replaceable>target</replaceable> can be a record variable, a row
@@ -1182,8 +1183,8 @@ DELETE ... RETURNING <replaceable>expres
<literal>INTO</literal> clause) just as described above,
and the plan is cached in the same way.
This works for <command>SELECT</command>,
- <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command> with
- <literal>RETURNING</literal>, and certain utility commands
+ <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command>
+ with <literal>RETURNING</literal>, and certain utility commands
that return row sets, such as <command>EXPLAIN</command>.
Except for the <literal>INTO</literal> clause, the SQL command is the same
as it would be written outside <application>PL/pgSQL</application>.
@@ -1259,7 +1260,7 @@ END;
</para>
<para>
- For <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command> with
+ For <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command> with
<literal>RETURNING</literal>, <application>PL/pgSQL</application> reports
an error for more than one returned row, even when
<literal>STRICT</literal> is not specified. This is because there
diff --git a/doc/src/sgml/queries.sgml b/doc/src/sgml/queries.sgml
new file mode 100644
index 648b283..372cce1
--- a/doc/src/sgml/queries.sgml
+++ b/doc/src/sgml/queries.sgml
@@ -2063,9 +2063,10 @@ SELECT <replaceable>select_list</replace
Table Expressions or <acronym>CTE</acronym>s, can be thought of as defining
temporary tables that exist just for one query. Each auxiliary statement
in a <literal>WITH</literal> clause can be a <command>SELECT</command>,
- <command>INSERT</command>, <command>UPDATE</command>, or <command>DELETE</command>; and the
+ <command>INSERT</command>, <command>UPDATE</command>, <command>DELETE</command>,
+ or <command>MERGE</command>; and the
<literal>WITH</literal> clause itself is attached to a primary statement that can
- be a <command>SELECT</command>, <command>INSERT</command>, <command>UPDATE</command>,
+ also be a <command>SELECT</command>, <command>INSERT</command>, <command>UPDATE</command>,
<command>DELETE</command>, or <command>MERGE</command>.
</para>
@@ -2599,8 +2600,8 @@ SELECT * FROM w AS w1 JOIN w AS w2 ON w1
<title>Data-Modifying Statements in <literal>WITH</literal></title>
<para>
- You can use most data-modifying statements (<command>INSERT</command>,
- <command>UPDATE</command>, or <command>DELETE</command>, but not
+ You can use data-modifying statements (<command>INSERT</command>,
+ <command>UPDATE</command>, <command>DELETE</command>, or
<command>MERGE</command>) in <literal>WITH</literal>. This
allows you to perform several different operations in the same query.
An example is:
diff --git a/doc/src/sgml/ref/copy.sgml b/doc/src/sgml/ref/copy.sgml
new file mode 100644
index 21a5c4a..d64db80
--- a/doc/src/sgml/ref/copy.sgml
+++ b/doc/src/sgml/ref/copy.sgml
@@ -123,16 +123,19 @@ COPY { <replaceable class="parameter">ta
A <link linkend="sql-select"><command>SELECT</command></link>,
<link linkend="sql-values"><command>VALUES</command></link>,
<link linkend="sql-insert"><command>INSERT</command></link>,
- <link linkend="sql-update"><command>UPDATE</command></link>, or
- <link linkend="sql-delete"><command>DELETE</command></link> command whose results are to be
- copied. Note that parentheses are required around the query.
+ <link linkend="sql-update"><command>UPDATE</command></link>,
+ <link linkend="sql-delete"><command>DELETE</command></link>, or
+ <link linkend="sql-merge"><command>MERGE</command></link> command
+ whose results are to be copied. Note that parentheses are required
+ around the query.
</para>
<para>
- For <command>INSERT</command>, <command>UPDATE</command> and
- <command>DELETE</command> queries a <literal>RETURNING</literal> clause
- must be provided, and the target relation must not have a conditional
- rule, nor an <literal>ALSO</literal> rule, nor an
- <literal>INSTEAD</literal> rule that expands to multiple statements.
+ For <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, and <command>MERGE</command> queries a
+ <literal>RETURNING</literal> clause must be provided, and the target
+ relation must not have a conditional rule, nor an
+ <literal>ALSO</literal> rule, nor an <literal>INSTEAD</literal> rule
+ that expands to multiple statements.
</para>
</listitem>
</varlistentry>
diff --git a/doc/src/sgml/ref/merge.sgml b/doc/src/sgml/ref/merge.sgml
new file mode 100644
index 655f7dc..44b3fa4
--- a/doc/src/sgml/ref/merge.sgml
+++ b/doc/src/sgml/ref/merge.sgml
@@ -25,6 +25,7 @@ PostgreSQL documentation
MERGE INTO [ ONLY ] <replaceable class="parameter">target_table_name</replaceable> [ * ] [ [ AS ] <replaceable class="parameter">target_alias</replaceable> ]
USING <replaceable class="parameter">data_source</replaceable> ON <replaceable class="parameter">join_condition</replaceable>
<replaceable class="parameter">when_clause</replaceable> [...]
+[ RETURNING * | <replaceable class="parameter">output_expression</replaceable> [ [ AS ] <replaceable class="parameter">output_name</replaceable> ] [, ...] ]
<phrase>where <replaceable class="parameter">data_source</replaceable> is:</phrase>
@@ -95,6 +96,18 @@ DELETE
</para>
<para>
+ The optional <literal>RETURNING</literal> clause causes <command>MERGE</command>
+ to compute and return value(s) based on each row inserted, updated, or
+ deleted. Any expression using the source or target table's columns, or
+ the <xref linkend="function-merging"/> function can be computed. When an
+ <command>INSERT</command> or <command>UPDATE</command> action is performed,
+ the new values of the target table's columns are used. When a
+ <command>DELETE</command> is performed, the old values of the target table's
+ columns are used. The syntax of the <literal>RETURNING</literal> list is
+ identical to that of the output list of <command>SELECT</command>.
+ </para>
+
+ <para>
There is no separate <literal>MERGE</literal> privilege.
If you specify an update action, you must have the
<literal>UPDATE</literal> privilege on the column(s)
@@ -125,6 +138,18 @@ DELETE
<variablelist>
<varlistentry>
+ <term><replaceable class="parameter">with_query</replaceable></term>
+ <listitem>
+ <para>
+ The <literal>WITH</literal> clause allows you to specify one or more
+ subqueries that can be referenced by name in the <command>MERGE</command>
+ query. See <xref linkend="queries-with"/> and <xref linkend="sql-select"/>
+ for details.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
<term><replaceable class="parameter">target_table_name</replaceable></term>
<listitem>
<para>
@@ -399,13 +424,30 @@ DELETE
</varlistentry>
<varlistentry>
- <term><replaceable class="parameter">with_query</replaceable></term>
+ <term><replaceable class="parameter">output_expression</replaceable></term>
<listitem>
<para>
- The <literal>WITH</literal> clause allows you to specify one or more
- subqueries that can be referenced by name in the <command>MERGE</command>
- query. See <xref linkend="queries-with"/> and <xref linkend="sql-select"/>
- for details.
+ An expression to be computed and returned by the <command>MERGE</command>
+ command after each row is merged. The expression can use any columns of
+ the source or target tables, or the <xref linkend="function-merging"/>
+ function to return additional information about the action executed.
+ </para>
+ <para>
+ Writing <literal>*</literal> will return all columns from the source
+ table, followed by all columns from the target table. Often this will
+ lead to a lot of duplication, since it is common for the source and
+ target tables to have a lot of the same columns. This can be avoided by
+ qualifying the <literal>*</literal> with the name or alias of the source
+ or target table.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><replaceable class="parameter">output_name</replaceable></term>
+ <listitem>
+ <para>
+ A name to use for a returned column.
</para>
</listitem>
</varlistentry>
@@ -428,6 +470,13 @@ MERGE <replaceable class="parameter">tot
were changed in any way.
</para>
+ <para>
+ If the <command>MERGE</command> command contains a <literal>RETURNING</literal>
+ clause, the result will be similar to that of a <command>SELECT</command>
+ statement containing the columns and values defined in the
+ <literal>RETURNING</literal> list, computed over the row(s) inserted, updated,
+ or deleted by the command.
+ </para>
</refsect1>
<refsect1>
@@ -544,13 +593,6 @@ MERGE <replaceable class="parameter">tot
</para>
<para>
- There is no <literal>RETURNING</literal> clause with
- <command>MERGE</command>. Actions of <command>INSERT</command>,
- <command>UPDATE</command> and <command>DELETE</command> cannot contain
- <literal>RETURNING</literal> or <literal>WITH</literal> clauses.
- </para>
-
- <para>
When <command>MERGE</command> is run concurrently with other commands
that modify the target table, the usual transaction isolation rules
apply; see <xref linkend="transaction-iso"/> for an explanation
@@ -602,7 +644,8 @@ WHEN NOT MATCHED THEN
<para>
Attempt to insert a new stock item along with the quantity of stock. If
the item already exists, instead update the stock count of the existing
- item. Don't allow entries that have zero stock.
+ item. Don't allow entries that have zero stock. Return details of all
+ changes made.
<programlisting>
MERGE INTO wines w
USING wine_stock_changes s
@@ -612,7 +655,8 @@ WHEN NOT MATCHED AND s.stock_delta > 0 T
WHEN MATCHED AND w.stock + s.stock_delta > 0 THEN
UPDATE SET stock = w.stock + s.stock_delta
WHEN MATCHED THEN
- DELETE;
+ DELETE
+RETURNING MERGING(ACTION), w.*;
</programlisting>
The <literal>wine_stock_changes</literal> table might be, for example, a
@@ -627,8 +671,9 @@ WHEN MATCHED THEN
This command conforms to the <acronym>SQL</acronym> standard.
</para>
<para>
- The <literal>WITH</literal> clause and <literal>DO NOTHING</literal>
- action are extensions to the <acronym>SQL</acronym> standard.
+ The <literal>WITH</literal> clause, <literal>DO NOTHING</literal> action,
+ and <literal>RETURNING</literal> clause are extensions to the
+ <acronym>SQL</acronym> standard.
</para>
</refsect1>
</refentry>
diff --git a/doc/src/sgml/ref/select.sgml b/doc/src/sgml/ref/select.sgml
new file mode 100644
index 9917df7..066aed4
--- a/doc/src/sgml/ref/select.sgml
+++ b/doc/src/sgml/ref/select.sgml
@@ -74,7 +74,7 @@ SELECT [ ALL | DISTINCT [ ON ( <replacea
<phrase>and <replaceable class="parameter">with_query</replaceable> is:</phrase>
- <replaceable class="parameter">with_query_name</replaceable> [ ( <replaceable class="parameter">column_name</replaceable> [, ...] ) ] AS [ [ NOT ] MATERIALIZED ] ( <replaceable class="parameter">select</replaceable> | <replaceable class="parameter">values</replaceable> | <replaceable class="parameter">insert</replaceable> | <replaceable class="parameter">update</replaceable> | <replaceable class="parameter">delete</replaceable> )
+ <replaceable class="parameter">with_query_name</replaceable> [ ( <replaceable class="parameter">column_name</replaceable> [, ...] ) ] AS [ [ NOT ] MATERIALIZED ] ( <replaceable class="parameter">select</replaceable> | <replaceable class="parameter">values</replaceable> | <replaceable class="parameter">insert</replaceable> | <replaceable class="parameter">update</replaceable> | <replaceable class="parameter">delete</replaceable> | <replaceable class="parameter">merge</replaceable> )
[ SEARCH { BREADTH | DEPTH } FIRST BY <replaceable>column_name</replaceable> [, ...] SET <replaceable>search_seq_col_name</replaceable> ]
[ CYCLE <replaceable>column_name</replaceable> [, ...] SET <replaceable>cycle_mark_col_name</replaceable> [ TO <replaceable>cycle_mark_value</replaceable> DEFAULT <replaceable>cycle_mark_default</replaceable> ] USING <replaceable>cycle_path_col_name</replaceable> ]
@@ -230,10 +230,10 @@ TABLE [ ONLY ] <replaceable class="param
The subqueries effectively act as temporary tables or views
for the duration of the primary query.
Each subquery can be a <command>SELECT</command>, <command>TABLE</command>, <command>VALUES</command>,
- <command>INSERT</command>, <command>UPDATE</command> or
- <command>DELETE</command> statement.
+ <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command> statement.
When writing a data-modifying statement (<command>INSERT</command>,
- <command>UPDATE</command> or <command>DELETE</command>) in
+ <command>UPDATE</command>, <command>DELETE</command>, or <command>MERGE</command>) in
<literal>WITH</literal>, it is usual to include a <literal>RETURNING</literal> clause.
It is the output of <literal>RETURNING</literal>, <emphasis>not</emphasis> the underlying
table that the statement modifies, that forms the temporary table that is
@@ -2184,7 +2184,8 @@ SELECT 2+2;
<para>
<productname>PostgreSQL</productname> allows <command>INSERT</command>,
- <command>UPDATE</command>, and <command>DELETE</command> to be used as <literal>WITH</literal>
+ <command>UPDATE</command>, <command>DELETE</command>, and
+ <command>MERGE</command> to be used as <literal>WITH</literal>
queries. This is not found in the SQL standard.
</para>
</refsect2>
diff --git a/doc/src/sgml/rowtypes.sgml b/doc/src/sgml/rowtypes.sgml
new file mode 100644
index 4d86f97..bbeac84
--- a/doc/src/sgml/rowtypes.sgml
+++ b/doc/src/sgml/rowtypes.sgml
@@ -348,7 +348,7 @@ SELECT m.* FROM some_table, LATERAL myfu
column expansion of this kind when it appears at the top level of
a <link linkend="queries-select-lists"><command>SELECT</command> output
list</link>, a <link linkend="dml-returning"><literal>RETURNING</literal>
- list</link> in <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>,
+ list</link> in <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command>,
a <link linkend="queries-values"><literal>VALUES</literal> clause</link>, or
a <link linkend="sql-syntax-row-constructors">row constructor</link>.
In all other contexts (including when nested inside one of those
diff --git a/doc/src/sgml/spi.sgml b/doc/src/sgml/spi.sgml
new file mode 100644
index 47f4b5b..bb37786
--- a/doc/src/sgml/spi.sgml
+++ b/doc/src/sgml/spi.sgml
@@ -301,8 +301,9 @@ SPI_execute("INSERT INTO foo SELECT * FR
is returned in the global variable <varname>SPI_processed</varname>.
If the return value of the function is <symbol>SPI_OK_SELECT</symbol>,
<symbol>SPI_OK_INSERT_RETURNING</symbol>,
- <symbol>SPI_OK_DELETE_RETURNING</symbol>, or
- <symbol>SPI_OK_UPDATE_RETURNING</symbol>,
+ <symbol>SPI_OK_DELETE_RETURNING</symbol>,
+ <symbol>SPI_OK_UPDATE_RETURNING</symbol>, or
+ <symbol>SPI_OK_MERGE_RETURNING</symbol>,
then you can use the
global pointer <literal>SPITupleTable *SPI_tuptable</literal> to
access the result rows. Some utility commands (such as
@@ -472,6 +473,15 @@ typedef struct SPITupleTable
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><symbol>SPI_OK_MERGE_RETURNING</symbol></term>
+ <listitem>
+ <para>
+ if a <command>MERGE RETURNING</command> was executed
+ </para>
+ </listitem>
+ </varlistentry>
<varlistentry>
<term><symbol>SPI_OK_UTILITY</symbol></term>
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
new file mode 100644
index 8a79ad0..7d05369
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -177,7 +177,8 @@
statements separated by semicolons. A semicolon after the last
statement is optional. Unless the function is declared to return
<type>void</type>, the last statement must be a <command>SELECT</command>,
- or an <command>INSERT</command>, <command>UPDATE</command>, or <command>DELETE</command>
+ or an <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command>
that has a <literal>RETURNING</literal> clause.
</para>
@@ -1236,8 +1237,9 @@ SELECT x, CASE WHEN x > 0 THEN genera
<note>
<para>
- If a function's last command is <command>INSERT</command>, <command>UPDATE</command>,
- or <command>DELETE</command> with <literal>RETURNING</literal>, that command will
+ If a function's last command is <command>INSERT</command>,
+ <command>UPDATE</command>, <command>DELETE</command>, or
+ <command>MERGE</command> with <literal>RETURNING</literal>, that command will
always be executed to completion, even if the function is not declared
with <literal>SETOF</literal> or the calling query does not fetch all the
result rows. Any extra rows produced by the <literal>RETURNING</literal>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
new file mode 100644
index cc0786c..e96067a
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -283,12 +283,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 d3dc3fc..c29fecd
--- 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 3181b11..c4c2fb5
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -1108,6 +1108,21 @@ ExecInitExprRec(Expr *node, ExprState *s
break;
}
+ case T_MergingFunc:
+ {
+ MergingFunc *mf_node = (MergingFunc *) node;
+
+ if (!state->parent ||
+ !IsA(state->parent, ModifyTableState) ||
+ ((ModifyTableState *) state->parent)->operation != CMD_MERGE)
+ elog(ERROR, "MergingFunc found in non-merge plan node");
+
+ scratch.opcode = EEOP_MERGING_FUNC;
+ scratch.d.merging_func.mfop = mf_node->mfop;
+ ExprEvalPushStep(state, &scratch);
+ break;
+ }
+
case T_SubscriptingRef:
{
SubscriptingRef *sbsref = (SubscriptingRef *) node;
diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
new file mode 100644
index 3f20f1d..4bcad29
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -485,6 +485,7 @@ ExecInterpExpr(ExprState *state, ExprCon
&&CASE_EEOP_AGGREF,
&&CASE_EEOP_GROUPING_FUNC,
&&CASE_EEOP_WINDOW_FUNC,
+ &&CASE_EEOP_MERGING_FUNC,
&&CASE_EEOP_SUBPLAN,
&&CASE_EEOP_AGG_STRICT_DESERIALIZE,
&&CASE_EEOP_AGG_DESERIALIZE,
@@ -1593,6 +1594,14 @@ ExecInterpExpr(ExprState *state, ExprCon
EEO_NEXT();
}
+ EEO_CASE(EEOP_MERGING_FUNC)
+ {
+ /* too complex/uncommon for an inline implementation */
+ ExecEvalMergingFunc(state, op, econtext);
+
+ EEO_NEXT();
+ }
+
EEO_CASE(EEOP_SUBPLAN)
{
/* too complex for an inline implementation */
@@ -4247,6 +4256,59 @@ ExecEvalGroupingFunc(ExprState *state, E
}
/*
+ * ExecEvalMergingFunc
+ *
+ * Returns information about the current MERGE action for its RETURNING list.
+ */
+void
+ExecEvalMergingFunc(ExprState *state, ExprEvalStep *op, ExprContext *econtext)
+{
+ MergingFuncOp mfop = op->d.merging_func.mfop;
+ ModifyTableState *mtstate = castNode(ModifyTableState, state->parent);
+ MergeActionState *relaction = mtstate->mt_merge_action;
+
+ if (!relaction)
+ elog(ERROR, "no merge action in progress");
+
+ switch (mfop)
+ {
+ case MERGING_ACTION:
+ /* Return the MERGE action ("INSERT", "UPDATE", or "DELETE") */
+ switch (relaction->mas_action->commandType)
+ {
+ case CMD_INSERT:
+ *op->resvalue = PointerGetDatum(cstring_to_text_with_len("INSERT", 6));
+ *op->resnull = false;
+ break;
+ case CMD_UPDATE:
+ *op->resvalue = PointerGetDatum(cstring_to_text_with_len("UPDATE", 6));
+ *op->resnull = false;
+ break;
+ case CMD_DELETE:
+ *op->resvalue = PointerGetDatum(cstring_to_text_with_len("DELETE", 6));
+ *op->resnull = false;
+ break;
+ case CMD_NOTHING:
+ elog(ERROR, "unexpected merge action: DO NOTHING");
+ break;
+ default:
+ elog(ERROR, "unrecognized commandType: %d",
+ (int) relaction->mas_action->commandType);
+ }
+ break;
+
+ case MERGING_CLAUSE_NUMBER:
+ /* Return the 1-based index of the MERGE action */
+ *op->resvalue = Int32GetDatum(relaction->mas_action->index);
+ *op->resnull = false;
+ break;
+
+ default:
+ elog(ERROR, "unrecognized MergingFuncOp: %d", (int) mfop);
+ }
+}
+
+/*
* Hand off evaluation of a subplan to nodeSubplan.c
*/
void
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
new file mode 100644
index b22040a..9c56a10
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -612,8 +612,8 @@ ExecInitPartitionInfo(ModifyTableState *
* Build the RETURNING projection for the partition. Note that we didn't
* build the returningList for partitions within the planner, but simple
* translation of varattnos will suffice. This only occurs for the INSERT
- * case or in the case of UPDATE tuple routing where we didn't find a
- * result rel to reuse.
+ * case or in the case of UPDATE/MERGE tuple routing where we didn't find
+ * a result rel to reuse.
*/
if (node && node->returningLists != NIL)
{
@@ -622,12 +622,14 @@ ExecInitPartitionInfo(ModifyTableState *
List *returningList;
/* See the comment above for WCO lists. */
- /* (except no RETURNING support for MERGE yet) */
Assert((node->operation == CMD_INSERT &&
list_length(node->returningLists) == 1 &&
list_length(node->resultRelations) == 1) ||
(node->operation == CMD_UPDATE &&
list_length(node->returningLists) ==
+ list_length(node->resultRelations)) ||
+ (node->operation == CMD_MERGE &&
+ list_length(node->returningLists) ==
list_length(node->resultRelations)));
/*
diff --git a/src/backend/executor/functions.c b/src/backend/executor/functions.c
new file mode 100644
index 0f811fd..7da02b0
--- a/src/backend/executor/functions.c
+++ b/src/backend/executor/functions.c
@@ -1660,8 +1660,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
@@ -1679,7 +1679,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;
@@ -1693,7 +1694,7 @@ check_sql_fn_retval(List *queryTreeLists
(errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
errmsg("return type mismatch in function declared to return %s",
format_type_be(rettype)),
- errdetail("Function's final statement must be SELECT or INSERT/UPDATE/DELETE RETURNING.")));
+ errdetail("Function's final statement must be SELECT or INSERT/UPDATE/DELETE/MERGE RETURNING.")));
return false; /* keep compiler quiet */
}
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
new file mode 100644
index 9fc5abf..b11eb7b
--- 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"
@@ -88,9 +87,6 @@ typedef struct ModifyTableContext
*/
TupleTableSlot *planSlot;
- /* MERGE specific */
- MergeActionState *relaction; /* MERGE action in progress */
-
/*
* Information about the changes that were made concurrently to a tuple
* being updated or deleted
@@ -153,13 +149,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);
/*
@@ -979,7 +976,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;
@@ -1833,7 +1830,7 @@ ExecCrossPartitionUpdate(ModifyTableCont
* additional rechecking, and might end up executing a different
* action entirely).
*/
- if (context->relaction != NULL)
+ if (mtstate->operation == CMD_MERGE)
return *tmresult == TM_Ok;
else if (TupIsNull(epqslot))
return true;
@@ -2075,7 +2072,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->operation == CMD_MERGE)
return result;
/*
@@ -2718,6 +2715,7 @@ static TupleTableSlot *
ExecMerge(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
ItemPointer tupleid, bool canSetTag)
{
+ TupleTableSlot *rslot = NULL;
bool matched;
/*-----
@@ -2765,18 +2763,18 @@ 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()
- * returned "false", indicating the previously MATCHED tuple no longer
- * matches.
+ * Deal with the NOT MATCHED case (either a NOT MATCHED tuple from the
+ * join, or a previously MATCHED tuple for which ExecMergeMatched() set
+ * "matched" to false, indicating that it no longer matches).
*/
if (!matched)
- ExecMergeNotMatched(context, resultRelInfo, canSetTag);
+ rslot = ExecMergeNotMatched(context, resultRelInfo, canSetTag);
- /* No RETURNING support yet */
- return NULL;
+ return rslot;
}
/*
@@ -2786,8 +2784,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.
*
@@ -2796,16 +2794,17 @@ 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 *newslot;
+ TupleTableSlot *rslot = NULL;
+ TupleTableSlot *newslot = NULL;
EState *estate = context->estate;
ExprContext *econtext = mtstate->ps.ps_ExprContext;
bool isNull;
@@ -2816,7 +2815,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
@@ -2895,12 +2897,15 @@ lmerge_matched:
*/
newslot = ExecProject(relaction->mas_proj);
- context->relaction = relaction;
+ mtstate->mt_merge_action = relaction;
if (!ExecUpdatePrologue(context, resultRelInfo,
tupleid, NULL, newslot, &result))
{
if (result == TM_Ok)
- return true; /* "do nothing" */
+ {
+ *matched = true;
+ return NULL; /* "do nothing" */
+ }
break; /* concurrent update/delete */
}
result = ExecUpdateAct(context, resultRelInfo, tupleid, NULL,
@@ -2911,12 +2916,13 @@ lmerge_matched:
* cross-partition update was done, then there's nothing else
* for us to do --- the UPDATE has been turned into a DELETE
* and an INSERT, and we must not perform any of the usual
- * post-update tasks.
+ * post-update tasks. Also, the RETURNING tuple (if any) has
+ * been projected, so we can just return that.
*/
if (updateCxt.crossPartUpdate)
{
mtstate->mt_merge_updated += 1;
- return true;
+ return context->cpUpdateReturningSlot;
}
if (result == TM_Ok && updateCxt.updated)
@@ -2928,12 +2934,15 @@ lmerge_matched:
break;
case CMD_DELETE:
- context->relaction = relaction;
+ mtstate->mt_merge_action = relaction;
if (!ExecDeletePrologue(context, resultRelInfo, tupleid,
NULL, NULL, &result))
{
if (result == TM_Ok)
- return true; /* "do nothing" */
+ {
+ *matched = true;
+ return NULL; /* "do nothing" */
+ }
break; /* concurrent update/delete */
}
result = ExecDeleteAct(context, resultRelInfo, tupleid, false);
@@ -2989,7 +2998,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:
{
@@ -3035,13 +3045,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
@@ -3076,7 +3092,8 @@ lmerge_matched:
* tuple already deleted; tell caller to run NOT
* MATCHED actions
*/
- return false;
+ *matched = false;
+ return NULL;
case TM_SelfModified:
@@ -3096,13 +3113,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;
}
}
@@ -3114,6 +3132,31 @@ 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, "unrecognized commandType: %d",
+ (int) commandType);
+ }
+ }
+
/*
* We've activated one of the WHEN clauses, so we don't search
* further. This is required behaviour, not an optimization.
@@ -3124,19 +3167,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;
/*
@@ -3186,10 +3232,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:
@@ -3205,6 +3251,8 @@ ExecMergeNotMatched(ModifyTableContext *
*/
break;
}
+
+ return rslot;
}
/*
@@ -3667,8 +3715,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");
@@ -3745,8 +3802,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");
@@ -3838,7 +3904,6 @@ ExecModifyTable(PlanState *pstate)
}
slot = ExecGetUpdateNewTuple(resultRelInfo, context.planSlot,
oldSlot);
- context.relaction = NULL;
/* Now apply the update. */
slot = ExecUpdate(&context, resultRelInfo, tupleid, oldtuple,
diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c
new file mode 100644
index d71484c..abb8b0d
--- a/src/backend/executor/spi.c
+++ b/src/backend/executor/spi.c
@@ -2033,6 +2033,8 @@ SPI_result_code_string(int code)
return "SPI_OK_TD_REGISTER";
case SPI_OK_MERGE:
return "SPI_OK_MERGE";
+ case SPI_OK_MERGE_RETURNING:
+ return "SPI_OK_MERGE_RETURNING";
}
/* Unrecognized code ... return something useful ... */
sprintf(buf, "Unrecognized SPI code %d", code);
@@ -2886,7 +2888,10 @@ _SPI_pquery(QueryDesc *queryDesc, bool f
res = SPI_OK_UPDATE;
break;
case CMD_MERGE:
- res = SPI_OK_MERGE;
+ if (queryDesc->plannedstmt->hasReturning)
+ res = SPI_OK_MERGE_RETURNING;
+ else
+ res = SPI_OK_MERGE;
break;
default:
return SPI_ERROR_OPUNKNOWN;
diff --git a/src/backend/jit/llvm/llvmjit_expr.c b/src/backend/jit/llvm/llvmjit_expr.c
new file mode 100644
index 0c44842..a5cd6fb
--- a/src/backend/jit/llvm/llvmjit_expr.c
+++ b/src/backend/jit/llvm/llvmjit_expr.c
@@ -1986,6 +1986,12 @@ llvm_compile_expr(ExprState *state)
break;
}
+ case EEOP_MERGING_FUNC:
+ build_EvalXFunc(b, mod, "ExecEvalMergingFunc",
+ v_state, op, v_econtext);
+ LLVMBuildBr(b, opblocks[opno + 1]);
+ break;
+
case EEOP_SUBPLAN:
build_EvalXFunc(b, mod, "ExecEvalSubPlan",
v_state, op, v_econtext);
diff --git a/src/backend/jit/llvm/llvmjit_types.c b/src/backend/jit/llvm/llvmjit_types.c
new file mode 100644
index 47c9daf..912d9ad
--- a/src/backend/jit/llvm/llvmjit_types.c
+++ b/src/backend/jit/llvm/llvmjit_types.c
@@ -155,6 +155,7 @@ void *referenced_functions[] =
ExecEvalFuncExprFusage,
ExecEvalFuncExprStrictFusage,
ExecEvalGroupingFunc,
+ ExecEvalMergingFunc,
ExecEvalMinMax,
ExecEvalNextValueExpr,
ExecEvalParamExec,
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
new file mode 100644
index e1a5bc7..2468063
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -67,6 +67,9 @@ exprType(const Node *expr)
case T_WindowFunc:
type = ((const WindowFunc *) expr)->wintype;
break;
+ case T_MergingFunc:
+ type = ((const MergingFunc *) expr)->mftype;
+ break;
case T_SubscriptingRef:
type = ((const SubscriptingRef *) expr)->refrestype;
break;
@@ -810,6 +813,9 @@ exprCollation(const Node *expr)
case T_WindowFunc:
coll = ((const WindowFunc *) expr)->wincollid;
break;
+ case T_MergingFunc:
+ coll = ((const MergingFunc *) expr)->mfcollid;
+ break;
case T_SubscriptingRef:
coll = ((const SubscriptingRef *) expr)->refcollid;
break;
@@ -1085,6 +1091,9 @@ exprSetCollation(Node *expr, Oid collati
case T_WindowFunc:
((WindowFunc *) expr)->wincollid = collation;
break;
+ case T_MergingFunc:
+ ((MergingFunc *) expr)->mfcollid = collation;
+ break;
case T_SubscriptingRef:
((SubscriptingRef *) expr)->refcollid = collation;
break;
@@ -1343,6 +1352,9 @@ exprLocation(const Node *expr)
/* function name should always be the first thing */
loc = ((const WindowFunc *) expr)->location;
break;
+ case T_MergingFunc:
+ loc = ((const MergingFunc *) expr)->location;
+ break;
case T_SubscriptingRef:
/* just use container argument's location */
loc = exprLocation((Node *) ((const SubscriptingRef *) expr)->refexpr);
@@ -2035,6 +2047,7 @@ expression_tree_walker_impl(Node *node,
case T_RangeTblRef:
case T_SortGroupClause:
case T_CTESearchClause:
+ case T_MergingFunc:
/* primitive node types with no expression subnodes */
break;
case T_WithCheckOption:
@@ -2869,6 +2882,7 @@ expression_tree_mutator_impl(Node *node,
case T_RangeTblRef:
case T_SortGroupClause:
case T_CTESearchClause:
+ case T_MergingFunc:
return (Node *) copyObject(node);
case T_WithCheckOption:
{
@@ -3833,6 +3847,7 @@ raw_expression_tree_walker_impl(Node *no
case T_ParamRef:
case T_A_Const:
case T_A_Star:
+ case T_MergingFunc:
/* primitive node types with no subnodes */
break;
case T_Alias:
@@ -4053,6 +4068,8 @@ raw_expression_tree_walker_impl(Node *no
return true;
if (WALK(stmt->mergeWhenClauses))
return true;
+ if (WALK(stmt->returningList))
+ return true;
if (WALK(stmt->withClause))
return true;
}
diff --git a/src/backend/optimizer/plan/subselect.c b/src/backend/optimizer/plan/subselect.c
new file mode 100644
index 3115d79..a3646c0
--- a/src/backend/optimizer/plan/subselect.c
+++ b/src/backend/optimizer/plan/subselect.c
@@ -1846,7 +1846,8 @@ convert_EXISTS_to_ANY(PlannerInfo *root,
/*
* Replace correlation vars (uplevel vars) with Params.
*
- * Uplevel PlaceHolderVars and aggregates are replaced, too.
+ * Uplevel PlaceHolderVars, aggregates, GROUPING() expressions, and MERGING()
+ * expressions are replaced, too.
*
* Note: it is critical that this runs immediately after SS_process_sublinks.
* Since we do not recurse into the arguments of uplevel PHVs and aggregates,
@@ -1900,6 +1901,11 @@ replace_correlation_vars_mutator(Node *n
if (((GroupingFunc *) node)->agglevelsup > 0)
return (Node *) replace_outer_grouping(root, (GroupingFunc *) node);
}
+ if (IsA(node, MergingFunc))
+ {
+ if (root->parse->commandType != CMD_MERGE)
+ return (Node *) replace_outer_merging(root, (MergingFunc *) node);
+ }
return expression_tree_mutator(node,
replace_correlation_vars_mutator,
(void *) root);
diff --git a/src/backend/optimizer/util/paramassign.c b/src/backend/optimizer/util/paramassign.c
new file mode 100644
index a58da7c..6ea6db2
--- a/src/backend/optimizer/util/paramassign.c
+++ b/src/backend/optimizer/util/paramassign.c
@@ -308,6 +308,56 @@ replace_outer_grouping(PlannerInfo *root
}
/*
+ * Generate a Param node to replace the given MergingFunc expression which is
+ * expected to be in the RETURNING list of an upper-level MERGE query. Record
+ * the need for the MergingFunc in the proper upper-level root->plan_params.
+ */
+Param *
+replace_outer_merging(PlannerInfo *root, MergingFunc *mf)
+{
+ Param *retval;
+ PlannerParamItem *pitem;
+ Oid ptype = exprType((Node *) mf);
+
+ Assert(root->parse->commandType != CMD_MERGE);
+
+ /*
+ * The parser should have ensured that the MergingFunc is in the RETURNING
+ * list of an upper-level MERGE query, so find that query.
+ */
+ do
+ {
+ root = root->parent_root;
+ if (root == NULL)
+ elog(ERROR, "MergingFunc found outside MERGE");
+ } while (root->parse->commandType != CMD_MERGE);
+
+ /*
+ * It does not seem worthwhile to try to de-duplicate references to outer
+ * MergingFunc expressions. Just make a new slot every time.
+ */
+ mf = copyObject(mf);
+
+ pitem = makeNode(PlannerParamItem);
+ pitem->item = (Node *) mf;
+ pitem->paramId = list_length(root->glob->paramExecTypes);
+ root->glob->paramExecTypes = lappend_oid(root->glob->paramExecTypes,
+ ptype);
+
+ root->plan_params = lappend(root->plan_params, pitem);
+
+ retval = makeNode(Param);
+ retval->paramkind = PARAM_EXEC;
+ retval->paramid = pitem->paramId;
+ retval->paramtype = ptype;
+ retval->paramtypmod = -1;
+ retval->paramcollid = InvalidOid;
+ retval->location = mf->location;
+
+ return retval;
+}
+
+/*
* Generate a Param node to replace the given Var,
* which is expected to come from some upper NestLoop plan node.
* Record the need for the Var in root->curOuterParams.
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
new file mode 100644
index dbdf6bf..44893d8
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -74,7 +74,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,
@@ -553,7 +552,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;
@@ -980,7 +980,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;
@@ -2445,7 +2446,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
@@ -2542,10 +2544,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;
@@ -2562,7 +2565,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 130f7fc..04fee0d
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -694,7 +694,7 @@ static Node *makeRecursiveViewSelect(cha
BOOLEAN_P BOTH BREADTH BY
CACHE CALL CALLED CASCADE CASCADED CASE CAST CATALOG_P CHAIN CHAR_P
- CHARACTER CHARACTERISTICS CHECK CHECKPOINT CLASS CLOSE
+ CHARACTER CHARACTERISTICS CHECK CHECKPOINT CLASS CLAUSE_NUMBER CLOSE
CLUSTER COALESCE COLLATE COLLATION COLUMN COLUMNS COMMENT COMMENTS COMMIT
COMMITTED COMPRESSION CONCURRENTLY CONFIGURATION CONFLICT
CONNECTION CONSTRAINT CONSTRAINTS CONTENT_P CONTINUE_P CONVERSION_P COPY
@@ -732,7 +732,7 @@ static Node *makeRecursiveViewSelect(cha
LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL
LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOGGED
- MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE METHOD
+ MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE MERGING METHOD
MINUTE_P MINVALUE MODE MONTH_P MOVE
NAME_P NAMES NATIONAL NATURAL NCHAR NEW NEXT NFC NFD NFKC NFKD NO NONE
@@ -12368,6 +12368,7 @@ MergeStmt:
USING table_ref
ON a_expr
merge_when_list
+ returning_clause
{
MergeStmt *m = makeNode(MergeStmt);
@@ -12376,6 +12377,7 @@ MergeStmt:
m->sourceRelation = $6;
m->joinCondition = $8;
m->mergeWhenClauses = $9;
+ m->returningList = $10;
$$ = (Node *) m;
}
@@ -15789,6 +15791,24 @@ func_expr_common_subexpr:
n->location = @1;
$$ = (Node *) n;
}
+ | MERGING '(' ACTION ')'
+ {
+ MergingFunc *m = makeNode(MergingFunc);
+
+ m->mfop = MERGING_ACTION;
+ m->mftype = TEXTOID;
+ m->location = @1;
+ $$ = (Node *) m;
+ }
+ | MERGING '(' CLAUSE_NUMBER ')'
+ {
+ MergingFunc *m = makeNode(MergingFunc);
+
+ m->mfop = MERGING_CLAUSE_NUMBER;
+ m->mftype = INT4OID;
+ m->location = @1;
+ $$ = (Node *) m;
+ }
;
@@ -17167,6 +17187,7 @@ unreserved_keyword:
| CHARACTERISTICS
| CHECKPOINT
| CLASS
+ | CLAUSE_NUMBER
| CLOSE
| CLUSTER
| COLUMNS
@@ -17486,6 +17507,7 @@ col_name_keyword:
| JSON_SCALAR
| JSON_SERIALIZE
| LEAST
+ | MERGING
| NATIONAL
| NCHAR
| NONE
@@ -17704,6 +17726,7 @@ bare_label_keyword:
| CHECK
| CHECKPOINT
| CLASS
+ | CLAUSE_NUMBER
| CLOSE
| CLUSTER
| COALESCE
@@ -17875,6 +17898,7 @@ bare_label_keyword:
| MATERIALIZED
| MAXVALUE
| MERGE
+ | MERGING
| METHOD
| MINVALUE
| MODE
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
new file mode 100644
index 7b211a7..f143310
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -467,6 +467,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:
@@ -914,6 +915,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 3c88c9a..6826d4f
--- 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 9300c7b..ab7e564
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -54,6 +54,7 @@ static Node *transformAExprDistinct(Pars
static Node *transformAExprNullIf(ParseState *pstate, A_Expr *a);
static Node *transformAExprIn(ParseState *pstate, A_Expr *a);
static Node *transformAExprBetween(ParseState *pstate, A_Expr *a);
+static Node *transformMergingFunc(ParseState *pstate, MergingFunc *f);
static Node *transformBoolExpr(ParseState *pstate, BoolExpr *a);
static Node *transformFuncCall(ParseState *pstate, FuncCall *fn);
static Node *transformMultiAssignRef(ParseState *pstate, MultiAssignRef *maref);
@@ -227,6 +228,10 @@ transformExprRecurse(ParseState *pstate,
result = transformGroupingFunc(pstate, (GroupingFunc *) expr);
break;
+ case T_MergingFunc:
+ result = transformMergingFunc(pstate, (MergingFunc *) expr);
+ break;
+
case T_NamedArgExpr:
{
NamedArgExpr *na = (NamedArgExpr *) expr;
@@ -541,6 +546,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:
@@ -1354,6 +1360,32 @@ transformAExprBetween(ParseState *pstate
}
static Node *
+transformMergingFunc(ParseState *pstate, MergingFunc *f)
+{
+ /*
+ * All we need to do is check that we're in the RETURNING list of a MERGE
+ * command. If so, we just return the node as-is.
+ */
+ if (pstate->p_expr_kind != EXPR_KIND_MERGE_RETURNING)
+ {
+ ParseState *parent_pstate = pstate->parentParseState;
+
+ while (parent_pstate &&
+ parent_pstate->p_expr_kind != EXPR_KIND_MERGE_RETURNING)
+ parent_pstate = parent_pstate->parentParseState;
+
+ if (!parent_pstate ||
+ parent_pstate->p_expr_kind != EXPR_KIND_MERGE_RETURNING)
+ ereport(ERROR,
+ errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("MERGING() can only be used in the RETURNING list of a MERGE command"),
+ parser_errposition(pstate, f->location));
+ }
+
+ return (Node *) f;
+}
+
+static Node *
transformBoolExpr(ParseState *pstate, BoolExpr *a)
{
List *args = NIL;
@@ -1767,6 +1799,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:
@@ -3115,6 +3148,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 fdb3e6d..0cbc950
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -2599,6 +2599,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 5f6a683..55936bb
--- 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.
@@ -254,6 +258,7 @@ transformMergeStmt(ParseState *pstate, M
MergeAction *action;
action = makeNode(MergeAction);
+ action->index = foreach_current_index(l) + 1;
action->commandType = mergeWhenClause->commandType;
action->matched = mergeWhenClause->matched;
@@ -390,9 +395,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 34a0ec5..60b7e4f
--- a/src/backend/parser/parse_relation.c
+++ b/src/backend/parser/parse_relation.c
@@ -2346,9 +2346,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/parser/parse_target.c b/src/backend/parser/parse_target.c
new file mode 100644
index 0cd904f..eab78f4
--- a/src/backend/parser/parse_target.c
+++ b/src/backend/parser/parse_target.c
@@ -1807,6 +1807,20 @@ FigureColnameInternal(Node *node, char *
/* make GROUPING() act like a regular function */
*name = "grouping";
return 2;
+ case T_MergingFunc:
+ switch (((MergingFunc *) node)->mfop)
+ {
+ case MERGING_ACTION:
+ *name = "action";
+ return 2;
+ case MERGING_CLAUSE_NUMBER:
+ *name = "clause_number";
+ return 2;
+ default:
+ elog(ERROR, "unrecognized MergingFuncOp: %d",
+ (int) ((MergingFunc *) node)->mfop);
+ }
+ break;
case T_SubLink:
switch (((SubLink *) node)->subLinkType)
{
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
new file mode 100644
index f60b34d..d719d07
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -3632,9 +3632,9 @@ RewriteQuery(Query *parsetree, List *rew
ListCell *lc1;
/*
- * First, recursively process any insert/update/delete statements in WITH
- * clauses. (We have to do this first because the WITH clauses may get
- * copied into rule actions below.)
+ * First, recursively process any insert/update/delete/merge statements in
+ * WITH clauses. (We have to do this first because the WITH clauses may
+ * get copied into rule actions below.)
*/
foreach(lc1, parsetree->cteList)
{
@@ -3659,7 +3659,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/rewrite/rowsecurity.c b/src/backend/rewrite/rowsecurity.c
new file mode 100644
index 94268a2..65ce579
--- a/src/backend/rewrite/rowsecurity.c
+++ b/src/backend/rewrite/rowsecurity.c
@@ -395,10 +395,10 @@ get_row_security_policies(Query *root, R
* on the final action we take.
*
* We already fetched the SELECT policies above, to check existing rows,
- * but we must also check that new rows created by UPDATE actions are
- * visible, if SELECT rights are required for this relation. We don't do
- * this for INSERT actions, since an INSERT command would only do this
- * check if it had a RETURNING list, and MERGE does not support RETURNING.
+ * but we must also check that new rows created by INSERT/UPDATE actions
+ * are visible, if SELECT rights are required. For INSERT actions, we only
+ * do this if RETURNING is specified, to be consistent with a plain INSERT
+ * command, which can only require SELECT rights when RETURNING is used.
*
* 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
@@ -420,6 +420,8 @@ get_row_security_policies(Query *root, R
List *merge_delete_restrictive_policies;
List *merge_insert_permissive_policies;
List *merge_insert_restrictive_policies;
+ List *merge_select_permissive_policies = NIL;
+ List *merge_select_restrictive_policies = NIL;
/*
* Fetch the UPDATE policies and set them up to execute on the
@@ -457,9 +459,6 @@ get_row_security_policies(Query *root, R
*/
if (perminfo->requiredPerms & ACL_SELECT)
{
- List *merge_select_permissive_policies;
- List *merge_select_restrictive_policies;
-
get_policies_for_relation(rel, CMD_SELECT, user_id,
&merge_select_permissive_policies,
&merge_select_restrictive_policies);
@@ -508,6 +507,21 @@ get_row_security_policies(Query *root, R
withCheckOptions,
hasSubLinks,
false);
+
+ /*
+ * Add ALL/SELECT policies as WCO_RLS_INSERT_CHECK WCOs, to ensure
+ * that the inserted row is visible when executing an INSERT action,
+ * if RETURNING is specified and SELECT rights are required for this
+ * relation.
+ */
+ if (perminfo->requiredPerms & ACL_SELECT && root->returningList)
+ add_with_check_options(rel, rt_index,
+ WCO_RLS_INSERT_CHECK,
+ merge_select_permissive_policies,
+ merge_select_restrictive_policies,
+ withCheckOptions,
+ hasSubLinks,
+ true);
}
table_close(rel, NoLock);
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
new file mode 100644
index 8de821f..f84a943
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -2145,11 +2145,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/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
new file mode 100644
index b625f47..e710d02
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -7199,8 +7199,13 @@ get_merge_query_def(Query *query, depars
appendStringInfoString(buf, "DO NOTHING");
}
- /* No RETURNING support in MERGE yet */
- Assert(query->returningList == NIL);
+ /* Add RETURNING if present */
+ if (query->returningList)
+ {
+ appendContextKeyword(context, " RETURNING",
+ -PRETTYINDENT_STD, PRETTYINDENT_STD, 1);
+ get_target_list(query->returningList, context, NULL, colNamesVisible);
+ }
}
@@ -8300,6 +8305,7 @@ isSimpleNode(Node *node, Node *parentNod
case T_Aggref:
case T_GroupingFunc:
case T_WindowFunc:
+ case T_MergingFunc:
case T_FuncExpr:
case T_JsonConstructorExpr:
/* function-like: name(..) or name[..] */
@@ -8654,6 +8660,27 @@ get_rule_expr(Node *node, deparse_contex
get_windowfunc_expr((WindowFunc *) node, context);
break;
+ case T_MergingFunc:
+ {
+ MergingFunc *mexpr = (MergingFunc *) node;
+
+ appendStringInfoString(buf, "MERGING(");
+ switch (mexpr->mfop)
+ {
+ case MERGING_ACTION:
+ appendStringInfoString(buf, "ACTION");
+ break;
+ case MERGING_CLAUSE_NUMBER:
+ appendStringInfoString(buf, "CLAUSE_NUMBER");
+ break;
+ default:
+ elog(ERROR, "unrecognized MergingFuncOp: %d",
+ (int) mexpr->mfop);
+ }
+ appendStringInfoChar(buf, ')');
+ }
+ break;
+
case T_SubscriptingRef:
{
SubscriptingRef *sbsref = (SubscriptingRef *) node;
diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c
new file mode 100644
index 76e01b0..2830bde
--- a/src/bin/psql/common.c
+++ b/src/bin/psql/common.c
@@ -983,13 +983,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/executor/execExpr.h b/src/include/executor/execExpr.h
new file mode 100644
index a28ddcd..abb0fb4
--- a/src/include/executor/execExpr.h
+++ b/src/include/executor/execExpr.h
@@ -243,6 +243,7 @@ typedef enum ExprEvalOp
EEOP_AGGREF,
EEOP_GROUPING_FUNC,
EEOP_WINDOW_FUNC,
+ EEOP_MERGING_FUNC,
EEOP_SUBPLAN,
/* aggregation related nodes */
@@ -626,6 +627,12 @@ typedef struct ExprEvalStep
WindowFuncExprState *wfstate;
} window_func;
+ /* for EEOP_MERGING_FUNC */
+ struct
+ {
+ MergingFuncOp mfop; /* the MERGING() operation to perform */
+ } merging_func;
+
/* for EEOP_SUBPLAN */
struct
{
@@ -810,6 +817,8 @@ extern void ExecEvalJsonConstructor(Expr
ExprContext *econtext);
extern void ExecEvalJsonIsPredicate(ExprState *state, ExprEvalStep *op);
extern void ExecEvalGroupingFunc(ExprState *state, ExprEvalStep *op);
+extern void ExecEvalMergingFunc(ExprState *state, ExprEvalStep *op,
+ ExprContext *econtext);
extern void ExecEvalSubPlan(ExprState *state, ExprEvalStep *op,
ExprContext *econtext);
extern void ExecEvalWholeRowVar(ExprState *state, ExprEvalStep *op,
diff --git a/src/include/executor/spi.h b/src/include/executor/spi.h
new file mode 100644
index c13e1f6..48b8773
--- a/src/include/executor/spi.h
+++ b/src/include/executor/spi.h
@@ -97,6 +97,7 @@ typedef struct _SPI_plan *SPIPlanPtr;
#define SPI_OK_REL_UNREGISTER 16
#define SPI_OK_TD_REGISTER 17
#define SPI_OK_MERGE 18
+#define SPI_OK_MERGE_RETURNING 19
#define SPI_OPT_NONATOMIC (1 << 0)
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
new file mode 100644
index 444a5f0..57fdfca
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1325,6 +1325,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 476d55d..eb42475
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -1927,6 +1927,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/nodes/primnodes.h b/src/include/nodes/primnodes.h
new file mode 100644
index 4a15460..97806f5
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -572,6 +572,39 @@ typedef struct WindowFunc
} WindowFunc;
/*
+ * MergingFunc
+ *
+ * A MergingFunc is a MERGING(...) expression that can only appear in the
+ * RETURNING list of a MERGE command. It returns information about the
+ * currently executing merge action. Possible operations are:
+ *
+ * MERGING_ACTION:
+ * Return the command string of the current merge action ("INSERT",
+ * "UPDATE" or "DELETE").
+ *
+ * MERGING_CLAUSE_NUMBER:
+ * Return the 1-based index of the current merge WHEN clause.
+ */
+typedef enum MergingFuncOp
+{
+ MERGING_ACTION,
+ MERGING_CLAUSE_NUMBER,
+} MergingFuncOp;
+
+typedef struct MergingFunc
+{
+ Expr xpr;
+ /* operation to perform */
+ MergingFuncOp mfop;
+ /* type Oid of result */
+ Oid mftype pg_node_attr(query_jumble_ignore);
+ /* OID of collation, or InvalidOid if none */
+ Oid mfcollid pg_node_attr(query_jumble_ignore);
+ /* token location, or -1 if unknown */
+ int location;
+} MergingFunc;
+
+/*
* SubscriptingRef: describes a subscripting operation over a container
* (array, etc).
*
@@ -1735,6 +1768,7 @@ typedef struct BooleanTest
typedef struct MergeAction
{
NodeTag type;
+ int index; /* 1-based index of the clause */
bool matched; /* true=MATCHED, false=NOT MATCHED */
CmdType commandType; /* INSERT/UPDATE/DELETE/DO NOTHING */
/* OVERRIDING clause */
diff --git a/src/include/optimizer/paramassign.h b/src/include/optimizer/paramassign.h
new file mode 100644
index 501f728..1e95c8f
--- a/src/include/optimizer/paramassign.h
+++ b/src/include/optimizer/paramassign.h
@@ -20,6 +20,7 @@ extern Param *replace_outer_placeholderv
PlaceHolderVar *phv);
extern Param *replace_outer_agg(PlannerInfo *root, Aggref *agg);
extern Param *replace_outer_grouping(PlannerInfo *root, GroupingFunc *grp);
+extern Param *replace_outer_merging(PlannerInfo *root, MergingFunc *mf);
extern Param *replace_nestloop_param_var(PlannerInfo *root, Var *var);
extern Param *replace_nestloop_param_placeholdervar(PlannerInfo *root,
PlaceHolderVar *phv);
diff --git a/src/include/parser/analyze.h b/src/include/parser/analyze.h
new file mode 100644
index 5ecdd10..28b66fc
--- 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/kwlist.h b/src/include/parser/kwlist.h
new file mode 100644
index 2331aca..9c5d6ea
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -80,6 +80,7 @@ PG_KEYWORD("characteristics", CHARACTERI
PG_KEYWORD("check", CHECK, RESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("checkpoint", CHECKPOINT, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("class", CLASS, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("clause_number", CLAUSE_NUMBER, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("close", CLOSE, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("cluster", CLUSTER, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("coalesce", COALESCE, COL_NAME_KEYWORD, BARE_LABEL)
@@ -266,6 +267,7 @@ PG_KEYWORD("matched", MATCHED, UNRESERVE
PG_KEYWORD("materialized", MATERIALIZED, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("maxvalue", MAXVALUE, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("merge", MERGE, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("merging", MERGING, COL_NAME_KEYWORD, BARE_LABEL)
PG_KEYWORD("method", METHOD, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("minute", MINUTE_P, UNRESERVED_KEYWORD, AS_LABEL)
PG_KEYWORD("minvalue", MINVALUE, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
new file mode 100644
index 99d6515..5b781d8
--- 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/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
new file mode 100644
index 6d16913..51de094
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -4267,9 +4267,9 @@ exec_stmt_execsql(PLpgSQL_execstate *est
/*
* If we have INTO, then we only need one row back ... but if we have INTO
* STRICT or extra check too_many_rows, ask for two rows, so that we can
- * verify the statement returns only one. INSERT/UPDATE/DELETE are always
- * treated strictly. Without INTO, just run the statement to completion
- * (tcount = 0).
+ * verify the statement returns only one. INSERT/UPDATE/DELETE/MERGE are
+ * always treated strictly. Without INTO, just run the statement to
+ * completion (tcount = 0).
*
* We could just ask for two rows always when using INTO, but there are
* some cases where demanding the extra row costs significant time, eg by
@@ -4307,10 +4307,11 @@ exec_stmt_execsql(PLpgSQL_execstate *est
case SPI_OK_INSERT:
case SPI_OK_UPDATE:
case SPI_OK_DELETE:
+ case SPI_OK_MERGE:
case SPI_OK_INSERT_RETURNING:
case SPI_OK_UPDATE_RETURNING:
case SPI_OK_DELETE_RETURNING:
- case SPI_OK_MERGE:
+ case SPI_OK_MERGE_RETURNING:
Assert(stmt->mod_stmt);
exec_set_found(estate, (SPI_processed != 0));
break;
@@ -4489,10 +4490,11 @@ exec_stmt_dynexecute(PLpgSQL_execstate *
case SPI_OK_INSERT:
case SPI_OK_UPDATE:
case SPI_OK_DELETE:
+ case SPI_OK_MERGE:
case SPI_OK_INSERT_RETURNING:
case SPI_OK_UPDATE_RETURNING:
case SPI_OK_DELETE_RETURNING:
- case SPI_OK_MERGE:
+ case SPI_OK_MERGE_RETURNING:
case SPI_OK_UTILITY:
case SPI_OK_REWRITTEN:
break;
diff --git a/src/pl/tcl/pltcl.c b/src/pl/tcl/pltcl.c
new file mode 100644
index 6187e15..18d14a2
--- a/src/pl/tcl/pltcl.c
+++ b/src/pl/tcl/pltcl.c
@@ -2456,6 +2456,7 @@ pltcl_process_SPI_result(Tcl_Interp *int
case SPI_OK_INSERT_RETURNING:
case SPI_OK_DELETE_RETURNING:
case SPI_OK_UPDATE_RETURNING:
+ case SPI_OK_MERGE_RETURNING:
/*
* Process the tuples we got
diff --git a/src/test/regress/expected/merge.out b/src/test/regress/expected/merge.out
new file mode 100644
index f87905f..8baa0f4
--- 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;
@@ -1256,7 +1256,7 @@ BEGIN;
MERGE INTO sq_target
USING v
ON tid = sid
-WHEN MATCHED AND tid > 2 THEN
+WHEN MATCHED AND tid >= 2 THEN
UPDATE SET balance = balance + delta
WHEN NOT MATCHED THEN
INSERT (balance, tid) VALUES (balance + delta, sid)
@@ -1271,7 +1271,7 @@ INSERT INTO sq_source (sid, balance, del
MERGE INTO sq_target t
USING v
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)
@@ -1295,7 +1295,7 @@ WITH targq AS (
MERGE INTO sq_target t
USING v
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)
@@ -1304,20 +1304,191 @@ WHEN MATCHED AND tid < 2 THEN
ROLLBACK;
-- RETURNING
BEGIN;
-INSERT INTO sq_source (sid, balance, delta) VALUES (-1, -1, -10);
+CREATE TABLE merge_actions(action text, abbrev text);
+INSERT INTO merge_actions VALUES ('INSERT', 'ins'), ('UPDATE', 'upd'), ('DELETE', 'del');
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)
+ 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 *;
- ^
+ DELETE
+RETURNING MERGING(CLAUSE_NUMBER) AS clause,
+ (SELECT abbrev FROM merge_actions
+ WHERE action = MERGING(ACTION)) AS action,
+ t.*,
+ CASE MERGING(ACTION)
+ WHEN 'INSERT' THEN 'Inserted '||t
+ WHEN 'UPDATE' THEN 'Added '||delta||' to balance'
+ WHEN 'DELETE' THEN 'Removed '||t
+ END AS description;
+ clause | action | tid | balance | description
+--------+--------+-----+---------+---------------------
+ 3 | del | 1 | 100 | Removed (1,100)
+ 1 | upd | 2 | 220 | Added 20 to balance
+ 2 | ins | 4 | 40 | Inserted (4,40)
+(3 rows)
+
+ROLLBACK;
+-- error when using MERGING() outside MERGE
+SELECT MERGING(ACTION) FROM sq_target;
+ERROR: MERGING() can only be used in the RETURNING list of a MERGE command
+LINE 1: SELECT MERGING(ACTION) FROM sq_target;
+ ^
+UPDATE sq_target SET balance = balance + 1 RETURNING MERGING(CLAUSE_NUMBER);
+ERROR: MERGING() can only be used in the RETURNING list of a MERGE command
+LINE 1: ...ATE sq_target SET balance = balance + 1 RETURNING MERGING(CL...
+ ^
+-- RETURNING in CTEs
+CREATE TABLE sq_target_merge_log (tid integer NOT NULL, last_change text);
+INSERT INTO sq_target_merge_log VALUES (1, 'Original value');
+BEGIN;
+WITH m AS (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 MERGING(CLAUSE_NUMBER), MERGING(ACTION), t.*,
+ CASE MERGING(ACTION)
+ WHEN 'INSERT' THEN 'Inserted '||t
+ WHEN 'UPDATE' THEN 'Added '||delta||' to balance'
+ WHEN 'DELETE' THEN 'Removed '||t
+ END AS description
+), m2 AS (
+ MERGE INTO sq_target_merge_log l
+ USING m
+ ON l.tid = m.tid
+ WHEN MATCHED THEN
+ UPDATE SET last_change = description
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (m.tid, description)
+ RETURNING action, MERGING(ACTION) AS log_action, l.*
+)
+SELECT * FROM m2;
+ action | log_action | tid | last_change
+--------+------------+-----+---------------------
+ DELETE | UPDATE | 1 | Removed (1,100)
+ UPDATE | INSERT | 2 | Added 20 to balance
+ INSERT | INSERT | 4 | Inserted (4,40)
+(3 rows)
+
+SELECT * FROM sq_target_merge_log ORDER BY tid;
+ tid | last_change
+-----+---------------------
+ 1 | Removed (1,100)
+ 2 | Added 20 to balance
+ 4 | Inserted (4,40)
+(3 rows)
+
+ROLLBACK;
+-- COPY (MERGE ... RETURNING) TO ...
+BEGIN;
+COPY (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 MERGING(ACTION), t.*
+) TO stdout;
+DELETE 1 100
+UPDATE 2 220
+INSERT 4 40
+ROLLBACK;
+-- SQL function with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT action text, OUT tid int, OUT new_balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING MERGING(ACTION), t.*;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT action, tid, new_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ action | tid | new_balance
+--------+-----+-------------
+ DELETE | 1 | 100
+ UPDATE | 3 | 320
+ INSERT | 4 | 110
+(3 rows)
+
+ROLLBACK;
+-- SQL SRF with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_sq_source_into_sq_target()
+RETURNS TABLE (action text, tid int, balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 MERGING(ACTION), t.*;
+$$;
+SELECT * FROM merge_sq_source_into_sq_target();
+ action | tid | balance
+--------+-----+---------
+ DELETE | 1 | 100
+ UPDATE | 2 | 220
+ INSERT | 4 | 40
+(3 rows)
+
+ROLLBACK;
+-- PL/pgSQL function with MERGE ... RETURNING ... INTO
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT r_action text, OUT r_tid int, OUT r_balance int)
+LANGUAGE plpgsql AS
+$$
+BEGIN
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING MERGING(ACTION), t.* INTO r_action, r_tid, r_balance;
+END;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT r_action, r_tid, r_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ r_action | r_tid | r_balance
+----------+-------+-----------
+ DELETE | 1 | 100
+ UPDATE | 3 | 320
+ INSERT | 4 | 110
+(3 rows)
+
ROLLBACK;
-- EXPLAIN
CREATE TABLE ex_mtarget (a int, b int)
@@ -1514,7 +1685,7 @@ SELECT * FROM sq_target WHERE tid = 1;
(1 row)
ROLLBACK;
-DROP TABLE sq_target, sq_source CASCADE;
+DROP TABLE sq_target, sq_target_merge_log, sq_source CASCADE;
NOTICE: drop cascades to view v
CREATE TABLE pa_target (tid integer, balance float, val text)
PARTITION BY LIST (tid);
@@ -1640,6 +1811,32 @@ SELECT * FROM pa_target ORDER BY tid;
(14 rows)
ROLLBACK;
+-- update partition key to partition not initially scanned
+BEGIN;
+MERGE INTO pa_target t
+ USING pa_source s
+ ON t.tid = s.sid AND t.tid = 1
+ WHEN MATCHED THEN
+ UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
+ RETURNING MERGING(ACTION), MERGING(CLAUSE_NUMBER), t.*;
+ action | clause_number | tid | balance | val
+--------+---------------+-----+---------+--------------------------
+ UPDATE | 1 | 2 | 110 | initial updated by merge
+(1 row)
+
+SELECT * FROM pa_target ORDER BY tid;
+ tid | balance | val
+-----+---------+--------------------------
+ 2 | 110 | initial updated by merge
+ 3 | 300 | initial
+ 5 | 500 | initial
+ 7 | 700 | initial
+ 9 | 900 | initial
+ 11 | 1100 | initial
+ 13 | 1300 | initial
+(7 rows)
+
+ROLLBACK;
DROP TABLE pa_target CASCADE;
-- The target table is partitioned in the same way, but this time by attaching
-- partitions which have columns in different order, dropped columns etc.
@@ -1889,7 +2086,21 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge');
+ INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge')
+ RETURNING MERGING(ACTION), MERGING(CLAUSE_NUMBER), t.*;
+ action | clause_number | logts | tid | balance | val
+--------+---------------+--------------------------+-----+---------+--------------------------
+ UPDATE | 1 | Tue Jan 31 00:00:00 2017 | 1 | 110 | initial updated by merge
+ UPDATE | 1 | Tue Feb 28 00:00:00 2017 | 2 | 220 | initial updated by merge
+ INSERT | 2 | Sun Jan 15 00:00:00 2017 | 3 | 30 | inserted by merge
+ UPDATE | 1 | Tue Jan 31 00:00:00 2017 | 4 | 440 | initial updated by merge
+ UPDATE | 1 | Tue Feb 28 00:00:00 2017 | 5 | 550 | initial updated by merge
+ INSERT | 2 | Sun Jan 15 00:00:00 2017 | 6 | 60 | inserted by merge
+ UPDATE | 1 | Tue Jan 31 00:00:00 2017 | 7 | 770 | initial updated by merge
+ UPDATE | 1 | Tue Feb 28 00:00:00 2017 | 8 | 880 | initial updated by merge
+ INSERT | 2 | Sun Jan 15 00:00:00 2017 | 9 | 90 | inserted by merge
+(9 rows)
+
SELECT * FROM pa_target ORDER BY tid;
logts | tid | balance | val
--------------------------+-----+---------+--------------------------
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
new file mode 100644
index 6988128..3fd4fab
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -2302,6 +2302,35 @@ WHEN MATCHED THEN
UPDATE SET dnotes = dnotes || ' notes added by merge8 '
WHEN NOT MATCHED THEN
INSERT VALUES (13, 44, 1, 'regress_rls_bob', 'new manga');
+SELECT * FROM document WHERE did = 13;
+ did | cid | dlevel | dauthor | dtitle | dnotes
+-----+-----+--------+---------+--------+--------
+(0 rows)
+
+-- but not OK if RETURNING is used
+MERGE INTO document d
+USING (SELECT 14 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge9 '
+WHEN NOT MATCHED THEN
+ INSERT VALUES (14, 44, 1, 'regress_rls_bob', 'new manga')
+RETURNING *;
+ERROR: new row violates row-level security policy for table "document"
+-- but OK if new row is visible
+MERGE INTO document d
+USING (SELECT 14 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge10 '
+WHEN NOT MATCHED THEN
+ INSERT VALUES (14, 11, 1, 'regress_rls_bob', 'new novel')
+RETURNING *;
+ sdid | did | cid | dlevel | dauthor | dtitle | dnotes
+------+-----+-----+--------+-----------------+-----------+--------
+ 14 | 14 | 11 | 1 | regress_rls_bob | new novel |
+(1 row)
+
RESET SESSION AUTHORIZATION;
-- drop the restrictive SELECT policy so that we can look at the
-- final state of the table
@@ -2325,7 +2354,8 @@ SELECT * FROM document;
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 notes added by merge7
13 | 44 | 1 | regress_rls_bob | new manga |
-(15 rows)
+ 14 | 11 | 1 | regress_rls_bob | new novel |
+(16 rows)
--
-- ROLE/GROUP
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
new file mode 100644
index abc944e..9a0ca93
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -3625,7 +3625,8 @@ MERGE INTO rule_merge2 t USING (SELECT 1
-- test deparsing
CREATE TABLE sf_target(id int, data text, filling int[]);
CREATE FUNCTION merge_sf_test()
- RETURNS void
+ RETURNS TABLE(action text, clause_number int,
+ a int, b text, id int, data text, filling int[])
LANGUAGE sql
BEGIN ATOMIC
MERGE INTO sf_target t
@@ -3662,11 +3663,13 @@ WHEN NOT MATCHED
VALUES (s.a, s.b, DEFAULT)
WHEN NOT MATCHED
THEN INSERT (filling[1], id)
- VALUES (s.a, s.a);
+ VALUES (s.a, s.a)
+RETURNING
+ merging(action), merging(clause_number) AS clause_number, *;
END;
\sf merge_sf_test
CREATE OR REPLACE FUNCTION public.merge_sf_test()
- RETURNS void
+ RETURNS TABLE(action text, clause_number integer, a integer, b text, id integer, data text, filling integer[])
LANGUAGE sql
BEGIN ATOMIC
MERGE INTO sf_target t
@@ -3703,7 +3706,14 @@ BEGIN ATOMIC
VALUES (s.a, s.b, DEFAULT)
WHEN NOT MATCHED
THEN INSERT (filling[1], id)
- VALUES (s.a, s.a);
+ VALUES (s.a, s.a)
+ RETURNING MERGING(ACTION) AS action,
+ MERGING(CLAUSE_NUMBER) AS clause_number,
+ s.a,
+ s.b,
+ t.id,
+ t.data,
+ t.filling;
END
DROP FUNCTION merge_sf_test;
DROP TABLE sf_target;
diff --git a/src/test/regress/expected/with.out b/src/test/regress/expected/with.out
new file mode 100644
index 7d796ea..4666873
--- a/src/test/regress/expected/with.out
+++ b/src/test/regress/expected/with.out
@@ -3493,6 +3493,16 @@ SELECT * FROM t;
ERROR: WITH query "t" does not have a RETURNING clause
LINE 4: SELECT * FROM t;
^
+-- RETURNING tries to return its own output
+WITH RECURSIVE t(action, a) AS (
+ MERGE INTO y USING (VALUES (11)) v(a) ON y.a = v.a
+ WHEN NOT MATCHED THEN INSERT VALUES (v.a)
+ RETURNING MERGING(ACTION), (SELECT a FROM t)
+)
+SELECT * FROM t;
+ERROR: recursive query "t" must not contain data-modifying statements
+LINE 1: WITH RECURSIVE t(action, a) AS (
+ ^
-- data-modifying WITH allowed only at the top level
SELECT * FROM (
WITH t AS (UPDATE y SET a=a+1 RETURNING *)
diff --git a/src/test/regress/sql/merge.sql b/src/test/regress/sql/merge.sql
new file mode 100644
index 66cb75a..daa9dde
--- 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
@@ -816,7 +816,7 @@ BEGIN;
MERGE INTO sq_target
USING v
ON tid = sid
-WHEN MATCHED AND tid > 2 THEN
+WHEN MATCHED AND tid >= 2 THEN
UPDATE SET balance = balance + delta
WHEN NOT MATCHED THEN
INSERT (balance, tid) VALUES (balance + delta, sid)
@@ -829,7 +829,7 @@ INSERT INTO sq_source (sid, balance, del
MERGE INTO sq_target t
USING v
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)
@@ -847,7 +847,7 @@ WITH targq AS (
MERGE INTO sq_target t
USING v
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)
@@ -857,17 +857,146 @@ ROLLBACK;
-- RETURNING
BEGIN;
-INSERT INTO sq_source (sid, balance, delta) VALUES (-1, -1, -10);
+CREATE TABLE merge_actions(action text, abbrev text);
+INSERT INTO merge_actions VALUES ('INSERT', 'ins'), ('UPDATE', 'upd'), ('DELETE', 'del');
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)
+ INSERT (balance, tid) VALUES (balance + delta, sid)
WHEN MATCHED AND tid < 2 THEN
- DELETE
-RETURNING *;
+ DELETE
+RETURNING MERGING(CLAUSE_NUMBER) AS clause,
+ (SELECT abbrev FROM merge_actions
+ WHERE action = MERGING(ACTION)) AS action,
+ t.*,
+ CASE MERGING(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 MERGING() outside MERGE
+SELECT MERGING(ACTION) FROM sq_target;
+UPDATE sq_target SET balance = balance + 1 RETURNING MERGING(CLAUSE_NUMBER);
+
+-- RETURNING in CTEs
+CREATE TABLE sq_target_merge_log (tid integer NOT NULL, last_change text);
+INSERT INTO sq_target_merge_log VALUES (1, 'Original value');
+BEGIN;
+WITH m AS (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 MERGING(CLAUSE_NUMBER), MERGING(ACTION), t.*,
+ CASE MERGING(ACTION)
+ WHEN 'INSERT' THEN 'Inserted '||t
+ WHEN 'UPDATE' THEN 'Added '||delta||' to balance'
+ WHEN 'DELETE' THEN 'Removed '||t
+ END AS description
+), m2 AS (
+ MERGE INTO sq_target_merge_log l
+ USING m
+ ON l.tid = m.tid
+ WHEN MATCHED THEN
+ UPDATE SET last_change = description
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (m.tid, description)
+ RETURNING action, MERGING(ACTION) AS log_action, l.*
+)
+SELECT * FROM m2;
+SELECT * FROM sq_target_merge_log ORDER BY tid;
+ROLLBACK;
+
+-- COPY (MERGE ... RETURNING) TO ...
+BEGIN;
+COPY (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 MERGING(ACTION), t.*
+) TO stdout;
+ROLLBACK;
+
+-- SQL function with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT action text, OUT tid int, OUT new_balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING MERGING(ACTION), t.*;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT action, tid, new_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ROLLBACK;
+
+-- SQL SRF with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_sq_source_into_sq_target()
+RETURNS TABLE (action text, tid int, balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 MERGING(ACTION), t.*;
+$$;
+SELECT * FROM merge_sq_source_into_sq_target();
+ROLLBACK;
+
+-- PL/pgSQL function with MERGE ... RETURNING ... INTO
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT r_action text, OUT r_tid int, OUT r_balance int)
+LANGUAGE plpgsql AS
+$$
+BEGIN
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING MERGING(ACTION), t.* INTO r_action, r_tid, r_balance;
+END;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT r_action, r_tid, r_balance FROM merge_into_sq_target(sid, balance, delta)) m;
ROLLBACK;
-- EXPLAIN
@@ -966,7 +1095,7 @@ WHEN MATCHED THEN
SELECT * FROM sq_target WHERE tid = 1;
ROLLBACK;
-DROP TABLE sq_target, sq_source CASCADE;
+DROP TABLE sq_target, sq_target_merge_log, sq_source CASCADE;
CREATE TABLE pa_target (tid integer, balance float, val text)
PARTITION BY LIST (tid);
@@ -1033,6 +1162,17 @@ SELECT merge_func();
SELECT * FROM pa_target ORDER BY tid;
ROLLBACK;
+-- update partition key to partition not initially scanned
+BEGIN;
+MERGE INTO pa_target t
+ USING pa_source s
+ ON t.tid = s.sid AND t.tid = 1
+ WHEN MATCHED THEN
+ UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
+ RETURNING MERGING(ACTION), MERGING(CLAUSE_NUMBER), t.*;
+SELECT * FROM pa_target ORDER BY tid;
+ROLLBACK;
+
DROP TABLE pa_target CASCADE;
-- The target table is partitioned in the same way, but this time by attaching
@@ -1209,7 +1349,8 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge');
+ INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge')
+ RETURNING MERGING(ACTION), MERGING(CLAUSE_NUMBER), t.*;
SELECT * FROM pa_target ORDER BY tid;
ROLLBACK;
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
new file mode 100644
index dec7340..1d5ed0a
--- a/src/test/regress/sql/rowsecurity.sql
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -992,6 +992,27 @@ WHEN MATCHED THEN
UPDATE SET dnotes = dnotes || ' notes added by merge8 '
WHEN NOT MATCHED THEN
INSERT VALUES (13, 44, 1, 'regress_rls_bob', 'new manga');
+SELECT * FROM document WHERE did = 13;
+
+-- but not OK if RETURNING is used
+MERGE INTO document d
+USING (SELECT 14 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge9 '
+WHEN NOT MATCHED THEN
+ INSERT VALUES (14, 44, 1, 'regress_rls_bob', 'new manga')
+RETURNING *;
+
+-- but OK if new row is visible
+MERGE INTO document d
+USING (SELECT 14 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge10 '
+WHEN NOT MATCHED THEN
+ INSERT VALUES (14, 11, 1, 'regress_rls_bob', 'new novel')
+RETURNING *;
RESET SESSION AUTHORIZATION;
-- drop the restrictive SELECT policy so that we can look at the
diff --git a/src/test/regress/sql/rules.sql b/src/test/regress/sql/rules.sql
new file mode 100644
index 93aff4e..4b15a13
--- a/src/test/regress/sql/rules.sql
+++ b/src/test/regress/sql/rules.sql
@@ -1281,7 +1281,8 @@ MERGE INTO rule_merge2 t USING (SELECT 1
CREATE TABLE sf_target(id int, data text, filling int[]);
CREATE FUNCTION merge_sf_test()
- RETURNS void
+ RETURNS TABLE(action text, clause_number int,
+ a int, b text, id int, data text, filling int[])
LANGUAGE sql
BEGIN ATOMIC
MERGE INTO sf_target t
@@ -1318,7 +1319,9 @@ WHEN NOT MATCHED
VALUES (s.a, s.b, DEFAULT)
WHEN NOT MATCHED
THEN INSERT (filling[1], id)
- VALUES (s.a, s.a);
+ VALUES (s.a, s.a)
+RETURNING
+ merging(action), merging(clause_number) AS clause_number, *;
END;
\sf merge_sf_test
diff --git a/src/test/regress/sql/with.sql b/src/test/regress/sql/with.sql
new file mode 100644
index f8a213e..1d82d4a
--- a/src/test/regress/sql/with.sql
+++ b/src/test/regress/sql/with.sql
@@ -1591,6 +1591,14 @@ WITH t AS (
)
SELECT * FROM t;
+-- RETURNING tries to return its own output
+WITH RECURSIVE t(action, a) AS (
+ MERGE INTO y USING (VALUES (11)) v(a) ON y.a = v.a
+ WHEN NOT MATCHED THEN INSERT VALUES (v.a)
+ RETURNING MERGING(ACTION), (SELECT a FROM t)
+)
+SELECT * FROM t;
+
-- data-modifying WITH allowed only at the top level
SELECT * FROM (
WITH t AS (UPDATE y SET a=a+1 RETURNING *)
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
new file mode 100644
index 90b37b9..fe1e1c2
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1575,6 +1575,8 @@ MergePath
MergeScanSelCache
MergeStmt
MergeWhenClause
+MergingFunc
+MergingFuncOp
MetaCommand
MinMaxAggInfo
MinMaxAggPath
I didn't find any issue with v15.
no commit message in the patch, If a commit message is there, I can
help proofread.
Attached is a rebased version on top of 5f2e179bd3 (support for MERGE
into views), with a few additional tests to confirm that MERGE ...
RETURNING works for views as well as tables.
I see that this patch was discussed at the PostgreSQL Developers
Meeting. Did anything new come out of that discussion?
Regards,
Dean
Attachments:
support-merge-returning-v16.patchtext/x-patch; charset=US-ASCII; name=support-merge-returning-v16.patchDownload
diff --git a/doc/src/sgml/dml.sgml b/doc/src/sgml/dml.sgml
new file mode 100644
index cbbc5e2..3d95bdb
--- a/doc/src/sgml/dml.sgml
+++ b/doc/src/sgml/dml.sgml
@@ -283,10 +283,15 @@ DELETE FROM products;
<secondary>RETURNING</secondary>
</indexterm>
+ <indexterm zone="dml-returning">
+ <primary>MERGE</primary>
+ <secondary>RETURNING</secondary>
+ </indexterm>
+
<para>
Sometimes it is useful to obtain data from modified rows while they are
being manipulated. The <command>INSERT</command>, <command>UPDATE</command>,
- and <command>DELETE</command> commands all have an
+ <command>DELETE</command>, and <command>MERGE</command> commands all have an
optional <literal>RETURNING</literal> clause that supports this. Use
of <literal>RETURNING</literal> avoids performing an extra database query to
collect the data, and is especially valuable when it would otherwise be
@@ -339,6 +344,21 @@ DELETE FROM products
</programlisting>
</para>
+ <para>
+ In a <command>MERGE</command>, the data available to <literal>RETURNING</literal> is
+ the content of the source row plus the content of the inserted, updated, or
+ deleted target row. Since it is quite common for the source and target to
+ have many of the same columns, specifying <literal>RETURNING *</literal>
+ can lead to a lot of duplicated columns, so it is often more useful to
+ qualify it so as to return just the source or target row. For example:
+<programlisting>
+MERGE INTO products p USING new_products n ON p.product_no = n.product_no
+ WHEN NOT MATCHED THEN INSERT VALUES (n.product_no, n.name, n.price)
+ WHEN MATCHED THEN UPDATE SET name = n.name, price = n.price
+ RETURNING p.*;
+</programlisting>
+ </para>
+
<para>
If there are triggers (<xref linkend="triggers"/>) on the target table,
the data available to <literal>RETURNING</literal> is the row as modified by
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
new file mode 100644
index e5fa82c..d3b87e5
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -22421,6 +22421,84 @@ SELECT count(*) FROM sometable;
</sect1>
+ <sect1 id="functions-merge-support">
+ <title>Merge Support Functions</title>
+
+ <indexterm>
+ <primary>MERGE</primary>
+ <secondary>RETURNING</secondary>
+ </indexterm>
+
+ <indexterm>
+ <primary>MERGING</primary>
+ </indexterm>
+
+ <para>
+ <productname>PostgreSQL</productname> includes one merge support function
+ that may be used in the <literal>RETURNING</literal> list of a
+ <xref linkend="sql-merge"/> command to return additional information about
+ the action taken for each row:
+<synopsis>
+<function id="function-merging">MERGING</function> ( <parameter>property</parameter> )
+</synopsis>
+ The <parameter>property</parameter> key word specifies what to return:
+
+ <variablelist>
+ <varlistentry>
+ <term><literal>ACTION</literal></term>
+ <listitem>
+ <para>
+ The merge action command executed for the current row. This will be
+ <literal>'INSERT'</literal>, <literal>'UPDATE'</literal>, or
+ <literal>'DELETE'</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>CLAUSE_NUMBER</literal></term>
+ <listitem>
+ <para>
+ The 1-based index of the <literal>WHEN</literal> clause executed for
+ the current row.
+ </para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </para>
+
+ <para>
+ The following example illustrates how this may be used to determine
+ which actions were performed for each row affected by the
+ <command>MERGE</command> command:
+<screen><![CDATA[
+MERGE INTO products p
+ USING stock s ON p.product_id = s.product_id
+ WHEN MATCHED AND s.quantity > 0 THEN
+ UPDATE SET in_stock = true, quantity = s.quantity
+ WHEN MATCHED THEN
+ UPDATE SET in_stock = false, quantity = 0
+ WHEN NOT MATCHED THEN
+ INSERT (product_id, in_stock, quantity)
+ VALUES (s.product_id, true, s.quantity)
+ RETURNING MERGING(ACTION), MERGING(CLAUSE_NUMBER), p.*;
+
+ action | clause_number | product_id | in_stock | quantity
+--------+---------------+------------+----------+----------
+ UPDATE | 1 | 1001 | t | 50
+ UPDATE | 2 | 1002 | f | 0
+ INSERT | 3 | 1003 | t | 10
+]]></screen>
+ </para>
+
+ <para>
+ Note that this function can only be used in the <literal>RETURNING</literal>
+ list of a <command>MERGE</command> command. It is an error to use it in any
+ other part of a query.
+ </para>
+
+ </sect1>
+
<sect1 id="functions-subquery">
<title>Subquery Expressions</title>
diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml
new file mode 100644
index 8c2f114..a81c17a
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -1442,9 +1442,9 @@
to a <glossterm linkend="glossary-client">client</glossterm> upon the
completion of an <acronym>SQL</acronym> command, usually a
<command>SELECT</command> but it can be an
- <command>INSERT</command>, <command>UPDATE</command>, or
- <command>DELETE</command> command if the <literal>RETURNING</literal>
- clause is specified.
+ <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command> command if the
+ <literal>RETURNING</literal> clause is specified.
</para>
<para>
The fact that a result set is a relation means that a query can be used
diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml
new file mode 100644
index c2b9c6a..406834e
--- a/doc/src/sgml/plpgsql.sgml
+++ b/doc/src/sgml/plpgsql.sgml
@@ -1043,8 +1043,8 @@ INSERT INTO mytable VALUES (1,'one'), (2
</para>
<para>
- If the command does return rows (for example <command>SELECT</command>,
- or <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>
+ If the command does return rows (for example <command>SELECT</command>, or
+ <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command>
with <literal>RETURNING</literal>), there are two ways to proceed.
When the command will return at most one row, or you only care about
the first row of output, write the command as usual but add
@@ -1172,6 +1172,7 @@ SELECT <replaceable>select_expressions</
INSERT ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
UPDATE ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
DELETE ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
+MERGE ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
</synopsis>
where <replaceable>target</replaceable> can be a record variable, a row
@@ -1182,8 +1183,8 @@ DELETE ... RETURNING <replaceable>expres
<literal>INTO</literal> clause) just as described above,
and the plan is cached in the same way.
This works for <command>SELECT</command>,
- <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command> with
- <literal>RETURNING</literal>, and certain utility commands
+ <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command>
+ with <literal>RETURNING</literal>, and certain utility commands
that return row sets, such as <command>EXPLAIN</command>.
Except for the <literal>INTO</literal> clause, the SQL command is the same
as it would be written outside <application>PL/pgSQL</application>.
@@ -1259,7 +1260,7 @@ END;
</para>
<para>
- For <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command> with
+ For <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command> with
<literal>RETURNING</literal>, <application>PL/pgSQL</application> reports
an error for more than one returned row, even when
<literal>STRICT</literal> is not specified. This is because there
diff --git a/doc/src/sgml/queries.sgml b/doc/src/sgml/queries.sgml
new file mode 100644
index 648b283..372cce1
--- a/doc/src/sgml/queries.sgml
+++ b/doc/src/sgml/queries.sgml
@@ -2063,9 +2063,10 @@ SELECT <replaceable>select_list</replace
Table Expressions or <acronym>CTE</acronym>s, can be thought of as defining
temporary tables that exist just for one query. Each auxiliary statement
in a <literal>WITH</literal> clause can be a <command>SELECT</command>,
- <command>INSERT</command>, <command>UPDATE</command>, or <command>DELETE</command>; and the
+ <command>INSERT</command>, <command>UPDATE</command>, <command>DELETE</command>,
+ or <command>MERGE</command>; and the
<literal>WITH</literal> clause itself is attached to a primary statement that can
- be a <command>SELECT</command>, <command>INSERT</command>, <command>UPDATE</command>,
+ also be a <command>SELECT</command>, <command>INSERT</command>, <command>UPDATE</command>,
<command>DELETE</command>, or <command>MERGE</command>.
</para>
@@ -2599,8 +2600,8 @@ SELECT * FROM w AS w1 JOIN w AS w2 ON w1
<title>Data-Modifying Statements in <literal>WITH</literal></title>
<para>
- You can use most data-modifying statements (<command>INSERT</command>,
- <command>UPDATE</command>, or <command>DELETE</command>, but not
+ You can use data-modifying statements (<command>INSERT</command>,
+ <command>UPDATE</command>, <command>DELETE</command>, or
<command>MERGE</command>) in <literal>WITH</literal>. This
allows you to perform several different operations in the same query.
An example is:
diff --git a/doc/src/sgml/ref/copy.sgml b/doc/src/sgml/ref/copy.sgml
new file mode 100644
index 55764fc..6c83e30
--- a/doc/src/sgml/ref/copy.sgml
+++ b/doc/src/sgml/ref/copy.sgml
@@ -130,16 +130,19 @@ COPY { <replaceable class="parameter">ta
A <link linkend="sql-select"><command>SELECT</command></link>,
<link linkend="sql-values"><command>VALUES</command></link>,
<link linkend="sql-insert"><command>INSERT</command></link>,
- <link linkend="sql-update"><command>UPDATE</command></link>, or
- <link linkend="sql-delete"><command>DELETE</command></link> command whose results are to be
- copied. Note that parentheses are required around the query.
+ <link linkend="sql-update"><command>UPDATE</command></link>,
+ <link linkend="sql-delete"><command>DELETE</command></link>, or
+ <link linkend="sql-merge"><command>MERGE</command></link> command
+ whose results are to be copied. Note that parentheses are required
+ around the query.
</para>
<para>
- For <command>INSERT</command>, <command>UPDATE</command> and
- <command>DELETE</command> queries a <literal>RETURNING</literal> clause
- must be provided, and the target relation must not have a conditional
- rule, nor an <literal>ALSO</literal> rule, nor an
- <literal>INSTEAD</literal> rule that expands to multiple statements.
+ For <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, and <command>MERGE</command> queries a
+ <literal>RETURNING</literal> clause must be provided, and the target
+ relation must not have a conditional rule, nor an
+ <literal>ALSO</literal> rule, nor an <literal>INSTEAD</literal> rule
+ that expands to multiple statements.
</para>
</listitem>
</varlistentry>
diff --git a/doc/src/sgml/ref/merge.sgml b/doc/src/sgml/ref/merge.sgml
new file mode 100644
index 7a8ac40..2f0ad34
--- a/doc/src/sgml/ref/merge.sgml
+++ b/doc/src/sgml/ref/merge.sgml
@@ -25,6 +25,7 @@ PostgreSQL documentation
MERGE INTO [ ONLY ] <replaceable class="parameter">target_table_name</replaceable> [ * ] [ [ AS ] <replaceable class="parameter">target_alias</replaceable> ]
USING <replaceable class="parameter">data_source</replaceable> ON <replaceable class="parameter">join_condition</replaceable>
<replaceable class="parameter">when_clause</replaceable> [...]
+[ RETURNING * | <replaceable class="parameter">output_expression</replaceable> [ [ AS ] <replaceable class="parameter">output_name</replaceable> ] [, ...] ]
<phrase>where <replaceable class="parameter">data_source</replaceable> is:</phrase>
@@ -95,6 +96,18 @@ DELETE
</para>
<para>
+ The optional <literal>RETURNING</literal> clause causes <command>MERGE</command>
+ to compute and return value(s) based on each row inserted, updated, or
+ deleted. Any expression using the source or target table's columns, or
+ the <xref linkend="function-merging"/> function can be computed. When an
+ <command>INSERT</command> or <command>UPDATE</command> action is performed,
+ the new values of the target table's columns are used. When a
+ <command>DELETE</command> is performed, the old values of the target table's
+ columns are used. The syntax of the <literal>RETURNING</literal> list is
+ identical to that of the output list of <command>SELECT</command>.
+ </para>
+
+ <para>
There is no separate <literal>MERGE</literal> privilege.
If you specify an update action, you must have the
<literal>UPDATE</literal> privilege on the column(s)
@@ -129,6 +142,18 @@ DELETE
<variablelist>
<varlistentry>
+ <term><replaceable class="parameter">with_query</replaceable></term>
+ <listitem>
+ <para>
+ The <literal>WITH</literal> clause allows you to specify one or more
+ subqueries that can be referenced by name in the <command>MERGE</command>
+ query. See <xref linkend="queries-with"/> and <xref linkend="sql-select"/>
+ for details.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
<term><replaceable class="parameter">target_table_name</replaceable></term>
<listitem>
<para>
@@ -411,13 +436,30 @@ DELETE
</varlistentry>
<varlistentry>
- <term><replaceable class="parameter">with_query</replaceable></term>
+ <term><replaceable class="parameter">output_expression</replaceable></term>
<listitem>
<para>
- The <literal>WITH</literal> clause allows you to specify one or more
- subqueries that can be referenced by name in the <command>MERGE</command>
- query. See <xref linkend="queries-with"/> and <xref linkend="sql-select"/>
- for details.
+ An expression to be computed and returned by the <command>MERGE</command>
+ command after each row is merged. The expression can use any columns of
+ the source or target tables, or the <xref linkend="function-merging"/>
+ function to return additional information about the action executed.
+ </para>
+ <para>
+ Writing <literal>*</literal> will return all columns from the source
+ table, followed by all columns from the target table. Often this will
+ lead to a lot of duplication, since it is common for the source and
+ target tables to have a lot of the same columns. This can be avoided by
+ qualifying the <literal>*</literal> with the name or alias of the source
+ or target table.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><replaceable class="parameter">output_name</replaceable></term>
+ <listitem>
+ <para>
+ A name to use for a returned column.
</para>
</listitem>
</varlistentry>
@@ -440,6 +482,13 @@ MERGE <replaceable class="parameter">tot
were changed in any way.
</para>
+ <para>
+ If the <command>MERGE</command> command contains a <literal>RETURNING</literal>
+ clause, the result will be similar to that of a <command>SELECT</command>
+ statement containing the columns and values defined in the
+ <literal>RETURNING</literal> list, computed over the row(s) inserted, updated,
+ or deleted by the command.
+ </para>
</refsect1>
<refsect1>
@@ -560,13 +609,6 @@ MERGE <replaceable class="parameter">tot
</para>
<para>
- There is no <literal>RETURNING</literal> clause with
- <command>MERGE</command>. Actions of <command>INSERT</command>,
- <command>UPDATE</command> and <command>DELETE</command> cannot contain
- <literal>RETURNING</literal> or <literal>WITH</literal> clauses.
- </para>
-
- <para>
When <command>MERGE</command> is run concurrently with other commands
that modify the target table, the usual transaction isolation rules
apply; see <xref linkend="transaction-iso"/> for an explanation
@@ -618,7 +660,8 @@ WHEN NOT MATCHED THEN
<para>
Attempt to insert a new stock item along with the quantity of stock. If
the item already exists, instead update the stock count of the existing
- item. Don't allow entries that have zero stock.
+ item. Don't allow entries that have zero stock. Return details of all
+ changes made.
<programlisting>
MERGE INTO wines w
USING wine_stock_changes s
@@ -628,7 +671,8 @@ WHEN NOT MATCHED AND s.stock_delta > 0 T
WHEN MATCHED AND w.stock + s.stock_delta > 0 THEN
UPDATE SET stock = w.stock + s.stock_delta
WHEN MATCHED THEN
- DELETE;
+ DELETE
+RETURNING MERGING(ACTION), w.*;
</programlisting>
The <literal>wine_stock_changes</literal> table might be, for example, a
@@ -643,8 +687,9 @@ WHEN MATCHED THEN
This command conforms to the <acronym>SQL</acronym> standard.
</para>
<para>
- The <literal>WITH</literal> clause and <literal>DO NOTHING</literal>
- action are extensions to the <acronym>SQL</acronym> standard.
+ The <literal>WITH</literal> clause, <literal>DO NOTHING</literal> action,
+ and <literal>RETURNING</literal> clause are extensions to the
+ <acronym>SQL</acronym> standard.
</para>
</refsect1>
</refentry>
diff --git a/doc/src/sgml/ref/select.sgml b/doc/src/sgml/ref/select.sgml
new file mode 100644
index 9917df7..066aed4
--- a/doc/src/sgml/ref/select.sgml
+++ b/doc/src/sgml/ref/select.sgml
@@ -74,7 +74,7 @@ SELECT [ ALL | DISTINCT [ ON ( <replacea
<phrase>and <replaceable class="parameter">with_query</replaceable> is:</phrase>
- <replaceable class="parameter">with_query_name</replaceable> [ ( <replaceable class="parameter">column_name</replaceable> [, ...] ) ] AS [ [ NOT ] MATERIALIZED ] ( <replaceable class="parameter">select</replaceable> | <replaceable class="parameter">values</replaceable> | <replaceable class="parameter">insert</replaceable> | <replaceable class="parameter">update</replaceable> | <replaceable class="parameter">delete</replaceable> )
+ <replaceable class="parameter">with_query_name</replaceable> [ ( <replaceable class="parameter">column_name</replaceable> [, ...] ) ] AS [ [ NOT ] MATERIALIZED ] ( <replaceable class="parameter">select</replaceable> | <replaceable class="parameter">values</replaceable> | <replaceable class="parameter">insert</replaceable> | <replaceable class="parameter">update</replaceable> | <replaceable class="parameter">delete</replaceable> | <replaceable class="parameter">merge</replaceable> )
[ SEARCH { BREADTH | DEPTH } FIRST BY <replaceable>column_name</replaceable> [, ...] SET <replaceable>search_seq_col_name</replaceable> ]
[ CYCLE <replaceable>column_name</replaceable> [, ...] SET <replaceable>cycle_mark_col_name</replaceable> [ TO <replaceable>cycle_mark_value</replaceable> DEFAULT <replaceable>cycle_mark_default</replaceable> ] USING <replaceable>cycle_path_col_name</replaceable> ]
@@ -230,10 +230,10 @@ TABLE [ ONLY ] <replaceable class="param
The subqueries effectively act as temporary tables or views
for the duration of the primary query.
Each subquery can be a <command>SELECT</command>, <command>TABLE</command>, <command>VALUES</command>,
- <command>INSERT</command>, <command>UPDATE</command> or
- <command>DELETE</command> statement.
+ <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command> statement.
When writing a data-modifying statement (<command>INSERT</command>,
- <command>UPDATE</command> or <command>DELETE</command>) in
+ <command>UPDATE</command>, <command>DELETE</command>, or <command>MERGE</command>) in
<literal>WITH</literal>, it is usual to include a <literal>RETURNING</literal> clause.
It is the output of <literal>RETURNING</literal>, <emphasis>not</emphasis> the underlying
table that the statement modifies, that forms the temporary table that is
@@ -2184,7 +2184,8 @@ SELECT 2+2;
<para>
<productname>PostgreSQL</productname> allows <command>INSERT</command>,
- <command>UPDATE</command>, and <command>DELETE</command> to be used as <literal>WITH</literal>
+ <command>UPDATE</command>, <command>DELETE</command>, and
+ <command>MERGE</command> to be used as <literal>WITH</literal>
queries. This is not found in the SQL standard.
</para>
</refsect2>
diff --git a/doc/src/sgml/rowtypes.sgml b/doc/src/sgml/rowtypes.sgml
new file mode 100644
index 4d86f97..bbeac84
--- a/doc/src/sgml/rowtypes.sgml
+++ b/doc/src/sgml/rowtypes.sgml
@@ -348,7 +348,7 @@ SELECT m.* FROM some_table, LATERAL myfu
column expansion of this kind when it appears at the top level of
a <link linkend="queries-select-lists"><command>SELECT</command> output
list</link>, a <link linkend="dml-returning"><literal>RETURNING</literal>
- list</link> in <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>,
+ list</link> in <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command>,
a <link linkend="queries-values"><literal>VALUES</literal> clause</link>, or
a <link linkend="sql-syntax-row-constructors">row constructor</link>.
In all other contexts (including when nested inside one of those
diff --git a/doc/src/sgml/spi.sgml b/doc/src/sgml/spi.sgml
new file mode 100644
index 47f4b5b..bb37786
--- a/doc/src/sgml/spi.sgml
+++ b/doc/src/sgml/spi.sgml
@@ -301,8 +301,9 @@ SPI_execute("INSERT INTO foo SELECT * FR
is returned in the global variable <varname>SPI_processed</varname>.
If the return value of the function is <symbol>SPI_OK_SELECT</symbol>,
<symbol>SPI_OK_INSERT_RETURNING</symbol>,
- <symbol>SPI_OK_DELETE_RETURNING</symbol>, or
- <symbol>SPI_OK_UPDATE_RETURNING</symbol>,
+ <symbol>SPI_OK_DELETE_RETURNING</symbol>,
+ <symbol>SPI_OK_UPDATE_RETURNING</symbol>, or
+ <symbol>SPI_OK_MERGE_RETURNING</symbol>,
then you can use the
global pointer <literal>SPITupleTable *SPI_tuptable</literal> to
access the result rows. Some utility commands (such as
@@ -472,6 +473,15 @@ typedef struct SPITupleTable
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><symbol>SPI_OK_MERGE_RETURNING</symbol></term>
+ <listitem>
+ <para>
+ if a <command>MERGE RETURNING</command> was executed
+ </para>
+ </listitem>
+ </varlistentry>
<varlistentry>
<term><symbol>SPI_OK_UTILITY</symbol></term>
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
new file mode 100644
index 8a79ad0..7d05369
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -177,7 +177,8 @@
statements separated by semicolons. A semicolon after the last
statement is optional. Unless the function is declared to return
<type>void</type>, the last statement must be a <command>SELECT</command>,
- or an <command>INSERT</command>, <command>UPDATE</command>, or <command>DELETE</command>
+ or an <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command>
that has a <literal>RETURNING</literal> clause.
</para>
@@ -1236,8 +1237,9 @@ SELECT x, CASE WHEN x > 0 THEN genera
<note>
<para>
- If a function's last command is <command>INSERT</command>, <command>UPDATE</command>,
- or <command>DELETE</command> with <literal>RETURNING</literal>, that command will
+ If a function's last command is <command>INSERT</command>,
+ <command>UPDATE</command>, <command>DELETE</command>, or
+ <command>MERGE</command> with <literal>RETURNING</literal>, that command will
always be executed to completion, even if the function is not declared
with <literal>SETOF</literal> or the calling query does not fetch all the
result rows. Any extra rows produced by the <literal>RETURNING</literal>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
new file mode 100644
index cc0786c..e96067a
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -283,12 +283,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 20ffc90..583f990
--- 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 3181b11..c4c2fb5
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -1108,6 +1108,21 @@ ExecInitExprRec(Expr *node, ExprState *s
break;
}
+ case T_MergingFunc:
+ {
+ MergingFunc *mf_node = (MergingFunc *) node;
+
+ if (!state->parent ||
+ !IsA(state->parent, ModifyTableState) ||
+ ((ModifyTableState *) state->parent)->operation != CMD_MERGE)
+ elog(ERROR, "MergingFunc found in non-merge plan node");
+
+ scratch.opcode = EEOP_MERGING_FUNC;
+ scratch.d.merging_func.mfop = mf_node->mfop;
+ ExprEvalPushStep(state, &scratch);
+ break;
+ }
+
case T_SubscriptingRef:
{
SubscriptingRef *sbsref = (SubscriptingRef *) node;
diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
new file mode 100644
index 3f20f1d..4bcad29
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -485,6 +485,7 @@ ExecInterpExpr(ExprState *state, ExprCon
&&CASE_EEOP_AGGREF,
&&CASE_EEOP_GROUPING_FUNC,
&&CASE_EEOP_WINDOW_FUNC,
+ &&CASE_EEOP_MERGING_FUNC,
&&CASE_EEOP_SUBPLAN,
&&CASE_EEOP_AGG_STRICT_DESERIALIZE,
&&CASE_EEOP_AGG_DESERIALIZE,
@@ -1593,6 +1594,14 @@ ExecInterpExpr(ExprState *state, ExprCon
EEO_NEXT();
}
+ EEO_CASE(EEOP_MERGING_FUNC)
+ {
+ /* too complex/uncommon for an inline implementation */
+ ExecEvalMergingFunc(state, op, econtext);
+
+ EEO_NEXT();
+ }
+
EEO_CASE(EEOP_SUBPLAN)
{
/* too complex for an inline implementation */
@@ -4247,6 +4256,59 @@ ExecEvalGroupingFunc(ExprState *state, E
}
/*
+ * ExecEvalMergingFunc
+ *
+ * Returns information about the current MERGE action for its RETURNING list.
+ */
+void
+ExecEvalMergingFunc(ExprState *state, ExprEvalStep *op, ExprContext *econtext)
+{
+ MergingFuncOp mfop = op->d.merging_func.mfop;
+ ModifyTableState *mtstate = castNode(ModifyTableState, state->parent);
+ MergeActionState *relaction = mtstate->mt_merge_action;
+
+ if (!relaction)
+ elog(ERROR, "no merge action in progress");
+
+ switch (mfop)
+ {
+ case MERGING_ACTION:
+ /* Return the MERGE action ("INSERT", "UPDATE", or "DELETE") */
+ switch (relaction->mas_action->commandType)
+ {
+ case CMD_INSERT:
+ *op->resvalue = PointerGetDatum(cstring_to_text_with_len("INSERT", 6));
+ *op->resnull = false;
+ break;
+ case CMD_UPDATE:
+ *op->resvalue = PointerGetDatum(cstring_to_text_with_len("UPDATE", 6));
+ *op->resnull = false;
+ break;
+ case CMD_DELETE:
+ *op->resvalue = PointerGetDatum(cstring_to_text_with_len("DELETE", 6));
+ *op->resnull = false;
+ break;
+ case CMD_NOTHING:
+ elog(ERROR, "unexpected merge action: DO NOTHING");
+ break;
+ default:
+ elog(ERROR, "unrecognized commandType: %d",
+ (int) relaction->mas_action->commandType);
+ }
+ break;
+
+ case MERGING_CLAUSE_NUMBER:
+ /* Return the 1-based index of the MERGE action */
+ *op->resvalue = Int32GetDatum(relaction->mas_action->index);
+ *op->resnull = false;
+ break;
+
+ default:
+ elog(ERROR, "unrecognized MergingFuncOp: %d", (int) mfop);
+ }
+}
+
+/*
* Hand off evaluation of a subplan to nodeSubplan.c
*/
void
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
new file mode 100644
index 429f0a8..813f76a
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -612,8 +612,8 @@ ExecInitPartitionInfo(ModifyTableState *
* Build the RETURNING projection for the partition. Note that we didn't
* build the returningList for partitions within the planner, but simple
* translation of varattnos will suffice. This only occurs for the INSERT
- * case or in the case of UPDATE tuple routing where we didn't find a
- * result rel to reuse.
+ * case or in the case of UPDATE/MERGE tuple routing where we didn't find
+ * a result rel to reuse.
*/
if (node && node->returningLists != NIL)
{
@@ -622,12 +622,14 @@ ExecInitPartitionInfo(ModifyTableState *
List *returningList;
/* See the comment above for WCO lists. */
- /* (except no RETURNING support for MERGE yet) */
Assert((node->operation == CMD_INSERT &&
list_length(node->returningLists) == 1 &&
list_length(node->resultRelations) == 1) ||
(node->operation == CMD_UPDATE &&
list_length(node->returningLists) ==
+ list_length(node->resultRelations)) ||
+ (node->operation == CMD_MERGE &&
+ list_length(node->returningLists) ==
list_length(node->resultRelations)));
/*
diff --git a/src/backend/executor/functions.c b/src/backend/executor/functions.c
new file mode 100644
index 0f811fd..7da02b0
--- a/src/backend/executor/functions.c
+++ b/src/backend/executor/functions.c
@@ -1660,8 +1660,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
@@ -1679,7 +1679,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;
@@ -1693,7 +1694,7 @@ check_sql_fn_retval(List *queryTreeLists
(errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
errmsg("return type mismatch in function declared to return %s",
format_type_be(rettype)),
- errdetail("Function's final statement must be SELECT or INSERT/UPDATE/DELETE RETURNING.")));
+ errdetail("Function's final statement must be SELECT or INSERT/UPDATE/DELETE/MERGE RETURNING.")));
return false; /* keep compiler quiet */
}
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
new file mode 100644
index ff7ec84..e36c66b
--- 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"
@@ -88,9 +87,6 @@ typedef struct ModifyTableContext
*/
TupleTableSlot *planSlot;
- /* MERGE specific */
- MergeActionState *relaction; /* MERGE action in progress */
-
/*
* Information about the changes that were made concurrently to a tuple
* being updated or deleted
@@ -153,14 +149,15 @@ static TupleTableSlot *ExecMerge(ModifyT
HeapTuple oldtuple,
bool canSetTag);
static void ExecInitMerge(ModifyTableState *mtstate, EState *estate);
-static bool ExecMergeMatched(ModifyTableContext *context,
- ResultRelInfo *resultRelInfo,
- ItemPointer tupleid,
- HeapTuple oldtuple,
- bool canSetTag);
-static void ExecMergeNotMatched(ModifyTableContext *context,
- ResultRelInfo *resultRelInfo,
- bool canSetTag);
+static TupleTableSlot *ExecMergeMatched(ModifyTableContext *context,
+ ResultRelInfo *resultRelInfo,
+ ItemPointer tupleid,
+ HeapTuple oldtuple,
+ bool canSetTag,
+ bool *matched);
+static TupleTableSlot *ExecMergeNotMatched(ModifyTableContext *context,
+ ResultRelInfo *resultRelInfo,
+ bool canSetTag);
/*
@@ -980,7 +977,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;
@@ -1834,7 +1831,7 @@ ExecCrossPartitionUpdate(ModifyTableCont
* additional rechecking, and might end up executing a different
* action entirely).
*/
- if (context->relaction != NULL)
+ if (mtstate->operation == CMD_MERGE)
return *tmresult == TM_Ok;
else if (TupIsNull(epqslot))
return true;
@@ -2075,7 +2072,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->operation == CMD_MERGE)
return result;
/*
@@ -2716,6 +2713,7 @@ static TupleTableSlot *
ExecMerge(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
ItemPointer tupleid, HeapTuple oldtuple, bool canSetTag)
{
+ TupleTableSlot *rslot = NULL;
bool matched;
/*-----
@@ -2764,19 +2762,18 @@ ExecMerge(ModifyTableContext *context, R
*/
matched = tupleid != NULL || oldtuple != NULL;
if (matched)
- matched = ExecMergeMatched(context, resultRelInfo, tupleid, oldtuple,
- canSetTag);
+ rslot = ExecMergeMatched(context, resultRelInfo, tupleid, oldtuple,
+ canSetTag, &matched);
/*
- * Either we were dealing with a NOT MATCHED tuple or ExecMergeMatched()
- * returned "false", indicating the previously MATCHED tuple no longer
- * matches.
+ * Deal with the NOT MATCHED case (either a NOT MATCHED tuple from the
+ * join, or a previously MATCHED tuple for which ExecMergeMatched() set
+ * "matched" to false, indicating that it no longer matches).
*/
if (!matched)
- ExecMergeNotMatched(context, resultRelInfo, canSetTag);
+ rslot = ExecMergeNotMatched(context, resultRelInfo, canSetTag);
- /* No RETURNING support yet */
- return NULL;
+ return rslot;
}
/*
@@ -2788,8 +2785,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.
*
@@ -2798,16 +2795,18 @@ 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, HeapTuple oldtuple, bool canSetTag)
+ ItemPointer tupleid, HeapTuple oldtuple, bool canSetTag,
+ bool *matched)
{
ModifyTableState *mtstate = context->mtstate;
- TupleTableSlot *newslot;
+ TupleTableSlot *rslot = NULL;
+ TupleTableSlot *newslot = NULL;
EState *estate = context->estate;
ExprContext *econtext = mtstate->ps.ps_ExprContext;
bool isNull;
@@ -2818,7 +2817,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
@@ -2908,12 +2910,15 @@ lmerge_matched:
*/
newslot = ExecProject(relaction->mas_proj);
- context->relaction = relaction;
+ mtstate->mt_merge_action = relaction;
if (!ExecUpdatePrologue(context, resultRelInfo,
tupleid, NULL, newslot, &result))
{
if (result == TM_Ok)
- return true; /* "do nothing" */
+ {
+ *matched = true;
+ return NULL; /* "do nothing" */
+ }
break; /* concurrent update/delete */
}
@@ -2923,7 +2928,10 @@ lmerge_matched:
{
if (!ExecIRUpdateTriggers(estate, resultRelInfo,
oldtuple, newslot))
- return true; /* "do nothing" */
+ {
+ *matched = true;
+ return NULL; /* "do nothing" */
+ }
}
else
{
@@ -2936,12 +2944,15 @@ lmerge_matched:
* cross-partition update was done, then there's nothing
* else for us to do --- the UPDATE has been turned into a
* DELETE and an INSERT, and we must not perform any of
- * the usual post-update tasks.
+ * the usual post-update tasks. Also, the RETURNING tuple
+ * (if any) has been projected, so we can just return
+ * that.
*/
if (updateCxt.crossPartUpdate)
{
mtstate->mt_merge_updated += 1;
- return true;
+ *matched = true;
+ return context->cpUpdateReturningSlot;
}
}
@@ -2954,12 +2965,15 @@ lmerge_matched:
break;
case CMD_DELETE:
- context->relaction = relaction;
+ mtstate->mt_merge_action = relaction;
if (!ExecDeletePrologue(context, resultRelInfo, tupleid,
NULL, NULL, &result))
{
if (result == TM_Ok)
- return true; /* "do nothing" */
+ {
+ *matched = true;
+ return NULL; /* "do nothing" */
+ }
break; /* concurrent update/delete */
}
@@ -2969,7 +2983,10 @@ lmerge_matched:
{
if (!ExecIRDeleteTriggers(estate, resultRelInfo,
oldtuple))
- return true; /* "do nothing" */
+ {
+ *matched = true;
+ return NULL; /* "do nothing" */
+ }
}
else
result = ExecDeleteAct(context, resultRelInfo, tupleid,
@@ -3027,7 +3044,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:
{
@@ -3073,13 +3091,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
@@ -3114,7 +3138,8 @@ lmerge_matched:
* tuple already deleted; tell caller to run NOT
* MATCHED actions
*/
- return false;
+ *matched = false;
+ return NULL;
case TM_SelfModified:
@@ -3134,13 +3159,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;
}
}
@@ -3152,6 +3178,31 @@ 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, "unrecognized commandType: %d",
+ (int) commandType);
+ }
+ }
+
/*
* We've activated one of the WHEN clauses, so we don't search
* further. This is required behaviour, not an optimization.
@@ -3162,19 +3213,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;
/*
@@ -3224,10 +3278,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:
@@ -3243,6 +3297,8 @@ ExecMergeNotMatched(ModifyTableContext *
*/
break;
}
+
+ return rslot;
}
/*
@@ -3705,9 +3761,17 @@ ExecModifyTable(PlanState *pstate)
{
EvalPlanQualSetSlot(&node->mt_epqstate, context.planSlot);
- ExecMerge(&context, node->resultRelInfo, NULL, NULL,
- node->canSetTag);
- continue; /* no RETURNING support yet */
+ slot = ExecMerge(&context, node->resultRelInfo,
+ NULL, 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");
@@ -3784,9 +3848,17 @@ ExecModifyTable(PlanState *pstate)
{
EvalPlanQualSetSlot(&node->mt_epqstate, context.planSlot);
- ExecMerge(&context, node->resultRelInfo, NULL, NULL,
- node->canSetTag);
- continue; /* no RETURNING support yet */
+ slot = ExecMerge(&context, node->resultRelInfo,
+ NULL, 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");
@@ -3833,9 +3905,17 @@ ExecModifyTable(PlanState *pstate)
{
EvalPlanQualSetSlot(&node->mt_epqstate, context.planSlot);
- ExecMerge(&context, node->resultRelInfo, NULL, NULL,
- node->canSetTag);
- continue; /* no RETURNING support yet */
+ slot = ExecMerge(&context, node->resultRelInfo,
+ NULL, 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, "wholerow is NULL");
@@ -3897,7 +3977,6 @@ ExecModifyTable(PlanState *pstate)
}
slot = ExecGetUpdateNewTuple(resultRelInfo, context.planSlot,
oldSlot);
- context.relaction = NULL;
/* Now apply the update. */
slot = ExecUpdate(&context, resultRelInfo, tupleid, oldtuple,
diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c
new file mode 100644
index d71484c..abb8b0d
--- a/src/backend/executor/spi.c
+++ b/src/backend/executor/spi.c
@@ -2033,6 +2033,8 @@ SPI_result_code_string(int code)
return "SPI_OK_TD_REGISTER";
case SPI_OK_MERGE:
return "SPI_OK_MERGE";
+ case SPI_OK_MERGE_RETURNING:
+ return "SPI_OK_MERGE_RETURNING";
}
/* Unrecognized code ... return something useful ... */
sprintf(buf, "Unrecognized SPI code %d", code);
@@ -2886,7 +2888,10 @@ _SPI_pquery(QueryDesc *queryDesc, bool f
res = SPI_OK_UPDATE;
break;
case CMD_MERGE:
- res = SPI_OK_MERGE;
+ if (queryDesc->plannedstmt->hasReturning)
+ res = SPI_OK_MERGE_RETURNING;
+ else
+ res = SPI_OK_MERGE;
break;
default:
return SPI_ERROR_OPUNKNOWN;
diff --git a/src/backend/jit/llvm/llvmjit_expr.c b/src/backend/jit/llvm/llvmjit_expr.c
new file mode 100644
index 0c44842..a5cd6fb
--- a/src/backend/jit/llvm/llvmjit_expr.c
+++ b/src/backend/jit/llvm/llvmjit_expr.c
@@ -1986,6 +1986,12 @@ llvm_compile_expr(ExprState *state)
break;
}
+ case EEOP_MERGING_FUNC:
+ build_EvalXFunc(b, mod, "ExecEvalMergingFunc",
+ v_state, op, v_econtext);
+ LLVMBuildBr(b, opblocks[opno + 1]);
+ break;
+
case EEOP_SUBPLAN:
build_EvalXFunc(b, mod, "ExecEvalSubPlan",
v_state, op, v_econtext);
diff --git a/src/backend/jit/llvm/llvmjit_types.c b/src/backend/jit/llvm/llvmjit_types.c
new file mode 100644
index 47c9daf..912d9ad
--- a/src/backend/jit/llvm/llvmjit_types.c
+++ b/src/backend/jit/llvm/llvmjit_types.c
@@ -155,6 +155,7 @@ void *referenced_functions[] =
ExecEvalFuncExprFusage,
ExecEvalFuncExprStrictFusage,
ExecEvalGroupingFunc,
+ ExecEvalMergingFunc,
ExecEvalMinMax,
ExecEvalNextValueExpr,
ExecEvalParamExec,
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
new file mode 100644
index e1a5bc7..2468063
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -67,6 +67,9 @@ exprType(const Node *expr)
case T_WindowFunc:
type = ((const WindowFunc *) expr)->wintype;
break;
+ case T_MergingFunc:
+ type = ((const MergingFunc *) expr)->mftype;
+ break;
case T_SubscriptingRef:
type = ((const SubscriptingRef *) expr)->refrestype;
break;
@@ -810,6 +813,9 @@ exprCollation(const Node *expr)
case T_WindowFunc:
coll = ((const WindowFunc *) expr)->wincollid;
break;
+ case T_MergingFunc:
+ coll = ((const MergingFunc *) expr)->mfcollid;
+ break;
case T_SubscriptingRef:
coll = ((const SubscriptingRef *) expr)->refcollid;
break;
@@ -1085,6 +1091,9 @@ exprSetCollation(Node *expr, Oid collati
case T_WindowFunc:
((WindowFunc *) expr)->wincollid = collation;
break;
+ case T_MergingFunc:
+ ((MergingFunc *) expr)->mfcollid = collation;
+ break;
case T_SubscriptingRef:
((SubscriptingRef *) expr)->refcollid = collation;
break;
@@ -1343,6 +1352,9 @@ exprLocation(const Node *expr)
/* function name should always be the first thing */
loc = ((const WindowFunc *) expr)->location;
break;
+ case T_MergingFunc:
+ loc = ((const MergingFunc *) expr)->location;
+ break;
case T_SubscriptingRef:
/* just use container argument's location */
loc = exprLocation((Node *) ((const SubscriptingRef *) expr)->refexpr);
@@ -2035,6 +2047,7 @@ expression_tree_walker_impl(Node *node,
case T_RangeTblRef:
case T_SortGroupClause:
case T_CTESearchClause:
+ case T_MergingFunc:
/* primitive node types with no expression subnodes */
break;
case T_WithCheckOption:
@@ -2869,6 +2882,7 @@ expression_tree_mutator_impl(Node *node,
case T_RangeTblRef:
case T_SortGroupClause:
case T_CTESearchClause:
+ case T_MergingFunc:
return (Node *) copyObject(node);
case T_WithCheckOption:
{
@@ -3833,6 +3847,7 @@ raw_expression_tree_walker_impl(Node *no
case T_ParamRef:
case T_A_Const:
case T_A_Star:
+ case T_MergingFunc:
/* primitive node types with no subnodes */
break;
case T_Alias:
@@ -4053,6 +4068,8 @@ raw_expression_tree_walker_impl(Node *no
return true;
if (WALK(stmt->mergeWhenClauses))
return true;
+ if (WALK(stmt->returningList))
+ return true;
if (WALK(stmt->withClause))
return true;
}
diff --git a/src/backend/optimizer/plan/subselect.c b/src/backend/optimizer/plan/subselect.c
new file mode 100644
index 47e1472..b6a83c2
--- a/src/backend/optimizer/plan/subselect.c
+++ b/src/backend/optimizer/plan/subselect.c
@@ -1855,7 +1855,8 @@ convert_EXISTS_to_ANY(PlannerInfo *root,
/*
* Replace correlation vars (uplevel vars) with Params.
*
- * Uplevel PlaceHolderVars and aggregates are replaced, too.
+ * Uplevel PlaceHolderVars, aggregates, GROUPING() expressions, and MERGING()
+ * expressions are replaced, too.
*
* Note: it is critical that this runs immediately after SS_process_sublinks.
* Since we do not recurse into the arguments of uplevel PHVs and aggregates,
@@ -1909,6 +1910,11 @@ replace_correlation_vars_mutator(Node *n
if (((GroupingFunc *) node)->agglevelsup > 0)
return (Node *) replace_outer_grouping(root, (GroupingFunc *) node);
}
+ if (IsA(node, MergingFunc))
+ {
+ if (root->parse->commandType != CMD_MERGE)
+ return (Node *) replace_outer_merging(root, (MergingFunc *) node);
+ }
return expression_tree_mutator(node,
replace_correlation_vars_mutator,
(void *) root);
diff --git a/src/backend/optimizer/util/paramassign.c b/src/backend/optimizer/util/paramassign.c
new file mode 100644
index a58da7c..6ea6db2
--- a/src/backend/optimizer/util/paramassign.c
+++ b/src/backend/optimizer/util/paramassign.c
@@ -308,6 +308,56 @@ replace_outer_grouping(PlannerInfo *root
}
/*
+ * Generate a Param node to replace the given MergingFunc expression which is
+ * expected to be in the RETURNING list of an upper-level MERGE query. Record
+ * the need for the MergingFunc in the proper upper-level root->plan_params.
+ */
+Param *
+replace_outer_merging(PlannerInfo *root, MergingFunc *mf)
+{
+ Param *retval;
+ PlannerParamItem *pitem;
+ Oid ptype = exprType((Node *) mf);
+
+ Assert(root->parse->commandType != CMD_MERGE);
+
+ /*
+ * The parser should have ensured that the MergingFunc is in the RETURNING
+ * list of an upper-level MERGE query, so find that query.
+ */
+ do
+ {
+ root = root->parent_root;
+ if (root == NULL)
+ elog(ERROR, "MergingFunc found outside MERGE");
+ } while (root->parse->commandType != CMD_MERGE);
+
+ /*
+ * It does not seem worthwhile to try to de-duplicate references to outer
+ * MergingFunc expressions. Just make a new slot every time.
+ */
+ mf = copyObject(mf);
+
+ pitem = makeNode(PlannerParamItem);
+ pitem->item = (Node *) mf;
+ pitem->paramId = list_length(root->glob->paramExecTypes);
+ root->glob->paramExecTypes = lappend_oid(root->glob->paramExecTypes,
+ ptype);
+
+ root->plan_params = lappend(root->plan_params, pitem);
+
+ retval = makeNode(Param);
+ retval->paramkind = PARAM_EXEC;
+ retval->paramid = pitem->paramId;
+ retval->paramtype = ptype;
+ retval->paramtypmod = -1;
+ retval->paramcollid = InvalidOid;
+ retval->location = mf->location;
+
+ return retval;
+}
+
+/*
* Generate a Param node to replace the given Var,
* which is expected to come from some upper NestLoop plan node.
* Record the need for the Var in root->curOuterParams.
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
new file mode 100644
index dbdf6bf..44893d8
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -74,7 +74,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,
@@ -553,7 +552,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;
@@ -980,7 +980,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;
@@ -2445,7 +2446,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
@@ -2542,10 +2544,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;
@@ -2562,7 +2565,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 130f7fc..04fee0d
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -694,7 +694,7 @@ static Node *makeRecursiveViewSelect(cha
BOOLEAN_P BOTH BREADTH BY
CACHE CALL CALLED CASCADE CASCADED CASE CAST CATALOG_P CHAIN CHAR_P
- CHARACTER CHARACTERISTICS CHECK CHECKPOINT CLASS CLOSE
+ CHARACTER CHARACTERISTICS CHECK CHECKPOINT CLASS CLAUSE_NUMBER CLOSE
CLUSTER COALESCE COLLATE COLLATION COLUMN COLUMNS COMMENT COMMENTS COMMIT
COMMITTED COMPRESSION CONCURRENTLY CONFIGURATION CONFLICT
CONNECTION CONSTRAINT CONSTRAINTS CONTENT_P CONTINUE_P CONVERSION_P COPY
@@ -732,7 +732,7 @@ static Node *makeRecursiveViewSelect(cha
LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL
LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOGGED
- MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE METHOD
+ MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE MERGING METHOD
MINUTE_P MINVALUE MODE MONTH_P MOVE
NAME_P NAMES NATIONAL NATURAL NCHAR NEW NEXT NFC NFD NFKC NFKD NO NONE
@@ -12368,6 +12368,7 @@ MergeStmt:
USING table_ref
ON a_expr
merge_when_list
+ returning_clause
{
MergeStmt *m = makeNode(MergeStmt);
@@ -12376,6 +12377,7 @@ MergeStmt:
m->sourceRelation = $6;
m->joinCondition = $8;
m->mergeWhenClauses = $9;
+ m->returningList = $10;
$$ = (Node *) m;
}
@@ -15789,6 +15791,24 @@ func_expr_common_subexpr:
n->location = @1;
$$ = (Node *) n;
}
+ | MERGING '(' ACTION ')'
+ {
+ MergingFunc *m = makeNode(MergingFunc);
+
+ m->mfop = MERGING_ACTION;
+ m->mftype = TEXTOID;
+ m->location = @1;
+ $$ = (Node *) m;
+ }
+ | MERGING '(' CLAUSE_NUMBER ')'
+ {
+ MergingFunc *m = makeNode(MergingFunc);
+
+ m->mfop = MERGING_CLAUSE_NUMBER;
+ m->mftype = INT4OID;
+ m->location = @1;
+ $$ = (Node *) m;
+ }
;
@@ -17167,6 +17187,7 @@ unreserved_keyword:
| CHARACTERISTICS
| CHECKPOINT
| CLASS
+ | CLAUSE_NUMBER
| CLOSE
| CLUSTER
| COLUMNS
@@ -17486,6 +17507,7 @@ col_name_keyword:
| JSON_SCALAR
| JSON_SERIALIZE
| LEAST
+ | MERGING
| NATIONAL
| NCHAR
| NONE
@@ -17704,6 +17726,7 @@ bare_label_keyword:
| CHECK
| CHECKPOINT
| CLASS
+ | CLAUSE_NUMBER
| CLOSE
| CLUSTER
| COALESCE
@@ -17875,6 +17898,7 @@ bare_label_keyword:
| MATERIALIZED
| MAXVALUE
| MERGE
+ | MERGING
| METHOD
| MINVALUE
| MODE
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
new file mode 100644
index 9d151a8..bee7d83
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -468,6 +468,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:
@@ -915,6 +916,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 3c88c9a..6826d4f
--- 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 9300c7b..ab7e564
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -54,6 +54,7 @@ static Node *transformAExprDistinct(Pars
static Node *transformAExprNullIf(ParseState *pstate, A_Expr *a);
static Node *transformAExprIn(ParseState *pstate, A_Expr *a);
static Node *transformAExprBetween(ParseState *pstate, A_Expr *a);
+static Node *transformMergingFunc(ParseState *pstate, MergingFunc *f);
static Node *transformBoolExpr(ParseState *pstate, BoolExpr *a);
static Node *transformFuncCall(ParseState *pstate, FuncCall *fn);
static Node *transformMultiAssignRef(ParseState *pstate, MultiAssignRef *maref);
@@ -227,6 +228,10 @@ transformExprRecurse(ParseState *pstate,
result = transformGroupingFunc(pstate, (GroupingFunc *) expr);
break;
+ case T_MergingFunc:
+ result = transformMergingFunc(pstate, (MergingFunc *) expr);
+ break;
+
case T_NamedArgExpr:
{
NamedArgExpr *na = (NamedArgExpr *) expr;
@@ -541,6 +546,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:
@@ -1354,6 +1360,32 @@ transformAExprBetween(ParseState *pstate
}
static Node *
+transformMergingFunc(ParseState *pstate, MergingFunc *f)
+{
+ /*
+ * All we need to do is check that we're in the RETURNING list of a MERGE
+ * command. If so, we just return the node as-is.
+ */
+ if (pstate->p_expr_kind != EXPR_KIND_MERGE_RETURNING)
+ {
+ ParseState *parent_pstate = pstate->parentParseState;
+
+ while (parent_pstate &&
+ parent_pstate->p_expr_kind != EXPR_KIND_MERGE_RETURNING)
+ parent_pstate = parent_pstate->parentParseState;
+
+ if (!parent_pstate ||
+ parent_pstate->p_expr_kind != EXPR_KIND_MERGE_RETURNING)
+ ereport(ERROR,
+ errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("MERGING() can only be used in the RETURNING list of a MERGE command"),
+ parser_errposition(pstate, f->location));
+ }
+
+ return (Node *) f;
+}
+
+static Node *
transformBoolExpr(ParseState *pstate, BoolExpr *a)
{
List *args = NIL;
@@ -1767,6 +1799,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:
@@ -3115,6 +3148,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 fdb3e6d..0cbc950
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -2599,6 +2599,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 4356d61..c568a59
--- a/src/backend/parser/parse_merge.c
+++ b/src/backend/parser/parse_merge.c
@@ -237,6 +237,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.
@@ -258,6 +262,7 @@ transformMergeStmt(ParseState *pstate, M
MergeAction *action;
action = makeNode(MergeAction);
+ action->index = foreach_current_index(l) + 1;
action->commandType = mergeWhenClause->commandType;
action->matched = mergeWhenClause->matched;
@@ -394,9 +399,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 34a0ec5..60b7e4f
--- a/src/backend/parser/parse_relation.c
+++ b/src/backend/parser/parse_relation.c
@@ -2346,9 +2346,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/parser/parse_target.c b/src/backend/parser/parse_target.c
new file mode 100644
index 0cd904f..eab78f4
--- a/src/backend/parser/parse_target.c
+++ b/src/backend/parser/parse_target.c
@@ -1807,6 +1807,20 @@ FigureColnameInternal(Node *node, char *
/* make GROUPING() act like a regular function */
*name = "grouping";
return 2;
+ case T_MergingFunc:
+ switch (((MergingFunc *) node)->mfop)
+ {
+ case MERGING_ACTION:
+ *name = "action";
+ return 2;
+ case MERGING_CLAUSE_NUMBER:
+ *name = "clause_number";
+ return 2;
+ default:
+ elog(ERROR, "unrecognized MergingFuncOp: %d",
+ (int) ((MergingFunc *) node)->mfop);
+ }
+ break;
case T_SubLink:
switch (((SubLink *) node)->subLinkType)
{
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
new file mode 100644
index b8839b5..b8e883f
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -3834,9 +3834,9 @@ RewriteQuery(Query *parsetree, List *rew
ListCell *lc1;
/*
- * First, recursively process any insert/update/delete statements in WITH
- * clauses. (We have to do this first because the WITH clauses may get
- * copied into rule actions below.)
+ * First, recursively process any insert/update/delete/merge statements in
+ * WITH clauses. (We have to do this first because the WITH clauses may
+ * get copied into rule actions below.)
*/
foreach(lc1, parsetree->cteList)
{
@@ -3861,7 +3861,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/rewrite/rowsecurity.c b/src/backend/rewrite/rowsecurity.c
new file mode 100644
index 94268a2..65ce579
--- a/src/backend/rewrite/rowsecurity.c
+++ b/src/backend/rewrite/rowsecurity.c
@@ -395,10 +395,10 @@ get_row_security_policies(Query *root, R
* on the final action we take.
*
* We already fetched the SELECT policies above, to check existing rows,
- * but we must also check that new rows created by UPDATE actions are
- * visible, if SELECT rights are required for this relation. We don't do
- * this for INSERT actions, since an INSERT command would only do this
- * check if it had a RETURNING list, and MERGE does not support RETURNING.
+ * but we must also check that new rows created by INSERT/UPDATE actions
+ * are visible, if SELECT rights are required. For INSERT actions, we only
+ * do this if RETURNING is specified, to be consistent with a plain INSERT
+ * command, which can only require SELECT rights when RETURNING is used.
*
* 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
@@ -420,6 +420,8 @@ get_row_security_policies(Query *root, R
List *merge_delete_restrictive_policies;
List *merge_insert_permissive_policies;
List *merge_insert_restrictive_policies;
+ List *merge_select_permissive_policies = NIL;
+ List *merge_select_restrictive_policies = NIL;
/*
* Fetch the UPDATE policies and set them up to execute on the
@@ -457,9 +459,6 @@ get_row_security_policies(Query *root, R
*/
if (perminfo->requiredPerms & ACL_SELECT)
{
- List *merge_select_permissive_policies;
- List *merge_select_restrictive_policies;
-
get_policies_for_relation(rel, CMD_SELECT, user_id,
&merge_select_permissive_policies,
&merge_select_restrictive_policies);
@@ -508,6 +507,21 @@ get_row_security_policies(Query *root, R
withCheckOptions,
hasSubLinks,
false);
+
+ /*
+ * Add ALL/SELECT policies as WCO_RLS_INSERT_CHECK WCOs, to ensure
+ * that the inserted row is visible when executing an INSERT action,
+ * if RETURNING is specified and SELECT rights are required for this
+ * relation.
+ */
+ if (perminfo->requiredPerms & ACL_SELECT && root->returningList)
+ add_with_check_options(rel, rt_index,
+ WCO_RLS_INSERT_CHECK,
+ merge_select_permissive_policies,
+ merge_select_restrictive_policies,
+ withCheckOptions,
+ hasSubLinks,
+ true);
}
table_close(rel, NoLock);
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
new file mode 100644
index 8de821f..f84a943
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -2145,11 +2145,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/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
new file mode 100644
index a928a8c..ff999d3
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -7197,8 +7197,13 @@ get_merge_query_def(Query *query, depars
appendStringInfoString(buf, "DO NOTHING");
}
- /* No RETURNING support in MERGE yet */
- Assert(query->returningList == NIL);
+ /* Add RETURNING if present */
+ if (query->returningList)
+ {
+ appendContextKeyword(context, " RETURNING",
+ -PRETTYINDENT_STD, PRETTYINDENT_STD, 1);
+ get_target_list(query->returningList, context, NULL, colNamesVisible);
+ }
}
@@ -8298,6 +8303,7 @@ isSimpleNode(Node *node, Node *parentNod
case T_Aggref:
case T_GroupingFunc:
case T_WindowFunc:
+ case T_MergingFunc:
case T_FuncExpr:
case T_JsonConstructorExpr:
/* function-like: name(..) or name[..] */
@@ -8652,6 +8658,27 @@ get_rule_expr(Node *node, deparse_contex
get_windowfunc_expr((WindowFunc *) node, context);
break;
+ case T_MergingFunc:
+ {
+ MergingFunc *mexpr = (MergingFunc *) node;
+
+ appendStringInfoString(buf, "MERGING(");
+ switch (mexpr->mfop)
+ {
+ case MERGING_ACTION:
+ appendStringInfoString(buf, "ACTION");
+ break;
+ case MERGING_CLAUSE_NUMBER:
+ appendStringInfoString(buf, "CLAUSE_NUMBER");
+ break;
+ default:
+ elog(ERROR, "unrecognized MergingFuncOp: %d",
+ (int) mexpr->mfop);
+ }
+ appendStringInfoChar(buf, ')');
+ }
+ break;
+
case T_SubscriptingRef:
{
SubscriptingRef *sbsref = (SubscriptingRef *) node;
diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c
new file mode 100644
index 76e01b0..2830bde
--- a/src/bin/psql/common.c
+++ b/src/bin/psql/common.c
@@ -983,13 +983,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/executor/execExpr.h b/src/include/executor/execExpr.h
new file mode 100644
index a28ddcd..abb0fb4
--- a/src/include/executor/execExpr.h
+++ b/src/include/executor/execExpr.h
@@ -243,6 +243,7 @@ typedef enum ExprEvalOp
EEOP_AGGREF,
EEOP_GROUPING_FUNC,
EEOP_WINDOW_FUNC,
+ EEOP_MERGING_FUNC,
EEOP_SUBPLAN,
/* aggregation related nodes */
@@ -626,6 +627,12 @@ typedef struct ExprEvalStep
WindowFuncExprState *wfstate;
} window_func;
+ /* for EEOP_MERGING_FUNC */
+ struct
+ {
+ MergingFuncOp mfop; /* the MERGING() operation to perform */
+ } merging_func;
+
/* for EEOP_SUBPLAN */
struct
{
@@ -810,6 +817,8 @@ extern void ExecEvalJsonConstructor(Expr
ExprContext *econtext);
extern void ExecEvalJsonIsPredicate(ExprState *state, ExprEvalStep *op);
extern void ExecEvalGroupingFunc(ExprState *state, ExprEvalStep *op);
+extern void ExecEvalMergingFunc(ExprState *state, ExprEvalStep *op,
+ ExprContext *econtext);
extern void ExecEvalSubPlan(ExprState *state, ExprEvalStep *op,
ExprContext *econtext);
extern void ExecEvalWholeRowVar(ExprState *state, ExprEvalStep *op,
diff --git a/src/include/executor/spi.h b/src/include/executor/spi.h
new file mode 100644
index c13e1f6..48b8773
--- a/src/include/executor/spi.h
+++ b/src/include/executor/spi.h
@@ -97,6 +97,7 @@ typedef struct _SPI_plan *SPIPlanPtr;
#define SPI_OK_REL_UNREGISTER 16
#define SPI_OK_TD_REGISTER 17
#define SPI_OK_MERGE 18
+#define SPI_OK_MERGE_RETURNING 19
#define SPI_OPT_NONATOMIC (1 << 0)
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
new file mode 100644
index 444a5f0..57fdfca
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1325,6 +1325,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 d60e148..21d8d64
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -1935,6 +1935,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/nodes/primnodes.h b/src/include/nodes/primnodes.h
new file mode 100644
index 4a15460..97806f5
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -572,6 +572,39 @@ typedef struct WindowFunc
} WindowFunc;
/*
+ * MergingFunc
+ *
+ * A MergingFunc is a MERGING(...) expression that can only appear in the
+ * RETURNING list of a MERGE command. It returns information about the
+ * currently executing merge action. Possible operations are:
+ *
+ * MERGING_ACTION:
+ * Return the command string of the current merge action ("INSERT",
+ * "UPDATE" or "DELETE").
+ *
+ * MERGING_CLAUSE_NUMBER:
+ * Return the 1-based index of the current merge WHEN clause.
+ */
+typedef enum MergingFuncOp
+{
+ MERGING_ACTION,
+ MERGING_CLAUSE_NUMBER,
+} MergingFuncOp;
+
+typedef struct MergingFunc
+{
+ Expr xpr;
+ /* operation to perform */
+ MergingFuncOp mfop;
+ /* type Oid of result */
+ Oid mftype pg_node_attr(query_jumble_ignore);
+ /* OID of collation, or InvalidOid if none */
+ Oid mfcollid pg_node_attr(query_jumble_ignore);
+ /* token location, or -1 if unknown */
+ int location;
+} MergingFunc;
+
+/*
* SubscriptingRef: describes a subscripting operation over a container
* (array, etc).
*
@@ -1735,6 +1768,7 @@ typedef struct BooleanTest
typedef struct MergeAction
{
NodeTag type;
+ int index; /* 1-based index of the clause */
bool matched; /* true=MATCHED, false=NOT MATCHED */
CmdType commandType; /* INSERT/UPDATE/DELETE/DO NOTHING */
/* OVERRIDING clause */
diff --git a/src/include/optimizer/paramassign.h b/src/include/optimizer/paramassign.h
new file mode 100644
index 501f728..1e95c8f
--- a/src/include/optimizer/paramassign.h
+++ b/src/include/optimizer/paramassign.h
@@ -20,6 +20,7 @@ extern Param *replace_outer_placeholderv
PlaceHolderVar *phv);
extern Param *replace_outer_agg(PlannerInfo *root, Aggref *agg);
extern Param *replace_outer_grouping(PlannerInfo *root, GroupingFunc *grp);
+extern Param *replace_outer_merging(PlannerInfo *root, MergingFunc *mf);
extern Param *replace_nestloop_param_var(PlannerInfo *root, Var *var);
extern Param *replace_nestloop_param_placeholdervar(PlannerInfo *root,
PlaceHolderVar *phv);
diff --git a/src/include/parser/analyze.h b/src/include/parser/analyze.h
new file mode 100644
index 5ecdd10..28b66fc
--- 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/kwlist.h b/src/include/parser/kwlist.h
new file mode 100644
index 2331aca..9c5d6ea
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -80,6 +80,7 @@ PG_KEYWORD("characteristics", CHARACTERI
PG_KEYWORD("check", CHECK, RESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("checkpoint", CHECKPOINT, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("class", CLASS, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("clause_number", CLAUSE_NUMBER, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("close", CLOSE, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("cluster", CLUSTER, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("coalesce", COALESCE, COL_NAME_KEYWORD, BARE_LABEL)
@@ -266,6 +267,7 @@ PG_KEYWORD("matched", MATCHED, UNRESERVE
PG_KEYWORD("materialized", MATERIALIZED, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("maxvalue", MAXVALUE, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("merge", MERGE, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("merging", MERGING, COL_NAME_KEYWORD, BARE_LABEL)
PG_KEYWORD("method", METHOD, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("minute", MINUTE_P, UNRESERVED_KEYWORD, AS_LABEL)
PG_KEYWORD("minvalue", MINVALUE, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
new file mode 100644
index 99d6515..5b781d8
--- 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/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
new file mode 100644
index 6d16913..51de094
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -4267,9 +4267,9 @@ exec_stmt_execsql(PLpgSQL_execstate *est
/*
* If we have INTO, then we only need one row back ... but if we have INTO
* STRICT or extra check too_many_rows, ask for two rows, so that we can
- * verify the statement returns only one. INSERT/UPDATE/DELETE are always
- * treated strictly. Without INTO, just run the statement to completion
- * (tcount = 0).
+ * verify the statement returns only one. INSERT/UPDATE/DELETE/MERGE are
+ * always treated strictly. Without INTO, just run the statement to
+ * completion (tcount = 0).
*
* We could just ask for two rows always when using INTO, but there are
* some cases where demanding the extra row costs significant time, eg by
@@ -4307,10 +4307,11 @@ exec_stmt_execsql(PLpgSQL_execstate *est
case SPI_OK_INSERT:
case SPI_OK_UPDATE:
case SPI_OK_DELETE:
+ case SPI_OK_MERGE:
case SPI_OK_INSERT_RETURNING:
case SPI_OK_UPDATE_RETURNING:
case SPI_OK_DELETE_RETURNING:
- case SPI_OK_MERGE:
+ case SPI_OK_MERGE_RETURNING:
Assert(stmt->mod_stmt);
exec_set_found(estate, (SPI_processed != 0));
break;
@@ -4489,10 +4490,11 @@ exec_stmt_dynexecute(PLpgSQL_execstate *
case SPI_OK_INSERT:
case SPI_OK_UPDATE:
case SPI_OK_DELETE:
+ case SPI_OK_MERGE:
case SPI_OK_INSERT_RETURNING:
case SPI_OK_UPDATE_RETURNING:
case SPI_OK_DELETE_RETURNING:
- case SPI_OK_MERGE:
+ case SPI_OK_MERGE_RETURNING:
case SPI_OK_UTILITY:
case SPI_OK_REWRITTEN:
break;
diff --git a/src/pl/tcl/pltcl.c b/src/pl/tcl/pltcl.c
new file mode 100644
index 6187e15..18d14a2
--- a/src/pl/tcl/pltcl.c
+++ b/src/pl/tcl/pltcl.c
@@ -2456,6 +2456,7 @@ pltcl_process_SPI_result(Tcl_Interp *int
case SPI_OK_INSERT_RETURNING:
case SPI_OK_DELETE_RETURNING:
case SPI_OK_UPDATE_RETURNING:
+ case SPI_OK_MERGE_RETURNING:
/*
* Process the tuples we got
diff --git a/src/test/regress/expected/merge.out b/src/test/regress/expected/merge.out
new file mode 100644
index e3ebf46..778e7c8
--- a/src/test/regress/expected/merge.out
+++ b/src/test/regress/expected/merge.out
@@ -124,20 +124,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
-- materialized view
CREATE MATERIALIZED VIEW mv AS SELECT * FROM target;
@@ -1255,7 +1255,7 @@ BEGIN;
MERGE INTO sq_target
USING v
ON tid = sid
-WHEN MATCHED AND tid > 2 THEN
+WHEN MATCHED AND tid >= 2 THEN
UPDATE SET balance = balance + delta
WHEN NOT MATCHED THEN
INSERT (balance, tid) VALUES (balance + delta, sid)
@@ -1270,7 +1270,7 @@ INSERT INTO sq_source (sid, balance, del
MERGE INTO sq_target t
USING v
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)
@@ -1294,7 +1294,7 @@ WITH targq AS (
MERGE INTO sq_target t
USING v
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)
@@ -1303,20 +1303,191 @@ WHEN MATCHED AND tid < 2 THEN
ROLLBACK;
-- RETURNING
BEGIN;
-INSERT INTO sq_source (sid, balance, delta) VALUES (-1, -1, -10);
+CREATE TABLE merge_actions(action text, abbrev text);
+INSERT INTO merge_actions VALUES ('INSERT', 'ins'), ('UPDATE', 'upd'), ('DELETE', 'del');
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)
+ 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 *;
- ^
+ DELETE
+RETURNING MERGING(CLAUSE_NUMBER) AS clause,
+ (SELECT abbrev FROM merge_actions
+ WHERE action = MERGING(ACTION)) AS action,
+ t.*,
+ CASE MERGING(ACTION)
+ WHEN 'INSERT' THEN 'Inserted '||t
+ WHEN 'UPDATE' THEN 'Added '||delta||' to balance'
+ WHEN 'DELETE' THEN 'Removed '||t
+ END AS description;
+ clause | action | tid | balance | description
+--------+--------+-----+---------+---------------------
+ 3 | del | 1 | 100 | Removed (1,100)
+ 1 | upd | 2 | 220 | Added 20 to balance
+ 2 | ins | 4 | 40 | Inserted (4,40)
+(3 rows)
+
+ROLLBACK;
+-- error when using MERGING() outside MERGE
+SELECT MERGING(ACTION) FROM sq_target;
+ERROR: MERGING() can only be used in the RETURNING list of a MERGE command
+LINE 1: SELECT MERGING(ACTION) FROM sq_target;
+ ^
+UPDATE sq_target SET balance = balance + 1 RETURNING MERGING(CLAUSE_NUMBER);
+ERROR: MERGING() can only be used in the RETURNING list of a MERGE command
+LINE 1: ...ATE sq_target SET balance = balance + 1 RETURNING MERGING(CL...
+ ^
+-- RETURNING in CTEs
+CREATE TABLE sq_target_merge_log (tid integer NOT NULL, last_change text);
+INSERT INTO sq_target_merge_log VALUES (1, 'Original value');
+BEGIN;
+WITH m AS (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 MERGING(CLAUSE_NUMBER), MERGING(ACTION), t.*,
+ CASE MERGING(ACTION)
+ WHEN 'INSERT' THEN 'Inserted '||t
+ WHEN 'UPDATE' THEN 'Added '||delta||' to balance'
+ WHEN 'DELETE' THEN 'Removed '||t
+ END AS description
+), m2 AS (
+ MERGE INTO sq_target_merge_log l
+ USING m
+ ON l.tid = m.tid
+ WHEN MATCHED THEN
+ UPDATE SET last_change = description
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (m.tid, description)
+ RETURNING action, MERGING(ACTION) AS log_action, l.*
+)
+SELECT * FROM m2;
+ action | log_action | tid | last_change
+--------+------------+-----+---------------------
+ DELETE | UPDATE | 1 | Removed (1,100)
+ UPDATE | INSERT | 2 | Added 20 to balance
+ INSERT | INSERT | 4 | Inserted (4,40)
+(3 rows)
+
+SELECT * FROM sq_target_merge_log ORDER BY tid;
+ tid | last_change
+-----+---------------------
+ 1 | Removed (1,100)
+ 2 | Added 20 to balance
+ 4 | Inserted (4,40)
+(3 rows)
+
+ROLLBACK;
+-- COPY (MERGE ... RETURNING) TO ...
+BEGIN;
+COPY (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 MERGING(ACTION), t.*
+) TO stdout;
+DELETE 1 100
+UPDATE 2 220
+INSERT 4 40
+ROLLBACK;
+-- SQL function with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT action text, OUT tid int, OUT new_balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING MERGING(ACTION), t.*;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT action, tid, new_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ action | tid | new_balance
+--------+-----+-------------
+ DELETE | 1 | 100
+ UPDATE | 3 | 320
+ INSERT | 4 | 110
+(3 rows)
+
+ROLLBACK;
+-- SQL SRF with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_sq_source_into_sq_target()
+RETURNS TABLE (action text, tid int, balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 MERGING(ACTION), t.*;
+$$;
+SELECT * FROM merge_sq_source_into_sq_target();
+ action | tid | balance
+--------+-----+---------
+ DELETE | 1 | 100
+ UPDATE | 2 | 220
+ INSERT | 4 | 40
+(3 rows)
+
+ROLLBACK;
+-- PL/pgSQL function with MERGE ... RETURNING ... INTO
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT r_action text, OUT r_tid int, OUT r_balance int)
+LANGUAGE plpgsql AS
+$$
+BEGIN
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING MERGING(ACTION), t.* INTO r_action, r_tid, r_balance;
+END;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT r_action, r_tid, r_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ r_action | r_tid | r_balance
+----------+-------+-----------
+ DELETE | 1 | 100
+ UPDATE | 3 | 320
+ INSERT | 4 | 110
+(3 rows)
+
ROLLBACK;
-- EXPLAIN
CREATE TABLE ex_mtarget (a int, b int)
@@ -1513,7 +1684,7 @@ SELECT * FROM sq_target WHERE tid = 1;
(1 row)
ROLLBACK;
-DROP TABLE sq_target, sq_source CASCADE;
+DROP TABLE sq_target, sq_target_merge_log, sq_source CASCADE;
NOTICE: drop cascades to view v
CREATE TABLE pa_target (tid integer, balance float, val text)
PARTITION BY LIST (tid);
@@ -1639,6 +1810,32 @@ SELECT * FROM pa_target ORDER BY tid;
(14 rows)
ROLLBACK;
+-- update partition key to partition not initially scanned
+BEGIN;
+MERGE INTO pa_target t
+ USING pa_source s
+ ON t.tid = s.sid AND t.tid = 1
+ WHEN MATCHED THEN
+ UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
+ RETURNING MERGING(ACTION), MERGING(CLAUSE_NUMBER), t.*;
+ action | clause_number | tid | balance | val
+--------+---------------+-----+---------+--------------------------
+ UPDATE | 1 | 2 | 110 | initial updated by merge
+(1 row)
+
+SELECT * FROM pa_target ORDER BY tid;
+ tid | balance | val
+-----+---------+--------------------------
+ 2 | 110 | initial updated by merge
+ 3 | 300 | initial
+ 5 | 500 | initial
+ 7 | 700 | initial
+ 9 | 900 | initial
+ 11 | 1100 | initial
+ 13 | 1300 | initial
+(7 rows)
+
+ROLLBACK;
DROP TABLE pa_target CASCADE;
-- The target table is partitioned in the same way, but this time by attaching
-- partitions which have columns in different order, dropped columns etc.
@@ -1888,7 +2085,21 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge');
+ INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge')
+ RETURNING MERGING(ACTION), MERGING(CLAUSE_NUMBER), t.*;
+ action | clause_number | logts | tid | balance | val
+--------+---------------+--------------------------+-----+---------+--------------------------
+ UPDATE | 1 | Tue Jan 31 00:00:00 2017 | 1 | 110 | initial updated by merge
+ UPDATE | 1 | Tue Feb 28 00:00:00 2017 | 2 | 220 | initial updated by merge
+ INSERT | 2 | Sun Jan 15 00:00:00 2017 | 3 | 30 | inserted by merge
+ UPDATE | 1 | Tue Jan 31 00:00:00 2017 | 4 | 440 | initial updated by merge
+ UPDATE | 1 | Tue Feb 28 00:00:00 2017 | 5 | 550 | initial updated by merge
+ INSERT | 2 | Sun Jan 15 00:00:00 2017 | 6 | 60 | inserted by merge
+ UPDATE | 1 | Tue Jan 31 00:00:00 2017 | 7 | 770 | initial updated by merge
+ UPDATE | 1 | Tue Feb 28 00:00:00 2017 | 8 | 880 | initial updated by merge
+ INSERT | 2 | Sun Jan 15 00:00:00 2017 | 9 | 90 | inserted by merge
+(9 rows)
+
SELECT * FROM pa_target ORDER BY tid;
logts | tid | balance | val
--------------------------+-----+---------+--------------------------
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
new file mode 100644
index 6988128..3fd4fab
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -2302,6 +2302,35 @@ WHEN MATCHED THEN
UPDATE SET dnotes = dnotes || ' notes added by merge8 '
WHEN NOT MATCHED THEN
INSERT VALUES (13, 44, 1, 'regress_rls_bob', 'new manga');
+SELECT * FROM document WHERE did = 13;
+ did | cid | dlevel | dauthor | dtitle | dnotes
+-----+-----+--------+---------+--------+--------
+(0 rows)
+
+-- but not OK if RETURNING is used
+MERGE INTO document d
+USING (SELECT 14 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge9 '
+WHEN NOT MATCHED THEN
+ INSERT VALUES (14, 44, 1, 'regress_rls_bob', 'new manga')
+RETURNING *;
+ERROR: new row violates row-level security policy for table "document"
+-- but OK if new row is visible
+MERGE INTO document d
+USING (SELECT 14 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge10 '
+WHEN NOT MATCHED THEN
+ INSERT VALUES (14, 11, 1, 'regress_rls_bob', 'new novel')
+RETURNING *;
+ sdid | did | cid | dlevel | dauthor | dtitle | dnotes
+------+-----+-----+--------+-----------------+-----------+--------
+ 14 | 14 | 11 | 1 | regress_rls_bob | new novel |
+(1 row)
+
RESET SESSION AUTHORIZATION;
-- drop the restrictive SELECT policy so that we can look at the
-- final state of the table
@@ -2325,7 +2354,8 @@ SELECT * FROM document;
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 notes added by merge7
13 | 44 | 1 | regress_rls_bob | new manga |
-(15 rows)
+ 14 | 11 | 1 | regress_rls_bob | new novel |
+(16 rows)
--
-- ROLE/GROUP
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
new file mode 100644
index 0cd2c64..31beacc
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -3638,7 +3638,8 @@ MERGE INTO rule_merge1 t USING (SELECT 1
-- test deparsing
CREATE TABLE sf_target(id int, data text, filling int[]);
CREATE FUNCTION merge_sf_test()
- RETURNS void
+ RETURNS TABLE(action text, clause_number int,
+ a int, b text, id int, data text, filling int[])
LANGUAGE sql
BEGIN ATOMIC
MERGE INTO sf_target t
@@ -3675,11 +3676,13 @@ WHEN NOT MATCHED
VALUES (s.a, s.b, DEFAULT)
WHEN NOT MATCHED
THEN INSERT (filling[1], id)
- VALUES (s.a, s.a);
+ VALUES (s.a, s.a)
+RETURNING
+ merging(action), merging(clause_number) AS clause_number, *;
END;
\sf merge_sf_test
CREATE OR REPLACE FUNCTION public.merge_sf_test()
- RETURNS void
+ RETURNS TABLE(action text, clause_number integer, a integer, b text, id integer, data text, filling integer[])
LANGUAGE sql
BEGIN ATOMIC
MERGE INTO sf_target t
@@ -3716,7 +3719,14 @@ BEGIN ATOMIC
VALUES (s.a, s.b, DEFAULT)
WHEN NOT MATCHED
THEN INSERT (filling[1], id)
- VALUES (s.a, s.a);
+ VALUES (s.a, s.a)
+ RETURNING MERGING(ACTION) AS action,
+ MERGING(CLAUSE_NUMBER) AS clause_number,
+ s.a,
+ s.b,
+ t.id,
+ t.data,
+ t.filling;
END
DROP FUNCTION merge_sf_test;
DROP TABLE sf_target;
diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out
new file mode 100644
index 794cf9c..9d1e68c
--- a/src/test/regress/expected/updatable_views.out
+++ b/src/test/regress/expected/updatable_views.out
@@ -479,7 +479,15 @@ MERGE INTO rw_view1 t
(2, 'ROW 2'), (3, 'ROW 3')) AS v(a,b) ON t.a = v.a
WHEN MATCHED AND t.a <= 1 THEN UPDATE SET b = v.b
WHEN MATCHED THEN DELETE
- WHEN NOT MATCHED AND a > 0 THEN INSERT (a) VALUES (v.a);
+ WHEN NOT MATCHED AND a > 0 THEN INSERT (a) VALUES (v.a)
+ RETURNING MERGING(ACTION), v.*, t.*;
+ action | a | b | a | b
+--------+---+-------+---+-------------
+ UPDATE | 1 | ROW 1 | 1 | ROW 1
+ DELETE | 3 | ROW 3 | 3 | Row 3
+ INSERT | 2 | ROW 2 | 2 | Unspecified
+(3 rows)
+
SELECT * FROM base_tbl ORDER BY a;
a | b
----+-------------
@@ -631,7 +639,15 @@ MERGE INTO rw_view2 t
USING (VALUES (3, 'R3'), (4, 'R4'), (5, 'R5')) AS v(a,b) ON aaa = v.a
WHEN MATCHED AND aaa = 3 THEN DELETE
WHEN MATCHED THEN UPDATE SET bbb = v.b
- WHEN NOT MATCHED THEN INSERT (aaa) VALUES (v.a);
+ WHEN NOT MATCHED THEN INSERT (aaa) VALUES (v.a)
+ RETURNING MERGING(ACTION), v.*, t.*;
+ action | a | b | aaa | bbb
+--------+---+----+-----+-------------
+ DELETE | 3 | R3 | 3 | Row 3
+ UPDATE | 4 | R4 | 4 | R4
+ INSERT | 5 | R5 | 5 | Unspecified
+(3 rows)
+
SELECT * FROM rw_view2 ORDER BY aaa;
aaa | bbb
-----+-------------
@@ -1071,7 +1087,15 @@ MERGE INTO rw_view2 t
USING (SELECT x, 'R'||x FROM generate_series(0,3) x) AS s(a,b) ON t.a = s.a
WHEN MATCHED AND t.a <= 1 THEN DELETE
WHEN MATCHED THEN UPDATE SET b = s.b
- WHEN NOT MATCHED AND s.a > 0 THEN INSERT VALUES (s.a, s.b);
+ WHEN NOT MATCHED AND s.a > 0 THEN INSERT VALUES (s.a, s.b)
+ RETURNING MERGING(ACTION), s.*, t.*;
+ action | a | b | a | b
+--------+---+----+---+-------
+ DELETE | 1 | R1 | 1 | Row 1
+ UPDATE | 2 | R2 | 2 | R2
+ INSERT | 3 | R3 | 3 | R3
+(3 rows)
+
SELECT * FROM base_tbl ORDER BY a;
a | b
----+--------
diff --git a/src/test/regress/expected/with.out b/src/test/regress/expected/with.out
new file mode 100644
index 7d796ea..4666873
--- a/src/test/regress/expected/with.out
+++ b/src/test/regress/expected/with.out
@@ -3493,6 +3493,16 @@ SELECT * FROM t;
ERROR: WITH query "t" does not have a RETURNING clause
LINE 4: SELECT * FROM t;
^
+-- RETURNING tries to return its own output
+WITH RECURSIVE t(action, a) AS (
+ MERGE INTO y USING (VALUES (11)) v(a) ON y.a = v.a
+ WHEN NOT MATCHED THEN INSERT VALUES (v.a)
+ RETURNING MERGING(ACTION), (SELECT a FROM t)
+)
+SELECT * FROM t;
+ERROR: recursive query "t" must not contain data-modifying statements
+LINE 1: WITH RECURSIVE t(action, a) AS (
+ ^
-- data-modifying WITH allowed only at the top level
SELECT * FROM (
WITH t AS (UPDATE y SET a=a+1 RETURNING *)
diff --git a/src/test/regress/sql/merge.sql b/src/test/regress/sql/merge.sql
new file mode 100644
index 79a1868..bcab004
--- a/src/test/regress/sql/merge.sql
+++ b/src/test/regress/sql/merge.sql
@@ -88,12 +88,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
@@ -817,7 +817,7 @@ BEGIN;
MERGE INTO sq_target
USING v
ON tid = sid
-WHEN MATCHED AND tid > 2 THEN
+WHEN MATCHED AND tid >= 2 THEN
UPDATE SET balance = balance + delta
WHEN NOT MATCHED THEN
INSERT (balance, tid) VALUES (balance + delta, sid)
@@ -830,7 +830,7 @@ INSERT INTO sq_source (sid, balance, del
MERGE INTO sq_target t
USING v
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)
@@ -848,7 +848,7 @@ WITH targq AS (
MERGE INTO sq_target t
USING v
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)
@@ -858,17 +858,146 @@ ROLLBACK;
-- RETURNING
BEGIN;
-INSERT INTO sq_source (sid, balance, delta) VALUES (-1, -1, -10);
+CREATE TABLE merge_actions(action text, abbrev text);
+INSERT INTO merge_actions VALUES ('INSERT', 'ins'), ('UPDATE', 'upd'), ('DELETE', 'del');
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)
+ INSERT (balance, tid) VALUES (balance + delta, sid)
WHEN MATCHED AND tid < 2 THEN
- DELETE
-RETURNING *;
+ DELETE
+RETURNING MERGING(CLAUSE_NUMBER) AS clause,
+ (SELECT abbrev FROM merge_actions
+ WHERE action = MERGING(ACTION)) AS action,
+ t.*,
+ CASE MERGING(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 MERGING() outside MERGE
+SELECT MERGING(ACTION) FROM sq_target;
+UPDATE sq_target SET balance = balance + 1 RETURNING MERGING(CLAUSE_NUMBER);
+
+-- RETURNING in CTEs
+CREATE TABLE sq_target_merge_log (tid integer NOT NULL, last_change text);
+INSERT INTO sq_target_merge_log VALUES (1, 'Original value');
+BEGIN;
+WITH m AS (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 MERGING(CLAUSE_NUMBER), MERGING(ACTION), t.*,
+ CASE MERGING(ACTION)
+ WHEN 'INSERT' THEN 'Inserted '||t
+ WHEN 'UPDATE' THEN 'Added '||delta||' to balance'
+ WHEN 'DELETE' THEN 'Removed '||t
+ END AS description
+), m2 AS (
+ MERGE INTO sq_target_merge_log l
+ USING m
+ ON l.tid = m.tid
+ WHEN MATCHED THEN
+ UPDATE SET last_change = description
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (m.tid, description)
+ RETURNING action, MERGING(ACTION) AS log_action, l.*
+)
+SELECT * FROM m2;
+SELECT * FROM sq_target_merge_log ORDER BY tid;
+ROLLBACK;
+
+-- COPY (MERGE ... RETURNING) TO ...
+BEGIN;
+COPY (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 MERGING(ACTION), t.*
+) TO stdout;
+ROLLBACK;
+
+-- SQL function with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT action text, OUT tid int, OUT new_balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING MERGING(ACTION), t.*;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT action, tid, new_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ROLLBACK;
+
+-- SQL SRF with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_sq_source_into_sq_target()
+RETURNS TABLE (action text, tid int, balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 MERGING(ACTION), t.*;
+$$;
+SELECT * FROM merge_sq_source_into_sq_target();
+ROLLBACK;
+
+-- PL/pgSQL function with MERGE ... RETURNING ... INTO
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT r_action text, OUT r_tid int, OUT r_balance int)
+LANGUAGE plpgsql AS
+$$
+BEGIN
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING MERGING(ACTION), t.* INTO r_action, r_tid, r_balance;
+END;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT r_action, r_tid, r_balance FROM merge_into_sq_target(sid, balance, delta)) m;
ROLLBACK;
-- EXPLAIN
@@ -967,7 +1096,7 @@ WHEN MATCHED THEN
SELECT * FROM sq_target WHERE tid = 1;
ROLLBACK;
-DROP TABLE sq_target, sq_source CASCADE;
+DROP TABLE sq_target, sq_target_merge_log, sq_source CASCADE;
CREATE TABLE pa_target (tid integer, balance float, val text)
PARTITION BY LIST (tid);
@@ -1034,6 +1163,17 @@ SELECT merge_func();
SELECT * FROM pa_target ORDER BY tid;
ROLLBACK;
+-- update partition key to partition not initially scanned
+BEGIN;
+MERGE INTO pa_target t
+ USING pa_source s
+ ON t.tid = s.sid AND t.tid = 1
+ WHEN MATCHED THEN
+ UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
+ RETURNING MERGING(ACTION), MERGING(CLAUSE_NUMBER), t.*;
+SELECT * FROM pa_target ORDER BY tid;
+ROLLBACK;
+
DROP TABLE pa_target CASCADE;
-- The target table is partitioned in the same way, but this time by attaching
@@ -1210,7 +1350,8 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge');
+ INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge')
+ RETURNING MERGING(ACTION), MERGING(CLAUSE_NUMBER), t.*;
SELECT * FROM pa_target ORDER BY tid;
ROLLBACK;
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
new file mode 100644
index dec7340..1d5ed0a
--- a/src/test/regress/sql/rowsecurity.sql
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -992,6 +992,27 @@ WHEN MATCHED THEN
UPDATE SET dnotes = dnotes || ' notes added by merge8 '
WHEN NOT MATCHED THEN
INSERT VALUES (13, 44, 1, 'regress_rls_bob', 'new manga');
+SELECT * FROM document WHERE did = 13;
+
+-- but not OK if RETURNING is used
+MERGE INTO document d
+USING (SELECT 14 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge9 '
+WHEN NOT MATCHED THEN
+ INSERT VALUES (14, 44, 1, 'regress_rls_bob', 'new manga')
+RETURNING *;
+
+-- but OK if new row is visible
+MERGE INTO document d
+USING (SELECT 14 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge10 '
+WHEN NOT MATCHED THEN
+ INSERT VALUES (14, 11, 1, 'regress_rls_bob', 'new novel')
+RETURNING *;
RESET SESSION AUTHORIZATION;
-- drop the restrictive SELECT policy so that we can look at the
diff --git a/src/test/regress/sql/rules.sql b/src/test/regress/sql/rules.sql
new file mode 100644
index 6924012..efd3287
--- a/src/test/regress/sql/rules.sql
+++ b/src/test/regress/sql/rules.sql
@@ -1294,7 +1294,8 @@ MERGE INTO rule_merge1 t USING (SELECT 1
CREATE TABLE sf_target(id int, data text, filling int[]);
CREATE FUNCTION merge_sf_test()
- RETURNS void
+ RETURNS TABLE(action text, clause_number int,
+ a int, b text, id int, data text, filling int[])
LANGUAGE sql
BEGIN ATOMIC
MERGE INTO sf_target t
@@ -1331,7 +1332,9 @@ WHEN NOT MATCHED
VALUES (s.a, s.b, DEFAULT)
WHEN NOT MATCHED
THEN INSERT (filling[1], id)
- VALUES (s.a, s.a);
+ VALUES (s.a, s.a)
+RETURNING
+ merging(action), merging(clause_number) AS clause_number, *;
END;
\sf merge_sf_test
diff --git a/src/test/regress/sql/updatable_views.sql b/src/test/regress/sql/updatable_views.sql
new file mode 100644
index ae11e46..650b315
--- a/src/test/regress/sql/updatable_views.sql
+++ b/src/test/regress/sql/updatable_views.sql
@@ -175,7 +175,8 @@ MERGE INTO rw_view1 t
(2, 'ROW 2'), (3, 'ROW 3')) AS v(a,b) ON t.a = v.a
WHEN MATCHED AND t.a <= 1 THEN UPDATE SET b = v.b
WHEN MATCHED THEN DELETE
- WHEN NOT MATCHED AND a > 0 THEN INSERT (a) VALUES (v.a);
+ WHEN NOT MATCHED AND a > 0 THEN INSERT (a) VALUES (v.a)
+ RETURNING MERGING(ACTION), v.*, t.*;
SELECT * FROM base_tbl ORDER BY a;
EXPLAIN (costs off) UPDATE rw_view1 SET a=6 WHERE a=5;
@@ -246,7 +247,8 @@ MERGE INTO rw_view2 t
USING (VALUES (3, 'R3'), (4, 'R4'), (5, 'R5')) AS v(a,b) ON aaa = v.a
WHEN MATCHED AND aaa = 3 THEN DELETE
WHEN MATCHED THEN UPDATE SET bbb = v.b
- WHEN NOT MATCHED THEN INSERT (aaa) VALUES (v.a);
+ WHEN NOT MATCHED THEN INSERT (aaa) VALUES (v.a)
+ RETURNING MERGING(ACTION), v.*, t.*;
SELECT * FROM rw_view2 ORDER BY aaa;
EXPLAIN (costs off) UPDATE rw_view2 SET aaa=5 WHERE aaa=4;
@@ -458,7 +460,8 @@ MERGE INTO rw_view2 t
USING (SELECT x, 'R'||x FROM generate_series(0,3) x) AS s(a,b) ON t.a = s.a
WHEN MATCHED AND t.a <= 1 THEN DELETE
WHEN MATCHED THEN UPDATE SET b = s.b
- WHEN NOT MATCHED AND s.a > 0 THEN INSERT VALUES (s.a, s.b);
+ WHEN NOT MATCHED AND s.a > 0 THEN INSERT VALUES (s.a, s.b)
+ RETURNING MERGING(ACTION), s.*, t.*;
SELECT * FROM base_tbl ORDER BY a;
EXPLAIN (costs off) UPDATE rw_view2 SET a=3 WHERE a=2;
diff --git a/src/test/regress/sql/with.sql b/src/test/regress/sql/with.sql
new file mode 100644
index f8a213e..1d82d4a
--- a/src/test/regress/sql/with.sql
+++ b/src/test/regress/sql/with.sql
@@ -1591,6 +1591,14 @@ WITH t AS (
)
SELECT * FROM t;
+-- RETURNING tries to return its own output
+WITH RECURSIVE t(action, a) AS (
+ MERGE INTO y USING (VALUES (11)) v(a) ON y.a = v.a
+ WHEN NOT MATCHED THEN INSERT VALUES (v.a)
+ RETURNING MERGING(ACTION), (SELECT a FROM t)
+)
+SELECT * FROM t;
+
-- data-modifying WITH allowed only at the top level
SELECT * FROM (
WITH t AS (UPDATE y SET a=a+1 RETURNING *)
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
new file mode 100644
index fc8b15d..9dc7e24
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1577,6 +1577,8 @@ MergePath
MergeScanSelCache
MergeStmt
MergeWhenClause
+MergingFunc
+MergingFuncOp
MetaCommand
MinMaxAggInfo
MinMaxAggPath
Can we get some input on whether the current MERGE ... RETURNING patch
is the right approach from a language standpoint?
We've gone through a lot of iterations -- thank you Dean, for
implementing so many variations.
To summarize, most of the problem has been in retrieving the action
(INSERT/UPDATE/DELETE) taken or the WHEN-clause number applied to a
particular matched row. The reason this is important is because the row
returned is the old row for a DELETE action, and the new row for an
INSERT or UPDATE action. Without a way to distinguish the particular
action, the RETURNING clause returns a mixture of old and new rows,
which would be hard to use sensibly.
Granted, DELETE in a MERGE may be a less common case. But given that we
also have INSERT ... ON CONFLICT, MERGE commands are more likely to be
the complicated cases where distinguishing the action or clause number
is important.
But linguistically it's not clear where the action or clause number
should come from. The clauses don't have assigned numbers, and even if
they did, linguistically it's not clear how to refer to the clause
number in a language like SQL. Would it be a special identifier, a
function, a special function, or be a column in a special table
reference? Or, do we just have one RETURNING-clause per WHEN-clause,
and let the user use a literal of their choice in the RETURNING clause?
The current implementation uses a special function MERGING (a
grammatical construct without an OID that parses into a new MergingFunc
expr), which takes keywords ACTION or CLAUSE_NUMBER in the argument
positions. That's not totally unprecedented in SQL -- the XML and JSON
functions are kind of similar. But it's different in the sense that
MERGING is also context-sensitive: grammatically, it fits pretty much
anywhere a function fits, but then gets rejected at parse analysis time
(or perhaps even execution time?) if it's not called from the right
place.
Is that a reasonable thing to do?
Another related topic came up, which is that the RETURNING clause (for
UPDATE as well as MERGE) should probably accept some kind of alias like
NEW/OLD or BEFORE/AFTER to address the version of the row that you
want. That doesn't eliminate the need for the MERGING function, but
it's good to think about how that might fit in with whatever we do
here.
Regards,
Jeff Davis
On Thu, 2024-02-29 at 19:24 +0000, Dean Rasheed wrote:
Attached is a rebased version on top of 5f2e179bd3 (support for MERGE
into views), with a few additional tests to confirm that MERGE ...
RETURNING works for views as well as tables.
Thank you for the rebase. I just missed your message (race condition),
so I replied to v15:
/messages/by-id/e03a87eb4e728c5e475b360b5845979f78d49020.camel@j-davis.com
I see that this patch was discussed at the PostgreSQL Developers
Meeting. Did anything new come out of that discussion?
I don't think we made any conclusions at the meeting, but I expressed
that we need input from one of them on this patch.
Regards,
Jeff Davis
On 29.02.24 20:49, Jeff Davis wrote:
To summarize, most of the problem has been in retrieving the action
(INSERT/UPDATE/DELETE) taken or the WHEN-clause number applied to a
particular matched row. The reason this is important is because the row
returned is the old row for a DELETE action, and the new row for an
INSERT or UPDATE action. Without a way to distinguish the particular
action, the RETURNING clause returns a mixture of old and new rows,
which would be hard to use sensibly.
For comparison with standard SQL (see <data change delta table>):
For an INSERT you could write
SELECT whatever FROM NEW TABLE (INSERT statement here)
or for an DELETE
SELECT whatever FROM OLD TABLE (DELETE statement here)
And for an UPDATE could can pick either OLD or NEW.
(There is also FINAL, which appears to be valid in cases where NEW is
valid. Here is an explanation:
<https://www.ibm.com/docs/en/db2oc?topic=statement-result-sets-from-sql-data-changes>)
For a MERGE statement, whether you can specify OLD or NEW (or FINAL)
depends on what actions appear in the MERGE statement.
So if we were to translate that to our syntax, it might be something like
MERGE ... RETURNING OLD *
or
MERGE ... RETURNING NEW *
This wouldn't give you the ability to return both old and new. (Is that
useful?) But maybe you could also do something like
MERGE ... RETURNING OLD 'old'::text, * RETURNING NEW 'new'::text, *
(I mean here you could insert your own constants into the returning lists.)
The current implementation uses a special function MERGING (a
grammatical construct without an OID that parses into a new MergingFunc
expr), which takes keywords ACTION or CLAUSE_NUMBER in the argument
positions. That's not totally unprecedented in SQL -- the XML and JSON
functions are kind of similar. But it's different in the sense that
MERGING is also context-sensitive: grammatically, it fits pretty much
anywhere a function fits, but then gets rejected at parse analysis time
(or perhaps even execution time?) if it's not called from the right
place.
An analogy here might be that MATCH_RECOGNIZE (row-pattern recognition)
has a magic function MATCH_NUMBER() that can be used inside that clause.
So a similar zero-argument magic function might make sense. I don't
like the MERGING(ACTION) syntax, but something like MERGE_ACTION() might
make sense. (This is just in terms of what kind of syntax might be
palatable. Depending on where the syntax of the overall clause ends up,
we might not need it (see above).)
On Wed, 6 Mar 2024 at 08:51, Peter Eisentraut <peter@eisentraut.org> wrote:
For comparison with standard SQL (see <data change delta table>):
For an INSERT you could write
SELECT whatever FROM NEW TABLE (INSERT statement here)
or for an DELETE
SELECT whatever FROM OLD TABLE (DELETE statement here)
And for an UPDATE could can pick either OLD or NEW.
Thanks, that's very interesting. I hadn't seen that syntax before.
Over on [1]/messages/by-id/CAEZATCWx0J0-v=Qjc6gXzR=KtsdvAE7Ow=D=mu50AgOe+pvisQ@mail.gmail.com, I have a patch in the works that extends RETURNING,
allowing it to return OLD.colname, NEW.colname, OLD.*, and NEW.*. It
looks like this new SQL standard syntax could be built on top of that
(perhaps by having the rewriter turn queries of the above form into
CTEs).
However, the RETURNING syntax is more powerful, because it allows OLD
and NEW to be used together in arbitrary expressions, for example:
RETURNING ..., NEW.val - OLD.val AS delta, ...
The current implementation uses a special function MERGING (a
grammatical construct without an OID that parses into a new MergingFunc
expr), which takes keywords ACTION or CLAUSE_NUMBER in the argument
positions. That's not totally unprecedented in SQL -- the XML and JSON
functions are kind of similar. But it's different in the sense that
MERGING is also context-sensitive: grammatically, it fits pretty much
anywhere a function fits, but then gets rejected at parse analysis time
(or perhaps even execution time?) if it's not called from the right
place.An analogy here might be that MATCH_RECOGNIZE (row-pattern recognition)
has a magic function MATCH_NUMBER() that can be used inside that clause.
So a similar zero-argument magic function might make sense. I don't
like the MERGING(ACTION) syntax, but something like MERGE_ACTION() might
make sense. (This is just in terms of what kind of syntax might be
palatable. Depending on where the syntax of the overall clause ends up,
we might not need it (see above).)
It could be that having the ability to return OLD and NEW values, as
in [1]/messages/by-id/CAEZATCWx0J0-v=Qjc6gXzR=KtsdvAE7Ow=D=mu50AgOe+pvisQ@mail.gmail.com, is sufficient for use in MERGE, to identify the action
performed. However, I still think that dedicated functions would be
useful, if we can agree on names/syntax.
I think that I prefer the names MERGE_ACTION() and
MERGE_CLAUSE_NUMBER() from an aesthetic point of view, but it requires
2 new COL_NAME_KEYWORD keywords. Maybe that's OK, I don't know.
Alternatively, we could avoid adding new keywords by going back to
making these regular functions, as they were in an earlier version of
this patch, and then use some special-case code during parse analysis
to turn them into MergeFunc nodes (not quite a complete revert back to
an earlier version of the patch, but not far off).
Regards,
Dean
[1]: /messages/by-id/CAEZATCWx0J0-v=Qjc6gXzR=KtsdvAE7Ow=D=mu50AgOe+pvisQ@mail.gmail.com
On Thu, Feb 29, 2024 at 1:49 PM Jeff Davis <pgsql@j-davis.com> wrote:
Can we get some input on whether the current MERGE ... RETURNING patch
is the right approach from a language standpoint?
MERGE_CLAUSE_NUMBER() seems really out of place to me, it feels out of
place to identify output set by number rather than some kind of name. Did
not see a lot of support for that position though.
merlin
Jeff Davis:
To summarize, most of the problem has been in retrieving the action
(INSERT/UPDATE/DELETE) taken or the WHEN-clause number applied to a
particular matched row. The reason this is important is because the row
returned is the old row for a DELETE action, and the new row for an
INSERT or UPDATE action. Without a way to distinguish the particular
action, the RETURNING clause returns a mixture of old and new rows,
which would be hard to use sensibly.
It seems to me that all of this is only a problem, because there is only
one RETURNING clause.
Dean Rasheed wrote in the very first post to this thread:
I considered allowing a separate RETURNING list at the end of each
action, but rapidly dismissed that idea. Firstly, it introduces
shift/reduce conflicts to the grammar. These can be resolved by making
the "AS" before column aliases non-optional, but that's pretty ugly,
and there may be a better way. More serious drawbacks are that this
syntax is much more cumbersome for the end user, having to repeat the
RETURNING clause several times, and the implementation is likely to be
pretty complex, so I didn't pursue it.
I can't judge the grammar and complexity issues, but as a potential user
it seems to me to be less complex to have multiple RETURNING clauses,
where I could inject my own constants about the specific actions, than
to have to deal with any of the suggested functions / clauses. More
repetitive, yes - but not more complex.
More importantly, I could add RETURNING to only some of the actions and
not always all at the same time - which seems pretty useful to me.
Best,
Wolfgang
On Fri, 8 Mar 2024 at 08:41, <walther@technowledgy.de> wrote:
I can't judge the grammar and complexity issues, but as a potential user
it seems to me to be less complex to have multiple RETURNING clauses,
where I could inject my own constants about the specific actions, than
to have to deal with any of the suggested functions / clauses. More
repetitive, yes - but not more complex.More importantly, I could add RETURNING to only some of the actions and
not always all at the same time - which seems pretty useful to me.
I think that would be a bad idea, since it would mean the number of
rows returned would no longer match the number of rows modified, which
is a general property of all data-modifying commands that support
RETURNING. It would also increase the chances of bugs for users who
might accidentally miss a WHEN clause.
Looking back over the thread the majority opinion seems to be:
1). Have a single RETURNING list, rather than one per action
2). Drop the "clause number" function
3). Call the other function MERGE_ACTION()
And from an implementation point-of-view, it seems better to stick
with having a new node type to handle MERGE_ACTION(), and make
MERGE_ACTION a COL_NAME_KEYWORD.
This seems like a reasonable compromise, and it still allows the
specific WHEN clause that was executed to be identified by using a
combination of MERGE_ACTION() and the attributes from the source and
target relations. More functions can always be added later, if there
is demand.
Attached is a rebased patch, with those changes.
Regards,
Dean
Attachments:
support-merge-returning-v17.patchtext/x-patch; charset=US-ASCII; name=support-merge-returning-v17.patchDownload
diff --git a/doc/src/sgml/dml.sgml b/doc/src/sgml/dml.sgml
new file mode 100644
index cbbc5e2..3d95bdb
--- a/doc/src/sgml/dml.sgml
+++ b/doc/src/sgml/dml.sgml
@@ -283,10 +283,15 @@ DELETE FROM products;
<secondary>RETURNING</secondary>
</indexterm>
+ <indexterm zone="dml-returning">
+ <primary>MERGE</primary>
+ <secondary>RETURNING</secondary>
+ </indexterm>
+
<para>
Sometimes it is useful to obtain data from modified rows while they are
being manipulated. The <command>INSERT</command>, <command>UPDATE</command>,
- and <command>DELETE</command> commands all have an
+ <command>DELETE</command>, and <command>MERGE</command> commands all have an
optional <literal>RETURNING</literal> clause that supports this. Use
of <literal>RETURNING</literal> avoids performing an extra database query to
collect the data, and is especially valuable when it would otherwise be
@@ -339,6 +344,21 @@ DELETE FROM products
</programlisting>
</para>
+ <para>
+ In a <command>MERGE</command>, the data available to <literal>RETURNING</literal> is
+ the content of the source row plus the content of the inserted, updated, or
+ deleted target row. Since it is quite common for the source and target to
+ have many of the same columns, specifying <literal>RETURNING *</literal>
+ can lead to a lot of duplicated columns, so it is often more useful to
+ qualify it so as to return just the source or target row. For example:
+<programlisting>
+MERGE INTO products p USING new_products n ON p.product_no = n.product_no
+ WHEN NOT MATCHED THEN INSERT VALUES (n.product_no, n.name, n.price)
+ WHEN MATCHED THEN UPDATE SET name = n.name, price = n.price
+ RETURNING p.*;
+</programlisting>
+ </para>
+
<para>
If there are triggers (<xref linkend="triggers"/>) on the target table,
the data available to <literal>RETURNING</literal> is the row as modified by
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
new file mode 100644
index 0bb7aeb..0e2b888
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -22421,6 +22421,85 @@ SELECT count(*) FROM sometable;
</sect1>
+ <sect1 id="functions-merge-support">
+ <title>Merge Support Functions</title>
+
+ <indexterm>
+ <primary>MERGE</primary>
+ <secondary>RETURNING</secondary>
+ </indexterm>
+
+ <para>
+ <productname>PostgreSQL</productname> includes one merge support function
+ that may be used in the <literal>RETURNING</literal> list of a
+ <xref linkend="sql-merge"/> command to identify the action taken for each
+ row.
+ </para>
+
+ <table id="functions-merge-support-table">
+ <title>Merge Support Functions</title>
+
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ Function
+ </para>
+ <para>
+ Description
+ </para></entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>merge_action</primary>
+ </indexterm>
+ <function id="merge_action">merge_action</function> ( )
+ <returnvalue>text</returnvalue>
+ </para>
+ <para>
+ Returns the merge action command executed for the current row. This
+ will be <literal>'INSERT'</literal>, <literal>'UPDATE'</literal>, or
+ <literal>'DELETE'</literal>.
+ </para></entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+
+ <para>
+ Example:
+<screen><![CDATA[
+MERGE INTO products p
+ USING stock s ON p.product_id = s.product_id
+ WHEN MATCHED AND s.quantity > 0 THEN
+ UPDATE SET in_stock = true, quantity = s.quantity
+ WHEN MATCHED THEN
+ UPDATE SET in_stock = false, quantity = 0
+ WHEN NOT MATCHED THEN
+ INSERT (product_id, in_stock, quantity)
+ VALUES (s.product_id, true, s.quantity)
+ RETURNING merge_action(), p.*;
+
+ merge_action | product_id | in_stock | quantity
+--------------+------------+----------+----------
+ UPDATE | 1001 | t | 50
+ UPDATE | 1002 | f | 0
+ INSERT | 1003 | t | 10
+]]></screen>
+ </para>
+
+ <para>
+ Note that this function can only be used in the <literal>RETURNING</literal>
+ list of a <command>MERGE</command> command. It is an error to use it in any
+ other part of a query.
+ </para>
+
+ </sect1>
+
<sect1 id="functions-subquery">
<title>Subquery Expressions</title>
diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml
new file mode 100644
index 8c2f114..a81c17a
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -1442,9 +1442,9 @@
to a <glossterm linkend="glossary-client">client</glossterm> upon the
completion of an <acronym>SQL</acronym> command, usually a
<command>SELECT</command> but it can be an
- <command>INSERT</command>, <command>UPDATE</command>, or
- <command>DELETE</command> command if the <literal>RETURNING</literal>
- clause is specified.
+ <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command> command if the
+ <literal>RETURNING</literal> clause is specified.
</para>
<para>
The fact that a result set is a relation means that a query can be used
diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml
new file mode 100644
index c2b9c6a..406834e
--- a/doc/src/sgml/plpgsql.sgml
+++ b/doc/src/sgml/plpgsql.sgml
@@ -1043,8 +1043,8 @@ INSERT INTO mytable VALUES (1,'one'), (2
</para>
<para>
- If the command does return rows (for example <command>SELECT</command>,
- or <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>
+ If the command does return rows (for example <command>SELECT</command>, or
+ <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command>
with <literal>RETURNING</literal>), there are two ways to proceed.
When the command will return at most one row, or you only care about
the first row of output, write the command as usual but add
@@ -1172,6 +1172,7 @@ SELECT <replaceable>select_expressions</
INSERT ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
UPDATE ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
DELETE ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
+MERGE ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
</synopsis>
where <replaceable>target</replaceable> can be a record variable, a row
@@ -1182,8 +1183,8 @@ DELETE ... RETURNING <replaceable>expres
<literal>INTO</literal> clause) just as described above,
and the plan is cached in the same way.
This works for <command>SELECT</command>,
- <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command> with
- <literal>RETURNING</literal>, and certain utility commands
+ <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command>
+ with <literal>RETURNING</literal>, and certain utility commands
that return row sets, such as <command>EXPLAIN</command>.
Except for the <literal>INTO</literal> clause, the SQL command is the same
as it would be written outside <application>PL/pgSQL</application>.
@@ -1259,7 +1260,7 @@ END;
</para>
<para>
- For <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command> with
+ For <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command> with
<literal>RETURNING</literal>, <application>PL/pgSQL</application> reports
an error for more than one returned row, even when
<literal>STRICT</literal> is not specified. This is because there
diff --git a/doc/src/sgml/queries.sgml b/doc/src/sgml/queries.sgml
new file mode 100644
index 648b283..372cce1
--- a/doc/src/sgml/queries.sgml
+++ b/doc/src/sgml/queries.sgml
@@ -2063,9 +2063,10 @@ SELECT <replaceable>select_list</replace
Table Expressions or <acronym>CTE</acronym>s, can be thought of as defining
temporary tables that exist just for one query. Each auxiliary statement
in a <literal>WITH</literal> clause can be a <command>SELECT</command>,
- <command>INSERT</command>, <command>UPDATE</command>, or <command>DELETE</command>; and the
+ <command>INSERT</command>, <command>UPDATE</command>, <command>DELETE</command>,
+ or <command>MERGE</command>; and the
<literal>WITH</literal> clause itself is attached to a primary statement that can
- be a <command>SELECT</command>, <command>INSERT</command>, <command>UPDATE</command>,
+ also be a <command>SELECT</command>, <command>INSERT</command>, <command>UPDATE</command>,
<command>DELETE</command>, or <command>MERGE</command>.
</para>
@@ -2599,8 +2600,8 @@ SELECT * FROM w AS w1 JOIN w AS w2 ON w1
<title>Data-Modifying Statements in <literal>WITH</literal></title>
<para>
- You can use most data-modifying statements (<command>INSERT</command>,
- <command>UPDATE</command>, or <command>DELETE</command>, but not
+ You can use data-modifying statements (<command>INSERT</command>,
+ <command>UPDATE</command>, <command>DELETE</command>, or
<command>MERGE</command>) in <literal>WITH</literal>. This
allows you to perform several different operations in the same query.
An example is:
diff --git a/doc/src/sgml/ref/copy.sgml b/doc/src/sgml/ref/copy.sgml
new file mode 100644
index 55764fc..6c83e30
--- a/doc/src/sgml/ref/copy.sgml
+++ b/doc/src/sgml/ref/copy.sgml
@@ -130,16 +130,19 @@ COPY { <replaceable class="parameter">ta
A <link linkend="sql-select"><command>SELECT</command></link>,
<link linkend="sql-values"><command>VALUES</command></link>,
<link linkend="sql-insert"><command>INSERT</command></link>,
- <link linkend="sql-update"><command>UPDATE</command></link>, or
- <link linkend="sql-delete"><command>DELETE</command></link> command whose results are to be
- copied. Note that parentheses are required around the query.
+ <link linkend="sql-update"><command>UPDATE</command></link>,
+ <link linkend="sql-delete"><command>DELETE</command></link>, or
+ <link linkend="sql-merge"><command>MERGE</command></link> command
+ whose results are to be copied. Note that parentheses are required
+ around the query.
</para>
<para>
- For <command>INSERT</command>, <command>UPDATE</command> and
- <command>DELETE</command> queries a <literal>RETURNING</literal> clause
- must be provided, and the target relation must not have a conditional
- rule, nor an <literal>ALSO</literal> rule, nor an
- <literal>INSTEAD</literal> rule that expands to multiple statements.
+ For <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, and <command>MERGE</command> queries a
+ <literal>RETURNING</literal> clause must be provided, and the target
+ relation must not have a conditional rule, nor an
+ <literal>ALSO</literal> rule, nor an <literal>INSTEAD</literal> rule
+ that expands to multiple statements.
</para>
</listitem>
</varlistentry>
diff --git a/doc/src/sgml/ref/merge.sgml b/doc/src/sgml/ref/merge.sgml
new file mode 100644
index 7a8ac40..3d8c792
--- a/doc/src/sgml/ref/merge.sgml
+++ b/doc/src/sgml/ref/merge.sgml
@@ -25,6 +25,7 @@ PostgreSQL documentation
MERGE INTO [ ONLY ] <replaceable class="parameter">target_table_name</replaceable> [ * ] [ [ AS ] <replaceable class="parameter">target_alias</replaceable> ]
USING <replaceable class="parameter">data_source</replaceable> ON <replaceable class="parameter">join_condition</replaceable>
<replaceable class="parameter">when_clause</replaceable> [...]
+[ RETURNING * | <replaceable class="parameter">output_expression</replaceable> [ [ AS ] <replaceable class="parameter">output_name</replaceable> ] [, ...] ]
<phrase>where <replaceable class="parameter">data_source</replaceable> is:</phrase>
@@ -95,6 +96,18 @@ DELETE
</para>
<para>
+ The optional <literal>RETURNING</literal> clause causes <command>MERGE</command>
+ to compute and return value(s) based on each row inserted, updated, or
+ deleted. Any expression using the source or target table's columns, or
+ the <xref linkend="merge_action"/> function can be computed. When an
+ <command>INSERT</command> or <command>UPDATE</command> action is performed,
+ the new values of the target table's columns are used. When a
+ <command>DELETE</command> is performed, the old values of the target table's
+ columns are used. The syntax of the <literal>RETURNING</literal> list is
+ identical to that of the output list of <command>SELECT</command>.
+ </para>
+
+ <para>
There is no separate <literal>MERGE</literal> privilege.
If you specify an update action, you must have the
<literal>UPDATE</literal> privilege on the column(s)
@@ -129,6 +142,18 @@ DELETE
<variablelist>
<varlistentry>
+ <term><replaceable class="parameter">with_query</replaceable></term>
+ <listitem>
+ <para>
+ The <literal>WITH</literal> clause allows you to specify one or more
+ subqueries that can be referenced by name in the <command>MERGE</command>
+ query. See <xref linkend="queries-with"/> and <xref linkend="sql-select"/>
+ for details.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
<term><replaceable class="parameter">target_table_name</replaceable></term>
<listitem>
<para>
@@ -411,13 +436,30 @@ DELETE
</varlistentry>
<varlistentry>
- <term><replaceable class="parameter">with_query</replaceable></term>
+ <term><replaceable class="parameter">output_expression</replaceable></term>
<listitem>
<para>
- The <literal>WITH</literal> clause allows you to specify one or more
- subqueries that can be referenced by name in the <command>MERGE</command>
- query. See <xref linkend="queries-with"/> and <xref linkend="sql-select"/>
- for details.
+ An expression to be computed and returned by the <command>MERGE</command>
+ command after each row is merged. The expression can use any columns of
+ the source or target tables, or the <xref linkend="merge_action"/>
+ function to return additional information about the action executed.
+ </para>
+ <para>
+ Writing <literal>*</literal> will return all columns from the source
+ table, followed by all columns from the target table. Often this will
+ lead to a lot of duplication, since it is common for the source and
+ target tables to have a lot of the same columns. This can be avoided by
+ qualifying the <literal>*</literal> with the name or alias of the source
+ or target table.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><replaceable class="parameter">output_name</replaceable></term>
+ <listitem>
+ <para>
+ A name to use for a returned column.
</para>
</listitem>
</varlistentry>
@@ -440,6 +482,13 @@ MERGE <replaceable class="parameter">tot
were changed in any way.
</para>
+ <para>
+ If the <command>MERGE</command> command contains a <literal>RETURNING</literal>
+ clause, the result will be similar to that of a <command>SELECT</command>
+ statement containing the columns and values defined in the
+ <literal>RETURNING</literal> list, computed over the row(s) inserted, updated,
+ or deleted by the command.
+ </para>
</refsect1>
<refsect1>
@@ -560,13 +609,6 @@ MERGE <replaceable class="parameter">tot
</para>
<para>
- There is no <literal>RETURNING</literal> clause with
- <command>MERGE</command>. Actions of <command>INSERT</command>,
- <command>UPDATE</command> and <command>DELETE</command> cannot contain
- <literal>RETURNING</literal> or <literal>WITH</literal> clauses.
- </para>
-
- <para>
When <command>MERGE</command> is run concurrently with other commands
that modify the target table, the usual transaction isolation rules
apply; see <xref linkend="transaction-iso"/> for an explanation
@@ -618,7 +660,8 @@ WHEN NOT MATCHED THEN
<para>
Attempt to insert a new stock item along with the quantity of stock. If
the item already exists, instead update the stock count of the existing
- item. Don't allow entries that have zero stock.
+ item. Don't allow entries that have zero stock. Return details of all
+ changes made.
<programlisting>
MERGE INTO wines w
USING wine_stock_changes s
@@ -628,7 +671,8 @@ WHEN NOT MATCHED AND s.stock_delta > 0 T
WHEN MATCHED AND w.stock + s.stock_delta > 0 THEN
UPDATE SET stock = w.stock + s.stock_delta
WHEN MATCHED THEN
- DELETE;
+ DELETE
+RETURNING merge_action(), w.*;
</programlisting>
The <literal>wine_stock_changes</literal> table might be, for example, a
@@ -643,8 +687,9 @@ WHEN MATCHED THEN
This command conforms to the <acronym>SQL</acronym> standard.
</para>
<para>
- The <literal>WITH</literal> clause and <literal>DO NOTHING</literal>
- action are extensions to the <acronym>SQL</acronym> standard.
+ The <literal>WITH</literal> clause, <literal>DO NOTHING</literal> action,
+ and <literal>RETURNING</literal> clause are extensions to the
+ <acronym>SQL</acronym> standard.
</para>
</refsect1>
</refentry>
diff --git a/doc/src/sgml/ref/select.sgml b/doc/src/sgml/ref/select.sgml
new file mode 100644
index 9917df7..066aed4
--- a/doc/src/sgml/ref/select.sgml
+++ b/doc/src/sgml/ref/select.sgml
@@ -74,7 +74,7 @@ SELECT [ ALL | DISTINCT [ ON ( <replacea
<phrase>and <replaceable class="parameter">with_query</replaceable> is:</phrase>
- <replaceable class="parameter">with_query_name</replaceable> [ ( <replaceable class="parameter">column_name</replaceable> [, ...] ) ] AS [ [ NOT ] MATERIALIZED ] ( <replaceable class="parameter">select</replaceable> | <replaceable class="parameter">values</replaceable> | <replaceable class="parameter">insert</replaceable> | <replaceable class="parameter">update</replaceable> | <replaceable class="parameter">delete</replaceable> )
+ <replaceable class="parameter">with_query_name</replaceable> [ ( <replaceable class="parameter">column_name</replaceable> [, ...] ) ] AS [ [ NOT ] MATERIALIZED ] ( <replaceable class="parameter">select</replaceable> | <replaceable class="parameter">values</replaceable> | <replaceable class="parameter">insert</replaceable> | <replaceable class="parameter">update</replaceable> | <replaceable class="parameter">delete</replaceable> | <replaceable class="parameter">merge</replaceable> )
[ SEARCH { BREADTH | DEPTH } FIRST BY <replaceable>column_name</replaceable> [, ...] SET <replaceable>search_seq_col_name</replaceable> ]
[ CYCLE <replaceable>column_name</replaceable> [, ...] SET <replaceable>cycle_mark_col_name</replaceable> [ TO <replaceable>cycle_mark_value</replaceable> DEFAULT <replaceable>cycle_mark_default</replaceable> ] USING <replaceable>cycle_path_col_name</replaceable> ]
@@ -230,10 +230,10 @@ TABLE [ ONLY ] <replaceable class="param
The subqueries effectively act as temporary tables or views
for the duration of the primary query.
Each subquery can be a <command>SELECT</command>, <command>TABLE</command>, <command>VALUES</command>,
- <command>INSERT</command>, <command>UPDATE</command> or
- <command>DELETE</command> statement.
+ <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command> statement.
When writing a data-modifying statement (<command>INSERT</command>,
- <command>UPDATE</command> or <command>DELETE</command>) in
+ <command>UPDATE</command>, <command>DELETE</command>, or <command>MERGE</command>) in
<literal>WITH</literal>, it is usual to include a <literal>RETURNING</literal> clause.
It is the output of <literal>RETURNING</literal>, <emphasis>not</emphasis> the underlying
table that the statement modifies, that forms the temporary table that is
@@ -2184,7 +2184,8 @@ SELECT 2+2;
<para>
<productname>PostgreSQL</productname> allows <command>INSERT</command>,
- <command>UPDATE</command>, and <command>DELETE</command> to be used as <literal>WITH</literal>
+ <command>UPDATE</command>, <command>DELETE</command>, and
+ <command>MERGE</command> to be used as <literal>WITH</literal>
queries. This is not found in the SQL standard.
</para>
</refsect2>
diff --git a/doc/src/sgml/rowtypes.sgml b/doc/src/sgml/rowtypes.sgml
new file mode 100644
index 4d86f97..bbeac84
--- a/doc/src/sgml/rowtypes.sgml
+++ b/doc/src/sgml/rowtypes.sgml
@@ -348,7 +348,7 @@ SELECT m.* FROM some_table, LATERAL myfu
column expansion of this kind when it appears at the top level of
a <link linkend="queries-select-lists"><command>SELECT</command> output
list</link>, a <link linkend="dml-returning"><literal>RETURNING</literal>
- list</link> in <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>,
+ list</link> in <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command>,
a <link linkend="queries-values"><literal>VALUES</literal> clause</link>, or
a <link linkend="sql-syntax-row-constructors">row constructor</link>.
In all other contexts (including when nested inside one of those
diff --git a/doc/src/sgml/spi.sgml b/doc/src/sgml/spi.sgml
new file mode 100644
index 47f4b5b..bb37786
--- a/doc/src/sgml/spi.sgml
+++ b/doc/src/sgml/spi.sgml
@@ -301,8 +301,9 @@ SPI_execute("INSERT INTO foo SELECT * FR
is returned in the global variable <varname>SPI_processed</varname>.
If the return value of the function is <symbol>SPI_OK_SELECT</symbol>,
<symbol>SPI_OK_INSERT_RETURNING</symbol>,
- <symbol>SPI_OK_DELETE_RETURNING</symbol>, or
- <symbol>SPI_OK_UPDATE_RETURNING</symbol>,
+ <symbol>SPI_OK_DELETE_RETURNING</symbol>,
+ <symbol>SPI_OK_UPDATE_RETURNING</symbol>, or
+ <symbol>SPI_OK_MERGE_RETURNING</symbol>,
then you can use the
global pointer <literal>SPITupleTable *SPI_tuptable</literal> to
access the result rows. Some utility commands (such as
@@ -472,6 +473,15 @@ typedef struct SPITupleTable
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><symbol>SPI_OK_MERGE_RETURNING</symbol></term>
+ <listitem>
+ <para>
+ if a <command>MERGE RETURNING</command> was executed
+ </para>
+ </listitem>
+ </varlistentry>
<varlistentry>
<term><symbol>SPI_OK_UTILITY</symbol></term>
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
new file mode 100644
index 8a79ad0..7d05369
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -177,7 +177,8 @@
statements separated by semicolons. A semicolon after the last
statement is optional. Unless the function is declared to return
<type>void</type>, the last statement must be a <command>SELECT</command>,
- or an <command>INSERT</command>, <command>UPDATE</command>, or <command>DELETE</command>
+ or an <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command>
that has a <literal>RETURNING</literal> clause.
</para>
@@ -1236,8 +1237,9 @@ SELECT x, CASE WHEN x > 0 THEN genera
<note>
<para>
- If a function's last command is <command>INSERT</command>, <command>UPDATE</command>,
- or <command>DELETE</command> with <literal>RETURNING</literal>, that command will
+ If a function's last command is <command>INSERT</command>,
+ <command>UPDATE</command>, <command>DELETE</command>, or
+ <command>MERGE</command> with <literal>RETURNING</literal>, that command will
always be executed to completion, even if the function is not declared
with <literal>SETOF</literal> or the calling query does not fetch all the
result rows. Any extra rows produced by the <literal>RETURNING</literal>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
new file mode 100644
index 056b673..28cf8b0
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -281,12 +281,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 a6962e0..ae8b2e3
--- a/src/backend/commands/copyto.c
+++ b/src/backend/commands/copyto.c
@@ -503,7 +503,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 ffd3ca4..728c8d5
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -1107,6 +1107,19 @@ ExecInitExprRec(Expr *node, ExprState *s
break;
}
+ case T_MergeSupportFunc:
+ {
+ /* must be in a MERGE, else something messed up */
+ if (!state->parent ||
+ !IsA(state->parent, ModifyTableState) ||
+ ((ModifyTableState *) state->parent)->operation != CMD_MERGE)
+ elog(ERROR, "MergeSupportFunc found in non-merge plan node");
+
+ scratch.opcode = EEOP_MERGE_SUPPORT_FUNC;
+ ExprEvalPushStep(state, &scratch);
+ break;
+ }
+
case T_SubscriptingRef:
{
SubscriptingRef *sbsref = (SubscriptingRef *) node;
diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
new file mode 100644
index 7c1f51e..a25ab75
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -484,6 +484,7 @@ ExecInterpExpr(ExprState *state, ExprCon
&&CASE_EEOP_AGGREF,
&&CASE_EEOP_GROUPING_FUNC,
&&CASE_EEOP_WINDOW_FUNC,
+ &&CASE_EEOP_MERGE_SUPPORT_FUNC,
&&CASE_EEOP_SUBPLAN,
&&CASE_EEOP_AGG_STRICT_DESERIALIZE,
&&CASE_EEOP_AGG_DESERIALIZE,
@@ -1592,6 +1593,14 @@ ExecInterpExpr(ExprState *state, ExprCon
EEO_NEXT();
}
+ EEO_CASE(EEOP_MERGE_SUPPORT_FUNC)
+ {
+ /* too complex/uncommon for an inline implementation */
+ ExecEvalMergeSupportFunc(state, op, econtext);
+
+ EEO_NEXT();
+ }
+
EEO_CASE(EEOP_SUBPLAN)
{
/* too complex for an inline implementation */
@@ -4246,6 +4255,45 @@ ExecEvalGroupingFunc(ExprState *state, E
}
/*
+ * ExecEvalMergeSupportFunc
+ *
+ * Returns information about the current MERGE action for its RETURNING list.
+ */
+void
+ExecEvalMergeSupportFunc(ExprState *state, ExprEvalStep *op,
+ ExprContext *econtext)
+{
+ ModifyTableState *mtstate = castNode(ModifyTableState, state->parent);
+ MergeActionState *relaction = mtstate->mt_merge_action;
+
+ if (!relaction)
+ elog(ERROR, "no merge action in progress");
+
+ /* Return the MERGE action ("INSERT", "UPDATE", or "DELETE") */
+ switch (relaction->mas_action->commandType)
+ {
+ case CMD_INSERT:
+ *op->resvalue = PointerGetDatum(cstring_to_text_with_len("INSERT", 6));
+ *op->resnull = false;
+ break;
+ case CMD_UPDATE:
+ *op->resvalue = PointerGetDatum(cstring_to_text_with_len("UPDATE", 6));
+ *op->resnull = false;
+ break;
+ case CMD_DELETE:
+ *op->resvalue = PointerGetDatum(cstring_to_text_with_len("DELETE", 6));
+ *op->resnull = false;
+ break;
+ case CMD_NOTHING:
+ elog(ERROR, "unexpected merge action: DO NOTHING");
+ break;
+ default:
+ elog(ERROR, "unrecognized commandType: %d",
+ (int) relaction->mas_action->commandType);
+ }
+}
+
+/*
* Hand off evaluation of a subplan to nodeSubplan.c
*/
void
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
new file mode 100644
index 8ca512d..64fcb01
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -609,8 +609,8 @@ ExecInitPartitionInfo(ModifyTableState *
* Build the RETURNING projection for the partition. Note that we didn't
* build the returningList for partitions within the planner, but simple
* translation of varattnos will suffice. This only occurs for the INSERT
- * case or in the case of UPDATE tuple routing where we didn't find a
- * result rel to reuse.
+ * case or in the case of UPDATE/MERGE tuple routing where we didn't find
+ * a result rel to reuse.
*/
if (node && node->returningLists != NIL)
{
@@ -619,12 +619,14 @@ ExecInitPartitionInfo(ModifyTableState *
List *returningList;
/* See the comment above for WCO lists. */
- /* (except no RETURNING support for MERGE yet) */
Assert((node->operation == CMD_INSERT &&
list_length(node->returningLists) == 1 &&
list_length(node->resultRelations) == 1) ||
(node->operation == CMD_UPDATE &&
list_length(node->returningLists) ==
+ list_length(node->resultRelations)) ||
+ (node->operation == CMD_MERGE &&
+ list_length(node->returningLists) ==
list_length(node->resultRelations)));
/*
diff --git a/src/backend/executor/functions.c b/src/backend/executor/functions.c
new file mode 100644
index a4b6e1e..f6ccdf0
--- a/src/backend/executor/functions.c
+++ b/src/backend/executor/functions.c
@@ -1660,8 +1660,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
@@ -1679,7 +1679,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;
@@ -1693,7 +1694,7 @@ check_sql_fn_retval(List *queryTreeLists
(errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
errmsg("return type mismatch in function declared to return %s",
format_type_be(rettype)),
- errdetail("Function's final statement must be SELECT or INSERT/UPDATE/DELETE RETURNING.")));
+ errdetail("Function's final statement must be SELECT or INSERT/UPDATE/DELETE/MERGE RETURNING.")));
return false; /* keep compiler quiet */
}
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
new file mode 100644
index 9351fbc..b8a75c5
--- 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"
@@ -85,9 +84,6 @@ typedef struct ModifyTableContext
*/
TupleTableSlot *planSlot;
- /* MERGE specific */
- MergeActionState *relaction; /* MERGE action in progress */
-
/*
* Information about the changes that were made concurrently to a tuple
* being updated or deleted
@@ -150,14 +146,15 @@ static TupleTableSlot *ExecMerge(ModifyT
HeapTuple oldtuple,
bool canSetTag);
static void ExecInitMerge(ModifyTableState *mtstate, EState *estate);
-static bool ExecMergeMatched(ModifyTableContext *context,
- ResultRelInfo *resultRelInfo,
- ItemPointer tupleid,
- HeapTuple oldtuple,
- bool canSetTag);
-static void ExecMergeNotMatched(ModifyTableContext *context,
- ResultRelInfo *resultRelInfo,
- bool canSetTag);
+static TupleTableSlot *ExecMergeMatched(ModifyTableContext *context,
+ ResultRelInfo *resultRelInfo,
+ ItemPointer tupleid,
+ HeapTuple oldtuple,
+ bool canSetTag,
+ bool *matched);
+static TupleTableSlot *ExecMergeNotMatched(ModifyTableContext *context,
+ ResultRelInfo *resultRelInfo,
+ bool canSetTag);
/*
@@ -977,7 +974,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;
@@ -1831,7 +1828,7 @@ ExecCrossPartitionUpdate(ModifyTableCont
* additional rechecking, and might end up executing a different
* action entirely).
*/
- if (context->relaction != NULL)
+ if (mtstate->operation == CMD_MERGE)
return *tmresult == TM_Ok;
else if (TupIsNull(epqslot))
return true;
@@ -2072,7 +2069,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->operation == CMD_MERGE)
return result;
/*
@@ -2713,6 +2710,7 @@ static TupleTableSlot *
ExecMerge(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
ItemPointer tupleid, HeapTuple oldtuple, bool canSetTag)
{
+ TupleTableSlot *rslot = NULL;
bool matched;
/*-----
@@ -2761,19 +2759,18 @@ ExecMerge(ModifyTableContext *context, R
*/
matched = tupleid != NULL || oldtuple != NULL;
if (matched)
- matched = ExecMergeMatched(context, resultRelInfo, tupleid, oldtuple,
- canSetTag);
+ rslot = ExecMergeMatched(context, resultRelInfo, tupleid, oldtuple,
+ canSetTag, &matched);
/*
- * Either we were dealing with a NOT MATCHED tuple or ExecMergeMatched()
- * returned "false", indicating the previously MATCHED tuple no longer
- * matches.
+ * Deal with the NOT MATCHED case (either a NOT MATCHED tuple from the
+ * join, or a previously MATCHED tuple for which ExecMergeMatched() set
+ * "matched" to false, indicating that it no longer matches).
*/
if (!matched)
- ExecMergeNotMatched(context, resultRelInfo, canSetTag);
+ rslot = ExecMergeNotMatched(context, resultRelInfo, canSetTag);
- /* No RETURNING support yet */
- return NULL;
+ return rslot;
}
/*
@@ -2785,8 +2782,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.
*
@@ -2795,16 +2792,18 @@ 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, HeapTuple oldtuple, bool canSetTag)
+ ItemPointer tupleid, HeapTuple oldtuple, bool canSetTag,
+ bool *matched)
{
ModifyTableState *mtstate = context->mtstate;
- TupleTableSlot *newslot;
+ TupleTableSlot *rslot = NULL;
+ TupleTableSlot *newslot = NULL;
EState *estate = context->estate;
ExprContext *econtext = mtstate->ps.ps_ExprContext;
bool isNull;
@@ -2815,7 +2814,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
@@ -2905,12 +2907,15 @@ lmerge_matched:
*/
newslot = ExecProject(relaction->mas_proj);
- context->relaction = relaction;
+ mtstate->mt_merge_action = relaction;
if (!ExecUpdatePrologue(context, resultRelInfo,
tupleid, NULL, newslot, &result))
{
if (result == TM_Ok)
- return true; /* "do nothing" */
+ {
+ *matched = true;
+ return NULL; /* "do nothing" */
+ }
break; /* concurrent update/delete */
}
@@ -2920,7 +2925,10 @@ lmerge_matched:
{
if (!ExecIRUpdateTriggers(estate, resultRelInfo,
oldtuple, newslot))
- return true; /* "do nothing" */
+ {
+ *matched = true;
+ return NULL; /* "do nothing" */
+ }
}
else
{
@@ -2933,12 +2941,15 @@ lmerge_matched:
* cross-partition update was done, then there's nothing
* else for us to do --- the UPDATE has been turned into a
* DELETE and an INSERT, and we must not perform any of
- * the usual post-update tasks.
+ * the usual post-update tasks. Also, the RETURNING tuple
+ * (if any) has been projected, so we can just return
+ * that.
*/
if (updateCxt.crossPartUpdate)
{
mtstate->mt_merge_updated += 1;
- return true;
+ *matched = true;
+ return context->cpUpdateReturningSlot;
}
}
@@ -2951,12 +2962,15 @@ lmerge_matched:
break;
case CMD_DELETE:
- context->relaction = relaction;
+ mtstate->mt_merge_action = relaction;
if (!ExecDeletePrologue(context, resultRelInfo, tupleid,
NULL, NULL, &result))
{
if (result == TM_Ok)
- return true; /* "do nothing" */
+ {
+ *matched = true;
+ return NULL; /* "do nothing" */
+ }
break; /* concurrent update/delete */
}
@@ -2966,7 +2980,10 @@ lmerge_matched:
{
if (!ExecIRDeleteTriggers(estate, resultRelInfo,
oldtuple))
- return true; /* "do nothing" */
+ {
+ *matched = true;
+ return NULL; /* "do nothing" */
+ }
}
else
result = ExecDeleteAct(context, resultRelInfo, tupleid,
@@ -3046,7 +3063,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:
{
@@ -3092,13 +3110,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
@@ -3133,7 +3157,8 @@ lmerge_matched:
* tuple already deleted; tell caller to run NOT
* MATCHED actions
*/
- return false;
+ *matched = false;
+ return NULL;
case TM_SelfModified:
@@ -3161,13 +3186,13 @@ lmerge_matched:
/* This shouldn't happen */
elog(ERROR, "attempted to update or delete invisible tuple");
- return false;
+ return NULL;
default:
/* see table_tuple_lock call in ExecDelete() */
elog(ERROR, "unexpected table_tuple_lock status: %u",
result);
- return false;
+ return NULL;
}
}
@@ -3179,6 +3204,31 @@ 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, "unrecognized commandType: %d",
+ (int) commandType);
+ }
+ }
+
/*
* We've activated one of the WHEN clauses, so we don't search
* further. This is required behaviour, not an optimization.
@@ -3189,19 +3239,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;
/*
@@ -3251,10 +3304,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:
@@ -3270,6 +3323,8 @@ ExecMergeNotMatched(ModifyTableContext *
*/
break;
}
+
+ return rslot;
}
/*
@@ -3732,9 +3787,17 @@ ExecModifyTable(PlanState *pstate)
{
EvalPlanQualSetSlot(&node->mt_epqstate, context.planSlot);
- ExecMerge(&context, node->resultRelInfo, NULL, NULL,
- node->canSetTag);
- continue; /* no RETURNING support yet */
+ slot = ExecMerge(&context, node->resultRelInfo,
+ NULL, 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");
@@ -3811,9 +3874,17 @@ ExecModifyTable(PlanState *pstate)
{
EvalPlanQualSetSlot(&node->mt_epqstate, context.planSlot);
- ExecMerge(&context, node->resultRelInfo, NULL, NULL,
- node->canSetTag);
- continue; /* no RETURNING support yet */
+ slot = ExecMerge(&context, node->resultRelInfo,
+ NULL, 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");
@@ -3860,9 +3931,17 @@ ExecModifyTable(PlanState *pstate)
{
EvalPlanQualSetSlot(&node->mt_epqstate, context.planSlot);
- ExecMerge(&context, node->resultRelInfo, NULL, NULL,
- node->canSetTag);
- continue; /* no RETURNING support yet */
+ slot = ExecMerge(&context, node->resultRelInfo,
+ NULL, 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, "wholerow is NULL");
@@ -3924,7 +4003,6 @@ ExecModifyTable(PlanState *pstate)
}
slot = ExecGetUpdateNewTuple(resultRelInfo, context.planSlot,
oldSlot);
- context.relaction = NULL;
/* Now apply the update. */
slot = ExecUpdate(&context, resultRelInfo, tupleid, oldtuple,
diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c
new file mode 100644
index 8585771..a97a7e3
--- a/src/backend/executor/spi.c
+++ b/src/backend/executor/spi.c
@@ -2032,6 +2032,8 @@ SPI_result_code_string(int code)
return "SPI_OK_TD_REGISTER";
case SPI_OK_MERGE:
return "SPI_OK_MERGE";
+ case SPI_OK_MERGE_RETURNING:
+ return "SPI_OK_MERGE_RETURNING";
}
/* Unrecognized code ... return something useful ... */
sprintf(buf, "Unrecognized SPI code %d", code);
@@ -2885,7 +2887,10 @@ _SPI_pquery(QueryDesc *queryDesc, bool f
res = SPI_OK_UPDATE;
break;
case CMD_MERGE:
- res = SPI_OK_MERGE;
+ if (queryDesc->plannedstmt->hasReturning)
+ res = SPI_OK_MERGE_RETURNING;
+ else
+ res = SPI_OK_MERGE;
break;
default:
return SPI_ERROR_OPUNKNOWN;
diff --git a/src/backend/jit/llvm/llvmjit_expr.c b/src/backend/jit/llvm/llvmjit_expr.c
new file mode 100644
index 0c44842..2a7d84f
--- a/src/backend/jit/llvm/llvmjit_expr.c
+++ b/src/backend/jit/llvm/llvmjit_expr.c
@@ -1986,6 +1986,12 @@ llvm_compile_expr(ExprState *state)
break;
}
+ case EEOP_MERGE_SUPPORT_FUNC:
+ build_EvalXFunc(b, mod, "ExecEvalMergeSupportFunc",
+ v_state, op, v_econtext);
+ LLVMBuildBr(b, opblocks[opno + 1]);
+ break;
+
case EEOP_SUBPLAN:
build_EvalXFunc(b, mod, "ExecEvalSubPlan",
v_state, op, v_econtext);
diff --git a/src/backend/jit/llvm/llvmjit_types.c b/src/backend/jit/llvm/llvmjit_types.c
new file mode 100644
index 47c9daf..7d7aeee
--- a/src/backend/jit/llvm/llvmjit_types.c
+++ b/src/backend/jit/llvm/llvmjit_types.c
@@ -155,6 +155,7 @@ void *referenced_functions[] =
ExecEvalFuncExprFusage,
ExecEvalFuncExprStrictFusage,
ExecEvalGroupingFunc,
+ ExecEvalMergeSupportFunc,
ExecEvalMinMax,
ExecEvalNextValueExpr,
ExecEvalParamExec,
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
new file mode 100644
index 6ba8e73..5b70280
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -66,6 +66,9 @@ exprType(const Node *expr)
case T_WindowFunc:
type = ((const WindowFunc *) expr)->wintype;
break;
+ case T_MergeSupportFunc:
+ type = ((const MergeSupportFunc *) expr)->msftype;
+ break;
case T_SubscriptingRef:
type = ((const SubscriptingRef *) expr)->refrestype;
break;
@@ -809,6 +812,9 @@ exprCollation(const Node *expr)
case T_WindowFunc:
coll = ((const WindowFunc *) expr)->wincollid;
break;
+ case T_MergeSupportFunc:
+ coll = ((const MergeSupportFunc *) expr)->msfcollid;
+ break;
case T_SubscriptingRef:
coll = ((const SubscriptingRef *) expr)->refcollid;
break;
@@ -1084,6 +1090,9 @@ exprSetCollation(Node *expr, Oid collati
case T_WindowFunc:
((WindowFunc *) expr)->wincollid = collation;
break;
+ case T_MergeSupportFunc:
+ ((MergeSupportFunc *) expr)->msfcollid = collation;
+ break;
case T_SubscriptingRef:
((SubscriptingRef *) expr)->refcollid = collation;
break;
@@ -1342,6 +1351,9 @@ exprLocation(const Node *expr)
/* function name should always be the first thing */
loc = ((const WindowFunc *) expr)->location;
break;
+ case T_MergeSupportFunc:
+ loc = ((const MergeSupportFunc *) expr)->location;
+ break;
case T_SubscriptingRef:
/* just use container argument's location */
loc = exprLocation((Node *) ((const SubscriptingRef *) expr)->refexpr);
@@ -2034,6 +2046,7 @@ expression_tree_walker_impl(Node *node,
case T_RangeTblRef:
case T_SortGroupClause:
case T_CTESearchClause:
+ case T_MergeSupportFunc:
/* primitive node types with no expression subnodes */
break;
case T_WithCheckOption:
@@ -2868,6 +2881,7 @@ expression_tree_mutator_impl(Node *node,
case T_RangeTblRef:
case T_SortGroupClause:
case T_CTESearchClause:
+ case T_MergeSupportFunc:
return (Node *) copyObject(node);
case T_WithCheckOption:
{
@@ -3832,6 +3846,7 @@ raw_expression_tree_walker_impl(Node *no
case T_ParamRef:
case T_A_Const:
case T_A_Star:
+ case T_MergeSupportFunc:
/* primitive node types with no subnodes */
break;
case T_Alias:
@@ -4052,6 +4067,8 @@ raw_expression_tree_walker_impl(Node *no
return true;
if (WALK(stmt->mergeWhenClauses))
return true;
+ if (WALK(stmt->returningList))
+ return true;
if (WALK(stmt->withClause))
return true;
}
diff --git a/src/backend/optimizer/plan/subselect.c b/src/backend/optimizer/plan/subselect.c
new file mode 100644
index 47e1472..b9e0c96
--- a/src/backend/optimizer/plan/subselect.c
+++ b/src/backend/optimizer/plan/subselect.c
@@ -1855,7 +1855,8 @@ convert_EXISTS_to_ANY(PlannerInfo *root,
/*
* Replace correlation vars (uplevel vars) with Params.
*
- * Uplevel PlaceHolderVars and aggregates are replaced, too.
+ * Uplevel PlaceHolderVars, aggregates, GROUPING() expressions, and
+ * MergeSupportFuncs are replaced, too.
*
* Note: it is critical that this runs immediately after SS_process_sublinks.
* Since we do not recurse into the arguments of uplevel PHVs and aggregates,
@@ -1909,6 +1910,12 @@ replace_correlation_vars_mutator(Node *n
if (((GroupingFunc *) node)->agglevelsup > 0)
return (Node *) replace_outer_grouping(root, (GroupingFunc *) node);
}
+ if (IsA(node, MergeSupportFunc))
+ {
+ if (root->parse->commandType != CMD_MERGE)
+ return (Node *) replace_outer_merge_support(root,
+ (MergeSupportFunc *) node);
+ }
return expression_tree_mutator(node,
replace_correlation_vars_mutator,
(void *) root);
diff --git a/src/backend/optimizer/util/paramassign.c b/src/backend/optimizer/util/paramassign.c
new file mode 100644
index a58da7c..f461fed
--- a/src/backend/optimizer/util/paramassign.c
+++ b/src/backend/optimizer/util/paramassign.c
@@ -308,6 +308,57 @@ replace_outer_grouping(PlannerInfo *root
}
/*
+ * Generate a Param node to replace the given MergeSupportFunc expression
+ * which is expected to be in the RETURNING list of an upper-level MERGE
+ * query. Record the need for the MergeSupportFunc in the proper upper-level
+ * root->plan_params.
+ */
+Param *
+replace_outer_merge_support(PlannerInfo *root, MergeSupportFunc *msf)
+{
+ Param *retval;
+ PlannerParamItem *pitem;
+ Oid ptype = exprType((Node *) msf);
+
+ Assert(root->parse->commandType != CMD_MERGE);
+
+ /*
+ * The parser should have ensured that the MergeSupportFunc is in the
+ * RETURNING list of an upper-level MERGE query, so find that query.
+ */
+ do
+ {
+ root = root->parent_root;
+ if (root == NULL)
+ elog(ERROR, "MergeSupportFunc found outside MERGE");
+ } while (root->parse->commandType != CMD_MERGE);
+
+ /*
+ * It does not seem worthwhile to try to de-duplicate references to outer
+ * MergeSupportFunc expressions. Just make a new slot every time.
+ */
+ msf = copyObject(msf);
+
+ pitem = makeNode(PlannerParamItem);
+ pitem->item = (Node *) msf;
+ pitem->paramId = list_length(root->glob->paramExecTypes);
+ root->glob->paramExecTypes = lappend_oid(root->glob->paramExecTypes,
+ ptype);
+
+ root->plan_params = lappend(root->plan_params, pitem);
+
+ retval = makeNode(Param);
+ retval->paramkind = PARAM_EXEC;
+ retval->paramid = pitem->paramId;
+ retval->paramtype = ptype;
+ retval->paramtypmod = -1;
+ retval->paramcollid = InvalidOid;
+ retval->location = msf->location;
+
+ return retval;
+}
+
+/*
* Generate a Param node to replace the given Var,
* which is expected to come from some upper NestLoop plan node.
* Record the need for the Var in root->curOuterParams.
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
new file mode 100644
index 2255314..7bd947b
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -72,7 +72,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,
@@ -551,7 +550,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;
@@ -978,7 +978,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;
@@ -2443,7 +2444,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
@@ -2540,10 +2542,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;
@@ -2560,7 +2563,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 c6e2f67..6419b32
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -733,7 +733,7 @@ static Node *makeRecursiveViewSelect(cha
LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL
LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOGGED
- MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE METHOD
+ MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE MERGE_ACTION METHOD
MINUTE_P MINVALUE MODE MONTH_P MOVE
NAME_P NAMES NATIONAL NATURAL NCHAR NEW NEXT NFC NFD NFKC NFKD NO NONE
@@ -12374,6 +12374,7 @@ MergeStmt:
USING table_ref
ON a_expr
merge_when_list
+ returning_clause
{
MergeStmt *m = makeNode(MergeStmt);
@@ -12382,6 +12383,7 @@ MergeStmt:
m->sourceRelation = $6;
m->joinCondition = $8;
m->mergeWhenClauses = $9;
+ m->returningList = $10;
$$ = (Node *) m;
}
@@ -15795,6 +15797,14 @@ func_expr_common_subexpr:
n->location = @1;
$$ = (Node *) n;
}
+ | MERGE_ACTION '(' ')'
+ {
+ MergeSupportFunc *m = makeNode(MergeSupportFunc);
+
+ m->msftype = TEXTOID;
+ m->location = @1;
+ $$ = (Node *) m;
+ }
;
@@ -17492,6 +17502,7 @@ col_name_keyword:
| JSON_SCALAR
| JSON_SERIALIZE
| LEAST
+ | MERGE_ACTION
| NATIONAL
| NCHAR
| NONE
@@ -17881,6 +17892,7 @@ bare_label_keyword:
| MATERIALIZED
| MAXVALUE
| MERGE
+ | MERGE_ACTION
| METHOD
| MINVALUE
| MODE
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
new file mode 100644
index 9d151a8..bee7d83
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -468,6 +468,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:
@@ -915,6 +916,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 3c88c9a..6826d4f
--- 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 9300c7b..fe56101
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -54,6 +54,7 @@ static Node *transformAExprDistinct(Pars
static Node *transformAExprNullIf(ParseState *pstate, A_Expr *a);
static Node *transformAExprIn(ParseState *pstate, A_Expr *a);
static Node *transformAExprBetween(ParseState *pstate, A_Expr *a);
+static Node *transformMergeSupportFunc(ParseState *pstate, MergeSupportFunc *f);
static Node *transformBoolExpr(ParseState *pstate, BoolExpr *a);
static Node *transformFuncCall(ParseState *pstate, FuncCall *fn);
static Node *transformMultiAssignRef(ParseState *pstate, MultiAssignRef *maref);
@@ -227,6 +228,11 @@ transformExprRecurse(ParseState *pstate,
result = transformGroupingFunc(pstate, (GroupingFunc *) expr);
break;
+ case T_MergeSupportFunc:
+ result = transformMergeSupportFunc(pstate,
+ (MergeSupportFunc *) expr);
+ break;
+
case T_NamedArgExpr:
{
NamedArgExpr *na = (NamedArgExpr *) expr;
@@ -541,6 +547,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:
@@ -1354,6 +1361,32 @@ transformAExprBetween(ParseState *pstate
}
static Node *
+transformMergeSupportFunc(ParseState *pstate, MergeSupportFunc *f)
+{
+ /*
+ * All we need to do is check that we're in the RETURNING list of a MERGE
+ * command. If so, we just return the node as-is.
+ */
+ if (pstate->p_expr_kind != EXPR_KIND_MERGE_RETURNING)
+ {
+ ParseState *parent_pstate = pstate->parentParseState;
+
+ while (parent_pstate &&
+ parent_pstate->p_expr_kind != EXPR_KIND_MERGE_RETURNING)
+ parent_pstate = parent_pstate->parentParseState;
+
+ if (!parent_pstate ||
+ parent_pstate->p_expr_kind != EXPR_KIND_MERGE_RETURNING)
+ ereport(ERROR,
+ errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("MERGE_ACTION() can only be used in the RETURNING list of a MERGE command"),
+ parser_errposition(pstate, f->location));
+ }
+
+ return (Node *) f;
+}
+
+static Node *
transformBoolExpr(ParseState *pstate, BoolExpr *a)
{
List *args = NIL;
@@ -1767,6 +1800,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:
@@ -3115,6 +3149,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 fdb3e6d..0cbc950
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -2599,6 +2599,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 6b99f55..efb739d
--- a/src/backend/parser/parse_merge.c
+++ b/src/backend/parser/parse_merge.c
@@ -234,6 +234,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.
@@ -391,9 +395,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 6f5d9e2..427b732
--- a/src/backend/parser/parse_relation.c
+++ b/src/backend/parser/parse_relation.c
@@ -2341,9 +2341,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/parser/parse_target.c b/src/backend/parser/parse_target.c
new file mode 100644
index f10fc42..8923be1
--- a/src/backend/parser/parse_target.c
+++ b/src/backend/parser/parse_target.c
@@ -1806,6 +1806,10 @@ FigureColnameInternal(Node *node, char *
/* make GROUPING() act like a regular function */
*name = "grouping";
return 2;
+ case T_MergeSupportFunc:
+ /* make MERGE_ACTION() act like a regular function */
+ *name = "merge_action";
+ return 2;
case T_SubLink:
switch (((SubLink *) node)->subLinkType)
{
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
new file mode 100644
index 89187d9..c3fa282
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -3833,9 +3833,9 @@ RewriteQuery(Query *parsetree, List *rew
ListCell *lc1;
/*
- * First, recursively process any insert/update/delete statements in WITH
- * clauses. (We have to do this first because the WITH clauses may get
- * copied into rule actions below.)
+ * First, recursively process any insert/update/delete/merge statements in
+ * WITH clauses. (We have to do this first because the WITH clauses may
+ * get copied into rule actions below.)
*/
foreach(lc1, parsetree->cteList)
{
@@ -3860,7 +3860,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/rewrite/rowsecurity.c b/src/backend/rewrite/rowsecurity.c
new file mode 100644
index 450af27..59fd305
--- a/src/backend/rewrite/rowsecurity.c
+++ b/src/backend/rewrite/rowsecurity.c
@@ -384,10 +384,10 @@ get_row_security_policies(Query *root, R
* on the final action we take.
*
* We already fetched the SELECT policies above, to check existing rows,
- * but we must also check that new rows created by UPDATE actions are
- * visible, if SELECT rights are required for this relation. We don't do
- * this for INSERT actions, since an INSERT command would only do this
- * check if it had a RETURNING list, and MERGE does not support RETURNING.
+ * but we must also check that new rows created by INSERT/UPDATE actions
+ * are visible, if SELECT rights are required. For INSERT actions, we only
+ * do this if RETURNING is specified, to be consistent with a plain INSERT
+ * command, which can only require SELECT rights when RETURNING is used.
*
* 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
@@ -409,6 +409,8 @@ get_row_security_policies(Query *root, R
List *merge_delete_restrictive_policies;
List *merge_insert_permissive_policies;
List *merge_insert_restrictive_policies;
+ List *merge_select_permissive_policies = NIL;
+ List *merge_select_restrictive_policies = NIL;
/*
* Fetch the UPDATE policies and set them up to execute on the
@@ -446,9 +448,6 @@ get_row_security_policies(Query *root, R
*/
if (perminfo->requiredPerms & ACL_SELECT)
{
- List *merge_select_permissive_policies;
- List *merge_select_restrictive_policies;
-
get_policies_for_relation(rel, CMD_SELECT, user_id,
&merge_select_permissive_policies,
&merge_select_restrictive_policies);
@@ -497,6 +496,21 @@ get_row_security_policies(Query *root, R
withCheckOptions,
hasSubLinks,
false);
+
+ /*
+ * Add ALL/SELECT policies as WCO_RLS_INSERT_CHECK WCOs, to ensure
+ * that the inserted row is visible when executing an INSERT action,
+ * if RETURNING is specified and SELECT rights are required for this
+ * relation.
+ */
+ if (perminfo->requiredPerms & ACL_SELECT && root->returningList)
+ add_with_check_options(rel, rt_index,
+ WCO_RLS_INSERT_CHECK,
+ merge_select_permissive_policies,
+ merge_select_restrictive_policies,
+ withCheckOptions,
+ hasSubLinks,
+ true);
}
table_close(rel, NoLock);
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
new file mode 100644
index 83f86a4..fa66b80
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -2138,11 +2138,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/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
new file mode 100644
index 2a1ee69..e2b46b6
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -7196,8 +7196,13 @@ get_merge_query_def(Query *query, depars
appendStringInfoString(buf, "DO NOTHING");
}
- /* No RETURNING support in MERGE yet */
- Assert(query->returningList == NIL);
+ /* Add RETURNING if present */
+ if (query->returningList)
+ {
+ appendContextKeyword(context, " RETURNING",
+ -PRETTYINDENT_STD, PRETTYINDENT_STD, 1);
+ get_target_list(query->returningList, context, NULL, colNamesVisible);
+ }
}
@@ -8297,6 +8302,7 @@ isSimpleNode(Node *node, Node *parentNod
case T_Aggref:
case T_GroupingFunc:
case T_WindowFunc:
+ case T_MergeSupportFunc:
case T_FuncExpr:
case T_JsonConstructorExpr:
/* function-like: name(..) or name[..] */
@@ -8651,6 +8657,10 @@ get_rule_expr(Node *node, deparse_contex
get_windowfunc_expr((WindowFunc *) node, context);
break;
+ case T_MergeSupportFunc:
+ appendStringInfoString(buf, "MERGE_ACTION()");
+ break;
+
case T_SubscriptingRef:
{
SubscriptingRef *sbsref = (SubscriptingRef *) node;
diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c
new file mode 100644
index 76e01b0..2830bde
--- a/src/bin/psql/common.c
+++ b/src/bin/psql/common.c
@@ -983,13 +983,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/executor/execExpr.h b/src/include/executor/execExpr.h
new file mode 100644
index a28ddcd..8953d76
--- a/src/include/executor/execExpr.h
+++ b/src/include/executor/execExpr.h
@@ -243,6 +243,7 @@ typedef enum ExprEvalOp
EEOP_AGGREF,
EEOP_GROUPING_FUNC,
EEOP_WINDOW_FUNC,
+ EEOP_MERGE_SUPPORT_FUNC,
EEOP_SUBPLAN,
/* aggregation related nodes */
@@ -810,6 +811,8 @@ extern void ExecEvalJsonConstructor(Expr
ExprContext *econtext);
extern void ExecEvalJsonIsPredicate(ExprState *state, ExprEvalStep *op);
extern void ExecEvalGroupingFunc(ExprState *state, ExprEvalStep *op);
+extern void ExecEvalMergeSupportFunc(ExprState *state, ExprEvalStep *op,
+ ExprContext *econtext);
extern void ExecEvalSubPlan(ExprState *state, ExprEvalStep *op,
ExprContext *econtext);
extern void ExecEvalWholeRowVar(ExprState *state, ExprEvalStep *op,
diff --git a/src/include/executor/spi.h b/src/include/executor/spi.h
new file mode 100644
index c13e1f6..48b8773
--- a/src/include/executor/spi.h
+++ b/src/include/executor/spi.h
@@ -97,6 +97,7 @@ typedef struct _SPI_plan *SPIPlanPtr;
#define SPI_OK_REL_UNREGISTER 16
#define SPI_OK_TD_REGISTER 17
#define SPI_OK_MERGE 18
+#define SPI_OK_MERGE_RETURNING 19
#define SPI_OPT_NONATOMIC (1 << 0)
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
new file mode 100644
index 444a5f0..57fdfca
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1325,6 +1325,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 2380821..2bae226
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -1938,6 +1938,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/nodes/primnodes.h b/src/include/nodes/primnodes.h
new file mode 100644
index 4a15460..8df8884
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -572,6 +572,27 @@ typedef struct WindowFunc
} WindowFunc;
/*
+ * MergeSupportFunc
+ *
+ * A MergeSupportFunc is a merge support function expression that can only
+ * appear in the RETURNING list of a MERGE command. It returns information
+ * about the currently executing merge action.
+ *
+ * Currently, the only supported function is MERGE_ACTION(), which returns the
+ * command executed ("INSERT", "UPDATE", or "DELETE").
+ */
+typedef struct MergeSupportFunc
+{
+ Expr xpr;
+ /* type Oid of result */
+ Oid msftype;
+ /* OID of collation, or InvalidOid if none */
+ Oid msfcollid;
+ /* token location, or -1 if unknown */
+ int location;
+} MergeSupportFunc;
+
+/*
* SubscriptingRef: describes a subscripting operation over a container
* (array, etc).
*
diff --git a/src/include/optimizer/paramassign.h b/src/include/optimizer/paramassign.h
new file mode 100644
index 501f728..4026b74
--- a/src/include/optimizer/paramassign.h
+++ b/src/include/optimizer/paramassign.h
@@ -20,6 +20,8 @@ extern Param *replace_outer_placeholderv
PlaceHolderVar *phv);
extern Param *replace_outer_agg(PlannerInfo *root, Aggref *agg);
extern Param *replace_outer_grouping(PlannerInfo *root, GroupingFunc *grp);
+extern Param *replace_outer_merge_support(PlannerInfo *root,
+ MergeSupportFunc *msf);
extern Param *replace_nestloop_param_var(PlannerInfo *root, Var *var);
extern Param *replace_nestloop_param_placeholdervar(PlannerInfo *root,
PlaceHolderVar *phv);
diff --git a/src/include/parser/analyze.h b/src/include/parser/analyze.h
new file mode 100644
index 5ecdd10..28b66fc
--- 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/kwlist.h b/src/include/parser/kwlist.h
new file mode 100644
index 2331aca..0993534
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -266,6 +266,7 @@ PG_KEYWORD("matched", MATCHED, UNRESERVE
PG_KEYWORD("materialized", MATERIALIZED, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("maxvalue", MAXVALUE, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("merge", MERGE, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("merge_action", MERGE_ACTION, COL_NAME_KEYWORD, BARE_LABEL)
PG_KEYWORD("method", METHOD, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("minute", MINUTE_P, UNRESERVED_KEYWORD, AS_LABEL)
PG_KEYWORD("minvalue", MINVALUE, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
new file mode 100644
index 99d6515..5b781d8
--- 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/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
new file mode 100644
index ed51694..6947575
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -4267,9 +4267,9 @@ exec_stmt_execsql(PLpgSQL_execstate *est
/*
* If we have INTO, then we only need one row back ... but if we have INTO
* STRICT or extra check too_many_rows, ask for two rows, so that we can
- * verify the statement returns only one. INSERT/UPDATE/DELETE are always
- * treated strictly. Without INTO, just run the statement to completion
- * (tcount = 0).
+ * verify the statement returns only one. INSERT/UPDATE/DELETE/MERGE are
+ * always treated strictly. Without INTO, just run the statement to
+ * completion (tcount = 0).
*
* We could just ask for two rows always when using INTO, but there are
* some cases where demanding the extra row costs significant time, eg by
@@ -4307,10 +4307,11 @@ exec_stmt_execsql(PLpgSQL_execstate *est
case SPI_OK_INSERT:
case SPI_OK_UPDATE:
case SPI_OK_DELETE:
+ case SPI_OK_MERGE:
case SPI_OK_INSERT_RETURNING:
case SPI_OK_UPDATE_RETURNING:
case SPI_OK_DELETE_RETURNING:
- case SPI_OK_MERGE:
+ case SPI_OK_MERGE_RETURNING:
Assert(stmt->mod_stmt);
exec_set_found(estate, (SPI_processed != 0));
break;
@@ -4489,10 +4490,11 @@ exec_stmt_dynexecute(PLpgSQL_execstate *
case SPI_OK_INSERT:
case SPI_OK_UPDATE:
case SPI_OK_DELETE:
+ case SPI_OK_MERGE:
case SPI_OK_INSERT_RETURNING:
case SPI_OK_UPDATE_RETURNING:
case SPI_OK_DELETE_RETURNING:
- case SPI_OK_MERGE:
+ case SPI_OK_MERGE_RETURNING:
case SPI_OK_UTILITY:
case SPI_OK_REWRITTEN:
break;
diff --git a/src/pl/tcl/pltcl.c b/src/pl/tcl/pltcl.c
new file mode 100644
index 6187e15..18d14a2
--- a/src/pl/tcl/pltcl.c
+++ b/src/pl/tcl/pltcl.c
@@ -2456,6 +2456,7 @@ pltcl_process_SPI_result(Tcl_Interp *int
case SPI_OK_INSERT_RETURNING:
case SPI_OK_DELETE_RETURNING:
case SPI_OK_UPDATE_RETURNING:
+ case SPI_OK_MERGE_RETURNING:
/*
* Process the tuples we got
diff --git a/src/test/regress/expected/merge.out b/src/test/regress/expected/merge.out
new file mode 100644
index e3ebf46..28b39b9
--- a/src/test/regress/expected/merge.out
+++ b/src/test/regress/expected/merge.out
@@ -124,20 +124,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
-- materialized view
CREATE MATERIALIZED VIEW mv AS SELECT * FROM target;
@@ -1255,7 +1255,7 @@ BEGIN;
MERGE INTO sq_target
USING v
ON tid = sid
-WHEN MATCHED AND tid > 2 THEN
+WHEN MATCHED AND tid >= 2 THEN
UPDATE SET balance = balance + delta
WHEN NOT MATCHED THEN
INSERT (balance, tid) VALUES (balance + delta, sid)
@@ -1270,7 +1270,7 @@ INSERT INTO sq_source (sid, balance, del
MERGE INTO sq_target t
USING v
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)
@@ -1294,7 +1294,7 @@ WITH targq AS (
MERGE INTO sq_target t
USING v
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)
@@ -1303,20 +1303,190 @@ WHEN MATCHED AND tid < 2 THEN
ROLLBACK;
-- RETURNING
BEGIN;
-INSERT INTO sq_source (sid, balance, delta) VALUES (-1, -1, -10);
+CREATE TABLE merge_actions(action text, abbrev text);
+INSERT INTO merge_actions VALUES ('INSERT', 'ins'), ('UPDATE', 'upd'), ('DELETE', 'del');
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)
+ 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 *;
- ^
+ DELETE
+RETURNING (SELECT abbrev FROM merge_actions
+ WHERE action = merge_action()) AS action,
+ t.*,
+ CASE merge_action()
+ WHEN 'INSERT' THEN 'Inserted '||t
+ WHEN 'UPDATE' THEN 'Added '||delta||' to balance'
+ WHEN 'DELETE' THEN 'Removed '||t
+ END AS description;
+ action | tid | balance | description
+--------+-----+---------+---------------------
+ del | 1 | 100 | Removed (1,100)
+ upd | 2 | 220 | Added 20 to balance
+ ins | 4 | 40 | Inserted (4,40)
+(3 rows)
+
+ROLLBACK;
+-- error when using merge_action() outside MERGE
+SELECT merge_action() FROM sq_target;
+ERROR: MERGE_ACTION() can only be used in the RETURNING list of a MERGE command
+LINE 1: SELECT merge_action() FROM sq_target;
+ ^
+UPDATE sq_target SET balance = balance + 1 RETURNING merge_action();
+ERROR: MERGE_ACTION() can only be used in the RETURNING list of a MERGE command
+LINE 1: ...ATE sq_target SET balance = balance + 1 RETURNING merge_acti...
+ ^
+-- RETURNING in CTEs
+CREATE TABLE sq_target_merge_log (tid integer NOT NULL, last_change text);
+INSERT INTO sq_target_merge_log VALUES (1, 'Original value');
+BEGIN;
+WITH m AS (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 merge_action() AS action, t.*,
+ CASE merge_action()
+ WHEN 'INSERT' THEN 'Inserted '||t
+ WHEN 'UPDATE' THEN 'Added '||delta||' to balance'
+ WHEN 'DELETE' THEN 'Removed '||t
+ END AS description
+), m2 AS (
+ MERGE INTO sq_target_merge_log l
+ USING m
+ ON l.tid = m.tid
+ WHEN MATCHED THEN
+ UPDATE SET last_change = description
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (m.tid, description)
+ RETURNING action, merge_action() AS log_action, l.*
+)
+SELECT * FROM m2;
+ action | log_action | tid | last_change
+--------+------------+-----+---------------------
+ DELETE | UPDATE | 1 | Removed (1,100)
+ UPDATE | INSERT | 2 | Added 20 to balance
+ INSERT | INSERT | 4 | Inserted (4,40)
+(3 rows)
+
+SELECT * FROM sq_target_merge_log ORDER BY tid;
+ tid | last_change
+-----+---------------------
+ 1 | Removed (1,100)
+ 2 | Added 20 to balance
+ 4 | Inserted (4,40)
+(3 rows)
+
+ROLLBACK;
+-- COPY (MERGE ... RETURNING) TO ...
+BEGIN;
+COPY (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 merge_action(), t.*
+) TO stdout;
+DELETE 1 100
+UPDATE 2 220
+INSERT 4 40
+ROLLBACK;
+-- SQL function with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT action text, OUT tid int, OUT new_balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING merge_action(), t.*;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT action, tid, new_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ action | tid | new_balance
+--------+-----+-------------
+ DELETE | 1 | 100
+ UPDATE | 3 | 320
+ INSERT | 4 | 110
+(3 rows)
+
+ROLLBACK;
+-- SQL SRF with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_sq_source_into_sq_target()
+RETURNS TABLE (action text, tid int, balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 merge_action(), t.*;
+$$;
+SELECT * FROM merge_sq_source_into_sq_target();
+ action | tid | balance
+--------+-----+---------
+ DELETE | 1 | 100
+ UPDATE | 2 | 220
+ INSERT | 4 | 40
+(3 rows)
+
+ROLLBACK;
+-- PL/pgSQL function with MERGE ... RETURNING ... INTO
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT r_action text, OUT r_tid int, OUT r_balance int)
+LANGUAGE plpgsql AS
+$$
+BEGIN
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING merge_action(), t.* INTO r_action, r_tid, r_balance;
+END;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT r_action, r_tid, r_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ r_action | r_tid | r_balance
+----------+-------+-----------
+ DELETE | 1 | 100
+ UPDATE | 3 | 320
+ INSERT | 4 | 110
+(3 rows)
+
ROLLBACK;
-- EXPLAIN
CREATE TABLE ex_mtarget (a int, b int)
@@ -1513,7 +1683,7 @@ SELECT * FROM sq_target WHERE tid = 1;
(1 row)
ROLLBACK;
-DROP TABLE sq_target, sq_source CASCADE;
+DROP TABLE sq_target, sq_target_merge_log, sq_source CASCADE;
NOTICE: drop cascades to view v
CREATE TABLE pa_target (tid integer, balance float, val text)
PARTITION BY LIST (tid);
@@ -1639,6 +1809,32 @@ SELECT * FROM pa_target ORDER BY tid;
(14 rows)
ROLLBACK;
+-- update partition key to partition not initially scanned
+BEGIN;
+MERGE INTO pa_target t
+ USING pa_source s
+ ON t.tid = s.sid AND t.tid = 1
+ WHEN MATCHED THEN
+ UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
+ RETURNING merge_action(), t.*;
+ merge_action | tid | balance | val
+--------------+-----+---------+--------------------------
+ UPDATE | 2 | 110 | initial updated by merge
+(1 row)
+
+SELECT * FROM pa_target ORDER BY tid;
+ tid | balance | val
+-----+---------+--------------------------
+ 2 | 110 | initial updated by merge
+ 3 | 300 | initial
+ 5 | 500 | initial
+ 7 | 700 | initial
+ 9 | 900 | initial
+ 11 | 1100 | initial
+ 13 | 1300 | initial
+(7 rows)
+
+ROLLBACK;
DROP TABLE pa_target CASCADE;
-- The target table is partitioned in the same way, but this time by attaching
-- partitions which have columns in different order, dropped columns etc.
@@ -1888,7 +2084,21 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge');
+ INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge')
+ RETURNING merge_action(), t.*;
+ merge_action | logts | tid | balance | val
+--------------+--------------------------+-----+---------+--------------------------
+ UPDATE | Tue Jan 31 00:00:00 2017 | 1 | 110 | initial updated by merge
+ UPDATE | Tue Feb 28 00:00:00 2017 | 2 | 220 | initial updated by merge
+ INSERT | Sun Jan 15 00:00:00 2017 | 3 | 30 | inserted by merge
+ UPDATE | Tue Jan 31 00:00:00 2017 | 4 | 440 | initial updated by merge
+ UPDATE | Tue Feb 28 00:00:00 2017 | 5 | 550 | initial updated by merge
+ INSERT | Sun Jan 15 00:00:00 2017 | 6 | 60 | inserted by merge
+ UPDATE | Tue Jan 31 00:00:00 2017 | 7 | 770 | initial updated by merge
+ UPDATE | Tue Feb 28 00:00:00 2017 | 8 | 880 | initial updated by merge
+ INSERT | Sun Jan 15 00:00:00 2017 | 9 | 90 | inserted by merge
+(9 rows)
+
SELECT * FROM pa_target ORDER BY tid;
logts | tid | balance | val
--------------------------+-----+---------+--------------------------
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
new file mode 100644
index 6988128..3fd4fab
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -2302,6 +2302,35 @@ WHEN MATCHED THEN
UPDATE SET dnotes = dnotes || ' notes added by merge8 '
WHEN NOT MATCHED THEN
INSERT VALUES (13, 44, 1, 'regress_rls_bob', 'new manga');
+SELECT * FROM document WHERE did = 13;
+ did | cid | dlevel | dauthor | dtitle | dnotes
+-----+-----+--------+---------+--------+--------
+(0 rows)
+
+-- but not OK if RETURNING is used
+MERGE INTO document d
+USING (SELECT 14 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge9 '
+WHEN NOT MATCHED THEN
+ INSERT VALUES (14, 44, 1, 'regress_rls_bob', 'new manga')
+RETURNING *;
+ERROR: new row violates row-level security policy for table "document"
+-- but OK if new row is visible
+MERGE INTO document d
+USING (SELECT 14 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge10 '
+WHEN NOT MATCHED THEN
+ INSERT VALUES (14, 11, 1, 'regress_rls_bob', 'new novel')
+RETURNING *;
+ sdid | did | cid | dlevel | dauthor | dtitle | dnotes
+------+-----+-----+--------+-----------------+-----------+--------
+ 14 | 14 | 11 | 1 | regress_rls_bob | new novel |
+(1 row)
+
RESET SESSION AUTHORIZATION;
-- drop the restrictive SELECT policy so that we can look at the
-- final state of the table
@@ -2325,7 +2354,8 @@ SELECT * FROM document;
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 notes added by merge7
13 | 44 | 1 | regress_rls_bob | new manga |
-(15 rows)
+ 14 | 11 | 1 | regress_rls_bob | new novel |
+(16 rows)
--
-- ROLE/GROUP
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
new file mode 100644
index 0cd2c64..84e359f
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -3638,7 +3638,7 @@ MERGE INTO rule_merge1 t USING (SELECT 1
-- test deparsing
CREATE TABLE sf_target(id int, data text, filling int[]);
CREATE FUNCTION merge_sf_test()
- RETURNS void
+ RETURNS TABLE(action text, a int, b text, id int, data text, filling int[])
LANGUAGE sql
BEGIN ATOMIC
MERGE INTO sf_target t
@@ -3675,11 +3675,13 @@ WHEN NOT MATCHED
VALUES (s.a, s.b, DEFAULT)
WHEN NOT MATCHED
THEN INSERT (filling[1], id)
- VALUES (s.a, s.a);
+ VALUES (s.a, s.a)
+RETURNING
+ merge_action() AS action, *;
END;
\sf merge_sf_test
CREATE OR REPLACE FUNCTION public.merge_sf_test()
- RETURNS void
+ RETURNS TABLE(action text, a integer, b text, id integer, data text, filling integer[])
LANGUAGE sql
BEGIN ATOMIC
MERGE INTO sf_target t
@@ -3716,7 +3718,13 @@ BEGIN ATOMIC
VALUES (s.a, s.b, DEFAULT)
WHEN NOT MATCHED
THEN INSERT (filling[1], id)
- VALUES (s.a, s.a);
+ VALUES (s.a, s.a)
+ RETURNING MERGE_ACTION() AS action,
+ s.a,
+ s.b,
+ t.id,
+ t.data,
+ t.filling;
END
DROP FUNCTION merge_sf_test;
DROP TABLE sf_target;
diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out
new file mode 100644
index 794cf9c..1062c34
--- a/src/test/regress/expected/updatable_views.out
+++ b/src/test/regress/expected/updatable_views.out
@@ -479,7 +479,15 @@ MERGE INTO rw_view1 t
(2, 'ROW 2'), (3, 'ROW 3')) AS v(a,b) ON t.a = v.a
WHEN MATCHED AND t.a <= 1 THEN UPDATE SET b = v.b
WHEN MATCHED THEN DELETE
- WHEN NOT MATCHED AND a > 0 THEN INSERT (a) VALUES (v.a);
+ WHEN NOT MATCHED AND a > 0 THEN INSERT (a) VALUES (v.a)
+ RETURNING merge_action(), v.*, t.*;
+ merge_action | a | b | a | b
+--------------+---+-------+---+-------------
+ UPDATE | 1 | ROW 1 | 1 | ROW 1
+ DELETE | 3 | ROW 3 | 3 | Row 3
+ INSERT | 2 | ROW 2 | 2 | Unspecified
+(3 rows)
+
SELECT * FROM base_tbl ORDER BY a;
a | b
----+-------------
@@ -631,7 +639,15 @@ MERGE INTO rw_view2 t
USING (VALUES (3, 'R3'), (4, 'R4'), (5, 'R5')) AS v(a,b) ON aaa = v.a
WHEN MATCHED AND aaa = 3 THEN DELETE
WHEN MATCHED THEN UPDATE SET bbb = v.b
- WHEN NOT MATCHED THEN INSERT (aaa) VALUES (v.a);
+ WHEN NOT MATCHED THEN INSERT (aaa) VALUES (v.a)
+ RETURNING merge_action(), v.*, t.*;
+ merge_action | a | b | aaa | bbb
+--------------+---+----+-----+-------------
+ DELETE | 3 | R3 | 3 | Row 3
+ UPDATE | 4 | R4 | 4 | R4
+ INSERT | 5 | R5 | 5 | Unspecified
+(3 rows)
+
SELECT * FROM rw_view2 ORDER BY aaa;
aaa | bbb
-----+-------------
@@ -1071,7 +1087,15 @@ MERGE INTO rw_view2 t
USING (SELECT x, 'R'||x FROM generate_series(0,3) x) AS s(a,b) ON t.a = s.a
WHEN MATCHED AND t.a <= 1 THEN DELETE
WHEN MATCHED THEN UPDATE SET b = s.b
- WHEN NOT MATCHED AND s.a > 0 THEN INSERT VALUES (s.a, s.b);
+ WHEN NOT MATCHED AND s.a > 0 THEN INSERT VALUES (s.a, s.b)
+ RETURNING merge_action(), s.*, t.*;
+ merge_action | a | b | a | b
+--------------+---+----+---+-------
+ DELETE | 1 | R1 | 1 | Row 1
+ UPDATE | 2 | R2 | 2 | R2
+ INSERT | 3 | R3 | 3 | R3
+(3 rows)
+
SELECT * FROM base_tbl ORDER BY a;
a | b
----+--------
diff --git a/src/test/regress/expected/with.out b/src/test/regress/expected/with.out
new file mode 100644
index 7d796ea..6de347b
--- a/src/test/regress/expected/with.out
+++ b/src/test/regress/expected/with.out
@@ -3493,6 +3493,16 @@ SELECT * FROM t;
ERROR: WITH query "t" does not have a RETURNING clause
LINE 4: SELECT * FROM t;
^
+-- RETURNING tries to return its own output
+WITH RECURSIVE t(action, a) AS (
+ MERGE INTO y USING (VALUES (11)) v(a) ON y.a = v.a
+ WHEN NOT MATCHED THEN INSERT VALUES (v.a)
+ RETURNING merge_action(), (SELECT a FROM t)
+)
+SELECT * FROM t;
+ERROR: recursive query "t" must not contain data-modifying statements
+LINE 1: WITH RECURSIVE t(action, a) AS (
+ ^
-- data-modifying WITH allowed only at the top level
SELECT * FROM (
WITH t AS (UPDATE y SET a=a+1 RETURNING *)
diff --git a/src/test/regress/sql/merge.sql b/src/test/regress/sql/merge.sql
new file mode 100644
index 79a1868..101f2e9
--- a/src/test/regress/sql/merge.sql
+++ b/src/test/regress/sql/merge.sql
@@ -88,12 +88,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
@@ -817,7 +817,7 @@ BEGIN;
MERGE INTO sq_target
USING v
ON tid = sid
-WHEN MATCHED AND tid > 2 THEN
+WHEN MATCHED AND tid >= 2 THEN
UPDATE SET balance = balance + delta
WHEN NOT MATCHED THEN
INSERT (balance, tid) VALUES (balance + delta, sid)
@@ -830,7 +830,7 @@ INSERT INTO sq_source (sid, balance, del
MERGE INTO sq_target t
USING v
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)
@@ -848,7 +848,7 @@ WITH targq AS (
MERGE INTO sq_target t
USING v
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)
@@ -858,17 +858,145 @@ ROLLBACK;
-- RETURNING
BEGIN;
-INSERT INTO sq_source (sid, balance, delta) VALUES (-1, -1, -10);
+CREATE TABLE merge_actions(action text, abbrev text);
+INSERT INTO merge_actions VALUES ('INSERT', 'ins'), ('UPDATE', 'upd'), ('DELETE', 'del');
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)
+ INSERT (balance, tid) VALUES (balance + delta, sid)
WHEN MATCHED AND tid < 2 THEN
- DELETE
-RETURNING *;
+ DELETE
+RETURNING (SELECT abbrev FROM merge_actions
+ WHERE action = merge_action()) AS action,
+ t.*,
+ CASE 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_action() outside MERGE
+SELECT merge_action() FROM sq_target;
+UPDATE sq_target SET balance = balance + 1 RETURNING merge_action();
+
+-- RETURNING in CTEs
+CREATE TABLE sq_target_merge_log (tid integer NOT NULL, last_change text);
+INSERT INTO sq_target_merge_log VALUES (1, 'Original value');
+BEGIN;
+WITH m AS (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 merge_action() AS action, t.*,
+ CASE merge_action()
+ WHEN 'INSERT' THEN 'Inserted '||t
+ WHEN 'UPDATE' THEN 'Added '||delta||' to balance'
+ WHEN 'DELETE' THEN 'Removed '||t
+ END AS description
+), m2 AS (
+ MERGE INTO sq_target_merge_log l
+ USING m
+ ON l.tid = m.tid
+ WHEN MATCHED THEN
+ UPDATE SET last_change = description
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (m.tid, description)
+ RETURNING action, merge_action() AS log_action, l.*
+)
+SELECT * FROM m2;
+SELECT * FROM sq_target_merge_log ORDER BY tid;
+ROLLBACK;
+
+-- COPY (MERGE ... RETURNING) TO ...
+BEGIN;
+COPY (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 merge_action(), t.*
+) TO stdout;
+ROLLBACK;
+
+-- SQL function with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT action text, OUT tid int, OUT new_balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING merge_action(), t.*;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT action, tid, new_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ROLLBACK;
+
+-- SQL SRF with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_sq_source_into_sq_target()
+RETURNS TABLE (action text, tid int, balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 merge_action(), t.*;
+$$;
+SELECT * FROM merge_sq_source_into_sq_target();
+ROLLBACK;
+
+-- PL/pgSQL function with MERGE ... RETURNING ... INTO
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT r_action text, OUT r_tid int, OUT r_balance int)
+LANGUAGE plpgsql AS
+$$
+BEGIN
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING merge_action(), t.* INTO r_action, r_tid, r_balance;
+END;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT r_action, r_tid, r_balance FROM merge_into_sq_target(sid, balance, delta)) m;
ROLLBACK;
-- EXPLAIN
@@ -967,7 +1095,7 @@ WHEN MATCHED THEN
SELECT * FROM sq_target WHERE tid = 1;
ROLLBACK;
-DROP TABLE sq_target, sq_source CASCADE;
+DROP TABLE sq_target, sq_target_merge_log, sq_source CASCADE;
CREATE TABLE pa_target (tid integer, balance float, val text)
PARTITION BY LIST (tid);
@@ -1034,6 +1162,17 @@ SELECT merge_func();
SELECT * FROM pa_target ORDER BY tid;
ROLLBACK;
+-- update partition key to partition not initially scanned
+BEGIN;
+MERGE INTO pa_target t
+ USING pa_source s
+ ON t.tid = s.sid AND t.tid = 1
+ WHEN MATCHED THEN
+ UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
+ RETURNING merge_action(), t.*;
+SELECT * FROM pa_target ORDER BY tid;
+ROLLBACK;
+
DROP TABLE pa_target CASCADE;
-- The target table is partitioned in the same way, but this time by attaching
@@ -1210,7 +1349,8 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge');
+ INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge')
+ RETURNING merge_action(), t.*;
SELECT * FROM pa_target ORDER BY tid;
ROLLBACK;
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
new file mode 100644
index dec7340..1d5ed0a
--- a/src/test/regress/sql/rowsecurity.sql
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -992,6 +992,27 @@ WHEN MATCHED THEN
UPDATE SET dnotes = dnotes || ' notes added by merge8 '
WHEN NOT MATCHED THEN
INSERT VALUES (13, 44, 1, 'regress_rls_bob', 'new manga');
+SELECT * FROM document WHERE did = 13;
+
+-- but not OK if RETURNING is used
+MERGE INTO document d
+USING (SELECT 14 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge9 '
+WHEN NOT MATCHED THEN
+ INSERT VALUES (14, 44, 1, 'regress_rls_bob', 'new manga')
+RETURNING *;
+
+-- but OK if new row is visible
+MERGE INTO document d
+USING (SELECT 14 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge10 '
+WHEN NOT MATCHED THEN
+ INSERT VALUES (14, 11, 1, 'regress_rls_bob', 'new novel')
+RETURNING *;
RESET SESSION AUTHORIZATION;
-- drop the restrictive SELECT policy so that we can look at the
diff --git a/src/test/regress/sql/rules.sql b/src/test/regress/sql/rules.sql
new file mode 100644
index 6924012..27340ba
--- a/src/test/regress/sql/rules.sql
+++ b/src/test/regress/sql/rules.sql
@@ -1294,7 +1294,7 @@ MERGE INTO rule_merge1 t USING (SELECT 1
CREATE TABLE sf_target(id int, data text, filling int[]);
CREATE FUNCTION merge_sf_test()
- RETURNS void
+ RETURNS TABLE(action text, a int, b text, id int, data text, filling int[])
LANGUAGE sql
BEGIN ATOMIC
MERGE INTO sf_target t
@@ -1331,7 +1331,9 @@ WHEN NOT MATCHED
VALUES (s.a, s.b, DEFAULT)
WHEN NOT MATCHED
THEN INSERT (filling[1], id)
- VALUES (s.a, s.a);
+ VALUES (s.a, s.a)
+RETURNING
+ merge_action() AS action, *;
END;
\sf merge_sf_test
diff --git a/src/test/regress/sql/updatable_views.sql b/src/test/regress/sql/updatable_views.sql
new file mode 100644
index ae11e46..afdf331
--- a/src/test/regress/sql/updatable_views.sql
+++ b/src/test/regress/sql/updatable_views.sql
@@ -175,7 +175,8 @@ MERGE INTO rw_view1 t
(2, 'ROW 2'), (3, 'ROW 3')) AS v(a,b) ON t.a = v.a
WHEN MATCHED AND t.a <= 1 THEN UPDATE SET b = v.b
WHEN MATCHED THEN DELETE
- WHEN NOT MATCHED AND a > 0 THEN INSERT (a) VALUES (v.a);
+ WHEN NOT MATCHED AND a > 0 THEN INSERT (a) VALUES (v.a)
+ RETURNING merge_action(), v.*, t.*;
SELECT * FROM base_tbl ORDER BY a;
EXPLAIN (costs off) UPDATE rw_view1 SET a=6 WHERE a=5;
@@ -246,7 +247,8 @@ MERGE INTO rw_view2 t
USING (VALUES (3, 'R3'), (4, 'R4'), (5, 'R5')) AS v(a,b) ON aaa = v.a
WHEN MATCHED AND aaa = 3 THEN DELETE
WHEN MATCHED THEN UPDATE SET bbb = v.b
- WHEN NOT MATCHED THEN INSERT (aaa) VALUES (v.a);
+ WHEN NOT MATCHED THEN INSERT (aaa) VALUES (v.a)
+ RETURNING merge_action(), v.*, t.*;
SELECT * FROM rw_view2 ORDER BY aaa;
EXPLAIN (costs off) UPDATE rw_view2 SET aaa=5 WHERE aaa=4;
@@ -458,7 +460,8 @@ MERGE INTO rw_view2 t
USING (SELECT x, 'R'||x FROM generate_series(0,3) x) AS s(a,b) ON t.a = s.a
WHEN MATCHED AND t.a <= 1 THEN DELETE
WHEN MATCHED THEN UPDATE SET b = s.b
- WHEN NOT MATCHED AND s.a > 0 THEN INSERT VALUES (s.a, s.b);
+ WHEN NOT MATCHED AND s.a > 0 THEN INSERT VALUES (s.a, s.b)
+ RETURNING merge_action(), s.*, t.*;
SELECT * FROM base_tbl ORDER BY a;
EXPLAIN (costs off) UPDATE rw_view2 SET a=3 WHERE a=2;
diff --git a/src/test/regress/sql/with.sql b/src/test/regress/sql/with.sql
new file mode 100644
index f8a213e..037bc0a
--- a/src/test/regress/sql/with.sql
+++ b/src/test/regress/sql/with.sql
@@ -1591,6 +1591,14 @@ WITH t AS (
)
SELECT * FROM t;
+-- RETURNING tries to return its own output
+WITH RECURSIVE t(action, a) AS (
+ MERGE INTO y USING (VALUES (11)) v(a) ON y.a = v.a
+ WHEN NOT MATCHED THEN INSERT VALUES (v.a)
+ RETURNING merge_action(), (SELECT a FROM t)
+)
+SELECT * FROM t;
+
-- data-modifying WITH allowed only at the top level
SELECT * FROM (
WITH t AS (UPDATE y SET a=a+1 RETURNING *)
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
new file mode 100644
index d3a7f75..4e3aa38
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1576,6 +1576,7 @@ MergeJoinState
MergePath
MergeScanSelCache
MergeStmt
+MergeSupportFunc
MergeWhenClause
MetaCommand
MinMaxAggInfo
Hi, some minor issues:
<synopsis>
[ WITH <replaceable class="parameter">with_query</replaceable> [, ...] ]
MERGE INTO [ ONLY ] <replaceable
class="parameter">target_table_name</replaceable> [ * ] [ [ AS ]
<replaceable class="parameter">target_alias</replaceable> ]
USING <replaceable class="parameter">data_source</replaceable> ON
<replaceable class="parameter">join_condition</replaceable>
<replaceable class="parameter">when_clause</replaceable> [...]
[ RETURNING * | <replaceable
class="parameter">output_expression</replaceable> [ [ AS ]
<replaceable class="parameter">output_name</replaceable> ] [, ...] ]
here the "WITH" part should have "[ RECURSIVE ]" like:
[ WITH [ RECURSIVE ] <replaceable
class="parameter">with_query</replaceable> [, ...] ]
+ An expression to be computed and returned by the <command>MERGE</command>
+ command after each row is merged. The expression can use any columns of
+ the source or target tables, or the <xref linkend="merge_action"/>
+ function to return additional information about the action executed.
+ </para>
should be:
+ An expression to be computed and returned by the <command>MERGE</command>
+ command after each row is changed.
one minor issue:
add
`
table sq_target;
table sq_source;
`
before `-- RETURNING` in src/test/regress/sql/merge.sql, so we can
easily understand the tests.
On Wed, 13 Mar 2024 at 06:44, jian he <jian.universality@gmail.com> wrote:
<synopsis>
[ WITH <replaceable class="parameter">with_query</replaceable> [, ...] ]
MERGE INTO [ ONLY ] <replaceablehere the "WITH" part should have "[ RECURSIVE ]"
Actually, no. MERGE doesn't support WITH RECURSIVE.
It's not entirely clear to me why though. I did a quick test, removing
that restriction in the parse analysis code, and it seemed to work
fine. Alvaro, do you remember why that restriction is there?
It's probably worth noting it in the docs, since it's different from
INSERT, UPDATE and DELETE. I think this would suffice:
<varlistentry>
<term><replaceable class="parameter">with_query</replaceable></term>
<listitem>
<para>
The <literal>WITH</literal> clause allows you to specify one or more
subqueries that can be referenced by name in the <command>MERGE</command>
query. See <xref linkend="queries-with"/> and <xref linkend="sql-select"/>
for details. Note that <literal>WITH RECURSIVE</literal> is not supported
by <command>MERGE</command>.
</para>
</listitem>
</varlistentry>
And then maybe we can remove that restriction in HEAD, if there really
isn't any need for it anymore.
I also noticed that the "UPDATE SET ..." syntax in the synopsis is
missing a couple of options that are supported -- the optional "ROW"
keyword in the multi-column assignment syntax, and the syntax to
assign from a subquery that returns multiple columns. So this should
be updated to match update.sgml:
UPDATE SET { <replaceable class="parameter">column_name</replaceable>
= { <replaceable class="parameter">expression</replaceable> | DEFAULT
} |
( <replaceable
class="parameter">column_name</replaceable> [, ...] ) = [ ROW ] ( {
<replaceable class="parameter">expression</replaceable> | DEFAULT } [,
...] ) |
( <replaceable
class="parameter">column_name</replaceable> [, ...] ) = ( <replaceable
class="parameter">sub-SELECT</replaceable> )
} [, ...]
and then in the parameter section:
<varlistentry>
<term><replaceable class="parameter">sub-SELECT</replaceable></term>
<listitem>
<para>
A <literal>SELECT</literal> sub-query that produces as many output columns
as are listed in the parenthesized column list preceding it. The
sub-query must yield no more than one row when executed. If it
yields one row, its column values are assigned to the target columns;
if it yields no rows, NULL values are assigned to the target columns.
The sub-query can refer to values from the original row in the
target table,
and values from the <replaceable>data_source</replaceable>.
</para>
</listitem>
</varlistentry>
(basically copied verbatim from update.sgml)
I think I'll go make those doc changes, and back-patch them
separately, since they're not related to this patch.
Regards,
Dean
On Wed, 13 Mar 2024 at 08:58, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
I think I'll go make those doc changes, and back-patch them
separately, since they're not related to this patch.
OK, I've done that. Here is a rebased patch on top of that, with the
other changes you suggested.
Regards,
Dean
Attachments:
support-merge-returning-v18.patchtext/x-patch; charset=US-ASCII; name=support-merge-returning-v18.patchDownload
diff --git a/doc/src/sgml/dml.sgml b/doc/src/sgml/dml.sgml
new file mode 100644
index cbbc5e2..3d95bdb
--- a/doc/src/sgml/dml.sgml
+++ b/doc/src/sgml/dml.sgml
@@ -283,10 +283,15 @@ DELETE FROM products;
<secondary>RETURNING</secondary>
</indexterm>
+ <indexterm zone="dml-returning">
+ <primary>MERGE</primary>
+ <secondary>RETURNING</secondary>
+ </indexterm>
+
<para>
Sometimes it is useful to obtain data from modified rows while they are
being manipulated. The <command>INSERT</command>, <command>UPDATE</command>,
- and <command>DELETE</command> commands all have an
+ <command>DELETE</command>, and <command>MERGE</command> commands all have an
optional <literal>RETURNING</literal> clause that supports this. Use
of <literal>RETURNING</literal> avoids performing an extra database query to
collect the data, and is especially valuable when it would otherwise be
@@ -339,6 +344,21 @@ DELETE FROM products
</programlisting>
</para>
+ <para>
+ In a <command>MERGE</command>, the data available to <literal>RETURNING</literal> is
+ the content of the source row plus the content of the inserted, updated, or
+ deleted target row. Since it is quite common for the source and target to
+ have many of the same columns, specifying <literal>RETURNING *</literal>
+ can lead to a lot of duplicated columns, so it is often more useful to
+ qualify it so as to return just the source or target row. For example:
+<programlisting>
+MERGE INTO products p USING new_products n ON p.product_no = n.product_no
+ WHEN NOT MATCHED THEN INSERT VALUES (n.product_no, n.name, n.price)
+ WHEN MATCHED THEN UPDATE SET name = n.name, price = n.price
+ RETURNING p.*;
+</programlisting>
+ </para>
+
<para>
If there are triggers (<xref linkend="triggers"/>) on the target table,
the data available to <literal>RETURNING</literal> is the row as modified by
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
new file mode 100644
index 0bb7aeb..0e2b888
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -22421,6 +22421,85 @@ SELECT count(*) FROM sometable;
</sect1>
+ <sect1 id="functions-merge-support">
+ <title>Merge Support Functions</title>
+
+ <indexterm>
+ <primary>MERGE</primary>
+ <secondary>RETURNING</secondary>
+ </indexterm>
+
+ <para>
+ <productname>PostgreSQL</productname> includes one merge support function
+ that may be used in the <literal>RETURNING</literal> list of a
+ <xref linkend="sql-merge"/> command to identify the action taken for each
+ row.
+ </para>
+
+ <table id="functions-merge-support-table">
+ <title>Merge Support Functions</title>
+
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ Function
+ </para>
+ <para>
+ Description
+ </para></entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>merge_action</primary>
+ </indexterm>
+ <function id="merge_action">merge_action</function> ( )
+ <returnvalue>text</returnvalue>
+ </para>
+ <para>
+ Returns the merge action command executed for the current row. This
+ will be <literal>'INSERT'</literal>, <literal>'UPDATE'</literal>, or
+ <literal>'DELETE'</literal>.
+ </para></entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+
+ <para>
+ Example:
+<screen><![CDATA[
+MERGE INTO products p
+ USING stock s ON p.product_id = s.product_id
+ WHEN MATCHED AND s.quantity > 0 THEN
+ UPDATE SET in_stock = true, quantity = s.quantity
+ WHEN MATCHED THEN
+ UPDATE SET in_stock = false, quantity = 0
+ WHEN NOT MATCHED THEN
+ INSERT (product_id, in_stock, quantity)
+ VALUES (s.product_id, true, s.quantity)
+ RETURNING merge_action(), p.*;
+
+ merge_action | product_id | in_stock | quantity
+--------------+------------+----------+----------
+ UPDATE | 1001 | t | 50
+ UPDATE | 1002 | f | 0
+ INSERT | 1003 | t | 10
+]]></screen>
+ </para>
+
+ <para>
+ Note that this function can only be used in the <literal>RETURNING</literal>
+ list of a <command>MERGE</command> command. It is an error to use it in any
+ other part of a query.
+ </para>
+
+ </sect1>
+
<sect1 id="functions-subquery">
<title>Subquery Expressions</title>
diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml
new file mode 100644
index 8c2f114..a81c17a
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -1442,9 +1442,9 @@
to a <glossterm linkend="glossary-client">client</glossterm> upon the
completion of an <acronym>SQL</acronym> command, usually a
<command>SELECT</command> but it can be an
- <command>INSERT</command>, <command>UPDATE</command>, or
- <command>DELETE</command> command if the <literal>RETURNING</literal>
- clause is specified.
+ <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command> command if the
+ <literal>RETURNING</literal> clause is specified.
</para>
<para>
The fact that a result set is a relation means that a query can be used
diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml
new file mode 100644
index c2b9c6a..406834e
--- a/doc/src/sgml/plpgsql.sgml
+++ b/doc/src/sgml/plpgsql.sgml
@@ -1043,8 +1043,8 @@ INSERT INTO mytable VALUES (1,'one'), (2
</para>
<para>
- If the command does return rows (for example <command>SELECT</command>,
- or <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>
+ If the command does return rows (for example <command>SELECT</command>, or
+ <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command>
with <literal>RETURNING</literal>), there are two ways to proceed.
When the command will return at most one row, or you only care about
the first row of output, write the command as usual but add
@@ -1172,6 +1172,7 @@ SELECT <replaceable>select_expressions</
INSERT ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
UPDATE ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
DELETE ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
+MERGE ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
</synopsis>
where <replaceable>target</replaceable> can be a record variable, a row
@@ -1182,8 +1183,8 @@ DELETE ... RETURNING <replaceable>expres
<literal>INTO</literal> clause) just as described above,
and the plan is cached in the same way.
This works for <command>SELECT</command>,
- <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command> with
- <literal>RETURNING</literal>, and certain utility commands
+ <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command>
+ with <literal>RETURNING</literal>, and certain utility commands
that return row sets, such as <command>EXPLAIN</command>.
Except for the <literal>INTO</literal> clause, the SQL command is the same
as it would be written outside <application>PL/pgSQL</application>.
@@ -1259,7 +1260,7 @@ END;
</para>
<para>
- For <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command> with
+ For <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command> with
<literal>RETURNING</literal>, <application>PL/pgSQL</application> reports
an error for more than one returned row, even when
<literal>STRICT</literal> is not specified. This is because there
diff --git a/doc/src/sgml/queries.sgml b/doc/src/sgml/queries.sgml
new file mode 100644
index 648b283..372cce1
--- a/doc/src/sgml/queries.sgml
+++ b/doc/src/sgml/queries.sgml
@@ -2063,9 +2063,10 @@ SELECT <replaceable>select_list</replace
Table Expressions or <acronym>CTE</acronym>s, can be thought of as defining
temporary tables that exist just for one query. Each auxiliary statement
in a <literal>WITH</literal> clause can be a <command>SELECT</command>,
- <command>INSERT</command>, <command>UPDATE</command>, or <command>DELETE</command>; and the
+ <command>INSERT</command>, <command>UPDATE</command>, <command>DELETE</command>,
+ or <command>MERGE</command>; and the
<literal>WITH</literal> clause itself is attached to a primary statement that can
- be a <command>SELECT</command>, <command>INSERT</command>, <command>UPDATE</command>,
+ also be a <command>SELECT</command>, <command>INSERT</command>, <command>UPDATE</command>,
<command>DELETE</command>, or <command>MERGE</command>.
</para>
@@ -2599,8 +2600,8 @@ SELECT * FROM w AS w1 JOIN w AS w2 ON w1
<title>Data-Modifying Statements in <literal>WITH</literal></title>
<para>
- You can use most data-modifying statements (<command>INSERT</command>,
- <command>UPDATE</command>, or <command>DELETE</command>, but not
+ You can use data-modifying statements (<command>INSERT</command>,
+ <command>UPDATE</command>, <command>DELETE</command>, or
<command>MERGE</command>) in <literal>WITH</literal>. This
allows you to perform several different operations in the same query.
An example is:
diff --git a/doc/src/sgml/ref/copy.sgml b/doc/src/sgml/ref/copy.sgml
new file mode 100644
index 55764fc..6c83e30
--- a/doc/src/sgml/ref/copy.sgml
+++ b/doc/src/sgml/ref/copy.sgml
@@ -130,16 +130,19 @@ COPY { <replaceable class="parameter">ta
A <link linkend="sql-select"><command>SELECT</command></link>,
<link linkend="sql-values"><command>VALUES</command></link>,
<link linkend="sql-insert"><command>INSERT</command></link>,
- <link linkend="sql-update"><command>UPDATE</command></link>, or
- <link linkend="sql-delete"><command>DELETE</command></link> command whose results are to be
- copied. Note that parentheses are required around the query.
+ <link linkend="sql-update"><command>UPDATE</command></link>,
+ <link linkend="sql-delete"><command>DELETE</command></link>, or
+ <link linkend="sql-merge"><command>MERGE</command></link> command
+ whose results are to be copied. Note that parentheses are required
+ around the query.
</para>
<para>
- For <command>INSERT</command>, <command>UPDATE</command> and
- <command>DELETE</command> queries a <literal>RETURNING</literal> clause
- must be provided, and the target relation must not have a conditional
- rule, nor an <literal>ALSO</literal> rule, nor an
- <literal>INSTEAD</literal> rule that expands to multiple statements.
+ For <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, and <command>MERGE</command> queries a
+ <literal>RETURNING</literal> clause must be provided, and the target
+ relation must not have a conditional rule, nor an
+ <literal>ALSO</literal> rule, nor an <literal>INSTEAD</literal> rule
+ that expands to multiple statements.
</para>
</listitem>
</varlistentry>
diff --git a/doc/src/sgml/ref/merge.sgml b/doc/src/sgml/ref/merge.sgml
new file mode 100644
index e745fbd..71feb66
--- a/doc/src/sgml/ref/merge.sgml
+++ b/doc/src/sgml/ref/merge.sgml
@@ -25,6 +25,7 @@ PostgreSQL documentation
MERGE INTO [ ONLY ] <replaceable class="parameter">target_table_name</replaceable> [ * ] [ [ AS ] <replaceable class="parameter">target_alias</replaceable> ]
USING <replaceable class="parameter">data_source</replaceable> ON <replaceable class="parameter">join_condition</replaceable>
<replaceable class="parameter">when_clause</replaceable> [...]
+[ RETURNING * | <replaceable class="parameter">output_expression</replaceable> [ [ AS ] <replaceable class="parameter">output_name</replaceable> ] [, ...] ]
<phrase>where <replaceable class="parameter">data_source</replaceable> is:</phrase>
@@ -97,6 +98,18 @@ DELETE
</para>
<para>
+ The optional <literal>RETURNING</literal> clause causes <command>MERGE</command>
+ to compute and return value(s) based on each row inserted, updated, or
+ deleted. Any expression using the source or target table's columns, or
+ the <xref linkend="merge_action"/> function can be computed. When an
+ <command>INSERT</command> or <command>UPDATE</command> action is performed,
+ the new values of the target table's columns are used. When a
+ <command>DELETE</command> is performed, the old values of the target table's
+ columns are used. The syntax of the <literal>RETURNING</literal> list is
+ identical to that of the output list of <command>SELECT</command>.
+ </para>
+
+ <para>
There is no separate <literal>MERGE</literal> privilege.
If you specify an update action, you must have the
<literal>UPDATE</literal> privilege on the column(s)
@@ -442,6 +455,36 @@ DELETE
</listitem>
</varlistentry>
+ <varlistentry>
+ <term><replaceable class="parameter">output_expression</replaceable></term>
+ <listitem>
+ <para>
+ An expression to be computed and returned by the <command>MERGE</command>
+ command after each row is changed (whether inserted, updated, or deleted).
+ The expression can use any columns of the source or target tables, or the
+ <xref linkend="merge_action"/> function to return additional information
+ about the action executed.
+ </para>
+ <para>
+ Writing <literal>*</literal> will return all columns from the source
+ table, followed by all columns from the target table. Often this will
+ lead to a lot of duplication, since it is common for the source and
+ target tables to have a lot of the same columns. This can be avoided by
+ qualifying the <literal>*</literal> with the name or alias of the source
+ or target table.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><replaceable class="parameter">output_name</replaceable></term>
+ <listitem>
+ <para>
+ A name to use for a returned column.
+ </para>
+ </listitem>
+ </varlistentry>
+
</variablelist>
</refsect1>
@@ -460,6 +503,13 @@ MERGE <replaceable class="parameter">tot
were changed in any way.
</para>
+ <para>
+ If the <command>MERGE</command> command contains a <literal>RETURNING</literal>
+ clause, the result will be similar to that of a <command>SELECT</command>
+ statement containing the columns and values defined in the
+ <literal>RETURNING</literal> list, computed over the row(s) inserted, updated,
+ or deleted by the command.
+ </para>
</refsect1>
<refsect1>
@@ -580,13 +630,6 @@ MERGE <replaceable class="parameter">tot
</para>
<para>
- There is no <literal>RETURNING</literal> clause with
- <command>MERGE</command>. Actions of <command>INSERT</command>,
- <command>UPDATE</command> and <command>DELETE</command> cannot contain
- <literal>RETURNING</literal> or <literal>WITH</literal> clauses.
- </para>
-
- <para>
When <command>MERGE</command> is run concurrently with other commands
that modify the target table, the usual transaction isolation rules
apply; see <xref linkend="transaction-iso"/> for an explanation
@@ -638,7 +681,8 @@ WHEN NOT MATCHED THEN
<para>
Attempt to insert a new stock item along with the quantity of stock. If
the item already exists, instead update the stock count of the existing
- item. Don't allow entries that have zero stock.
+ item. Don't allow entries that have zero stock. Return details of all
+ changes made.
<programlisting>
MERGE INTO wines w
USING wine_stock_changes s
@@ -648,7 +692,8 @@ WHEN NOT MATCHED AND s.stock_delta > 0 T
WHEN MATCHED AND w.stock + s.stock_delta > 0 THEN
UPDATE SET stock = w.stock + s.stock_delta
WHEN MATCHED THEN
- DELETE;
+ DELETE
+RETURNING merge_action(), w.*;
</programlisting>
The <literal>wine_stock_changes</literal> table might be, for example, a
@@ -663,8 +708,9 @@ WHEN MATCHED THEN
This command conforms to the <acronym>SQL</acronym> standard.
</para>
<para>
- The <literal>WITH</literal> clause and <literal>DO NOTHING</literal>
- action are extensions to the <acronym>SQL</acronym> standard.
+ The <literal>WITH</literal> clause, <literal>DO NOTHING</literal> action,
+ and <literal>RETURNING</literal> clause are extensions to the
+ <acronym>SQL</acronym> standard.
</para>
</refsect1>
</refentry>
diff --git a/doc/src/sgml/ref/select.sgml b/doc/src/sgml/ref/select.sgml
new file mode 100644
index 9917df7..066aed4
--- a/doc/src/sgml/ref/select.sgml
+++ b/doc/src/sgml/ref/select.sgml
@@ -74,7 +74,7 @@ SELECT [ ALL | DISTINCT [ ON ( <replacea
<phrase>and <replaceable class="parameter">with_query</replaceable> is:</phrase>
- <replaceable class="parameter">with_query_name</replaceable> [ ( <replaceable class="parameter">column_name</replaceable> [, ...] ) ] AS [ [ NOT ] MATERIALIZED ] ( <replaceable class="parameter">select</replaceable> | <replaceable class="parameter">values</replaceable> | <replaceable class="parameter">insert</replaceable> | <replaceable class="parameter">update</replaceable> | <replaceable class="parameter">delete</replaceable> )
+ <replaceable class="parameter">with_query_name</replaceable> [ ( <replaceable class="parameter">column_name</replaceable> [, ...] ) ] AS [ [ NOT ] MATERIALIZED ] ( <replaceable class="parameter">select</replaceable> | <replaceable class="parameter">values</replaceable> | <replaceable class="parameter">insert</replaceable> | <replaceable class="parameter">update</replaceable> | <replaceable class="parameter">delete</replaceable> | <replaceable class="parameter">merge</replaceable> )
[ SEARCH { BREADTH | DEPTH } FIRST BY <replaceable>column_name</replaceable> [, ...] SET <replaceable>search_seq_col_name</replaceable> ]
[ CYCLE <replaceable>column_name</replaceable> [, ...] SET <replaceable>cycle_mark_col_name</replaceable> [ TO <replaceable>cycle_mark_value</replaceable> DEFAULT <replaceable>cycle_mark_default</replaceable> ] USING <replaceable>cycle_path_col_name</replaceable> ]
@@ -230,10 +230,10 @@ TABLE [ ONLY ] <replaceable class="param
The subqueries effectively act as temporary tables or views
for the duration of the primary query.
Each subquery can be a <command>SELECT</command>, <command>TABLE</command>, <command>VALUES</command>,
- <command>INSERT</command>, <command>UPDATE</command> or
- <command>DELETE</command> statement.
+ <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command> statement.
When writing a data-modifying statement (<command>INSERT</command>,
- <command>UPDATE</command> or <command>DELETE</command>) in
+ <command>UPDATE</command>, <command>DELETE</command>, or <command>MERGE</command>) in
<literal>WITH</literal>, it is usual to include a <literal>RETURNING</literal> clause.
It is the output of <literal>RETURNING</literal>, <emphasis>not</emphasis> the underlying
table that the statement modifies, that forms the temporary table that is
@@ -2184,7 +2184,8 @@ SELECT 2+2;
<para>
<productname>PostgreSQL</productname> allows <command>INSERT</command>,
- <command>UPDATE</command>, and <command>DELETE</command> to be used as <literal>WITH</literal>
+ <command>UPDATE</command>, <command>DELETE</command>, and
+ <command>MERGE</command> to be used as <literal>WITH</literal>
queries. This is not found in the SQL standard.
</para>
</refsect2>
diff --git a/doc/src/sgml/rowtypes.sgml b/doc/src/sgml/rowtypes.sgml
new file mode 100644
index 4d86f97..bbeac84
--- a/doc/src/sgml/rowtypes.sgml
+++ b/doc/src/sgml/rowtypes.sgml
@@ -348,7 +348,7 @@ SELECT m.* FROM some_table, LATERAL myfu
column expansion of this kind when it appears at the top level of
a <link linkend="queries-select-lists"><command>SELECT</command> output
list</link>, a <link linkend="dml-returning"><literal>RETURNING</literal>
- list</link> in <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>,
+ list</link> in <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command>,
a <link linkend="queries-values"><literal>VALUES</literal> clause</link>, or
a <link linkend="sql-syntax-row-constructors">row constructor</link>.
In all other contexts (including when nested inside one of those
diff --git a/doc/src/sgml/spi.sgml b/doc/src/sgml/spi.sgml
new file mode 100644
index 47f4b5b..bb37786
--- a/doc/src/sgml/spi.sgml
+++ b/doc/src/sgml/spi.sgml
@@ -301,8 +301,9 @@ SPI_execute("INSERT INTO foo SELECT * FR
is returned in the global variable <varname>SPI_processed</varname>.
If the return value of the function is <symbol>SPI_OK_SELECT</symbol>,
<symbol>SPI_OK_INSERT_RETURNING</symbol>,
- <symbol>SPI_OK_DELETE_RETURNING</symbol>, or
- <symbol>SPI_OK_UPDATE_RETURNING</symbol>,
+ <symbol>SPI_OK_DELETE_RETURNING</symbol>,
+ <symbol>SPI_OK_UPDATE_RETURNING</symbol>, or
+ <symbol>SPI_OK_MERGE_RETURNING</symbol>,
then you can use the
global pointer <literal>SPITupleTable *SPI_tuptable</literal> to
access the result rows. Some utility commands (such as
@@ -472,6 +473,15 @@ typedef struct SPITupleTable
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><symbol>SPI_OK_MERGE_RETURNING</symbol></term>
+ <listitem>
+ <para>
+ if a <command>MERGE RETURNING</command> was executed
+ </para>
+ </listitem>
+ </varlistentry>
<varlistentry>
<term><symbol>SPI_OK_UTILITY</symbol></term>
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
new file mode 100644
index 8a79ad0..7d05369
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -177,7 +177,8 @@
statements separated by semicolons. A semicolon after the last
statement is optional. Unless the function is declared to return
<type>void</type>, the last statement must be a <command>SELECT</command>,
- or an <command>INSERT</command>, <command>UPDATE</command>, or <command>DELETE</command>
+ or an <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command>
that has a <literal>RETURNING</literal> clause.
</para>
@@ -1236,8 +1237,9 @@ SELECT x, CASE WHEN x > 0 THEN genera
<note>
<para>
- If a function's last command is <command>INSERT</command>, <command>UPDATE</command>,
- or <command>DELETE</command> with <literal>RETURNING</literal>, that command will
+ If a function's last command is <command>INSERT</command>,
+ <command>UPDATE</command>, <command>DELETE</command>, or
+ <command>MERGE</command> with <literal>RETURNING</literal>, that command will
always be executed to completion, even if the function is not declared
with <literal>SETOF</literal> or the calling query does not fetch all the
result rows. Any extra rows produced by the <literal>RETURNING</literal>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
new file mode 100644
index 056b673..28cf8b0
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -281,12 +281,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 a6962e0..ae8b2e3
--- a/src/backend/commands/copyto.c
+++ b/src/backend/commands/copyto.c
@@ -503,7 +503,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 ffd3ca4..728c8d5
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -1107,6 +1107,19 @@ ExecInitExprRec(Expr *node, ExprState *s
break;
}
+ case T_MergeSupportFunc:
+ {
+ /* must be in a MERGE, else something messed up */
+ if (!state->parent ||
+ !IsA(state->parent, ModifyTableState) ||
+ ((ModifyTableState *) state->parent)->operation != CMD_MERGE)
+ elog(ERROR, "MergeSupportFunc found in non-merge plan node");
+
+ scratch.opcode = EEOP_MERGE_SUPPORT_FUNC;
+ ExprEvalPushStep(state, &scratch);
+ break;
+ }
+
case T_SubscriptingRef:
{
SubscriptingRef *sbsref = (SubscriptingRef *) node;
diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
new file mode 100644
index 7c1f51e..a25ab75
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -484,6 +484,7 @@ ExecInterpExpr(ExprState *state, ExprCon
&&CASE_EEOP_AGGREF,
&&CASE_EEOP_GROUPING_FUNC,
&&CASE_EEOP_WINDOW_FUNC,
+ &&CASE_EEOP_MERGE_SUPPORT_FUNC,
&&CASE_EEOP_SUBPLAN,
&&CASE_EEOP_AGG_STRICT_DESERIALIZE,
&&CASE_EEOP_AGG_DESERIALIZE,
@@ -1592,6 +1593,14 @@ ExecInterpExpr(ExprState *state, ExprCon
EEO_NEXT();
}
+ EEO_CASE(EEOP_MERGE_SUPPORT_FUNC)
+ {
+ /* too complex/uncommon for an inline implementation */
+ ExecEvalMergeSupportFunc(state, op, econtext);
+
+ EEO_NEXT();
+ }
+
EEO_CASE(EEOP_SUBPLAN)
{
/* too complex for an inline implementation */
@@ -4246,6 +4255,45 @@ ExecEvalGroupingFunc(ExprState *state, E
}
/*
+ * ExecEvalMergeSupportFunc
+ *
+ * Returns information about the current MERGE action for its RETURNING list.
+ */
+void
+ExecEvalMergeSupportFunc(ExprState *state, ExprEvalStep *op,
+ ExprContext *econtext)
+{
+ ModifyTableState *mtstate = castNode(ModifyTableState, state->parent);
+ MergeActionState *relaction = mtstate->mt_merge_action;
+
+ if (!relaction)
+ elog(ERROR, "no merge action in progress");
+
+ /* Return the MERGE action ("INSERT", "UPDATE", or "DELETE") */
+ switch (relaction->mas_action->commandType)
+ {
+ case CMD_INSERT:
+ *op->resvalue = PointerGetDatum(cstring_to_text_with_len("INSERT", 6));
+ *op->resnull = false;
+ break;
+ case CMD_UPDATE:
+ *op->resvalue = PointerGetDatum(cstring_to_text_with_len("UPDATE", 6));
+ *op->resnull = false;
+ break;
+ case CMD_DELETE:
+ *op->resvalue = PointerGetDatum(cstring_to_text_with_len("DELETE", 6));
+ *op->resnull = false;
+ break;
+ case CMD_NOTHING:
+ elog(ERROR, "unexpected merge action: DO NOTHING");
+ break;
+ default:
+ elog(ERROR, "unrecognized commandType: %d",
+ (int) relaction->mas_action->commandType);
+ }
+}
+
+/*
* Hand off evaluation of a subplan to nodeSubplan.c
*/
void
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
new file mode 100644
index 8ca512d..64fcb01
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -609,8 +609,8 @@ ExecInitPartitionInfo(ModifyTableState *
* Build the RETURNING projection for the partition. Note that we didn't
* build the returningList for partitions within the planner, but simple
* translation of varattnos will suffice. This only occurs for the INSERT
- * case or in the case of UPDATE tuple routing where we didn't find a
- * result rel to reuse.
+ * case or in the case of UPDATE/MERGE tuple routing where we didn't find
+ * a result rel to reuse.
*/
if (node && node->returningLists != NIL)
{
@@ -619,12 +619,14 @@ ExecInitPartitionInfo(ModifyTableState *
List *returningList;
/* See the comment above for WCO lists. */
- /* (except no RETURNING support for MERGE yet) */
Assert((node->operation == CMD_INSERT &&
list_length(node->returningLists) == 1 &&
list_length(node->resultRelations) == 1) ||
(node->operation == CMD_UPDATE &&
list_length(node->returningLists) ==
+ list_length(node->resultRelations)) ||
+ (node->operation == CMD_MERGE &&
+ list_length(node->returningLists) ==
list_length(node->resultRelations)));
/*
diff --git a/src/backend/executor/functions.c b/src/backend/executor/functions.c
new file mode 100644
index 6e926ef..539cd0a
--- a/src/backend/executor/functions.c
+++ b/src/backend/executor/functions.c
@@ -1662,8 +1662,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
@@ -1681,7 +1681,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;
@@ -1695,7 +1696,7 @@ check_sql_fn_retval(List *queryTreeLists
(errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
errmsg("return type mismatch in function declared to return %s",
format_type_be(rettype)),
- errdetail("Function's final statement must be SELECT or INSERT/UPDATE/DELETE RETURNING.")));
+ errdetail("Function's final statement must be SELECT or INSERT/UPDATE/DELETE/MERGE RETURNING.")));
return false; /* keep compiler quiet */
}
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
new file mode 100644
index 9351fbc..b8a75c5
--- 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"
@@ -85,9 +84,6 @@ typedef struct ModifyTableContext
*/
TupleTableSlot *planSlot;
- /* MERGE specific */
- MergeActionState *relaction; /* MERGE action in progress */
-
/*
* Information about the changes that were made concurrently to a tuple
* being updated or deleted
@@ -150,14 +146,15 @@ static TupleTableSlot *ExecMerge(ModifyT
HeapTuple oldtuple,
bool canSetTag);
static void ExecInitMerge(ModifyTableState *mtstate, EState *estate);
-static bool ExecMergeMatched(ModifyTableContext *context,
- ResultRelInfo *resultRelInfo,
- ItemPointer tupleid,
- HeapTuple oldtuple,
- bool canSetTag);
-static void ExecMergeNotMatched(ModifyTableContext *context,
- ResultRelInfo *resultRelInfo,
- bool canSetTag);
+static TupleTableSlot *ExecMergeMatched(ModifyTableContext *context,
+ ResultRelInfo *resultRelInfo,
+ ItemPointer tupleid,
+ HeapTuple oldtuple,
+ bool canSetTag,
+ bool *matched);
+static TupleTableSlot *ExecMergeNotMatched(ModifyTableContext *context,
+ ResultRelInfo *resultRelInfo,
+ bool canSetTag);
/*
@@ -977,7 +974,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;
@@ -1831,7 +1828,7 @@ ExecCrossPartitionUpdate(ModifyTableCont
* additional rechecking, and might end up executing a different
* action entirely).
*/
- if (context->relaction != NULL)
+ if (mtstate->operation == CMD_MERGE)
return *tmresult == TM_Ok;
else if (TupIsNull(epqslot))
return true;
@@ -2072,7 +2069,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->operation == CMD_MERGE)
return result;
/*
@@ -2713,6 +2710,7 @@ static TupleTableSlot *
ExecMerge(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
ItemPointer tupleid, HeapTuple oldtuple, bool canSetTag)
{
+ TupleTableSlot *rslot = NULL;
bool matched;
/*-----
@@ -2761,19 +2759,18 @@ ExecMerge(ModifyTableContext *context, R
*/
matched = tupleid != NULL || oldtuple != NULL;
if (matched)
- matched = ExecMergeMatched(context, resultRelInfo, tupleid, oldtuple,
- canSetTag);
+ rslot = ExecMergeMatched(context, resultRelInfo, tupleid, oldtuple,
+ canSetTag, &matched);
/*
- * Either we were dealing with a NOT MATCHED tuple or ExecMergeMatched()
- * returned "false", indicating the previously MATCHED tuple no longer
- * matches.
+ * Deal with the NOT MATCHED case (either a NOT MATCHED tuple from the
+ * join, or a previously MATCHED tuple for which ExecMergeMatched() set
+ * "matched" to false, indicating that it no longer matches).
*/
if (!matched)
- ExecMergeNotMatched(context, resultRelInfo, canSetTag);
+ rslot = ExecMergeNotMatched(context, resultRelInfo, canSetTag);
- /* No RETURNING support yet */
- return NULL;
+ return rslot;
}
/*
@@ -2785,8 +2782,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.
*
@@ -2795,16 +2792,18 @@ 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, HeapTuple oldtuple, bool canSetTag)
+ ItemPointer tupleid, HeapTuple oldtuple, bool canSetTag,
+ bool *matched)
{
ModifyTableState *mtstate = context->mtstate;
- TupleTableSlot *newslot;
+ TupleTableSlot *rslot = NULL;
+ TupleTableSlot *newslot = NULL;
EState *estate = context->estate;
ExprContext *econtext = mtstate->ps.ps_ExprContext;
bool isNull;
@@ -2815,7 +2814,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
@@ -2905,12 +2907,15 @@ lmerge_matched:
*/
newslot = ExecProject(relaction->mas_proj);
- context->relaction = relaction;
+ mtstate->mt_merge_action = relaction;
if (!ExecUpdatePrologue(context, resultRelInfo,
tupleid, NULL, newslot, &result))
{
if (result == TM_Ok)
- return true; /* "do nothing" */
+ {
+ *matched = true;
+ return NULL; /* "do nothing" */
+ }
break; /* concurrent update/delete */
}
@@ -2920,7 +2925,10 @@ lmerge_matched:
{
if (!ExecIRUpdateTriggers(estate, resultRelInfo,
oldtuple, newslot))
- return true; /* "do nothing" */
+ {
+ *matched = true;
+ return NULL; /* "do nothing" */
+ }
}
else
{
@@ -2933,12 +2941,15 @@ lmerge_matched:
* cross-partition update was done, then there's nothing
* else for us to do --- the UPDATE has been turned into a
* DELETE and an INSERT, and we must not perform any of
- * the usual post-update tasks.
+ * the usual post-update tasks. Also, the RETURNING tuple
+ * (if any) has been projected, so we can just return
+ * that.
*/
if (updateCxt.crossPartUpdate)
{
mtstate->mt_merge_updated += 1;
- return true;
+ *matched = true;
+ return context->cpUpdateReturningSlot;
}
}
@@ -2951,12 +2962,15 @@ lmerge_matched:
break;
case CMD_DELETE:
- context->relaction = relaction;
+ mtstate->mt_merge_action = relaction;
if (!ExecDeletePrologue(context, resultRelInfo, tupleid,
NULL, NULL, &result))
{
if (result == TM_Ok)
- return true; /* "do nothing" */
+ {
+ *matched = true;
+ return NULL; /* "do nothing" */
+ }
break; /* concurrent update/delete */
}
@@ -2966,7 +2980,10 @@ lmerge_matched:
{
if (!ExecIRDeleteTriggers(estate, resultRelInfo,
oldtuple))
- return true; /* "do nothing" */
+ {
+ *matched = true;
+ return NULL; /* "do nothing" */
+ }
}
else
result = ExecDeleteAct(context, resultRelInfo, tupleid,
@@ -3046,7 +3063,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:
{
@@ -3092,13 +3110,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
@@ -3133,7 +3157,8 @@ lmerge_matched:
* tuple already deleted; tell caller to run NOT
* MATCHED actions
*/
- return false;
+ *matched = false;
+ return NULL;
case TM_SelfModified:
@@ -3161,13 +3186,13 @@ lmerge_matched:
/* This shouldn't happen */
elog(ERROR, "attempted to update or delete invisible tuple");
- return false;
+ return NULL;
default:
/* see table_tuple_lock call in ExecDelete() */
elog(ERROR, "unexpected table_tuple_lock status: %u",
result);
- return false;
+ return NULL;
}
}
@@ -3179,6 +3204,31 @@ 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, "unrecognized commandType: %d",
+ (int) commandType);
+ }
+ }
+
/*
* We've activated one of the WHEN clauses, so we don't search
* further. This is required behaviour, not an optimization.
@@ -3189,19 +3239,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;
/*
@@ -3251,10 +3304,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:
@@ -3270,6 +3323,8 @@ ExecMergeNotMatched(ModifyTableContext *
*/
break;
}
+
+ return rslot;
}
/*
@@ -3732,9 +3787,17 @@ ExecModifyTable(PlanState *pstate)
{
EvalPlanQualSetSlot(&node->mt_epqstate, context.planSlot);
- ExecMerge(&context, node->resultRelInfo, NULL, NULL,
- node->canSetTag);
- continue; /* no RETURNING support yet */
+ slot = ExecMerge(&context, node->resultRelInfo,
+ NULL, 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");
@@ -3811,9 +3874,17 @@ ExecModifyTable(PlanState *pstate)
{
EvalPlanQualSetSlot(&node->mt_epqstate, context.planSlot);
- ExecMerge(&context, node->resultRelInfo, NULL, NULL,
- node->canSetTag);
- continue; /* no RETURNING support yet */
+ slot = ExecMerge(&context, node->resultRelInfo,
+ NULL, 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");
@@ -3860,9 +3931,17 @@ ExecModifyTable(PlanState *pstate)
{
EvalPlanQualSetSlot(&node->mt_epqstate, context.planSlot);
- ExecMerge(&context, node->resultRelInfo, NULL, NULL,
- node->canSetTag);
- continue; /* no RETURNING support yet */
+ slot = ExecMerge(&context, node->resultRelInfo,
+ NULL, 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, "wholerow is NULL");
@@ -3924,7 +4003,6 @@ ExecModifyTable(PlanState *pstate)
}
slot = ExecGetUpdateNewTuple(resultRelInfo, context.planSlot,
oldSlot);
- context.relaction = NULL;
/* Now apply the update. */
slot = ExecUpdate(&context, resultRelInfo, tupleid, oldtuple,
diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c
new file mode 100644
index 8585771..a97a7e3
--- a/src/backend/executor/spi.c
+++ b/src/backend/executor/spi.c
@@ -2032,6 +2032,8 @@ SPI_result_code_string(int code)
return "SPI_OK_TD_REGISTER";
case SPI_OK_MERGE:
return "SPI_OK_MERGE";
+ case SPI_OK_MERGE_RETURNING:
+ return "SPI_OK_MERGE_RETURNING";
}
/* Unrecognized code ... return something useful ... */
sprintf(buf, "Unrecognized SPI code %d", code);
@@ -2885,7 +2887,10 @@ _SPI_pquery(QueryDesc *queryDesc, bool f
res = SPI_OK_UPDATE;
break;
case CMD_MERGE:
- res = SPI_OK_MERGE;
+ if (queryDesc->plannedstmt->hasReturning)
+ res = SPI_OK_MERGE_RETURNING;
+ else
+ res = SPI_OK_MERGE;
break;
default:
return SPI_ERROR_OPUNKNOWN;
diff --git a/src/backend/jit/llvm/llvmjit_expr.c b/src/backend/jit/llvm/llvmjit_expr.c
new file mode 100644
index 0c44842..2a7d84f
--- a/src/backend/jit/llvm/llvmjit_expr.c
+++ b/src/backend/jit/llvm/llvmjit_expr.c
@@ -1986,6 +1986,12 @@ llvm_compile_expr(ExprState *state)
break;
}
+ case EEOP_MERGE_SUPPORT_FUNC:
+ build_EvalXFunc(b, mod, "ExecEvalMergeSupportFunc",
+ v_state, op, v_econtext);
+ LLVMBuildBr(b, opblocks[opno + 1]);
+ break;
+
case EEOP_SUBPLAN:
build_EvalXFunc(b, mod, "ExecEvalSubPlan",
v_state, op, v_econtext);
diff --git a/src/backend/jit/llvm/llvmjit_types.c b/src/backend/jit/llvm/llvmjit_types.c
new file mode 100644
index 47c9daf..7d7aeee
--- a/src/backend/jit/llvm/llvmjit_types.c
+++ b/src/backend/jit/llvm/llvmjit_types.c
@@ -155,6 +155,7 @@ void *referenced_functions[] =
ExecEvalFuncExprFusage,
ExecEvalFuncExprStrictFusage,
ExecEvalGroupingFunc,
+ ExecEvalMergeSupportFunc,
ExecEvalMinMax,
ExecEvalNextValueExpr,
ExecEvalParamExec,
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
new file mode 100644
index 6ba8e73..5b70280
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -66,6 +66,9 @@ exprType(const Node *expr)
case T_WindowFunc:
type = ((const WindowFunc *) expr)->wintype;
break;
+ case T_MergeSupportFunc:
+ type = ((const MergeSupportFunc *) expr)->msftype;
+ break;
case T_SubscriptingRef:
type = ((const SubscriptingRef *) expr)->refrestype;
break;
@@ -809,6 +812,9 @@ exprCollation(const Node *expr)
case T_WindowFunc:
coll = ((const WindowFunc *) expr)->wincollid;
break;
+ case T_MergeSupportFunc:
+ coll = ((const MergeSupportFunc *) expr)->msfcollid;
+ break;
case T_SubscriptingRef:
coll = ((const SubscriptingRef *) expr)->refcollid;
break;
@@ -1084,6 +1090,9 @@ exprSetCollation(Node *expr, Oid collati
case T_WindowFunc:
((WindowFunc *) expr)->wincollid = collation;
break;
+ case T_MergeSupportFunc:
+ ((MergeSupportFunc *) expr)->msfcollid = collation;
+ break;
case T_SubscriptingRef:
((SubscriptingRef *) expr)->refcollid = collation;
break;
@@ -1342,6 +1351,9 @@ exprLocation(const Node *expr)
/* function name should always be the first thing */
loc = ((const WindowFunc *) expr)->location;
break;
+ case T_MergeSupportFunc:
+ loc = ((const MergeSupportFunc *) expr)->location;
+ break;
case T_SubscriptingRef:
/* just use container argument's location */
loc = exprLocation((Node *) ((const SubscriptingRef *) expr)->refexpr);
@@ -2034,6 +2046,7 @@ expression_tree_walker_impl(Node *node,
case T_RangeTblRef:
case T_SortGroupClause:
case T_CTESearchClause:
+ case T_MergeSupportFunc:
/* primitive node types with no expression subnodes */
break;
case T_WithCheckOption:
@@ -2868,6 +2881,7 @@ expression_tree_mutator_impl(Node *node,
case T_RangeTblRef:
case T_SortGroupClause:
case T_CTESearchClause:
+ case T_MergeSupportFunc:
return (Node *) copyObject(node);
case T_WithCheckOption:
{
@@ -3832,6 +3846,7 @@ raw_expression_tree_walker_impl(Node *no
case T_ParamRef:
case T_A_Const:
case T_A_Star:
+ case T_MergeSupportFunc:
/* primitive node types with no subnodes */
break;
case T_Alias:
@@ -4052,6 +4067,8 @@ raw_expression_tree_walker_impl(Node *no
return true;
if (WALK(stmt->mergeWhenClauses))
return true;
+ if (WALK(stmt->returningList))
+ return true;
if (WALK(stmt->withClause))
return true;
}
diff --git a/src/backend/optimizer/plan/subselect.c b/src/backend/optimizer/plan/subselect.c
new file mode 100644
index 47e1472..b9e0c96
--- a/src/backend/optimizer/plan/subselect.c
+++ b/src/backend/optimizer/plan/subselect.c
@@ -1855,7 +1855,8 @@ convert_EXISTS_to_ANY(PlannerInfo *root,
/*
* Replace correlation vars (uplevel vars) with Params.
*
- * Uplevel PlaceHolderVars and aggregates are replaced, too.
+ * Uplevel PlaceHolderVars, aggregates, GROUPING() expressions, and
+ * MergeSupportFuncs are replaced, too.
*
* Note: it is critical that this runs immediately after SS_process_sublinks.
* Since we do not recurse into the arguments of uplevel PHVs and aggregates,
@@ -1909,6 +1910,12 @@ replace_correlation_vars_mutator(Node *n
if (((GroupingFunc *) node)->agglevelsup > 0)
return (Node *) replace_outer_grouping(root, (GroupingFunc *) node);
}
+ if (IsA(node, MergeSupportFunc))
+ {
+ if (root->parse->commandType != CMD_MERGE)
+ return (Node *) replace_outer_merge_support(root,
+ (MergeSupportFunc *) node);
+ }
return expression_tree_mutator(node,
replace_correlation_vars_mutator,
(void *) root);
diff --git a/src/backend/optimizer/util/paramassign.c b/src/backend/optimizer/util/paramassign.c
new file mode 100644
index a58da7c..f461fed
--- a/src/backend/optimizer/util/paramassign.c
+++ b/src/backend/optimizer/util/paramassign.c
@@ -308,6 +308,57 @@ replace_outer_grouping(PlannerInfo *root
}
/*
+ * Generate a Param node to replace the given MergeSupportFunc expression
+ * which is expected to be in the RETURNING list of an upper-level MERGE
+ * query. Record the need for the MergeSupportFunc in the proper upper-level
+ * root->plan_params.
+ */
+Param *
+replace_outer_merge_support(PlannerInfo *root, MergeSupportFunc *msf)
+{
+ Param *retval;
+ PlannerParamItem *pitem;
+ Oid ptype = exprType((Node *) msf);
+
+ Assert(root->parse->commandType != CMD_MERGE);
+
+ /*
+ * The parser should have ensured that the MergeSupportFunc is in the
+ * RETURNING list of an upper-level MERGE query, so find that query.
+ */
+ do
+ {
+ root = root->parent_root;
+ if (root == NULL)
+ elog(ERROR, "MergeSupportFunc found outside MERGE");
+ } while (root->parse->commandType != CMD_MERGE);
+
+ /*
+ * It does not seem worthwhile to try to de-duplicate references to outer
+ * MergeSupportFunc expressions. Just make a new slot every time.
+ */
+ msf = copyObject(msf);
+
+ pitem = makeNode(PlannerParamItem);
+ pitem->item = (Node *) msf;
+ pitem->paramId = list_length(root->glob->paramExecTypes);
+ root->glob->paramExecTypes = lappend_oid(root->glob->paramExecTypes,
+ ptype);
+
+ root->plan_params = lappend(root->plan_params, pitem);
+
+ retval = makeNode(Param);
+ retval->paramkind = PARAM_EXEC;
+ retval->paramid = pitem->paramId;
+ retval->paramtype = ptype;
+ retval->paramtypmod = -1;
+ retval->paramcollid = InvalidOid;
+ retval->location = msf->location;
+
+ return retval;
+}
+
+/*
* Generate a Param node to replace the given Var,
* which is expected to come from some upper NestLoop plan node.
* Record the need for the Var in root->curOuterParams.
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
new file mode 100644
index 2255314..7bd947b
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -72,7 +72,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,
@@ -551,7 +550,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;
@@ -978,7 +978,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;
@@ -2443,7 +2444,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
@@ -2540,10 +2542,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;
@@ -2560,7 +2563,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 c6e2f67..6419b32
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -733,7 +733,7 @@ static Node *makeRecursiveViewSelect(cha
LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL
LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOGGED
- MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE METHOD
+ MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE MERGE_ACTION METHOD
MINUTE_P MINVALUE MODE MONTH_P MOVE
NAME_P NAMES NATIONAL NATURAL NCHAR NEW NEXT NFC NFD NFKC NFKD NO NONE
@@ -12374,6 +12374,7 @@ MergeStmt:
USING table_ref
ON a_expr
merge_when_list
+ returning_clause
{
MergeStmt *m = makeNode(MergeStmt);
@@ -12382,6 +12383,7 @@ MergeStmt:
m->sourceRelation = $6;
m->joinCondition = $8;
m->mergeWhenClauses = $9;
+ m->returningList = $10;
$$ = (Node *) m;
}
@@ -15795,6 +15797,14 @@ func_expr_common_subexpr:
n->location = @1;
$$ = (Node *) n;
}
+ | MERGE_ACTION '(' ')'
+ {
+ MergeSupportFunc *m = makeNode(MergeSupportFunc);
+
+ m->msftype = TEXTOID;
+ m->location = @1;
+ $$ = (Node *) m;
+ }
;
@@ -17492,6 +17502,7 @@ col_name_keyword:
| JSON_SCALAR
| JSON_SERIALIZE
| LEAST
+ | MERGE_ACTION
| NATIONAL
| NCHAR
| NONE
@@ -17881,6 +17892,7 @@ bare_label_keyword:
| MATERIALIZED
| MAXVALUE
| MERGE
+ | MERGE_ACTION
| METHOD
| MINVALUE
| MODE
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
new file mode 100644
index 9d151a8..bee7d83
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -468,6 +468,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:
@@ -915,6 +916,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 3c88c9a..6826d4f
--- 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 9300c7b..fe56101
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -54,6 +54,7 @@ static Node *transformAExprDistinct(Pars
static Node *transformAExprNullIf(ParseState *pstate, A_Expr *a);
static Node *transformAExprIn(ParseState *pstate, A_Expr *a);
static Node *transformAExprBetween(ParseState *pstate, A_Expr *a);
+static Node *transformMergeSupportFunc(ParseState *pstate, MergeSupportFunc *f);
static Node *transformBoolExpr(ParseState *pstate, BoolExpr *a);
static Node *transformFuncCall(ParseState *pstate, FuncCall *fn);
static Node *transformMultiAssignRef(ParseState *pstate, MultiAssignRef *maref);
@@ -227,6 +228,11 @@ transformExprRecurse(ParseState *pstate,
result = transformGroupingFunc(pstate, (GroupingFunc *) expr);
break;
+ case T_MergeSupportFunc:
+ result = transformMergeSupportFunc(pstate,
+ (MergeSupportFunc *) expr);
+ break;
+
case T_NamedArgExpr:
{
NamedArgExpr *na = (NamedArgExpr *) expr;
@@ -541,6 +547,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:
@@ -1354,6 +1361,32 @@ transformAExprBetween(ParseState *pstate
}
static Node *
+transformMergeSupportFunc(ParseState *pstate, MergeSupportFunc *f)
+{
+ /*
+ * All we need to do is check that we're in the RETURNING list of a MERGE
+ * command. If so, we just return the node as-is.
+ */
+ if (pstate->p_expr_kind != EXPR_KIND_MERGE_RETURNING)
+ {
+ ParseState *parent_pstate = pstate->parentParseState;
+
+ while (parent_pstate &&
+ parent_pstate->p_expr_kind != EXPR_KIND_MERGE_RETURNING)
+ parent_pstate = parent_pstate->parentParseState;
+
+ if (!parent_pstate ||
+ parent_pstate->p_expr_kind != EXPR_KIND_MERGE_RETURNING)
+ ereport(ERROR,
+ errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("MERGE_ACTION() can only be used in the RETURNING list of a MERGE command"),
+ parser_errposition(pstate, f->location));
+ }
+
+ return (Node *) f;
+}
+
+static Node *
transformBoolExpr(ParseState *pstate, BoolExpr *a)
{
List *args = NIL;
@@ -1767,6 +1800,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:
@@ -3115,6 +3149,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 fdb3e6d..0cbc950
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -2599,6 +2599,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 6b99f55..efb739d
--- a/src/backend/parser/parse_merge.c
+++ b/src/backend/parser/parse_merge.c
@@ -234,6 +234,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.
@@ -391,9 +395,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 6f5d9e2..427b732
--- a/src/backend/parser/parse_relation.c
+++ b/src/backend/parser/parse_relation.c
@@ -2341,9 +2341,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/parser/parse_target.c b/src/backend/parser/parse_target.c
new file mode 100644
index f10fc42..8923be1
--- a/src/backend/parser/parse_target.c
+++ b/src/backend/parser/parse_target.c
@@ -1806,6 +1806,10 @@ FigureColnameInternal(Node *node, char *
/* make GROUPING() act like a regular function */
*name = "grouping";
return 2;
+ case T_MergeSupportFunc:
+ /* make MERGE_ACTION() act like a regular function */
+ *name = "merge_action";
+ return 2;
case T_SubLink:
switch (((SubLink *) node)->subLinkType)
{
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
new file mode 100644
index 89187d9..c3fa282
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -3833,9 +3833,9 @@ RewriteQuery(Query *parsetree, List *rew
ListCell *lc1;
/*
- * First, recursively process any insert/update/delete statements in WITH
- * clauses. (We have to do this first because the WITH clauses may get
- * copied into rule actions below.)
+ * First, recursively process any insert/update/delete/merge statements in
+ * WITH clauses. (We have to do this first because the WITH clauses may
+ * get copied into rule actions below.)
*/
foreach(lc1, parsetree->cteList)
{
@@ -3860,7 +3860,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/rewrite/rowsecurity.c b/src/backend/rewrite/rowsecurity.c
new file mode 100644
index 450af27..59fd305
--- a/src/backend/rewrite/rowsecurity.c
+++ b/src/backend/rewrite/rowsecurity.c
@@ -384,10 +384,10 @@ get_row_security_policies(Query *root, R
* on the final action we take.
*
* We already fetched the SELECT policies above, to check existing rows,
- * but we must also check that new rows created by UPDATE actions are
- * visible, if SELECT rights are required for this relation. We don't do
- * this for INSERT actions, since an INSERT command would only do this
- * check if it had a RETURNING list, and MERGE does not support RETURNING.
+ * but we must also check that new rows created by INSERT/UPDATE actions
+ * are visible, if SELECT rights are required. For INSERT actions, we only
+ * do this if RETURNING is specified, to be consistent with a plain INSERT
+ * command, which can only require SELECT rights when RETURNING is used.
*
* 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
@@ -409,6 +409,8 @@ get_row_security_policies(Query *root, R
List *merge_delete_restrictive_policies;
List *merge_insert_permissive_policies;
List *merge_insert_restrictive_policies;
+ List *merge_select_permissive_policies = NIL;
+ List *merge_select_restrictive_policies = NIL;
/*
* Fetch the UPDATE policies and set them up to execute on the
@@ -446,9 +448,6 @@ get_row_security_policies(Query *root, R
*/
if (perminfo->requiredPerms & ACL_SELECT)
{
- List *merge_select_permissive_policies;
- List *merge_select_restrictive_policies;
-
get_policies_for_relation(rel, CMD_SELECT, user_id,
&merge_select_permissive_policies,
&merge_select_restrictive_policies);
@@ -497,6 +496,21 @@ get_row_security_policies(Query *root, R
withCheckOptions,
hasSubLinks,
false);
+
+ /*
+ * Add ALL/SELECT policies as WCO_RLS_INSERT_CHECK WCOs, to ensure
+ * that the inserted row is visible when executing an INSERT action,
+ * if RETURNING is specified and SELECT rights are required for this
+ * relation.
+ */
+ if (perminfo->requiredPerms & ACL_SELECT && root->returningList)
+ add_with_check_options(rel, rt_index,
+ WCO_RLS_INSERT_CHECK,
+ merge_select_permissive_policies,
+ merge_select_restrictive_policies,
+ withCheckOptions,
+ hasSubLinks,
+ true);
}
table_close(rel, NoLock);
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
new file mode 100644
index 83f86a4..fa66b80
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -2138,11 +2138,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/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
new file mode 100644
index 2a1ee69..e2b46b6
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -7196,8 +7196,13 @@ get_merge_query_def(Query *query, depars
appendStringInfoString(buf, "DO NOTHING");
}
- /* No RETURNING support in MERGE yet */
- Assert(query->returningList == NIL);
+ /* Add RETURNING if present */
+ if (query->returningList)
+ {
+ appendContextKeyword(context, " RETURNING",
+ -PRETTYINDENT_STD, PRETTYINDENT_STD, 1);
+ get_target_list(query->returningList, context, NULL, colNamesVisible);
+ }
}
@@ -8297,6 +8302,7 @@ isSimpleNode(Node *node, Node *parentNod
case T_Aggref:
case T_GroupingFunc:
case T_WindowFunc:
+ case T_MergeSupportFunc:
case T_FuncExpr:
case T_JsonConstructorExpr:
/* function-like: name(..) or name[..] */
@@ -8651,6 +8657,10 @@ get_rule_expr(Node *node, deparse_contex
get_windowfunc_expr((WindowFunc *) node, context);
break;
+ case T_MergeSupportFunc:
+ appendStringInfoString(buf, "MERGE_ACTION()");
+ break;
+
case T_SubscriptingRef:
{
SubscriptingRef *sbsref = (SubscriptingRef *) node;
diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c
new file mode 100644
index 76e01b0..2830bde
--- a/src/bin/psql/common.c
+++ b/src/bin/psql/common.c
@@ -983,13 +983,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/executor/execExpr.h b/src/include/executor/execExpr.h
new file mode 100644
index a28ddcd..8953d76
--- a/src/include/executor/execExpr.h
+++ b/src/include/executor/execExpr.h
@@ -243,6 +243,7 @@ typedef enum ExprEvalOp
EEOP_AGGREF,
EEOP_GROUPING_FUNC,
EEOP_WINDOW_FUNC,
+ EEOP_MERGE_SUPPORT_FUNC,
EEOP_SUBPLAN,
/* aggregation related nodes */
@@ -810,6 +811,8 @@ extern void ExecEvalJsonConstructor(Expr
ExprContext *econtext);
extern void ExecEvalJsonIsPredicate(ExprState *state, ExprEvalStep *op);
extern void ExecEvalGroupingFunc(ExprState *state, ExprEvalStep *op);
+extern void ExecEvalMergeSupportFunc(ExprState *state, ExprEvalStep *op,
+ ExprContext *econtext);
extern void ExecEvalSubPlan(ExprState *state, ExprEvalStep *op,
ExprContext *econtext);
extern void ExecEvalWholeRowVar(ExprState *state, ExprEvalStep *op,
diff --git a/src/include/executor/spi.h b/src/include/executor/spi.h
new file mode 100644
index c13e1f6..48b8773
--- a/src/include/executor/spi.h
+++ b/src/include/executor/spi.h
@@ -97,6 +97,7 @@ typedef struct _SPI_plan *SPIPlanPtr;
#define SPI_OK_REL_UNREGISTER 16
#define SPI_OK_TD_REGISTER 17
#define SPI_OK_MERGE 18
+#define SPI_OK_MERGE_RETURNING 19
#define SPI_OPT_NONATOMIC (1 << 0)
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
new file mode 100644
index 444a5f0..57fdfca
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1325,6 +1325,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 2380821..2bae226
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -1938,6 +1938,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/nodes/primnodes.h b/src/include/nodes/primnodes.h
new file mode 100644
index 4a15460..8df8884
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -572,6 +572,27 @@ typedef struct WindowFunc
} WindowFunc;
/*
+ * MergeSupportFunc
+ *
+ * A MergeSupportFunc is a merge support function expression that can only
+ * appear in the RETURNING list of a MERGE command. It returns information
+ * about the currently executing merge action.
+ *
+ * Currently, the only supported function is MERGE_ACTION(), which returns the
+ * command executed ("INSERT", "UPDATE", or "DELETE").
+ */
+typedef struct MergeSupportFunc
+{
+ Expr xpr;
+ /* type Oid of result */
+ Oid msftype;
+ /* OID of collation, or InvalidOid if none */
+ Oid msfcollid;
+ /* token location, or -1 if unknown */
+ int location;
+} MergeSupportFunc;
+
+/*
* SubscriptingRef: describes a subscripting operation over a container
* (array, etc).
*
diff --git a/src/include/optimizer/paramassign.h b/src/include/optimizer/paramassign.h
new file mode 100644
index 501f728..4026b74
--- a/src/include/optimizer/paramassign.h
+++ b/src/include/optimizer/paramassign.h
@@ -20,6 +20,8 @@ extern Param *replace_outer_placeholderv
PlaceHolderVar *phv);
extern Param *replace_outer_agg(PlannerInfo *root, Aggref *agg);
extern Param *replace_outer_grouping(PlannerInfo *root, GroupingFunc *grp);
+extern Param *replace_outer_merge_support(PlannerInfo *root,
+ MergeSupportFunc *msf);
extern Param *replace_nestloop_param_var(PlannerInfo *root, Var *var);
extern Param *replace_nestloop_param_placeholdervar(PlannerInfo *root,
PlaceHolderVar *phv);
diff --git a/src/include/parser/analyze.h b/src/include/parser/analyze.h
new file mode 100644
index 5ecdd10..28b66fc
--- 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/kwlist.h b/src/include/parser/kwlist.h
new file mode 100644
index 2331aca..0993534
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -266,6 +266,7 @@ PG_KEYWORD("matched", MATCHED, UNRESERVE
PG_KEYWORD("materialized", MATERIALIZED, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("maxvalue", MAXVALUE, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("merge", MERGE, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("merge_action", MERGE_ACTION, COL_NAME_KEYWORD, BARE_LABEL)
PG_KEYWORD("method", METHOD, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("minute", MINUTE_P, UNRESERVED_KEYWORD, AS_LABEL)
PG_KEYWORD("minvalue", MINVALUE, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
new file mode 100644
index 99d6515..5b781d8
--- 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/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
new file mode 100644
index ed51694..6947575
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -4267,9 +4267,9 @@ exec_stmt_execsql(PLpgSQL_execstate *est
/*
* If we have INTO, then we only need one row back ... but if we have INTO
* STRICT or extra check too_many_rows, ask for two rows, so that we can
- * verify the statement returns only one. INSERT/UPDATE/DELETE are always
- * treated strictly. Without INTO, just run the statement to completion
- * (tcount = 0).
+ * verify the statement returns only one. INSERT/UPDATE/DELETE/MERGE are
+ * always treated strictly. Without INTO, just run the statement to
+ * completion (tcount = 0).
*
* We could just ask for two rows always when using INTO, but there are
* some cases where demanding the extra row costs significant time, eg by
@@ -4307,10 +4307,11 @@ exec_stmt_execsql(PLpgSQL_execstate *est
case SPI_OK_INSERT:
case SPI_OK_UPDATE:
case SPI_OK_DELETE:
+ case SPI_OK_MERGE:
case SPI_OK_INSERT_RETURNING:
case SPI_OK_UPDATE_RETURNING:
case SPI_OK_DELETE_RETURNING:
- case SPI_OK_MERGE:
+ case SPI_OK_MERGE_RETURNING:
Assert(stmt->mod_stmt);
exec_set_found(estate, (SPI_processed != 0));
break;
@@ -4489,10 +4490,11 @@ exec_stmt_dynexecute(PLpgSQL_execstate *
case SPI_OK_INSERT:
case SPI_OK_UPDATE:
case SPI_OK_DELETE:
+ case SPI_OK_MERGE:
case SPI_OK_INSERT_RETURNING:
case SPI_OK_UPDATE_RETURNING:
case SPI_OK_DELETE_RETURNING:
- case SPI_OK_MERGE:
+ case SPI_OK_MERGE_RETURNING:
case SPI_OK_UTILITY:
case SPI_OK_REWRITTEN:
break;
diff --git a/src/pl/tcl/pltcl.c b/src/pl/tcl/pltcl.c
new file mode 100644
index 6187e15..18d14a2
--- a/src/pl/tcl/pltcl.c
+++ b/src/pl/tcl/pltcl.c
@@ -2456,6 +2456,7 @@ pltcl_process_SPI_result(Tcl_Interp *int
case SPI_OK_INSERT_RETURNING:
case SPI_OK_DELETE_RETURNING:
case SPI_OK_UPDATE_RETURNING:
+ case SPI_OK_MERGE_RETURNING:
/*
* Process the tuples we got
diff --git a/src/test/regress/expected/merge.out b/src/test/regress/expected/merge.out
new file mode 100644
index e3ebf46..f9f6c2a
--- a/src/test/regress/expected/merge.out
+++ b/src/test/regress/expected/merge.out
@@ -124,20 +124,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
-- materialized view
CREATE MATERIALIZED VIEW mv AS SELECT * FROM target;
@@ -1255,7 +1255,7 @@ BEGIN;
MERGE INTO sq_target
USING v
ON tid = sid
-WHEN MATCHED AND tid > 2 THEN
+WHEN MATCHED AND tid >= 2 THEN
UPDATE SET balance = balance + delta
WHEN NOT MATCHED THEN
INSERT (balance, tid) VALUES (balance + delta, sid)
@@ -1270,7 +1270,7 @@ INSERT INTO sq_source (sid, balance, del
MERGE INTO sq_target t
USING v
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)
@@ -1294,7 +1294,7 @@ WITH targq AS (
MERGE INTO sq_target t
USING v
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)
@@ -1302,21 +1302,207 @@ WHEN MATCHED AND tid < 2 THEN
DELETE;
ROLLBACK;
-- RETURNING
+SELECT * FROM sq_source ORDER BY sid;
+ delta | sid | balance
+-------+-----+---------
+ 10 | 1 | 0
+ 20 | 2 | 0
+ 40 | 4 | 0
+(3 rows)
+
+SELECT * FROM sq_target ORDER BY tid;
+ tid | balance
+-----+---------
+ 1 | 100
+ 2 | 200
+ 3 | 300
+(3 rows)
+
BEGIN;
-INSERT INTO sq_source (sid, balance, delta) VALUES (-1, -1, -10);
+CREATE TABLE merge_actions(action text, abbrev text);
+INSERT INTO merge_actions VALUES ('INSERT', 'ins'), ('UPDATE', 'upd'), ('DELETE', 'del');
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)
+ 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 *;
- ^
+ DELETE
+RETURNING (SELECT abbrev FROM merge_actions
+ WHERE action = merge_action()) AS action,
+ t.*,
+ CASE merge_action()
+ WHEN 'INSERT' THEN 'Inserted '||t
+ WHEN 'UPDATE' THEN 'Added '||delta||' to balance'
+ WHEN 'DELETE' THEN 'Removed '||t
+ END AS description;
+ action | tid | balance | description
+--------+-----+---------+---------------------
+ del | 1 | 100 | Removed (1,100)
+ upd | 2 | 220 | Added 20 to balance
+ ins | 4 | 40 | Inserted (4,40)
+(3 rows)
+
+ROLLBACK;
+-- error when using merge_action() outside MERGE
+SELECT merge_action() FROM sq_target;
+ERROR: MERGE_ACTION() can only be used in the RETURNING list of a MERGE command
+LINE 1: SELECT merge_action() FROM sq_target;
+ ^
+UPDATE sq_target SET balance = balance + 1 RETURNING merge_action();
+ERROR: MERGE_ACTION() can only be used in the RETURNING list of a MERGE command
+LINE 1: ...ATE sq_target SET balance = balance + 1 RETURNING merge_acti...
+ ^
+-- RETURNING in CTEs
+CREATE TABLE sq_target_merge_log (tid integer NOT NULL, last_change text);
+INSERT INTO sq_target_merge_log VALUES (1, 'Original value');
+BEGIN;
+WITH m AS (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 merge_action() AS action, t.*,
+ CASE merge_action()
+ WHEN 'INSERT' THEN 'Inserted '||t
+ WHEN 'UPDATE' THEN 'Added '||delta||' to balance'
+ WHEN 'DELETE' THEN 'Removed '||t
+ END AS description
+), m2 AS (
+ MERGE INTO sq_target_merge_log l
+ USING m
+ ON l.tid = m.tid
+ WHEN MATCHED THEN
+ UPDATE SET last_change = description
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (m.tid, description)
+ RETURNING action, merge_action() AS log_action, l.*
+)
+SELECT * FROM m2;
+ action | log_action | tid | last_change
+--------+------------+-----+---------------------
+ DELETE | UPDATE | 1 | Removed (1,100)
+ UPDATE | INSERT | 2 | Added 20 to balance
+ INSERT | INSERT | 4 | Inserted (4,40)
+(3 rows)
+
+SELECT * FROM sq_target_merge_log ORDER BY tid;
+ tid | last_change
+-----+---------------------
+ 1 | Removed (1,100)
+ 2 | Added 20 to balance
+ 4 | Inserted (4,40)
+(3 rows)
+
+ROLLBACK;
+-- COPY (MERGE ... RETURNING) TO ...
+BEGIN;
+COPY (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 merge_action(), t.*
+) TO stdout;
+DELETE 1 100
+UPDATE 2 220
+INSERT 4 40
+ROLLBACK;
+-- SQL function with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT action text, OUT tid int, OUT new_balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING merge_action(), t.*;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT action, tid, new_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ action | tid | new_balance
+--------+-----+-------------
+ DELETE | 1 | 100
+ UPDATE | 3 | 320
+ INSERT | 4 | 110
+(3 rows)
+
+ROLLBACK;
+-- SQL SRF with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_sq_source_into_sq_target()
+RETURNS TABLE (action text, tid int, balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 merge_action(), t.*;
+$$;
+SELECT * FROM merge_sq_source_into_sq_target();
+ action | tid | balance
+--------+-----+---------
+ DELETE | 1 | 100
+ UPDATE | 2 | 220
+ INSERT | 4 | 40
+(3 rows)
+
+ROLLBACK;
+-- PL/pgSQL function with MERGE ... RETURNING ... INTO
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT r_action text, OUT r_tid int, OUT r_balance int)
+LANGUAGE plpgsql AS
+$$
+BEGIN
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING merge_action(), t.* INTO r_action, r_tid, r_balance;
+END;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT r_action, r_tid, r_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ r_action | r_tid | r_balance
+----------+-------+-----------
+ DELETE | 1 | 100
+ UPDATE | 3 | 320
+ INSERT | 4 | 110
+(3 rows)
+
ROLLBACK;
-- EXPLAIN
CREATE TABLE ex_mtarget (a int, b int)
@@ -1513,7 +1699,7 @@ SELECT * FROM sq_target WHERE tid = 1;
(1 row)
ROLLBACK;
-DROP TABLE sq_target, sq_source CASCADE;
+DROP TABLE sq_target, sq_target_merge_log, sq_source CASCADE;
NOTICE: drop cascades to view v
CREATE TABLE pa_target (tid integer, balance float, val text)
PARTITION BY LIST (tid);
@@ -1639,6 +1825,32 @@ SELECT * FROM pa_target ORDER BY tid;
(14 rows)
ROLLBACK;
+-- update partition key to partition not initially scanned
+BEGIN;
+MERGE INTO pa_target t
+ USING pa_source s
+ ON t.tid = s.sid AND t.tid = 1
+ WHEN MATCHED THEN
+ UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
+ RETURNING merge_action(), t.*;
+ merge_action | tid | balance | val
+--------------+-----+---------+--------------------------
+ UPDATE | 2 | 110 | initial updated by merge
+(1 row)
+
+SELECT * FROM pa_target ORDER BY tid;
+ tid | balance | val
+-----+---------+--------------------------
+ 2 | 110 | initial updated by merge
+ 3 | 300 | initial
+ 5 | 500 | initial
+ 7 | 700 | initial
+ 9 | 900 | initial
+ 11 | 1100 | initial
+ 13 | 1300 | initial
+(7 rows)
+
+ROLLBACK;
DROP TABLE pa_target CASCADE;
-- The target table is partitioned in the same way, but this time by attaching
-- partitions which have columns in different order, dropped columns etc.
@@ -1888,7 +2100,21 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge');
+ INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge')
+ RETURNING merge_action(), t.*;
+ merge_action | logts | tid | balance | val
+--------------+--------------------------+-----+---------+--------------------------
+ UPDATE | Tue Jan 31 00:00:00 2017 | 1 | 110 | initial updated by merge
+ UPDATE | Tue Feb 28 00:00:00 2017 | 2 | 220 | initial updated by merge
+ INSERT | Sun Jan 15 00:00:00 2017 | 3 | 30 | inserted by merge
+ UPDATE | Tue Jan 31 00:00:00 2017 | 4 | 440 | initial updated by merge
+ UPDATE | Tue Feb 28 00:00:00 2017 | 5 | 550 | initial updated by merge
+ INSERT | Sun Jan 15 00:00:00 2017 | 6 | 60 | inserted by merge
+ UPDATE | Tue Jan 31 00:00:00 2017 | 7 | 770 | initial updated by merge
+ UPDATE | Tue Feb 28 00:00:00 2017 | 8 | 880 | initial updated by merge
+ INSERT | Sun Jan 15 00:00:00 2017 | 9 | 90 | inserted by merge
+(9 rows)
+
SELECT * FROM pa_target ORDER BY tid;
logts | tid | balance | val
--------------------------+-----+---------+--------------------------
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
new file mode 100644
index 6988128..3fd4fab
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -2302,6 +2302,35 @@ WHEN MATCHED THEN
UPDATE SET dnotes = dnotes || ' notes added by merge8 '
WHEN NOT MATCHED THEN
INSERT VALUES (13, 44, 1, 'regress_rls_bob', 'new manga');
+SELECT * FROM document WHERE did = 13;
+ did | cid | dlevel | dauthor | dtitle | dnotes
+-----+-----+--------+---------+--------+--------
+(0 rows)
+
+-- but not OK if RETURNING is used
+MERGE INTO document d
+USING (SELECT 14 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge9 '
+WHEN NOT MATCHED THEN
+ INSERT VALUES (14, 44, 1, 'regress_rls_bob', 'new manga')
+RETURNING *;
+ERROR: new row violates row-level security policy for table "document"
+-- but OK if new row is visible
+MERGE INTO document d
+USING (SELECT 14 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge10 '
+WHEN NOT MATCHED THEN
+ INSERT VALUES (14, 11, 1, 'regress_rls_bob', 'new novel')
+RETURNING *;
+ sdid | did | cid | dlevel | dauthor | dtitle | dnotes
+------+-----+-----+--------+-----------------+-----------+--------
+ 14 | 14 | 11 | 1 | regress_rls_bob | new novel |
+(1 row)
+
RESET SESSION AUTHORIZATION;
-- drop the restrictive SELECT policy so that we can look at the
-- final state of the table
@@ -2325,7 +2354,8 @@ SELECT * FROM document;
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 notes added by merge7
13 | 44 | 1 | regress_rls_bob | new manga |
-(15 rows)
+ 14 | 11 | 1 | regress_rls_bob | new novel |
+(16 rows)
--
-- ROLE/GROUP
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
new file mode 100644
index 0cd2c64..84e359f
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -3638,7 +3638,7 @@ MERGE INTO rule_merge1 t USING (SELECT 1
-- test deparsing
CREATE TABLE sf_target(id int, data text, filling int[]);
CREATE FUNCTION merge_sf_test()
- RETURNS void
+ RETURNS TABLE(action text, a int, b text, id int, data text, filling int[])
LANGUAGE sql
BEGIN ATOMIC
MERGE INTO sf_target t
@@ -3675,11 +3675,13 @@ WHEN NOT MATCHED
VALUES (s.a, s.b, DEFAULT)
WHEN NOT MATCHED
THEN INSERT (filling[1], id)
- VALUES (s.a, s.a);
+ VALUES (s.a, s.a)
+RETURNING
+ merge_action() AS action, *;
END;
\sf merge_sf_test
CREATE OR REPLACE FUNCTION public.merge_sf_test()
- RETURNS void
+ RETURNS TABLE(action text, a integer, b text, id integer, data text, filling integer[])
LANGUAGE sql
BEGIN ATOMIC
MERGE INTO sf_target t
@@ -3716,7 +3718,13 @@ BEGIN ATOMIC
VALUES (s.a, s.b, DEFAULT)
WHEN NOT MATCHED
THEN INSERT (filling[1], id)
- VALUES (s.a, s.a);
+ VALUES (s.a, s.a)
+ RETURNING MERGE_ACTION() AS action,
+ s.a,
+ s.b,
+ t.id,
+ t.data,
+ t.filling;
END
DROP FUNCTION merge_sf_test;
DROP TABLE sf_target;
diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out
new file mode 100644
index 794cf9c..1062c34
--- a/src/test/regress/expected/updatable_views.out
+++ b/src/test/regress/expected/updatable_views.out
@@ -479,7 +479,15 @@ MERGE INTO rw_view1 t
(2, 'ROW 2'), (3, 'ROW 3')) AS v(a,b) ON t.a = v.a
WHEN MATCHED AND t.a <= 1 THEN UPDATE SET b = v.b
WHEN MATCHED THEN DELETE
- WHEN NOT MATCHED AND a > 0 THEN INSERT (a) VALUES (v.a);
+ WHEN NOT MATCHED AND a > 0 THEN INSERT (a) VALUES (v.a)
+ RETURNING merge_action(), v.*, t.*;
+ merge_action | a | b | a | b
+--------------+---+-------+---+-------------
+ UPDATE | 1 | ROW 1 | 1 | ROW 1
+ DELETE | 3 | ROW 3 | 3 | Row 3
+ INSERT | 2 | ROW 2 | 2 | Unspecified
+(3 rows)
+
SELECT * FROM base_tbl ORDER BY a;
a | b
----+-------------
@@ -631,7 +639,15 @@ MERGE INTO rw_view2 t
USING (VALUES (3, 'R3'), (4, 'R4'), (5, 'R5')) AS v(a,b) ON aaa = v.a
WHEN MATCHED AND aaa = 3 THEN DELETE
WHEN MATCHED THEN UPDATE SET bbb = v.b
- WHEN NOT MATCHED THEN INSERT (aaa) VALUES (v.a);
+ WHEN NOT MATCHED THEN INSERT (aaa) VALUES (v.a)
+ RETURNING merge_action(), v.*, t.*;
+ merge_action | a | b | aaa | bbb
+--------------+---+----+-----+-------------
+ DELETE | 3 | R3 | 3 | Row 3
+ UPDATE | 4 | R4 | 4 | R4
+ INSERT | 5 | R5 | 5 | Unspecified
+(3 rows)
+
SELECT * FROM rw_view2 ORDER BY aaa;
aaa | bbb
-----+-------------
@@ -1071,7 +1087,15 @@ MERGE INTO rw_view2 t
USING (SELECT x, 'R'||x FROM generate_series(0,3) x) AS s(a,b) ON t.a = s.a
WHEN MATCHED AND t.a <= 1 THEN DELETE
WHEN MATCHED THEN UPDATE SET b = s.b
- WHEN NOT MATCHED AND s.a > 0 THEN INSERT VALUES (s.a, s.b);
+ WHEN NOT MATCHED AND s.a > 0 THEN INSERT VALUES (s.a, s.b)
+ RETURNING merge_action(), s.*, t.*;
+ merge_action | a | b | a | b
+--------------+---+----+---+-------
+ DELETE | 1 | R1 | 1 | Row 1
+ UPDATE | 2 | R2 | 2 | R2
+ INSERT | 3 | R3 | 3 | R3
+(3 rows)
+
SELECT * FROM base_tbl ORDER BY a;
a | b
----+--------
diff --git a/src/test/regress/expected/with.out b/src/test/regress/expected/with.out
new file mode 100644
index 7d796ea..6de347b
--- a/src/test/regress/expected/with.out
+++ b/src/test/regress/expected/with.out
@@ -3493,6 +3493,16 @@ SELECT * FROM t;
ERROR: WITH query "t" does not have a RETURNING clause
LINE 4: SELECT * FROM t;
^
+-- RETURNING tries to return its own output
+WITH RECURSIVE t(action, a) AS (
+ MERGE INTO y USING (VALUES (11)) v(a) ON y.a = v.a
+ WHEN NOT MATCHED THEN INSERT VALUES (v.a)
+ RETURNING merge_action(), (SELECT a FROM t)
+)
+SELECT * FROM t;
+ERROR: recursive query "t" must not contain data-modifying statements
+LINE 1: WITH RECURSIVE t(action, a) AS (
+ ^
-- data-modifying WITH allowed only at the top level
SELECT * FROM (
WITH t AS (UPDATE y SET a=a+1 RETURNING *)
diff --git a/src/test/regress/sql/merge.sql b/src/test/regress/sql/merge.sql
new file mode 100644
index 79a1868..dfcc243
--- a/src/test/regress/sql/merge.sql
+++ b/src/test/regress/sql/merge.sql
@@ -88,12 +88,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
@@ -817,7 +817,7 @@ BEGIN;
MERGE INTO sq_target
USING v
ON tid = sid
-WHEN MATCHED AND tid > 2 THEN
+WHEN MATCHED AND tid >= 2 THEN
UPDATE SET balance = balance + delta
WHEN NOT MATCHED THEN
INSERT (balance, tid) VALUES (balance + delta, sid)
@@ -830,7 +830,7 @@ INSERT INTO sq_source (sid, balance, del
MERGE INTO sq_target t
USING v
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)
@@ -848,7 +848,7 @@ WITH targq AS (
MERGE INTO sq_target t
USING v
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)
@@ -857,18 +857,149 @@ WHEN MATCHED AND tid < 2 THEN
ROLLBACK;
-- RETURNING
+SELECT * FROM sq_source ORDER BY sid;
+SELECT * FROM sq_target ORDER BY tid;
+
BEGIN;
-INSERT INTO sq_source (sid, balance, delta) VALUES (-1, -1, -10);
+CREATE TABLE merge_actions(action text, abbrev text);
+INSERT INTO merge_actions VALUES ('INSERT', 'ins'), ('UPDATE', 'upd'), ('DELETE', 'del');
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)
+ INSERT (balance, tid) VALUES (balance + delta, sid)
WHEN MATCHED AND tid < 2 THEN
- DELETE
-RETURNING *;
+ DELETE
+RETURNING (SELECT abbrev FROM merge_actions
+ WHERE action = merge_action()) AS action,
+ t.*,
+ CASE 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_action() outside MERGE
+SELECT merge_action() FROM sq_target;
+UPDATE sq_target SET balance = balance + 1 RETURNING merge_action();
+
+-- RETURNING in CTEs
+CREATE TABLE sq_target_merge_log (tid integer NOT NULL, last_change text);
+INSERT INTO sq_target_merge_log VALUES (1, 'Original value');
+BEGIN;
+WITH m AS (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 merge_action() AS action, t.*,
+ CASE merge_action()
+ WHEN 'INSERT' THEN 'Inserted '||t
+ WHEN 'UPDATE' THEN 'Added '||delta||' to balance'
+ WHEN 'DELETE' THEN 'Removed '||t
+ END AS description
+), m2 AS (
+ MERGE INTO sq_target_merge_log l
+ USING m
+ ON l.tid = m.tid
+ WHEN MATCHED THEN
+ UPDATE SET last_change = description
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (m.tid, description)
+ RETURNING action, merge_action() AS log_action, l.*
+)
+SELECT * FROM m2;
+SELECT * FROM sq_target_merge_log ORDER BY tid;
+ROLLBACK;
+
+-- COPY (MERGE ... RETURNING) TO ...
+BEGIN;
+COPY (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 merge_action(), t.*
+) TO stdout;
+ROLLBACK;
+
+-- SQL function with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT action text, OUT tid int, OUT new_balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING merge_action(), t.*;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT action, tid, new_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ROLLBACK;
+
+-- SQL SRF with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_sq_source_into_sq_target()
+RETURNS TABLE (action text, tid int, balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 merge_action(), t.*;
+$$;
+SELECT * FROM merge_sq_source_into_sq_target();
+ROLLBACK;
+
+-- PL/pgSQL function with MERGE ... RETURNING ... INTO
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT r_action text, OUT r_tid int, OUT r_balance int)
+LANGUAGE plpgsql AS
+$$
+BEGIN
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING merge_action(), t.* INTO r_action, r_tid, r_balance;
+END;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT r_action, r_tid, r_balance FROM merge_into_sq_target(sid, balance, delta)) m;
ROLLBACK;
-- EXPLAIN
@@ -967,7 +1098,7 @@ WHEN MATCHED THEN
SELECT * FROM sq_target WHERE tid = 1;
ROLLBACK;
-DROP TABLE sq_target, sq_source CASCADE;
+DROP TABLE sq_target, sq_target_merge_log, sq_source CASCADE;
CREATE TABLE pa_target (tid integer, balance float, val text)
PARTITION BY LIST (tid);
@@ -1034,6 +1165,17 @@ SELECT merge_func();
SELECT * FROM pa_target ORDER BY tid;
ROLLBACK;
+-- update partition key to partition not initially scanned
+BEGIN;
+MERGE INTO pa_target t
+ USING pa_source s
+ ON t.tid = s.sid AND t.tid = 1
+ WHEN MATCHED THEN
+ UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
+ RETURNING merge_action(), t.*;
+SELECT * FROM pa_target ORDER BY tid;
+ROLLBACK;
+
DROP TABLE pa_target CASCADE;
-- The target table is partitioned in the same way, but this time by attaching
@@ -1210,7 +1352,8 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge');
+ INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge')
+ RETURNING merge_action(), t.*;
SELECT * FROM pa_target ORDER BY tid;
ROLLBACK;
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
new file mode 100644
index dec7340..1d5ed0a
--- a/src/test/regress/sql/rowsecurity.sql
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -992,6 +992,27 @@ WHEN MATCHED THEN
UPDATE SET dnotes = dnotes || ' notes added by merge8 '
WHEN NOT MATCHED THEN
INSERT VALUES (13, 44, 1, 'regress_rls_bob', 'new manga');
+SELECT * FROM document WHERE did = 13;
+
+-- but not OK if RETURNING is used
+MERGE INTO document d
+USING (SELECT 14 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge9 '
+WHEN NOT MATCHED THEN
+ INSERT VALUES (14, 44, 1, 'regress_rls_bob', 'new manga')
+RETURNING *;
+
+-- but OK if new row is visible
+MERGE INTO document d
+USING (SELECT 14 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge10 '
+WHEN NOT MATCHED THEN
+ INSERT VALUES (14, 11, 1, 'regress_rls_bob', 'new novel')
+RETURNING *;
RESET SESSION AUTHORIZATION;
-- drop the restrictive SELECT policy so that we can look at the
diff --git a/src/test/regress/sql/rules.sql b/src/test/regress/sql/rules.sql
new file mode 100644
index 6924012..27340ba
--- a/src/test/regress/sql/rules.sql
+++ b/src/test/regress/sql/rules.sql
@@ -1294,7 +1294,7 @@ MERGE INTO rule_merge1 t USING (SELECT 1
CREATE TABLE sf_target(id int, data text, filling int[]);
CREATE FUNCTION merge_sf_test()
- RETURNS void
+ RETURNS TABLE(action text, a int, b text, id int, data text, filling int[])
LANGUAGE sql
BEGIN ATOMIC
MERGE INTO sf_target t
@@ -1331,7 +1331,9 @@ WHEN NOT MATCHED
VALUES (s.a, s.b, DEFAULT)
WHEN NOT MATCHED
THEN INSERT (filling[1], id)
- VALUES (s.a, s.a);
+ VALUES (s.a, s.a)
+RETURNING
+ merge_action() AS action, *;
END;
\sf merge_sf_test
diff --git a/src/test/regress/sql/updatable_views.sql b/src/test/regress/sql/updatable_views.sql
new file mode 100644
index ae11e46..afdf331
--- a/src/test/regress/sql/updatable_views.sql
+++ b/src/test/regress/sql/updatable_views.sql
@@ -175,7 +175,8 @@ MERGE INTO rw_view1 t
(2, 'ROW 2'), (3, 'ROW 3')) AS v(a,b) ON t.a = v.a
WHEN MATCHED AND t.a <= 1 THEN UPDATE SET b = v.b
WHEN MATCHED THEN DELETE
- WHEN NOT MATCHED AND a > 0 THEN INSERT (a) VALUES (v.a);
+ WHEN NOT MATCHED AND a > 0 THEN INSERT (a) VALUES (v.a)
+ RETURNING merge_action(), v.*, t.*;
SELECT * FROM base_tbl ORDER BY a;
EXPLAIN (costs off) UPDATE rw_view1 SET a=6 WHERE a=5;
@@ -246,7 +247,8 @@ MERGE INTO rw_view2 t
USING (VALUES (3, 'R3'), (4, 'R4'), (5, 'R5')) AS v(a,b) ON aaa = v.a
WHEN MATCHED AND aaa = 3 THEN DELETE
WHEN MATCHED THEN UPDATE SET bbb = v.b
- WHEN NOT MATCHED THEN INSERT (aaa) VALUES (v.a);
+ WHEN NOT MATCHED THEN INSERT (aaa) VALUES (v.a)
+ RETURNING merge_action(), v.*, t.*;
SELECT * FROM rw_view2 ORDER BY aaa;
EXPLAIN (costs off) UPDATE rw_view2 SET aaa=5 WHERE aaa=4;
@@ -458,7 +460,8 @@ MERGE INTO rw_view2 t
USING (SELECT x, 'R'||x FROM generate_series(0,3) x) AS s(a,b) ON t.a = s.a
WHEN MATCHED AND t.a <= 1 THEN DELETE
WHEN MATCHED THEN UPDATE SET b = s.b
- WHEN NOT MATCHED AND s.a > 0 THEN INSERT VALUES (s.a, s.b);
+ WHEN NOT MATCHED AND s.a > 0 THEN INSERT VALUES (s.a, s.b)
+ RETURNING merge_action(), s.*, t.*;
SELECT * FROM base_tbl ORDER BY a;
EXPLAIN (costs off) UPDATE rw_view2 SET a=3 WHERE a=2;
diff --git a/src/test/regress/sql/with.sql b/src/test/regress/sql/with.sql
new file mode 100644
index f8a213e..037bc0a
--- a/src/test/regress/sql/with.sql
+++ b/src/test/regress/sql/with.sql
@@ -1591,6 +1591,14 @@ WITH t AS (
)
SELECT * FROM t;
+-- RETURNING tries to return its own output
+WITH RECURSIVE t(action, a) AS (
+ MERGE INTO y USING (VALUES (11)) v(a) ON y.a = v.a
+ WHEN NOT MATCHED THEN INSERT VALUES (v.a)
+ RETURNING merge_action(), (SELECT a FROM t)
+)
+SELECT * FROM t;
+
-- data-modifying WITH allowed only at the top level
SELECT * FROM (
WITH t AS (UPDATE y SET a=a+1 RETURNING *)
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
new file mode 100644
index aa7a25b..7a102e8
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1577,6 +1577,7 @@ MergeJoinState
MergePath
MergeScanSelCache
MergeStmt
+MergeSupportFunc
MergeWhenClause
MetaCommand
MinMaxAggInfo
Hi
mainly document issues. Other than that, it looks good!
MERGE not supported in COPY
MERGE not supported in WITH query
These entries in src/backend/po.* need to be deleted if this patch is
committed?
------------------------------------------------------
<indexterm zone="dml-returning">
<primary>RETURNING</primary>
</indexterm>
<indexterm zone="dml-returning">
<primary>INSERT</primary>
<secondary>RETURNING</secondary>
</indexterm>
<indexterm zone="dml-returning">
<primary>UPDATE</primary>
<secondary>RETURNING</secondary>
</indexterm>
<indexterm zone="dml-returning">
<primary>DELETE</primary>
<secondary>RETURNING</secondary>
</indexterm>
<indexterm zone="dml-returning">
<primary>MERGE</primary>
<secondary>RETURNING</secondary>
</indexterm>
in doc/src/sgml/dml.sgml, what is the point of these?
It is not rendered in the html file, deleting it still generates all the
html file.
------------------------------------------------------
The following part is about doc/src/sgml/plpgsql.sgml.
<para>
The <replaceable>query</replaceable> used in this type of
<literal>FOR</literal>
statement can be any SQL command that returns rows to the caller:
<command>SELECT</command> is the most common case,
but you can also use <command>INSERT</command>,
<command>UPDATE</command>, or
<command>DELETE</command> with a <literal>RETURNING</literal> clause.
Some utility
commands such as <command>EXPLAIN</command> will work too.
</para>
here we need to add <command>MERGE</command>?
<para>
Row-level triggers fired <literal>BEFORE</literal> can return null to
signal the
trigger manager to skip the rest of the operation for this row
(i.e., subsequent triggers are not fired, and the
<command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>
does not occur
for this row). If a nonnull
here we need to add <command>MERGE</command>?
<para>
Variable substitution currently works only in <command>SELECT</command>,
<command>INSERT</command>, <command>UPDATE</command>,
<command>DELETE</command>, and commands containing one of
these (such as <command>EXPLAIN</command> and <command>CREATE TABLE
... AS SELECT</command>),
because the main SQL engine allows query parameters only in these
commands. To use a non-constant name or value in other statement
types (generically called utility statements), you must construct
the utility statement as a string and <command>EXECUTE</command> it.
</para>
here we need to add <command>MERGE</command>?
demo:
CREATE OR REPlACE FUNCTION stamp_user2(id int, comment text) RETURNS void
AS $$
<<fn>>
DECLARE
curtime timestamp := now();
BEGIN
MERGE INTO users
USING (SELECT 1)
ON true
WHEN MATCHED and (users.id = stamp_user2.id) THEN
update SET last_modified = fn.curtime, comment =
stamp_user2.comment;
raise notice 'test';
END;
$$ LANGUAGE plpgsql;
<literal>INSTEAD OF</literal> triggers (which are always row-level
triggers,
and may only be used on views) can return null to signal that they did
not perform any updates, and that the rest of the operation for this
row should be skipped (i.e., subsequent triggers are not fired, and the
row is not counted in the rows-affected status for the surrounding
<command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>).
I am not sure we need to add <command>MERGE</command >. Maybe not.
On 2024-Mar-13, Dean Rasheed wrote:
On Wed, 13 Mar 2024 at 06:44, jian he <jian.universality@gmail.com> wrote:
<synopsis>
[ WITH <replaceable class="parameter">with_query</replaceable> [, ...] ]
MERGE INTO [ ONLY ] <replaceablehere the "WITH" part should have "[ RECURSIVE ]"
Actually, no. MERGE doesn't support WITH RECURSIVE.
It's not entirely clear to me why though. I did a quick test, removing
that restriction in the parse analysis code, and it seemed to work
fine. Alvaro, do you remember why that restriction is there?
There's no real reason for it, other than I didn't want to have to think
it through; I did suspect that it might Just Work, but I felt I would
have had to come up with more nontrivial test cases than I wanted to
write at the time.
--
Álvaro Herrera PostgreSQL Developer — https://www.EnterpriseDB.com/
"People get annoyed when you try to debug them." (Larry Wall)
On Thu, 14 Mar 2024 at 05:30, jian he <jian.universality@gmail.com> wrote:
Hi
mainly document issues. Other than that, it looks good!
Thanks for the review.
MERGE not supported in COPY
MERGE not supported in WITH query
These entries in src/backend/po.* need to be deleted if this patch is committed?
No, translation updates are handled separately.
<indexterm zone="dml-returning">
<primary>MERGE</primary>
<secondary>RETURNING</secondary>
</indexterm>in doc/src/sgml/dml.sgml, what is the point of these?
It is not rendered in the html file, deleting it still generates all the html file.
These generate entries in the index -- see
https://www.postgresql.org/docs/current/bookindex.html
The following part is about doc/src/sgml/plpgsql.sgml.
<para>
The <replaceable>query</replaceable> used in this type of <literal>FOR</literal>
statement can be any SQL command that returns rows to the caller:
<command>SELECT</command> is the most common case,
but you can also use <command>INSERT</command>, <command>UPDATE</command>, or
<command>DELETE</command> with a <literal>RETURNING</literal> clause. Some utility
commands such as <command>EXPLAIN</command> will work too.
</para>
here we need to add <command>MERGE</command>?
Ah yes. I'm not sure how I missed that one.
<para>
Row-level triggers fired <literal>BEFORE</literal> can return null to signal the
trigger manager to skip the rest of the operation for this row
(i.e., subsequent triggers are not fired, and the
<command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command> does not occur
for this row). If a nonnull
here we need to add <command>MERGE</command>?
No, because there are no MERGE triggers. I suppose it could be updated
to mention that this also applies to INSERT, UPDATE, and DELETE
actions in a MERGE, but I'm not sure it's really necessary. In any
case, that's not something changed in this patch, so if we want to do
this, it should be a separate doc patch.
<para>
Variable substitution currently works only in <command>SELECT</command>,
<command>INSERT</command>, <command>UPDATE</command>,
<command>DELETE</command>, and commands containing one of
these (such as <command>EXPLAIN</command> and <command>CREATE TABLE
... AS SELECT</command>),
because the main SQL engine allows query parameters only in these
commands. To use a non-constant name or value in other statement
types (generically called utility statements), you must construct
the utility statement as a string and <command>EXECUTE</command> it.
</para>
here we need to add <command>MERGE</command>?
Yes, I suppose so (though arguably it falls into the category of
"commands containing" one of INSERT, UPDATE or DELETE). As above, this
isn't something changed by this patch, so it should be done
separately.
<literal>INSTEAD OF</literal> triggers (which are always row-level triggers,
and may only be used on views) can return null to signal that they did
not perform any updates, and that the rest of the operation for this
row should be skipped (i.e., subsequent triggers are not fired, and the
row is not counted in the rows-affected status for the surrounding
<command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>).
I am not sure we need to add <command>MERGE</command >. Maybe not.
Ditto.
Updated patch attached.
Regards,
Dean
Attachments:
support-merge-returning-v19.patchtext/x-patch; charset=US-ASCII; name=support-merge-returning-v19.patchDownload
diff --git a/doc/src/sgml/dml.sgml b/doc/src/sgml/dml.sgml
new file mode 100644
index cbbc5e2..3d95bdb
--- a/doc/src/sgml/dml.sgml
+++ b/doc/src/sgml/dml.sgml
@@ -283,10 +283,15 @@ DELETE FROM products;
<secondary>RETURNING</secondary>
</indexterm>
+ <indexterm zone="dml-returning">
+ <primary>MERGE</primary>
+ <secondary>RETURNING</secondary>
+ </indexterm>
+
<para>
Sometimes it is useful to obtain data from modified rows while they are
being manipulated. The <command>INSERT</command>, <command>UPDATE</command>,
- and <command>DELETE</command> commands all have an
+ <command>DELETE</command>, and <command>MERGE</command> commands all have an
optional <literal>RETURNING</literal> clause that supports this. Use
of <literal>RETURNING</literal> avoids performing an extra database query to
collect the data, and is especially valuable when it would otherwise be
@@ -339,6 +344,21 @@ DELETE FROM products
</programlisting>
</para>
+ <para>
+ In a <command>MERGE</command>, the data available to <literal>RETURNING</literal> is
+ the content of the source row plus the content of the inserted, updated, or
+ deleted target row. Since it is quite common for the source and target to
+ have many of the same columns, specifying <literal>RETURNING *</literal>
+ can lead to a lot of duplicated columns, so it is often more useful to
+ qualify it so as to return just the source or target row. For example:
+<programlisting>
+MERGE INTO products p USING new_products n ON p.product_no = n.product_no
+ WHEN NOT MATCHED THEN INSERT VALUES (n.product_no, n.name, n.price)
+ WHEN MATCHED THEN UPDATE SET name = n.name, price = n.price
+ RETURNING p.*;
+</programlisting>
+ </para>
+
<para>
If there are triggers (<xref linkend="triggers"/>) on the target table,
the data available to <literal>RETURNING</literal> is the row as modified by
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
new file mode 100644
index 72c5175..ebb984a
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -22421,6 +22421,85 @@ SELECT count(*) FROM sometable;
</sect1>
+ <sect1 id="functions-merge-support">
+ <title>Merge Support Functions</title>
+
+ <indexterm>
+ <primary>MERGE</primary>
+ <secondary>RETURNING</secondary>
+ </indexterm>
+
+ <para>
+ <productname>PostgreSQL</productname> includes one merge support function
+ that may be used in the <literal>RETURNING</literal> list of a
+ <xref linkend="sql-merge"/> command to identify the action taken for each
+ row.
+ </para>
+
+ <table id="functions-merge-support-table">
+ <title>Merge Support Functions</title>
+
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ Function
+ </para>
+ <para>
+ Description
+ </para></entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>merge_action</primary>
+ </indexterm>
+ <function id="merge_action">merge_action</function> ( )
+ <returnvalue>text</returnvalue>
+ </para>
+ <para>
+ Returns the merge action command executed for the current row. This
+ will be <literal>'INSERT'</literal>, <literal>'UPDATE'</literal>, or
+ <literal>'DELETE'</literal>.
+ </para></entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+
+ <para>
+ Example:
+<screen><![CDATA[
+MERGE INTO products p
+ USING stock s ON p.product_id = s.product_id
+ WHEN MATCHED AND s.quantity > 0 THEN
+ UPDATE SET in_stock = true, quantity = s.quantity
+ WHEN MATCHED THEN
+ UPDATE SET in_stock = false, quantity = 0
+ WHEN NOT MATCHED THEN
+ INSERT (product_id, in_stock, quantity)
+ VALUES (s.product_id, true, s.quantity)
+ RETURNING merge_action(), p.*;
+
+ merge_action | product_id | in_stock | quantity
+--------------+------------+----------+----------
+ UPDATE | 1001 | t | 50
+ UPDATE | 1002 | f | 0
+ INSERT | 1003 | t | 10
+]]></screen>
+ </para>
+
+ <para>
+ Note that this function can only be used in the <literal>RETURNING</literal>
+ list of a <command>MERGE</command> command. It is an error to use it in any
+ other part of a query.
+ </para>
+
+ </sect1>
+
<sect1 id="functions-subquery">
<title>Subquery Expressions</title>
diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml
new file mode 100644
index 8c2f114..a81c17a
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -1442,9 +1442,9 @@
to a <glossterm linkend="glossary-client">client</glossterm> upon the
completion of an <acronym>SQL</acronym> command, usually a
<command>SELECT</command> but it can be an
- <command>INSERT</command>, <command>UPDATE</command>, or
- <command>DELETE</command> command if the <literal>RETURNING</literal>
- clause is specified.
+ <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command> command if the
+ <literal>RETURNING</literal> clause is specified.
</para>
<para>
The fact that a result set is a relation means that a query can be used
diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml
new file mode 100644
index c2b9c6a..6f880b7
--- a/doc/src/sgml/plpgsql.sgml
+++ b/doc/src/sgml/plpgsql.sgml
@@ -1043,8 +1043,8 @@ INSERT INTO mytable VALUES (1,'one'), (2
</para>
<para>
- If the command does return rows (for example <command>SELECT</command>,
- or <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>
+ If the command does return rows (for example <command>SELECT</command>, or
+ <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command>
with <literal>RETURNING</literal>), there are two ways to proceed.
When the command will return at most one row, or you only care about
the first row of output, write the command as usual but add
@@ -1172,6 +1172,7 @@ SELECT <replaceable>select_expressions</
INSERT ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
UPDATE ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
DELETE ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
+MERGE ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
</synopsis>
where <replaceable>target</replaceable> can be a record variable, a row
@@ -1182,8 +1183,8 @@ DELETE ... RETURNING <replaceable>expres
<literal>INTO</literal> clause) just as described above,
and the plan is cached in the same way.
This works for <command>SELECT</command>,
- <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command> with
- <literal>RETURNING</literal>, and certain utility commands
+ <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command>
+ with <literal>RETURNING</literal>, and certain utility commands
that return row sets, such as <command>EXPLAIN</command>.
Except for the <literal>INTO</literal> clause, the SQL command is the same
as it would be written outside <application>PL/pgSQL</application>.
@@ -1259,7 +1260,7 @@ END;
</para>
<para>
- For <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command> with
+ For <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command> with
<literal>RETURNING</literal>, <application>PL/pgSQL</application> reports
an error for more than one returned row, even when
<literal>STRICT</literal> is not specified. This is because there
@@ -2657,8 +2658,9 @@ $$ LANGUAGE plpgsql;
The <replaceable>query</replaceable> used in this type of <literal>FOR</literal>
statement can be any SQL command that returns rows to the caller:
<command>SELECT</command> is the most common case,
- but you can also use <command>INSERT</command>, <command>UPDATE</command>, or
- <command>DELETE</command> with a <literal>RETURNING</literal> clause. Some utility
+ but you can also use <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command> with a
+ <literal>RETURNING</literal> clause. Some utility
commands such as <command>EXPLAIN</command> will work too.
</para>
diff --git a/doc/src/sgml/queries.sgml b/doc/src/sgml/queries.sgml
new file mode 100644
index 648b283..372cce1
--- a/doc/src/sgml/queries.sgml
+++ b/doc/src/sgml/queries.sgml
@@ -2063,9 +2063,10 @@ SELECT <replaceable>select_list</replace
Table Expressions or <acronym>CTE</acronym>s, can be thought of as defining
temporary tables that exist just for one query. Each auxiliary statement
in a <literal>WITH</literal> clause can be a <command>SELECT</command>,
- <command>INSERT</command>, <command>UPDATE</command>, or <command>DELETE</command>; and the
+ <command>INSERT</command>, <command>UPDATE</command>, <command>DELETE</command>,
+ or <command>MERGE</command>; and the
<literal>WITH</literal> clause itself is attached to a primary statement that can
- be a <command>SELECT</command>, <command>INSERT</command>, <command>UPDATE</command>,
+ also be a <command>SELECT</command>, <command>INSERT</command>, <command>UPDATE</command>,
<command>DELETE</command>, or <command>MERGE</command>.
</para>
@@ -2599,8 +2600,8 @@ SELECT * FROM w AS w1 JOIN w AS w2 ON w1
<title>Data-Modifying Statements in <literal>WITH</literal></title>
<para>
- You can use most data-modifying statements (<command>INSERT</command>,
- <command>UPDATE</command>, or <command>DELETE</command>, but not
+ You can use data-modifying statements (<command>INSERT</command>,
+ <command>UPDATE</command>, <command>DELETE</command>, or
<command>MERGE</command>) in <literal>WITH</literal>. This
allows you to perform several different operations in the same query.
An example is:
diff --git a/doc/src/sgml/ref/copy.sgml b/doc/src/sgml/ref/copy.sgml
new file mode 100644
index 55764fc..6c83e30
--- a/doc/src/sgml/ref/copy.sgml
+++ b/doc/src/sgml/ref/copy.sgml
@@ -130,16 +130,19 @@ COPY { <replaceable class="parameter">ta
A <link linkend="sql-select"><command>SELECT</command></link>,
<link linkend="sql-values"><command>VALUES</command></link>,
<link linkend="sql-insert"><command>INSERT</command></link>,
- <link linkend="sql-update"><command>UPDATE</command></link>, or
- <link linkend="sql-delete"><command>DELETE</command></link> command whose results are to be
- copied. Note that parentheses are required around the query.
+ <link linkend="sql-update"><command>UPDATE</command></link>,
+ <link linkend="sql-delete"><command>DELETE</command></link>, or
+ <link linkend="sql-merge"><command>MERGE</command></link> command
+ whose results are to be copied. Note that parentheses are required
+ around the query.
</para>
<para>
- For <command>INSERT</command>, <command>UPDATE</command> and
- <command>DELETE</command> queries a <literal>RETURNING</literal> clause
- must be provided, and the target relation must not have a conditional
- rule, nor an <literal>ALSO</literal> rule, nor an
- <literal>INSTEAD</literal> rule that expands to multiple statements.
+ For <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, and <command>MERGE</command> queries a
+ <literal>RETURNING</literal> clause must be provided, and the target
+ relation must not have a conditional rule, nor an
+ <literal>ALSO</literal> rule, nor an <literal>INSTEAD</literal> rule
+ that expands to multiple statements.
</para>
</listitem>
</varlistentry>
diff --git a/doc/src/sgml/ref/merge.sgml b/doc/src/sgml/ref/merge.sgml
new file mode 100644
index e745fbd..71feb66
--- a/doc/src/sgml/ref/merge.sgml
+++ b/doc/src/sgml/ref/merge.sgml
@@ -25,6 +25,7 @@ PostgreSQL documentation
MERGE INTO [ ONLY ] <replaceable class="parameter">target_table_name</replaceable> [ * ] [ [ AS ] <replaceable class="parameter">target_alias</replaceable> ]
USING <replaceable class="parameter">data_source</replaceable> ON <replaceable class="parameter">join_condition</replaceable>
<replaceable class="parameter">when_clause</replaceable> [...]
+[ RETURNING * | <replaceable class="parameter">output_expression</replaceable> [ [ AS ] <replaceable class="parameter">output_name</replaceable> ] [, ...] ]
<phrase>where <replaceable class="parameter">data_source</replaceable> is:</phrase>
@@ -97,6 +98,18 @@ DELETE
</para>
<para>
+ The optional <literal>RETURNING</literal> clause causes <command>MERGE</command>
+ to compute and return value(s) based on each row inserted, updated, or
+ deleted. Any expression using the source or target table's columns, or
+ the <xref linkend="merge_action"/> function can be computed. When an
+ <command>INSERT</command> or <command>UPDATE</command> action is performed,
+ the new values of the target table's columns are used. When a
+ <command>DELETE</command> is performed, the old values of the target table's
+ columns are used. The syntax of the <literal>RETURNING</literal> list is
+ identical to that of the output list of <command>SELECT</command>.
+ </para>
+
+ <para>
There is no separate <literal>MERGE</literal> privilege.
If you specify an update action, you must have the
<literal>UPDATE</literal> privilege on the column(s)
@@ -442,6 +455,36 @@ DELETE
</listitem>
</varlistentry>
+ <varlistentry>
+ <term><replaceable class="parameter">output_expression</replaceable></term>
+ <listitem>
+ <para>
+ An expression to be computed and returned by the <command>MERGE</command>
+ command after each row is changed (whether inserted, updated, or deleted).
+ The expression can use any columns of the source or target tables, or the
+ <xref linkend="merge_action"/> function to return additional information
+ about the action executed.
+ </para>
+ <para>
+ Writing <literal>*</literal> will return all columns from the source
+ table, followed by all columns from the target table. Often this will
+ lead to a lot of duplication, since it is common for the source and
+ target tables to have a lot of the same columns. This can be avoided by
+ qualifying the <literal>*</literal> with the name or alias of the source
+ or target table.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><replaceable class="parameter">output_name</replaceable></term>
+ <listitem>
+ <para>
+ A name to use for a returned column.
+ </para>
+ </listitem>
+ </varlistentry>
+
</variablelist>
</refsect1>
@@ -460,6 +503,13 @@ MERGE <replaceable class="parameter">tot
were changed in any way.
</para>
+ <para>
+ If the <command>MERGE</command> command contains a <literal>RETURNING</literal>
+ clause, the result will be similar to that of a <command>SELECT</command>
+ statement containing the columns and values defined in the
+ <literal>RETURNING</literal> list, computed over the row(s) inserted, updated,
+ or deleted by the command.
+ </para>
</refsect1>
<refsect1>
@@ -580,13 +630,6 @@ MERGE <replaceable class="parameter">tot
</para>
<para>
- There is no <literal>RETURNING</literal> clause with
- <command>MERGE</command>. Actions of <command>INSERT</command>,
- <command>UPDATE</command> and <command>DELETE</command> cannot contain
- <literal>RETURNING</literal> or <literal>WITH</literal> clauses.
- </para>
-
- <para>
When <command>MERGE</command> is run concurrently with other commands
that modify the target table, the usual transaction isolation rules
apply; see <xref linkend="transaction-iso"/> for an explanation
@@ -638,7 +681,8 @@ WHEN NOT MATCHED THEN
<para>
Attempt to insert a new stock item along with the quantity of stock. If
the item already exists, instead update the stock count of the existing
- item. Don't allow entries that have zero stock.
+ item. Don't allow entries that have zero stock. Return details of all
+ changes made.
<programlisting>
MERGE INTO wines w
USING wine_stock_changes s
@@ -648,7 +692,8 @@ WHEN NOT MATCHED AND s.stock_delta > 0 T
WHEN MATCHED AND w.stock + s.stock_delta > 0 THEN
UPDATE SET stock = w.stock + s.stock_delta
WHEN MATCHED THEN
- DELETE;
+ DELETE
+RETURNING merge_action(), w.*;
</programlisting>
The <literal>wine_stock_changes</literal> table might be, for example, a
@@ -663,8 +708,9 @@ WHEN MATCHED THEN
This command conforms to the <acronym>SQL</acronym> standard.
</para>
<para>
- The <literal>WITH</literal> clause and <literal>DO NOTHING</literal>
- action are extensions to the <acronym>SQL</acronym> standard.
+ The <literal>WITH</literal> clause, <literal>DO NOTHING</literal> action,
+ and <literal>RETURNING</literal> clause are extensions to the
+ <acronym>SQL</acronym> standard.
</para>
</refsect1>
</refentry>
diff --git a/doc/src/sgml/ref/select.sgml b/doc/src/sgml/ref/select.sgml
new file mode 100644
index 9917df7..066aed4
--- a/doc/src/sgml/ref/select.sgml
+++ b/doc/src/sgml/ref/select.sgml
@@ -74,7 +74,7 @@ SELECT [ ALL | DISTINCT [ ON ( <replacea
<phrase>and <replaceable class="parameter">with_query</replaceable> is:</phrase>
- <replaceable class="parameter">with_query_name</replaceable> [ ( <replaceable class="parameter">column_name</replaceable> [, ...] ) ] AS [ [ NOT ] MATERIALIZED ] ( <replaceable class="parameter">select</replaceable> | <replaceable class="parameter">values</replaceable> | <replaceable class="parameter">insert</replaceable> | <replaceable class="parameter">update</replaceable> | <replaceable class="parameter">delete</replaceable> )
+ <replaceable class="parameter">with_query_name</replaceable> [ ( <replaceable class="parameter">column_name</replaceable> [, ...] ) ] AS [ [ NOT ] MATERIALIZED ] ( <replaceable class="parameter">select</replaceable> | <replaceable class="parameter">values</replaceable> | <replaceable class="parameter">insert</replaceable> | <replaceable class="parameter">update</replaceable> | <replaceable class="parameter">delete</replaceable> | <replaceable class="parameter">merge</replaceable> )
[ SEARCH { BREADTH | DEPTH } FIRST BY <replaceable>column_name</replaceable> [, ...] SET <replaceable>search_seq_col_name</replaceable> ]
[ CYCLE <replaceable>column_name</replaceable> [, ...] SET <replaceable>cycle_mark_col_name</replaceable> [ TO <replaceable>cycle_mark_value</replaceable> DEFAULT <replaceable>cycle_mark_default</replaceable> ] USING <replaceable>cycle_path_col_name</replaceable> ]
@@ -230,10 +230,10 @@ TABLE [ ONLY ] <replaceable class="param
The subqueries effectively act as temporary tables or views
for the duration of the primary query.
Each subquery can be a <command>SELECT</command>, <command>TABLE</command>, <command>VALUES</command>,
- <command>INSERT</command>, <command>UPDATE</command> or
- <command>DELETE</command> statement.
+ <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command> statement.
When writing a data-modifying statement (<command>INSERT</command>,
- <command>UPDATE</command> or <command>DELETE</command>) in
+ <command>UPDATE</command>, <command>DELETE</command>, or <command>MERGE</command>) in
<literal>WITH</literal>, it is usual to include a <literal>RETURNING</literal> clause.
It is the output of <literal>RETURNING</literal>, <emphasis>not</emphasis> the underlying
table that the statement modifies, that forms the temporary table that is
@@ -2184,7 +2184,8 @@ SELECT 2+2;
<para>
<productname>PostgreSQL</productname> allows <command>INSERT</command>,
- <command>UPDATE</command>, and <command>DELETE</command> to be used as <literal>WITH</literal>
+ <command>UPDATE</command>, <command>DELETE</command>, and
+ <command>MERGE</command> to be used as <literal>WITH</literal>
queries. This is not found in the SQL standard.
</para>
</refsect2>
diff --git a/doc/src/sgml/rowtypes.sgml b/doc/src/sgml/rowtypes.sgml
new file mode 100644
index 4d86f97..bbeac84
--- a/doc/src/sgml/rowtypes.sgml
+++ b/doc/src/sgml/rowtypes.sgml
@@ -348,7 +348,7 @@ SELECT m.* FROM some_table, LATERAL myfu
column expansion of this kind when it appears at the top level of
a <link linkend="queries-select-lists"><command>SELECT</command> output
list</link>, a <link linkend="dml-returning"><literal>RETURNING</literal>
- list</link> in <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>,
+ list</link> in <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command>,
a <link linkend="queries-values"><literal>VALUES</literal> clause</link>, or
a <link linkend="sql-syntax-row-constructors">row constructor</link>.
In all other contexts (including when nested inside one of those
diff --git a/doc/src/sgml/spi.sgml b/doc/src/sgml/spi.sgml
new file mode 100644
index 47f4b5b..bb37786
--- a/doc/src/sgml/spi.sgml
+++ b/doc/src/sgml/spi.sgml
@@ -301,8 +301,9 @@ SPI_execute("INSERT INTO foo SELECT * FR
is returned in the global variable <varname>SPI_processed</varname>.
If the return value of the function is <symbol>SPI_OK_SELECT</symbol>,
<symbol>SPI_OK_INSERT_RETURNING</symbol>,
- <symbol>SPI_OK_DELETE_RETURNING</symbol>, or
- <symbol>SPI_OK_UPDATE_RETURNING</symbol>,
+ <symbol>SPI_OK_DELETE_RETURNING</symbol>,
+ <symbol>SPI_OK_UPDATE_RETURNING</symbol>, or
+ <symbol>SPI_OK_MERGE_RETURNING</symbol>,
then you can use the
global pointer <literal>SPITupleTable *SPI_tuptable</literal> to
access the result rows. Some utility commands (such as
@@ -472,6 +473,15 @@ typedef struct SPITupleTable
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><symbol>SPI_OK_MERGE_RETURNING</symbol></term>
+ <listitem>
+ <para>
+ if a <command>MERGE RETURNING</command> was executed
+ </para>
+ </listitem>
+ </varlistentry>
<varlistentry>
<term><symbol>SPI_OK_UTILITY</symbol></term>
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
new file mode 100644
index 8a79ad0..7d05369
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -177,7 +177,8 @@
statements separated by semicolons. A semicolon after the last
statement is optional. Unless the function is declared to return
<type>void</type>, the last statement must be a <command>SELECT</command>,
- or an <command>INSERT</command>, <command>UPDATE</command>, or <command>DELETE</command>
+ or an <command>INSERT</command>, <command>UPDATE</command>,
+ <command>DELETE</command>, or <command>MERGE</command>
that has a <literal>RETURNING</literal> clause.
</para>
@@ -1236,8 +1237,9 @@ SELECT x, CASE WHEN x > 0 THEN genera
<note>
<para>
- If a function's last command is <command>INSERT</command>, <command>UPDATE</command>,
- or <command>DELETE</command> with <literal>RETURNING</literal>, that command will
+ If a function's last command is <command>INSERT</command>,
+ <command>UPDATE</command>, <command>DELETE</command>, or
+ <command>MERGE</command> with <literal>RETURNING</literal>, that command will
always be executed to completion, even if the function is not declared
with <literal>SETOF</literal> or the calling query does not fetch all the
result rows. Any extra rows produced by the <literal>RETURNING</literal>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
new file mode 100644
index 056b673..28cf8b0
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -281,12 +281,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 a6962e0..ae8b2e3
--- a/src/backend/commands/copyto.c
+++ b/src/backend/commands/copyto.c
@@ -503,7 +503,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 ffd3ca4..728c8d5
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -1107,6 +1107,19 @@ ExecInitExprRec(Expr *node, ExprState *s
break;
}
+ case T_MergeSupportFunc:
+ {
+ /* must be in a MERGE, else something messed up */
+ if (!state->parent ||
+ !IsA(state->parent, ModifyTableState) ||
+ ((ModifyTableState *) state->parent)->operation != CMD_MERGE)
+ elog(ERROR, "MergeSupportFunc found in non-merge plan node");
+
+ scratch.opcode = EEOP_MERGE_SUPPORT_FUNC;
+ ExprEvalPushStep(state, &scratch);
+ break;
+ }
+
case T_SubscriptingRef:
{
SubscriptingRef *sbsref = (SubscriptingRef *) node;
diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
new file mode 100644
index 7c1f51e..a25ab75
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -484,6 +484,7 @@ ExecInterpExpr(ExprState *state, ExprCon
&&CASE_EEOP_AGGREF,
&&CASE_EEOP_GROUPING_FUNC,
&&CASE_EEOP_WINDOW_FUNC,
+ &&CASE_EEOP_MERGE_SUPPORT_FUNC,
&&CASE_EEOP_SUBPLAN,
&&CASE_EEOP_AGG_STRICT_DESERIALIZE,
&&CASE_EEOP_AGG_DESERIALIZE,
@@ -1592,6 +1593,14 @@ ExecInterpExpr(ExprState *state, ExprCon
EEO_NEXT();
}
+ EEO_CASE(EEOP_MERGE_SUPPORT_FUNC)
+ {
+ /* too complex/uncommon for an inline implementation */
+ ExecEvalMergeSupportFunc(state, op, econtext);
+
+ EEO_NEXT();
+ }
+
EEO_CASE(EEOP_SUBPLAN)
{
/* too complex for an inline implementation */
@@ -4246,6 +4255,45 @@ ExecEvalGroupingFunc(ExprState *state, E
}
/*
+ * ExecEvalMergeSupportFunc
+ *
+ * Returns information about the current MERGE action for its RETURNING list.
+ */
+void
+ExecEvalMergeSupportFunc(ExprState *state, ExprEvalStep *op,
+ ExprContext *econtext)
+{
+ ModifyTableState *mtstate = castNode(ModifyTableState, state->parent);
+ MergeActionState *relaction = mtstate->mt_merge_action;
+
+ if (!relaction)
+ elog(ERROR, "no merge action in progress");
+
+ /* Return the MERGE action ("INSERT", "UPDATE", or "DELETE") */
+ switch (relaction->mas_action->commandType)
+ {
+ case CMD_INSERT:
+ *op->resvalue = PointerGetDatum(cstring_to_text_with_len("INSERT", 6));
+ *op->resnull = false;
+ break;
+ case CMD_UPDATE:
+ *op->resvalue = PointerGetDatum(cstring_to_text_with_len("UPDATE", 6));
+ *op->resnull = false;
+ break;
+ case CMD_DELETE:
+ *op->resvalue = PointerGetDatum(cstring_to_text_with_len("DELETE", 6));
+ *op->resnull = false;
+ break;
+ case CMD_NOTHING:
+ elog(ERROR, "unexpected merge action: DO NOTHING");
+ break;
+ default:
+ elog(ERROR, "unrecognized commandType: %d",
+ (int) relaction->mas_action->commandType);
+ }
+}
+
+/*
* Hand off evaluation of a subplan to nodeSubplan.c
*/
void
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
new file mode 100644
index 8ca512d..64fcb01
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -609,8 +609,8 @@ ExecInitPartitionInfo(ModifyTableState *
* Build the RETURNING projection for the partition. Note that we didn't
* build the returningList for partitions within the planner, but simple
* translation of varattnos will suffice. This only occurs for the INSERT
- * case or in the case of UPDATE tuple routing where we didn't find a
- * result rel to reuse.
+ * case or in the case of UPDATE/MERGE tuple routing where we didn't find
+ * a result rel to reuse.
*/
if (node && node->returningLists != NIL)
{
@@ -619,12 +619,14 @@ ExecInitPartitionInfo(ModifyTableState *
List *returningList;
/* See the comment above for WCO lists. */
- /* (except no RETURNING support for MERGE yet) */
Assert((node->operation == CMD_INSERT &&
list_length(node->returningLists) == 1 &&
list_length(node->resultRelations) == 1) ||
(node->operation == CMD_UPDATE &&
list_length(node->returningLists) ==
+ list_length(node->resultRelations)) ||
+ (node->operation == CMD_MERGE &&
+ list_length(node->returningLists) ==
list_length(node->resultRelations)));
/*
diff --git a/src/backend/executor/functions.c b/src/backend/executor/functions.c
new file mode 100644
index 6e926ef..539cd0a
--- a/src/backend/executor/functions.c
+++ b/src/backend/executor/functions.c
@@ -1662,8 +1662,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
@@ -1681,7 +1681,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;
@@ -1695,7 +1696,7 @@ check_sql_fn_retval(List *queryTreeLists
(errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
errmsg("return type mismatch in function declared to return %s",
format_type_be(rettype)),
- errdetail("Function's final statement must be SELECT or INSERT/UPDATE/DELETE RETURNING.")));
+ errdetail("Function's final statement must be SELECT or INSERT/UPDATE/DELETE/MERGE RETURNING.")));
return false; /* keep compiler quiet */
}
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
new file mode 100644
index 9351fbc..4abfe82
--- 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"
@@ -85,9 +84,6 @@ typedef struct ModifyTableContext
*/
TupleTableSlot *planSlot;
- /* MERGE specific */
- MergeActionState *relaction; /* MERGE action in progress */
-
/*
* Information about the changes that were made concurrently to a tuple
* being updated or deleted
@@ -150,14 +146,15 @@ static TupleTableSlot *ExecMerge(ModifyT
HeapTuple oldtuple,
bool canSetTag);
static void ExecInitMerge(ModifyTableState *mtstate, EState *estate);
-static bool ExecMergeMatched(ModifyTableContext *context,
- ResultRelInfo *resultRelInfo,
- ItemPointer tupleid,
- HeapTuple oldtuple,
- bool canSetTag);
-static void ExecMergeNotMatched(ModifyTableContext *context,
- ResultRelInfo *resultRelInfo,
- bool canSetTag);
+static TupleTableSlot *ExecMergeMatched(ModifyTableContext *context,
+ ResultRelInfo *resultRelInfo,
+ ItemPointer tupleid,
+ HeapTuple oldtuple,
+ bool canSetTag,
+ bool *matched);
+static TupleTableSlot *ExecMergeNotMatched(ModifyTableContext *context,
+ ResultRelInfo *resultRelInfo,
+ bool canSetTag);
/*
@@ -977,7 +974,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;
@@ -1831,7 +1828,7 @@ ExecCrossPartitionUpdate(ModifyTableCont
* additional rechecking, and might end up executing a different
* action entirely).
*/
- if (context->relaction != NULL)
+ if (mtstate->operation == CMD_MERGE)
return *tmresult == TM_Ok;
else if (TupIsNull(epqslot))
return true;
@@ -2072,7 +2069,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->operation == CMD_MERGE)
return result;
/*
@@ -2713,6 +2710,7 @@ static TupleTableSlot *
ExecMerge(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
ItemPointer tupleid, HeapTuple oldtuple, bool canSetTag)
{
+ TupleTableSlot *rslot = NULL;
bool matched;
/*-----
@@ -2761,19 +2759,18 @@ ExecMerge(ModifyTableContext *context, R
*/
matched = tupleid != NULL || oldtuple != NULL;
if (matched)
- matched = ExecMergeMatched(context, resultRelInfo, tupleid, oldtuple,
- canSetTag);
+ rslot = ExecMergeMatched(context, resultRelInfo, tupleid, oldtuple,
+ canSetTag, &matched);
/*
- * Either we were dealing with a NOT MATCHED tuple or ExecMergeMatched()
- * returned "false", indicating the previously MATCHED tuple no longer
- * matches.
+ * Deal with the NOT MATCHED case (either a NOT MATCHED tuple from the
+ * join, or a previously MATCHED tuple for which ExecMergeMatched() set
+ * "matched" to false, indicating that it no longer matches).
*/
if (!matched)
- ExecMergeNotMatched(context, resultRelInfo, canSetTag);
+ rslot = ExecMergeNotMatched(context, resultRelInfo, canSetTag);
- /* No RETURNING support yet */
- return NULL;
+ return rslot;
}
/*
@@ -2785,8 +2782,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.
*
@@ -2795,16 +2792,18 @@ 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, HeapTuple oldtuple, bool canSetTag)
+ ItemPointer tupleid, HeapTuple oldtuple, bool canSetTag,
+ bool *matched)
{
ModifyTableState *mtstate = context->mtstate;
- TupleTableSlot *newslot;
+ TupleTableSlot *newslot = NULL;
+ TupleTableSlot *rslot = NULL;
EState *estate = context->estate;
ExprContext *econtext = mtstate->ps.ps_ExprContext;
bool isNull;
@@ -2815,7 +2814,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
@@ -2905,12 +2907,15 @@ lmerge_matched:
*/
newslot = ExecProject(relaction->mas_proj);
- context->relaction = relaction;
+ mtstate->mt_merge_action = relaction;
if (!ExecUpdatePrologue(context, resultRelInfo,
tupleid, NULL, newslot, &result))
{
if (result == TM_Ok)
- return true; /* "do nothing" */
+ {
+ *matched = true;
+ return NULL; /* "do nothing" */
+ }
break; /* concurrent update/delete */
}
@@ -2920,7 +2925,10 @@ lmerge_matched:
{
if (!ExecIRUpdateTriggers(estate, resultRelInfo,
oldtuple, newslot))
- return true; /* "do nothing" */
+ {
+ *matched = true;
+ return NULL; /* "do nothing" */
+ }
}
else
{
@@ -2933,12 +2941,15 @@ lmerge_matched:
* cross-partition update was done, then there's nothing
* else for us to do --- the UPDATE has been turned into a
* DELETE and an INSERT, and we must not perform any of
- * the usual post-update tasks.
+ * the usual post-update tasks. Also, the RETURNING tuple
+ * (if any) has been projected, so we can just return
+ * that.
*/
if (updateCxt.crossPartUpdate)
{
mtstate->mt_merge_updated += 1;
- return true;
+ *matched = true;
+ return context->cpUpdateReturningSlot;
}
}
@@ -2951,12 +2962,15 @@ lmerge_matched:
break;
case CMD_DELETE:
- context->relaction = relaction;
+ mtstate->mt_merge_action = relaction;
if (!ExecDeletePrologue(context, resultRelInfo, tupleid,
NULL, NULL, &result))
{
if (result == TM_Ok)
- return true; /* "do nothing" */
+ {
+ *matched = true;
+ return NULL; /* "do nothing" */
+ }
break; /* concurrent update/delete */
}
@@ -2966,7 +2980,10 @@ lmerge_matched:
{
if (!ExecIRDeleteTriggers(estate, resultRelInfo,
oldtuple))
- return true; /* "do nothing" */
+ {
+ *matched = true;
+ return NULL; /* "do nothing" */
+ }
}
else
result = ExecDeleteAct(context, resultRelInfo, tupleid,
@@ -3046,7 +3063,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:
{
@@ -3092,13 +3110,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
@@ -3133,7 +3157,8 @@ lmerge_matched:
* tuple already deleted; tell caller to run NOT
* MATCHED actions
*/
- return false;
+ *matched = false;
+ return NULL;
case TM_SelfModified:
@@ -3161,13 +3186,13 @@ lmerge_matched:
/* This shouldn't happen */
elog(ERROR, "attempted to update or delete invisible tuple");
- return false;
+ return NULL;
default:
/* see table_tuple_lock call in ExecDelete() */
elog(ERROR, "unexpected table_tuple_lock status: %u",
result);
- return false;
+ return NULL;
}
}
@@ -3179,6 +3204,31 @@ 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, "unrecognized commandType: %d",
+ (int) commandType);
+ }
+ }
+
/*
* We've activated one of the WHEN clauses, so we don't search
* further. This is required behaviour, not an optimization.
@@ -3189,19 +3239,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;
/*
@@ -3251,10 +3304,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:
@@ -3270,6 +3323,8 @@ ExecMergeNotMatched(ModifyTableContext *
*/
break;
}
+
+ return rslot;
}
/*
@@ -3732,9 +3787,17 @@ ExecModifyTable(PlanState *pstate)
{
EvalPlanQualSetSlot(&node->mt_epqstate, context.planSlot);
- ExecMerge(&context, node->resultRelInfo, NULL, NULL,
- node->canSetTag);
- continue; /* no RETURNING support yet */
+ slot = ExecMerge(&context, node->resultRelInfo,
+ NULL, 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");
@@ -3811,9 +3874,17 @@ ExecModifyTable(PlanState *pstate)
{
EvalPlanQualSetSlot(&node->mt_epqstate, context.planSlot);
- ExecMerge(&context, node->resultRelInfo, NULL, NULL,
- node->canSetTag);
- continue; /* no RETURNING support yet */
+ slot = ExecMerge(&context, node->resultRelInfo,
+ NULL, 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");
@@ -3860,9 +3931,17 @@ ExecModifyTable(PlanState *pstate)
{
EvalPlanQualSetSlot(&node->mt_epqstate, context.planSlot);
- ExecMerge(&context, node->resultRelInfo, NULL, NULL,
- node->canSetTag);
- continue; /* no RETURNING support yet */
+ slot = ExecMerge(&context, node->resultRelInfo,
+ NULL, 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, "wholerow is NULL");
@@ -3924,7 +4003,6 @@ ExecModifyTable(PlanState *pstate)
}
slot = ExecGetUpdateNewTuple(resultRelInfo, context.planSlot,
oldSlot);
- context.relaction = NULL;
/* Now apply the update. */
slot = ExecUpdate(&context, resultRelInfo, tupleid, oldtuple,
diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c
new file mode 100644
index 8585771..a97a7e3
--- a/src/backend/executor/spi.c
+++ b/src/backend/executor/spi.c
@@ -2032,6 +2032,8 @@ SPI_result_code_string(int code)
return "SPI_OK_TD_REGISTER";
case SPI_OK_MERGE:
return "SPI_OK_MERGE";
+ case SPI_OK_MERGE_RETURNING:
+ return "SPI_OK_MERGE_RETURNING";
}
/* Unrecognized code ... return something useful ... */
sprintf(buf, "Unrecognized SPI code %d", code);
@@ -2885,7 +2887,10 @@ _SPI_pquery(QueryDesc *queryDesc, bool f
res = SPI_OK_UPDATE;
break;
case CMD_MERGE:
- res = SPI_OK_MERGE;
+ if (queryDesc->plannedstmt->hasReturning)
+ res = SPI_OK_MERGE_RETURNING;
+ else
+ res = SPI_OK_MERGE;
break;
default:
return SPI_ERROR_OPUNKNOWN;
diff --git a/src/backend/jit/llvm/llvmjit_expr.c b/src/backend/jit/llvm/llvmjit_expr.c
new file mode 100644
index 0c44842..2a7d84f
--- a/src/backend/jit/llvm/llvmjit_expr.c
+++ b/src/backend/jit/llvm/llvmjit_expr.c
@@ -1986,6 +1986,12 @@ llvm_compile_expr(ExprState *state)
break;
}
+ case EEOP_MERGE_SUPPORT_FUNC:
+ build_EvalXFunc(b, mod, "ExecEvalMergeSupportFunc",
+ v_state, op, v_econtext);
+ LLVMBuildBr(b, opblocks[opno + 1]);
+ break;
+
case EEOP_SUBPLAN:
build_EvalXFunc(b, mod, "ExecEvalSubPlan",
v_state, op, v_econtext);
diff --git a/src/backend/jit/llvm/llvmjit_types.c b/src/backend/jit/llvm/llvmjit_types.c
new file mode 100644
index 47c9daf..7d7aeee
--- a/src/backend/jit/llvm/llvmjit_types.c
+++ b/src/backend/jit/llvm/llvmjit_types.c
@@ -155,6 +155,7 @@ void *referenced_functions[] =
ExecEvalFuncExprFusage,
ExecEvalFuncExprStrictFusage,
ExecEvalGroupingFunc,
+ ExecEvalMergeSupportFunc,
ExecEvalMinMax,
ExecEvalNextValueExpr,
ExecEvalParamExec,
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
new file mode 100644
index 6ba8e73..5b70280
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -66,6 +66,9 @@ exprType(const Node *expr)
case T_WindowFunc:
type = ((const WindowFunc *) expr)->wintype;
break;
+ case T_MergeSupportFunc:
+ type = ((const MergeSupportFunc *) expr)->msftype;
+ break;
case T_SubscriptingRef:
type = ((const SubscriptingRef *) expr)->refrestype;
break;
@@ -809,6 +812,9 @@ exprCollation(const Node *expr)
case T_WindowFunc:
coll = ((const WindowFunc *) expr)->wincollid;
break;
+ case T_MergeSupportFunc:
+ coll = ((const MergeSupportFunc *) expr)->msfcollid;
+ break;
case T_SubscriptingRef:
coll = ((const SubscriptingRef *) expr)->refcollid;
break;
@@ -1084,6 +1090,9 @@ exprSetCollation(Node *expr, Oid collati
case T_WindowFunc:
((WindowFunc *) expr)->wincollid = collation;
break;
+ case T_MergeSupportFunc:
+ ((MergeSupportFunc *) expr)->msfcollid = collation;
+ break;
case T_SubscriptingRef:
((SubscriptingRef *) expr)->refcollid = collation;
break;
@@ -1342,6 +1351,9 @@ exprLocation(const Node *expr)
/* function name should always be the first thing */
loc = ((const WindowFunc *) expr)->location;
break;
+ case T_MergeSupportFunc:
+ loc = ((const MergeSupportFunc *) expr)->location;
+ break;
case T_SubscriptingRef:
/* just use container argument's location */
loc = exprLocation((Node *) ((const SubscriptingRef *) expr)->refexpr);
@@ -2034,6 +2046,7 @@ expression_tree_walker_impl(Node *node,
case T_RangeTblRef:
case T_SortGroupClause:
case T_CTESearchClause:
+ case T_MergeSupportFunc:
/* primitive node types with no expression subnodes */
break;
case T_WithCheckOption:
@@ -2868,6 +2881,7 @@ expression_tree_mutator_impl(Node *node,
case T_RangeTblRef:
case T_SortGroupClause:
case T_CTESearchClause:
+ case T_MergeSupportFunc:
return (Node *) copyObject(node);
case T_WithCheckOption:
{
@@ -3832,6 +3846,7 @@ raw_expression_tree_walker_impl(Node *no
case T_ParamRef:
case T_A_Const:
case T_A_Star:
+ case T_MergeSupportFunc:
/* primitive node types with no subnodes */
break;
case T_Alias:
@@ -4052,6 +4067,8 @@ raw_expression_tree_walker_impl(Node *no
return true;
if (WALK(stmt->mergeWhenClauses))
return true;
+ if (WALK(stmt->returningList))
+ return true;
if (WALK(stmt->withClause))
return true;
}
diff --git a/src/backend/optimizer/plan/subselect.c b/src/backend/optimizer/plan/subselect.c
new file mode 100644
index 47e1472..b9e0c96
--- a/src/backend/optimizer/plan/subselect.c
+++ b/src/backend/optimizer/plan/subselect.c
@@ -1855,7 +1855,8 @@ convert_EXISTS_to_ANY(PlannerInfo *root,
/*
* Replace correlation vars (uplevel vars) with Params.
*
- * Uplevel PlaceHolderVars and aggregates are replaced, too.
+ * Uplevel PlaceHolderVars, aggregates, GROUPING() expressions, and
+ * MergeSupportFuncs are replaced, too.
*
* Note: it is critical that this runs immediately after SS_process_sublinks.
* Since we do not recurse into the arguments of uplevel PHVs and aggregates,
@@ -1909,6 +1910,12 @@ replace_correlation_vars_mutator(Node *n
if (((GroupingFunc *) node)->agglevelsup > 0)
return (Node *) replace_outer_grouping(root, (GroupingFunc *) node);
}
+ if (IsA(node, MergeSupportFunc))
+ {
+ if (root->parse->commandType != CMD_MERGE)
+ return (Node *) replace_outer_merge_support(root,
+ (MergeSupportFunc *) node);
+ }
return expression_tree_mutator(node,
replace_correlation_vars_mutator,
(void *) root);
diff --git a/src/backend/optimizer/util/paramassign.c b/src/backend/optimizer/util/paramassign.c
new file mode 100644
index a58da7c..f461fed
--- a/src/backend/optimizer/util/paramassign.c
+++ b/src/backend/optimizer/util/paramassign.c
@@ -308,6 +308,57 @@ replace_outer_grouping(PlannerInfo *root
}
/*
+ * Generate a Param node to replace the given MergeSupportFunc expression
+ * which is expected to be in the RETURNING list of an upper-level MERGE
+ * query. Record the need for the MergeSupportFunc in the proper upper-level
+ * root->plan_params.
+ */
+Param *
+replace_outer_merge_support(PlannerInfo *root, MergeSupportFunc *msf)
+{
+ Param *retval;
+ PlannerParamItem *pitem;
+ Oid ptype = exprType((Node *) msf);
+
+ Assert(root->parse->commandType != CMD_MERGE);
+
+ /*
+ * The parser should have ensured that the MergeSupportFunc is in the
+ * RETURNING list of an upper-level MERGE query, so find that query.
+ */
+ do
+ {
+ root = root->parent_root;
+ if (root == NULL)
+ elog(ERROR, "MergeSupportFunc found outside MERGE");
+ } while (root->parse->commandType != CMD_MERGE);
+
+ /*
+ * It does not seem worthwhile to try to de-duplicate references to outer
+ * MergeSupportFunc expressions. Just make a new slot every time.
+ */
+ msf = copyObject(msf);
+
+ pitem = makeNode(PlannerParamItem);
+ pitem->item = (Node *) msf;
+ pitem->paramId = list_length(root->glob->paramExecTypes);
+ root->glob->paramExecTypes = lappend_oid(root->glob->paramExecTypes,
+ ptype);
+
+ root->plan_params = lappend(root->plan_params, pitem);
+
+ retval = makeNode(Param);
+ retval->paramkind = PARAM_EXEC;
+ retval->paramid = pitem->paramId;
+ retval->paramtype = ptype;
+ retval->paramtypmod = -1;
+ retval->paramcollid = InvalidOid;
+ retval->location = msf->location;
+
+ return retval;
+}
+
+/*
* Generate a Param node to replace the given Var,
* which is expected to come from some upper NestLoop plan node.
* Record the need for the Var in root->curOuterParams.
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
new file mode 100644
index 7f23d18..40ea19e
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -72,7 +72,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,
@@ -551,7 +550,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;
@@ -978,7 +978,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;
@@ -2454,7 +2455,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
@@ -2551,10 +2553,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;
@@ -2571,7 +2574,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 c6e2f67..6419b32
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -733,7 +733,7 @@ static Node *makeRecursiveViewSelect(cha
LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL
LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOGGED
- MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE METHOD
+ MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE MERGE_ACTION METHOD
MINUTE_P MINVALUE MODE MONTH_P MOVE
NAME_P NAMES NATIONAL NATURAL NCHAR NEW NEXT NFC NFD NFKC NFKD NO NONE
@@ -12374,6 +12374,7 @@ MergeStmt:
USING table_ref
ON a_expr
merge_when_list
+ returning_clause
{
MergeStmt *m = makeNode(MergeStmt);
@@ -12382,6 +12383,7 @@ MergeStmt:
m->sourceRelation = $6;
m->joinCondition = $8;
m->mergeWhenClauses = $9;
+ m->returningList = $10;
$$ = (Node *) m;
}
@@ -15795,6 +15797,14 @@ func_expr_common_subexpr:
n->location = @1;
$$ = (Node *) n;
}
+ | MERGE_ACTION '(' ')'
+ {
+ MergeSupportFunc *m = makeNode(MergeSupportFunc);
+
+ m->msftype = TEXTOID;
+ m->location = @1;
+ $$ = (Node *) m;
+ }
;
@@ -17492,6 +17502,7 @@ col_name_keyword:
| JSON_SCALAR
| JSON_SERIALIZE
| LEAST
+ | MERGE_ACTION
| NATIONAL
| NCHAR
| NONE
@@ -17881,6 +17892,7 @@ bare_label_keyword:
| MATERIALIZED
| MAXVALUE
| MERGE
+ | MERGE_ACTION
| METHOD
| MINVALUE
| MODE
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
new file mode 100644
index 9d151a8..bee7d83
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -468,6 +468,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:
@@ -915,6 +916,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 3c88c9a..6826d4f
--- 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 9300c7b..d44b1f2
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -54,6 +54,7 @@ static Node *transformAExprDistinct(Pars
static Node *transformAExprNullIf(ParseState *pstate, A_Expr *a);
static Node *transformAExprIn(ParseState *pstate, A_Expr *a);
static Node *transformAExprBetween(ParseState *pstate, A_Expr *a);
+static Node *transformMergeSupportFunc(ParseState *pstate, MergeSupportFunc *f);
static Node *transformBoolExpr(ParseState *pstate, BoolExpr *a);
static Node *transformFuncCall(ParseState *pstate, FuncCall *fn);
static Node *transformMultiAssignRef(ParseState *pstate, MultiAssignRef *maref);
@@ -227,6 +228,11 @@ transformExprRecurse(ParseState *pstate,
result = transformGroupingFunc(pstate, (GroupingFunc *) expr);
break;
+ case T_MergeSupportFunc:
+ result = transformMergeSupportFunc(pstate,
+ (MergeSupportFunc *) expr);
+ break;
+
case T_NamedArgExpr:
{
NamedArgExpr *na = (NamedArgExpr *) expr;
@@ -541,6 +547,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:
@@ -1354,6 +1361,31 @@ transformAExprBetween(ParseState *pstate
}
static Node *
+transformMergeSupportFunc(ParseState *pstate, MergeSupportFunc *f)
+{
+ /*
+ * All we need to do is check that we're in the RETURNING list of a MERGE
+ * command. If so, we just return the node as-is.
+ */
+ if (pstate->p_expr_kind != EXPR_KIND_MERGE_RETURNING)
+ {
+ ParseState *parent_pstate = pstate->parentParseState;
+
+ while (parent_pstate &&
+ parent_pstate->p_expr_kind != EXPR_KIND_MERGE_RETURNING)
+ parent_pstate = parent_pstate->parentParseState;
+
+ if (!parent_pstate)
+ ereport(ERROR,
+ errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("MERGE_ACTION() can only be used in the RETURNING list of a MERGE command"),
+ parser_errposition(pstate, f->location));
+ }
+
+ return (Node *) f;
+}
+
+static Node *
transformBoolExpr(ParseState *pstate, BoolExpr *a)
{
List *args = NIL;
@@ -1767,6 +1799,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:
@@ -3115,6 +3148,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 fdb3e6d..0cbc950
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -2599,6 +2599,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 a7d8ba7..04ed5e6
--- a/src/backend/parser/parse_merge.c
+++ b/src/backend/parser/parse_merge.c
@@ -234,6 +234,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.
@@ -391,9 +395,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 6f5d9e2..427b732
--- a/src/backend/parser/parse_relation.c
+++ b/src/backend/parser/parse_relation.c
@@ -2341,9 +2341,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/parser/parse_target.c b/src/backend/parser/parse_target.c
new file mode 100644
index 5b92502..ea522b9
--- a/src/backend/parser/parse_target.c
+++ b/src/backend/parser/parse_target.c
@@ -1820,6 +1820,10 @@ FigureColnameInternal(Node *node, char *
/* make GROUPING() act like a regular function */
*name = "grouping";
return 2;
+ case T_MergeSupportFunc:
+ /* make MERGE_ACTION() act like a regular function */
+ *name = "merge_action";
+ return 2;
case T_SubLink:
switch (((SubLink *) node)->subLinkType)
{
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
new file mode 100644
index 7a46e8b..9fd05b1
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -3833,9 +3833,9 @@ RewriteQuery(Query *parsetree, List *rew
ListCell *lc1;
/*
- * First, recursively process any insert/update/delete statements in WITH
- * clauses. (We have to do this first because the WITH clauses may get
- * copied into rule actions below.)
+ * First, recursively process any insert/update/delete/merge statements in
+ * WITH clauses. (We have to do this first because the WITH clauses may
+ * get copied into rule actions below.)
*/
foreach(lc1, parsetree->cteList)
{
@@ -3860,7 +3860,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/rewrite/rowsecurity.c b/src/backend/rewrite/rowsecurity.c
new file mode 100644
index 450af27..59fd305
--- a/src/backend/rewrite/rowsecurity.c
+++ b/src/backend/rewrite/rowsecurity.c
@@ -384,10 +384,10 @@ get_row_security_policies(Query *root, R
* on the final action we take.
*
* We already fetched the SELECT policies above, to check existing rows,
- * but we must also check that new rows created by UPDATE actions are
- * visible, if SELECT rights are required for this relation. We don't do
- * this for INSERT actions, since an INSERT command would only do this
- * check if it had a RETURNING list, and MERGE does not support RETURNING.
+ * but we must also check that new rows created by INSERT/UPDATE actions
+ * are visible, if SELECT rights are required. For INSERT actions, we only
+ * do this if RETURNING is specified, to be consistent with a plain INSERT
+ * command, which can only require SELECT rights when RETURNING is used.
*
* 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
@@ -409,6 +409,8 @@ get_row_security_policies(Query *root, R
List *merge_delete_restrictive_policies;
List *merge_insert_permissive_policies;
List *merge_insert_restrictive_policies;
+ List *merge_select_permissive_policies = NIL;
+ List *merge_select_restrictive_policies = NIL;
/*
* Fetch the UPDATE policies and set them up to execute on the
@@ -446,9 +448,6 @@ get_row_security_policies(Query *root, R
*/
if (perminfo->requiredPerms & ACL_SELECT)
{
- List *merge_select_permissive_policies;
- List *merge_select_restrictive_policies;
-
get_policies_for_relation(rel, CMD_SELECT, user_id,
&merge_select_permissive_policies,
&merge_select_restrictive_policies);
@@ -497,6 +496,21 @@ get_row_security_policies(Query *root, R
withCheckOptions,
hasSubLinks,
false);
+
+ /*
+ * Add ALL/SELECT policies as WCO_RLS_INSERT_CHECK WCOs, to ensure
+ * that the inserted row is visible when executing an INSERT action,
+ * if RETURNING is specified and SELECT rights are required for this
+ * relation.
+ */
+ if (perminfo->requiredPerms & ACL_SELECT && root->returningList)
+ add_with_check_options(rel, rt_index,
+ WCO_RLS_INSERT_CHECK,
+ merge_select_permissive_policies,
+ merge_select_restrictive_policies,
+ withCheckOptions,
+ hasSubLinks,
+ true);
}
table_close(rel, NoLock);
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
new file mode 100644
index 83f86a4..fa66b80
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -2138,11 +2138,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/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
new file mode 100644
index 2a1ee69..e2b46b6
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -7196,8 +7196,13 @@ get_merge_query_def(Query *query, depars
appendStringInfoString(buf, "DO NOTHING");
}
- /* No RETURNING support in MERGE yet */
- Assert(query->returningList == NIL);
+ /* Add RETURNING if present */
+ if (query->returningList)
+ {
+ appendContextKeyword(context, " RETURNING",
+ -PRETTYINDENT_STD, PRETTYINDENT_STD, 1);
+ get_target_list(query->returningList, context, NULL, colNamesVisible);
+ }
}
@@ -8297,6 +8302,7 @@ isSimpleNode(Node *node, Node *parentNod
case T_Aggref:
case T_GroupingFunc:
case T_WindowFunc:
+ case T_MergeSupportFunc:
case T_FuncExpr:
case T_JsonConstructorExpr:
/* function-like: name(..) or name[..] */
@@ -8651,6 +8657,10 @@ get_rule_expr(Node *node, deparse_contex
get_windowfunc_expr((WindowFunc *) node, context);
break;
+ case T_MergeSupportFunc:
+ appendStringInfoString(buf, "MERGE_ACTION()");
+ break;
+
case T_SubscriptingRef:
{
SubscriptingRef *sbsref = (SubscriptingRef *) node;
diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c
new file mode 100644
index 76e01b0..2830bde
--- a/src/bin/psql/common.c
+++ b/src/bin/psql/common.c
@@ -983,13 +983,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/executor/execExpr.h b/src/include/executor/execExpr.h
new file mode 100644
index a28ddcd..8953d76
--- a/src/include/executor/execExpr.h
+++ b/src/include/executor/execExpr.h
@@ -243,6 +243,7 @@ typedef enum ExprEvalOp
EEOP_AGGREF,
EEOP_GROUPING_FUNC,
EEOP_WINDOW_FUNC,
+ EEOP_MERGE_SUPPORT_FUNC,
EEOP_SUBPLAN,
/* aggregation related nodes */
@@ -810,6 +811,8 @@ extern void ExecEvalJsonConstructor(Expr
ExprContext *econtext);
extern void ExecEvalJsonIsPredicate(ExprState *state, ExprEvalStep *op);
extern void ExecEvalGroupingFunc(ExprState *state, ExprEvalStep *op);
+extern void ExecEvalMergeSupportFunc(ExprState *state, ExprEvalStep *op,
+ ExprContext *econtext);
extern void ExecEvalSubPlan(ExprState *state, ExprEvalStep *op,
ExprContext *econtext);
extern void ExecEvalWholeRowVar(ExprState *state, ExprEvalStep *op,
diff --git a/src/include/executor/spi.h b/src/include/executor/spi.h
new file mode 100644
index c13e1f6..48b8773
--- a/src/include/executor/spi.h
+++ b/src/include/executor/spi.h
@@ -97,6 +97,7 @@ typedef struct _SPI_plan *SPIPlanPtr;
#define SPI_OK_REL_UNREGISTER 16
#define SPI_OK_TD_REGISTER 17
#define SPI_OK_MERGE 18
+#define SPI_OK_MERGE_RETURNING 19
#define SPI_OPT_NONATOMIC (1 << 0)
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
new file mode 100644
index 27614ab..9259352
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1325,6 +1325,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 aadaf67..2c648a6
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -1939,6 +1939,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/nodes/primnodes.h b/src/include/nodes/primnodes.h
new file mode 100644
index 4a15460..8df8884
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -572,6 +572,27 @@ typedef struct WindowFunc
} WindowFunc;
/*
+ * MergeSupportFunc
+ *
+ * A MergeSupportFunc is a merge support function expression that can only
+ * appear in the RETURNING list of a MERGE command. It returns information
+ * about the currently executing merge action.
+ *
+ * Currently, the only supported function is MERGE_ACTION(), which returns the
+ * command executed ("INSERT", "UPDATE", or "DELETE").
+ */
+typedef struct MergeSupportFunc
+{
+ Expr xpr;
+ /* type Oid of result */
+ Oid msftype;
+ /* OID of collation, or InvalidOid if none */
+ Oid msfcollid;
+ /* token location, or -1 if unknown */
+ int location;
+} MergeSupportFunc;
+
+/*
* SubscriptingRef: describes a subscripting operation over a container
* (array, etc).
*
diff --git a/src/include/optimizer/paramassign.h b/src/include/optimizer/paramassign.h
new file mode 100644
index 501f728..4026b74
--- a/src/include/optimizer/paramassign.h
+++ b/src/include/optimizer/paramassign.h
@@ -20,6 +20,8 @@ extern Param *replace_outer_placeholderv
PlaceHolderVar *phv);
extern Param *replace_outer_agg(PlannerInfo *root, Aggref *agg);
extern Param *replace_outer_grouping(PlannerInfo *root, GroupingFunc *grp);
+extern Param *replace_outer_merge_support(PlannerInfo *root,
+ MergeSupportFunc *msf);
extern Param *replace_nestloop_param_var(PlannerInfo *root, Var *var);
extern Param *replace_nestloop_param_placeholdervar(PlannerInfo *root,
PlaceHolderVar *phv);
diff --git a/src/include/parser/analyze.h b/src/include/parser/analyze.h
new file mode 100644
index 5ecdd10..28b66fc
--- 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/kwlist.h b/src/include/parser/kwlist.h
new file mode 100644
index 2331aca..0993534
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -266,6 +266,7 @@ PG_KEYWORD("matched", MATCHED, UNRESERVE
PG_KEYWORD("materialized", MATERIALIZED, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("maxvalue", MAXVALUE, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("merge", MERGE, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("merge_action", MERGE_ACTION, COL_NAME_KEYWORD, BARE_LABEL)
PG_KEYWORD("method", METHOD, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("minute", MINUTE_P, UNRESERVED_KEYWORD, AS_LABEL)
PG_KEYWORD("minvalue", MINVALUE, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
new file mode 100644
index 99d6515..5b781d8
--- 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/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
new file mode 100644
index ed51694..6947575
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -4267,9 +4267,9 @@ exec_stmt_execsql(PLpgSQL_execstate *est
/*
* If we have INTO, then we only need one row back ... but if we have INTO
* STRICT or extra check too_many_rows, ask for two rows, so that we can
- * verify the statement returns only one. INSERT/UPDATE/DELETE are always
- * treated strictly. Without INTO, just run the statement to completion
- * (tcount = 0).
+ * verify the statement returns only one. INSERT/UPDATE/DELETE/MERGE are
+ * always treated strictly. Without INTO, just run the statement to
+ * completion (tcount = 0).
*
* We could just ask for two rows always when using INTO, but there are
* some cases where demanding the extra row costs significant time, eg by
@@ -4307,10 +4307,11 @@ exec_stmt_execsql(PLpgSQL_execstate *est
case SPI_OK_INSERT:
case SPI_OK_UPDATE:
case SPI_OK_DELETE:
+ case SPI_OK_MERGE:
case SPI_OK_INSERT_RETURNING:
case SPI_OK_UPDATE_RETURNING:
case SPI_OK_DELETE_RETURNING:
- case SPI_OK_MERGE:
+ case SPI_OK_MERGE_RETURNING:
Assert(stmt->mod_stmt);
exec_set_found(estate, (SPI_processed != 0));
break;
@@ -4489,10 +4490,11 @@ exec_stmt_dynexecute(PLpgSQL_execstate *
case SPI_OK_INSERT:
case SPI_OK_UPDATE:
case SPI_OK_DELETE:
+ case SPI_OK_MERGE:
case SPI_OK_INSERT_RETURNING:
case SPI_OK_UPDATE_RETURNING:
case SPI_OK_DELETE_RETURNING:
- case SPI_OK_MERGE:
+ case SPI_OK_MERGE_RETURNING:
case SPI_OK_UTILITY:
case SPI_OK_REWRITTEN:
break;
diff --git a/src/pl/tcl/pltcl.c b/src/pl/tcl/pltcl.c
new file mode 100644
index 6187e15..18d14a2
--- a/src/pl/tcl/pltcl.c
+++ b/src/pl/tcl/pltcl.c
@@ -2456,6 +2456,7 @@ pltcl_process_SPI_result(Tcl_Interp *int
case SPI_OK_INSERT_RETURNING:
case SPI_OK_DELETE_RETURNING:
case SPI_OK_UPDATE_RETURNING:
+ case SPI_OK_MERGE_RETURNING:
/*
* Process the tuples we got
diff --git a/src/test/regress/expected/merge.out b/src/test/regress/expected/merge.out
new file mode 100644
index e3ebf46..f9f6c2a
--- a/src/test/regress/expected/merge.out
+++ b/src/test/regress/expected/merge.out
@@ -124,20 +124,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
-- materialized view
CREATE MATERIALIZED VIEW mv AS SELECT * FROM target;
@@ -1255,7 +1255,7 @@ BEGIN;
MERGE INTO sq_target
USING v
ON tid = sid
-WHEN MATCHED AND tid > 2 THEN
+WHEN MATCHED AND tid >= 2 THEN
UPDATE SET balance = balance + delta
WHEN NOT MATCHED THEN
INSERT (balance, tid) VALUES (balance + delta, sid)
@@ -1270,7 +1270,7 @@ INSERT INTO sq_source (sid, balance, del
MERGE INTO sq_target t
USING v
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)
@@ -1294,7 +1294,7 @@ WITH targq AS (
MERGE INTO sq_target t
USING v
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)
@@ -1302,21 +1302,207 @@ WHEN MATCHED AND tid < 2 THEN
DELETE;
ROLLBACK;
-- RETURNING
+SELECT * FROM sq_source ORDER BY sid;
+ delta | sid | balance
+-------+-----+---------
+ 10 | 1 | 0
+ 20 | 2 | 0
+ 40 | 4 | 0
+(3 rows)
+
+SELECT * FROM sq_target ORDER BY tid;
+ tid | balance
+-----+---------
+ 1 | 100
+ 2 | 200
+ 3 | 300
+(3 rows)
+
BEGIN;
-INSERT INTO sq_source (sid, balance, delta) VALUES (-1, -1, -10);
+CREATE TABLE merge_actions(action text, abbrev text);
+INSERT INTO merge_actions VALUES ('INSERT', 'ins'), ('UPDATE', 'upd'), ('DELETE', 'del');
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)
+ 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 *;
- ^
+ DELETE
+RETURNING (SELECT abbrev FROM merge_actions
+ WHERE action = merge_action()) AS action,
+ t.*,
+ CASE merge_action()
+ WHEN 'INSERT' THEN 'Inserted '||t
+ WHEN 'UPDATE' THEN 'Added '||delta||' to balance'
+ WHEN 'DELETE' THEN 'Removed '||t
+ END AS description;
+ action | tid | balance | description
+--------+-----+---------+---------------------
+ del | 1 | 100 | Removed (1,100)
+ upd | 2 | 220 | Added 20 to balance
+ ins | 4 | 40 | Inserted (4,40)
+(3 rows)
+
+ROLLBACK;
+-- error when using merge_action() outside MERGE
+SELECT merge_action() FROM sq_target;
+ERROR: MERGE_ACTION() can only be used in the RETURNING list of a MERGE command
+LINE 1: SELECT merge_action() FROM sq_target;
+ ^
+UPDATE sq_target SET balance = balance + 1 RETURNING merge_action();
+ERROR: MERGE_ACTION() can only be used in the RETURNING list of a MERGE command
+LINE 1: ...ATE sq_target SET balance = balance + 1 RETURNING merge_acti...
+ ^
+-- RETURNING in CTEs
+CREATE TABLE sq_target_merge_log (tid integer NOT NULL, last_change text);
+INSERT INTO sq_target_merge_log VALUES (1, 'Original value');
+BEGIN;
+WITH m AS (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 merge_action() AS action, t.*,
+ CASE merge_action()
+ WHEN 'INSERT' THEN 'Inserted '||t
+ WHEN 'UPDATE' THEN 'Added '||delta||' to balance'
+ WHEN 'DELETE' THEN 'Removed '||t
+ END AS description
+), m2 AS (
+ MERGE INTO sq_target_merge_log l
+ USING m
+ ON l.tid = m.tid
+ WHEN MATCHED THEN
+ UPDATE SET last_change = description
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (m.tid, description)
+ RETURNING action, merge_action() AS log_action, l.*
+)
+SELECT * FROM m2;
+ action | log_action | tid | last_change
+--------+------------+-----+---------------------
+ DELETE | UPDATE | 1 | Removed (1,100)
+ UPDATE | INSERT | 2 | Added 20 to balance
+ INSERT | INSERT | 4 | Inserted (4,40)
+(3 rows)
+
+SELECT * FROM sq_target_merge_log ORDER BY tid;
+ tid | last_change
+-----+---------------------
+ 1 | Removed (1,100)
+ 2 | Added 20 to balance
+ 4 | Inserted (4,40)
+(3 rows)
+
+ROLLBACK;
+-- COPY (MERGE ... RETURNING) TO ...
+BEGIN;
+COPY (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 merge_action(), t.*
+) TO stdout;
+DELETE 1 100
+UPDATE 2 220
+INSERT 4 40
+ROLLBACK;
+-- SQL function with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT action text, OUT tid int, OUT new_balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING merge_action(), t.*;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT action, tid, new_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ action | tid | new_balance
+--------+-----+-------------
+ DELETE | 1 | 100
+ UPDATE | 3 | 320
+ INSERT | 4 | 110
+(3 rows)
+
+ROLLBACK;
+-- SQL SRF with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_sq_source_into_sq_target()
+RETURNS TABLE (action text, tid int, balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 merge_action(), t.*;
+$$;
+SELECT * FROM merge_sq_source_into_sq_target();
+ action | tid | balance
+--------+-----+---------
+ DELETE | 1 | 100
+ UPDATE | 2 | 220
+ INSERT | 4 | 40
+(3 rows)
+
+ROLLBACK;
+-- PL/pgSQL function with MERGE ... RETURNING ... INTO
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT r_action text, OUT r_tid int, OUT r_balance int)
+LANGUAGE plpgsql AS
+$$
+BEGIN
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING merge_action(), t.* INTO r_action, r_tid, r_balance;
+END;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT r_action, r_tid, r_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ r_action | r_tid | r_balance
+----------+-------+-----------
+ DELETE | 1 | 100
+ UPDATE | 3 | 320
+ INSERT | 4 | 110
+(3 rows)
+
ROLLBACK;
-- EXPLAIN
CREATE TABLE ex_mtarget (a int, b int)
@@ -1513,7 +1699,7 @@ SELECT * FROM sq_target WHERE tid = 1;
(1 row)
ROLLBACK;
-DROP TABLE sq_target, sq_source CASCADE;
+DROP TABLE sq_target, sq_target_merge_log, sq_source CASCADE;
NOTICE: drop cascades to view v
CREATE TABLE pa_target (tid integer, balance float, val text)
PARTITION BY LIST (tid);
@@ -1639,6 +1825,32 @@ SELECT * FROM pa_target ORDER BY tid;
(14 rows)
ROLLBACK;
+-- update partition key to partition not initially scanned
+BEGIN;
+MERGE INTO pa_target t
+ USING pa_source s
+ ON t.tid = s.sid AND t.tid = 1
+ WHEN MATCHED THEN
+ UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
+ RETURNING merge_action(), t.*;
+ merge_action | tid | balance | val
+--------------+-----+---------+--------------------------
+ UPDATE | 2 | 110 | initial updated by merge
+(1 row)
+
+SELECT * FROM pa_target ORDER BY tid;
+ tid | balance | val
+-----+---------+--------------------------
+ 2 | 110 | initial updated by merge
+ 3 | 300 | initial
+ 5 | 500 | initial
+ 7 | 700 | initial
+ 9 | 900 | initial
+ 11 | 1100 | initial
+ 13 | 1300 | initial
+(7 rows)
+
+ROLLBACK;
DROP TABLE pa_target CASCADE;
-- The target table is partitioned in the same way, but this time by attaching
-- partitions which have columns in different order, dropped columns etc.
@@ -1888,7 +2100,21 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge');
+ INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge')
+ RETURNING merge_action(), t.*;
+ merge_action | logts | tid | balance | val
+--------------+--------------------------+-----+---------+--------------------------
+ UPDATE | Tue Jan 31 00:00:00 2017 | 1 | 110 | initial updated by merge
+ UPDATE | Tue Feb 28 00:00:00 2017 | 2 | 220 | initial updated by merge
+ INSERT | Sun Jan 15 00:00:00 2017 | 3 | 30 | inserted by merge
+ UPDATE | Tue Jan 31 00:00:00 2017 | 4 | 440 | initial updated by merge
+ UPDATE | Tue Feb 28 00:00:00 2017 | 5 | 550 | initial updated by merge
+ INSERT | Sun Jan 15 00:00:00 2017 | 6 | 60 | inserted by merge
+ UPDATE | Tue Jan 31 00:00:00 2017 | 7 | 770 | initial updated by merge
+ UPDATE | Tue Feb 28 00:00:00 2017 | 8 | 880 | initial updated by merge
+ INSERT | Sun Jan 15 00:00:00 2017 | 9 | 90 | inserted by merge
+(9 rows)
+
SELECT * FROM pa_target ORDER BY tid;
logts | tid | balance | val
--------------------------+-----+---------+--------------------------
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
new file mode 100644
index 4538f0c..d507a2c
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -2302,6 +2302,35 @@ WHEN MATCHED THEN
UPDATE SET dnotes = dnotes || ' notes added by merge8 '
WHEN NOT MATCHED THEN
INSERT VALUES (13, 44, 1, 'regress_rls_bob', 'new manga');
+SELECT * FROM document WHERE did = 13;
+ did | cid | dlevel | dauthor | dtitle | dnotes
+-----+-----+--------+---------+--------+--------
+(0 rows)
+
+-- but not OK if RETURNING is used
+MERGE INTO document d
+USING (SELECT 14 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge9 '
+WHEN NOT MATCHED THEN
+ INSERT VALUES (14, 44, 1, 'regress_rls_bob', 'new manga')
+RETURNING *;
+ERROR: new row violates row-level security policy for table "document"
+-- but OK if new row is visible
+MERGE INTO document d
+USING (SELECT 14 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge10 '
+WHEN NOT MATCHED THEN
+ INSERT VALUES (14, 11, 1, 'regress_rls_bob', 'new novel')
+RETURNING *;
+ sdid | did | cid | dlevel | dauthor | dtitle | dnotes
+------+-----+-----+--------+-----------------+-----------+--------
+ 14 | 14 | 11 | 1 | regress_rls_bob | new novel |
+(1 row)
+
RESET SESSION AUTHORIZATION;
-- drop the restrictive SELECT policy so that we can look at the
-- final state of the table
@@ -2325,7 +2354,8 @@ SELECT * FROM document;
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 notes added by merge7
13 | 44 | 1 | regress_rls_bob | new manga |
-(15 rows)
+ 14 | 11 | 1 | regress_rls_bob | new novel |
+(16 rows)
--
-- ROLE/GROUP
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
new file mode 100644
index 0cd2c64..84e359f
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -3638,7 +3638,7 @@ MERGE INTO rule_merge1 t USING (SELECT 1
-- test deparsing
CREATE TABLE sf_target(id int, data text, filling int[]);
CREATE FUNCTION merge_sf_test()
- RETURNS void
+ RETURNS TABLE(action text, a int, b text, id int, data text, filling int[])
LANGUAGE sql
BEGIN ATOMIC
MERGE INTO sf_target t
@@ -3675,11 +3675,13 @@ WHEN NOT MATCHED
VALUES (s.a, s.b, DEFAULT)
WHEN NOT MATCHED
THEN INSERT (filling[1], id)
- VALUES (s.a, s.a);
+ VALUES (s.a, s.a)
+RETURNING
+ merge_action() AS action, *;
END;
\sf merge_sf_test
CREATE OR REPLACE FUNCTION public.merge_sf_test()
- RETURNS void
+ RETURNS TABLE(action text, a integer, b text, id integer, data text, filling integer[])
LANGUAGE sql
BEGIN ATOMIC
MERGE INTO sf_target t
@@ -3716,7 +3718,13 @@ BEGIN ATOMIC
VALUES (s.a, s.b, DEFAULT)
WHEN NOT MATCHED
THEN INSERT (filling[1], id)
- VALUES (s.a, s.a);
+ VALUES (s.a, s.a)
+ RETURNING MERGE_ACTION() AS action,
+ s.a,
+ s.b,
+ t.id,
+ t.data,
+ t.filling;
END
DROP FUNCTION merge_sf_test;
DROP TABLE sf_target;
diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out
new file mode 100644
index 794cf9c..1062c34
--- a/src/test/regress/expected/updatable_views.out
+++ b/src/test/regress/expected/updatable_views.out
@@ -479,7 +479,15 @@ MERGE INTO rw_view1 t
(2, 'ROW 2'), (3, 'ROW 3')) AS v(a,b) ON t.a = v.a
WHEN MATCHED AND t.a <= 1 THEN UPDATE SET b = v.b
WHEN MATCHED THEN DELETE
- WHEN NOT MATCHED AND a > 0 THEN INSERT (a) VALUES (v.a);
+ WHEN NOT MATCHED AND a > 0 THEN INSERT (a) VALUES (v.a)
+ RETURNING merge_action(), v.*, t.*;
+ merge_action | a | b | a | b
+--------------+---+-------+---+-------------
+ UPDATE | 1 | ROW 1 | 1 | ROW 1
+ DELETE | 3 | ROW 3 | 3 | Row 3
+ INSERT | 2 | ROW 2 | 2 | Unspecified
+(3 rows)
+
SELECT * FROM base_tbl ORDER BY a;
a | b
----+-------------
@@ -631,7 +639,15 @@ MERGE INTO rw_view2 t
USING (VALUES (3, 'R3'), (4, 'R4'), (5, 'R5')) AS v(a,b) ON aaa = v.a
WHEN MATCHED AND aaa = 3 THEN DELETE
WHEN MATCHED THEN UPDATE SET bbb = v.b
- WHEN NOT MATCHED THEN INSERT (aaa) VALUES (v.a);
+ WHEN NOT MATCHED THEN INSERT (aaa) VALUES (v.a)
+ RETURNING merge_action(), v.*, t.*;
+ merge_action | a | b | aaa | bbb
+--------------+---+----+-----+-------------
+ DELETE | 3 | R3 | 3 | Row 3
+ UPDATE | 4 | R4 | 4 | R4
+ INSERT | 5 | R5 | 5 | Unspecified
+(3 rows)
+
SELECT * FROM rw_view2 ORDER BY aaa;
aaa | bbb
-----+-------------
@@ -1071,7 +1087,15 @@ MERGE INTO rw_view2 t
USING (SELECT x, 'R'||x FROM generate_series(0,3) x) AS s(a,b) ON t.a = s.a
WHEN MATCHED AND t.a <= 1 THEN DELETE
WHEN MATCHED THEN UPDATE SET b = s.b
- WHEN NOT MATCHED AND s.a > 0 THEN INSERT VALUES (s.a, s.b);
+ WHEN NOT MATCHED AND s.a > 0 THEN INSERT VALUES (s.a, s.b)
+ RETURNING merge_action(), s.*, t.*;
+ merge_action | a | b | a | b
+--------------+---+----+---+-------
+ DELETE | 1 | R1 | 1 | Row 1
+ UPDATE | 2 | R2 | 2 | R2
+ INSERT | 3 | R3 | 3 | R3
+(3 rows)
+
SELECT * FROM base_tbl ORDER BY a;
a | b
----+--------
diff --git a/src/test/regress/expected/with.out b/src/test/regress/expected/with.out
new file mode 100644
index 7d796ea..6de347b
--- a/src/test/regress/expected/with.out
+++ b/src/test/regress/expected/with.out
@@ -3493,6 +3493,16 @@ SELECT * FROM t;
ERROR: WITH query "t" does not have a RETURNING clause
LINE 4: SELECT * FROM t;
^
+-- RETURNING tries to return its own output
+WITH RECURSIVE t(action, a) AS (
+ MERGE INTO y USING (VALUES (11)) v(a) ON y.a = v.a
+ WHEN NOT MATCHED THEN INSERT VALUES (v.a)
+ RETURNING merge_action(), (SELECT a FROM t)
+)
+SELECT * FROM t;
+ERROR: recursive query "t" must not contain data-modifying statements
+LINE 1: WITH RECURSIVE t(action, a) AS (
+ ^
-- data-modifying WITH allowed only at the top level
SELECT * FROM (
WITH t AS (UPDATE y SET a=a+1 RETURNING *)
diff --git a/src/test/regress/sql/merge.sql b/src/test/regress/sql/merge.sql
new file mode 100644
index 79a1868..dfcc243
--- a/src/test/regress/sql/merge.sql
+++ b/src/test/regress/sql/merge.sql
@@ -88,12 +88,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
@@ -817,7 +817,7 @@ BEGIN;
MERGE INTO sq_target
USING v
ON tid = sid
-WHEN MATCHED AND tid > 2 THEN
+WHEN MATCHED AND tid >= 2 THEN
UPDATE SET balance = balance + delta
WHEN NOT MATCHED THEN
INSERT (balance, tid) VALUES (balance + delta, sid)
@@ -830,7 +830,7 @@ INSERT INTO sq_source (sid, balance, del
MERGE INTO sq_target t
USING v
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)
@@ -848,7 +848,7 @@ WITH targq AS (
MERGE INTO sq_target t
USING v
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)
@@ -857,18 +857,149 @@ WHEN MATCHED AND tid < 2 THEN
ROLLBACK;
-- RETURNING
+SELECT * FROM sq_source ORDER BY sid;
+SELECT * FROM sq_target ORDER BY tid;
+
BEGIN;
-INSERT INTO sq_source (sid, balance, delta) VALUES (-1, -1, -10);
+CREATE TABLE merge_actions(action text, abbrev text);
+INSERT INTO merge_actions VALUES ('INSERT', 'ins'), ('UPDATE', 'upd'), ('DELETE', 'del');
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)
+ INSERT (balance, tid) VALUES (balance + delta, sid)
WHEN MATCHED AND tid < 2 THEN
- DELETE
-RETURNING *;
+ DELETE
+RETURNING (SELECT abbrev FROM merge_actions
+ WHERE action = merge_action()) AS action,
+ t.*,
+ CASE 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_action() outside MERGE
+SELECT merge_action() FROM sq_target;
+UPDATE sq_target SET balance = balance + 1 RETURNING merge_action();
+
+-- RETURNING in CTEs
+CREATE TABLE sq_target_merge_log (tid integer NOT NULL, last_change text);
+INSERT INTO sq_target_merge_log VALUES (1, 'Original value');
+BEGIN;
+WITH m AS (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 merge_action() AS action, t.*,
+ CASE merge_action()
+ WHEN 'INSERT' THEN 'Inserted '||t
+ WHEN 'UPDATE' THEN 'Added '||delta||' to balance'
+ WHEN 'DELETE' THEN 'Removed '||t
+ END AS description
+), m2 AS (
+ MERGE INTO sq_target_merge_log l
+ USING m
+ ON l.tid = m.tid
+ WHEN MATCHED THEN
+ UPDATE SET last_change = description
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (m.tid, description)
+ RETURNING action, merge_action() AS log_action, l.*
+)
+SELECT * FROM m2;
+SELECT * FROM sq_target_merge_log ORDER BY tid;
+ROLLBACK;
+
+-- COPY (MERGE ... RETURNING) TO ...
+BEGIN;
+COPY (
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 merge_action(), t.*
+) TO stdout;
+ROLLBACK;
+
+-- SQL function with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT action text, OUT tid int, OUT new_balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING merge_action(), t.*;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT action, tid, new_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ROLLBACK;
+
+-- SQL SRF with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_sq_source_into_sq_target()
+RETURNS TABLE (action text, tid int, balance int)
+LANGUAGE sql AS
+$$
+ MERGE INTO sq_target t
+ USING sq_source s
+ ON tid = sid
+ 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 merge_action(), t.*;
+$$;
+SELECT * FROM merge_sq_source_into_sq_target();
+ROLLBACK;
+
+-- PL/pgSQL function with MERGE ... RETURNING ... INTO
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+ OUT r_action text, OUT r_tid int, OUT r_balance int)
+LANGUAGE plpgsql AS
+$$
+BEGIN
+ MERGE INTO sq_target t
+ USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+ ON tid = v.sid
+ WHEN MATCHED AND tid >= 2 THEN
+ UPDATE SET balance = t.balance + v.delta
+ WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+ WHEN MATCHED AND tid < 2 THEN
+ DELETE
+ RETURNING merge_action(), t.* INTO r_action, r_tid, r_balance;
+END;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT r_action, r_tid, r_balance FROM merge_into_sq_target(sid, balance, delta)) m;
ROLLBACK;
-- EXPLAIN
@@ -967,7 +1098,7 @@ WHEN MATCHED THEN
SELECT * FROM sq_target WHERE tid = 1;
ROLLBACK;
-DROP TABLE sq_target, sq_source CASCADE;
+DROP TABLE sq_target, sq_target_merge_log, sq_source CASCADE;
CREATE TABLE pa_target (tid integer, balance float, val text)
PARTITION BY LIST (tid);
@@ -1034,6 +1165,17 @@ SELECT merge_func();
SELECT * FROM pa_target ORDER BY tid;
ROLLBACK;
+-- update partition key to partition not initially scanned
+BEGIN;
+MERGE INTO pa_target t
+ USING pa_source s
+ ON t.tid = s.sid AND t.tid = 1
+ WHEN MATCHED THEN
+ UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
+ RETURNING merge_action(), t.*;
+SELECT * FROM pa_target ORDER BY tid;
+ROLLBACK;
+
DROP TABLE pa_target CASCADE;
-- The target table is partitioned in the same way, but this time by attaching
@@ -1210,7 +1352,8 @@ MERGE INTO pa_target t
WHEN MATCHED THEN
UPDATE SET balance = balance + delta, val = val || ' updated by merge'
WHEN NOT MATCHED THEN
- INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge');
+ INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge')
+ RETURNING merge_action(), t.*;
SELECT * FROM pa_target ORDER BY tid;
ROLLBACK;
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
new file mode 100644
index dec7340..1d5ed0a
--- a/src/test/regress/sql/rowsecurity.sql
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -992,6 +992,27 @@ WHEN MATCHED THEN
UPDATE SET dnotes = dnotes || ' notes added by merge8 '
WHEN NOT MATCHED THEN
INSERT VALUES (13, 44, 1, 'regress_rls_bob', 'new manga');
+SELECT * FROM document WHERE did = 13;
+
+-- but not OK if RETURNING is used
+MERGE INTO document d
+USING (SELECT 14 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge9 '
+WHEN NOT MATCHED THEN
+ INSERT VALUES (14, 44, 1, 'regress_rls_bob', 'new manga')
+RETURNING *;
+
+-- but OK if new row is visible
+MERGE INTO document d
+USING (SELECT 14 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge10 '
+WHEN NOT MATCHED THEN
+ INSERT VALUES (14, 11, 1, 'regress_rls_bob', 'new novel')
+RETURNING *;
RESET SESSION AUTHORIZATION;
-- drop the restrictive SELECT policy so that we can look at the
diff --git a/src/test/regress/sql/rules.sql b/src/test/regress/sql/rules.sql
new file mode 100644
index 6924012..27340ba
--- a/src/test/regress/sql/rules.sql
+++ b/src/test/regress/sql/rules.sql
@@ -1294,7 +1294,7 @@ MERGE INTO rule_merge1 t USING (SELECT 1
CREATE TABLE sf_target(id int, data text, filling int[]);
CREATE FUNCTION merge_sf_test()
- RETURNS void
+ RETURNS TABLE(action text, a int, b text, id int, data text, filling int[])
LANGUAGE sql
BEGIN ATOMIC
MERGE INTO sf_target t
@@ -1331,7 +1331,9 @@ WHEN NOT MATCHED
VALUES (s.a, s.b, DEFAULT)
WHEN NOT MATCHED
THEN INSERT (filling[1], id)
- VALUES (s.a, s.a);
+ VALUES (s.a, s.a)
+RETURNING
+ merge_action() AS action, *;
END;
\sf merge_sf_test
diff --git a/src/test/regress/sql/updatable_views.sql b/src/test/regress/sql/updatable_views.sql
new file mode 100644
index ae11e46..afdf331
--- a/src/test/regress/sql/updatable_views.sql
+++ b/src/test/regress/sql/updatable_views.sql
@@ -175,7 +175,8 @@ MERGE INTO rw_view1 t
(2, 'ROW 2'), (3, 'ROW 3')) AS v(a,b) ON t.a = v.a
WHEN MATCHED AND t.a <= 1 THEN UPDATE SET b = v.b
WHEN MATCHED THEN DELETE
- WHEN NOT MATCHED AND a > 0 THEN INSERT (a) VALUES (v.a);
+ WHEN NOT MATCHED AND a > 0 THEN INSERT (a) VALUES (v.a)
+ RETURNING merge_action(), v.*, t.*;
SELECT * FROM base_tbl ORDER BY a;
EXPLAIN (costs off) UPDATE rw_view1 SET a=6 WHERE a=5;
@@ -246,7 +247,8 @@ MERGE INTO rw_view2 t
USING (VALUES (3, 'R3'), (4, 'R4'), (5, 'R5')) AS v(a,b) ON aaa = v.a
WHEN MATCHED AND aaa = 3 THEN DELETE
WHEN MATCHED THEN UPDATE SET bbb = v.b
- WHEN NOT MATCHED THEN INSERT (aaa) VALUES (v.a);
+ WHEN NOT MATCHED THEN INSERT (aaa) VALUES (v.a)
+ RETURNING merge_action(), v.*, t.*;
SELECT * FROM rw_view2 ORDER BY aaa;
EXPLAIN (costs off) UPDATE rw_view2 SET aaa=5 WHERE aaa=4;
@@ -458,7 +460,8 @@ MERGE INTO rw_view2 t
USING (SELECT x, 'R'||x FROM generate_series(0,3) x) AS s(a,b) ON t.a = s.a
WHEN MATCHED AND t.a <= 1 THEN DELETE
WHEN MATCHED THEN UPDATE SET b = s.b
- WHEN NOT MATCHED AND s.a > 0 THEN INSERT VALUES (s.a, s.b);
+ WHEN NOT MATCHED AND s.a > 0 THEN INSERT VALUES (s.a, s.b)
+ RETURNING merge_action(), s.*, t.*;
SELECT * FROM base_tbl ORDER BY a;
EXPLAIN (costs off) UPDATE rw_view2 SET a=3 WHERE a=2;
diff --git a/src/test/regress/sql/with.sql b/src/test/regress/sql/with.sql
new file mode 100644
index f8a213e..037bc0a
--- a/src/test/regress/sql/with.sql
+++ b/src/test/regress/sql/with.sql
@@ -1591,6 +1591,14 @@ WITH t AS (
)
SELECT * FROM t;
+-- RETURNING tries to return its own output
+WITH RECURSIVE t(action, a) AS (
+ MERGE INTO y USING (VALUES (11)) v(a) ON y.a = v.a
+ WHEN NOT MATCHED THEN INSERT VALUES (v.a)
+ RETURNING merge_action(), (SELECT a FROM t)
+)
+SELECT * FROM t;
+
-- data-modifying WITH allowed only at the top level
SELECT * FROM (
WITH t AS (UPDATE y SET a=a+1 RETURNING *)
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
new file mode 100644
index aa7a25b..7a102e8
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1577,6 +1577,7 @@ MergeJoinState
MergePath
MergeScanSelCache
MergeStmt
+MergeSupportFunc
MergeWhenClause
MetaCommand
MinMaxAggInfo
On Fri, 15 Mar 2024 at 11:06, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
Updated patch attached.
I have gone over this patch again in detail, and I believe that the
code is in good shape. All review comments have been addressed, and
the only thing remaining is the syntax question.
To recap, this adds support for a single RETURNING list at the end of
a MERGE command, and a special MERGE_ACTION() function that may be
used in the RETURNING list to return the action command string
('INSERT', 'UPDATE', or 'DELETE') that was executed.
Looking for similar precedents in other databases, SQL Server uses a
slightly different (non-standard) syntax for MERGE, and uses "OUTPUT"
instead of "RETURNING" to return rows. But it does allow "$action" in
the output list, which is functionally equivalent to MERGE_ACTION():
In the future, we may choose to support the SQL standard syntax for
returning rows modified by INSERT, UPDATE, DELETE, and MERGE commands,
but I don't think that this patch needs to do that.
What this patch does is to make MERGE more consistent with INSERT,
UPDATE, and DELETE, by allowing RETURNING. And if the patch to add
support for returning OLD/NEW values [1]/messages/by-id/CAEZATCWx0J0-v=Qjc6gXzR=KtsdvAE7Ow=D=mu50AgOe+pvisQ@mail.gmail.com makes it in too, it will be
more powerful than the SQL standard syntax, since it will allow both
old and new values to be returned at the same time, in arbitrary
expressions.
So barring any further objections, I'd like to go ahead and get this
patch committed.
Regards,
Dean
[1]: /messages/by-id/CAEZATCWx0J0-v=Qjc6gXzR=KtsdvAE7Ow=D=mu50AgOe+pvisQ@mail.gmail.com
On Fri, 2024-03-15 at 11:20 +0000, Dean Rasheed wrote:
To recap, this adds support for a single RETURNING list at the end of
a MERGE command, and a special MERGE_ACTION() function that may be
used in the RETURNING list to return the action command string
('INSERT', 'UPDATE', or 'DELETE') that was executed.
...
So barring any further objections, I'd like to go ahead and get this
patch committed.
All of my concerns have been extensively discussed and it seems like
they are just the cost of having a good feature. Thank you for going
through so many alternative approaches, I think the one you've arrived
at is consistent with what Vik endorsed[1]/messages/by-id/7db39b45-821f-4894-ada9-c19570b11b63@postgresfriends.org.
The MERGE_ACTION keyword is added to the 'col_name_keyword' and the
'bare_label_keyword' lists. That has some annoying effects, like:
CREATE FUNCTION merge_action() RETURNS TEXT
LANGUAGE SQL AS $$ SELECT 'asdf'; $$;
ERROR: syntax error at or near "("
LINE 1: CREATE FUNCTION merge_action() RETURNS TEXT
I didn't see any affirmative endorsement of exactly how the keyword is
implemented, but that patch has been around for a while, and I didn't
see any objection, either.
I like this feature from a user perspective. So +1 from me.
Regards,
Jeff Davis
[1]: /messages/by-id/7db39b45-821f-4894-ada9-c19570b11b63@postgresfriends.org
/messages/by-id/7db39b45-821f-4894-ada9-c19570b11b63@postgresfriends.org
On Fri, 15 Mar 2024 at 17:14, Jeff Davis <pgsql@j-davis.com> wrote:
On Fri, 2024-03-15 at 11:20 +0000, Dean Rasheed wrote:
So barring any further objections, I'd like to go ahead and get this
patch committed.I like this feature from a user perspective. So +1 from me.
I have committed this. Thanks for all the feedback everyone.
Regards,
Dean