Skip to content

Conversation

johnedquinn
Copy link
Member

@johnedquinn johnedquinn commented Mar 10, 2025

Description

  • Adds parsing, planning, and evaluation support for windows and window functions
    • Parsing support for the window clause itself (and window references) has not been implemented, however, their modeling has been carved out for future support.
  • Adds end-to-end support for the window clause of a SFW query. Window functions can reference the user-defined window -- allowing them to share the computation of the windows across multiple window functions (bringing down the computational cost several-fold).
  • Adds RANK, DENSE_RANK, LAG, LEAD, & ROW_NUMBER window functions

Deprecated

  • Deprecated the previous AST modeling of window functions. We weren't using it, so it slipped by the 1.0 package audit. It does not follow the SQL-spec and is not able to be updated to support the SQL spec.

Window Design

  • Researched implementations previously implemented in PLK (WindowFunction, Window Plan Node), Trino (Plan WindowNode, Compiled WindowOperator, Compiled WindowFunction), and PostgreSQL (Compiled window functions, Compiled Lead/Lag func).
  • This PR, given optional ORDER BY and PARTITION BY clauses on a window specification, will inject a RelOpSort as input to the compiled RelOpWindow. The RelOpSort will sort by both the partition keys and sort keys -- which allows our implementation of the RelOpWindow to grab contiguous rows of a particular partition and peer group (sort group). This makes creation of a WindowPartition much easier (and fairly performant).
    • With the modeling exposed, we can always provide additional metadata to indicate whether some columns are pre-sorted. This may be added in the future for further optimization.
  • In the future, we may add a new WindowFunction API that takes in frame information as well.
  • This PR does NOT create a mechanism for loading SPI implementations of a window function. This PR intentionally creates public interfaces in the plan and evaluator. We may, in the future, add the ability to load SPI-provided window functions and perform a conversion between SPI's definition of a WindowFunction and the plan and evaluator's definition of a WindowFunctionNode and WindowFunction. This PR acts this way to maintain a separation of concerns. For now, we are only modeling the built-ins as provided by the database system, however, the APIs can be extended in the future to accept and converts SPI-provided window functions.
    • This was and still is my personal opinion on tables, scalar functions, and aggregate functions. It allows for a tiny SPI package in the future if we'd like.

Future Changes

  • I anticipate that the definition of a RelWindow, WindowFunctionNode, and WindowFunction will be need to be updated to support frames, which is totally fine. For WindowFunction, we'll likely need to add a new method that takes as input some frame information, which by default should call the existing (probably deprecated in the future) eval method. This will not break the public API, nor would it break the existing customer flow.

Other Information

  • Updated Unreleased Section in CHANGELOG: NO: Not yet
  • Any backward-incompatible changes? NO: Not technically.
  • Any new external dependencies? NO
  • Do your changes comply with the Contributing Guidelines and Code Style Guidelines? YES

License Information

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

@codecov-commenter
Copy link

codecov-commenter commented Mar 11, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 31.84%. Comparing base (2c17795) to head (bf7b73c).
⚠️ Report is 20 commits behind head on main.

Additional details and impacted files
@@             Coverage Diff              @@
##               main    #1746      +/-   ##
============================================
+ Coverage     31.10%   31.84%   +0.74%     
- Complexity       99      102       +3     
============================================
  Files            31       31              
  Lines          2016     2013       -3     
  Branches        152      151       -1     
============================================
+ Hits            627      641      +14     
+ Misses         1366     1350      -16     
+ Partials         23       22       -1     
Flag Coverage Δ
CLI 15.66% <100.00%> (+0.95%) ⬆️
EXAMPLES 80.03% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@johnedquinn johnedquinn force-pushed the window branch 2 times, most recently from 6111dc6 to eb013ab Compare March 11, 2025 21:46
@johnedquinn johnedquinn changed the title [DRAFT] Adds RANK, DENSE_RANK, LAG, LEAD, & ROW_NUMBER Window Functions & Operator Adds RANK, DENSE_RANK, LAG, LEAD, & ROW_NUMBER Window Functions & Operator Mar 11, 2025
@johnedquinn johnedquinn marked this pull request as ready for review March 11, 2025 22:01
@johnedquinn johnedquinn requested review from jpschorr and yliuuuu March 11, 2025 22:02
Copy link
Contributor

@yliuuuu yliuuuu left a comment

Choose a reason for hiding this comment

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

