use std::collections::{HashMap, HashSet};
use vibesql_ast::{Expression, SelectItem, SelectStmt};
use vibesql_catalog::{TableIdentifier, TableSchema};
use super::aggregates::is_aggregate_function;
use crate::{errors::ExecutorError, schema::CombinedSchema};
fn find_outer_column_in_expression(
expr: &Expression,
subquery_tables: &[String],
inner_schema: Option<&CombinedSchema>,
outer_schema: &CombinedSchema,
) -> Option<String> {
match expr {
Expression::ColumnRef(col_id) => {
let table = col_id.table_canonical();
let column = col_id.column_canonical();
if column == "*" {
return None;
}
if let Some(table_name) = table {
let table_lower = table_name.to_lowercase();
if !subquery_tables.iter().any(|t| t.to_lowercase() == table_lower) {
if outer_schema.get_column_index(Some(table_name), column).is_some() {
return Some(column.to_string());
}
}
} else {
let in_inner = inner_schema
.map(|s| s.get_column_index(None, column).is_some())
.unwrap_or(false);
if !in_inner {
if outer_schema.get_column_index(None, column).is_some() {
return Some(column.to_string());
}
}
}
None
}
Expression::BinaryOp { left, right, .. } => {
find_outer_column_in_expression(left, subquery_tables, inner_schema, outer_schema)
.or_else(|| {
find_outer_column_in_expression(
right,
subquery_tables,
inner_schema,
outer_schema,
)
})
}
Expression::UnaryOp { expr, .. } => {
find_outer_column_in_expression(expr, subquery_tables, inner_schema, outer_schema)
}
Expression::Function { args, .. } => {
for arg in args {
if let Some(col) = find_outer_column_in_expression(
arg,
subquery_tables,
inner_schema,
outer_schema,
) {
return Some(col);
}
}
None
}
Expression::Cast { expr, .. } => {
find_outer_column_in_expression(expr, subquery_tables, inner_schema, outer_schema)
}
Expression::Case { operand, when_clauses, else_result } => {
if let Some(op) = operand {
if let Some(col) =
find_outer_column_in_expression(op, subquery_tables, inner_schema, outer_schema)
{
return Some(col);
}
}
for when_clause in when_clauses {
for cond in &when_clause.conditions {
if let Some(col) = find_outer_column_in_expression(
cond,
subquery_tables,
inner_schema,
outer_schema,
) {
return Some(col);
}
}
if let Some(col) = find_outer_column_in_expression(
&when_clause.result,
subquery_tables,
inner_schema,
outer_schema,
) {
return Some(col);
}
}
if let Some(else_expr) = else_result {
find_outer_column_in_expression(
else_expr,
subquery_tables,
inner_schema,
outer_schema,
)
} else {
None
}
}
Expression::IsNull { expr, .. } => {
find_outer_column_in_expression(expr, subquery_tables, inner_schema, outer_schema)
}
Expression::Between { expr, low, high, .. } => {
find_outer_column_in_expression(expr, subquery_tables, inner_schema, outer_schema)
.or_else(|| {
find_outer_column_in_expression(
low,
subquery_tables,
inner_schema,
outer_schema,
)
})
.or_else(|| {
find_outer_column_in_expression(
high,
subquery_tables,
inner_schema,
outer_schema,
)
})
}
Expression::InList { expr, values, .. } => {
if let Some(col) =
find_outer_column_in_expression(expr, subquery_tables, inner_schema, outer_schema)
{
return Some(col);
}
for val in values {
if let Some(col) = find_outer_column_in_expression(
val,
subquery_tables,
inner_schema,
outer_schema,
) {
return Some(col);
}
}
None
}
Expression::Like { expr, pattern, .. } => {
find_outer_column_in_expression(expr, subquery_tables, inner_schema, outer_schema)
.or_else(|| {
find_outer_column_in_expression(
pattern,
subquery_tables,
inner_schema,
outer_schema,
)
})
}
Expression::Conjunction(children) | Expression::Disjunction(children) => {
for child in children {
if let Some(col) = find_outer_column_in_expression(
child,
subquery_tables,
inner_schema,
outer_schema,
) {
return Some(col);
}
}
None
}
_ => None,
}
}
fn find_aggregate_with_outer_column(
expr: &Expression,
subquery_tables: &[String],
inner_schema: Option<&CombinedSchema>,
outer_schema: &CombinedSchema,
) -> Option<String> {
match expr {
Expression::AggregateFunction { name, args, .. } => {
for arg in args {
if find_outer_column_in_expression(arg, subquery_tables, inner_schema, outer_schema)
.is_some()
{
return Some(name.to_string());
}
}
None
}
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 {
for arg in args {
if let Some(found) = find_aggregate_with_outer_column(
arg,
subquery_tables,
inner_schema,
outer_schema,
) {
return Some(found);
}
}
None
} else {
for arg in args {
if find_outer_column_in_expression(
arg,
subquery_tables,
inner_schema,
outer_schema,
)
.is_some()
{
return Some(name.to_string());
}
}
None
}
} else {
for arg in args {
if let Some(found) = find_aggregate_with_outer_column(
arg,
subquery_tables,
inner_schema,
outer_schema,
) {
return Some(found);
}
}
None
}
}
Expression::BinaryOp { left, right, .. } => {
find_aggregate_with_outer_column(left, subquery_tables, inner_schema, outer_schema)
.or_else(|| {
find_aggregate_with_outer_column(
right,
subquery_tables,
inner_schema,
outer_schema,
)
})
}
Expression::UnaryOp { expr, .. } => {
find_aggregate_with_outer_column(expr, subquery_tables, inner_schema, outer_schema)
}
Expression::Cast { expr, .. } => {
find_aggregate_with_outer_column(expr, subquery_tables, inner_schema, outer_schema)
}
Expression::Case { operand, when_clauses, else_result } => {
if let Some(op) = operand {
if let Some(found) = find_aggregate_with_outer_column(
op,
subquery_tables,
inner_schema,
outer_schema,
) {
return Some(found);
}
}
for when_clause in when_clauses {
for cond in &when_clause.conditions {
if let Some(found) = find_aggregate_with_outer_column(
cond,
subquery_tables,
inner_schema,
outer_schema,
) {
return Some(found);
}
}
if let Some(found) = find_aggregate_with_outer_column(
&when_clause.result,
subquery_tables,
inner_schema,
outer_schema,
) {
return Some(found);
}
}
if let Some(else_expr) = else_result {
find_aggregate_with_outer_column(
else_expr,
subquery_tables,
inner_schema,
outer_schema,
)
} else {
None
}
}
Expression::IsNull { expr, .. } => {
find_aggregate_with_outer_column(expr, subquery_tables, inner_schema, outer_schema)
}
Expression::Between { expr, low, high, .. } => {
find_aggregate_with_outer_column(expr, subquery_tables, inner_schema, outer_schema)
.or_else(|| {
find_aggregate_with_outer_column(
low,
subquery_tables,
inner_schema,
outer_schema,
)
})
.or_else(|| {
find_aggregate_with_outer_column(
high,
subquery_tables,
inner_schema,
outer_schema,
)
})
}
Expression::InList { expr, values, .. } => {
if let Some(found) =
find_aggregate_with_outer_column(expr, subquery_tables, inner_schema, outer_schema)
{
return Some(found);
}
for val in values {
if let Some(found) = find_aggregate_with_outer_column(
val,
subquery_tables,
inner_schema,
outer_schema,
) {
return Some(found);
}
}
None
}
Expression::Like { expr, pattern, .. } => {
find_aggregate_with_outer_column(expr, subquery_tables, inner_schema, outer_schema)
.or_else(|| {
find_aggregate_with_outer_column(
pattern,
subquery_tables,
inner_schema,
outer_schema,
)
})
}
Expression::Conjunction(children) | Expression::Disjunction(children) => {
for child in children {
if let Some(found) = find_aggregate_with_outer_column(
child,
subquery_tables,
inner_schema,
outer_schema,
) {
return Some(found);
}
}
None
}
Expression::ScalarSubquery(_) | Expression::Exists { .. } | Expression::In { .. } => None,
_ => None,
}
}
fn extract_table_names(from: Option<&vibesql_ast::FromClause>) -> Vec<String> {
let mut tables = Vec::new();
if let Some(from_clause) = from {
extract_table_names_recursive(from_clause, &mut tables);
}
tables
}
fn extract_table_names_recursive(from: &vibesql_ast::FromClause, tables: &mut Vec<String>) {
match from {
vibesql_ast::FromClause::Table { name, alias, .. } => {
tables.push(alias.clone().unwrap_or_else(|| name.clone()));
}
vibesql_ast::FromClause::Join { left, right, .. } => {
extract_table_names_recursive(left, tables);
extract_table_names_recursive(right, tables);
}
vibesql_ast::FromClause::Subquery { alias, .. } => {
tables.push(alias.clone());
}
vibesql_ast::FromClause::Values { alias, .. } => {
tables.push(alias.clone());
}
}
}
fn build_schema_from_tables(
tables: &[String],
database: &vibesql_storage::Database,
) -> Option<CombinedSchema> {
if tables.is_empty() {
return None;
}
let mut total_columns = 0;
let mut table_schemas = HashMap::<TableIdentifier, (usize, TableSchema)>::new();
for table_name in tables {
if let Some(table) = database.get_table(table_name) {
let table_schema = table.schema.clone();
let table_id = TableIdentifier::unquoted(table_name);
table_schemas.insert(table_id, (total_columns, table_schema.clone()));
total_columns += table_schema.columns.len();
}
}
if table_schemas.is_empty() {
None
} else {
Some(CombinedSchema {
table_schemas,
total_columns,
hidden_columns: HashSet::new(),
outer_schema: None,
duplicate_aliases: HashSet::new(),
joined_columns: HashSet::new(),
using_coalesce_indices: HashMap::new(),
column_replacement_map: HashMap::new(),
alias_tables: HashSet::new(),
shadowed_tables: HashMap::new(),
})
}
}
pub fn validate_no_aggregate_with_outer_column(
stmt: &SelectStmt,
outer_schema: &CombinedSchema,
database: &vibesql_storage::Database,
) -> Result<(), ExecutorError> {
let subquery_tables = extract_table_names(stmt.from.as_ref());
let inner_schema = build_schema_from_tables(&subquery_tables, database);
if !subquery_tables.is_empty() && inner_schema.is_none() {
return Ok(());
}
for item in &stmt.select_list {
if let SelectItem::Expression { expr, .. } = item {
if let Some(agg_name) = find_aggregate_with_outer_column(
expr,
&subquery_tables,
inner_schema.as_ref(),
outer_schema,
) {
return Err(ExecutorError::MisuseOfAggregateContext { function_name: agg_name });
}
}
}
if let Some(having) = &stmt.having {
if let Some(agg_name) = find_aggregate_with_outer_column(
having,
&subquery_tables,
inner_schema.as_ref(),
outer_schema,
) {
return Err(ExecutorError::MisuseOfAggregateContext { function_name: agg_name });
}
}
Ok(())
}
pub fn validate_aggregate_subquery_outer_refs(
stmt: &SelectStmt,
database: &vibesql_storage::Database,
) -> Result<(), ExecutorError> {
let outer_tables = extract_table_names(stmt.from.as_ref());
let outer_schema = match build_schema_from_tables(&outer_tables, database) {
Some(schema) => schema,
None => return Ok(()), };
for item in &stmt.select_list {
if let SelectItem::Expression { expr, .. } = item {
validate_aggregates_with_subquery_args(expr, &outer_schema, database)?;
}
}
if let Some(having) = &stmt.having {
validate_aggregates_with_subquery_args(having, &outer_schema, database)?;
}
Ok(())
}
fn validate_aggregates_with_subquery_args(
expr: &Expression,
outer_schema: &CombinedSchema,
database: &vibesql_storage::Database,
) -> Result<(), ExecutorError> {
match expr {
Expression::AggregateFunction { args, .. } => {
for arg in args {
validate_subqueries_in_aggregate_arg(arg, outer_schema, database)?;
}
Ok(())
}
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 {
validate_subqueries_in_aggregate_arg(arg, outer_schema, database)?;
}
return Ok(());
}
}
for arg in args {
validate_aggregates_with_subquery_args(arg, outer_schema, database)?;
}
Ok(())
}
Expression::BinaryOp { left, right, .. } => {
validate_aggregates_with_subquery_args(left, outer_schema, database)?;
validate_aggregates_with_subquery_args(right, outer_schema, database)
}
Expression::UnaryOp { expr, .. } => {
validate_aggregates_with_subquery_args(expr, outer_schema, database)
}
Expression::Cast { expr, .. } => {
validate_aggregates_with_subquery_args(expr, outer_schema, database)
}
Expression::Case { operand, when_clauses, else_result } => {
if let Some(op) = operand {
validate_aggregates_with_subquery_args(op, outer_schema, database)?;
}
for when_clause in when_clauses {
for cond in &when_clause.conditions {
validate_aggregates_with_subquery_args(cond, outer_schema, database)?;
}
validate_aggregates_with_subquery_args(
&when_clause.result,
outer_schema,
database,
)?;
}
if let Some(else_expr) = else_result {
validate_aggregates_with_subquery_args(else_expr, outer_schema, database)?;
}
Ok(())
}
Expression::IsNull { expr, .. } => {
validate_aggregates_with_subquery_args(expr, outer_schema, database)
}
Expression::Between { expr, low, high, .. } => {
validate_aggregates_with_subquery_args(expr, outer_schema, database)?;
validate_aggregates_with_subquery_args(low, outer_schema, database)?;
validate_aggregates_with_subquery_args(high, outer_schema, database)
}
Expression::InList { expr, values, .. } => {
validate_aggregates_with_subquery_args(expr, outer_schema, database)?;
for val in values {
validate_aggregates_with_subquery_args(val, outer_schema, database)?;
}
Ok(())
}
Expression::Like { expr, pattern, .. } => {
validate_aggregates_with_subquery_args(expr, outer_schema, database)?;
validate_aggregates_with_subquery_args(pattern, outer_schema, database)
}
Expression::Conjunction(children) | Expression::Disjunction(children) => {
for child in children {
validate_aggregates_with_subquery_args(child, outer_schema, database)?;
}
Ok(())
}
Expression::ScalarSubquery(_) | Expression::Exists { .. } | Expression::In { .. } => Ok(()),
_ => Ok(()),
}
}
fn validate_subqueries_in_aggregate_arg(
expr: &Expression,
outer_schema: &CombinedSchema,
database: &vibesql_storage::Database,
) -> Result<(), ExecutorError> {
match expr {
Expression::ScalarSubquery(subquery) => {
validate_no_aggregate_with_outer_column(subquery, outer_schema, database)
}
Expression::BinaryOp { left, right, .. } => {
validate_subqueries_in_aggregate_arg(left, outer_schema, database)?;
validate_subqueries_in_aggregate_arg(right, outer_schema, database)
}
Expression::UnaryOp { expr, .. } => {
validate_subqueries_in_aggregate_arg(expr, outer_schema, database)
}
Expression::Function { args, .. } => {
for arg in args {
validate_subqueries_in_aggregate_arg(arg, outer_schema, database)?;
}
Ok(())
}
Expression::Cast { expr, .. } => {
validate_subqueries_in_aggregate_arg(expr, outer_schema, database)
}
Expression::Case { operand, when_clauses, else_result } => {
if let Some(op) = operand {
validate_subqueries_in_aggregate_arg(op, outer_schema, database)?;
}
for when_clause in when_clauses {
for cond in &when_clause.conditions {
validate_subqueries_in_aggregate_arg(cond, outer_schema, database)?;
}
validate_subqueries_in_aggregate_arg(&when_clause.result, outer_schema, database)?;
}
if let Some(else_expr) = else_result {
validate_subqueries_in_aggregate_arg(else_expr, outer_schema, database)?;
}
Ok(())
}
Expression::IsNull { expr, .. } => {
validate_subqueries_in_aggregate_arg(expr, outer_schema, database)
}
Expression::Between { expr, low, high, .. } => {
validate_subqueries_in_aggregate_arg(expr, outer_schema, database)?;
validate_subqueries_in_aggregate_arg(low, outer_schema, database)?;
validate_subqueries_in_aggregate_arg(high, outer_schema, database)
}
Expression::InList { expr, values, .. } => {
validate_subqueries_in_aggregate_arg(expr, outer_schema, database)?;
for val in values {
validate_subqueries_in_aggregate_arg(val, outer_schema, database)?;
}
Ok(())
}
Expression::Like { expr, pattern, .. } => {
validate_subqueries_in_aggregate_arg(expr, outer_schema, database)?;
validate_subqueries_in_aggregate_arg(pattern, outer_schema, database)
}
Expression::Conjunction(children) | Expression::Disjunction(children) => {
for child in children {
validate_subqueries_in_aggregate_arg(child, outer_schema, database)?;
}
Ok(())
}
_ => Ok(()),
}
}