use std::ops::Bound;
use crate::index::error::IndexError;
use crate::index::spec::{ElementType, IndexPredicate, IndexSpec};
use crate::value::Value;
#[derive(Clone, Debug)]
pub struct IndexFilter {
pub element_type: ElementType,
pub label: Option<String>,
pub property: String,
pub predicate: IndexPredicate,
}
impl IndexFilter {
pub fn eq(
element_type: ElementType,
label: Option<String>,
property: impl Into<String>,
value: Value,
) -> Self {
Self {
element_type,
label,
property: property.into(),
predicate: IndexPredicate::Eq(value),
}
}
pub fn gte(
element_type: ElementType,
label: Option<String>,
property: impl Into<String>,
value: Value,
) -> Self {
Self {
element_type,
label,
property: property.into(),
predicate: IndexPredicate::Gte(value),
}
}
pub fn gt(
element_type: ElementType,
label: Option<String>,
property: impl Into<String>,
value: Value,
) -> Self {
Self {
element_type,
label,
property: property.into(),
predicate: IndexPredicate::Gt(value),
}
}
pub fn lte(
element_type: ElementType,
label: Option<String>,
property: impl Into<String>,
value: Value,
) -> Self {
Self {
element_type,
label,
property: property.into(),
predicate: IndexPredicate::Lte(value),
}
}
pub fn lt(
element_type: ElementType,
label: Option<String>,
property: impl Into<String>,
value: Value,
) -> Self {
Self {
element_type,
label,
property: property.into(),
predicate: IndexPredicate::Lt(value),
}
}
}
#[derive(Clone, Debug, Default)]
pub struct IndexStatistics {
pub cardinality: u64,
pub total_elements: u64,
pub min_value: Option<Value>,
pub max_value: Option<Value>,
pub last_updated: u64,
}
impl IndexStatistics {
pub fn new() -> Self {
Self::default()
}
pub fn estimate_eq_selectivity(&self) -> f64 {
if self.cardinality == 0 || self.total_elements == 0 {
1.0
} else {
1.0 / self.cardinality as f64
}
}
pub fn estimate_range_selectivity(&self, _start: Option<&Value>, _end: Option<&Value>) -> f64 {
0.1
}
}
pub trait PropertyIndex: Send + Sync {
fn spec(&self) -> &IndexSpec;
fn covers(&self, filter: &IndexFilter) -> bool;
fn lookup_eq(&self, value: &Value) -> Box<dyn Iterator<Item = u64> + '_>;
fn lookup_range(
&self,
start: Bound<&Value>,
end: Bound<&Value>,
) -> Box<dyn Iterator<Item = u64> + '_>;
fn insert(&mut self, value: Value, element_id: u64) -> Result<(), IndexError>;
fn remove(&mut self, value: &Value, element_id: u64) -> Result<(), IndexError>;
fn update(
&mut self,
old_value: &Value,
new_value: Value,
element_id: u64,
) -> Result<(), IndexError> {
self.remove(old_value, element_id)?;
self.insert(new_value, element_id)
}
fn statistics(&self) -> &IndexStatistics;
fn refresh_statistics(&mut self);
fn clear(&mut self);
fn is_empty(&self) -> bool {
self.statistics().total_elements == 0
}
fn len(&self) -> u64 {
self.statistics().total_elements
}
}
pub fn index_covers_filter(spec: &IndexSpec, filter: &IndexFilter, supports_range: bool) -> bool {
if filter.element_type != spec.element_type {
return false;
}
if filter.property != spec.property {
return false;
}
match (&spec.label, &filter.label) {
(Some(idx_label), Some(filter_label)) => {
if idx_label != filter_label {
return false;
}
}
(Some(_), None) => {
return false;
}
(None, _) => {
}
}
match &filter.predicate {
IndexPredicate::Eq(_) => true,
IndexPredicate::Neq(_) => false, IndexPredicate::Lt(_)
| IndexPredicate::Lte(_)
| IndexPredicate::Gt(_)
| IndexPredicate::Gte(_)
| IndexPredicate::Between { .. } => supports_range,
IndexPredicate::Within(values) => {
!values.is_empty()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::index::spec::IndexBuilder;
#[test]
fn index_filter_eq() {
let filter = IndexFilter::eq(
ElementType::Vertex,
Some("person".to_string()),
"age",
Value::Int(30),
);
assert_eq!(filter.element_type, ElementType::Vertex);
assert_eq!(filter.label, Some("person".to_string()));
assert_eq!(filter.property, "age");
assert!(matches!(
filter.predicate,
IndexPredicate::Eq(Value::Int(30))
));
}
#[test]
fn index_filter_gte() {
let filter = IndexFilter::gte(
ElementType::Vertex,
Some("person".to_string()),
"age",
Value::Int(18),
);
assert!(matches!(
filter.predicate,
IndexPredicate::Gte(Value::Int(18))
));
}
#[test]
fn index_statistics_default() {
let stats = IndexStatistics::default();
assert_eq!(stats.cardinality, 0);
assert_eq!(stats.total_elements, 0);
assert!(stats.min_value.is_none());
assert!(stats.max_value.is_none());
}
#[test]
fn index_statistics_eq_selectivity() {
let stats = IndexStatistics {
cardinality: 100,
total_elements: 1000,
..Default::default()
};
let selectivity = stats.estimate_eq_selectivity();
assert!((selectivity - 0.01).abs() < 0.001);
}
#[test]
fn index_statistics_eq_selectivity_empty() {
let stats = IndexStatistics::default();
assert_eq!(stats.estimate_eq_selectivity(), 1.0);
}
#[test]
fn index_covers_filter_basic() {
let spec = IndexBuilder::vertex()
.label("person")
.property("age")
.build()
.unwrap();
let filter = IndexFilter::eq(
ElementType::Vertex,
Some("person".to_string()),
"age",
Value::Int(30),
);
assert!(index_covers_filter(&spec, &filter, true));
}
#[test]
fn index_covers_filter_wrong_element_type() {
let spec = IndexBuilder::vertex()
.label("person")
.property("age")
.build()
.unwrap();
let filter = IndexFilter::eq(
ElementType::Edge, Some("person".to_string()),
"age",
Value::Int(30),
);
assert!(!index_covers_filter(&spec, &filter, true));
}
#[test]
fn index_covers_filter_wrong_property() {
let spec = IndexBuilder::vertex()
.label("person")
.property("age")
.build()
.unwrap();
let filter = IndexFilter::eq(
ElementType::Vertex,
Some("person".to_string()),
"name", Value::String("Alice".to_string()),
);
assert!(!index_covers_filter(&spec, &filter, true));
}
#[test]
fn index_covers_filter_wrong_label() {
let spec = IndexBuilder::vertex()
.label("person")
.property("age")
.build()
.unwrap();
let filter = IndexFilter::eq(
ElementType::Vertex,
Some("user".to_string()), "age",
Value::Int(30),
);
assert!(!index_covers_filter(&spec, &filter, true));
}
#[test]
fn index_covers_filter_no_label_index() {
let spec = IndexBuilder::vertex()
.property("created_at")
.build()
.unwrap();
let filter = IndexFilter::eq(
ElementType::Vertex,
Some("person".to_string()),
"created_at",
Value::Int(12345),
);
assert!(index_covers_filter(&spec, &filter, true));
}
#[test]
fn index_covers_filter_label_index_no_filter_label() {
let spec = IndexBuilder::vertex()
.label("person")
.property("age")
.build()
.unwrap();
let filter = IndexFilter::eq(
ElementType::Vertex,
None, "age",
Value::Int(30),
);
assert!(!index_covers_filter(&spec, &filter, true));
}
#[test]
fn index_covers_filter_range_with_support() {
let spec = IndexBuilder::vertex()
.label("person")
.property("age")
.build()
.unwrap();
let filter = IndexFilter::gte(
ElementType::Vertex,
Some("person".to_string()),
"age",
Value::Int(18),
);
assert!(index_covers_filter(&spec, &filter, true));
}
#[test]
fn index_covers_filter_range_without_support() {
let spec = IndexBuilder::vertex()
.label("person")
.property("age")
.unique()
.build()
.unwrap();
let filter = IndexFilter::gte(
ElementType::Vertex,
Some("person".to_string()),
"age",
Value::Int(18),
);
assert!(!index_covers_filter(&spec, &filter, false));
}
#[test]
fn index_covers_filter_neq_not_supported() {
let spec = IndexBuilder::vertex()
.label("person")
.property("age")
.build()
.unwrap();
let filter = IndexFilter {
element_type: ElementType::Vertex,
label: Some("person".to_string()),
property: "age".to_string(),
predicate: IndexPredicate::Neq(Value::Int(30)),
};
assert!(!index_covers_filter(&spec, &filter, true));
}
#[test]
fn index_covers_filter_within() {
let spec = IndexBuilder::vertex()
.label("person")
.property("status")
.build()
.unwrap();
let filter = IndexFilter {
element_type: ElementType::Vertex,
label: Some("person".to_string()),
property: "status".to_string(),
predicate: IndexPredicate::Within(vec![
Value::String("active".to_string()),
Value::String("pending".to_string()),
]),
};
assert!(index_covers_filter(&spec, &filter, true));
}
#[test]
fn index_covers_filter_within_empty() {
let spec = IndexBuilder::vertex()
.label("person")
.property("status")
.build()
.unwrap();
let filter = IndexFilter {
element_type: ElementType::Vertex,
label: Some("person".to_string()),
property: "status".to_string(),
predicate: IndexPredicate::Within(vec![]),
};
assert!(!index_covers_filter(&spec, &filter, true));
}
}