use vibesql_ast::{Expression, SelectItem, SelectStmt};
pub(crate) fn is_correlated(subquery: &SelectStmt) -> bool {
if let Some(where_clause) = &subquery.where_clause {
if has_external_column_refs(where_clause, subquery) {
return true;
}
}
for item in &subquery.select_list {
if let SelectItem::Expression { expr, .. } = item {
if has_external_column_refs(expr, subquery) {
return true;
}
}
}
if let Some(having) = &subquery.having {
if has_external_column_refs(having, subquery) {
return true;
}
}
if let Some(from) = &subquery.from {
if from_clause_has_external_refs(from, subquery) {
return true;
}
}
false
}
pub(crate) fn has_external_column_refs(expr: &Expression, subquery: &SelectStmt) -> bool {
match expr {
Expression::ColumnRef(col_id) if col_id.schema_canonical().is_some() => {
true
}
Expression::ColumnRef(col_id) if col_id.table_canonical().is_some() => {
!subquery_references_table(subquery, col_id.table_canonical().unwrap())
}
Expression::ColumnRef(col_id) => {
let column = col_id.column_canonical();
if let Some(from) = &subquery.from {
let col_prefix = column.chars().next().unwrap_or('_').to_ascii_lowercase();
if column.chars().nth(1) == Some('_') {
let from_table_prefixes = extract_table_prefixes(from);
!from_table_prefixes.contains(&col_prefix)
} else {
false
}
} else {
true
}
}
Expression::BinaryOp { left, right, .. } => {
has_external_column_refs(left, subquery) || has_external_column_refs(right, subquery)
}
Expression::UnaryOp { expr, .. } => has_external_column_refs(expr, subquery),
Expression::IsNull { expr, .. } => has_external_column_refs(expr, subquery),
Expression::IsDistinctFrom { left, right, .. } => {
has_external_column_refs(left, subquery) || has_external_column_refs(right, subquery)
}
Expression::IsTruthValue { expr, .. } => has_external_column_refs(expr, subquery),
Expression::Case { operand, when_clauses, else_result } => {
operand.as_ref().is_some_and(|e| has_external_column_refs(e, subquery))
|| when_clauses.iter().any(|clause| {
clause.conditions.iter().any(|cond| has_external_column_refs(cond, subquery))
|| has_external_column_refs(&clause.result, subquery)
})
|| else_result.as_ref().is_some_and(|e| has_external_column_refs(e, subquery))
}
Expression::ScalarSubquery(subq) | Expression::Exists { subquery: subq, .. } => {
is_correlated(subq)
}
Expression::In { subquery: subq, expr, .. } => {
has_external_column_refs(expr, subquery) || is_correlated(subq)
}
Expression::QuantifiedComparison { expr, subquery: subq, .. } => {
has_external_column_refs(expr, subquery) || is_correlated(subq)
}
Expression::InList { expr, values, .. } => {
has_external_column_refs(expr, subquery)
|| values.iter().any(|v| has_external_column_refs(v, subquery))
}
Expression::Between { expr, low, high, .. } => {
has_external_column_refs(expr, subquery)
|| has_external_column_refs(low, subquery)
|| has_external_column_refs(high, subquery)
}
Expression::Cast { expr, .. } => has_external_column_refs(expr, subquery),
Expression::Function { args, .. } | Expression::AggregateFunction { args, .. } => {
args.iter().any(|arg| has_external_column_refs(arg, subquery))
}
Expression::Position { substring, string, .. } => {
has_external_column_refs(substring, subquery)
|| has_external_column_refs(string, subquery)
}
Expression::Trim { removal_char, string, .. } => {
removal_char.as_ref().is_some_and(|e| has_external_column_refs(e, subquery))
|| has_external_column_refs(string, subquery)
}
Expression::Extract { expr, .. } => has_external_column_refs(expr, subquery),
Expression::Like { expr, pattern, .. } | Expression::Glob { expr, pattern, .. } => {
has_external_column_refs(expr, subquery) || has_external_column_refs(pattern, subquery)
}
Expression::Interval { value, .. } => has_external_column_refs(value, subquery),
Expression::Conjunction(children)
| Expression::Disjunction(children)
| Expression::RowValueConstructor(children) => {
children.iter().any(|child| has_external_column_refs(child, subquery))
}
Expression::Collate { expr, .. } => has_external_column_refs(expr, subquery),
Expression::Raise { error_message, .. } => {
error_message.as_ref().is_some_and(|msg| has_external_column_refs(msg, subquery))
}
Expression::Literal(_)
| Expression::Wildcard
| Expression::CurrentDate
| Expression::CurrentTime { .. }
| Expression::CurrentTimestamp { .. }
| Expression::Default
| Expression::DuplicateKeyValue { .. }
| Expression::WindowFunction { .. }
| Expression::NextValue { .. }
| Expression::MatchAgainst { .. }
| Expression::PseudoVariable { .. }
| Expression::SessionVariable { .. }
| Expression::Placeholder(_)
| Expression::NumberedPlaceholder(_)
| Expression::NamedPlaceholder(_) => false,
}
}
pub(super) fn subquery_references_table(subquery: &SelectStmt, table_name: &str) -> bool {
if let Some(from) = &subquery.from {
from_clause_contains_table(from, table_name)
} else {
false
}
}
fn extract_table_prefixes(from: &vibesql_ast::FromClause) -> Vec<char> {
fn collect(from: &vibesql_ast::FromClause, prefixes: &mut Vec<char>) {
match from {
vibesql_ast::FromClause::Table { name, .. } => {
if let Some(c) = name.chars().next() {
prefixes.push(c.to_ascii_lowercase());
}
}
vibesql_ast::FromClause::Join { left, right, .. } => {
collect(left, prefixes);
collect(right, prefixes);
}
vibesql_ast::FromClause::Subquery { alias, .. } => {
if let Some(c) = alias.chars().next() {
prefixes.push(c.to_ascii_lowercase());
}
}
vibesql_ast::FromClause::Values { alias, .. } => {
if let Some(c) = alias.chars().next() {
prefixes.push(c.to_ascii_lowercase());
}
}
}
}
let mut prefixes = Vec::new();
collect(from, &mut prefixes);
prefixes
}
pub(crate) fn from_clause_has_external_refs(
from: &vibesql_ast::FromClause,
subquery: &SelectStmt,
) -> bool {
match from {
vibesql_ast::FromClause::Table { .. } => false,
vibesql_ast::FromClause::Subquery { query, .. } => {
is_correlated(query)
}
vibesql_ast::FromClause::Join { left, right, condition, .. } => {
from_clause_has_external_refs(left, subquery)
|| from_clause_has_external_refs(right, subquery)
|| condition.as_ref().is_some_and(|cond| has_external_column_refs(cond, subquery))
}
vibesql_ast::FromClause::Values { rows, .. } => {
rows.iter().any(|row| row.iter().any(|expr| has_external_column_refs(expr, subquery)))
}
}
}
fn from_clause_contains_table(from: &vibesql_ast::FromClause, table_name: &str) -> bool {
match from {
vibesql_ast::FromClause::Table { name, alias, .. } => {
if let Some(a) = alias {
a.eq_ignore_ascii_case(table_name)
} else {
name.eq_ignore_ascii_case(table_name)
}
}
vibesql_ast::FromClause::Join { left, right, .. } => {
from_clause_contains_table(left, table_name)
|| from_clause_contains_table(right, table_name)
}
vibesql_ast::FromClause::Subquery { alias, .. } => alias == table_name,
vibesql_ast::FromClause::Values { alias, .. } => alias == table_name,
}
}