use vibesql_ast::{BinaryOperator, ColumnIdentifier, Expression, SelectItem, SelectStmt};
pub(super) fn rewrite_in_to_exists(
in_expr: &Expression,
subquery: &SelectStmt,
negated: bool,
outer_tables: &[String],
) -> Expression {
let mut exists_subquery = subquery.clone();
exists_subquery.select_list = vec![SelectItem::Expression {
expr: Expression::Literal(vibesql_types::SqlValue::Integer(1)),
alias: None,
source_text: None,
}];
exists_subquery.limit = Some(Expression::Literal(vibesql_types::SqlValue::Integer(1)));
if let Some(SelectItem::Expression { expr: subquery_col, .. }) = subquery.select_list.first() {
let qualified_in_expr = qualify_outer_column_refs(in_expr, outer_tables);
let correlation_predicate = Expression::BinaryOp {
op: BinaryOperator::Equal,
left: Box::new(subquery_col.clone()),
right: Box::new(qualified_in_expr),
};
exists_subquery.where_clause = if let Some(existing_where) = &subquery.where_clause {
Some(Expression::BinaryOp {
op: BinaryOperator::And,
left: Box::new(existing_where.clone()),
right: Box::new(correlation_predicate),
})
} else {
Some(correlation_predicate)
};
}
Expression::Exists { subquery: Box::new(exists_subquery), negated }
}
fn qualify_outer_column_refs(expr: &Expression, outer_tables: &[String]) -> Expression {
let outer_table = match outer_tables.first() {
Some(t) => t,
None => return expr.clone(),
};
match expr {
Expression::ColumnRef(col_id) => {
if col_id.table_canonical().is_none() {
let qualified_col = ColumnIdentifier::qualified(
outer_table, false, col_id.column_display(), false, );
Expression::ColumnRef(qualified_col)
} else {
expr.clone()
}
}
Expression::BinaryOp { op, left, right } => Expression::BinaryOp {
op: *op,
left: Box::new(qualify_outer_column_refs(left, outer_tables)),
right: Box::new(qualify_outer_column_refs(right, outer_tables)),
},
Expression::UnaryOp { op, expr: inner } => Expression::UnaryOp {
op: *op,
expr: Box::new(qualify_outer_column_refs(inner, outer_tables)),
},
Expression::IsNull { expr: inner, negated } => Expression::IsNull {
expr: Box::new(qualify_outer_column_refs(inner, outer_tables)),
negated: *negated,
},
Expression::Between { expr: inner, low, high, negated, symmetric } => Expression::Between {
expr: Box::new(qualify_outer_column_refs(inner, outer_tables)),
low: Box::new(qualify_outer_column_refs(low, outer_tables)),
high: Box::new(qualify_outer_column_refs(high, outer_tables)),
negated: *negated,
symmetric: *symmetric,
},
Expression::Cast { expr: inner, data_type } => Expression::Cast {
expr: Box::new(qualify_outer_column_refs(inner, outer_tables)),
data_type: data_type.clone(),
},
_ => expr.clone(),
}
}
pub(super) fn rewrite_exists_to_in(
subquery: &SelectStmt,
negated: bool,
outer_tables: &[String],
) -> Option<(Expression, SelectStmt, bool)> {
if subquery.set_operation.is_some() {
return None;
}
let (inner_table, inner_alias) = match &subquery.from {
Some(vibesql_ast::FromClause::Table { name, alias, .. }) => (name.clone(), alias.clone()),
_ => return None,
};
let inner_table_ref = inner_alias.as_ref().unwrap_or(&inner_table);
let where_clause = subquery.where_clause.as_ref()?;
let (correlation, remaining_predicates) =
extract_correlation_predicate(where_clause, inner_table_ref, outer_tables)?;
let mut decorrelated = subquery.clone();
decorrelated.distinct = true;
decorrelated.select_list = vec![SelectItem::Expression {
expr: correlation.inner_expr.clone(),
alias: None,
source_text: None,
}];
decorrelated.where_clause = remaining_predicates;
Some((correlation.outer_expr, decorrelated, negated))
}
struct CorrelationPredicate {
inner_expr: Expression,
outer_expr: Expression,
}
fn extract_correlation_predicate(
expr: &Expression,
inner_table: &str,
outer_tables: &[String],
) -> Option<(CorrelationPredicate, Option<Expression>)> {
match expr {
Expression::BinaryOp { op: BinaryOperator::Equal, left, right } => {
if let Some(correlation) =
try_extract_correlation(left, right, inner_table, outer_tables)
{
return Some((correlation, None));
}
if let Some(correlation) =
try_extract_correlation(right, left, inner_table, outer_tables)
{
return Some((correlation, None));
}
None
}
Expression::BinaryOp { op: BinaryOperator::And, left, right } => {
if let Some((correlation, left_remaining)) =
extract_correlation_predicate(left, inner_table, outer_tables)
{
let remaining = combine_predicates(left_remaining, Some((**right).clone()));
return Some((correlation, remaining));
}
if let Some((correlation, right_remaining)) =
extract_correlation_predicate(right, inner_table, outer_tables)
{
let remaining = combine_predicates(Some((**left).clone()), right_remaining);
return Some((correlation, remaining));
}
None
}
_ => None,
}
}
fn try_extract_correlation(
left: &Expression,
right: &Expression,
inner_table: &str,
outer_tables: &[String],
) -> Option<CorrelationPredicate> {
if is_from_table(left, inner_table) && is_from_outer_tables(right, outer_tables, inner_table) {
return Some(CorrelationPredicate { inner_expr: left.clone(), outer_expr: right.clone() });
}
if is_from_table(right, inner_table) && is_from_outer_tables(left, outer_tables, inner_table) {
return Some(CorrelationPredicate { inner_expr: right.clone(), outer_expr: left.clone() });
}
None
}
fn is_from_table(expr: &Expression, table: &str) -> bool {
match expr {
Expression::ColumnRef(col_id) if col_id.schema_canonical().is_none() => {
match col_id.table_canonical() {
Some(t) => t.eq_ignore_ascii_case(table),
None => true, }
}
_ => false,
}
}
fn is_from_outer_tables(expr: &Expression, outer_tables: &[String], inner_table: &str) -> bool {
match expr {
Expression::ColumnRef(col_id) if col_id.schema_canonical().is_none() => {
match col_id.table_canonical() {
Some(t) => outer_tables.iter().any(|ot| ot.eq_ignore_ascii_case(t)),
None => {
let column = col_id.column_canonical();
let inner_prefix =
inner_table.chars().next().unwrap_or('_').to_ascii_lowercase();
let col_prefix = column.chars().next().unwrap_or('_').to_ascii_lowercase();
if col_prefix != inner_prefix {
for outer in outer_tables {
let outer_prefix =
outer.chars().next().unwrap_or('_').to_ascii_lowercase();
if col_prefix == outer_prefix {
return true;
}
}
}
false
}
}
}
_ => false,
}
}
fn combine_predicates(left: Option<Expression>, right: Option<Expression>) -> Option<Expression> {
match (left, right) {
(Some(l), Some(r)) => Some(Expression::BinaryOp {
op: BinaryOperator::And,
left: Box::new(l),
right: Box::new(r),
}),
(Some(l), None) => Some(l),
(None, Some(r)) => Some(r),
(None, None) => None,
}
}
pub(super) fn add_distinct_to_in_subquery(subquery: &SelectStmt) -> SelectStmt {
let mut optimized = subquery.clone();
if !subquery.distinct && !group_by_ensures_distinct(subquery) {
optimized.distinct = true;
}
optimized
}
fn group_by_ensures_distinct(subquery: &SelectStmt) -> bool {
use vibesql_ast::{Expression, GroupByClause, SelectItem};
let group_by_exprs = match &subquery.group_by {
Some(GroupByClause::Simple(exprs)) => exprs,
_ => return false,
};
if group_by_exprs.is_empty() {
return false;
}
let group_by_cols: Vec<&str> = group_by_exprs
.iter()
.filter_map(|expr| match expr {
Expression::ColumnRef(col_id) => Some(col_id.column_canonical()),
_ => None,
})
.collect();
if group_by_cols.len() != group_by_exprs.len() {
return false;
}
for item in &subquery.select_list {
match item {
SelectItem::Expression { expr, .. } => match expr {
Expression::ColumnRef(col_id) => {
let column = col_id.column_canonical();
if !group_by_cols.iter().any(|&gc| gc.eq_ignore_ascii_case(column)) {
return false;
}
}
Expression::AggregateFunction { .. } => {
}
_ => {
return false;
}
},
SelectItem::Wildcard { .. } | SelectItem::QualifiedWildcard { .. } => {
return false;
}
}
}
true
}