use crate::query::datalog::types::{AttributeSpec, EdnValue, Pattern};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum IndexHint {
Eavt,
Aevt,
Avet,
Vaet,
}
fn is_variable(v: &EdnValue) -> bool {
v.is_variable()
}
fn is_entity_literal(v: &EdnValue) -> bool {
matches!(v, EdnValue::Uuid(_))
}
fn attr_is_index_bound(a: &AttributeSpec) -> bool {
match a {
AttributeSpec::Real(edn) => !is_variable(edn),
AttributeSpec::Pseudo(_) => false,
}
}
#[cfg(not(feature = "wasm"))]
fn selectivity_score(p: &Pattern) -> u8 {
let e = !is_variable(&p.entity);
let a = attr_is_index_bound(&p.attribute);
let v = !is_variable(&p.value);
e as u8 + a as u8 + v as u8
}
pub fn select_index(p: &Pattern) -> IndexHint {
let e_bound = !is_variable(&p.entity);
let a_bound = attr_is_index_bound(&p.attribute);
let v_bound = !is_variable(&p.value);
if e_bound {
return IndexHint::Eavt;
}
if a_bound && v_bound {
return IndexHint::Avet;
}
if a_bound {
return IndexHint::Aevt;
}
if v_bound && is_entity_literal(&p.value) {
return IndexHint::Vaet;
}
IndexHint::Eavt
}
pub fn plan(
patterns: Vec<Pattern>,
_indexes: &crate::storage::index::Indexes,
) -> Vec<(Pattern, IndexHint)> {
let planned: Vec<(Pattern, IndexHint)> = patterns
.into_iter()
.map(|p| {
let h = select_index(&p);
(p, h)
})
.collect();
#[cfg(not(feature = "wasm"))]
let planned = {
let mut v = planned;
v.sort_by_key(|(p, _)| std::cmp::Reverse(selectivity_score(p)));
v
};
planned
}
#[cfg(test)]
mod tests {
use super::*;
use crate::query::datalog::types::{EdnValue, Pattern};
use crate::storage::index::Indexes;
use uuid::Uuid;
fn make_pattern(entity: EdnValue, attribute: EdnValue, value: EdnValue) -> Pattern {
Pattern::new(entity, attribute, value)
}
fn var(s: &str) -> EdnValue {
EdnValue::Symbol(format!("?{s}"))
}
fn kw(s: &str) -> EdnValue {
EdnValue::Keyword(s.to_string())
}
fn str_val(s: &str) -> EdnValue {
EdnValue::String(s.to_string())
}
fn entity_lit() -> EdnValue {
EdnValue::Uuid(Uuid::new_v4())
}
#[test]
fn test_entity_bound_selects_eavt() {
let p = make_pattern(entity_lit(), var("a"), var("v"));
assert_eq!(select_index(&p), IndexHint::Eavt);
}
#[test]
fn test_entity_and_attr_bound_selects_eavt() {
let p = make_pattern(entity_lit(), kw(":name"), var("v"));
assert_eq!(select_index(&p), IndexHint::Eavt);
}
#[test]
fn test_attr_and_value_bound_selects_avet() {
let p = make_pattern(var("e"), kw(":name"), str_val("Alice"));
assert_eq!(select_index(&p), IndexHint::Avet);
}
#[test]
fn test_attr_and_ref_bound_selects_avet() {
let p = make_pattern(var("e"), kw(":friend"), entity_lit());
assert_eq!(select_index(&p), IndexHint::Avet);
}
#[test]
fn test_attr_only_selects_aevt() {
let p = make_pattern(var("e"), kw(":name"), var("v"));
assert_eq!(select_index(&p), IndexHint::Aevt);
}
#[test]
fn test_ref_only_selects_vaet() {
let p = make_pattern(var("e"), var("a"), entity_lit());
assert_eq!(select_index(&p), IndexHint::Vaet);
}
#[test]
fn test_nothing_bound_selects_eavt_full_scan() {
let p = make_pattern(var("e"), var("a"), var("v"));
assert_eq!(select_index(&p), IndexHint::Eavt);
}
#[cfg(not(feature = "wasm"))]
#[test]
fn test_join_ordering_moves_selective_pattern_first() {
let p1 = make_pattern(var("e"), kw(":age"), var("a")); let p2 = make_pattern(entity_lit(), kw(":name"), var("v")); let p1_attr = p1.attribute.clone();
let p2_attr = p2.attribute.clone();
let planned = plan(vec![p1, p2], &Indexes::new());
assert_ne!(
planned[0].0.attribute, p1_attr,
"Lower-selectivity pattern must not be first"
);
assert_eq!(
planned[0].0.attribute, p2_attr,
"Higher-selectivity pattern must be first"
);
}
}