Skip to content

Commit cea2c7d

Browse files
MagicalTuxclaude
andcommitted
fix(exec): eagerly validate window function args, FILTER and WHERE too
Extends the window `OVER` column validator to a complete window-query column check. In addition to each window spec's `PARTITION BY` / `ORDER BY`, it now resolves the projection expressions (a window function's arguments and its `FILTER` predicate are visited by the shallow-column walker), the `WHERE`, and each join `ON` against the base source columns, plus `GROUP BY` / `HAVING` / the query's top-level `ORDER BY` with the usual output-alias exemption for a bare name. So `sum(zzz) OVER ()`, `sum(a) FILTER(WHERE zzz > 0) OVER ()`, and a bad `WHERE` column in a window query are now rejected at prepare time — matching SQLite — even over an empty or filtered table, closing the last window-query lazy-validation gaps. Verified vs sqlite3 3.50.4 (`tests/eager_window_over_columns.rs`). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 774e996 commit cea2c7d

2 files changed

Lines changed: 94 additions & 7 deletions

File tree

src/exec/mod.rs

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21104,12 +21104,49 @@ impl Connection {
2110421104
for t in &sel.order_by {
2110521105
gather(&t.expr, &mut specs);
2110621106
}
21107-
let mut parts: Vec<&Expr> = Vec::new();
21107+
// Base-column targets with NO output-alias exemption: the projection exprs
21108+
// (`walk_shallow_columns` visits a window function's arguments and `FILTER`
21109+
// predicate), `WHERE`, each join `ON`, and every window spec's
21110+
// `PARTITION BY` / `ORDER BY` (which never bind to an output alias).
21111+
let mut strict: Vec<&Expr> = Vec::new();
21112+
for rc in &sel.columns {
21113+
if let ResultColumn::Expr { expr, .. } = rc {
21114+
strict.push(expr);
21115+
}
21116+
}
21117+
if let Some(w) = &sel.where_clause {
21118+
strict.push(w);
21119+
}
21120+
for j in &from.joins {
21121+
if let Some(on) = &j.on {
21122+
strict.push(on);
21123+
}
21124+
}
2110821125
for spec in &specs {
21109-
windowspec_parts(spec, &mut parts);
21126+
windowspec_parts(spec, &mut strict);
21127+
}
21128+
// `GROUP BY` / `HAVING` / the query's top-level `ORDER BY` may name an
21129+
// output alias with a bare identifier, which is not a base column.
21130+
let aliases: Vec<&str> = sel
21131+
.columns
21132+
.iter()
21133+
.filter_map(|c| match c {
21134+
ResultColumn::Expr { alias: Some(a), .. } => Some(a.as_str()),
21135+
_ => None,
21136+
})
21137+
.collect();
21138+
let mut clause_refs: Vec<&Expr> = Vec::new();
21139+
for g in &sel.group_by {
21140+
clause_refs.push(g);
21141+
}
21142+
if let Some(h) = &sel.having {
21143+
clause_refs.push(h);
21144+
}
21145+
for t in &sel.order_by {
21146+
clause_refs.push(&t.expr);
2111021147
}
2111121148
let mut missing: Option<Error> = None;
21112-
for e in parts {
21149+
for e in strict {
2111321150
if missing.is_some() {
2111421151
break;
2111521152
}
@@ -21119,6 +21156,22 @@ impl Connection {
2111921156
}
2112021157
});
2112121158
}
21159+
for e in clause_refs {
21160+
if missing.is_some() {
21161+
break;
21162+
}
21163+
walk_shallow_columns(e, &mut |schema, table, column, quoted| {
21164+
if missing.is_some() {
21165+
return;
21166+
}
21167+
if table.is_none() && aliases.iter().any(|a| a.eq_ignore_ascii_case(column)) {
21168+
return;
21169+
}
21170+
if !column_resolves_scoped(columns, schema, table, column) {
21171+
missing = Some(eval::no_such_column(schema, table, column, quoted));
21172+
}
21173+
});
21174+
}
2112221175
match missing {
2112321176
Some(e) => Err(e),
2112421177
None => Ok(()),

tests/eager_window_over_columns.rs

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,21 +40,55 @@ fn window_over_column_validation_matches_sqlite() {
4040
// Bad column in an OVER clause → rejected (empty table, so caught eagerly).
4141
(empty, "SELECT sum(a) OVER (PARTITION BY zzz) FROM t"),
4242
(empty, "SELECT sum(a) OVER (ORDER BY zzz) FROM t"),
43-
(empty, "SELECT sum(a) OVER (PARTITION BY a ORDER BY zzz) FROM t"),
44-
(empty, "SELECT sum(a) OVER w FROM t WINDOW w AS (PARTITION BY zzz)"),
43+
(
44+
empty,
45+
"SELECT sum(a) OVER (PARTITION BY a ORDER BY zzz) FROM t",
46+
),
47+
(
48+
empty,
49+
"SELECT sum(a) OVER w FROM t WINDOW w AS (PARTITION BY zzz)",
50+
),
4551
(empty, "SELECT 1 + sum(a) OVER (PARTITION BY zzz) FROM t"),
4652
// An output alias is NOT a valid window partition/order term.
4753
(full, "SELECT a AS x, sum(b) OVER (PARTITION BY x) FROM t"),
54+
// A bad column in a window function's argument, its FILTER, or the WHERE
55+
// of a window query is likewise rejected at prepare time.
56+
(empty, "SELECT sum(zzz) OVER () FROM t"),
57+
(empty, "SELECT sum(a) FILTER(WHERE zzz > 0) OVER () FROM t"),
58+
(
59+
empty,
60+
"SELECT count(*) OVER (PARTITION BY a ORDER BY b), sum(zzz) OVER () FROM t",
61+
),
62+
(
63+
empty,
64+
"SELECT a FROM t WHERE zzz > 0 AND sum(a) OVER () > 0",
65+
),
4866
// Valid — must not be a false positive.
49-
(full, "SELECT sum(a) OVER (PARTITION BY b ORDER BY a) FROM t"),
67+
(
68+
full,
69+
"SELECT sum(a) OVER (PARTITION BY b ORDER BY a) FROM t",
70+
),
5071
(full, "SELECT sum(a) OVER (PARTITION BY a + 0) FROM t"),
5172
(full, "SELECT sum(b) OVER (PARTITION BY rowid) FROM t"),
5273
(full, "SELECT sum(b) OVER w FROM t WINDOW w AS (ORDER BY a)"),
5374
(
5475
full,
5576
"SELECT t.a, sum(d) OVER (PARTITION BY t.b) FROM t JOIN u ON t.a = u.c",
5677
),
57-
(full, "SELECT row_number() OVER (ORDER BY a), a FROM t ORDER BY a"),
78+
(
79+
full,
80+
"SELECT row_number() OVER (ORDER BY a), a FROM t ORDER BY a",
81+
),
82+
(full, "SELECT sum(a) FILTER(WHERE b > 0) OVER () FROM t"),
83+
(
84+
full,
85+
"SELECT a AS x, sum(b) OVER (ORDER BY a) FROM t ORDER BY x",
86+
),
87+
(full, "SELECT sum(a + b) OVER () FROM t"),
88+
(
89+
full,
90+
"SELECT a, row_number() OVER (ORDER BY b) AS rn FROM t ORDER BY rn",
91+
),
5892
];
5993
for (base, q) in cases {
6094
assert_eq!(

0 commit comments

Comments
 (0)