[Bug] ForeignScan with join params
- Dominant language
- C
- Stars
- 1.4k
- Forks
- 247
- Avg merge
- 4d 3h
- Merged PRs (30d)
- 39
Description
### Apache Cloudberry version
any version
### What happened
There are two foreign tables named `ft1` and `ft2`:
```
contrib_regression=# \d ft1
Foreign table "public.ft1"
Column | Type | Collation | Nullable | Default | FDW options
--------+-----------------------------+-----------+----------+---------------+---------------------
c1 | integer | | not null | | (column_name 'C 1')
c2 | integer | | not null | |
c3 | text | | | |
c4 | timestamp with time zone | | | |
c5 | timestamp without time zone | | | |
c6 | character varying(10) | | | |
c7 | character(10) | | | 'ft1'::bpchar |
c8 | user_enum | | | |
Server: pgserver
FDW options: (schema_name 'S 1', table_name 'T 1', mpp_execute 'all segments')
contrib_regression=# \d ft2
Foreign table "public.ft2"
Column | Type | Collation | Nullable | Default | FDW options
--------+-----------------------------+-----------+----------+---------------+---------------------
c1 | integer | | not null | | (column_name 'C 1')
c2 | integer | | not null | |
c3 | text | | | |
c4 | timestamp with time zone | | | |
c5 | timestamp without time zone | | | |
c6 | character varying(10) | | | |
c7 | character(10) | | | 'ft2'::bpchar |
c8 | user_enum | | | |
Server: pgserver
FDW options: (schema_name 'S 1', table_name 'T 1', mpp_execute 'all segments', use_remote_estimate 'true')
```
And a local table named `s1t1`:
```
contrib_regression=# \d s1t1
Table "public.s1t1"
Column | Type | Collation | Nullable | Default
--------+-----------------------------+-----------+----------+---------
C 1 | integer | | not null |
c2 | integer | | not null |
c3 | text | | |
c4 | timestamp with time zone | | |
c5 | timestamp without time zone | | |
c6 | character varying(10) | | |
c7 | character(10) | | |
c8 | character varying(255) | | |
Indexes:
"s1t1_pkey" PRIMARY KEY, btree ("C 1")
Distributed by: ("C 1")
```
Then the query plan is:
```
contrib_regression=# explain (verbose, costs off) SELECT ref_0.c2, subq_1.*
FROM
s1t1 AS ref_0,
LATERAL (
SELECT ref_0."C 1" c1, subq_0.*
FROM (SELECT ref_0.c2, ref_1.c3
FROM ft1 AS ref_1) AS subq_0
RIGHT JOIN ft2 AS ref_3 ON (subq_0.c3 = ref_3.c3)
) AS subq_1
WHERE ref_0."C 1" < 10 AND subq_1.c3 = '00001';
QUERY PLAN
-----------------------------------------------------------------------------------------------------
Gather Motion 3:1 (slice1; segments: 3)
Output: ref_0.c2, ref_0."C 1", (ref_0.c2), ref_1.c3
-> Nested Loop
Output: ref_0.c2, ref_0."C 1", (ref_0.c2), ref_1.c3
-> Broadcast Motion 3:3 (slice2; segments: 3)
Output: ref_3.c3
-> Foreign Scan on public.ft2 ref_3
Output: ref_3.c3
Remote SQL: SELECT c3 FROM "S 1"."T 1" WHERE ((c3 = '00001'::text))
-> Materialize
Output: ref_0.c2, ref_0."C 1", ref_1.c3, (ref_0.c2)
-> Nested Loop
Output: ref_0.c2, ref_0."C 1", ref_1.c3, (ref_0.c2)
-> Broadcast Motion 3:3 (slice3; segments: 3)
Output: ref_0.c2, ref_0."C 1"
-> Seq Scan on public.s1t1 ref_0
Output: ref_0.c2, ref_0."C 1"
Filter: (ref_0."C 1" < 10)
-> Materialize
Output: ref_1.c3, (ref_0.c2)
-> Foreign Scan on public.ft1 ref_1
Output: ref_1.c3, ref_0.c2
Remote SQL: SELECT c3 FROM "S 1"."T 1" WHERE ((c3 = '00001'::text))
Optimizer: Postgres query optimizer
(24 rows)
```
And focus on the foreign scan node on `ft1`:
```
-> Materialize
Output: ref_1.c3, (ref_0.c2)
-> Foreign Scan on public.ft1 ref_1
Output: ref_1.c3, ref_0.c2
Remote SQL: SELECT c3 FROM "S 1"."T 1" WHERE ((c3 = '00001'::text))
```
We can see that the remote sql does not contain the `ref_0.c2` field, but it appears in the target list anyway, and if the foreign table is not rescannable, but the Materialize node will do rescan because it assumes there is a param in subplan, see code details:
```
/*
* If subnode is to be rescanned then we forget previous stored
* results; we have to re-read the subplan and re-store. Also, if we
* told tuplestore it needn't support rescan, we lose and must
* re-read. (This last should not happen in common cases; else our
* caller lied by not passing EXEC_FLAG_REWIND to us.)
*
* Otherwise we can just rewind and rescan the stored output. The
* state of the subnode does not change.
*/
if (outerPlan->chgParam != NULL ||
(node->eflags & EXEC_FLAG_REWIND) == 0)
{
tuplestore_end(node->tuplestorestate);
node->tuplestorestate = NULL;
node->ts_destroyed = true;
if (outerPlan->chgParam == NULL)
ExecReScan(outerPlan);
node->eof_underlying = false;
}
else
tuplestore_rescan(node->tuplestorestate);
```
This is not expected, beacause our foreign table with `mpp_execute all segments` option is not rescanable.
### What you think should happen instead
the expected plan should be:
```
-> Result
Output: ref_1.c3, (ref_0.c2)
-> Materialize
Output: ref_1.c3
-> Foreign Scan on public.ft1 ref_1
Output: ref_1.c3
Remote SQL: SELECT c3 FROM "S 1"."T 1" WHERE ((c3 = '00001'::text))
```
### How to reproduce
As mentioned above
### Operating System
CentOS 7
### Anything else
_No response_
### Are you willing to submit PR?
- [x] Yes, I am willing to submit a PR!
### Code of Conduct
- [x] I agree to follow this project's [Code of Conduct](https://github.com/apache/cloudberry/blob/main/CODE_OF_CONDUCT.md).
Contributor guide
Research direction
Reproduce the shown LATERAL join with EXPLAIN and inspect the quoted Materialize rescan logic alongside the Foreign Scan planning for join parameters. The fix is complete when ref_0.c2 is no longer included in the ft1 Foreign Scan target list and the plan places it above Materialize through a Result node.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- c, postgresql
- Domain
- databases, distributed-systems
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100