Skip to main content

oxirs_core/query/
exec.rs

1//! Query execution engine
2//!
3//! This module executes query plans against RDF stores.
4
5use crate::model::*;
6use crate::query::algebra::*;
7use crate::query::plan::ExecutionPlan;
8use crate::OxirsError;
9use crate::Store;
10use std::collections::{HashMap, HashSet};
11
12/// A solution mapping (binding of variables to values)
13#[derive(Debug, Clone, PartialEq)]
14pub struct Solution {
15    bindings: HashMap<Variable, Term>,
16}
17
18impl Solution {
19    /// Creates a new empty solution
20    pub fn new() -> Self {
21        Solution {
22            bindings: HashMap::new(),
23        }
24    }
25
26    /// Binds a variable to a value
27    pub fn bind(&mut self, var: Variable, value: Term) {
28        self.bindings.insert(var, value);
29    }
30
31    /// Gets the value bound to a variable
32    pub fn get(&self, var: &Variable) -> Option<&Term> {
33        self.bindings.get(var)
34    }
35
36    /// Merges two solutions (for joins)
37    pub fn merge(&self, other: &Solution) -> Option<Solution> {
38        let mut merged = self.clone();
39
40        for (var, value) in &other.bindings {
41            if let Some(existing) = merged.bindings.get(var) {
42                if existing != value {
43                    return None; // Incompatible bindings
44                }
45            } else {
46                merged.bindings.insert(var.clone(), value.clone());
47            }
48        }
49
50        Some(merged)
51    }
52
53    /// Projects specific variables
54    pub fn project(&self, vars: &[Variable]) -> Solution {
55        let mut projected = Solution::new();
56        for var in vars {
57            if let Some(value) = self.bindings.get(var) {
58                projected.bind(var.clone(), value.clone());
59            }
60        }
61        projected
62    }
63
64    /// Returns an iterator over the variable-term bindings
65    pub fn iter(&self) -> std::collections::hash_map::Iter<'_, Variable, Term> {
66        self.bindings.iter()
67    }
68
69    /// Returns an iterator over the variables in this solution
70    pub fn variables(&self) -> impl Iterator<Item = &Variable> {
71        self.bindings.keys()
72    }
73
74    /// Build a deterministic, order-independent key for this solution.
75    ///
76    /// The bindings live in a `HashMap` whose iteration order is not stable
77    /// between instances, so any key derived from raw iteration order (such as
78    /// the `Debug` string) is unreliable for equality/deduplication. Sorting the
79    /// `(variable, term)` pairs by their canonical string form yields a key that
80    /// is identical for any two solutions with identical bindings.
81    pub fn canonical_key(&self) -> Vec<(String, String)> {
82        let mut pairs: Vec<(String, String)> = self
83            .bindings
84            .iter()
85            .map(|(var, term)| (var.name().to_string(), term.to_string()))
86            .collect();
87        pairs.sort();
88        pairs
89    }
90}
91
92/// Query results
93#[derive(Debug)]
94pub enum QueryResults {
95    /// Boolean result (for ASK queries)
96    Boolean(bool),
97    /// Solutions (for SELECT queries)
98    Solutions(Vec<Solution>),
99    /// Graph (for CONSTRUCT queries)
100    Graph(Vec<Triple>),
101}
102
103/// Query executor
104pub struct QueryExecutor<'a> {
105    store: &'a dyn Store,
106}
107
108impl<'a> QueryExecutor<'a> {
109    /// Creates a new query executor
110    pub fn new(store: &'a dyn Store) -> Self {
111        QueryExecutor { store }
112    }
113
114    /// Executes a query plan
115    pub fn execute(&self, plan: &ExecutionPlan) -> Result<Vec<Solution>, OxirsError> {
116        self.execute_plan(plan)
117    }
118
119    fn execute_plan(&self, plan: &ExecutionPlan) -> Result<Vec<Solution>, OxirsError> {
120        match plan {
121            ExecutionPlan::TripleScan { pattern } => self.execute_triple_scan(pattern),
122            ExecutionPlan::HashJoin {
123                left,
124                right,
125                join_vars,
126            } => self.execute_hash_join(left, right, join_vars),
127            ExecutionPlan::Filter { input, condition } => self.execute_filter(input, condition),
128            ExecutionPlan::Project { input, vars } => self.execute_project(input, vars),
129            ExecutionPlan::Sort { input, order_by } => self.execute_sort(input, order_by),
130            ExecutionPlan::Limit {
131                input,
132                limit,
133                offset,
134            } => self.execute_limit(input, *limit, *offset),
135            ExecutionPlan::Union { left, right } => self.execute_union(left, right),
136            ExecutionPlan::Distinct { input } => self.execute_distinct(input),
137        }
138    }
139
140    fn execute_triple_scan(
141        &self,
142        pattern: &crate::model::pattern::TriplePattern,
143    ) -> Result<Vec<Solution>, OxirsError> {
144        let mut solutions = Vec::new();
145
146        // SPARQL default-graph semantics: a BGP outside a GRAPH clause must
147        // match ONLY the active (default) graph, never the union of the default
148        // graph and every named graph. Scanning `store.triples()` (which unions
149        // all graphs) would incorrectly surface triples that live exclusively in
150        // named graphs. Restrict the scan to the default graph.
151        let quads = self.store.default_graph_quads()?;
152
153        for quad in quads {
154            let triple = Triple::new(
155                quad.subject().clone(),
156                quad.predicate().clone(),
157                quad.object().clone(),
158            );
159            if let Some(solution) = self.match_triple_pattern(&triple, pattern) {
160                solutions.push(solution);
161            }
162        }
163
164        Ok(solutions)
165    }
166
167    fn match_triple_pattern(
168        &self,
169        triple: &Triple,
170        pattern: &crate::model::pattern::TriplePattern,
171    ) -> Option<Solution> {
172        let mut solution = Solution::new();
173
174        // Match subject
175        if let Some(ref subject_pattern) = pattern.subject {
176            if !self.match_subject_pattern(triple.subject(), subject_pattern, &mut solution) {
177                return None;
178            }
179        }
180
181        // Match predicate
182        if let Some(ref predicate_pattern) = pattern.predicate {
183            if !self.match_predicate_pattern(triple.predicate(), predicate_pattern, &mut solution) {
184                return None;
185            }
186        }
187
188        // Match object
189        if let Some(ref object_pattern) = pattern.object {
190            if !self.match_object_pattern(triple.object(), object_pattern, &mut solution) {
191                return None;
192            }
193        }
194
195        Some(solution)
196    }
197
198    #[allow(dead_code)]
199    fn match_term_pattern(
200        &self,
201        term: &Term,
202        pattern: &TermPattern,
203        solution: &mut Solution,
204    ) -> bool {
205        match pattern {
206            TermPattern::Variable(var) => {
207                if let Some(bound_value) = solution.get(var) {
208                    bound_value == term
209                } else {
210                    solution.bind(var.clone(), term.clone());
211                    true
212                }
213            }
214            TermPattern::NamedNode(n) => {
215                matches!(term, Term::NamedNode(nn) if nn == n)
216            }
217            TermPattern::BlankNode(b) => {
218                matches!(term, Term::BlankNode(bn) if bn == b)
219            }
220            TermPattern::Literal(l) => {
221                matches!(term, Term::Literal(lit) if lit == l)
222            }
223            TermPattern::QuotedTriple(_) => {
224                // Fine-grained RDF-star quoted-triple matching is not implemented
225                // in this helper. Rather than crashing the query thread, treat
226                // the pattern as non-matching so an unsupported pattern degrades
227                // gracefully instead of panicking.
228                false
229            }
230        }
231    }
232
233    fn match_subject_pattern(
234        &self,
235        subject: &Subject,
236        pattern: &crate::model::pattern::SubjectPattern,
237        solution: &mut Solution,
238    ) -> bool {
239        use crate::model::pattern::SubjectPattern;
240        match pattern {
241            SubjectPattern::Variable(var) => {
242                if let Some(bound_value) = solution.get(var) {
243                    match (subject, bound_value) {
244                        (Subject::NamedNode(n1), Term::NamedNode(n2)) => n1 == n2,
245                        (Subject::BlankNode(b1), Term::BlankNode(b2)) => b1 == b2,
246                        _ => false,
247                    }
248                } else {
249                    solution
250                        .bindings
251                        .insert(var.clone(), Term::from_subject(subject));
252                    true
253                }
254            }
255            SubjectPattern::NamedNode(n) => matches!(subject, Subject::NamedNode(nn) if nn == n),
256            SubjectPattern::BlankNode(b) => matches!(subject, Subject::BlankNode(bn) if bn == b),
257            // A quoted-triple pattern matches any quoted-triple subject; variable binding
258            // refinement for the inner triple is handled at a higher level.
259            SubjectPattern::QuotedTriple(_) => matches!(subject, Subject::QuotedTriple(_)),
260        }
261    }
262
263    fn match_predicate_pattern(
264        &self,
265        predicate: &Predicate,
266        pattern: &crate::model::pattern::PredicatePattern,
267        solution: &mut Solution,
268    ) -> bool {
269        use crate::model::pattern::PredicatePattern;
270        match pattern {
271            PredicatePattern::Variable(var) => {
272                if let Some(bound_value) = solution.get(var) {
273                    match (predicate, bound_value) {
274                        (Predicate::NamedNode(n1), Term::NamedNode(n2)) => n1 == n2,
275                        _ => false,
276                    }
277                } else {
278                    solution
279                        .bindings
280                        .insert(var.clone(), Term::from_predicate(predicate));
281                    true
282                }
283            }
284            PredicatePattern::NamedNode(n) => {
285                matches!(predicate, Predicate::NamedNode(nn) if nn == n)
286            }
287        }
288    }
289
290    fn match_object_pattern(
291        &self,
292        object: &Object,
293        pattern: &crate::model::pattern::ObjectPattern,
294        solution: &mut Solution,
295    ) -> bool {
296        use crate::model::pattern::ObjectPattern;
297        match pattern {
298            ObjectPattern::Variable(var) => {
299                if let Some(bound_value) = solution.get(var) {
300                    match (object, bound_value) {
301                        (Object::NamedNode(n1), Term::NamedNode(n2)) => n1 == n2,
302                        (Object::BlankNode(b1), Term::BlankNode(b2)) => b1 == b2,
303                        (Object::Literal(l1), Term::Literal(l2)) => l1 == l2,
304                        _ => false,
305                    }
306                } else {
307                    solution
308                        .bindings
309                        .insert(var.clone(), Term::from_object(object));
310                    true
311                }
312            }
313            ObjectPattern::NamedNode(n) => matches!(object, Object::NamedNode(nn) if nn == n),
314            ObjectPattern::BlankNode(b) => matches!(object, Object::BlankNode(bn) if bn == b),
315            ObjectPattern::Literal(l) => matches!(object, Object::Literal(lit) if lit == l),
316            // A quoted-triple pattern matches any quoted-triple object.
317            ObjectPattern::QuotedTriple(_) => matches!(object, Object::QuotedTriple(_)),
318        }
319    }
320
321    fn execute_hash_join(
322        &self,
323        left: &ExecutionPlan,
324        right: &ExecutionPlan,
325        join_vars: &[Variable],
326    ) -> Result<Vec<Solution>, OxirsError> {
327        let left_solutions = self.execute_plan(left)?;
328        let right_solutions = self.execute_plan(right)?;
329
330        let mut results = Vec::new();
331
332        // Build hash table from left solutions
333        let mut hash_table: HashMap<Vec<Term>, Vec<Solution>> = HashMap::new();
334        for solution in left_solutions {
335            let key: Vec<Term> = join_vars
336                .iter()
337                .filter_map(|var| solution.get(var).cloned())
338                .collect();
339            hash_table.entry(key).or_default().push(solution);
340        }
341
342        // Probe with right solutions
343        for right_solution in right_solutions {
344            let key: Vec<Term> = join_vars
345                .iter()
346                .filter_map(|var| right_solution.get(var).cloned())
347                .collect();
348
349            if let Some(left_solutions) = hash_table.get(&key) {
350                for left_solution in left_solutions {
351                    if let Some(merged) = left_solution.merge(&right_solution) {
352                        results.push(merged);
353                    }
354                }
355            }
356        }
357
358        Ok(results)
359    }
360
361    fn execute_filter(
362        &self,
363        input: &ExecutionPlan,
364        condition: &Expression,
365    ) -> Result<Vec<Solution>, OxirsError> {
366        let solutions = self.execute_plan(input)?;
367
368        Ok(solutions
369            .into_iter()
370            .filter(|solution| {
371                self.evaluate_expression(condition, solution)
372                    .unwrap_or(false)
373            })
374            .collect())
375    }
376
377    fn execute_project(
378        &self,
379        input: &ExecutionPlan,
380        vars: &[Variable],
381    ) -> Result<Vec<Solution>, OxirsError> {
382        let solutions = self.execute_plan(input)?;
383
384        Ok(solutions
385            .into_iter()
386            .map(|solution| solution.project(vars))
387            .collect())
388    }
389
390    fn execute_sort(
391        &self,
392        input: &ExecutionPlan,
393        order_by: &[OrderExpression],
394    ) -> Result<Vec<Solution>, OxirsError> {
395        let mut solutions = self.execute_plan(input)?;
396
397        // Stable sort so that equal keys preserve their relative input order.
398        solutions.sort_by(|a, b| {
399            for order in order_by {
400                let (expr, descending) = match order {
401                    OrderExpression::Asc(e) => (e, false),
402                    OrderExpression::Desc(e) => (e, true),
403                };
404                let ta = self.evaluate_expression_to_term(expr, a);
405                let tb = self.evaluate_expression_to_term(expr, b);
406                let mut ord = Self::order_compare(ta.as_ref(), tb.as_ref());
407                if descending {
408                    ord = ord.reverse();
409                }
410                if ord != std::cmp::Ordering::Equal {
411                    return ord;
412                }
413            }
414            std::cmp::Ordering::Equal
415        });
416
417        Ok(solutions)
418    }
419
420    /// Total ordering used by `ORDER BY`, following the SPARQL term ordering:
421    /// unbound values sort first, then blank nodes, IRIs, and literals; within a
422    /// kind, values are compared by their typed value (falling back to lexical
423    /// order for otherwise-incomparable literals so the sort stays total and
424    /// deterministic).
425    fn order_compare(a: Option<&Term>, b: Option<&Term>) -> std::cmp::Ordering {
426        use std::cmp::Ordering;
427        match (a, b) {
428            (None, None) => Ordering::Equal,
429            (None, Some(_)) => Ordering::Less,
430            (Some(_), None) => Ordering::Greater,
431            (Some(a), Some(b)) => {
432                if let Some(ord) = Self::compare_terms(a, b) {
433                    return ord;
434                }
435                Self::term_kind_rank(a)
436                    .cmp(&Self::term_kind_rank(b))
437                    .then_with(|| a.to_string().cmp(&b.to_string()))
438            }
439        }
440    }
441
442    /// Rank of an RDF term kind for the total ORDER BY ordering.
443    fn term_kind_rank(term: &Term) -> u8 {
444        match term {
445            Term::BlankNode(_) => 0,
446            Term::NamedNode(_) => 1,
447            Term::Literal(_) => 2,
448            _ => 3,
449        }
450    }
451
452    fn execute_limit(
453        &self,
454        input: &ExecutionPlan,
455        limit: usize,
456        offset: usize,
457    ) -> Result<Vec<Solution>, OxirsError> {
458        let solutions = self.execute_plan(input)?;
459
460        Ok(solutions.into_iter().skip(offset).take(limit).collect())
461    }
462
463    fn execute_union(
464        &self,
465        left: &ExecutionPlan,
466        right: &ExecutionPlan,
467    ) -> Result<Vec<Solution>, OxirsError> {
468        let mut solutions = self.execute_plan(left)?;
469        solutions.extend(self.execute_plan(right)?);
470        Ok(solutions)
471    }
472
473    fn execute_distinct(&self, input: &ExecutionPlan) -> Result<Vec<Solution>, OxirsError> {
474        let solutions = self.execute_plan(input)?;
475        let mut seen = HashSet::new();
476        let mut distinct_solutions = Vec::new();
477
478        for solution in solutions {
479            // Build a canonical, order-independent key. `Solution` wraps a
480            // `HashMap`, whose `Debug` iteration order varies per instance (the
481            // hasher is seeded per map), so hashing `format!("{solution:?}")`
482            // could give two identical binding sets different keys and fail to
483            // deduplicate them. Sorting the (variable, term) pairs makes the key
484            // deterministic.
485            if seen.insert(solution.canonical_key()) {
486                distinct_solutions.push(solution);
487            }
488        }
489
490        Ok(distinct_solutions)
491    }
492
493    fn evaluate_expression(&self, expr: &Expression, solution: &Solution) -> Option<bool> {
494        match expr {
495            Expression::Variable(var) => {
496                if let Some(term) = solution.get(var) {
497                    // Convert term to boolean (non-empty strings and non-zero numbers are true)
498                    match term {
499                        Term::Literal(lit) => {
500                            let value = lit.as_str();
501                            match lit.datatype().as_str() {
502                                "http://www.w3.org/2001/XMLSchema#boolean" => {
503                                    value.parse::<bool>().ok()
504                                }
505                                "http://www.w3.org/2001/XMLSchema#integer"
506                                | "http://www.w3.org/2001/XMLSchema#decimal"
507                                | "http://www.w3.org/2001/XMLSchema#double" => {
508                                    value.parse::<f64>().map(|n| n != 0.0).ok()
509                                }
510                                "http://www.w3.org/2001/XMLSchema#string" => {
511                                    Some(!value.is_empty())
512                                }
513                                _ => Some(!value.is_empty()),
514                            }
515                        }
516                        _ => Some(true), // Non-literal terms are considered true
517                    }
518                } else {
519                    Some(false) // Unbound variables are false
520                }
521            }
522            Expression::Literal(lit) => {
523                let value = lit.as_str();
524                match lit.datatype().as_str() {
525                    "http://www.w3.org/2001/XMLSchema#boolean" => value.parse::<bool>().ok(),
526                    "http://www.w3.org/2001/XMLSchema#integer"
527                    | "http://www.w3.org/2001/XMLSchema#decimal"
528                    | "http://www.w3.org/2001/XMLSchema#double" => {
529                        value.parse::<f64>().map(|n| n != 0.0).ok()
530                    }
531                    _ => Some(!value.is_empty()),
532                }
533            }
534            Expression::And(left, right) => {
535                let left_result = self.evaluate_expression(left, solution)?;
536                let right_result = self.evaluate_expression(right, solution)?;
537                Some(left_result && right_result)
538            }
539            Expression::Or(left, right) => {
540                let left_result = self.evaluate_expression(left, solution)?;
541                let right_result = self.evaluate_expression(right, solution)?;
542                Some(left_result || right_result)
543            }
544            Expression::Not(expr) => {
545                let result = self.evaluate_expression(expr, solution)?;
546                Some(!result)
547            }
548            Expression::Equal(left, right) => {
549                let left_term = self.evaluate_expression_to_term(left, solution)?;
550                let right_term = self.evaluate_expression_to_term(right, solution)?;
551                Some(left_term == right_term)
552            }
553            Expression::NotEqual(left, right) => {
554                let left_term = self.evaluate_expression_to_term(left, solution)?;
555                let right_term = self.evaluate_expression_to_term(right, solution)?;
556                Some(left_term != right_term)
557            }
558            Expression::Less(left, right) => self
559                .compare_terms_expr(left, right, solution)
560                .map(|ord| ord == std::cmp::Ordering::Less),
561            Expression::LessOrEqual(left, right) => self
562                .compare_terms_expr(left, right, solution)
563                .map(|ord| ord != std::cmp::Ordering::Greater),
564            Expression::Greater(left, right) => self
565                .compare_terms_expr(left, right, solution)
566                .map(|ord| ord == std::cmp::Ordering::Greater),
567            Expression::GreaterOrEqual(left, right) => self
568                .compare_terms_expr(left, right, solution)
569                .map(|ord| ord != std::cmp::Ordering::Less),
570            Expression::Bound(var) => Some(solution.get(var).is_some()),
571            Expression::IsIri(expr) => {
572                if let Some(term) = self.evaluate_expression_to_term(expr, solution) {
573                    Some(matches!(term, Term::NamedNode(_)))
574                } else {
575                    Some(false)
576                }
577            }
578            Expression::IsBlank(expr) => {
579                if let Some(term) = self.evaluate_expression_to_term(expr, solution) {
580                    Some(matches!(term, Term::BlankNode(_)))
581                } else {
582                    Some(false)
583                }
584            }
585            Expression::IsLiteral(expr) => {
586                if let Some(term) = self.evaluate_expression_to_term(expr, solution) {
587                    Some(matches!(term, Term::Literal(_)))
588                } else {
589                    Some(false)
590                }
591            }
592            Expression::IsNumeric(expr) => {
593                if let Some(Term::Literal(lit)) = self.evaluate_expression_to_term(expr, solution) {
594                    let datatype_str = lit.datatype().as_str().to_string();
595                    Some(matches!(
596                        datatype_str.as_str(),
597                        "http://www.w3.org/2001/XMLSchema#integer"
598                            | "http://www.w3.org/2001/XMLSchema#decimal"
599                            | "http://www.w3.org/2001/XMLSchema#double"
600                            | "http://www.w3.org/2001/XMLSchema#float"
601                    ))
602                } else {
603                    Some(false)
604                }
605            }
606            Expression::Str(expr) => {
607                // STR() always succeeds, so it's always "true" for filtering purposes
608                Some(self.evaluate_expression_to_term(expr, solution).is_some())
609            }
610            Expression::Regex(text_expr, pattern_expr, flags_expr) => {
611                let text = self.evaluate_expression_to_string(text_expr, solution)?;
612                let pattern = self.evaluate_expression_to_string(pattern_expr, solution)?;
613
614                let flags = if let Some(flags_expr) = flags_expr {
615                    self.evaluate_expression_to_string(flags_expr, solution)
616                        .unwrap_or_default()
617                } else {
618                    String::new()
619                };
620
621                // Basic regex implementation (would need full regex crate for production)
622                if flags.is_empty() {
623                    Some(text.contains(&pattern))
624                } else {
625                    // For now, just do case-insensitive matching if 'i' flag is present
626                    if flags.contains('i') {
627                        Some(text.to_lowercase().contains(&pattern.to_lowercase()))
628                    } else {
629                        Some(text.contains(&pattern))
630                    }
631                }
632            }
633            _ => {
634                // For unsupported expressions, default to true
635                // This is a simplified implementation
636                Some(true)
637            }
638        }
639    }
640
641    /// Evaluate an expression to a term value
642    #[allow(clippy::only_used_in_recursion)]
643    fn evaluate_expression_to_term(&self, expr: &Expression, solution: &Solution) -> Option<Term> {
644        match expr {
645            Expression::Variable(var) => solution.get(var).cloned(),
646            Expression::Term(term) => Some(term.clone()),
647            Expression::FunctionCall(Function::Str, args) => {
648                if let Some(arg) = args.first() {
649                    if let Some(term) = self.evaluate_expression_to_term(arg, solution) {
650                        match term {
651                            Term::NamedNode(n) => Some(Term::Literal(Literal::new(n.as_str()))),
652                            Term::Literal(l) => Some(Term::Literal(Literal::new(l.as_str()))),
653                            Term::BlankNode(b) => Some(Term::Literal(Literal::new(b.as_str()))),
654                            _ => None,
655                        }
656                    } else {
657                        None
658                    }
659                } else {
660                    None
661                }
662            }
663            _ => None, // Other expressions don't directly evaluate to terms
664        }
665    }
666
667    /// Evaluate an expression to a string value
668    fn evaluate_expression_to_string(
669        &self,
670        expr: &Expression,
671        solution: &Solution,
672    ) -> Option<String> {
673        if let Some(term) = self.evaluate_expression_to_term(expr, solution) {
674            match term {
675                Term::NamedNode(n) => Some(n.as_str().to_string()),
676                Term::Literal(l) => Some(l.as_str().to_string()),
677                Term::BlankNode(b) => Some(b.as_str().to_string()),
678                _ => None,
679            }
680        } else {
681            None
682        }
683    }
684
685    /// Evaluate the ordered comparison of two expressions per the SPARQL
686    /// operator-mapping rules, returning the [`Ordering`](std::cmp::Ordering)
687    /// of the two operand values.
688    ///
689    /// Supports numeric (`xsd:integer`/`decimal`/`double`/`float`), `xsd:string`
690    /// (Unicode codepoint order), `xsd:boolean` (`false` < `true`), and
691    /// `xsd:date`/`xsd:dateTime` (timezone-aware temporal order) operands.
692    ///
693    /// Returns `None` for genuinely incomparable operand pairs (a SPARQL type
694    /// error). Per SPARQL semantics a type error in a FILTER excludes the
695    /// solution, so `None` is mapped to a dropped row by the caller — never a
696    /// silently over-broad result.
697    fn compare_terms_expr(
698        &self,
699        left: &Expression,
700        right: &Expression,
701        solution: &Solution,
702    ) -> Option<std::cmp::Ordering> {
703        let left_term = self.evaluate_expression_to_term(left, solution)?;
704        let right_term = self.evaluate_expression_to_term(right, solution)?;
705        Self::compare_terms(&left_term, &right_term)
706    }
707
708    /// Compare two RDF terms per the SPARQL/XPath operator mapping.
709    ///
710    /// Returns `None` when the operands are of incomparable kinds (a type
711    /// error under SPARQL semantics).
712    fn compare_terms(left: &Term, right: &Term) -> Option<std::cmp::Ordering> {
713        let (l, r) = match (left, right) {
714            (Term::Literal(l), Term::Literal(r)) => (l, r),
715            // Ordered comparison is only defined between literals.
716            _ => return None,
717        };
718
719        let l_dt = l.datatype().as_str().to_string();
720        let r_dt = r.datatype().as_str().to_string();
721
722        // Numeric comparison (mixed numeric datatypes promote to f64).
723        if let (Some(a), Some(b)) = (
724            Self::numeric_value(&l_dt, l.as_str()),
725            Self::numeric_value(&r_dt, r.as_str()),
726        ) {
727            return a.partial_cmp(&b);
728        }
729
730        // Both operands must share the same datatype family for the remaining
731        // comparisons.
732        if l_dt != r_dt {
733            return None;
734        }
735
736        match l_dt.as_str() {
737            "http://www.w3.org/2001/XMLSchema#string" => Some(l.as_str().cmp(r.as_str())),
738            "http://www.w3.org/2001/XMLSchema#boolean" => {
739                let a = Self::boolean_value(l.as_str())?;
740                let b = Self::boolean_value(r.as_str())?;
741                Some(a.cmp(&b))
742            }
743            "http://www.w3.org/2001/XMLSchema#dateTime" => {
744                use std::str::FromStr;
745                let a = oxsdatatypes::DateTime::from_str(l.as_str()).ok()?;
746                let b = oxsdatatypes::DateTime::from_str(r.as_str()).ok()?;
747                a.partial_cmp(&b)
748            }
749            "http://www.w3.org/2001/XMLSchema#date" => {
750                use std::str::FromStr;
751                let a = oxsdatatypes::Date::from_str(l.as_str()).ok()?;
752                let b = oxsdatatypes::Date::from_str(r.as_str()).ok()?;
753                a.partial_cmp(&b)
754            }
755            _ => None,
756        }
757    }
758
759    /// Parse a numeric literal value into an `f64`, or `None` if the datatype is
760    /// not a recognised XSD numeric type.
761    fn numeric_value(datatype: &str, value: &str) -> Option<f64> {
762        match datatype {
763            "http://www.w3.org/2001/XMLSchema#integer"
764            | "http://www.w3.org/2001/XMLSchema#decimal"
765            | "http://www.w3.org/2001/XMLSchema#double"
766            | "http://www.w3.org/2001/XMLSchema#float" => value.parse::<f64>().ok(),
767            _ => None,
768        }
769    }
770
771    /// Parse an `xsd:boolean` lexical form (`true`/`1`, `false`/`0`).
772    fn boolean_value(value: &str) -> Option<bool> {
773        match value {
774            "true" | "1" => Some(true),
775            "false" | "0" => Some(false),
776            _ => None,
777        }
778    }
779}
780
781impl Default for Solution {
782    fn default() -> Self {
783        Self::new()
784    }
785}
786
787#[cfg(test)]
788mod tests {
789    use crate::model::{GraphName, Literal, NamedNode, Object, Predicate, Quad, Subject};
790    use crate::query::{QueryEngine, QueryResult};
791    use crate::rdf_store::RdfStore;
792    use crate::Store;
793
794    fn iri(s: &str) -> NamedNode {
795        NamedNode::new(s).expect("valid iri")
796    }
797
798    fn count_bindings(result: &QueryResult) -> usize {
799        match result {
800            QueryResult::Select { bindings, .. } => bindings.len(),
801            _ => panic!("expected SELECT result"),
802        }
803    }
804
805    /// P1: a BGP outside a GRAPH clause must match only the default graph, not
806    /// the union of the default graph and every named graph.
807    #[test]
808    fn regression_bgp_scans_default_graph_only() {
809        let store = RdfStore::new().expect("store");
810        // Default-graph triple.
811        store
812            .insert_quad(Quad::new(
813                Subject::NamedNode(iri("http://example.org/s1")),
814                Predicate::NamedNode(iri("http://example.org/p")),
815                Object::NamedNode(iri("http://example.org/o1")),
816                GraphName::DefaultGraph,
817            ))
818            .expect("insert default");
819        // Named-graph-only triple: must NOT be visible to a default-graph BGP.
820        store
821            .insert_quad(Quad::new(
822                Subject::NamedNode(iri("http://example.org/s2")),
823                Predicate::NamedNode(iri("http://example.org/p")),
824                Object::NamedNode(iri("http://example.org/o2")),
825                GraphName::NamedNode(iri("http://example.org/g")),
826            ))
827            .expect("insert named");
828
829        let engine = QueryEngine::new();
830        let result = engine
831            .query("SELECT * WHERE { ?s ?p ?o . }", &store)
832            .expect("query ok");
833        assert_eq!(
834            count_bindings(&result),
835            1,
836            "only the default-graph triple must match a default-graph BGP"
837        );
838    }
839
840    /// P2: DISTINCT must deduplicate multi-variable solutions reliably.
841    #[test]
842    fn regression_distinct_multi_variable_dedup() {
843        let store = RdfStore::new().expect("store");
844        let p = iri("http://example.org/p");
845        let o = iri("http://example.org/o");
846        // Two distinct subjects sharing identical (p, o) bindings.
847        for s in ["http://example.org/s1", "http://example.org/s2"] {
848            store
849                .insert_quad(Quad::new(
850                    Subject::NamedNode(iri(s)),
851                    Predicate::NamedNode(p.clone()),
852                    Object::NamedNode(o.clone()),
853                    GraphName::DefaultGraph,
854                ))
855                .expect("insert");
856        }
857
858        let engine = QueryEngine::new();
859        let result = engine
860            .query("SELECT DISTINCT ?p ?o WHERE { ?s ?p ?o . }", &store)
861            .expect("query ok");
862        assert_eq!(
863            count_bindings(&result),
864            1,
865            "identical (p, o) bindings must collapse to a single DISTINCT row"
866        );
867    }
868
869    /// P2: FILTER string comparison must perform a real ordered comparison, not
870    /// drop every row.
871    #[test]
872    fn regression_filter_string_comparison() {
873        let store = RdfStore::new().expect("store");
874        let p = iri("http://example.org/name");
875        for (s, name) in [
876            ("http://example.org/a", "apple"),
877            ("http://example.org/z", "zebra"),
878        ] {
879            store
880                .insert_quad(Quad::new(
881                    Subject::NamedNode(iri(s)),
882                    Predicate::NamedNode(p.clone()),
883                    Object::Literal(Literal::new(name)),
884                    GraphName::DefaultGraph,
885                ))
886                .expect("insert");
887        }
888
889        let engine = QueryEngine::new();
890        let result = engine
891            .query(
892                "SELECT ?name WHERE { ?s ?p ?name . FILTER(?name > \"m\") }",
893                &store,
894            )
895            .expect("query ok");
896        assert_eq!(
897            count_bindings(&result),
898            1,
899            "only 'zebra' is greater than 'm'"
900        );
901    }
902
903    /// P2: FILTER date comparison must compare temporal values, not drop rows.
904    #[test]
905    fn regression_filter_date_comparison() {
906        let store = RdfStore::new().expect("store");
907        let p = iri("http://example.org/born");
908        let date_dt = crate::vocab::xsd::DATE.clone();
909        for (s, d) in [
910            ("http://example.org/old", "1990-01-01"),
911            ("http://example.org/new", "2020-01-01"),
912        ] {
913            store
914                .insert_quad(Quad::new(
915                    Subject::NamedNode(iri(s)),
916                    Predicate::NamedNode(p.clone()),
917                    Object::Literal(Literal::new_typed(d, date_dt.clone())),
918                    GraphName::DefaultGraph,
919                ))
920                .expect("insert");
921        }
922
923        let engine = QueryEngine::new();
924        let result = engine
925            .query(
926                "SELECT ?d WHERE { ?s ?p ?d . FILTER(?d < \"2000-01-01\"^^<http://www.w3.org/2001/XMLSchema#date>) }",
927                &store,
928            )
929            .expect("query ok");
930        assert_eq!(
931            count_bindings(&result),
932            1,
933            "only the 1990 date is before 2000"
934        );
935    }
936
937    /// P1: ORDER BY must actually order the results.
938    #[test]
939    fn regression_order_by_sorts_results() {
940        let store = RdfStore::new().expect("store");
941        let p = iri("http://example.org/n");
942        let int_dt = crate::vocab::xsd::INTEGER.clone();
943        for (s, n) in [
944            ("http://example.org/b", "3"),
945            ("http://example.org/a", "1"),
946            ("http://example.org/c", "2"),
947        ] {
948            store
949                .insert_quad(Quad::new(
950                    Subject::NamedNode(iri(s)),
951                    Predicate::NamedNode(p.clone()),
952                    Object::Literal(Literal::new_typed(n, int_dt.clone())),
953                    GraphName::DefaultGraph,
954                ))
955                .expect("insert");
956        }
957
958        let engine = QueryEngine::new();
959        let result = engine
960            .query("SELECT ?n WHERE { ?s ?p ?n . } ORDER BY ?n", &store)
961            .expect("query ok");
962        let QueryResult::Select { bindings, .. } = result else {
963            panic!("expected SELECT");
964        };
965        let values: Vec<String> = bindings
966            .iter()
967            .filter_map(|b| b.get("n"))
968            .map(|t| t.to_string())
969            .collect();
970        let sorted_positions: Vec<&String> = values.iter().collect();
971        // Values must be in ascending numeric order: 1, 2, 3.
972        assert_eq!(values.len(), 3);
973        assert!(
974            sorted_positions[0].contains('1')
975                && sorted_positions[1].contains('2')
976                && sorted_positions[2].contains('3'),
977            "ORDER BY ?n should yield 1,2,3 — got {values:?}"
978        );
979    }
980
981    /// P1: LIMIT/OFFSET must bound the result set.
982    #[test]
983    fn regression_limit_offset_applied() {
984        let store = RdfStore::new().expect("store");
985        let p = iri("http://example.org/n");
986        let int_dt = crate::vocab::xsd::INTEGER.clone();
987        for n in 0..5 {
988            store
989                .insert_quad(Quad::new(
990                    Subject::NamedNode(iri(&format!("http://example.org/s{n}"))),
991                    Predicate::NamedNode(p.clone()),
992                    Object::Literal(Literal::new_typed(n.to_string(), int_dt.clone())),
993                    GraphName::DefaultGraph,
994                ))
995                .expect("insert");
996        }
997
998        let engine = QueryEngine::new();
999        let result = engine
1000            .query(
1001                "SELECT ?n WHERE { ?s ?p ?n . } ORDER BY ?n LIMIT 2 OFFSET 1",
1002                &store,
1003            )
1004            .expect("query ok");
1005        assert_eq!(
1006            count_bindings(&result),
1007            2,
1008            "LIMIT 2 must cap the result set"
1009        );
1010    }
1011}