use std::collections::{HashMap, HashSet};
use vibesql_ast::{BinaryOperator, Expression, FromClause, JoinType};
use crate::{errors::ExecutorError, schema::CombinedSchema, select::columnar};
pub(super) fn is_columnar_supported_join(from: &FromClause) -> bool {
match from {
FromClause::Table { .. } | FromClause::Subquery { .. } | FromClause::Values { .. } => true,
FromClause::Join { left, right, join_type, .. } => {
matches!(
join_type,
JoinType::Inner | JoinType::Cross | JoinType::LeftOuter | JoinType::RightOuter
) && is_columnar_supported_join(left)
&& is_columnar_supported_join(right)
}
}
}
pub(super) fn has_outer_join(from: &FromClause) -> bool {
match from {
FromClause::Table { .. } | FromClause::Subquery { .. } | FromClause::Values { .. } => false,
FromClause::Join { left, right, join_type, .. } => {
matches!(
join_type,
JoinType::LeftOuter | JoinType::RightOuter | JoinType::FullOuter
) || has_outer_join(left)
|| has_outer_join(right)
}
}
}
pub(super) fn expression_has_is_null(expr: &Expression) -> bool {
match expr {
Expression::IsNull { .. } => true,
Expression::BinaryOp { left, right, .. } => {
expression_has_is_null(left) || expression_has_is_null(right)
}
Expression::UnaryOp { expr, .. } => expression_has_is_null(expr),
_ => false,
}
}
pub(super) fn has_cross_join_with_on_condition(from: &FromClause) -> bool {
match from {
FromClause::Table { .. } | FromClause::Subquery { .. } | FromClause::Values { .. } => false,
FromClause::Join { left, right, join_type, condition, using_columns, natural, .. } => {
if matches!(join_type, JoinType::Cross)
&& (condition.is_some() || using_columns.is_some() || *natural)
{
return true;
}
has_cross_join_with_on_condition(left) || has_cross_join_with_on_condition(right)
}
}
}
pub(super) type SimpleTableRef = (String, Option<String>, bool);
pub(super) fn flatten_join_tree_simple(from: &FromClause, tables: &mut Vec<SimpleTableRef>) {
match from {
FromClause::Table { name, alias, .. } => {
tables.push((name.clone(), alias.clone(), false));
}
FromClause::Subquery { alias, .. } => {
tables.push((alias.clone(), Some(alias.clone()), true));
}
FromClause::Values { alias, .. } => {
tables.push((alias.clone(), Some(alias.clone()), true));
}
FromClause::Join { left, right, .. } => {
flatten_join_tree_simple(left, tables);
flatten_join_tree_simple(right, tables);
}
}
}
pub(super) fn flatten_join_tree_with_types(
from: &FromClause,
tables: &mut Vec<(SimpleTableRef, Option<JoinType>)>,
) {
match from {
FromClause::Table { name, alias, .. } => {
tables.push(((name.clone(), alias.clone(), false), None));
}
FromClause::Subquery { alias, .. } => {
tables.push(((alias.clone(), Some(alias.clone()), true), None));
}
FromClause::Values { alias, .. } => {
tables.push(((alias.clone(), Some(alias.clone()), true), None));
}
FromClause::Join { left, right, join_type, .. } => {
flatten_join_tree_with_types(left, tables);
match right.as_ref() {
FromClause::Table { name, alias, .. } => {
tables.push(((name.clone(), alias.clone(), false), Some(join_type.clone())));
}
FromClause::Subquery { alias, .. } => {
tables.push((
(alias.clone(), Some(alias.clone()), true),
Some(join_type.clone()),
));
}
FromClause::Values { alias, .. } => {
tables.push((
(alias.clone(), Some(alias.clone()), true),
Some(join_type.clone()),
));
}
FromClause::Join { .. } => {
let start_idx = tables.len();
flatten_join_tree_with_types(right, tables);
if start_idx < tables.len() {
tables[start_idx].1 = Some(join_type.clone());
}
}
}
}
}
}
#[derive(Debug, Clone)]
pub(super) struct EquiJoinCondition {
pub left_table: Option<String>,
pub left_column: String,
pub right_table: Option<String>,
pub right_column: String,
}
pub(super) fn extract_join_conditions(from: &FromClause, conditions: &mut Vec<EquiJoinCondition>) {
match from {
FromClause::Table { .. } | FromClause::Subquery { .. } | FromClause::Values { .. } => {}
FromClause::Join { left, right, condition, join_type, .. } => {
if !matches!(
join_type,
JoinType::Inner | JoinType::Cross | JoinType::LeftOuter | JoinType::RightOuter
) {
return;
}
if let Some(cond) = condition {
extract_equijoin_conditions(cond, conditions);
}
extract_join_conditions(left, conditions);
extract_join_conditions(right, conditions);
}
}
}
pub(super) fn extract_equijoin_conditions(
expr: &Expression,
conditions: &mut Vec<EquiJoinCondition>,
) {
match expr {
Expression::BinaryOp { left, op: BinaryOperator::And, right } => {
extract_equijoin_conditions(left, conditions);
extract_equijoin_conditions(right, conditions);
}
Expression::BinaryOp { left, op: BinaryOperator::Equal, right } => {
if let (Expression::ColumnRef(left_col_id), Expression::ColumnRef(right_col_id)) =
(left.as_ref(), right.as_ref())
{
if left_col_id.schema_canonical().is_none()
&& right_col_id.schema_canonical().is_none()
{
conditions.push(EquiJoinCondition {
left_table: left_col_id.table_canonical().map(|t| t.to_string()),
left_column: left_col_id.column_canonical().to_string(),
right_table: right_col_id.table_canonical().map(|t| t.to_string()),
right_column: right_col_id.column_canonical().to_string(),
});
}
}
}
_ => {}
}
}
pub(super) fn extract_non_join_predicates(
expr: &Expression,
schema: &CombinedSchema,
case_sensitive_like: bool,
) -> Option<Vec<columnar::ColumnPredicate>> {
let mut predicates = Vec::new();
extract_non_join_predicates_recursive(expr, schema, case_sensitive_like, &mut predicates);
if predicates.is_empty() {
None
} else {
Some(predicates)
}
}
fn extract_non_join_predicates_recursive(
expr: &Expression,
schema: &CombinedSchema,
case_sensitive_like: bool,
predicates: &mut Vec<columnar::ColumnPredicate>,
) {
match expr {
Expression::BinaryOp { left, op: BinaryOperator::And, right } => {
extract_non_join_predicates_recursive(left, schema, case_sensitive_like, predicates);
extract_non_join_predicates_recursive(right, schema, case_sensitive_like, predicates);
}
Expression::BinaryOp { left, op: BinaryOperator::Equal, right } => {
if matches!(left.as_ref(), Expression::ColumnRef(_))
&& matches!(right.as_ref(), Expression::ColumnRef(_))
{
return;
}
if let Some(pred) = columnar::extract_column_predicates(expr, schema, case_sensitive_like) {
predicates.extend(pred);
}
}
_ => {
if let Some(pred) = columnar::extract_column_predicates(expr, schema, case_sensitive_like) {
predicates.extend(pred);
}
}
}
}
pub(super) fn build_combined_schema(
batches: &[(String, Option<String>, columnar::ColumnarBatch, vibesql_catalog::TableSchema)],
) -> CombinedSchema {
let mut combined = CombinedSchema {
table_schemas: HashMap::new(),
total_columns: 0,
hidden_columns: HashSet::new(),
outer_schema: None,
duplicate_aliases: HashSet::new(),
joined_columns: HashSet::new(),
using_coalesce_indices: HashMap::new(),
column_replacement_map: HashMap::new(),
alias_tables: HashSet::new(),
shadowed_tables: HashMap::new(),
};
for (table_name, alias, _batch, schema) in batches {
let name = alias.as_ref().unwrap_or(table_name);
combined.insert_table(name.clone(), combined.total_columns, schema.clone());
combined.total_columns += schema.columns.len();
}
combined
}
pub(super) fn is_column_in_tables(column: &str, tables: &[&str], schema: &CombinedSchema) -> bool {
tables.iter().any(|t| is_column_in_table(column, t, schema))
}
pub(super) fn is_column_in_table(column: &str, table: &str, schema: &CombinedSchema) -> bool {
if let Some((_, table_schema)) = schema.get_table(table) {
table_schema.columns.iter().any(|c| c.name.eq_ignore_ascii_case(column))
} else {
false
}
}
pub(super) fn resolve_join_column_indices(
cond: &EquiJoinCondition,
joined_tables: &[&str],
new_table: &str,
new_table_schema: &vibesql_catalog::TableSchema,
combined_schema: &CombinedSchema,
) -> Result<(usize, usize), ExecutorError> {
let left_in_joined = cond.left_table.as_deref().map_or_else(
|| is_column_in_tables(&cond.left_column, joined_tables, combined_schema),
|t| joined_tables.contains(&t),
);
let (left_col, right_col) = if left_in_joined {
(&cond.left_column, &cond.right_column)
} else {
(&cond.right_column, &cond.left_column)
};
let left_idx = combined_schema.get_column_index(None, left_col).ok_or_else(|| {
ExecutorError::ColumnNotFound {
column_name: left_col.clone(),
table_name: String::new(),
searched_tables: joined_tables.iter().map(|s| s.to_string()).collect(),
available_columns: vec![],
}
})?;
let right_idx = new_table_schema
.columns
.iter()
.position(|c| c.name.eq_ignore_ascii_case(right_col))
.ok_or_else(|| ExecutorError::ColumnNotFound {
column_name: right_col.clone(),
table_name: new_table.to_string(),
searched_tables: vec![new_table.to_string()],
available_columns: new_table_schema.columns.iter().map(|c| c.name.clone()).collect(),
})?;
Ok((left_idx, right_idx))
}
pub(super) fn extract_single_table_name(from_clause: &FromClause) -> Option<String> {
match from_clause {
FromClause::Table { name, .. } => Some(name.clone()),
FromClause::Join { .. } => None, FromClause::Subquery { .. } => None, FromClause::Values { .. } => None, }
}
pub(super) fn extract_table_name_and_alias(
from_clause: &FromClause,
) -> Option<(String, Option<String>)> {
match from_clause {
FromClause::Table { name, alias, .. } => Some((name.clone(), alias.clone())),
FromClause::Join { .. } => None, FromClause::Subquery { .. } => None, FromClause::Values { .. } => None, }
}