fix(sql): fix alias hiding original column name in CASE with window function#6775
fix(sql): fix alias hiding original column name in CASE with window function#6775bluestreak01 merged 4 commits intomasterfrom
Conversation
When an operation expression (e.g., CASE/WHEN) contains child window functions, emitWindowFunctions() extracts the window calls but previously did not emit the remaining non-window column literals to the translating model. This caused "invalid column" errors when a column was referenced by its original name while also being aliased in the SELECT list (e.g., `price AS p` then `THEN price`). Introduce replaceWindowFunctionOrLiteral() which first tries to replace a node as a window function, then falls back to replaceLiteral to emit column references through the model chain. This is called during the emitWindowFunctions tree traversal instead of the bare replaceIfWindowFunction, so all literals are propagated in a single pass without a separate emitLiterals call. Fixes #6769. Co-Authored-By: Claude Opus 4.6 <[email protected]>
WalkthroughThe pull request fixes an issue where aliases hide original column names in CASE expressions with window functions. A new helper method Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
⚔️ Resolve merge conflicts (beta)
No actionable comments were generated in the recent review. 🎉 Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ast-grep (0.40.5)core/src/main/java/io/questdb/griffin/SqlOptimiser.javacore/src/test/java/io/questdb/test/griffin/engine/window/WindowFunctionTest.javaThanks 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 |
When a CASE expression mixes window functions with plain column references in THEN/ELSE branches, replaceWindowFunctionOrLiteral correctly added those column literals to translatingModel and innerVirtualModel via replaceLiteral. However, windowModel — which sits between innerVirtualModel and outerVirtualModel in the model chain — was never given a passthrough column. This caused "Invalid column" errors when the referenced column was not separately selected (e.g. SELECT ts, CASE WHEN price > lag(price) OVER (...) THEN price END FROM trades). The fix adds a passthrough column to windowModel whenever replaceLiteral successfully processes a literal, mirroring what createSelectColumn already does for standalone SELECT columns. The existing tests passed only because the column happened to be independently selected (price AS p), which added it to windowModel via createSelectColumn. A new test covers the case where the column appears exclusively inside the CASE expression. Co-Authored-By: Claude Opus 4.6 <[email protected]>
[PR Coverage check]😍 pass : 12 / 12 (100.00%) file detail
|
|
@CodeRabbit review critically |
🧩 Analysis chain🏁 Script executed: Length of output: 287553
🏁 Script executed: Length of output: 1412
What looks good
Push-back and risks
I recommend adding these to WindowFunctionTest.java: `@Test`
public void testCaseWhenNoElseWithWindow() throws Exception {
assertMemoryLeak(() -> {
execute("CREATE TABLE t(ts TIMESTAMP, price DOUBLE) TIMESTAMP(ts)");
execute("INSERT INTO t VALUES ('2024-01-01', 100.0), ('2024-01-02', 110.0)");
// No ELSE, column only in THEN
assertQueryNoLeakCheck(
"ts\tp\tcase\n" +
"2024-01-01T00:00:00.000000Z\t100.0\tnull\n" +
"2024-01-02T00:00:00.000000Z\t110.0\t110.0\n",
"SELECT ts, price AS p,\n" +
" CASE WHEN price > lag(price) OVER (ORDER BY ts) THEN price END\n" +
"FROM t",
"ts", false, true
);
});
}
`@Test`
public void testCaseWithAliasOnlyNoBaseColumnSelected() throws Exception {
assertMemoryLeak(() -> {
execute("CREATE TABLE trades (ts TIMESTAMP, price DOUBLE) TIMESTAMP(ts)");
execute(\"\"\"\n" +
"INSERT INTO trades VALUES\n" +
" ('2024-01-01', 100.0),\n" +
" ('2024-01-02', 110.0),\n" +
" ('2024-01-03', 90.0)\n" +
"\"\"\");
assertQueryNoLeakCheck(
"ts\tp\tcase\n" +
"2024-01-01T00:00:00.000000Z\t100.0\tnull\n" +
"2024-01-02T00:00:00.000000Z\t110.0\t110.0\n" +
"2024-01-03T00:00:00.000000Z\t90.0\t-90.0\n",
"SELECT ts, price AS p,\n" +
" CASE\n" +
" WHEN p > lag(p) OVER (ORDER BY ts) THEN p\n" +
" WHEN p < lag(p) OVER (ORDER BY ts) THEN -p\n" +
" END\n" +
"FROM trades",
"ts", false, true
);
});
}And for join ambiguity (expected failure): `@Test`
public void testCaseAmbiguousColumnInJoinWithWindow() {
// Expect invalid/ambiguous column error when unqualified "price" appears with joins
assertException(
"SELECT ts,\n" +
" CASE WHEN price > lag(price) OVER (ORDER BY ts) THEN price END\n" +
"FROM t1 JOIN t2 ON t1.ts = t2.ts",
0, // adjust to the position where 'price' first appears
"ambiguous column" // or the exact message your SqlException emits
);
}Small code hardening suggestion
Documentation trace
Overall recommendation
If you’d like, I can run an additional grep to confirm the single-definition now. 🧠 Learnings used✅ Actions performedReview triggered.
|
Fixes #6769
Summary
CASE/WHENexpression contains a window function (e.g.,lag() OVER (...)), column references outside the window function (inTHEN/ELSEbranches) were not emitted to the translating model. This caused "invalid column" errors when using the original column name alongside a SELECT alias (e.g.,price AS pthenTHEN price).replaceWindowFunctionOrLiteral()that chains window function replacement with literal emission in a single tree traversal pass, so all column references are propagated through the model chain.Test plan
testAliasedColumnVisibleByRealNameInCaseThenreproduces the exact scenario from the issue:SELECT ts, price AS p, CASE WHEN price > lag(price) OVER (...) THEN price END FROM tradesWindowFunctionTestsuite passes (CASE with window functions, lag/lead, partitioned windows, etc.)SqlParserTestsuite passes (model tree structure unchanged for existing queries)🤖 Generated with Claude Code