use std::collections::HashSet;
use vibesql_ast::{Expression, SelectItem};
use crate::{errors::ExecutorError, schema::CombinedSchema};
pub fn is_aggregate_function(name: &str) -> bool {
let upper = name.to_uppercase();
matches!(upper.as_str(), "COUNT" | "SUM" | "AVG" | "MIN" | "MAX" | "TOTAL" | "GROUP_CONCAT")
}
enum WalkAction<T> {
Stop(T),
Skip,
Descend,
}
fn walk_first<T>(expr: &Expression, f: &impl Fn(&Expression) -> WalkAction<T>) -> Option<T> {
match f(expr) {
WalkAction::Stop(t) => return Some(t),
WalkAction::Skip => return None,
WalkAction::Descend => {}
}
match expr {
Expression::BinaryOp { left, right, .. } => {
walk_first(left, f).or_else(|| walk_first(right, f))
}
Expression::Conjunction(children) | Expression::Disjunction(children) => {
children.iter().find_map(|c| walk_first(c, f))
}
Expression::UnaryOp { expr, .. } => walk_first(expr, f),
Expression::Function { args, .. } => args.iter().find_map(|a| walk_first(a, f)),
Expression::AggregateFunction { args, order_by, filter, .. } => args
.iter()
.find_map(|a| walk_first(a, f))
.or_else(|| {
order_by
.as_ref()
.and_then(|items| items.iter().find_map(|item| walk_first(&item.expr, f)))
})
.or_else(|| filter.as_deref().and_then(|fexpr| walk_first(fexpr, f))),
Expression::IsNull { expr, .. } => walk_first(expr, f),
Expression::IsDistinctFrom { left, right, .. } => {
walk_first(left, f).or_else(|| walk_first(right, f))
}
Expression::IsTruthValue { expr, .. } => walk_first(expr, f),
Expression::Case { operand, when_clauses, else_result } => operand
.as_deref()
.and_then(|op| walk_first(op, f))
.or_else(|| {
when_clauses.iter().find_map(|w| {
w.conditions
.iter()
.find_map(|c| walk_first(c, f))
.or_else(|| walk_first(&w.result, f))
})
})
.or_else(|| else_result.as_deref().and_then(|e| walk_first(e, f))),
Expression::In { expr, .. } => walk_first(expr, f),
Expression::InList { expr, values, .. } => {
walk_first(expr, f).or_else(|| values.iter().find_map(|v| walk_first(v, f)))
}
Expression::Between { expr, low, high, .. } => {
walk_first(expr, f).or_else(|| walk_first(low, f)).or_else(|| walk_first(high, f))
}
Expression::Cast { expr, .. } => walk_first(expr, f),
Expression::Position { substring, string, .. } => {
walk_first(substring, f).or_else(|| walk_first(string, f))
}
Expression::Trim { removal_char, string, .. } => removal_char
.as_deref()
.and_then(|rc| walk_first(rc, f))
.or_else(|| walk_first(string, f)),
Expression::Extract { expr, .. } => walk_first(expr, f),
Expression::Like { expr, pattern, escape, .. }
| Expression::Glob { expr, pattern, escape, .. } => walk_first(expr, f)
.or_else(|| walk_first(pattern, f))
.or_else(|| escape.as_deref().and_then(|e| walk_first(e, f))),
Expression::QuantifiedComparison { expr, .. } => walk_first(expr, f),
Expression::Interval { value, .. } => walk_first(value, f),
Expression::WindowFunction { function, .. } => {
match function {
vibesql_ast::WindowFunctionSpec::Aggregate { args, filter, .. } => args
.iter()
.find_map(|a| walk_first(a, f))
.or_else(|| filter.as_deref().and_then(|fx| walk_first(fx, f))),
vibesql_ast::WindowFunctionSpec::Ranking { args, .. }
| vibesql_ast::WindowFunctionSpec::Value { args, .. } => {
args.iter().find_map(|a| walk_first(a, f))
}
}
}
Expression::MatchAgainst { search_modifier, .. } => walk_first(search_modifier, f),
Expression::RowValueConstructor(children) => children.iter().find_map(|c| walk_first(c, f)),
Expression::Collate { expr, .. } => walk_first(expr, f),
Expression::Raise { error_message, .. } => {
error_message.as_deref().and_then(|msg| walk_first(msg, f))
}
Expression::ScalarSubquery(_)
| Expression::Exists { .. }
| Expression::Literal(_)
| Expression::Placeholder(_)
| Expression::NumberedPlaceholder(_)
| Expression::NamedPlaceholder(_)
| Expression::ColumnRef(_)
| Expression::Wildcard
| Expression::CurrentDate
| Expression::CurrentTime { .. }
| Expression::CurrentTimestamp { .. }
| Expression::Default
| Expression::DuplicateKeyValue { .. }
| Expression::NextValue { .. }
| Expression::PseudoVariable { .. }
| Expression::SessionVariable { .. } => None,
}
}
pub fn check_aggregate_arg_count(expr: &Expression) -> Option<String> {
match expr {
Expression::AggregateFunction { name, args, distinct, .. } => {
let upper = name.to_uppercase();
let arg_count = args.len();
let has_wildcard = args.iter().any(|arg| {
let is_wildcard = matches!(arg, Expression::Wildcard);
let is_star_ref = matches!(
arg,
Expression::ColumnRef(col_id) if col_id.schema_canonical().is_none() && col_id.table_canonical().is_none() && col_id.column_canonical() == "*"
);
is_wildcard || is_star_ref
});
match upper.as_str() {
"COUNT" => {
if arg_count > 1 && !*distinct {
Some(name.display().to_string())
} else {
None
}
}
"MIN" | "MAX" => {
if has_wildcard || arg_count == 0 {
Some(name.display().to_string())
} else {
None
}
}
"SUM" | "AVG" | "TOTAL" => {
if has_wildcard || arg_count == 0 || arg_count > 1 {
Some(name.display().to_string())
} else {
None
}
}
"GROUP_CONCAT" => {
if arg_count == 0 || arg_count > 2 {
Some(name.display().to_string())
} else {
None
}
}
_ => None,
}
}
Expression::Function { name, args, .. } => {
if is_aggregate_function(name.as_str()) {
let upper = name.to_uppercase();
let arg_count = args.len();
let has_wildcard = args.iter().any(|arg| {
matches!(arg, Expression::Wildcard)
|| matches!(
arg,
Expression::ColumnRef(col_id) if col_id.schema_canonical().is_none() && col_id.table_canonical().is_none() && col_id.column_canonical() == "*"
)
});
match upper.as_str() {
"COUNT" => {
if arg_count > 1 {
Some(name.display().to_string())
} else {
None
}
}
"MIN" | "MAX" => {
if arg_count <= 1 && (has_wildcard || arg_count == 0) {
Some(name.display().to_string())
} else {
None
}
}
"SUM" | "AVG" | "TOTAL" => {
if has_wildcard || arg_count == 0 || arg_count > 1 {
Some(name.display().to_string())
} else {
None
}
}
"GROUP_CONCAT" => {
if arg_count == 0 || arg_count > 2 {
Some(name.display().to_string())
} else {
None
}
}
_ => None,
}
} else {
for arg in args {
if let Some(found) = check_aggregate_arg_count(arg) {
return Some(found);
}
}
None
}
}
Expression::BinaryOp { left, right, .. } => {
check_aggregate_arg_count(left).or_else(|| check_aggregate_arg_count(right))
}
Expression::UnaryOp { expr, .. } => check_aggregate_arg_count(expr),
Expression::Case { operand, when_clauses, else_result } => {
if let Some(op) = operand {
if let Some(found) = check_aggregate_arg_count(op) {
return Some(found);
}
}
for case_when in when_clauses {
for cond in &case_when.conditions {
if let Some(found) = check_aggregate_arg_count(cond) {
return Some(found);
}
}
if let Some(found) = check_aggregate_arg_count(&case_when.result) {
return Some(found);
}
}
if let Some(else_expr) = else_result {
check_aggregate_arg_count(else_expr)
} else {
None
}
}
Expression::IsNull { expr, .. } => check_aggregate_arg_count(expr),
Expression::Cast { expr, .. } => check_aggregate_arg_count(expr),
Expression::Conjunction(children) | Expression::Disjunction(children) => {
for child in children {
if let Some(found) = check_aggregate_arg_count(child) {
return Some(found);
}
}
None
}
_ => None,
}
}
pub fn find_aggregate_in_expression(expr: &Expression) -> Option<String> {
walk_first(expr, &|e| match e {
Expression::AggregateFunction { name, .. } => WalkAction::Stop(name.to_string()),
Expression::Function { name, args, .. } if is_aggregate_function(name.as_str()) => {
let upper = name.to_uppercase();
if matches!(upper.as_str(), "MIN" | "MAX") && args.len() > 1 {
WalkAction::Descend
} else {
WalkAction::Stop(name.to_string())
}
}
Expression::WindowFunction { .. } => WalkAction::Skip,
Expression::Glob { .. }
| Expression::RowValueConstructor(_)
| Expression::Collate { .. } => WalkAction::Skip,
_ => WalkAction::Descend,
})
}
pub fn find_nested_aggregate(expr: &Expression) -> Option<String> {
match expr {
Expression::AggregateFunction { args, order_by, .. } => {
for arg in args {
if let Some(inner_name) = find_aggregate_in_expression(arg) {
return Some(inner_name);
}
}
if let Some(order_items) = order_by {
for item in order_items {
if let Some(inner_name) = find_aggregate_in_expression(&item.expr) {
return Some(inner_name);
}
}
}
None
}
Expression::Function { name, args, .. } => {
if is_aggregate_function(name.as_str()) {
let upper = name.to_uppercase();
let is_scalar_minmax = matches!(upper.as_str(), "MIN" | "MAX") && args.len() > 1;
if !is_scalar_minmax {
for arg in args {
if let Some(inner_name) = find_aggregate_in_expression(arg) {
return Some(inner_name);
}
}
}
}
for arg in args {
if let Some(found) = find_nested_aggregate(arg) {
return Some(found);
}
}
None
}
Expression::BinaryOp { left, right, .. } => {
find_nested_aggregate(left).or_else(|| find_nested_aggregate(right))
}
Expression::UnaryOp { expr, .. } => find_nested_aggregate(expr),
Expression::Cast { expr, .. } => find_nested_aggregate(expr),
Expression::Case { operand, when_clauses, else_result } => {
if let Some(op) = operand {
if let Some(found) = find_nested_aggregate(op) {
return Some(found);
}
}
for case_when in when_clauses {
for cond in &case_when.conditions {
if let Some(found) = find_nested_aggregate(cond) {
return Some(found);
}
}
if let Some(found) = find_nested_aggregate(&case_when.result) {
return Some(found);
}
}
if let Some(else_expr) = else_result {
find_nested_aggregate(else_expr)
} else {
None
}
}
Expression::IsNull { expr, .. } => find_nested_aggregate(expr),
Expression::Between { expr, low, high, .. } => find_nested_aggregate(expr)
.or_else(|| find_nested_aggregate(low))
.or_else(|| find_nested_aggregate(high)),
Expression::InList { expr, values, .. } => {
if let Some(found) = find_nested_aggregate(expr) {
return Some(found);
}
for val in values {
if let Some(found) = find_nested_aggregate(val) {
return Some(found);
}
}
None
}
Expression::Conjunction(children) | Expression::Disjunction(children) => {
for child in children {
if let Some(found) = find_nested_aggregate(child) {
return Some(found);
}
}
None
}
_ => None,
}
}
pub fn validate_aggregate_arguments(select_list: &[SelectItem]) -> Result<(), ExecutorError> {
for item in select_list {
if let SelectItem::Expression { expr, .. } = item {
if let Some(agg_name) = check_aggregate_arg_count(expr) {
return Err(ExecutorError::WrongNumberOfArguments { function_name: agg_name });
}
}
}
Ok(())
}
pub fn validate_no_nested_aggregates(select_list: &[SelectItem]) -> Result<(), ExecutorError> {
for item in select_list {
if let SelectItem::Expression { expr, .. } = item {
if let Some(inner_agg_name) = find_nested_aggregate(expr) {
return Err(ExecutorError::MisuseOfAggregate { function_name: inner_agg_name });
}
}
}
Ok(())
}
pub fn build_aggregate_aliases(select_list: &[SelectItem]) -> HashSet<String> {
let mut aliases = HashSet::new();
for item in select_list {
if let SelectItem::Expression { expr, alias: Some(alias_name), .. } = item {
if expression_contains_aggregate(expr) {
aliases.insert(alias_name.to_lowercase());
}
}
}
aliases
}
pub fn expression_contains_aggregate(expr: &Expression) -> bool {
walk_first(expr, &|e| match e {
Expression::AggregateFunction { .. } => WalkAction::Stop(()),
Expression::Function { name, args, .. } if is_aggregate_function(name.as_str()) => {
let upper = name.to_uppercase();
if matches!(upper.as_str(), "MIN" | "MAX") && args.len() > 1 {
WalkAction::Descend
} else {
WalkAction::Stop(())
}
}
Expression::WindowFunction { .. } => WalkAction::Skip,
Expression::Glob { .. }
| Expression::IsDistinctFrom { .. }
| Expression::IsTruthValue { .. }
| Expression::QuantifiedComparison { .. }
| Expression::RowValueConstructor(_)
| Expression::MatchAgainst { .. }
| Expression::Collate { .. } => WalkAction::Skip,
_ => WalkAction::Descend,
})
.is_some()
}
pub fn find_window_function_in_expression(expr: &Expression) -> Option<String> {
walk_first(expr, &|e| match e {
Expression::WindowFunction { function, .. } => WalkAction::Stop(function.name()),
Expression::Glob { .. }
| Expression::RowValueConstructor(_)
| Expression::Collate { .. } => WalkAction::Skip,
_ => WalkAction::Descend,
})
}
fn find_aliased_aggregate_misuse_in_expression(
expr: &Expression,
aggregate_aliases: &HashSet<String>,
schema_columns: &HashSet<String>,
inside_aggregate: bool,
) -> Option<String> {
match expr {
Expression::AggregateFunction { args, .. } => {
for arg in args {
if let Some(alias) = find_aliased_aggregate_misuse_in_expression(
arg,
aggregate_aliases,
schema_columns,
true,
) {
return Some(alias);
}
}
None
}
Expression::Function { name, args, .. } => {
let is_agg = is_aggregate_function(name.as_str());
let upper = name.to_uppercase();
let effectively_aggregate =
is_agg && !(matches!(upper.as_str(), "MIN" | "MAX") && args.len() > 1);
let new_inside_aggregate = inside_aggregate || effectively_aggregate;
for arg in args {
if let Some(alias) = find_aliased_aggregate_misuse_in_expression(
arg,
aggregate_aliases,
schema_columns,
new_inside_aggregate,
) {
return Some(alias);
}
}
None
}
Expression::ColumnRef(col_id)
if col_id.schema_canonical().is_none() && col_id.table_canonical().is_none() =>
{
let column = col_id.column_canonical();
if schema_columns.contains(&column.to_lowercase()) {
return None; }
if inside_aggregate && aggregate_aliases.contains(&column.to_lowercase()) {
Some(column.to_string())
} else {
None
}
}
Expression::ColumnRef(_) => None, Expression::BinaryOp { left, right, .. } => find_aliased_aggregate_misuse_in_expression(
left,
aggregate_aliases,
schema_columns,
inside_aggregate,
)
.or_else(|| {
find_aliased_aggregate_misuse_in_expression(
right,
aggregate_aliases,
schema_columns,
inside_aggregate,
)
}),
Expression::UnaryOp { expr, .. } => find_aliased_aggregate_misuse_in_expression(
expr,
aggregate_aliases,
schema_columns,
inside_aggregate,
),
Expression::Cast { expr, .. } => find_aliased_aggregate_misuse_in_expression(
expr,
aggregate_aliases,
schema_columns,
inside_aggregate,
),
Expression::Case { operand, when_clauses, else_result } => {
if let Some(op) = operand {
if let Some(alias) = find_aliased_aggregate_misuse_in_expression(
op,
aggregate_aliases,
schema_columns,
inside_aggregate,
) {
return Some(alias);
}
}
for when_clause in when_clauses {
for cond in &when_clause.conditions {
if let Some(alias) = find_aliased_aggregate_misuse_in_expression(
cond,
aggregate_aliases,
schema_columns,
inside_aggregate,
) {
return Some(alias);
}
}
if let Some(alias) = find_aliased_aggregate_misuse_in_expression(
&when_clause.result,
aggregate_aliases,
schema_columns,
inside_aggregate,
) {
return Some(alias);
}
}
if let Some(else_expr) = else_result {
return find_aliased_aggregate_misuse_in_expression(
else_expr,
aggregate_aliases,
schema_columns,
inside_aggregate,
);
}
None
}
Expression::IsNull { expr, .. } => find_aliased_aggregate_misuse_in_expression(
expr,
aggregate_aliases,
schema_columns,
inside_aggregate,
),
Expression::Between { expr, low, high, .. } => find_aliased_aggregate_misuse_in_expression(
expr,
aggregate_aliases,
schema_columns,
inside_aggregate,
)
.or_else(|| {
find_aliased_aggregate_misuse_in_expression(
low,
aggregate_aliases,
schema_columns,
inside_aggregate,
)
})
.or_else(|| {
find_aliased_aggregate_misuse_in_expression(
high,
aggregate_aliases,
schema_columns,
inside_aggregate,
)
}),
Expression::InList { expr, values, .. } => {
if let Some(alias) = find_aliased_aggregate_misuse_in_expression(
expr,
aggregate_aliases,
schema_columns,
inside_aggregate,
) {
return Some(alias);
}
for val in values {
if let Some(alias) = find_aliased_aggregate_misuse_in_expression(
val,
aggregate_aliases,
schema_columns,
inside_aggregate,
) {
return Some(alias);
}
}
None
}
Expression::In { expr, .. } => find_aliased_aggregate_misuse_in_expression(
expr,
aggregate_aliases,
schema_columns,
inside_aggregate,
),
Expression::Like { expr, pattern, .. } => find_aliased_aggregate_misuse_in_expression(
expr,
aggregate_aliases,
schema_columns,
inside_aggregate,
)
.or_else(|| {
find_aliased_aggregate_misuse_in_expression(
pattern,
aggregate_aliases,
schema_columns,
inside_aggregate,
)
}),
Expression::Position { substring, string, .. } => {
find_aliased_aggregate_misuse_in_expression(
substring,
aggregate_aliases,
schema_columns,
inside_aggregate,
)
.or_else(|| {
find_aliased_aggregate_misuse_in_expression(
string,
aggregate_aliases,
schema_columns,
inside_aggregate,
)
})
}
Expression::Trim { removal_char, string, .. } => {
if let Some(rc) = removal_char {
if let Some(alias) = find_aliased_aggregate_misuse_in_expression(
rc,
aggregate_aliases,
schema_columns,
inside_aggregate,
) {
return Some(alias);
}
}
find_aliased_aggregate_misuse_in_expression(
string,
aggregate_aliases,
schema_columns,
inside_aggregate,
)
}
Expression::Extract { expr, .. } => find_aliased_aggregate_misuse_in_expression(
expr,
aggregate_aliases,
schema_columns,
inside_aggregate,
),
Expression::Interval { value, .. } => find_aliased_aggregate_misuse_in_expression(
value,
aggregate_aliases,
schema_columns,
inside_aggregate,
),
Expression::Conjunction(children) | Expression::Disjunction(children) => {
for child in children {
if let Some(alias) = find_aliased_aggregate_misuse_in_expression(
child,
aggregate_aliases,
schema_columns,
inside_aggregate,
) {
return Some(alias);
}
}
None
}
Expression::ScalarSubquery(_) | Expression::Exists { .. } => None,
Expression::QuantifiedComparison { expr, .. } => {
find_aliased_aggregate_misuse_in_expression(
expr,
aggregate_aliases,
schema_columns,
inside_aggregate,
)
}
Expression::IsDistinctFrom { left, right, .. } => {
find_aliased_aggregate_misuse_in_expression(
left,
aggregate_aliases,
schema_columns,
inside_aggregate,
)
.or_else(|| {
find_aliased_aggregate_misuse_in_expression(
right,
aggregate_aliases,
schema_columns,
inside_aggregate,
)
})
}
Expression::IsTruthValue { expr, .. } => find_aliased_aggregate_misuse_in_expression(
expr,
aggregate_aliases,
schema_columns,
inside_aggregate,
),
_ => None,
}
}
pub fn validate_having_aliased_aggregates(
having_clause: Option<&Expression>,
select_list: &[SelectItem],
schema: &CombinedSchema,
) -> Result<(), ExecutorError> {
let Some(having_expr) = having_clause else {
return Ok(());
};
let aggregate_aliases = build_aggregate_aliases(select_list);
if aggregate_aliases.is_empty() {
return Ok(()); }
let schema_columns: HashSet<String> = schema
.table_schemas
.values()
.flat_map(|(_, table_schema)| table_schema.columns.iter().map(|c| c.name.to_lowercase()))
.collect();
if let Some(alias_name) = find_aliased_aggregate_misuse_in_expression(
having_expr,
&aggregate_aliases,
&schema_columns,
false,
) {
return Err(ExecutorError::MisuseOfAliasedAggregate { alias_name });
}
Ok(())
}
pub fn build_window_aliases(select_list: &[SelectItem]) -> HashSet<String> {
let mut aliases = HashSet::new();
for item in select_list {
if let SelectItem::Expression { expr, alias: Some(alias_name), .. } = item {
if expression_contains_window_function(expr) {
aliases.insert(alias_name.to_lowercase());
}
}
}
aliases
}
fn expression_contains_window_function(expr: &Expression) -> bool {
walk_first(expr, &|e| match e {
Expression::WindowFunction { .. } => WalkAction::Stop(()),
Expression::BinaryOp { .. }
| Expression::UnaryOp { .. }
| Expression::Cast { .. }
| Expression::Function { .. }
| Expression::Case { .. } => WalkAction::Descend,
_ => WalkAction::Skip,
})
.is_some()
}
pub fn validate_order_by_aliased_window_functions(
order_by: Option<&[vibesql_ast::OrderByItem]>,
select_list: &[SelectItem],
) -> Result<(), ExecutorError> {
let Some(order_items) = order_by else {
return Ok(());
};
let window_aliases = build_window_aliases(select_list);
if window_aliases.is_empty() {
return Ok(());
}
for item in order_items {
if let Some(alias_name) =
find_aliased_window_misuse_in_order_by(&item.expr, &window_aliases)
{
return Err(ExecutorError::MisuseOfAliasedWindowFunction { alias_name });
}
}
Ok(())
}
fn find_aliased_window_misuse_in_order_by(
expr: &Expression,
window_aliases: &HashSet<String>,
) -> Option<String> {
match expr {
Expression::ScalarSubquery(select_stmt) => {
if select_stmt.select_list.len() == 1
&& select_stmt.from.is_none()
&& select_stmt.where_clause.is_none()
{
if let SelectItem::Expression { expr: Expression::ColumnRef(col_id), .. } =
&select_stmt.select_list[0]
{
if col_id.table_canonical().is_none() {
let col_name = col_id.column_canonical().to_lowercase();
if window_aliases.contains(&col_name) {
return Some(col_name);
}
}
}
}
None
}
Expression::BinaryOp { left, right, .. } => {
find_aliased_window_misuse_in_order_by(left, window_aliases)
.or_else(|| find_aliased_window_misuse_in_order_by(right, window_aliases))
}
Expression::UnaryOp { expr, .. } => {
find_aliased_window_misuse_in_order_by(expr, window_aliases)
}
_ => None,
}
}
pub fn validate_window_query_order_by_aggregates(
order_by: Option<&[vibesql_ast::OrderByItem]>,
select_list: &[SelectItem],
outer_schema: &CombinedSchema,
database: &vibesql_storage::Database,
) -> Result<(), ExecutorError> {
let Some(order_items) = order_by else {
return Ok(());
};
let has_window = select_list.iter().any(|item| match item {
SelectItem::Expression { expr, .. } => expression_contains_window_function(expr),
_ => false,
});
if !has_window {
return Ok(());
}
for item in order_items {
if let Some(name) =
find_outer_correlated_agg_in_order_by_subquery(&item.expr, outer_schema, database)
{
return Err(ExecutorError::MisuseOfAggregateContext { function_name: name });
}
}
Ok(())
}
fn find_outer_correlated_agg_in_order_by_subquery(
expr: &Expression,
outer_schema: &CombinedSchema,
database: &vibesql_storage::Database,
) -> Option<String> {
match expr {
Expression::ScalarSubquery(stmt) => {
let inner_columns = collect_from_clause_column_names(stmt.from.as_ref(), database)?;
for item in &stmt.select_list {
if let SelectItem::Expression { expr: inner, .. } = item {
if let Some(name) =
find_outer_correlated_aggregate(inner, &inner_columns, outer_schema)
{
return Some(name);
}
}
}
None
}
Expression::BinaryOp { left, right, .. } => {
find_outer_correlated_agg_in_order_by_subquery(left, outer_schema, database).or_else(
|| find_outer_correlated_agg_in_order_by_subquery(right, outer_schema, database),
)
}
Expression::UnaryOp { expr, .. } => {
find_outer_correlated_agg_in_order_by_subquery(expr, outer_schema, database)
}
Expression::Cast { expr, .. } => {
find_outer_correlated_agg_in_order_by_subquery(expr, outer_schema, database)
}
Expression::Function { args, .. } => {
for arg in args {
if let Some(name) =
find_outer_correlated_agg_in_order_by_subquery(arg, outer_schema, database)
{
return Some(name);
}
}
None
}
_ => None,
}
}
fn collect_from_clause_column_names(
from: Option<&vibesql_ast::FromClause>,
database: &vibesql_storage::Database,
) -> Option<HashSet<String>> {
let mut columns = HashSet::new();
let from = from?;
if collect_from_columns_recursive(from, database, &mut columns) {
Some(columns)
} else {
None
}
}
fn collect_from_columns_recursive(
from: &vibesql_ast::FromClause,
database: &vibesql_storage::Database,
columns: &mut HashSet<String>,
) -> bool {
use vibesql_ast::FromClause;
match from {
FromClause::Table { name, .. } => {
let table_name = name.rsplit_once('.').map(|(_, t)| t).unwrap_or(name.as_str());
if let Some(table) = database.get_table(table_name) {
for col in &table.schema.columns {
columns.insert(col.name.to_lowercase());
}
true
} else {
false
}
}
FromClause::Join { left, right, .. } => {
collect_from_columns_recursive(left, database, columns)
&& collect_from_columns_recursive(right, database, columns)
}
FromClause::Subquery { .. } | FromClause::Values { .. } => false,
}
}
fn find_outer_correlated_aggregate(
expr: &Expression,
inner_columns: &HashSet<String>,
outer_schema: &CombinedSchema,
) -> Option<String> {
match expr {
Expression::AggregateFunction { name, args, order_by, filter, .. } => {
if aggregate_args_correlate_outer(
args,
order_by.as_deref(),
filter.as_deref(),
inner_columns,
outer_schema,
) {
return Some(name.to_string());
}
for arg in args {
if let Some(found) =
find_outer_correlated_aggregate(arg, inner_columns, outer_schema)
{
return Some(found);
}
}
None
}
Expression::Function { name, args, .. } => {
if is_aggregate_function(name.as_str()) {
let upper = name.to_uppercase();
let is_scalar_minmax = matches!(upper.as_str(), "MIN" | "MAX") && args.len() > 1;
if !is_scalar_minmax {
if args
.iter()
.any(|a| column_ref_resolves_to_outer(a, inner_columns, outer_schema))
{
return Some(name.to_string());
}
}
}
for arg in args {
if let Some(found) =
find_outer_correlated_aggregate(arg, inner_columns, outer_schema)
{
return Some(found);
}
}
None
}
Expression::BinaryOp { left, right, .. } => {
find_outer_correlated_aggregate(left, inner_columns, outer_schema)
.or_else(|| find_outer_correlated_aggregate(right, inner_columns, outer_schema))
}
Expression::UnaryOp { expr, .. } => {
find_outer_correlated_aggregate(expr, inner_columns, outer_schema)
}
Expression::Cast { expr, .. } => {
find_outer_correlated_aggregate(expr, inner_columns, outer_schema)
}
Expression::Case { operand, when_clauses, else_result } => {
if let Some(op) = operand {
if let Some(found) =
find_outer_correlated_aggregate(op, inner_columns, outer_schema)
{
return Some(found);
}
}
for w in when_clauses {
for cond in &w.conditions {
if let Some(found) =
find_outer_correlated_aggregate(cond, inner_columns, outer_schema)
{
return Some(found);
}
}
if let Some(found) =
find_outer_correlated_aggregate(&w.result, inner_columns, outer_schema)
{
return Some(found);
}
}
if let Some(else_expr) = else_result {
find_outer_correlated_aggregate(else_expr, inner_columns, outer_schema)
} else {
None
}
}
_ => None,
}
}
fn aggregate_args_correlate_outer(
args: &[Expression],
order_by: Option<&[vibesql_ast::OrderByItem]>,
filter: Option<&Expression>,
inner_columns: &HashSet<String>,
outer_schema: &CombinedSchema,
) -> bool {
let arg_correlated =
args.iter().any(|a| column_ref_resolves_to_outer(a, inner_columns, outer_schema));
let order_correlated = order_by.is_some_and(|items| {
items.iter().any(|i| column_ref_resolves_to_outer(&i.expr, inner_columns, outer_schema))
});
let filter_correlated =
filter.is_some_and(|f| column_ref_resolves_to_outer(f, inner_columns, outer_schema));
arg_correlated || order_correlated || filter_correlated
}
fn column_ref_resolves_to_outer(
expr: &Expression,
inner_columns: &HashSet<String>,
outer_schema: &CombinedSchema,
) -> bool {
match expr {
Expression::ColumnRef(col_id) => {
let col_name = col_id.column_canonical().to_lowercase();
if inner_columns.contains(&col_name) {
return false;
}
outer_schema.has_column(&col_name)
}
Expression::BinaryOp { left, right, .. } => {
column_ref_resolves_to_outer(left, inner_columns, outer_schema)
|| column_ref_resolves_to_outer(right, inner_columns, outer_schema)
}
Expression::UnaryOp { expr, .. } => {
column_ref_resolves_to_outer(expr, inner_columns, outer_schema)
}
Expression::Cast { expr, .. } => {
column_ref_resolves_to_outer(expr, inner_columns, outer_schema)
}
Expression::Function { args, .. } | Expression::AggregateFunction { args, .. } => {
args.iter().any(|a| column_ref_resolves_to_outer(a, inner_columns, outer_schema))
}
Expression::Case { operand, when_clauses, else_result } => {
operand
.as_ref()
.is_some_and(|e| column_ref_resolves_to_outer(e, inner_columns, outer_schema))
|| when_clauses.iter().any(|w| {
w.conditions
.iter()
.any(|c| column_ref_resolves_to_outer(c, inner_columns, outer_schema))
|| column_ref_resolves_to_outer(&w.result, inner_columns, outer_schema)
})
|| else_result
.as_ref()
.is_some_and(|e| column_ref_resolves_to_outer(e, inner_columns, outer_schema))
}
Expression::IsNull { expr, .. } => {
column_ref_resolves_to_outer(expr, inner_columns, outer_schema)
}
Expression::Conjunction(children) | Expression::Disjunction(children) => {
children.iter().any(|c| column_ref_resolves_to_outer(c, inner_columns, outer_schema))
}
_ => false,
}
}
fn expression_contains_column_ref(expr: &Expression) -> bool {
walk_first(expr, &|e| match e {
Expression::ColumnRef(_) => WalkAction::Stop(()),
Expression::AggregateFunction { args, .. } => {
if args.iter().any(expression_contains_column_ref) {
WalkAction::Stop(())
} else {
WalkAction::Skip
}
}
Expression::WindowFunction { function, .. } => {
let args = match function {
vibesql_ast::WindowFunctionSpec::Aggregate { args, .. }
| vibesql_ast::WindowFunctionSpec::Ranking { args, .. }
| vibesql_ast::WindowFunctionSpec::Value { args, .. } => args,
};
if args.iter().any(expression_contains_column_ref) {
WalkAction::Stop(())
} else {
WalkAction::Skip
}
}
Expression::Glob { .. } | Expression::Collate { .. } | Expression::MatchAgainst { .. } => {
WalkAction::Skip
}
_ => WalkAction::Descend,
})
.is_some()
}
fn find_outer_referencing_aggregate(expr: &Expression) -> Option<String> {
walk_first(expr, &|e| match e {
Expression::AggregateFunction { name, args, order_by, filter, .. } => {
let arg_refs = args.iter().any(expression_contains_column_ref);
let order_refs = order_by.as_ref().is_some_and(|items| {
items.iter().any(|item| expression_contains_column_ref(&item.expr))
});
let filter_refs = filter.as_ref().is_some_and(|f| expression_contains_column_ref(f));
if arg_refs || order_refs || filter_refs {
WalkAction::Stop(name.to_string())
} else {
args.iter()
.find_map(find_outer_referencing_aggregate)
.map_or(WalkAction::Skip, WalkAction::Stop)
}
}
Expression::Function { name, args, .. } if is_aggregate_function(name.as_str()) => {
let upper = name.to_uppercase();
let is_scalar_minmax = matches!(upper.as_str(), "MIN" | "MAX") && args.len() > 1;
if !is_scalar_minmax && args.iter().any(expression_contains_column_ref) {
WalkAction::Stop(name.to_string())
} else {
WalkAction::Descend
}
}
Expression::IsDistinctFrom { .. }
| Expression::IsTruthValue { .. }
| Expression::Position { .. }
| Expression::Trim { .. }
| Expression::Extract { .. }
| Expression::Interval { .. }
| Expression::QuantifiedComparison { .. }
| Expression::RowValueConstructor(_)
| Expression::MatchAgainst { .. }
| Expression::Collate { .. }
| Expression::Glob { .. }
| Expression::WindowFunction { .. } => WalkAction::Skip,
_ => WalkAction::Descend,
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SubqueryContext {
WhereOrEqual,
SelectList,
}
pub fn validate_subquery_context_misuse(
expr: &Expression,
context: SubqueryContext,
) -> Result<(), ExecutorError> {
match expr {
Expression::ScalarSubquery(stmt) => {
let mut arm: &vibesql_ast::SelectStmt = stmt;
loop {
if arm.from.is_none() {
if arm.select_list.len() == 1 {
if let SelectItem::Expression {
expr: Expression::RowValueConstructor(items),
..
} = &arm.select_list[0]
{
if items.len() > 1 {
return Err(ExecutorError::RowValueMisused);
}
}
}
if context == SubqueryContext::WhereOrEqual {
for item in &arm.select_list {
if let SelectItem::Expression { expr: inner, .. } = item {
if let Some(name) = find_outer_referencing_aggregate(inner) {
return Err(ExecutorError::MisuseOfAggregateContext {
function_name: name,
});
}
}
}
}
for item in &arm.select_list {
if let SelectItem::Expression { expr: inner, .. } = item {
validate_subquery_context_misuse(inner, context)?;
}
}
}
match arm.set_operation.as_ref() {
Some(set_op) => arm = &set_op.right,
None => break,
}
}
Ok(())
}
Expression::BinaryOp { left, right, .. } => {
validate_subquery_context_misuse(left, context)?;
validate_subquery_context_misuse(right, context)
}
Expression::UnaryOp { expr, .. } => validate_subquery_context_misuse(expr, context),
Expression::IsNull { expr, .. } => validate_subquery_context_misuse(expr, context),
Expression::IsDistinctFrom { left, right, .. } => {
validate_subquery_context_misuse(left, context)?;
validate_subquery_context_misuse(right, context)
}
Expression::IsTruthValue { expr, .. } => validate_subquery_context_misuse(expr, context),
Expression::Cast { expr, .. } => validate_subquery_context_misuse(expr, context),
Expression::Like { expr, pattern, .. } => {
validate_subquery_context_misuse(expr, context)?;
validate_subquery_context_misuse(pattern, context)
}
Expression::Between { expr, low, high, .. } => {
validate_subquery_context_misuse(expr, context)?;
validate_subquery_context_misuse(low, context)?;
validate_subquery_context_misuse(high, context)
}
Expression::InList { expr, values, .. } => {
validate_subquery_context_misuse(expr, context)?;
for v in values {
validate_subquery_context_misuse(v, context)?;
}
Ok(())
}
Expression::In { expr, .. } => validate_subquery_context_misuse(expr, context),
Expression::Case { operand, when_clauses, else_result } => {
if let Some(op) = operand {
validate_subquery_context_misuse(op, context)?;
}
for w in when_clauses {
for cond in &w.conditions {
validate_subquery_context_misuse(cond, context)?;
}
validate_subquery_context_misuse(&w.result, context)?;
}
if let Some(else_expr) = else_result {
validate_subquery_context_misuse(else_expr, context)?;
}
Ok(())
}
Expression::Function { args, .. } => {
for arg in args {
validate_subquery_context_misuse(arg, context)?;
}
Ok(())
}
Expression::AggregateFunction { args, filter, order_by, .. } => {
for arg in args {
validate_subquery_context_misuse(arg, SubqueryContext::WhereOrEqual)?;
}
if let Some(f) = filter {
validate_subquery_context_misuse(f, SubqueryContext::WhereOrEqual)?;
}
if let Some(items) = order_by {
for item in items {
validate_subquery_context_misuse(&item.expr, SubqueryContext::WhereOrEqual)?;
}
}
Ok(())
}
Expression::WindowFunction { function, .. } => {
let args = match function {
vibesql_ast::WindowFunctionSpec::Aggregate { args, .. }
| vibesql_ast::WindowFunctionSpec::Ranking { args, .. }
| vibesql_ast::WindowFunctionSpec::Value { args, .. } => args,
};
for a in args {
validate_subquery_context_misuse(a, SubqueryContext::WhereOrEqual)?;
}
Ok(())
}
Expression::QuantifiedComparison { expr, .. } => {
validate_subquery_context_misuse(expr, context)
}
Expression::Conjunction(children) | Expression::Disjunction(children) => {
for child in children {
validate_subquery_context_misuse(child, context)?;
}
Ok(())
}
Expression::RowValueConstructor(children) => {
for child in children {
validate_subquery_context_misuse(child, context)?;
}
Ok(())
}
_ => Ok(()),
}
}
pub fn validate_group_by_window_misuse(
stmt: &vibesql_ast::SelectStmt,
) -> Result<(), ExecutorError> {
if let Some(ref group_by_clause) = stmt.group_by {
for (term_index, group_expr) in group_by_clause.all_expressions().iter().enumerate() {
if let Some(window_name) = find_window_function_in_expression(group_expr) {
return Err(ExecutorError::MisuseOfWindowFunction { function_name: window_name });
}
let resolved = crate::select::grouping::resolve_group_by_alias(
group_expr,
&stmt.select_list,
term_index,
)?;
if let Some(window_name) = find_window_function_in_expression(&resolved) {
return Err(ExecutorError::MisuseOfWindowFunction { function_name: window_name });
}
}
}
for item in &stmt.select_list {
if let SelectItem::Expression { expr, .. } = item {
walk_expr_for_subquery_group_by(expr)?;
}
}
if let Some(where_expr) = stmt.where_clause.as_ref() {
walk_expr_for_subquery_group_by(where_expr)?;
}
if let Some(having_expr) = stmt.having.as_ref() {
walk_expr_for_subquery_group_by(having_expr)?;
}
if let Some(group_by) = stmt.group_by.as_ref() {
for expr in group_by.all_expressions() {
walk_expr_for_subquery_group_by(expr)?;
}
}
if let Some(order_by) = stmt.order_by.as_ref() {
for item in order_by {
walk_expr_for_subquery_group_by(&item.expr)?;
}
}
if let Some(from) = stmt.from.as_ref() {
walk_from_for_subquery_group_by(from)?;
}
if let Some(ctes) = stmt.with_clause.as_ref() {
for cte in ctes {
validate_group_by_window_misuse(&cte.query)?;
}
}
Ok(())
}
fn walk_expr_for_subquery_group_by(expr: &Expression) -> Result<(), ExecutorError> {
match expr {
Expression::ScalarSubquery(stmt) => validate_group_by_window_misuse(stmt),
Expression::Exists { subquery, .. } => validate_group_by_window_misuse(subquery),
Expression::BinaryOp { left, right, .. } => {
walk_expr_for_subquery_group_by(left)?;
walk_expr_for_subquery_group_by(right)
}
Expression::UnaryOp { expr, .. } => walk_expr_for_subquery_group_by(expr),
Expression::IsNull { expr, .. } => walk_expr_for_subquery_group_by(expr),
Expression::IsDistinctFrom { left, right, .. } => {
walk_expr_for_subquery_group_by(left)?;
walk_expr_for_subquery_group_by(right)
}
Expression::IsTruthValue { expr, .. } => walk_expr_for_subquery_group_by(expr),
Expression::Cast { expr, .. } => walk_expr_for_subquery_group_by(expr),
Expression::Like { expr, pattern, .. } => {
walk_expr_for_subquery_group_by(expr)?;
walk_expr_for_subquery_group_by(pattern)
}
Expression::Between { expr, low, high, .. } => {
walk_expr_for_subquery_group_by(expr)?;
walk_expr_for_subquery_group_by(low)?;
walk_expr_for_subquery_group_by(high)
}
Expression::InList { expr, values, .. } => {
walk_expr_for_subquery_group_by(expr)?;
for v in values {
walk_expr_for_subquery_group_by(v)?;
}
Ok(())
}
Expression::In { expr, subquery, .. } => {
walk_expr_for_subquery_group_by(expr)?;
validate_group_by_window_misuse(subquery)
}
Expression::Case { operand, when_clauses, else_result } => {
if let Some(op) = operand {
walk_expr_for_subquery_group_by(op)?;
}
for w in when_clauses {
for cond in &w.conditions {
walk_expr_for_subquery_group_by(cond)?;
}
walk_expr_for_subquery_group_by(&w.result)?;
}
if let Some(else_expr) = else_result {
walk_expr_for_subquery_group_by(else_expr)?;
}
Ok(())
}
Expression::Function { args, .. } => {
for arg in args {
walk_expr_for_subquery_group_by(arg)?;
}
Ok(())
}
Expression::AggregateFunction { args, filter, order_by, .. } => {
for arg in args {
walk_expr_for_subquery_group_by(arg)?;
}
if let Some(f) = filter {
walk_expr_for_subquery_group_by(f)?;
}
if let Some(items) = order_by {
for item in items {
walk_expr_for_subquery_group_by(&item.expr)?;
}
}
Ok(())
}
Expression::WindowFunction { function, .. } => {
let args = match function {
vibesql_ast::WindowFunctionSpec::Aggregate { args, .. }
| vibesql_ast::WindowFunctionSpec::Ranking { args, .. }
| vibesql_ast::WindowFunctionSpec::Value { args, .. } => args,
};
for a in args {
walk_expr_for_subquery_group_by(a)?;
}
Ok(())
}
Expression::QuantifiedComparison { expr, subquery, .. } => {
walk_expr_for_subquery_group_by(expr)?;
validate_group_by_window_misuse(subquery)
}
Expression::Conjunction(children) | Expression::Disjunction(children) => {
for child in children {
walk_expr_for_subquery_group_by(child)?;
}
Ok(())
}
Expression::RowValueConstructor(children) => {
for child in children {
walk_expr_for_subquery_group_by(child)?;
}
Ok(())
}
_ => Ok(()),
}
}
fn walk_from_for_subquery_group_by(from: &vibesql_ast::FromClause) -> Result<(), ExecutorError> {
match from {
vibesql_ast::FromClause::Table { .. } => Ok(()),
vibesql_ast::FromClause::Join { left, right, condition, .. } => {
walk_from_for_subquery_group_by(left)?;
walk_from_for_subquery_group_by(right)?;
if let Some(expr) = condition {
walk_expr_for_subquery_group_by(expr)?;
}
Ok(())
}
vibesql_ast::FromClause::Subquery { query, .. } => validate_group_by_window_misuse(query),
vibesql_ast::FromClause::Values { rows, .. } => {
for row in rows {
for expr in row {
walk_expr_for_subquery_group_by(expr)?;
}
}
Ok(())
}
}
}
#[cfg(test)]
mod tests {
use vibesql_ast::{BinaryOperator, ColumnIdentifier, FunctionIdentifier, UnaryOperator};
use vibesql_catalog::{ColumnSchema, TableSchema};
use vibesql_types::{DataType, SqlValue};
use super::*;
fn make_f1_f2_schema() -> CombinedSchema {
let columns = vec![
ColumnSchema {
name: "f1".to_string(),
data_type: DataType::Integer,
nullable: true,
default_value: None,
generated_expr: None,
is_exact_integer_type: false,
collation: None,
},
ColumnSchema {
name: "f2".to_string(),
data_type: DataType::Integer,
nullable: true,
default_value: None,
generated_expr: None,
is_exact_integer_type: false,
collation: None,
},
];
let table_schema = TableSchema::new("test1".to_string(), columns);
CombinedSchema::from_table("test1".to_string(), table_schema)
}
#[test]
fn test_min_star_invalid() {
let expr = Expression::AggregateFunction {
name: FunctionIdentifier::new("MIN"),
distinct: false,
args: vec![Expression::ColumnRef(ColumnIdentifier::simple("*", false))],
order_by: None,
filter: None,
};
let result = check_aggregate_arg_count(&expr);
assert!(result.is_some(), "MIN(*) should be invalid");
assert_eq!(result.unwrap(), "MIN"); }
#[test]
fn test_max_star_invalid() {
let expr = Expression::AggregateFunction {
name: FunctionIdentifier::new("MAX"),
distinct: false,
args: vec![Expression::ColumnRef(ColumnIdentifier::simple("*", false))],
order_by: None,
filter: None,
};
let result = check_aggregate_arg_count(&expr);
assert!(result.is_some(), "MAX(*) should be invalid");
assert_eq!(result.unwrap(), "MAX"); }
#[test]
fn test_min_no_args_invalid() {
let expr = Expression::AggregateFunction {
name: FunctionIdentifier::new("MIN"),
distinct: false,
args: vec![],
order_by: None,
filter: None,
};
let result = check_aggregate_arg_count(&expr);
assert!(result.is_some(), "MIN() should be invalid");
assert_eq!(result.unwrap(), "MIN"); }
#[test]
fn test_validate_aggregate_arguments() {
let select_list = vec![SelectItem::Expression {
expr: Expression::AggregateFunction {
name: FunctionIdentifier::new("MIN"),
distinct: false,
args: vec![Expression::ColumnRef(ColumnIdentifier::simple("*", false))],
order_by: None,
filter: None,
},
alias: None,
source_text: None,
}];
let result = validate_aggregate_arguments(&select_list);
assert!(result.is_err());
}
#[test]
fn test_having_with_aliased_aggregate_inside_aggregate() {
let select_list = vec![SelectItem::Expression {
expr: Expression::AggregateFunction {
name: FunctionIdentifier::new("min"),
distinct: false,
args: vec![Expression::ColumnRef(ColumnIdentifier::simple("f1", false))],
order_by: None,
filter: None,
},
alias: Some("m".to_string()),
source_text: None,
}];
let having_expr = Expression::BinaryOp {
op: BinaryOperator::LessThan,
left: Box::new(Expression::AggregateFunction {
name: FunctionIdentifier::new("max"),
distinct: false,
args: vec![Expression::BinaryOp {
op: BinaryOperator::Plus,
left: Box::new(Expression::ColumnRef(ColumnIdentifier::simple("m", false))),
right: Box::new(Expression::Literal(SqlValue::Integer(5))),
}],
order_by: None,
filter: None,
}),
right: Box::new(Expression::Literal(SqlValue::Integer(10))),
};
let schema = make_f1_f2_schema();
let result = validate_having_aliased_aggregates(Some(&having_expr), &select_list, &schema);
assert!(result.is_err());
match result {
Err(ExecutorError::MisuseOfAliasedAggregate { alias_name }) => {
assert_eq!(alias_name, "m");
}
_ => panic!("Expected MisuseOfAliasedAggregate error"),
}
}
#[test]
fn test_having_with_aggregate_alias_not_inside_aggregate() {
let select_list = vec![SelectItem::Expression {
expr: Expression::AggregateFunction {
name: FunctionIdentifier::new("min"),
distinct: false,
args: vec![Expression::ColumnRef(ColumnIdentifier::simple("f1", false))],
order_by: None,
filter: None,
},
alias: Some("m".to_string()),
source_text: None,
}];
let having_expr = Expression::BinaryOp {
op: BinaryOperator::GreaterThan,
left: Box::new(Expression::ColumnRef(ColumnIdentifier::simple("m", false))),
right: Box::new(Expression::Literal(SqlValue::Integer(0))),
};
let schema = make_f1_f2_schema();
let result = validate_having_aliased_aggregates(Some(&having_expr), &select_list, &schema);
assert!(result.is_ok());
}
#[test]
fn test_having_without_aggregate_alias() {
let select_list = vec![SelectItem::Expression {
expr: Expression::AggregateFunction {
name: FunctionIdentifier::new("count"),
distinct: false,
args: vec![Expression::Wildcard],
order_by: None,
filter: None,
},
alias: None, source_text: None,
}];
let having_expr = Expression::BinaryOp {
op: BinaryOperator::GreaterThan,
left: Box::new(Expression::ColumnRef(ColumnIdentifier::simple("f1", false))),
right: Box::new(Expression::Literal(SqlValue::Integer(0))),
};
let schema = make_f1_f2_schema();
let result = validate_having_aliased_aggregates(Some(&having_expr), &select_list, &schema);
assert!(result.is_ok());
}
#[test]
fn test_having_with_non_aggregate_alias() {
let select_list = vec![
SelectItem::Expression {
expr: Expression::ColumnRef(ColumnIdentifier::simple("f1", false)),
alias: Some("x".to_string()),
source_text: None,
},
SelectItem::Expression {
expr: Expression::AggregateFunction {
name: FunctionIdentifier::new("count"),
distinct: false,
args: vec![Expression::Wildcard],
order_by: None,
filter: None,
},
alias: None,
source_text: None,
},
];
let having_expr = Expression::BinaryOp {
op: BinaryOperator::LessThan,
left: Box::new(Expression::AggregateFunction {
name: FunctionIdentifier::new("max"),
distinct: false,
args: vec![Expression::ColumnRef(ColumnIdentifier::simple("x", false))],
order_by: None,
filter: None,
}),
right: Box::new(Expression::Literal(SqlValue::Integer(10))),
};
let schema = make_f1_f2_schema();
let result = validate_having_aliased_aggregates(Some(&having_expr), &select_list, &schema);
assert!(result.is_ok());
}
#[test]
fn test_having_alias_shadows_column_uses_column() {
let select_list = vec![SelectItem::Expression {
expr: Expression::BinaryOp {
op: BinaryOperator::Multiply,
left: Box::new(Expression::UnaryOp {
op: UnaryOperator::Minus,
expr: Box::new(Expression::ColumnRef(ColumnIdentifier::simple("col2", false))),
}),
right: Box::new(Expression::UnaryOp {
op: UnaryOperator::Minus,
expr: Box::new(Expression::AggregateFunction {
name: FunctionIdentifier::new("AVG"),
distinct: false,
args: vec![Expression::UnaryOp {
op: UnaryOperator::Minus,
expr: Box::new(Expression::ColumnRef(ColumnIdentifier::simple(
"col2", false,
))),
}],
order_by: None,
filter: None,
}),
}),
},
alias: Some("col0".to_string()), source_text: None,
}];
let having_expr = Expression::IsNull {
expr: Box::new(Expression::AggregateFunction {
name: FunctionIdentifier::new("AVG"),
distinct: false,
args: vec![Expression::ColumnRef(ColumnIdentifier::simple("col0", false))],
order_by: None,
filter: None,
}),
negated: false,
};
let columns = vec![
ColumnSchema {
name: "col0".to_string(),
data_type: DataType::Integer,
nullable: true,
default_value: None,
generated_expr: None,
is_exact_integer_type: false,
collation: None,
},
ColumnSchema {
name: "col1".to_string(),
data_type: DataType::Integer,
nullable: true,
default_value: None,
generated_expr: None,
is_exact_integer_type: false,
collation: None,
},
ColumnSchema {
name: "col2".to_string(),
data_type: DataType::Integer,
nullable: true,
default_value: None,
generated_expr: None,
is_exact_integer_type: false,
collation: None,
},
];
let table_schema = TableSchema::new("tab0".to_string(), columns);
let schema = CombinedSchema::from_table("tab0".to_string(), table_schema);
let result = validate_having_aliased_aggregates(Some(&having_expr), &select_list, &schema);
assert!(result.is_ok(), "Expected Ok but got {:?}", result);
}
#[test]
fn test_build_aggregate_aliases() {
let select_list = vec![
SelectItem::Expression {
expr: Expression::AggregateFunction {
name: FunctionIdentifier::new("min"),
distinct: false,
args: vec![Expression::ColumnRef(ColumnIdentifier::simple("f1", false))],
order_by: None,
filter: None,
},
alias: Some("m".to_string()),
source_text: None,
},
SelectItem::Expression {
expr: Expression::ColumnRef(ColumnIdentifier::simple("f2", false)),
alias: Some("col2".to_string()),
source_text: None,
},
SelectItem::Expression {
expr: Expression::Function {
name: FunctionIdentifier::new("coalesce"),
args: vec![
Expression::BinaryOp {
op: BinaryOperator::Plus,
left: Box::new(Expression::AggregateFunction {
name: FunctionIdentifier::new("min"),
distinct: false,
args: vec![Expression::ColumnRef(ColumnIdentifier::simple(
"f1", false,
))],
order_by: None,
filter: None,
}),
right: Box::new(Expression::Literal(SqlValue::Integer(5))),
},
Expression::Literal(SqlValue::Integer(11)),
],
character_unit: None,
},
alias: Some("m2".to_string()),
source_text: None,
},
];
let aliases = build_aggregate_aliases(&select_list);
assert!(aliases.contains("m")); assert!(!aliases.contains("col2")); assert!(aliases.contains("m2")); }
fn make_bare_subquery(select_list: Vec<SelectItem>) -> Expression {
Expression::ScalarSubquery(Box::new(vibesql_ast::SelectStmt {
with_clause: None,
distinct: false,
select_list,
into_table: None,
into_variables: None,
from: None,
where_clause: None,
group_by: None,
having: None,
window_definitions: None,
order_by: None,
limit: None,
offset: None,
set_operation: None,
values: None,
}))
}
#[test]
fn test_subquery_misuse_aggregate_with_outer_filter_ref() {
let inner_agg = Expression::AggregateFunction {
name: FunctionIdentifier::new("AVG"),
distinct: false,
args: vec![Expression::Literal(SqlValue::Integer(0))],
order_by: None,
filter: Some(Box::new(Expression::ColumnRef(ColumnIdentifier::simple(
"outer_col",
false,
)))),
};
let subquery = make_bare_subquery(vec![SelectItem::Expression {
expr: inner_agg,
alias: None,
source_text: None,
}]);
let result = validate_subquery_context_misuse(&subquery, SubqueryContext::WhereOrEqual);
match result {
Err(ExecutorError::MisuseOfAggregateContext { function_name }) => {
assert_eq!(function_name, "AVG");
}
other => panic!("expected MisuseOfAggregateContext(AVG), got {:?}", other),
}
let result_select_list =
validate_subquery_context_misuse(&subquery, SubqueryContext::SelectList);
assert!(
result_select_list.is_ok(),
"SelectList context should allow outer-correlated aggregate, got {:?}",
result_select_list
);
}
#[test]
fn test_subquery_misuse_aggregate_with_outer_arg_ref() {
let inner_agg = Expression::AggregateFunction {
name: FunctionIdentifier::new("sum"),
distinct: false,
args: vec![Expression::ColumnRef(ColumnIdentifier::simple("x", false))],
order_by: None,
filter: None,
};
let subquery = make_bare_subquery(vec![SelectItem::Expression {
expr: inner_agg,
alias: None,
source_text: None,
}]);
let result = validate_subquery_context_misuse(&subquery, SubqueryContext::WhereOrEqual);
match result {
Err(ExecutorError::MisuseOfAggregateContext { function_name }) => {
assert_eq!(function_name, "sum");
}
other => panic!("expected MisuseOfAggregateContext(sum), got {:?}", other),
}
let result_select_list =
validate_subquery_context_misuse(&subquery, SubqueryContext::SelectList);
assert!(
result_select_list.is_ok(),
"SelectList context should allow outer-correlated aggregate, got {:?}",
result_select_list
);
}
#[test]
fn test_subquery_misuse_aggregate_with_constant_args_does_not_fire() {
let inner_agg = Expression::AggregateFunction {
name: FunctionIdentifier::new("sum"),
distinct: false,
args: vec![Expression::Literal(SqlValue::Integer(0))],
order_by: None,
filter: None,
};
let subquery = make_bare_subquery(vec![SelectItem::Expression {
expr: inner_agg,
alias: None,
source_text: None,
}]);
let result = validate_subquery_context_misuse(&subquery, SubqueryContext::WhereOrEqual);
assert!(result.is_ok(), "constant aggregate in bare subquery should be OK");
let result_select =
validate_subquery_context_misuse(&subquery, SubqueryContext::SelectList);
assert!(result_select.is_ok(), "constant aggregate in SelectList should be OK");
}
#[test]
fn test_subquery_row_value_misused() {
let row_value = Expression::RowValueConstructor(vec![
Expression::ColumnRef(ColumnIdentifier::simple("a", false)),
Expression::ColumnRef(ColumnIdentifier::simple("b", false)),
]);
let subquery = make_bare_subquery(vec![SelectItem::Expression {
expr: row_value,
alias: None,
source_text: None,
}]);
let result = validate_subquery_context_misuse(&subquery, SubqueryContext::WhereOrEqual);
assert!(matches!(result, Err(ExecutorError::RowValueMisused)));
let result_select =
validate_subquery_context_misuse(&subquery, SubqueryContext::SelectList);
assert!(
matches!(result_select, Err(ExecutorError::RowValueMisused)),
"row value misuse should still error in SelectList, got {:?}",
result_select
);
}
#[test]
fn test_subquery_window_function_does_not_fire() {
let inner_window = Expression::WindowFunction {
function: vibesql_ast::WindowFunctionSpec::Aggregate {
name: FunctionIdentifier::new("count"),
args: vec![Expression::ColumnRef(ColumnIdentifier::simple("a", false))],
filter: None,
},
over: vibesql_ast::WindowSpec {
base_window_name: None,
partition_by: None,
order_by: None,
frame: None,
},
};
let subquery = make_bare_subquery(vec![SelectItem::Expression {
expr: inner_window,
alias: None,
source_text: None,
}]);
let result = validate_subquery_context_misuse(&subquery, SubqueryContext::WhereOrEqual);
assert!(
result.is_ok(),
"window function in bare subquery should not be flagged, got {:?}",
result
);
}
fn make_select(
select_list: Vec<SelectItem>,
from_table: Option<&str>,
where_clause: Option<Expression>,
group_by: Option<vibesql_ast::GroupByClause>,
) -> vibesql_ast::SelectStmt {
vibesql_ast::SelectStmt {
with_clause: None,
distinct: false,
select_list,
into_table: None,
into_variables: None,
from: from_table.map(|name| vibesql_ast::FromClause::Table {
index_hint: None,
name: name.to_string(),
alias: None,
column_aliases: None,
quoted: false,
}),
where_clause,
group_by,
having: None,
window_definitions: None,
order_by: None,
limit: None,
offset: None,
set_operation: None,
values: None,
}
}
fn make_window_sum_a() -> Expression {
Expression::WindowFunction {
function: vibesql_ast::WindowFunctionSpec::Aggregate {
name: FunctionIdentifier::new("sum"),
args: vec![Expression::ColumnRef(ColumnIdentifier::simple("a", false))],
filter: None,
},
over: vibesql_ast::WindowSpec {
base_window_name: None,
partition_by: None,
order_by: None,
frame: None,
},
}
}
#[test]
fn test_group_by_positional_resolves_to_window_function() {
let stmt = make_select(
vec![SelectItem::Expression {
expr: make_window_sum_a(),
alias: None,
source_text: None,
}],
Some("t1"),
None,
Some(vibesql_ast::GroupByClause::Simple(vec![Expression::Literal(SqlValue::Integer(
1,
))])),
);
let result = validate_group_by_window_misuse(&stmt);
match result {
Err(ExecutorError::MisuseOfWindowFunction { function_name }) => {
assert_eq!(function_name, "sum");
}
other => panic!("expected MisuseOfWindowFunction(sum), got {:?}", other),
}
}
#[test]
fn test_group_by_positional_non_window_does_not_fire() {
let stmt = make_select(
vec![SelectItem::Expression {
expr: Expression::ColumnRef(ColumnIdentifier::simple("a", false)),
alias: None,
source_text: None,
}],
Some("t1"),
None,
Some(vibesql_ast::GroupByClause::Simple(vec![Expression::Literal(SqlValue::Integer(
1,
))])),
);
assert!(validate_group_by_window_misuse(&stmt).is_ok());
}
#[test]
fn test_group_by_alias_resolves_to_window_function() {
let stmt = make_select(
vec![SelectItem::Expression {
expr: make_window_sum_a(),
alias: Some("w".to_string()),
source_text: None,
}],
Some("t1"),
None,
Some(vibesql_ast::GroupByClause::Simple(vec![Expression::ColumnRef(
ColumnIdentifier::simple("w", false),
)])),
);
let result = validate_group_by_window_misuse(&stmt);
match result {
Err(ExecutorError::MisuseOfWindowFunction { function_name }) => {
assert_eq!(function_name, "sum");
}
other => panic!("expected MisuseOfWindowFunction(sum), got {:?}", other),
}
}
#[test]
fn test_group_by_window_in_nested_subquery() {
let inner_subquery = Expression::ScalarSubquery(Box::new(make_select(
vec![SelectItem::Expression {
expr: make_window_sum_a(),
alias: None,
source_text: None,
}],
Some("t1"),
None,
Some(vibesql_ast::GroupByClause::Simple(vec![Expression::Literal(SqlValue::Integer(
1,
))])),
)));
let middle_subquery = Expression::ScalarSubquery(Box::new(make_select(
vec![SelectItem::Expression {
expr: Expression::ColumnRef(ColumnIdentifier::simple("k", false)),
alias: None,
source_text: None,
}],
Some("t2"),
Some(inner_subquery),
None,
)));
let outer = make_select(
vec![SelectItem::Expression {
expr: Expression::Literal(SqlValue::Integer(234)),
alias: None,
source_text: None,
}],
Some("t2"),
Some(Expression::Disjunction(vec![
Expression::BinaryOp {
op: BinaryOperator::Equal,
left: Box::new(Expression::ColumnRef(ColumnIdentifier::simple("k", false))),
right: Box::new(Expression::Literal(SqlValue::Integer(1))),
},
middle_subquery,
])),
None,
);
let result = validate_group_by_window_misuse(&outer);
match result {
Err(ExecutorError::MisuseOfWindowFunction { function_name }) => {
assert_eq!(function_name, "sum");
}
other => {
panic!("expected MisuseOfWindowFunction(sum) from nested subquery, got {:?}", other)
}
}
}
}