Skip to content

Commit e3101a0

Browse files
committed
Ensure that foreign scans with lateral refs are planned correctly.
As reported in bug #15613 from Srinivasan S A, file_fdw and postgres_fdw neglected to mark plain baserel foreign paths as parameterized when the relation has lateral_relids. Other FDWs have surely copied this mistake, so rather than just patching those two modules, install a band-aid fix in create_foreignscan_path to rectify the mistake centrally. Although the band-aid is enough to fix the visible symptom, correct the calls in file_fdw and postgres_fdw anyway, so that they are valid examples for external FDWs. Also, since the band-aid isn't enough to make this work for parameterized foreign joins, throw an elog(ERROR) if such a case is passed to create_foreignscan_path. This shouldn't pose much of a problem for existing external FDWs, since it's likely they aren't trying to make such paths anyway (though some of them may need a defense against joins with lateral_relids, similar to the one this patch installs into postgres_fdw). Add some assertions in relnode.c to catch future occurrences of the same error --- in particular, as backstop against core-code mistakes like the one fixed by commit bdd9a99. Discussion: https://postgr.es/m/15613-092be1be9576c728@postgresql.org
1 parent bbcafab commit e3101a0

File tree

6 files changed

+131
-5
lines changed

6 files changed

+131
-5
lines changed

