1
0
mirror of https://github.com/postgres/postgres.git synced 2025-12-21 05:21:08 +03:00

Support MERGE into updatable views.

This allows the target relation of MERGE to be an auto-updatable or
trigger-updatable view, and includes support for WITH CHECK OPTION,
security barrier views, and security invoker views.

A trigger-updatable view must have INSTEAD OF triggers for every type
of action (INSERT, UPDATE, and DELETE) mentioned in the MERGE command.
An auto-updatable view must not have any INSTEAD OF triggers. Mixing
auto-update and trigger-update actions (i.e., having a partial set of
INSTEAD OF triggers) is not supported.

Rule-updatable views are also not supported, since there is no
rewriter support for non-SELECT rules with MERGE operations.

Dean Rasheed, reviewed by Jian He and Alvaro Herrera.

Discussion: https://postgr.es/m/CAEZATCVcB1g0nmxuEc-A+gGB0HnfcGQNGYH7gS=7rq0u0zOBXA@mail.gmail.com
This commit is contained in:
Dean Rasheed
2024-02-29 15:56:59 +00:00
parent 8b29a119fd
commit 5f2e179bd3
23 changed files with 1380 additions and 288 deletions

View File

@@ -767,7 +767,7 @@ CopyFrom(CopyFromState cstate)
ExecInitResultRelation(estate, resultRelInfo, 1);
/* Verify the named relation is a valid target for INSERT */
CheckValidResultRel(resultRelInfo, CMD_INSERT);
CheckValidResultRel(resultRelInfo, CMD_INSERT, NIL);
ExecOpenIndices(resultRelInfo, false);

View File

@@ -56,6 +56,7 @@
#include "miscadmin.h"
#include "parser/parse_relation.h"
#include "parser/parsetree.h"
#include "rewrite/rewriteHandler.h"
#include "storage/bufmgr.h"
#include "storage/lmgr.h"
#include "tcop/utility.h"
@@ -1017,14 +1018,18 @@ InitPlan(QueryDesc *queryDesc, int eflags)
* Generally the parser and/or planner should have noticed any such mistake
* already, but let's make sure.
*
* For MERGE, mergeActions is the list of actions that may be performed. The
* result relation is required to support every action, regardless of whether
* or not they are all executed.
*
* Note: when changing this function, you probably also need to look at
* CheckValidRowMarkRel.
*/
void
CheckValidResultRel(ResultRelInfo *resultRelInfo, CmdType operation)
CheckValidResultRel(ResultRelInfo *resultRelInfo, CmdType operation,
List *mergeActions)
{
Relation resultRel = resultRelInfo->ri_RelationDesc;
TriggerDesc *trigDesc = resultRel->trigdesc;
FdwRoutine *fdwroutine;
switch (resultRel->rd_rel->relkind)
@@ -1048,42 +1053,14 @@ CheckValidResultRel(ResultRelInfo *resultRelInfo, CmdType operation)
case RELKIND_VIEW:
/*
* Okay only if there's a suitable INSTEAD OF trigger. Messages
* here should match rewriteHandler.c's rewriteTargetView and
* RewriteQuery, except that we omit errdetail because we haven't
* got the information handy (and given that we really shouldn't
* get here anyway, it's not worth great exertion to get).
* Okay only if there's a suitable INSTEAD OF trigger. Otherwise,
* complain, but omit errdetail because we haven't got the
* information handy (and given that it really shouldn't happen,
* it's not worth great exertion to get).
*/
switch (operation)
{
case CMD_INSERT:
if (!trigDesc || !trigDesc->trig_insert_instead_row)
ereport(ERROR,
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("cannot insert into view \"%s\"",
RelationGetRelationName(resultRel)),
errhint("To enable inserting into the view, provide an INSTEAD OF INSERT trigger or an unconditional ON INSERT DO INSTEAD rule.")));
break;
case CMD_UPDATE:
if (!trigDesc || !trigDesc->trig_update_instead_row)
ereport(ERROR,
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("cannot update view \"%s\"",
RelationGetRelationName(resultRel)),
errhint("To enable updating the view, provide an INSTEAD OF UPDATE trigger or an unconditional ON UPDATE DO INSTEAD rule.")));
break;
case CMD_DELETE:
if (!trigDesc || !trigDesc->trig_delete_instead_row)
ereport(ERROR,
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("cannot delete from view \"%s\"",
RelationGetRelationName(resultRel)),
errhint("To enable deleting from the view, provide an INSTEAD OF DELETE trigger or an unconditional ON DELETE DO INSTEAD rule.")));
break;
default:
elog(ERROR, "unrecognized CmdType: %d", (int) operation);
break;
}
if (!view_has_instead_trigger(resultRel, operation, mergeActions))
error_view_not_updatable(resultRel, operation, mergeActions,
NULL);
break;
case RELKIND_MATVIEW:
if (!MatViewIncrementalMaintenanceIsEnabled())

View File

