#![allow(clippy::needless_return, clippy::collapsible_if)]
use std::collections::HashMap;
use crate::{
errors::ExecutorError,
schema::CombinedSchema,
select::cte::CteResult,
sqlite_schema::{
get_sqlite_schema_table_schema, is_sqlite_schema_table, is_sqlite_temp_schema_table,
},
};
pub(super) fn validate_where_clause_subqueries(
expr: &vibesql_ast::Expression,
database: &vibesql_storage::Database,
cte_results: Option<&HashMap<String, CteResult>>,
) -> Result<(), ExecutorError> {
use vibesql_ast::Expression;
match expr {
Expression::In { expr: lhs, subquery, .. } => {
let expected = match lhs.as_ref() {
Expression::RowValueConstructor(elements) => elements.len(),
_ => 1,
};
let column_count = compute_select_list_column_count(subquery, database, cte_results)?;
if column_count != expected {
return Err(ExecutorError::SubqueryColumnCountMismatch {
expected,
actual: column_count,
});
}
Ok(())
}
Expression::BinaryOp { left, right, .. } => {
validate_where_clause_subqueries(left, database, cte_results)?;
validate_where_clause_subqueries(right, database, cte_results)
}
Expression::UnaryOp { expr, .. } => {
validate_where_clause_subqueries(expr, database, cte_results)
}
Expression::IsNull { expr, .. } => {
validate_where_clause_subqueries(expr, database, cte_results)
}
Expression::IsDistinctFrom { left, right, .. } => {
validate_where_clause_subqueries(left, database, cte_results)?;
validate_where_clause_subqueries(right, database, cte_results)
}
Expression::IsTruthValue { expr, .. } => {
validate_where_clause_subqueries(expr, database, cte_results)
}
Expression::InList { expr, values, .. } => {
validate_where_clause_subqueries(expr, database, cte_results)?;
for val in values {
validate_where_clause_subqueries(val, database, cte_results)?;
}
Ok(())
}
Expression::Between { expr, low, high, .. } => {
validate_where_clause_subqueries(expr, database, cte_results)?;
validate_where_clause_subqueries(low, database, cte_results)?;
validate_where_clause_subqueries(high, database, cte_results)
}
Expression::Case { operand, when_clauses, else_result } => {
if let Some(op) = operand {
validate_where_clause_subqueries(op, database, cte_results)?;
}
for when_clause in when_clauses {
for cond in &when_clause.conditions {
validate_where_clause_subqueries(cond, database, cte_results)?;
}
validate_where_clause_subqueries(&when_clause.result, database, cte_results)?;
}
if let Some(else_res) = else_result {
validate_where_clause_subqueries(else_res, database, cte_results)?;
}
Ok(())
}
_ => Ok(()),
}
}
pub(crate) fn compute_select_list_column_count(
stmt: &vibesql_ast::SelectStmt,
database: &vibesql_storage::Database,
cte_results: Option<&HashMap<String, CteResult>>,
) -> Result<usize, ExecutorError> {
let left_count = compute_single_select_column_count(stmt, database, cte_results)?;
if let Some(set_op) = &stmt.set_operation {
validate_set_operation_column_counts(left_count, set_op, database, cte_results)?;
}
Ok(left_count)
}
fn validate_set_operation_column_counts(
left_count: usize,
set_op: &vibesql_ast::SetOperation,
database: &vibesql_storage::Database,
cte_results: Option<&HashMap<String, CteResult>>,
) -> Result<(), ExecutorError> {
let right_stmt = &set_op.right;
let right_count = compute_single_select_column_count(right_stmt, database, cte_results)?;
if left_count != right_count {
let operator = match (&set_op.op, set_op.all) {
(vibesql_ast::SetOperator::Union, true) => "UNION ALL",
(vibesql_ast::SetOperator::Union, false) => "UNION",
(vibesql_ast::SetOperator::Intersect, true) => "INTERSECT ALL",
(vibesql_ast::SetOperator::Intersect, false) => "INTERSECT",
(vibesql_ast::SetOperator::Except, true) => "EXCEPT ALL",
(vibesql_ast::SetOperator::Except, false) => "EXCEPT",
};
return Err(ExecutorError::SetOperationColumnMismatch { operator: operator.to_string() });
}
if let Some(next_set_op) = &right_stmt.set_operation {
validate_set_operation_column_counts(right_count, next_set_op, database, cte_results)?;
}
Ok(())
}
fn compute_single_select_column_count(
stmt: &vibesql_ast::SelectStmt,
database: &vibesql_storage::Database,
cte_results: Option<&HashMap<String, CteResult>>,
) -> Result<usize, ExecutorError> {
if stmt.select_list.is_empty() {
if let Some(values_rows) = &stmt.values {
if let Some(first_row) = values_rows.first() {
return Ok(first_row.len());
}
}
return Err(ExecutorError::UnsupportedFeature(
"Cannot determine column count for empty SELECT".to_string(),
));
}
let mut count = 0;
for item in &stmt.select_list {
match item {
vibesql_ast::SelectItem::Wildcard { .. } => {
if let Some(from) = &stmt.from {
count += count_columns_in_from_clause(from, database, cte_results)?;
} else {
return Err(ExecutorError::UnsupportedFeature(
"SELECT * requires FROM clause".to_string(),
));
}
}
vibesql_ast::SelectItem::QualifiedWildcard { qualifier, .. } => {
if let Some(cte_ctx) = cte_results {
if let Some((schema, _)) = cte_ctx.get(qualifier).or_else(|| {
cte_ctx
.iter()
.find(|(k, _)| k.eq_ignore_ascii_case(qualifier))
.map(|(_, v)| v)
}) {
count += schema.columns.len();
continue;
}
}
if is_sqlite_schema_table(qualifier) || is_sqlite_temp_schema_table(qualifier) {
count += get_sqlite_schema_table_schema().columns.len();
continue;
}
if let Some(view) = database.catalog.get_view(qualifier) {
count += compute_select_list_column_count(&view.query, database, cte_results)?;
continue;
}
let tbl = database
.get_table(qualifier)
.ok_or_else(|| ExecutorError::TableNotFound(qualifier.clone()))?;
count += tbl.schema.columns.len();
}
vibesql_ast::SelectItem::Expression { .. } => {
count += 1;
}
}
}
Ok(count)
}
fn count_columns_in_from_clause(
from: &vibesql_ast::FromClause,
database: &vibesql_storage::Database,
cte_results: Option<&HashMap<String, CteResult>>,
) -> Result<usize, ExecutorError> {
match from {
vibesql_ast::FromClause::Table { name, .. } => {
if let Some(cte_ctx) = cte_results {
if let Some((schema, _)) = cte_ctx.get(name).or_else(|| {
cte_ctx.iter().find(|(k, _)| k.eq_ignore_ascii_case(name)).map(|(_, v)| v)
}) {
return Ok(schema.columns.len());
}
}
if is_sqlite_schema_table(name) || is_sqlite_temp_schema_table(name) {
return Ok(get_sqlite_schema_table_schema().columns.len());
}
if let Some(view) = database.catalog.get_view(name) {
return compute_select_list_column_count(&view.query, database, cte_results);
}
let table = database
.get_table(name)
.ok_or_else(|| ExecutorError::TableNotFound(name.clone()))?;
Ok(table.schema.columns.len())
}
vibesql_ast::FromClause::Join { left, right, .. } => {
let left_count = count_columns_in_from_clause(left, database, cte_results)?;
let right_count = count_columns_in_from_clause(right, database, cte_results)?;
Ok(left_count + right_count)
}
vibesql_ast::FromClause::Subquery { query, column_aliases, .. } => {
if let Some(aliases) = column_aliases {
Ok(aliases.len())
} else {
compute_select_list_column_count(query, database, cte_results)
}
}
vibesql_ast::FromClause::Values { rows, column_aliases, .. } => {
if let Some(aliases) = column_aliases {
Ok(aliases.len())
} else if let Some(first_row) = rows.first() {
Ok(first_row.len())
} else {
Ok(0) }
}
}
}
fn collect_select_aliases(
select_list: &[vibesql_ast::SelectItem],
) -> std::collections::HashSet<String> {
let mut aliases = std::collections::HashSet::new();
for item in select_list {
if let vibesql_ast::SelectItem::Expression { alias: Some(alias), .. } = item {
aliases.insert(alias.clone());
aliases.insert(alias.to_lowercase());
}
}
aliases
}
pub(super) fn validate_select_column_references_with_context(
stmt: &vibesql_ast::SelectStmt,
schema: &CombinedSchema,
procedural_context: Option<&crate::procedural::ExecutionContext>,
outer_schema: Option<&CombinedSchema>,
) -> Result<(), ExecutorError> {
let select_aliases = collect_select_aliases(&stmt.select_list);
let proc_vars: std::collections::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();
for item in &stmt.select_list {
if let vibesql_ast::SelectItem::Expression { expr, .. } = item {
validate_expression_column_refs(expr, schema, outer_schema, &proc_vars)?;
}
}
if let Some(where_expr) = &stmt.where_clause {
let combined: std::collections::HashSet<String> =
select_aliases.union(&proc_vars).cloned().collect();
log::debug!(
"WHERE alias validation: select_aliases={:?}, combined={:?}",
select_aliases,
combined
);
validate_expression_column_refs(where_expr, schema, outer_schema, &combined)?;
}
if let Some(order_by) = &stmt.order_by {
if stmt.set_operation.is_none() {
let combined: std::collections::HashSet<String> =
select_aliases.union(&proc_vars).cloned().collect();
for order_item in order_by {
validate_expression_column_refs(&order_item.expr, schema, outer_schema, &combined)?;
}
}
}
if let Some(group_by) = &stmt.group_by {
let combined: std::collections::HashSet<String> =
select_aliases.union(&proc_vars).cloned().collect();
for group_expr in group_by.all_expressions() {
validate_expression_column_refs(group_expr, schema, outer_schema, &combined)?;
}
}
if let Some(having_expr) = &stmt.having {
let combined: std::collections::HashSet<String> =
select_aliases.union(&proc_vars).cloned().collect();
validate_expression_column_refs(having_expr, schema, outer_schema, &combined)?;
}
Ok(())
}
fn is_rowid_pseudo_column(column: &str) -> bool {
let lower = column.to_lowercase();
lower == "rowid" || lower == "_rowid_" || lower == "oid"
}
fn validate_expression_column_refs(
expr: &vibesql_ast::Expression,
schema: &CombinedSchema,
outer_schema: Option<&CombinedSchema>,
allowed_aliases: &std::collections::HashSet<String>,
) -> Result<(), ExecutorError> {
use vibesql_ast::Expression;
match expr {
Expression::ColumnRef(col_id) => {
let table = col_id.table_canonical();
let column = col_id.column_canonical();
if column == "*" {
return Ok(());
}
if table.is_none() {
if allowed_aliases.contains(column)
|| allowed_aliases.contains(&column.to_lowercase())
{
return Ok(());
}
}
if schema.get_column_index(table, column).is_some() {
return Ok(());
}
if is_rowid_pseudo_column(column) {
if let Some(qualifier) = table {
let qualifier_lower = qualifier.to_lowercase();
let table_exists =
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_exists || table_in_outer {
return Ok(());
}
} else {
if !schema.table_schemas.is_empty() {
return Ok(());
}
}
}
if let Some(outer) = outer_schema {
if outer.get_column_index(table, column).is_some() {
return Ok(());
}
}
let mut searched_tables: Vec<String> = schema.table_names();
let mut available_columns: Vec<String> = schema
.table_schemas
.values()
.flat_map(|(_, tbl_schema)| tbl_schema.columns.iter().map(|c| c.name.clone()))
.collect();
if let Some(outer) = outer_schema {
searched_tables.extend(outer.table_names());
available_columns.extend(
outer.table_schemas.values().flat_map(|(_, tbl_schema)| {
tbl_schema.columns.iter().map(|c| c.name.clone())
}),
);
}
return Err(ExecutorError::ColumnNotFound {
column_name: column.to_string(),
table_name: table.map(|t| t.to_string()).unwrap_or_else(|| "unknown".to_string()),
searched_tables,
available_columns,
});
}
Expression::BinaryOp { left, right, .. } => {
validate_expression_column_refs(left, schema, outer_schema, allowed_aliases)?;
validate_expression_column_refs(right, schema, outer_schema, allowed_aliases)
}
Expression::UnaryOp { expr, .. } => {
validate_expression_column_refs(expr, schema, outer_schema, allowed_aliases)
}
Expression::Function { args, .. } => {
for arg in args {
validate_expression_column_refs(arg, schema, outer_schema, allowed_aliases)?;
}
Ok(())
}
Expression::AggregateFunction { args, .. } => {
for arg in args {
validate_expression_column_refs(arg, schema, outer_schema, allowed_aliases)?;
}
Ok(())
}
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,
};
for arg in args {
validate_expression_column_refs(arg, schema, outer_schema, allowed_aliases)?;
}
if let Some(partition_exprs) = &over.partition_by {
for partition_expr in partition_exprs {
validate_expression_column_refs(
partition_expr,
schema,
outer_schema,
allowed_aliases,
)?;
}
}
if let Some(order_items) = &over.order_by {
for order_item in order_items {
validate_expression_column_refs(
&order_item.expr,
schema,
outer_schema,
allowed_aliases,
)?;
}
}
Ok(())
}
Expression::Case { operand, when_clauses, else_result } => {
if let Some(op) = operand {
validate_expression_column_refs(op, schema, outer_schema, allowed_aliases)?;
}
for when_clause in when_clauses {
for cond in &when_clause.conditions {
validate_expression_column_refs(cond, schema, outer_schema, allowed_aliases)?;
}
validate_expression_column_refs(
&when_clause.result,
schema,
outer_schema,
allowed_aliases,
)?;
}
if let Some(else_res) = else_result {
validate_expression_column_refs(else_res, schema, outer_schema, allowed_aliases)?;
}
Ok(())
}
Expression::IsNull { expr, .. } => {
validate_expression_column_refs(expr, schema, outer_schema, allowed_aliases)
}
Expression::IsDistinctFrom { left, right, .. } => {
validate_expression_column_refs(left, schema, outer_schema, allowed_aliases)?;
validate_expression_column_refs(right, schema, outer_schema, allowed_aliases)
}
Expression::IsTruthValue { expr, .. } => {
validate_expression_column_refs(expr, schema, outer_schema, allowed_aliases)
}
Expression::InList { expr, values, .. } => {
validate_expression_column_refs(expr, schema, outer_schema, allowed_aliases)?;
for val in values {
validate_expression_column_refs(val, schema, outer_schema, allowed_aliases)?;
}
Ok(())
}
Expression::In { expr, .. } => {
validate_expression_column_refs(expr, schema, outer_schema, allowed_aliases)
}
Expression::Exists { .. } => Ok(()),
Expression::Between { expr, low, high, .. } => {
validate_expression_column_refs(expr, schema, outer_schema, allowed_aliases)?;
validate_expression_column_refs(low, schema, outer_schema, allowed_aliases)?;
validate_expression_column_refs(high, schema, outer_schema, allowed_aliases)
}
Expression::Like { expr, pattern, .. } | Expression::Glob { expr, pattern, .. } => {
validate_expression_column_refs(expr, schema, outer_schema, allowed_aliases)?;
validate_expression_column_refs(pattern, schema, outer_schema, allowed_aliases)
}
Expression::Cast { expr, .. } => {
validate_expression_column_refs(expr, schema, outer_schema, allowed_aliases)
}
Expression::Literal(_)
| Expression::Wildcard
| Expression::CurrentDate
| Expression::CurrentTime { .. }
| Expression::CurrentTimestamp { .. }
| Expression::Default
| Expression::NextValue { .. }
| Expression::SessionVariable { .. } => Ok(()),
Expression::ScalarSubquery(_) => Ok(()),
Expression::QuantifiedComparison { expr, .. } => {
validate_expression_column_refs(expr, schema, outer_schema, allowed_aliases)
}
Expression::Interval { value, .. } => {
validate_expression_column_refs(value, schema, outer_schema, allowed_aliases)
}
Expression::Position { substring, string, .. } => {
validate_expression_column_refs(substring, schema, outer_schema, allowed_aliases)?;
validate_expression_column_refs(string, schema, outer_schema, allowed_aliases)
}
Expression::Trim { removal_char, string, .. } => {
if let Some(char_expr) = removal_char {
validate_expression_column_refs(char_expr, schema, outer_schema, allowed_aliases)?;
}
validate_expression_column_refs(string, schema, outer_schema, allowed_aliases)
}
Expression::Extract { expr, .. } => {
validate_expression_column_refs(expr, schema, outer_schema, allowed_aliases)
}
Expression::MatchAgainst { search_modifier, .. } => {
validate_expression_column_refs(search_modifier, schema, outer_schema, allowed_aliases)
}
Expression::PseudoVariable { .. } => Ok(()),
Expression::DuplicateKeyValue { .. } => Ok(()),
Expression::Placeholder(_)
| Expression::NumberedPlaceholder(_)
| Expression::NamedPlaceholder(_) => Ok(()),
Expression::Conjunction(children)
| Expression::Disjunction(children)
| Expression::RowValueConstructor(children) => {
for child in children {
validate_expression_column_refs(child, schema, outer_schema, allowed_aliases)?;
}
Ok(())
}
Expression::Collate { expr, .. } => {
validate_expression_column_refs(expr, schema, outer_schema, allowed_aliases)
}
Expression::Raise { error_message, .. } => {
if let Some(msg) = error_message {
validate_expression_column_refs(msg, schema, outer_schema, allowed_aliases)?;
}
Ok(())
}
}
}