use super::*;
use crate::bgp::{PatternTerm, TriplePattern};
use crate::{Dictionary, Rete};
use std::collections::{HashMap, HashSet};
const RDF_TYPE: &str = "<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>";
const SUBCLASS_OF: &str = "<http://www.w3.org/2000/01/rdf-schema#subClassOf>";
const SUBPROPERTY_OF: &str = "<http://www.w3.org/2000/01/rdf-schema#subPropertyOf>";
const RDFS_DOMAIN: &str = "<http://www.w3.org/2000/01/rdf-schema#domain>";
const RDFS_RANGE: &str = "<http://www.w3.org/2000/01/rdf-schema#range>";
const OWL_INVERSE_OF: &str = "<http://www.w3.org/2002/07/owl#inverseOf>";
const OWL_ON_PROPERTY: &str = "<http://www.w3.org/2002/07/owl#onProperty>";
const OWL_SOME_VALUES_FROM: &str = "<http://www.w3.org/2002/07/owl#someValuesFrom>";
struct QlTbox {
sub_classes: HashMap<String, Vec<String>>,
sub_props: HashMap<String, Vec<String>>,
domain_of: HashMap<String, Vec<String>>,
range_of: HashMap<String, Vec<String>>,
inverses: HashMap<String, Vec<String>>,
has_restrictions: bool,
}
impl QlTbox {
fn read(rete: &Rete) -> Self {
QlTbox {
sub_classes: by_object(rete, SUBCLASS_OF),
sub_props: by_object(rete, SUBPROPERTY_OF),
domain_of: by_object(rete, RDFS_DOMAIN),
range_of: by_object(rete, RDFS_RANGE),
inverses: inverse_pairs(rete),
has_restrictions: has_predicate(rete, OWL_SOME_VALUES_FROM),
}
}
fn is_empty(&self) -> bool {
self.sub_classes.is_empty()
&& self.sub_props.is_empty()
&& self.domain_of.is_empty()
&& self.range_of.is_empty()
&& self.inverses.is_empty()
&& !self.has_restrictions
}
fn classes_below(&self, c: &str) -> Vec<String> {
closure_below(&self.sub_classes, c)
}
fn props_below(&self, p: &str) -> Vec<String> {
closure_below(&self.sub_props, p)
}
fn typing_props(&self, c: &str, domain: bool) -> Vec<String> {
let decl = if domain {
&self.domain_of
} else {
&self.range_of
};
if decl.is_empty() {
return Vec::new();
}
let mut out: Vec<String> = Vec::new();
for d in self.classes_below(c) {
for p in decl.get(&d).into_iter().flatten() {
out.extend(self.props_below(p));
}
}
out.sort();
out.dedup();
out
}
}
fn closure_below(below: &HashMap<String, Vec<String>>, top: &str) -> Vec<String> {
let mut seen: HashSet<String> = HashSet::new();
let mut out = vec![top.to_string()];
seen.insert(top.to_string());
let mut i = 0;
while i < out.len() {
if let Some(subs) = below.get(&out[i]) {
for s in subs {
if seen.insert(s.clone()) {
out.push(s.clone());
}
}
}
i += 1;
}
out
}
fn by_object(rete: &Rete, pred: &str) -> HashMap<String, Vec<String>> {
let q = format!("SELECT ?s ?o WHERE {{ ?s {pred} ?o }}");
let mut m: HashMap<String, Vec<String>> = HashMap::new();
if let Ok((_, sols)) = super::eval_sparql(rete, &q) {
for b in sols {
if let (Some(s), Some(o)) = (b.get("s"), b.get("o")) {
m.entry(o.clone()).or_default().push(s.clone());
}
}
}
for v in m.values_mut() {
v.sort();
v.dedup();
}
m
}
fn has_predicate(rete: &Rete, pred: &str) -> bool {
let q = format!("SELECT ?s WHERE {{ ?s {pred} ?o }} LIMIT 1");
matches!(super::eval_sparql(rete, &q), Ok((_, s)) if !s.is_empty())
}
fn inverse_pairs(rete: &Rete) -> HashMap<String, Vec<String>> {
let mut m: HashMap<String, Vec<String>> = HashMap::new();
let q = format!("SELECT ?s ?o WHERE {{ ?s {OWL_INVERSE_OF} ?o }}");
if let Ok((_, sols)) = super::eval_sparql(rete, &q) {
for b in sols {
if let (Some(s), Some(o)) = (b.get("s"), b.get("o")) {
m.entry(s.clone()).or_default().push(o.clone());
m.entry(o.clone()).or_default().push(s.clone());
}
}
}
for v in m.values_mut() {
v.sort();
v.dedup();
}
m
}
fn plan_var_occurrences(plan: &Plan, c: &mut HashMap<String, usize>) {
let term = |t: &PatternTerm, c: &mut HashMap<String, usize>| {
if let PatternTerm::Var(v) = t {
*c.entry(v.clone()).or_insert(0) += 1;
}
};
match plan {
Plan::Bgp(pats) => {
for tp in pats {
term(&tp.s, c);
term(&tp.p, c);
term(&tp.o, c);
}
}
Plan::Path(s, _, o) => {
term(s, c);
term(o, c);
}
Plan::Join(a, b) | Plan::Union(a, b) | Plan::Minus(a, b) => {
plan_var_occurrences(a, c);
plan_var_occurrences(b, c);
}
Plan::LeftJoin(a, b, cond) => {
plan_var_occurrences(a, c);
plan_var_occurrences(b, c);
if let Some(e) = cond {
fexpr_var_occurrences(e, c);
}
}
Plan::Filter(e, p) => {
fexpr_var_occurrences(e, c);
plan_var_occurrences(p, c);
}
Plan::Extend(v, e, p) => {
*c.entry(v.clone()).or_insert(0) += 1;
fexpr_var_occurrences(e, c);
plan_var_occurrences(p, c);
}
Plan::Graph(target, p) => {
if let GraphTarget::Var(v) = target {
*c.entry(v.clone()).or_insert(0) += 1;
}
plan_var_occurrences(p, c);
}
Plan::Values(vars, _) => {
for v in vars {
*c.entry(v.clone()).or_insert(0) += 1;
}
}
Plan::Subquery(sel) => {
for v in &sel.project {
*c.entry(v.clone()).or_insert(0) += 1;
}
}
Plan::Service { vars, .. } => {
for v in vars {
*c.entry(v.clone()).or_insert(0) += 1;
}
}
Plan::DistinctUnion { sources, .. } => {
for tp in sources {
term(&tp.s, c);
term(&tp.p, c);
term(&tp.o, c);
}
}
}
}
fn fexpr_var_occurrences(e: &FExpr, c: &mut HashMap<String, usize>) {
match e {
FExpr::Var(v) | FExpr::Bound(v) => {
*c.entry(v.clone()).or_insert(0) += 1;
}
FExpr::Const(_) => {}
FExpr::Arith(_, a, b)
| FExpr::SameTerm(a, b)
| FExpr::Compare(_, a, b)
| FExpr::And(a, b)
| FExpr::Or(a, b) => {
fexpr_var_occurrences(a, c);
fexpr_var_occurrences(b, c);
}
FExpr::Not(a) => fexpr_var_occurrences(a, c),
FExpr::Func(_, xs) | FExpr::Coalesce(xs) => {
xs.iter().for_each(|x| fexpr_var_occurrences(x, c))
}
FExpr::If(a, b, d) => {
fexpr_var_occurrences(a, c);
fexpr_var_occurrences(b, c);
fexpr_var_occurrences(d, c);
}
FExpr::In(a, xs) => {
fexpr_var_occurrences(a, c);
xs.iter().for_each(|x| fexpr_var_occurrences(x, c));
}
FExpr::Exists(p) => plan_var_occurrences(p, c),
}
}
pub(crate) fn reason_rewrite(plan: Plan, rete: &Rete, projected: &[String]) -> Plan {
let tbox = QlTbox::read(rete);
if tbox.is_empty() {
return plan; }
let exq: HashSet<String> = if tbox.has_restrictions && !projected.is_empty() {
let mut counts = HashMap::new();
plan_var_occurrences(&plan, &mut counts);
let proj: HashSet<&String> = projected.iter().collect();
counts
.into_iter()
.filter(|(v, n)| *n == 1 && !proj.contains(v))
.map(|(v, _)| v)
.collect()
} else {
HashSet::new()
};
let mut rw = Rewriter {
tbox: &tbox,
dict: rete.dictionary(),
exq: &exq,
counter: 0,
};
rw.plan(plan)
}
struct Rewriter<'a> {
tbox: &'a QlTbox,
dict: &'a Dictionary,
exq: &'a HashSet<String>,
counter: usize,
}
impl Rewriter<'_> {
fn plan(&mut self, plan: Plan) -> Plan {
match plan {
Plan::Bgp(patterns) => self.bgp(patterns),
Plan::Join(a, b) => Plan::Join(self.boxed(a), self.boxed(b)),
Plan::Union(a, b) => Plan::Union(self.boxed(a), self.boxed(b)),
Plan::LeftJoin(a, b, cond) => Plan::LeftJoin(self.boxed(a), self.boxed(b), cond),
Plan::Filter(f, p) => Plan::Filter(f, self.boxed(p)),
Plan::Extend(v, e, p) => Plan::Extend(v, e, self.boxed(p)),
Plan::Minus(a, b) => Plan::Minus(self.boxed(a), self.boxed(b)),
Plan::Graph(t, p) => Plan::Graph(t, self.boxed(p)),
Plan::Subquery(mut sel) => {
sel.plan = self.plan(sel.plan);
Plan::Subquery(sel)
}
other => other,
}
}
fn boxed(&mut self, mut p: Box<Plan>) -> Box<Plan> {
let inner = std::mem::replace(&mut *p, Plan::Bgp(Vec::new()));
*p = self.plan(inner);
p
}
fn fresh(&mut self, tag: &str) -> String {
self.counter += 1;
format!("__ql{tag}{}", self.counter)
}
fn uses_class(&self, c: &str) -> bool {
self.dict.object_id(c).is_some()
}
fn uses_predicate(&self, p: &str) -> bool {
self.dict.predicate_id(p).is_some()
}
fn bgp(&mut self, patterns: Vec<TriplePattern>) -> Plan {
let mut residual: Vec<TriplePattern> = Vec::new();
let mut extra: Vec<Plan> = Vec::new();
for tp in patterns {
let rewritten = if let Some(class) = type_atom_class(&tp) {
let class = class.to_string();
self.class_atom(&tp.s, &class)
} else if let Some(pred) = role_atom_pred(&tp) {
let pred = pred.to_string();
self.role_atom(&tp.s, &pred, &tp.o)
} else {
None
};
match rewritten {
Some(plan) => extra.push(plan),
None => residual.push(tp),
}
}
if extra.is_empty() {
return Plan::Bgp(residual);
}
let mut iter = extra.into_iter();
let mut plan = if residual.is_empty() {
iter.next().expect("extra is non-empty")
} else {
Plan::Bgp(residual)
};
for p in iter {
plan = join(plan, p);
}
plan
}
fn class_atom(&mut self, subject: &PatternTerm, class: &str) -> Option<Plan> {
let mut sources: Vec<TriplePattern> = Vec::new();
for d in self.tbox.classes_below(class) {
if self.uses_class(&d) {
sources.push(atom(subject.clone(), RDF_TYPE, PatternTerm::Const(d)));
}
}
for p in self.tbox.typing_props(class, true) {
if self.uses_predicate(&p) {
let anon = PatternTerm::Var(self.fresh("dra"));
sources.push(atom(subject.clone(), &p, anon));
}
}
for p in self.tbox.typing_props(class, false) {
if self.uses_predicate(&p) {
let anon = PatternTerm::Var(self.fresh("dra"));
sources.push(atom(anon, &p, subject.clone()));
}
}
let only_itself = match sources.as_slice() {
[] => true,
[one] => type_atom_class(one) == Some(class),
_ => false,
};
if only_itself {
return None;
}
Some(Plan::DistinctUnion {
vars: term_vars(&[subject]),
sources,
})
}
fn role_atom(&mut self, s: &PatternTerm, pred: &str, o: &PatternTerm) -> Option<Plan> {
let mut sources: Vec<TriplePattern> = Vec::new();
for q in self.tbox.props_below(pred) {
if self.uses_predicate(&q) {
sources.push(atom(s.clone(), &q, o.clone()));
}
}
if let Some(invs) = self.tbox.inverses.get(pred) {
for inv in invs {
for q in self.tbox.props_below(inv) {
if self.uses_predicate(&q) {
sources.push(atom(o.clone(), &q, s.clone()));
}
}
}
}
let has_r = self.tbox.has_restrictions;
let forward_ex = has_r && is_existential(o, self.exq);
let inverse_ex = has_r
&& is_existential(s, self.exq)
&& self.tbox.inverses.get(pred).is_some_and(|v| !v.is_empty());
let only_itself = match sources.as_slice() {
[] => true,
[one] => role_atom_pred(one) == Some(pred) && one.s == *s,
_ => false,
};
if only_itself && !forward_ex && !inverse_ex {
return None;
}
let mut plan = if sources.is_empty() {
None
} else {
Some(Plan::DistinctUnion {
vars: term_vars(&[s, o]),
sources,
})
};
let add = |plan: &mut Option<Plan>, branch: Plan| {
*plan = Some(match plan.take() {
Some(p) => union(p, branch),
None => branch,
});
};
if forward_ex {
let branch = existential_branch(s.clone(), pred, &mut self.counter);
add(&mut plan, distinct_on(branch, s));
}
if inverse_ex {
let invs = self.tbox.inverses.get(pred).cloned().unwrap_or_default();
for q in invs {
let branch = existential_branch(o.clone(), &q, &mut self.counter);
add(&mut plan, distinct_on(branch, o));
}
}
plan
}
}
fn term_vars(terms: &[&PatternTerm]) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
for t in terms {
if let PatternTerm::Var(v) = t {
if !out.contains(v) {
out.push(v.clone());
}
}
}
out
}
fn distinct_on(plan: Plan, subject: &PatternTerm) -> Plan {
let mut sel = Select::default();
match subject {
PatternTerm::Var(v) => {
sel.project = vec![v.clone()];
sel.distinct = true;
}
PatternTerm::Const(_) => sel.limit = Some(1),
}
sel.plan = plan;
Plan::Subquery(Box::new(sel))
}
fn type_atom_class(tp: &TriplePattern) -> Option<&str> {
match (&tp.p, &tp.o) {
(PatternTerm::Const(p), PatternTerm::Const(c)) if p == RDF_TYPE => Some(c.as_str()),
_ => None,
}
}
fn role_atom_pred(tp: &TriplePattern) -> Option<&str> {
match &tp.p {
PatternTerm::Const(p) if p != RDF_TYPE => Some(p.as_str()),
_ => None,
}
}
fn star_path(subject: PatternTerm, pred: &str, target: PatternTerm) -> Plan {
Plan::Path(
subject,
PathAst::Rep(
Box::new(PathAst::Pred(pred.to_string(), false)),
Rep::ZeroOrMore,
),
target,
)
}
fn fresh(counter: &mut usize, tag: &str) -> String {
*counter += 1;
format!("__ql{tag}{}", counter)
}
fn join(a: Plan, b: Plan) -> Plan {
Plan::Join(Box::new(a), Box::new(b))
}
fn union(a: Plan, b: Plan) -> Plan {
Plan::Union(Box::new(a), Box::new(b))
}
fn atom(s: PatternTerm, p: &str, o: PatternTerm) -> TriplePattern {
TriplePattern {
s,
p: PatternTerm::Const(p.to_string()),
o,
}
}
fn existential_branch(subject: PatternTerm, pred: &str, counter: &mut usize) -> Plan {
let (r, b, a, ax) = (
fresh(counter, "er"),
fresh(counter, "eb"),
fresh(counter, "ea"),
fresh(counter, "ex"),
);
let bgp = Plan::Bgp(vec![
atom(
PatternTerm::Var(r.clone()),
OWL_ON_PROPERTY,
PatternTerm::Const(pred.to_string()),
),
atom(
PatternTerm::Var(r.clone()),
OWL_SOME_VALUES_FROM,
PatternTerm::Var(b),
),
atom(
PatternTerm::Var(a.clone()),
SUBCLASS_OF,
PatternTerm::Var(r),
),
atom(subject, RDF_TYPE, PatternTerm::Var(ax.clone())),
]);
join(
bgp,
star_path(PatternTerm::Var(ax), SUBCLASS_OF, PatternTerm::Var(a)),
)
}
fn is_existential(t: &PatternTerm, exq: &HashSet<String>) -> bool {
matches!(t, PatternTerm::Var(v) if exq.contains(v))
}