use std::collections::{HashMap, HashSet};
use crate::{
errors::ExecutorError, optimizer::PredicatePlan, schema::CombinedSchema,
select::cte::CteResult, timeout::TimeoutContext,
};
mod inl;
mod predicates;
mod pushdown;
mod semi_anti;
const INL_BASE_THRESHOLD: usize = 1000;
const INL_MAX_THRESHOLD: usize = 100_000;
const INL_SIZE_RATIO_THRESHOLD: usize = 10;
#[allow(clippy::too_many_arguments)]
pub(crate) fn execute_join<F>(
left: &vibesql_ast::FromClause,
right: &vibesql_ast::FromClause,
join_type: &vibesql_ast::JoinType,
condition: &Option<vibesql_ast::Expression>,
using_columns: &Option<Vec<String>>,
natural: bool,
cte_results: &HashMap<String, CteResult>,
database: &vibesql_storage::Database,
where_clause: Option<&vibesql_ast::Expression>,
outer_row: Option<&vibesql_storage::Row>,
outer_schema: Option<&crate::schema::CombinedSchema>,
join_alias: Option<&str>,
execute_subquery: F,
) -> Result<super::FromResult, ExecutorError>
where
F: Fn(&vibesql_ast::SelectStmt) -> Result<crate::select::SelectResult, ExecutorError> + Copy,
{
let left_where_clause = match join_type {
vibesql_ast::JoinType::RightOuter | vibesql_ast::JoinType::FullOuter => {
if let Some(where_expr) = where_clause {
pushdown::filter_out_nullable_side_predicates(left, where_expr, database)
} else {
None
}
}
_ => where_clause.cloned(),
};
let left_result = super::execute_from_clause(
left,
cte_results,
database,
left_where_clause.as_ref(),
None,
None,
outer_row,
outer_schema,
execute_subquery,
)?;
if matches!(join_type, vibesql_ast::JoinType::Semi) {
let left_row_count = left_result.as_slice().len();
if left_row_count > 0 {
let should_use_inl =
semi_anti::should_use_inl_for_semi_join(left_row_count, right, condition, database);
if should_use_inl {
if let Some(result) =
inl::try_index_nested_loop_semi_join(&left_result, right, condition, database)?
{
return Ok(result);
}
if let Some(result) =
inl::try_prefix_scan_semi_join(&left_result, right, condition, database)?
{
return Ok(result);
}
}
}
}
let right_where_clause = match join_type {
vibesql_ast::JoinType::Semi | vibesql_ast::JoinType::Anti => {
if let Some(cond) = condition {
pushdown::extract_right_only_predicates(right, cond, database)
} else {
None
}
}
vibesql_ast::JoinType::LeftOuter | vibesql_ast::JoinType::FullOuter => {
if let Some(where_expr) = where_clause {
pushdown::filter_out_nullable_side_predicates(right, where_expr, database)
} else {
None
}
}
_ => where_clause.cloned(),
};
let right_result = super::execute_from_clause(
right,
cte_results,
database,
right_where_clause.as_ref(),
None,
None,
outer_row,
outer_schema,
execute_subquery,
)?;
let is_outer_join = matches!(
join_type,
vibesql_ast::JoinType::LeftOuter
| vibesql_ast::JoinType::RightOuter
| vibesql_ast::JoinType::FullOuter
);
if is_outer_join {
if let Some(on_condition) = condition {
validate_on_clause_table_references(
on_condition,
&left_result.schema,
&right_result.schema,
database,
join_alias,
)?;
}
}
let natural_join_condition = if natural {
generate_natural_join_condition(&left_result.schema, &right_result.schema)?
} else {
None
};
let using_join_condition = if let Some(cols) = using_columns {
generate_using_join_condition(cols, &left_result.schema, &right_result.schema)?
} else {
None
};
let effective_condition =
natural_join_condition.or(using_join_condition).or_else(|| condition.clone());
let equijoin_predicates = if let Some(where_expr) = where_clause {
let mut schema_builder =
crate::schema::SchemaBuilder::from_schema(left_result.schema.clone());
for (table_id, (_start_idx, table_schema)) in &right_result.schema.table_schemas {
let _ = schema_builder.add_table(table_id.display().to_string(), table_schema.clone());
}
let combined_schema = schema_builder.build();
let predicate_plan = PredicatePlan::from_where_clause(Some(where_expr), &combined_schema)
.map_err(ExecutorError::InvalidWhereClause)?;
let left_schema_tables: std::collections::HashSet<String> =
left_result.schema.table_names().into_iter().collect();
let right_schema_tables: std::collections::HashSet<String> =
right_result.schema.table_names().into_iter().collect();
predicate_plan
.get_equijoin_conditions()
.iter()
.filter_map(|(left_table, _left_col, right_table, _right_col, expr)| {
let left_in_left = left_schema_tables.contains(left_table);
let right_in_right = right_schema_tables.contains(right_table);
let right_in_left = left_schema_tables.contains(right_table);
let left_in_right = right_schema_tables.contains(left_table);
if (left_in_left && right_in_right) || (right_in_left && left_in_right) {
Some(expr.clone())
} else {
None
}
})
.collect()
} else {
Vec::new()
};
let (left_schema_for_using, right_schema_for_using) = if using_columns.is_some() {
(Some(left_result.schema.clone()), Some(right_result.schema.clone()))
} else {
(None, None)
};
use crate::select::join::nested_loop_join;
let timeout_ctx = TimeoutContext::new_default();
let mut result = nested_loop_join(
left_result,
right_result,
join_type,
&effective_condition,
natural,
None, database,
&equijoin_predicates,
&timeout_ctx,
cte_results,
outer_row,
outer_schema,
)?;
if let (Some(using_cols), Some(ref left_schema), Some(ref right_schema)) =
(using_columns.as_ref(), &left_schema_for_using, &right_schema_for_using)
{
result = remove_duplicate_columns_for_using_join(
result,
left_schema,
right_schema,
using_cols,
join_type,
)?;
}
if let Some(alias) = join_alias {
result.schema = result.schema.add_join_alias(alias);
}
Ok(result)
}
fn remove_duplicate_columns_for_using_join(
mut result: super::FromResult,
left_schema: &CombinedSchema,
right_schema: &CombinedSchema,
using_columns: &[String],
join_type: &vibesql_ast::JoinType,
) -> Result<super::FromResult, crate::errors::ExecutorError> {
let using_cols_lower: HashSet<String> =
using_columns.iter().map(|c| c.to_lowercase()).collect();
for col in using_columns {
result.schema.add_joined_column(col);
}
let left_col_count = left_schema.total_columns;
let needs_coalesce =
matches!(join_type, vibesql_ast::JoinType::RightOuter | vibesql_ast::JoinType::FullOuter);
let mut leftmost_left_indices: std::collections::HashMap<String, (usize, bool)> =
std::collections::HashMap::new();
if needs_coalesce {
for (table_start_idx, table_schema) in left_schema.table_schemas.values() {
for (col_idx, col) in table_schema.columns.iter().enumerate() {
let lowercase = col.name.to_lowercase();
if using_cols_lower.contains(&lowercase) {
let absolute_idx = table_start_idx + col_idx;
let is_hidden = left_schema.hidden_columns.contains(&absolute_idx);
let should_update = match leftmost_left_indices.get(&lowercase) {
None => true,
Some((best_idx, best_is_hidden)) => {
if *best_is_hidden && !is_hidden {
true } else if !*best_is_hidden && is_hidden {
false } else {
absolute_idx < *best_idx }
}
};
if should_update {
leftmost_left_indices.insert(lowercase, (absolute_idx, is_hidden));
}
}
}
}
}
let is_right_or_full =
matches!(join_type, vibesql_ast::JoinType::RightOuter | vibesql_ast::JoinType::FullOuter);
if is_right_or_full {
for (table_start_idx, table_schema) in left_schema.table_schemas.values() {
for (col_idx, col) in table_schema.columns.iter().enumerate() {
let lowercase = col.name.to_lowercase();
if using_cols_lower.contains(&lowercase) {
let left_idx = table_start_idx + col_idx;
result.schema.hide_column(left_idx);
}
}
}
let mut best_right_indices: std::collections::HashMap<String, (usize, bool)> =
std::collections::HashMap::new();
for (table_start_idx, table_schema) in right_schema.table_schemas.values() {
for (col_idx, col) in table_schema.columns.iter().enumerate() {
let lowercase = col.name.to_lowercase();
if using_cols_lower.contains(&lowercase) {
let right_rel_idx = table_start_idx + col_idx;
let is_hidden = right_schema.hidden_columns.contains(&right_rel_idx);
let should_update = match best_right_indices.get(&lowercase) {
None => true,
Some((best_idx, best_is_hidden)) => {
if *best_is_hidden && !is_hidden {
true } else if !*best_is_hidden && is_hidden {
false } else {
right_rel_idx < *best_idx }
}
};
if should_update {
best_right_indices.insert(lowercase, (right_rel_idx, is_hidden));
}
}
}
}
for (col_name, (right_rel_idx, _)) in &best_right_indices {
let right_idx = left_col_count + right_rel_idx;
if let Some(&(left_idx, _)) = leftmost_left_indices.get(col_name) {
let original_name = using_columns
.iter()
.find(|c| c.to_lowercase() == *col_name)
.map(|s| s.as_str())
.unwrap_or(col_name);
result.schema.add_using_coalesce_pair(original_name, left_idx, right_idx);
}
}
} else {
let mut leftmost_left_indices: std::collections::HashMap<String, usize> =
std::collections::HashMap::new();
for (table_start_idx, table_schema) in left_schema.table_schemas.values() {
for (col_idx, col) in table_schema.columns.iter().enumerate() {
let lowercase = col.name.to_lowercase();
if using_cols_lower.contains(&lowercase) {
let absolute_idx = table_start_idx + col_idx;
leftmost_left_indices
.entry(lowercase)
.and_modify(|e| {
if absolute_idx < *e {
*e = absolute_idx
}
})
.or_insert(absolute_idx);
}
}
}
for (table_start_idx, table_schema) in right_schema.table_schemas.values() {
for (col_idx, col) in table_schema.columns.iter().enumerate() {
let lowercase = col.name.to_lowercase();
if using_cols_lower.contains(&lowercase) {
let right_idx = left_col_count + table_start_idx + col_idx;
result.schema.hide_column(right_idx);
if let Some(&left_idx) = leftmost_left_indices.get(&lowercase) {
let original_name = using_columns
.iter()
.find(|c| c.to_lowercase() == lowercase)
.map(|s| s.as_str())
.unwrap_or(&col.name);
result.schema.add_using_coalesce_pair(original_name, left_idx, right_idx);
}
}
}
}
}
Ok(result)
}
fn generate_natural_join_condition(
left_schema: &crate::schema::CombinedSchema,
right_schema: &crate::schema::CombinedSchema,
) -> Result<Option<vibesql_ast::Expression>, ExecutorError> {
use std::collections::HashMap;
let left_table_names: std::collections::HashSet<_> =
left_schema.table_schemas.keys().map(|k| k.canonical().to_lowercase()).collect();
let right_table_names: std::collections::HashSet<_> =
right_schema.table_schemas.keys().map(|k| k.canonical().to_lowercase()).collect();
let is_self_join = !left_table_names.is_disjoint(&right_table_names);
let replacement_targets: std::collections::HashSet<usize> =
left_schema.column_replacement_map.values().copied().collect();
let mut left_columns: HashMap<String, (String, String, usize, Option<String>, bool)> =
HashMap::new();
for (table_name, (table_start_idx, table_schema)) in &left_schema.table_schemas {
for (col_offset, col) in table_schema.columns.iter().enumerate() {
let lowercase_name = col.name.to_lowercase();
let absolute_idx = table_start_idx + col_offset;
let is_hidden = left_schema.hidden_columns.contains(&absolute_idx);
let is_replacement_target = replacement_targets.contains(&absolute_idx);
let should_update = match left_columns.get(&lowercase_name) {
None => true,
Some((_, _, best_idx, _, best_is_hidden)) => {
let best_is_replacement_target = replacement_targets.contains(best_idx);
if *best_is_hidden && !is_hidden {
true
} else if !*best_is_hidden && is_hidden {
false
} else if *best_is_hidden && is_hidden {
if is_replacement_target && !best_is_replacement_target {
true
} else if !is_replacement_target && best_is_replacement_target {
false
} else {
absolute_idx < *best_idx
}
} else {
absolute_idx < *best_idx
}
}
};
if should_update {
left_columns.insert(
lowercase_name,
(
table_name.to_string(),
col.name.clone(),
absolute_idx,
col.collation.clone(),
is_hidden,
),
);
}
}
}
let mut common_columns: Vec<(String, String, String, String, usize, Option<String>)> =
Vec::new();
let mut matched_column_names: std::collections::HashSet<String> =
std::collections::HashSet::new();
for (table_name, (table_idx, table_schema)) in &right_schema.table_schemas {
for col in &table_schema.columns {
let lowercase_name = col.name.to_lowercase();
if matched_column_names.contains(&lowercase_name) {
continue;
}
if let Some((left_table, left_col, _absolute_idx, left_collation, _is_hidden)) =
left_columns.get(&lowercase_name)
{
let collation = left_collation.clone().or_else(|| col.collation.clone());
common_columns.push((
left_table.clone(),
left_col.clone(),
table_name.to_string(),
col.name.clone(),
*table_idx,
collation,
));
matched_column_names.insert(lowercase_name);
}
}
}
if common_columns.is_empty() {
return Ok(None);
}
let mut left_idx_to_col: HashMap<usize, (String, String)> = HashMap::new();
for (table_name, (start_idx, table_schema)) in &left_schema.table_schemas {
for (col_offset, col) in table_schema.columns.iter().enumerate() {
let absolute_idx = start_idx + col_offset;
left_idx_to_col.insert(absolute_idx, (table_name.to_string(), col.name.clone()));
}
}
let mut right_idx_to_col: HashMap<usize, (String, String)> = HashMap::new();
for (table_name, (start_idx, table_schema)) in &right_schema.table_schemas {
for (col_offset, col) in table_schema.columns.iter().enumerate() {
let absolute_idx = start_idx + col_offset;
right_idx_to_col.insert(absolute_idx, (table_name.to_string(), col.name.clone()));
}
}
let mut condition: Option<vibesql_ast::Expression> = None;
for (left_table, left_col, right_table, right_col, right_table_start, collation) in
common_columns
{
let right_table_ref = if is_self_join {
let adjusted_start = left_schema.total_columns + right_table_start;
format!("__selfjoin_right_{}_{}", right_table.to_lowercase(), adjusted_start)
} else {
right_table
};
let right_col_lower = right_col.to_lowercase();
let right_base_expr: vibesql_ast::Expression = if let Some(coalesce_indices) =
right_schema.using_coalesce_indices.get(&right_col_lower)
{
if coalesce_indices.len() > 1 {
let coalesce_args: Vec<vibesql_ast::Expression> = coalesce_indices
.iter()
.filter_map(|&idx| {
right_idx_to_col.get(&idx).map(|(table, col)| {
let table_ref = if is_self_join {
let adjusted_start = left_schema.total_columns + idx;
format!(
"__selfjoin_right_{}_{}",
table.to_lowercase(),
adjusted_start
)
} else {
table.clone()
};
vibesql_ast::Expression::ColumnRef(
vibesql_ast::ColumnIdentifier::qualified(
&table_ref, false, col, false,
),
)
})
})
.collect();
if coalesce_args.len() > 1 {
vibesql_ast::Expression::Function {
name: "COALESCE".into(),
args: coalesce_args,
character_unit: None,
}
} else {
vibesql_ast::Expression::ColumnRef(vibesql_ast::ColumnIdentifier::qualified(
&right_table_ref,
false,
&right_col,
false,
))
}
} else {
vibesql_ast::Expression::ColumnRef(vibesql_ast::ColumnIdentifier::qualified(
&right_table_ref,
false,
&right_col,
false,
))
}
} else {
vibesql_ast::Expression::ColumnRef(vibesql_ast::ColumnIdentifier::qualified(
&right_table_ref,
false,
&right_col,
false,
))
};
let right_col_with_collate = if let Some(ref coll) = collation {
vibesql_ast::Expression::Collate {
expr: Box::new(right_base_expr),
collation: coll.clone(),
}
} else {
right_base_expr
};
let left_col_lower = left_col.to_lowercase();
let left_expr: vibesql_ast::Expression = if let Some(coalesce_indices) =
left_schema.using_coalesce_indices.get(&left_col_lower)
{
if coalesce_indices.len() > 1 {
let coalesce_args: Vec<vibesql_ast::Expression> = coalesce_indices
.iter()
.filter_map(|&idx| {
left_idx_to_col.get(&idx).map(|(table, col)| {
vibesql_ast::Expression::ColumnRef(
vibesql_ast::ColumnIdentifier::qualified(table, false, col, false),
)
})
})
.collect();
if coalesce_args.len() > 1 {
vibesql_ast::Expression::Function {
name: "COALESCE".into(),
args: coalesce_args,
character_unit: None,
}
} else {
vibesql_ast::Expression::ColumnRef(vibesql_ast::ColumnIdentifier::qualified(
&left_table,
false,
&left_col,
false,
))
}
} else {
vibesql_ast::Expression::ColumnRef(vibesql_ast::ColumnIdentifier::qualified(
&left_table,
false,
&left_col,
false,
))
}
} else {
vibesql_ast::Expression::ColumnRef(vibesql_ast::ColumnIdentifier::qualified(
&left_table,
false,
&left_col,
false,
))
};
let equality = vibesql_ast::Expression::BinaryOp {
left: Box::new(left_expr),
op: vibesql_ast::BinaryOperator::Equal,
right: Box::new(right_col_with_collate),
};
condition = Some(match condition {
None => equality,
Some(existing) => vibesql_ast::Expression::BinaryOp {
left: Box::new(existing),
op: vibesql_ast::BinaryOperator::And,
right: Box::new(equality),
},
});
}
Ok(condition)
}
fn generate_using_join_condition(
columns: &[String],
left_schema: &crate::schema::CombinedSchema,
right_schema: &crate::schema::CombinedSchema,
) -> Result<Option<vibesql_ast::Expression>, ExecutorError> {
let mut condition: Option<vibesql_ast::Expression> = None;
let left_table_names: std::collections::HashSet<_> =
left_schema.table_schemas.keys().map(|k| k.canonical().to_lowercase()).collect();
let right_table_names: std::collections::HashSet<_> =
right_schema.table_schemas.keys().map(|k| k.canonical().to_lowercase()).collect();
let is_self_join = !left_table_names.is_disjoint(&right_table_names);
let mut left_idx_to_col: HashMap<usize, (String, String)> = HashMap::new();
for (table_name, (start_idx, table_schema)) in &left_schema.table_schemas {
for (col_offset, col) in table_schema.columns.iter().enumerate() {
let absolute_idx = start_idx + col_offset;
left_idx_to_col.insert(absolute_idx, (table_name.to_string(), col.name.clone()));
}
}
let mut right_idx_to_col: HashMap<usize, (String, String)> = HashMap::new();
for (table_name, (start_idx, table_schema)) in &right_schema.table_schemas {
for (col_offset, col) in table_schema.columns.iter().enumerate() {
let absolute_idx = start_idx + col_offset;
right_idx_to_col.insert(absolute_idx, (table_name.to_string(), col.name.clone()));
}
}
for col_name in columns {
let col_lower = col_name.to_lowercase();
let left_col = {
let mut best_match: Option<(
vibesql_catalog::TableIdentifier,
String,
usize,
Option<String>,
bool, // is_hidden
)> = None;
for (table_name, (start_idx, table_schema)) in &left_schema.table_schemas {
for (col_offset, col) in table_schema.columns.iter().enumerate() {
if col.name.to_lowercase() == col_lower {
let absolute_idx = start_idx + col_offset;
let is_hidden = left_schema.hidden_columns.contains(&absolute_idx);
let should_update = match &best_match {
None => true,
Some((_, _, _, _, best_is_hidden)) => {
if *best_is_hidden && !is_hidden {
true
} else if !*best_is_hidden && is_hidden {
false
} else {
absolute_idx < best_match.as_ref().unwrap().2
}
}
};
if should_update {
best_match = Some((
table_name.clone(),
col.name.clone(),
absolute_idx,
col.collation.clone(),
is_hidden,
));
}
}
}
}
best_match.map(|(table, col, idx, collation, _)| (table, col, idx, collation))
}
.ok_or_else(|| ExecutorError::JoinUsingColumnNotPresent {
column_name: col_name.to_string(),
})?;
let right_col = {
let mut best_match: Option<(
vibesql_catalog::TableIdentifier,
String,
usize,
usize,
Option<String>,
bool, // is_hidden
)> = None;
for (table_name, (start_idx, table_schema)) in &right_schema.table_schemas {
for (col_offset, col) in table_schema.columns.iter().enumerate() {
if col.name.to_lowercase() == col_lower {
let absolute_idx = start_idx + col_offset;
let is_hidden = right_schema.hidden_columns.contains(&absolute_idx);
let should_update = match &best_match {
None => true,
Some((_, _, best_idx, _, _, best_is_hidden)) => {
if *best_is_hidden && !is_hidden {
true
} else if !*best_is_hidden && is_hidden {
false
} else {
absolute_idx < *best_idx
}
}
};
if should_update {
best_match = Some((
table_name.clone(),
col.name.clone(),
absolute_idx,
*start_idx,
col.collation.clone(),
is_hidden,
));
}
}
}
}
best_match
.map(|(table, col, idx, start, collation, _)| (table, col, idx, start, collation))
}
.ok_or_else(|| ExecutorError::JoinUsingColumnNotPresent {
column_name: col_name.to_string(),
})?;
let collation = left_col.3.clone().or(right_col.4.clone());
let right_table_name = if is_self_join {
let adjusted_start = left_schema.total_columns + right_col.3;
format!("__selfjoin_right_{}_{}", right_col.0.canonical(), adjusted_start)
} else {
right_col.0.to_string()
};
let right_base_expr: vibesql_ast::Expression = if let Some(coalesce_indices) =
right_schema.using_coalesce_indices.get(&col_lower)
{
if coalesce_indices.len() > 1 {
let coalesce_args: Vec<vibesql_ast::Expression> = coalesce_indices
.iter()
.filter_map(|&idx| {
right_idx_to_col.get(&idx).map(|(table, col)| {
let table_ref = if is_self_join {
let adjusted_start = left_schema.total_columns + idx;
format!(
"__selfjoin_right_{}_{}",
table.to_lowercase(),
adjusted_start
)
} else {
table.clone()
};
vibesql_ast::Expression::ColumnRef(
vibesql_ast::ColumnIdentifier::qualified(
&table_ref, false, col, false,
),
)
})
})
.collect();
if coalesce_args.len() > 1 {
vibesql_ast::Expression::Function {
name: "COALESCE".into(),
args: coalesce_args,
character_unit: None,
}
} else {
vibesql_ast::Expression::ColumnRef(vibesql_ast::ColumnIdentifier::qualified(
&right_table_name,
false,
&right_col.1,
false,
))
}
} else {
vibesql_ast::Expression::ColumnRef(vibesql_ast::ColumnIdentifier::qualified(
&right_table_name,
false,
&right_col.1,
false,
))
}
} else {
vibesql_ast::Expression::ColumnRef(vibesql_ast::ColumnIdentifier::qualified(
&right_table_name,
false,
&right_col.1,
false,
))
};
let right_col_with_collate = if let Some(ref coll) = collation {
vibesql_ast::Expression::Collate {
expr: Box::new(right_base_expr),
collation: coll.clone(),
}
} else {
right_base_expr
};
let left_expr: vibesql_ast::Expression = if let Some(coalesce_indices) =
left_schema.using_coalesce_indices.get(&col_lower)
{
if coalesce_indices.len() > 1 {
let coalesce_args: Vec<vibesql_ast::Expression> = coalesce_indices
.iter()
.filter_map(|&idx| {
left_idx_to_col.get(&idx).map(|(table, col)| {
vibesql_ast::Expression::ColumnRef(
vibesql_ast::ColumnIdentifier::qualified(table, false, col, false),
)
})
})
.collect();
if coalesce_args.len() > 1 {
vibesql_ast::Expression::Function {
name: "COALESCE".into(),
args: coalesce_args,
character_unit: None,
}
} else {
vibesql_ast::Expression::ColumnRef(vibesql_ast::ColumnIdentifier::qualified(
&left_col.0.to_string(),
false,
&left_col.1,
false,
))
}
} else {
vibesql_ast::Expression::ColumnRef(vibesql_ast::ColumnIdentifier::qualified(
&left_col.0.to_string(),
false,
&left_col.1,
false,
))
}
} else {
vibesql_ast::Expression::ColumnRef(vibesql_ast::ColumnIdentifier::qualified(
&left_col.0.to_string(),
false,
&left_col.1,
false,
))
};
let equality = vibesql_ast::Expression::BinaryOp {
left: Box::new(left_expr),
op: vibesql_ast::BinaryOperator::Equal,
right: Box::new(right_col_with_collate),
};
condition = Some(match condition {
None => equality,
Some(existing) => vibesql_ast::Expression::BinaryOp {
left: Box::new(existing),
op: vibesql_ast::BinaryOperator::And,
right: Box::new(equality),
},
});
}
Ok(condition)
}
fn validate_on_clause_table_references(
on_condition: &vibesql_ast::Expression,
left_schema: &CombinedSchema,
right_schema: &CombinedSchema,
database: &vibesql_storage::Database,
join_alias: Option<&str>,
) -> Result<(), ExecutorError> {
let mut valid_tables: HashSet<String> = HashSet::new();
for table_id in left_schema.table_schemas.keys() {
valid_tables.insert(table_id.canonical().to_lowercase());
}
for table_id in right_schema.table_schemas.keys() {
valid_tables.insert(table_id.canonical().to_lowercase());
}
if let Some(alias) = join_alias {
valid_tables.insert(alias.to_lowercase());
}
let mut valid_columns: HashSet<String> = HashSet::new();
for (_, (_, table_schema)) in &left_schema.table_schemas {
for col in &table_schema.columns {
valid_columns.insert(col.name.to_lowercase());
}
}
for (_, (_, table_schema)) in &right_schema.table_schemas {
for col in &table_schema.columns {
valid_columns.insert(col.name.to_lowercase());
}
}
let mut all_db_columns: HashSet<String> = HashSet::new();
for table_name in database.list_tables() {
if let Some(table) = database.get_table(&table_name) {
for col in &table.schema.columns {
all_db_columns.insert(col.name.to_lowercase());
}
}
}
if on_clause_references_unknown_table_or_column(
on_condition,
&valid_tables,
&valid_columns,
&all_db_columns,
) {
return Err(ExecutorError::OnClauseReferencesRightTable);
}
Ok(())
}
fn on_clause_references_unknown_table_or_column(
expr: &vibesql_ast::Expression,
valid_tables: &HashSet<String>,
valid_columns: &HashSet<String>,
all_db_columns: &HashSet<String>,
) -> bool {
match expr {
vibesql_ast::Expression::ColumnRef(col_id) => {
if let Some(table_name) = col_id.table_canonical() {
let table_lower = table_name.to_lowercase();
!valid_tables.contains(&table_lower)
} else {
let col_lower = col_id.column_canonical().to_lowercase();
if valid_columns.contains(&col_lower) {
false
} else if all_db_columns.contains(&col_lower) {
true
} else {
false
}
}
}
vibesql_ast::Expression::BinaryOp { left, right, .. } => {
on_clause_references_unknown_table_or_column(
left,
valid_tables,
valid_columns,
all_db_columns,
) || on_clause_references_unknown_table_or_column(
right,
valid_tables,
valid_columns,
all_db_columns,
)
}
vibesql_ast::Expression::UnaryOp { expr, .. } => {
on_clause_references_unknown_table_or_column(
expr,
valid_tables,
valid_columns,
all_db_columns,
)
}
vibesql_ast::Expression::IsNull { expr, .. } => {
on_clause_references_unknown_table_or_column(
expr,
valid_tables,
valid_columns,
all_db_columns,
)
}
vibesql_ast::Expression::Between { expr, low, high, .. } => {
on_clause_references_unknown_table_or_column(
expr,
valid_tables,
valid_columns,
all_db_columns,
) || on_clause_references_unknown_table_or_column(
low,
valid_tables,
valid_columns,
all_db_columns,
) || on_clause_references_unknown_table_or_column(
high,
valid_tables,
valid_columns,
all_db_columns,
)
}
vibesql_ast::Expression::InList { expr, values, .. } => {
on_clause_references_unknown_table_or_column(
expr,
valid_tables,
valid_columns,
all_db_columns,
) || values.iter().any(|e| {
on_clause_references_unknown_table_or_column(
e,
valid_tables,
valid_columns,
all_db_columns,
)
})
}
vibesql_ast::Expression::Case { operand, when_clauses, else_result } => {
operand
.as_ref()
.map(|e| {
on_clause_references_unknown_table_or_column(
e,
valid_tables,
valid_columns,
all_db_columns,
)
})
.unwrap_or(false)
|| when_clauses.iter().any(|case_when| {
case_when.conditions.iter().any(|c| {
on_clause_references_unknown_table_or_column(
c,
valid_tables,
valid_columns,
all_db_columns,
)
}) || on_clause_references_unknown_table_or_column(
&case_when.result,
valid_tables,
valid_columns,
all_db_columns,
)
})
|| else_result
.as_ref()
.map(|e| {
on_clause_references_unknown_table_or_column(
e,
valid_tables,
valid_columns,
all_db_columns,
)
})
.unwrap_or(false)
}
vibesql_ast::Expression::Function { args, .. } => args.iter().any(|e| {
on_clause_references_unknown_table_or_column(
e,
valid_tables,
valid_columns,
all_db_columns,
)
}),
vibesql_ast::Expression::AggregateFunction { args, filter, .. } => {
args.iter().any(|e| {
on_clause_references_unknown_table_or_column(
e,
valid_tables,
valid_columns,
all_db_columns,
)
}) || filter
.as_ref()
.map(|e| {
on_clause_references_unknown_table_or_column(
e,
valid_tables,
valid_columns,
all_db_columns,
)
})
.unwrap_or(false)
}
vibesql_ast::Expression::WindowFunction { function, over } => {
let func_refs = match function {
vibesql_ast::WindowFunctionSpec::Aggregate { args, filter, .. } => {
args.iter().any(|e| {
on_clause_references_unknown_table_or_column(
e,
valid_tables,
valid_columns,
all_db_columns,
)
}) || filter
.as_ref()
.map(|e| {
on_clause_references_unknown_table_or_column(
e,
valid_tables,
valid_columns,
all_db_columns,
)
})
.unwrap_or(false)
}
vibesql_ast::WindowFunctionSpec::Ranking { args, .. }
| vibesql_ast::WindowFunctionSpec::Value { args, .. } => args.iter().any(|e| {
on_clause_references_unknown_table_or_column(
e,
valid_tables,
valid_columns,
all_db_columns,
)
}),
};
func_refs
|| over
.partition_by
.as_ref()
.map(|exprs| {
exprs.iter().any(|e| {
on_clause_references_unknown_table_or_column(
e,
valid_tables,
valid_columns,
all_db_columns,
)
})
})
.unwrap_or(false)
|| over
.order_by
.as_ref()
.map(|items| {
items.iter().any(|item| {
on_clause_references_unknown_table_or_column(
&item.expr,
valid_tables,
valid_columns,
all_db_columns,
)
})
})
.unwrap_or(false)
}
vibesql_ast::Expression::Collate { expr, .. } => {
on_clause_references_unknown_table_or_column(
expr,
valid_tables,
valid_columns,
all_db_columns,
)
}
vibesql_ast::Expression::Cast { expr, .. } => on_clause_references_unknown_table_or_column(
expr,
valid_tables,
valid_columns,
all_db_columns,
),
vibesql_ast::Expression::ScalarSubquery(select_stmt) => {
subquery_references_unknown_table(
select_stmt,
valid_tables,
valid_columns,
all_db_columns,
)
}
vibesql_ast::Expression::In { expr, subquery, .. } => {
if on_clause_references_unknown_table_or_column(
expr,
valid_tables,
valid_columns,
all_db_columns,
) {
return true;
}
subquery_references_unknown_table(subquery, valid_tables, valid_columns, all_db_columns)
}
vibesql_ast::Expression::Exists { subquery, .. } => {
subquery_references_unknown_table(subquery, valid_tables, valid_columns, all_db_columns)
}
vibesql_ast::Expression::Conjunction(exprs)
| vibesql_ast::Expression::Disjunction(exprs) => exprs.iter().any(|e| {
on_clause_references_unknown_table_or_column(
e,
valid_tables,
valid_columns,
all_db_columns,
)
}),
_ => false,
}
}
fn collect_tables_from_from_clause(
from: &Option<vibesql_ast::FromClause>,
tables: &mut HashSet<String>,
) {
if let Some(from_clause) = from {
collect_tables_from_from_clause_inner(from_clause, tables);
}
}
fn collect_tables_from_from_clause_inner(
from: &vibesql_ast::FromClause,
tables: &mut HashSet<String>,
) {
match from {
vibesql_ast::FromClause::Table { name, alias, .. } => {
let table_name = alias.as_ref().unwrap_or(name);
tables.insert(table_name.to_lowercase());
}
vibesql_ast::FromClause::Join { left, right, .. } => {
collect_tables_from_from_clause_inner(left, tables);
collect_tables_from_from_clause_inner(right, tables);
}
vibesql_ast::FromClause::Subquery { alias, .. } => {
tables.insert(alias.to_lowercase());
}
vibesql_ast::FromClause::Values { alias, .. } => {
tables.insert(alias.to_lowercase());
}
}
}
fn collect_columns_from_from_clause(
from: &Option<vibesql_ast::FromClause>,
all_db_columns: &HashSet<String>,
columns: &mut HashSet<String>,
) {
if let Some(from_clause) = from {
collect_columns_from_from_clause_inner(from_clause, all_db_columns, columns);
}
}
fn collect_columns_from_from_clause_inner(
from: &vibesql_ast::FromClause,
all_db_columns: &HashSet<String>,
columns: &mut HashSet<String>,
) {
match from {
vibesql_ast::FromClause::Table { .. } => {
columns.extend(all_db_columns.iter().cloned());
}
vibesql_ast::FromClause::Join { left, right, .. } => {
collect_columns_from_from_clause_inner(left, all_db_columns, columns);
collect_columns_from_from_clause_inner(right, all_db_columns, columns);
}
vibesql_ast::FromClause::Subquery { .. } | vibesql_ast::FromClause::Values { .. } => {
}
}
}
fn subquery_references_unknown_table(
select_stmt: &vibesql_ast::SelectStmt,
valid_tables: &HashSet<String>,
valid_columns: &HashSet<String>,
all_db_columns: &HashSet<String>,
) -> bool {
let mut subquery_all_tables = valid_tables.clone();
collect_tables_from_from_clause(&select_stmt.from, &mut subquery_all_tables);
let mut subquery_all_columns = valid_columns.clone();
collect_columns_from_from_clause(&select_stmt.from, all_db_columns, &mut subquery_all_columns);
for item in &select_stmt.select_list {
if let vibesql_ast::SelectItem::Expression { expr, .. } = item {
if on_clause_references_unknown_table_or_column(
expr,
&subquery_all_tables,
&subquery_all_columns,
all_db_columns,
) {
return true;
}
}
}
if let Some(where_expr) = &select_stmt.where_clause {
if on_clause_references_unknown_table_or_column(
where_expr,
&subquery_all_tables,
&subquery_all_columns,
all_db_columns,
) {
return true;
}
}
if let Some(having_expr) = &select_stmt.having {
if on_clause_references_unknown_table_or_column(
having_expr,
&subquery_all_tables,
&subquery_all_columns,
all_db_columns,
) {
return true;
}
}
if let Some(from) = &select_stmt.from {
if from_clause_references_unknown_table(from, valid_tables, valid_columns, all_db_columns) {
return true;
}
}
false
}
fn from_clause_references_unknown_table(
from: &vibesql_ast::FromClause,
valid_tables: &HashSet<String>,
valid_columns: &HashSet<String>,
all_db_columns: &HashSet<String>,
) -> bool {
match from {
vibesql_ast::FromClause::Table { .. } => false,
vibesql_ast::FromClause::Join { left, right, condition, .. } => {
let mut join_valid_tables = valid_tables.clone();
collect_tables_from_from_clause_inner(left, &mut join_valid_tables);
collect_tables_from_from_clause_inner(right, &mut join_valid_tables);
if let Some(cond) = condition {
if on_clause_references_unknown_table_or_column(
cond,
&join_valid_tables,
valid_columns,
all_db_columns,
) {
return true;
}
}
let mut left_context = valid_tables.clone();
if from_clause_references_unknown_table(
left,
&left_context,
valid_columns,
all_db_columns,
) {
return true;
}
collect_tables_from_from_clause_inner(left, &mut left_context);
from_clause_references_unknown_table(
right,
&left_context,
valid_columns,
all_db_columns,
)
}
vibesql_ast::FromClause::Subquery { query, .. } => {
let mut subquery_valid_tables = valid_tables.clone();
collect_tables_from_from_clause(&query.from, &mut subquery_valid_tables);
subquery_references_unknown_table(
query,
&subquery_valid_tables,
valid_columns,
all_db_columns,
)
}
vibesql_ast::FromClause::Values { .. } => false,
}
}