use shifty_algebra::render::path_to_string;
use shifty_algebra::{Path, Schema, Selector, Shape, ShapeId};
use spargebra::Query;
use spargebra::algebra::{GraphPattern, PropertyPathExpression};
use spargebra::term::TermPattern;
use std::collections::HashMap;
pub type PathId = String;
#[derive(Debug, Clone, Default)]
pub struct PathDemand {
pub path: PathId,
pub forward_probes: u64,
pub reverse_probes: u64,
pub membership_probes: u64,
pub open_scans: u64,
pub expected_focuses: u64,
}
fn insert_or_update(
demand: &mut HashMap<PathId, PathDemand>,
key: PathId,
update: impl FnOnce(&mut PathDemand),
) {
let d = demand.entry(key.clone()).or_insert_with(|| PathDemand {
path: key,
..Default::default()
});
update(d);
}
pub fn extract_sparql_demand(query: &Query) -> HashMap<PathId, PathDemand> {
let mut demand: HashMap<PathId, PathDemand> = HashMap::new();
let this = spargebra::term::Variable::new("this").unwrap();
let pattern = match query {
Query::Select { pattern, .. } | Query::Ask { pattern, .. } => pattern,
_ => return demand,
};
collect_from_pattern(pattern, &this, &mut demand);
demand
}
pub fn extract_algebra_demand(schema: &Schema) -> HashMap<PathId, PathDemand> {
let mut demand: HashMap<PathId, PathDemand> = HashMap::new();
for st in &schema.statements {
if let Selector::HasPath(path, _) = &st.selector {
let key = path_to_string(path);
insert_or_update(&mut demand, key, |d| d.forward_probes += 1);
}
}
for i in 0..schema.arena.len() {
let id = ShapeId(i as u32);
let path = match schema.arena.get(id) {
Shape::Eq(p, _)
| Shape::Disj(p, _)
| Shape::Lt(p, _)
| Shape::Le(p, _)
| Shape::UniqueLang(p) => Some(p.clone()),
Shape::Count { path, .. } => Some(path.clone()),
_ => None,
};
if let Some(p) = path {
let key = path_to_string(&p);
insert_or_update(&mut demand, key, |d| d.forward_probes += 1);
}
}
demand
}
fn collect_from_pattern(
pattern: &GraphPattern,
this: &spargebra::term::Variable,
demand: &mut HashMap<PathId, PathDemand>,
) {
match pattern {
GraphPattern::Path {
subject,
path,
object,
} => {
if let Some(alg_path) = property_path_to_algebra(path) {
let key = path_to_string(&alg_path);
let s_bound = tp_is_bound(subject, this);
let o_bound = tp_is_bound(object, this);
insert_or_update(demand, key, |d| match (s_bound, o_bound) {
(true, true) => d.membership_probes += 1,
(true, false) => d.forward_probes += 1,
(false, true) => d.reverse_probes += 1,
(false, false) => d.open_scans += 1,
});
}
}
GraphPattern::Bgp { .. } | GraphPattern::Values { .. } => {}
GraphPattern::Join { left, right }
| GraphPattern::Union { left, right }
| GraphPattern::Minus { left, right } => {
collect_from_pattern(left, this, demand);
collect_from_pattern(right, this, demand);
}
GraphPattern::Lateral { left, right } => {
collect_from_pattern(left, this, demand);
collect_from_pattern(right, this, demand);
}
GraphPattern::LeftJoin { left, right, .. } => {
collect_from_pattern(left, this, demand);
collect_from_pattern(right, this, demand);
}
GraphPattern::Filter { inner, .. }
| GraphPattern::Graph { inner, .. }
| GraphPattern::Extend { inner, .. }
| GraphPattern::OrderBy { inner, .. }
| GraphPattern::Project { inner, .. }
| GraphPattern::Distinct { inner }
| GraphPattern::Reduced { inner }
| GraphPattern::Slice { inner, .. }
| GraphPattern::Group { inner, .. }
| GraphPattern::Service { inner, .. } => {
collect_from_pattern(inner, this, demand);
}
}
}
fn tp_is_bound(tp: &TermPattern, this: &spargebra::term::Variable) -> bool {
match tp {
TermPattern::Variable(v) => v == this,
TermPattern::NamedNode(_) | TermPattern::BlankNode(_) | TermPattern::Literal(_) => true,
#[allow(unreachable_patterns)]
_ => false,
}
}
pub fn property_path_to_algebra(path: &PropertyPathExpression) -> Option<Path> {
match path {
PropertyPathExpression::NamedNode(n) => Some(Path::Pred(n.clone())),
PropertyPathExpression::Reverse(p) => {
property_path_to_algebra(p).map(|inner| inner.inverse())
}
PropertyPathExpression::Sequence(a, b) => {
let la = property_path_to_algebra(a)?;
let lb = property_path_to_algebra(b)?;
Some(Path::seq(vec![la, lb]))
}
PropertyPathExpression::Alternative(a, b) => {
let la = property_path_to_algebra(a)?;
let lb = property_path_to_algebra(b)?;
Some(Path::alt(vec![la, lb]))
}
PropertyPathExpression::ZeroOrMore(p) => {
property_path_to_algebra(p).map(|inner| inner.star())
}
PropertyPathExpression::OneOrMore(p) => {
property_path_to_algebra(p).map(|inner| inner.one_or_more())
}
PropertyPathExpression::ZeroOrOne(p) => {
property_path_to_algebra(p).map(|inner| inner.zero_or_one())
}
PropertyPathExpression::NegatedPropertySet(_) => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use spargebra::SparqlParser;
fn parse(q: &str) -> Query {
SparqlParser::new().parse_query(q).unwrap()
}
#[test]
fn path_star_demand() {
let q = parse(
"ASK { ?this <http://www.w3.org/1999/02/22-rdf-syntax-ns#type>/<http://www.w3.org/2000/01/rdf-schema#subClassOf>* <http://ex/C> }",
);
let demand = extract_sparql_demand(&q);
assert!(!demand.is_empty());
let totals: u64 = demand
.values()
.map(|d| d.forward_probes + d.membership_probes)
.sum();
assert!(totals > 0, "expected at least one forward/membership probe");
}
#[test]
fn algebra_demand_picks_up_count_paths() {
use shifty_algebra::{NamedNode, NodeKindSet, ShapeArena, Statement};
let nn = |s: &str| NamedNode::new(s).unwrap();
let mut arena = ShapeArena::new();
let top = arena.insert(Shape::TestKind(NodeKindSet::IRI));
let path = Path::Pred(nn("http://ex/p"));
arena.insert(Shape::Count {
path,
min: Some(1),
max: None,
qualifier: top,
});
let schema = Schema {
arena,
statements: vec![Statement {
selector: Selector::HasOut(nn("http://ex/q")),
shape: top,
}],
rules: Vec::new(),
names: Default::default(),
};
let demand = extract_algebra_demand(&schema);
assert!(
demand.contains_key("<http://ex/p>"),
"should record path <http://ex/p>; got: {demand:?}"
);
}
}