#![allow(clippy::collapsible_if)]
mod aggregates;
mod column_refs;
mod in_subquery_columns;
mod index_hints;
mod join_limits;
mod schema;
use std::collections::HashSet;
pub use aggregates::{
check_aggregate_arg_count, find_aggregate_in_expression, find_window_function_in_expression,
validate_aggregate_arguments, validate_group_by_window_misuse,
validate_having_aliased_aggregates, validate_no_nested_aggregates,
validate_order_by_aliased_window_functions, validate_subquery_context_misuse,
validate_window_query_order_by_aggregates, SubqueryContext,
};
pub use column_refs::{extract_column_refs, validate_column_ref};
pub use in_subquery_columns::validate_in_subquery_column_counts;
pub use index_hints::validate_index_hints;
pub use join_limits::validate_join_table_limit;
pub use schema::validate_aggregate_subquery_outer_refs;
use vibesql_ast::{Expression, SelectItem};
use crate::{errors::ExecutorError, schema::CombinedSchema};
pub fn validate_select_columns_with_context(
select_list: &[SelectItem],
where_clause: Option<&Expression>,
schema: &CombinedSchema,
procedural_context: Option<&crate::procedural::ExecutionContext>,
outer_schema: Option<&CombinedSchema>,
) -> Result<(), ExecutorError> {
let proc_vars: HashSet<String> = procedural_context
.map(|ctx| {
ctx.get_available_names()
.into_iter()
.flat_map(|name| vec![name.clone(), name.to_lowercase()])
.collect()
})
.unwrap_or_default();
let select_aliases: HashSet<String> = select_list
.iter()
.filter_map(|item| {
if let SelectItem::Expression { alias: Some(alias), .. } = item {
Some(vec![alias.clone(), alias.to_lowercase()])
} else {
None
}
})
.flatten()
.collect();
let allowed_in_where: HashSet<String> = proc_vars.union(&select_aliases).cloned().collect();
let mut column_refs = Vec::new();
let mut where_column_refs = Vec::new();
for item in select_list {
match item {
SelectItem::Expression { expr, .. } => {
extract_column_refs(expr, &mut column_refs);
}
SelectItem::Wildcard { .. } => {
}
SelectItem::QualifiedWildcard { qualifier, .. } => {
let qualifier_lower = qualifier.to_lowercase();
let table_in_inner =
schema.table_schemas.keys().any(|k| k.canonical() == qualifier_lower);
let table_in_outer = outer_schema.is_some_and(|outer| {
outer.table_schemas.keys().any(|k| k.canonical() == qualifier_lower)
});
if !table_in_inner && !table_in_outer {
let mut available_tables: Vec<String> = schema.table_names();
if let Some(outer) = outer_schema {
available_tables.extend(outer.table_names());
}
return Err(ExecutorError::InvalidTableQualifier {
qualifier: qualifier.clone(),
column: "*".to_string(),
available_tables,
});
}
}
}
}
if let Some(where_expr) = where_clause {
extract_column_refs(where_expr, &mut where_column_refs);
if let Some(agg_name) = check_aggregate_arg_count(where_expr) {
return Err(ExecutorError::WrongNumberOfArguments { function_name: agg_name });
}
if let Some(agg_name) = find_aggregate_in_expression(where_expr) {
return Err(ExecutorError::MisuseOfAggregate { function_name: agg_name });
}
if let Some(window_name) = find_window_function_in_expression(where_expr) {
return Err(ExecutorError::MisuseOfWindowFunction { function_name: window_name });
}
validate_subquery_context_misuse(where_expr, SubqueryContext::WhereOrEqual)?;
}
for col_ref in &column_refs {
if col_ref.table.is_none() {
if proc_vars.contains(&col_ref.column)
|| proc_vars.contains(&col_ref.column.to_lowercase())
{
continue;
}
}
validate_column_ref(col_ref, schema, outer_schema)?;
}
log::debug!("Validating WHERE: allowed_in_where={:?}", allowed_in_where);
for col_ref in &where_column_refs {
if col_ref.table.is_none() {
if allowed_in_where.contains(&col_ref.column)
|| allowed_in_where.contains(&col_ref.column.to_lowercase())
{
log::debug!("WHERE validation: allowing alias '{}'", col_ref.column);
continue;
}
}
log::debug!("WHERE validation: checking column '{}' against schema", col_ref.column);
validate_column_ref(col_ref, schema, outer_schema)?;
}
Ok(())
}
#[cfg(test)]
pub fn validate_select_columns(
select_list: &[SelectItem],
where_clause: Option<&Expression>,
schema: &CombinedSchema,
) -> Result<(), ExecutorError> {
validate_select_columns_with_context(select_list, where_clause, schema, None, None)
}
#[cfg(test)]
mod tests {
use vibesql_ast::{BinaryOperator, ColumnIdentifier};
use vibesql_catalog::{ColumnSchema, TableSchema};
use vibesql_types::{DataType, SqlValue};
use super::*;
fn make_test_schema() -> CombinedSchema {
let columns = vec![
ColumnSchema {
name: "ID".to_string(),
data_type: DataType::Integer,
nullable: false,
default_value: None,
generated_expr: None,
is_exact_integer_type: false,
collation: None,
},
ColumnSchema {
name: "NAME".to_string(),
data_type: DataType::Varchar { max_length: Some(50) },
nullable: true,
default_value: None,
generated_expr: None,
is_exact_integer_type: false,
collation: None,
},
];
let table_schema = TableSchema::new("PRODUCTS".to_string(), columns);
CombinedSchema::from_table("PRODUCTS".to_string(), table_schema)
}
#[test]
fn test_valid_column_ref() {
let schema = make_test_schema();
let select_list = vec![SelectItem::Expression {
expr: Expression::ColumnRef(ColumnIdentifier::simple("id", false)),
alias: None,
source_text: None,
}];
let result = validate_select_columns(&select_list, None, &schema);
assert!(result.is_ok());
}
#[test]
fn test_invalid_column_ref() {
let schema = make_test_schema();
let select_list = vec![SelectItem::Expression {
expr: Expression::ColumnRef(ColumnIdentifier::simple("invalid_column", false)),
alias: None,
source_text: None,
}];
let result = validate_select_columns(&select_list, None, &schema);
assert!(result.is_err());
match result {
Err(ExecutorError::ColumnNotFound { column_name, available_columns, .. }) => {
assert_eq!(column_name, "invalid_column");
assert!(available_columns.contains(&"ID".to_string()));
assert!(available_columns.contains(&"NAME".to_string()));
}
_ => panic!("Expected ColumnNotFound error"),
}
}
#[test]
fn test_qualified_column_ref() {
let schema = make_test_schema();
let select_list = vec![SelectItem::Expression {
expr: Expression::ColumnRef(ColumnIdentifier::qualified(
"products", false, "id", false,
)),
alias: None,
source_text: None,
}];
let result = validate_select_columns(&select_list, None, &schema);
assert!(result.is_ok());
}
#[test]
fn test_invalid_qualified_column_ref() {
let schema = make_test_schema();
let select_list = vec![SelectItem::Expression {
expr: Expression::ColumnRef(ColumnIdentifier::qualified(
"products",
false,
"invalid_column",
false,
)),
alias: None,
source_text: None,
}];
let result = validate_select_columns(&select_list, None, &schema);
assert!(result.is_err());
match result {
Err(ExecutorError::ColumnNotFound { column_name, .. }) => {
assert_eq!(column_name, "invalid_column");
}
_ => panic!("Expected ColumnNotFound error"),
}
}
#[test]
fn test_column_in_expression() {
let schema = make_test_schema();
let select_list = vec![SelectItem::Expression {
expr: Expression::BinaryOp {
op: BinaryOperator::Plus,
left: Box::new(Expression::ColumnRef(ColumnIdentifier::simple(
"invalid_col",
false,
))),
right: Box::new(Expression::Literal(SqlValue::Integer(1))),
},
alias: None,
source_text: None,
}];
let result = validate_select_columns(&select_list, None, &schema);
assert!(result.is_err());
}
#[test]
fn test_where_clause_validation() {
let schema = make_test_schema();
let select_list = vec![SelectItem::Wildcard { alias: None }];
let where_clause = Expression::BinaryOp {
op: BinaryOperator::Equal,
left: Box::new(Expression::ColumnRef(ColumnIdentifier::simple("nonexistent", false))),
right: Box::new(Expression::Literal(SqlValue::Integer(1))),
};
let result = validate_select_columns(&select_list, Some(&where_clause), &schema);
assert!(result.is_err());
}
}