POC-support-merge-returning-per-action.patch
text/x-patch
Filename: POC-support-merge-returning-per-action.patch
Type: text/x-patch
Part: 0
Message:
Re: MERGE ... RETURNING
Patch
Same data as JSON:
GET /api/v1/attachments/:id/patch
the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes.
API reference →
Format: unified
| File | + | − |
|---|---|---|
| src/backend/commands/copy.c | 0 | 6 |
| src/backend/commands/copyto.c | 2 | 1 |
| src/backend/executor/execPartition.c | 22 | 3 |
| src/backend/executor/functions.c | 5 | 4 |
| src/backend/executor/nodeModifyTable.c | 179 | 51 |
| src/backend/executor/spi.c | 6 | 1 |
| src/backend/nodes/nodeFuncs.c | 5 | 0 |
| src/backend/optimizer/plan/planner.c | 10 | 0 |
| src/backend/optimizer/plan/setrefs.c | 9 | 0 |
| src/backend/optimizer/prep/prepjointree.c | 2 | 0 |
| src/backend/optimizer/prep/preptlist.c | 8 | 7 |
| src/backend/parser/analyze.c | 2 | 3 |
| src/backend/parser/gram.y | 86 | 11 |
| src/backend/parser/parse_cte.c | 2 | 8 |
| src/backend/parser/parse_merge.c | 138 | 2 |
| src/backend/parser/parse_relation.c | 4 | 3 |
| src/backend/rewrite/rewriteHandler.c | 5 | 4 |
| src/backend/rewrite/rowsecurity.c | 21 | 7 |
| src/backend/tcop/utility.c | 1 | 2 |
| src/backend/utils/adt/ruleutils.c | 9 | 3 |
| src/bin/psql/common.c | 6 | 2 |
| src/include/executor/spi.h | 1 | 0 |
| src/include/nodes/execnodes.h | 2 | 0 |
| src/include/nodes/parsenodes.h | 3 | 0 |
| src/include/parser/analyze.h | 1 | 0 |
| src/pl/plpgsql/src/pl_exec.c | 7 | 5 |
| src/pl/tcl/pltcl.c | 1 | 0 |
| src/test/regress/expected/merge.out | 243 | 19 |
| src/test/regress/expected/rowsecurity.out | 33 | 1 |
| src/test/regress/expected/rules.out | 77 | 4 |
| src/test/regress/sql/merge.sql | 153 | 12 |
| src/test/regress/sql/rowsecurity.sql | 23 | 0 |
| src/test/regress/sql/rules.sql | 12 | 2 |
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