@@ -361,7 +361,7 @@ ExecFindPartition(ModifyTableState *mtstate,
if (rri)
{
/* Verify this ResultRelInfo allows INSERTs */
CheckValidResultRel(rri, CMD_INSERT);
CheckValidResultRel(rri, CMD_INSERT, NIL);
/*
* Initialize information needed to insert this and
@@ -527,7 +527,7 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
* partition-key becomes a DELETE+INSERT operation, so this check is still
* required when the operation is CMD_UPDATE.
*/
CheckValidResultRel(leaf_part_rri, CMD_INSERT);
CheckValidResultRel(leaf_part_rri, CMD_INSERT, NIL);
/*
* Open partition indices. The user may have asked to check for conflicts

View File

@@ -150,11 +150,13 @@ static TupleTableSlot *ExecPrepareTupleRouting(ModifyTableState *mtstate,
static TupleTableSlot *ExecMerge(ModifyTableContext *context,
ResultRelInfo *resultRelInfo,
ItemPointer tupleid,
HeapTuple oldtuple,
bool canSetTag);
static void ExecInitMerge(ModifyTableState *mtstate, EState *estate);
static bool ExecMergeMatched(ModifyTableContext *context,
ResultRelInfo *resultRelInfo,
ItemPointer tupleid,
HeapTuple oldtuple,
bool canSetTag);
static void ExecMergeNotMatched(ModifyTableContext *context,
ResultRelInfo *resultRelInfo,
@@ -2712,13 +2714,14 @@ ExecOnConflictUpdate(ModifyTableContext *context,
*/
static TupleTableSlot *
ExecMerge(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
ItemPointer tupleid, bool canSetTag)
ItemPointer tupleid, HeapTuple oldtuple, bool canSetTag)
{
bool matched;
/*-----
* If we are dealing with a WHEN MATCHED case (tupleid is valid), 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.
*
@@ -2759,9 +2762,10 @@ ExecMerge(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
* from ExecMergeNotMatched to ExecMergeMatched, there is no risk of a
* livelock.
*/
matched = tupleid != NULL;
matched = tupleid != NULL || oldtuple != NULL;
if (matched)
matched = ExecMergeMatched(context, resultRelInfo, tupleid, canSetTag);
matched = ExecMergeMatched(context, resultRelInfo, tupleid, oldtuple,
canSetTag);
/*
* Either we were dealing with a NOT MATCHED tuple or ExecMergeMatched()
@@ -2776,8 +2780,10 @@ ExecMerge(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
}
/*
* Check and execute the first qualifying MATCHED action. The current target
* tuple is identified by tupleid.
* Check and execute the first qualifying MATCHED action. 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
@@ -2798,7 +2804,7 @@ ExecMerge(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
*/
static bool
ExecMergeMatched(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
ItemPointer tupleid, bool canSetTag)
ItemPointer tupleid, HeapTuple oldtuple, bool canSetTag)
{
ModifyTableState *mtstate = context->mtstate;
TupleTableSlot *newslot;
@@ -2824,22 +2830,33 @@ ExecMergeMatched(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
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.
*/
Assert(tupleid != NULL || oldtuple != NULL);
if (oldtuple != NULL)
ExecForceStoreHeapTuple(oldtuple, resultRelInfo->ri_oldTupleSlot,
false);
lmerge_matched:
/*
* This routine is only invoked for matched rows, and we must have found
* the tupleid of the target row in that case; fetch that tuple.
* If passed a tupleid, use it to fetch the old target row.
*
* 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.
*/
if (!table_tuple_fetch_row_version(resultRelInfo->ri_RelationDesc,
tupleid,
SnapshotAny,
resultRelInfo->ri_oldTupleSlot))
elog(ERROR, "failed to fetch the target tuple");
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");
}
foreach(l, resultRelInfo->ri_matchedMergeAction)
{
@@ -2899,20 +2916,33 @@ lmerge_matched:
return true; /* "do nothing" */
break; /* concurrent update/delete */
}
result = ExecUpdateAct(context, resultRelInfo, tupleid, NULL,
newslot, canSetTag, &updateCxt);
/*
* As in ExecUpdate(), if ExecUpdateAct() reports that a
* cross-partition update was done, then there's nothing else
* for us to do --- the UPDATE has been turned into a DELETE
* and an INSERT, and we must not perform any of the usual
* post-update tasks.
*/
if (updateCxt.crossPartUpdate)
/* INSTEAD OF ROW UPDATE Triggers */
if (resultRelInfo->ri_TrigDesc &&
resultRelInfo->ri_TrigDesc->trig_update_instead_row)
{
mtstate->mt_merge_updated += 1;
return true;
if (!ExecIRUpdateTriggers(estate, resultRelInfo,
oldtuple, newslot))
return true; /* "do nothing" */
}
else
{
result = ExecUpdateAct(context, resultRelInfo, tupleid,
NULL, newslot, canSetTag,
&updateCxt);
/*
* As in ExecUpdate(), if ExecUpdateAct() reports that a
* cross-partition update was done, then there's nothing
* else for us to do --- the UPDATE has been turned into a
* DELETE and an INSERT, and we must not perform any of
* the usual post-update tasks.
*/
if (updateCxt.crossPartUpdate)
{
mtstate->mt_merge_updated += 1;
return true;
}
}
if (result == TM_Ok)
@@ -2932,7 +2962,19 @@ lmerge_matched:
return true; /* "do nothing" */
break; /* concurrent update/delete */
}
result = ExecDeleteAct(context, resultRelInfo, tupleid, false);
/* INSTEAD OF ROW DELETE Triggers */
if (resultRelInfo->ri_TrigDesc &&
resultRelInfo->ri_TrigDesc->trig_delete_instead_row)
{
if (!ExecIRDeleteTriggers(estate, resultRelInfo,
oldtuple))
return true; /* "do nothing" */
}
else
result = ExecDeleteAct(context, resultRelInfo, tupleid,
false);
if (result == TM_Ok)
{
ExecDeleteEpilogue(context, resultRelInfo, tupleid, NULL,
@@ -3663,7 +3705,8 @@ ExecModifyTable(PlanState *pstate)
{
EvalPlanQualSetSlot(&node->mt_epqstate, context.planSlot);
ExecMerge(&context, node->resultRelInfo, NULL, node->canSetTag);
ExecMerge(&context, node->resultRelInfo, NULL, NULL,
node->canSetTag);
continue; /* no RETURNING support yet */
}
@@ -3741,7 +3784,8 @@ ExecModifyTable(PlanState *pstate)
{
EvalPlanQualSetSlot(&node->mt_epqstate, context.planSlot);
ExecMerge(&context, node->resultRelInfo, NULL, node->canSetTag);
ExecMerge(&context, node->resultRelInfo, NULL, NULL,
node->canSetTag);
continue; /* no RETURNING support yet */
}
@@ -3774,9 +3818,28 @@ ExecModifyTable(PlanState *pstate)
datum = ExecGetJunkAttribute(slot,
resultRelInfo->ri_RowIdAttNo,
&isNull);
/* shouldn't ever get a null result... */
/*
* For commands other than MERGE, any tuples having a null row
* identifier are errors. For MERGE, we may need to handle
* them as WHEN NOT MATCHED clauses if any, so do that.
*
* Note that we use the node's toplevel resultRelInfo, not any
* specific partition's.
*/
if (isNull)
{
if (operation == CMD_MERGE)
{
EvalPlanQualSetSlot(&node->mt_epqstate, context.planSlot);
ExecMerge(&context, node->resultRelInfo, NULL, NULL,
node->canSetTag);
continue; /* no RETURNING support yet */
}
elog(ERROR, "wholerow is NULL");
}
oldtupdata.t_data = DatumGetHeapTupleHeader(datum);
oldtupdata.t_len =
@@ -3847,7 +3910,8 @@ ExecModifyTable(PlanState *pstate)
break;
case CMD_MERGE:
slot = ExecMerge(&context, resultRelInfo, tupleid, node->canSetTag);
slot = ExecMerge(&context, resultRelInfo, tupleid, oldtuple,
node->canSetTag);
break;
default:
@@ -4025,6 +4089,10 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
foreach(l, node->resultRelations)
{
Index resultRelation = lfirst_int(l);
List *mergeActions = NIL;
if (node->mergeActionLists)
mergeActions = list_nth(node->mergeActionLists, i);
if (resultRelInfo != mtstate->rootResultRelInfo)
{
@@ -4046,7 +4114,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
/*
* Verify result relation is a valid target for the current operation
*/
CheckValidResultRel(resultRelInfo, operation);
CheckValidResultRel(resultRelInfo, operation, mergeActions);
resultRelInfo++;
i++;
@@ -4122,8 +4190,6 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
}
else
{
/* No support for MERGE */
Assert(operation != CMD_MERGE);
/* Other valid target relkinds must provide wholerow */
resultRelInfo->ri_RowIdAttNo =
ExecFindJunkAttributeInTlist(subplan->targetlist,

View File

@@ -198,12 +198,17 @@ transform_MERGE_to_join(Query *parse)
/*
* Create a JOIN between the target and the source relation.
*
* Here the target is identified by parse->mergeTargetRelation. For a
* regular table, this will equal parse->resultRelation, but for a
* trigger-updatable view, it will be the expanded view subquery that we
* need to pull data from.
*/
joinexpr = makeNode(JoinExpr);
joinexpr->jointype = jointype;
joinexpr->isNatural = false;
joinexpr->larg = (Node *) makeNode(RangeTblRef);
((RangeTblRef *) joinexpr->larg)->rtindex = parse->resultRelation;
((RangeTblRef *) joinexpr->larg)->rtindex = parse->mergeTargetRelation;
joinexpr->rarg = linitial(parse->jointree->fromlist); /* original join */
joinexpr->usingClause = NIL;
joinexpr->join_using_alias = NULL;
@@ -215,6 +220,19 @@ transform_MERGE_to_join(Query *parse)
/* Make the new join be the sole entry in the query's jointree */
parse->jointree->fromlist = list_make1(joinexpr);
parse->jointree->quals = NULL;
/*
* If necessary, mark parse->targetlist entries that refer to the target
* as nullable by the join. Normally the targetlist will be empty for a
* MERGE, but if the target is a trigger-updatable view, it will contain a
* whole-row Var referring to the expanded view query.
*/
if (parse->targetList != NIL &&
(jointype == JOIN_RIGHT || jointype == JOIN_FULL))
parse->targetList = (List *)
add_nulling_relids((Node *) parse->targetList,
bms_make_singleton(parse->mergeTargetRelation),
bms_make_singleton(joinrti));
}
/*

View File

@@ -891,8 +891,7 @@ add_row_identity_columns(PlannerInfo *root, Index rtindex,
Assert(commandType == CMD_UPDATE || commandType == CMD_DELETE || commandType == CMD_MERGE);
if (commandType == CMD_MERGE ||
relkind == RELKIND_RELATION ||
if (relkind == RELKIND_RELATION ||
relkind == RELKIND_MATVIEW ||
relkind == RELKIND_PARTITIONED_TABLE)
{

View File

@@ -172,28 +172,27 @@ transformMergeStmt(ParseState *pstate, MergeStmt *stmt)
* 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.
*
* Initially mergeTargetRelation is the same as resultRelation, so data is
* read from the table being updated. However, that might be changed by
* the rewriter, if the target is a trigger-updatable view, to allow
* target data to be read from the expanded view query while updating the
* original view relation.
*/
qry->resultRelation = setTargetTable(pstate, stmt->relation,
stmt->relation->inh,
false, targetPerms);
qry->mergeTargetRelation = qry->resultRelation;
/*
* MERGE is unsupported in various cases
*/
/* The target relation must be a table or a view */
if (pstate->p_target_relation->rd_rel->relkind != RELKIND_RELATION &&
pstate->p_target_relation->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
pstate->p_target_relation->rd_rel->relkind != RELKIND_PARTITIONED_TABLE &&
pstate->p_target_relation->rd_rel->relkind != RELKIND_VIEW)
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,

View File

@@ -87,10 +87,9 @@ static void rewriteValuesRTEToNulls(Query *parsetree, RangeTblEntry *rte);
static void markQueryForLocking(Query *qry, Node *jtnode,
LockClauseStrength strength, LockWaitPolicy waitPolicy,
bool pushedDown);
static List *matchLocks(CmdType event, RuleLock *rulelocks,
static List *matchLocks(CmdType event, Relation relation,
int varno, Query *parsetree, bool *hasUpdate);
static Query *fireRIRrules(Query *parsetree, List *activeRIRs);
static bool view_has_instead_trigger(Relation view, CmdType event);
static Bitmapset *adjust_view_column_set(Bitmapset *cols, List *targetlist);
@@ -1482,7 +1481,7 @@ rewriteValuesRTE(Query *parsetree, RangeTblEntry *rte, int rti,
*/
isAutoUpdatableView = false;
if (target_relation->rd_rel->relkind == RELKIND_VIEW &&
!view_has_instead_trigger(target_relation, CMD_INSERT))
!view_has_instead_trigger(target_relation, CMD_INSERT, NIL))
{
List *locks;
bool hasUpdate;
@@ -1490,7 +1489,7 @@ rewriteValuesRTE(Query *parsetree, RangeTblEntry *rte, int rti,
ListCell *l;
/* Look for an unconditional DO INSTEAD rule */
locks = matchLocks(CMD_INSERT, target_relation->rd_rules,
locks = matchLocks(CMD_INSERT, target_relation,
parsetree->resultRelation, parsetree, &hasUpdate);
found = false;
@@ -1654,15 +1653,16 @@ rewriteValuesRTEToNulls(Query *parsetree, RangeTblEntry *rte)
/*
* matchLocks -
* match the list of locks and returns the matching rules
* match a relation's list of locks and returns the matching rules
*/
static List *
matchLocks(CmdType event,
RuleLock *rulelocks,
Relation relation,
int varno,
Query *parsetree,
bool *hasUpdate)
{
RuleLock *rulelocks = relation->rd_rules;
List *matching_locks = NIL;
int nlocks;
int i;
@@ -1670,10 +1670,6 @@ matchLocks(CmdType event,
if (rulelocks == NULL)
return NIL;
/* No rule support for MERGE */
if (parsetree->commandType == CMD_MERGE)
return NIL;
if (parsetree->commandType != CMD_SELECT)
{
if (parsetree->resultRelation != varno)
@@ -1691,7 +1687,7 @@ matchLocks(CmdType event,
/*
* Suppress ON INSERT/UPDATE/DELETE rules that are disabled or
* configured to not fire during the current sessions replication
* configured to not fire during the current session's replication
* role. ON SELECT rules will always be applied in order to keep views
* working even in LOCAL or REPLICA role.
*/
@@ -1709,6 +1705,14 @@ matchLocks(CmdType event,
oneLock->enabled == RULE_DISABLED)
continue;
}
/* Non-SELECT rules are not supported for MERGE */
if (parsetree->commandType == CMD_MERGE)
ereport(ERROR,
errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot execute MERGE on relation \"%s\"",
RelationGetRelationName(relation)),
errdetail("MERGE is not supported for relations with rules."));
}
if (oneLock->event == event)
@@ -1755,9 +1759,9 @@ ApplyRetrieveRule(Query *parsetree,
* For INSERT, we needn't do anything. The unmodified RTE will serve
* fine as the result relation.
*
* For UPDATE/DELETE, we need to expand the view so as to have source
* data for the operation. But we also need an unmodified RTE to
* serve as the target. So, copy the RTE and add the copy to the
* For UPDATE/DELETE/MERGE, we need to expand the view so as to have
* source data for the operation. But we also need an unmodified RTE
* to serve as the target. So, copy the RTE and add the copy to the
* rangetable. Note that the copy does not get added to the jointree.
* Also note that there's a hack in fireRIRrules to avoid calling this
* function again when it arrives at the copied RTE.
@@ -1765,7 +1769,8 @@ ApplyRetrieveRule(Query *parsetree,
if (parsetree->commandType == CMD_INSERT)
return parsetree;
else if (parsetree->commandType == CMD_UPDATE ||
parsetree->commandType == CMD_DELETE)
parsetree->commandType == CMD_DELETE ||
parsetree->commandType == CMD_MERGE)
{
RangeTblEntry *newrte;
Var *var;
@@ -1775,6 +1780,7 @@ ApplyRetrieveRule(Query *parsetree,
newrte = copyObject(rte);
parsetree->rtable = lappend(parsetree->rtable, newrte);
parsetree->resultRelation = list_length(parsetree->rtable);
/* parsetree->mergeTargetRelation unchanged (use expanded view) */
/*
* For the most part, Vars referencing the view should remain as
@@ -2470,9 +2476,15 @@ get_view_query(Relation view)
* If it does, we don't want to treat it as auto-updatable. This test can't
* be folded into view_query_is_auto_updatable because it's not an error
* condition.
*
* For MERGE, this will return true if there is an INSTEAD OF trigger for
* every action in mergeActionList, and false if there are any actions that
* lack an INSTEAD OF trigger. If there are no data-modifying MERGE actions
* (only DO NOTHING actions), true is returned so that the view is treated
* as trigger-updatable, rather than erroring out if it's not auto-updatable.
*/
static bool
view_has_instead_trigger(Relation view, CmdType event)
bool
view_has_instead_trigger(Relation view, CmdType event, List *mergeActionList)
{
TriggerDesc *trigDesc = view->trigdesc;
@@ -2490,6 +2502,32 @@ view_has_instead_trigger(Relation view, CmdType event)
if (trigDesc && trigDesc->trig_delete_instead_row)
return true;
break;
case CMD_MERGE:
foreach_node(MergeAction, action, mergeActionList)
{
switch (action->commandType)
{
case CMD_INSERT:
if (!trigDesc || !trigDesc->trig_insert_instead_row)
return false;
break;
case CMD_UPDATE:
if (!trigDesc || !trigDesc->trig_update_instead_row)
return false;
break;
case CMD_DELETE:
if (!trigDesc || !trigDesc->trig_delete_instead_row)
return false;
break;
case CMD_NOTHING:
/* No trigger required */
break;
default:
elog(ERROR, "unrecognized commandType: %d", action->commandType);
break;
}
}
return true; /* no actions without an INSTEAD OF trigger */
default:
elog(ERROR, "unrecognized CmdType: %d", (int) event);
break;
@@ -3030,6 +3068,105 @@ adjust_view_column_set(Bitmapset *cols, List *targetlist)
}
/*
* error_view_not_updatable -
* Report an error due to an attempt to update a non-updatable view.
*
* Generally this is expected to be called from the rewriter, with suitable
* error detail explaining why the view is not updatable. Note, however, that
* the executor also performs a just-in-case check that the target view is
* updatable. That check is expected to never fail, but if it does, it will
* call this function with NULL error detail --- see CheckValidResultRel().
*
* Note: for MERGE, at least one of the actions in mergeActionList is expected
* to lack a suitable INSTEAD OF trigger --- see view_has_instead_trigger().
*/
void
error_view_not_updatable(Relation view,
CmdType command,
List *mergeActionList,
const char *detail)
{
TriggerDesc *trigDesc = view->trigdesc;
switch (command)
{
case CMD_INSERT:
ereport(ERROR,
errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("cannot insert into view \"%s\"",
RelationGetRelationName(view)),
detail ? errdetail_internal("%s", _(detail)) : 0,
errhint("To enable inserting into the view, provide an INSTEAD OF INSERT trigger or an unconditional ON INSERT DO INSTEAD rule."));
break;
case CMD_UPDATE:
ereport(ERROR,
errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("cannot update view \"%s\"",
RelationGetRelationName(view)),
detail ? errdetail_internal("%s", _(detail)) : 0,
errhint("To enable updating the view, provide an INSTEAD OF UPDATE trigger or an unconditional ON UPDATE DO INSTEAD rule."));
break;
case CMD_DELETE:
ereport(ERROR,
errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("cannot delete from view \"%s\"",
RelationGetRelationName(view)),
detail ? errdetail_internal("%s", _(detail)) : 0,
errhint("To enable deleting from the view, provide an INSTEAD OF DELETE trigger or an unconditional ON DELETE DO INSTEAD rule."));
break;
case CMD_MERGE:
/*
* Note that the error hints here differ from above, since MERGE
* doesn't support rules.
*/
foreach_node(MergeAction, action, mergeActionList)
{
switch (action->commandType)
{
case CMD_INSERT:
if (!trigDesc || !trigDesc->trig_insert_instead_row)
ereport(ERROR,
errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("cannot insert into view \"%s\"",
RelationGetRelationName(view)),
detail ? errdetail_internal("%s", _(detail)) : 0,
errhint("To enable inserting into the view using MERGE, provide an INSTEAD OF INSERT trigger."));
break;
case CMD_UPDATE:
if (!trigDesc || !trigDesc->trig_update_instead_row)
ereport(ERROR,
errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("cannot update view \"%s\"",
RelationGetRelationName(view)),
detail ? errdetail_internal("%s", _(detail)) : 0,
errhint("To enable updating the view using MERGE, provide an INSTEAD OF UPDATE trigger."));
break;
case CMD_DELETE:
if (!trigDesc || !trigDesc->trig_delete_instead_row)
ereport(ERROR,
errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("cannot delete from view \"%s\"",
RelationGetRelationName(view)),
detail ? errdetail_internal("%s", _(detail)) : 0,
errhint("To enable deleting from the view using MERGE, provide an INSTEAD OF DELETE trigger."));
break;
case CMD_NOTHING:
break;
default:
elog(ERROR, "unrecognized commandType: %d", action->commandType);
break;
}
}
break;
default:
elog(ERROR, "unrecognized CmdType: %d", (int) command);
break;
}
}
/*
* rewriteTargetView -
* Attempt to rewrite a query where the target relation is a view, so that
@@ -3043,6 +3180,7 @@ static Query *
rewriteTargetView(Query *parsetree, Relation view)
{
Query *viewquery;
bool insert_or_update;
const char *auto_update_detail;
RangeTblRef *rtr;
int base_rt_index;
@@ -3066,55 +3204,52 @@ rewriteTargetView(Query *parsetree, Relation view)
*/
viewquery = copyObject(get_view_query(view));
/* The view must be updatable, else fail */
auto_update_detail =
view_query_is_auto_updatable(viewquery,
parsetree->commandType != CMD_DELETE);
/*
* Are we doing INSERT/UPDATE, or MERGE containing INSERT/UPDATE? If so,
* various additional checks on the view columns need to be applied, and
* any view CHECK OPTIONs need to be enforced.
*/
insert_or_update =
(parsetree->commandType == CMD_INSERT ||
parsetree->commandType == CMD_UPDATE);
if (auto_update_detail)
if (parsetree->commandType == CMD_MERGE)
{
/* messages here should match execMain.c's CheckValidResultRel */
switch (parsetree->commandType)
foreach_node(MergeAction, action, parsetree->mergeActionList)
{
case CMD_INSERT:
ereport(ERROR,
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("cannot insert into view \"%s\"",
RelationGetRelationName(view)),
errdetail_internal("%s", _(auto_update_detail)),
errhint("To enable inserting into the view, provide an INSTEAD OF INSERT trigger or an unconditional ON INSERT DO INSTEAD rule.")));
break;
case CMD_UPDATE:
ereport(ERROR,
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("cannot update view \"%s\"",
RelationGetRelationName(view)),
errdetail_internal("%s", _(auto_update_detail)),
errhint("To enable updating the view, provide an INSTEAD OF UPDATE trigger or an unconditional ON UPDATE DO INSTEAD rule.")));
break;
case CMD_DELETE:
ereport(ERROR,
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("cannot delete from view \"%s\"",
RelationGetRelationName(view)),
errdetail_internal("%s", _(auto_update_detail)),
errhint("To enable deleting from the view, provide an INSTEAD OF DELETE trigger or an unconditional ON DELETE DO INSTEAD rule.")));
break;
default:
elog(ERROR, "unrecognized CmdType: %d",
(int) parsetree->commandType);
if (action->commandType == CMD_INSERT ||
action->commandType == CMD_UPDATE)
{
insert_or_update = true;
break;
}
}
}
/*
* For INSERT/UPDATE the modified columns must all be updatable. Note that
* we get the modified columns from the query's targetlist, not from the
* result RTE's insertedCols and/or updatedCols set, since
* rewriteTargetListIU may have added additional targetlist entries for
* view defaults, and these must also be updatable.
* The view must be updatable, else fail.
*
* If we are doing INSERT/UPDATE (or MERGE containing INSERT/UPDATE), we
* also check that there is at least one updatable column.
*/
if (parsetree->commandType != CMD_DELETE)
auto_update_detail =
view_query_is_auto_updatable(viewquery, insert_or_update);
if (auto_update_detail)
error_view_not_updatable(view,
parsetree->commandType,
parsetree->mergeActionList,
auto_update_detail);
/*
* For INSERT/UPDATE (or MERGE containing INSERT/UPDATE) the modified
* columns must all be updatable. Note that we get the modified columns
* from the query's targetlist, not from the result RTE's insertedCols
* and/or updatedCols set, since rewriteTargetListIU may have added
* additional targetlist entries for view defaults, and these must also be
* updatable.
*/
if (insert_or_update)
{
Bitmapset *modified_cols = NULL;
char *non_updatable_col;
@@ -3140,6 +3275,20 @@ rewriteTargetView(Query *parsetree, Relation view)
}
}
foreach_node(MergeAction, action, parsetree->mergeActionList)
{
if (action->commandType == CMD_INSERT ||
action->commandType == CMD_UPDATE)
{
foreach_node(TargetEntry, tle, action->targetList)
{
if (!tle->resjunk)
modified_cols = bms_add_member(modified_cols,
tle->resno - FirstLowInvalidHeapAttributeNumber);
}
}
}
auto_update_detail = view_cols_are_auto_updatable(viewquery,
modified_cols,
NULL,
@@ -3168,6 +3317,14 @@ rewriteTargetView(Query *parsetree, Relation view)
RelationGetRelationName(view)),
errdetail_internal("%s", _(auto_update_detail))));
break;
case CMD_MERGE:
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot merge into column \"%s\" of view \"%s\"",
non_updatable_col,
RelationGetRelationName(view)),
errdetail_internal("%s", _(auto_update_detail))));
break;
default:
elog(ERROR, "unrecognized CmdType: %d",
(int) parsetree->commandType);
@@ -3176,6 +3333,28 @@ rewriteTargetView(Query *parsetree, Relation view)
}
}
/*
* For MERGE, there must not be any INSTEAD OF triggers on an otherwise
* updatable view. The caller already checked that there isn't a full set
* of INSTEAD OF triggers, so this is to guard against having a partial
* set (mixing auto-update and trigger-update actions in a single command
* isn't supported).
*/
if (parsetree->commandType == CMD_MERGE)
{
foreach_node(MergeAction, action, parsetree->mergeActionList)
{
if (action->commandType != CMD_NOTHING &&
view_has_instead_trigger(view, action->commandType, NIL))
ereport(ERROR,
errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot merge into view \"%s\"",
RelationGetRelationName(view)),
errdetail("MERGE is not supported for views with INSTEAD OF triggers for some actions, but not others."),
errhint("To enable merging into the view, either provide a full set of INSTEAD OF triggers or drop the existing INSTEAD OF triggers."));
}
}
/* Locate RTE describing the view in the outer query */
view_rte = rt_fetch(parsetree->resultRelation, parsetree->rtable);
@@ -3239,8 +3418,8 @@ rewriteTargetView(Query *parsetree, Relation view)
new_rt_index = list_length(parsetree->rtable);
/*
* INSERTs never inherit. For UPDATE/DELETE, we use the view query's
* inheritance flag for the base relation.
* INSERTs never inherit. For UPDATE/DELETE/MERGE, we use the view
* query's inheritance flag for the base relation.
*/
if (parsetree->commandType == CMD_INSERT)
new_rte->inh = false;
@@ -3362,11 +3541,12 @@ rewriteTargetView(Query *parsetree, Relation view)
/*
* For INSERT/UPDATE we must also update resnos in the targetlist to refer
* to columns of the base relation, since those indicate the target
* columns to be affected.
* columns to be affected. Similarly, for MERGE we must update the resnos
* in the merge action targetlists of any INSERT/UPDATE actions.
*
* Note that this destroys the resno ordering of the targetlist, but that
* Note that this destroys the resno ordering of the targetlists, but that
* will be fixed when we recurse through RewriteQuery, which will invoke
* rewriteTargetListIU again on the updated targetlist.
* rewriteTargetListIU again on the updated targetlists.
*/
if (parsetree->commandType != CMD_DELETE)
{
@@ -3385,6 +3565,28 @@ rewriteTargetView(Query *parsetree, Relation view)
elog(ERROR, "attribute number %d not found in view targetlist",
tle->resno);
}
foreach_node(MergeAction, action, parsetree->mergeActionList)
{
if (action->commandType == CMD_INSERT ||
action->commandType == CMD_UPDATE)
{
foreach_node(TargetEntry, tle, action->targetList)
{
TargetEntry *view_tle;
if (tle->resjunk)
continue;
view_tle = get_tle_by_resno(view_targetlist, tle->resno);
if (view_tle != NULL && !view_tle->resjunk && IsA(view_tle->expr, Var))
tle->resno = ((Var *) view_tle->expr)->varattno;
else
elog(ERROR, "attribute number %d not found in view targetlist",
tle->resno);
}
}
}
}
/*
@@ -3477,10 +3679,10 @@ rewriteTargetView(Query *parsetree, Relation view)
}
/*
* For UPDATE/DELETE, pull up any WHERE quals from the view. We know that
* any Vars in the quals must reference the one base relation, so we need
* only adjust their varnos to reference the new target (just the same as
* we did with the view targetlist).
* For UPDATE/DELETE/MERGE, pull up any WHERE quals from the view. We
* know that any Vars in the quals must reference the one base relation,
* so we need only adjust their varnos to reference the new target (just
* the same as we did with the view targetlist).
*
* If it's a security-barrier view, its WHERE quals must be applied before
* quals from the outer query, so we attach them to the RTE as security
@@ -3532,11 +3734,12 @@ rewriteTargetView(Query *parsetree, Relation view)
}
/*
* For INSERT/UPDATE, if the view has the WITH CHECK OPTION, or any parent
* view specified WITH CASCADED CHECK OPTION, add the quals from the view
* to the query's withCheckOptions list.
* For INSERT/UPDATE (or MERGE containing INSERT/UPDATE), if the view has
* the WITH CHECK OPTION, or any parent view specified WITH CASCADED CHECK
* OPTION, add the quals from the view to the query's withCheckOptions
* list.
*/
if (parsetree->commandType != CMD_DELETE)
if (insert_or_update)
{
bool has_wco = RelationHasCheckOption(view);
bool cascaded = RelationHasCascadedCheckOption(view);
@@ -3590,14 +3793,13 @@ rewriteTargetView(Query *parsetree, Relation view)
ChangeVarNodes(wco->qual, base_rt_index, new_rt_index, 0);
/*
* Make sure that the query is marked correctly if the added
* qual has sublinks. We can skip this check if the query is
* already marked, or if the command is an UPDATE, in which
* case the same qual will have already been added, and this
* check will already have been done.
* For INSERT, make sure that the query is marked correctly if
* the added qual has sublinks. This can be skipped for
* UPDATE/MERGE, since the same qual will have already been
* added above, and the check will already have been done.
*/
if (!parsetree->hasSubLinks &&
parsetree->commandType != CMD_UPDATE)
parsetree->commandType == CMD_INSERT)
parsetree->hasSubLinks = checkExprHasSubLink(wco->qual);
}
}
@@ -3867,7 +4069,7 @@ RewriteQuery(Query *parsetree, List *rewrite_events, int orig_rt_length)
/*
* Collect and apply the appropriate rules.
*/
locks = matchLocks(event, rt_entry_relation->rd_rules,
locks = matchLocks(event, rt_entry_relation,
result_relation, parsetree, &hasUpdate);
product_orig_rt_length = list_length(parsetree->rtable);
@@ -3938,7 +4140,8 @@ RewriteQuery(Query *parsetree, List *rewrite_events, int orig_rt_length)
* automatically updated. If so, we perform the necessary query
* transformation here and add the resulting query to the
* product_queries list, so that it gets recursively rewritten if
* necessary.
* necessary. For MERGE, the view must be automatically updatable if
* any of the merge actions lack a corresponding INSTEAD OF trigger.
*
* If the view cannot be automatically updated, we throw an error here
* which is OK since the query would fail at runtime anyway. Throwing
@@ -3948,51 +4151,19 @@ RewriteQuery(Query *parsetree, List *rewrite_events, int orig_rt_length)
*/
if (!instead &&
rt_entry_relation->rd_rel->relkind == RELKIND_VIEW &&
!view_has_instead_trigger(rt_entry_relation, event))
!view_has_instead_trigger(rt_entry_relation, event,
parsetree->mergeActionList))
{
/*
* If there were any qualified INSTEAD rules, don't allow the view
* to be automatically updated (an unqualified INSTEAD rule or
* INSTEAD OF trigger is required).
*
* The messages here should match execMain.c's CheckValidResultRel
* and in principle make those checks in executor unnecessary, but
* we keep them just in case.
*/
if (qual_product != NULL)
{
switch (parsetree->commandType)
{
case CMD_INSERT:
ereport(ERROR,
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("cannot insert into view \"%s\"",
RelationGetRelationName(rt_entry_relation)),
errdetail("Views with conditional DO INSTEAD rules are not automatically updatable."),
errhint("To enable inserting into the view, provide an INSTEAD OF INSERT trigger or an unconditional ON INSERT DO INSTEAD rule.")));
break;
case CMD_UPDATE:
ereport(ERROR,
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("cannot update view \"%s\"",
RelationGetRelationName(rt_entry_relation)),
errdetail("Views with conditional DO INSTEAD rules are not automatically updatable."),
errhint("To enable updating the view, provide an INSTEAD OF UPDATE trigger or an unconditional ON UPDATE DO INSTEAD rule.")));
break;
case CMD_DELETE:
ereport(ERROR,
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("cannot delete from view \"%s\"",
RelationGetRelationName(rt_entry_relation)),
errdetail("Views with conditional DO INSTEAD rules are not automatically updatable."),
errhint("To enable deleting from the view, provide an INSTEAD OF DELETE trigger or an unconditional ON DELETE DO INSTEAD rule.")));
break;
default:
elog(ERROR, "unrecognized CmdType: %d",
(int) parsetree->commandType);
break;
}
}
error_view_not_updatable(rt_entry_relation,
parsetree->commandType,
parsetree->mergeActionList,
gettext_noop("Views with conditional DO INSTEAD rules are not automatically updatable."));
/*
* Attempt to rewrite the query to automatically update the view.

View File

@@ -496,9 +496,10 @@ OffsetVarNodes(Node *node, int offset, int sublevels_up)
/*
* If we are starting at a Query, and sublevels_up is zero, then we
* must also fix rangetable indexes in the Query itself --- namely
* resultRelation, exclRelIndex and rowMarks entries. sublevels_up
* cannot be zero when recursing into a subquery, so there's no need
* to have the same logic inside OffsetVarNodes_walker.
* resultRelation, mergeTargetRelation, exclRelIndex and rowMarks
* entries. sublevels_up cannot be zero when recursing into a
* subquery, so there's no need to have the same logic inside
* OffsetVarNodes_walker.
*/
if (sublevels_up == 0)
{
@@ -507,6 +508,9 @@ OffsetVarNodes(Node *node, int offset, int sublevels_up)
if (qry->resultRelation)
qry->resultRelation += offset;
if (qry->mergeTargetRelation)
qry->mergeTargetRelation += offset;
if (qry->onConflict && qry->onConflict->exclRelIndex)
qry->onConflict->exclRelIndex += offset;
@@ -687,9 +691,10 @@ ChangeVarNodes(Node *node, int rt_index, int new_index, int sublevels_up)
/*
* If we are starting at a Query, and sublevels_up is zero, then we
* must also fix rangetable indexes in the Query itself --- namely
* resultRelation and rowMarks entries. sublevels_up cannot be zero
* when recursing into a subquery, so there's no need to have the same
* logic inside ChangeVarNodes_walker.
* resultRelation, mergeTargetRelation, exclRelIndex and rowMarks
* entries. sublevels_up cannot be zero when recursing into a
* subquery, so there's no need to have the same logic inside
* ChangeVarNodes_walker.
*/
if (sublevels_up == 0)
{
@@ -698,6 +703,9 @@ ChangeVarNodes(Node *node, int rt_index, int new_index, int sublevels_up)
if (qry->resultRelation == rt_index)
qry->resultRelation = new_index;
if (qry->mergeTargetRelation == rt_index)
qry->mergeTargetRelation = new_index;
/* this is unlikely to ever be used, but ... */
if (qry->onConflict && qry->onConflict->exclRelIndex == rt_index)
qry->onConflict->exclRelIndex = new_index;