use crate::graph::schema::ElementKind;
use crate::graph::{GraphStorage, PropertyIndexInfo, PropertyIndexRange, PropertyIndexTransform};
use crate::traversal::optimizer::OptimizerContext;
use crate::traversal::optimizer::plan::Plan;
use crate::traversal::optimizer::rule::OptimizationRule;
use crate::traversal::step::{GraphStep, IndexAccess};
const PURE_FILTERS: [&str; 7] = [
"has",
"has_p",
"has_label",
"has_id",
"has_not",
"has_key",
"has_property",
];
#[derive(Default)]
pub(crate) struct PropertyIndexRule;
impl PropertyIndexRule {
pub fn new() -> Self {
Self
}
}
fn estimate(info: &PropertyIndexInfo, ranges: &[PropertyIndexRange]) -> u64 {
let entries = info.stats.entries;
let per_value = entries / info.stats.distinct_values.max(1);
ranges
.iter()
.map(|range| match (&range.lower, &range.upper) {
(std::ops::Bound::Included(a), std::ops::Bound::Included(b)) if a == b => {
per_value.max(1)
}
_ => (entries / 2).max(1),
})
.fold(0u64, u64::saturating_add)
}
impl<'g, S: GraphStorage + 'g> OptimizationRule<'g, S> for PropertyIndexRule {
fn name(&self) -> &'static str {
"property_index"
}
fn apply(&self, _plan: &mut Plan<'g, S>) -> bool {
false
}
fn apply_in(&self, plan: &mut Plan<'g, S>, context: &OptimizerContext<'_>) -> bool {
if plan.is_empty() {
return false;
}
let (element, labels, has_access) = match &plan[0] {
GraphStep::Init(name, init) if *name == "v" || *name == "e" => {
let element = if *name == "v" {
ElementKind::Vertex
} else {
ElementKind::Edge
};
let labels = match init.labels() {
Some(labels)
if !labels.is_empty()
&& init.ids().is_none()
&& !init.is_count_only()
&& !init.is_group_count_by_label() =>
{
Some(labels.to_vec())
}
_ => None,
};
(element, labels, init.index_access().is_some())
}
_ => return false,
};
let clear = |plan: &mut Plan<'g, S>| {
if has_access && let GraphStep::Init(_, init) = plan.step_mut(0) {
init.set_index_access(None);
return true;
}
false
};
let Some(labels) = labels else {
return clear(plan);
};
if context.indexes.is_empty() {
return clear(plan);
}
let cost = |key: &str,
transform: Option<&PropertyIndexTransform>,
ranges: &[PropertyIndexRange]|
-> Option<u64> {
labels.iter().try_fold(0u64, |total, label| {
let info = context.indexes.iter().find(|info| {
info.element == element
&& info.label == *label
&& info.key == key
&& info.transform.as_ref() == transform
&& (transform.is_none() || info.stats.skipped == 0)
})?;
Some(total.saturating_add(estimate(info, ranges)))
})
};
let mut best: Option<(u64, IndexAccess)> = None;
let mut consider = |key: &str,
transform: Option<PropertyIndexTransform>,
ranges: Vec<PropertyIndexRange>| {
if let Some(cost) = cost(key, transform.as_ref(), &ranges)
&& best.as_ref().is_none_or(|(best, _)| cost < *best)
{
best = Some((
cost,
IndexAccess {
key: key.to_string(),
transform,
ranges,
},
));
}
};
if let GraphStep::Init(_, init) = &plan[0] {
for filter in init.property_filters() {
if crate::graph::property_index::is_indexable(&filter.value) {
consider(
&filter.key,
None,
vec![PropertyIndexRange::equal(filter.value.clone())],
);
}
}
}
for index in 1..plan.len() {
match &plan[index] {
GraphStep::Filter(name, filter)
if PURE_FILTERS.contains(name)
|| (*name == "has_traversal" && filter.index_condition().is_some()) =>
{
if let Some(condition) = filter.index_condition()
&& (context.root || !condition.reads_path_labels)
{
consider(condition.key, condition.transform, condition.ranges);
}
}
_ => break,
}
}
match best {
Some((_, access)) => {
if let GraphStep::Init(_, init) = plan.step_mut(0) {
init.set_index_access(Some(access))
} else {
false
}
}
None => clear(plan),
}
}
}