use std::collections::HashMap;
use vibesql_storage::{Database, Row};
use vibesql_types::SqlValue;
use super::predicate::{
extract_prefix_equality_predicates, extract_prefix_with_trailing_range, PrefixWithRangeResult,
};
use crate::{
errors::ExecutorError,
schema::CombinedSchema,
select::{cte::CteResult, scan::FromResult},
};
#[derive(Debug)]
pub(crate) struct CoveringIndexInfo {
pub column_to_key_position: HashMap<String, usize>,
}
pub(crate) fn check_covering_index(
index_column_names: &[&str],
needed_columns: &[String],
) -> Option<CoveringIndexInfo> {
let mut column_to_key_position = HashMap::new();
for needed_col in needed_columns {
let needed_lower = needed_col.to_ascii_lowercase();
let mut found = false;
for (pos, &index_col) in index_column_names.iter().enumerate() {
if index_col.to_ascii_lowercase() == needed_lower {
column_to_key_position.insert(needed_col.clone(), pos);
found = true;
break;
}
}
if !found {
return None;
}
}
Some(CoveringIndexInfo { column_to_key_position })
}
pub(super) fn execute_covering_index_scan(
table_name: &str,
index_name: &str,
alias: Option<&String>,
where_clause: Option<&vibesql_ast::Expression>,
needed_columns: &[String],
covering_info: &CoveringIndexInfo,
database: &Database,
_cte_results: &HashMap<String, CteResult>,
) -> Result<FromResult, ExecutorError> {
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 table = database
.get_table(table_name)
.ok_or_else(|| ExecutorError::TableNotFound(table_name.to_string()))?;
let index_column_names: Vec<&str> =
index_metadata.columns.iter().map(|col| col.expect_column_name()).collect();
let prefix_with_range: Option<PrefixWithRangeResult> =
where_clause.and_then(|expr| extract_prefix_with_trailing_range(expr, &index_column_names));
let prefix_only = if prefix_with_range.is_none() {
where_clause.and_then(|expr| extract_prefix_equality_predicates(expr, &index_column_names))
} else {
None
};
let covering_results: Vec<(Vec<SqlValue>, Vec<usize>)> = if let Some(ref pr) = prefix_with_range
{
index_data.prefix_range_scan_covering(
&pr.prefix_key,
pr.lower_bound.as_ref(),
pr.inclusive_lower,
pr.upper_bound.as_ref(),
pr.inclusive_upper,
)
} else if let Some(ref po) = prefix_only {
index_data.prefix_scan_covering(&po.prefix_key)
} else {
Vec::new()
};
let mut rows = Vec::new();
for (key_values, row_indices) in covering_results {
let _row_count = row_indices.len();
let projected_values: Vec<SqlValue> = needed_columns
.iter()
.map(|col| {
let key_pos = covering_info.column_to_key_position[col];
key_values[key_pos].clone()
})
.collect();
for _ in row_indices {
rows.push(Row::from_vec(projected_values.clone()));
}
}
let effective_name = alias.cloned().unwrap_or_else(|| table_name.to_string());
let mut result_columns = Vec::new();
for col_name in needed_columns {
if let Some(col_schema) =
table.schema.columns.iter().find(|c| c.name.eq_ignore_ascii_case(col_name))
{
result_columns.push(col_schema.clone());
}
}
let result_schema = vibesql_catalog::TableSchema::new(effective_name.clone(), result_columns);
let schema = CombinedSchema::from_table(effective_name, result_schema);
Ok(FromResult::from_rows_where_filtered(schema, rows, None))
}
pub(in crate::select) fn try_covering_index_scan(
table_name: &str,
index_name: &str,
alias: Option<&String>,
where_clause: Option<&vibesql_ast::Expression>,
needed_columns: &[String],
database: &Database,
cte_results: &HashMap<String, CteResult>,
) -> Result<Option<FromResult>, ExecutorError> {
let index_metadata = match database.get_index(index_name) {
Some(m) => m,
None => return Ok(None),
};
let index_column_names: Vec<&str> =
index_metadata.columns.iter().map(|col| col.expect_column_name()).collect();
let covering_info = match check_covering_index(&index_column_names, needed_columns) {
Some(info) => info,
None => return Ok(None),
};
if let Some(expr) = where_clause {
let prefix_with_range = extract_prefix_with_trailing_range(expr, &index_column_names);
let prefix_only = if prefix_with_range.is_none() {
extract_prefix_equality_predicates(expr, &index_column_names)
} else {
None
};
if prefix_with_range.is_none() && prefix_only.is_none() {
return Ok(None);
}
}
let result = execute_covering_index_scan(
table_name,
index_name,
alias,
where_clause,
needed_columns,
&covering_info,
database,
cte_results,
)?;
Ok(Some(result))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_check_covering_index_all_columns_present() {
let index_cols = vec!["s_w_id", "s_quantity", "s_i_id"];
let needed = vec!["s_i_id".to_string()];
let result = check_covering_index(&index_cols, &needed);
assert!(result.is_some());
let info = result.unwrap();
assert_eq!(info.column_to_key_position["s_i_id"], 2);
}
#[test]
fn test_check_covering_index_multiple_columns() {
let index_cols = vec!["s_w_id", "s_quantity", "s_i_id"];
let needed = vec!["s_w_id".to_string(), "s_i_id".to_string()];
let result = check_covering_index(&index_cols, &needed);
assert!(result.is_some());
let info = result.unwrap();
assert_eq!(info.column_to_key_position["s_w_id"], 0);
assert_eq!(info.column_to_key_position["s_i_id"], 2);
}
#[test]
fn test_check_covering_index_missing_column() {
let index_cols = vec!["s_w_id", "s_quantity"];
let needed = vec!["s_i_id".to_string()];
let result = check_covering_index(&index_cols, &needed);
assert!(result.is_none());
}
#[test]
fn test_check_covering_index_case_insensitive() {
let index_cols = vec!["S_W_ID", "S_QUANTITY", "S_I_ID"];
let needed = vec!["s_i_id".to_string()];
let result = check_covering_index(&index_cols, &needed);
assert!(result.is_some());
}
}