use super::{index_key_values, index_predicate_accepts, PhysicalIndexDefinitions};
use crate::mutation::constraints::{
index_keys::{changes_error, key_values_equal},
ConstraintContext,
};
use std::collections::BTreeSet;
use uqa_core::{DocId, Predicate, Value};
use uqa_sql::{ast::IndexKey, SQLError};
use uqa_storage::{document_store::Document, ValueIndexKey};
impl PhysicalIndexDefinitions {
pub fn validate_key_comparisons(
&self,
context: ConstraintContext<'_>,
table: &str,
document: &Document,
ignored_doc_id: Option<DocId>,
) -> Result<(), SQLError> {
for ((_, physical_key), index) in &self.indexes {
if index.table != table
|| !index.method.eq_ignore_ascii_case("btree")
|| !index
.definition
.key_types
.iter()
.any(uqa_sql::expr::type_comparison_can_fail)
{
continue;
}
let predicate = index.definition.predicate.as_deref();
let expressions = context.index_expressions();
if !index_predicate_accepts(expressions, table, predicate, document)? {
continue;
}
let values = index_key_values(expressions, table, &index.keys, document)?;
let (key, probe) = if index.keys.iter().any(|key| key.column().is_none()) {
(
ValueIndexKey::Index(physical_key.clone()),
Predicate::Equals(Value::Row(values.clone().into())),
)
} else {
let Some(IndexKey::Column(first)) = index.keys.first() else {
continue;
};
let probe = match &values[0] {
Value::Null => Predicate::IsNull,
value => Predicate::Equals(value.clone()),
};
(ValueIndexKey::Column(first.clone()), probe)
};
let candidates: BTreeSet<DocId> = if ignored_doc_id.is_some() {
context
.reads
.live_table_doc_ids(table)?
.into_iter()
.collect()
} else {
match context.indexes.value_index_scan_key(table, &key, &probe)? {
Some(list) => list.entries().iter().map(|entry| entry.doc_id).collect(),
None => context
.reads
.live_table_doc_ids(table)?
.into_iter()
.collect(),
}
};
let changes = context
.reads
.command_overlay_changes(table)?
.unwrap_or_default();
let mut changed = changes.changes().peekable();
let mut candidates = candidates.into_iter().peekable();
loop {
let next_changed = match changed.peek() {
Some(Ok((id, _))) => Some(*id),
Some(Err(_)) => {
let Some(Err(error)) = changed.next() else {
unreachable!("a peeked failure");
};
return Err(changes_error(error));
}
None => None,
};
let id = match (candidates.peek().copied(), next_changed) {
(None, None) => break,
(Some(candidate), Some(changed_id)) if changed_id < candidate => {
changed.next();
changed_id
}
(Some(candidate), next_changed) => {
candidates.next();
if next_changed == Some(candidate) {
changed.next();
}
candidate
}
(None, Some(changed_id)) => {
changed.next();
changed_id
}
};
if ignored_doc_id == Some(id) {
continue;
}
let Some(stored) = context.reads.get_document(table, id)? else {
continue;
};
if index_predicate_accepts(expressions, table, predicate, &stored)? {
key_values_equal(
&index_key_values(expressions, table, &index.keys, &stored)?,
&values,
)?;
}
}
}
Ok(())
}
}