Skip to main content

oxilite_core/
reason.rs

1//! RDFS / OWL reasoning: the schema closure, query rewriting and OWL 2 RL materialization.
2//!
3//! Query-time reasoning rewrites each triple pattern into a derived table of *entailed*
4//! triples, computed from the asserted quads and `tbox_closure` (a small table recomputed by
5//! `optimize()` and by writes that touch schema predicates). Queries therefore stay single SQL
6//! statements. Materialization is explicit: OWL 2 RL rules run as `INSERT … SELECT`
7//! statements into `quads_inf` until nothing changes.
8//!
9// @lat: [[architecture#Reasoning]]
10
11use 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/// Entailment regime of a query.
20#[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    /// Asserted triples only (Oxigraph's behaviour).
25    #[default]
26    None,
27    /// RDFS: subclass, subproperty, domain and range (plus equivalent classes and properties).
28    Rdfs,
29    /// RDFS plus the OWL QL property axioms: inverse and symmetric properties, and transitive
30    /// properties.
31    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/// Ids of the vocabulary used by the closure and the rules.
41#[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
98/// `tbox_closure.kind` values.
99pub mod kind {
100    /// `sub ⊑ sup` for classes (subClassOf and equivalentClass, transitive, irreflexive rows).
101    pub const CLASS: i64 = 1;
102    /// RDFS sub-property closure (subPropertyOf and equivalentProperty).
103    pub const PROPERTY: i64 = 2;
104    /// OWL: `(x sub y)` entails `(x sup y)` (sub-properties, inverses of inverses).
105    pub const OWL_SAME: i64 = 3;
106    /// OWL: `(x sub y)` entails `(y sup x)` (inverse and symmetric properties).
107    pub const OWL_INVERSE: i64 = 4;
108    /// Transitive properties (`sub = sup`).
109    pub const TRANSITIVE: i64 = 5;
110    /// RDFS: the subject of `sub` has type `sup` (domains through sub-properties/classes).
111    pub const SUBJECT_TYPE: i64 = 6;
112    /// RDFS: the object of `sub` has type `sup`.
113    pub const OBJECT_TYPE: i64 = 7;
114    /// OWL: the subject of `sub` has type `sup` (also through inverse ranges).
115    pub const OWL_SUBJECT_TYPE: i64 = 8;
116    /// OWL: the object of `sub` has type `sup`.
117    pub const OWL_OBJECT_TYPE: i64 = 9;
118}
119
120/// Predicates whose triples change the schema closure.
121pub fn schema_predicates() -> [i64; 6] {
122    let v = vocab();
123    [v.sco, v.spo, v.dom, v.rng, v.eqc, v.eqp]
124}
125
126/// Does a triple with this predicate and object change the closure?
127pub 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).is_some_and(|l| {
138            // `owl:imports` pulls another registered ontology into a scope.
139            matches!(
140                l,
141                "equivalentClass" | "equivalentProperty" | "inverseOf" | "imports"
142            )
143        })
144}
145
146fn is_axiom_class(o: &str) -> bool {
147    o.strip_prefix(OWL)
148        .is_some_and(|l| matches!(l, "SymmetricProperty" | "TransitiveProperty"))
149}
150
151/// Does writing this quad change the schema closure? (A schema axiom, or anything in the
152/// registry graph, which scopes the closure.)
153pub fn is_schema_quad(q: QuadRef<'_>) -> bool {
154    crate::registry::is_registry_quad(q)
155        || is_schema_iri(q.predicate.as_str())
156        || (q.predicate == rdf::TYPE
157            && matches!(q.object, TermRef::NamedNode(n) if is_axiom_class(n.as_str())))
158}
159
160/// Can this update change the schema closure? (Conservative: variables count as schema.)
161pub fn update_touches_schema(update: &Update) -> bool {
162    crate::registry::update_touches_registry(update) || touches_axioms(update)
163}
164
165fn touches_axioms(update: &Update) -> bool {
166    let pattern = |p: &NamedNodePattern, o: &TermPattern| match p {
167        NamedNodePattern::Variable(_) => true,
168        NamedNodePattern::NamedNode(n) if is_schema_iri(n.as_str()) => true,
169        NamedNodePattern::NamedNode(n) if *n == rdf::TYPE => match o {
170            TermPattern::NamedNode(c) => is_axiom_class(c.as_str()),
171            TermPattern::Variable(_) => true,
172            _ => false,
173        },
174        NamedNodePattern::NamedNode(_) => false,
175    };
176    update.operations.iter().any(|op| match op {
177        GraphUpdateOperation::InsertData { data } => data.iter().any(|q| {
178            is_schema_iri(q.predicate.as_str())
179                || (q.predicate == rdf::TYPE && matches!(&q.object, Term::NamedNode(n) if is_axiom_class(n.as_str())))
180        }),
181        GraphUpdateOperation::DeleteData { data } => data.iter().any(|q| {
182            is_schema_iri(q.predicate.as_str())
183                || (q.predicate == rdf::TYPE
184                    && matches!(&q.object, spargebra::term::GroundTerm::NamedNode(n) if is_axiom_class(n.as_str())))
185        }),
186        GraphUpdateOperation::DeleteInsert { delete, insert, .. } => {
187            delete.iter().any(|q| {
188                let o: TermPattern = q.object.clone().into();
189                pattern(&q.predicate, &o)
190            }) || insert.iter().any(|q| pattern(&q.predicate, &q.object))
191        }
192        GraphUpdateOperation::Create { .. } => false,
193        GraphUpdateOperation::Load { .. }
194        | GraphUpdateOperation::Clear { .. }
195        | GraphUpdateOperation::Drop { .. } => true,
196    })
197}
198
199/// SQL: the id is an IRI, a blank node or a triple term (a valid subject).
200pub(crate) fn non_literal(x: &str) -> String {
201    format!("(({x}) >> {PAYLOAD_BITS}) IN (1, 2, 9)")
202}
203
204/// Statements recomputing `tbox_closure`, one closure per scope (see
205/// [`crate::registry::ontology_axioms`]): the scope of every graph, and one per graph that an
206/// active ontology is mapped to. Each statement reads the ontology axioms tagged with their
207/// scope; recursion only joins rows of the same scope.
208pub fn closure_statements() -> Vec<Statement> {
209    let v = vocab();
210    let ax = |cond: String| crate::registry::ontology_axioms(&cond);
211    let (c, p, s_, t) = (
212        kind::CLASS,
213        kind::PROPERTY,
214        kind::OWL_SAME,
215        kind::TRANSITIVE,
216    );
217    let class_edges = format!(
218        "SELECT scope AS k, s AS a, o AS b FROM {} UNION SELECT scope, o, s FROM {}",
219        ax(format!("q.p IN ({}, {})", v.sco, v.eqc)),
220        ax(format!("q.p = {}", v.eqc))
221    );
222    let prop_edges = format!(
223        "SELECT scope AS k, s AS a, o AS b FROM {} UNION SELECT scope, o, s FROM {}",
224        ax(format!("q.p IN ({}, {})", v.spo, v.eqp)),
225        ax(format!("q.p = {}", v.eqp))
226    );
227    let closure = |k: i64, edges: &str| {
228        Statement::new(format!(
229            "WITH RECURSIVE e(k, a, b) AS ({edges}), \
230             c(k, a, b) AS (SELECT k, a, b FROM e UNION SELECT c.k, c.a, e.b FROM c JOIN e ON e.k = c.k AND e.a = c.b) \
231             INSERT OR IGNORE INTO tbox_closure(kind, scope, sub, sup) SELECT {k}, k, a, b FROM c WHERE a <> b"
232        ))
233    };
234    // OWL: property edges with a direction bit (1 = inverse), composed modulo 2.
235    let owl = Statement::new(format!(
236        "WITH RECURSIVE e(k, a, b, d) AS (SELECT k, a, b, 0 FROM ({prop_edges}) \
237           UNION SELECT scope, s, o, 1 FROM {inv} UNION SELECT scope, o, s, 1 FROM {inv} \
238           UNION SELECT scope, s, s, 1 FROM {sym}), \
239         c(k, a, b, d) AS (SELECT k, a, b, d FROM e UNION SELECT c.k, c.a, e.b, (c.d + e.d) % 2 FROM c JOIN e ON e.k = c.k AND e.a = c.b) \
240         INSERT OR IGNORE INTO tbox_closure(kind, scope, sub, sup) SELECT {s_} + d, k, a, b FROM c WHERE d = 1 OR a <> b",
241        inv = ax(format!("q.p = {}", v.inv)),
242        sym = ax(format!("q.p = {} AND q.o = {}", v.ty, v.sym)),
243    ));
244    let dom_rng = || ax(format!("q.p IN ({}, {})", v.dom, v.rng));
245    // Classes a domain/range class is a subclass of (reflexive), per scope.
246    let classes = format!(
247        "SELECT scope AS k, o AS a, o AS b FROM {} UNION SELECT scope, sub, sup FROM tbox_closure WHERE kind = {c}",
248        dom_rng()
249    );
250    // Properties with (sub, sup) where sup is reflexive over properties having a domain/range.
251    let subs = |k: i64| {
252        format!(
253            "SELECT scope AS k, s AS a, s AS b FROM {} UNION SELECT scope, sub, sup FROM tbox_closure WHERE kind = {k}",
254            dom_rng()
255        )
256    };
257    let typed = |k: i64, props: &str, axiom: i64| {
258        format!(
259            "SELECT {k}, pq.k, pq.a, cd.b FROM ({props}) pq JOIN {} x ON x.s = pq.b AND x.scope = pq.k \
260             JOIN ({classes}) cd ON cd.a = x.o AND cd.k = pq.k",
261            ax(format!("q.p = {axiom}"))
262        )
263    };
264    let inverse_typed = |k: i64, axiom: i64| {
265        format!(
266            "SELECT {k}, pq.scope, pq.sub, cd.b FROM tbox_closure pq JOIN {} x ON x.s = pq.sup AND x.scope = pq.scope \
267             JOIN ({classes}) cd ON cd.a = x.o AND cd.k = pq.scope WHERE pq.kind = {inv}",
268            ax(format!("q.p = {axiom}")),
269            inv = kind::OWL_INVERSE
270        )
271    };
272    let insert = |sql: String| {
273        Statement::new(format!(
274            "INSERT OR IGNORE INTO tbox_closure(kind, scope, sub, sup) {sql}"
275        ))
276    };
277    vec![
278        Statement::new("DELETE FROM tbox_closure"),
279        closure(c, &class_edges),
280        closure(p, &prop_edges),
281        owl,
282        Statement::new(format!(
283            "INSERT OR IGNORE INTO tbox_closure(kind, scope, sub, sup) SELECT {t}, scope, s, s FROM {}",
284            ax(format!("q.p = {} AND q.o = {}", v.ty, v.trans))
285        )),
286        insert(typed(kind::SUBJECT_TYPE, &subs(p), v.dom)),
287        insert(typed(kind::OBJECT_TYPE, &subs(p), v.rng)),
288        insert(format!(
289            "{} UNION {}",
290            typed(kind::OWL_SUBJECT_TYPE, &subs(s_), v.dom),
291            inverse_typed(kind::OWL_SUBJECT_TYPE, v.rng)
292        )),
293        insert(format!(
294            "{} UNION {}",
295            typed(kind::OWL_OBJECT_TYPE, &subs(s_), v.rng),
296            inverse_typed(kind::OWL_OBJECT_TYPE, v.dom)
297        )),
298    ]
299}
300
301/// Loads the transitive properties (kept in memory with the planner statistics).
302pub fn transitive_statement(id_col: impl Fn(&str) -> String) -> Statement {
303    Statement::new(format!(
304        "SELECT DISTINCT {} FROM tbox_closure WHERE kind = {}",
305        id_col("sub"),
306        kind::TRANSITIVE
307    ))
308}
309
310/// The merge of asserted and materialized quads as a set: `asserted` (a quad table or derived
311/// table), plus each inference not also asserted. An inference can repeat an asserted quad
312/// (a rule re-deriving a fact, or a fact asserted after it was inferred), and a plain
313/// `UNION ALL` would then return that triple twice. Both arms stay `UNION ALL`, so SQLite
314/// still pushes a caller's filters into them and reads the indexes.
315pub fn with_inferences(asserted: &str) -> String {
316    format!(
317        "(SELECT s, p, o, g FROM {asserted} UNION ALL SELECT i.s, i.p, i.o, i.g FROM quads_inf i \
318         WHERE NOT EXISTS (SELECT 1 FROM {asserted} a WHERE a.s = i.s AND a.p = i.p AND a.o = i.o AND a.g = i.g))"
319    )
320}
321
322/// Which graphs a reasoned pattern reads.
323#[derive(Debug, Clone)]
324pub enum GraphFilter {
325    /// Keep each quad's graph (the caller constrains `g`).
326    Keep,
327    /// The RDF merge of these graphs (`None`: every graph), exposed as graph 0.
328    Merge(Option<Vec<i64>>),
329}
330
331/// Builds derived tables of entailed triples.
332#[derive(Debug, Clone)]
333pub struct Entailment<'a> {
334    pub reasoning: Reasoning,
335    /// Also read materialized inferences (`quads_inf`).
336    pub inferred: bool,
337    /// Exclude every registered schema graph from the stored triples
338    /// (`QueryOptions::include_schema_graphs`).
339    pub hide_schema: bool,
340    pub transitive: &'a BTreeSet<i64>,
341    /// Graphs with a closure of their own (see `tbox_closure.scope`); every other graph uses
342    /// the closure of [`crate::registry::all_scope`].
343    pub scopes: &'a BTreeSet<i64>,
344    /// Maximum terms of a compound SELECT.
345    pub max_compound: usize,
346    /// Read the store as it was at this tick (see `version`), from the change log.
347    pub as_of: Option<i64>,
348}
349
350impl Entailment<'_> {
351    /// SQL: the closure scope of the quads whose graph column is `g`.
352    fn scope_of(&self, g: &str) -> String {
353        let all = crate::registry::all_scope();
354        if self.scopes.is_empty() {
355            return all.to_string();
356        }
357        let list = self
358            .scopes
359            .iter()
360            .map(i64::to_string)
361            .collect::<Vec<_>>()
362            .join(", ");
363        format!("(CASE WHEN {g} IN ({list}) THEN {g} ELSE {all} END)")
364    }
365
366    /// Is any rewriting needed?
367    pub fn active(&self) -> bool {
368        self.reasoning != Reasoning::None || self.inferred
369    }
370
371    /// The table of stored triples: asserted, plus materialized inferences on request, minus
372    /// the registered schema graphs when they are hidden.
373    ///
374    /// Every quad source of the compiler goes through this or [`Self::source`], so hiding is
375    /// applied once and covers patterns, paths, `OPTIONAL` and `GRAPH ?g` alike. Inferences
376    /// are conclusions rather than schema, so they are never hidden.
377    pub fn base(&self) -> String {
378        let asserted = match (self.as_of, self.hide_schema) {
379            (Some(t), true) => {
380                crate::registry::without_schema_graphs(&crate::version::as_of_sql(&t.to_string()))
381            }
382            (Some(t), false) => crate::version::as_of_sql(&t.to_string()),
383            (None, true) => crate::registry::quads_without_schema_graphs().to_owned(),
384            (None, false) => "quads".to_owned(),
385        };
386        if self.inferred {
387            format!(
388                "(SELECT s, p, o, g FROM {asserted} UNION ALL SELECT s, p, o, g FROM quads_inf)"
389            )
390        } else {
391            asserted.to_string()
392        }
393    }
394
395    fn kinds(&self) -> (i64, Option<i64>, i64, i64) {
396        match self.reasoning {
397            Reasoning::OwlQl => (
398                kind::OWL_SAME,
399                Some(kind::OWL_INVERSE),
400                kind::OWL_SUBJECT_TYPE,
401                kind::OWL_OBJECT_TYPE,
402            ),
403            _ => (kind::PROPERTY, None, kind::SUBJECT_TYPE, kind::OBJECT_TYPE),
404        }
405    }
406
407    /// A `(s, p, o, g)` derived table of the entailed triples matching the constants.
408    pub fn source(
409        &self,
410        s: Option<i64>,
411        p: Option<i64>,
412        o: Option<i64>,
413        graphs: &GraphFilter,
414    ) -> String {
415        let base = self.base();
416        let g = |x: &str| match graphs {
417            GraphFilter::Keep => format!("{x}.g"),
418            GraphFilter::Merge(_) => DEFAULT_GRAPH_ID.to_string(),
419        };
420        let gw = |x: &str| match graphs {
421            GraphFilter::Merge(Some(l)) => format!(
422                " AND {x}.g IN ({})",
423                l.iter().map(i64::to_string).collect::<Vec<_>>().join(", ")
424            ),
425            _ => String::new(),
426        };
427        let eq =
428            |col: &str, v: Option<i64>| v.map(|v| format!(" AND {col} = {v}")).unwrap_or_default();
429        if self.reasoning == Reasoning::None {
430            let mut w = String::from("1");
431            w.push_str(&eq("x.s", s));
432            w.push_str(&eq("x.p", p));
433            w.push_str(&eq("x.o", o));
434            w.push_str(&gw("x"));
435            let d = if matches!(graphs, GraphFilter::Merge(_)) {
436                "DISTINCT "
437            } else {
438                ""
439            };
440            return format!(
441                "(SELECT {d}x.s AS s, x.p AS p, x.o AS o, {} AS g FROM {base} x WHERE {w})",
442                g("x")
443            );
444        }
445        let v = vocab();
446        let (same, inv, subj, obj) = self.kinds();
447        // Each quad is entailed with the closure of its graph's scope.
448        let cs = format!(" AND c.scope = {}", self.scope_of("x.g"));
449        let cs = cs.as_str();
450        // One arm: `SELECT s AS s, p AS p, o AS o, g AS g FROM …` (compound SELECTs take their
451        // column names from the first arm, and derived tables cannot rename columns).
452        let row = |s: &str, p: &str, o: &str, gx: &str, rest: String| {
453            format!(
454                "SELECT {s} AS s, {p} AS p, {o} AS o, {} AS g FROM {rest}",
455                g(gx)
456            )
457        };
458        let ty = v.ty.to_string();
459        let type_arms = |s: Option<i64>, o: Option<i64>, asserted: bool| -> Vec<String> {
460            let mut a = Vec::new();
461            if asserted {
462                a.push(row(
463                    "x.s",
464                    &ty,
465                    "x.o",
466                    "x",
467                    format!(
468                        "{base} x WHERE x.p = {ty}{}{}{}",
469                        eq("x.s", s),
470                        eq("x.o", o),
471                        gw("x")
472                    ),
473                ));
474            }
475            // Superclasses of asserted types, then domains and ranges.
476            a.push(row("x.s", &ty, "c.sup", "x", format!(
477                "{base} x JOIN tbox_closure c ON c.kind = {} AND c.sub = x.o WHERE x.p = {ty}{cs}{}{}{}",
478                kind::CLASS, eq("x.s", s), eq("c.sup", o), gw("x")
479            )));
480            a.push(row(
481                "x.s",
482                &ty,
483                "c.sup",
484                "x",
485                format!(
486                    "tbox_closure c JOIN {base} x ON x.p = c.sub WHERE c.kind = {subj}{cs}{}{}{}",
487                    eq("x.s", s),
488                    eq("c.sup", o),
489                    gw("x")
490                ),
491            ));
492            a.push(row(
493                "x.o",
494                &ty,
495                "c.sup",
496                "x",
497                format!(
498                    "tbox_closure c JOIN {base} x ON x.p = c.sub WHERE c.kind = {obj}{cs} AND {}{}{}{}",
499                    non_literal("x.o"),
500                    eq("x.o", s),
501                    eq("c.sup", o),
502                    gw("x")
503                ),
504            ));
505            a
506        };
507        // Triples of property `pid` entailed without transitivity.
508        let prop_arms = |pid: i64, s: Option<i64>, o: Option<i64>| -> Vec<String> {
509            let pid_s = pid.to_string();
510            let mut a = vec![
511                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"))),
512                row("x.s", &pid_s, "x.o", "x", format!(
513                    "tbox_closure c JOIN {base} x ON x.p = c.sub WHERE c.kind = {same} AND c.sup = {pid}{cs}{}{}{}",
514                    eq("x.s", s), eq("x.o", o), gw("x")
515                )),
516            ];
517            if let Some(inv) = inv {
518                a.push(row("x.o", &pid_s, "x.s", "x", format!(
519                    "tbox_closure c JOIN {base} x ON x.p = c.sub WHERE c.kind = {inv} AND c.sup = {pid}{cs} AND {}{}{}{}",
520                    non_literal("x.o"), eq("x.o", s), eq("x.s", o), gw("x")
521                )));
522            }
523            // The schema closure itself answers subClassOf / subPropertyOf patterns.
524            let schema_kind = if pid == v.sco {
525                Some(kind::CLASS)
526            } else if pid == v.spo {
527                Some(kind::PROPERTY)
528            } else {
529                None
530            };
531            if let Some(k) = schema_kind {
532                a.push(format!(
533                    "SELECT sub AS s, {pid} AS p, sup AS o, {DEFAULT_GRAPH_ID} AS g FROM tbox_closure WHERE kind = {k}{}{}",
534                    eq("sub", s),
535                    eq("sup", o)
536                ));
537            }
538            a
539        };
540        // A transitive property: the closure of its (otherwise) entailed triples, walked from a
541        // constant endpoint when there is one.
542        // With graphs of their own scope, a property is transitive only in the graphs whose
543        // scope declares it so; elsewhere its triples are entailed as for any property.
544        let declared = |pid: i64, g: &str| {
545            format!(
546                "EXISTS (SELECT 1 FROM tbox_closure t WHERE t.kind = {} AND t.sub = {pid} AND t.scope = {})",
547                kind::TRANSITIVE,
548                self.scope_of(g)
549            )
550        };
551        let transitive = |pid: i64, s: Option<i64>, o: Option<i64>| -> Vec<String> {
552            let mut u = union_all(prop_arms(pid, None, None), self.max_compound);
553            let mut arms = Vec::new();
554            if !self.scopes.is_empty() {
555                u = format!(
556                    "SELECT z.s, z.p, z.o, z.g FROM ({u}) z WHERE {}",
557                    declared(pid, "z.g")
558                );
559                arms.push(format!(
560                    "SELECT z.s, z.p, z.o, z.g FROM ({}) z WHERE NOT {}",
561                    union_all(prop_arms(pid, s, o), self.max_compound),
562                    declared(pid, "z.g")
563                ));
564            }
565            let rec = match (s, o) {
566                (Some(s), _) => format!(
567                    "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) \
568                     SELECT {s} AS s, {pid} AS p, n AS o, g FROM r{}",
569                    o.map(|o| format!(" WHERE n = {o}")).unwrap_or_default()
570                ),
571                (None, Some(o)) => format!(
572                    "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) \
573                     SELECT n AS s, {pid} AS p, {o} AS o, g FROM r"
574                ),
575                (None, None) => format!(
576                    "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) \
577                     SELECT s, {pid} AS p, o, g FROM r"
578                ),
579            };
580            arms.push(format!(
581                "SELECT * FROM (WITH RECURSIVE u(s, p, o, g) AS ({u}), {rec})"
582            ));
583            arms
584        };
585        let mut arms = Vec::new();
586        match p {
587            Some(pid) if pid == v.ty => arms.extend(type_arms(s, o, true)),
588            Some(pid) if self.reasoning == Reasoning::OwlQl && self.transitive.contains(&pid) => {
589                arms.extend(transitive(pid, s, o));
590            }
591            Some(pid) => arms.extend(prop_arms(pid, s, o)),
592            None => {
593                // Every entailed triple: asserted, through property axioms, the schema closure,
594                // types, and transitive properties.
595                arms.push(row(
596                    "x.s",
597                    "x.p",
598                    "x.o",
599                    "x",
600                    format!(
601                        "{base} x WHERE 1{}{}{}",
602                        eq("x.s", s),
603                        eq("x.o", o),
604                        gw("x")
605                    ),
606                ));
607                arms.push(row(
608                    "x.s",
609                    "c.sup",
610                    "x.o",
611                    "x",
612                    format!(
613                        "tbox_closure c JOIN {base} x ON x.p = c.sub WHERE c.kind = {same}{cs}{}{}{}",
614                        eq("x.s", s),
615                        eq("x.o", o),
616                        gw("x")
617                    ),
618                ));
619                if let Some(inv) = inv {
620                    arms.push(row("x.o", "c.sup", "x.s", "x", format!(
621                        "tbox_closure c JOIN {base} x ON x.p = c.sub WHERE c.kind = {inv}{cs} AND {}{}{}{}",
622                        non_literal("x.o"), eq("x.o", s), eq("x.s", o), gw("x")
623                    )));
624                }
625                for (k, pid) in [(kind::CLASS, v.sco), (kind::PROPERTY, v.spo)] {
626                    arms.push(format!(
627                        "SELECT sub AS s, {pid} AS p, sup AS o, {DEFAULT_GRAPH_ID} AS g FROM tbox_closure WHERE kind = {k}{}{}",
628                        eq("sub", s),
629                        eq("sup", o)
630                    ));
631                }
632                arms.extend(type_arms(s, o, false));
633                if self.reasoning == Reasoning::OwlQl {
634                    for &pid in self.transitive {
635                        arms.extend(transitive(pid, s, o));
636                    }
637                }
638            }
639        }
640        format!(
641            "(SELECT DISTINCT s, p, o, g FROM ({}))",
642            union_all(arms, self.max_compound)
643        )
644    }
645}
646
647/// Statements of one OWL 2 RL round: every rule inserts its new conclusions into
648/// `quads_inf` (graph 0), reading asserted and inferred triples of every graph.
649pub fn materialize_round() -> Vec<Statement> {
650    let v = vocab();
651    let a = "(SELECT s, p, o FROM quads UNION ALL SELECT s, p, o FROM quads_inf)";
652    let nl = |x: &str| non_literal(x);
653    let lists = format!(
654        "RECURSIVE l(head, node, item) AS (SELECT s, s, o FROM {a} WHERE p = {first} \
655           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})",
656        first = v.first,
657        rest = v.rest
658    );
659    let rules: Vec<String> = vec![
660        // eq-sym, eq-trans
661        format!("SELECT o, {same}, s FROM {a} WHERE p = {same}", same = v.same),
662        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),
663        // eq-rep-s, eq-rep-p, eq-rep-o
664        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),
665        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),
666        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),
667        // prp-dom, prp-rng
668        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),
669        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),
670        // prp-fp, prp-ifp
671        format!(
672            "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 \
673             WHERE f.p = {ty} AND f.o = {func} AND t1.o <> t2.o AND {} AND {}",
674            nl("t1.o"), nl("t2.o"), same = v.same, ty = v.ty, func = v.func
675        ),
676        format!(
677            "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 \
678             WHERE f.p = {ty} AND f.o = {ifunc} AND t1.s <> t2.s",
679            same = v.same, ty = v.ty, ifunc = v.ifunc
680        ),
681        // prp-symp, prp-trp
682        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),
683        format!(
684            "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}",
685            ty = v.ty, trans = v.trans
686        ),
687        // prp-spo1, prp-eqp1, prp-eqp2
688        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),
689        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),
690        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),
691        // prp-inv1, prp-inv2
692        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),
693        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),
694        // cax-sco, cax-eqc1, cax-eqc2
695        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),
696        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),
697        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),
698        // scm-sco (transitive subclasses)
699        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),
700        // scm-eqc1 (the other scm-* rules only restate schema triples whose instance-level
701        // consequences the rules above already derive; `reasonable` omits them too)
702        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),
703        // Every subject is an owl:Thing; owl:Thing and owl:Nothing are classes (cls-thing, cls-nothing1)
704        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")),
705        // cls-hv1, cls-hv2
706        format!(
707            "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}",
708            onp = v.on_property, ty = v.ty, hv = v.has_value
709        ),
710        format!(
711            "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}",
712            onp = v.on_property, ty = v.ty, hv = v.has_value
713        ),
714        // cls-svf1, cls-svf2
715        format!(
716            "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 \
717             JOIN {a} vt ON vt.s = t.o AND vt.p = {ty} AND vt.o = sv.o WHERE sv.p = {svf}",
718            onp = v.on_property, ty = v.ty, svf = v.some_values
719        ),
720        format!(
721            "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}",
722            onp = v.on_property, ty = v.ty, svf = v.some_values, thing = v.thing
723        ),
724        // cls-avf
725        format!(
726            "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 \
727             JOIN {a} t ON t.s = xt.s AND t.p = op.o WHERE av.p = {avf} AND {}",
728            nl("t.o"), onp = v.on_property, ty = v.ty, avf = v.all_values
729        ),
730        // cls-int1, cls-int2, cls-uni (list members through rdf:first / rdf:rest)
731        format!(
732            "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}) \
733             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 \
734             HAVING COUNT(DISTINCT m.item) = (SELECT COUNT(DISTINCT m2.item) FROM m m2 WHERE m2.c = m.c)",
735            ty = v.ty, int = v.intersection
736        ),
737        format!(
738            "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}",
739            ty = v.ty, int = v.intersection
740        ),
741        format!(
742            "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}",
743            ty = v.ty, uni = v.union
744        ),
745        // prp-spo2 (property chains)
746        format!(
747            "WITH {lists}, \
748               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}), \
749               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}), \
750               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 \
751                 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) \
752             SELECT w.start, w.chain, w.cur FROM w WHERE w.i = (SELECT MAX(i) FROM steps s2 WHERE s2.chain = w.chain)",
753            first = v.first, rest = v.rest, nil = v.nil, chain = v.chain
754        ),
755    ];
756    let mut statements: Vec<Statement> = rules
757        .into_iter()
758        .map(|r| {
759            let (with, body) = match r.strip_prefix("WITH ") {
760                Some(rest) => split_with(rest),
761                None => (String::new(), r),
762            };
763            let body = name_columns(&body);
764            Statement::new(format!(
765                "{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 \
766                 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})",
767                non_literal("r.s")
768            ))
769        })
770        .collect();
771    statements.push(inference_attribute(OWL_PRODUCER));
772    statements
773}
774
775/// Renames the three result columns of a rule's first SELECT to `s, p, o`.
776fn name_columns(select: &str) -> String {
777    let rest = select.strip_prefix("SELECT ").unwrap_or(select);
778    let from = top_level(rest, " FROM ").unwrap_or(rest.len());
779    let cols: Vec<&str> = split_top(&rest[..from], ',');
780    if cols.len() != 3 {
781        return select.to_string();
782    }
783    format!(
784        "SELECT {} AS s, {} AS p, {} AS o{}",
785        cols[0].trim(),
786        cols[1].trim(),
787        cols[2].trim(),
788        &rest[from..]
789    )
790}
791
792/// Byte offset of the first top-level (outside parentheses) occurrence of `pat`.
793fn top_level(s: &str, pat: &str) -> Option<usize> {
794    let mut depth = 0i32;
795    for (i, c) in s.char_indices() {
796        match c {
797            '(' => depth += 1,
798            ')' => depth -= 1,
799            _ if depth == 0 && s[i..].starts_with(pat) => return Some(i),
800            _ => {}
801        }
802    }
803    None
804}
805
806fn split_top(s: &str, sep: char) -> Vec<&str> {
807    let (mut depth, mut start, mut out) = (0i32, 0usize, Vec::new());
808    for (i, c) in s.char_indices() {
809        match c {
810            '(' => depth += 1,
811            ')' => depth -= 1,
812            c if c == sep && depth == 0 => {
813                out.push(&s[start..i]);
814                start = i + 1;
815            }
816            _ => {}
817        }
818    }
819    out.push(&s[start..]);
820    out
821}
822
823/// Splits `ctes SELECT …` (after `WITH `) into the `WITH … ` prefix and the final SELECT.
824fn split_with(rest: &str) -> (String, String) {
825    // The final SELECT is the last top-level "SELECT" (outside parentheses).
826    let bytes = rest.as_bytes();
827    let (mut depth, mut last) = (0i32, 0usize);
828    for (i, &c) in bytes.iter().enumerate() {
829        match c {
830            b'(' => depth += 1,
831            b')' => depth -= 1,
832            b'S' if depth == 0 && rest[i..].starts_with("SELECT ") => last = i,
833            _ => {}
834        }
835    }
836    (
837        format!("WITH {} ", rest[..last].trim_end()),
838        rest[last..].to_string(),
839    )
840}
841
842/// The producer id of OWL 2 RL materialization (SQL rules and `reasonable` alike).
843pub const OWL_PRODUCER: &str = "owl2rl";
844
845/// The id a producer name is stored under in `quads_inf_src` and `inf_producers`.
846pub fn producer_id(name: &str) -> i64 {
847    // A positive 62-bit hash: stable across runs and backends, computed without a lookup.
848    (xxhash_rust::xxh3::xxh3_64(name.as_bytes()) >> 2) as i64
849}
850
851/// Discards one producer's inferences: its attributions go, and so does every inferred quad no
852/// other producer derived. The producer is (re)registered under its name.
853pub fn inference_reset(name: &str) -> Vec<Statement> {
854    let id = producer_id(name);
855    vec![
856        Statement::new(format!(
857            "INSERT OR REPLACE INTO inf_producers(id, name) VALUES ({id}, '{}')",
858            name.replace('\'', "''")
859        )),
860        Statement::new(format!("DELETE FROM quads_inf_src WHERE src = {id}")),
861        Statement::new(
862            "DELETE FROM quads_inf WHERE NOT EXISTS (SELECT 1 FROM quads_inf_src q \
863             WHERE q.s = quads_inf.s AND q.p = quads_inf.p AND q.o = quads_inf.o AND q.g = quads_inf.g)",
864        ),
865    ]
866}
867
868/// Attributes every inferred quad no producer claims yet to `name`. Run after a producer
869/// writes, so what it derived (and nobody had derived before) is recorded as its own.
870pub fn inference_attribute(name: &str) -> Statement {
871    Statement::new(format!(
872        "INSERT OR IGNORE INTO quads_inf_src(src, s, p, o, g) SELECT {}, i.s, i.p, i.o, i.g FROM quads_inf i \
873         WHERE NOT EXISTS (SELECT 1 FROM quads_inf_src q WHERE q.s = i.s AND q.p = i.p AND q.o = i.o AND q.g = i.g)",
874        producer_id(name)
875    ))
876}
877
878/// Statements run before OWL 2 RL materialization (see [`materialize_reset_for`]).
879pub fn materialize_reset(caps: &crate::sql::Capabilities) -> Vec<Statement> {
880    materialize_reset_for(caps, OWL_PRODUCER)
881}
882
883/// Statements run before a producer materializes: its previous inferences are discarded, and
884/// the vocabulary that rule conclusions use (`owl:sameAs`, `rdfs:subClassOf`, …) gets its term
885/// rows, since it may not appear in the data.
886pub fn materialize_reset_for(caps: &crate::sql::Capabilities, producer: &str) -> Vec<Statement> {
887    let mut rows = crate::encoding::EncodedRows::default();
888    for iri in [
889        rdf::TYPE.as_str(),
890        rdfs::SUB_CLASS_OF.as_str(),
891        rdfs::SUB_PROPERTY_OF.as_str(),
892        rdfs::DOMAIN.as_str(),
893        rdfs::RANGE.as_str(),
894    ] {
895        rows.iri(iri);
896    }
897    for local in [
898        "sameAs",
899        "equivalentClass",
900        "equivalentProperty",
901        "Thing",
902        "Nothing",
903        "Class",
904    ] {
905        rows.iri(&format!("{OWL}{local}"));
906    }
907    rows.dedup();
908    let mut out = inference_reset(producer);
909    out.extend(crate::writer::term_statements(&rows, caps));
910    out
911}