use std::collections::{HashMap, HashSet};
use ahash::AHashSet;
use crate::{
errors::ExecutorError, evaluator::CombinedExpressionEvaluator,
optimizer::where_pushdown::flatten_conjuncts, schema::CombinedSchema,
};
use super::semi_anti::parse_semi_join_condition;
#[derive(Debug, Clone)]
pub(super) enum KeySlot {
Constant(vibesql_types::SqlValue),
JoinKey,
}
pub(super) fn try_index_nested_loop_semi_join(
left_result: &super::super::FromResult,
right_from: &vibesql_ast::FromClause,
condition: &Option<vibesql_ast::Expression>,
database: &vibesql_storage::Database,
) -> Result<Option<super::super::FromResult>, ExecutorError> {
let cond = match condition {
Some(c) => c,
None => return Ok(None),
};
let (right_table_name, _right_alias) = match right_from {
vibesql_ast::FromClause::Table { name, alias, .. } => (name.clone(), alias.clone()),
_ => return Ok(None), };
let right_table = match database.get_table(&right_table_name) {
Some(t) => t,
None => return Ok(None),
};
let (equi_join, right_filters) =
match parse_semi_join_condition(cond, left_result, &right_table_name) {
Some(parsed) => parsed,
None => return Ok(None),
};
let pk_columns = match &right_table.schema.primary_key {
Some(cols) => cols.clone(),
None => return Ok(None), };
let constant_prefix =
extract_constant_prefix_for_pk(&right_filters, &pk_columns, &equi_join.right_col);
let lookup_key_template =
match build_lookup_key_template(&pk_columns, &equi_join.right_col, &constant_prefix) {
Some(template) => template,
None => return Ok(None),
};
let pk_index = match right_table.primary_key_index() {
Some(idx) => idx,
None => return Ok(None),
};
let left_col_idx = match find_column_index(&left_result.schema, &equi_join.left_col) {
Some(idx) => idx,
None => return Ok(None),
};
let right_schema =
CombinedSchema::from_table(right_table_name.clone(), right_table.schema.clone());
let residual_filter = build_residual_filter(&right_filters, &pk_columns);
let evaluator = residual_filter
.as_ref()
.map(|_| CombinedExpressionEvaluator::with_database(&right_schema, database));
let left_slice = left_result.as_slice();
let mut seen_keys: AHashSet<vibesql_types::SqlValue> =
AHashSet::with_capacity(left_slice.len());
let mut matching_keys: AHashSet<vibesql_types::SqlValue> = AHashSet::new();
for left_row in left_slice {
let join_key = &left_row.values[left_col_idx];
if *join_key == vibesql_types::SqlValue::Null {
continue;
}
if seen_keys.contains(join_key) {
continue;
}
seen_keys.insert(join_key.clone());
let lookup_key: Vec<vibesql_types::SqlValue> = lookup_key_template
.iter()
.map(|slot| match slot {
KeySlot::Constant(v) => v.clone(),
KeySlot::JoinKey => join_key.clone(),
})
.collect();
if let Some(&row_idx) = pk_index.get(&lookup_key) {
let right_row = match right_table.get_row(row_idx) {
Some(row) => row,
None => continue, };
let passes = if let (Some(filter), Some(eval)) = (&residual_filter, &evaluator) {
eval.clear_cse_cache();
matches!(eval.eval(filter, right_row), Ok(vibesql_types::SqlValue::Boolean(true)))
} else {
true };
if passes {
matching_keys.insert(join_key.clone());
}
}
}
let result_rows: Vec<vibesql_storage::Row> = left_slice
.iter()
.filter(|row| {
let key = &row.values[left_col_idx];
matching_keys.contains(key)
})
.cloned()
.collect();
Ok(Some(super::super::FromResult::from_rows(left_result.schema.clone(), result_rows)))
}
pub(super) fn try_prefix_scan_semi_join(
left_result: &super::super::FromResult,
right_from: &vibesql_ast::FromClause,
condition: &Option<vibesql_ast::Expression>,
database: &vibesql_storage::Database,
) -> Result<Option<super::super::FromResult>, ExecutorError> {
let debug = std::env::var("PREFIX_SEMI_DEBUG").is_ok();
let cond = match condition {
Some(c) => c,
None => return Ok(None),
};
let (right_table_name, _right_alias) = match right_from {
vibesql_ast::FromClause::Table { name, alias, .. } => (name.clone(), alias.clone()),
_ => return Ok(None),
};
let right_table = match database.get_table(&right_table_name) {
Some(t) => t,
None => return Ok(None),
};
let (equi_join, right_filters) =
match parse_semi_join_condition(cond, left_result, &right_table_name) {
Some(parsed) => parsed,
None => return Ok(None),
};
if debug {
eprintln!(
"[PREFIX_SEMI] left_rows={}, join on {}={}, right_filters={:?}",
left_result.as_slice().len(),
equi_join.left_col,
equi_join.right_col,
right_filters
);
}
let right_col_upper = equi_join.right_col.to_lowercase();
let index_names = database.list_indexes_for_table(&right_table_name);
let mut usable_index_name: Option<String> = None;
for index_name in &index_names {
if let Some(idx_metadata) = database.get_index(index_name) {
if let Some(first_col) = idx_metadata.columns.first() {
if first_col.expect_column_name().to_lowercase() == right_col_upper {
if !crate::optimizer::predicate_implication::partial_index_usable(
database,
index_name,
right_filters.as_ref(),
) {
continue;
}
usable_index_name = Some(index_name.clone());
break;
}
}
}
}
let index_name = match usable_index_name {
Some(name) => name,
None => {
if debug {
eprintln!(
"[PREFIX_SEMI] No index starting with {} on {}",
equi_join.right_col, right_table_name
);
}
return Ok(None);
}
};
let index = match database.get_index_data(&index_name) {
Some(idx) => idx,
None => return Ok(None),
};
if debug {
eprintln!(
"[PREFIX_SEMI] Using index {} for prefix scan on {}",
index_name, right_table_name
);
}
let left_col_idx = match find_column_index(&left_result.schema, &equi_join.left_col) {
Some(idx) => idx,
None => return Ok(None),
};
let right_schema =
CombinedSchema::from_table(right_table_name.clone(), right_table.schema.clone());
let evaluator = right_filters
.as_ref()
.map(|_| CombinedExpressionEvaluator::with_database(&right_schema, database));
let left_slice = left_result.as_slice();
let mut seen_keys: AHashSet<vibesql_types::SqlValue> =
AHashSet::with_capacity(left_slice.len());
let mut matching_keys: AHashSet<vibesql_types::SqlValue> = AHashSet::new();
let start = std::time::Instant::now();
for left_row in left_slice {
let join_key = &left_row.values[left_col_idx];
if *join_key == vibesql_types::SqlValue::Null {
continue;
}
if seen_keys.contains(join_key) {
continue;
}
seen_keys.insert(join_key.clone());
let row_indices = index.prefix_multi_lookup(std::slice::from_ref(join_key));
for row_idx in row_indices {
let right_row = match right_table.get_row(row_idx) {
Some(row) => row,
None => continue, };
let passes = if let (Some(filter), Some(eval)) = (&right_filters, &evaluator) {
eval.clear_cse_cache();
matches!(eval.eval(filter, right_row), Ok(vibesql_types::SqlValue::Boolean(true)))
} else {
true };
if passes {
matching_keys.insert(join_key.clone());
break; }
}
}
if debug {
let elapsed = start.elapsed();
eprintln!(
"[PREFIX_SEMI] {} distinct keys, {} matches, {:?} elapsed",
seen_keys.len(),
matching_keys.len(),
elapsed
);
}
let result_rows: Vec<vibesql_storage::Row> = left_slice
.iter()
.filter(|row| {
let key = &row.values[left_col_idx];
matching_keys.contains(key)
})
.cloned()
.collect();
Ok(Some(super::super::FromResult::from_rows(left_result.schema.clone(), result_rows)))
}
fn extract_constant_prefix_for_pk(
right_filters: &Option<vibesql_ast::Expression>,
pk_columns: &[String],
join_key_col: &str,
) -> HashMap<String, vibesql_types::SqlValue> {
let mut constants: HashMap<String, vibesql_types::SqlValue> = HashMap::new();
let filter = match right_filters {
Some(f) => f,
None => return constants,
};
let conjuncts = flatten_conjuncts(filter);
let join_key_upper = join_key_col.to_lowercase();
for pred in conjuncts {
if let vibesql_ast::Expression::BinaryOp {
left,
op: vibesql_ast::BinaryOperator::Equal,
right,
} = &pred
{
if let (
vibesql_ast::Expression::ColumnRef(col_id),
vibesql_ast::Expression::Literal(value),
) = (left.as_ref(), right.as_ref())
{
let column = col_id.column_canonical();
let col_upper = column.to_lowercase();
if pk_columns.iter().any(|pk| pk.to_lowercase() == col_upper)
&& col_upper != join_key_upper
{
constants.insert(col_upper, value.clone());
}
}
if let (
vibesql_ast::Expression::Literal(value),
vibesql_ast::Expression::ColumnRef(col_id),
) = (left.as_ref(), right.as_ref())
{
let column = col_id.column_canonical();
let col_upper = column.to_lowercase();
if pk_columns.iter().any(|pk| pk.to_lowercase() == col_upper)
&& col_upper != join_key_upper
{
constants.insert(col_upper, value.clone());
}
}
}
}
constants
}
fn build_lookup_key_template(
pk_columns: &[String],
join_key_col: &str,
constant_prefix: &HashMap<String, vibesql_types::SqlValue>,
) -> Option<Vec<KeySlot>> {
let join_key_upper = join_key_col.to_lowercase();
let mut template = Vec::with_capacity(pk_columns.len());
for pk_col in pk_columns {
let pk_upper = pk_col.to_lowercase();
if pk_upper == join_key_upper {
template.push(KeySlot::JoinKey);
} else if let Some(value) = constant_prefix.get(&pk_upper) {
template.push(KeySlot::Constant(value.clone()));
} else {
return None;
}
}
Some(template)
}
fn build_residual_filter(
right_filters: &Option<vibesql_ast::Expression>,
pk_columns: &[String],
) -> Option<vibesql_ast::Expression> {
let filter = match right_filters {
Some(f) => f,
None => return None,
};
let pk_upper: HashSet<String> = pk_columns.iter().map(|s| s.to_lowercase()).collect();
let conjuncts = flatten_conjuncts(filter);
let residual: Vec<vibesql_ast::Expression> = conjuncts
.into_iter()
.filter(|pred| {
if let vibesql_ast::Expression::BinaryOp {
left,
op: vibesql_ast::BinaryOperator::Equal,
right,
} = pred
{
if let vibesql_ast::Expression::ColumnRef(col_id) = left.as_ref() {
if pk_upper.contains(&col_id.column_canonical().to_lowercase())
&& matches!(right.as_ref(), vibesql_ast::Expression::Literal(_))
{
return false; }
}
if let vibesql_ast::Expression::ColumnRef(col_id) = right.as_ref() {
if pk_upper.contains(&col_id.column_canonical().to_lowercase())
&& matches!(left.as_ref(), vibesql_ast::Expression::Literal(_))
{
return false; }
}
}
true })
.collect();
crate::optimizer::combine_with_and(residual)
}
fn find_column_index(schema: &CombinedSchema, col_name: &str) -> Option<usize> {
let col_upper = col_name.to_lowercase();
let mut offset = 0;
for (_, table_schema) in schema.table_schemas.values() {
for (idx, col) in table_schema.columns.iter().enumerate() {
if col.name.to_lowercase() == col_upper {
return Some(offset + idx);
}
}
offset += table_schema.columns.len();
}
None
}