1use std::collections::HashSet;
9
10use vibesql_ast::{Expression, SelectItem};
11
12use crate::{errors::ExecutorError, schema::CombinedSchema};
13
14pub 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
20enum WalkAction<T> {
25 Stop(T),
27 Skip,
30 Descend,
32}
33
34fn 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 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 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 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
165pub 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 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 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 if is_aggregate_function(name.as_str()) {
220 let upper = name.to_uppercase();
221 let arg_count = args.len();
222
223 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 if arg_count > 1 {
237 Some(name.display().to_string())
238 } else {
239 None
240 }
241 }
242 "MIN" | "MAX" => {
243 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 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
316pub 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 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 Expression::WindowFunction { .. } => WalkAction::Skip,
332 Expression::Glob { .. }
335 | Expression::RowValueConstructor(_)
336 | Expression::Collate { .. } => WalkAction::Skip,
337 _ => WalkAction::Descend,
338 })
339}
340
341pub fn find_nested_aggregate(expr: &Expression) -> Option<String> {
348 match expr {
349 Expression::AggregateFunction { args, order_by, .. } => {
350 for arg in args {
352 if let Some(inner_name) = find_aggregate_in_expression(arg) {
353 return Some(inner_name);
354 }
355 }
356 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 if is_aggregate_function(name.as_str()) {
370 let upper = name.to_uppercase();
371 let is_scalar_minmax = matches!(upper.as_str(), "MIN" | "MAX") && args.len() > 1;
373 if !is_scalar_minmax {
374 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 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
444pub 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
463pub 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
481pub 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 if expression_contains_aggregate(expr) {
492 aliases.insert(alias_name.to_lowercase());
494 }
495 }
496 }
497
498 aliases
499}
500
501pub 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 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 Expression::WindowFunction { .. } => WalkAction::Skip,
517 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
531pub 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 Expression::Glob { .. }
544 | Expression::RowValueConstructor(_)
545 | Expression::Collate { .. } => WalkAction::Skip,
546 _ => WalkAction::Descend,
547 })
548}
549
550fn 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 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 let is_agg = is_aggregate_function(name.as_str());
585 let upper = name.to_uppercase();
586 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 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 schema_columns.contains(&column.to_lowercase()) {
613 return None; }
615
616 if inside_aggregate && aggregate_aliases.contains(&column.to_lowercase()) {
617 Some(column.to_string())
619 } else {
620 None
621 }
622 }
623 Expression::ColumnRef(_) => None, 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 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 _ => None,
854 }
855}
856
857pub 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 let aggregate_aliases = build_aggregate_aliases(select_list);
876
877 if aggregate_aliases.is_empty() {
878 return Ok(()); }
880
881 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 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
901pub 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 aliases.insert(alias_name.to_lowercase());
913 }
914 }
915 }
916
917 aliases
918}
919
920fn 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 _ => WalkAction::Skip,
939 })
940 .is_some()
941}
942
943pub 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
974fn 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 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
1012pub 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 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
1076fn 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 let inner_columns = collect_from_clause_column_names(stmt.from.as_ref(), database)?;
1090
1091 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 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
1132fn 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 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 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 FromClause::Subquery { .. } | FromClause::Values { .. } => false,
1175 }
1176}
1177
1178fn 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 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 _ => None,
1272 }
1273}
1274
1275fn 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
1294fn 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 inner_columns.contains(&col_name) {
1311 return false;
1312 }
1313 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 _ => false,
1352 }
1353}
1354
1355fn expression_contains_column_ref(expr: &Expression) -> bool {
1365 walk_first(expr, &|e| match e {
1366 Expression::ColumnRef(_) => WalkAction::Stop(()),
1367 Expression::AggregateFunction { args, .. } => {
1370 if args.iter().any(expression_contains_column_ref) {
1371 WalkAction::Stop(())
1372 } else {
1373 WalkAction::Skip
1374 }
1375 }
1376 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 Expression::Glob { .. } | Expression::Collate { .. } | Expression::MatchAgainst { .. } => {
1394 WalkAction::Skip
1395 }
1396 _ => WalkAction::Descend,
1397 })
1398 .is_some()
1399}
1400
1401fn 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 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 WalkAction::Descend
1434 }
1435 }
1436 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1480pub enum SubqueryContext {
1481 WhereOrEqual,
1484 SelectList,
1488}
1489
1490pub fn validate_subquery_context_misuse(
1527 expr: &Expression,
1528 context: SubqueryContext,
1529) -> Result<(), ExecutorError> {
1530 match expr {
1531 Expression::ScalarSubquery(stmt) => {
1532 let mut arm: &vibesql_ast::SelectStmt = stmt;
1538 loop {
1539 if arm.from.is_none() {
1541 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 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 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 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 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 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 _ => Ok(()),
1699 }
1700}
1701
1702pub fn validate_group_by_window_misuse(
1714 stmt: &vibesql_ast::SelectStmt,
1715) -> Result<(), ExecutorError> {
1716 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 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 if let Some(from) = stmt.from.as_ref() {
1759 walk_from_for_subquery_group_by(from)?;
1760 }
1761
1762 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
1772fn 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
1876fn 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 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 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"); }
1948
1949 #[test]
1950 fn test_max_star_invalid() {
1951 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"); }
1963
1964 #[test]
1965 fn test_min_no_args_invalid() {
1966 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"); }
1978
1979 #[test]
1980 fn test_validate_aggregate_arguments() {
1981 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 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 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 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 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 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 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 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, 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 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 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 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()), source_text: None,
2175 }];
2176
2177 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 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 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 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 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")); assert!(!aliases.contains("col2")); assert!(aliases.contains("m2")); }
2279
2280 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 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 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 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 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 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 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 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 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 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 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 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 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}