-
Notifications
You must be signed in to change notification settings - Fork 593
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(binder): bind RCTE #15522
feat(binder): bind RCTE #15522
Conversation
Signed-off-by: TennyZhuang <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Intuitively, if we have decoupled the declaration (knowing its schema) and definition (binding the recursive query) stage of CTE, we can use the existing BoundShare
to refer to all CTEs no matter they are recursive or not. I feel that this new structure is not necessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, it's a back reference. If you take a look at the definition of BoundShare
, you'll know it's impossible to use that, otherwise the loop will not end.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
src/frontend/src/binder/set_expr.rs
Outdated
/// UNION in recursive CTE definition | ||
RecursiveUnion { | ||
base: Box<BoundSetExpr>, | ||
recursive: Box<BoundSetExpr>, | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel this is not necessary as well. Consider recursive functions in common programming languages, compiler won't have any special representations of the expressions in this function just because it is recursive, right? 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But it's a special operator.
RecursiveUnion has extremely different schema/stream-key derivation logical, and have unique executor implementations in all databases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://github.com/duckdb/duckdb/blob/main/src/execution/operator/set/physical_recursive_cte.cpp
In duckdb (UNION is executed in the operator)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You will find that in RCTE definition, UNION is special enough. The top level operator of RCTE must be a UNION
or UNION ALL
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am on the other side of the spectrum 😂 It is too special to be part of BoundSetExpr
. Most places accepting BoundSetExpr
would not accept this variant, while the place that is designated for this special variant would not allow other variants.
In my mind it would look like the following:
pub enum BindingCteState {
/// We know nothing about the CTE before resolve the body.
#[default]
Init,
/// We know the schema from after the base term resolved.
BaseResolved { schema: Schema },
/// We get the whole bound result.
Bound { query: Either<BoundQuery, RecursiveUnion> },
}
struct RecursiveUnion {
all: bool,
base: Box<BoundQuery>,
recursive: Box<BoundQuery>,
}
(or the Either
can be flatten into BoundNonRecursiveQuery
and BoundRecursiveUnion
)
Note the difference between Query
and SetExpr
is the former can accept order
/limit
/etc. I have tried in PostgreSQL that:
test=# with recursive t as (select 1 union all select n+1 from t as t(n) where n < 5 order by 1) select * from t;
ERROR: ORDER BY in a recursive query is not implemented
LINE 1: ...ll select n+1 from t as t(n) where n < 5 order by 1) select ...
^
test=# with recursive t as ((select 1 limit 1) union all (select n+1 from t as t(n) where n < 5 limit 3)) select * from t;
?column?
----------
1
2
3
4
5
(5 rows)
That is, a recursive cte cannot have order
/limit
/etc at root, but can have it as part of base or recursive term.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rest lgtm
src/frontend/src/binder/set_expr.rs
Outdated
/// UNION in recursive CTE definition | ||
RecursiveUnion { | ||
base: Box<BoundSetExpr>, | ||
recursive: Box<BoundSetExpr>, | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am on the other side of the spectrum 😂 It is too special to be part of BoundSetExpr
. Most places accepting BoundSetExpr
would not accept this variant, while the place that is designated for this special variant would not allow other variants.
In my mind it would look like the following:
pub enum BindingCteState {
/// We know nothing about the CTE before resolve the body.
#[default]
Init,
/// We know the schema from after the base term resolved.
BaseResolved { schema: Schema },
/// We get the whole bound result.
Bound { query: Either<BoundQuery, RecursiveUnion> },
}
struct RecursiveUnion {
all: bool,
base: Box<BoundQuery>,
recursive: Box<BoundQuery>,
}
(or the Either
can be flatten into BoundNonRecursiveQuery
and BoundRecursiveUnion
)
Note the difference between Query
and SetExpr
is the former can accept order
/limit
/etc. I have tried in PostgreSQL that:
test=# with recursive t as (select 1 union all select n+1 from t as t(n) where n < 5 order by 1) select * from t;
ERROR: ORDER BY in a recursive query is not implemented
LINE 1: ...ll select n+1 from t as t(n) where n < 5 order by 1) select ...
^
test=# with recursive t as ((select 1 limit 1) union all (select n+1 from t as t(n) where n < 5 limit 3)) select * from t;
?column?
----------
1
2
3
4
5
(5 rows)
That is, a recursive cte cannot have order
/limit
/etc at root, but can have it as part of base or recursive term.
let share_id = self.next_share_id(); | ||
let Cte { alias, query, .. } = cte_table; | ||
let table_name = alias.name.real_value(); | ||
if with.recursive { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately this recursive
is a single toggle for all CTEs (doc). When absent it allows a binder shortcut, but when specified we need to attempt in both directions for all CTEs. (Update on April 2nd: attempt and recover context is hard)
Example in PostgreSQL:
test=# with recursive t as (select 1) select * from t; -- unnecessary `recursive` allowed
?column?
----------
1
(1 row)
Co-authored-by: xiangjinwu <[email protected]>
Signed-off-by: TennyZhuang <[email protected]>
Signed-off-by: TennyZhuang <[email protected]>
need some further review(s) to merge this pr, before things getting too large... ps. some additional contexts / discussions / reviews / to-do(s) can be found in #16023. |
src/frontend/src/planner/relation.rs
Outdated
// todo: ensure this will always be wrapped in a `Relation::Share` | ||
// so that it will not be explicitly planned here | ||
Relation::RecursiveUnion(..) => { | ||
bail_not_implemented!(issue = 15135, "recursive CTE is not supported") | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This suggest RecursiveUnion
shall not be a variant of Relation
, similar to how it is not a variant of SetExpr
.
It seems this variant is mainly introduced so that it can fit into BoundShare { input: Relation }
. And also similarly, I feel like it shall be updated to BoundShare { input: Either<BoundQuery, RecursiveUnion> }
.
All existing BoundShare.input
before this PR are always Relation::Subquery(BoundSubquery{ lateral: false, query: BoundQuery})
which can be simplified into input: BoundQuery
. @chenzl25 Could you help clarify whether this simplification is valid?
Even if the simplification is not valid, it might be possible to use Either<Relation, RecursiveUnion>
(so that relations other than inside BoundShare
are impossible to include RecursiveUnion
).
Furthermore, it looks unnecessary to have both bind_context::RecursiveUnion
and relation::BoundRecursiveUnion
. The one in bind_context
is already bound (maybe update its name) and can be reused in BoundShare
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All existing BoundShare.input before this PR are always Relation::Subquery(BoundSubquery{ lateral: false, query: BoundQuery}) which can be simplified into input: BoundQuery. @chenzl25 Could you help clarify whether this simplification is valid?
I have revisited the usage of BoundShare.input
and indeed the input right now could only be BoundQuery
. Previously, this struct was designed to support source sharing as well, however, source sharing is implemented in the optimizer phase, so we can use BoundQuery
to replace the Relation
and finally, we could update it to BoundShare { input: Either<BoundQuery, RecursiveUnion> }
to support recursive CTE.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks 🩵 - this make sense to me, let me do some refactor later.
if let BindingCteState::Bound { .. } = | ||
self.cte_to_relation.get(&k).unwrap().borrow().state.clone() | ||
{ | ||
// do nothing |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We shall add the test case to showcase the necessity of this special check. (Btw I am still trying to understand it at this time. The ask for test case is not an endorsement of it being valid 😂 )
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Panicked here:
thread 'rw-main' panicked at src/frontend/src/binder/bind_context.rs:368:54:
called `Option::unwrap()` on a `None` value
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should be fixed now - just update the logic when checking if merge_context
is applying for rcte
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems this special bypass just suppress something that is actually wrong behind the scene:
with recursive t(n) as (
values (1)
union all
select n + 1 from t where n < 100
)
select * from t;
______________^ here
Without the update here, the error is saying that t
is already in BindContext
before binding t
. This error is TRUE, and adding this bypass does not fix the actual dirty-context problem: before binding t
, the context is supposed to be empty.
As an example, having a dirty initial context makes the following malformed query to pass binder validation (and panic in planner):
with recursive t(n) as (
values (1)
union all
select n + 1 from t where n < 100
)
select n; -- n is not supposed to be in context, but the binder implementation here contains it somehow
I am still tracing down how the context become dirty.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
risingwave/src/frontend/src/binder/query.rs
Lines 378 to 379 in 3f50c60
} else { | |
let bound_query = self.bind_query(query)?; |
The nonrecursive branch's bind_query
pushes a new context upon entry and clears it before returning, while the recursive branch's two bind_set_expr
calls write directly into current context. Maybe (?) this is the reason 🤔
The problem would not arise if both sides of RecursiveUnion were considered Query
rather than SetExpr
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix available for review: #16282
Signed-off-by: TennyZhuang <[email protected]>
Signed-off-by: TennyZhuang <[email protected]>
Added a test |
ready to merge, cc @chenzl25 @TennyZhuang @xiangjinwu. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Noting down limitations to be refined in the future:
recursive
keyword may be followed by multiple CTEs, some of which may not be actual recursive.align_schema
may either promote lhs (base) or rhs (recursive) data type to reach a common type. However, given the lhs schema is already persisted inBindingCteState::BaseResolved
, it shall not be promoted in the later stage. As in PostgreSQL:
// lhs bigint larger than rhs int, OK
test=# WITH RECURSIVE t1 AS (SELECT 1::bigint AS a UNION ALL SELECT (a + 1)::int as b FROM t1 WHERE a < 5) SELECT * FROM t1;
a
---
1
2
3
4
5
(5 rows)
// lhs int smaller than rhs bigint, Err
test=# WITH RECURSIVE t1 AS (SELECT 1::int AS a UNION ALL SELECT (a + 1)::bigint as b FROM t1 WHERE a < 5) SELECT * FROM t1;
ERROR: recursive query "t1" column 1 has type integer in non-recursive term but type bigint overall
LINE 1: WITH RECURSIVE t1 AS (SELECT 1::int AS a UNION ALL SELECT (a...
^
HINT: Cast the output of the non-recursive term to the correct type.
// lhs null not inferred as rhs int but fall back to text
test=# WITH RECURSIVE t1 AS (SELECT null AS a UNION ALL SELECT (a + 1)::int as b FROM t1 WHERE a < 5) SELECT * FROM t1;
ERROR: operator does not exist: text + integer
LINE 1: ...RSIVE t1 AS (SELECT null AS a UNION ALL SELECT (a + 1)::int ...
^
HINT: No operator matches the given name and argument types. You might need to add explicit type casts.
// When nonrecursive, lhs int allowed to be smaller than rhs bigint
test=# WITH t1 AS (SELECT 1::int AS a UNION ALL SELECT 2::bigint as b) SELECT * FROM t1;
a
---
1
2
(2 rows)
// When nonrecursive, lhs null inferred as rhs int
test=# WITH t1 AS (SELECT null AS a UNION ALL SELECT 2::int as b) SELECT * FROM t1;
a
---
2
(2 rows)
I hereby agree to the terms of the RisingWave Labs, Inc. Contributor License Agreement.
What's changed and what's your intention?
#15135
Close #15681
Checklist
./risedev check
(or alias,./risedev c
)Documentation
Release note
If this PR includes changes that directly affect users or other significant modifications relevant to the community, kindly draft a release note to provide a concise summary of these changes. Please prioritize highlighting the impact these changes will have on users.