Skip to main content

vibesql_executor/select/executor/validation/
aggregates.rs

1//! Aggregate function validation
2//!
3//! Validates aggregate function usage including:
4//! - Argument count validation
5//! - Nested aggregate detection
6//! - Aliased aggregate misuse in HAVING clause
7
8use std::collections::HashSet;
9
10use vibesql_ast::{Expression, SelectItem};
11
12use crate::{errors::ExecutorError, schema::CombinedSchema};
13
14/// Check if a function name is an aggregate function
15pub fn is_aggregate_function(name: &str) -> bool {
16    let upper = name.to_uppercase();
17    matches!(upper.as_str(), "COUNT" | "SUM" | "AVG" | "MIN" | "MAX" | "TOTAL" | "GROUP_CONCAT")
18}
19
20/// Action taken at each node during a generic expression walk.
21///
22/// Used by [`walk_first`] to let callers control short-circuiting and
23/// recursion on a per-node basis.
24enum WalkAction<T> {
25    /// Stop the walk and return this value wrapped in `Some`.
26    Stop(T),
27    /// Skip this node entirely — do not descend into its children.
28    /// The walk continues with the next sibling.
29    Skip,
30    /// Descend into this node's children using the standard recursion.
31    Descend,
32}
33
34/// Walk an `Expression` tree and return the first `Some(T)` produced by the
35/// visitor, or `None` if no node matched.
36///
37/// The visitor `f` is called for every node before its children are visited.
38/// It returns a [`WalkAction`] that controls the walk:
39/// - [`WalkAction::Stop`] short-circuits with the wrapped value.
40/// - [`WalkAction::Skip`] skips this subtree (children are NOT visited).
41/// - [`WalkAction::Descend`] visits all of the node's children using the
42///   standard recursion defined here.
43///
44/// **Subtree skipping** is the mechanism callers use to opt out of certain
45/// scopes (e.g. `ScalarSubquery` / `Exists`, the OVER-clause of a
46/// `WindowFunction`, etc.). Any node that should be treated as a barrier
47/// should return [`WalkAction::Skip`] from the visitor.
48///
49/// The recursion descends into every child of every compound `Expression`
50/// variant. Variants with no child expressions (literals, column refs, etc.)
51/// terminate the walk on their branch.
52fn walk_first<T>(expr: &Expression, f: &impl Fn(&Expression) -> WalkAction<T>) -> Option<T> {
53    match f(expr) {
54        WalkAction::Stop(t) => return Some(t),
55        WalkAction::Skip => return None,
56        WalkAction::Descend => {}
57    }
58
59    match expr {
60        // Compound expressions — recurse into children.
61        Expression::BinaryOp { left, right, .. } => {
62            walk_first(left, f).or_else(|| walk_first(right, f))
63        }
64        Expression::Conjunction(children) | Expression::Disjunction(children) => {
65            children.iter().find_map(|c| walk_first(c, f))
66        }
67        Expression::UnaryOp { expr, .. } => walk_first(expr, f),
68        Expression::Function { args, .. } => args.iter().find_map(|a| walk_first(a, f)),
69        Expression::AggregateFunction { args, order_by, filter, .. } => args
70            .iter()
71            .find_map(|a| walk_first(a, f))
72            .or_else(|| {
73                order_by
74                    .as_ref()
75                    .and_then(|items| items.iter().find_map(|item| walk_first(&item.expr, f)))
76            })
77            .or_else(|| filter.as_deref().and_then(|fexpr| walk_first(fexpr, f))),
78        Expression::IsNull { expr, .. } => walk_first(expr, f),
79        Expression::IsDistinctFrom { left, right, .. } => {
80            walk_first(left, f).or_else(|| walk_first(right, f))
81        }
82        Expression::IsTruthValue { expr, .. } => walk_first(expr, f),
83        Expression::Case { operand, when_clauses, else_result } => operand
84            .as_deref()
85            .and_then(|op| walk_first(op, f))
86            .or_else(|| {
87                when_clauses.iter().find_map(|w| {
88                    w.conditions
89                        .iter()
90                        .find_map(|c| walk_first(c, f))
91                        .or_else(|| walk_first(&w.result, f))
92                })
93            })
94            .or_else(|| else_result.as_deref().and_then(|e| walk_first(e, f))),
95        Expression::In { expr, .. } => walk_first(expr, f),
96        Expression::InList { expr, values, .. } => {
97            walk_first(expr, f).or_else(|| values.iter().find_map(|v| walk_first(v, f)))
98        }
99        Expression::Between { expr, low, high, .. } => {
100            walk_first(expr, f).or_else(|| walk_first(low, f)).or_else(|| walk_first(high, f))
101        }
102        Expression::Cast { expr, .. } => walk_first(expr, f),
103        Expression::Position { substring, string, .. } => {
104            walk_first(substring, f).or_else(|| walk_first(string, f))
105        }
106        Expression::Trim { removal_char, string, .. } => removal_char
107            .as_deref()
108            .and_then(|rc| walk_first(rc, f))
109            .or_else(|| walk_first(string, f)),
110        Expression::Extract { expr, .. } => walk_first(expr, f),
111        Expression::Like { expr, pattern, escape, .. }
112        | Expression::Glob { expr, pattern, escape, .. } => walk_first(expr, f)
113            .or_else(|| walk_first(pattern, f))
114            .or_else(|| escape.as_deref().and_then(|e| walk_first(e, f))),
115        Expression::QuantifiedComparison { expr, .. } => walk_first(expr, f),
116        Expression::Interval { value, .. } => walk_first(value, f),
117        Expression::WindowFunction { function, .. } => {
118            // By default, recurse into the function's arguments AND its
119            // FILTER clause (for Aggregate variants). The OVER clause is
120            // intentionally NOT visited here — the `WindowFunction` arm of
121            // every existing walker either ignores it or treats it as a
122            // separate scope. Callers that need OVER-clause inspection
123            // should match `WindowFunction` themselves and return
124            // `WalkAction::Skip` from the visitor.
125            match function {
126                vibesql_ast::WindowFunctionSpec::Aggregate { args, filter, .. } => args
127                    .iter()
128                    .find_map(|a| walk_first(a, f))
129                    .or_else(|| filter.as_deref().and_then(|fx| walk_first(fx, f))),
130                vibesql_ast::WindowFunctionSpec::Ranking { args, .. }
131                | vibesql_ast::WindowFunctionSpec::Value { args, .. } => {
132                    args.iter().find_map(|a| walk_first(a, f))
133                }
134            }
135        }
136        Expression::MatchAgainst { search_modifier, .. } => walk_first(search_modifier, f),
137        Expression::RowValueConstructor(children) => children.iter().find_map(|c| walk_first(c, f)),
138        Expression::Collate { expr, .. } => walk_first(expr, f),
139        Expression::Raise { error_message, .. } => {
140            error_message.as_deref().and_then(|msg| walk_first(msg, f))
141        }
142        // Leaf-like or scope-closing expressions. ScalarSubquery / Exists
143        // intentionally terminate the walk — most callers want subqueries
144        // treated as their own scope. Callers that need to recurse must
145        // handle these in the visitor.
146        Expression::ScalarSubquery(_)
147        | Expression::Exists { .. }
148        | Expression::Literal(_)
149        | Expression::Placeholder(_)
150        | Expression::NumberedPlaceholder(_)
151        | Expression::NamedPlaceholder(_)
152        | Expression::ColumnRef(_)
153        | Expression::Wildcard
154        | Expression::CurrentDate
155        | Expression::CurrentTime { .. }
156        | Expression::CurrentTimestamp { .. }
157        | Expression::Default
158        | Expression::DuplicateKeyValue { .. }
159        | Expression::NextValue { .. }
160        | Expression::PseudoVariable { .. }
161        | Expression::SessionVariable { .. } => None,
162    }
163}
164
165/// Check if an aggregate function has wrong number of arguments
166/// Returns Some((function_name, arg_count)) if there's an error, None otherwise
167pub fn check_aggregate_arg_count(expr: &Expression) -> Option<String> {
168    match expr {
169        Expression::AggregateFunction { name, args, distinct, .. } => {
170            let upper = name.to_uppercase();
171            let arg_count = args.len();
172
173            // Check for wildcard in non-COUNT aggregates
174            let has_wildcard = args.iter().any(|arg| {
175                let is_wildcard = matches!(arg, Expression::Wildcard);
176                let is_star_ref = matches!(
177                    arg,
178                    Expression::ColumnRef(col_id) if col_id.schema_canonical().is_none() && col_id.table_canonical().is_none() && col_id.column_canonical() == "*"
179                );
180                is_wildcard || is_star_ref
181            });
182
183            match upper.as_str() {
184                "COUNT" => {
185                    // Multi-arg COUNT without DISTINCT is an error
186                    // SQLite: "wrong number of arguments to function count()"
187                    if arg_count > 1 && !*distinct {
188                        Some(name.display().to_string())
189                    } else {
190                        None
191                    }
192                }
193                "MIN" | "MAX" => {
194                    if has_wildcard || arg_count == 0 {
195                        Some(name.display().to_string())
196                    } else {
197                        None
198                    }
199                }
200                "SUM" | "AVG" | "TOTAL" => {
201                    if has_wildcard || arg_count == 0 || arg_count > 1 {
202                        Some(name.display().to_string())
203                    } else {
204                        None
205                    }
206                }
207                "GROUP_CONCAT" => {
208                    if arg_count == 0 || arg_count > 2 {
209                        Some(name.display().to_string())
210                    } else {
211                        None
212                    }
213                }
214                _ => None,
215            }
216        }
217        Expression::Function { name, args, .. } => {
218            // Check if this is an aggregate function with wrong args
219            if is_aggregate_function(name.as_str()) {
220                let upper = name.to_uppercase();
221                let arg_count = args.len();
222
223                // Check for wildcard
224                let has_wildcard = args.iter().any(|arg| {
225                    matches!(arg, Expression::Wildcard)
226                        || matches!(
227                            arg,
228                            Expression::ColumnRef(col_id) if col_id.schema_canonical().is_none() && col_id.table_canonical().is_none() && col_id.column_canonical() == "*"
229                        )
230                });
231
232                match upper.as_str() {
233                    "COUNT" => {
234                        // count(a, b) without DISTINCT is wrong
235                        // Regular count without DISTINCT can only have 0-1 args
236                        if arg_count > 1 {
237                            Some(name.display().to_string())
238                        } else {
239                            None
240                        }
241                    }
242                    "MIN" | "MAX" => {
243                        // Multi-arg min/max are scalar, so only check single arg case
244                        if arg_count <= 1 && (has_wildcard || arg_count == 0) {
245                            Some(name.display().to_string())
246                        } else {
247                            None
248                        }
249                    }
250                    "SUM" | "AVG" | "TOTAL" => {
251                        if has_wildcard || arg_count == 0 || arg_count > 1 {
252                            Some(name.display().to_string())
253                        } else {
254                            None
255                        }
256                    }
257                    "GROUP_CONCAT" => {
258                        if arg_count == 0 || arg_count > 2 {
259                            Some(name.display().to_string())
260                        } else {
261                            None
262                        }
263                    }
264                    _ => None,
265                }
266            } else {
267                // Check function arguments recursively
268                for arg in args {
269                    if let Some(found) = check_aggregate_arg_count(arg) {
270                        return Some(found);
271                    }
272                }
273                None
274            }
275        }
276        Expression::BinaryOp { left, right, .. } => {
277            check_aggregate_arg_count(left).or_else(|| check_aggregate_arg_count(right))
278        }
279        Expression::UnaryOp { expr, .. } => check_aggregate_arg_count(expr),
280        Expression::Case { operand, when_clauses, else_result } => {
281            if let Some(op) = operand {
282                if let Some(found) = check_aggregate_arg_count(op) {
283                    return Some(found);
284                }
285            }
286            for case_when in when_clauses {
287                for cond in &case_when.conditions {
288                    if let Some(found) = check_aggregate_arg_count(cond) {
289                        return Some(found);
290                    }
291                }
292                if let Some(found) = check_aggregate_arg_count(&case_when.result) {
293                    return Some(found);
294                }
295            }
296            if let Some(else_expr) = else_result {
297                check_aggregate_arg_count(else_expr)
298            } else {
299                None
300            }
301        }
302        Expression::IsNull { expr, .. } => check_aggregate_arg_count(expr),
303        Expression::Cast { expr, .. } => check_aggregate_arg_count(expr),
304        Expression::Conjunction(children) | Expression::Disjunction(children) => {
305            for child in children {
306                if let Some(found) = check_aggregate_arg_count(child) {
307                    return Some(found);
308                }
309            }
310            None
311        }
312        _ => None,
313    }
314}
315
316/// Find the first aggregate function in an expression
317/// Returns the function name (original case preserved) if found, None otherwise
318pub fn find_aggregate_in_expression(expr: &Expression) -> Option<String> {
319    walk_first(expr, &|e| match e {
320        Expression::AggregateFunction { name, .. } => WalkAction::Stop(name.to_string()),
321        Expression::Function { name, args, .. } if is_aggregate_function(name.as_str()) => {
322            // Multi-arg MIN/MAX are scalar functions in SQLite, not aggregates.
323            let upper = name.to_uppercase();
324            if matches!(upper.as_str(), "MIN" | "MAX") && args.len() > 1 {
325                WalkAction::Descend
326            } else {
327                WalkAction::Stop(name.to_string())
328            }
329        }
330        // Window functions are not regular aggregates — skip them entirely.
331        Expression::WindowFunction { .. } => WalkAction::Skip,
332        // The original walker treated these variants as leaves; preserve
333        // that behavior here.
334        Expression::Glob { .. }
335        | Expression::RowValueConstructor(_)
336        | Expression::Collate { .. } => WalkAction::Skip,
337        _ => WalkAction::Descend,
338    })
339}
340
341/// Find nested aggregate function in an expression
342///
343/// A nested aggregate is when one aggregate's arguments contain another aggregate,
344/// e.g., `SUM(MIN(x))`. This is invalid in SQL.
345///
346/// Returns Some(inner_aggregate_name) if found, None otherwise.
347pub fn find_nested_aggregate(expr: &Expression) -> Option<String> {
348    match expr {
349        Expression::AggregateFunction { args, order_by, .. } => {
350            // Check if any argument contains an aggregate function
351            for arg in args {
352                if let Some(inner_name) = find_aggregate_in_expression(arg) {
353                    return Some(inner_name);
354                }
355            }
356            // Also check ORDER BY expressions for aggregate functions
357            // e.g., group_concat(a ORDER BY max(d)) is invalid
358            if let Some(order_items) = order_by {
359                for item in order_items {
360                    if let Some(inner_name) = find_aggregate_in_expression(&item.expr) {
361                        return Some(inner_name);
362                    }
363                }
364            }
365            None
366        }
367        Expression::Function { name, args, .. } => {
368            // Check if this function is a built-in aggregate with nested aggregate args
369            if is_aggregate_function(name.as_str()) {
370                let upper = name.to_uppercase();
371                // Multi-arg MIN/MAX are scalar functions, not aggregates
372                let is_scalar_minmax = matches!(upper.as_str(), "MIN" | "MAX") && args.len() > 1;
373                if !is_scalar_minmax {
374                    // This is an aggregate - check for nested aggregates in args
375                    for arg in args {
376                        if let Some(inner_name) = find_aggregate_in_expression(arg) {
377                            return Some(inner_name);
378                        }
379                    }
380                }
381            }
382            // Check arguments recursively (for non-aggregate functions)
383            for arg in args {
384                if let Some(found) = find_nested_aggregate(arg) {
385                    return Some(found);
386                }
387            }
388            None
389        }
390        Expression::BinaryOp { left, right, .. } => {
391            find_nested_aggregate(left).or_else(|| find_nested_aggregate(right))
392        }
393        Expression::UnaryOp { expr, .. } => find_nested_aggregate(expr),
394        Expression::Cast { expr, .. } => find_nested_aggregate(expr),
395        Expression::Case { operand, when_clauses, else_result } => {
396            if let Some(op) = operand {
397                if let Some(found) = find_nested_aggregate(op) {
398                    return Some(found);
399                }
400            }
401            for case_when in when_clauses {
402                for cond in &case_when.conditions {
403                    if let Some(found) = find_nested_aggregate(cond) {
404                        return Some(found);
405                    }
406                }
407                if let Some(found) = find_nested_aggregate(&case_when.result) {
408                    return Some(found);
409                }
410            }
411            if let Some(else_expr) = else_result {
412                find_nested_aggregate(else_expr)
413            } else {
414                None
415            }
416        }
417        Expression::IsNull { expr, .. } => find_nested_aggregate(expr),
418        Expression::Between { expr, low, high, .. } => find_nested_aggregate(expr)
419            .or_else(|| find_nested_aggregate(low))
420            .or_else(|| find_nested_aggregate(high)),
421        Expression::InList { expr, values, .. } => {
422            if let Some(found) = find_nested_aggregate(expr) {
423                return Some(found);
424            }
425            for val in values {
426                if let Some(found) = find_nested_aggregate(val) {
427                    return Some(found);
428                }
429            }
430            None
431        }
432        Expression::Conjunction(children) | Expression::Disjunction(children) => {
433            for child in children {
434                if let Some(found) = find_nested_aggregate(child) {
435                    return Some(found);
436                }
437            }
438            None
439        }
440        _ => None,
441    }
442}
443
444/// Validate aggregate function argument counts in SELECT list
445///
446/// This validates that aggregate functions have the correct number of arguments:
447/// - MIN, MAX, SUM, AVG, TOTAL require exactly 1 argument (no wildcard)
448/// - COUNT allows 0-1 arguments (supports *)
449/// - GROUP_CONCAT requires 1-2 arguments
450///
451/// Returns an error with SQLite-compatible message if validation fails.
452pub fn validate_aggregate_arguments(select_list: &[SelectItem]) -> Result<(), ExecutorError> {
453    for item in select_list {
454        if let SelectItem::Expression { expr, .. } = item {
455            if let Some(agg_name) = check_aggregate_arg_count(expr) {
456                return Err(ExecutorError::WrongNumberOfArguments { function_name: agg_name });
457            }
458        }
459    }
460    Ok(())
461}
462
463/// Validate that there are no nested aggregate functions in the SELECT list
464///
465/// Nested aggregates like `SUM(MIN(x))` are invalid in SQL.
466/// Returns an error with SQLite-compatible message if nested aggregates are found.
467///
468/// Note: This uses the "misuse of aggregate function X()" format (with "function")
469/// as SQLite detects this during name resolution, not during execution.
470pub fn validate_no_nested_aggregates(select_list: &[SelectItem]) -> Result<(), ExecutorError> {
471    for item in select_list {
472        if let SelectItem::Expression { expr, .. } = item {
473            if let Some(inner_agg_name) = find_nested_aggregate(expr) {
474                return Err(ExecutorError::MisuseOfAggregate { function_name: inner_agg_name });
475            }
476        }
477    }
478    Ok(())
479}
480
481/// Build a set of aggregate alias names from the SELECT list
482///
483/// An "aggregate alias" is an alias that refers to an expression containing
484/// an aggregate function, e.g., `min(f1) AS m` makes `m` an aggregate alias.
485pub fn build_aggregate_aliases(select_list: &[SelectItem]) -> HashSet<String> {
486    let mut aliases = HashSet::new();
487
488    for item in select_list {
489        if let SelectItem::Expression { expr, alias: Some(alias_name), .. } = item {
490            // Check if this expression contains an aggregate
491            if expression_contains_aggregate(expr) {
492                // Store alias in lowercase for case-insensitive matching
493                aliases.insert(alias_name.to_lowercase());
494            }
495        }
496    }
497
498    aliases
499}
500
501/// Check if an expression contains an aggregate function
502pub fn expression_contains_aggregate(expr: &Expression) -> bool {
503    walk_first(expr, &|e| match e {
504        Expression::AggregateFunction { .. } => WalkAction::Stop(()),
505        Expression::Function { name, args, .. } if is_aggregate_function(name.as_str()) => {
506            // Multi-arg MIN/MAX are scalar functions — keep walking children
507            // (they may still contain a nested aggregate).
508            let upper = name.to_uppercase();
509            if matches!(upper.as_str(), "MIN" | "MAX") && args.len() > 1 {
510                WalkAction::Descend
511            } else {
512                WalkAction::Stop(())
513            }
514        }
515        // Window functions are not aggregates in this context — skip entirely.
516        Expression::WindowFunction { .. } => WalkAction::Skip,
517        // The original walker treated these variants as leaves
518        // (`_ => false`); preserve that by skipping them here.
519        Expression::Glob { .. }
520        | Expression::IsDistinctFrom { .. }
521        | Expression::IsTruthValue { .. }
522        | Expression::QuantifiedComparison { .. }
523        | Expression::RowValueConstructor(_)
524        | Expression::MatchAgainst { .. }
525        | Expression::Collate { .. } => WalkAction::Skip,
526        _ => WalkAction::Descend,
527    })
528    .is_some()
529}
530
531/// Find the first window function in an expression
532///
533/// Window functions can only appear in SELECT list and ORDER BY clauses.
534/// This function finds window functions in expressions where they are not allowed
535/// (e.g., WHERE, HAVING, GROUP BY, aggregate function arguments).
536///
537/// Returns the function name if found, None otherwise.
538pub fn find_window_function_in_expression(expr: &Expression) -> Option<String> {
539    walk_first(expr, &|e| match e {
540        Expression::WindowFunction { function, .. } => WalkAction::Stop(function.name()),
541        // The original walker treated these variants as leaves; preserve
542        // that behavior here.
543        Expression::Glob { .. }
544        | Expression::RowValueConstructor(_)
545        | Expression::Collate { .. } => WalkAction::Skip,
546        _ => WalkAction::Descend,
547    })
548}
549
550/// Check for misuse of aliased aggregates in HAVING clause
551///
552/// SQLite error: When an aggregate alias (e.g., `m` from `min(f1) AS m`) is used
553/// inside another aggregate function in the HAVING clause (e.g., `HAVING max(m) < 10`),
554/// it's an error. This function detects such misuse.
555///
556/// The `schema_columns` set contains column names from the actual table schema.
557/// If a column reference matches an actual table column, it's NOT a reference to an alias,
558/// even if an alias with the same name exists.
559///
560/// Returns Some(alias_name) if misuse is found, None otherwise.
561fn find_aliased_aggregate_misuse_in_expression(
562    expr: &Expression,
563    aggregate_aliases: &HashSet<String>,
564    schema_columns: &HashSet<String>,
565    inside_aggregate: bool,
566) -> Option<String> {
567    match expr {
568        // Check if this is an aggregate function - if so, mark that we're inside one
569        Expression::AggregateFunction { args, .. } => {
570            for arg in args {
571                if let Some(alias) = find_aliased_aggregate_misuse_in_expression(
572                    arg,
573                    aggregate_aliases,
574                    schema_columns,
575                    true,
576                ) {
577                    return Some(alias);
578                }
579            }
580            None
581        }
582        Expression::Function { name, args, .. } => {
583            // Check if this function is a built-in aggregate
584            let is_agg = is_aggregate_function(name.as_str());
585            let upper = name.to_uppercase();
586            // Multi-arg MIN/MAX are scalar functions
587            let effectively_aggregate =
588                is_agg && !(matches!(upper.as_str(), "MIN" | "MAX") && args.len() > 1);
589
590            let new_inside_aggregate = inside_aggregate || effectively_aggregate;
591
592            for arg in args {
593                if let Some(alias) = find_aliased_aggregate_misuse_in_expression(
594                    arg,
595                    aggregate_aliases,
596                    schema_columns,
597                    new_inside_aggregate,
598                ) {
599                    return Some(alias);
600                }
601            }
602            None
603        }
604        // Column reference - check if it's an aggregate alias used inside an aggregate
605        Expression::ColumnRef(col_id)
606            if col_id.schema_canonical().is_none() && col_id.table_canonical().is_none() =>
607        {
608            let column = col_id.column_canonical();
609            // If this column exists in the actual table schema, it's a real column reference,
610            // not a reference to a SELECT alias (even if an alias with the same name exists).
611            // Table columns take precedence over aliases in HAVING clause.
612            if schema_columns.contains(&column.to_lowercase()) {
613                return None; // Real column, not an alias reference
614            }
615
616            if inside_aggregate && aggregate_aliases.contains(&column.to_lowercase()) {
617                // Found misuse: aggregate alias used inside another aggregate
618                Some(column.to_string())
619            } else {
620                None
621            }
622        }
623        Expression::ColumnRef(_) => None, // Qualified refs can't be aliases
624        // Recursively check composite expressions
625        Expression::BinaryOp { left, right, .. } => find_aliased_aggregate_misuse_in_expression(
626            left,
627            aggregate_aliases,
628            schema_columns,
629            inside_aggregate,
630        )
631        .or_else(|| {
632            find_aliased_aggregate_misuse_in_expression(
633                right,
634                aggregate_aliases,
635                schema_columns,
636                inside_aggregate,
637            )
638        }),
639        Expression::UnaryOp { expr, .. } => find_aliased_aggregate_misuse_in_expression(
640            expr,
641            aggregate_aliases,
642            schema_columns,
643            inside_aggregate,
644        ),
645        Expression::Cast { expr, .. } => find_aliased_aggregate_misuse_in_expression(
646            expr,
647            aggregate_aliases,
648            schema_columns,
649            inside_aggregate,
650        ),
651        Expression::Case { operand, when_clauses, else_result } => {
652            if let Some(op) = operand {
653                if let Some(alias) = find_aliased_aggregate_misuse_in_expression(
654                    op,
655                    aggregate_aliases,
656                    schema_columns,
657                    inside_aggregate,
658                ) {
659                    return Some(alias);
660                }
661            }
662            for when_clause in when_clauses {
663                for cond in &when_clause.conditions {
664                    if let Some(alias) = find_aliased_aggregate_misuse_in_expression(
665                        cond,
666                        aggregate_aliases,
667                        schema_columns,
668                        inside_aggregate,
669                    ) {
670                        return Some(alias);
671                    }
672                }
673                if let Some(alias) = find_aliased_aggregate_misuse_in_expression(
674                    &when_clause.result,
675                    aggregate_aliases,
676                    schema_columns,
677                    inside_aggregate,
678                ) {
679                    return Some(alias);
680                }
681            }
682            if let Some(else_expr) = else_result {
683                return find_aliased_aggregate_misuse_in_expression(
684                    else_expr,
685                    aggregate_aliases,
686                    schema_columns,
687                    inside_aggregate,
688                );
689            }
690            None
691        }
692        Expression::IsNull { expr, .. } => find_aliased_aggregate_misuse_in_expression(
693            expr,
694            aggregate_aliases,
695            schema_columns,
696            inside_aggregate,
697        ),
698        Expression::Between { expr, low, high, .. } => find_aliased_aggregate_misuse_in_expression(
699            expr,
700            aggregate_aliases,
701            schema_columns,
702            inside_aggregate,
703        )
704        .or_else(|| {
705            find_aliased_aggregate_misuse_in_expression(
706                low,
707                aggregate_aliases,
708                schema_columns,
709                inside_aggregate,
710            )
711        })
712        .or_else(|| {
713            find_aliased_aggregate_misuse_in_expression(
714                high,
715                aggregate_aliases,
716                schema_columns,
717                inside_aggregate,
718            )
719        }),
720        Expression::InList { expr, values, .. } => {
721            if let Some(alias) = find_aliased_aggregate_misuse_in_expression(
722                expr,
723                aggregate_aliases,
724                schema_columns,
725                inside_aggregate,
726            ) {
727                return Some(alias);
728            }
729            for val in values {
730                if let Some(alias) = find_aliased_aggregate_misuse_in_expression(
731                    val,
732                    aggregate_aliases,
733                    schema_columns,
734                    inside_aggregate,
735                ) {
736                    return Some(alias);
737                }
738            }
739            None
740        }
741        Expression::In { expr, .. } => find_aliased_aggregate_misuse_in_expression(
742            expr,
743            aggregate_aliases,
744            schema_columns,
745            inside_aggregate,
746        ),
747        Expression::Like { expr, pattern, .. } => find_aliased_aggregate_misuse_in_expression(
748            expr,
749            aggregate_aliases,
750            schema_columns,
751            inside_aggregate,
752        )
753        .or_else(|| {
754            find_aliased_aggregate_misuse_in_expression(
755                pattern,
756                aggregate_aliases,
757                schema_columns,
758                inside_aggregate,
759            )
760        }),
761        Expression::Position { substring, string, .. } => {
762            find_aliased_aggregate_misuse_in_expression(
763                substring,
764                aggregate_aliases,
765                schema_columns,
766                inside_aggregate,
767            )
768            .or_else(|| {
769                find_aliased_aggregate_misuse_in_expression(
770                    string,
771                    aggregate_aliases,
772                    schema_columns,
773                    inside_aggregate,
774                )
775            })
776        }
777        Expression::Trim { removal_char, string, .. } => {
778            if let Some(rc) = removal_char {
779                if let Some(alias) = find_aliased_aggregate_misuse_in_expression(
780                    rc,
781                    aggregate_aliases,
782                    schema_columns,
783                    inside_aggregate,
784                ) {
785                    return Some(alias);
786                }
787            }
788            find_aliased_aggregate_misuse_in_expression(
789                string,
790                aggregate_aliases,
791                schema_columns,
792                inside_aggregate,
793            )
794        }
795        Expression::Extract { expr, .. } => find_aliased_aggregate_misuse_in_expression(
796            expr,
797            aggregate_aliases,
798            schema_columns,
799            inside_aggregate,
800        ),
801        Expression::Interval { value, .. } => find_aliased_aggregate_misuse_in_expression(
802            value,
803            aggregate_aliases,
804            schema_columns,
805            inside_aggregate,
806        ),
807        Expression::Conjunction(children) | Expression::Disjunction(children) => {
808            for child in children {
809                if let Some(alias) = find_aliased_aggregate_misuse_in_expression(
810                    child,
811                    aggregate_aliases,
812                    schema_columns,
813                    inside_aggregate,
814                ) {
815                    return Some(alias);
816                }
817            }
818            None
819        }
820        // Subqueries have their own scope
821        Expression::ScalarSubquery(_) | Expression::Exists { .. } => None,
822        Expression::QuantifiedComparison { expr, .. } => {
823            find_aliased_aggregate_misuse_in_expression(
824                expr,
825                aggregate_aliases,
826                schema_columns,
827                inside_aggregate,
828            )
829        }
830        Expression::IsDistinctFrom { left, right, .. } => {
831            find_aliased_aggregate_misuse_in_expression(
832                left,
833                aggregate_aliases,
834                schema_columns,
835                inside_aggregate,
836            )
837            .or_else(|| {
838                find_aliased_aggregate_misuse_in_expression(
839                    right,
840                    aggregate_aliases,
841                    schema_columns,
842                    inside_aggregate,
843                )
844            })
845        }
846        Expression::IsTruthValue { expr, .. } => find_aliased_aggregate_misuse_in_expression(
847            expr,
848            aggregate_aliases,
849            schema_columns,
850            inside_aggregate,
851        ),
852        // Other expressions don't contain column refs that could be aggregate aliases
853        _ => None,
854    }
855}
856
857/// Validate HAVING clause for misuse of aliased aggregates
858///
859/// This should be called after building the aggregate aliases from the SELECT list.
860/// Returns an error if an aggregate alias is used inside another aggregate in HAVING.
861///
862/// The `schema` parameter provides the actual table columns. If a column reference
863/// matches an actual table column, it's not considered an alias reference, even if
864/// an alias with the same name exists in the SELECT list.
865pub fn validate_having_aliased_aggregates(
866    having_clause: Option<&Expression>,
867    select_list: &[SelectItem],
868    schema: &CombinedSchema,
869) -> Result<(), ExecutorError> {
870    let Some(having_expr) = having_clause else {
871        return Ok(());
872    };
873
874    // Build the set of aggregate aliases
875    let aggregate_aliases = build_aggregate_aliases(select_list);
876
877    if aggregate_aliases.is_empty() {
878        return Ok(()); // No aggregate aliases to check
879    }
880
881    // Build the set of actual table column names (lowercase for case-insensitive matching)
882    let schema_columns: HashSet<String> = schema
883        .table_schemas
884        .values()
885        .flat_map(|(_, table_schema)| table_schema.columns.iter().map(|c| c.name.to_lowercase()))
886        .collect();
887
888    // Check for misuse in HAVING clause
889    if let Some(alias_name) = find_aliased_aggregate_misuse_in_expression(
890        having_expr,
891        &aggregate_aliases,
892        &schema_columns,
893        false,
894    ) {
895        return Err(ExecutorError::MisuseOfAliasedAggregate { alias_name });
896    }
897
898    Ok(())
899}
900
901/// Build a set of window function alias names from the SELECT list
902///
903/// A "window alias" is an alias that refers to an expression containing
904/// a window function, e.g., `count(*) OVER() AS m` makes `m` a window alias.
905pub fn build_window_aliases(select_list: &[SelectItem]) -> HashSet<String> {
906    let mut aliases = HashSet::new();
907
908    for item in select_list {
909        if let SelectItem::Expression { expr, alias: Some(alias_name), .. } = item {
910            if expression_contains_window_function(expr) {
911                // Store alias in lowercase for case-insensitive matching
912                aliases.insert(alias_name.to_lowercase());
913            }
914        }
915    }
916
917    aliases
918}
919
920/// Check if an expression contains a window function.
921///
922/// This walker has a deliberately narrow shape: it descends only through
923/// `BinaryOp`, `UnaryOp`, `Cast`, `Function` and `Case`. All other
924/// compound variants (e.g. `IsNull`, `Between`, `InList`) are treated as
925/// leaves, matching the historical behavior. If you need to detect a
926/// window function inside such variants, use a different walker.
927fn expression_contains_window_function(expr: &Expression) -> bool {
928    walk_first(expr, &|e| match e {
929        Expression::WindowFunction { .. } => WalkAction::Stop(()),
930        Expression::BinaryOp { .. }
931        | Expression::UnaryOp { .. }
932        | Expression::Cast { .. }
933        | Expression::Function { .. }
934        | Expression::Case { .. } => WalkAction::Descend,
935        // Every other variant (including AggregateFunction, IsNull,
936        // Between, etc.) is treated as a leaf to preserve original
937        // behavior.
938        _ => WalkAction::Skip,
939    })
940    .is_some()
941}
942
943/// Validate ORDER BY clause for misuse of aliased window functions
944///
945/// In SQLite, `ORDER BY (SELECT alias)` where alias refers to a window function is an error:
946/// "misuse of aliased window function <alias>"
947///
948/// This happens when ORDER BY contains a scalar subquery like `(SELECT m)` and `m` is
949/// an alias for a window function expression in the SELECT list.
950pub fn validate_order_by_aliased_window_functions(
951    order_by: Option<&[vibesql_ast::OrderByItem]>,
952    select_list: &[SelectItem],
953) -> Result<(), ExecutorError> {
954    let Some(order_items) = order_by else {
955        return Ok(());
956    };
957
958    let window_aliases = build_window_aliases(select_list);
959    if window_aliases.is_empty() {
960        return Ok(());
961    }
962
963    for item in order_items {
964        if let Some(alias_name) =
965            find_aliased_window_misuse_in_order_by(&item.expr, &window_aliases)
966        {
967            return Err(ExecutorError::MisuseOfAliasedWindowFunction { alias_name });
968        }
969    }
970
971    Ok(())
972}
973
974/// Find misuse of aliased window functions in ORDER BY expressions
975///
976/// Detects scalar subqueries like `(SELECT m)` where `m` matches a window function alias.
977fn find_aliased_window_misuse_in_order_by(
978    expr: &Expression,
979    window_aliases: &HashSet<String>,
980) -> Option<String> {
981    match expr {
982        Expression::ScalarSubquery(select_stmt) => {
983            // Check if the scalar subquery is a simple `(SELECT alias)` pattern
984            if select_stmt.select_list.len() == 1
985                && select_stmt.from.is_none()
986                && select_stmt.where_clause.is_none()
987            {
988                if let SelectItem::Expression { expr: Expression::ColumnRef(col_id), .. } =
989                    &select_stmt.select_list[0]
990                {
991                    if col_id.table_canonical().is_none() {
992                        let col_name = col_id.column_canonical().to_lowercase();
993                        if window_aliases.contains(&col_name) {
994                            return Some(col_name);
995                        }
996                    }
997                }
998            }
999            None
1000        }
1001        Expression::BinaryOp { left, right, .. } => {
1002            find_aliased_window_misuse_in_order_by(left, window_aliases)
1003                .or_else(|| find_aliased_window_misuse_in_order_by(right, window_aliases))
1004        }
1005        Expression::UnaryOp { expr, .. } => {
1006            find_aliased_window_misuse_in_order_by(expr, window_aliases)
1007        }
1008        _ => None,
1009    }
1010}
1011
1012/// Validate ORDER BY for misuse of aggregates that are correlated to the outer
1013/// scope when the outer query is a window-aggregate query.
1014///
1015/// **Mirrors SQLite's rule** at `src/window.c::disallowAggregatesInOrderByCb`
1016/// (invoked from `sqlite3WindowRewrite` when the SELECT is not a regular
1017/// aggregate query — i.e. has windows but no GROUP BY/HAVING/aggregate
1018/// SELECT items). SQLite walks the ORDER BY (recursing through subqueries)
1019/// looking for `TK_AGG_FUNCTION` nodes whose `pAggInfo` is null — that is,
1020/// aggregate references that resolved to the *outer* scope rather than the
1021/// nested subquery's own scope. Such an aggregate is reported as
1022/// `"misuse of aggregate: <name>()"`.
1023///
1024/// We approximate the resolver's behaviour: an aggregate inside a scalar
1025/// subquery is "correlated to the outer window query" iff it has at least
1026/// one column-argument that
1027///   - is unqualified or refers to an outer table, AND
1028///   - does not resolve against the nested subquery's own FROM clause, AND
1029///   - resolves against the outer schema.
1030///
1031/// Test case 61.4.3 (must error):
1032/// ```sql
1033/// SELECT sum(a) OVER (ORDER BY a) FROM t1
1034/// ORDER BY (SELECT sum(a) FROM t2)   -- t2 has no column 'a'; 'a' is t1.a (outer)
1035/// ```
1036///
1037/// Test case 61.4.4 (must NOT error):
1038/// ```sql
1039/// SELECT sum(a) OVER (ORDER BY a) FROM t1
1040/// ORDER BY (SELECT sum(y) FROM t2)   -- 'y' is t2.y, fully local to subquery
1041/// ```
1042///
1043/// The walker only fires when the outer SELECT contains a window function;
1044/// non-window queries already validate ORDER BY through other paths.
1045pub fn validate_window_query_order_by_aggregates(
1046    order_by: Option<&[vibesql_ast::OrderByItem]>,
1047    select_list: &[SelectItem],
1048    outer_schema: &CombinedSchema,
1049    database: &vibesql_storage::Database,
1050) -> Result<(), ExecutorError> {
1051    let Some(order_items) = order_by else {
1052        return Ok(());
1053    };
1054
1055    // Rule only applies to window-aggregate queries (has window function in
1056    // SELECT list). Plain aggregate or scalar queries use other validators.
1057    let has_window = select_list.iter().any(|item| match item {
1058        SelectItem::Expression { expr, .. } => expression_contains_window_function(expr),
1059        _ => false,
1060    });
1061    if !has_window {
1062        return Ok(());
1063    }
1064
1065    for item in order_items {
1066        if let Some(name) =
1067            find_outer_correlated_agg_in_order_by_subquery(&item.expr, outer_schema, database)
1068        {
1069            return Err(ExecutorError::MisuseOfAggregateContext { function_name: name });
1070        }
1071    }
1072
1073    Ok(())
1074}
1075
1076/// Walk an ORDER BY expression looking for scalar subqueries whose body
1077/// contains an aggregate function correlated to the outer (window) query.
1078fn find_outer_correlated_agg_in_order_by_subquery(
1079    expr: &Expression,
1080    outer_schema: &CombinedSchema,
1081    database: &vibesql_storage::Database,
1082) -> Option<String> {
1083    match expr {
1084        Expression::ScalarSubquery(stmt) => {
1085            // Build the set of column names visible inside the subquery's own
1086            // FROM clause. If we cannot determine it (e.g. nested subquery in
1087            // FROM), fall through to None — be conservative and don't flag
1088            // anything we can't prove correlated.
1089            let inner_columns = collect_from_clause_column_names(stmt.from.as_ref(), database)?;
1090
1091            // Walk the subquery's SELECT list looking for aggregate functions
1092            // whose column args resolve to the outer scope.
1093            for item in &stmt.select_list {
1094                if let SelectItem::Expression { expr: inner, .. } = item {
1095                    if let Some(name) =
1096                        find_outer_correlated_aggregate(inner, &inner_columns, outer_schema)
1097                    {
1098                        return Some(name);
1099                    }
1100                }
1101            }
1102            None
1103        }
1104        // Recurse through composite expressions in ORDER BY (an ORDER BY
1105        // expression may e.g. add a constant to a subquery: `(SELECT sum(a)
1106        // FROM t2) + 1`).
1107        Expression::BinaryOp { left, right, .. } => {
1108            find_outer_correlated_agg_in_order_by_subquery(left, outer_schema, database).or_else(
1109                || find_outer_correlated_agg_in_order_by_subquery(right, outer_schema, database),
1110            )
1111        }
1112        Expression::UnaryOp { expr, .. } => {
1113            find_outer_correlated_agg_in_order_by_subquery(expr, outer_schema, database)
1114        }
1115        Expression::Cast { expr, .. } => {
1116            find_outer_correlated_agg_in_order_by_subquery(expr, outer_schema, database)
1117        }
1118        Expression::Function { args, .. } => {
1119            for arg in args {
1120                if let Some(name) =
1121                    find_outer_correlated_agg_in_order_by_subquery(arg, outer_schema, database)
1122                {
1123                    return Some(name);
1124                }
1125            }
1126            None
1127        }
1128        _ => None,
1129    }
1130}
1131
1132/// Collect column names visible inside a FROM clause by looking up each
1133/// referenced table in the catalog. Returns `None` if the FROM contains a
1134/// subquery / VALUES / unknown table — in those cases we conservatively skip
1135/// the check rather than risk a false positive.
1136fn collect_from_clause_column_names(
1137    from: Option<&vibesql_ast::FromClause>,
1138    database: &vibesql_storage::Database,
1139) -> Option<HashSet<String>> {
1140    let mut columns = HashSet::new();
1141    let from = from?;
1142    if collect_from_columns_recursive(from, database, &mut columns) {
1143        Some(columns)
1144    } else {
1145        None
1146    }
1147}
1148
1149fn collect_from_columns_recursive(
1150    from: &vibesql_ast::FromClause,
1151    database: &vibesql_storage::Database,
1152    columns: &mut HashSet<String>,
1153) -> bool {
1154    use vibesql_ast::FromClause;
1155    match from {
1156        FromClause::Table { name, .. } => {
1157            // Strip any schema qualifier ("main.t" → "t").
1158            let table_name = name.rsplit_once('.').map(|(_, t)| t).unwrap_or(name.as_str());
1159            if let Some(table) = database.get_table(table_name) {
1160                for col in &table.schema.columns {
1161                    columns.insert(col.name.to_lowercase());
1162                }
1163                true
1164            } else {
1165                // Unknown table — bail out and skip the check.
1166                false
1167            }
1168        }
1169        FromClause::Join { left, right, .. } => {
1170            collect_from_columns_recursive(left, database, columns)
1171                && collect_from_columns_recursive(right, database, columns)
1172        }
1173        // Subqueries / VALUES in FROM: too complex to resolve here. Bail.
1174        FromClause::Subquery { .. } | FromClause::Values { .. } => false,
1175    }
1176}
1177
1178/// Recursively search an expression for an aggregate function whose column
1179/// arguments resolve to the outer scope rather than the inner subquery's FROM.
1180///
1181/// Returns the function name of the first such aggregate found.
1182fn find_outer_correlated_aggregate(
1183    expr: &Expression,
1184    inner_columns: &HashSet<String>,
1185    outer_schema: &CombinedSchema,
1186) -> Option<String> {
1187    match expr {
1188        Expression::AggregateFunction { name, args, order_by, filter, .. } => {
1189            if aggregate_args_correlate_outer(
1190                args,
1191                order_by.as_deref(),
1192                filter.as_deref(),
1193                inner_columns,
1194                outer_schema,
1195            ) {
1196                return Some(name.to_string());
1197            }
1198            // Recurse into args in case there's a deeper aggregate (which
1199            // would itself be a separate validation issue).
1200            for arg in args {
1201                if let Some(found) =
1202                    find_outer_correlated_aggregate(arg, inner_columns, outer_schema)
1203                {
1204                    return Some(found);
1205                }
1206            }
1207            None
1208        }
1209        Expression::Function { name, args, .. } => {
1210            if is_aggregate_function(name.as_str()) {
1211                let upper = name.to_uppercase();
1212                let is_scalar_minmax = matches!(upper.as_str(), "MIN" | "MAX") && args.len() > 1;
1213                if !is_scalar_minmax {
1214                    if args
1215                        .iter()
1216                        .any(|a| column_ref_resolves_to_outer(a, inner_columns, outer_schema))
1217                    {
1218                        return Some(name.to_string());
1219                    }
1220                }
1221            }
1222            for arg in args {
1223                if let Some(found) =
1224                    find_outer_correlated_aggregate(arg, inner_columns, outer_schema)
1225                {
1226                    return Some(found);
1227                }
1228            }
1229            None
1230        }
1231        Expression::BinaryOp { left, right, .. } => {
1232            find_outer_correlated_aggregate(left, inner_columns, outer_schema)
1233                .or_else(|| find_outer_correlated_aggregate(right, inner_columns, outer_schema))
1234        }
1235        Expression::UnaryOp { expr, .. } => {
1236            find_outer_correlated_aggregate(expr, inner_columns, outer_schema)
1237        }
1238        Expression::Cast { expr, .. } => {
1239            find_outer_correlated_aggregate(expr, inner_columns, outer_schema)
1240        }
1241        Expression::Case { operand, when_clauses, else_result } => {
1242            if let Some(op) = operand {
1243                if let Some(found) =
1244                    find_outer_correlated_aggregate(op, inner_columns, outer_schema)
1245                {
1246                    return Some(found);
1247                }
1248            }
1249            for w in when_clauses {
1250                for cond in &w.conditions {
1251                    if let Some(found) =
1252                        find_outer_correlated_aggregate(cond, inner_columns, outer_schema)
1253                    {
1254                        return Some(found);
1255                    }
1256                }
1257                if let Some(found) =
1258                    find_outer_correlated_aggregate(&w.result, inner_columns, outer_schema)
1259                {
1260                    return Some(found);
1261                }
1262            }
1263            if let Some(else_expr) = else_result {
1264                find_outer_correlated_aggregate(else_expr, inner_columns, outer_schema)
1265            } else {
1266                None
1267            }
1268        }
1269        // Don't recurse into nested scalar subqueries — those have their own
1270        // scope and are validated separately.
1271        _ => None,
1272    }
1273}
1274
1275/// True if the column references inside an aggregate's args/order_by/filter
1276/// resolve to the outer schema rather than the subquery's own FROM columns.
1277fn aggregate_args_correlate_outer(
1278    args: &[Expression],
1279    order_by: Option<&[vibesql_ast::OrderByItem]>,
1280    filter: Option<&Expression>,
1281    inner_columns: &HashSet<String>,
1282    outer_schema: &CombinedSchema,
1283) -> bool {
1284    let arg_correlated =
1285        args.iter().any(|a| column_ref_resolves_to_outer(a, inner_columns, outer_schema));
1286    let order_correlated = order_by.is_some_and(|items| {
1287        items.iter().any(|i| column_ref_resolves_to_outer(&i.expr, inner_columns, outer_schema))
1288    });
1289    let filter_correlated =
1290        filter.is_some_and(|f| column_ref_resolves_to_outer(f, inner_columns, outer_schema));
1291    arg_correlated || order_correlated || filter_correlated
1292}
1293
1294/// True if the expression contains at least one ColumnRef that resolves to
1295/// the outer scope but not the inner subquery's own FROM columns.
1296///
1297/// "Resolves to outer" means: the column name is present in `outer_schema`
1298/// AND not present in `inner_columns`. A qualified reference whose table
1299/// name is not visible in the inner scope but whose name is in `outer_schema`
1300/// also counts as outer-correlated.
1301fn column_ref_resolves_to_outer(
1302    expr: &Expression,
1303    inner_columns: &HashSet<String>,
1304    outer_schema: &CombinedSchema,
1305) -> bool {
1306    match expr {
1307        Expression::ColumnRef(col_id) => {
1308            let col_name = col_id.column_canonical().to_lowercase();
1309            // If the inner scope has this column, the reference is local.
1310            if inner_columns.contains(&col_name) {
1311                return false;
1312            }
1313            // Otherwise it must resolve to the outer scope to be a "misuse"
1314            // (an unresolvable reference would be reported elsewhere).
1315            outer_schema.has_column(&col_name)
1316        }
1317        Expression::BinaryOp { left, right, .. } => {
1318            column_ref_resolves_to_outer(left, inner_columns, outer_schema)
1319                || column_ref_resolves_to_outer(right, inner_columns, outer_schema)
1320        }
1321        Expression::UnaryOp { expr, .. } => {
1322            column_ref_resolves_to_outer(expr, inner_columns, outer_schema)
1323        }
1324        Expression::Cast { expr, .. } => {
1325            column_ref_resolves_to_outer(expr, inner_columns, outer_schema)
1326        }
1327        Expression::Function { args, .. } | Expression::AggregateFunction { args, .. } => {
1328            args.iter().any(|a| column_ref_resolves_to_outer(a, inner_columns, outer_schema))
1329        }
1330        Expression::Case { operand, when_clauses, else_result } => {
1331            operand
1332                .as_ref()
1333                .is_some_and(|e| column_ref_resolves_to_outer(e, inner_columns, outer_schema))
1334                || when_clauses.iter().any(|w| {
1335                    w.conditions
1336                        .iter()
1337                        .any(|c| column_ref_resolves_to_outer(c, inner_columns, outer_schema))
1338                        || column_ref_resolves_to_outer(&w.result, inner_columns, outer_schema)
1339                })
1340                || else_result
1341                    .as_ref()
1342                    .is_some_and(|e| column_ref_resolves_to_outer(e, inner_columns, outer_schema))
1343        }
1344        Expression::IsNull { expr, .. } => {
1345            column_ref_resolves_to_outer(expr, inner_columns, outer_schema)
1346        }
1347        Expression::Conjunction(children) | Expression::Disjunction(children) => {
1348            children.iter().any(|c| column_ref_resolves_to_outer(c, inner_columns, outer_schema))
1349        }
1350        // Don't recurse into nested subqueries — their scope is independent.
1351        _ => false,
1352    }
1353}
1354
1355/// Check if an expression contains any column reference (recursively).
1356///
1357/// Used by `validate_subquery_context_misuse` to determine whether an
1358/// aggregate inside a bare (FROM-less) scalar subquery references columns
1359/// from the outer scope — those references are what make the aggregate a
1360/// "misuse" of the outer aggregation context.
1361///
1362/// Window functions are inspected only through their argument list, not
1363/// the OVER clause or FILTER (which have their own scope).
1364fn expression_contains_column_ref(expr: &Expression) -> bool {
1365    walk_first(expr, &|e| match e {
1366        Expression::ColumnRef(_) => WalkAction::Stop(()),
1367        // For AggregateFunction we only check `args` (matching the
1368        // original walker, which excluded ORDER BY and FILTER).
1369        Expression::AggregateFunction { args, .. } => {
1370            if args.iter().any(expression_contains_column_ref) {
1371                WalkAction::Stop(())
1372            } else {
1373                WalkAction::Skip
1374            }
1375        }
1376        // Window function: only the argument list is considered to
1377        // reference the outer scope. The OVER clause and FILTER are
1378        // evaluated against the window's own scope.
1379        Expression::WindowFunction { function, .. } => {
1380            let args = match function {
1381                vibesql_ast::WindowFunctionSpec::Aggregate { args, .. }
1382                | vibesql_ast::WindowFunctionSpec::Ranking { args, .. }
1383                | vibesql_ast::WindowFunctionSpec::Value { args, .. } => args,
1384            };
1385            if args.iter().any(expression_contains_column_ref) {
1386                WalkAction::Stop(())
1387            } else {
1388                WalkAction::Skip
1389            }
1390        }
1391        // The original walker treated these variants as leaves
1392        // (`_ => false`), so preserve that behavior here.
1393        Expression::Glob { .. } | Expression::Collate { .. } | Expression::MatchAgainst { .. } => {
1394            WalkAction::Skip
1395        }
1396        _ => WalkAction::Descend,
1397    })
1398    .is_some()
1399}
1400
1401/// Find the first aggregate function whose args/order_by/filter reference a
1402/// column. Returns the function name if found, None otherwise.
1403///
1404/// This is used inside bare (FROM-less) scalar subqueries: any column there
1405/// is necessarily an outer reference, so an aggregate referencing one is a
1406/// misuse of the outer aggregation context.
1407fn find_outer_referencing_aggregate(expr: &Expression) -> Option<String> {
1408    walk_first(expr, &|e| match e {
1409        Expression::AggregateFunction { name, args, order_by, filter, .. } => {
1410            let arg_refs = args.iter().any(expression_contains_column_ref);
1411            let order_refs = order_by.as_ref().is_some_and(|items| {
1412                items.iter().any(|item| expression_contains_column_ref(&item.expr))
1413            });
1414            let filter_refs = filter.as_ref().is_some_and(|f| expression_contains_column_ref(f));
1415            if arg_refs || order_refs || filter_refs {
1416                WalkAction::Stop(name.to_string())
1417            } else {
1418                // The aggregate itself does not reference an outer column,
1419                // but a deeper aggregate (in args) might. Descend into
1420                // args only — order_by / filter are already handled above.
1421                args.iter()
1422                    .find_map(find_outer_referencing_aggregate)
1423                    .map_or(WalkAction::Skip, WalkAction::Stop)
1424            }
1425        }
1426        Expression::Function { name, args, .. } if is_aggregate_function(name.as_str()) => {
1427            let upper = name.to_uppercase();
1428            let is_scalar_minmax = matches!(upper.as_str(), "MIN" | "MAX") && args.len() > 1;
1429            if !is_scalar_minmax && args.iter().any(expression_contains_column_ref) {
1430                WalkAction::Stop(name.to_string())
1431            } else {
1432                // Descend into args looking for nested aggregates.
1433                WalkAction::Descend
1434            }
1435        }
1436        // The original walker only recursed into a subset of compound
1437        // expressions; everything else fell into `_ => None`. Preserve
1438        // that behavior by skipping the variants the original ignored.
1439        Expression::IsDistinctFrom { .. }
1440        | Expression::IsTruthValue { .. }
1441        | Expression::Position { .. }
1442        | Expression::Trim { .. }
1443        | Expression::Extract { .. }
1444        | Expression::Interval { .. }
1445        | Expression::QuantifiedComparison { .. }
1446        | Expression::RowValueConstructor(_)
1447        | Expression::MatchAgainst { .. }
1448        | Expression::Collate { .. }
1449        | Expression::Glob { .. }
1450        | Expression::WindowFunction { .. } => WalkAction::Skip,
1451        _ => WalkAction::Descend,
1452    })
1453}
1454
1455/// Context in which a subquery appears, controlling how
1456/// `validate_subquery_context_misuse` treats outer-correlated aggregates inside
1457/// bare scalar subqueries.
1458///
1459/// SQLite has two distinct behaviors for a bare scalar subquery whose body
1460/// contains an aggregate referencing an *outer* column:
1461///
1462/// 1. **WHERE / HAVING / ORDER BY** (and arguments to other outer functions):
1463///    SQLite raises `"misuse of aggregate: X()"`. The outer aggregation context
1464///    is not implicitly borrowed in these positions.
1465///
1466/// 2. **SELECT list** (issue #5104): SQLite implicitly collapses the outer
1467///    query into a single-row aggregate, with the inner aggregate computed
1468///    over all outer rows. The aggregate is *not* a misuse here — it's
1469///    well-defined and produces a single output row.
1470///
1471/// We therefore parameterize the validator on context: SELECT-list calls pass
1472/// [`SubqueryContext::SelectList`] to skip the outer-correlated-aggregate
1473/// rejection (it will be handled by the implicit-collapse path). All other
1474/// callers pass [`SubqueryContext::WhereOrEqual`] to preserve the SQLite
1475/// rejection.
1476///
1477/// Row-value misuse is checked in *both* contexts — `(SELECT (a, b))` is
1478/// always an error regardless of where the subquery appears.
1479#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1480pub enum SubqueryContext {
1481    /// Bare scalar subquery in WHERE, HAVING, ORDER BY, or as an argument to
1482    /// an outer function. Outer-correlated aggregates → "misuse of aggregate".
1483    WhereOrEqual,
1484    /// Bare scalar subquery in the SELECT list. Outer-correlated aggregates
1485    /// trigger SQLite's implicit-outer-aggregate-collapse instead of a misuse
1486    /// error (#5104).
1487    SelectList,
1488}
1489
1490/// Detect aggregate / row-value misuse inside *bare* scalar subqueries.
1491///
1492/// SQLite has a special rule: when a scalar subquery has no FROM clause and
1493/// its body contains an aggregate function whose argument or FILTER references
1494/// an *outer* column, the aggregate implicitly borrows the outer aggregation
1495/// context. SQLite reports this as `"misuse of aggregate: X()"` *unless* the
1496/// subquery appears in the SELECT list, where it instead triggers
1497/// implicit-outer-aggregate-collapse (#5104).
1498///
1499/// Examples that trigger this rule (in WHERE/HAVING/ORDER BY context):
1500/// - `WHERE (SELECT AVG(0) FILTER(WHERE outer.c))`  → "misuse of aggregate: AVG()"
1501/// - `ntile((SELECT sum(x))) OVER (...)`             → "misuse of aggregate: sum()"
1502///
1503/// In SELECT-list context (#5104), the same patterns are *allowed* and trigger
1504/// outer-aggregate collapse:
1505/// - `SELECT (SELECT avg(a)) FROM t2` → outer collapses to single row
1506///
1507/// Additionally, when a bare scalar subquery's sole projection is a
1508/// `RowValueConstructor` with multiple elements, that's `RowValueMisused`
1509/// (a row value where a scalar is required) — checked in both contexts.
1510///
1511/// Note: pure *window* functions inside bare scalar subqueries are NOT flagged
1512/// here — SQLite allows them (they are evaluated row-by-row against the outer
1513/// scope). Misuse for window functions is detected through other paths
1514/// (WHERE-clause walker, GROUP BY positional refs, etc.).
1515///
1516/// This walker is meant to be called on expressions appearing in the outer
1517/// scope — typically WHERE, HAVING, ORDER BY, or arguments to outer functions
1518/// (with `WhereOrEqual`), or the SELECT list (with `SelectList`).
1519/// It deliberately does NOT recurse into scalar subqueries that have a FROM
1520/// clause (those have their own aggregation context and are validated
1521/// independently).
1522///
1523/// To preserve SQLite's exact error text (which uses the function name as
1524/// originally written by the user), we walk the inner expression and report
1525/// the first such aggregate's name.
1526pub fn validate_subquery_context_misuse(
1527    expr: &Expression,
1528    context: SubqueryContext,
1529) -> Result<(), ExecutorError> {
1530    match expr {
1531        Expression::ScalarSubquery(stmt) => {
1532            // Walk the full compound chain: the head SELECT plus every
1533            // set-operation arm (UNION/INTERSECT/EXCEPT). Each arm carries
1534            // its own FROM clause, and SQLite flags misuse in *any* FROM-less
1535            // arm — e.g. `(SELECT 2,2 UNION SELECT sum(b),... )` errors even
1536            // though the head arm is clean (window1.test 71.0).
1537            let mut arm: &vibesql_ast::SelectStmt = stmt;
1538            loop {
1539                // Only "bare" arms (no FROM) re-borrow the outer context.
1540                if arm.from.is_none() {
1541                    // Check for row value misuse (sole projection is a row value
1542                    // constructor with multiple elements). This is an error in
1543                    // any context.
1544                    if arm.select_list.len() == 1 {
1545                        if let SelectItem::Expression {
1546                            expr: Expression::RowValueConstructor(items),
1547                            ..
1548                        } = &arm.select_list[0]
1549                        {
1550                            if items.len() > 1 {
1551                                return Err(ExecutorError::RowValueMisused);
1552                            }
1553                        }
1554                    }
1555
1556                    // Check each projection for an aggregate function. We only
1557                    // flag aggregates whose body references something outside the
1558                    // bare subquery's own scope (i.e., references a column — the
1559                    // bare subquery has no tables, so any column is an outer ref).
1560                    //
1561                    // In SELECT-list context, this is *not* a misuse; it triggers
1562                    // implicit-outer-aggregate-collapse instead (#5104). Skip the
1563                    // rejection so the executor's collapse path can handle it.
1564                    if context == SubqueryContext::WhereOrEqual {
1565                        for item in &arm.select_list {
1566                            if let SelectItem::Expression { expr: inner, .. } = item {
1567                                if let Some(name) = find_outer_referencing_aggregate(inner) {
1568                                    return Err(ExecutorError::MisuseOfAggregateContext {
1569                                        function_name: name,
1570                                    });
1571                                }
1572                            }
1573                        }
1574                    }
1575
1576                    // Recurse into nested expressions inside the bare arm's
1577                    // SELECT list as well — e.g. (SELECT (SELECT sum(x))).
1578                    // Inner subqueries inherit the same context: nested bare
1579                    // subqueries in a SELECT-list context still treat their own
1580                    // SELECT-list position as SELECT-list.
1581                    for item in &arm.select_list {
1582                        if let SelectItem::Expression { expr: inner, .. } = item {
1583                            validate_subquery_context_misuse(inner, context)?;
1584                        }
1585                    }
1586                }
1587
1588                match arm.set_operation.as_ref() {
1589                    Some(set_op) => arm = &set_op.right,
1590                    None => break,
1591                }
1592            }
1593            Ok(())
1594        }
1595        // For non-subquery expressions, recurse into their children. We do
1596        // NOT recurse into AggregateFunction / WindowFunction children here —
1597        // those are checked by their own dedicated validators which understand
1598        // the surrounding context (FILTER, ORDER BY, etc.).
1599        Expression::BinaryOp { left, right, .. } => {
1600            validate_subquery_context_misuse(left, context)?;
1601            validate_subquery_context_misuse(right, context)
1602        }
1603        Expression::UnaryOp { expr, .. } => validate_subquery_context_misuse(expr, context),
1604        Expression::IsNull { expr, .. } => validate_subquery_context_misuse(expr, context),
1605        Expression::IsDistinctFrom { left, right, .. } => {
1606            validate_subquery_context_misuse(left, context)?;
1607            validate_subquery_context_misuse(right, context)
1608        }
1609        Expression::IsTruthValue { expr, .. } => validate_subquery_context_misuse(expr, context),
1610        Expression::Cast { expr, .. } => validate_subquery_context_misuse(expr, context),
1611        Expression::Like { expr, pattern, .. } => {
1612            validate_subquery_context_misuse(expr, context)?;
1613            validate_subquery_context_misuse(pattern, context)
1614        }
1615        Expression::Between { expr, low, high, .. } => {
1616            validate_subquery_context_misuse(expr, context)?;
1617            validate_subquery_context_misuse(low, context)?;
1618            validate_subquery_context_misuse(high, context)
1619        }
1620        Expression::InList { expr, values, .. } => {
1621            validate_subquery_context_misuse(expr, context)?;
1622            for v in values {
1623                validate_subquery_context_misuse(v, context)?;
1624            }
1625            Ok(())
1626        }
1627        Expression::In { expr, .. } => validate_subquery_context_misuse(expr, context),
1628        Expression::Case { operand, when_clauses, else_result } => {
1629            if let Some(op) = operand {
1630                validate_subquery_context_misuse(op, context)?;
1631            }
1632            for w in when_clauses {
1633                for cond in &w.conditions {
1634                    validate_subquery_context_misuse(cond, context)?;
1635                }
1636                validate_subquery_context_misuse(&w.result, context)?;
1637            }
1638            if let Some(else_expr) = else_result {
1639                validate_subquery_context_misuse(else_expr, context)?;
1640            }
1641            Ok(())
1642        }
1643        Expression::Function { args, .. } => {
1644            for arg in args {
1645                validate_subquery_context_misuse(arg, context)?;
1646            }
1647            Ok(())
1648        }
1649        Expression::AggregateFunction { args, filter, order_by, .. } => {
1650            // Aggregate function arguments are evaluated in WHERE/HAVING-like
1651            // context (their value must be a scalar). Subqueries inside their
1652            // args are NOT in SELECT-list position, so reset context.
1653            for arg in args {
1654                validate_subquery_context_misuse(arg, SubqueryContext::WhereOrEqual)?;
1655            }
1656            if let Some(f) = filter {
1657                validate_subquery_context_misuse(f, SubqueryContext::WhereOrEqual)?;
1658            }
1659            if let Some(items) = order_by {
1660                for item in items {
1661                    validate_subquery_context_misuse(&item.expr, SubqueryContext::WhereOrEqual)?;
1662                }
1663            }
1664            Ok(())
1665        }
1666        Expression::WindowFunction { function, .. } => {
1667            // Only recurse into the function's arguments — the OVER clause has
1668            // its own context and may legally contain aggregates / nested
1669            // subqueries (window functions are evaluated AFTER aggregation).
1670            // Window function arguments are like aggregate-function arguments
1671            // (must be scalar) — reset context to WhereOrEqual.
1672            let args = match function {
1673                vibesql_ast::WindowFunctionSpec::Aggregate { args, .. }
1674                | vibesql_ast::WindowFunctionSpec::Ranking { args, .. }
1675                | vibesql_ast::WindowFunctionSpec::Value { args, .. } => args,
1676            };
1677            for a in args {
1678                validate_subquery_context_misuse(a, SubqueryContext::WhereOrEqual)?;
1679            }
1680            Ok(())
1681        }
1682        Expression::QuantifiedComparison { expr, .. } => {
1683            validate_subquery_context_misuse(expr, context)
1684        }
1685        Expression::Conjunction(children) | Expression::Disjunction(children) => {
1686            for child in children {
1687                validate_subquery_context_misuse(child, context)?;
1688            }
1689            Ok(())
1690        }
1691        Expression::RowValueConstructor(children) => {
1692            for child in children {
1693                validate_subquery_context_misuse(child, context)?;
1694            }
1695            Ok(())
1696        }
1697        // Leaf expressions and EXISTS (which has its own scope) — nothing to check
1698        _ => Ok(()),
1699    }
1700}
1701
1702/// Recursively validate that no SELECT statement (the given one or any nested
1703/// subquery) has a GROUP BY whose expression — directly or after positional
1704/// resolution against its own SELECT list — is a window function.
1705///
1706/// SQLite reports this as `"misuse of window function X()"` at *prepare* time,
1707/// before any rows are fetched, so it must be detected during query
1708/// preparation rather than only at the execution path of the inner SELECT.
1709///
1710/// The check fires both on the GROUP BY expression as written and on its
1711/// alias-resolved form so that `GROUP BY 1` referring to a window-function
1712/// SELECT item (window1.test 47.2) is also caught.
1713pub fn validate_group_by_window_misuse(
1714    stmt: &vibesql_ast::SelectStmt,
1715) -> Result<(), ExecutorError> {
1716    // 1. Check this statement's own GROUP BY for window misuse, with
1717    //    positional/alias resolution against its SELECT list.
1718    if let Some(ref group_by_clause) = stmt.group_by {
1719        for (term_index, group_expr) in group_by_clause.all_expressions().iter().enumerate() {
1720            if let Some(window_name) = find_window_function_in_expression(group_expr) {
1721                return Err(ExecutorError::MisuseOfWindowFunction { function_name: window_name });
1722            }
1723            let resolved = crate::select::grouping::resolve_group_by_alias(
1724                group_expr,
1725                &stmt.select_list,
1726                term_index,
1727            )?;
1728            if let Some(window_name) = find_window_function_in_expression(&resolved) {
1729                return Err(ExecutorError::MisuseOfWindowFunction { function_name: window_name });
1730            }
1731        }
1732    }
1733
1734    // 2. Recurse into all expressions of this statement to find nested SELECTs.
1735    for item in &stmt.select_list {
1736        if let SelectItem::Expression { expr, .. } = item {
1737            walk_expr_for_subquery_group_by(expr)?;
1738        }
1739    }
1740    if let Some(where_expr) = stmt.where_clause.as_ref() {
1741        walk_expr_for_subquery_group_by(where_expr)?;
1742    }
1743    if let Some(having_expr) = stmt.having.as_ref() {
1744        walk_expr_for_subquery_group_by(having_expr)?;
1745    }
1746    if let Some(group_by) = stmt.group_by.as_ref() {
1747        for expr in group_by.all_expressions() {
1748            walk_expr_for_subquery_group_by(expr)?;
1749        }
1750    }
1751    if let Some(order_by) = stmt.order_by.as_ref() {
1752        for item in order_by {
1753            walk_expr_for_subquery_group_by(&item.expr)?;
1754        }
1755    }
1756
1757    // 3. Recurse into FROM clause subqueries (derived tables, JOINs).
1758    if let Some(from) = stmt.from.as_ref() {
1759        walk_from_for_subquery_group_by(from)?;
1760    }
1761
1762    // 4. Recurse into CTEs.
1763    if let Some(ctes) = stmt.with_clause.as_ref() {
1764        for cte in ctes {
1765            validate_group_by_window_misuse(&cte.query)?;
1766        }
1767    }
1768
1769    Ok(())
1770}
1771
1772/// Walk an expression tree, finding nested ScalarSubquery / Exists subqueries
1773/// and validating their GROUP BY clauses for window-function misuse.
1774fn walk_expr_for_subquery_group_by(expr: &Expression) -> Result<(), ExecutorError> {
1775    match expr {
1776        Expression::ScalarSubquery(stmt) => validate_group_by_window_misuse(stmt),
1777        Expression::Exists { subquery, .. } => validate_group_by_window_misuse(subquery),
1778        Expression::BinaryOp { left, right, .. } => {
1779            walk_expr_for_subquery_group_by(left)?;
1780            walk_expr_for_subquery_group_by(right)
1781        }
1782        Expression::UnaryOp { expr, .. } => walk_expr_for_subquery_group_by(expr),
1783        Expression::IsNull { expr, .. } => walk_expr_for_subquery_group_by(expr),
1784        Expression::IsDistinctFrom { left, right, .. } => {
1785            walk_expr_for_subquery_group_by(left)?;
1786            walk_expr_for_subquery_group_by(right)
1787        }
1788        Expression::IsTruthValue { expr, .. } => walk_expr_for_subquery_group_by(expr),
1789        Expression::Cast { expr, .. } => walk_expr_for_subquery_group_by(expr),
1790        Expression::Like { expr, pattern, .. } => {
1791            walk_expr_for_subquery_group_by(expr)?;
1792            walk_expr_for_subquery_group_by(pattern)
1793        }
1794        Expression::Between { expr, low, high, .. } => {
1795            walk_expr_for_subquery_group_by(expr)?;
1796            walk_expr_for_subquery_group_by(low)?;
1797            walk_expr_for_subquery_group_by(high)
1798        }
1799        Expression::InList { expr, values, .. } => {
1800            walk_expr_for_subquery_group_by(expr)?;
1801            for v in values {
1802                walk_expr_for_subquery_group_by(v)?;
1803            }
1804            Ok(())
1805        }
1806        Expression::In { expr, subquery, .. } => {
1807            walk_expr_for_subquery_group_by(expr)?;
1808            validate_group_by_window_misuse(subquery)
1809        }
1810        Expression::Case { operand, when_clauses, else_result } => {
1811            if let Some(op) = operand {
1812                walk_expr_for_subquery_group_by(op)?;
1813            }
1814            for w in when_clauses {
1815                for cond in &w.conditions {
1816                    walk_expr_for_subquery_group_by(cond)?;
1817                }
1818                walk_expr_for_subquery_group_by(&w.result)?;
1819            }
1820            if let Some(else_expr) = else_result {
1821                walk_expr_for_subquery_group_by(else_expr)?;
1822            }
1823            Ok(())
1824        }
1825        Expression::Function { args, .. } => {
1826            for arg in args {
1827                walk_expr_for_subquery_group_by(arg)?;
1828            }
1829            Ok(())
1830        }
1831        Expression::AggregateFunction { args, filter, order_by, .. } => {
1832            for arg in args {
1833                walk_expr_for_subquery_group_by(arg)?;
1834            }
1835            if let Some(f) = filter {
1836                walk_expr_for_subquery_group_by(f)?;
1837            }
1838            if let Some(items) = order_by {
1839                for item in items {
1840                    walk_expr_for_subquery_group_by(&item.expr)?;
1841                }
1842            }
1843            Ok(())
1844        }
1845        Expression::WindowFunction { function, .. } => {
1846            let args = match function {
1847                vibesql_ast::WindowFunctionSpec::Aggregate { args, .. }
1848                | vibesql_ast::WindowFunctionSpec::Ranking { args, .. }
1849                | vibesql_ast::WindowFunctionSpec::Value { args, .. } => args,
1850            };
1851            for a in args {
1852                walk_expr_for_subquery_group_by(a)?;
1853            }
1854            Ok(())
1855        }
1856        Expression::QuantifiedComparison { expr, subquery, .. } => {
1857            walk_expr_for_subquery_group_by(expr)?;
1858            validate_group_by_window_misuse(subquery)
1859        }
1860        Expression::Conjunction(children) | Expression::Disjunction(children) => {
1861            for child in children {
1862                walk_expr_for_subquery_group_by(child)?;
1863            }
1864            Ok(())
1865        }
1866        Expression::RowValueConstructor(children) => {
1867            for child in children {
1868                walk_expr_for_subquery_group_by(child)?;
1869            }
1870            Ok(())
1871        }
1872        _ => Ok(()),
1873    }
1874}
1875
1876/// Walk a FROM clause to find nested SELECT statements (derived tables, JOINs).
1877fn walk_from_for_subquery_group_by(from: &vibesql_ast::FromClause) -> Result<(), ExecutorError> {
1878    match from {
1879        vibesql_ast::FromClause::Table { .. } => Ok(()),
1880        vibesql_ast::FromClause::Join { left, right, condition, .. } => {
1881            walk_from_for_subquery_group_by(left)?;
1882            walk_from_for_subquery_group_by(right)?;
1883            if let Some(expr) = condition {
1884                walk_expr_for_subquery_group_by(expr)?;
1885            }
1886            Ok(())
1887        }
1888        vibesql_ast::FromClause::Subquery { query, .. } => validate_group_by_window_misuse(query),
1889        vibesql_ast::FromClause::Values { rows, .. } => {
1890            for row in rows {
1891                for expr in row {
1892                    walk_expr_for_subquery_group_by(expr)?;
1893                }
1894            }
1895            Ok(())
1896        }
1897    }
1898}
1899
1900#[cfg(test)]
1901mod tests {
1902    use vibesql_ast::{BinaryOperator, ColumnIdentifier, FunctionIdentifier, UnaryOperator};
1903    use vibesql_catalog::{ColumnSchema, TableSchema};
1904    use vibesql_types::{DataType, SqlValue};
1905
1906    use super::*;
1907
1908    /// Create a schema with f1 and f2 columns (for aliased aggregate tests)
1909    fn make_f1_f2_schema() -> CombinedSchema {
1910        let columns = vec![
1911            ColumnSchema {
1912                name: "f1".to_string(),
1913                data_type: DataType::Integer,
1914                nullable: true,
1915                default_value: None,
1916                generated_expr: None,
1917                is_exact_integer_type: false,
1918                collation: None,
1919            },
1920            ColumnSchema {
1921                name: "f2".to_string(),
1922                data_type: DataType::Integer,
1923                nullable: true,
1924                default_value: None,
1925                generated_expr: None,
1926                is_exact_integer_type: false,
1927                collation: None,
1928            },
1929        ];
1930        let table_schema = TableSchema::new("test1".to_string(), columns);
1931        CombinedSchema::from_table("test1".to_string(), table_schema)
1932    }
1933
1934    #[test]
1935    fn test_min_star_invalid() {
1936        // MIN(*) should be invalid - returns error with function name (preserving original case)
1937        let expr = Expression::AggregateFunction {
1938            name: FunctionIdentifier::new("MIN"),
1939            distinct: false,
1940            args: vec![Expression::ColumnRef(ColumnIdentifier::simple("*", false))],
1941            order_by: None,
1942            filter: None,
1943        };
1944        let result = check_aggregate_arg_count(&expr);
1945        assert!(result.is_some(), "MIN(*) should be invalid");
1946        assert_eq!(result.unwrap(), "MIN"); // Preserves original case
1947    }
1948
1949    #[test]
1950    fn test_max_star_invalid() {
1951        // MAX(*) should be invalid
1952        let expr = Expression::AggregateFunction {
1953            name: FunctionIdentifier::new("MAX"),
1954            distinct: false,
1955            args: vec![Expression::ColumnRef(ColumnIdentifier::simple("*", false))],
1956            order_by: None,
1957            filter: None,
1958        };
1959        let result = check_aggregate_arg_count(&expr);
1960        assert!(result.is_some(), "MAX(*) should be invalid");
1961        assert_eq!(result.unwrap(), "MAX"); // Preserves original case
1962    }
1963
1964    #[test]
1965    fn test_min_no_args_invalid() {
1966        // MIN() with no arguments should be invalid
1967        let expr = Expression::AggregateFunction {
1968            name: FunctionIdentifier::new("MIN"),
1969            distinct: false,
1970            args: vec![],
1971            order_by: None,
1972            filter: None,
1973        };
1974        let result = check_aggregate_arg_count(&expr);
1975        assert!(result.is_some(), "MIN() should be invalid");
1976        assert_eq!(result.unwrap(), "MIN"); // Preserves original case
1977    }
1978
1979    #[test]
1980    fn test_validate_aggregate_arguments() {
1981        // Test the public function
1982        let select_list = vec![SelectItem::Expression {
1983            expr: Expression::AggregateFunction {
1984                name: FunctionIdentifier::new("MIN"),
1985                distinct: false,
1986                args: vec![Expression::ColumnRef(ColumnIdentifier::simple("*", false))],
1987                order_by: None,
1988                filter: None,
1989            },
1990            alias: None,
1991            source_text: None,
1992        }];
1993        let result = validate_aggregate_arguments(&select_list);
1994        assert!(result.is_err());
1995    }
1996
1997    #[test]
1998    fn test_having_with_aliased_aggregate_inside_aggregate() {
1999        // SELECT min(f1) AS m FROM test1 GROUP BY f1 HAVING max(m+5)<10
2000        // The alias 'm' refers to an aggregate and is used inside max() - should error
2001        // Note: 'm' is NOT a column in the table, so it's treated as an alias reference
2002        let select_list = vec![SelectItem::Expression {
2003            expr: Expression::AggregateFunction {
2004                name: FunctionIdentifier::new("min"),
2005                distinct: false,
2006                args: vec![Expression::ColumnRef(ColumnIdentifier::simple("f1", false))],
2007                order_by: None,
2008                filter: None,
2009            },
2010            alias: Some("m".to_string()),
2011            source_text: None,
2012        }];
2013
2014        // HAVING max(m+5)<10
2015        let having_expr = Expression::BinaryOp {
2016            op: BinaryOperator::LessThan,
2017            left: Box::new(Expression::AggregateFunction {
2018                name: FunctionIdentifier::new("max"),
2019                distinct: false,
2020                args: vec![Expression::BinaryOp {
2021                    op: BinaryOperator::Plus,
2022                    left: Box::new(Expression::ColumnRef(ColumnIdentifier::simple("m", false))),
2023                    right: Box::new(Expression::Literal(SqlValue::Integer(5))),
2024                }],
2025                order_by: None,
2026                filter: None,
2027            }),
2028            right: Box::new(Expression::Literal(SqlValue::Integer(10))),
2029        };
2030
2031        // Use schema with f1, f2 - 'm' is not a column, so it's an alias reference
2032        let schema = make_f1_f2_schema();
2033        let result = validate_having_aliased_aggregates(Some(&having_expr), &select_list, &schema);
2034        assert!(result.is_err());
2035        match result {
2036            Err(ExecutorError::MisuseOfAliasedAggregate { alias_name }) => {
2037                assert_eq!(alias_name, "m");
2038            }
2039            _ => panic!("Expected MisuseOfAliasedAggregate error"),
2040        }
2041    }
2042
2043    #[test]
2044    fn test_having_with_aggregate_alias_not_inside_aggregate() {
2045        // SELECT min(f1) AS m FROM test1 GROUP BY f1 HAVING m>0
2046        // The alias 'm' refers to an aggregate but is NOT used inside another aggregate
2047        // This is a gray area - SQLite actually treats this as an error too,
2048        // because the alias cannot be resolved in HAVING context at all.
2049        // For now, we only detect the case where it's inside an aggregate.
2050        let select_list = vec![SelectItem::Expression {
2051            expr: Expression::AggregateFunction {
2052                name: FunctionIdentifier::new("min"),
2053                distinct: false,
2054                args: vec![Expression::ColumnRef(ColumnIdentifier::simple("f1", false))],
2055                order_by: None,
2056                filter: None,
2057            },
2058            alias: Some("m".to_string()),
2059            source_text: None,
2060        }];
2061
2062        // HAVING m>0 - alias used directly, not inside an aggregate
2063        let having_expr = Expression::BinaryOp {
2064            op: BinaryOperator::GreaterThan,
2065            left: Box::new(Expression::ColumnRef(ColumnIdentifier::simple("m", false))),
2066            right: Box::new(Expression::Literal(SqlValue::Integer(0))),
2067        };
2068
2069        // This should pass our current validation (alias not inside aggregate)
2070        // SQLite would error on this too, but we'll catch it later during evaluation
2071        let schema = make_f1_f2_schema();
2072        let result = validate_having_aliased_aggregates(Some(&having_expr), &select_list, &schema);
2073        assert!(result.is_ok());
2074    }
2075
2076    #[test]
2077    fn test_having_without_aggregate_alias() {
2078        // SELECT count(*) FROM test1 GROUP BY f1 HAVING f1>0
2079        // No aliased aggregate, should pass
2080        let select_list = vec![SelectItem::Expression {
2081            expr: Expression::AggregateFunction {
2082                name: FunctionIdentifier::new("count"),
2083                distinct: false,
2084                args: vec![Expression::Wildcard],
2085                order_by: None,
2086                filter: None,
2087            },
2088            alias: None, // No alias
2089            source_text: None,
2090        }];
2091
2092        let having_expr = Expression::BinaryOp {
2093            op: BinaryOperator::GreaterThan,
2094            left: Box::new(Expression::ColumnRef(ColumnIdentifier::simple("f1", false))),
2095            right: Box::new(Expression::Literal(SqlValue::Integer(0))),
2096        };
2097
2098        let schema = make_f1_f2_schema();
2099        let result = validate_having_aliased_aggregates(Some(&having_expr), &select_list, &schema);
2100        assert!(result.is_ok());
2101    }
2102
2103    #[test]
2104    fn test_having_with_non_aggregate_alias() {
2105        // SELECT f1 AS x, count(*) FROM test1 GROUP BY f1 HAVING max(x)<10
2106        // 'x' is an alias for f1, not an aggregate - should pass
2107        let select_list = vec![
2108            SelectItem::Expression {
2109                expr: Expression::ColumnRef(ColumnIdentifier::simple("f1", false)),
2110                alias: Some("x".to_string()),
2111                source_text: None,
2112            },
2113            SelectItem::Expression {
2114                expr: Expression::AggregateFunction {
2115                    name: FunctionIdentifier::new("count"),
2116                    distinct: false,
2117                    args: vec![Expression::Wildcard],
2118                    order_by: None,
2119                    filter: None,
2120                },
2121                alias: None,
2122                source_text: None,
2123            },
2124        ];
2125
2126        // HAVING max(x)<10 - 'x' is not an aggregate alias
2127        let having_expr = Expression::BinaryOp {
2128            op: BinaryOperator::LessThan,
2129            left: Box::new(Expression::AggregateFunction {
2130                name: FunctionIdentifier::new("max"),
2131                distinct: false,
2132                args: vec![Expression::ColumnRef(ColumnIdentifier::simple("x", false))],
2133                order_by: None,
2134                filter: None,
2135            }),
2136            right: Box::new(Expression::Literal(SqlValue::Integer(10))),
2137        };
2138
2139        let schema = make_f1_f2_schema();
2140        let result = validate_having_aliased_aggregates(Some(&having_expr), &select_list, &schema);
2141        assert!(result.is_ok());
2142    }
2143
2144    #[test]
2145    fn test_having_alias_shadows_column_uses_column() {
2146        // SELECT - col2 * - AVG(-col2) AS col0 FROM tab0 GROUP BY col2 HAVING AVG(col0) IS NULL
2147        // The alias 'col0' happens to match the aggregate expression, but 'col0' is also
2148        // a real column in the table. In this case, the HAVING clause refers to the
2149        // actual column col0, NOT the alias. This should NOT be an error.
2150        let select_list = vec![SelectItem::Expression {
2151            expr: Expression::BinaryOp {
2152                op: BinaryOperator::Multiply,
2153                left: Box::new(Expression::UnaryOp {
2154                    op: UnaryOperator::Minus,
2155                    expr: Box::new(Expression::ColumnRef(ColumnIdentifier::simple("col2", false))),
2156                }),
2157                right: Box::new(Expression::UnaryOp {
2158                    op: UnaryOperator::Minus,
2159                    expr: Box::new(Expression::AggregateFunction {
2160                        name: FunctionIdentifier::new("AVG"),
2161                        distinct: false,
2162                        args: vec![Expression::UnaryOp {
2163                            op: UnaryOperator::Minus,
2164                            expr: Box::new(Expression::ColumnRef(ColumnIdentifier::simple(
2165                                "col2", false,
2166                            ))),
2167                        }],
2168                        order_by: None,
2169                        filter: None,
2170                    }),
2171                }),
2172            },
2173            alias: Some("col0".to_string()), // Alias matches a column name!
2174            source_text: None,
2175        }];
2176
2177        // HAVING AVG(col0) IS NULL - col0 is a real column, not the alias
2178        let having_expr = Expression::IsNull {
2179            expr: Box::new(Expression::AggregateFunction {
2180                name: FunctionIdentifier::new("AVG"),
2181                distinct: false,
2182                args: vec![Expression::ColumnRef(ColumnIdentifier::simple("col0", false))],
2183                order_by: None,
2184                filter: None,
2185            }),
2186            negated: false,
2187        };
2188
2189        // Schema with col0, col1, col2 - col0 exists as an actual column
2190        let columns = vec![
2191            ColumnSchema {
2192                name: "col0".to_string(),
2193                data_type: DataType::Integer,
2194                nullable: true,
2195                default_value: None,
2196                generated_expr: None,
2197                is_exact_integer_type: false,
2198                collation: None,
2199            },
2200            ColumnSchema {
2201                name: "col1".to_string(),
2202                data_type: DataType::Integer,
2203                nullable: true,
2204                default_value: None,
2205                generated_expr: None,
2206                is_exact_integer_type: false,
2207                collation: None,
2208            },
2209            ColumnSchema {
2210                name: "col2".to_string(),
2211                data_type: DataType::Integer,
2212                nullable: true,
2213                default_value: None,
2214                generated_expr: None,
2215                is_exact_integer_type: false,
2216                collation: None,
2217            },
2218        ];
2219        let table_schema = TableSchema::new("tab0".to_string(), columns);
2220        let schema = CombinedSchema::from_table("tab0".to_string(), table_schema);
2221
2222        // This should pass - col0 refers to the real column, not the alias
2223        let result = validate_having_aliased_aggregates(Some(&having_expr), &select_list, &schema);
2224        assert!(result.is_ok(), "Expected Ok but got {:?}", result);
2225    }
2226
2227    #[test]
2228    fn test_build_aggregate_aliases() {
2229        // Test the helper function
2230        let select_list = vec![
2231            SelectItem::Expression {
2232                expr: Expression::AggregateFunction {
2233                    name: FunctionIdentifier::new("min"),
2234                    distinct: false,
2235                    args: vec![Expression::ColumnRef(ColumnIdentifier::simple("f1", false))],
2236                    order_by: None,
2237                    filter: None,
2238                },
2239                alias: Some("m".to_string()),
2240                source_text: None,
2241            },
2242            SelectItem::Expression {
2243                expr: Expression::ColumnRef(ColumnIdentifier::simple("f2", false)),
2244                alias: Some("col2".to_string()),
2245                source_text: None,
2246            },
2247            SelectItem::Expression {
2248                // coalesce(min(f1)+5, 11) AS m2
2249                expr: Expression::Function {
2250                    name: FunctionIdentifier::new("coalesce"),
2251                    args: vec![
2252                        Expression::BinaryOp {
2253                            op: BinaryOperator::Plus,
2254                            left: Box::new(Expression::AggregateFunction {
2255                                name: FunctionIdentifier::new("min"),
2256                                distinct: false,
2257                                args: vec![Expression::ColumnRef(ColumnIdentifier::simple(
2258                                    "f1", false,
2259                                ))],
2260                                order_by: None,
2261                                filter: None,
2262                            }),
2263                            right: Box::new(Expression::Literal(SqlValue::Integer(5))),
2264                        },
2265                        Expression::Literal(SqlValue::Integer(11)),
2266                    ],
2267                    character_unit: None,
2268                },
2269                alias: Some("m2".to_string()),
2270                source_text: None,
2271            },
2272        ];
2273
2274        let aliases = build_aggregate_aliases(&select_list);
2275        assert!(aliases.contains("m")); // min(f1) AS m is an aggregate alias
2276        assert!(!aliases.contains("col2")); // f2 AS col2 is NOT an aggregate alias
2277        assert!(aliases.contains("m2")); // coalesce(min(f1)+5, 11) contains an aggregate
2278    }
2279
2280    // Helper to build a bare scalar subquery with a given SELECT list.
2281    fn make_bare_subquery(select_list: Vec<SelectItem>) -> Expression {
2282        Expression::ScalarSubquery(Box::new(vibesql_ast::SelectStmt {
2283            with_clause: None,
2284            distinct: false,
2285            select_list,
2286            into_table: None,
2287            into_variables: None,
2288            from: None,
2289            where_clause: None,
2290            group_by: None,
2291            having: None,
2292            window_definitions: None,
2293            order_by: None,
2294            limit: None,
2295            offset: None,
2296            set_operation: None,
2297            values: None,
2298        }))
2299    }
2300
2301    #[test]
2302    fn test_subquery_misuse_aggregate_with_outer_filter_ref() {
2303        // (SELECT AVG(0) FILTER(WHERE outer_col)) — bare subquery, FILTER ref outer col
2304        // Expected: misuse of aggregate: AVG()
2305        let inner_agg = Expression::AggregateFunction {
2306            name: FunctionIdentifier::new("AVG"),
2307            distinct: false,
2308            args: vec![Expression::Literal(SqlValue::Integer(0))],
2309            order_by: None,
2310            filter: Some(Box::new(Expression::ColumnRef(ColumnIdentifier::simple(
2311                "outer_col",
2312                false,
2313            )))),
2314        };
2315        let subquery = make_bare_subquery(vec![SelectItem::Expression {
2316            expr: inner_agg,
2317            alias: None,
2318            source_text: None,
2319        }]);
2320
2321        // WhereOrEqual context: SQLite reports "misuse of aggregate" here.
2322        let result = validate_subquery_context_misuse(&subquery, SubqueryContext::WhereOrEqual);
2323        match result {
2324            Err(ExecutorError::MisuseOfAggregateContext { function_name }) => {
2325                assert_eq!(function_name, "AVG");
2326            }
2327            other => panic!("expected MisuseOfAggregateContext(AVG), got {:?}", other),
2328        }
2329
2330        // SelectList context (#5104): allowed — triggers implicit-collapse
2331        // instead of erroring.
2332        let result_select_list =
2333            validate_subquery_context_misuse(&subquery, SubqueryContext::SelectList);
2334        assert!(
2335            result_select_list.is_ok(),
2336            "SelectList context should allow outer-correlated aggregate, got {:?}",
2337            result_select_list
2338        );
2339    }
2340
2341    #[test]
2342    fn test_subquery_misuse_aggregate_with_outer_arg_ref() {
2343        // (SELECT sum(x)) — bare subquery, sum's arg is an outer column ref
2344        // Expected: misuse of aggregate: sum() in WhereOrEqual context;
2345        // allowed in SelectList context (#5104).
2346        let inner_agg = Expression::AggregateFunction {
2347            name: FunctionIdentifier::new("sum"),
2348            distinct: false,
2349            args: vec![Expression::ColumnRef(ColumnIdentifier::simple("x", false))],
2350            order_by: None,
2351            filter: None,
2352        };
2353        let subquery = make_bare_subquery(vec![SelectItem::Expression {
2354            expr: inner_agg,
2355            alias: None,
2356            source_text: None,
2357        }]);
2358
2359        let result = validate_subquery_context_misuse(&subquery, SubqueryContext::WhereOrEqual);
2360        match result {
2361            Err(ExecutorError::MisuseOfAggregateContext { function_name }) => {
2362                assert_eq!(function_name, "sum");
2363            }
2364            other => panic!("expected MisuseOfAggregateContext(sum), got {:?}", other),
2365        }
2366
2367        let result_select_list =
2368            validate_subquery_context_misuse(&subquery, SubqueryContext::SelectList);
2369        assert!(
2370            result_select_list.is_ok(),
2371            "SelectList context should allow outer-correlated aggregate, got {:?}",
2372            result_select_list
2373        );
2374    }
2375
2376    #[test]
2377    fn test_subquery_misuse_aggregate_with_constant_args_does_not_fire() {
2378        // (SELECT sum(0)) — bare subquery whose aggregate has no column refs.
2379        // Should NOT error in either context (no outer reference).
2380        let inner_agg = Expression::AggregateFunction {
2381            name: FunctionIdentifier::new("sum"),
2382            distinct: false,
2383            args: vec![Expression::Literal(SqlValue::Integer(0))],
2384            order_by: None,
2385            filter: None,
2386        };
2387        let subquery = make_bare_subquery(vec![SelectItem::Expression {
2388            expr: inner_agg,
2389            alias: None,
2390            source_text: None,
2391        }]);
2392
2393        let result = validate_subquery_context_misuse(&subquery, SubqueryContext::WhereOrEqual);
2394        assert!(result.is_ok(), "constant aggregate in bare subquery should be OK");
2395        let result_select =
2396            validate_subquery_context_misuse(&subquery, SubqueryContext::SelectList);
2397        assert!(result_select.is_ok(), "constant aggregate in SelectList should be OK");
2398    }
2399
2400    #[test]
2401    fn test_subquery_row_value_misused() {
2402        // (SELECT (a, b)) — bare subquery returning a row value where a scalar
2403        // is expected. Expected: row value misused — in BOTH contexts.
2404        let row_value = Expression::RowValueConstructor(vec![
2405            Expression::ColumnRef(ColumnIdentifier::simple("a", false)),
2406            Expression::ColumnRef(ColumnIdentifier::simple("b", false)),
2407        ]);
2408        let subquery = make_bare_subquery(vec![SelectItem::Expression {
2409            expr: row_value,
2410            alias: None,
2411            source_text: None,
2412        }]);
2413
2414        let result = validate_subquery_context_misuse(&subquery, SubqueryContext::WhereOrEqual);
2415        assert!(matches!(result, Err(ExecutorError::RowValueMisused)));
2416
2417        let result_select =
2418            validate_subquery_context_misuse(&subquery, SubqueryContext::SelectList);
2419        assert!(
2420            matches!(result_select, Err(ExecutorError::RowValueMisused)),
2421            "row value misuse should still error in SelectList, got {:?}",
2422            result_select
2423        );
2424    }
2425
2426    #[test]
2427    fn test_subquery_window_function_does_not_fire() {
2428        // (SELECT count(a) OVER ()) — bare subquery with a window function
2429        // referencing outer column. SQLite allows this; we should NOT flag it.
2430        let inner_window = Expression::WindowFunction {
2431            function: vibesql_ast::WindowFunctionSpec::Aggregate {
2432                name: FunctionIdentifier::new("count"),
2433                args: vec![Expression::ColumnRef(ColumnIdentifier::simple("a", false))],
2434                filter: None,
2435            },
2436            over: vibesql_ast::WindowSpec {
2437                base_window_name: None,
2438                partition_by: None,
2439                order_by: None,
2440                frame: None,
2441            },
2442        };
2443        let subquery = make_bare_subquery(vec![SelectItem::Expression {
2444            expr: inner_window,
2445            alias: None,
2446            source_text: None,
2447        }]);
2448
2449        let result = validate_subquery_context_misuse(&subquery, SubqueryContext::WhereOrEqual);
2450        assert!(
2451            result.is_ok(),
2452            "window function in bare subquery should not be flagged, got {:?}",
2453            result
2454        );
2455    }
2456
2457    // ----- validate_group_by_window_misuse (#5093) -----
2458
2459    /// Build a SELECT statement with a single FROM table reference and a given
2460    /// SELECT list / WHERE / GROUP BY for testing.
2461    fn make_select(
2462        select_list: Vec<SelectItem>,
2463        from_table: Option<&str>,
2464        where_clause: Option<Expression>,
2465        group_by: Option<vibesql_ast::GroupByClause>,
2466    ) -> vibesql_ast::SelectStmt {
2467        vibesql_ast::SelectStmt {
2468            with_clause: None,
2469            distinct: false,
2470            select_list,
2471            into_table: None,
2472            into_variables: None,
2473            from: from_table.map(|name| vibesql_ast::FromClause::Table {
2474                index_hint: None,
2475                name: name.to_string(),
2476                alias: None,
2477                column_aliases: None,
2478                quoted: false,
2479            }),
2480            where_clause,
2481            group_by,
2482            having: None,
2483            window_definitions: None,
2484            order_by: None,
2485            limit: None,
2486            offset: None,
2487            set_operation: None,
2488            values: None,
2489        }
2490    }
2491
2492    fn make_window_sum_a() -> Expression {
2493        Expression::WindowFunction {
2494            function: vibesql_ast::WindowFunctionSpec::Aggregate {
2495                name: FunctionIdentifier::new("sum"),
2496                args: vec![Expression::ColumnRef(ColumnIdentifier::simple("a", false))],
2497                filter: None,
2498            },
2499            over: vibesql_ast::WindowSpec {
2500                base_window_name: None,
2501                partition_by: None,
2502                order_by: None,
2503                frame: None,
2504            },
2505        }
2506    }
2507
2508    #[test]
2509    fn test_group_by_positional_resolves_to_window_function() {
2510        // SELECT sum(a) OVER() FROM t1 GROUP BY 1
2511        // Position 1 in select list is a window function — must error.
2512        let stmt = make_select(
2513            vec![SelectItem::Expression {
2514                expr: make_window_sum_a(),
2515                alias: None,
2516                source_text: None,
2517            }],
2518            Some("t1"),
2519            None,
2520            Some(vibesql_ast::GroupByClause::Simple(vec![Expression::Literal(SqlValue::Integer(
2521                1,
2522            ))])),
2523        );
2524
2525        let result = validate_group_by_window_misuse(&stmt);
2526        match result {
2527            Err(ExecutorError::MisuseOfWindowFunction { function_name }) => {
2528                assert_eq!(function_name, "sum");
2529            }
2530            other => panic!("expected MisuseOfWindowFunction(sum), got {:?}", other),
2531        }
2532    }
2533
2534    #[test]
2535    fn test_group_by_positional_non_window_does_not_fire() {
2536        // SELECT a FROM t1 GROUP BY 1
2537        // Position 1 is a plain column — must NOT error.
2538        let stmt = make_select(
2539            vec![SelectItem::Expression {
2540                expr: Expression::ColumnRef(ColumnIdentifier::simple("a", false)),
2541                alias: None,
2542                source_text: None,
2543            }],
2544            Some("t1"),
2545            None,
2546            Some(vibesql_ast::GroupByClause::Simple(vec![Expression::Literal(SqlValue::Integer(
2547                1,
2548            ))])),
2549        );
2550
2551        assert!(validate_group_by_window_misuse(&stmt).is_ok());
2552    }
2553
2554    #[test]
2555    fn test_group_by_alias_resolves_to_window_function() {
2556        // SELECT sum(a) OVER() AS w FROM t1 GROUP BY w
2557        // Alias 'w' refers to a window function — must error.
2558        let stmt = make_select(
2559            vec![SelectItem::Expression {
2560                expr: make_window_sum_a(),
2561                alias: Some("w".to_string()),
2562                source_text: None,
2563            }],
2564            Some("t1"),
2565            None,
2566            Some(vibesql_ast::GroupByClause::Simple(vec![Expression::ColumnRef(
2567                ColumnIdentifier::simple("w", false),
2568            )])),
2569        );
2570
2571        let result = validate_group_by_window_misuse(&stmt);
2572        match result {
2573            Err(ExecutorError::MisuseOfWindowFunction { function_name }) => {
2574                assert_eq!(function_name, "sum");
2575            }
2576            other => panic!("expected MisuseOfWindowFunction(sum), got {:?}", other),
2577        }
2578    }
2579
2580    #[test]
2581    fn test_group_by_window_in_nested_subquery() {
2582        // window1.test 47.2 shape:
2583        // SELECT 234 FROM t2 WHERE k=1 OR (SELECT k FROM t2 WHERE
2584        //     (SELECT sum(a) OVER() FROM t1 GROUP BY 1));
2585        // The deepest subquery's GROUP BY 1 resolves to a window function.
2586        let inner_subquery = Expression::ScalarSubquery(Box::new(make_select(
2587            vec![SelectItem::Expression {
2588                expr: make_window_sum_a(),
2589                alias: None,
2590                source_text: None,
2591            }],
2592            Some("t1"),
2593            None,
2594            Some(vibesql_ast::GroupByClause::Simple(vec![Expression::Literal(SqlValue::Integer(
2595                1,
2596            ))])),
2597        )));
2598
2599        let middle_subquery = Expression::ScalarSubquery(Box::new(make_select(
2600            vec![SelectItem::Expression {
2601                expr: Expression::ColumnRef(ColumnIdentifier::simple("k", false)),
2602                alias: None,
2603                source_text: None,
2604            }],
2605            Some("t2"),
2606            Some(inner_subquery),
2607            None,
2608        )));
2609
2610        let outer = make_select(
2611            vec![SelectItem::Expression {
2612                expr: Expression::Literal(SqlValue::Integer(234)),
2613                alias: None,
2614                source_text: None,
2615            }],
2616            Some("t2"),
2617            Some(Expression::Disjunction(vec![
2618                Expression::BinaryOp {
2619                    op: BinaryOperator::Equal,
2620                    left: Box::new(Expression::ColumnRef(ColumnIdentifier::simple("k", false))),
2621                    right: Box::new(Expression::Literal(SqlValue::Integer(1))),
2622                },
2623                middle_subquery,
2624            ])),
2625            None,
2626        );
2627
2628        let result = validate_group_by_window_misuse(&outer);
2629        match result {
2630            Err(ExecutorError::MisuseOfWindowFunction { function_name }) => {
2631                assert_eq!(function_name, "sum");
2632            }
2633            other => {
2634                panic!("expected MisuseOfWindowFunction(sum) from nested subquery, got {:?}", other)
2635            }
2636        }
2637    }
2638}