use std::collections::HashSet;
use vibesql_storage::Database;
use vibesql_types::SqlValue;
use crate::evaluator::ExpressionEvaluator;
#[inline]
pub fn is_predicate_truthy(value: &SqlValue) -> bool {
match value {
SqlValue::Boolean(b) => *b,
SqlValue::Null => false,
SqlValue::Integer(n) => *n != 0,
SqlValue::Smallint(n) => *n != 0,
SqlValue::Bigint(n) => *n != 0,
SqlValue::Unsigned(n) => *n != 0,
SqlValue::Float(f) => *f != 0.0,
SqlValue::Real(f) => *f != 0.0,
SqlValue::Double(f) => *f != 0.0,
SqlValue::Numeric(f) => *f != 0.0,
SqlValue::Varchar(s) | SqlValue::Character(s) => {
s.parse::<f64>().map(|n| n != 0.0).unwrap_or(false)
}
_ => false,
}
}
pub fn evaluate_partial_index_predicates(
db: &Database,
table_name: &str,
row: &vibesql_storage::Row,
) -> HashSet<String> {
if !db.has_partial_indexes(table_name) {
return HashSet::new();
}
let table_schema = match db.catalog.get_table(table_name) {
Some(schema) => schema.clone(),
None => return HashSet::new(),
};
let evaluator = ExpressionEvaluator::new(&table_schema);
let mut included: HashSet<String> = HashSet::new();
for (index_name, metadata) in db.get_partial_indexes_for_table(table_name) {
let Some(predicate) = metadata.where_clause.as_deref() else { continue };
match evaluator.eval(predicate, row) {
Ok(v) if is_predicate_truthy(&v) => {
included.insert(index_name);
}
Ok(_) => {}
Err(e) => {
log::warn!(
"Failed to evaluate WHERE predicate for partial index '{}': {:?}; \
treating row as not-in-index",
index_name,
e
);
}
}
}
included
}
pub fn maintain_partial_indexes_for_insert(
db: &mut Database,
table_name: &str,
row: &vibesql_storage::Row,
row_index: usize,
) {
if !db.has_partial_indexes(table_name) {
return;
}
let included = evaluate_partial_index_predicates(db, table_name, row);
if included.is_empty() {
return;
}
db.add_to_partial_indexes_for_insert(table_name, row, row_index, &included);
}
pub fn maintain_partial_indexes_for_update(
db: &mut Database,
table_name: &str,
old_row: &vibesql_storage::Row,
new_row: &vibesql_storage::Row,
row_index: usize,
) {
if !db.has_partial_indexes(table_name) {
return;
}
let old_included = evaluate_partial_index_predicates(db, table_name, old_row);
let new_included = evaluate_partial_index_predicates(db, table_name, new_row);
if old_included.is_empty() && new_included.is_empty() {
return;
}
db.update_partial_indexes_for_update(
table_name,
old_row,
new_row,
row_index,
&old_included,
&new_included,
);
}
pub fn maintain_partial_indexes_for_delete(
db: &mut Database,
table_name: &str,
row: &vibesql_storage::Row,
row_index: usize,
) {
if !db.has_partial_indexes(table_name) {
return;
}
let included = evaluate_partial_index_predicates(db, table_name, row);
if included.is_empty() {
return;
}
db.update_partial_indexes_for_delete_with_values(table_name, &row.values, row_index, &included);
}
pub fn rebuild_partial_indexes_after_compaction(db: &mut Database, table_name: &str) {
if !db.has_partial_indexes(table_name) {
return;
}
let rows: Vec<vibesql_storage::Row> = match db.get_table(table_name) {
Some(table) => table.scan().to_vec(),
None => return,
};
db.clear_partial_index_data(table_name);
for (row_index, row) in rows.iter().enumerate() {
let included = evaluate_partial_index_predicates(db, table_name, row);
if included.is_empty() {
continue;
}
db.add_to_partial_indexes_for_insert(table_name, row, row_index, &included);
}
}
pub fn check_partial_unique_for_insert(
db: &Database,
table_name: &str,
row: &vibesql_storage::Row,
) -> Result<(), crate::errors::ExecutorError> {
if !db.has_partial_indexes(table_name) {
return Ok(());
}
let table_schema = match db.catalog.get_table(table_name) {
Some(schema) => schema.clone(),
None => return Ok(()),
};
let evaluator = ExpressionEvaluator::new(&table_schema);
for (index_name, metadata) in db.get_partial_indexes_for_table(table_name) {
if !metadata.unique {
continue;
}
if metadata.columns.iter().any(|col| col.is_expression()) {
continue;
}
let Some(predicate) = metadata.where_clause.as_deref() else { continue };
match evaluator.eval(predicate, row) {
Ok(v) if is_predicate_truthy(&v) => {}
_ => continue,
}
let key_values: Vec<SqlValue> = metadata
.columns
.iter()
.map(|col| {
let col_name =
col.column_name().expect("Partial-index column should have a column name");
let col_idx =
table_schema.get_column_index(col_name).expect("Index column should exist");
row.values[col_idx].clone()
})
.collect();
if key_values.iter().any(|v| matches!(v, SqlValue::Null)) {
continue;
}
match db.check_partial_unique_conflict(&index_name, &key_values) {
Ok(true) => {
let columns_str = metadata
.columns
.iter()
.map(|col| {
format!("{}.{}", metadata.table_name, col.column_name().unwrap_or(""))
})
.collect::<Vec<_>>()
.join(", ");
return Err(crate::errors::ExecutorError::UnsupportedExpression(format!(
"UNIQUE constraint failed: {}",
columns_str
)));
}
Ok(false) => {}
Err(e) => {
log::warn!(
"Failed to check partial-unique conflict for index '{}': {:?}",
index_name,
e
);
}
}
}
Ok(())
}