Skip to content
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

Merged
merged 24 commits into from
Apr 9, 2024
Merged

feat(binder): bind RCTE #15522

merged 24 commits into from
Apr 9, 2024

Conversation

TennyZhuang
Copy link
Contributor

@TennyZhuang TennyZhuang commented Mar 7, 2024

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

  • I have written necessary rustdoc comments
  • I have added necessary unit tests and integration tests
  • I have added test labels as necessary. See details.
  • I have added fuzzing tests or opened an issue to track them. (Optional, recommended for new SQL features Sqlsmith: Sql feature generation #7934).
  • My PR contains breaking changes. (If it deprecates some features, please create a tracking issue to remove them in the future).
  • All checks passed in ./risedev check (or alias, ./risedev c)
  • My PR changes performance-critical code. (Please run macro/micro-benchmarks and show the results.)
  • My PR contains critical fixes that are necessary to be merged into the latest release. (Please check out the details)

Documentation

  • My PR needs documentation updates. (Please use the Release note section below to summarize the impact on users)

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.

Signed-off-by: TennyZhuang <[email protected]>
@TennyZhuang TennyZhuang marked this pull request as ready for review March 13, 2024 07:06
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

@TennyZhuang TennyZhuang Mar 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

DuckDB also have two kind of CTE nodes.

Comment on lines 39 to 43
/// UNION in recursive CTE definition
RecursiveUnion {
base: Box<BoundSetExpr>,
recursive: Box<BoundSetExpr>,
},
Copy link
Contributor

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? 🤔

Copy link
Contributor Author

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

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.

Copy link
Contributor

@xiangjinwu xiangjinwu Mar 14, 2024

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.

Copy link
Contributor

@xiangjinwu xiangjinwu left a 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/expr/mod.rs Outdated Show resolved Hide resolved
Comment on lines 39 to 43
/// UNION in recursive CTE definition
RecursiveUnion {
base: Box<BoundSetExpr>,
recursive: Box<BoundSetExpr>,
},
Copy link
Contributor

@xiangjinwu xiangjinwu Mar 14, 2024

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 {
Copy link
Contributor

@xiangjinwu xiangjinwu Mar 14, 2024

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)

src/frontend/src/binder/query.rs Outdated Show resolved Hide resolved
@xzhseh xzhseh self-assigned this Mar 24, 2024
@xzhseh
Copy link
Contributor

xzhseh commented Apr 3, 2024

need some further review(s) to merge this pr, before things getting too large...
cc @wangrunji0408, @xiangjinwu.

ps. some additional contexts / discussions / reviews / to-do(s) can be found in #16023.

Comment on lines 61 to 65
// 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")
}
Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor

@xzhseh xzhseh Apr 7, 2024

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.

src/frontend/src/binder/relation/mod.rs Outdated Show resolved Hide resolved
if let BindingCteState::Bound { .. } =
self.cte_to_relation.get(&k).unwrap().borrow().state.clone()
{
// do nothing
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#16023 (comment)

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 😂 )

Copy link
Contributor Author

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

Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor

@xiangjinwu xiangjinwu Apr 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

} 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

Copy link
Contributor

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

src/frontend/src/binder/query.rs Outdated Show resolved Hide resolved
xzhseh and others added 3 commits April 4, 2024 11:17
@TennyZhuang TennyZhuang requested a review from a team as a code owner April 7, 2024 05:44
@TennyZhuang
Copy link
Contributor Author

Added a test

@xzhseh xzhseh requested a review from xiangjinwu April 8, 2024 21:14
@xzhseh
Copy link
Contributor

xzhseh commented Apr 8, 2024

ready to merge, cc @chenzl25 @TennyZhuang @xiangjinwu.

Copy link
Contributor

@xiangjinwu xiangjinwu left a 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 in BindingCteState::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)

@TennyZhuang TennyZhuang enabled auto-merge April 9, 2024 08:02
@TennyZhuang TennyZhuang added this pull request to the merge queue Apr 9, 2024
Merged via the queue into main with commit 0c8371a Apr 9, 2024
28 of 29 checks passed
@TennyZhuang TennyZhuang deleted the bind_rcte branch April 9, 2024 08:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Bind Recursive CTE
5 participants