use vibesql_ast::{BinaryOperator, Expression};
use crate::schema::CombinedSchema;
#[derive(Debug, Clone)]
pub struct EquiJoinInfo {
pub left_col_idx: usize,
pub right_col_idx: usize,
}
#[derive(Debug, Clone)]
pub struct ArithmeticEquiJoinInfo {
pub left_col_idx: usize,
pub right_col_idx: usize,
pub offset: i64,
}
#[derive(Debug, Clone)]
pub struct MultiColumnEquiJoinInfo {
pub left_col_indices: Vec<usize>,
pub right_col_indices: Vec<usize>,
}
pub fn analyze_equi_join(
condition: &Expression,
schema: &CombinedSchema,
left_column_count: usize,
) -> Option<EquiJoinInfo> {
analyze_single_equi_join(condition, schema, left_column_count)
}
fn analyze_single_equi_join(
condition: &Expression,
schema: &CombinedSchema,
left_column_count: usize,
) -> Option<EquiJoinInfo> {
match condition {
Expression::BinaryOp { op: BinaryOperator::Equal, left, right } => {
if let (Some(left_idx), Some(right_idx)) =
(extract_column_index(left, schema), extract_column_index(right, schema))
{
if left_idx < left_column_count && right_idx >= left_column_count {
return Some(EquiJoinInfo {
left_col_idx: left_idx,
right_col_idx: right_idx - left_column_count,
});
} else if right_idx < left_column_count && left_idx >= left_column_count {
return Some(EquiJoinInfo {
left_col_idx: right_idx,
right_col_idx: left_idx - left_column_count,
});
}
}
None
}
_ => None,
}
}
#[derive(Debug)]
pub struct CompoundEquiJoinResult {
pub equi_join: EquiJoinInfo,
pub remaining_conditions: Vec<Expression>,
}
#[derive(Debug)]
pub struct MultiColumnEquiJoinResult {
pub equi_joins: MultiColumnEquiJoinInfo,
pub remaining_conditions: Vec<Expression>,
}
pub fn analyze_multi_column_equi_join(
condition: &Expression,
schema: &CombinedSchema,
left_column_count: usize,
) -> Option<MultiColumnEquiJoinResult> {
if let Some(equi_join) = analyze_single_equi_join(condition, schema, left_column_count) {
return Some(MultiColumnEquiJoinResult {
equi_joins: MultiColumnEquiJoinInfo {
left_col_indices: vec![equi_join.left_col_idx],
right_col_indices: vec![equi_join.right_col_idx],
},
remaining_conditions: vec![],
});
}
match condition {
Expression::BinaryOp { op: BinaryOperator::And, .. } => {
let mut conditions = Vec::new();
flatten_and_conditions(condition, &mut conditions);
let mut left_indices = Vec::new();
let mut right_indices = Vec::new();
let mut remaining = Vec::new();
for cond in conditions {
if let Some(equi_join) = analyze_single_equi_join(cond, schema, left_column_count) {
left_indices.push(equi_join.left_col_idx);
right_indices.push(equi_join.right_col_idx);
} else {
remaining.push(cond.clone());
}
}
if !left_indices.is_empty() {
return Some(MultiColumnEquiJoinResult {
equi_joins: MultiColumnEquiJoinInfo {
left_col_indices: left_indices,
right_col_indices: right_indices,
},
remaining_conditions: remaining,
});
}
None
}
_ => None,
}
}
pub fn analyze_compound_equi_join(
condition: &Expression,
schema: &CombinedSchema,
left_column_count: usize,
) -> Option<CompoundEquiJoinResult> {
if let Some(equi_join) = analyze_single_equi_join(condition, schema, left_column_count) {
return Some(CompoundEquiJoinResult { equi_join, remaining_conditions: vec![] });
}
match condition {
Expression::BinaryOp { op: BinaryOperator::And, left: _, right: _ } => {
let mut conditions = Vec::new();
flatten_and_conditions(condition, &mut conditions);
for (i, cond) in conditions.iter().enumerate() {
if let Some(equi_join) = analyze_single_equi_join(cond, schema, left_column_count) {
let remaining: Vec<Expression> = conditions
.iter()
.enumerate()
.filter(|(j, _)| *j != i)
.map(|(_, c)| (*c).clone())
.collect();
return Some(CompoundEquiJoinResult {
equi_join,
remaining_conditions: remaining,
});
}
}
None
}
_ => None,
}
}
fn flatten_and_conditions<'a>(expr: &'a Expression, out: &mut Vec<&'a Expression>) {
match expr {
Expression::BinaryOp { op: BinaryOperator::And, left, right } => {
flatten_and_conditions(left, out);
flatten_and_conditions(right, out);
}
_ => out.push(expr),
}
}
fn extract_column_index(expr: &Expression, schema: &CombinedSchema) -> Option<usize> {
match expr {
Expression::ColumnRef(col_id) => {
schema.get_column_index(col_id.table_canonical(), col_id.column_canonical())
}
_ => None,
}
}
fn flatten_or_conditions<'a>(expr: &'a Expression, out: &mut Vec<&'a Expression>) {
match expr {
Expression::BinaryOp { op: BinaryOperator::Or, left, right } => {
flatten_or_conditions(left, out);
flatten_or_conditions(right, out);
}
_ => out.push(expr),
}
}
pub fn analyze_or_equi_join(
condition: &Expression,
schema: &CombinedSchema,
left_column_count: usize,
) -> Option<CompoundEquiJoinResult> {
if !matches!(condition, Expression::BinaryOp { op: BinaryOperator::Or, .. }) {
return None;
}
let mut or_branches = Vec::new();
flatten_or_conditions(condition, &mut or_branches);
if or_branches.is_empty() {
return None;
}
let mut branch_equijoins: Vec<Vec<EquiJoinInfo>> = Vec::new();
for branch in &or_branches {
let mut branch_joins = Vec::new();
match branch {
Expression::BinaryOp { op: BinaryOperator::Equal, .. } => {
if let Some(equi_join) = analyze_single_equi_join(branch, schema, left_column_count)
{
branch_joins.push(equi_join);
}
}
Expression::BinaryOp { op: BinaryOperator::And, .. } => {
let mut and_conditions = Vec::new();
flatten_and_conditions(branch, &mut and_conditions);
for cond in and_conditions {
if let Some(equi_join) =
analyze_single_equi_join(cond, schema, left_column_count)
{
branch_joins.push(equi_join);
}
}
}
_ => {
return None;
}
}
if branch_joins.is_empty() {
return None;
}
branch_equijoins.push(branch_joins);
}
if branch_equijoins.is_empty() {
return None;
}
for first_equijoin in &branch_equijoins[0] {
let mut found_in_all = true;
for other_branch in &branch_equijoins[1..] {
let mut found_in_this_branch = false;
for other_equijoin in other_branch {
if first_equijoin.left_col_idx == other_equijoin.left_col_idx
&& first_equijoin.right_col_idx == other_equijoin.right_col_idx
{
found_in_this_branch = true;
break;
}
}
if !found_in_this_branch {
found_in_all = false;
break;
}
}
if found_in_all {
return Some(CompoundEquiJoinResult {
equi_join: first_equijoin.clone(),
remaining_conditions: vec![condition.clone()],
});
}
}
None
}
pub fn analyze_arithmetic_equi_join(
condition: &Expression,
schema: &CombinedSchema,
left_column_count: usize,
) -> Option<ArithmeticEquiJoinInfo> {
match condition {
Expression::BinaryOp { op: BinaryOperator::Equal, left, right } => {
if let Some(info) = try_extract_arithmetic_join(left, right, schema, left_column_count)
{
return Some(info);
}
if let Some(info) = try_extract_arithmetic_join(right, left, schema, left_column_count)
{
return Some(info);
}
None
}
_ => None,
}
}
fn try_extract_arithmetic_join(
simple_side: &Expression,
arithmetic_side: &Expression,
schema: &CombinedSchema,
left_column_count: usize,
) -> Option<ArithmeticEquiJoinInfo> {
let simple_col_idx = extract_column_index(simple_side, schema)?;
match arithmetic_side {
Expression::BinaryOp { op, left: arith_left, right: arith_right } => {
let is_plus = matches!(op, BinaryOperator::Plus);
let is_minus = matches!(op, BinaryOperator::Minus);
if !is_plus && !is_minus {
return None;
}
let arith_col_idx = extract_column_index(arith_left, schema)?;
let offset_value = match arith_right.as_ref() {
Expression::Literal(vibesql_types::SqlValue::Integer(n)) => *n,
_ => return None,
};
let offset = if is_plus { offset_value } else { -offset_value };
if simple_col_idx < left_column_count && arith_col_idx >= left_column_count {
Some(ArithmeticEquiJoinInfo {
left_col_idx: simple_col_idx,
right_col_idx: arith_col_idx - left_column_count,
offset,
})
} else if arith_col_idx < left_column_count && simple_col_idx >= left_column_count {
Some(ArithmeticEquiJoinInfo {
left_col_idx: arith_col_idx,
right_col_idx: simple_col_idx - left_column_count,
offset: -offset, })
} else {
None
}
}
_ => None,
}
}