use std::collections::HashMap;
use vibesql_ast::Expression;
use vibesql_storage::{Database, Row};
use super::predicate::{
build_residual_where_clause, coerce_index_predicate_for_temporal_keys,
extract_composite_predicates_with_in, extract_index_predicate_for_indexed_column,
extract_prefix_equality_predicates, extract_prefix_with_trailing_range,
generate_composite_keys, where_clause_fully_satisfied_by_composite_key,
where_clause_fully_satisfied_by_indexed_column, CompositePredicateType, IndexPredicate,
PrefixPredicateResult, PrefixWithRangeResult,
};
use crate::{
errors::ExecutorError, optimizer::PredicatePlan, schema::CombinedSchema, select::cte::CteResult,
};
#[allow(private_interfaces)]
pub(crate) fn execute_index_scan(
table_name: &str,
index_name: &str,
alias: Option<&String>,
where_clause: Option<&Expression>,
sorted_columns: Option<Vec<(String, vibesql_ast::OrderDirection)>>,
limit: Option<usize>,
database: &Database,
cte_results: &HashMap<String, CteResult>,
) -> Result<super::super::FromResult, ExecutorError> {
let table = database
.get_table(table_name)
.ok_or_else(|| ExecutorError::TableNotFound(table_name.to_string()))?;
let index_metadata = database
.get_index(index_name)
.ok_or_else(|| ExecutorError::IndexNotFound(index_name.to_string()))?;
let index_data = database
.get_index_data(index_name)
.ok_or_else(|| ExecutorError::IndexNotFound(index_name.to_string()))?;
let is_multi_column_index = index_metadata.columns.len() > 1;
let first_indexed_column = index_metadata.columns.first();
let is_expression_index = first_indexed_column.map(|col| col.is_expression()).unwrap_or(false);
let index_column_names: Vec<&str> = if is_expression_index {
vec![]
} else {
index_metadata.columns.iter().filter_map(|col| col.column_name()).collect()
};
let composite_predicates = if is_multi_column_index {
where_clause
.and_then(|expr| extract_composite_predicates_with_in(expr, &index_column_names))
} else {
None
};
let composite_keys: Option<Vec<Vec<vibesql_types::SqlValue>>> =
composite_predicates.as_ref().map(|preds| generate_composite_keys(preds));
let has_in_predicate = composite_predicates
.as_ref()
.map(|preds| preds.iter().any(|p| matches!(p, CompositePredicateType::In(_))))
.unwrap_or(false);
let use_composite_lookup = composite_keys.as_ref().map(|k| !k.is_empty()).unwrap_or(false);
let prefix_with_range_result: Option<PrefixWithRangeResult> = if !use_composite_lookup
&& is_multi_column_index
{
where_clause.and_then(|expr| extract_prefix_with_trailing_range(expr, &index_column_names))
} else {
None
};
let use_prefix_bounded_lookup = prefix_with_range_result.is_some();
let prefix_result: Option<PrefixPredicateResult> =
if !use_composite_lookup && !use_prefix_bounded_lookup && is_multi_column_index {
where_clause
.and_then(|expr| extract_prefix_equality_predicates(expr, &index_column_names))
} else {
None
};
let use_prefix_lookup =
prefix_result.is_some() && !use_composite_lookup && !use_prefix_bounded_lookup;
let index_predicate = if use_composite_lookup || use_prefix_lookup || use_prefix_bounded_lookup
{
None } else {
first_indexed_column.and_then(|idx_col| {
where_clause.and_then(|expr| extract_index_predicate_for_indexed_column(expr, idx_col))
})
};
let index_predicate = coerce_index_predicate_for_temporal_keys(index_predicate, index_data);
let residual_where = if let Some(ref prefix_range) = prefix_with_range_result {
if let Some(where_expr) = where_clause {
build_residual_where_clause(where_expr, &prefix_range.covered_columns)
} else {
None
}
} else if let Some(ref prefix) = prefix_result {
if let Some(where_expr) = where_clause {
build_residual_where_clause(where_expr, &prefix.covered_columns)
} else {
None
}
} else {
None
};
let (need_where_filter, effective_where) = if use_composite_lookup {
match where_clause {
Some(where_expr) => {
let satisfied =
where_clause_fully_satisfied_by_composite_key(where_expr, &index_column_names);
if satisfied {
(false, None)
} else {
(true, Some((*where_expr).clone()))
}
}
None => (false, None),
}
} else if use_prefix_bounded_lookup {
match &residual_where {
Some(residual) => (true, Some(residual.clone())), None => (false, None), }
} else if use_prefix_lookup {
match &residual_where {
Some(residual) => (true, Some(residual.clone())), None => (false, None),
}
} else {
match (&where_clause, &index_predicate, first_indexed_column) {
(Some(where_expr), Some(_), Some(idx_col)) => {
let need_filter = !where_clause_fully_satisfied_by_indexed_column(
where_expr,
idx_col,
&index_predicate,
);
(need_filter, if need_filter { Some((*where_expr).clone()) } else { None })
}
(Some(where_expr), None, _) => (true, Some((*where_expr).clone())),
(Some(where_expr), Some(_), None) => (true, Some((*where_expr).clone())),
(None, _, _) => (false, None), }
};
let can_use_streaming = !use_composite_lookup
&& !use_prefix_lookup
&& !use_prefix_bounded_lookup
&& !need_where_filter
&& sorted_columns.is_none()
&& limit.is_none()
&& matches!(&index_predicate, Some(IndexPredicate::Range(_)));
let snapshot = crate::mvcc::read_snapshot(database);
if can_use_streaming {
if let Some(IndexPredicate::Range(range)) = &index_predicate {
if let Some(streaming_iter) = index_data.range_scan_streaming(
range.start.as_ref(),
range.end.as_ref(),
range.inclusive_start,
range.inclusive_end,
) {
let null_filter_col_idx = if range.exclude_nulls {
first_indexed_column.and_then(|idx_col| idx_col.column_name()).and_then(
|col_name| {
table
.schema
.columns
.iter()
.position(|c| c.name.eq_ignore_ascii_case(col_name))
},
)
} else {
None
};
let profile = std::env::var("RANGE_SCAN_PROFILE").is_ok();
let rows: Vec<Row> = if profile {
use std::time::Instant;
let mut index_time = std::time::Duration::ZERO;
let mut lookup_time = std::time::Duration::ZERO;
let mut clone_time = std::time::Duration::ZERO;
let mut rows = Vec::new();
let mut row_count = 0usize;
let mut streaming_iter = streaming_iter;
loop {
let t0 = Instant::now();
let idx = streaming_iter.next();
index_time += t0.elapsed();
let Some(idx) = idx else { break };
let t1 = Instant::now();
if !table.is_row_visible(idx, &snapshot) {
lookup_time += t1.elapsed();
continue;
}
if let Some(row_ref) = table.get_row(idx) {
lookup_time += t1.elapsed();
if let Some(col_idx) = null_filter_col_idx {
if matches!(
row_ref.values.get(col_idx),
Some(vibesql_types::SqlValue::Null)
) {
continue;
}
}
let t2 = Instant::now();
let mut cloned = row_ref.clone();
if cloned.row_id.is_none() {
cloned.set_row_id((idx + 1) as u64);
}
rows.push(cloned);
clone_time += t2.elapsed();
row_count += 1;
} else {
lookup_time += t1.elapsed();
}
}
static PROFILE_COUNT: std::sync::atomic::AtomicUsize =
std::sync::atomic::AtomicUsize::new(0);
let count = PROFILE_COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
if count.is_multiple_of(1000) {
eprintln!(
"[RangeScan Profile] rows={}, index={:?}, lookup={:?}, clone={:?}",
row_count, index_time, lookup_time, clone_time
);
}
rows
} else {
streaming_iter
.filter(|idx| table.is_row_visible(*idx, &snapshot))
.filter_map(|idx| table.get_row(idx).map(|row| (idx, row)))
.filter(|(_, row)| {
if let Some(col_idx) = null_filter_col_idx {
!matches!(
row.values.get(col_idx),
Some(vibesql_types::SqlValue::Null)
)
} else {
true
}
})
.map(|(idx, row)| {
let mut cloned = row.clone();
if cloned.row_id.is_none() {
cloned.set_row_id((idx + 1) as u64);
}
cloned
})
.collect()
};
database.increment_search_count(rows.len() as u64);
let effective_name = alias.cloned().unwrap_or_else(|| table_name.to_string());
let schema = CombinedSchema::from_table(effective_name, table.schema.clone());
return Ok(super::super::FromResult::from_rows_where_filtered(schema, rows, None));
}
}
}
let mut used_reverse_iteration = false;
let matching_row_indices: Vec<usize> = if let Some(ref keys) = composite_keys {
if keys.is_empty() {
vec![]
} else if keys.len() == 1 && !has_in_predicate {
index_data.get(&keys[0]).unwrap_or_default()
} else {
let mut all_indices = Vec::new();
for key in keys {
let indices = index_data.get(key).unwrap_or_default();
all_indices.extend(indices);
}
all_indices.sort_unstable();
all_indices.dedup();
all_indices
}
} else if let Some(ref prefix_range) = prefix_with_range_result {
index_data.prefix_range_scan(
&prefix_range.prefix_key,
prefix_range.lower_bound.as_ref(),
prefix_range.inclusive_lower,
prefix_range.upper_bound.as_ref(),
prefix_range.inclusive_upper,
)
} else if let Some(ref prefix) = prefix_result {
let needs_desc_order = sorted_columns
.as_ref()
.and_then(|cols| cols.first())
.map(|(_, dir)| *dir == vibesql_ast::OrderDirection::Desc)
.unwrap_or(false);
if needs_desc_order {
used_reverse_iteration = true;
if let (Some(limit_val), false) = (limit, need_where_filter) {
index_data.prefix_scan_reverse_limit(&prefix.prefix_key, limit_val)
} else {
index_data.prefix_scan_reverse(&prefix.prefix_key)
}
} else {
index_data.prefix_scan(&prefix.prefix_key)
}
} else {
match index_predicate {
Some(IndexPredicate::Range(ref range)) => {
let use_limit_optimization =
limit.is_some() && !need_where_filter && sorted_columns.is_none();
if use_limit_optimization {
index_data.range_scan_limit(
range.start.as_ref(),
range.end.as_ref(),
range.inclusive_start,
range.inclusive_end,
limit,
)
} else {
index_data.range_scan(
range.start.as_ref(),
range.end.as_ref(),
range.inclusive_start,
range.inclusive_end,
)
}
}
Some(IndexPredicate::In(ref values)) => {
if is_multi_column_index {
index_data.prefix_multi_lookup(values)
} else {
index_data.multi_lookup(values)
}
}
None => {
index_data.values().flatten().collect()
}
}
};
let mut matching_row_indices = matching_row_indices;
if sorted_columns.is_none() {
matching_row_indices.sort_unstable();
}
let limit_already_applied =
if let (Some(sorted_cols), Some(limit_val)) = (&sorted_columns, limit) {
if need_where_filter {
false
} else {
let is_desc = sorted_cols
.first()
.is_some_and(|(_, dir)| *dir == vibesql_ast::OrderDirection::Desc);
if is_desc {
matching_row_indices.reverse();
matching_row_indices.truncate(limit_val);
true } else {
matching_row_indices.truncate(limit_val);
false }
}
} else {
false
};
let effective_name = alias.cloned().unwrap_or_else(|| table_name.to_string());
let schema = CombinedSchema::from_table(effective_name, table.schema.clone());
let null_filter_col_idx = if let Some(IndexPredicate::Range(ref range)) = index_predicate {
if range.exclude_nulls {
first_indexed_column.and_then(|idx_col| idx_col.column_name()).and_then(|col_name| {
table.schema.columns.iter().position(|c| c.name.eq_ignore_ascii_case(col_name))
})
} else {
None
}
} else {
None
};
let indexed_row_refs: Vec<(usize, &Row)> = matching_row_indices
.iter()
.filter(|idx| table.is_row_visible(**idx, &snapshot))
.filter_map(|idx| table.get_row(*idx).map(|row| (*idx, row)))
.filter(|(_, row)| {
if let Some(col_idx) = null_filter_col_idx {
!matches!(row.values.get(col_idx), Some(vibesql_types::SqlValue::Null))
} else {
true
}
})
.collect();
let row_ptr_to_idx: std::collections::HashMap<usize, usize> =
indexed_row_refs.iter().map(|(idx, row)| (*row as *const Row as usize, *idx)).collect();
let row_refs: Vec<&Row> = indexed_row_refs.into_iter().map(|(_, row)| row).collect();
database.increment_search_count(row_refs.len() as u64);
let filtered_row_refs: Vec<&Row> = if need_where_filter && effective_where.is_some() {
let predicate_plan = PredicatePlan::from_where_clause(effective_where.as_ref(), &schema)
.map_err(ExecutorError::InvalidWhereClause)?;
apply_where_filter_zerocopy(
row_refs,
&schema,
&predicate_plan,
table_name,
database,
cte_results,
)?
} else {
row_refs
};
let mut filtered_row_refs = filtered_row_refs;
if !limit_already_applied && !used_reverse_iteration {
if let Some(ref sorted_cols) = sorted_columns {
if let Some((_, first_order_direction)) = sorted_cols.first() {
if *first_order_direction == vibesql_ast::OrderDirection::Desc {
filtered_row_refs.reverse();
}
}
}
}
let rows: Vec<Row> = filtered_row_refs
.into_iter()
.map(|row| {
let mut cloned = row.clone();
if cloned.row_id.is_none() {
if let Some(&idx) = row_ptr_to_idx.get(&(row as *const Row as usize)) {
cloned.set_row_id((idx + 1) as u64);
}
}
cloned
})
.collect();
if !need_where_filter {
Ok(super::super::FromResult::from_rows_where_filtered(schema, rows, sorted_columns))
} else {
match sorted_columns {
Some(sorted) => Ok(super::super::FromResult::from_rows_sorted(schema, rows, sorted)),
None => Ok(super::super::FromResult::from_rows(schema, rows)),
}
}
}
fn apply_where_filter_zerocopy<'a>(
row_refs: Vec<&'a Row>,
schema: &CombinedSchema,
predicate_plan: &PredicatePlan,
table_name: &str,
database: &vibesql_storage::Database,
cte_results: &HashMap<String, CteResult>,
) -> Result<Vec<&'a Row>, ExecutorError> {
use crate::{
evaluator::{compiled::CompiledPredicate, CombinedExpressionEvaluator},
select::scan::predicates::combine_predicates_with_and,
};
let table_stats_owned = database.get_table(table_name).map(|table| {
table.get_statistics().cloned().unwrap_or_else(|| {
vibesql_storage::statistics::TableStatistics::estimate_from_schema(
table.row_count(),
&table.schema,
)
})
});
let ordered_preds =
predicate_plan.get_table_filters_ordered(table_name, table_stats_owned.as_ref());
if ordered_preds.is_empty() {
return Ok(row_refs);
}
let combined_where = combine_predicates_with_and(ordered_preds);
let compiled = CompiledPredicate::compile(&combined_where, schema);
if compiled.is_fully_compiled() {
return apply_where_filter_compiled(row_refs, &compiled);
}
let evaluator = if cte_results.is_empty() {
CombinedExpressionEvaluator::with_database(schema, database)
} else {
CombinedExpressionEvaluator::with_database_and_cte(schema, database, cte_results)
};
#[cfg(feature = "parallel")]
{
let config = crate::select::parallel::ParallelConfig::global();
if config.should_parallelize_scan(row_refs.len()) {
return apply_where_filter_zerocopy_parallel(
row_refs,
schema,
combined_where,
evaluator,
);
}
}
let mut filtered = Vec::new();
for row_ref in row_refs {
evaluator.clear_cse_cache();
let include_row = match evaluator.eval(&combined_where, row_ref)? {
vibesql_types::SqlValue::Boolean(true) => true,
vibesql_types::SqlValue::Boolean(false) | vibesql_types::SqlValue::Null => false,
vibesql_types::SqlValue::Integer(0) => false,
vibesql_types::SqlValue::Integer(_) => true,
vibesql_types::SqlValue::Smallint(0) => false,
vibesql_types::SqlValue::Smallint(_) => true,
vibesql_types::SqlValue::Bigint(0) => false,
vibesql_types::SqlValue::Bigint(_) => true,
vibesql_types::SqlValue::Float(0.0) => false,
vibesql_types::SqlValue::Float(_) => true,
vibesql_types::SqlValue::Real(0.0) => false,
vibesql_types::SqlValue::Real(_) => true,
vibesql_types::SqlValue::Double(0.0) => false,
vibesql_types::SqlValue::Double(_) => true,
vibesql_types::SqlValue::Numeric(n) if n == 0.0 => false,
vibesql_types::SqlValue::Numeric(_) => true,
vibesql_types::SqlValue::Varchar(s) | vibesql_types::SqlValue::Character(s) => {
string_to_truthy(&s)
}
other => {
return Err(ExecutorError::InvalidWhereClause(format!(
"WHERE clause must evaluate to boolean, got: {:?}",
other
)))
}
};
if include_row {
filtered.push(row_ref);
}
}
Ok(filtered)
}
#[inline]
fn apply_where_filter_compiled<'a>(
row_refs: Vec<&'a Row>,
compiled: &crate::evaluator::compiled::CompiledPredicate,
) -> Result<Vec<&'a Row>, ExecutorError> {
let mut filtered = Vec::with_capacity(row_refs.len() / 2);
for row_ref in row_refs {
let include_row = compiled.evaluate(row_ref).unwrap_or(false);
if include_row {
filtered.push(row_ref);
}
}
Ok(filtered)
}
#[cfg(feature = "parallel")]
fn apply_where_filter_zerocopy_parallel<'a>(
row_refs: Vec<&'a Row>,
_schema: &CombinedSchema,
combined_where: vibesql_ast::Expression,
evaluator: crate::evaluator::CombinedExpressionEvaluator,
) -> Result<Vec<&'a Row>, ExecutorError> {
use std::sync::Arc;
use rayon::prelude::*;
let where_expr_arc = Arc::new(combined_where);
let (schema, database, outer_row, outer_schema, window_mapping, cte_context, enable_cse) =
evaluator.get_parallel_components();
let result: Result<Vec<_>, ExecutorError> = row_refs
.into_par_iter()
.map(|row_ref| {
let thread_evaluator =
crate::evaluator::CombinedExpressionEvaluator::from_parallel_components(
schema,
database,
outer_row,
outer_schema,
window_mapping,
cte_context,
enable_cse,
);
let include_row = match thread_evaluator.eval(&where_expr_arc, row_ref)? {
vibesql_types::SqlValue::Boolean(true) => true,
vibesql_types::SqlValue::Boolean(false) | vibesql_types::SqlValue::Null => false,
vibesql_types::SqlValue::Integer(0) => false,
vibesql_types::SqlValue::Integer(_) => true,
vibesql_types::SqlValue::Smallint(0) => false,
vibesql_types::SqlValue::Smallint(_) => true,
vibesql_types::SqlValue::Bigint(0) => false,
vibesql_types::SqlValue::Bigint(_) => true,
vibesql_types::SqlValue::Float(0.0) => false,
vibesql_types::SqlValue::Float(_) => true,
vibesql_types::SqlValue::Real(0.0) => false,
vibesql_types::SqlValue::Real(_) => true,
vibesql_types::SqlValue::Double(0.0) => false,
vibesql_types::SqlValue::Double(_) => true,
vibesql_types::SqlValue::Numeric(n) if n == 0.0 => false,
vibesql_types::SqlValue::Numeric(_) => true,
vibesql_types::SqlValue::Varchar(s) | vibesql_types::SqlValue::Character(s) => {
string_to_truthy(&s)
}
other => {
return Err(ExecutorError::InvalidWhereClause(format!(
"WHERE clause must evaluate to boolean, got: {:?}",
other
)))
}
};
if include_row {
Ok(Some(row_ref))
} else {
Ok(None)
}
})
.collect();
result.map(|v| v.into_iter().flatten().collect())
}
#[inline(always)]
fn string_to_truthy(s: &str) -> bool {
if s.is_empty() {
return false;
}
let trimmed = s.trim();
if trimmed.is_empty() {
return false;
}
let mut end = 0;
let mut has_dot = false;
let mut has_digit = false;
let chars: Vec<char> = trimmed.chars().collect();
if !chars.is_empty() && (chars[0] == '-' || chars[0] == '+') {
end = 1;
}
while end < chars.len() {
let c = chars[end];
if c.is_ascii_digit() {
has_digit = true;
end += 1;
} else if c == '.' && !has_dot {
has_dot = true;
end += 1;
} else {
break;
}
}
if !has_digit {
return false;
}
let num_str: String = chars[..end].iter().collect();
num_str.parse::<f64>().map(|n| n != 0.0).unwrap_or(false)
}
pub(in crate::select::scan) fn execute_skip_scan(
table_name: &str,
index_name: &str,
alias: Option<&String>,
where_clause: &Expression,
skip_scan_info: &crate::optimizer::index_planner::SkipScanInfo,
database: &Database,
cte_results: &HashMap<String, CteResult>,
) -> Result<super::super::FromResult, ExecutorError> {
let table = database
.get_table(table_name)
.ok_or_else(|| ExecutorError::TableNotFound(table_name.to_string()))?;
let _index_metadata = database
.get_index(index_name)
.ok_or_else(|| ExecutorError::IndexNotFound(index_name.to_string()))?;
let index_data = database
.get_index_data(index_name)
.ok_or_else(|| ExecutorError::IndexNotFound(index_name.to_string()))?;
let filter_column = &skip_scan_info.filter_column;
let filter_column_idx = skip_scan_info.skip_columns;
let skip_scan_predicate = extract_skip_scan_predicate(where_clause, filter_column);
let matching_row_indices: Vec<usize> = match skip_scan_predicate {
SkipScanPredicate::Equality(value) => {
if std::env::var("SKIP_SCAN_DEBUG").is_ok() {
eprintln!(
"[SKIP_SCAN] Executing equality skip-scan: index={}, filter_col={}, filter_col_idx={}, value={:?}",
index_name, filter_column, filter_column_idx, value
);
}
index_data.skip_scan_equality(filter_column_idx, &value)
}
SkipScanPredicate::Range { lower, inclusive_lower, upper, inclusive_upper } => {
if std::env::var("SKIP_SCAN_DEBUG").is_ok() {
eprintln!(
"[SKIP_SCAN] Executing range skip-scan: index={}, filter_col={}, filter_col_idx={}, lower={:?}, upper={:?}",
index_name, filter_column, filter_column_idx, lower, upper
);
}
index_data.skip_scan_range(
filter_column_idx,
lower.as_ref(),
inclusive_lower,
upper.as_ref(),
inclusive_upper,
)
}
SkipScanPredicate::None => {
if std::env::var("SKIP_SCAN_DEBUG").is_ok() {
eprintln!(
"[SKIP_SCAN] Warning: No predicate extracted for filter column '{}', returning empty",
filter_column
);
}
vec![]
}
};
if std::env::var("SKIP_SCAN_DEBUG").is_ok() {
eprintln!("[SKIP_SCAN] Found {} matching rows from skip-scan", matching_row_indices.len());
}
let effective_name = alias.cloned().unwrap_or_else(|| table_name.to_string());
let schema = CombinedSchema::from_table(effective_name.clone(), table.schema.clone());
let snapshot = crate::mvcc::read_snapshot(database);
let row_refs: Vec<&Row> = matching_row_indices
.iter()
.filter(|idx| table.is_row_visible(**idx, &snapshot))
.filter_map(|idx| table.get_row(*idx))
.collect();
let predicate_plan = PredicatePlan::from_where_clause(Some(where_clause), &schema)
.map_err(ExecutorError::InvalidWhereClause)?;
let filtered_row_refs = apply_where_filter_for_skip_scan(
row_refs,
&schema,
&predicate_plan,
table_name,
database,
cte_results,
)?;
let rows: Vec<Row> = filtered_row_refs.into_iter().cloned().collect();
Ok(super::super::FromResult::from_rows(schema, rows))
}
#[derive(Debug)]
enum SkipScanPredicate {
Equality(vibesql_types::SqlValue),
Range {
lower: Option<vibesql_types::SqlValue>,
inclusive_lower: bool,
upper: Option<vibesql_types::SqlValue>,
inclusive_upper: bool,
},
None,
}
fn extract_skip_scan_predicate(
where_clause: &Expression,
filter_column: &str,
) -> SkipScanPredicate {
use vibesql_ast::BinaryOperator;
use super::selection::is_column_reference;
match where_clause {
Expression::BinaryOp { left, op, right } => {
match op {
BinaryOperator::Equal => {
if is_column_reference(left, filter_column) {
if let Expression::Literal(value) = right.as_ref() {
return SkipScanPredicate::Equality(value.clone());
}
}
if is_column_reference(right, filter_column) {
if let Expression::Literal(value) = left.as_ref() {
return SkipScanPredicate::Equality(value.clone());
}
}
}
BinaryOperator::GreaterThan => {
if is_column_reference(left, filter_column) {
if let Expression::Literal(value) = right.as_ref() {
return SkipScanPredicate::Range {
lower: Some(value.clone()),
inclusive_lower: false,
upper: None,
inclusive_upper: false,
};
}
}
if is_column_reference(right, filter_column) {
if let Expression::Literal(value) = left.as_ref() {
return SkipScanPredicate::Range {
lower: None,
inclusive_lower: false,
upper: Some(value.clone()),
inclusive_upper: false,
};
}
}
}
BinaryOperator::GreaterThanOrEqual => {
if is_column_reference(left, filter_column) {
if let Expression::Literal(value) = right.as_ref() {
return SkipScanPredicate::Range {
lower: Some(value.clone()),
inclusive_lower: true,
upper: None,
inclusive_upper: false,
};
}
}
if is_column_reference(right, filter_column) {
if let Expression::Literal(value) = left.as_ref() {
return SkipScanPredicate::Range {
lower: None,
inclusive_lower: false,
upper: Some(value.clone()),
inclusive_upper: true,
};
}
}
}
BinaryOperator::LessThan => {
if is_column_reference(left, filter_column) {
if let Expression::Literal(value) = right.as_ref() {
return SkipScanPredicate::Range {
lower: None,
inclusive_lower: false,
upper: Some(value.clone()),
inclusive_upper: false,
};
}
}
if is_column_reference(right, filter_column) {
if let Expression::Literal(value) = left.as_ref() {
return SkipScanPredicate::Range {
lower: Some(value.clone()),
inclusive_lower: false,
upper: None,
inclusive_upper: false,
};
}
}
}
BinaryOperator::LessThanOrEqual => {
if is_column_reference(left, filter_column) {
if let Expression::Literal(value) = right.as_ref() {
return SkipScanPredicate::Range {
lower: None,
inclusive_lower: false,
upper: Some(value.clone()),
inclusive_upper: true,
};
}
}
if is_column_reference(right, filter_column) {
if let Expression::Literal(value) = left.as_ref() {
return SkipScanPredicate::Range {
lower: Some(value.clone()),
inclusive_lower: true,
upper: None,
inclusive_upper: false,
};
}
}
}
BinaryOperator::And => {
let left_pred = extract_skip_scan_predicate(left, filter_column);
let right_pred = extract_skip_scan_predicate(right, filter_column);
match (left_pred, right_pred) {
(SkipScanPredicate::Equality(v), SkipScanPredicate::None) => {
return SkipScanPredicate::Equality(v);
}
(SkipScanPredicate::None, SkipScanPredicate::Equality(v)) => {
return SkipScanPredicate::Equality(v);
}
(
SkipScanPredicate::Range {
lower: l1,
inclusive_lower: il1,
upper: u1,
inclusive_upper: iu1,
},
SkipScanPredicate::Range {
lower: l2,
inclusive_lower: il2,
upper: u2,
inclusive_upper: iu2,
},
) => {
let (lower, inclusive_lower) = match (l1, l2) {
(Some(v1), Some(v2)) => {
if v1 >= v2 {
(Some(v1), il1)
} else {
(Some(v2), il2)
}
}
(Some(v), None) | (None, Some(v)) => (Some(v), il1 || il2),
(None, None) => (None, false),
};
let (upper, inclusive_upper) = match (u1, u2) {
(Some(v1), Some(v2)) => {
if v1 <= v2 {
(Some(v1), iu1)
} else {
(Some(v2), iu2)
}
}
(Some(v), None) | (None, Some(v)) => (Some(v), iu1 || iu2),
(None, None) => (None, false),
};
return SkipScanPredicate::Range {
lower,
inclusive_lower,
upper,
inclusive_upper,
};
}
(r @ SkipScanPredicate::Range { .. }, SkipScanPredicate::None) => return r,
(SkipScanPredicate::None, r @ SkipScanPredicate::Range { .. }) => return r,
_ => {}
}
}
_ => {}
}
}
Expression::Between { expr, low, high, negated: false, .. } => {
if is_column_reference(expr, filter_column) {
if let (Expression::Literal(low_val), Expression::Literal(high_val)) =
(low.as_ref(), high.as_ref())
{
return SkipScanPredicate::Range {
lower: Some(low_val.clone()),
inclusive_lower: true,
upper: Some(high_val.clone()),
inclusive_upper: true,
};
}
}
}
_ => {}
}
SkipScanPredicate::None
}
fn apply_where_filter_for_skip_scan<'a>(
row_refs: Vec<&'a Row>,
schema: &CombinedSchema,
predicate_plan: &PredicatePlan,
table_name: &str,
database: &Database,
cte_results: &HashMap<String, CteResult>,
) -> Result<Vec<&'a Row>, ExecutorError> {
use crate::{
evaluator::{compiled::CompiledPredicate, CombinedExpressionEvaluator},
select::scan::predicates::combine_predicates_with_and,
};
let table_stats_owned = database.get_table(table_name).map(|table| {
table.get_statistics().cloned().unwrap_or_else(|| {
vibesql_storage::statistics::TableStatistics::estimate_from_schema(
table.row_count(),
&table.schema,
)
})
});
let ordered_preds =
predicate_plan.get_table_filters_ordered(table_name, table_stats_owned.as_ref());
if ordered_preds.is_empty() {
return Ok(row_refs);
}
let combined_where = combine_predicates_with_and(ordered_preds);
let compiled = CompiledPredicate::compile(&combined_where, schema);
if compiled.is_fully_compiled() {
let mut filtered = Vec::with_capacity(row_refs.len() / 2);
for row_ref in row_refs {
if compiled.evaluate(row_ref).unwrap_or(false) {
filtered.push(row_ref);
}
}
return Ok(filtered);
}
let evaluator = if cte_results.is_empty() {
CombinedExpressionEvaluator::with_database(schema, database)
} else {
CombinedExpressionEvaluator::with_database_and_cte(schema, database, cte_results)
};
let mut filtered = Vec::new();
for row_ref in row_refs {
evaluator.clear_cse_cache();
let include_row = match evaluator.eval(&combined_where, row_ref)? {
vibesql_types::SqlValue::Boolean(true) => true,
vibesql_types::SqlValue::Boolean(false) | vibesql_types::SqlValue::Null => false,
vibesql_types::SqlValue::Integer(0) => false,
vibesql_types::SqlValue::Integer(_) => true,
_ => false,
};
if include_row {
filtered.push(row_ref);
}
}
Ok(filtered)
}