From 0294df2f1f842dfb0eed79007b21016f486a3c6c Mon Sep 17 00:00:00 2001 From: Dean Rasheed Date: Sat, 30 Mar 2024 10:00:26 +0000 Subject: Add support for MERGE ... WHEN NOT MATCHED BY SOURCE. This allows MERGE commands to include WHEN NOT MATCHED BY SOURCE actions, which operate on rows that exist in the target relation, but not in the data source. These actions can execute UPDATE, DELETE, or DO NOTHING sub-commands. This is in contrast to already-supported WHEN NOT MATCHED actions, which operate on rows that exist in the data source, but not in the target relation. To make this distinction clearer, such actions may now be written as WHEN NOT MATCHED BY TARGET. Writing WHEN NOT MATCHED without specifying BY SOURCE or BY TARGET is equivalent to writing WHEN NOT MATCHED BY TARGET. Dean Rasheed, reviewed by Alvaro Herrera, Ted Yu and Vik Fearing. Discussion: https://postgr.es/m/CAEZATCWqnKGc57Y_JanUBHQXNKcXd7r=0R4NEZUVwP+syRkWbA@mail.gmail.com --- src/backend/executor/nodeModifyTable.c | 387 ++++++++++++++++++++------------- 1 file changed, 236 insertions(+), 151 deletions(-) (limited to 'src/backend/executor/nodeModifyTable.c') diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c index d1917f2fea7..325d380b0a9 100644 --- a/src/backend/executor/nodeModifyTable.c +++ b/src/backend/executor/nodeModifyTable.c @@ -24,13 +24,15 @@ * values plus row-locating info for UPDATE and MERGE cases, or just the * row-locating info for DELETE cases. * - * MERGE runs a join between the source relation and the target - * table; if any WHEN NOT MATCHED clauses are present, then the - * join is an outer join. In this case, any unmatched tuples will - * have NULL row-locating info, and only INSERT can be run. But for - * matched tuples, then row-locating info is used to determine the - * tuple to UPDATE or DELETE. When all clauses are WHEN MATCHED, - * then an inner join is used, so all tuples contain row-locating info. + * MERGE runs a join between the source relation and the target table. + * If any WHEN NOT MATCHED [BY TARGET] clauses are present, then the join + * is an outer join that might output tuples without a matching target + * tuple. In this case, any unmatched target tuples will have NULL + * row-locating info, and only INSERT can be run. But for matched target + * tuples, the row-locating info is used to determine the tuple to UPDATE + * or DELETE. When all clauses are WHEN MATCHED or WHEN NOT MATCHED BY + * SOURCE, all tuples produced by the join will include a matching target + * tuple, so all tuples contain row-locating info. * * If the query specifies RETURNING, then the ModifyTable returns a * RETURNING tuple after completing each row insert, update, or delete. @@ -2659,48 +2661,65 @@ ExecMerge(ModifyTableContext *context, ResultRelInfo *resultRelInfo, bool matched; /*----- - * If we are dealing with a WHEN MATCHED case (tupleid or oldtuple is - * valid, depending on whether the result relation is a table or a view), - * we execute the first action for which the additional WHEN MATCHED AND + * If we are dealing with a WHEN MATCHED case, tupleid or oldtuple is + * valid, depending on whether the result relation is a table or a view. + * We execute the first action for which the additional WHEN MATCHED AND * quals pass. If an action without quals is found, that action is * executed. * - * Similarly, if we are dealing with WHEN NOT MATCHED case, we look at - * the given WHEN NOT MATCHED actions in sequence until one passes. + * Similarly, in the WHEN NOT MATCHED BY SOURCE case, tupleid or oldtuple + * is valid, and we look at the given WHEN NOT MATCHED BY SOURCE actions + * in sequence until one passes. This is almost identical to the WHEN + * MATCHED case, and both cases are handled by ExecMergeMatched(). + * + * Finally, in the WHEN NOT MATCHED [BY TARGET] case, both tupleid and + * oldtuple are invalid, and we look at the given WHEN NOT MATCHED [BY + * TARGET] actions in sequence until one passes. * * Things get interesting in case of concurrent update/delete of the * target tuple. Such concurrent update/delete is detected while we are - * executing a WHEN MATCHED action. + * executing a WHEN MATCHED or WHEN NOT MATCHED BY SOURCE action. * * A concurrent update can: * - * 1. modify the target tuple so that it no longer satisfies the - * additional quals attached to the current WHEN MATCHED action + * 1. modify the target tuple so that the results from checking any + * additional quals attached to WHEN MATCHED or WHEN NOT MATCHED BY + * SOURCE actions potentially change, but the result from the join + * quals does not change. * - * In this case, we are still dealing with a WHEN MATCHED case. - * We recheck the list of WHEN MATCHED actions from the start and - * choose the first one that satisfies the new target tuple. + * In this case, we are still dealing with the same kind of match + * (MATCHED or NOT MATCHED BY SOURCE). We recheck the same list of + * actions from the start and choose the first one that satisfies the + * new target tuple. * - * 2. modify the target tuple so that the join quals no longer pass and - * hence the source tuple no longer has a match. + * 2. modify the target tuple in the WHEN MATCHED case so that the join + * quals no longer pass and hence the source and target tuples no + * longer match. * - * In this case, the source tuple no longer matches the target tuple, - * so we now instead find a qualifying WHEN NOT MATCHED action to - * execute. + * In this case, we are now dealing with a NOT MATCHED case, and we + * process both WHEN NOT MATCHED BY SOURCE and WHEN NOT MATCHED [BY + * TARGET] actions. First ExecMergeMatched() processes the list of + * WHEN NOT MATCHED BY SOURCE actions in sequence until one passes, + * then ExecMergeNotMatched() processes any WHEN NOT MATCHED [BY + * TARGET] actions in sequence until one passes. Thus we may execute + * two actions; one of each kind. * - * XXX Hmmm, what if the updated tuple would now match one that was - * considered NOT MATCHED so far? + * Thus we support concurrent updates that turn MATCHED candidate rows + * into NOT MATCHED rows. However, we do not attempt to support cases + * that would turn NOT MATCHED rows into MATCHED rows, or which would + * cause a target row to match a different source row. * - * A concurrent delete changes a WHEN MATCHED case to WHEN NOT MATCHED. + * A concurrent delete changes a WHEN MATCHED case to WHEN NOT MATCHED + * [BY TARGET]. * - * ExecMergeMatched takes care of following the update chain and - * re-finding the qualifying WHEN MATCHED action, as long as the updated - * target tuple still satisfies the join quals, i.e., it remains a WHEN - * MATCHED case. If the tuple gets deleted or the join quals fail, it - * returns and we try ExecMergeNotMatched. Given that ExecMergeMatched - * always make progress by following the update chain and we never switch - * from ExecMergeNotMatched to ExecMergeMatched, there is no risk of a - * livelock. + * ExecMergeMatched() takes care of following the update chain and + * re-finding the qualifying WHEN MATCHED or WHEN NOT MATCHED BY SOURCE + * action, as long as the target tuple still exists. If the target tuple + * gets deleted or a concurrent update causes the join quals to fail, it + * returns a matched status of false and we call ExecMergeNotMatched(). + * Given that ExecMergeMatched() always makes progress by following the + * update chain and we never switch from ExecMergeNotMatched() to + * ExecMergeMatched(), there is no risk of a livelock. */ matched = tupleid != NULL || oldtuple != NULL; if (matched) @@ -2713,33 +2732,52 @@ ExecMerge(ModifyTableContext *context, ResultRelInfo *resultRelInfo, * "matched" to false, indicating that it no longer matches). */ if (!matched) - rslot = ExecMergeNotMatched(context, resultRelInfo, canSetTag); + { + /* + * If a concurrent update turned a MATCHED case into a NOT MATCHED + * case, and we have both WHEN NOT MATCHED BY SOURCE and WHEN NOT + * MATCHED [BY TARGET] actions, and there is a RETURNING clause, + * ExecMergeMatched() may have already executed a WHEN NOT MATCHED BY + * SOURCE action, and computed the row to return. If so, we cannot + * execute a WHEN NOT MATCHED [BY TARGET] action now, so mark it as + * pending (to be processed on the next call to ExecModifyTable()). + * Otherwise, just process the action now. + */ + if (rslot == NULL) + rslot = ExecMergeNotMatched(context, resultRelInfo, canSetTag); + else + context->mtstate->mt_merge_pending_not_matched = context->planSlot; + } return rslot; } /* - * Check and execute the first qualifying MATCHED action. If the target + * Check and execute the first qualifying MATCHED or NOT MATCHED BY SOURCE + * action, depending on whether the join quals are satisfied. If the target * relation is a table, the current target tuple is identified by tupleid. * Otherwise, if the target relation is a view, oldtuple is the current target * tuple from the view. * - * 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 "matched" is set to true, indicating that no further - * action is required for this tuple. + * We start from the first WHEN MATCHED or WHEN NOT MATCHED BY SOURCE 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 the end without finding a qualifying action, we return NULL. + * Otherwise, we execute the qualifying action and return its RETURNING + * result, if any, or NULL. * - * If we do find a qualifying action, then we attempt to execute the action. + * On entry, "*matched" is assumed to be true. If a concurrent update or + * delete is detected that causes the join quals to no longer pass, we set it + * to false, indicating that the caller should process any NOT MATCHED [BY + * TARGET] actions. * - * If the tuple is concurrently updated, EvalPlanQual is run with the updated - * tuple to recheck the join quals. Note that the additional quals associated - * 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, "matched" is set to - * false -- meaning that a NOT MATCHED action must now be executed for the - * current source tuple. + * After a concurrent update, we restart from the first action to look for a + * new qualifying action to execute. If the join quals originally passed, and + * the concurrent update caused them to no longer pass, then we switch from + * the MATCHED to the NOT MATCHED BY SOURCE list of actions before restarting + * (and setting "*matched" to false). As a result we may execute a WHEN NOT + * MATCHED BY SOURCE action, and set "*matched" to false, causing the caller + * to also execute a WHEN NOT MATCHED [BY TARGET] action. */ static TupleTableSlot * ExecMergeMatched(ModifyTableContext *context, ResultRelInfo *resultRelInfo, @@ -2747,6 +2785,8 @@ ExecMergeMatched(ModifyTableContext *context, ResultRelInfo *resultRelInfo, bool *matched) { ModifyTableState *mtstate = context->mtstate; + List **mergeActions = resultRelInfo->ri_MergeActions; + List *actionStates; TupleTableSlot *newslot = NULL; TupleTableSlot *rslot = NULL; EState *estate = context->estate; @@ -2755,54 +2795,58 @@ ExecMergeMatched(ModifyTableContext *context, ResultRelInfo *resultRelInfo, EPQState *epqstate = &mtstate->mt_epqstate; ListCell *l; + /* Expect matched to be true on entry */ + Assert(*matched); + /* - * If there are no WHEN MATCHED actions, we are done. + * If there are no WHEN MATCHED or WHEN NOT MATCHED BY SOURCE actions, we + * are done. */ - if (resultRelInfo->ri_matchedMergeAction == NIL) - { - *matched = true; + if (mergeActions[MERGE_WHEN_MATCHED] == NIL && + mergeActions[MERGE_WHEN_NOT_MATCHED_BY_SOURCE] == NIL) return NULL; - } /* * Make tuple and any needed join variables available to ExecQual and * ExecProject. The target's existing tuple is installed in the scantuple. - * Again, this target relation's slot is required only in the case of a - * MATCHED tuple and UPDATE/DELETE actions. + * This target relation's slot is required only in the case of a MATCHED + * or NOT MATCHED BY SOURCE tuple and UPDATE/DELETE actions. */ econtext->ecxt_scantuple = resultRelInfo->ri_oldTupleSlot; econtext->ecxt_innertuple = context->planSlot; econtext->ecxt_outertuple = NULL; /* - * This routine is only invoked for matched rows, so we should either have - * the tupleid of the target row, or an old tuple from the target wholerow - * junk attr. + * This routine is only invoked for matched target rows, so we should + * either have the tupleid of the target row, or an old tuple from the + * target wholerow junk attr. */ Assert(tupleid != NULL || oldtuple != NULL); if (oldtuple != NULL) ExecForceStoreHeapTuple(oldtuple, resultRelInfo->ri_oldTupleSlot, false); - -lmerge_matched: + else if (!table_tuple_fetch_row_version(resultRelInfo->ri_RelationDesc, + tupleid, + SnapshotAny, + resultRelInfo->ri_oldTupleSlot)) + elog(ERROR, "failed to fetch the target tuple"); /* - * If passed a tupleid, use it to fetch the old target row. + * Test the join condition. If it's satisfied, perform a MATCHED action. + * Otherwise, perform a NOT MATCHED BY SOURCE action. * - * We use SnapshotAny for this because we might get called again after - * EvalPlanQual returns us a new tuple, which may not be visible to our - * MVCC snapshot. + * Note that this join condition will be NULL if there are no NOT MATCHED + * BY SOURCE actions --- see transform_MERGE_to_join(). In that case, we + * need only consider MATCHED actions here. */ - if (tupleid != NULL) - { - if (!table_tuple_fetch_row_version(resultRelInfo->ri_RelationDesc, - tupleid, - SnapshotAny, - resultRelInfo->ri_oldTupleSlot)) - elog(ERROR, "failed to fetch the target tuple"); - } + if (ExecQual(resultRelInfo->ri_MergeJoinCondition, econtext)) + actionStates = mergeActions[MERGE_WHEN_MATCHED]; + else + actionStates = mergeActions[MERGE_WHEN_NOT_MATCHED_BY_SOURCE]; - foreach(l, resultRelInfo->ri_matchedMergeAction) +lmerge_matched: + + foreach(l, actionStates) { MergeActionState *relaction = (MergeActionState *) lfirst(l); CmdType commandType = relaction->mas_action->commandType; @@ -2857,10 +2901,8 @@ lmerge_matched: tupleid, NULL, newslot, &result)) { if (result == TM_Ok) - { - *matched = true; return NULL; /* "do nothing" */ - } + break; /* concurrent update/delete */ } @@ -2870,10 +2912,7 @@ lmerge_matched: { if (!ExecIRUpdateTriggers(estate, resultRelInfo, oldtuple, newslot)) - { - *matched = true; return NULL; /* "do nothing" */ - } } else { @@ -2894,7 +2933,6 @@ lmerge_matched: if (updateCxt.crossPartUpdate) { mtstate->mt_merge_updated += 1; - *matched = true; return context->cpUpdateReturningSlot; } } @@ -2914,10 +2952,8 @@ lmerge_matched: NULL, NULL, &result)) { if (result == TM_Ok) - { - *matched = true; return NULL; /* "do nothing" */ - } + break; /* concurrent update/delete */ } @@ -2927,10 +2963,7 @@ lmerge_matched: { if (!ExecIRDeleteTriggers(estate, resultRelInfo, oldtuple)) - { - *matched = true; return NULL; /* "do nothing" */ - } } else result = ExecDeleteAct(context, resultRelInfo, tupleid, @@ -2950,7 +2983,7 @@ lmerge_matched: break; default: - elog(ERROR, "unknown action in MERGE WHEN MATCHED clause"); + elog(ERROR, "unknown action in MERGE WHEN clause"); } switch (result) @@ -3007,14 +3040,15 @@ lmerge_matched: errmsg("could not serialize access due to concurrent delete"))); /* - * If the tuple was already deleted, return to let caller - * handle it under NOT MATCHED clauses. + * If the tuple was already deleted, set matched to false to + * let caller handle it under NOT MATCHED [BY TARGET] clauses. */ *matched = false; return NULL; case TM_Updated: { + bool was_matched; Relation resultRelationDesc; TupleTableSlot *epqslot, *inputslot; @@ -3022,19 +3056,23 @@ lmerge_matched: /* * The target tuple was concurrently updated by some other - * transaction. Run EvalPlanQual() with the new version of - * the tuple. If it does not return a tuple, then we - * switch to the NOT MATCHED list of actions. If it does - * return a tuple and the join qual is still satisfied, - * then we just need to recheck the MATCHED actions, - * starting from the top, and execute the first qualifying - * action. + * transaction. If we are currently processing a MATCHED + * action, use EvalPlanQual() with the new version of the + * tuple and recheck the join qual, to detect a change + * from the MATCHED to the NOT MATCHED cases. If we are + * already processing a NOT MATCHED BY SOURCE action, we + * skip this (cannot switch from NOT MATCHED BY SOURCE to + * MATCHED). */ + was_matched = relaction->mas_action->matchKind == MERGE_WHEN_MATCHED; resultRelationDesc = resultRelInfo->ri_RelationDesc; lockmode = ExecUpdateLockMode(estate, resultRelInfo); - inputslot = EvalPlanQualSlot(epqstate, resultRelationDesc, - resultRelInfo->ri_RangeTableIndex); + if (was_matched) + inputslot = EvalPlanQualSlot(epqstate, resultRelationDesc, + resultRelInfo->ri_RangeTableIndex); + else + inputslot = resultRelInfo->ri_oldTupleSlot; result = table_tuple_lock(resultRelationDesc, tupleid, estate->es_snapshot, @@ -3045,34 +3083,9 @@ lmerge_matched: switch (result) { case TM_Ok: - epqslot = EvalPlanQual(epqstate, - resultRelationDesc, - resultRelInfo->ri_RangeTableIndex, - inputslot); /* - * If we got no tuple, or the tuple we get has a - * NULL ctid, go back to caller: this one is not a - * MATCHED tuple anymore, so they can retry with - * NOT MATCHED actions. - */ - if (TupIsNull(epqslot)) - { - *matched = false; - return NULL; - } - - (void) ExecGetJunkAttribute(epqslot, - resultRelInfo->ri_RowIdAttNo, - &isNull); - if (isNull) - { - *matched = false; - return NULL; - } - - /* - * When a tuple was updated and migrated to + * If the tuple was updated and migrated to * another partition concurrently, the current * MERGE implementation can't follow. There's * probably a better way to handle this case, but @@ -3083,26 +3096,72 @@ lmerge_matched: if (ItemPointerIndicatesMovedPartitions(&context->tmfd.ctid)) ereport(ERROR, (errcode(ERRCODE_T_R_SERIALIZATION_FAILURE), - errmsg("tuple to be deleted was already moved to another partition due to concurrent update"))); + errmsg("tuple to be merged was already moved to another partition due to concurrent update"))); /* - * A non-NULL ctid means that we are still dealing - * with MATCHED case. Restart the loop so that we - * apply all the MATCHED rules again, to ensure - * that the first qualifying WHEN MATCHED action - * is executed. - * - * Update tupleid to that of the new tuple, for - * the refetch we do at the top. + * If this was a MATCHED case, use EvalPlanQual() + * to recheck the join condition. + */ + if (was_matched) + { + epqslot = EvalPlanQual(epqstate, + resultRelationDesc, + resultRelInfo->ri_RangeTableIndex, + inputslot); + + /* + * If the subplan didn't return a tuple, then + * we must be dealing with an inner join for + * which the join condition no longer matches. + * This can only happen if there are no NOT + * MATCHED actions, and so there is nothing + * more to do. + */ + if (TupIsNull(epqslot)) + return NULL; + + /* + * If we got a NULL ctid from the subplan, the + * join quals no longer pass and we switch to + * the NOT MATCHED BY SOURCE case. + */ + (void) ExecGetJunkAttribute(epqslot, + resultRelInfo->ri_RowIdAttNo, + &isNull); + if (isNull) + *matched = false; + + /* + * Otherwise, recheck the join quals to see if + * we need to switch to the NOT MATCHED BY + * SOURCE case. + */ + if (!table_tuple_fetch_row_version(resultRelationDesc, + &context->tmfd.ctid, + SnapshotAny, + resultRelInfo->ri_oldTupleSlot)) + elog(ERROR, "failed to fetch the target tuple"); + + if (*matched) + *matched = ExecQual(resultRelInfo->ri_MergeJoinCondition, + econtext); + + /* Switch lists, if necessary */ + if (!*matched) + actionStates = mergeActions[MERGE_WHEN_NOT_MATCHED_BY_SOURCE]; + } + + /* + * Loop back and process the MATCHED or NOT + * MATCHED BY SOURCE actions from the start. */ - ItemPointerCopy(&context->tmfd.ctid, tupleid); goto lmerge_matched; case TM_Deleted: /* * tuple already deleted; tell caller to run NOT - * MATCHED actions + * MATCHED [BY TARGET] actions */ *matched = false; return NULL; @@ -3186,13 +3245,11 @@ lmerge_matched: /* * Successfully executed an action or no qualifying action was found. */ - *matched = true; - return rslot; } /* - * Execute the first qualifying NOT MATCHED action. + * Execute the first qualifying NOT MATCHED [BY TARGET] action. */ static TupleTableSlot * ExecMergeNotMatched(ModifyTableContext *context, ResultRelInfo *resultRelInfo, @@ -3200,7 +3257,7 @@ ExecMergeNotMatched(ModifyTableContext *context, ResultRelInfo *resultRelInfo, { ModifyTableState *mtstate = context->mtstate; ExprContext *econtext = mtstate->ps.ps_ExprContext; - List *actionStates = NIL; + List *actionStates; TupleTableSlot *rslot = NULL; ListCell *l; @@ -3213,7 +3270,7 @@ ExecMergeNotMatched(ModifyTableContext *context, ResultRelInfo *resultRelInfo, * XXX does this mean that we can avoid creating copies of actionStates on * partitioned tables, for not-matched actions? */ - actionStates = resultRelInfo->ri_notMatchedMergeAction; + actionStates = resultRelInfo->ri_MergeActions[MERGE_WHEN_NOT_MATCHED_BY_TARGET]; /* * Make source tuple available to ExecQual and ExecProject. We don't need @@ -3307,9 +3364,11 @@ ExecInitMerge(ModifyTableState *mtstate, EState *estate) foreach(lc, node->mergeActionLists) { List *mergeActionList = lfirst(lc); + Node *joinCondition; TupleDesc relationDesc; ListCell *l; + joinCondition = (Node *) list_nth(node->mergeJoinConditions, i); resultRelInfo = mtstate->resultRelInfo + i; i++; relationDesc = RelationGetDescr(resultRelInfo->ri_RelationDesc); @@ -3318,13 +3377,16 @@ ExecInitMerge(ModifyTableState *mtstate, EState *estate) if (unlikely(!resultRelInfo->ri_projectNewInfoValid)) ExecInitMergeTupleSlots(mtstate, resultRelInfo); + /* initialize state for join condition checking */ + resultRelInfo->ri_MergeJoinCondition = + ExecInitQual((List *) joinCondition, &mtstate->ps); + foreach(l, mergeActionList) { MergeAction *action = (MergeAction *) lfirst(l); MergeActionState *action_state; TupleTableSlot *tgtslot; TupleDesc tgtdesc; - List **list; /* * Build action merge state for this rel. (For partitions, @@ -3336,15 +3398,12 @@ ExecInitMerge(ModifyTableState *mtstate, EState *estate) &mtstate->ps); /* - * We create two lists - one for WHEN MATCHED actions and one for - * WHEN NOT MATCHED actions - and stick the MergeActionState into - * the appropriate list. + * We create three lists - one for each MergeMatchKind - and stick + * the MergeActionState into the appropriate list. */ - if (action_state->mas_action->matched) - list = &resultRelInfo->ri_matchedMergeAction; - else - list = &resultRelInfo->ri_notMatchedMergeAction; - *list = lappend(*list, action_state); + resultRelInfo->ri_MergeActions[action->matchKind] = + lappend(resultRelInfo->ri_MergeActions[action->matchKind], + action_state); switch (action->commandType) { @@ -3701,6 +3760,31 @@ ExecModifyTable(PlanState *pstate) if (pstate->ps_ExprContext) ResetExprContext(pstate->ps_ExprContext); + /* + * If there is a pending MERGE ... WHEN NOT MATCHED [BY TARGET] action + * to execute, do so now --- see the comments in ExecMerge(). + */ + if (node->mt_merge_pending_not_matched != NULL) + { + context.planSlot = node->mt_merge_pending_not_matched; + + slot = ExecMergeNotMatched(&context, node->resultRelInfo, + node->canSetTag); + + /* Clear the pending action */ + node->mt_merge_pending_not_matched = NULL; + + /* + * 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 */ + } + + /* Fetch the next row from subplan */ context.planSlot = ExecProcNode(subplanstate); /* No more tuples to process? */ @@ -4092,6 +4176,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags) mtstate->resultRelInfo = (ResultRelInfo *) palloc(nrels * sizeof(ResultRelInfo)); + mtstate->mt_merge_pending_not_matched = NULL; mtstate->mt_merge_inserted = 0; mtstate->mt_merge_updated = 0; mtstate->mt_merge_deleted = 0; -- cgit v1.2.3