Skip to content

Commit 876fd37

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 b33d5e0 commit 876fd37

File tree

6 files changed

+123
-2
lines changed

6 files changed

+123
-2
lines changed

contrib/file_fdw/file_fdw.c

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -516,14 +516,18 @@ fileGetForeignPaths(PlannerInfo *root,
516516
* Create a ForeignPath node and add it as only possible path. We use the
517517
* fdw_private list of the path to carry the convert_selectively option;
518518
* it will be propagated into the fdw_private list of the Plan node.
519+
*
520+
* We don't support pushing join clauses into the quals of this path, but
521+
* it could still have required parameterization due to LATERAL refs in
522+
* its tlist.
519523
*/
520524
add_path(baserel, (Path *)
521525
create_foreignscan_path(root, baserel,
522526
baserel->rows,
523527
startup_cost,
524528
total_cost,
525529
NIL, /* no pathkeys */
526-
NULL, /* no outer rel either */
530+
baserel->lateral_relids,
527531
coptions));
528532

529533
/*

contrib/postgres_fdw/expected/postgres_fdw.out

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -682,6 +682,62 @@ SELECT * FROM ft2 WHERE c1 = ANY (ARRAY(SELECT c1 FROM ft1 WHERE c1 < 5));
682682
4 | 4 | 00004 | Mon Jan 05 00:00:00 1970 PST | Mon Jan 05 00:00:00 1970 | 4 | 4 | foo
683683
(4 rows)
684684

685+
-- bug #15613: bad plan for foreign table scan with lateral reference
686+
EXPLAIN (VERBOSE, COSTS OFF)
687+
SELECT ref_0.c2, subq_1.*
688+
FROM
689+
"S 1"."T 1" AS ref_0,
690+
LATERAL (
691+
SELECT ref_0."C 1" c1, subq_0.*
692+
FROM (SELECT ref_0.c2, ref_1.c3
693+
FROM ft1 AS ref_1) AS subq_0
694+
RIGHT JOIN ft2 AS ref_3 ON (subq_0.c3 = ref_3.c3)
695+
) AS subq_1
696+
WHERE ref_0."C 1" < 10 AND subq_1.c3 = '00001'
697+
ORDER BY ref_0."C 1";
698+
QUERY PLAN
699+
---------------------------------------------------------------------------------------------------------
700+
Nested Loop
701+
Output: ref_0.c2, ref_0."C 1", (ref_0.c2), ref_1.c3, ref_0."C 1"
702+
-> Nested Loop
703+
Output: ref_0.c2, ref_0."C 1", ref_1.c3, (ref_0.c2)
704+
-> Index Scan using t1_pkey on "S 1"."T 1" ref_0
705+
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
706+
Index Cond: (ref_0."C 1" < 10)
707+
-> Foreign Scan on public.ft1 ref_1
708+
Output: ref_1.c3, ref_0.c2
709+
Remote SQL: SELECT c3 FROM "S 1"."T 1" WHERE ((c3 = '00001'::text))
710+
-> Materialize
711+
Output: ref_3.c3
712+
-> Foreign Scan on public.ft2 ref_3
713+
Output: ref_3.c3
714+
Remote SQL: SELECT c3 FROM "S 1"."T 1" WHERE ((c3 = '00001'::text))
715+
(15 rows)
716+
717+
SELECT ref_0.c2, subq_1.*
718+
FROM
719+
"S 1"."T 1" AS ref_0,
720+
LATERAL (
721+
SELECT ref_0."C 1" c1, subq_0.*
722+
FROM (SELECT ref_0.c2, ref_1.c3
723+
FROM ft1 AS ref_1) AS subq_0
724+
RIGHT JOIN ft2 AS ref_3 ON (subq_0.c3 = ref_3.c3)
725+
) AS subq_1
726+
WHERE ref_0."C 1" < 10 AND subq_1.c3 = '00001'
727+
ORDER BY ref_0."C 1";
728+
c2 | c1 | c2 | c3
729+
----+----+----+-------
730+
1 | 1 | 1 | 00001
731+
2 | 2 | 2 | 00001
732+
3 | 3 | 3 | 00001
733+
4 | 4 | 4 | 00001
734+
5 | 5 | 5 | 00001
735+
6 | 6 | 6 | 00001
736+
7 | 7 | 7 | 00001
737+
8 | 8 | 8 | 00001
738+
9 | 9 | 9 | 00001
739+
(9 rows)
740+
685741
-- ===================================================================
686742
-- parameterized queries
687743
-- ===================================================================

contrib/postgres_fdw/postgres_fdw.c

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -547,13 +547,16 @@ postgresGetForeignPaths(PlannerInfo *root,
547547
* baserestrict conditions we were able to send to remote, there might
548548
* actually be an indexscan happening there). We already did all the work
549549
* to estimate cost and size of this path.
550+
*
551+
* Although this path uses no join clauses, it could still have required
552+
* parameterization due to LATERAL refs in its tlist.
550553
*/
551554
path = create_foreignscan_path(root, baserel,
552555
fpinfo->rows,
553556
fpinfo->startup_cost,
554557
fpinfo->total_cost,
555558
NIL, /* no pathkeys */
556-
NULL, /* no outer rel either */
559+
baserel->lateral_relids,
557560
NIL); /* no fdw_private list */
558561
add_path(baserel, (Path *) path);
559562

contrib/postgres_fdw/sql/postgres_fdw.sql

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,32 @@ WHERE a.c2 = 6 AND b.c1 = a.c1 AND a.c8 = 'foo' AND b.c7 = upper(a.c7);
232232
SELECT * FROM ft1 WHERE c1 = ANY (ARRAY(SELECT c1 FROM ft2 WHERE c1 < 5));
233233
SELECT * FROM ft2 WHERE c1 = ANY (ARRAY(SELECT c1 FROM ft1 WHERE c1 < 5));
234234

235+
-- bug #15613: bad plan for foreign table scan with lateral reference
236+
EXPLAIN (VERBOSE, COSTS OFF)
237+
SELECT ref_0.c2, subq_1.*
238+
FROM
239+
"S 1"."T 1" AS ref_0,
240+
LATERAL (
241+
SELECT ref_0."C 1" c1, subq_0.*
242+
FROM (SELECT ref_0.c2, ref_1.c3
243+
FROM ft1 AS ref_1) AS subq_0
244+
RIGHT JOIN ft2 AS ref_3 ON (subq_0.c3 = ref_3.c3)
245+
) AS subq_1
246+
WHERE ref_0."C 1" < 10 AND subq_1.c3 = '00001'
247+
ORDER BY ref_0."C 1";
248+
249+
SELECT ref_0.c2, subq_1.*
250+
FROM
251+
"S 1"."T 1" AS ref_0,
252+
LATERAL (
253+
SELECT ref_0."C 1" c1, subq_0.*
254+
FROM (SELECT ref_0.c2, ref_1.c3
255+
FROM ft1 AS ref_1) AS subq_0
256+
RIGHT JOIN ft2 AS ref_3 ON (subq_0.c3 = ref_3.c3)
257+
) AS subq_1
258+
WHERE ref_0."C 1" < 10 AND subq_1.c3 = '00001'
259+
ORDER BY ref_0."C 1";
260+
235261
-- ===================================================================
236262
-- parameterized queries
237263
-- ===================================================================

src/backend/optimizer/util/pathnode.c

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1736,6 +1736,29 @@ create_foreignscan_path(PlannerInfo *root, RelOptInfo *rel,
17361736
{
17371737
ForeignPath *pathnode = makeNode(ForeignPath);
17381738

1739+
/*
1740+
* Since the path's required_outer should always include all the rel's
1741+
* lateral_relids, forcibly add those if necessary. This is a bit of a
1742+
* hack, but up till early 2019 the contrib FDWs failed to ensure that,
1743+
* and it's likely that the same error has propagated into many external
1744+
* FDWs. Don't risk modifying the passed-in relid set here.
1745+
*/
1746+
if (rel->lateral_relids && !bms_is_subset(rel->lateral_relids,
1747+
required_outer))
1748+
required_outer = bms_union(required_outer, rel->lateral_relids);
1749+
1750+
/*
1751+
* Although this function is only designed to be used for scans of
1752+
* baserels, before v12 postgres_fdw abused it to make paths for join and
1753+
* upper rels. It will work for such cases as long as required_outer is
1754+
* empty (otherwise get_baserel_parampathinfo does the wrong thing), which
1755+
* fortunately is the expected case for now.
1756+
*/
1757+
if (!bms_is_empty(required_outer) &&
1758+
!(rel->reloptkind == RELOPT_BASEREL ||
1759+
rel->reloptkind == RELOPT_OTHER_MEMBER_REL))
1760+
elog(ERROR, "parameterized foreign joins are not supported yet");
1761+
17391762
pathnode->path.pathtype = T_ForeignScan;
17401763
pathnode->path.parent = rel;
17411764
pathnode->path.param_info = get_baserel_parampathinfo(root, rel,

src/backend/optimizer/util/relnode.c

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -857,6 +857,9 @@ get_baserel_parampathinfo(PlannerInfo *root, RelOptInfo *baserel,
857857
double rows;
858858
ListCell *lc;
859859

860+
/* If rel has LATERAL refs, every path for it should account for them */
861+
Assert(bms_is_subset(baserel->lateral_relids, required_outer));
862+
860863
/* Unparameterized paths have no ParamPathInfo */
861864
if (bms_is_empty(required_outer))
862865
return NULL;
@@ -956,6 +959,9 @@ get_joinrel_parampathinfo(PlannerInfo *root, RelOptInfo *joinrel,
956959
double rows;
957960
ListCell *lc;
958961

962+
/* If rel has LATERAL refs, every path for it should account for them */
963+
Assert(bms_is_subset(joinrel->lateral_relids, required_outer));
964+
959965
/* Unparameterized paths have no ParamPathInfo or extra join clauses */
960966
if (bms_is_empty(required_outer))
961967
return NULL;
@@ -1152,6 +1158,9 @@ get_appendrel_parampathinfo(RelOptInfo *appendrel, Relids required_outer)
11521158
ParamPathInfo *ppi;
11531159
ListCell *lc;
11541160

1161+
/* If rel has LATERAL refs, every path for it should account for them */
1162+
Assert(bms_is_subset(appendrel->lateral_relids, required_outer));
1163+
11551164
/* Unparameterized paths have no ParamPathInfo */
11561165
if (bms_is_empty(required_outer))
11571166
return NULL;

0 commit comments

Comments
 (0)