use std::collections::{HashMap, HashSet};
use vibesql_ast::{BinaryOperator, Expression};
use vibesql_types::SqlValue;
use super::range::extract_range_predicate;
#[derive(Debug, Clone)]
pub(crate) enum CompositePredicateType {
Equality(SqlValue),
In(Vec<SqlValue>),
}
#[derive(Debug)]
pub(crate) struct PrefixPredicateResult {
pub prefix_key: Vec<SqlValue>,
pub covered_columns: HashSet<String>,
}
#[derive(Debug)]
pub(crate) struct PrefixWithRangeResult {
pub prefix_key: Vec<SqlValue>,
pub lower_bound: Option<SqlValue>,
pub inclusive_lower: bool,
pub upper_bound: Option<SqlValue>,
pub inclusive_upper: bool,
pub covered_columns: HashSet<String>,
}
#[allow(dead_code)]
pub(crate) fn extract_composite_equality_predicates(
expr: &Expression,
column_names: &[&str],
) -> Option<Vec<SqlValue>> {
if column_names.is_empty() {
return None;
}
let mut predicates: HashMap<String, SqlValue> = HashMap::new();
collect_equality_predicates(expr, &mut predicates);
let mut composite_key = Vec::with_capacity(column_names.len());
for col_name in column_names {
let col_upper = col_name.to_uppercase();
if let Some(value) = predicates.get(&col_upper) {
composite_key.push(value.clone());
} else {
return None;
}
}
Some(composite_key)
}
pub(crate) fn extract_prefix_equality_predicates(
expr: &Expression,
column_names: &[&str],
) -> Option<PrefixPredicateResult> {
if column_names.is_empty() {
return None;
}
let mut predicates: HashMap<String, SqlValue> = HashMap::new();
collect_equality_predicates(expr, &mut predicates);
let mut prefix_key = Vec::new();
let mut covered_columns = HashSet::new();
for col_name in column_names {
let col_upper = col_name.to_uppercase();
if let Some(value) = predicates.get(&col_upper) {
prefix_key.push(value.clone());
covered_columns.insert(col_upper);
} else {
break;
}
}
if prefix_key.is_empty() {
return None;
}
Some(PrefixPredicateResult { prefix_key, covered_columns })
}
pub(crate) fn extract_prefix_with_trailing_range(
expr: &Expression,
column_names: &[&str],
) -> Option<PrefixWithRangeResult> {
if column_names.len() < 2 {
return None;
}
let mut equality_predicates: HashMap<String, SqlValue> = HashMap::new();
collect_equality_predicates(expr, &mut equality_predicates);
let mut prefix_key = Vec::new();
let mut covered_columns = HashSet::new();
let mut prefix_end_idx = 0;
for (idx, col_name) in column_names.iter().enumerate() {
let col_upper = col_name.to_uppercase();
if let Some(value) = equality_predicates.get(&col_upper) {
prefix_key.push(value.clone());
covered_columns.insert(col_upper);
prefix_end_idx = idx + 1;
} else {
break;
}
}
if prefix_key.is_empty() || prefix_end_idx >= column_names.len() {
return None;
}
let next_col = column_names[prefix_end_idx];
let range = extract_range_predicate(expr, next_col);
if let Some(range_pred) = range {
if range_pred.start.is_some() || range_pred.end.is_some() {
covered_columns.insert(next_col.to_uppercase());
return Some(PrefixWithRangeResult {
prefix_key,
lower_bound: range_pred.start,
inclusive_lower: range_pred.inclusive_start,
upper_bound: range_pred.end,
inclusive_upper: range_pred.inclusive_end,
covered_columns,
});
}
}
None
}
pub(crate) fn build_residual_where_clause(
expr: &Expression,
covered_columns: &HashSet<String>,
) -> Option<Expression> {
filter_expression(expr, covered_columns)
}
fn filter_expression(expr: &Expression, covered_columns: &HashSet<String>) -> Option<Expression> {
match expr {
Expression::BinaryOp { left, op: BinaryOperator::Equal, right } => {
if let Expression::ColumnRef(col_id) = left.as_ref() {
if covered_columns.contains(&col_id.column_canonical().to_uppercase())
&& matches!(right.as_ref(), Expression::Literal(_))
{
return None;
}
}
if let Expression::ColumnRef(col_id) = right.as_ref() {
if covered_columns.contains(&col_id.column_canonical().to_uppercase())
&& matches!(left.as_ref(), Expression::Literal(_))
{
return None;
}
}
Some(expr.clone())
}
Expression::BinaryOp { left, op: BinaryOperator::And, right } => {
let left_filtered = filter_expression(left, covered_columns);
let right_filtered = filter_expression(right, covered_columns);
match (left_filtered, right_filtered) {
(Some(l), Some(r)) => Some(Expression::BinaryOp {
left: Box::new(l),
op: BinaryOperator::And,
right: Box::new(r),
}),
(Some(l), None) => Some(l),
(None, Some(r)) => Some(r),
(None, None) => None, }
}
_ => Some(expr.clone()),
}
}
pub(super) fn collect_equality_predicates(
expr: &Expression,
predicates: &mut HashMap<String, SqlValue>,
) {
match expr {
Expression::BinaryOp { left, op: BinaryOperator::Equal, right } => {
if let Expression::ColumnRef(col_id) = left.as_ref() {
if let Expression::Literal(value) = right.as_ref() {
if !matches!(value, SqlValue::Null) {
predicates.insert(col_id.column_canonical().to_uppercase(), value.clone());
}
}
}
if let Expression::ColumnRef(col_id) = right.as_ref() {
if let Expression::Literal(value) = left.as_ref() {
if !matches!(value, SqlValue::Null) {
predicates.insert(col_id.column_canonical().to_uppercase(), value.clone());
}
}
}
}
Expression::BinaryOp { left, op: BinaryOperator::And, right } => {
collect_equality_predicates(left, predicates);
collect_equality_predicates(right, predicates);
}
_ => {}
}
}
pub(crate) fn extract_composite_predicates_with_in(
expr: &Expression,
column_names: &[&str],
) -> Option<Vec<CompositePredicateType>> {
if column_names.is_empty() {
return None;
}
let mut equality_predicates: HashMap<String, SqlValue> = HashMap::new();
let mut in_predicates: HashMap<String, Vec<SqlValue>> = HashMap::new();
collect_predicates_with_in(expr, &mut equality_predicates, &mut in_predicates);
let mut result = Vec::with_capacity(column_names.len());
for col_name in column_names {
let col_upper = col_name.to_uppercase();
if let Some(value) = equality_predicates.get(&col_upper) {
result.push(CompositePredicateType::Equality(value.clone()));
}
else if let Some(values) = in_predicates.get(&col_upper) {
if values.is_empty() {
return None; }
result.push(CompositePredicateType::In(values.clone()));
} else {
return None;
}
}
Some(result)
}
fn collect_predicates_with_in(
expr: &Expression,
equality_predicates: &mut HashMap<String, SqlValue>,
in_predicates: &mut HashMap<String, Vec<SqlValue>>,
) {
match expr {
Expression::BinaryOp { left, op: BinaryOperator::Equal, right } => {
if let Expression::ColumnRef(col_id) = left.as_ref() {
if let Expression::Literal(value) = right.as_ref() {
if !matches!(value, SqlValue::Null) {
equality_predicates
.insert(col_id.column_canonical().to_uppercase(), value.clone());
}
}
}
if let Expression::ColumnRef(col_id) = right.as_ref() {
if let Expression::Literal(value) = left.as_ref() {
if !matches!(value, SqlValue::Null) {
equality_predicates
.insert(col_id.column_canonical().to_uppercase(), value.clone());
}
}
}
}
Expression::InList { expr: col_expr, values, negated } => {
if !negated {
if let Expression::ColumnRef(col_id) = col_expr.as_ref() {
let mut in_values = Vec::new();
let mut all_literals = true;
let mut has_null = false;
for item in values {
if let Expression::Literal(value) = item {
if matches!(value, SqlValue::Null) {
has_null = true;
}
in_values.push(value.clone());
} else {
all_literals = false;
break;
}
}
if all_literals && !has_null && !in_values.is_empty() {
in_predicates.insert(col_id.column_canonical().to_uppercase(), in_values);
}
}
}
}
Expression::BinaryOp { left, op: BinaryOperator::And, right } => {
collect_predicates_with_in(left, equality_predicates, in_predicates);
collect_predicates_with_in(right, equality_predicates, in_predicates);
}
_ => {}
}
}
pub(crate) fn generate_composite_keys(predicates: &[CompositePredicateType]) -> Vec<Vec<SqlValue>> {
if predicates.is_empty() {
return vec![];
}
let mut result: Vec<Vec<SqlValue>> = vec![vec![]];
for pred in predicates {
match pred {
CompositePredicateType::Equality(value) => {
for key in &mut result {
key.push(value.clone());
}
}
CompositePredicateType::In(values) => {
let mut new_result = Vec::with_capacity(result.len() * values.len());
for key in &result {
for value in values {
let mut new_key = key.clone();
new_key.push(value.clone());
new_result.push(new_key);
}
}
result = new_result;
}
}
}
result
}
pub(crate) fn where_clause_fully_satisfied_by_composite_key(
where_expr: &Expression,
index_column_names: &[&str],
) -> bool {
let mut predicate_count = 0;
let satisfied =
check_composite_satisfaction(where_expr, index_column_names, &mut predicate_count);
satisfied && predicate_count == index_column_names.len()
}
fn check_composite_satisfaction(
expr: &Expression,
index_column_names: &[&str],
predicate_count: &mut usize,
) -> bool {
match expr {
Expression::BinaryOp { left, op: BinaryOperator::Equal, right } => {
let col_name = extract_column_name(left).or_else(|| extract_column_name(right));
let has_non_null_literal = matches!(left.as_ref(), Expression::Literal(val) if !matches!(val, SqlValue::Null))
|| matches!(right.as_ref(), Expression::Literal(val) if !matches!(val, SqlValue::Null));
if let Some(name) = col_name {
let name_upper = name.to_uppercase();
let is_index_col =
index_column_names.iter().any(|c| c.to_uppercase() == name_upper);
if is_index_col && has_non_null_literal {
*predicate_count += 1;
return true;
}
}
false
}
Expression::InList { expr: col_expr, values, negated } => {
if *negated {
return false;
}
if let Some(col_name) = extract_column_name(col_expr) {
let name_upper = col_name.to_uppercase();
let is_index_col =
index_column_names.iter().any(|c| c.to_uppercase() == name_upper);
let all_literals = values.iter().all(
|v| matches!(v, Expression::Literal(val) if !matches!(val, SqlValue::Null)),
);
if is_index_col && all_literals && !values.is_empty() {
*predicate_count += 1;
return true;
}
}
false
}
Expression::BinaryOp { left, op: BinaryOperator::And, right } => {
let left_ok = check_composite_satisfaction(left, index_column_names, predicate_count);
let right_ok = check_composite_satisfaction(right, index_column_names, predicate_count);
left_ok && right_ok
}
_ => false,
}
}
fn extract_column_name(expr: &Expression) -> Option<&str> {
match expr {
Expression::ColumnRef(col_id) => Some(col_id.column_canonical()),
_ => None,
}
}