use std::collections::HashSet;
use super::super::builder::SelectExecutor;
fn expression_contains_outer_aggregate_collapse(
expr: &vibesql_ast::Expression,
database: &vibesql_storage::Database,
) -> bool {
use vibesql_ast::Expression;
match expr {
Expression::ScalarSubquery(stmt) => subquery_stmt_triggers_collapse(stmt, database),
Expression::BinaryOp { left, right, .. } => {
expression_contains_outer_aggregate_collapse(left, database)
|| expression_contains_outer_aggregate_collapse(right, database)
}
Expression::UnaryOp { expr, .. } => {
expression_contains_outer_aggregate_collapse(expr, database)
}
Expression::Cast { expr, .. } => {
expression_contains_outer_aggregate_collapse(expr, database)
}
Expression::IsNull { expr, .. } => {
expression_contains_outer_aggregate_collapse(expr, database)
}
Expression::Like { expr, pattern, .. } => {
expression_contains_outer_aggregate_collapse(expr, database)
|| expression_contains_outer_aggregate_collapse(pattern, database)
}
Expression::Between { expr, low, high, .. } => {
expression_contains_outer_aggregate_collapse(expr, database)
|| expression_contains_outer_aggregate_collapse(low, database)
|| expression_contains_outer_aggregate_collapse(high, database)
}
Expression::InList { expr, values, .. } => {
expression_contains_outer_aggregate_collapse(expr, database)
|| values.iter().any(|v| expression_contains_outer_aggregate_collapse(v, database))
}
Expression::In { expr, subquery, .. } => {
expression_contains_outer_aggregate_collapse(expr, database)
|| subquery_stmt_triggers_collapse(subquery, database)
}
Expression::Case { operand, when_clauses, else_result } => {
operand
.as_ref()
.is_some_and(|e| expression_contains_outer_aggregate_collapse(e, database))
|| when_clauses.iter().any(|w| {
w.conditions
.iter()
.any(|c| expression_contains_outer_aggregate_collapse(c, database))
|| expression_contains_outer_aggregate_collapse(&w.result, database)
})
|| else_result
.as_ref()
.is_some_and(|e| expression_contains_outer_aggregate_collapse(e, database))
}
Expression::Function { args, .. } => {
args.iter().any(|a| expression_contains_outer_aggregate_collapse(a, database))
}
Expression::AggregateFunction { args, filter, order_by, .. } => {
args.iter().any(|a| expression_contains_outer_aggregate_collapse(a, database))
|| filter
.as_ref()
.is_some_and(|f| expression_contains_outer_aggregate_collapse(f, database))
|| order_by.as_ref().is_some_and(|items| {
items
.iter()
.any(|i| expression_contains_outer_aggregate_collapse(&i.expr, database))
})
}
Expression::WindowFunction { function, over, .. } => {
let args = match function {
vibesql_ast::WindowFunctionSpec::Aggregate { args, .. }
| vibesql_ast::WindowFunctionSpec::Ranking { args, .. }
| vibesql_ast::WindowFunctionSpec::Value { args, .. } => args,
};
if args.iter().any(|a| expression_contains_outer_aggregate_collapse(a, database)) {
return true;
}
if let Some(partition_by) = &over.partition_by {
if partition_by
.iter()
.any(|p| expression_contains_outer_aggregate_collapse(p, database))
{
return true;
}
}
if let Some(order_by) = &over.order_by {
if order_by
.iter()
.any(|item| expression_contains_outer_aggregate_collapse(&item.expr, database))
{
return true;
}
}
false
}
Expression::QuantifiedComparison { expr, .. } => {
expression_contains_outer_aggregate_collapse(expr, database)
}
_ => false,
}
}
fn subquery_stmt_triggers_collapse(
stmt: &vibesql_ast::SelectStmt,
database: &vibesql_storage::Database,
) -> bool {
if stmt.from.is_none() {
for item in &stmt.select_list {
if let vibesql_ast::SelectItem::Expression { expr: inner, .. } = item {
if expr_has_column_referencing_aggregate(inner) {
return true;
}
if expression_contains_outer_aggregate_collapse(inner, database) {
return true;
}
}
}
return false;
}
scope_chain_contains_outer_correlated_aggregate(stmt, &[], database)
}
fn columns_from_from_clause(
from: &vibesql_ast::FromClause,
database: &vibesql_storage::Database,
) -> Option<HashSet<String>> {
use vibesql_ast::FromClause;
let mut out = HashSet::new();
match from {
FromClause::Table { name, .. } => {
let table = database.get_table(name)?;
for col in &table.schema.columns {
out.insert(col.name.to_ascii_lowercase());
}
Some(out)
}
FromClause::Join { left, right, .. } => {
let l = columns_from_from_clause(left, database)?;
let r = columns_from_from_clause(right, database)?;
out.extend(l);
out.extend(r);
Some(out)
}
FromClause::Subquery { query, column_aliases, .. } => {
if let Some(aliases) = column_aliases {
for a in aliases {
out.insert(a.to_ascii_lowercase());
}
return Some(out);
}
for item in &query.select_list {
if let vibesql_ast::SelectItem::Expression { expr, alias, .. } = item {
if let Some(a) = alias {
out.insert(a.to_ascii_lowercase());
} else if let vibesql_ast::Expression::ColumnRef(col) = expr {
out.insert(col.column_canonical().to_ascii_lowercase());
} else {
return None;
}
} else {
return None;
}
}
Some(out)
}
FromClause::Values { rows, column_aliases, .. } => {
if let Some(aliases) = column_aliases {
for a in aliases {
out.insert(a.to_ascii_lowercase());
}
Some(out)
} else {
let _ = rows;
None
}
}
}
}
fn scope_chain_contains_outer_correlated_aggregate(
stmt: &vibesql_ast::SelectStmt,
outer_inner_scopes: &[HashSet<String>],
database: &vibesql_storage::Database,
) -> bool {
let this_scope: Option<HashSet<String>> = match stmt.from.as_ref() {
Some(from) => columns_from_from_clause(from, database),
None => Some(HashSet::new()),
};
let this_scope = match this_scope {
Some(s) => s,
None => return false,
};
let mut extended: Vec<HashSet<String>> = outer_inner_scopes.to_vec();
extended.push(this_scope);
for item in &stmt.select_list {
if let vibesql_ast::SelectItem::Expression { expr, .. } = item {
if expression_in_scope_chain_has_outer_correlated_aggregate(expr, &extended, database) {
return true;
}
}
}
if let Some(w) = &stmt.where_clause {
if expression_in_scope_chain_has_outer_correlated_aggregate(w, &extended, database) {
return true;
}
}
if let Some(h) = &stmt.having {
if expression_in_scope_chain_has_outer_correlated_aggregate(h, &extended, database) {
return true;
}
}
if let Some(items) = &stmt.order_by {
for item in items {
if expression_in_scope_chain_has_outer_correlated_aggregate(
&item.expr, &extended, database,
) {
return true;
}
}
}
if let Some(group_by) = &stmt.group_by {
for expr in group_by.all_expressions() {
if expression_in_scope_chain_has_outer_correlated_aggregate(&expr, &extended, database)
{
return true;
}
}
}
if let Some(from) = &stmt.from {
if from_clause_has_outer_correlated_aggregate(from, &extended, database) {
return true;
}
}
false
}
fn from_clause_has_outer_correlated_aggregate(
from: &vibesql_ast::FromClause,
inner_scopes: &[HashSet<String>],
database: &vibesql_storage::Database,
) -> bool {
use vibesql_ast::FromClause;
match from {
FromClause::Table { .. } | FromClause::Values { .. } => false,
FromClause::Join { left, right, condition, .. } => {
from_clause_has_outer_correlated_aggregate(left, inner_scopes, database)
|| from_clause_has_outer_correlated_aggregate(right, inner_scopes, database)
|| condition.as_ref().is_some_and(|c| {
expression_in_scope_chain_has_outer_correlated_aggregate(
c,
inner_scopes,
database,
)
})
}
FromClause::Subquery { query, .. } => {
scope_chain_contains_outer_correlated_aggregate(query, inner_scopes, database)
}
}
}
fn expression_in_scope_chain_has_outer_correlated_aggregate(
expr: &vibesql_ast::Expression,
inner_scopes: &[HashSet<String>],
database: &vibesql_storage::Database,
) -> bool {
use vibesql_ast::Expression;
match expr {
Expression::AggregateFunction { args, filter, order_by, .. } => {
let has_outer =
args.iter().any(|a| expression_has_column_outside_scopes(a, inner_scopes))
|| filter
.as_ref()
.is_some_and(|f| expression_has_column_outside_scopes(f, inner_scopes))
|| order_by.as_ref().is_some_and(|items| {
items
.iter()
.any(|i| expression_has_column_outside_scopes(&i.expr, inner_scopes))
});
let has_inner = args
.iter()
.any(|a| expression_has_column_inside_scopes(a, inner_scopes))
|| filter
.as_ref()
.is_some_and(|f| expression_has_column_inside_scopes(f, inner_scopes))
|| order_by.as_ref().is_some_and(|items| {
items.iter().any(|i| expression_has_column_inside_scopes(&i.expr, inner_scopes))
});
if has_outer && !has_inner {
return true;
}
args.iter().any(|a| {
expression_in_scope_chain_has_outer_correlated_aggregate(a, inner_scopes, database)
})
}
Expression::Function { name, args, .. }
if matches!(
name.to_uppercase().as_str(),
"COUNT" | "SUM" | "AVG" | "TOTAL" | "MIN" | "MAX" | "GROUP_CONCAT" | "STRING_AGG"
) =>
{
let upper = name.to_uppercase();
let is_scalar_minmax = matches!(upper.as_str(), "MIN" | "MAX") && args.len() > 1;
if !is_scalar_minmax {
let has_outer =
args.iter().any(|a| expression_has_column_outside_scopes(a, inner_scopes));
let has_inner =
args.iter().any(|a| expression_has_column_inside_scopes(a, inner_scopes));
if has_outer && !has_inner {
return true;
}
}
args.iter().any(|a| {
expression_in_scope_chain_has_outer_correlated_aggregate(a, inner_scopes, database)
})
}
Expression::ScalarSubquery(stmt) => {
scope_chain_contains_outer_correlated_aggregate(stmt, inner_scopes, database)
}
Expression::BinaryOp { left, right, .. } => {
expression_in_scope_chain_has_outer_correlated_aggregate(left, inner_scopes, database)
|| expression_in_scope_chain_has_outer_correlated_aggregate(
right,
inner_scopes,
database,
)
}
Expression::UnaryOp { expr, .. }
| Expression::Cast { expr, .. }
| Expression::IsNull { expr, .. } => {
expression_in_scope_chain_has_outer_correlated_aggregate(expr, inner_scopes, database)
}
Expression::Like { expr, pattern, .. } => {
expression_in_scope_chain_has_outer_correlated_aggregate(expr, inner_scopes, database)
|| expression_in_scope_chain_has_outer_correlated_aggregate(
pattern,
inner_scopes,
database,
)
}
Expression::Between { expr, low, high, .. } => {
expression_in_scope_chain_has_outer_correlated_aggregate(expr, inner_scopes, database)
|| expression_in_scope_chain_has_outer_correlated_aggregate(
low,
inner_scopes,
database,
)
|| expression_in_scope_chain_has_outer_correlated_aggregate(
high,
inner_scopes,
database,
)
}
Expression::InList { expr, values, .. } => {
expression_in_scope_chain_has_outer_correlated_aggregate(expr, inner_scopes, database)
|| values.iter().any(|v| {
expression_in_scope_chain_has_outer_correlated_aggregate(
v,
inner_scopes,
database,
)
})
}
Expression::In { expr, .. } => {
expression_in_scope_chain_has_outer_correlated_aggregate(expr, inner_scopes, database)
}
Expression::Case { operand, when_clauses, else_result } => {
operand.as_ref().is_some_and(|e| {
expression_in_scope_chain_has_outer_correlated_aggregate(e, inner_scopes, database)
}) || when_clauses.iter().any(|w| {
w.conditions.iter().any(|c| {
expression_in_scope_chain_has_outer_correlated_aggregate(
c,
inner_scopes,
database,
)
}) || expression_in_scope_chain_has_outer_correlated_aggregate(
&w.result,
inner_scopes,
database,
)
}) || else_result.as_ref().is_some_and(|e| {
expression_in_scope_chain_has_outer_correlated_aggregate(e, inner_scopes, database)
})
}
Expression::Function { args, .. } => args.iter().any(|a| {
expression_in_scope_chain_has_outer_correlated_aggregate(a, inner_scopes, database)
}),
Expression::WindowFunction { function, over } => {
let args = match function {
vibesql_ast::WindowFunctionSpec::Aggregate { args, .. }
| vibesql_ast::WindowFunctionSpec::Ranking { args, .. }
| vibesql_ast::WindowFunctionSpec::Value { args, .. } => args,
};
if args.iter().any(|a| {
expression_in_scope_chain_has_outer_correlated_aggregate(a, inner_scopes, database)
}) {
return true;
}
if let Some(p) = &over.partition_by {
if p.iter().any(|e| {
expression_in_scope_chain_has_outer_correlated_aggregate(
e,
inner_scopes,
database,
)
}) {
return true;
}
}
if let Some(o) = &over.order_by {
if o.iter().any(|i| {
expression_in_scope_chain_has_outer_correlated_aggregate(
&i.expr,
inner_scopes,
database,
)
}) {
return true;
}
}
false
}
Expression::QuantifiedComparison { expr, .. } => {
expression_in_scope_chain_has_outer_correlated_aggregate(expr, inner_scopes, database)
}
_ => false,
}
}
fn expression_has_column_outside_scopes(
expr: &vibesql_ast::Expression,
inner_scopes: &[HashSet<String>],
) -> bool {
use vibesql_ast::Expression;
match expr {
Expression::ColumnRef(col) => {
if col.schema_canonical().is_none()
&& col.table_canonical().is_none()
&& col.column_canonical() == "*"
{
return false;
}
let name = col.column_canonical().to_ascii_lowercase();
!inner_scopes.iter().any(|s| s.contains(&name))
}
Expression::BinaryOp { left, right, .. } => {
expression_has_column_outside_scopes(left, inner_scopes)
|| expression_has_column_outside_scopes(right, inner_scopes)
}
Expression::UnaryOp { expr, .. }
| Expression::Cast { expr, .. }
| Expression::IsNull { expr, .. } => {
expression_has_column_outside_scopes(expr, inner_scopes)
}
Expression::Like { expr, pattern, .. } => {
expression_has_column_outside_scopes(expr, inner_scopes)
|| expression_has_column_outside_scopes(pattern, inner_scopes)
}
Expression::Between { expr, low, high, .. } => {
expression_has_column_outside_scopes(expr, inner_scopes)
|| expression_has_column_outside_scopes(low, inner_scopes)
|| expression_has_column_outside_scopes(high, inner_scopes)
}
Expression::InList { expr, values, .. } => {
expression_has_column_outside_scopes(expr, inner_scopes)
|| values.iter().any(|v| expression_has_column_outside_scopes(v, inner_scopes))
}
Expression::Function { args, .. } => {
args.iter().any(|a| expression_has_column_outside_scopes(a, inner_scopes))
}
Expression::AggregateFunction { args, filter, order_by, .. } => {
args.iter().any(|a| expression_has_column_outside_scopes(a, inner_scopes))
|| filter
.as_ref()
.is_some_and(|f| expression_has_column_outside_scopes(f, inner_scopes))
|| order_by.as_ref().is_some_and(|items| {
items
.iter()
.any(|i| expression_has_column_outside_scopes(&i.expr, inner_scopes))
})
}
Expression::Case { operand, when_clauses, else_result } => {
operand.as_ref().is_some_and(|e| expression_has_column_outside_scopes(e, inner_scopes))
|| when_clauses.iter().any(|w| {
w.conditions
.iter()
.any(|c| expression_has_column_outside_scopes(c, inner_scopes))
|| expression_has_column_outside_scopes(&w.result, inner_scopes)
})
|| else_result
.as_ref()
.is_some_and(|e| expression_has_column_outside_scopes(e, inner_scopes))
}
_ => false,
}
}
fn expression_has_column_inside_scopes(
expr: &vibesql_ast::Expression,
inner_scopes: &[HashSet<String>],
) -> bool {
use vibesql_ast::Expression;
match expr {
Expression::ColumnRef(col) => {
if col.schema_canonical().is_none()
&& col.table_canonical().is_none()
&& col.column_canonical() == "*"
{
return false;
}
let name = col.column_canonical().to_ascii_lowercase();
inner_scopes.iter().any(|s| s.contains(&name))
}
Expression::BinaryOp { left, right, .. } => {
expression_has_column_inside_scopes(left, inner_scopes)
|| expression_has_column_inside_scopes(right, inner_scopes)
}
Expression::UnaryOp { expr, .. }
| Expression::Cast { expr, .. }
| Expression::IsNull { expr, .. } => {
expression_has_column_inside_scopes(expr, inner_scopes)
}
Expression::Like { expr, pattern, .. } => {
expression_has_column_inside_scopes(expr, inner_scopes)
|| expression_has_column_inside_scopes(pattern, inner_scopes)
}
Expression::Between { expr, low, high, .. } => {
expression_has_column_inside_scopes(expr, inner_scopes)
|| expression_has_column_inside_scopes(low, inner_scopes)
|| expression_has_column_inside_scopes(high, inner_scopes)
}
Expression::InList { expr, values, .. } => {
expression_has_column_inside_scopes(expr, inner_scopes)
|| values.iter().any(|v| expression_has_column_inside_scopes(v, inner_scopes))
}
Expression::Function { args, .. } => {
args.iter().any(|a| expression_has_column_inside_scopes(a, inner_scopes))
}
Expression::AggregateFunction { args, filter, order_by, .. } => {
args.iter().any(|a| expression_has_column_inside_scopes(a, inner_scopes))
|| filter
.as_ref()
.is_some_and(|f| expression_has_column_inside_scopes(f, inner_scopes))
|| order_by.as_ref().is_some_and(|items| {
items.iter().any(|i| expression_has_column_inside_scopes(&i.expr, inner_scopes))
})
}
Expression::Case { operand, when_clauses, else_result } => {
operand.as_ref().is_some_and(|e| expression_has_column_inside_scopes(e, inner_scopes))
|| when_clauses.iter().any(|w| {
w.conditions
.iter()
.any(|c| expression_has_column_inside_scopes(c, inner_scopes))
|| expression_has_column_inside_scopes(&w.result, inner_scopes)
})
|| else_result
.as_ref()
.is_some_and(|e| expression_has_column_inside_scopes(e, inner_scopes))
}
_ => false,
}
}
fn expr_has_column_referencing_aggregate(expr: &vibesql_ast::Expression) -> bool {
use vibesql_ast::Expression;
match expr {
Expression::AggregateFunction { args, filter, order_by, .. } => {
if args.iter().any(expression_contains_column_ref_simple) {
return true;
}
if filter.as_ref().is_some_and(|f| expression_contains_column_ref_simple(f)) {
return true;
}
if order_by.as_ref().is_some_and(|items| {
items.iter().any(|i| expression_contains_column_ref_simple(&i.expr))
}) {
return true;
}
args.iter().any(expr_has_column_referencing_aggregate)
}
Expression::Function { name, args, .. }
if matches!(
name.to_uppercase().as_str(),
"COUNT" | "SUM" | "AVG" | "TOTAL" | "MIN" | "MAX" | "GROUP_CONCAT" | "STRING_AGG"
) =>
{
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_simple) {
return true;
}
args.iter().any(expr_has_column_referencing_aggregate)
}
Expression::BinaryOp { left, right, .. } => {
expr_has_column_referencing_aggregate(left)
|| expr_has_column_referencing_aggregate(right)
}
Expression::UnaryOp { expr, .. } => expr_has_column_referencing_aggregate(expr),
Expression::Cast { expr, .. } => expr_has_column_referencing_aggregate(expr),
Expression::IsNull { expr, .. } => expr_has_column_referencing_aggregate(expr),
Expression::Function { args, .. } => args.iter().any(expr_has_column_referencing_aggregate),
Expression::Case { operand, when_clauses, else_result } => {
operand.as_ref().is_some_and(|e| expr_has_column_referencing_aggregate(e))
|| when_clauses.iter().any(|w| {
w.conditions.iter().any(expr_has_column_referencing_aggregate)
|| expr_has_column_referencing_aggregate(&w.result)
})
|| else_result.as_ref().is_some_and(|e| expr_has_column_referencing_aggregate(e))
}
Expression::WindowFunction { function, over } => {
let args = match function {
vibesql_ast::WindowFunctionSpec::Aggregate { args, .. }
| vibesql_ast::WindowFunctionSpec::Ranking { args, .. }
| vibesql_ast::WindowFunctionSpec::Value { args, .. } => args,
};
if args.iter().any(expr_has_column_referencing_aggregate) {
return true;
}
if over.partition_by.as_ref().is_some_and(|exprs| {
exprs.iter().any(expr_has_column_referencing_aggregate)
}) {
return true;
}
if over.order_by.as_ref().is_some_and(|items| {
items.iter().any(|i| expr_has_column_referencing_aggregate(&i.expr))
}) {
return true;
}
if let Some(frame) = &over.frame {
if let vibesql_ast::FrameBound::Preceding(e)
| vibesql_ast::FrameBound::Following(e) = &frame.start
{
if expr_has_column_referencing_aggregate(e) {
return true;
}
}
if let Some(vibesql_ast::FrameBound::Preceding(e))
| Some(vibesql_ast::FrameBound::Following(e)) = &frame.end
{
if expr_has_column_referencing_aggregate(e) {
return true;
}
}
}
false
}
_ => false,
}
}
fn expression_contains_column_ref_simple(expr: &vibesql_ast::Expression) -> bool {
use vibesql_ast::Expression;
match expr {
Expression::ColumnRef(col) => {
!(col.schema_canonical().is_none()
&& col.table_canonical().is_none()
&& col.column_canonical() == "*")
}
Expression::BinaryOp { left, right, .. } => {
expression_contains_column_ref_simple(left)
|| expression_contains_column_ref_simple(right)
}
Expression::UnaryOp { expr, .. } => expression_contains_column_ref_simple(expr),
Expression::Cast { expr, .. } => expression_contains_column_ref_simple(expr),
Expression::IsNull { expr, .. } => expression_contains_column_ref_simple(expr),
Expression::Function { args, .. } => args.iter().any(expression_contains_column_ref_simple),
Expression::AggregateFunction { args, filter, order_by, .. } => {
args.iter().any(expression_contains_column_ref_simple)
|| filter.as_ref().is_some_and(|f| expression_contains_column_ref_simple(f))
|| order_by.as_ref().is_some_and(|items| {
items.iter().any(|i| expression_contains_column_ref_simple(&i.expr))
})
}
Expression::Case { operand, when_clauses, else_result } => {
operand.as_ref().is_some_and(|e| expression_contains_column_ref_simple(e))
|| when_clauses.iter().any(|w| {
w.conditions.iter().any(expression_contains_column_ref_simple)
|| expression_contains_column_ref_simple(&w.result)
})
|| else_result.as_ref().is_some_and(|e| expression_contains_column_ref_simple(e))
}
Expression::Like { expr, pattern, .. } => {
expression_contains_column_ref_simple(expr)
|| expression_contains_column_ref_simple(pattern)
}
Expression::Between { expr, low, high, .. } => {
expression_contains_column_ref_simple(expr)
|| expression_contains_column_ref_simple(low)
|| expression_contains_column_ref_simple(high)
}
Expression::InList { expr, values, .. } => {
expression_contains_column_ref_simple(expr)
|| values.iter().any(expression_contains_column_ref_simple)
}
_ => false,
}
}
impl SelectExecutor<'_> {
pub(in crate::select::executor) fn has_aggregates(
&self,
select_list: &[vibesql_ast::SelectItem],
) -> bool {
select_list.iter().any(|item| match item {
vibesql_ast::SelectItem::Expression { expr, .. } => self.expression_has_aggregate(expr),
_ => false,
})
}
#[allow(clippy::only_used_in_recursion)]
pub(in crate::select::executor) fn expression_has_aggregate(
&self,
expr: &vibesql_ast::Expression,
) -> bool {
match expr {
vibesql_ast::Expression::AggregateFunction { .. } => true,
vibesql_ast::Expression::Function { name, args, .. } => {
let name_upper = name.to_uppercase();
let is_aggregate = match name_upper.as_str() {
"COUNT" | "SUM" | "AVG" | "TOTAL" | "GROUP_CONCAT" | "STRING_AGG"
| "JSON_GROUP_ARRAY" | "MD5SUM" => true,
"MIN" | "MAX" => args.len() <= 1, _ => false,
};
if is_aggregate {
return true;
}
args.iter().any(|arg| self.expression_has_aggregate(arg))
}
vibesql_ast::Expression::BinaryOp { left, right, .. } => {
self.expression_has_aggregate(left) || self.expression_has_aggregate(right)
}
vibesql_ast::Expression::UnaryOp { expr, .. } => self.expression_has_aggregate(expr),
vibesql_ast::Expression::Cast { expr, .. } => self.expression_has_aggregate(expr),
vibesql_ast::Expression::Case { operand, when_clauses, else_result } => {
operand.as_ref().is_some_and(|e| self.expression_has_aggregate(e))
|| when_clauses.iter().any(|when_clause| {
when_clause.conditions.iter().any(|c| self.expression_has_aggregate(c))
|| self.expression_has_aggregate(&when_clause.result)
})
|| else_result.as_ref().is_some_and(|e| self.expression_has_aggregate(e))
}
vibesql_ast::Expression::Between { expr, low, high, .. } => {
self.expression_has_aggregate(expr)
|| self.expression_has_aggregate(low)
|| self.expression_has_aggregate(high)
}
vibesql_ast::Expression::InList { expr, values, .. } => {
self.expression_has_aggregate(expr)
|| values.iter().any(|v| self.expression_has_aggregate(v))
}
vibesql_ast::Expression::In { expr, .. } => self.expression_has_aggregate(expr),
vibesql_ast::Expression::Like { expr, pattern, .. } => {
self.expression_has_aggregate(expr) || self.expression_has_aggregate(pattern)
}
vibesql_ast::Expression::IsNull { expr, .. } => self.expression_has_aggregate(expr),
vibesql_ast::Expression::Position { substring, string, .. } => {
self.expression_has_aggregate(substring) || self.expression_has_aggregate(string)
}
vibesql_ast::Expression::Trim { removal_char, string, .. } => {
removal_char.as_ref().is_some_and(|e| self.expression_has_aggregate(e))
|| self.expression_has_aggregate(string)
}
vibesql_ast::Expression::Interval { value, .. } => self.expression_has_aggregate(value),
vibesql_ast::Expression::QuantifiedComparison { expr, .. } => {
self.expression_has_aggregate(expr)
}
vibesql_ast::Expression::ScalarSubquery(_) | vibesql_ast::Expression::Exists { .. } => {
false
}
vibesql_ast::Expression::WindowFunction { function, over } => {
let args = match function {
vibesql_ast::WindowFunctionSpec::Aggregate { args, .. } => args,
vibesql_ast::WindowFunctionSpec::Ranking { args, .. } => args,
vibesql_ast::WindowFunctionSpec::Value { args, .. } => args,
};
args.iter().any(|arg| self.expression_has_aggregate(arg))
|| over.partition_by.as_ref().is_some_and(|exprs| {
exprs.iter().any(|e| self.expression_has_aggregate(e))
})
|| over.order_by.as_ref().is_some_and(|items| {
items.iter().any(|i| self.expression_has_aggregate(&i.expr))
})
}
vibesql_ast::Expression::DuplicateKeyValue { .. } => false,
_ => false,
}
}
pub(in crate::select::executor) fn select_list_has_outer_aggregate_collapse(
&self,
select_list: &[vibesql_ast::SelectItem],
) -> bool {
select_list.iter().any(|item| match item {
vibesql_ast::SelectItem::Expression { expr, .. } => {
expression_contains_outer_aggregate_collapse(expr, self.database)
}
_ => false,
})
}
pub(in crate::select::executor) fn is_simple_count_star(
&self,
stmt: &vibesql_ast::SelectStmt,
) -> Option<String> {
if stmt.select_list.len() != 1 {
return None;
}
let is_count_star = match &stmt.select_list[0] {
vibesql_ast::SelectItem::Expression { expr, .. } => {
match expr {
vibesql_ast::Expression::AggregateFunction { name, distinct, args, .. } => {
if name.to_uppercase() != "COUNT" || *distinct || args.len() != 1 {
return None;
}
matches!(args[0], vibesql_ast::Expression::Wildcard)
|| matches!(
&args[0],
vibesql_ast::Expression::ColumnRef(col_id) if col_id.schema_canonical().is_none() && col_id.table_canonical().is_none() && col_id.column_canonical() == "*"
)
}
vibesql_ast::Expression::Function { name, args, .. } => {
if name.to_uppercase() != "COUNT" || args.len() != 1 {
return None;
}
matches!(args[0], vibesql_ast::Expression::Wildcard)
|| matches!(
&args[0],
vibesql_ast::Expression::ColumnRef(col_id) if col_id.schema_canonical().is_none() && col_id.table_canonical().is_none() && col_id.column_canonical() == "*"
)
}
_ => false,
}
}
_ => false,
};
if !is_count_star {
return None;
}
if stmt.where_clause.is_some()
|| stmt.group_by.is_some()
|| stmt.having.is_some()
|| stmt.distinct
|| stmt.set_operation.is_some()
{
return None;
}
let table_name = match &stmt.from {
Some(vibesql_ast::FromClause::Table { name, .. }) => name.clone(),
Some(vibesql_ast::FromClause::Join { .. }) => return None, Some(vibesql_ast::FromClause::Subquery { .. }) => return None, Some(vibesql_ast::FromClause::Values { .. }) => return None, None => return None, };
Some(table_name)
}
}
#[cfg(test)]
mod tests {
use vibesql_ast::{
ColumnIdentifier, Expression, FromClause, FunctionIdentifier, SelectItem, SelectStmt,
};
use vibesql_catalog::{ColumnSchema, TableSchema};
use vibesql_storage::Database;
use vibesql_types::DataType;
use super::*;
fn empty_select_stmt() -> SelectStmt {
SelectStmt {
with_clause: None,
distinct: false,
select_list: Vec::new(),
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,
}
}
fn from_table(name: &str) -> FromClause {
FromClause::Table {
index_hint: None,
name: name.to_string(),
alias: None,
column_aliases: None,
quoted: false,
}
}
fn agg(name: &str, arg: Expression) -> Expression {
Expression::AggregateFunction {
name: FunctionIdentifier::new(name),
distinct: false,
args: vec![arg],
order_by: None,
filter: None,
}
}
fn col(name: &str) -> Expression {
Expression::ColumnRef(ColumnIdentifier::simple(name, false))
}
fn setup_db_t1_t2() -> Database {
let mut db = Database::new();
db.create_table(TableSchema::new(
"t1".to_string(),
vec![ColumnSchema::new("a".to_string(), DataType::Integer, true)],
))
.expect("create t1");
db.create_table(TableSchema::new(
"t2".to_string(),
vec![ColumnSchema::new("x".to_string(), DataType::Integer, true)],
))
.expect("create t2");
db
}
fn setup_db_t1_t3() -> Database {
let mut db = Database::new();
db.create_table(TableSchema::new(
"t1".to_string(),
vec![ColumnSchema::new("a".to_string(), DataType::Integer, true)],
))
.expect("create t1");
db.create_table(TableSchema::new(
"t3".to_string(),
vec![ColumnSchema::new("y".to_string(), DataType::Integer, true)],
))
.expect("create t3");
db
}
#[test]
fn no_collapse_when_aggregate_resolves_to_inner_from_table() {
let db = setup_db_t1_t2();
let mut inner = empty_select_stmt();
inner.select_list.push(SelectItem::Expression {
expr: agg("sum", col("x")),
alias: None,
source_text: None,
});
inner.from = Some(from_table("t2"));
let outer_subq = Expression::ScalarSubquery(Box::new(inner));
assert!(
!expression_contains_outer_aggregate_collapse(&outer_subq, &db),
"SELECT (SELECT sum(x) FROM t2) FROM t1 must NOT trigger collapse — \
`x` resolves to inner scope t2.x, not outer scope t1"
);
}
#[test]
fn collapse_when_aggregate_references_outer_through_nested_from_subqueries() {
let db = setup_db_t1_t3();
let mut innermost = empty_select_stmt();
innermost.select_list.push(SelectItem::Expression {
expr: agg("sum", col("y")),
alias: Some("x".to_string()),
source_text: None,
});
innermost.from = Some(from_table("t1"));
let mut middle = empty_select_stmt();
middle.select_list.push(SelectItem::Expression {
expr: col("x"),
alias: None,
source_text: None,
});
middle.from = Some(FromClause::Subquery {
query: Box::new(innermost),
alias: "_anon".to_string(),
column_aliases: None,
});
let outer_subq = Expression::ScalarSubquery(Box::new(middle));
assert!(
expression_contains_outer_aggregate_collapse(&outer_subq, &db),
"57.3 pattern: nested FROM subqueries with sum(y) aggregating over \
outer t3.y must trigger collapse"
);
}
#[test]
fn no_collapse_when_aggregate_references_both_inner_and_outer() {
let db = setup_db_t1_t2();
let plus = Expression::BinaryOp {
op: vibesql_ast::BinaryOperator::Plus,
left: Box::new(col("a")),
right: Box::new(col("x")),
};
let mut inner = empty_select_stmt();
inner.select_list.push(SelectItem::Expression {
expr: agg("count", plus),
alias: None,
source_text: None,
});
inner.from = Some(from_table("t2"));
let outer_subq = Expression::ScalarSubquery(Box::new(inner));
assert!(
!expression_contains_outer_aggregate_collapse(&outer_subq, &db),
"An aggregate referencing both an outer column (a from t1) and an \
inner column (x from t2) must NOT trigger collapse — it's a normal \
correlated aggregate evaluated per outer row"
);
}
fn window_fn(
function: vibesql_ast::WindowFunctionSpec,
order_by_expr: Option<Expression>,
) -> Expression {
Expression::WindowFunction {
function,
over: vibesql_ast::WindowSpec {
base_window_name: None,
partition_by: None,
order_by: order_by_expr.map(|e| {
vec![vibesql_ast::OrderByItem {
expr: e,
direction: vibesql_ast::OrderDirection::Asc,
nulls_order: None,
}]
}),
frame: None,
},
}
}
#[test]
fn collapse_detected_through_in_subquery() {
let db = setup_db_t1_t2();
let mut inner = empty_select_stmt();
inner.select_list.push(SelectItem::Expression {
expr: agg("min", col("a")),
alias: None,
source_text: None,
});
inner.select_list.push(SelectItem::Expression {
expr: window_fn(
vibesql_ast::WindowFunctionSpec::Ranking {
name: FunctionIdentifier::new("ntile"),
args: vec![Expression::Literal(vibesql_types::SqlValue::Integer(1))],
},
None,
),
alias: None,
source_text: None,
});
let in_expr = Expression::In {
expr: Box::new(Expression::RowValueConstructor(vec![
Expression::Literal(vibesql_types::SqlValue::Integer(0)),
Expression::Literal(vibesql_types::SqlValue::Integer(0)),
])),
subquery: Box::new(inner),
negated: false,
};
assert!(
expression_contains_outer_aggregate_collapse(&in_expr, &db),
"(0,0) IN (SELECT MIN(a), NTILE(1) OVER()) must trigger collapse — \
the FROM-less IN subquery aggregates over an outer column"
);
}
#[test]
fn collapse_detected_for_aggregate_inside_over_order_by() {
let db = setup_db_t1_t2();
let mut inner = empty_select_stmt();
inner.select_list.push(SelectItem::Expression {
expr: window_fn(
vibesql_ast::WindowFunctionSpec::Aggregate {
name: FunctionIdentifier::new("count"),
args: vec![col("a")],
filter: None,
},
Some(agg("sum", col("a"))),
),
alias: None,
source_text: None,
});
let outer_subq = Expression::ScalarSubquery(Box::new(inner));
assert!(
expression_contains_outer_aggregate_collapse(&outer_subq, &db),
"(SELECT count(a) OVER (ORDER BY sum(a))) must trigger collapse — \
sum(a) inside the OVER ORDER BY is an outer-correlated aggregate"
);
}
#[test]
fn no_collapse_for_window_aggregate_spec() {
let db = setup_db_t1_t2();
let mut inner = empty_select_stmt();
inner.select_list.push(SelectItem::Expression {
expr: window_fn(
vibesql_ast::WindowFunctionSpec::Aggregate {
name: FunctionIdentifier::new("total"),
args: vec![col("a")],
filter: None,
},
None,
),
alias: None,
source_text: None,
});
let outer_subq = Expression::ScalarSubquery(Box::new(inner));
assert!(
!expression_contains_outer_aggregate_collapse(&outer_subq, &db),
"(SELECT total(a) OVER()) must NOT trigger collapse — the window \
aggregate has its own scope and is not a plain aggregate"
);
}
}