Conversation
Add support for SQL WINDOW clause allowing named window definitions that can be referenced by multiple window functions: SELECT sum(x) OVER w, avg(x) OVER w FROM t WINDOW w AS (ORDER BY ts ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) Changes: - WindowExpression: add windowName field and copySpecFrom() method - QueryModel: add namedWindows map for storing window definitions - SqlParser: parse WINDOW clause after ORDER BY, before LIMIT - ExpressionParser: handle OVER w syntax and parseWindowSpec() - SqlCodeGenerator: resolve named window references - SqlOptimiser: validate named windows and copy to new window models - SqlKeywords: fix isWindowKeyword() index bug Known limitation: ORDER BY columns must be in SELECT list for now. This will be addressed in a follow-up commit.
Two bugs fixed: 1. Window function deduplication incorrectly treated different named window references as duplicates because compareWindowExpressions() didn't compare the windowName field. Added windowName to both comparison and hash functions in ExpressionNode. 2. Lexer buffer reuse caused second window name to be stored as empty string. Fixed by calling Chars.toString() immediately when capturing the window name in SqlParser. Added test testMultipleNamedWindowsPartitionedFirst to cover both bugs.
Named windows are stored on the original SELECT model, but during optimization a SELECT_MODEL_WINDOW wrapper is created. The column propagation code was only checking the immediate model, not the nested model hierarchy, causing "Invalid column" errors when ORDER BY or PARTITION BY columns weren't in the SELECT list. Added lookupNamedWindow() helper that walks the model hierarchy to find named window definitions. Updated emitColumnLiteralsTopDown() and rewriteSelectClause0() to use this helper.
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the Use the checkbox below for a quick retry:
WalkthroughAdds named WINDOW clause support and shared inline window-spec parsing: parsing, model storage, optimiser resolution, validation, and extensive tests for named-window semantics and equivalence with inline OVER specifications. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…OIN disambiguation - Fix named windows not working with CTEs and subqueries by storing them on masterModel instead of the FROM model in parseFromClause() - Rewrite WINDOW JOIN lookahead logic to use cleaner backTo() approach, avoiding unnecessary String allocations during disambiguation - Add 9 tests for WINDOW clause with CTEs, subqueries, and nested queries - Add 2 tests verifying explain plans are identical for named vs inline windows
Remove windowEndPos variable that was assigned but never used.
|
@CodeRabbit pls review critically |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 1980 🏁 Script executed: Length of output: 36 🏁 Script executed: Length of output: 36 🏁 Script executed: Length of output: 36 🏁 Script executed: Length of output: 36 🏁 Script executed: Length of output: 36 🏁 Script executed: Length of output: 80 🏁 Script executed: Length of output: 11527 🏁 Script executed: Length of output: 63703 🏁 Script executed: Length of output: 619 🏁 Script executed: Length of output: 2651 🏁 Script executed: Length of output: 1277 🏁 Script executed: Length of output: 0 🏁 Script executed: Length of output: 1142 🏁 Script executed: Length of output: 2441 🏁 Script executed: Length of output: 1820 🏁 Script executed: Length of output: 535 🏁 Script executed: Length of output: 384 🏁 Script executed: Length of output: 482 🏁 Script executed: Length of output: 1038 🏁 Script executed: Length of output: 584 🏁 Script executed: Length of output: 0
Critical Issues1. Inadequate Window Name Validation 🔴The window name validation is dangerously weak: // Lines 524-527 in ExpressionParser.java
if (tok.length() == 1 && !Character.isLetterOrDigit(tok.charAt(0))) {
throw SqlException.$(lexer.lastTokenPosition(), "'(' or window name expected after OVER");
}Problems:
Same issue in SqlParser.java line 2875-2877 - identical weak validation. 2. SqlKeywords.isWindowKeyword Bug Fix Not Tested 🟡You fixed a real bug (checking indices 3,4,5 instead of 4,5,6), but excludedCases.add("isWindowKeyword");Why is this excluded? The bug proves this needs testing. 3. Late Error Detection - Design Flaw 🔴Named windows are resolved at code generation time (SqlCodeGenerator), not during parsing or optimization. This means:
Why not resolve immediately after parsing the WINDOW clause? You have all the information at that point. 4. Performance: Repeated Lookups 🟡
For a query with N window functions using M named windows across D depth of nested models, this is O(N * M * D) lookups. Could these be cached or resolved once? 5. Lookahead Logic Fragility 🟡Lines 2628-2650 in SqlParser.java: lexer.backTo(windowLastPos, null); // Line 2643You're passing 6. Changed Deduplication Behavior 🟡In CharSequence windowName = w.getWindowName();
int hash = windowName != null ? Chars.lowerCaseHashCode(windowName) : 0;This means Conversely, 7. Low Coverage on Critical Code 🔴ExpressionParser.java has only 61.84% coverage despite adding 145 new lines. This is your parsing entry point - it should have the highest coverage! What paths aren't tested? 8. Missing Edge Case TestsNo tests for:
9. Unclear Model Copying Logic 🟡Lines 8770 and 8782 in SqlOptimiser.java: // Copy named windows from original model for WINDOW clause support
windowModel.getNamedWindows().putAll(model.getNamedWindows());Why is this necessary? If a window wrapper model is created during optimization, why does it need the named windows? What happens if there are naming conflicts between nested models? The comment doesn't explain the reasoning. 10. Inconsistent Error Messages
The first two should be identical but aren't. Use consistent error messages. Questions
Recommendations
The feature works for the happy path, but the edge cases and design decisions need hardening. The validation gaps are concerning. 🧠 Learnings used |
- Deduplicate window spec parsing between OVER and WINDOW clauses - Fix hash/equality contract in ExpressionNode (case-insensitive match) - Validate window names with validateIdentifier and keyword rejection - Fix WINDOW/JOIN disambiguation with robust 2-token lookahead - Cache lookupNamedWindow results to avoid repeated model traversals - Guard stopOrderByPropagate for unresolved named windows - Remove isWindowKeyword test exclusion in SqlKeywordsTest - Remove dead code in SqlOptimiser - Add comprehensive edge case tests (UNION ALL, GROUP BY, RANGE frame, ROWS frame, multiple windows, scope isolation, keyword rejection, etc.)
Move named window resolution (OVER w where w is defined in WINDOW clause) from SqlCodeGenerator to SqlOptimiser. This enables proper deduplication of named vs inline windows with identical specs and removes conservative ORDER BY propagation guards that were needed when specs were unresolved. - Add resolveNamedWindows() to SqlOptimiser, called before rewriteSelectClause - Remove resolveNamedWindowReference() from SqlCodeGenerator - Simplify emitColumnLiteralsTopDown and rewriteSelectClause0 (no ad-hoc lookups) - Remove windowName from ExpressionNode hash/compare (resolved before dedup) - Remove conservative stopOrderByPropagate guard for unresolved names - Add NamedWindowFuzzTest with combinatorial named-vs-inline equivalence checks - Add tests for dedup, table aliases, edge cases, and window spec error paths
The assertEquivalent method catches all exceptions internally and wraps them in AssertionError, so the throws Exception declaration is redundant.
|
@CodeRabbit pls review critically |
🧩 Analysis chain🏁 Script executed: Length of output: 281767
🏁 Script executed: Length of output: 75
🏁 Script executed: Length of output: 286992
🏁 Script executed: Length of output: 5770
What’s changed and what hasn’t
Findings
Corrections to earlier commentary (for the record)
Concrete suggestions (low-risk, targeted)
If you want, I can draft the specific diffs for:
Would you like me to plan those code changes now? ✏️ Learnings added
🧠 Learnings used✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
core/src/main/java/io/questdb/griffin/SqlParser.java (1)
4618-4645:⚠️ Potential issue | 🔴 CriticalMove
validateIdentifierto be grouped with otherstatichelper methods per coding guidelines.The visibility change to package-private is necessary and correct—
ExpressionParsercallsSqlParser.validateIdentifier()at line 524. However, the method is now placed at line 4618, far from otherstaticmethods (which are grouped at lines 210–467). Per coding guidelines, Java class members must be grouped by kind; move this method to align with other package-private/private static helpers around lines 406–467.core/src/main/java/io/questdb/griffin/SqlOptimiser.java (1)
9655-9677:⚠️ Potential issue | 🟡 MinorUse enhanced switch syntax in named-window validation
The newly added
switchblocks use legacy syntax. Convert to enhanced switch to align with repo guidelines (Java 17 target supports this feature).♻️ Example conversion
- switch (ac.getRowsLoKind()) { - case WindowExpression.PRECEDING: - rowsLo = rowsLo != Long.MAX_VALUE ? -rowsLo : Long.MIN_VALUE; - break; - case WindowExpression.FOLLOWING: - break; - default: - // CURRENT ROW - rowsLo = 0; - break; - } + rowsLo = switch (ac.getRowsLoKind()) { + case WindowExpression.PRECEDING -> rowsLo != Long.MAX_VALUE ? -rowsLo : Long.MIN_VALUE; + case WindowExpression.FOLLOWING -> rowsLo; + default -> 0; // CURRENT ROW + };
🤖 Fix all issues with AI agents
In `@core/src/main/java/io/questdb/griffin/SqlOptimiser.java`:
- Around line 6168-6183: resolveNamedWindows currently only checks top-level
QueryColumn instances, so WindowExpression nodes nested inside expression trees
retain windowName and never get their spec copied before
extractAndRegisterNestedWindowFunctions does hashing/dedup; fix by traversing
expression trees (visit expression nodes inside each QueryColumn and any nested
FunctionNode/ExpressionNode structures) and for every WindowExpression where
isNamedWindowReference() is true call lookupNamedWindow(model, windowName) and
then ac.copySpecFrom(namedWindow) (or perform the same resolution at the start
of extractAndRegisterNestedWindowFunctions before hashing), using the existing
methods lookupNamedWindow and WindowExpression.copySpecFrom to ensure all nested
windows are resolved before extraction/dedup.
In `@core/src/main/java/io/questdb/griffin/SqlParser.java`:
- Around line 2870-2915: The stored window name is not being unquoted before
insertion causing mismatches between quoted and unquoted usages; update the
storage sites to unquote identifiers using GenericLexer.unquote() (or
equivalent) before interning/putting into maps. Specifically, in SqlParser where
you currently set CharSequence windowName = Chars.toString(tok) (and use
windowNamePos / masterModel.getNamedWindows().put(...)), call
GenericLexer.unquote(windowName) or unquote the token text first and use that
unquoted value for keying; do the same in ExpressionParser at the window-name
storage site (the code path that parallels parseWithClauses/expectLiteral) so
both readers and lookups use the same unquoted form. Ensure you still use
lexer.lastTokenPosition() for error positions and retain the interning behavior
after unquoting.
In
`@core/src/test/java/io/questdb/test/griffin/engine/window/WindowFunctionTest.java`:
- Around line 10627-10649: The test method testNamedWindowMixedWithInline
currently only exercises a named window; update it so its name or contents
reflect that: either rename the method to something like
testNamedWindowCumulativeSum (update the method declaration and any references)
or modify the SQL in testNamedWindowMixedWithInline to include an inline window
expression (e.g., add a second selected expression using OVER (...) inline) so
the test truly mixes named and inline windows; refer to the existing
testNamedWindowWithInlineWindowInSameQuery for an example of mixing styles when
making changes.
In `@core/src/test/java/io/questdb/test/griffin/SqlParserTest.java`:
- Around line 13631-13685: Move the new testWindowName* methods so they appear
alphabetically with other test methods: place testWindowNameKeywordInOverClause,
testWindowNameKeywordInWindowClause, testWindowNameNumberInOverClause,
testWindowNameNumberInWindowClause, and testWindowNameJoinKeywordInWindowClause
above the existing testWindowOrderDirection and testWindowPartitionByMultiple
methods; ensure the block of test methods remains grouped by kind (tests) and
follows alphabetical ordering by method name.
🧹 Nitpick comments (7)
core/src/main/java/io/questdb/griffin/ExpressionParser.java (2)
760-765:parseWindowSpecis public but could be package-private; also placed among private methods.
SqlParser(the only external caller) is in the sameio.questdb.griffinpackage, so package-private visibility would suffice and reduce the public API surface. Additionally, this public method is placed at line 760 among private methods, while the other public instance method (setStopOnTopINOperator) is near line 149.As per coding guidelines,
**/*.java: "Group Java class members by kind and sort alphabetically."
791-792: Add null check forpartitionExprto match ORDER BY handling.
parseWindowExprcan returnnullwhen the inner expression parser finds nothing to consume (e.g., encountering a comma or unexpected token). The ORDER BY path at line 813-815 explicitly guards against this, but the PARTITION BY path adds the result unconditionally. In practice, subsequent error paths preventnullfrom reaching downstream consumers, but the inconsistency makes the PARTITION BY parsing less robust and harder to reason about.Proposed defensive check
lexer.unparseLast(); ExpressionNode partitionExpr = parseWindowExpr(lexer, sqlParserCallback, decls); + if (partitionExpr == null) { + throw SqlException.$(lexer.lastTokenPosition(), "Expression expected"); + } windowCol.getPartitionBy().add(partitionExpr);core/src/test/java/io/questdb/test/griffin/engine/window/NamedWindowFuzzTest.java (1)
177-207: Consider expanding the randomised window spec space.Currently
randomWindowSpeccovers single PARTITION/ORDER BY columns and only ROWS frames. A few low-risk expansions would strengthen coverage of the named-window resolution path without complicating the generator much:
- Multiple PARTITION BY / ORDER BY columns (e.g.,
PARTITION BY category, x).- RANGE frames (constrained to single ORDER BY when using offsets) — the resolver must copy frame mode correctly.
- CUMULATIVE shorthand — it's a new framing mode in this PR.
These can be gated behind probability checks just like the existing ROWS path.
core/src/test/java/io/questdb/test/griffin/engine/window/WindowFunctionTest.java (4)
10415-11769: Test methods are not alphabetically sorted.The new methods are organized thematically (basic → partition → frame → errors → CTEs → …). The coding guideline requires alphabetical ordering within the same member kind. For example,
testInlineOverWithTableAlias(Line 11500) andtestInlineWindowOrderByNotInSelect(Line 10733) are scattered amongtestMultiple*andtestNamedWindow*methods.Insert each new test in the correct alphabetical position among existing test methods in the class.
As per coding guidelines, "Group Java class members by kind and sort alphabetically. When adding new methods or fields, insert them in the correct alphabetical position among existing members of the same kind."
11631-11648: Inconsistent use ofassertExceptionvs.assertExceptionNoLeakCheck.Every other error test in this batch (
testNamedWindowUndefinedError,testNamedWindowDuplicateNameError,testNamedWindowKeywordAsName, etc.) usesassertExceptionNoLeakCheck, but this test usesassertException. If the distinction is intentional (e.g., the error path here needs leak verification because tables are actually opened), a brief comment would help; otherwise, align with the rest.
11198-11239: Consider adding a few more parser edge-case tests.The validation edge cases are solid (numbers, special characters, keywords, quoted keywords). A few scenarios recommended in the PR review discussion are still absent:
- Unicode identifiers as window names (e.g.,
OVER wïndow).- Very long names — stress the identifier length limit.
- Empty quoted name —
OVER ""orWINDOW "" AS (...).These would harden the
validateIdentifier()path end-to-end.
10423-10435: Consider using text blocks for multi-line expected-result and SQL strings.This applies to all new test methods in this batch. For example:
Illustrative diff (one test; apply pattern throughout)
// Basic named window - empty window (whole table as partition) assertQueryNoLeakCheck( - "x\ty\trow_num\n" + - "1\t10\t1\n" + - "2\t20\t2\n" + - "3\t30\t3\n", - "SELECT x, y, row_number() OVER w as row_num " + - "FROM t " + - "WINDOW w AS ()", + """ + x\ty\trow_num + 1\t10\t1 + 2\t20\t2 + 3\t30\t3 + """, + """ + SELECT x, y, row_number() OVER w as row_num \ + FROM t \ + WINDOW w AS ()\ + """, null, false, // window functions don't support random access trueThe existing file likely uses the old style throughout, so this may be better as a follow-up cleanup across the whole file rather than just the new tests.
As per coding guidelines, "Use multiline string literals in Java".
core/src/test/java/io/questdb/test/griffin/engine/window/WindowFunctionTest.java
Show resolved
Hide resolved
- Unquote window names at storage time in both ExpressionParser (OVER clause) and SqlParser (WINDOW clause) so quoted and unquoted names resolve correctly - Add fail-fast validation of named window references in SqlParser before the optimizer runs, catching undefined window names at parse time with proper error positions - Handle nested window function named references in both parser validation and optimizer resolution (e.g. sum(row_number() OVER w)) - Extract resolveNamedWindowReference and resolveNamedWindowsInExpr helpers in SqlOptimiser for clarity - Add isWindowKeyword case-insensitivity test to SqlKeywordsTest - Add tests for quoted name resolution, nested window functions, unicode names, and long window names - Reorder testWindowName* methods alphabetically in SqlParserTest - Rename testNamedWindowMixedWithInline to testNamedWindowCumulativeSum
- Move WINDOW clause before ORDER BY in parser (SQL standard ordering) - Fix double-normalization bug by removing putAll that shared WindowExpression references across wrapper models - Extract normalizeWindowFrame helper to eliminate duplicated switch blocks - Add window inheritance detection with clear error message - Improve WINDOW AS (missing name) error message - Make parseWindowSpec package-private - Fix copySpecFrom comment precision - Add lookupNamedWindow scope comment - Add RANGE frame generation to NamedWindowFuzzTest - Add tests for WINDOW before ORDER BY, EXCLUDE CURRENT ROW, and window inheritance error - Restore intent comments on testWindowName* methods in SqlParserTest
RANGE with SECOND PRECEDING in the fuzz test can trigger transient failures due to edge cases with avg() frame boundary computation. Restrict RANGE generation to UNBOUNDED PRECEDING AND CURRENT ROW which is the well-tested default RANGE behavior.
Replace \u7A97\u53E3 with literal 窗口 to fix Qodana UnnecessaryUnicodeEscape warnings.
|
@CodeRabbit, all issues have been addressed and we added more tests. Please review |
|
I'll examine the latest code to verify:
Triggering a fresh review now. 🧠 Learnings used✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
core/src/main/java/io/questdb/griffin/ExpressionParser.java (1)
688-697:⚠️ Potential issue | 🟡 MinorMissing validation for invalid frame:
BETWEEN ... FOLLOWING AND CURRENT ROW.The frame bound order validation at lines 690–697 checks when the frame end is
PRECEDING, catching invalid start positions. However, it doesn't validate the case where the frame end isCURRENTand the start isFOLLOWING, which is also invalid—the frame would start after it ends. A query likeBETWEEN 1 FOLLOWING AND CURRENT ROWwould parse successfully but represents an invalid window frame specification.Add the missing check:
Proposed fix
if (hiKind == WindowExpression.PRECEDING) { if (loKind == WindowExpression.CURRENT) { throw SqlException.$(windowCol.getRowsHiKindPos(), "frame starting from CURRENT ROW must end with CURRENT ROW or FOLLOWING"); } if (loKind == WindowExpression.FOLLOWING) { throw SqlException.$(windowCol.getRowsHiKindPos(), "frame starting from FOLLOWING must end with FOLLOWING"); } } + if (hiKind == WindowExpression.CURRENT && loKind == WindowExpression.FOLLOWING) { + throw SqlException.$(windowCol.getRowsHiKindPos(), "frame starting from FOLLOWING must end with FOLLOWING"); + }core/src/main/java/io/questdb/griffin/SqlParser.java (1)
4665-4692:⚠️ Potential issue | 🟡 MinorMove
validateIdentifierinto the static helper block (member grouping/order).
It’s a static helper but currently sits among instance methods; please relocate it near the other static helpers (e.g., afterrecursiveReplace) to keep kind grouping/alphabetical order.♻️ Proposed relocation
@@ public static ExpressionNode recursiveReplace(ExpressionNode node, ReplacingVisitor visitor) throws SqlException { if (node == null) { return null; } @@ return visitor.visit(node); } + + static void validateIdentifier(GenericLexer lexer, CharSequence tok) throws SqlException { + if (tok == null || tok.isEmpty()) { + throw SqlException.position(lexer.lastTokenPosition()).put("non-empty identifier expected"); + } + + if (Chars.isQuoted(tok)) { + if (tok.length() == 2) { + throw SqlException.position(lexer.lastTokenPosition()).put("non-empty identifier expected"); + } + return; + } + + char c = tok.charAt(0); + + if (!(Character.isLetter(c) || c == '_')) { + throw SqlException.position(lexer.lastTokenPosition()).put("identifier should start with a letter or '_'"); + } + + for (int i = 1, n = tok.length(); i < n; i++) { + c = tok.charAt(i); + if (!(Character.isLetter(c) || + Character.isDigit(c) || + c == '_' || + c == '$')) { + throw SqlException.position(lexer.lastTokenPosition()).put("identifier can contain letters, digits, '_' or '$'"); + } + } + } @@ - static void validateIdentifier(GenericLexer lexer, CharSequence tok) throws SqlException { - if (tok == null || tok.isEmpty()) { - throw SqlException.position(lexer.lastTokenPosition()).put("non-empty identifier expected"); - } - - if (Chars.isQuoted(tok)) { - if (tok.length() == 2) { - throw SqlException.position(lexer.lastTokenPosition()).put("non-empty identifier expected"); - } - return; - } - - char c = tok.charAt(0); - - if (!(Character.isLetter(c) || c == '_')) { - throw SqlException.position(lexer.lastTokenPosition()).put("identifier should start with a letter or '_'"); - } - - for (int i = 1, n = tok.length(); i < n; i++) { - c = tok.charAt(i); - if (!(Character.isLetter(c) || - Character.isDigit(c) || - c == '_' || - c == '$')) { - throw SqlException.position(lexer.lastTokenPosition()).put("identifier can contain letters, digits, '_' or '$'"); - } - } - }As per coding guidelines "Java class members are grouped by kind and sorted alphabetically. When adding new methods or fields, insert them in the correct alphabetical position among existing members of the same kind".
🤖 Fix all issues with AI agents
In `@core/src/main/java/io/questdb/griffin/ExpressionParser.java`:
- Around line 774-796: In ExpressionParser (around the window inheritance
detection block using variables tok, nextTok and lexer), add a check for
SqlKeywords.isCumulativeKeyword(nextTok) to the conditional that determines
whether the following token indicates inheritance (the condition that currently
tests nextTok for ')' or PARTITION/ORDER/ROWS/RANGE/GROUPS); this mirrors the
earlier isCumulativeKeyword(tok) check and ensures constructs like "WINDOW w2 AS
(w1 CUMULATIVE)" are correctly detected and cause the SqlException for
unsupported window inheritance.
In `@core/src/main/java/io/questdb/griffin/SqlOptimiser.java`:
- Around line 6186-6247: The three new private helper methods are out of
alphabetical order; reorder them so members are grouped and sorted: place
resolveNamedWindowReference(...) first, then resolveNamedWindows(...), then
resolveNamedWindowsInExpr(...); keep each method's signature, visibility, throws
clause and internal logic unchanged and ensure any calls (e.g.,
resolveNamedWindowReference from resolveNamedWindows and
resolveNamedWindowsInExpr) still compile after moving.
- Around line 4142-4154: The lookupNamedWindow method currently walks into
nested models without stopping at subquery boundaries; update
lookupNamedWindow(QueryModel model, CharSequence windowName) to check the
nested-model boundary by calling isNestedModelIsSubQuery() on the current model
and break the loop if it returns true (i.e., do not set current =
current.getNestedModel() when the nested model is a subquery), so outer queries
cannot resolve window names defined only in FROM subqueries; also reorder the
private method so lookupNamedWindow appears before makeJoinAlias to satisfy the
alphabetical placement guideline.
In `@core/src/main/java/io/questdb/griffin/SqlParser.java`:
- Around line 4694-4728: The validator currently only calls
validateNamedWindowReferencesInExpr when the top-level QueryColumn is a
WindowExpression, so expressions embedded in non-window columns (e.g.,
row_number() OVER w + 1) are not traversed; update validateNamedWindowReferences
to always call validateNamedWindowReferencesInExpr(qc.getAst(), namedWindows)
for every QueryColumn qc in columns (regardless of qc.isWindowExpression()), and
keep or adjust the existing top-level WindowExpression check to avoid
duplicate/contradictory error logic while using namedWindows and window name
position from qc.getAst().
In
`@core/src/test/java/io/questdb/test/griffin/engine/window/WindowFunctionTest.java`:
- Around line 10978-11000: The test method testNamedWindowWithSubqueryAndJoin is
misnamed and creates/populates table t2 which is never used; rename the test to
testNamedWindowWithSubqueryProjection (or similar) and remove the unused
create/insert calls for t2 and its rows (remove execute("create table t2...")
and the two execute("insert into t2...") lines) so the test only sets up t1 and
runs the subquery-window assertion against WINDOW w in the SELECT; alternatively
if you prefer to keep the original name, modify the SQL to actually join t2
(e.g., reference t2 in the FROM/Subquery and adjust expected results) but pick
one consistent change and apply it to the testNamedWindowWithSubqueryAndJoin
method and its assertions.
🧹 Nitpick comments (5)
core/src/test/java/io/questdb/test/griffin/engine/window/NamedWindowFuzzTest.java (2)
40-63: Static constants are not in alphabetical order.Per coding guidelines, class members of the same kind should be sorted alphabetically. The current order is
AGGREGATE_FUNCTIONS,RANGE_COMPATIBLE_FUNCTIONS,ITERATIONS,ORDER_COLUMNS,PARTITION_COLUMNS. Expected:AGGREGATE_FUNCTIONS,ITERATIONS,ORDER_COLUMNS,PARTITION_COLUMNS,RANGE_COMPATIBLE_FUNCTIONS.Suggested reordering
private static final String[] AGGREGATE_FUNCTIONS = { "sum(x)", "avg(x)", "count(x)", "count(*)", "max(x)", "min(x)", "row_number()", "rank()", "dense_rank()", }; - // Functions that work with RANGE frames (excludes row_number/rank/dense_rank) - private static final String[] RANGE_COMPATIBLE_FUNCTIONS = { - "sum(x)", - "avg(x)", - "count(x)", - "count(*)", - "max(x)", - "min(x)", - }; private static final int ITERATIONS = 200; private static final String[] ORDER_COLUMNS = {"ts", "x"}; private static final String[] PARTITION_COLUMNS = {"category", "x"}; + // Functions that work with RANGE frames (excludes row_number/rank/dense_rank) + private static final String[] RANGE_COMPATIBLE_FUNCTIONS = { + "sum(x)", + "avg(x)", + "count(x)", + "count(*)", + "max(x)", + "min(x)", + }; private Rnd rnd;As per coding guidelines: "Java class members are grouped by kind and sorted alphabetically."
193-235: Theboolean[]out-parameter is functional but could be cleaner.Using
boolean[]as an out-parameter to communicate the range-frame choice back to the caller is a bit unidiomatic. A small inner record or a dedicated return type (e.g.,record WindowSpec(String spec, boolean isRange)) would improve readability. Low priority given this is test code.core/src/test/java/io/questdb/test/griffin/engine/window/WindowFunctionTest.java (3)
10415-10464: Solid foundational test — minor overlap withtestNamedWindowCumulativeSumandtestNamedWindowEmptySpec.The three-assertion structure (empty window → inline baseline → named window with ORDER BY) is a clean progression. However, the first assertion (empty named window with
row_number()) duplicatestestNamedWindowEmptySpec(Line 10604), and the third assertion (cumulativesum(x)) is nearly identical totestNamedWindowCumulativeSum(Line 10628). Consider consolidating to reduce redundancy.
11709-11726: Inconsistent use ofassertExceptionvsassertExceptionNoLeakCheck.Every other error test in this batch uses
assertExceptionNoLeakCheck, but this test usesassertException. If the distinction is intentional (e.g., the WINDOW JOIN path allocates resources differently), a comment explaining why would help. Otherwise, switch toassertExceptionNoLeakCheckfor consistency.Proposed fix
- assertException( + assertExceptionNoLeakCheck( "SELECT price, bid, avg(price) OVER w as avg_price " + "FROM trades " + "WINDOW JOIN quotes ON ts " +
10415-11909: Test methods are not sorted alphabetically.The coding guideline requires Java class members to be sorted alphabetically within their kind. Currently the test methods are grouped thematically (e.g.
testMultipleNamedWindowsat Line 10546, thentestNamedWindow*variants, thentestInlineWindowOrderByNotInSelectat Line 10733, then back totestNamedWindow*, thentestInlineOverWithTableAliasat Line 11578, etc.). Re-sorting alphabetically would satisfy the guideline and avoid churn when future tests are inserted.As per coding guidelines, "Java class members are grouped by kind and sorted alphabetically. When adding new methods or fields, insert them in the correct alphabetical position among existing members of the same kind."
core/src/test/java/io/questdb/test/griffin/engine/window/WindowFunctionTest.java
Outdated
Show resolved
Hide resolved
- Add CUMULATIVE keyword to window inheritance detection - Add subquery boundary guard in lookupNamedWindow - Reorder lookupNamedWindow and resolveNamedWindow* methods alphabetically - Move validateNamedWindowReferencesInExpr outside isWindowExpression check - Rename testNamedWindowWithSubqueryAndJoin to testNamedWindowWithSubqueryProjection
|
@CodeRabbit, thanks for the review. Addressed your concerns |
|
Tip For best results, initiate chat on the files or code changes.
Coverage gains:
Key fixes verified: The feature is now production-ready. Great job addressing all the concerns thoroughly! 🧠 Learnings usedIf you found this review helpful, would you consider giving us a shout-out on X? Thank you for using CodeRabbit! |
Allow a named window definition to reference another named window as its
base, inheriting PARTITION BY, ORDER BY, and frame clauses per the SQL
standard. For example:
SELECT avg(price) OVER w2
FROM trades
WINDOW w1 AS (PARTITION BY symbol ORDER BY ts),
w2 AS (w1 ROWS BETWEEN 1 PRECEDING AND CURRENT ROW)
Model: Added baseWindowName/baseWindowNamePosition fields to
WindowExpression to store the referenced base window name.
Parser: ExpressionParser.parseWindowSpec() now detects a base window
reference (non-keyword identifier at the start of a window spec) and
stores it instead of throwing an error. PARTITION BY in a child window
that references a base is rejected per the SQL standard.
Validation: SqlParser validates at parse time that the base window is
defined earlier in the same WINDOW clause, preventing forward references
and self-references.
Optimizer: SqlOptimiser.resolveWindowInheritance() runs before
resolveNamedWindows() and merges base window specs into child windows.
Supports chained inheritance (w3 -> w2 -> w1) with recursive resolution
and defensive cycle detection.
Merge rules follow the SQL standard:
- PARTITION BY: always inherited from base (child cannot specify its own)
- ORDER BY: child's takes precedence if specified, otherwise inherited
- Frame: child's takes precedence if non-default, otherwise inherited
Co-Authored-By: Claude Opus 4.6 <[email protected]>
[PR Coverage check]😍 pass : 324 / 357 (90.76%) file detail
|
Adding docs for questdb/questdb#6746 --------- Co-authored-by: bluestreak <[email protected]> Co-authored-by: Claude Opus 4.6 <[email protected]>
Adding WINDOW definition for reusable window clauses in a single query
This PR goes in tandem with the docs at questdb/documentation#361
So you can do things like:
Window Inheritance
This PR also implements SQL standard window inheritance, where a named window
can reference another named window as its base, inheriting PARTITION BY, ORDER BY,
and frame clauses:
Chained inheritance is supported:
Merge rules follow the SQL standard:
Validation:
Test plan