Finished Parser and Ast Package. As always, very delightful code : ).

One preliminary comment:

Perhaps it is worth to track the feature gap between components if anything exists.

i.e., if something is supported in parsing but not supported in planning, etc.

*/
@Builder(builderClassName = "Builder")
@EqualsAndHashCode(callSuper = false)
public static final class NoArg extends WindowFunctionType {
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmmm, mind evaluating why the choice of NoArg vs say Ranking and RowNumber.

So seemingly for one we are modeling using number of args, for the other we are modeling using "function characteristic" for lack of better words.

I understand that both Ranking and RowNumber must not contains argument... And no arg is, well, no arg... and you don't have to worry about argument type....

But perhaps using NoArg is bit over-smart, in that it might cause difficulty when (especially a user is) constructing the ast.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, well, a user can just use a factory method that we provide. We could do what you're proposing, but also the enum is also a small footprint. I'm not sold in either direction. If you feel strongly, I don't mind changing it to be that way.

Copy link
Member Author

Choose a reason for hiding this comment

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

Decided to go ahead with your observation. Trying to thread the needle between your comments and @alancai98's.

@johnedquinn
Copy link
Member Author

@yliuuuu added a commit to fix the nullable references to lag/lead arguments.

Regarding any potential feature gaps, I've cut: #1748. The only other thing not supported is the IGNORE/RESPECT NULLS for the LAG/LEAD functions. See the test in the planner -- that should be sufficient for tracking.

@johnedquinn johnedquinn requested a review from yliuuuu March 19, 2025 15:31
Copy link
Member

@alancai98 alancai98 left a comment

Choose a reason for hiding this comment

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

Leaving some initial comments for the parsing + AST modeling. I'll need some more time to read through the rest of the PR

Comment on lines +991 to +1008
* EBNF 2023:
* <window function> ::= <window function type> OVER <window name or specification>
*/
windowFunction: funcType=windowFunctionType OVER spec=windowNameOrSpecification;

/**
* EBNF 2023:
* <window function type> ::=
* <rank function type> <left paren> <right paren>
* | ROW_NUMBER <left paren> <right paren>
* | <lead or lag function>
* | and more...
*/
windowFunctionType
: rankFunctionType PAREN_LEFT PAREN_RIGHT # WindowFunctionTypeRank
| ROW_NUMBER PAREN_LEFT PAREN_RIGHT # WindowFunctionTypeRowNumber
| leadOrLagFunction # WindowFunctionTypeLeadOrLag
;
Copy link
Member

Choose a reason for hiding this comment

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

Rather than have the window function type be hard-coded, could we do something similar to what we do for scalar and aggregations (i.e. the modeling of functionCall uses a qualifiedName rather than any hard-coded names)? Doing so will make it a lot easier to add new window functions and also user-defined window functions in the future.

Relevant prior PRs to get rid of hard-coded scalar and aggregations from the antlr grammar

Copy link
Member Author

Choose a reason for hiding this comment

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

I'd say let's do that when we get there. The EBNF does not contribute to API breaking changes. WindowFunctionType also is not a sealed interface. If we want to make it more explicit, I can make NoArg specific to RowNumber and Rank. Then, if we ever decided to allow user-defined window functions, we can do so by adding a variant specifically for this.

Copy link
Member

@alancai98 alancai98 Aug 29, 2025

Choose a reason for hiding this comment

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

I'm still not fully-convinced that the modeling for the window functions needs to be hard-coded in the parser and AST. In the future if we are to support more window functions, we would need additional rules in the parser and variants within the WindowFunctionType, which may be redundant. Trino, for instance, defines all scalar, aggregate, and window functions as a FunctionCall (see their grammar).

Since we've marked all the public APIs as experimental though, I'm still fine w/ proceeding and experimenting further as we get feedback from customers.

// TODO: In the future: @Nullable private final WindowFrameClause frameClause;

@Nullable
private final Identifier.Simple existingName;
Copy link
Member

Choose a reason for hiding this comment

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

nit: could we just call this name? The SQL2023 spec uses <existing window name> but it just wraps a <window name>

Copy link
Member Author

Choose a reason for hiding this comment

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

Personally, I like the fact that it matches the EBNF and that it makes it apparent that the WindowSpecification operates against a pre-defined (aka existing) window, effectively creating a new window.

Comment on lines 13 to 19
/**
* Represents a window function type.
* @see ExprWindowFunction#getFunctionType()
*/
public abstract class WindowFunctionType extends AstNode {
Copy link
Member

Choose a reason for hiding this comment

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

Similar to the parser comment, I'm wondering if we should change the window functions to not be as hard-coded within the AST modeling and follow something more like scalar and aggregate function calls. IMO this can make it a lot more straightforward to add future support for user-defined window functions.

Trino for example models window functions (along with scalars and aggregates) using FunctionsCall. For all of the function names, they just use a QualifiedName rather than any sort of hard-coded names within their AST, leaving the validation to a later stage. I'm not sure if we should lump all three of those under ExprCall but modeling window functions with just an identifier name rather than the WindowFunctionType could be a cleaner modeling as more window functions are added over time.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'd be more keen to add a user-defined variant in the future. See the 2003 EBNF. Trino doesn't have support for over half of the specified variants of the window functions.

*/
@Builder(builderClassName = "Builder")
@EqualsAndHashCode(callSuper = false)
public static final class InLineSpecification extends WindowReference {
Copy link
Member

Choose a reason for hiding this comment

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

It's not clear to me why we need a separate in-line window specification class. Perhaps a simpler modeling could follow what Trino does with their Window interface.

The Window interface is implemented by two separate classes

Following a similar modeling, we could have a Window abstract class that is used by the ExprWindowFunction.

Copy link
Member Author

Choose a reason for hiding this comment

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

We could put WindowSpecification to be a variant of WindowReference (and make it a nested class, or leave it out separately, doesn't matter to me). Alternatively, extract a WindowReference interface. Did this locally, and while it's nice, I'm really despising how AstNode is an abstract class. Should've just created a public AstNodeBase. I'll publish a rev, and it should be apparent why it's a burden.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm actually going to take @yliuuuu's recommendation of removing WindowReference entirely. It removes all the shenanigans, and we get what you're describing also 😄

Comment on lines +67 to +177
private final Expr extent;
private final Long offset;
private final Expr defaultValue;
Copy link
Member

Choose a reason for hiding this comment

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

If window functions were modeled more closely to ExprCall, these could all be function arguments rather than hard-coded properties.

Copy link
Contributor

@yliuuuu yliuuuu left a comment

Choose a reason for hiding this comment

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

Overall looks good to me. Just one comment on the Typing of Lag/Lead Function.

*/
@Builder(builderClassName = "Builder")
@EqualsAndHashCode(callSuper = false)
public static final class LeadOrLag extends WindowFunctionType {
Copy link
Contributor

Choose a reason for hiding this comment

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

No objection here, but might argue that by breaking it down into two classes, the API is more consistent.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, I mean I'm fine with that too 😄 Whatever floats your boat

Comment on lines +51 to +52
returnTyper.accumulate(expr)
returnTyper.accumulate(default)
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we want to accumulate or throw on mismatch?

Copy link
Member Author

Choose a reason for hiding this comment

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

I might be missing something. The lag/lead's output type will be the expr or default if the offset row doesn't exist, so they don't need to match. In the case of coercible types, maybe we coerce them to their common super type, hence the TODO. But, I don't see any problem with the output type being ANY if the types aren't coercible.

The only validation I'd say is required is that the offset by coercible to BIGINT (or for now just BIGINT).

… clause

Adds AST modeling for window functions

Adds plan modeling for window functions

Deprecates old window function AST APIs due to incompatibility

Adds RANK, DENSE_RANK window functions

Adds ROW_NUMBER window function

Adds LAG and LEAD window functions

Adds support for referencing window clause from window functions
Adds children check

Leverages different warning type
Copy link
Member

@alancai98 alancai98 left a comment

Choose a reason for hiding this comment

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

Thanks for updating the PR and adding the experimental notices! Mostly left some minor comments/feedback that should be quick to address before we merge. A couple other things to highlight:

  • I think we should try to use a separate tracking issue for things that are left as TODOs. Otherwise, the TODOs may be hidden in the code and hard to prioritize.
  • The current modeling of window functions in the parser/ast may need further experimentation as we integrate w/ customers. Since everything that's public will be marked w/ experimental, I think we can still proceed with merging in its current modeling.

Copy link
Member

Choose a reason for hiding this comment

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

partiql-tests submodule change should be reverted

@@ -27,6 +27,7 @@ Thank you to all who have contributed!

### Added
- partiql-ast: add `With` and `WithListElement` to `AstVisitor` and `SqlDialect`
- Adds end-to-end support for window functions including RANK, DENSE_RANK, LAG, LEAD, and ROW_NUMBER functions alongside the WINDOW clause.
Copy link
Member

Choose a reason for hiding this comment

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

We should call out that the new window clause and window function support is experimental.

*/
@lombok.Builder(builderClassName = "Builder")
@EqualsAndHashCode(callSuper = false)
public static final class WindowDefinition extends AstNode {
Copy link
Member

Choose a reason for hiding this comment

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

nit: can unnest from the WindowClause or just call this Definition. Currently the type is WindowClause.WindowDefinition, which is pretty lengthy

Copy link
Member

Choose a reason for hiding this comment

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

also need a deprecated/experimental notice

* @deprecated This feature is experimental and is subject to change.
*/
@Deprecated
public abstract class WindowPartition extends AstNode {
Copy link
Member

Choose a reason for hiding this comment

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

Why is the WindowPartition abstract? If I'm matching this up properly to the SQL2023 spec, this lines up with <window partition column reference>, which will have a <column reference> and an optional <collate clause>. Perhaps a simpler modeling is just as a final class with a columnReference field.

@@ -27,6 +27,7 @@ Thank you to all who have contributed!

### Added
- partiql-ast: add `With` and `WithListElement` to `AstVisitor` and `SqlDialect`
- Adds end-to-end support for window functions including RANK, DENSE_RANK, LAG, LEAD, and ROW_NUMBER functions alongside the WINDOW clause.
- **EXPERIMENTAL** partiql-plan: add representation of `RelWith` and `WithListElement` to the plan and the
`OperatorVisitor`
- **EXPERIMENTAL** partiql-planner: add planner builder function to control whether `With` table references are
Copy link
Member

Choose a reason for hiding this comment

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

Should probably also call out that the prior modeling of window is deprecated and replaced with the newer modeling.

// if (t != null) {
// return t;
// }
return null;
Copy link
Member

Choose a reason for hiding this comment

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

Why are we returning null for the cause here?

@@ -35,7 +35,7 @@ public PType getType() {
public String toString() {
return "DatumString{" +
"_type=" + _type +
", _value=" + _value +
", _value='" + _value + '\'' +
Copy link
Member

Choose a reason for hiding this comment

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

nit: for consistency and don't need the escape if we use double-quotes

Suggested change
", _value='" + _value + '\'' +
", _value='" + _value + "'" +

@@ -324,6 +325,106 @@ public R visitExprArray(ExprArray node, C ctx) {
return defaultVisit(node, ctx);
}

/**
Copy link
Member

Choose a reason for hiding this comment

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

For these functions, probably need an experimental notice.

@EqualsAndHashCode(callSuper = false)
@Deprecated
public final class WindowSpecification extends AstNode {
// TODO: In the future: @Nullable private final WindowFrameClause frameClause;
Copy link
Member

Choose a reason for hiding this comment

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

Could we create a tracking issue for the TODOs brought up in this PR? In the code and in PR description there are a lot of TODOs mentioned (e.g. window frame clause, unsupported functions in WindowBuiltins) but may be hard to discover unless we do a code search of the repo.

Comment on lines +991 to +1008
* EBNF 2023:
* <window function> ::= <window function type> OVER <window name or specification>
*/
windowFunction: funcType=windowFunctionType OVER spec=windowNameOrSpecification;

/**
* EBNF 2023:
* <window function type> ::=
* <rank function type> <left paren> <right paren>
* | ROW_NUMBER <left paren> <right paren>
* | <lead or lag function>
* | and more...
*/
windowFunctionType
: rankFunctionType PAREN_LEFT PAREN_RIGHT # WindowFunctionTypeRank
| ROW_NUMBER PAREN_LEFT PAREN_RIGHT # WindowFunctionTypeRowNumber
| leadOrLagFunction # WindowFunctionTypeLeadOrLag
;
Copy link
Member

@alancai98 alancai98 Aug 29, 2025

Choose a reason for hiding this comment

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

I'm still not fully-convinced that the modeling for the window functions needs to be hard-coded in the parser and AST. In the future if we are to support more window functions, we would need additional rules in the parser and variants within the WindowFunctionType, which may be redundant. Trino, for instance, defines all scalar, aggregate, and window functions as a FunctionCall (see their grammar).

Since we've marked all the public APIs as experimental though, I'm still fine w/ proceeding and experimenting further as we get feedback from customers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants