From 2f1a96ac7f5c9d9d8fb45e22692e0135d4a200c5 Mon Sep 17 00:00:00 2001 From: Ajit Awekar Date: Tue, 3 Nov 2026 12:25:08 +0530 Subject: [PATCH v5 2/2] postgres_fdw: Identify remote rows by ctid and tableoid in UPDATE/DELETE When a foreign table points at a partitioned table on the remote server, ctid alone does not identify a row: rows in different partitions can share a ctid. A non-direct UPDATE or DELETE could therefore modify or delete unrelated rows. Also fetch the remote tableoid, as a pseudo-column in fdw_scan_tlist, and send it with ctid so the per-row remote command is "WHERE ctid = $1 AND tableoid = $2". Whole-row references to the target relation are rebuilt locally from the fetched columns rather than shipped as a second ROW(...), so columns are not transferred twice. A whole-row Var is typed RECORD in fdw_scan_tlist so that references with different row types resolve to one entry; references in quals are relabeled to their original row type, which keeps the conversion of a partition's whole row to its parent's row type working. The per-row commands cannot benefit from remote partition pruning on tableoid, and chained foreign tables are not protected; both are documented. Author: Nikita Malakhov Author: Ajit Awekar Suggested-by: Robert Haas Reported-by: Jehan-Guillaume de Rorthais Reviewed-by: Jakub Wartak Reviewed-by: Manuel Reyes Bravo Discussion: https://postgr.es/m/20250718175314.4513c00a@karst --- contrib/postgres_fdw/deparse.c | 100 +++- .../postgres_fdw/expected/postgres_fdw.out | 535 +++++++++++------- contrib/postgres_fdw/postgres_fdw.c | 437 +++++++++++++- contrib/postgres_fdw/postgres_fdw.h | 9 + contrib/postgres_fdw/sql/postgres_fdw.sql | 72 +++ doc/src/sgml/postgres-fdw.sgml | 34 ++ 6 files changed, 950 insertions(+), 237 deletions(-) diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c index ff9fe0f87e4..40d9a47336d 100644 --- a/contrib/postgres_fdw/deparse.c +++ b/contrib/postgres_fdw/deparse.c @@ -1391,11 +1391,14 @@ deparseSelectSql(List *tlist, bool is_subquery, List **retrieved_attrs, */ deparseSubqueryTargetList(context); } - else if (IS_JOIN_REL(foreignrel) || IS_UPPER_REL(foreignrel)) + else if (IS_JOIN_REL(foreignrel) || IS_UPPER_REL(foreignrel) || + tlist != NIL) { /* * For a join or upper relation the input tlist gives the list of - * columns required to be fetched from the foreign server. + * columns required to be fetched from the foreign server; likewise + * for a base-relation scan given an explicit tlist (fdw_scan_tlist) + * to fetch the remote tableoid (see postgresGetForeignPlan()). */ deparseExplicitTargetList(tlist, false, retrieved_attrs, context); } @@ -1721,13 +1724,55 @@ get_jointype_name(JoinType jointype) return NULL; } +/* + * Does tlist contain an individual real column (varattno > 0, excluding the + * RemoteTableOidAttributeNumber pseudo-column) of relid, besides a whole-row + * entry for it? Used to tell whether a whole-row reference has anything to + * be locally reconstructed from (see deparseExplicitTargetList()). + */ +static bool +tlist_has_other_column_of_rel(List *tlist, Index relid) +{ + ListCell *lc; + + foreach(lc, tlist) + { + TargetEntry *tle = lfirst_node(TargetEntry, lc); + + if (IsA(tle->expr, Var) && + ((Var *) tle->expr)->varno == relid && + ((Var *) tle->expr)->varattno > 0 && + ((Var *) tle->expr)->varattno != RemoteTableOidAttributeNumber) + return true; + } + return false; +} + /* * Deparse given targetlist and append it to context->buf. * * tlist is list of TargetEntry's which in turn contain Var nodes. * - * retrieved_attrs is the list of continuously increasing integers starting - * from 1. It has same number of entries as tlist. + * retrieved_attrs is normally the list of continuously increasing integers + * starting from 1, with the same number of entries as tlist. There are two + * exceptions, both specific to a base relation's SELECT list built as an + * explicit fdw_scan_tlist for row-identity purposes: + * + * - the local-tableoid pseudo-entry (a Var on TableOidAttributeNumber): its + * value is already known at plan time, so instead of shipping it to the + * remote server as a literal Const, it's skipped here and filled in + * locally afterward, by make_tuple_from_result_row(). + * + * - a plain whole-row reference to the relation itself (a Var on attno 0), + * when its columns are also being fetched individually elsewhere in this + * tlist (e.g. for EPQ recheck): shipping the whole row too would transfer + * those columns twice, so it's skipped here and reconstructed locally + * afterward instead, again by make_tuple_from_result_row(). Absent such + * sibling columns (e.g. the relation has none), it is shipped as a + * literal ROW(...) as usual, since there is nothing to reconstruct it + * from. There can be more than one such whole-row entry for the same + * relation (e.g. one from the query itself, another added by core code + * for row identity); each is skipped and reconstructed independently. * * This is used for both SELECT and RETURNING targetlists; the is_returning * parameter is true only for a RETURNING targetlist. @@ -1740,7 +1785,9 @@ deparseExplicitTargetList(List *tlist, { ListCell *lc; StringInfo buf = context->buf; + bool is_base_rel = !is_returning && IS_SIMPLE_REL(context->scanrel); int i = 0; + bool first = true; *retrieved_attrs = NIL; @@ -1748,18 +1795,38 @@ deparseExplicitTargetList(List *tlist, { TargetEntry *tle = lfirst_node(TargetEntry, lc); - if (i > 0) + i++; + + /* Local tableoid: known locally, skip the remote round trip. */ + if (is_base_rel && + IsA(tle->expr, Var) && + ((Var *) tle->expr)->varattno == TableOidAttributeNumber) + continue; + + /* + * Whole-row reference to this relation with a sibling column + * elsewhere in tlist: skip the redundant ROW(...) and reconstruct + * it locally instead (see make_tuple_from_result_row()). + */ + if (is_base_rel && + IsA(tle->expr, Var) && + ((Var *) tle->expr)->varattno == InvalidAttrNumber && + ((Var *) tle->expr)->varno == context->scanrel->relid && + tlist_has_other_column_of_rel(tlist, context->scanrel->relid)) + continue; + + if (!first) appendStringInfoString(buf, ", "); else if (is_returning) appendStringInfoString(buf, " RETURNING "); + first = false; deparseExpr((Expr *) tle->expr, context); - *retrieved_attrs = lappend_int(*retrieved_attrs, i + 1); - i++; + *retrieved_attrs = lappend_int(*retrieved_attrs, i); } - if (i == 0 && !is_returning) + if (first && !is_returning) appendStringInfoString(buf, "NULL"); } @@ -2364,7 +2431,7 @@ deparseUpdateSql(StringInfo buf, RangeTblEntry *rte, deparseRelation(buf, rel); appendStringInfoString(buf, " SET "); - pindex = 2; /* ctid is always the first param */ + pindex = 3; /* ctid ($1) and tableoid ($2) come first */ first = true; foreach(lc, targetAttrs) { @@ -2384,7 +2451,7 @@ deparseUpdateSql(StringInfo buf, RangeTblEntry *rte, pindex++; } } - appendStringInfoString(buf, " WHERE ctid = $1"); + appendStringInfoString(buf, " WHERE ctid = $1 AND tableoid = $2"); deparseReturningList(buf, rte, rtindex, rel, rel->trigdesc && rel->trigdesc->trig_update_after_row, @@ -2502,7 +2569,7 @@ deparseDeleteSql(StringInfo buf, RangeTblEntry *rte, { appendStringInfoString(buf, "DELETE FROM "); deparseRelation(buf, rel); - appendStringInfoString(buf, " WHERE ctid = $1"); + appendStringInfoString(buf, " WHERE ctid = $1 AND tableoid = $2"); deparseReturningList(buf, rte, rtindex, rel, rel->trigdesc && rel->trigdesc->trig_delete_after_row, @@ -2889,6 +2956,17 @@ deparseColumnRef(StringInfo buf, int varno, int varattno, RangeTblEntry *rte, ADD_REL_QUALIFIER(buf, varno); appendStringInfoString(buf, "ctid"); } + else if (varattno == RemoteTableOidAttributeNumber) + { + /* + * Pseudo-column carrying the remote table OID as part of the row + * identity for UPDATE/DELETE (see postgresAddForeignUpdateTargets); + * fetch it as the remote "tableoid" system column. + */ + if (qualify_col) + ADD_REL_QUALIFIER(buf, varno); + appendStringInfoString(buf, "tableoid"); + } else if (varattno < 0) { /* diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out index 115ebc7925f..c3673f68a3b 100644 --- a/contrib/postgres_fdw/expected/postgres_fdw.out +++ b/contrib/postgres_fdw/expected/postgres_fdw.out @@ -5979,14 +5979,14 @@ BEGIN; EXPLAIN (verbose, costs off) UPDATE ft2 SET c2 = c2 + 400, c3 = c3 || '_update7b' WHERE c1 % 10 = 7 AND c1 < 40 RETURNING old.*, new.*; -- can't be pushed down - QUERY PLAN ----------------------------------------------------------------------------------------------------------------------------------------------- + QUERY PLAN +-------------------------------------------------------------------------------------------------------------------------------------------------------- Update on public.ft2 Output: old.c1, old.c2, old.c3, old.c4, old.c5, old.c6, old.c7, old.c8, new.c1, new.c2, new.c3, new.c4, new.c5, new.c6, new.c7, new.c8 - Remote SQL: UPDATE "S 1"."T 1" SET c2 = $2, c3 = $3 WHERE ctid = $1 RETURNING "C 1", c2, c3, c4, c5, c6, c7, c8 + Remote SQL: UPDATE "S 1"."T 1" SET c2 = $3, c3 = $4 WHERE ctid = $1 AND tableoid = $2 RETURNING "C 1", c2, c3, c4, c5, c6, c7, c8 -> Foreign Scan on public.ft2 - Output: (c2 + 400), (c3 || '_update7b'::text), ctid, ft2.* - Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8, ctid FROM "S 1"."T 1" WHERE (("C 1" < 40)) AND ((("C 1" % 10) = 7)) FOR UPDATE + Output: (c2 + 400), (c3 || '_update7b'::text), ctid, remotetableoid, ft2.* + Remote SQL: SELECT c2, c3, ctid, tableoid, "C 1", c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE (("C 1" < 40)) AND ((("C 1" % 10) = 7)) FOR UPDATE (6 rows) UPDATE ft2 SET c2 = c2 + 400, c3 = c3 || '_update7b' WHERE c1 % 10 = 7 AND c1 < 40 @@ -6133,14 +6133,14 @@ DELETE FROM ft2 WHERE c1 % 10 = 5 RETURNING c1, c4; BEGIN; EXPLAIN (verbose, costs off) DELETE FROM ft2 WHERE c1 % 10 = 6 AND c1 < 40 RETURNING old.c1, c4; -- can't be pushed down - QUERY PLAN ------------------------------------------------------------------------------------------------------------ + QUERY PLAN +---------------------------------------------------------------------------------------------------------------------------- Delete on public.ft2 Output: old.c1, c4 - Remote SQL: DELETE FROM "S 1"."T 1" WHERE ctid = $1 RETURNING "C 1", c4 + Remote SQL: DELETE FROM "S 1"."T 1" WHERE ctid = $1 AND tableoid = $2 RETURNING "C 1", c4 -> Foreign Scan on public.ft2 - Output: ctid - Remote SQL: SELECT ctid FROM "S 1"."T 1" WHERE (("C 1" < 40)) AND ((("C 1" % 10) = 6)) FOR UPDATE + Output: ctid, remotetableoid + Remote SQL: SELECT ctid, tableoid, "C 1" FROM "S 1"."T 1" WHERE (("C 1" < 40)) AND ((("C 1" % 10) = 6)) FOR UPDATE (6 rows) DELETE FROM ft2 WHERE c1 % 10 = 6 AND c1 < 40 RETURNING old.c1, c4; @@ -7083,27 +7083,27 @@ BEGIN; FROM ft4 INNER JOIN ft5 ON (ft4.c1 = ft5.c1) WHERE ft2.c1 > 1200 AND ft2.c2 = ft4.c1 RETURNING old, new, ft2, ft2.*, ft4, ft4.*; -- can't be pushed down - QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Update on public.ft2 Output: old.*, new.*, ft2.*, ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8, ft4.*, ft4.c1, ft4.c2, ft4.c3 - Remote SQL: UPDATE "S 1"."T 1" SET c3 = $2 WHERE ctid = $1 RETURNING "C 1", c2, c3, c4, c5, c6, c7, c8 + Remote SQL: UPDATE "S 1"."T 1" SET c3 = $3 WHERE ctid = $1 AND tableoid = $2 RETURNING "C 1", c2, c3, c4, c5, c6, c7, c8 -> Foreign Scan - Output: 'bar'::text, ft2.ctid, ft2.*, ft4.*, ft5.*, ft4.c1, ft4.c2, ft4.c3 + Output: 'bar'::text, ft2.ctid, remotetableoid, ft2.*, ft4.*, ft5.*, ft4.c1, ft4.c2, ft4.c3 Relations: ((public.ft2) INNER JOIN (public.ft4)) INNER JOIN (public.ft5) - Remote SQL: SELECT r1.ctid, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, CASE WHEN (r2.*)::text IS NOT NULL THEN ROW(r2.c1, r2.c2, r2.c3) END, CASE WHEN (r3.*)::text IS NOT NULL THEN ROW(r3.c1, r3.c2, r3.c3) END, r2.c1, r2.c2, r2.c3 FROM (("S 1"."T 1" r1 INNER JOIN "S 1"."T 3" r2 ON (((r1.c2 = r2.c1)) AND ((r1."C 1" > 1200)))) INNER JOIN "S 1"."T 4" r3 ON (((r2.c1 = r3.c1)))) FOR UPDATE OF r1 + Remote SQL: SELECT r1.ctid, r1.tableoid, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, CASE WHEN (r2.*)::text IS NOT NULL THEN ROW(r2.c1, r2.c2, r2.c3) END, CASE WHEN (r3.*)::text IS NOT NULL THEN ROW(r3.c1, r3.c2, r3.c3) END, r2.c1, r2.c2, r2.c3 FROM (("S 1"."T 1" r1 INNER JOIN "S 1"."T 3" r2 ON (((r1.c2 = r2.c1)) AND ((r1."C 1" > 1200)))) INNER JOIN "S 1"."T 4" r3 ON (((r2.c1 = r3.c1)))) FOR UPDATE OF r1 -> Nested Loop - Output: ft2.ctid, ft2.*, ft4.*, ft5.*, ft4.c1, ft4.c2, ft4.c3 + Output: ft2.ctid, remotetableoid, ft2.*, ft4.*, ft5.*, ft4.c1, ft4.c2, ft4.c3 Join Filter: (ft4.c1 = ft5.c1) -> Sort - Output: ft2.ctid, ft2.*, ft2.c2, ft4.*, ft4.c1, ft4.c2, ft4.c3 + Output: ft2.ctid, remotetableoid, ft2.*, ft2.c2, ft4.*, ft4.c1, ft4.c2, ft4.c3 Sort Key: ft2.c2 -> Hash Join - Output: ft2.ctid, ft2.*, ft2.c2, ft4.*, ft4.c1, ft4.c2, ft4.c3 + Output: ft2.ctid, remotetableoid, ft2.*, ft2.c2, ft4.*, ft4.c1, ft4.c2, ft4.c3 Hash Cond: (ft2.c2 = ft4.c1) -> Foreign Scan on public.ft2 - Output: ft2.ctid, ft2.*, ft2.c2 - Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8, ctid FROM "S 1"."T 1" WHERE (("C 1" > 1200)) FOR UPDATE + Output: ft2.ctid, remotetableoid, ft2.*, ft2.c2 + Remote SQL: SELECT ctid, tableoid, c2, "C 1", c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE (("C 1" > 1200)) FOR UPDATE -> Hash Output: ft4.*, ft4.c1, ft4.c2, ft4.c3 -> Foreign Scan on public.ft4 @@ -7181,13 +7181,13 @@ UPDATE ft2 AS target SET (c2, c7) = ( FROM ft2 AS src WHERE target.c1 = src.c1 ) WHERE c1 > 1100; - QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------ + QUERY PLAN +--------------------------------------------------------------------------------------------------------------------------------------------- Update on public.ft2 target - Remote SQL: UPDATE "S 1"."T 1" SET c2 = $2, c7 = $3 WHERE ctid = $1 + Remote SQL: UPDATE "S 1"."T 1" SET c2 = $3, c7 = $4 WHERE ctid = $1 AND tableoid = $2 -> Foreign Scan on public.ft2 target - Output: (SubPlan multiexpr_1).col1, (SubPlan multiexpr_1).col2, (rescan SubPlan multiexpr_1), target.ctid, target.* - Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8, ctid FROM "S 1"."T 1" WHERE (("C 1" > 1100)) FOR UPDATE + Output: (SubPlan multiexpr_1).col1, (SubPlan multiexpr_1).col2, (rescan SubPlan multiexpr_1), target.ctid, remotetableoid, target.* + Remote SQL: SELECT "C 1", ctid, tableoid, c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE (("C 1" > 1100)) FOR UPDATE SubPlan multiexpr_1 -> Foreign Scan on public.ft2 src Output: (src.c2 * 10), src.c7 @@ -7209,20 +7209,20 @@ UPDATE ft2 AS target SET (c2) = ( EXPLAIN (VERBOSE, COSTS OFF) UPDATE ft2 d SET c2 = CASE WHEN random() >= 0 THEN d.c2 ELSE 0 END FROM ft2 AS t WHERE d.c1 = t.c1 AND d.c1 > 1000; - QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ Update on public.ft2 d - Remote SQL: UPDATE "S 1"."T 1" SET c2 = $2 WHERE ctid = $1 + Remote SQL: UPDATE "S 1"."T 1" SET c2 = $3 WHERE ctid = $1 AND tableoid = $2 -> Foreign Scan - Output: CASE WHEN (random() >= '0'::double precision) THEN d.c2 ELSE 0 END, d.ctid, d.*, t.* + Output: CASE WHEN (random() >= '0'::double precision) THEN d.c2 ELSE 0 END, d.ctid, remotetableoid, d.*, t.* Relations: (public.ft2 d) INNER JOIN (public.ft2 t) - Remote SQL: SELECT r1.c2, r1.ctid, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, CASE WHEN (r2.*)::text IS NOT NULL THEN ROW(r2."C 1", r2.c2, r2.c3, r2.c4, r2.c5, r2.c6, r2.c7, r2.c8) END FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1")) AND ((r1."C 1" > 1000)))) FOR UPDATE OF r1 + Remote SQL: SELECT r1.c2, r1.ctid, r1.tableoid, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, CASE WHEN (r2.*)::text IS NOT NULL THEN ROW(r2."C 1", r2.c2, r2.c3, r2.c4, r2.c5, r2.c6, r2.c7, r2.c8) END FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1")) AND ((r1."C 1" > 1000)))) FOR UPDATE OF r1 -> Hash Join - Output: d.c2, d.ctid, d.*, t.* + Output: d.c2, d.ctid, remotetableoid, d.*, t.* Hash Cond: (d.c1 = t.c1) -> Foreign Scan on public.ft2 d - Output: d.c2, d.ctid, d.*, d.c1 - Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8, ctid FROM "S 1"."T 1" WHERE (("C 1" > 1000)) ORDER BY "C 1" ASC NULLS LAST FOR UPDATE + Output: d.c2, d.ctid, remotetableoid, d.*, d.c1 + Remote SQL: SELECT c2, ctid, tableoid, "C 1", c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE (("C 1" > 1000)) ORDER BY "C 1" ASC NULLS LAST FOR UPDATE -> Hash Output: t.*, t.c1 -> Foreign Scan on public.ft2 t @@ -7242,19 +7242,19 @@ EXPLAIN (verbose, costs off) WITH cte AS ( UPDATE ft2 SET c3 = 'bar' WHERE postgres_fdw_abs(c1) > 2000 RETURNING * ) SELECT * FROM cte ORDER BY c1; -- can't be pushed down - QUERY PLAN ------------------------------------------------------------------------------------------------------------------- + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------ Sort Output: cte.c1, cte.c2, cte.c3, cte.c4, cte.c5, cte.c6, cte.c7, cte.c8 Sort Key: cte.c1 CTE cte -> Update on public.ft2 Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8 - Remote SQL: UPDATE "S 1"."T 1" SET c3 = $2 WHERE ctid = $1 RETURNING "C 1", c2, c3, c4, c5, c6, c7, c8 + Remote SQL: UPDATE "S 1"."T 1" SET c3 = $3 WHERE ctid = $1 AND tableoid = $2 RETURNING "C 1", c2, c3, c4, c5, c6, c7, c8 -> Foreign Scan on public.ft2 - Output: 'bar'::text, ft2.ctid, ft2.* + Output: 'bar'::text, ft2.ctid, remotetableoid, ft2.* Filter: (postgres_fdw_abs(ft2.c1) > 2000) - Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8, ctid FROM "S 1"."T 1" FOR UPDATE + Remote SQL: SELECT ctid, tableoid, "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" FOR UPDATE -> CTE Scan on cte Output: cte.c1, cte.c2, cte.c3, cte.c4, cte.c5, cte.c6, cte.c7, cte.c8 (13 rows) @@ -7285,13 +7285,13 @@ UPDATE ft2 SET c3 = 'baz' ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Update on public.ft2 Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8, ft4.c1, ft4.c2, ft4.c3, ft5.c1, ft5.c2, ft5.c3 - Remote SQL: UPDATE "S 1"."T 1" SET c3 = $2 WHERE ctid = $1 RETURNING "C 1", c2, c3, c4, c5, c6, c7, c8 + Remote SQL: UPDATE "S 1"."T 1" SET c3 = $3 WHERE ctid = $1 AND tableoid = $2 RETURNING "C 1", c2, c3, c4, c5, c6, c7, c8 -> Nested Loop - Output: 'baz'::text, ft2.ctid, ft2.*, ft4.*, ft5.*, ft4.c1, ft4.c2, ft4.c3, ft5.c1, ft5.c2, ft5.c3 + Output: 'baz'::text, ft2.ctid, remotetableoid, ft2.*, ft4.*, ft5.*, ft4.c1, ft4.c2, ft4.c3, ft5.c1, ft5.c2, ft5.c3 Join Filter: (ft2.c2 === ft4.c1) -> Foreign Scan on public.ft2 - Output: ft2.ctid, ft2.*, ft2.c2 - Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8, ctid FROM "S 1"."T 1" WHERE (("C 1" > 2000)) FOR UPDATE + Output: ft2.ctid, remotetableoid, ft2.*, ft2.c2 + Remote SQL: SELECT ctid, tableoid, c2, "C 1", c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE (("C 1" > 2000)) FOR UPDATE -> Foreign Scan Output: ft4.*, ft4.c1, ft4.c2, ft4.c3, ft5.*, ft5.c1, ft5.c2, ft5.c3 Relations: (public.ft4) INNER JOIN (public.ft5) @@ -7323,24 +7323,24 @@ DELETE FROM ft2 USING ft4 INNER JOIN ft5 ON (ft4.c1 === ft5.c1) WHERE ft2.c1 > 2000 AND ft2.c2 = ft4.c1 RETURNING ft2.c1, ft2.c2, ft2.c3; -- can't be pushed down - QUERY PLAN ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + QUERY PLAN +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Delete on public.ft2 Output: ft2.c1, ft2.c2, ft2.c3 - Remote SQL: DELETE FROM "S 1"."T 1" WHERE ctid = $1 RETURNING "C 1", c2, c3 + Remote SQL: DELETE FROM "S 1"."T 1" WHERE ctid = $1 AND tableoid = $2 RETURNING "C 1", c2, c3 -> Foreign Scan - Output: ft2.ctid, ft4.*, ft5.* + Output: ft2.ctid, remotetableoid, ft4.*, ft5.* Filter: (ft4.c1 === ft5.c1) Relations: ((public.ft2) INNER JOIN (public.ft4)) INNER JOIN (public.ft5) - Remote SQL: SELECT r1.ctid, CASE WHEN (r2.*)::text IS NOT NULL THEN ROW(r2.c1, r2.c2, r2.c3) END, CASE WHEN (r3.*)::text IS NOT NULL THEN ROW(r3.c1, r3.c2, r3.c3) END, r2.c1, r3.c1 FROM (("S 1"."T 1" r1 INNER JOIN "S 1"."T 3" r2 ON (((r1.c2 = r2.c1)) AND ((r1."C 1" > 2000)))) INNER JOIN "S 1"."T 4" r3 ON (TRUE)) FOR UPDATE OF r1 + Remote SQL: SELECT r1.ctid, r1.tableoid, CASE WHEN (r2.*)::text IS NOT NULL THEN ROW(r2.c1, r2.c2, r2.c3) END, CASE WHEN (r3.*)::text IS NOT NULL THEN ROW(r3.c1, r3.c2, r3.c3) END, r2.c1, r3.c1 FROM (("S 1"."T 1" r1 INNER JOIN "S 1"."T 3" r2 ON (((r1.c2 = r2.c1)) AND ((r1."C 1" > 2000)))) INNER JOIN "S 1"."T 4" r3 ON (TRUE)) FOR UPDATE OF r1 -> Nested Loop - Output: ft2.ctid, ft4.*, ft5.*, ft4.c1, ft5.c1 + Output: ft2.ctid, remotetableoid, ft4.*, ft5.*, ft4.c1, ft5.c1 -> Nested Loop - Output: ft2.ctid, ft4.*, ft4.c1 + Output: ft2.ctid, remotetableoid, ft4.*, ft4.c1 Join Filter: (ft2.c2 = ft4.c1) -> Foreign Scan on public.ft2 - Output: ft2.ctid, ft2.c2 - Remote SQL: SELECT c2, ctid FROM "S 1"."T 1" WHERE (("C 1" > 2000)) FOR UPDATE + Output: ft2.ctid, remotetableoid, ft2.c2 + Remote SQL: SELECT ctid, tableoid, c2, "C 1" FROM "S 1"."T 1" WHERE (("C 1" > 2000)) FOR UPDATE -> Foreign Scan on public.ft4 Output: ft4.*, ft4.c1 Remote SQL: SELECT c1, c2, c3 FROM "S 1"."T 3" @@ -7852,19 +7852,19 @@ SET enable_hashjoin TO false; SET enable_material TO false; EXPLAIN (VERBOSE, COSTS OFF) UPDATE remt2 SET c2 = remt2.c2 || remt2.c2 FROM loct1 WHERE loct1.c1 = remt2.c1 RETURNING remt2.*; - QUERY PLAN --------------------------------------------------------------------------------- + QUERY PLAN +-------------------------------------------------------------------------------------------------- Update on public.remt2 Output: remt2.c1, remt2.c2 - Remote SQL: UPDATE public.loct2 SET c2 = $2 WHERE ctid = $1 RETURNING c1, c2 + Remote SQL: UPDATE public.loct2 SET c2 = $3 WHERE ctid = $1 AND tableoid = $2 RETURNING c1, c2 -> Nested Loop - Output: (remt2.c2 || remt2.c2), remt2.ctid, remt2.*, loct1.ctid + Output: (remt2.c2 || remt2.c2), remt2.ctid, remotetableoid, remt2.*, loct1.ctid Join Filter: (remt2.c1 = loct1.c1) -> Seq Scan on public.loct1 Output: loct1.ctid, loct1.c1 -> Foreign Scan on public.remt2 - Output: remt2.c2, remt2.ctid, remt2.*, remt2.c1 - Remote SQL: SELECT c1, c2, ctid FROM public.loct2 FOR UPDATE + Output: remt2.c2, remt2.ctid, remotetableoid, remt2.*, remt2.c1 + Remote SQL: SELECT c2, ctid, tableoid, c1 FROM public.loct2 FOR UPDATE (11 rows) UPDATE remt2 SET c2 = remt2.c2 || remt2.c2 FROM loct1 WHERE loct1.c1 = remt2.c1 RETURNING remt2.*; @@ -7921,17 +7921,17 @@ prepare fdw_part_upd2(int) as returning tableoid::regclass, a, b; explain (verbose, costs off) execute fdw_part_upd2(2); - QUERY PLAN --------------------------------------------------------------------------------------------------------------------------------------------------------- + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------ Update on public.fdw_part_update Output: (fdw_part_update_1.tableoid)::regclass, fdw_part_update_1.a, fdw_part_update_1.b Foreign Update on public.fdw_part_update_p2 fdw_part_update_2 - Remote SQL: UPDATE public.fdw_part_update_remote SET b = $2 WHERE ctid = $1 RETURNING a, b + Remote SQL: UPDATE public.fdw_part_update_remote SET b = $3 WHERE ctid = $1 AND tableoid = $2 RETURNING a, b -> Append Subplans Removed: 1 -> Foreign Scan on public.fdw_part_update_p2 fdw_part_update_2 - Output: ((fdw_part_update_2.b + ((random())::integer * 0)) + 1), fdw_part_update_2.tableoid, fdw_part_update_2.ctid, fdw_part_update_2.* - Remote SQL: SELECT a, b, ctid FROM public.fdw_part_update_remote WHERE ((a = $1::integer)) FOR UPDATE + Output: ((fdw_part_update_2.b + ((random())::integer * 0)) + 1), fdw_part_update_2.tableoid, fdw_part_update_2.ctid, remotetableoid, fdw_part_update_2.* + Remote SQL: SELECT b, ctid, tableoid, a FROM public.fdw_part_update_remote WHERE ((a = $1::integer)) FOR UPDATE (9 rows) execute fdw_part_upd2(2); @@ -8089,13 +8089,13 @@ SELECT * FROM foreign_tbl; EXPLAIN (VERBOSE, COSTS OFF) UPDATE rw_view SET b = b + 5; - QUERY PLAN ---------------------------------------------------------------------------------------- + QUERY PLAN +-------------------------------------------------------------------------------------------------- Update on public.foreign_tbl - Remote SQL: UPDATE public.base_tbl SET b = $2 WHERE ctid = $1 RETURNING a, b + Remote SQL: UPDATE public.base_tbl SET b = $3 WHERE ctid = $1 AND tableoid = $2 RETURNING a, b -> Foreign Scan on public.foreign_tbl - Output: (foreign_tbl.b + 5), foreign_tbl.ctid, foreign_tbl.* - Remote SQL: SELECT a, b, ctid FROM public.base_tbl WHERE ((a < b)) FOR UPDATE + Output: (foreign_tbl.b + 5), foreign_tbl.ctid, remotetableoid, foreign_tbl.* + Remote SQL: SELECT b, ctid, tableoid, a FROM public.base_tbl WHERE ((a < b)) FOR UPDATE (5 rows) UPDATE rw_view SET b = b + 5; -- should fail @@ -8103,13 +8103,13 @@ ERROR: new row violates check option for view "rw_view" DETAIL: Failing row contains (20, 20). EXPLAIN (VERBOSE, COSTS OFF) UPDATE rw_view SET b = b + 15; - QUERY PLAN ---------------------------------------------------------------------------------------- + QUERY PLAN +-------------------------------------------------------------------------------------------------- Update on public.foreign_tbl - Remote SQL: UPDATE public.base_tbl SET b = $2 WHERE ctid = $1 RETURNING a, b + Remote SQL: UPDATE public.base_tbl SET b = $3 WHERE ctid = $1 AND tableoid = $2 RETURNING a, b -> Foreign Scan on public.foreign_tbl - Output: (foreign_tbl.b + 15), foreign_tbl.ctid, foreign_tbl.* - Remote SQL: SELECT a, b, ctid FROM public.base_tbl WHERE ((a < b)) FOR UPDATE + Output: (foreign_tbl.b + 15), foreign_tbl.ctid, remotetableoid, foreign_tbl.* + Remote SQL: SELECT b, ctid, tableoid, a FROM public.base_tbl WHERE ((a < b)) FOR UPDATE (5 rows) UPDATE rw_view SET b = b + 15; -- ok @@ -8202,14 +8202,14 @@ SELECT * FROM foreign_tbl; EXPLAIN (VERBOSE, COSTS OFF) UPDATE rw_view SET b = b + 5; - QUERY PLAN ------------------------------------------------------------------------------------------------- + QUERY PLAN +---------------------------------------------------------------------------------------------------------------- Update on public.parent_tbl Foreign Update on public.foreign_tbl parent_tbl_1 - Remote SQL: UPDATE public.child_tbl SET b = $2 WHERE ctid = $1 RETURNING a, b + Remote SQL: UPDATE public.child_tbl SET b = $3 WHERE ctid = $1 AND tableoid = $2 RETURNING a, b -> Foreign Scan on public.foreign_tbl parent_tbl_1 - Output: (parent_tbl_1.b + 5), parent_tbl_1.tableoid, parent_tbl_1.ctid, parent_tbl_1.* - Remote SQL: SELECT a, b, ctid FROM public.child_tbl WHERE ((a < b)) FOR UPDATE + Output: (parent_tbl_1.b + 5), parent_tbl_1.tableoid, parent_tbl_1.ctid, remotetableoid, parent_tbl_1.* + Remote SQL: SELECT b, ctid, tableoid, a FROM public.child_tbl WHERE ((a < b)) FOR UPDATE (6 rows) UPDATE rw_view SET b = b + 5; -- should fail @@ -8217,14 +8217,14 @@ ERROR: new row violates check option for view "rw_view" DETAIL: Failing row contains (20, 20). EXPLAIN (VERBOSE, COSTS OFF) UPDATE rw_view SET b = b + 15; - QUERY PLAN -------------------------------------------------------------------------------------------------- + QUERY PLAN +----------------------------------------------------------------------------------------------------------------- Update on public.parent_tbl Foreign Update on public.foreign_tbl parent_tbl_1 - Remote SQL: UPDATE public.child_tbl SET b = $2 WHERE ctid = $1 RETURNING a, b + Remote SQL: UPDATE public.child_tbl SET b = $3 WHERE ctid = $1 AND tableoid = $2 RETURNING a, b -> Foreign Scan on public.foreign_tbl parent_tbl_1 - Output: (parent_tbl_1.b + 15), parent_tbl_1.tableoid, parent_tbl_1.ctid, parent_tbl_1.* - Remote SQL: SELECT a, b, ctid FROM public.child_tbl WHERE ((a < b)) FOR UPDATE + Output: (parent_tbl_1.b + 15), parent_tbl_1.tableoid, parent_tbl_1.ctid, remotetableoid, parent_tbl_1.* + Remote SQL: SELECT b, ctid, tableoid, a FROM public.child_tbl WHERE ((a < b)) FOR UPDATE (6 rows) UPDATE rw_view SET b = b + 15; -- ok @@ -8273,14 +8273,14 @@ CREATE VIEW rw_view AS SELECT * FROM parent_tbl WHERE a < 5 WITH CHECK OPTION; INSERT INTO parent_tbl (a) VALUES(1),(5); EXPLAIN (VERBOSE, COSTS OFF) UPDATE rw_view SET b = 'text', c = 123.456; - QUERY PLAN -------------------------------------------------------------------------------------------------- + QUERY PLAN +----------------------------------------------------------------------------------------------------------------- Update on public.parent_tbl Foreign Update on public.child_foreign parent_tbl_1 - Remote SQL: UPDATE public.child_local SET b = $2, c = $3 WHERE ctid = $1 RETURNING a + Remote SQL: UPDATE public.child_local SET b = $3, c = $4 WHERE ctid = $1 AND tableoid = $2 RETURNING a -> Foreign Scan on public.child_foreign parent_tbl_1 - Output: 'text'::text, 123.456, parent_tbl_1.tableoid, parent_tbl_1.ctid, parent_tbl_1.* - Remote SQL: SELECT b, c, a, ctid FROM public.child_local WHERE ((a < 5)) FOR UPDATE + Output: 'text'::text, 123.456, parent_tbl_1.tableoid, parent_tbl_1.ctid, remotetableoid, parent_tbl_1.* + Remote SQL: SELECT ctid, tableoid, a, b, c FROM public.child_local WHERE ((a < 5)) FOR UPDATE (6 rows) UPDATE rw_view SET b = 'text', c = 123.456; @@ -8359,13 +8359,13 @@ insert into grem1 (a) values (1), (2); insert into grem1 (a) values (1), (2); explain (verbose, costs off) update grem1 set a = 22 where a = 2; - QUERY PLAN ----------------------------------------------------------------------------------------- + QUERY PLAN +---------------------------------------------------------------------------------------------------------- Update on public.grem1 - Remote SQL: UPDATE public.gloc1 SET a = $2, b = DEFAULT, c = DEFAULT WHERE ctid = $1 + Remote SQL: UPDATE public.gloc1 SET a = $3, b = DEFAULT, c = DEFAULT WHERE ctid = $1 AND tableoid = $2 -> Foreign Scan on public.grem1 - Output: 22, ctid, grem1.* - Remote SQL: SELECT a, b, c, ctid FROM public.gloc1 WHERE ((a = 2)) FOR UPDATE + Output: 22, ctid, remotetableoid, grem1.* + Remote SQL: SELECT ctid, tableoid, a, b, c FROM public.gloc1 WHERE ((a = 2)) FOR UPDATE (5 rows) update grem1 set a = 22 where a = 2; @@ -8692,13 +8692,13 @@ SELECT * from loc1; EXPLAIN (verbose, costs off) UPDATE rem1 set f1 = 10; -- all columns should be transmitted - QUERY PLAN ------------------------------------------------------------------------ + QUERY PLAN +----------------------------------------------------------------------------------------- Update on public.rem1 - Remote SQL: UPDATE public.loc1 SET f1 = $2, f2 = $3 WHERE ctid = $1 + Remote SQL: UPDATE public.loc1 SET f1 = $3, f2 = $4 WHERE ctid = $1 AND tableoid = $2 -> Foreign Scan on public.rem1 - Output: 10, ctid, rem1.* - Remote SQL: SELECT f1, f2, ctid FROM public.loc1 FOR UPDATE + Output: 10, ctid, remotetableoid, rem1.* + Remote SQL: SELECT ctid, tableoid, f1, f2 FROM public.loc1 FOR UPDATE (5 rows) UPDATE rem1 set f1 = 10; @@ -8840,12 +8840,12 @@ DELETE FROM rem1; -- can be pushed down EXPLAIN (verbose, costs off) DELETE FROM rem1 WHERE false; -- currently can't be pushed down - QUERY PLAN -------------------------------------------------------- + QUERY PLAN +------------------------------------------------------------------------- Delete on public.rem1 - Remote SQL: DELETE FROM public.loc1 WHERE ctid = $1 + Remote SQL: DELETE FROM public.loc1 WHERE ctid = $1 AND tableoid = $2 -> Result - Output: ctid + Output: ctid, NULL::oid Replaces: Scan on rem1 One-Time Filter: false (6 rows) @@ -8946,13 +8946,13 @@ BEFORE UPDATE ON rem1 FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo'); EXPLAIN (verbose, costs off) UPDATE rem1 set f2 = ''; -- can't be pushed down - QUERY PLAN ------------------------------------------------------------------------ + QUERY PLAN +----------------------------------------------------------------------------------------- Update on public.rem1 - Remote SQL: UPDATE public.loc1 SET f1 = $2, f2 = $3 WHERE ctid = $1 + Remote SQL: UPDATE public.loc1 SET f1 = $3, f2 = $4 WHERE ctid = $1 AND tableoid = $2 -> Foreign Scan on public.rem1 - Output: ''::text, ctid, rem1.* - Remote SQL: SELECT f1, f2, ctid FROM public.loc1 FOR UPDATE + Output: ''::text, ctid, remotetableoid, rem1.* + Remote SQL: SELECT ctid, tableoid, f1, f2 FROM public.loc1 FOR UPDATE (5 rows) EXPLAIN (verbose, costs off) @@ -8970,13 +8970,13 @@ AFTER UPDATE ON rem1 FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo'); EXPLAIN (verbose, costs off) UPDATE rem1 set f2 = ''; -- can't be pushed down - QUERY PLAN -------------------------------------------------------------------------------- + QUERY PLAN +------------------------------------------------------------------------------------------------- Update on public.rem1 - Remote SQL: UPDATE public.loc1 SET f2 = $2 WHERE ctid = $1 RETURNING f1, f2 + Remote SQL: UPDATE public.loc1 SET f2 = $3 WHERE ctid = $1 AND tableoid = $2 RETURNING f1, f2 -> Foreign Scan on public.rem1 - Output: ''::text, ctid, rem1.* - Remote SQL: SELECT f1, f2, ctid FROM public.loc1 FOR UPDATE + Output: ''::text, ctid, remotetableoid, rem1.* + Remote SQL: SELECT ctid, tableoid, f1, f2 FROM public.loc1 FOR UPDATE (5 rows) EXPLAIN (verbose, costs off) @@ -9004,13 +9004,13 @@ UPDATE rem1 set f2 = ''; -- can be pushed down EXPLAIN (verbose, costs off) DELETE FROM rem1; -- can't be pushed down - QUERY PLAN ---------------------------------------------------------------------- + QUERY PLAN +------------------------------------------------------------------------------- Delete on public.rem1 - Remote SQL: DELETE FROM public.loc1 WHERE ctid = $1 + Remote SQL: DELETE FROM public.loc1 WHERE ctid = $1 AND tableoid = $2 -> Foreign Scan on public.rem1 - Output: ctid, rem1.* - Remote SQL: SELECT f1, f2, ctid FROM public.loc1 FOR UPDATE + Output: ctid, remotetableoid, rem1.* + Remote SQL: SELECT ctid, tableoid, f1, f2 FROM public.loc1 FOR UPDATE (5 rows) DROP TRIGGER trig_row_before_delete ON rem1; @@ -9028,13 +9028,13 @@ UPDATE rem1 set f2 = ''; -- can be pushed down EXPLAIN (verbose, costs off) DELETE FROM rem1; -- can't be pushed down - QUERY PLAN ------------------------------------------------------------------------- + QUERY PLAN +------------------------------------------------------------------------------------------ Delete on public.rem1 - Remote SQL: DELETE FROM public.loc1 WHERE ctid = $1 RETURNING f1, f2 + Remote SQL: DELETE FROM public.loc1 WHERE ctid = $1 AND tableoid = $2 RETURNING f1, f2 -> Foreign Scan on public.rem1 - Output: ctid, rem1.* - Remote SQL: SELECT f1, f2, ctid FROM public.loc1 FOR UPDATE + Output: ctid, remotetableoid, rem1.* + Remote SQL: SELECT ctid, tableoid, f1, f2 FROM public.loc1 FOR UPDATE (5 rows) DROP TRIGGER trig_row_after_delete ON rem1; @@ -9071,28 +9071,28 @@ CONTEXT: COPY parent_tbl, line 1: "AAA 42" ALTER SERVER loopback OPTIONS (DROP batch_size); EXPLAIN (VERBOSE, COSTS OFF) UPDATE parent_tbl SET b = b + 1; - QUERY PLAN ------------------------------------------------------------------------------------------------- + QUERY PLAN +---------------------------------------------------------------------------------------------------------------- Update on public.parent_tbl Foreign Update on public.foreign_tbl parent_tbl_1 - Remote SQL: UPDATE public.local_tbl SET b = $2 WHERE ctid = $1 + Remote SQL: UPDATE public.local_tbl SET b = $3 WHERE ctid = $1 AND tableoid = $2 -> Foreign Scan on public.foreign_tbl parent_tbl_1 - Output: (parent_tbl_1.b + 1), parent_tbl_1.tableoid, parent_tbl_1.ctid, parent_tbl_1.* - Remote SQL: SELECT a, b, ctid FROM public.local_tbl FOR UPDATE + Output: (parent_tbl_1.b + 1), parent_tbl_1.tableoid, parent_tbl_1.ctid, remotetableoid, parent_tbl_1.* + Remote SQL: SELECT b, ctid, tableoid, a FROM public.local_tbl FOR UPDATE (6 rows) UPDATE parent_tbl SET b = b + 1; ERROR: cannot collect transition tuples from child foreign tables EXPLAIN (VERBOSE, COSTS OFF) DELETE FROM parent_tbl; - QUERY PLAN ------------------------------------------------------------------- + QUERY PLAN +-------------------------------------------------------------------------------- Delete on public.parent_tbl Foreign Delete on public.foreign_tbl parent_tbl_1 - Remote SQL: DELETE FROM public.local_tbl WHERE ctid = $1 + Remote SQL: DELETE FROM public.local_tbl WHERE ctid = $1 AND tableoid = $2 -> Foreign Scan on public.foreign_tbl parent_tbl_1 - Output: parent_tbl_1.tableoid, parent_tbl_1.ctid - Remote SQL: SELECT ctid FROM public.local_tbl FOR UPDATE + Output: parent_tbl_1.tableoid, parent_tbl_1.ctid, remotetableoid + Remote SQL: SELECT ctid, tableoid FROM public.local_tbl FOR UPDATE (6 rows) DELETE FROM parent_tbl; @@ -9110,38 +9110,38 @@ CREATE TRIGGER parent_tbl_delete_trig FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func(); EXPLAIN (VERBOSE, COSTS OFF) UPDATE parent_tbl SET b = b + 1; - QUERY PLAN ------------------------------------------------------------------------------------------------------- + QUERY PLAN +---------------------------------------------------------------------------------------------------------------------- Update on public.parent_tbl Update on public.parent_tbl parent_tbl_1 Foreign Update on public.foreign_tbl parent_tbl_2 - Remote SQL: UPDATE public.local_tbl SET b = $2 WHERE ctid = $1 + Remote SQL: UPDATE public.local_tbl SET b = $3 WHERE ctid = $1 AND tableoid = $2 -> Result - Output: (parent_tbl.b + 1), parent_tbl.tableoid, parent_tbl.ctid, (NULL::record) + Output: (parent_tbl.b + 1), parent_tbl.tableoid, parent_tbl.ctid, (NULL::oid), (NULL::record) -> Append -> Seq Scan on public.parent_tbl parent_tbl_1 - Output: parent_tbl_1.b, parent_tbl_1.tableoid, parent_tbl_1.ctid, NULL::record + Output: parent_tbl_1.b, parent_tbl_1.tableoid, parent_tbl_1.ctid, NULL::oid, NULL::record -> Foreign Scan on public.foreign_tbl parent_tbl_2 - Output: parent_tbl_2.b, parent_tbl_2.tableoid, parent_tbl_2.ctid, parent_tbl_2.* - Remote SQL: SELECT a, b, ctid FROM public.local_tbl FOR UPDATE + Output: parent_tbl_2.b, parent_tbl_2.tableoid, parent_tbl_2.ctid, remotetableoid, parent_tbl_2.* + Remote SQL: SELECT b, ctid, tableoid, a FROM public.local_tbl FOR UPDATE (12 rows) UPDATE parent_tbl SET b = b + 1; ERROR: cannot collect transition tuples from child foreign tables EXPLAIN (VERBOSE, COSTS OFF) DELETE FROM parent_tbl; - QUERY PLAN ------------------------------------------------------------------------- + QUERY PLAN +---------------------------------------------------------------------------------- Delete on public.parent_tbl Delete on public.parent_tbl parent_tbl_1 Foreign Delete on public.foreign_tbl parent_tbl_2 - Remote SQL: DELETE FROM public.local_tbl WHERE ctid = $1 + Remote SQL: DELETE FROM public.local_tbl WHERE ctid = $1 AND tableoid = $2 -> Append -> Seq Scan on public.parent_tbl parent_tbl_1 - Output: parent_tbl_1.tableoid, parent_tbl_1.ctid + Output: parent_tbl_1.tableoid, parent_tbl_1.ctid, NULL::oid -> Foreign Scan on public.foreign_tbl parent_tbl_2 - Output: parent_tbl_2.tableoid, parent_tbl_2.ctid - Remote SQL: SELECT ctid FROM public.local_tbl FOR UPDATE + Output: parent_tbl_2.tableoid, parent_tbl_2.ctid, remotetableoid + Remote SQL: SELECT ctid, tableoid FROM public.local_tbl FOR UPDATE (10 rows) DELETE FROM parent_tbl; @@ -9484,22 +9484,22 @@ drop table foo2child; -- Check UPDATE with inherited target and an inherited source table explain (verbose, costs off) update bar set f2 = f2 + 100 where f1 in (select f1 from foo); - QUERY PLAN -------------------------------------------------------------------------------------------------------- + QUERY PLAN +-------------------------------------------------------------------------------------------------------------------- Update on public.bar Update on public.bar bar_1 Foreign Update on public.bar2 bar_2 - Remote SQL: UPDATE public.loct2 SET f2 = $2 WHERE ctid = $1 + Remote SQL: UPDATE public.loct2 SET f2 = $3 WHERE ctid = $1 AND tableoid = $2 -> Hash Join - Output: (bar.f2 + 100), foo.ctid, bar.tableoid, bar.ctid, (NULL::record), foo.*, foo.tableoid + Output: (bar.f2 + 100), foo.ctid, bar.tableoid, bar.ctid, (NULL::oid), (NULL::record), foo.*, foo.tableoid Inner Unique: true Hash Cond: (bar.f1 = foo.f1) -> Append -> Seq Scan on public.bar bar_1 - Output: bar_1.f2, bar_1.f1, bar_1.tableoid, bar_1.ctid, NULL::record + Output: bar_1.f2, bar_1.f1, bar_1.tableoid, bar_1.ctid, NULL::oid, NULL::record -> Foreign Scan on public.bar2 bar_2 - Output: bar_2.f2, bar_2.f1, bar_2.tableoid, bar_2.ctid, bar_2.* - Remote SQL: SELECT f1, f2, f3, ctid FROM public.loct2 FOR UPDATE + Output: bar_2.f2, bar_2.f1, bar_2.tableoid, bar_2.ctid, remotetableoid, bar_2.* + Remote SQL: SELECT f2, f1, ctid, tableoid, f3 FROM public.loct2 FOR UPDATE -> Hash Output: foo.ctid, foo.f1, foo.*, foo.tableoid -> HashAggregate @@ -9531,24 +9531,24 @@ update bar set f2 = f2 + 100 from ( select f1 from foo union all select f1+3 from foo ) ss where bar.f1 = ss.f1; - QUERY PLAN ------------------------------------------------------------------------------------------------- + QUERY PLAN +----------------------------------------------------------------------------------------------------------- Update on public.bar Update on public.bar bar_1 Foreign Update on public.bar2 bar_2 - Remote SQL: UPDATE public.loct2 SET f2 = $2 WHERE ctid = $1 + Remote SQL: UPDATE public.loct2 SET f2 = $3 WHERE ctid = $1 AND tableoid = $2 -> Merge Join - Output: (bar.f2 + 100), (ROW(foo.f1)), bar.tableoid, bar.ctid, (NULL::record) + Output: (bar.f2 + 100), (ROW(foo.f1)), bar.tableoid, bar.ctid, (NULL::oid), (NULL::record) Merge Cond: (bar.f1 = foo.f1) -> Sort - Output: bar.f2, bar.f1, bar.tableoid, bar.ctid, (NULL::record) + Output: bar.f2, bar.f1, bar.tableoid, bar.ctid, (NULL::oid), (NULL::record) Sort Key: bar.f1 -> Append -> Seq Scan on public.bar bar_1 - Output: bar_1.f2, bar_1.f1, bar_1.tableoid, bar_1.ctid, NULL::record + Output: bar_1.f2, bar_1.f1, bar_1.tableoid, bar_1.ctid, NULL::oid, NULL::record -> Foreign Scan on public.bar2 bar_2 - Output: bar_2.f2, bar_2.f1, bar_2.tableoid, bar_2.ctid, bar_2.* - Remote SQL: SELECT f1, f2, f3, ctid FROM public.loct2 FOR UPDATE + Output: bar_2.f2, bar_2.f1, bar_2.tableoid, bar_2.ctid, remotetableoid, bar_2.* + Remote SQL: SELECT f2, f1, ctid, tableoid, f3 FROM public.loct2 FOR UPDATE -> Sort Output: (ROW(foo.f1)), foo.f1 Sort Key: foo.f1 @@ -9697,7 +9697,7 @@ delete from foo where f1 < 5 returning *; Foreign Delete on public.foo2 foo_2 -> Append -> Index Scan using i_foo_f1 on public.foo foo_1 - Output: foo_1.tableoid, foo_1.ctid + Output: foo_1.tableoid, foo_1.ctid, NULL::oid Index Cond: (foo_1.f1 < 5) -> Foreign Delete on public.foo2 foo_2 Remote SQL: DELETE FROM public.loct1 WHERE ((f1 < 5)) RETURNING f1, f2 @@ -9715,17 +9715,17 @@ delete from foo where f1 < 5 returning *; explain (verbose, costs off) update bar set f2 = f2 + 100 returning *; - QUERY PLAN ------------------------------------------------------------------------------------------- + QUERY PLAN +------------------------------------------------------------------------------------------- Update on public.bar Output: bar_1.f1, bar_1.f2 Update on public.bar bar_1 Foreign Update on public.bar2 bar_2 -> Result - Output: (bar.f2 + 100), bar.tableoid, bar.ctid, (NULL::record) + Output: (bar.f2 + 100), bar.tableoid, bar.ctid, (NULL::oid), (NULL::record) -> Append -> Seq Scan on public.bar bar_1 - Output: bar_1.f2, bar_1.tableoid, bar_1.ctid, NULL::record + Output: bar_1.f2, bar_1.tableoid, bar_1.ctid, NULL::oid, NULL::record -> Foreign Update on public.bar2 bar_2 Remote SQL: UPDATE public.loct2 SET f2 = (f2 + 100) RETURNING f1, f2 (11 rows) @@ -9750,20 +9750,20 @@ 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 --------------------------------------------------------------------------------------------------------- + QUERY PLAN +-------------------------------------------------------------------------------------------------------------------------- Update on public.bar Update on public.bar bar_1 Foreign Update on public.bar2 bar_2 - Remote SQL: UPDATE public.loct2 SET f1 = $2, f2 = $3, f3 = $4 WHERE ctid = $1 RETURNING f1, f2, f3 + Remote SQL: UPDATE public.loct2 SET f1 = $3, f2 = $4, f3 = $5 WHERE ctid = $1 AND tableoid = $2 RETURNING f1, f2, f3 -> Result - Output: (bar.f2 + 100), bar.tableoid, bar.ctid, (NULL::record) + Output: (bar.f2 + 100), bar.tableoid, bar.ctid, (NULL::oid), (NULL::record) -> Append -> Seq Scan on public.bar bar_1 - Output: bar_1.f2, bar_1.tableoid, bar_1.ctid, NULL::record + Output: bar_1.f2, bar_1.tableoid, bar_1.ctid, NULL::oid, NULL::record -> Foreign Scan on public.bar2 bar_2 - Output: bar_2.f2, bar_2.tableoid, bar_2.ctid, bar_2.* - Remote SQL: SELECT f1, f2, f3, ctid FROM public.loct2 FOR UPDATE + Output: bar_2.f2, bar_2.tableoid, bar_2.ctid, remotetableoid, bar_2.* + Remote SQL: SELECT f2, ctid, tableoid, f1, f3 FROM public.loct2 FOR UPDATE (12 rows) update bar set f2 = f2 + 100; @@ -9781,19 +9781,19 @@ 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 ---------------------------------------------------------------------------------------------------- + QUERY PLAN +------------------------------------------------------------------------------------------------------------- Delete on public.bar Delete on public.bar bar_1 Foreign Delete on public.bar2 bar_2 - Remote SQL: DELETE FROM public.loct2 WHERE ctid = $1 RETURNING f1, f2, f3 + Remote SQL: DELETE FROM public.loct2 WHERE ctid = $1 AND tableoid = $2 RETURNING f1, f2, f3 -> Append -> Seq Scan on public.bar bar_1 - Output: bar_1.tableoid, bar_1.ctid, NULL::record + Output: bar_1.tableoid, bar_1.ctid, NULL::oid, NULL::record Filter: (bar_1.f2 < 400) -> Foreign Scan on public.bar2 bar_2 - Output: bar_2.tableoid, bar_2.ctid, bar_2.* - Remote SQL: SELECT f1, f2, f3, ctid FROM public.loct2 WHERE ((f2 < 400)) FOR UPDATE + Output: bar_2.tableoid, bar_2.ctid, remotetableoid, bar_2.* + Remote SQL: SELECT ctid, tableoid, f2, f1, f3 FROM public.loct2 WHERE ((f2 < 400)) FOR UPDATE (11 rows) delete from bar where f2 < 400; @@ -9825,22 +9825,22 @@ 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 ----------------------------------------------------------------------------------------------------------------- + QUERY PLAN +----------------------------------------------------------------------------------------------------------------------------- Update on public.parent Output: parent_1.a, parent_1.b, remt2.a, remt2.b Update on public.parent parent_1 Foreign Update on public.remt1 parent_2 - Remote SQL: UPDATE public.loct1 SET b = $2 WHERE ctid = $1 RETURNING a, b + Remote SQL: UPDATE public.loct1 SET b = $3 WHERE ctid = $1 AND tableoid = $2 RETURNING a, b -> Nested Loop - Output: (parent.b || remt2.b), remt2.*, remt2.a, remt2.b, parent.tableoid, parent.ctid, (NULL::record) + Output: (parent.b || remt2.b), remt2.*, remt2.a, remt2.b, parent.tableoid, parent.ctid, (NULL::oid), (NULL::record) Join Filter: (parent.a = remt2.a) -> Append -> Seq Scan on public.parent parent_1 - Output: parent_1.b, parent_1.a, parent_1.tableoid, parent_1.ctid, NULL::record + Output: parent_1.b, parent_1.a, parent_1.tableoid, parent_1.ctid, NULL::oid, NULL::record -> Foreign Scan on public.remt1 parent_2 - Output: parent_2.b, parent_2.a, parent_2.tableoid, parent_2.ctid, parent_2.* - Remote SQL: SELECT a, b, ctid FROM public.loct1 FOR UPDATE + Output: parent_2.b, parent_2.a, parent_2.tableoid, parent_2.ctid, remotetableoid, parent_2.* + Remote SQL: SELECT b, a, ctid, tableoid FROM public.loct1 FOR UPDATE -> Materialize Output: remt2.b, remt2.*, remt2.a -> Foreign Scan on public.remt2 @@ -9857,22 +9857,22 @@ update parent set b = parent.b || remt2.b from remt2 where parent.a = remt2.a re explain (verbose, costs off) delete from parent using remt2 where parent.a = remt2.a returning parent; - QUERY PLAN ------------------------------------------------------------------------------ + QUERY PLAN +------------------------------------------------------------------------------------------- Delete on public.parent Output: parent_1.* Delete on public.parent parent_1 Foreign Delete on public.remt1 parent_2 - Remote SQL: DELETE FROM public.loct1 WHERE ctid = $1 RETURNING a, b + Remote SQL: DELETE FROM public.loct1 WHERE ctid = $1 AND tableoid = $2 RETURNING a, b -> Nested Loop - Output: remt2.*, parent.tableoid, parent.ctid + Output: remt2.*, parent.tableoid, parent.ctid, (NULL::oid) Join Filter: (parent.a = remt2.a) -> Append -> Seq Scan on public.parent parent_1 - Output: parent_1.a, parent_1.tableoid, parent_1.ctid + Output: parent_1.a, parent_1.tableoid, parent_1.ctid, NULL::oid -> Foreign Scan on public.remt1 parent_2 - Output: parent_2.a, parent_2.tableoid, parent_2.ctid - Remote SQL: SELECT a, ctid FROM public.loct1 FOR UPDATE + Output: parent_2.a, parent_2.tableoid, parent_2.ctid, remotetableoid + Remote SQL: SELECT a, ctid, tableoid FROM public.loct1 FOR UPDATE -> Materialize Output: remt2.*, remt2.a -> Foreign Scan on public.remt2 @@ -10102,7 +10102,7 @@ update utrtest set a = 1 where a = 1 or a = 2 returning *; -> Foreign Update on public.remp utrtest_1 Remote SQL: UPDATE public.loct SET a = 1 WHERE (((a = 1) OR (a = 2))) RETURNING a, b -> Seq Scan on public.locp utrtest_2 - Output: 1, utrtest_2.tableoid, utrtest_2.ctid, NULL::record + Output: 1, utrtest_2.tableoid, utrtest_2.ctid, NULL::oid, NULL::record Filter: ((utrtest_2.a = 1) OR (utrtest_2.a = 2)) (10 rows) @@ -10141,8 +10141,8 @@ insert into utrtest values (2, 'qux'); -- with a direct modification plan explain (verbose, costs off) update utrtest set a = 1 returning *; - QUERY PLAN ---------------------------------------------------------------------------- + QUERY PLAN +-------------------------------------------------------------------------------------- Update on public.utrtest Output: utrtest_1.a, utrtest_1.b Foreign Update on public.remp utrtest_1 @@ -10151,7 +10151,7 @@ update utrtest set a = 1 returning *; -> Foreign Update on public.remp utrtest_1 Remote SQL: UPDATE public.loct SET a = 1 RETURNING a, b -> Seq Scan on public.locp utrtest_2 - Output: 1, utrtest_2.tableoid, utrtest_2.ctid, NULL::record + Output: 1, utrtest_2.tableoid, utrtest_2.ctid, NULL::oid, NULL::record (9 rows) update utrtest set a = 1 returning *; @@ -10162,22 +10162,22 @@ insert into utrtest values (2, 'qux'); -- with a non-direct modification plan explain (verbose, costs off) update utrtest set a = 1 from (values (1), (2)) s(x) where a = s.x returning *; - QUERY PLAN ------------------------------------------------------------------------------------------------- + QUERY PLAN +---------------------------------------------------------------------------------------------------------------- Update on public.utrtest Output: utrtest_1.a, utrtest_1.b, "*VALUES*".column1 Foreign Update on public.remp utrtest_1 - Remote SQL: UPDATE public.loct SET a = $2 WHERE ctid = $1 RETURNING a, b + Remote SQL: UPDATE public.loct SET a = $3 WHERE ctid = $1 AND tableoid = $2 RETURNING a, b Update on public.locp utrtest_2 -> Hash Join - Output: 1, "*VALUES*".*, "*VALUES*".column1, utrtest.tableoid, utrtest.ctid, utrtest.* + Output: 1, "*VALUES*".*, "*VALUES*".column1, utrtest.tableoid, utrtest.ctid, remotetableoid, utrtest.* Hash Cond: (utrtest.a = "*VALUES*".column1) -> Append -> Foreign Scan on public.remp utrtest_1 - Output: utrtest_1.a, utrtest_1.tableoid, utrtest_1.ctid, utrtest_1.* - Remote SQL: SELECT a, b, ctid FROM public.loct FOR UPDATE + Output: utrtest_1.a, utrtest_1.tableoid, utrtest_1.ctid, remotetableoid, utrtest_1.* + Remote SQL: SELECT a, ctid, tableoid, b FROM public.loct FOR UPDATE -> Seq Scan on public.locp utrtest_2 - Output: utrtest_2.a, utrtest_2.tableoid, utrtest_2.ctid, NULL::record + Output: utrtest_2.a, utrtest_2.tableoid, utrtest_2.ctid, NULL::oid, NULL::record -> Hash Output: "*VALUES*".*, "*VALUES*".column1 -> Values Scan on "*VALUES*" @@ -10201,15 +10201,15 @@ insert into utrtest values (3, 'xyzzy'); -- with a direct modification plan explain (verbose, costs off) update utrtest set a = 3 returning *; - QUERY PLAN ---------------------------------------------------------------------------- + QUERY PLAN +-------------------------------------------------------------------------------------- Update on public.utrtest Output: utrtest_1.a, utrtest_1.b Update on public.locp utrtest_1 Foreign Update on public.remp utrtest_2 -> Append -> Seq Scan on public.locp utrtest_1 - Output: 3, utrtest_1.tableoid, utrtest_1.ctid, NULL::record + Output: 3, utrtest_1.tableoid, utrtest_1.ctid, NULL::oid, NULL::record -> Foreign Update on public.remp utrtest_2 Remote SQL: UPDATE public.loct SET a = 3 RETURNING a, b (9 rows) @@ -10219,22 +10219,22 @@ ERROR: cannot route tuples into foreign table to be updated "remp" -- with a non-direct modification plan explain (verbose, costs off) update utrtest set a = 3 from (values (2), (3)) s(x) where a = s.x returning *; - QUERY PLAN ------------------------------------------------------------------------------------------------------ + QUERY PLAN +------------------------------------------------------------------------------------------------------------------ Update on public.utrtest Output: utrtest_1.a, utrtest_1.b, "*VALUES*".column1 Update on public.locp utrtest_1 Foreign Update on public.remp utrtest_2 - Remote SQL: UPDATE public.loct SET a = $2 WHERE ctid = $1 RETURNING a, b + Remote SQL: UPDATE public.loct SET a = $3 WHERE ctid = $1 AND tableoid = $2 RETURNING a, b -> Hash Join - Output: 3, "*VALUES*".*, "*VALUES*".column1, utrtest.tableoid, utrtest.ctid, (NULL::record) + Output: 3, "*VALUES*".*, "*VALUES*".column1, utrtest.tableoid, utrtest.ctid, (NULL::oid), (NULL::record) Hash Cond: (utrtest.a = "*VALUES*".column1) -> Append -> Seq Scan on public.locp utrtest_1 - Output: utrtest_1.a, utrtest_1.tableoid, utrtest_1.ctid, NULL::record + Output: utrtest_1.a, utrtest_1.tableoid, utrtest_1.ctid, NULL::oid, NULL::record -> Foreign Scan on public.remp utrtest_2 - Output: utrtest_2.a, utrtest_2.tableoid, utrtest_2.ctid, utrtest_2.* - Remote SQL: SELECT a, b, ctid FROM public.loct FOR UPDATE + Output: utrtest_2.a, utrtest_2.tableoid, utrtest_2.ctid, remotetableoid, utrtest_2.* + Remote SQL: SELECT a, ctid, tableoid, b FROM public.loct FOR UPDATE -> Hash Output: "*VALUES*".*, "*VALUES*".column1 -> Values Scan on "*VALUES*" @@ -13187,8 +13187,8 @@ RESET enable_hashjoin; -- Test that UPDATE/DELETE with inherited target works with async_capable enabled EXPLAIN (VERBOSE, COSTS OFF) UPDATE async_pt SET c = c || c WHERE b = 0 RETURNING *; - QUERY PLAN ----------------------------------------------------------------------------------------------------------- + QUERY PLAN +--------------------------------------------------------------------------------------------------------------------- Update on public.async_pt Output: async_pt_1.a, async_pt_1.b, async_pt_1.c Foreign Update on public.async_p1 async_pt_1 @@ -13200,7 +13200,7 @@ UPDATE async_pt SET c = c || c WHERE b = 0 RETURNING *; -> Foreign Update on public.async_p2 async_pt_2 Remote SQL: UPDATE public.base_tbl2 SET c = (c || c) WHERE ((b = 0)) RETURNING a, b, c -> Seq Scan on public.async_p3 async_pt_3 - Output: (async_pt_3.c || async_pt_3.c), async_pt_3.tableoid, async_pt_3.ctid, NULL::record + Output: (async_pt_3.c || async_pt_3.c), async_pt_3.tableoid, async_pt_3.ctid, NULL::oid, NULL::record Filter: (async_pt_3.b = 0) (13 rows) @@ -13227,7 +13227,7 @@ DELETE FROM async_pt WHERE b = 0 RETURNING *; -> Foreign Delete on public.async_p2 async_pt_2 Remote SQL: DELETE FROM public.base_tbl2 WHERE ((b = 0)) RETURNING a, b, c -> Seq Scan on public.async_p3 async_pt_3 - Output: async_pt_3.tableoid, async_pt_3.ctid + Output: async_pt_3.tableoid, async_pt_3.ctid, NULL::oid Filter: (async_pt_3.b = 0) (13 rows) @@ -14048,3 +14048,106 @@ RESET client_min_messages; DROP FUNCTION wait_for_backend_termination(int); DROP FOREIGN TABLE remote_backend_pid; DROP VIEW my_backend_pid; +-- =================================================================== +-- test UPDATE/DELETE on a foreign table whose remote counterpart is +-- itself a partitioned table +-- =================================================================== +-- The two rows below land in separate partitions and, being the first +-- row in each partition's heap, collide on ctid (0,1). A row-identity +-- scheme that relies on ctid alone can't tell them apart. +CREATE TABLE parted_remote (a int, b int) PARTITION BY LIST (a); +CREATE TABLE parted_remote_p1 PARTITION OF parted_remote FOR VALUES IN (1); +CREATE TABLE parted_remote_p2 PARTITION OF parted_remote FOR VALUES IN (2); +INSERT INTO parted_remote_p1 VALUES (1, 100); +INSERT INTO parted_remote_p2 VALUES (2, 200); +CREATE FOREIGN TABLE foreign_parted_remote (a int, b int) + SERVER loopback OPTIONS (table_name 'parted_remote'); +-- Force a non-direct-modify UPDATE (random() isn't shippable), so the +-- ctid/tableoid row-identity path below is actually exercised. +UPDATE foreign_parted_remote SET b = 999 WHERE a = 1 AND random() <= 1; +-- Only the a = 1 row (in parted_remote_p1) should have changed. +SELECT tableoid::regclass, a, b FROM parted_remote ORDER BY a; + tableoid | a | b +------------------+---+----- + parted_remote_p1 | 1 | 999 + parted_remote_p2 | 2 | 200 +(2 rows) + +DELETE FROM foreign_parted_remote WHERE a = 2 AND random() <= 1; +-- Only the a = 2 row (in parted_remote_p2) should be gone. +SELECT tableoid::regclass, a, b FROM parted_remote ORDER BY a; + tableoid | a | b +------------------+---+----- + parted_remote_p1 | 1 | 999 +(1 row) + +DROP FOREIGN TABLE foreign_parted_remote; +DROP TABLE parted_remote; +-- A whole-row reference to the target relation in a local qual must resolve +-- consistently with the row-identity columns fetched for UPDATE/DELETE. +CREATE TABLE wr_remote (a int PRIMARY KEY, b text, c int); +INSERT INTO wr_remote SELECT i, 'row' || i, i * 10 FROM generate_series(1, 5) i; +CREATE FOREIGN TABLE wr_ft (a int, b text, c int) + SERVER loopback OPTIONS (table_name 'wr_remote'); +BEGIN; +EXPLAIN (VERBOSE, COSTS OFF) +DELETE FROM wr_ft WHERE wr_ft.* IS NULL AND random() <= 1; + QUERY PLAN +-------------------------------------------------------------------------------------- + Delete on public.wr_ft + Remote SQL: DELETE FROM public.wr_remote WHERE ctid = $1 AND tableoid = $2 + -> Foreign Scan on public.wr_ft + Output: ctid, remotetableoid + Filter: (((wr_ft.*)::wr_ft IS NULL) AND (random() <= '1'::double precision)) + Remote SQL: SELECT ctid, tableoid, a, b, c FROM public.wr_remote FOR UPDATE +(6 rows) + +DELETE FROM wr_ft WHERE wr_ft.* IS NULL AND random() <= 1; +UPDATE wr_ft SET b = 'x' WHERE wr_ft.* IS NULL AND random() <= 1; +DELETE FROM wr_ft WHERE wr_ft.* IS NOT NULL AND a = 2 AND random() <= 1 RETURNING a, b; + a | b +---+------ + 2 | row2 +(1 row) + +SELECT count(*) FROM wr_remote; + count +------- + 5 +(1 row) + +ROLLBACK; +DROP FOREIGN TABLE wr_ft; +DROP TABLE wr_remote; +-- Same, when the foreign table is a partition of a local partitioned table: +-- the parent's whole-row reference is then converted from the partition's +-- row type, which must remain visible above the retyped whole-row Var. +CREATE TABLE wr_remote2 (a int PRIMARY KEY, b text, c int); +INSERT INTO wr_remote2 SELECT i, 'row' || i, i * 10 FROM generate_series(1, 5) i; +CREATE TABLE wr_parent (a int, b text, c int) PARTITION BY RANGE (a); +CREATE FOREIGN TABLE wr_part1 PARTITION OF wr_parent + FOR VALUES FROM (1) TO (10) SERVER loopback OPTIONS (table_name 'wr_remote2'); +CREATE TABLE wr_part2 PARTITION OF wr_parent FOR VALUES FROM (10) TO (20); +INSERT INTO wr_part2 VALUES (11, 'local11', 110); +BEGIN; +DELETE FROM wr_parent WHERE wr_parent.* IS NULL AND random() <= 1; +UPDATE wr_parent SET b = 'x' WHERE wr_parent.* IS NULL AND random() <= 1; +UPDATE wr_parent SET b = 'x' WHERE wr_parent.* IS NOT NULL AND a < 5 AND random() <= 1 RETURNING a, b; + a | b +---+--- + 1 | x + 2 | x + 3 | x + 4 | x +(4 rows) + +DELETE FROM wr_parent WHERE wr_parent.* IS NOT NULL AND a IN (2, 11) AND random() <= 1 RETURNING a, b; + a | b +----+--------- + 2 | x + 11 | local11 +(2 rows) + +ROLLBACK; +DROP TABLE wr_parent; +DROP TABLE wr_remote2; diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c index 6757ee923cb..f636e0a997b 100644 --- a/contrib/postgres_fdw/postgres_fdw.c +++ b/contrib/postgres_fdw/postgres_fdw.c @@ -233,6 +233,8 @@ typedef struct PgFdwModifyState /* info about parameters for prepared statement */ AttrNumber ctidAttno; /* attnum of input resjunk ctid column */ + AttrNumber tableoidAttno; /* attnum of input resjunk remote tableoid + * column, or 0 if none */ int p_nums; /* number of parameters to transmit */ FmgrInfo *p_flinfo; /* output conversion functions for them */ @@ -558,6 +560,7 @@ static TupleTableSlot **execute_foreign_modify(EState *estate, static void prepare_foreign_modify(PgFdwModifyState *fmstate); static const char **convert_prep_stmt_params(PgFdwModifyState *fmstate, ItemPointer tupleid, + Oid tableoid, TupleTableSlot **slots, int numSlots); static void store_returning_result(PgFdwModifyState *fmstate, @@ -567,6 +570,7 @@ static void deallocate_query(PgFdwModifyState *fmstate); static List *build_remote_returning(Index rtindex, Relation rel, List *returningList); static void rebuild_fdw_scan_tlist(ForeignScan *fscan, List *tlist); +static void set_remote_tableoid_resnames(List *fdw_scan_tlist); static void execute_dml_stmt(ForeignScanState *node); static TupleTableSlot *get_returning_data(ForeignScanState *node); static void init_returning_filter(PgFdwDirectModifyState *dmstate, @@ -1347,6 +1351,84 @@ postgresGetForeignPaths(PlannerInfo *root, } } +/* + * Context for normalize_wholerow_vartype_mutator(). + */ +typedef struct WholeRowRetypeContext +{ + Index relid; /* RT index of the foreign table */ + bool relabel; /* keep the original type via RelabelType? */ +} WholeRowRetypeContext; + +/* + * normalize_wholerow_vartype_mutator + * Mutator for normalize_wholerow_vartype(). + */ +static Node * +normalize_wholerow_vartype_mutator(Node *node, WholeRowRetypeContext *context) +{ + if (node == NULL) + return NULL; + if (IsA(node, Var)) + { + Var *var = (Var *) node; + + if (var->varno == context->relid && + var->varattno == InvalidAttrNumber && + var->vartype != RECORDOID) + { + Oid origtype = var->vartype; + int32 origtypmod = var->vartypmod; + Oid origcollid = var->varcollid; + + var = copyObject(var); + var->vartype = RECORDOID; + var->vartypmod = -1; + + /* + * An expression above the Var (e.g. the ConvertRowtypeExpr that + * converts a partition's whole row to its parent's row type) + * still needs to see the original row type, so relabel. + */ + if (context->relabel) + return (Node *) makeRelabelType((Expr *) var, origtype, + origtypmod, origcollid, + COERCE_IMPLICIT_CAST); + } + return (Node *) var; + } + return expression_tree_mutator(node, normalize_wholerow_vartype_mutator, + (void *) context); +} + +/* + * normalize_wholerow_vartype + * Return a copy of exprs in which every whole-row Var on relid is + * typed RECORD, regardless of what it was typed originally. + * + * Core's own row-identity whole-row Var (see add_row_identity_columns()), + * which shows up in the scan's output targetlist for UPDATE or when there are + * row triggers, is always typed RECORD, while a direct whole-row reference in + * a qual (e.g. "t.* IS NULL") has the relation's real row type. setrefs.c's + * Var-matching keys purely on varno/varattno and keeps whichever vartype the + * reference being fixed up already has, so mismatched types would make two + * Vars that denote the same fdw_scan_tlist entry resolve inconsistently. + * Normalizing the Vars to RECORD avoids that. + * + * If relabel is true, each retyped Var is wrapped in a RelabelType back to its + * original type, so that the surrounding expression is unchanged. Pass true + * for expressions (quals) and false for the Vars of fdw_scan_tlist itself. + */ +static List * +normalize_wholerow_vartype(List *exprs, Index relid, bool relabel) +{ + WholeRowRetypeContext context; + + context.relid = relid; + context.relabel = relabel; + return (List *) normalize_wholerow_vartype_mutator((Node *) exprs, &context); +} + /* * postgresGetForeignPlan * Create ForeignScan plan node which implements selected best path @@ -1434,6 +1516,144 @@ postgresGetForeignPlan(PlannerInfo *root, * should recheck all the remote quals. */ fdw_recheck_quals = remote_exprs; + + /* + * If a non-direct UPDATE/DELETE needs the remote tableoid (flagged by + * a pseudo-column Var in the rel's targetlist), build an explicit + * fdw_scan_tlist as for a join instead of scanning positionally. + */ + foreach(lc, foreignrel->reltarget->exprs) + { + Var *var = (Var *) lfirst(lc); + + if (IsA(var, Var) && + var->varattno == RemoteTableOidAttributeNumber) + { + List *epq_vars; + List *scan_vars = NIL; + ListCell *lc2; + + fdw_scan_tlist = build_tlist_to_deparse(foreignrel); + + /* + * fdw_scan_tlist may carry this relation's whole row twice + * with different types: typed with the relation's row type if + * it came from a local qual (e.g. "t.* IS NULL"), and typed + * RECORD once the qual's Var is retyped below to match + * core's own row-identity Var (see + * add_row_identity_columns()), which appears in the scan's + * output targetlist for UPDATE or with row triggers. + * setrefs.c matches Vars against the tlist by varno/varattno + * only and keeps the referencing Var's own vartype, so having + * both entries makes a reference resolve to a slot attribute + * of the other type ("attribute N of type record has wrong + * type"). The output targetlist is attached to the scan only + * after this function returns, so we cannot retype core's + * Var; instead retype every whole-row entry here to RECORD + * and rebuild the tlist so that duplicates fold into a single + * entry. The quals are retyped to match below. + */ + { + List *tl_exprs = NIL; + + foreach(lc2, fdw_scan_tlist) + tl_exprs = lappend(tl_exprs, + lfirst_node(TargetEntry, lc2)->expr); + tl_exprs = normalize_wholerow_vartype(tl_exprs, + foreignrel->relid, + false); + fdw_scan_tlist = add_to_flat_tlist(NIL, tl_exprs); + } + + /* + * If a local qual or recheck qual also references this + * relation's whole row directly, retype that reference to + * match, so it resolves consistently against the single + * fdw_scan_tlist entry (see normalize_wholerow_vartype()). + */ + local_exprs = normalize_wholerow_vartype(local_exprs, + foreignrel->relid, + true); + fdw_recheck_quals = normalize_wholerow_vartype(fdw_recheck_quals, + foreignrel->relid, + true); + + /* + * Vars in the EPQ recheck and local quals now resolve against + * the scan output, so add any not already covered. Skip any + * Var that isn't a column of this relation (e.g. a join + * partner's column referenced by a pushed-down join qual): + * that isn't ours to fetch, and it's already handled as a + * bound parameter wherever it appears inside a qual. + */ + epq_vars = pull_var_clause((Node *) list_concat_copy(fdw_recheck_quals, + local_exprs), + PVC_RECURSE_PLACEHOLDERS); + foreach(lc2, epq_vars) + { + Var *epq_var = lfirst_node(Var, lc2); + + if (bms_is_member(epq_var->varno, foreignrel->relids)) + scan_vars = lappend(scan_vars, epq_var); + } + fdw_scan_tlist = add_to_flat_tlist(fdw_scan_tlist, scan_vars); + + /* + * If this relation's whole row is referenced anywhere in + * the scan output, every one of its real columns must be + * fetched too, so make_tuple_from_result_row() can + * reconstruct it locally afterward instead of the remote + * side building and shipping a second, redundant ROW(...) + * (see deparseExplicitTargetList()). + */ + { + bool has_wholerow = false; + ListCell *lc3; + + foreach(lc3, fdw_scan_tlist) + { + TargetEntry *tle = lfirst_node(TargetEntry, lc3); + + if (IsA(tle->expr, Var) && + ((Var *) tle->expr)->varattno == InvalidAttrNumber && + ((Var *) tle->expr)->varno == foreignrel->relid) + { + has_wholerow = true; + break; + } + } + + if (has_wholerow) + { + Relation rel = table_open(foreigntableid, NoLock); + TupleDesc tupdesc = RelationGetDescr(rel); + List *relcols = NIL; + int attnum; + + for (attnum = 1; attnum <= tupdesc->natts; attnum++) + { + Form_pg_attribute attr = TupleDescAttr(tupdesc, attnum - 1); + Var *cvar; + + if (attr->attisdropped) + continue; + + cvar = makeVar(foreignrel->relid, attnum, + attr->atttypid, attr->atttypmod, + attr->attcollation, 0); + relcols = lappend(relcols, cvar); + } + + fdw_scan_tlist = add_to_flat_tlist(fdw_scan_tlist, relcols); + + table_close(rel, NoLock); + } + } + + set_remote_tableoid_resnames(fdw_scan_tlist); + break; + } + } } else { @@ -1469,6 +1689,7 @@ postgresGetForeignPlan(PlannerInfo *root, /* Build the list of columns to be fetched from the foreign server. */ fdw_scan_tlist = build_tlist_to_deparse(foreignrel); + set_remote_tableoid_resnames(fdw_scan_tlist); /* * Ensure that the outer plan produces a tuple whose descriptor @@ -1782,8 +2003,12 @@ postgresBeginForeignScan(ForeignScanState *node, int eflags) /* * Get info we'll need for converting data fetched from the foreign server * into local representation and error reporting during that process. + * + * Key the choice off fdw_scan_tlist, not scanrelid: an explicit + * fdw_scan_tlist (built to fetch the remote tableoid) fetches into the + * scan tuple slot like a join, even for a base-relation scan. */ - if (fsplan->scan.scanrelid > 0) + if (fsplan->scan.scanrelid > 0 && fsplan->fdw_scan_tlist == NIL) { fsstate->rel = node->ss.ss_currentRelation; fsstate->tupdesc = RelationGetDescr(fsstate->rel); @@ -1990,6 +2215,21 @@ postgresAddForeignUpdateTargets(PlannerInfo *root, /* Register it as a row-identity column needed by this target rel */ add_row_identity_var(root, var, rtindex, "ctid"); + + /* + * ctid alone isn't unique if the foreign table maps to a partitioned + * table remotely, so also fetch the remote tableoid, via an out-of-range + * attnum rather than TableOidAttributeNumber (see deparseColumnRef). + */ + var = makeVar(rtindex, + RemoteTableOidAttributeNumber, + OIDOID, + -1, + InvalidOid, + 0); + + /* Register it as a second row-identity column needed by this target rel */ + add_row_identity_var(root, var, rtindex, "remotetableoid"); } /* @@ -2854,6 +3094,31 @@ postgresPlanDirectModify(PlannerInfo *root, if (returningList) rebuild_fdw_scan_tlist(fscan, returningList); } + else + { + ListCell *lc; + + /* + * A direct modification identifies rows by pushed-down qualifiers, so + * discard any fdw_scan_tlist built to fetch the remote tableoid, + * falling back to the positional path. + */ + fscan->fdw_scan_tlist = NIL; + + /* + * The tlist's remote-tableoid Var likewise isn't needed; replace it + * with a NULL const rather than dropping the (positionally indexed) + * entry. + */ + foreach(lc, fscan->scan.plan.targetlist) + { + TargetEntry *tle = lfirst_node(TargetEntry, lc); + + if (IsA(tle->expr, Var) && + ((Var *) tle->expr)->varattno == RemoteTableOidAttributeNumber) + tle->expr = (Expr *) makeNullConst(OIDOID, -1, InvalidOid); + } + } /* * Finally, unset the async-capable flag if it is set, as we currently @@ -4334,8 +4599,12 @@ create_foreign_modify(EState *estate, if (fmstate->has_returning) fmstate->attinmeta = TupleDescGetAttInMetadata(tupdesc); - /* Prepare for output conversion of parameters used in prepared stmt. */ - n_params = list_length(fmstate->target_attrs) + 1; + /* + * Prepare for output conversion of parameters used in prepared stmt. + * UPDATE/DELETE transmit two extra leading parameters (ctid, tableoid) to + * identify the row; INSERT transmits none. + */ + n_params = list_length(fmstate->target_attrs) + 2; fmstate->p_flinfo = palloc0_array(FmgrInfo, n_params); fmstate->p_nums = 0; @@ -4353,6 +4622,19 @@ create_foreign_modify(EState *estate, getTypeOutputInfo(TIDOID, &typefnoid, &isvarlena); fmgr_info(typefnoid, &fmstate->p_flinfo[fmstate->p_nums]); fmstate->p_nums++; + + /* + * Find the remote tableoid resjunk column; it's the second + * transmittable parameter, disambiguating ctid across partitions. + */ + fmstate->tableoidAttno = + ExecFindJunkAttributeInTlist(subplan->targetlist, "remotetableoid"); + if (!AttributeNumberIsValid(fmstate->tableoidAttno)) + elog(ERROR, "could not find junk remotetableoid column"); + + getTypeOutputInfo(OIDOID, &typefnoid, &isvarlena); + fmgr_info(typefnoid, &fmstate->p_flinfo[fmstate->p_nums]); + fmstate->p_nums++; } if (operation == CMD_INSERT || operation == CMD_UPDATE) @@ -4405,6 +4687,7 @@ execute_foreign_modify(EState *estate, { PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState; ItemPointer ctid = NULL; + Oid tableoid = InvalidOid; const char **p_values; PGresult *res; int n_rows; @@ -4445,7 +4728,8 @@ execute_foreign_modify(EState *estate, prepare_foreign_modify(fmstate); /* - * For UPDATE/DELETE, get the ctid that was passed up as a resjunk column + * For UPDATE/DELETE, get the ctid and remote tableoid that were passed up + * as resjunk columns; together they identify the remote row to modify. */ if (operation == CMD_UPDATE || operation == CMD_DELETE) { @@ -4459,10 +4743,19 @@ execute_foreign_modify(EState *estate, if (isNull) elog(ERROR, "ctid is NULL"); ctid = (ItemPointer) DatumGetPointer(datum); + + datum = ExecGetJunkAttribute(planSlots[0], + fmstate->tableoidAttno, + &isNull); + /* shouldn't ever get a null result... */ + if (isNull) + elog(ERROR, "remote tableoid is NULL"); + tableoid = DatumGetObjectId(datum); } /* Convert parameters needed by prepared statement to text form */ - p_values = convert_prep_stmt_params(fmstate, ctid, slots, *numSlots); + p_values = convert_prep_stmt_params(fmstate, ctid, tableoid, + slots, *numSlots); /* * Execute the prepared statement. @@ -4560,6 +4853,7 @@ prepare_foreign_modify(PgFdwModifyState *fmstate) * Create array of text strings representing parameter values * * tupleid is ctid to send, or NULL if none + * tableoid is the remote tableoid to send; used only when tupleid != NULL * slot is slot to get remaining parameters from, or NULL if none * * Data is constructed in temp_cxt; caller should reset that after use. @@ -4567,6 +4861,7 @@ prepare_foreign_modify(PgFdwModifyState *fmstate) static const char ** convert_prep_stmt_params(PgFdwModifyState *fmstate, ItemPointer tupleid, + Oid tableoid, TupleTableSlot **slots, int numSlots) { @@ -4583,7 +4878,7 @@ convert_prep_stmt_params(PgFdwModifyState *fmstate, /* ctid is provided only for UPDATE/DELETE, which don't allow batching */ Assert(!(tupleid != NULL && numSlots > 1)); - /* 1st parameter should be ctid, if it's in use */ + /* 1st and 2nd parameters should be ctid and tableoid, if in use */ if (tupleid != NULL) { Assert(numSlots == 1); @@ -4591,6 +4886,10 @@ convert_prep_stmt_params(PgFdwModifyState *fmstate, p_values[pindex] = OutputFunctionCall(&fmstate->p_flinfo[pindex], PointerGetDatum(tupleid)); pindex++; + /* don't need set_transmission_modes for OID output */ + p_values[pindex] = OutputFunctionCall(&fmstate->p_flinfo[pindex], + ObjectIdGetDatum(tableoid)); + pindex++; } /* get following parameters from slots */ @@ -4604,7 +4903,8 @@ convert_prep_stmt_params(PgFdwModifyState *fmstate, for (i = 0; i < numSlots; i++) { - j = (tupleid != NULL) ? 1 : 0; + /* ctid and tableoid occupy the first two parameter slots */ + j = (tupleid != NULL) ? 2 : 0; foreach(lc, fmstate->target_attrs) { int attnum = lfirst_int(lc); @@ -4830,6 +5130,26 @@ rebuild_fdw_scan_tlist(ForeignScan *fscan, List *tlist) fscan->fdw_scan_tlist = new_tlist; } +/* + * set_remote_tableoid_resnames + * Name fdw_scan_tlist's remote-tableoid pseudo-column entries + * "remotetableoid", since their out-of-range attno has no catalog name. + */ +static void +set_remote_tableoid_resnames(List *fdw_scan_tlist) +{ + ListCell *lc; + + foreach(lc, fdw_scan_tlist) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + + if (IsA(tle->expr, Var) && + ((Var *) tle->expr)->varattno == RemoteTableOidAttributeNumber) + tle->resname = pstrdup("remotetableoid"); + } +} + /* * Execute a direct UPDATE/DELETE statement. */ @@ -9030,6 +9350,95 @@ make_tuple_from_result_row(PGresult *res, j++; } + /* + * Fill in any local-tableoid pseudo-entry and any whole-row reference + * that deparseExplicitTargetList() skipped shipping to the remote + * server (see there): the tableoid is simply this scan's own relation + * OID, and the whole row is built here from the columns already decoded + * above. + */ + if (fsstate) + { + ForeignScan *fsplan = castNode(ForeignScan, fsstate->ss.ps.plan); + + if (fsplan->scan.scanrelid > 0 && fsplan->fdw_scan_tlist != NIL) + { + Oid reloid = RelationGetRelid(fsstate->ss.ss_currentRelation); + TupleDesc reltupdesc = RelationGetDescr(fsstate->ss.ss_currentRelation); + Datum *relvalues = NULL; + bool *relnulls = NULL; + List *wholerow_positions = NIL; + ListCell *lc2; + int pos = 0; + + /* + * There can be more than one whole-row Var for this relation in + * the tlist (a query's own reference alongside core's row- + * identity one); collect every position to fill in below. + */ + foreach(lc2, fsplan->fdw_scan_tlist) + { + TargetEntry *tle = lfirst_node(TargetEntry, lc2); + Var *tvar; + + pos++; + + if (!IsA(tle->expr, Var)) + continue; + tvar = (Var *) tle->expr; + + if (tvar->varattno == TableOidAttributeNumber) + { + values[pos - 1] = ObjectIdGetDatum(reloid); + nulls[pos - 1] = false; + } + else if (tvar->varno == fsplan->scan.scanrelid && + tvar->varattno == InvalidAttrNumber) + { + /* Whole-row Var: fill in after collecting its columns. */ + wholerow_positions = lappend_int(wholerow_positions, pos); + } + else if (tvar->varno == fsplan->scan.scanrelid && + tvar->varattno > InvalidAttrNumber && + tvar->varattno <= reltupdesc->natts) + { + if (relvalues == NULL) + { + relvalues = palloc0_array(Datum, reltupdesc->natts); + relnulls = palloc_array(bool, reltupdesc->natts); + memset(relnulls, true, reltupdesc->natts * sizeof(bool)); + } + relvalues[tvar->varattno - 1] = values[pos - 1]; + relnulls[tvar->varattno - 1] = nulls[pos - 1]; + } + } + + /* + * Only reconstruct locally if there are other columns to build + * from (relvalues != NULL); otherwise the whole row was shipped + * as a literal ROW(...) instead (see + * tlist_has_other_column_of_rel()), and the value already + * fetched above is correct as is. + */ + if (wholerow_positions != NIL && relvalues != NULL) + { + HeapTuple rowtup; + Datum rowdatum; + + rowtup = heap_form_tuple(reltupdesc, relvalues, relnulls); + rowdatum = heap_copy_tuple_as_datum(rowtup, reltupdesc); + + foreach(lc2, wholerow_positions) + { + int wholerow_pos = lfirst_int(lc2); + + values[wholerow_pos - 1] = rowdatum; + nulls[wholerow_pos - 1] = false; + } + } + } + } + /* Uninstall error context callback. */ error_context_stack = errcallback.previous; @@ -9104,15 +9513,21 @@ conversion_error_callback(void *arg) int varno = 0; AttrNumber colno = 0; - if (fsplan->scan.scanrelid > 0) + if (fsplan->scan.scanrelid > 0 && fsplan->fdw_scan_tlist == NIL) { - /* error occurred in a scan against a foreign table */ + /* + * Error occurred in a scan against a foreign table, fetched + * positionally, so cur_attno is the table's attribute number. + */ varno = fsplan->scan.scanrelid; colno = errpos->cur_attno; } else { - /* error occurred in a scan against a foreign join */ + /* + * Error occurred in a scan against a foreign join, or a base + * relation with an explicit fdw_scan_tlist; cur_attno indexes it. + */ TargetEntry *tle; tle = list_nth_node(TargetEntry, fsplan->fdw_scan_tlist, @@ -9145,6 +9560,8 @@ conversion_error_callback(void *arg) attname = strVal(list_nth(rte->eref->colnames, colno - 1)); else if (colno == SelfItemPointerAttributeNumber) attname = "ctid"; + else if (colno == RemoteTableOidAttributeNumber) + attname = "tableoid"; } } else if (rel) diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h index da7da1c2ea9..eafcf42844f 100644 --- a/contrib/postgres_fdw/postgres_fdw.h +++ b/contrib/postgres_fdw/postgres_fdw.h @@ -13,6 +13,7 @@ #ifndef POSTGRES_FDW_H #define POSTGRES_FDW_H +#include "access/htup_details.h" #include "foreign/foreign.h" #include "lib/stringinfo.h" #include "libpq/libpq-be-fe.h" @@ -20,6 +21,14 @@ #include "nodes/pathnodes.h" #include "utils/relcache.h" +/* + * Pseudo-attribute number for the remote table OID, used to identify a row for + * UPDATE/DELETE when a foreign table maps to a remote partitioned table (see + * postgresAddForeignUpdateTargets). Larger than any real attno, so it never + * collides with a genuine column; fetched as an ordinary fdw_scan_tlist column. + */ +#define RemoteTableOidAttributeNumber (MaxHeapAttributeNumber + 1) + /* * FDW-specific planner information kept in RelOptInfo.fdw_private for a * postgres_fdw foreign table. For a baserel, this struct is created by diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql index 77c9a25a890..67dac94dfae 100644 --- a/contrib/postgres_fdw/sql/postgres_fdw.sql +++ b/contrib/postgres_fdw/sql/postgres_fdw.sql @@ -5115,3 +5115,75 @@ RESET client_min_messages; DROP FUNCTION wait_for_backend_termination(int); DROP FOREIGN TABLE remote_backend_pid; DROP VIEW my_backend_pid; + +-- =================================================================== +-- test UPDATE/DELETE on a foreign table whose remote counterpart is +-- itself a partitioned table +-- =================================================================== + +-- The two rows below land in separate partitions and, being the first +-- row in each partition's heap, collide on ctid (0,1). A row-identity +-- scheme that relies on ctid alone can't tell them apart. +CREATE TABLE parted_remote (a int, b int) PARTITION BY LIST (a); +CREATE TABLE parted_remote_p1 PARTITION OF parted_remote FOR VALUES IN (1); +CREATE TABLE parted_remote_p2 PARTITION OF parted_remote FOR VALUES IN (2); +INSERT INTO parted_remote_p1 VALUES (1, 100); +INSERT INTO parted_remote_p2 VALUES (2, 200); + +CREATE FOREIGN TABLE foreign_parted_remote (a int, b int) + SERVER loopback OPTIONS (table_name 'parted_remote'); + +-- Force a non-direct-modify UPDATE (random() isn't shippable), so the +-- ctid/tableoid row-identity path below is actually exercised. +UPDATE foreign_parted_remote SET b = 999 WHERE a = 1 AND random() <= 1; + +-- Only the a = 1 row (in parted_remote_p1) should have changed. +SELECT tableoid::regclass, a, b FROM parted_remote ORDER BY a; + +DELETE FROM foreign_parted_remote WHERE a = 2 AND random() <= 1; + +-- Only the a = 2 row (in parted_remote_p2) should be gone. +SELECT tableoid::regclass, a, b FROM parted_remote ORDER BY a; + +DROP FOREIGN TABLE foreign_parted_remote; +DROP TABLE parted_remote; + +-- A whole-row reference to the target relation in a local qual must resolve +-- consistently with the row-identity columns fetched for UPDATE/DELETE. +CREATE TABLE wr_remote (a int PRIMARY KEY, b text, c int); +INSERT INTO wr_remote SELECT i, 'row' || i, i * 10 FROM generate_series(1, 5) i; +CREATE FOREIGN TABLE wr_ft (a int, b text, c int) + SERVER loopback OPTIONS (table_name 'wr_remote'); + +BEGIN; +EXPLAIN (VERBOSE, COSTS OFF) +DELETE FROM wr_ft WHERE wr_ft.* IS NULL AND random() <= 1; +DELETE FROM wr_ft WHERE wr_ft.* IS NULL AND random() <= 1; +UPDATE wr_ft SET b = 'x' WHERE wr_ft.* IS NULL AND random() <= 1; +DELETE FROM wr_ft WHERE wr_ft.* IS NOT NULL AND a = 2 AND random() <= 1 RETURNING a, b; +SELECT count(*) FROM wr_remote; +ROLLBACK; + +DROP FOREIGN TABLE wr_ft; +DROP TABLE wr_remote; + +-- Same, when the foreign table is a partition of a local partitioned table: +-- the parent's whole-row reference is then converted from the partition's +-- row type, which must remain visible above the retyped whole-row Var. +CREATE TABLE wr_remote2 (a int PRIMARY KEY, b text, c int); +INSERT INTO wr_remote2 SELECT i, 'row' || i, i * 10 FROM generate_series(1, 5) i; +CREATE TABLE wr_parent (a int, b text, c int) PARTITION BY RANGE (a); +CREATE FOREIGN TABLE wr_part1 PARTITION OF wr_parent + FOR VALUES FROM (1) TO (10) SERVER loopback OPTIONS (table_name 'wr_remote2'); +CREATE TABLE wr_part2 PARTITION OF wr_parent FOR VALUES FROM (10) TO (20); +INSERT INTO wr_part2 VALUES (11, 'local11', 110); + +BEGIN; +DELETE FROM wr_parent WHERE wr_parent.* IS NULL AND random() <= 1; +UPDATE wr_parent SET b = 'x' WHERE wr_parent.* IS NULL AND random() <= 1; +UPDATE wr_parent SET b = 'x' WHERE wr_parent.* IS NOT NULL AND a < 5 AND random() <= 1 RETURNING a, b; +DELETE FROM wr_parent WHERE wr_parent.* IS NOT NULL AND a IN (2, 11) AND random() <= 1 RETURNING a, b; +ROLLBACK; + +DROP TABLE wr_parent; +DROP TABLE wr_remote2; diff --git a/doc/src/sgml/postgres-fdw.sgml b/doc/src/sgml/postgres-fdw.sgml index 01577d8d69b..0f818e46c89 100644 --- a/doc/src/sgml/postgres-fdw.sgml +++ b/doc/src/sgml/postgres-fdw.sgml @@ -1195,6 +1195,40 @@ CREATE SUBSCRIPTION my_subscription SERVER subscription_server PUBLICATION testp to reduce the risk of misexecution of the query. + + When such optimized whole-query shipping is not possible, an + UPDATE or DELETE is instead executed + by first selecting the affected rows from the remote server and then + issuing a separate remote UPDATE or + DELETE for each row, identified by its + ctid and remote tableoid. If the + remote table is the root of a partitioning hierarchy, each such per-row + command targets that root and so cannot benefit from partition pruning + (the remote planner has no way to prune based on the transmitted + tableoid parameter), which can make this considerably + slower than the whole-query case when many rows are affected. For best + performance, prefer whole-query + shipping when possible, and otherwise try to limit the number of rows + affected by an UPDATE or DELETE on + a foreign table that maps to a partitioned table on the remote server. + + + + The ctid/tableoid row identity + described above only disambiguates rows when the remote table named by + table_name is itself an ordinary table, partitioned + table, or inheritance parent on the remote server. If that remote table + is itself a foreign table (for example, another + postgres_fdw table forwarding to a partitioned + table one hop further away), the remote tableoid + returned for every row is simply that foreign table's own OID, which does + not distinguish the rows it forwards. In that situation, a non-direct + UPDATE or DELETE that affects more + than one row can still update or delete the wrong rows, exactly as + without this disambiguation; chaining foreign tables in this way is not + currently detected or protected against. + + When postgres_fdw encounters a join between foreign tables on the same foreign server, it sends the entire join to the foreign server, -- 2.52.0