/*------------------------------------------------------------------------- * * parse_merge.c * handle merge-statement in parser * * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * * IDENTIFICATION * src/backend/parser/parse_merge.c * *------------------------------------------------------------------------- */ #include "postgres.h" #include "access/sysattr.h" #include "miscadmin.h" #include "nodes/makefuncs.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_cte.h" #include "parser/parse_expr.h" #include "parser/parse_merge.h" #include "parser/parse_relation.h" #include "parser/parse_target.h" #include "utils/rel.h" #include "utils/relcache.h" static void setNamespaceForMergeWhen(ParseState *pstate, MergeWhenClause *mergeWhenClause, Index targetRTI, Index sourceRTI); static void setNamespaceVisibilityForRTE(List *namespace, RangeTblEntry *rte, bool rel_visible, bool cols_visible); /* * Make appropriate changes to the namespace visibility while transforming * individual action's quals and targetlist expressions. In particular, for * INSERT actions we must only see the source relation (since INSERT action is * invoked for NOT MATCHED tuples and hence there is no target tuple to deal * with). On the other hand, UPDATE and DELETE actions can see both source and * target relations. * * Also, since the internal join node can hide the source and target * relations, we must explicitly make the respective relation as visible so * that columns can be referenced unqualified from these relations. */ static void setNamespaceForMergeWhen(ParseState *pstate, MergeWhenClause *mergeWhenClause, Index targetRTI, Index sourceRTI) { RangeTblEntry *targetRelRTE, *sourceRelRTE; targetRelRTE = rt_fetch(targetRTI, pstate->p_rtable); sourceRelRTE = rt_fetch(sourceRTI, pstate->p_rtable); if (mergeWhenClause->matched) { Assert(mergeWhenClause->commandType == CMD_UPDATE || mergeWhenClause->commandType == CMD_DELETE || mergeWhenClause->commandType == CMD_NOTHING); /* MATCHED actions can see both target and source relations. */ setNamespaceVisibilityForRTE(pstate->p_namespace, targetRelRTE, true, true); setNamespaceVisibilityForRTE(pstate->p_namespace, sourceRelRTE, true, true); } else { /* * NOT MATCHED actions can't see target relation, but they can see * source relation. */ Assert(mergeWhenClause->commandType == CMD_INSERT || mergeWhenClause->commandType == CMD_NOTHING); setNamespaceVisibilityForRTE(pstate->p_namespace, targetRelRTE, false, false); setNamespaceVisibilityForRTE(pstate->p_namespace, sourceRelRTE, true, true); } } /* * transformMergeStmt - * transforms a MERGE statement */ Query * transformMergeStmt(ParseState *pstate, MergeStmt *stmt) { Query *qry = makeNode(Query); ListCell *l; AclMode targetPerms = ACL_NO_RIGHTS; bool is_terminal[2]; Index sourceRTI; List *mergeActionList; Node *joinExpr; ParseNamespaceItem *nsitem; /* There can't be any outer WITH to worry about */ Assert(pstate->p_ctenamespace == NIL); qry->commandType = CMD_MERGE; qry->hasRecursive = false; /* process the WITH clause independently of all else */ if (stmt->withClause) { if (stmt->withClause->recursive) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("WITH RECURSIVE is not supported for MERGE statement"))); qry->cteList = transformWithClause(pstate, stmt->withClause); qry->hasModifyingCTE = pstate->p_hasModifyingCTE; } /* * Check WHEN clauses for permissions and sanity */ is_terminal[0] = false; is_terminal[1] = false; foreach(l, stmt->mergeWhenClauses) { MergeWhenClause *mergeWhenClause = (MergeWhenClause *) lfirst(l); int when_type = (mergeWhenClause->matched ? 0 : 1); /* * Collect permissions to check, according to action types. We require * SELECT privileges for DO NOTHING because it'd be irregular to have * a target relation with zero privileges checked, in case DO NOTHING * is the only action. There's no damage from that: any meaningful * MERGE command requires at least some access to the table anyway. */ switch (mergeWhenClause->commandType) { case CMD_INSERT: targetPerms |= ACL_INSERT; break; case CMD_UPDATE: targetPerms |= ACL_UPDATE; break; case CMD_DELETE: targetPerms |= ACL_DELETE; break; case CMD_NOTHING: targetPerms |= ACL_SELECT; break; default: elog(ERROR, "unknown action in MERGE WHEN clause"); } /* * Check for unreachable WHEN clauses */ if (is_terminal[when_type]) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("unreachable WHEN clause specified after unconditional WHEN clause"))); if (mergeWhenClause->condition == NULL) is_terminal[when_type] = true; } /* * Set up the MERGE target table. The target table is added to the * namespace below and to joinlist in transform_MERGE_to_join, so don't do * it here. */ qry->resultRelation = setTargetTable(pstate, stmt->relation, stmt->relation->inh, false, targetPerms); /* * MERGE is unsupported in various cases */ if (pstate->p_target_relation->rd_rel->relkind != RELKIND_RELATION && pstate->p_target_relation->rd_rel->relkind != RELKIND_PARTITIONED_TABLE) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("cannot execute MERGE on relation \"%s\"", RelationGetRelationName(pstate->p_target_relation)), errdetail_relkind_not_supported(pstate->p_target_relation->rd_rel->relkind))); if (pstate->p_target_relation->rd_rules != NULL && pstate->p_target_relation->rd_rules->numLocks > 0) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("cannot execute MERGE on relation \"%s\"", RelationGetRelationName(pstate->p_target_relation)), errdetail("MERGE is not supported for relations with rules."))); /* Now transform the source relation to produce the source RTE. */ transformFromClause(pstate, list_make1(stmt->sourceRelation)); sourceRTI = list_length(pstate->p_rtable); nsitem = GetNSItemByRangeTablePosn(pstate, sourceRTI, 0); /* * Check that the target table doesn't conflict with the source table. * This would typically be a checkNameSpaceConflicts call, but we want a * more specific error message. */ if (strcmp(pstate->p_target_nsitem->p_names->aliasname, nsitem->p_names->aliasname) == 0) ereport(ERROR, errcode(ERRCODE_DUPLICATE_ALIAS), errmsg("name \"%s\" specified more than once", pstate->p_target_nsitem->p_names->aliasname), errdetail("The name is used both as MERGE target table and data source.")); /* * There's no need for a targetlist here; it'll be set up by * preprocess_targetlist later. */ qry->targetList = NIL; qry->rtable = pstate->p_rtable; qry->rteperminfos = pstate->p_rteperminfos; /* * Transform the join condition. This includes references to the target * side, so add that to the namespace. */ addNSItemToQuery(pstate, pstate->p_target_nsitem, false, true, true); joinExpr = transformExpr(pstate, stmt->joinCondition, EXPR_KIND_JOIN_ON); /* * Create the temporary query's jointree using the joinlist we built using * just the source relation; the target relation is not included. The * quals we use are the join conditions to the merge target. The join * will be constructed fully by transform_MERGE_to_join. */ qry->jointree = makeFromExpr(pstate->p_joinlist, joinExpr); /* * We now have a good query shape, so now look at the WHEN conditions and * action targetlists. * * Overall, the MERGE Query's targetlist is NIL. * * Each individual action has its own targetlist that needs separate * transformation. These transforms don't do anything to the overall * targetlist, since that is only used for resjunk columns. * * We can reference any column in Target or Source, which is OK because * both of those already have RTEs. There is nothing like the EXCLUDED * pseudo-relation for INSERT ON CONFLICT. */ mergeActionList = NIL; foreach(l, stmt->mergeWhenClauses) { MergeWhenClause *mergeWhenClause = lfirst_node(MergeWhenClause, l); MergeAction *action; action = makeNode(MergeAction); action->commandType = mergeWhenClause->commandType; action->matched = mergeWhenClause->matched; /* Use an outer join if any INSERT actions exist in the command. */ if (action->commandType == CMD_INSERT) qry->mergeUseOuterJoin = true; /* * Set namespace for the specific action. This must be done before * analyzing the WHEN quals and the action targetlist. */ setNamespaceForMergeWhen(pstate, mergeWhenClause, qry->resultRelation, sourceRTI); /* * Transform the WHEN condition. * * Note that these quals are NOT added to the join quals; instead they * are evaluated separately during execution to decide which of the * WHEN MATCHED or WHEN NOT MATCHED actions to execute. */ action->qual = transformWhereClause(pstate, mergeWhenClause->condition, EXPR_KIND_MERGE_WHEN, "WHEN"); /* * Transform target lists for each INSERT and UPDATE action stmt */ switch (action->commandType) { case CMD_INSERT: { List *exprList = NIL; ListCell *lc; RTEPermissionInfo *perminfo; ListCell *icols; ListCell *attnos; List *icolumns; List *attrnos; pstate->p_is_insert = true; icolumns = checkInsertTargets(pstate, mergeWhenClause->targetList, &attrnos); Assert(list_length(icolumns) == list_length(attrnos)); action->override = mergeWhenClause->override; /* * Handle INSERT much like in transformInsertStmt */ if (mergeWhenClause->values == NIL) { /* * We have INSERT ... DEFAULT VALUES. We can handle * this case by emitting an empty targetlist --- all * columns will be defaulted when the planner expands * the targetlist. */ exprList = NIL; } else { /* * Process INSERT ... VALUES with a single VALUES * sublist. We treat this case separately for * efficiency. The sublist is just computed directly * as the Query's targetlist, with no VALUES RTE. So * it works just like a SELECT without any FROM. */ /* * Do basic expression transformation (same as a ROW() * expr, but allow SetToDefault at top level) */ exprList = transformExpressionList(pstate, mergeWhenClause->values, EXPR_KIND_VALUES_SINGLE, true); /* Prepare row for assignment to target table */ exprList = transformInsertRow(pstate, exprList, mergeWhenClause->targetList, icolumns, attrnos, false); } /* * Generate action's target list using the computed list * of expressions. Also, mark all the target columns as * needing insert permissions. */ perminfo = pstate->p_target_nsitem->p_perminfo; forthree(lc, exprList, icols, icolumns, attnos, attrnos) { Expr *expr = (Expr *) lfirst(lc); ResTarget *col = lfirst_node(ResTarget, icols); AttrNumber attr_num = (AttrNumber) lfirst_int(attnos); TargetEntry *tle; tle = makeTargetEntry(expr, attr_num, col->name, false); action->targetList = lappend(action->targetList, tle); perminfo->insertedCols = bms_add_member(perminfo->insertedCols, attr_num - FirstLowInvalidHeapAttributeNumber); } } break; case CMD_UPDATE: { pstate->p_is_insert = false; action->targetList = transformUpdateTargetList(pstate, mergeWhenClause->targetList); } break; case CMD_DELETE: break; case CMD_NOTHING: action->targetList = NIL; break; default: elog(ERROR, "unknown action in MERGE WHEN clause"); } mergeActionList = lappend(mergeActionList, action); } qry->mergeActionList = mergeActionList; /* RETURNING could potentially be added in the future, but not in SQL std */ qry->returningList = NULL; qry->hasTargetSRFs = false; qry->hasSubLinks = pstate->p_hasSubLinks; assign_query_collations(pstate, qry); return qry; } static void setNamespaceVisibilityForRTE(List *namespace, RangeTblEntry *rte, bool rel_visible, bool cols_visible) { ListCell *lc; foreach(lc, namespace) { ParseNamespaceItem *nsitem = (ParseNamespaceItem *) lfirst(lc); if (nsitem->p_rte == rte) { nsitem->p_rel_visible = rel_visible; nsitem->p_cols_visible = cols_visible; break; } } }