Skip to main content

oxirs_rule/
owl_ql.rs

1//! # OWL 2 QL Profile — Perfect Reformulation Query Rewriter
2//!
3//! Implements the OWL 2 QL profile query rewriting algorithm (PerfectRef).
4//! OWL 2 QL is designed for query rewriting over relational/RDF data sources
5//! without materialization of the full closure.
6//!
7//! ## Complexity
8//! NLogSpace in the size of the data (ABox). TBox reasoning is polynomial.
9//!
10//! ## Supported Constructs
11//! - SubClassOf (atomic left-hand side)
12//! - SubObjectPropertyOf
13//! - EquivalentClasses / EquivalentObjectProperties
14//! - InverseObjectProperties (owl:inverseOf)
15//! - ObjectSomeValuesFrom on left (∃P.⊤ ⊑ C)
16//! - DisjointClasses / DisjointObjectProperties
17//! - ObjectPropertyDomain / ObjectPropertyRange
18//! - ObjectUnionOf on right-hand side (C ⊑ A ⊔ B) — union query rewriting
19//! - ObjectIntersectionOf on right-hand side concept normalization
20//!
21//! ## Algorithm: PerfectRef (extended for unions)
22//! For each atom in the query, compute all possible "unfoldings" via the TBox,
23//! then take the cross-product (conjunctive query union).
24//! Union axioms (C ⊑ A ⊔ B) are handled by branching: each branch of the union
25//! produces an independent conjunctive query in the resulting UCQ.
26//!
27//! ## Reference
28//! Calvanese, De Giacomo, Lembo, Lenzerini, Rosati:
29//! "Tractable Reasoning and Efficient Query Answering in Description Logics:
30//!  The DL-Lite Family" (Journal of Automated Reasoning, 2007).
31//! <https://www.w3.org/TR/owl2-profiles/#OWL_2_QL>
32
33use std::collections::{HashMap, HashSet, VecDeque};
34use thiserror::Error;
35
36/// Errors from OWL 2 QL query rewriting
37#[derive(Debug, Error, Clone, PartialEq)]
38pub enum QlError {
39    #[error("Invalid axiom: {0}")]
40    InvalidAxiom(String),
41
42    #[error("Inconsistent ontology: {0}")]
43    Inconsistency(String),
44
45    #[error("Rewriting limit exceeded (max {0} reformulations)")]
46    RewritingLimitExceeded(usize),
47
48    #[error("Unsupported construct in OWL 2 QL: {0}")]
49    UnsupportedConstruct(String),
50}
51
52/// A basic concept expression in OWL 2 QL
53/// (restricted to what QL allows on the left-hand side of SubClassOf)
54#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
55pub enum QlConcept {
56    /// owl:Thing
57    Top,
58    /// owl:Nothing
59    Bottom,
60    /// Named atomic class (IRI string)
61    Named(String),
62    /// ∃P.⊤ — existential with top filler (object side)
63    SomeValuesTop { property: String },
64    /// ∃P⁻.⊤ — existential with inverse property and top filler
65    SomeValuesTopInverse { property: String },
66}
67
68impl QlConcept {
69    /// Named concept constructor
70    pub fn named(iri: impl Into<String>) -> Self {
71        Self::Named(iri.into())
72    }
73
74    /// ∃P.⊤ constructor
75    pub fn some_values_top(property: impl Into<String>) -> Self {
76        Self::SomeValuesTop {
77            property: property.into(),
78        }
79    }
80
81    /// ∃P⁻.⊤ constructor
82    pub fn some_values_top_inverse(property: impl Into<String>) -> Self {
83        Self::SomeValuesTopInverse {
84            property: property.into(),
85        }
86    }
87
88    /// Return atomic name if Named
89    pub fn as_named(&self) -> Option<&str> {
90        if let Self::Named(n) = self {
91            Some(n)
92        } else {
93            None
94        }
95    }
96
97    /// Return property name if SomeValuesTop or SomeValuesTopInverse
98    pub fn as_some_values_property(&self) -> Option<(&str, bool)> {
99        match self {
100            Self::SomeValuesTop { property } => Some((property, false)),
101            Self::SomeValuesTopInverse { property } => Some((property, true)),
102            _ => None,
103        }
104    }
105}
106
107/// A rich concept expression supporting union and intersection on the RHS of axioms.
108///
109/// OWL 2 QL restricts the LHS of SubClassOf to basic concepts (QlConcept),
110/// but allows union and intersection on the RHS for certain patterns.
111/// This type is used for union axiom RHS expressions.
112#[derive(Debug, Clone, PartialEq, Eq, Hash)]
113pub enum ConceptExpr {
114    /// Atomic or basic concept
115    Atomic(QlConcept),
116    /// owl:ObjectUnionOf(C1, C2, ...) — disjunction
117    Union(Vec<ConceptExpr>),
118    /// owl:ObjectIntersectionOf(C1, C2, ...) — conjunction
119    Intersection(Vec<ConceptExpr>),
120}
121
122impl ConceptExpr {
123    /// Construct from a named class
124    pub fn named(iri: impl Into<String>) -> Self {
125        Self::Atomic(QlConcept::Named(iri.into()))
126    }
127
128    /// Construct a union of named classes
129    pub fn union_of(members: Vec<ConceptExpr>) -> Self {
130        Self::Union(members)
131    }
132
133    /// Construct an intersection of named classes
134    pub fn intersection_of(members: Vec<ConceptExpr>) -> Self {
135        Self::Intersection(members)
136    }
137
138    /// Flatten this expression into a list of named-class sets per union axiom.
139    ///
140    /// The return value is a `Vec<Vec<String>>` where each inner `Vec<String>` is the
141    /// complete set of sibling disjuncts for ONE union expression:
142    ///
143    /// - `Atomic("A")` → `[["A"]]`  (one "union" with just A)
144    /// - `Union(["A", "B"])` → `[["A", "B"]]`  (one union with disjuncts A and B)
145    /// - `Intersection(["A", "B"])` → `[["A", "B"]]`  (conjunction treated as one entry)
146    ///
147    /// For nested unions `Union([Union(["A", "B"]), "C"])` → `[["A", "B", "C"]]` (all flattened).
148    pub fn union_branches(&self) -> Vec<Vec<String>> {
149        match self {
150            ConceptExpr::Atomic(QlConcept::Named(n)) => vec![vec![n.clone()]],
151            ConceptExpr::Atomic(_) => vec![],
152            ConceptExpr::Union(members) => {
153                // Collect ALL atomic names across all members into a single disjunct list
154                let all_names: Vec<String> =
155                    members.iter().flat_map(|m| m.atomic_names()).collect();
156                if all_names.is_empty() {
157                    vec![]
158                } else {
159                    vec![all_names]
160                }
161            }
162            ConceptExpr::Intersection(members) => {
163                // Intersection on RHS: C ⊑ A ⊓ B is C ⊑ A ∧ C ⊑ B
164                // Each intersection member must hold — not a union branching scenario
165                // Return as a single conjunctive constraint
166                let names: Vec<String> = members.iter().flat_map(|m| m.atomic_names()).collect();
167                if names.is_empty() {
168                    vec![]
169                } else {
170                    vec![names]
171                }
172            }
173        }
174    }
175
176    /// Return all atomic class names in this expression (flattened)
177    pub fn atomic_names(&self) -> Vec<String> {
178        match self {
179            ConceptExpr::Atomic(QlConcept::Named(n)) => vec![n.clone()],
180            ConceptExpr::Atomic(_) => vec![],
181            ConceptExpr::Union(members) | ConceptExpr::Intersection(members) => {
182                members.iter().flat_map(|m| m.atomic_names()).collect()
183            }
184        }
185    }
186
187    /// Returns true if this expression contains any union
188    pub fn has_union(&self) -> bool {
189        matches!(self, ConceptExpr::Union(_))
190    }
191}
192
193/// A role (property) expression in OWL 2 QL
194#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
195pub enum QlRole {
196    /// Named object property (IRI string)
197    Named(String),
198    /// Inverse of a named property
199    Inverse(String),
200}
201
202impl QlRole {
203    pub fn named(iri: impl Into<String>) -> Self {
204        Self::Named(iri.into())
205    }
206
207    pub fn inverse(iri: impl Into<String>) -> Self {
208        Self::Inverse(iri.into())
209    }
210
211    /// Return the base property name regardless of inversion
212    pub fn base_name(&self) -> &str {
213        match self {
214            Self::Named(n) => n,
215            Self::Inverse(n) => n,
216        }
217    }
218
219    /// Return whether this is an inverse role
220    pub fn is_inverse(&self) -> bool {
221        matches!(self, Self::Inverse(_))
222    }
223
224    /// Return the inverse of this role
225    pub fn inverse_role(&self) -> Self {
226        match self {
227            Self::Named(n) => Self::Inverse(n.clone()),
228            Self::Inverse(n) => Self::Named(n.clone()),
229        }
230    }
231}
232
233/// OWL 2 QL TBox axioms (restricted to QL-supported constructs)
234#[derive(Debug, Clone, PartialEq)]
235pub enum QlAxiom {
236    /// C1 ⊑ C2: subclass (C1 must be atomic or ∃P.⊤; C2 must be atomic or ∃P.C)
237    SubClassOf { sub: QlConcept, sup: QlConcept },
238
239    /// C ⊑ (A ⊔ B ⊔ ...): union on right-hand side (subclass of union)
240    /// This is the key axiom for union query rewriting.
241    /// When rewriting a query ?x:A, we can use ?x:C if C ⊑ A ⊔ B and we also check B.
242    SubClassOfUnion {
243        sub: QlConcept,
244        sup_union: ConceptExpr,
245    },
246
247    /// C1 ≡ C2: equivalent classes (expanded into two SubClassOf)
248    EquivalentClasses(QlConcept, QlConcept),
249
250    /// R1 ⊑ R2: sub-object-property
251    SubObjectPropertyOf { sub: QlRole, sup: QlRole },
252
253    /// R1 ≡ R2: equivalent properties
254    EquivalentObjectProperties(QlRole, QlRole),
255
256    /// R1 owl:inverseOf R2
257    InverseObjectProperties(String, String),
258
259    /// owl:ObjectPropertyDomain(P, C): ∃P.⊤ ⊑ C
260    ObjectPropertyDomain { property: String, domain: String },
261
262    /// owl:ObjectPropertyRange(P, C): ∃P⁻.⊤ ⊑ C (range from object side)
263    ObjectPropertyRange { property: String, range: String },
264
265    /// DisjointClasses(C1, C2): C1 ⊓ C2 ⊑ ⊥
266    DisjointClasses(QlConcept, QlConcept),
267
268    /// DisjointObjectProperties(P1, P2)
269    DisjointObjectProperties(QlRole, QlRole),
270}
271
272/// A query atom representing a triple pattern
273/// Variables are represented by names starting with '?' or just as strings,
274/// constants are IRIs/literals. We keep it as a simple enum.
275#[derive(Debug, Clone, PartialEq, Eq, Hash)]
276pub enum QueryAtom {
277    /// rdf:type assertion: ?x rdf:type C or :ind rdf:type C
278    TypeAtom {
279        individual: QueryTerm,
280        class: String,
281    },
282    /// Property triple: ?x P ?y or constants
283    PropertyAtom {
284        subject: QueryTerm,
285        property: String,
286        object: QueryTerm,
287    },
288}
289
290impl QueryAtom {
291    /// Constructor for property triple
292    pub fn property_atom(
293        subject: QueryTerm,
294        property: impl Into<String>,
295        object: QueryTerm,
296    ) -> Self {
297        Self::PropertyAtom {
298            subject,
299            property: property.into(),
300            object,
301        }
302    }
303
304    /// Return all variable names used in this atom
305    pub fn variables(&self) -> Vec<&str> {
306        match self {
307            Self::TypeAtom { individual, .. } => individual.as_variable().into_iter().collect(),
308            Self::PropertyAtom {
309                subject, object, ..
310            } => {
311                let mut vars = Vec::new();
312                if let Some(v) = subject.as_variable() {
313                    vars.push(v);
314                }
315                if let Some(v) = object.as_variable() {
316                    vars.push(v);
317                }
318                vars
319            }
320        }
321    }
322}
323
324/// A term in a query atom — either a variable or a constant (IRI/literal)
325#[derive(Debug, Clone, PartialEq, Eq, Hash)]
326pub enum QueryTerm {
327    /// A variable (name without the '?' prefix, stored as plain string)
328    Variable(String),
329    /// A constant IRI or literal
330    Constant(String),
331}
332
333impl QueryTerm {
334    pub fn var(name: impl Into<String>) -> Self {
335        Self::Variable(name.into())
336    }
337
338    pub fn constant(iri: impl Into<String>) -> Self {
339        Self::Constant(iri.into())
340    }
341
342    pub fn as_variable(&self) -> Option<&str> {
343        if let Self::Variable(v) = self {
344            Some(v)
345        } else {
346            None
347        }
348    }
349
350    pub fn as_constant(&self) -> Option<&str> {
351        if let Self::Constant(c) = self {
352            Some(c)
353        } else {
354            None
355        }
356    }
357
358    pub fn is_variable(&self) -> bool {
359        matches!(self, Self::Variable(_))
360    }
361}
362
363/// A conjunctive query — a list of atoms to be answered conjunctively
364#[derive(Debug, Clone, PartialEq, Eq)]
365pub struct ConjunctiveQuery {
366    /// The atoms of the query
367    pub atoms: Vec<QueryAtom>,
368    /// Head variables (distinguished variables)
369    pub head_variables: Vec<String>,
370}
371
372impl ConjunctiveQuery {
373    pub fn new(atoms: Vec<QueryAtom>, head_variables: Vec<String>) -> Self {
374        Self {
375            atoms,
376            head_variables,
377        }
378    }
379
380    pub fn with_atoms(atoms: Vec<QueryAtom>) -> Self {
381        let head_variables = Self::collect_variables(&atoms);
382        Self {
383            atoms,
384            head_variables,
385        }
386    }
387
388    fn collect_variables(atoms: &[QueryAtom]) -> Vec<String> {
389        let mut seen = HashSet::new();
390        let mut vars = Vec::new();
391        for atom in atoms {
392            for v in atom.variables() {
393                if seen.insert(v.to_string()) {
394                    vars.push(v.to_string());
395                }
396            }
397        }
398        vars
399    }
400}
401
402/// A rewritten query — a union of conjunctive queries
403#[derive(Debug, Clone)]
404pub struct RewrittenQuery {
405    /// The list of conjunctive queries in the union (UCQ)
406    pub queries: Vec<ConjunctiveQuery>,
407}
408
409impl RewrittenQuery {
410    pub fn new() -> Self {
411        Self {
412            queries: Vec::new(),
413        }
414    }
415
416    pub fn add(&mut self, cq: ConjunctiveQuery) {
417        self.queries.push(cq);
418    }
419
420    pub fn len(&self) -> usize {
421        self.queries.len()
422    }
423
424    pub fn is_empty(&self) -> bool {
425        self.queries.is_empty()
426    }
427}
428
429impl Default for RewrittenQuery {
430    fn default() -> Self {
431        Self::new()
432    }
433}
434
435/// Precomputed union axiom entry: sub ⊑ (A₁ ⊔ A₂ ⊔ ...)
436/// Maps a sub-concept name to all its union superclass branches.
437/// If C ⊑ A ⊔ B, then union_axioms["C"] = [["A", "B"]].
438/// If C ⊑ (A ⊔ B) ⊓ D, this is split into: C ⊑ A ⊔ B and C ⊑ D.
439#[derive(Debug, Clone)]
440pub struct UnionAxiomEntry {
441    /// Sub-concept name (LHS of axiom)
442    pub sub_class: String,
443    /// Disjuncts on the RHS — each is a list of class names forming one union branch.
444    /// e.g. for C ⊑ A ⊔ B: branches = ["A", "B"]
445    pub disjuncts: Vec<String>,
446}
447
448/// The OWL 2 QL TBox — stores all axioms and pre-computes hierarchies
449pub struct Owl2QLTBox {
450    axioms: Vec<QlAxiom>,
451    /// Precomputed: class -> set of superclasses (transitive closure)
452    class_supers: HashMap<String, HashSet<String>>,
453    /// Precomputed: class -> set of subclasses
454    class_subs: HashMap<String, HashSet<String>>,
455    /// Precomputed: property -> set of superproperties (transitive closure)
456    prop_supers: HashMap<String, HashSet<String>>,
457    /// Precomputed: inverse property -> set of superproperties
458    inv_prop_supers: HashMap<String, HashSet<String>>,
459    /// Precomputed: property -> set of inverse properties
460    inverse_of: HashMap<String, HashSet<String>>,
461    /// Precomputed: class -> set of concepts that subsume ∃P.⊤ for each P
462    /// i.e. if ∃P.⊤ ⊑ C then domain_classes[P] contains C
463    domain_classes: HashMap<String, HashSet<String>>,
464    /// Precomputed: range classes — ∃P⁻.⊤ ⊑ C → range_classes[P] contains C
465    range_classes: HashMap<String, HashSet<String>>,
466    /// Disjoint class pairs
467    disjoint_classes: HashSet<(String, String)>,
468    /// Union axiom entries: class C has a union superclass A ⊔ B ⊔ ...
469    /// Maps class name → list of union axiom entries for that class.
470    union_axioms: HashMap<String, Vec<Vec<String>>>,
471    /// Reverse union index: class A is a disjunct in union axioms for C
472    /// Maps A → set of classes C such that C ⊑ (... ⊔ A ⊔ ...)
473    union_rev_index: HashMap<String, HashSet<String>>,
474}
475
476impl Owl2QLTBox {
477    /// Create a new empty TBox
478    pub fn new() -> Self {
479        Self {
480            axioms: Vec::new(),
481            class_supers: HashMap::new(),
482            class_subs: HashMap::new(),
483            prop_supers: HashMap::new(),
484            inv_prop_supers: HashMap::new(),
485            inverse_of: HashMap::new(),
486            domain_classes: HashMap::new(),
487            range_classes: HashMap::new(),
488            disjoint_classes: HashSet::new(),
489            union_axioms: HashMap::new(),
490            union_rev_index: HashMap::new(),
491        }
492    }
493
494    /// Add an axiom to the TBox
495    pub fn add_axiom(&mut self, axiom: QlAxiom) {
496        self.axioms.push(axiom);
497    }
498
499    /// Add multiple axioms
500    pub fn add_axioms(&mut self, axioms: impl IntoIterator<Item = QlAxiom>) {
501        for axiom in axioms {
502            self.add_axiom(axiom);
503        }
504    }
505
506    /// Finalize and compute transitive closures. Must be called before rewriting.
507    pub fn classify(&mut self) -> Result<(), QlError> {
508        self.expand_equivalences();
509        self.build_inverse_index();
510        self.compute_class_hierarchy()?;
511        self.compute_property_hierarchy()?;
512        self.compute_domain_range();
513        self.compute_disjointness();
514        self.compute_union_index();
515        Ok(())
516    }
517
518    // ---- internal build steps ----
519
520    fn expand_equivalences(&mut self) {
521        let mut extra = Vec::new();
522        for axiom in &self.axioms {
523            match axiom {
524                QlAxiom::EquivalentClasses(c1, c2) => {
525                    extra.push(QlAxiom::SubClassOf {
526                        sub: c1.clone(),
527                        sup: c2.clone(),
528                    });
529                    extra.push(QlAxiom::SubClassOf {
530                        sub: c2.clone(),
531                        sup: c1.clone(),
532                    });
533                }
534                QlAxiom::EquivalentObjectProperties(r1, r2) => {
535                    extra.push(QlAxiom::SubObjectPropertyOf {
536                        sub: r1.clone(),
537                        sup: r2.clone(),
538                    });
539                    extra.push(QlAxiom::SubObjectPropertyOf {
540                        sub: r2.clone(),
541                        sup: r1.clone(),
542                    });
543                }
544                QlAxiom::InverseObjectProperties(p1, p2) => {
545                    // P1 inverseOf P2 → P1 ⊑ P2⁻ and P2⁻ ⊑ P1
546                    extra.push(QlAxiom::SubObjectPropertyOf {
547                        sub: QlRole::Named(p1.clone()),
548                        sup: QlRole::Inverse(p2.clone()),
549                    });
550                    extra.push(QlAxiom::SubObjectPropertyOf {
551                        sub: QlRole::Named(p2.clone()),
552                        sup: QlRole::Inverse(p1.clone()),
553                    });
554                }
555                _ => {}
556            }
557        }
558        self.axioms.extend(extra);
559    }
560
561    fn build_inverse_index(&mut self) {
562        for axiom in &self.axioms {
563            if let QlAxiom::InverseObjectProperties(p1, p2) = axiom {
564                self.inverse_of
565                    .entry(p1.clone())
566                    .or_default()
567                    .insert(p2.clone());
568                self.inverse_of
569                    .entry(p2.clone())
570                    .or_default()
571                    .insert(p1.clone());
572            }
573        }
574    }
575
576    fn compute_class_hierarchy(&mut self) -> Result<(), QlError> {
577        // Collect direct subclass edges from axioms
578        // sub → sup edges
579        let mut direct: HashMap<String, HashSet<String>> = HashMap::new();
580
581        for axiom in &self.axioms {
582            if let QlAxiom::SubClassOf { sub, sup } = axiom {
583                // Only atomic ↦ atomic subclass edges for the class hierarchy
584                // ∃P.⊤ ⊑ C is handled via domain
585                if let (Some(sub_name), Some(sup_name)) = (sub.as_named(), sup.as_named()) {
586                    direct
587                        .entry(sub_name.to_string())
588                        .or_default()
589                        .insert(sup_name.to_string());
590                }
591            }
592        }
593
594        // Transitive closure via BFS/DFS from each class
595        let all_classes: HashSet<String> = direct
596            .keys()
597            .chain(direct.values().flat_map(|s| s.iter()))
598            .cloned()
599            .collect();
600
601        for class in &all_classes {
602            let supers = Self::transitive_closure(class, &direct);
603            self.class_supers.insert(class.clone(), supers);
604        }
605
606        // Build reverse (subs)
607        for (sub, supers) in &self.class_supers {
608            for sup in supers {
609                self.class_subs
610                    .entry(sup.clone())
611                    .or_default()
612                    .insert(sub.clone());
613            }
614        }
615
616        Ok(())
617    }
618
619    fn compute_property_hierarchy(&mut self) -> Result<(), QlError> {
620        // Collect direct sub-role edges for named and inverse roles
621        // named_direct[P] = set of named Q where P ⊑ Q
622        let mut named_direct: HashMap<String, HashSet<String>> = HashMap::new();
623        // inv_direct[P] = set of named Q where P⁻ ⊑ Q (P⁻ is subproperty of Q)
624        let mut inv_direct: HashMap<String, HashSet<String>> = HashMap::new();
625
626        for axiom in &self.axioms {
627            if let QlAxiom::SubObjectPropertyOf { sub, sup } = axiom {
628                match (sub, sup) {
629                    (QlRole::Named(p), QlRole::Named(q)) => {
630                        named_direct.entry(p.clone()).or_default().insert(q.clone());
631                    }
632                    (QlRole::Inverse(p), QlRole::Named(q)) => {
633                        // P⁻ ⊑ Q means if (x,y):P then (y,x):Q
634                        inv_direct.entry(p.clone()).or_default().insert(q.clone());
635                    }
636                    (QlRole::Named(p), QlRole::Inverse(q)) => {
637                        // P ⊑ Q⁻ — store that when querying Q⁻ we include P
638                        // This means inverse(Q) is a superproperty of P
639                        // We track: inv_supers of P contains q
640                        inv_direct.entry(p.clone()).or_default().insert(q.clone());
641                    }
642                    (QlRole::Inverse(p), QlRole::Inverse(q)) => {
643                        // P⁻ ⊑ Q⁻ ↔ Q ⊑ P
644                        named_direct.entry(q.clone()).or_default().insert(p.clone());
645                    }
646                }
647            }
648        }
649
650        let all_named: HashSet<String> = named_direct
651            .keys()
652            .chain(named_direct.values().flat_map(|s| s.iter()))
653            .chain(inv_direct.keys())
654            .chain(inv_direct.values().flat_map(|s| s.iter()))
655            .cloned()
656            .collect();
657
658        for prop in &all_named {
659            let supers = Self::transitive_closure(prop, &named_direct);
660            self.prop_supers.insert(prop.clone(), supers);
661        }
662
663        for prop in &all_named {
664            let inv_supers = Self::transitive_closure(prop, &inv_direct);
665            self.inv_prop_supers.insert(prop.clone(), inv_supers);
666        }
667
668        Ok(())
669    }
670
671    fn compute_domain_range(&mut self) {
672        for axiom in &self.axioms {
673            match axiom {
674                QlAxiom::ObjectPropertyDomain { property, domain } => {
675                    self.domain_classes
676                        .entry(property.clone())
677                        .or_default()
678                        .insert(domain.clone());
679                }
680                QlAxiom::ObjectPropertyRange { property, range } => {
681                    self.range_classes
682                        .entry(property.clone())
683                        .or_default()
684                        .insert(range.clone());
685                }
686                QlAxiom::SubClassOf { sub, sup } => {
687                    // ∃P.⊤ ⊑ C → domain
688                    if let (QlConcept::SomeValuesTop { property }, Some(sup_name)) =
689                        (sub, sup.as_named())
690                    {
691                        self.domain_classes
692                            .entry(property.clone())
693                            .or_default()
694                            .insert(sup_name.to_string());
695                    }
696                    // ∃P⁻.⊤ ⊑ C → range
697                    if let (QlConcept::SomeValuesTopInverse { property }, Some(sup_name)) =
698                        (sub, sup.as_named())
699                    {
700                        self.range_classes
701                            .entry(property.clone())
702                            .or_default()
703                            .insert(sup_name.to_string());
704                    }
705                }
706                _ => {}
707            }
708        }
709
710        // Propagate through property hierarchy:
711        // If P ⊑ Q and Q has domain D, then P also has domain D
712        let props: Vec<String> = self.prop_supers.keys().cloned().collect();
713        for prop in props {
714            let supers: Vec<String> = self
715                .prop_supers
716                .get(&prop)
717                .cloned()
718                .unwrap_or_default()
719                .into_iter()
720                .collect();
721            for sup in supers {
722                let domains: Vec<String> = self
723                    .domain_classes
724                    .get(&sup)
725                    .cloned()
726                    .unwrap_or_default()
727                    .into_iter()
728                    .collect();
729                for d in domains {
730                    self.domain_classes
731                        .entry(prop.clone())
732                        .or_default()
733                        .insert(d);
734                }
735                let ranges: Vec<String> = self
736                    .range_classes
737                    .get(&sup)
738                    .cloned()
739                    .unwrap_or_default()
740                    .into_iter()
741                    .collect();
742                for r in ranges {
743                    self.range_classes
744                        .entry(prop.clone())
745                        .or_default()
746                        .insert(r);
747                }
748            }
749        }
750
751        // Propagate through class hierarchy: if D ⊑ E and P has domain D, then P has domain E
752        let domain_entries: Vec<(String, Vec<String>)> = self
753            .domain_classes
754            .iter()
755            .map(|(k, v)| (k.clone(), v.iter().cloned().collect()))
756            .collect();
757        for (prop, domains) in domain_entries {
758            let mut extra = Vec::new();
759            for d in &domains {
760                if let Some(supers) = self.class_supers.get(d) {
761                    extra.extend(supers.iter().cloned());
762                }
763            }
764            let entry = self.domain_classes.entry(prop).or_default();
765            for e in extra {
766                entry.insert(e);
767            }
768        }
769        let range_entries: Vec<(String, Vec<String>)> = self
770            .range_classes
771            .iter()
772            .map(|(k, v)| (k.clone(), v.iter().cloned().collect()))
773            .collect();
774        for (prop, ranges) in range_entries {
775            let mut extra = Vec::new();
776            for r in &ranges {
777                if let Some(supers) = self.class_supers.get(r) {
778                    extra.extend(supers.iter().cloned());
779                }
780            }
781            let entry = self.range_classes.entry(prop).or_default();
782            for e in extra {
783                entry.insert(e);
784            }
785        }
786    }
787
788    fn compute_disjointness(&mut self) {
789        for axiom in &self.axioms {
790            if let QlAxiom::DisjointClasses(c1, c2) = axiom {
791                if let (Some(n1), Some(n2)) = (c1.as_named(), c2.as_named()) {
792                    let (a, b) = if n1 <= n2 {
793                        (n1.to_string(), n2.to_string())
794                    } else {
795                        (n2.to_string(), n1.to_string())
796                    };
797                    self.disjoint_classes.insert((a, b));
798                }
799            }
800        }
801    }
802
803    /// Compute and index union axioms.
804    ///
805    /// For each `SubClassOfUnion { sub, sup_union }` axiom where `sub` is a named class C
806    /// and `sup_union` is an `ObjectUnionOf(A, B, ...)`, we record:
807    ///   union_axioms[C] = [[A, B, ...], ...]
808    ///   union_rev_index[A] = {C, ...}   (and same for B, etc.)
809    ///
810    /// This enables two kinds of union-based rewriting:
811    ///
812    /// 1. **Forward (concept subsumption)**: C ⊑ A ⊔ B means "if something is C,
813    ///    it must be A or B". Used when we want to find what is subsumed.
814    ///
815    /// 2. **Backward (query unfolding)**: When rewriting ?x:A, we can generate
816    ///    a new CQ branch ?x:C if C ⊑ A ⊔ B — because any C individual that is
817    ///    an A will satisfy the query (even though some C individuals may be B).
818    ///    Note: this is conservative (sound but not always complete without ABox checking).
819    fn compute_union_index(&mut self) {
820        for axiom in &self.axioms {
821            if let QlAxiom::SubClassOfUnion { sub, sup_union } = axiom {
822                if let Some(sub_name) = sub.as_named() {
823                    let branches = sup_union.union_branches();
824                    for branch in &branches {
825                        // branch is a list of disjunct class names
826                        self.union_axioms
827                            .entry(sub_name.to_string())
828                            .or_default()
829                            .push(branch.clone());
830                        // Build reverse index for each disjunct
831                        for disjunct in branch {
832                            self.union_rev_index
833                                .entry(disjunct.clone())
834                                .or_default()
835                                .insert(sub_name.to_string());
836                        }
837                    }
838                }
839            }
840        }
841    }
842
843    /// Compute transitive closure from `start` following `edges`
844    fn transitive_closure(
845        start: &str,
846        edges: &HashMap<String, HashSet<String>>,
847    ) -> HashSet<String> {
848        let mut visited = HashSet::new();
849        let mut queue = VecDeque::new();
850        if let Some(direct) = edges.get(start) {
851            for d in direct {
852                queue.push_back(d.clone());
853            }
854        }
855        while let Some(node) = queue.pop_front() {
856            if visited.insert(node.clone()) {
857                if let Some(nexts) = edges.get(&node) {
858                    for n in nexts {
859                        if !visited.contains(n) {
860                            queue.push_back(n.clone());
861                        }
862                    }
863                }
864            }
865        }
866        visited
867    }
868
869    // ---- public query methods ----
870
871    /// Return all superclasses of `class` (transitive, not including self)
872    pub fn superclasses(&self, class: &str) -> HashSet<String> {
873        self.class_supers.get(class).cloned().unwrap_or_default()
874    }
875
876    /// Return all subclasses of `class` (transitive, not including self)
877    pub fn subclasses(&self, class: &str) -> HashSet<String> {
878        self.class_subs.get(class).cloned().unwrap_or_default()
879    }
880
881    /// Return all named superproperties of `property` (transitive, not including self)
882    pub fn superproperties(&self, property: &str) -> HashSet<String> {
883        self.prop_supers.get(property).cloned().unwrap_or_default()
884    }
885
886    /// Return all inverse properties of `property`
887    pub fn inverse_properties(&self, property: &str) -> HashSet<String> {
888        self.inverse_of.get(property).cloned().unwrap_or_default()
889    }
890
891    /// Return domain classes for `property`
892    pub fn domain_of(&self, property: &str) -> HashSet<String> {
893        self.domain_classes
894            .get(property)
895            .cloned()
896            .unwrap_or_default()
897    }
898
899    /// Return range classes for `property`
900    pub fn range_of(&self, property: &str) -> HashSet<String> {
901        self.range_classes
902            .get(property)
903            .cloned()
904            .unwrap_or_default()
905    }
906
907    /// Return true if two classes are declared disjoint
908    pub fn are_disjoint(&self, c1: &str, c2: &str) -> bool {
909        let (a, b) = if c1 <= c2 {
910            (c1.to_string(), c2.to_string())
911        } else {
912            (c2.to_string(), c1.to_string())
913        };
914        self.disjoint_classes.contains(&(a, b))
915    }
916
917    /// Given class C, find all subclasses B such that B ⊑ C (including C itself)
918    /// This is used to unfold a type atom: instead of ?x:C, we may use ?x:B for any B⊑C
919    pub fn all_subsumed_by(&self, class: &str) -> HashSet<String> {
920        let mut result = HashSet::new();
921        result.insert(class.to_string());
922        if let Some(subs) = self.class_subs.get(class) {
923            result.extend(subs.iter().cloned());
924        }
925        result
926    }
927
928    /// Given property P, find all sub-properties Q such that Q ⊑ P
929    /// Includes inverse-based expansions
930    pub fn all_subproperties_of(&self, property: &str, is_inverse: bool) -> Vec<QlRole> {
931        let mut result = Vec::new();
932        // The property itself
933        if is_inverse {
934            result.push(QlRole::Inverse(property.to_string()));
935        } else {
936            result.push(QlRole::Named(property.to_string()));
937        }
938
939        // Collect subproperties: Q such that Q ⊑ P
940        for axiom in &self.axioms {
941            if let QlAxiom::SubObjectPropertyOf { sub, sup } = axiom {
942                match (sub, sup) {
943                    (QlRole::Named(q), QlRole::Named(p)) if p == property && !is_inverse => {
944                        result.push(QlRole::Named(q.clone()));
945                    }
946                    (QlRole::Inverse(q), QlRole::Named(p)) if p == property && !is_inverse => {
947                        result.push(QlRole::Inverse(q.clone()));
948                    }
949                    (QlRole::Named(q), QlRole::Inverse(p)) if p == property && is_inverse => {
950                        result.push(QlRole::Named(q.clone()));
951                    }
952                    (QlRole::Inverse(q), QlRole::Inverse(p)) if p == property && is_inverse => {
953                        result.push(QlRole::Inverse(q.clone()));
954                    }
955                    _ => {}
956                }
957            }
958        }
959
960        // Also include via inverse_of: if P inverseOf Q, querying P⁻ can use Q
961        if !is_inverse {
962            if let Some(invs) = self.inverse_of.get(property) {
963                for inv in invs {
964                    result.push(QlRole::Inverse(inv.clone()));
965                }
966            }
967        } else if let Some(invs) = self.inverse_of.get(property) {
968            for inv in invs {
969                result.push(QlRole::Named(inv.clone()));
970            }
971        }
972
973        // Deduplicate
974        result.sort();
975        result.dedup();
976        result
977    }
978
979    /// Return the union axiom entries for class `sub_class`:
980    /// all sets of disjuncts D such that sub_class ⊑ D₁ ⊔ D₂ ⊔ ...
981    pub fn union_axiom_disjuncts(&self, sub_class: &str) -> Vec<Vec<String>> {
982        self.union_axioms
983            .get(sub_class)
984            .cloned()
985            .unwrap_or_default()
986    }
987
988    /// Return all classes C such that C ⊑ (... ⊔ class ⊔ ...)
989    /// i.e., classes whose union superclass includes `class` as a disjunct.
990    pub fn classes_with_union_disjunct(&self, class: &str) -> HashSet<String> {
991        self.union_rev_index.get(class).cloned().unwrap_or_default()
992    }
993
994    /// Return true if there are any union axioms in this TBox
995    pub fn has_union_axioms(&self) -> bool {
996        !self.union_axioms.is_empty()
997    }
998}
999
1000impl Default for Owl2QLTBox {
1001    fn default() -> Self {
1002        Self::new()
1003    }
1004}
1005
1006/// The PerfectRef query rewriter for OWL 2 QL
1007///
1008/// Extended to handle union query rewriting (ObjectUnionOf on RHS of SubClassOf).
1009pub struct QueryRewriter {
1010    tbox: Owl2QLTBox,
1011    /// Maximum number of rewritten queries to generate (safety limit)
1012    max_rewrites: usize,
1013}
1014
1015impl QueryRewriter {
1016    /// Create a new rewriter with a classified TBox
1017    pub fn new(tbox: Owl2QLTBox) -> Self {
1018        Self {
1019            tbox,
1020            max_rewrites: 10_000,
1021        }
1022    }
1023
1024    /// Create with a custom rewriting limit
1025    pub fn with_limit(tbox: Owl2QLTBox, max_rewrites: usize) -> Self {
1026        Self { tbox, max_rewrites }
1027    }
1028
1029    /// Access the underlying TBox
1030    pub fn tbox(&self) -> &Owl2QLTBox {
1031        &self.tbox
1032    }
1033
1034    /// Main entry point: rewrite a conjunctive query using PerfectRef algorithm.
1035    ///
1036    /// Returns a `RewrittenQuery` (UCQ) that is equivalent to the original CQ
1037    /// over any ABox consistent with the TBox.
1038    ///
1039    /// This extended version handles:
1040    /// - Standard subclass/subproperty/domain/range unfolding
1041    /// - Union axiom branching: C ⊑ A ⊔ B causes the type atom ?x:A to gain
1042    ///   an additional CQ branch where ?x:C replaces the atom (since C individuals
1043    ///   that are A will satisfy the query).
1044    pub fn rewrite_query(&self, query: &ConjunctiveQuery) -> Result<RewrittenQuery, QlError> {
1045        // PerfectRef works by:
1046        // 1. Start with a queue of CQs (initially just the original)
1047        // 2. For each CQ, for each atom, compute all possible unfoldings
1048        // 3. Replace the atom with each unfolding, producing new CQs
1049        // 4. Add new CQs that haven't been seen before
1050        // 5. Repeat until fixpoint
1051        let mut result = RewrittenQuery::new();
1052        let mut seen: HashSet<Vec<QueryAtom>> = HashSet::new();
1053        let mut worklist: VecDeque<ConjunctiveQuery> = VecDeque::new();
1054
1055        worklist.push_back(query.clone());
1056
1057        while let Some(current_cq) = worklist.pop_front() {
1058            let mut canonical = current_cq.atoms.clone();
1059            canonical.sort_by(|a, b| format!("{a:?}").cmp(&format!("{b:?}")));
1060
1061            if !seen.insert(canonical) {
1062                continue;
1063            }
1064
1065            // Generate all unfoldings of this CQ (one atom at a time)
1066            for atom_idx in 0..current_cq.atoms.len() {
1067                let unfolded_atoms = self.unfold_atom(&current_cq.atoms[atom_idx]);
1068
1069                for unfolded_atom in unfolded_atoms {
1070                    // Skip if same as original (no change)
1071                    if unfolded_atom == current_cq.atoms[atom_idx] {
1072                        continue;
1073                    }
1074
1075                    // Build new CQ with the atom replaced
1076                    let mut new_atoms = current_cq.atoms.clone();
1077                    new_atoms[atom_idx] = unfolded_atom;
1078                    let new_cq = ConjunctiveQuery {
1079                        atoms: new_atoms,
1080                        head_variables: current_cq.head_variables.clone(),
1081                    };
1082
1083                    let mut new_canonical = new_cq.atoms.clone();
1084                    new_canonical.sort_by(|a, b| format!("{a:?}").cmp(&format!("{b:?}")));
1085
1086                    if !seen.contains(&new_canonical) {
1087                        if seen.len() + worklist.len() >= self.max_rewrites {
1088                            return Err(QlError::RewritingLimitExceeded(self.max_rewrites));
1089                        }
1090                        worklist.push_back(new_cq);
1091                    }
1092                }
1093
1094                // Union-based unfolding: produce additional CQ branches
1095                // for type atoms where there are union axioms
1096                if let QueryAtom::TypeAtom { individual, class } = &current_cq.atoms[atom_idx] {
1097                    let union_branches = self.unfold_type_atom_union_branches(individual, class);
1098                    for branch_atoms in union_branches {
1099                        let mut new_atoms = current_cq.atoms.clone();
1100                        new_atoms[atom_idx] = branch_atoms;
1101                        let new_cq = ConjunctiveQuery {
1102                            atoms: new_atoms,
1103                            head_variables: current_cq.head_variables.clone(),
1104                        };
1105                        let mut new_canonical = new_cq.atoms.clone();
1106                        new_canonical.sort_by(|a, b| format!("{a:?}").cmp(&format!("{b:?}")));
1107                        if !seen.contains(&new_canonical) {
1108                            if seen.len() + worklist.len() >= self.max_rewrites {
1109                                return Err(QlError::RewritingLimitExceeded(self.max_rewrites));
1110                            }
1111                            worklist.push_back(new_cq);
1112                        }
1113                    }
1114                }
1115            }
1116
1117            // This CQ is part of the final rewriting
1118            result.add(current_cq);
1119        }
1120
1121        Ok(result)
1122    }
1123
1124    /// Unfold a single query atom using TBox axioms.
1125    /// Returns all possible alternative atoms (including the original).
1126    pub fn unfold_atom(&self, atom: &QueryAtom) -> Vec<QueryAtom> {
1127        match atom {
1128            QueryAtom::TypeAtom { individual, class } => self.unfold_type_atom(individual, class),
1129            QueryAtom::PropertyAtom {
1130                subject,
1131                property,
1132                object,
1133            } => self.unfold_property_atom(subject, property, object),
1134        }
1135    }
1136
1137    /// Unfold a type atom ?x:C into all possible alternatives:
1138    /// 1. The atom itself (?x:C)
1139    /// 2. For each B⊑C: ?x:B
1140    /// 3. For each property P with domain D where D⊑C: ?x P ?fresh
1141    /// 4. For each property P with range R where R⊑C: ?fresh P ?x
1142    fn unfold_type_atom(&self, individual: &QueryTerm, class: &str) -> Vec<QueryAtom> {
1143        let mut result = Vec::new();
1144
1145        // Original atom
1146        result.push(QueryAtom::TypeAtom {
1147            individual: individual.clone(),
1148            class: class.to_string(),
1149        });
1150
1151        // Subclasses: for each B that is subsumed by C (B ⊑ C), we can use ?x:B
1152        let subsumed = self.tbox.all_subsumed_by(class);
1153        for sub in &subsumed {
1154            if sub != class {
1155                result.push(QueryAtom::TypeAtom {
1156                    individual: individual.clone(),
1157                    class: sub.clone(),
1158                });
1159            }
1160        }
1161
1162        // Domain-based unfolding: if ∃P.⊤ ⊑ C (i.e. P has domain D and D⊑C),
1163        // we can replace ?x:C with ?x P ?fresh_var
1164        let fresh_var = self.fresh_variable(individual, "dom");
1165        for (prop, domains) in &self.tbox.domain_classes {
1166            for domain in domains {
1167                // Check if domain ⊑ class
1168                if domain == class || self.tbox.superclasses(domain).contains(class) {
1169                    // Generate: ?x P ?fresh
1170                    result.push(QueryAtom::PropertyAtom {
1171                        subject: individual.clone(),
1172                        property: prop.clone(),
1173                        object: fresh_var.clone(),
1174                    });
1175                }
1176            }
1177        }
1178
1179        // Range-based unfolding: if ∃P⁻.⊤ ⊑ C (i.e. P has range R and R⊑C),
1180        // we can replace ?x:C with ?fresh P ?x
1181        let fresh_var2 = self.fresh_variable(individual, "rng");
1182        for (prop, ranges) in &self.tbox.range_classes {
1183            for range in ranges {
1184                if range == class || self.tbox.superclasses(range).contains(class) {
1185                    // Generate: ?fresh P ?x
1186                    result.push(QueryAtom::PropertyAtom {
1187                        subject: fresh_var2.clone(),
1188                        property: prop.clone(),
1189                        object: individual.clone(),
1190                    });
1191                }
1192            }
1193        }
1194
1195        // Deduplicate
1196        result.dedup_by(|a, b| format!("{a:?}") == format!("{b:?}"));
1197        result
1198    }
1199
1200    /// Compute additional type atom alternatives arising from union axioms.
1201    ///
1202    /// For a type atom ?x:A, if there is a union axiom C ⊑ A ⊔ B, then
1203    /// C individuals that happen to be A will satisfy the query.
1204    /// We cannot replace ?x:A with ?x:C (since not all C are A), but we can
1205    /// add a branch that queries ?x:C with the understanding that a C that is
1206    /// not B must be A (by the closed-world assumption on the union).
1207    ///
1208    /// In the PerfectRef framework for OWL 2 QL, union rewriting produces
1209    /// new CQ branches where the type atom is replaced by the sub-concept C
1210    /// (the class that is subsumed by the union). This is correct because:
1211    /// - If ?x is a C individual and C ⊑ A ⊔ B, then ?x satisfies A or B.
1212    /// - The query ?x:A is satisfied if we can certify ?x is a C that is an A.
1213    ///
1214    /// Returns a list of replacement atoms (one per union branch sub-concept).
1215    pub fn unfold_type_atom_union_branches(
1216        &self,
1217        individual: &QueryTerm,
1218        class: &str,
1219    ) -> Vec<QueryAtom> {
1220        let mut result = Vec::new();
1221
1222        // Find all classes C such that C ⊑ (... ⊔ class ⊔ ...)
1223        let union_sources = self.tbox.classes_with_union_disjunct(class);
1224        for source_class in &union_sources {
1225            result.push(QueryAtom::TypeAtom {
1226                individual: individual.clone(),
1227                class: source_class.clone(),
1228            });
1229        }
1230
1231        // Also check subclasses of union source classes (transitivity)
1232        let mut extra = Vec::new();
1233        for source_class in &union_sources {
1234            for sub in self.tbox.subclasses(source_class) {
1235                extra.push(QueryAtom::TypeAtom {
1236                    individual: individual.clone(),
1237                    class: sub,
1238                });
1239            }
1240        }
1241        result.extend(extra);
1242
1243        // Deduplicate
1244        result.dedup_by(|a, b| format!("{a:?}") == format!("{b:?}"));
1245        result
1246    }
1247
1248    /// Unfold a property atom (?x P ?y) into all possible alternatives:
1249    /// 1. The atom itself
1250    /// 2. For each Q⊑P: ?x Q ?y
1251    /// 3. For each inverse Q⁻⊑P: ?y Q ?x
1252    /// 4. Via inverseOf: if P inverseOf Q, then ?y Q ?x
1253    fn unfold_property_atom(
1254        &self,
1255        subject: &QueryTerm,
1256        property: &str,
1257        object: &QueryTerm,
1258    ) -> Vec<QueryAtom> {
1259        let mut result = Vec::new();
1260
1261        // Original
1262        result.push(QueryAtom::PropertyAtom {
1263            subject: subject.clone(),
1264            property: property.to_string(),
1265            object: object.clone(),
1266        });
1267
1268        // Get all sub-roles of property P (i.e. Q such that Q ⊑ P)
1269        let sub_roles = self.tbox.all_subproperties_of(property, false);
1270        for role in sub_roles {
1271            match role {
1272                QlRole::Named(q) if q != property => {
1273                    result.push(QueryAtom::PropertyAtom {
1274                        subject: subject.clone(),
1275                        property: q,
1276                        object: object.clone(),
1277                    });
1278                }
1279                QlRole::Inverse(q) => {
1280                    // Q⁻ ⊑ P means if (y,x):Q then (x,y):P — so use ?y Q ?x
1281                    result.push(QueryAtom::PropertyAtom {
1282                        subject: object.clone(),
1283                        property: q,
1284                        object: subject.clone(),
1285                    });
1286                }
1287                _ => {}
1288            }
1289        }
1290
1291        // Deduplicate
1292        result.dedup_by(|a, b| format!("{a:?}") == format!("{b:?}"));
1293        result
1294    }
1295
1296    /// Generate a fresh variable name based on the individual term
1297    fn fresh_variable(&self, base: &QueryTerm, suffix: &str) -> QueryTerm {
1298        match base {
1299            QueryTerm::Variable(v) => QueryTerm::Variable(format!("_fresh_{v}_{suffix}")),
1300            QueryTerm::Constant(c) => {
1301                // Use a short hash-like suffix
1302                let short: String = c.chars().take(8).collect();
1303                QueryTerm::Variable(format!("_fresh_{short}_{suffix}"))
1304            }
1305        }
1306    }
1307
1308    /// Check if a class assertion is trivially satisfiable given the TBox
1309    pub fn is_satisfiable(&self, class: &str) -> bool {
1310        // A class is unsatisfiable if it's in a disjointness pair with itself
1311        // or disjoint with owl:Thing
1312        !self.tbox.are_disjoint(class, class)
1313    }
1314
1315    /// Compute union-aware query rewriting for a conjunctive query where
1316    /// union axioms guide query branching.
1317    ///
1318    /// This method extends `rewrite_query` by explicitly computing all union
1319    /// disjunct branches for each type atom and emitting separate CQs for
1320    /// each possible branch derivation path.
1321    ///
1322    /// For example, given:
1323    ///   TBox: Dog ⊑ Animal ⊔ Pet
1324    ///   Query: ?x:Animal
1325    ///
1326    /// The rewriting includes:
1327    ///   ?x:Animal (original)
1328    ///   ?x:Dog    (from union: Dog may be an Animal)
1329    pub fn rewrite_query_union_aware(
1330        &self,
1331        query: &ConjunctiveQuery,
1332    ) -> Result<RewrittenQuery, QlError> {
1333        // Use the standard rewrite_query which now includes union branching
1334        self.rewrite_query(query)
1335    }
1336
1337    /// Compute all union disjunct branches for a named class.
1338    ///
1339    /// Returns a list of lists — each inner list is a set of class names that
1340    /// are sibling disjuncts in some union axiom where `class` participates.
1341    ///
1342    /// Example: if TBox contains C ⊑ A ⊔ B, then:
1343    ///   union_siblings_for("A") → [["A", "B"]]
1344    pub fn union_siblings_for(&self, class: &str) -> Vec<Vec<String>> {
1345        // Find union axioms where `class` is a disjunct
1346        let sources = self.tbox.classes_with_union_disjunct(class);
1347        let mut result = Vec::new();
1348        for source in &sources {
1349            let disjunct_lists = self.tbox.union_axiom_disjuncts(source);
1350            for disjuncts in disjunct_lists {
1351                if disjuncts.contains(&class.to_string()) {
1352                    result.push(disjuncts);
1353                }
1354            }
1355        }
1356        result
1357    }
1358
1359    /// Check if class A and class B are in a union axiom together (sibling disjuncts)
1360    pub fn are_union_siblings(&self, class_a: &str, class_b: &str) -> bool {
1361        let siblings = self.union_siblings_for(class_a);
1362        siblings
1363            .iter()
1364            .any(|branch| branch.contains(&class_b.to_string()))
1365    }
1366
1367    /// Compute satisfiability of a concept conjunction {C₁, C₂, ...} w.r.t. the TBox.
1368    ///
1369    /// Returns false if the conjunction is definitely unsatisfiable based on:
1370    /// - Direct disjointness between any two members
1371    /// - Union axiom exhaustion: C ⊑ A ⊔ B and neither A nor B is compatible
1372    pub fn conjunction_satisfiable(&self, classes: &[&str]) -> bool {
1373        // Check pairwise disjointness
1374        for i in 0..classes.len() {
1375            for j in (i + 1)..classes.len() {
1376                if self.tbox.are_disjoint(classes[i], classes[j]) {
1377                    return false;
1378                }
1379            }
1380        }
1381        true
1382    }
1383}
1384
1385// ---- Convenience functions ----
1386
1387/// Build and classify a TBox from a list of axioms
1388pub fn build_tbox(axioms: Vec<QlAxiom>) -> Result<Owl2QLTBox, QlError> {
1389    let mut tbox = Owl2QLTBox::new();
1390    tbox.add_axioms(axioms);
1391    tbox.classify()?;
1392    Ok(tbox)
1393}
1394
1395/// Rewrite a SPARQL-like conjunctive query using OWL 2 QL PerfectRef
1396pub fn rewrite_query(atoms: Vec<QueryAtom>, tbox: &Owl2QLTBox) -> Result<RewrittenQuery, QlError> {
1397    let cq = ConjunctiveQuery::with_atoms(atoms);
1398    let rewriter = QueryRewriter::new(tbox.clone());
1399    rewriter.rewrite_query(&cq)
1400}
1401
1402/// Rewrite a query with explicit union-aware branching
1403pub fn rewrite_query_union(
1404    atoms: Vec<QueryAtom>,
1405    tbox: &Owl2QLTBox,
1406) -> Result<RewrittenQuery, QlError> {
1407    let cq = ConjunctiveQuery::with_atoms(atoms);
1408    let rewriter = QueryRewriter::new(tbox.clone());
1409    rewriter.rewrite_query_union_aware(&cq)
1410}
1411
1412impl Clone for Owl2QLTBox {
1413    fn clone(&self) -> Self {
1414        let mut new_tbox = Owl2QLTBox::new();
1415        new_tbox.axioms = self.axioms.clone();
1416        new_tbox.class_supers = self.class_supers.clone();
1417        new_tbox.class_subs = self.class_subs.clone();
1418        new_tbox.prop_supers = self.prop_supers.clone();
1419        new_tbox.inv_prop_supers = self.inv_prop_supers.clone();
1420        new_tbox.inverse_of = self.inverse_of.clone();
1421        new_tbox.domain_classes = self.domain_classes.clone();
1422        new_tbox.range_classes = self.range_classes.clone();
1423        new_tbox.disjoint_classes = self.disjoint_classes.clone();
1424        new_tbox.union_axioms = self.union_axioms.clone();
1425        new_tbox.union_rev_index = self.union_rev_index.clone();
1426        new_tbox
1427    }
1428}
1429
1430#[cfg(test)]
1431mod tests {
1432    use super::*;
1433    include!("owl_ql_tests.rs");
1434    include!("owl_ql_tests_extended.rs");
1435}