1use crate::encoding::{named_node_id, DEFAULT_GRAPH_ID, PAYLOAD_BITS};
12use crate::sql::{union_all, Statement};
13use oxrdf::vocab::{rdf, rdfs};
14use oxrdf::{QuadRef, Term, TermRef};
15use spargebra::term::{NamedNodePattern, TermPattern};
16use spargebra::{GraphUpdateOperation, Update};
17use std::collections::BTreeSet;
18
19#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
21#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
22#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
23pub enum Reasoning {
24 #[default]
26 None,
27 Rdfs,
29 OwlQl,
32}
33
34const OWL: &str = "http://www.w3.org/2002/07/owl#";
35
36fn owl(local: &str) -> i64 {
37 named_node_id(&format!("{OWL}{local}"))
38}
39
40#[derive(Debug, Clone, Copy)]
42struct Vocab {
43 ty: i64,
44 sco: i64,
45 spo: i64,
46 dom: i64,
47 rng: i64,
48 eqc: i64,
49 eqp: i64,
50 inv: i64,
51 sym: i64,
52 trans: i64,
53 same: i64,
54 func: i64,
55 ifunc: i64,
56 has_value: i64,
57 on_property: i64,
58 some_values: i64,
59 all_values: i64,
60 intersection: i64,
61 union: i64,
62 chain: i64,
63 first: i64,
64 rest: i64,
65 nil: i64,
66 thing: i64,
67}
68
69fn vocab() -> Vocab {
70 Vocab {
71 ty: named_node_id(rdf::TYPE.as_str()),
72 sco: named_node_id(rdfs::SUB_CLASS_OF.as_str()),
73 spo: named_node_id(rdfs::SUB_PROPERTY_OF.as_str()),
74 dom: named_node_id(rdfs::DOMAIN.as_str()),
75 rng: named_node_id(rdfs::RANGE.as_str()),
76 eqc: owl("equivalentClass"),
77 eqp: owl("equivalentProperty"),
78 inv: owl("inverseOf"),
79 sym: owl("SymmetricProperty"),
80 trans: owl("TransitiveProperty"),
81 same: owl("sameAs"),
82 func: owl("FunctionalProperty"),
83 ifunc: owl("InverseFunctionalProperty"),
84 has_value: owl("hasValue"),
85 on_property: owl("onProperty"),
86 some_values: owl("someValuesFrom"),
87 all_values: owl("allValuesFrom"),
88 intersection: owl("intersectionOf"),
89 union: owl("unionOf"),
90 chain: owl("propertyChainAxiom"),
91 first: named_node_id(rdf::FIRST.as_str()),
92 rest: named_node_id(rdf::REST.as_str()),
93 nil: named_node_id(rdf::NIL.as_str()),
94 thing: owl("Thing"),
95 }
96}
97
98pub mod kind {
100 pub const CLASS: i64 = 1;
102 pub const PROPERTY: i64 = 2;
104 pub const OWL_SAME: i64 = 3;
106 pub const OWL_INVERSE: i64 = 4;
108 pub const TRANSITIVE: i64 = 5;
110 pub const SUBJECT_TYPE: i64 = 6;
112 pub const OBJECT_TYPE: i64 = 7;
114 pub const OWL_SUBJECT_TYPE: i64 = 8;
116 pub const OWL_OBJECT_TYPE: i64 = 9;
118}
119
120pub fn schema_predicates() -> [i64; 6] {
122 let v = vocab();
123 [v.sco, v.spo, v.dom, v.rng, v.eqc, v.eqp]
124}
125
126pub fn is_schema_triple(p: i64, o: i64) -> bool {
128 let v = vocab();
129 schema_predicates().contains(&p) || p == v.inv || (p == v.ty && (o == v.sym || o == v.trans))
130}
131
132fn is_schema_iri(p: &str) -> bool {
133 p == rdfs::SUB_CLASS_OF.as_str()
134 || p == rdfs::SUB_PROPERTY_OF.as_str()
135 || p == rdfs::DOMAIN.as_str()
136 || p == rdfs::RANGE.as_str()
137 || p.strip_prefix(OWL)
138 .is_some_and(|l| matches!(l, "equivalentClass" | "equivalentProperty" | "inverseOf"))
139}
140
141fn is_axiom_class(o: &str) -> bool {
142 o.strip_prefix(OWL)
143 .is_some_and(|l| matches!(l, "SymmetricProperty" | "TransitiveProperty"))
144}
145
146pub fn is_schema_quad(q: QuadRef<'_>) -> bool {
148 is_schema_iri(q.predicate.as_str())
149 || (q.predicate == rdf::TYPE
150 && matches!(q.object, TermRef::NamedNode(n) if is_axiom_class(n.as_str())))
151}
152
153pub fn update_touches_schema(update: &Update) -> bool {
155 let pattern = |p: &NamedNodePattern, o: &TermPattern| match p {
156 NamedNodePattern::Variable(_) => true,
157 NamedNodePattern::NamedNode(n) if is_schema_iri(n.as_str()) => true,
158 NamedNodePattern::NamedNode(n) if *n == rdf::TYPE => match o {
159 TermPattern::NamedNode(c) => is_axiom_class(c.as_str()),
160 TermPattern::Variable(_) => true,
161 _ => false,
162 },
163 NamedNodePattern::NamedNode(_) => false,
164 };
165 update.operations.iter().any(|op| match op {
166 GraphUpdateOperation::InsertData { data } => data.iter().any(|q| {
167 is_schema_iri(q.predicate.as_str())
168 || (q.predicate == rdf::TYPE && matches!(&q.object, Term::NamedNode(n) if is_axiom_class(n.as_str())))
169 }),
170 GraphUpdateOperation::DeleteData { data } => data.iter().any(|q| {
171 is_schema_iri(q.predicate.as_str())
172 || (q.predicate == rdf::TYPE
173 && matches!(&q.object, spargebra::term::GroundTerm::NamedNode(n) if is_axiom_class(n.as_str())))
174 }),
175 GraphUpdateOperation::DeleteInsert { delete, insert, .. } => {
176 delete.iter().any(|q| {
177 let o: TermPattern = q.object.clone().into();
178 pattern(&q.predicate, &o)
179 }) || insert.iter().any(|q| pattern(&q.predicate, &q.object))
180 }
181 GraphUpdateOperation::Create { .. } => false,
182 GraphUpdateOperation::Load { .. }
183 | GraphUpdateOperation::Clear { .. }
184 | GraphUpdateOperation::Drop { .. } => true,
185 })
186}
187
188pub(crate) fn non_literal(x: &str) -> String {
190 format!("(({x}) >> {PAYLOAD_BITS}) IN (1, 2, 9)")
191}
192
193pub fn closure_statements() -> Vec<Statement> {
195 let v = vocab();
196 let ontology_quads = crate::registry::scoped_quads(crate::registry::SchemaRole::Ontology);
199 let quads = ontology_quads.as_str();
200 let (c, p, s_, t) = (
201 kind::CLASS,
202 kind::PROPERTY,
203 kind::OWL_SAME,
204 kind::TRANSITIVE,
205 );
206 let class_edges = format!(
207 "SELECT s AS a, o AS b FROM {quads} WHERE p = {sco} UNION SELECT s, o FROM {quads} WHERE p = {eqc} UNION SELECT o, s FROM {quads} WHERE p = {eqc}",
208 sco = v.sco,
209 eqc = v.eqc
210 );
211 let prop_edges = format!(
212 "SELECT s AS a, o AS b FROM {quads} WHERE p = {spo} UNION SELECT s, o FROM {quads} WHERE p = {eqp} UNION SELECT o, s FROM {quads} WHERE p = {eqp}",
213 spo = v.spo,
214 eqp = v.eqp
215 );
216 let closure = |k: i64, edges: &str| {
217 Statement::new(format!(
218 "WITH RECURSIVE e(a, b) AS ({edges}), c(a, b) AS (SELECT a, b FROM e UNION SELECT c.a, e.b FROM c JOIN e ON e.a = c.b) \
219 INSERT OR IGNORE INTO tbox_closure(kind, sub, sup) SELECT {k}, a, b FROM c WHERE a <> b"
220 ))
221 };
222 let owl = Statement::new(format!(
224 "WITH RECURSIVE e(a, b, d) AS (SELECT a, b, 0 FROM ({prop_edges}) \
225 UNION SELECT s, o, 1 FROM {quads} WHERE p = {inv} UNION SELECT o, s, 1 FROM {quads} WHERE p = {inv} \
226 UNION SELECT s, s, 1 FROM {quads} WHERE p = {ty} AND o = {sym}), \
227 c(a, b, d) AS (SELECT a, b, d FROM e UNION SELECT c.a, e.b, (c.d + e.d) % 2 FROM c JOIN e ON e.a = c.b) \
228 INSERT OR IGNORE INTO tbox_closure(kind, sub, sup) SELECT {s_} + d, a, b FROM c WHERE d = 1 OR a <> b",
229 inv = v.inv,
230 ty = v.ty,
231 sym = v.sym,
232 ));
233 let classes = format!(
235 "SELECT o AS a, o AS b FROM {quads} WHERE p IN ({dom}, {rng}) UNION SELECT sub, sup FROM tbox_closure WHERE kind = {c}",
236 dom = v.dom,
237 rng = v.rng
238 );
239 let subs = |k: i64| {
241 format!(
242 "SELECT s AS a, s AS b FROM {quads} WHERE p IN ({dom}, {rng}) UNION SELECT sub, sup FROM tbox_closure WHERE kind = {k}",
243 dom = v.dom,
244 rng = v.rng
245 )
246 };
247 let typed = |k: i64, props: &str, axiom: i64| {
248 format!(
249 "SELECT {k}, pq.a, cd.b FROM ({props}) pq JOIN {quads} ax ON ax.p = {axiom} AND ax.s = pq.b JOIN ({classes}) cd ON cd.a = ax.o"
250 )
251 };
252 let inverse_typed = |k: i64, axiom: i64| {
253 format!(
254 "SELECT {k}, pq.sub, cd.b FROM tbox_closure pq JOIN {quads} ax ON ax.p = {axiom} AND ax.s = pq.sup JOIN ({classes}) cd ON cd.a = ax.o WHERE pq.kind = {inv}",
255 inv = kind::OWL_INVERSE
256 )
257 };
258 let insert = |sql: String| {
259 Statement::new(format!(
260 "INSERT OR IGNORE INTO tbox_closure(kind, sub, sup) {sql}"
261 ))
262 };
263 vec![
264 Statement::new("DELETE FROM tbox_closure"),
265 closure(c, &class_edges),
266 closure(p, &prop_edges),
267 owl,
268 Statement::new(format!(
269 "INSERT OR IGNORE INTO tbox_closure(kind, sub, sup) SELECT {t}, s, s FROM {quads} WHERE p = {ty} AND o = {trans}",
270 ty = v.ty,
271 trans = v.trans
272 )),
273 insert(typed(kind::SUBJECT_TYPE, &subs(p), v.dom)),
274 insert(typed(kind::OBJECT_TYPE, &subs(p), v.rng)),
275 insert(format!(
276 "{} UNION {}",
277 typed(kind::OWL_SUBJECT_TYPE, &subs(s_), v.dom),
278 inverse_typed(kind::OWL_SUBJECT_TYPE, v.rng)
279 )),
280 insert(format!(
281 "{} UNION {}",
282 typed(kind::OWL_OBJECT_TYPE, &subs(s_), v.rng),
283 inverse_typed(kind::OWL_OBJECT_TYPE, v.dom)
284 )),
285 ]
286}
287
288pub fn transitive_statement(id_col: impl Fn(&str) -> String) -> Statement {
290 Statement::new(format!(
291 "SELECT {} FROM tbox_closure WHERE kind = {}",
292 id_col("sub"),
293 kind::TRANSITIVE
294 ))
295}
296
297#[derive(Debug, Clone)]
299pub enum GraphFilter {
300 Keep,
302 Merge(Option<Vec<i64>>),
304}
305
306#[derive(Debug, Clone)]
308pub struct Entailment<'a> {
309 pub reasoning: Reasoning,
310 pub inferred: bool,
312 pub hide_schema: bool,
315 pub transitive: &'a BTreeSet<i64>,
316 pub max_compound: usize,
318}
319
320impl Entailment<'_> {
321 pub fn active(&self) -> bool {
323 self.reasoning != Reasoning::None || self.inferred
324 }
325
326 pub fn base(&self) -> String {
333 let asserted = if self.hide_schema {
334 crate::registry::quads_without_schema_graphs()
335 } else {
336 "quads"
337 };
338 if self.inferred {
339 format!(
340 "(SELECT s, p, o, g FROM {asserted} UNION ALL SELECT s, p, o, g FROM quads_inf)"
341 )
342 } else {
343 asserted.to_string()
344 }
345 }
346
347 fn kinds(&self) -> (i64, Option<i64>, i64, i64) {
348 match self.reasoning {
349 Reasoning::OwlQl => (
350 kind::OWL_SAME,
351 Some(kind::OWL_INVERSE),
352 kind::OWL_SUBJECT_TYPE,
353 kind::OWL_OBJECT_TYPE,
354 ),
355 _ => (kind::PROPERTY, None, kind::SUBJECT_TYPE, kind::OBJECT_TYPE),
356 }
357 }
358
359 pub fn source(
361 &self,
362 s: Option<i64>,
363 p: Option<i64>,
364 o: Option<i64>,
365 graphs: &GraphFilter,
366 ) -> String {
367 let base = self.base();
368 let g = |x: &str| match graphs {
369 GraphFilter::Keep => format!("{x}.g"),
370 GraphFilter::Merge(_) => DEFAULT_GRAPH_ID.to_string(),
371 };
372 let gw = |x: &str| match graphs {
373 GraphFilter::Merge(Some(l)) => format!(
374 " AND {x}.g IN ({})",
375 l.iter().map(i64::to_string).collect::<Vec<_>>().join(", ")
376 ),
377 _ => String::new(),
378 };
379 let eq =
380 |col: &str, v: Option<i64>| v.map(|v| format!(" AND {col} = {v}")).unwrap_or_default();
381 if self.reasoning == Reasoning::None {
382 let mut w = String::from("1");
383 w.push_str(&eq("x.s", s));
384 w.push_str(&eq("x.p", p));
385 w.push_str(&eq("x.o", o));
386 w.push_str(&gw("x"));
387 let d = if matches!(graphs, GraphFilter::Merge(_)) {
388 "DISTINCT "
389 } else {
390 ""
391 };
392 return format!(
393 "(SELECT {d}x.s AS s, x.p AS p, x.o AS o, {} AS g FROM {base} x WHERE {w})",
394 g("x")
395 );
396 }
397 let v = vocab();
398 let (same, inv, subj, obj) = self.kinds();
399 let row = |s: &str, p: &str, o: &str, gx: &str, rest: String| {
402 format!(
403 "SELECT {s} AS s, {p} AS p, {o} AS o, {} AS g FROM {rest}",
404 g(gx)
405 )
406 };
407 let ty = v.ty.to_string();
408 let type_arms = |s: Option<i64>, o: Option<i64>, asserted: bool| -> Vec<String> {
409 let mut a = Vec::new();
410 if asserted {
411 a.push(row(
412 "x.s",
413 &ty,
414 "x.o",
415 "x",
416 format!(
417 "{base} x WHERE x.p = {ty}{}{}{}",
418 eq("x.s", s),
419 eq("x.o", o),
420 gw("x")
421 ),
422 ));
423 }
424 a.push(row("x.s", &ty, "c.sup", "x", format!(
426 "{base} x JOIN tbox_closure c ON c.kind = {} AND c.sub = x.o WHERE x.p = {ty}{}{}{}",
427 kind::CLASS, eq("x.s", s), eq("c.sup", o), gw("x")
428 )));
429 a.push(row(
430 "x.s",
431 &ty,
432 "c.sup",
433 "x",
434 format!(
435 "tbox_closure c JOIN {base} x ON x.p = c.sub WHERE c.kind = {subj}{}{}{}",
436 eq("x.s", s),
437 eq("c.sup", o),
438 gw("x")
439 ),
440 ));
441 a.push(row(
442 "x.o",
443 &ty,
444 "c.sup",
445 "x",
446 format!(
447 "tbox_closure c JOIN {base} x ON x.p = c.sub WHERE c.kind = {obj} AND {}{}{}{}",
448 non_literal("x.o"),
449 eq("x.o", s),
450 eq("c.sup", o),
451 gw("x")
452 ),
453 ));
454 a
455 };
456 let prop_arms = |pid: i64, s: Option<i64>, o: Option<i64>| -> Vec<String> {
458 let pid_s = pid.to_string();
459 let mut a = vec![
460 row("x.s", &pid_s, "x.o", "x", format!("{base} x WHERE x.p = {pid}{}{}{}", eq("x.s", s), eq("x.o", o), gw("x"))),
461 row("x.s", &pid_s, "x.o", "x", format!(
462 "tbox_closure c JOIN {base} x ON x.p = c.sub WHERE c.kind = {same} AND c.sup = {pid}{}{}{}",
463 eq("x.s", s), eq("x.o", o), gw("x")
464 )),
465 ];
466 if let Some(inv) = inv {
467 a.push(row("x.o", &pid_s, "x.s", "x", format!(
468 "tbox_closure c JOIN {base} x ON x.p = c.sub WHERE c.kind = {inv} AND c.sup = {pid} AND {}{}{}{}",
469 non_literal("x.o"), eq("x.o", s), eq("x.s", o), gw("x")
470 )));
471 }
472 let schema_kind = if pid == v.sco {
474 Some(kind::CLASS)
475 } else if pid == v.spo {
476 Some(kind::PROPERTY)
477 } else {
478 None
479 };
480 if let Some(k) = schema_kind {
481 a.push(format!(
482 "SELECT sub AS s, {pid} AS p, sup AS o, {DEFAULT_GRAPH_ID} AS g FROM tbox_closure WHERE kind = {k}{}{}",
483 eq("sub", s),
484 eq("sup", o)
485 ));
486 }
487 a
488 };
489 let transitive = |pid: i64, s: Option<i64>, o: Option<i64>| -> String {
492 let u = union_all(prop_arms(pid, None, None), self.max_compound);
493 let rec = match (s, o) {
494 (Some(s), _) => format!(
495 "r(n, g) AS (SELECT o, g FROM u WHERE s = {s} UNION SELECT u.o, u.g FROM r JOIN u ON u.s = r.n AND u.g = r.g) \
496 SELECT {s} AS s, {pid} AS p, n AS o, g FROM r{}",
497 o.map(|o| format!(" WHERE n = {o}")).unwrap_or_default()
498 ),
499 (None, Some(o)) => format!(
500 "r(n, g) AS (SELECT s, g FROM u WHERE o = {o} UNION SELECT u.s, u.g FROM r JOIN u ON u.o = r.n AND u.g = r.g) \
501 SELECT n AS s, {pid} AS p, {o} AS o, g FROM r"
502 ),
503 (None, None) => format!(
504 "r(s, o, g) AS (SELECT s, o, g FROM u UNION SELECT r.s, u.o, r.g FROM r JOIN u ON u.s = r.o AND u.g = r.g) \
505 SELECT s, {pid} AS p, o, g FROM r"
506 ),
507 };
508 format!("SELECT * FROM (WITH RECURSIVE u(s, p, o, g) AS ({u}), {rec})")
509 };
510 let mut arms = Vec::new();
511 match p {
512 Some(pid) if pid == v.ty => arms.extend(type_arms(s, o, true)),
513 Some(pid) if self.reasoning == Reasoning::OwlQl && self.transitive.contains(&pid) => {
514 arms.push(transitive(pid, s, o));
515 }
516 Some(pid) => arms.extend(prop_arms(pid, s, o)),
517 None => {
518 arms.push(row(
521 "x.s",
522 "x.p",
523 "x.o",
524 "x",
525 format!(
526 "{base} x WHERE 1{}{}{}",
527 eq("x.s", s),
528 eq("x.o", o),
529 gw("x")
530 ),
531 ));
532 arms.push(row(
533 "x.s",
534 "c.sup",
535 "x.o",
536 "x",
537 format!(
538 "tbox_closure c JOIN {base} x ON x.p = c.sub WHERE c.kind = {same}{}{}{}",
539 eq("x.s", s),
540 eq("x.o", o),
541 gw("x")
542 ),
543 ));
544 if let Some(inv) = inv {
545 arms.push(row("x.o", "c.sup", "x.s", "x", format!(
546 "tbox_closure c JOIN {base} x ON x.p = c.sub WHERE c.kind = {inv} AND {}{}{}{}",
547 non_literal("x.o"), eq("x.o", s), eq("x.s", o), gw("x")
548 )));
549 }
550 for (k, pid) in [(kind::CLASS, v.sco), (kind::PROPERTY, v.spo)] {
551 arms.push(format!(
552 "SELECT sub AS s, {pid} AS p, sup AS o, {DEFAULT_GRAPH_ID} AS g FROM tbox_closure WHERE kind = {k}{}{}",
553 eq("sub", s),
554 eq("sup", o)
555 ));
556 }
557 arms.extend(type_arms(s, o, false));
558 if self.reasoning == Reasoning::OwlQl {
559 for &pid in self.transitive {
560 arms.push(transitive(pid, s, o));
561 }
562 }
563 }
564 }
565 format!(
566 "(SELECT DISTINCT s, p, o, g FROM ({}))",
567 union_all(arms, self.max_compound)
568 )
569 }
570}
571
572pub fn materialize_round() -> Vec<Statement> {
575 let v = vocab();
576 let a = "(SELECT s, p, o FROM quads UNION ALL SELECT s, p, o FROM quads_inf)";
577 let nl = |x: &str| non_literal(x);
578 let lists = format!(
579 "RECURSIVE l(head, node, item) AS (SELECT s, s, o FROM {a} WHERE p = {first} \
580 UNION SELECT l.head, r.o, f.o FROM l JOIN {a} r ON r.s = l.node AND r.p = {rest} JOIN {a} f ON f.s = r.o AND f.p = {first})",
581 first = v.first,
582 rest = v.rest
583 );
584 let rules: Vec<String> = vec![
585 format!("SELECT o, {same}, s FROM {a} WHERE p = {same}", same = v.same),
587 format!("SELECT x.s, {same}, y.o FROM {a} x JOIN {a} y ON y.s = x.o AND y.p = {same} WHERE x.p = {same}", same = v.same),
588 format!("SELECT e.o, t.p, t.o FROM {a} e JOIN {a} t ON t.s = e.s WHERE e.p = {same} AND {}", nl("e.o"), same = v.same),
590 format!("SELECT t.s, e.o, t.o FROM {a} e JOIN {a} t ON t.p = e.s WHERE e.p = {same} AND ((e.o) >> {PAYLOAD_BITS}) = 1", same = v.same),
591 format!("SELECT t.s, t.p, e.o FROM {a} e JOIN {a} t ON t.o = e.s WHERE e.p = {same}", same = v.same),
592 format!("SELECT t.s, {ty}, d.o FROM {a} d JOIN {a} t ON t.p = d.s WHERE d.p = {dom}", ty = v.ty, dom = v.dom),
594 format!("SELECT t.o, {ty}, d.o FROM {a} d JOIN {a} t ON t.p = d.s WHERE d.p = {rng} AND {}", nl("t.o"), ty = v.ty, rng = v.rng),
595 format!(
597 "SELECT t1.o, {same}, t2.o FROM {a} f JOIN {a} t1 ON t1.p = f.s JOIN {a} t2 ON t2.p = f.s AND t2.s = t1.s \
598 WHERE f.p = {ty} AND f.o = {func} AND t1.o <> t2.o AND {} AND {}",
599 nl("t1.o"), nl("t2.o"), same = v.same, ty = v.ty, func = v.func
600 ),
601 format!(
602 "SELECT t1.s, {same}, t2.s FROM {a} f JOIN {a} t1 ON t1.p = f.s JOIN {a} t2 ON t2.p = f.s AND t2.o = t1.o \
603 WHERE f.p = {ty} AND f.o = {ifunc} AND t1.s <> t2.s",
604 same = v.same, ty = v.ty, ifunc = v.ifunc
605 ),
606 format!("SELECT t.o, t.p, t.s FROM {a} f JOIN {a} t ON t.p = f.s WHERE f.p = {ty} AND f.o = {sym} AND {}", nl("t.o"), ty = v.ty, sym = v.sym),
608 format!(
609 "SELECT t1.s, t1.p, t2.o FROM {a} f JOIN {a} t1 ON t1.p = f.s JOIN {a} t2 ON t2.p = f.s AND t2.s = t1.o WHERE f.p = {ty} AND f.o = {trans}",
610 ty = v.ty, trans = v.trans
611 ),
612 format!("SELECT t.s, x.o, t.o FROM {a} x JOIN {a} t ON t.p = x.s WHERE x.p = {spo}", spo = v.spo),
614 format!("SELECT t.s, x.o, t.o FROM {a} x JOIN {a} t ON t.p = x.s WHERE x.p = {eqp}", eqp = v.eqp),
615 format!("SELECT t.s, x.s, t.o FROM {a} x JOIN {a} t ON t.p = x.o WHERE x.p = {eqp}", eqp = v.eqp),
616 format!("SELECT t.o, x.o, t.s FROM {a} x JOIN {a} t ON t.p = x.s WHERE x.p = {inv} AND {}", nl("t.o"), inv = v.inv),
618 format!("SELECT t.o, x.s, t.s FROM {a} x JOIN {a} t ON t.p = x.o WHERE x.p = {inv} AND {}", nl("t.o"), inv = v.inv),
619 format!("SELECT t.s, {ty}, x.o FROM {a} x JOIN {a} t ON t.p = {ty} AND t.o = x.s WHERE x.p = {sco}", ty = v.ty, sco = v.sco),
621 format!("SELECT t.s, {ty}, x.o FROM {a} x JOIN {a} t ON t.p = {ty} AND t.o = x.s WHERE x.p = {eqc}", ty = v.ty, eqc = v.eqc),
622 format!("SELECT t.s, {ty}, x.s FROM {a} x JOIN {a} t ON t.p = {ty} AND t.o = x.o WHERE x.p = {eqc}", ty = v.ty, eqc = v.eqc),
623 format!("SELECT x.s, {sco}, y.o FROM {a} x JOIN {a} y ON y.s = x.o AND y.p = {sco} WHERE x.p = {sco}", sco = v.sco),
625 format!("SELECT s, {sco}, o FROM {a} WHERE p = {eqc} UNION ALL SELECT o, {sco}, s FROM {a} WHERE p = {eqc}", sco = v.sco, eqc = v.eqc),
628 format!("SELECT s, {ty}, {thing} FROM {a} WHERE {} UNION SELECT {thing}, {ty}, {class} UNION SELECT {nothing}, {ty}, {class}", nl("s"), ty = v.ty, thing = v.thing, class = owl("Class"), nothing = owl("Nothing")),
630 format!(
632 "SELECT t.s, op.o, hv.o FROM {a} hv JOIN {a} op ON op.s = hv.s AND op.p = {onp} JOIN {a} t ON t.p = {ty} AND t.o = hv.s WHERE hv.p = {hv}",
633 onp = v.on_property, ty = v.ty, hv = v.has_value
634 ),
635 format!(
636 "SELECT t.s, {ty}, hv.s FROM {a} hv JOIN {a} op ON op.s = hv.s AND op.p = {onp} JOIN {a} t ON t.p = op.o AND t.o = hv.o WHERE hv.p = {hv}",
637 onp = v.on_property, ty = v.ty, hv = v.has_value
638 ),
639 format!(
641 "SELECT t.s, {ty}, sv.s FROM {a} sv JOIN {a} op ON op.s = sv.s AND op.p = {onp} JOIN {a} t ON t.p = op.o \
642 JOIN {a} vt ON vt.s = t.o AND vt.p = {ty} AND vt.o = sv.o WHERE sv.p = {svf}",
643 onp = v.on_property, ty = v.ty, svf = v.some_values
644 ),
645 format!(
646 "SELECT t.s, {ty}, sv.s FROM {a} sv JOIN {a} op ON op.s = sv.s AND op.p = {onp} JOIN {a} t ON t.p = op.o WHERE sv.p = {svf} AND sv.o = {thing}",
647 onp = v.on_property, ty = v.ty, svf = v.some_values, thing = v.thing
648 ),
649 format!(
651 "SELECT t.o, {ty}, av.o FROM {a} av JOIN {a} op ON op.s = av.s AND op.p = {onp} JOIN {a} xt ON xt.p = {ty} AND xt.o = av.s \
652 JOIN {a} t ON t.s = xt.s AND t.p = op.o WHERE av.p = {avf} AND {}",
653 nl("t.o"), onp = v.on_property, ty = v.ty, avf = v.all_values
654 ),
655 format!(
657 "WITH {lists}, m(c, item) AS (SELECT x.s, l.item FROM {a} x JOIN l ON l.head = x.o WHERE x.p = {int}) \
658 SELECT t.s, {ty}, m.c FROM m JOIN {a} t ON t.p = {ty} AND t.o = m.item GROUP BY m.c, t.s \
659 HAVING COUNT(DISTINCT m.item) = (SELECT COUNT(DISTINCT m2.item) FROM m m2 WHERE m2.c = m.c)",
660 ty = v.ty, int = v.intersection
661 ),
662 format!(
663 "WITH {lists} SELECT t.s, {ty}, l.item FROM {a} x JOIN l ON l.head = x.o JOIN {a} t ON t.p = {ty} AND t.o = x.s WHERE x.p = {int}",
664 ty = v.ty, int = v.intersection
665 ),
666 format!(
667 "WITH {lists} SELECT t.s, {ty}, x.s FROM {a} x JOIN l ON l.head = x.o JOIN {a} t ON t.p = {ty} AND t.o = l.item WHERE x.p = {uni}",
668 ty = v.ty, uni = v.union
669 ),
670 format!(
672 "WITH {lists}, \
673 ix(head, node, i) AS (SELECT s, s, 1 FROM {a} WHERE p = {first} UNION SELECT ix.head, r.o, ix.i + 1 FROM ix JOIN {a} r ON r.s = ix.node AND r.p = {rest} WHERE r.o <> {nil}), \
674 steps(chain, i, prop) AS (SELECT x.s, ix.i, f.o FROM {a} x JOIN ix ON ix.head = x.o JOIN {a} f ON f.s = ix.node AND f.p = {first} WHERE x.p = {chain}), \
675 w(chain, start, i, cur) AS (SELECT st.chain, t.s, 1, t.o FROM steps st JOIN {a} t ON t.p = st.prop WHERE st.i = 1 \
676 UNION SELECT w.chain, w.start, w.i + 1, t.o FROM w JOIN steps st ON st.chain = w.chain AND st.i = w.i + 1 JOIN {a} t ON t.s = w.cur AND t.p = st.prop) \
677 SELECT w.start, w.chain, w.cur FROM w WHERE w.i = (SELECT MAX(i) FROM steps s2 WHERE s2.chain = w.chain)",
678 first = v.first, rest = v.rest, nil = v.nil, chain = v.chain
679 ),
680 ];
681 rules
682 .into_iter()
683 .map(|r| {
684 let (with, body) = match r.strip_prefix("WITH ") {
685 Some(rest) => split_with(rest),
686 None => (String::new(), r),
687 };
688 let body = name_columns(&body);
689 Statement::new(format!(
690 "{with}INSERT OR IGNORE INTO quads_inf(s, p, o, g) SELECT DISTINCT r.s, r.p, r.o, {DEFAULT_GRAPH_ID} FROM ({body}) AS r \
691 WHERE {} AND NOT EXISTS (SELECT 1 FROM quads a WHERE a.s = r.s AND a.p = r.p AND a.o = r.o AND a.g = {DEFAULT_GRAPH_ID})",
692 non_literal("r.s")
693 ))
694 })
695 .collect()
696}
697
698fn name_columns(select: &str) -> String {
700 let rest = select.strip_prefix("SELECT ").unwrap_or(select);
701 let from = top_level(rest, " FROM ").unwrap_or(rest.len());
702 let cols: Vec<&str> = split_top(&rest[..from], ',');
703 if cols.len() != 3 {
704 return select.to_string();
705 }
706 format!(
707 "SELECT {} AS s, {} AS p, {} AS o{}",
708 cols[0].trim(),
709 cols[1].trim(),
710 cols[2].trim(),
711 &rest[from..]
712 )
713}
714
715fn top_level(s: &str, pat: &str) -> Option<usize> {
717 let mut depth = 0i32;
718 for (i, c) in s.char_indices() {
719 match c {
720 '(' => depth += 1,
721 ')' => depth -= 1,
722 _ if depth == 0 && s[i..].starts_with(pat) => return Some(i),
723 _ => {}
724 }
725 }
726 None
727}
728
729fn split_top(s: &str, sep: char) -> Vec<&str> {
730 let (mut depth, mut start, mut out) = (0i32, 0usize, Vec::new());
731 for (i, c) in s.char_indices() {
732 match c {
733 '(' => depth += 1,
734 ')' => depth -= 1,
735 c if c == sep && depth == 0 => {
736 out.push(&s[start..i]);
737 start = i + 1;
738 }
739 _ => {}
740 }
741 }
742 out.push(&s[start..]);
743 out
744}
745
746fn split_with(rest: &str) -> (String, String) {
748 let bytes = rest.as_bytes();
750 let (mut depth, mut last) = (0i32, 0usize);
751 for (i, &c) in bytes.iter().enumerate() {
752 match c {
753 b'(' => depth += 1,
754 b')' => depth -= 1,
755 b'S' if depth == 0 && rest[i..].starts_with("SELECT ") => last = i,
756 _ => {}
757 }
758 }
759 (
760 format!("WITH {} ", rest[..last].trim_end()),
761 rest[last..].to_string(),
762 )
763}
764
765pub fn materialize_reset(caps: &crate::sql::Capabilities) -> Vec<Statement> {
769 let mut rows = crate::encoding::EncodedRows::default();
770 for iri in [
771 rdf::TYPE.as_str(),
772 rdfs::SUB_CLASS_OF.as_str(),
773 rdfs::SUB_PROPERTY_OF.as_str(),
774 rdfs::DOMAIN.as_str(),
775 rdfs::RANGE.as_str(),
776 ] {
777 rows.iri(iri);
778 }
779 for local in [
780 "sameAs",
781 "equivalentClass",
782 "equivalentProperty",
783 "Thing",
784 "Nothing",
785 "Class",
786 ] {
787 rows.iri(&format!("{OWL}{local}"));
788 }
789 rows.dedup();
790 let mut out = vec![Statement::new("DELETE FROM quads_inf")];
791 out.extend(crate::writer::term_statements(&rows, caps));
792 out
793}