contrib/file_fdw/file_fdw.c

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,10 @@ fileGetForeignPaths(PlannerInfo *root,
541541
* Create a ForeignPath node and add it as only possible path. We use the
542542
* fdw_private list of the path to carry the convert_selectively option;
543543
* it will be propagated into the fdw_private list of the Plan node.
544+
*
545+
* We don't support pushing join clauses into the quals of this path, but
546+
* it could still have required parameterization due to LATERAL refs in
547+
* its tlist.
544548
*/
545549
add_path(baserel, (Path *)
546550
create_foreignscan_path(root, baserel,
@@ -549,7 +553,7 @@ fileGetForeignPaths(PlannerInfo *root,
549553
startup_cost,
550554
total_cost,
551555
NIL, /* no pathkeys */
552-
NULL, /* no outer rel either */
556+
baserel->lateral_relids,
553557
NULL, /* no extra plan */
554558
coptions));
555559

contrib/postgres_fdw/expected/postgres_fdw.out

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3420,6 +3420,62 @@ select c2, sum from "S 1"."T 1" t1, lateral (select sum(t2.c1 + t1."C 1") sum fr
34203420
(2 rows)
34213421

34223422
reset enable_hashagg;
3423+
-- bug #15613: bad plan for foreign table scan with lateral reference
3424+
EXPLAIN (VERBOSE, COSTS OFF)
3425+
SELECT ref_0.c2, subq_1.*
3426+
FROM
3427+
"S 1"."T 1" AS ref_0,
3428+
LATERAL (
3429+
SELECT ref_0."C 1" c1, subq_0.*
3430+
FROM (SELECT ref_0.c2, ref_1.c3
3431+
FROM ft1 AS ref_1) AS subq_0
3432+
RIGHT JOIN ft2 AS ref_3 ON (subq_0.c3 = ref_3.c3)
3433+
) AS subq_1
3434+
WHERE ref_0."C 1" < 10 AND subq_1.c3 = '00001'
3435+
ORDER BY ref_0."C 1";
3436+
QUERY PLAN
3437+
---------------------------------------------------------------------------------------------------------
3438+
Nested Loop
3439+
Output: ref_0.c2, ref_0."C 1", (ref_0.c2), ref_1.c3, ref_0."C 1"
3440+
-> Nested Loop
3441+
Output: ref_0.c2, ref_0."C 1", ref_1.c3, (ref_0.c2)
3442+
-> Index Scan using t1_pkey on "S 1"."T 1" ref_0
3443+
Output: ref_0."C 1", ref_0.c2, ref_0.c3, ref_0.c4, ref_0.c5, ref_0.c6, ref_0.c7, ref_0.c8
3444+
Index Cond: (ref_0."C 1" < 10)
3445+
-> Foreign Scan on public.ft1 ref_1
3446+
Output: ref_1.c3, ref_0.c2
3447+
Remote SQL: SELECT c3 FROM "S 1"."T 1" WHERE ((c3 = '00001'::text))
3448+
-> Materialize
3449+
Output: ref_3.c3
3450+
-> Foreign Scan on public.ft2 ref_3
3451+
Output: ref_3.c3
3452+
Remote SQL: SELECT c3 FROM "S 1"."T 1" WHERE ((c3 = '00001'::text))
3453+
(15 rows)
3454+
3455+
SELECT ref_0.c2, subq_1.*
3456+
FROM
3457+
"S 1"."T 1" AS ref_0,
3458+
LATERAL (
3459+
SELECT ref_0."C 1" c1, subq_0.*
3460+
FROM (SELECT ref_0.c2, ref_1.c3
3461+
FROM ft1 AS ref_1) AS subq_0
3462+
RIGHT JOIN ft2 AS ref_3 ON (subq_0.c3 = ref_3.c3)
3463+
) AS subq_1
3464+
WHERE ref_0."C 1" < 10 AND subq_1.c3 = '00001'
3465+
ORDER BY ref_0."C 1";
3466+
c2 | c1 | c2 | c3
3467+
----+----+----+-------
3468+
1 | 1 | 1 | 00001
3469+
2 | 2 | 2 | 00001
3470+
3 | 3 | 3 | 00001
3471+
4 | 4 | 4 | 00001
3472+
5 | 5 | 5 | 00001
3473+
6 | 6 | 6 | 00001
3474+
7 | 7 | 7 | 00001
3475+
8 | 8 | 8 | 00001
3476+
9 | 9 | 9 | 00001
3477+
(9 rows)
3478+
34233479
-- Check with placeHolderVars
34243480
explain (verbose, costs off)
34253481
select sum(q.a), count(q.b) from ft4 left join (select 13, avg(ft1.c1), sum(ft2.c1) from ft1 right join ft2 on (ft1.c1 = ft2.c1)) q(a, b, c) on (ft4.c1 <= q.b);

contrib/postgres_fdw/postgres_fdw.c

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -897,14 +897,17 @@ postgresGetForeignPaths(PlannerInfo *root,
897897
* baserestrict conditions we were able to send to remote, there might
898898
* actually be an indexscan happening there). We already did all the work
899899
* to estimate cost and size of this path.
900+
*
901+
* Although this path uses no join clauses, it could still have required
902+
* parameterization due to LATERAL refs in its tlist.
900903
*/
901904
path = create_foreignscan_path(root, baserel,
902905
NULL, /* default pathtarget */
903906
fpinfo->rows,
904907
fpinfo->startup_cost,
905908
fpinfo->total_cost,
906909
NIL, /* no pathkeys */
907-
NULL, /* no outer rel either */
910+
baserel->lateral_relids,
908911
NULL, /* no extra plan */
909912
NIL); /* no fdw_private list */
910913
add_path(baserel, (Path *) path);
@@ -4374,7 +4377,7 @@ add_paths_with_pathkeys_for_rel(PlannerInfo *root, RelOptInfo *rel,
43744377
startup_cost,
43754378
total_cost,
43764379
useful_pathkeys,
4377-
NULL,
4380+
rel->lateral_relids,
43784381
sorted_epq_path,
43794382
NIL));
43804383
}
@@ -4511,6 +4514,13 @@ postgresGetForeignJoinPaths(PlannerInfo *root,
45114514
if (joinrel->fdw_private)
45124515
return;
45134516

4517+
/*
4518+
* This code does not work for joins with lateral references, since those
4519+
* must have parameterized paths, which we don't generate yet.
4520+
*/
4521+
if (!bms_is_empty(joinrel->lateral_relids))
4522+
return;
4523+
45144524
/*
45154525
* Create unfinished PgFdwRelationInfo entry which is used to indicate
45164526
* that the join relation is already considered, so that we won't waste
@@ -4602,7 +4612,7 @@ postgresGetForeignJoinPaths(PlannerInfo *root,
46024612
startup_cost,
46034613
total_cost,
46044614
NIL, /* no pathkeys */
4605-
NULL, /* no required_outer */
4615+
joinrel->lateral_relids,
46064616
epq_path,
46074617
NIL); /* no fdw_private */
46084618

@@ -4921,7 +4931,7 @@ add_foreign_grouping_paths(PlannerInfo *root, RelOptInfo *input_rel,
49214931
startup_cost,
49224932
total_cost,
49234933
NIL, /* no pathkeys */
4924-
NULL, /* no required_outer */
4934+
grouped_rel->lateral_relids,
49254935
NULL,
49264936
NIL); /* no fdw_private */
49274937

contrib/postgres_fdw/sql/postgres_fdw.sql

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -878,6 +878,32 @@ select c2, sum from "S 1"."T 1" t1, lateral (select sum(t2.c1 + t1."C 1") sum fr
878878
select c2, sum from "S 1"."T 1" t1, lateral (select sum(t2.c1 + t1."C 1") sum from ft2 t2 group by t2.c1) qry where t1.c2 * 2 = qry.sum and t1.c2 < 3 and t1."C 1" < 100 order by 1;
879879
reset enable_hashagg;
880880

881+
-- bug #15613: bad plan for foreign table scan with lateral reference
882+
EXPLAIN (VERBOSE, COSTS OFF)
883+
SELECT ref_0.c2, subq_1.*
884+
FROM
885+
"S 1"."T 1" AS ref_0,
886+
LATERAL (
887+
SELECT ref_0."C 1" c1, subq_0.*
888+
FROM (SELECT ref_0.c2, ref_1.c3
889+
FROM ft1 AS ref_1) AS subq_0
890+
RIGHT JOIN ft2 AS ref_3 ON (subq_0.c3 = ref_3.c3)
891+
) AS subq_1
892+
WHERE ref_0."C 1" < 10 AND subq_1.c3 = '00001'
893+
ORDER BY ref_0."C 1";
894+
895+
SELECT ref_0.c2, subq_1.*
896+
FROM
897+
"S 1"."T 1" AS ref_0,
898+
LATERAL (
899+
SELECT ref_0."C 1" c1, subq_0.*
900+
FROM (SELECT ref_0.c2, ref_1.c3
901+
FROM ft1 AS ref_1) AS subq_0
902+
RIGHT JOIN ft2 AS ref_3 ON (subq_0.c3 = ref_3.c3)
903+
) AS subq_1
904+
WHERE ref_0."C 1" < 10 AND subq_1.c3 = '00001'
905+
ORDER BY ref_0."C 1";
906+
881907
-- Check with placeHolderVars
882908
explain (verbose, costs off)
883909
select sum(q.a), count(q.b) from ft4 left join (select 13, avg(ft1.c1), sum(ft2.c1) from ft1 right join ft2 on (ft1.c1 = ft2.c1)) q(a, b, c) on (ft4.c1 <= q.b);

src/backend/optimizer/util/pathnode.c

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1967,6 +1967,27 @@ create_foreignscan_path(PlannerInfo *root, RelOptInfo *rel,
19671967
{
19681968
ForeignPath *pathnode = makeNode(ForeignPath);
19691969

1970+
/*
1971+
* Since the path's required_outer should always include all the rel's
1972+
* lateral_relids, forcibly add those if necessary. This is a bit of a
1973+
* hack, but up till early 2019 the contrib FDWs failed to ensure that,
1974+
* and it's likely that the same error has propagated into many external
1975+
* FDWs. Don't risk modifying the passed-in relid set here.
1976+
*/
1977+
if (rel->lateral_relids && !bms_is_subset(rel->lateral_relids,
1978+
required_outer))
1979+
required_outer = bms_union(required_outer, rel->lateral_relids);
1980+
1981+
/*
1982+
* Although this function is only designed to be used for scans of
1983+
* baserels, before v12 postgres_fdw abused it to make paths for join and
1984+
* upper rels. It will work for such cases as long as required_outer is
1985+
* empty (otherwise get_baserel_parampathinfo does the wrong thing), which
1986+
* fortunately is the expected case for now.
1987+
*/
1988+
if (!bms_is_empty(required_outer) && !IS_SIMPLE_REL(rel))
1989+
elog(ERROR, "parameterized foreign joins are not supported yet");
1990+
19701991
pathnode->path.pathtype = T_ForeignScan;
19711992
pathnode->path.parent = rel;
19721993
pathnode->path.pathtarget = target ? target : rel->reltarget;

src/backend/optimizer/util/relnode.c

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1041,6 +1041,9 @@ get_baserel_parampathinfo(PlannerInfo *root, RelOptInfo *baserel,
10411041
double rows;
10421042
ListCell *lc;
10431043

1044+
/* If rel has LATERAL refs, every path for it should account for them */
1045+
Assert(bms_is_subset(baserel->lateral_relids, required_outer));
1046+
10441047
/* Unparameterized paths have no ParamPathInfo */
10451048
if (bms_is_empty(required_outer))
10461049
return NULL;
@@ -1140,6 +1143,9 @@ get_joinrel_parampathinfo(PlannerInfo *root, RelOptInfo *joinrel,
11401143
double rows;
11411144
ListCell *lc;
11421145

1146+
/* If rel has LATERAL refs, every path for it should account for them */
1147+
Assert(bms_is_subset(joinrel->lateral_relids, required_outer));
1148+
11431149
/* Unparameterized paths have no ParamPathInfo or extra join clauses */
11441150
if (bms_is_empty(required_outer))
11451151
return NULL;
@@ -1336,6 +1342,9 @@ get_appendrel_parampathinfo(RelOptInfo *appendrel, Relids required_outer)
13361342
ParamPathInfo *ppi;
13371343
ListCell *lc;
13381344

1345+
/* If rel has LATERAL refs, every path for it should account for them */
1346+
Assert(bms_is_subset(appendrel->lateral_relids, required_outer));
1347+
13391348
/* Unparameterized paths have no ParamPathInfo */
13401349
if (bms_is_empty(required_outer))
13411350
return NULL;

0 commit comments

Comments
 (0)