While doing a bit more review of the partitionwise-join-fix patch, I noticed $SUBJECT. Here is an example that causes an assertion failure "TRAP: FailedAssertion("!(foreignrel)", File: "postgres_fdw.c", Line: 2213)":
postgres=# create table t1 (a int, b text); CREATE TABLE postgres=# create table t2 (a int, b text); CREATE TABLE postgres=# create foreign table ft1 (a int, b text) server loopback options (table_name 't1'); CREATE FOREIGN TABLE postgres=# create foreign table ft2 (a int, b text) server loopback options (table_name 't2'); CREATE FOREIGN TABLE postgres=# insert into ft1 values (1, 'foo'); INSERT 0 1 postgres=# insert into ft1 values (2, 'bar'); INSERT 0 1 postgres=# insert into ft2 values (1, 'test1'); INSERT 0 1 postgres=# insert into ft2 values (2, 'test2'); INSERT 0 1 postgres=# analyze ft1; ANALYZE postgres=# analyze ft2; ANALYZE postgres=# create table parent (a int, b text); CREATE TABLE postgres=# alter foreign table ft1 inherit parent; ALTER FOREIGN TABLE postgres=# update parent set b = ft2.b from ft2 where parent.a = ft2.a; server closed the connection unexpectedly This probably means the server terminated abnormally before or while processing the request. The connection to the server was lost. Attempting reset: Failed. !> I think the reason for that is: in that case we try to find the target foreign-join RelOptInfo using find_join_rel in postgresPlanDirectModify, but can't find it, because the given root is the *parent* root and doesn't have join RelOptInfos with it. To fix this, I'd like to propose to modify make_modifytable so that in case of an inherited UPDATE/DELETE, it passes to PlanDirectModify the per-child modified subroot, not the parent root, for the FDW to get the foreign-join RelOptInfo from the given root in PlanDirectModify. I think that that would be more consistent with the non-inherited UPDATE/DELETE case in that the FDW can look at any join RelOptInfos in the given root in PlanDirectModify, which I think would be a good thing because some FDWs might need to do that for some reason. For the same reason, I'd also like to propose to pass to PlanForeignModify the per-child modified subroot, not the parent root, as for PlanDirectModify. Attached is a proposed patch for that. I'll add this to the open items list for PG11. Best regards, Etsuro Fujita
*** a/contrib/postgres_fdw/expected/postgres_fdw.out --- b/contrib/postgres_fdw/expected/postgres_fdw.out *************** *** 7370,7375 **** drop table bar cascade; --- 7370,7450 ---- NOTICE: drop cascades to foreign table bar2 drop table loct1; drop table loct2; + -- Test pushing down UPDATE/DELETE joins to remote servers + create table parent (a int, b text); + create table loct1 (a int, b text); + create table loct2 (a int, b text); + create foreign table remt1 (a int, b text) + server loopback options (table_name 'loct1'); + create foreign table remt2 (a int, b text) + server loopback options (table_name 'loct2'); + alter foreign table remt1 inherit parent; + insert into remt1 values (1, 'foo'); + insert into remt1 values (2, 'bar'); + insert into remt2 values (1, 'foo'); + insert into remt2 values (2, 'bar'); + analyze remt1; + analyze remt2; + explain (verbose, costs off) + update parent set b = parent.b || remt2.b from remt2 where parent.a = remt2.a returning *; + QUERY PLAN + ----------------------------------------------------------------------------------------------------------------------------------------------- + Update on public.parent + Output: parent.a, parent.b, remt2.a, remt2.b + Update on public.parent + Foreign Update on public.remt1 + -> Nested Loop + Output: parent.a, (parent.b || remt2.b), parent.ctid, remt2.*, remt2.a, remt2.b + Join Filter: (parent.a = remt2.a) + -> Seq Scan on public.parent + Output: parent.a, parent.b, parent.ctid + -> Foreign Scan on public.remt2 + Output: remt2.b, remt2.*, remt2.a + Remote SQL: SELECT a, b FROM public.loct2 + -> Foreign Update + Remote SQL: UPDATE public.loct1 r4 SET b = (r4.b || r2.b) FROM public.loct2 r2 WHERE ((r4.a = r2.a)) RETURNING r4.a, r4.b, r2.a, r2.b + (14 rows) + + update parent set b = parent.b || remt2.b from remt2 where parent.a = remt2.a returning *; + a | b | a | b + ---+--------+---+----- + 1 | foofoo | 1 | foo + 2 | barbar | 2 | bar + (2 rows) + + explain (verbose, costs off) + delete from parent using remt2 where parent.a = remt2.a returning parent; + QUERY PLAN + ------------------------------------------------------------------------------------------------------------------ + Delete on public.parent + Output: parent.* + Delete on public.parent + Foreign Delete on public.remt1 + -> Nested Loop + Output: parent.ctid, remt2.* + Join Filter: (parent.a = remt2.a) + -> Seq Scan on public.parent + Output: parent.ctid, parent.a + -> Foreign Scan on public.remt2 + Output: remt2.*, remt2.a + Remote SQL: SELECT a, b FROM public.loct2 + -> Foreign Delete + Remote SQL: DELETE FROM public.loct1 r4 USING public.loct2 r2 WHERE ((r4.a = r2.a)) RETURNING r4.a, r4.b + (14 rows) + + delete from parent using remt2 where parent.a = remt2.a returning parent; + parent + ------------ + (1,foofoo) + (2,barbar) + (2 rows) + + -- cleanup + drop foreign table remt1; + drop foreign table remt2; + drop table loct1; + drop table loct2; + drop table parent; -- =================================================================== -- test tuple routing for foreign-table partitions -- =================================================================== *** a/contrib/postgres_fdw/sql/postgres_fdw.sql --- b/contrib/postgres_fdw/sql/postgres_fdw.sql *************** *** 1767,1772 **** drop table bar cascade; --- 1767,1804 ---- drop table loct1; drop table loct2; + -- Test pushing down UPDATE/DELETE joins to remote servers + create table parent (a int, b text); + create table loct1 (a int, b text); + create table loct2 (a int, b text); + create foreign table remt1 (a int, b text) + server loopback options (table_name 'loct1'); + create foreign table remt2 (a int, b text) + server loopback options (table_name 'loct2'); + alter foreign table remt1 inherit parent; + + insert into remt1 values (1, 'foo'); + insert into remt1 values (2, 'bar'); + insert into remt2 values (1, 'foo'); + insert into remt2 values (2, 'bar'); + + analyze remt1; + analyze remt2; + + explain (verbose, costs off) + update parent set b = parent.b || remt2.b from remt2 where parent.a = remt2.a returning *; + update parent set b = parent.b || remt2.b from remt2 where parent.a = remt2.a returning *; + explain (verbose, costs off) + delete from parent using remt2 where parent.a = remt2.a returning parent; + delete from parent using remt2 where parent.a = remt2.a returning parent; + + -- cleanup + drop foreign table remt1; + drop foreign table remt2; + drop table loct1; + drop table loct2; + drop table parent; + -- =================================================================== -- test tuple routing for foreign-table partitions -- =================================================================== *** a/src/backend/optimizer/plan/createplan.c --- b/src/backend/optimizer/plan/createplan.c *************** *** 289,295 **** static ModifyTable *make_modifytable(PlannerInfo *root, CmdType operation, bool canSetTag, Index nominalRelation, List *partitioned_rels, bool partColsUpdated, ! List *resultRelations, List *subplans, List *withCheckOptionLists, List *returningLists, List *rowMarks, OnConflictExpr *onconflict, int epqParam); static GatherMerge *create_gather_merge_plan(PlannerInfo *root, --- 289,295 ---- CmdType operation, bool canSetTag, Index nominalRelation, List *partitioned_rels, bool partColsUpdated, ! List *resultRelations, List *subplans, List *subroots, List *withCheckOptionLists, List *returningLists, List *rowMarks, OnConflictExpr *onconflict, int epqParam); static GatherMerge *create_gather_merge_plan(PlannerInfo *root, *************** *** 2484,2489 **** create_modifytable_plan(PlannerInfo *root, ModifyTablePath *best_path) --- 2484,2490 ---- best_path->partColsUpdated, best_path->resultRelations, subplans, + best_path->subroots, best_path->withCheckOptionLists, best_path->returningLists, best_path->rowMarks, *************** *** 6558,6564 **** make_modifytable(PlannerInfo *root, CmdType operation, bool canSetTag, Index nominalRelation, List *partitioned_rels, bool partColsUpdated, ! List *resultRelations, List *subplans, List *withCheckOptionLists, List *returningLists, List *rowMarks, OnConflictExpr *onconflict, int epqParam) { --- 6559,6565 ---- CmdType operation, bool canSetTag, Index nominalRelation, List *partitioned_rels, bool partColsUpdated, ! List *resultRelations, List *subplans, List *subroots, List *withCheckOptionLists, List *returningLists, List *rowMarks, OnConflictExpr *onconflict, int epqParam) { *************** *** 6566,6571 **** make_modifytable(PlannerInfo *root, --- 6567,6573 ---- List *fdw_private_list; Bitmapset *direct_modify_plans; ListCell *lc; + ListCell *lc2; int i; Assert(list_length(resultRelations) == list_length(subplans)); *************** *** 6627,6635 **** make_modifytable(PlannerInfo *root, fdw_private_list = NIL; direct_modify_plans = NULL; i = 0; ! foreach(lc, resultRelations) { Index rti = lfirst_int(lc); FdwRoutine *fdwroutine; List *fdw_private; bool direct_modify; --- 6629,6638 ---- fdw_private_list = NIL; direct_modify_plans = NULL; i = 0; ! forboth(lc, resultRelations, lc2, subroots) { Index rti = lfirst_int(lc); + PlannerInfo *subroot = lfirst_node(PlannerInfo, lc2); FdwRoutine *fdwroutine; List *fdw_private; bool direct_modify; *************** *** 6641,6656 **** make_modifytable(PlannerInfo *root, * so it's not a baserel; and there are also corner cases for * updatable views where the target rel isn't a baserel.) */ ! if (rti < root->simple_rel_array_size && ! root->simple_rel_array[rti] != NULL) { ! RelOptInfo *resultRel = root->simple_rel_array[rti]; fdwroutine = resultRel->fdwroutine; } else { ! RangeTblEntry *rte = planner_rt_fetch(rti, root); Assert(rte->rtekind == RTE_RELATION); if (rte->relkind == RELKIND_FOREIGN_TABLE) --- 6644,6659 ---- * so it's not a baserel; and there are also corner cases for * updatable views where the target rel isn't a baserel.) */ ! if (rti < subroot->simple_rel_array_size && ! subroot->simple_rel_array[rti] != NULL) { ! RelOptInfo *resultRel = subroot->simple_rel_array[rti]; fdwroutine = resultRel->fdwroutine; } else { ! RangeTblEntry *rte = planner_rt_fetch(rti, subroot); Assert(rte->rtekind == RTE_RELATION); if (rte->relkind == RELKIND_FOREIGN_TABLE) *************** *** 6672,6686 **** make_modifytable(PlannerInfo *root, fdwroutine->IterateDirectModify != NULL && fdwroutine->EndDirectModify != NULL && withCheckOptionLists == NIL && ! !has_row_triggers(root, rti, operation)) ! direct_modify = fdwroutine->PlanDirectModify(root, node, rti, i); if (direct_modify) direct_modify_plans = bms_add_member(direct_modify_plans, i); if (!direct_modify && fdwroutine != NULL && fdwroutine->PlanForeignModify != NULL) ! fdw_private = fdwroutine->PlanForeignModify(root, node, rti, i); else fdw_private = NIL; fdw_private_list = lappend(fdw_private_list, fdw_private); --- 6675,6689 ---- fdwroutine->IterateDirectModify != NULL && fdwroutine->EndDirectModify != NULL && withCheckOptionLists == NIL && ! !has_row_triggers(subroot, rti, operation)) ! direct_modify = fdwroutine->PlanDirectModify(subroot, node, rti, i); if (direct_modify) direct_modify_plans = bms_add_member(direct_modify_plans, i); if (!direct_modify && fdwroutine != NULL && fdwroutine->PlanForeignModify != NULL) ! fdw_private = fdwroutine->PlanForeignModify(subroot, node, rti, i); else fdw_private = NIL; fdw_private_list = lappend(fdw_private_list, fdw_private);