*** a/contrib/postgres_fdw/expected/postgres_fdw.out --- b/contrib/postgres_fdw/expected/postgres_fdw.out *************** *** 6924,6929 **** update bar set f2 = f2 + 100 returning *; --- 6924,6988 ---- 7 | 277 (6 rows) + -- Test that UPDATE/DELETE with inherited target works with row-level triggers + CREATE TRIGGER trig_row_before + BEFORE UPDATE OR DELETE ON bar2 + FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo'); + CREATE TRIGGER trig_row_after + AFTER UPDATE OR DELETE ON bar2 + FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo'); + explain (verbose, costs off) + update bar set f2 = f2 + 100; + QUERY PLAN + -------------------------------------------------------------------------------------- + Update on public.bar + Update on public.bar + Foreign Update on public.bar2 + Remote SQL: UPDATE public.loct2 SET f2 = $2 WHERE ctid = $1 RETURNING f1, f2, f3 + -> Seq Scan on public.bar + Output: bar.f1, (bar.f2 + 100), bar.ctid + -> Foreign Scan on public.bar2 + Output: bar2.f1, (bar2.f2 + 100), bar2.f3, bar2.ctid, bar2.* + Remote SQL: SELECT f1, f2, f3, ctid FROM public.loct2 FOR UPDATE + (9 rows) + + update bar set f2 = f2 + 100; + NOTICE: trig_row_before(23, skidoo) BEFORE ROW UPDATE ON bar2 + NOTICE: OLD: (3,333,33),NEW: (3,433,33) + NOTICE: trig_row_before(23, skidoo) BEFORE ROW UPDATE ON bar2 + NOTICE: OLD: (4,344,44),NEW: (4,444,44) + NOTICE: trig_row_before(23, skidoo) BEFORE ROW UPDATE ON bar2 + NOTICE: OLD: (7,277,77),NEW: (7,377,77) + NOTICE: trig_row_after(23, skidoo) AFTER ROW UPDATE ON bar2 + NOTICE: OLD: (3,333,33),NEW: (3,433,33) + NOTICE: trig_row_after(23, skidoo) AFTER ROW UPDATE ON bar2 + NOTICE: OLD: (4,344,44),NEW: (4,444,44) + NOTICE: trig_row_after(23, skidoo) AFTER ROW UPDATE ON bar2 + NOTICE: OLD: (7,277,77),NEW: (7,377,77) + explain (verbose, costs off) + delete from bar where f2 < 400; + QUERY PLAN + --------------------------------------------------------------------------------------------- + Delete on public.bar + Delete on public.bar + Foreign Delete on public.bar2 + Remote SQL: DELETE FROM public.loct2 WHERE ctid = $1 RETURNING f1, f2, f3 + -> Seq Scan on public.bar + Output: bar.ctid + Filter: (bar.f2 < 400) + -> Foreign Scan on public.bar2 + Output: bar2.ctid, bar2.* + Remote SQL: SELECT f1, f2, f3, ctid FROM public.loct2 WHERE ((f2 < 400)) FOR UPDATE + (10 rows) + + delete from bar where f2 < 400; + NOTICE: trig_row_before(23, skidoo) BEFORE ROW DELETE ON bar2 + NOTICE: OLD: (7,377,77) + NOTICE: trig_row_after(23, skidoo) AFTER ROW DELETE ON bar2 + NOTICE: OLD: (7,377,77) + -- cleanup + DROP TRIGGER trig_row_before ON bar2; + DROP TRIGGER trig_row_after ON bar2; drop table foo cascade; NOTICE: drop cascades to foreign table foo2 drop table bar cascade; *** a/contrib/postgres_fdw/sql/postgres_fdw.sql --- b/contrib/postgres_fdw/sql/postgres_fdw.sql *************** *** 1609,1614 **** explain (verbose, costs off) --- 1609,1634 ---- update bar set f2 = f2 + 100 returning *; update bar set f2 = f2 + 100 returning *; + -- Test that UPDATE/DELETE with inherited target works with row-level triggers + CREATE TRIGGER trig_row_before + BEFORE UPDATE OR DELETE ON bar2 + FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo'); + + CREATE TRIGGER trig_row_after + AFTER UPDATE OR DELETE ON bar2 + FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo'); + + explain (verbose, costs off) + update bar set f2 = f2 + 100; + update bar set f2 = f2 + 100; + + explain (verbose, costs off) + delete from bar where f2 < 400; + delete from bar where f2 < 400; + + -- cleanup + DROP TRIGGER trig_row_before ON bar2; + DROP TRIGGER trig_row_after ON bar2; drop table foo cascade; drop table bar cascade; drop table loct1; *** a/doc/src/sgml/rules.sgml --- b/doc/src/sgml/rules.sgml *************** *** 167,178 **** DELETE commands don't need a normal target list ! because they don't produce any result. Instead, the rule system adds a special CTID entry to the empty target list, to allow the executor to find the row to be deleted. (CTID is added when the result relation is an ordinary ! table. If it is a view, a whole-row variable is added instead, ! as described in .) --- 167,178 ---- DELETE commands don't need a normal target list ! because they don't produce any result. Instead, the planner adds a special CTID entry to the empty target list, to allow the executor to find the row to be deleted. (CTID is added when the result relation is an ordinary ! table. If it is a view, a whole-row variable is added instead, by ! the rule system, as described in .) *************** *** 194,200 **** column = expression part of the command. The planner will handle missing columns by inserting expressions that copy the values from the old row into the new one. Just as for DELETE, ! the rule system adds a CTID or whole-row variable so that the executor can identify the old row to be updated. --- 194,200 ---- column = expression part of the command. The planner will handle missing columns by inserting expressions that copy the values from the old row into the new one. Just as for DELETE, ! a CTID or whole-row variable is added so that the executor can identify the old row to be updated. *** a/src/backend/executor/nodeModifyTable.c --- b/src/backend/executor/nodeModifyTable.c *************** *** 1661,1666 **** ExecModifyTable(ModifyTableState *node) --- 1661,1667 ---- EvalPlanQualSetSlot(&node->mt_epqstate, planSlot); slot = planSlot; + tupleid = NULL; oldtuple = NULL; if (junkfilter != NULL) { *** a/src/backend/optimizer/plan/planner.c --- b/src/backend/optimizer/plan/planner.c *************** *** 1458,1464 **** grouping_planner(PlannerInfo *root, bool inheritance_update, double tuple_fraction) { Query *parse = root->parse; ! List *tlist = parse->targetList; int64 offset_est = 0; int64 count_est = 0; double limit_tuples = -1.0; --- 1458,1464 ---- double tuple_fraction) { Query *parse = root->parse; ! List *tlist; int64 offset_est = 0; int64 count_est = 0; double limit_tuples = -1.0; *************** *** 1588,1594 **** grouping_planner(PlannerInfo *root, bool inheritance_update, } /* Preprocess targetlist */ ! tlist = preprocess_targetlist(root, tlist); if (parse->onConflict) parse->onConflict->onConflictSet = --- 1588,1594 ---- } /* Preprocess targetlist */ ! tlist = preprocess_targetlist(root); if (parse->onConflict) parse->onConflict->onConflictSet = *** a/src/backend/optimizer/prep/preptlist.c --- b/src/backend/optimizer/prep/preptlist.c *************** *** 4,23 **** * Routines to preprocess the parse tree target list * * For INSERT and UPDATE queries, the targetlist must contain an entry for ! * each attribute of the target relation in the correct order. For all query ! * types, we may need to add junk tlist entries for Vars used in the RETURNING ! * list and row ID information needed for SELECT FOR UPDATE locking and/or ! * EvalPlanQual checking. * ! * The rewriter's rewriteTargetListIU and rewriteTargetListUD routines ! * also do preprocessing of the targetlist. The division of labor between ! * here and there is partially historical, but it's not entirely arbitrary. ! * In particular, consider an UPDATE across an inheritance tree. What the ! * rewriter does need be done only once (because it depends only on the ! * properties of the parent relation). What's done here has to be done over ! * again for each child relation, because it depends on the column list of ! * the child, which might have more columns and/or a different column order ! * than the parent. * * The fact that rewriteTargetListIU sorts non-resjunk tlist entries by column * position, which expand_targetlist depends on, violates the above comment --- 4,24 ---- * Routines to preprocess the parse tree target list * * For INSERT and UPDATE queries, the targetlist must contain an entry for ! * each attribute of the target relation in the correct order. For UPDATE and ! * DELETE queries, it must also contain a junk tlist entry needed to allow the ! * executor to identify the physical locations of the rows to be updated or ! * deleted. For all query types, we may need to add junk tlist entries for ! * Vars used in the RETURNING list and row ID information needed for SELECT ! * FOR UPDATE locking and/or EvalPlanQual checking. * ! * The rewriter's rewriteTargetListIU routine also does preprocessing of the ! * targetlist. The division of labor between here and there is partially ! * historical, but it's not entirely arbitrary. In particular, consider an ! * UPDATE across an inheritance tree. What the rewriter does need be done ! * only once (because it depends only on the properties of the parent ! * relation). What's done here has to be done over again for each child ! * relation, because it depends on the column list of the child, which might ! * have more columns and/or a different column order than the parent. * * The fact that rewriteTargetListIU sorts non-resjunk tlist entries by column * position, which expand_targetlist depends on, violates the above comment *************** *** 41,46 **** --- 42,48 ---- #include "access/heapam.h" #include "access/sysattr.h" #include "catalog/pg_type.h" + #include "foreign/fdwapi.h" #include "nodes/makefuncs.h" #include "optimizer/prep.h" #include "optimizer/tlist.h" *************** *** 52,57 **** --- 54,61 ---- static List *expand_targetlist(List *tlist, int command_type, Index result_relation, List *range_table); + static void rewrite_targetlist(Query *parse, + Index result_relation, List *range_table); /* *************** *** 61,72 **** static List *expand_targetlist(List *tlist, int command_type, * Returns the new targetlist. */ List * ! preprocess_targetlist(PlannerInfo *root, List *tlist) { Query *parse = root->parse; int result_relation = parse->resultRelation; List *range_table = parse->rtable; CmdType command_type = parse->commandType; ListCell *lc; /* --- 65,77 ---- * Returns the new targetlist. */ List * ! preprocess_targetlist(PlannerInfo *root) { Query *parse = root->parse; int result_relation = parse->resultRelation; List *range_table = parse->rtable; CmdType command_type = parse->commandType; + List *tlist; ListCell *lc; /* *************** *** 82,87 **** preprocess_targetlist(PlannerInfo *root, List *tlist) --- 87,102 ---- } /* + * For UPDATE/DELETE, add a necessary junk column needed to allow the + * executor to identify the physical locations of the rows to be updated + * or deleted. + */ + if (command_type == CMD_UPDATE || command_type == CMD_DELETE) + rewrite_targetlist(parse, result_relation, range_table); + + tlist = parse->targetList; + + /* * for heap_form_tuple to work, the targetlist must match the exact order * of the attributes. We also need to fill in any missing attributes. -ay * 10/94 *************** *** 391,396 **** expand_targetlist(List *tlist, int command_type, --- 406,504 ---- return new_tlist; } + /* + * rewrite_targetlist - rewrite UPDATE/DELETE targetlist as needed + * + * This function adds a "junk" TLE that is needed to allow the executor to + * find the original row for the update or delete. When the target relation + * is a regular table, the junk TLE emits the ctid attribute of the original + * row. When the target relation is a foreign table, we let the FDW decide + * what to add. + * + * For UPDATE queries, this is applied after rewriteTargetListIU. The + * ordering isn't actually critical at the moment. + */ + static void + rewrite_targetlist(Query *parse, Index result_relation, List *range_table) + { + Var *var = NULL; + const char *attrname; + TargetEntry *tle; + RangeTblEntry *target_rte; + Relation target_relation; + + target_rte = rt_fetch(result_relation, range_table); + + /* + * We assume that the relation was already locked by the rewriter, so + * we need no lock here. + */ + target_relation = heap_open(target_rte->relid, NoLock); + + if (target_relation->rd_rel->relkind == RELKIND_RELATION || + target_relation->rd_rel->relkind == RELKIND_MATVIEW || + target_relation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + { + /* + * Emit CTID so that executor can find the row to update or delete. + */ + var = makeVar(result_relation, + SelfItemPointerAttributeNumber, + TIDOID, + -1, + InvalidOid, + 0); + + attrname = "ctid"; + } + else if (target_relation->rd_rel->relkind == RELKIND_FOREIGN_TABLE) + { + /* + * Let the foreign table's FDW add whatever junk TLEs it wants. + */ + FdwRoutine *fdwroutine; + + fdwroutine = GetFdwRoutineForRelation(target_relation, false); + + if (fdwroutine->AddForeignUpdateTargets != NULL) + fdwroutine->AddForeignUpdateTargets(parse, target_rte, + target_relation); + + /* + * If we have a row-level trigger corresponding to the operation, emit + * a whole-row Var so that executor will have the "old" row to pass to + * the trigger. Alas, this misses system columns. + */ + if (target_relation->trigdesc && + ((parse->commandType == CMD_UPDATE && + (target_relation->trigdesc->trig_update_after_row || + target_relation->trigdesc->trig_update_before_row)) || + (parse->commandType == CMD_DELETE && + (target_relation->trigdesc->trig_delete_after_row || + target_relation->trigdesc->trig_delete_before_row)))) + { + var = makeWholeRowVar(target_rte, + result_relation, + 0, + false); + + attrname = "wholerow"; + } + } + + if (var != NULL) + { + tle = makeTargetEntry((Expr *) var, + list_length(parse->targetList) + 1, + pstrdup(attrname), + true); + + parse->targetList = lappend(parse->targetList, tle); + } + + heap_close(target_relation, NoLock); + } + /* * Locate PlanRowMark for given RT index, or return NULL if none *** a/src/backend/rewrite/rewriteHandler.c --- b/src/backend/rewrite/rewriteHandler.c *************** *** 72,79 **** static TargetEntry *process_matched_tle(TargetEntry *src_tle, static Node *get_assignment_input(Node *node); static void rewriteValuesRTE(RangeTblEntry *rte, Relation target_relation, List *attrnos); - static void rewriteTargetListUD(Query *parsetree, RangeTblEntry *target_rte, - Relation target_relation); static void markQueryForLocking(Query *qry, Node *jtnode, LockClauseStrength strength, LockWaitPolicy waitPolicy, bool pushedDown); --- 72,77 ---- *************** *** 1288,1390 **** rewriteValuesRTE(RangeTblEntry *rte, Relation target_relation, List *attrnos) /* - * rewriteTargetListUD - rewrite UPDATE/DELETE targetlist as needed - * - * This function adds a "junk" TLE that is needed to allow the executor to - * find the original row for the update or delete. When the target relation - * is a regular table, the junk TLE emits the ctid attribute of the original - * row. When the target relation is a view, there is no ctid, so we instead - * emit a whole-row Var that will contain the "old" values of the view row. - * If it's a foreign table, we let the FDW decide what to add. - * - * For UPDATE queries, this is applied after rewriteTargetListIU. The - * ordering isn't actually critical at the moment. - */ - static void - rewriteTargetListUD(Query *parsetree, RangeTblEntry *target_rte, - Relation target_relation) - { - Var *var = NULL; - const char *attrname; - TargetEntry *tle; - - if (target_relation->rd_rel->relkind == RELKIND_RELATION || - target_relation->rd_rel->relkind == RELKIND_MATVIEW || - target_relation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) - { - /* - * Emit CTID so that executor can find the row to update or delete. - */ - var = makeVar(parsetree->resultRelation, - SelfItemPointerAttributeNumber, - TIDOID, - -1, - InvalidOid, - 0); - - attrname = "ctid"; - } - else if (target_relation->rd_rel->relkind == RELKIND_FOREIGN_TABLE) - { - /* - * Let the foreign table's FDW add whatever junk TLEs it wants. - */ - FdwRoutine *fdwroutine; - - fdwroutine = GetFdwRoutineForRelation(target_relation, false); - - if (fdwroutine->AddForeignUpdateTargets != NULL) - fdwroutine->AddForeignUpdateTargets(parsetree, target_rte, - target_relation); - - /* - * If we have a row-level trigger corresponding to the operation, emit - * a whole-row Var so that executor will have the "old" row to pass to - * the trigger. Alas, this misses system columns. - */ - if (target_relation->trigdesc && - ((parsetree->commandType == CMD_UPDATE && - (target_relation->trigdesc->trig_update_after_row || - target_relation->trigdesc->trig_update_before_row)) || - (parsetree->commandType == CMD_DELETE && - (target_relation->trigdesc->trig_delete_after_row || - target_relation->trigdesc->trig_delete_before_row)))) - { - var = makeWholeRowVar(target_rte, - parsetree->resultRelation, - 0, - false); - - attrname = "wholerow"; - } - } - else - { - /* - * Emit whole-row Var so that executor will have the "old" view row to - * pass to the INSTEAD OF trigger. - */ - var = makeWholeRowVar(target_rte, - parsetree->resultRelation, - 0, - false); - - attrname = "wholerow"; - } - - if (var != NULL) - { - tle = makeTargetEntry((Expr *) var, - list_length(parsetree->targetList) + 1, - pstrdup(attrname), - true); - - parsetree->targetList = lappend(parsetree->targetList, tle); - } - } - - - /* * matchLocks - * match the list of locks and returns the matching rules */ --- 1286,1291 ---- *************** *** 1497,1502 **** ApplyRetrieveRule(Query *parsetree, --- 1398,1405 ---- parsetree->commandType == CMD_DELETE) { RangeTblEntry *newrte; + Var *var; + TargetEntry *tle; rte = rt_fetch(rt_index, parsetree->rtable); newrte = copyObject(rte); *************** *** 1527,1532 **** ApplyRetrieveRule(Query *parsetree, --- 1430,1448 ---- ChangeVarNodes((Node *) parsetree->returningList, rt_index, parsetree->resultRelation, 0); + /* + * To allow the executor to find the original view row to pass to + * the INSTEAD OF trigger, we add a whole-row reference to the old + * RTE to the query's targetlist. + */ + var = makeWholeRowVar(rte, rt_index, 0, false); + tle = makeTargetEntry((Expr *) var, + list_length(parsetree->targetList) + 1, + pstrdup("wholerow"), + true); + + parsetree->targetList = lappend(parsetree->targetList, tle); + /* Now, continue with expanding the original view RTE */ } else *************** *** 2967,2992 **** rewriteTargetView(Query *parsetree, Relation view) view_rte->securityQuals = NIL; /* - * For UPDATE/DELETE, rewriteTargetListUD will have added a wholerow junk - * TLE for the view to the end of the targetlist, which we no longer need. - * Remove it to avoid unnecessary work when we process the targetlist. - * Note that when we recurse through rewriteQuery a new junk TLE will be - * added to allow the executor to find the proper row in the new target - * relation. (So, if we failed to do this, we might have multiple junk - * TLEs with the same name, which would be disastrous.) - */ - if (parsetree->commandType != CMD_INSERT) - { - TargetEntry *tle = (TargetEntry *) llast(parsetree->targetList); - - Assert(tle->resjunk); - Assert(IsA(tle->expr, Var) && - ((Var *) tle->expr)->varno == parsetree->resultRelation && - ((Var *) tle->expr)->varattno == 0); - parsetree->targetList = list_delete_ptr(parsetree->targetList, tle); - } - - /* * Now update all Vars in the outer query that reference the view to * reference the appropriate column of the base relation instead. */ --- 2883,2888 ---- *************** *** 3347,3357 **** RewriteQuery(Query *parsetree, List *rewrite_events) parsetree->override, rt_entry_relation, parsetree->resultRelation, NULL); - rewriteTargetListUD(parsetree, rt_entry, rt_entry_relation); } else if (event == CMD_DELETE) { ! rewriteTargetListUD(parsetree, rt_entry, rt_entry_relation); } else elog(ERROR, "unrecognized commandType: %d", (int) event); --- 3243,3252 ---- parsetree->override, rt_entry_relation, parsetree->resultRelation, NULL); } else if (event == CMD_DELETE) { ! /* Nothing to do here */ } else elog(ERROR, "unrecognized commandType: %d", (int) event); *** a/src/include/optimizer/prep.h --- b/src/include/optimizer/prep.h *************** *** 38,44 **** extern Expr *canonicalize_qual(Expr *qual); /* * prototypes for preptlist.c */ ! extern List *preprocess_targetlist(PlannerInfo *root, List *tlist); extern List *preprocess_onconflict_targetlist(List *tlist, int result_relation, List *range_table); --- 38,44 ---- /* * prototypes for preptlist.c */ ! extern List *preprocess_targetlist(PlannerInfo *root); extern List *preprocess_onconflict_targetlist(List *tlist, int result_relation, List *range_table);