Skip to main content

rete_core/
sparql.rs

1//! SPARQL parsing, lowering, and evaluation (SPEC.md §8).
2//!
3//! Queries are parsed with `spargebra`, lowered into a small plan algebra, and
4//! evaluated against the `.rete` indexes. BGPs use the integer-space hash-join
5//! engine in [`bgp`]; filters, joins, OPTIONAL, UNION, MINUS, VALUES, property
6//! paths, named graphs, aggregates, and query forms are handled in the sibling
7//! lowering/evaluation modules. Unsupported features are rejected explicitly
8//! rather than silently dropped.
9//!
10//! [`bgp`]: crate::bgp
11
12use spargebra::Query;
13
14use crate::bgp::{Binding, PatternTerm, TriplePattern};
15use crate::file::Rete;
16
17mod aggregate;
18mod eval;
19mod expr;
20mod lower;
21mod path;
22mod ql;
23
24use eval::{ask_solution, instantiate, raw_solutions, run_select, run_select_communities};
25use lower::{lower_pattern, lower_select, parse_query};
26pub use lower::{parse_select, query_predicates};
27// Re-exported so the sibling modules' `use super::*` can reach the evaluator
28// (expr's FILTER EXISTS evaluates a sub-plan via `eval_plan_in`).
29pub(crate) use eval::eval_plan_in;
30
31#[derive(Debug, thiserror::Error)]
32#[non_exhaustive]
33pub enum SparqlError {
34    #[error("parse error: {0}")]
35    Parse(String),
36    #[error("unsupported query feature: {0}")]
37    Unsupported(&'static str),
38    /// A (non-SILENT) `SERVICE` block failed: the endpoint errored, returned
39    /// unparseable results, or no [`ServiceClient`](crate::ServiceClient) is
40    /// attached to the file handle. Partial results are never returned.
41    #[error("SERVICE federation: {0}")]
42    Service(String),
43}
44
45/// A lowered SELECT query: solution modifiers plus the evaluation plan tree.
46#[derive(Debug, Clone)]
47pub struct Select {
48    /// Projected variable names (empty = SELECT *).
49    pub project: Vec<String>,
50    /// DISTINCT (or REDUCED) requested.
51    pub distinct: bool,
52    /// OFFSET (solutions to skip).
53    pub offset: usize,
54    /// LIMIT (max solutions), if any.
55    pub limit: Option<usize>,
56    /// GROUP BY + aggregates, applied after the plan and before projection.
57    pub group: Option<GroupSpec>,
58    /// BIND/aggregate-alias assignments `(result_var, expr)`, applied after
59    /// aggregation (e.g. `(COUNT(?f) AS ?n)` aliases an internal var to `?n`).
60    pub extends: Vec<(String, FExpr)>,
61    /// ORDER BY keys `(expr, descending)`, applied before projection/slice.
62    pub order: Vec<(FExpr, bool)>,
63    /// HAVING conditions, applied after aggregation.
64    pub having: Vec<FExpr>,
65    /// `FROM <iri>` graphs: when non-empty, the query's default graph is the
66    /// union (RDF merge) of these named graphs.
67    pub from: Vec<String>,
68    /// `FROM NAMED <iri>` graphs: when `Some`, `GRAPH` may only see these.
69    pub from_named: Option<Vec<String>>,
70    /// The graph-pattern evaluation plan.
71    pub plan: Plan,
72    /// Monotonic counter for minting query-unique fresh variable names while
73    /// lowering (e.g. the `?__qtN` holders that desugar RDF-star quoted-triple
74    /// patterns). Kept on `Select` so it stays unique across every BGP in one
75    /// query — two joined BGPs must not both mint `__qt1` and get unified.
76    pub star_counter: usize,
77}
78
79impl Default for Select {
80    fn default() -> Self {
81        Select {
82            project: Vec::new(),
83            distinct: false,
84            offset: 0,
85            limit: None,
86            group: None,
87            extends: Vec::new(),
88            order: Vec::new(),
89            having: Vec::new(),
90            from: Vec::new(),
91            from_named: None,
92            plan: Plan::Bgp(Vec::new()),
93            star_counter: 0,
94        }
95    }
96}
97
98/// GROUP BY specification: grouping variables and result aggregates.
99#[derive(Debug, Clone)]
100pub struct GroupSpec {
101    /// Variables to group by (empty = single group over all solutions).
102    pub by: Vec<String>,
103    /// `(result_variable, aggregate)` pairs.
104    pub aggs: Vec<(String, Agg)>,
105    /// Synthetic per-row columns computed *before* grouping — `(var, expr)` for
106    /// each aggregate whose argument is an expression rather than a bare variable
107    /// (e.g. `SUM(?a * 2)` lowers to a hidden `__aggN = ?a * 2`, then `SUM(__aggN)`).
108    /// Empty for the common case (aggregating a plain variable).
109    pub pre: Vec<(String, FExpr)>,
110}
111
112/// A supported aggregate function.
113#[derive(Debug, Clone)]
114pub enum Agg {
115    /// COUNT(*) — number of solutions in the group.
116    CountStar {
117        distinct: bool,
118    },
119    /// COUNT(?v) — number of (optionally distinct) bound values.
120    Count(String, bool),
121    Sum(String),
122    Avg(String),
123    Min(String),
124    Max(String),
125    /// SAMPLE(?v) — any one value from the group.
126    Sample(String),
127    /// GROUP_CONCAT(\[DISTINCT\] ?v; SEPARATOR=...) — values joined by the
128    /// separator (deduplicated when `distinct`).
129    GroupConcat(String, String, bool),
130}
131
132/// A SPARQL graph-pattern evaluation plan (the supported algebra subset).
133#[derive(Debug, Clone)]
134#[must_use]
135pub enum Plan {
136    /// Basic graph pattern: triple patterns joined on shared variables.
137    Bgp(Vec<TriplePattern>),
138    /// Conjunction of two patterns (inner join on shared variables).
139    Join(Box<Plan>, Box<Plan>),
140    /// UNION: all solutions of either side.
141    Union(Box<Plan>, Box<Plan>),
142    /// OPTIONAL (left join): left solutions, extended by the right where it
143    /// matches (and passes the optional condition), kept as-is where it doesn't.
144    LeftJoin(Box<Plan>, Box<Plan>, Option<FExpr>),
145    /// FILTER over an inner pattern.
146    Filter(FExpr, Box<Plan>),
147    /// In-pattern `BIND(expr AS ?var)`: each inner solution extended with `?var`
148    /// (left unbound where `expr` errors). Distinct from the projection-time
149    /// alias list (`Select::extends`) — this one is *inside* the graph pattern,
150    /// so a following FILTER or join sees the bound variable.
151    Extend(String, FExpr, Box<Plan>),
152    /// A property path `subject <path> object`.
153    Path(PatternTerm, PathAst, PatternTerm),
154    /// Inline `VALUES`: variable names and rows of optional ground-term tokens
155    /// (`None` = UNDEF).
156    Values(Vec<String>, Vec<Vec<Option<String>>>),
157    /// `MINUS`: left solutions, minus those compatible with a right solution
158    /// that shares at least one bound variable.
159    Minus(Box<Plan>, Box<Plan>),
160    /// `GRAPH <iri>|?g { … }` — evaluate the inner pattern against a named graph.
161    Graph(GraphTarget, Box<Plan>),
162    /// A nested `SELECT` subquery: evaluated independently to its projected
163    /// solutions, which then join with the surrounding pattern on shared
164    /// variables (only the subquery's projected variables are visible outside).
165    Subquery(Box<Select>),
166    /// `SERVICE [SILENT] <endpoint> { … }` — SPARQL 1.1 federated query. The
167    /// inner pattern is shipped (as `query`, its re-serialized SPARQL text) to
168    /// the remote endpoint through the file's attached
169    /// [`ServiceClient`](crate::ServiceClient) at evaluation time; the returned
170    /// solutions join the surrounding pattern on shared variables like any
171    /// other operand. Under `silent`, a failed call degrades to one empty
172    /// solution (per the spec); otherwise it fails the whole query.
173    Service {
174        silent: bool,
175        /// The endpoint IRI (no angle brackets) — where the sub-query is sent.
176        endpoint: String,
177        /// Variables the inner pattern can bind (an over-approximation is fine:
178        /// unreturned variables just stay unbound).
179        vars: Vec<String>,
180        /// The inner pattern as a standalone `SELECT`, exactly what is sent.
181        query: String,
182    },
183}
184
185/// The target of a `GRAPH` block.
186#[derive(Debug, Clone)]
187pub enum GraphTarget {
188    /// `GRAPH <iri>` — one specific named graph.
189    Named(String),
190    /// `GRAPH ?g` — every named graph, binding the variable to its IRI.
191    Var(String),
192}
193
194/// Path repetition operator.
195#[derive(Debug, Clone, Copy, PartialEq, Eq)]
196pub enum Rep {
197    /// exactly one hop (a plain predicate)
198    One,
199    /// `+` — one or more hops (transitive closure)
200    OneOrMore,
201    /// `*` — zero or more hops (reflexive-transitive closure)
202    ZeroOrMore,
203    /// `?` — zero or one hop
204    ZeroOrOne,
205}
206
207/// A lowered property path expression, evaluated as a binary relation over the
208/// graph's nodes.
209#[derive(Debug, Clone)]
210pub enum PathAst {
211    /// A single predicate, optionally reversed.
212    Pred(String, bool),
213    /// Repetition (`*`/`+`/`?`) of a sub-path.
214    Rep(Box<PathAst>, Rep),
215    /// Sequence `a/b` (relational composition).
216    Seq(Box<PathAst>, Box<PathAst>),
217    /// Alternative `a|b` (union).
218    Alt(Box<PathAst>, Box<PathAst>),
219    /// Negated property set `!(p1|…|pn)` — one step over any predicate **not**
220    /// in the set, in the given direction (`reversed` for the `^p` members,
221    /// which `spargebra` wraps in a `Reverse`).
222    NegatedSet(Vec<String>, bool),
223}
224
225/// Comparison operators supported in FILTER.
226#[derive(Debug, Clone, Copy, PartialEq, Eq)]
227pub enum Op {
228    Eq,
229    Ne,
230    Lt,
231    Le,
232    Gt,
233    Ge,
234}
235
236/// Arithmetic operators (numeric).
237#[derive(Debug, Clone, Copy, PartialEq, Eq)]
238pub enum ArithOp {
239    Add,
240    Sub,
241    Mul,
242    Div,
243}
244
245/// Supported SPARQL built-in functions (the unambiguous subset over our
246/// term-token model).
247#[derive(Debug, Clone, Copy, PartialEq, Eq)]
248pub enum Builtin {
249    Str,
250    StrLen,
251    UCase,
252    LCase,
253    Abs,
254    Ceil,
255    Floor,
256    Round,
257    Concat,
258    SubStr,
259    StrBefore,
260    StrAfter,
261    Contains,
262    StrStarts,
263    StrEnds,
264    IsIri,
265    IsBlank,
266    IsLiteral,
267    IsNumeric,
268    Datatype,
269    Lang,
270    Regex,
271    LangMatches,
272    StrDt,
273    StrLang,
274    Iri,
275    EncodeForUri,
276    Replace,
277    Md5,
278    Sha1,
279    Sha256,
280    Sha384,
281    Sha512,
282    Year,
283    Month,
284    Day,
285    Hours,
286    Minutes,
287    Seconds,
288    Timezone,
289    Tz,
290    CastInteger,
291    CastDecimal,
292    CastFloat,
293    CastDouble,
294    CastBoolean,
295    CastString,
296    Rand,
297    Uuid,
298    StrUuid,
299    BNode,
300    // GeoSPARQL (geof:) — topological relations → xsd:boolean, distance →
301    // xsd:double, envelope → geo:wktLiteral. See crate::geo.
302    GeoSfContains,
303    GeoSfWithin,
304    GeoSfIntersects,
305    GeoSfDisjoint,
306    GeoSfEquals,
307    GeoDistance,
308    GeoEnvelope,
309    // geo3 — 3D extension of GeoSPARQL (AABB topology + distance). See crate::geo3.
310    Geo3Distance,
311    Geo3Contains,
312    Geo3Within,
313    Geo3Adjacent,
314    // RDF-star / SPARQL-star: construct + inspect quoted triples (`<<s p o>>`).
315    /// `TRIPLE(s, p, o)` → the quoted triple term.
316    TripleTerm,
317    /// `isTRIPLE(t)` → whether `t` is a quoted triple (a boolean builtin).
318    IsTriple,
319    /// `SUBJECT(t)` / `PREDICATE(t)` / `OBJECT(t)` → the component of a quoted triple.
320    Subject,
321    Predicate,
322    Object,
323}
324
325/// A small boolean/comparison expression for FILTER (a subset of SPARQL exprs).
326#[derive(Debug, Clone)]
327pub enum FExpr {
328    Var(String),
329    /// A constant term token (IRI/literal) or numeric literal's text.
330    Const(String),
331    /// Numeric arithmetic on two sub-expressions.
332    Arith(ArithOp, Box<FExpr>, Box<FExpr>),
333    /// A built-in function call.
334    Func(Builtin, Vec<FExpr>),
335    /// `COALESCE(...)` — first sub-expression that yields a value.
336    Coalesce(Vec<FExpr>),
337    /// `IF(cond, then, else)` — evaluate `cond` as a boolean and pick a branch.
338    If(Box<FExpr>, Box<FExpr>, Box<FExpr>),
339    /// `expr IN (a, b, …)` — true if `expr` value-equals any list member.
340    In(Box<FExpr>, Vec<FExpr>),
341    /// `sameTerm(a, b)` — strict term identity (no value coercion).
342    SameTerm(Box<FExpr>, Box<FExpr>),
343    Compare(Op, Box<FExpr>, Box<FExpr>),
344    And(Box<FExpr>, Box<FExpr>),
345    Or(Box<FExpr>, Box<FExpr>),
346    Not(Box<FExpr>),
347    Bound(String),
348    /// `EXISTS { … }` — true if the sub-pattern has a solution compatible with
349    /// the current binding. (`NOT EXISTS` is `Not(Exists(..))`.)
350    Exists(Box<Plan>),
351}
352
353/// Memoizes each EXISTS sub-plan's solutions (plus a lazily-built semi-join
354/// index) within one filter application.
355type ExistsCache = std::collections::HashMap<*const Plan, ExistsEntry>;
356
357/// A cached EXISTS sub-plan: its solutions and a semi-join index built on first
358/// probe, so repeated probes are O(1) instead of O(sols) — turning FILTER (NOT)
359/// EXISTS over a BGP from O(L×R) into O(L+R), like the MINUS anti-join.
360struct ExistsEntry {
361    sols: Vec<crate::row::Row>,
362    probe: Option<ExistsProbe>,
363}
364
365/// The semi-join index: solution rows keyed by the slots they share with the
366/// probing rows. A probe `b` satisfies EXISTS iff some solution is compatible
367/// with it (agrees on every shared slot).
368struct ExistsProbe {
369    /// All slots bound by some solution (ascending).
370    svars: Vec<usize>,
371    /// The shared slots with the probing rows (ascending) — the index key.
372    jvars: Vec<usize>,
373    /// `jvars`-value tuples of the solutions bound on all of `jvars`.
374    keys: std::collections::HashSet<Vec<crate::row::Val>>,
375    /// Solutions missing a `jvars` slot (e.g. via a nested OPTIONAL): scanned.
376    partial: Vec<crate::row::Row>,
377}
378
379/// Build the semi-join index for `sols`, keyed by the slots shared with the
380/// probe row `b`.
381fn build_exists_probe(b: &crate::row::Row, sols: &[crate::row::Row]) -> ExistsProbe {
382    let mask = crate::row::bound_mask(sols, b.len());
383    let svars: Vec<usize> = (0..b.len()).filter(|&i| mask[i]).collect();
384    let jvars: Vec<usize> = svars.iter().copied().filter(|&i| b[i].is_some()).collect();
385    let mut keys = std::collections::HashSet::new();
386    let mut partial = Vec::new();
387    for s in sols {
388        match jvars
389            .iter()
390            .map(|&i| s[i].clone())
391            .collect::<Option<Vec<crate::row::Val>>>()
392        {
393            Some(k) => {
394                keys.insert(k);
395            }
396            None => partial.push(s.clone()),
397        }
398    }
399    ExistsProbe {
400        svars,
401        jvars,
402        keys,
403        partial,
404    }
405}
406
407/// Does `b` satisfy the cached EXISTS? Uses the keyed index when `b`'s shared
408/// slots match the index's `jvars` (the common, homogeneous case); otherwise
409/// falls back to scanning all solutions (exact semantics on irregular rows).
410fn exists_matches(b: &crate::row::Row, entry: &ExistsEntry) -> bool {
411    let probe = entry.probe.as_ref().unwrap();
412    let bj: Vec<usize> = probe
413        .svars
414        .iter()
415        .copied()
416        .filter(|&i| b[i].is_some())
417        .collect();
418    if bj == probe.jvars {
419        let k: Vec<crate::row::Val> = probe.jvars.iter().map(|&i| b[i].clone().unwrap()).collect();
420        probe.keys.contains(&k)
421            || probe
422                .partial
423                .iter()
424                .any(|s| crate::row::compatible_rows(b, s))
425    } else {
426        entry.sols.iter().any(|s| crate::row::compatible_rows(b, s))
427    }
428}
429
430/// Lexical value of a term token: the text inside a literal's quotes, else the
431/// IRI/blank-node text unchanged.
432fn lexical(token: &str) -> String {
433    if let Some(rest) = token.strip_prefix('"') {
434        if let Some(end) = rest.find('"') {
435            return rest[..end].to_string();
436        }
437    }
438    token.to_string()
439}
440
441/// Numeric value of a term: the lexical part of a literal (`"30"^^...` → 30) or
442/// a bare numeric token, else `None`. (`term_number` is the crate-visible name
443/// used by the row resolver's memoized parse.)
444pub(crate) fn term_number(s: &str) -> Option<f64> {
445    as_number(s)
446}
447
448use crate::terms::as_number;
449
450/// Compare two term values numerically when both are numbers, else lexically.
451fn compare(op: Op, a: &str, b: &str) -> bool {
452    use std::cmp::Ordering;
453    let ord = match (as_number(a), as_number(b)) {
454        (Some(x), Some(y)) => match x.partial_cmp(&y) {
455            Some(o) => o,
456            None => return false, // NaN
457        },
458        _ => a.cmp(b),
459    };
460    match op {
461        Op::Eq => ord == Ordering::Equal,
462        Op::Ne => ord != Ordering::Equal,
463        Op::Lt => ord == Ordering::Less,
464        Op::Le => ord != Ordering::Greater,
465        Op::Gt => ord == Ordering::Greater,
466        Op::Ge => ord != Ordering::Less,
467    }
468}
469
470/// The result of evaluating any SPARQL query form.
471#[derive(Debug, Clone)]
472#[must_use]
473#[non_exhaustive]
474pub enum QueryOutput {
475    /// SELECT: projected variables and their solution rows.
476    Select(Vec<String>, Vec<Binding>),
477    /// ASK: whether the pattern has any solution.
478    Ask(bool),
479    /// CONSTRUCT: the constructed triples as `(s, p, o)` term tokens.
480    Construct(Vec<(String, String, String)>),
481}
482
483/// Query shapes that can be answered exactly from [`crate::range::SummaryView`] predicate
484/// totals, without opening the triple index.
485#[derive(Debug, Clone, PartialEq, Eq)]
486#[must_use]
487#[non_exhaustive]
488pub enum SummaryQueryShape {
489    /// `SELECT (COUNT(*) AS ?n) WHERE { ?s <p> ?o }`
490    PredicateCount { predicate: String, variable: String },
491    /// `SELECT (COUNT(*) AS ?n) WHERE { ?s ?p ?o }`
492    TripleCount { variable: String },
493    /// `SELECT ?p (COUNT(*) AS ?n) WHERE { ?s ?p ?o } GROUP BY ?p`
494    PredicateTotals {
495        predicate_variable: String,
496        count_variable: String,
497    },
498    /// `SELECT DISTINCT ?p WHERE { ?s ?p ?o }`
499    PredicateList { variable: String },
500    /// `SELECT (COUNT(DISTINCT ?p) AS ?n) WHERE { ?s ?p ?o }`
501    PredicateDistinctCount { variable: String },
502    /// `ASK { ?s ?p ?o }`
503    TripleExists,
504    /// `ASK { ?s <p> ?o }`
505    PredicateExists { predicate: String },
506}
507
508/// A single triple pattern that can be answered by the range-routed permutation
509/// reader. `None` means the position is a variable/wildcard; `Some(term)` means
510/// the query pins that term.
511#[derive(Debug, Clone, PartialEq, Eq)]
512pub struct RoutedTriplePattern {
513    pub subject: Option<String>,
514    pub predicate: Option<String>,
515    pub object: Option<String>,
516}
517
518/// Classify queries whose graph access is exactly one default-graph triple
519/// pattern. Solution modifiers (projection, LIMIT, aggregate wrappers) do not
520/// change the underlying range access, but named graphs, FROM, joins, filters,
521/// paths, and other algebra need the full SPARQL evaluator.
522pub fn routed_triple_pattern(query: &str) -> Result<Option<RoutedTriplePattern>, SparqlError> {
523    let parsed = parse_query(query)?;
524    let sel = match parsed {
525        Query::Select {
526            pattern, dataset, ..
527        } => lower_select(&pattern, &dataset)?,
528        Query::Ask { pattern, .. } => lower_pattern(&pattern)?,
529        Query::Construct { pattern, .. } => lower_pattern(&pattern)?,
530        Query::Describe { pattern, .. } => lower_pattern(&pattern)?,
531    };
532    if !sel.from.is_empty() || sel.from_named.is_some() {
533        return Ok(None);
534    }
535    let Plan::Bgp(patterns) = sel.plan else {
536        return Ok(None);
537    };
538    let [tp] = patterns.as_slice() else {
539        return Ok(None);
540    };
541    Ok(Some(RoutedTriplePattern {
542        subject: term_const(&tp.s),
543        predicate: term_const(&tp.p),
544        object: term_const(&tp.o),
545    }))
546}
547
548fn term_const(term: &PatternTerm) -> Option<String> {
549    match term {
550        PatternTerm::Const(t) => Some(t.clone()),
551        PatternTerm::Var(_) => None,
552    }
553}
554
555/// Classify SPARQL queries that can be answered exactly from the pyramid
556/// summary's per-predicate totals. This is intentionally conservative: anything
557/// with constants, repeated variables, filters, joins, paths, named graphs,
558/// ORDER BY, OFFSET/LIMIT, or non-summary-safe aggregates still requires the
559/// index. The only accepted DISTINCT shape is a predicate list over one fully
560/// unbound triple pattern.
561pub fn summary_query_shape(query: &str) -> Result<Option<SummaryQueryShape>, SparqlError> {
562    let parsed = parse_query(query)?;
563    match parsed {
564        Query::Select {
565            pattern, dataset, ..
566        } => {
567            let sel = lower_select(&pattern, &dataset)?;
568            if !sel.from.is_empty()
569                || sel.from_named.is_some()
570                || sel.offset != 0
571                || sel.limit.is_some()
572                || !sel.order.is_empty()
573                || !sel.having.is_empty()
574            {
575                return Ok(None);
576            }
577            if sel.distinct {
578                if sel.group.is_some() || !sel.extends.is_empty() {
579                    return Ok(None);
580                }
581                let [projected] = sel.project.as_slice() else {
582                    return Ok(None);
583                };
584                let Some(SummaryPatternShape::AnyPredicate { variable }) =
585                    single_summary_pattern(&sel.plan)
586                else {
587                    return Ok(None);
588                };
589                return if projected == &variable {
590                    Ok(Some(SummaryQueryShape::PredicateList { variable }))
591                } else {
592                    Ok(None)
593                };
594            }
595            let Some(group) = &sel.group else {
596                return Ok(None);
597            };
598            if group.aggs.len() != 1 {
599                return Ok(None);
600            }
601            match group.by.as_slice() {
602                [] => match &group.aggs[0].1 {
603                    Agg::CountStar { distinct: false } => {
604                        let Some(variable) = public_aggregate_variable(&sel, &group.aggs[0].0)
605                        else {
606                            return Ok(None);
607                        };
608                        Ok(single_summary_pattern(&sel.plan).map(|shape| match shape {
609                            SummaryPatternShape::Predicate(predicate) => {
610                                SummaryQueryShape::PredicateCount {
611                                    predicate,
612                                    variable,
613                                }
614                            }
615                            SummaryPatternShape::AnyPredicate { .. } => {
616                                SummaryQueryShape::TripleCount { variable }
617                            }
618                        }))
619                    }
620                    Agg::Count(counted, true) => {
621                        let Some(public_variable) =
622                            public_aggregate_variable(&sel, &group.aggs[0].0)
623                        else {
624                            return Ok(None);
625                        };
626                        let Some(SummaryPatternShape::AnyPredicate { variable }) =
627                            single_summary_pattern(&sel.plan)
628                        else {
629                            return Ok(None);
630                        };
631                        if &variable != counted {
632                            return Ok(None);
633                        }
634                        Ok(Some(SummaryQueryShape::PredicateDistinctCount {
635                            variable: public_variable,
636                        }))
637                    }
638                    _ => Ok(None),
639                },
640                [group_var] => {
641                    if !matches!(group.aggs[0].1, Agg::CountStar { distinct: false }) {
642                        return Ok(None);
643                    }
644                    let Some(SummaryPatternShape::AnyPredicate { variable }) =
645                        single_summary_pattern(&sel.plan)
646                    else {
647                        return Ok(None);
648                    };
649                    if &variable != group_var {
650                        return Ok(None);
651                    }
652                    let Some(count_variable) =
653                        public_group_aggregate_variable(&sel, &group.aggs[0].0, group_var)
654                    else {
655                        return Ok(None);
656                    };
657                    Ok(Some(SummaryQueryShape::PredicateTotals {
658                        predicate_variable: group_var.clone(),
659                        count_variable,
660                    }))
661                }
662                _ => Ok(None),
663            }
664        }
665        Query::Ask { pattern, .. } => {
666            let sel = lower_pattern(&pattern)?;
667            Ok(single_summary_pattern(&sel.plan).map(|shape| match shape {
668                SummaryPatternShape::Predicate(predicate) => {
669                    SummaryQueryShape::PredicateExists { predicate }
670                }
671                SummaryPatternShape::AnyPredicate { .. } => SummaryQueryShape::TripleExists,
672            }))
673        }
674        Query::Construct { .. } | Query::Describe { .. } => Ok(None),
675    }
676}
677
678enum SummaryPatternShape {
679    Predicate(String),
680    AnyPredicate { variable: String },
681}
682
683fn single_summary_pattern(plan: &Plan) -> Option<SummaryPatternShape> {
684    let Plan::Bgp(patterns) = plan else {
685        return None;
686    };
687    let [tp] = patterns.as_slice() else {
688        return None;
689    };
690    let (PatternTerm::Var(s), PatternTerm::Var(o)) = (&tp.s, &tp.o) else {
691        return None;
692    };
693    if s == o {
694        return None;
695    }
696    match &tp.p {
697        PatternTerm::Const(p) => Some(SummaryPatternShape::Predicate(p.clone())),
698        PatternTerm::Var(p) if p != s && p != o => Some(SummaryPatternShape::AnyPredicate {
699            variable: p.clone(),
700        }),
701        _ => None,
702    }
703}
704
705fn public_aggregate_variable(sel: &Select, aggregate_var: &str) -> Option<String> {
706    let [projected] = sel.project.as_slice() else {
707        return None;
708    };
709    if projected == aggregate_var {
710        return Some(projected.clone());
711    }
712    sel.extends.iter().find_map(|(var, expr)| match expr {
713        FExpr::Var(source) if var == projected && source == aggregate_var => Some(var.clone()),
714        _ => None,
715    })
716}
717
718fn public_group_aggregate_variable(
719    sel: &Select,
720    aggregate_var: &str,
721    group_var: &str,
722) -> Option<String> {
723    let [projected_group, projected_aggregate] = sel.project.as_slice() else {
724        return None;
725    };
726    if projected_group != group_var {
727        return None;
728    }
729    if projected_aggregate == aggregate_var {
730        return Some(projected_aggregate.clone());
731    }
732    sel.extends.iter().find_map(|(var, expr)| match expr {
733        FExpr::Var(source) if var == projected_aggregate && source == aggregate_var => {
734            Some(var.clone())
735        }
736        _ => None,
737    })
738}
739
740/// One community's contribution to a community-split evaluation: how many
741/// member subjects it holds and how many solution rows it produced.
742#[derive(Debug, Clone, Copy)]
743pub struct CommunityPartial {
744    pub community: usize,
745    pub subjects: usize,
746    pub rows: usize,
747}
748
749/// The outcome of a community-split SELECT: the projected variables, the
750/// merged solution rows, and each community's contribution.
751pub type CommunitySelect = (Vec<String>, Vec<Binding>, Vec<CommunityPartial>);
752
753/// Evaluate a SELECT **per pyramid community**, then merge: each community's
754/// subjects are pushed into the plan as a VALUES binding, the partial rows
755/// are concatenated, and the solution modifiers (GROUP BY / ORDER BY / LIMIT
756/// / DISTINCT) run once on the union — so the rows are identical to
757/// [`eval_query`]'s answer. Sound only for subject-star queries over the
758/// default graph (every triple pattern sharing one subject variable; FILTERs
759/// allowed); anything else returns [`SparqlError::Unsupported`] rather than a
760/// possibly-wrong split answer. `round` picks the dendrogram granularity
761/// (`None` = the build's tile-budget round). Also returns each community's
762/// subject and row counts for display.
763pub fn eval_select_communities(
764    rete: &Rete,
765    query: &str,
766    round: Option<usize>,
767) -> Result<CommunitySelect, SparqlError> {
768    let parsed = parse_query(query)?;
769    let out = match parsed {
770        Query::Select {
771            pattern, dataset, ..
772        } => run_select_communities(rete, &lower_select(&pattern, &dataset)?, round),
773        _ => Err(SparqlError::Unsupported(
774            "community-split evaluation supports SELECT queries only",
775        )),
776    };
777    // See `eval_query`: a failed non-SILENT SERVICE call must become an error
778    // (and must not linger to poison a later query on the same handle).
779    match rete.take_service_error() {
780        Some(e) => Err(SparqlError::Service(e)),
781        None => out,
782    }
783}
784
785/// Evaluate any supported SPARQL query form (SELECT / ASK / CONSTRUCT).
786pub fn eval_query(rete: &Rete, query: &str) -> Result<QueryOutput, SparqlError> {
787    eval_query_opts(rete, query, false)
788}
789
790/// Like [`eval_query`], but with **OWL 2 QL entailment** on: the lowered plan is
791/// rewritten by the internal QL lowering pass so the answer includes ontology-entailed solutions
792/// (Stage 1a: `rdfs:subClassOf`), computed over the raw data with no
793/// materialization. Opt-in — a plain [`eval_query`] is byte-identical to before.
794pub fn eval_query_reasoned(rete: &Rete, query: &str) -> Result<QueryOutput, SparqlError> {
795    eval_query_opts(rete, query, true)
796}
797
798fn eval_query_opts(rete: &Rete, query: &str, reason: bool) -> Result<QueryOutput, SparqlError> {
799    let out = eval_query_inner(rete, query, reason);
800    // A failed non-SILENT SERVICE call is recorded out-of-band (the row
801    // pipeline is infallible, like lazy tile fetches) — surface it here so a
802    // partial answer is never returned as if it were complete.
803    match rete.take_service_error() {
804        Some(e) => Err(SparqlError::Service(e)),
805        None => out,
806    }
807}
808
809/// Apply OWL 2 QL plan rewriting when reasoning is on (no-op otherwise). Reads a
810/// small TBox slice from `rete` to gate the rewrite (see [`ql`]).
811fn maybe_reason(rete: &Rete, mut sel: Select, reason: bool) -> Select {
812    if reason {
813        let projected = sel.project.clone();
814        sel.plan = ql::reason_rewrite(sel.plan, rete, &projected);
815    }
816    sel
817}
818
819fn eval_query_inner(rete: &Rete, query: &str, reason: bool) -> Result<QueryOutput, SparqlError> {
820    let parsed = parse_query(query)?;
821    match parsed {
822        Query::Select {
823            pattern, dataset, ..
824        } => {
825            let (vars, rows) = run_select(
826                rete,
827                &maybe_reason(rete, lower_select(&pattern, &dataset)?, reason),
828            );
829            Ok(QueryOutput::Select(vars, rows))
830        }
831        Query::Ask { pattern, .. } => {
832            let sel = maybe_reason(rete, lower_pattern(&pattern)?, reason);
833            Ok(QueryOutput::Ask(ask_solution(rete, &sel)))
834        }
835        Query::Construct {
836            template, pattern, ..
837        } => {
838            let sel = maybe_reason(rete, lower_pattern(&pattern)?, reason);
839            let (ctx, sols) = raw_solutions(rete, &sel);
840            Ok(QueryOutput::Construct(instantiate(&ctx, &template, &sols)))
841        }
842        Query::Describe {
843            pattern, dataset, ..
844        } => {
845            // The projected variables' values are the resources to describe;
846            // we return each one's outgoing triples (concise bounded description).
847            let sel = maybe_reason(rete, lower_select(&pattern, &dataset)?, reason);
848            let (ctx, rows) = raw_solutions(rete, &sel);
849            let mut resources = std::collections::BTreeSet::new();
850            for row in &rows {
851                if sel.project.is_empty() {
852                    for val in row.iter().flatten() {
853                        if let Some(t) = ctx.resolver.str_of(val) {
854                            resources.insert(t.to_string());
855                        }
856                    }
857                } else {
858                    for v in &sel.project {
859                        if let Some(val) = ctx.slots.slot(v).and_then(|s| row[s].as_ref()) {
860                            if let Some(t) = ctx.resolver.str_of(val) {
861                                resources.insert(t.to_string());
862                            }
863                        }
864                    }
865                }
866            }
867            let mut triples = std::collections::BTreeSet::new();
868            for r in &resources {
869                for t in rete.query(Some(r), None, None) {
870                    triples.insert(t);
871                }
872            }
873            Ok(QueryOutput::Construct(triples.into_iter().collect()))
874        }
875    }
876}
877
878/// Parse and evaluate a SELECT against a file, applying the plan then
879/// projection, DISTINCT, OFFSET, and LIMIT. Returns `(projected_vars,
880/// solutions)`.
881pub fn eval_sparql(rete: &Rete, query: &str) -> Result<(Vec<String>, Vec<Binding>), SparqlError> {
882    eval_sparql_opts(rete, query, false)
883}
884
885/// Like [`eval_sparql`], but with OWL 2 QL entailment on (see [`eval_query_reasoned`]).
886pub fn eval_sparql_reasoned(
887    rete: &Rete,
888    query: &str,
889) -> Result<(Vec<String>, Vec<Binding>), SparqlError> {
890    eval_sparql_opts(rete, query, true)
891}
892
893fn eval_sparql_opts(
894    rete: &Rete,
895    query: &str,
896    reason: bool,
897) -> Result<(Vec<String>, Vec<Binding>), SparqlError> {
898    let sel = maybe_reason(rete, parse_select(query)?, reason);
899    let out = run_select(rete, &sel);
900    // See `eval_query`: a failed non-SILENT SERVICE call must become an error.
901    match rete.take_service_error() {
902        Some(e) => Err(SparqlError::Service(e)),
903        None => Ok(out),
904    }
905}
906
907/// Format a computed number as an N-Triples *typed* literal so the result
908/// serializer emits its datatype (SPARQL requires arithmetic/aggregates/numeric
909/// functions to yield typed numerics, not bare strings). Whole values are
910/// `xsd:integer`; fractional ones `xsd:decimal` — the common cases in the data
911/// (`xsd:double` would need operand-type tracking we don't carry through `f64`).
912pub(crate) fn fmt_num_typed(x: f64) -> String {
913    if x.fract() == 0.0 {
914        format!(
915            "\"{}\"^^<http://www.w3.org/2001/XMLSchema#integer>",
916            x as i64
917        )
918    } else {
919        // Round to 15 significant digits before emitting the shortest form, so a
920        // sum/avg of decimals that lands on a binary-float artifact (e.g.
921        // 11.100000000000001) serializes as the intended "11.1".
922        let cleaned: f64 = format!("{x:.14e}").parse().unwrap_or(x);
923        format!("\"{cleaned}\"^^<http://www.w3.org/2001/XMLSchema#decimal>")
924    }
925}
926
927/// Push a reverse through a path (reverses each predicate and swaps sequences).
928fn reverse(ast: PathAst) -> PathAst {
929    match ast {
930        PathAst::Pred(p, r) => PathAst::Pred(p, !r),
931        PathAst::Rep(inner, rep) => PathAst::Rep(Box::new(reverse(*inner)), rep),
932        PathAst::Seq(a, b) => PathAst::Seq(Box::new(reverse(*b)), Box::new(reverse(*a))),
933        PathAst::Alt(a, b) => PathAst::Alt(Box::new(reverse(*a)), Box::new(reverse(*b))),
934        PathAst::NegatedSet(s, r) => PathAst::NegatedSet(s, !r),
935    }
936}
937
938#[cfg(test)]
939mod tests {
940    use super::*;
941    use crate::dictionary::DictionaryBuilder;
942    use crate::index::GraphIndexBuilder;
943    use crate::write_file;
944
945    fn rete_from(triples: &[(&str, &str, &str)]) -> Vec<u8> {
946        let mut db = DictionaryBuilder::new();
947        for (s, p, o) in triples {
948            db.observe(s, p, o);
949        }
950        let dict = db.build();
951        let mut ib = GraphIndexBuilder::new();
952        for (s, p, o) in triples {
953            ib.push(dict.encode(s, p, o).unwrap());
954        }
955        write_file(&dict, &ib.build(), false, &[], 0)
956    }
957
958    #[test]
959    fn parses_select_with_two_patterns() {
960        let q = r#"
961            PREFIX ex: <http://ex/>
962            SELECT ?x ?z WHERE { ?x ex:knows ?y . ?y ex:knows ?z }
963        "#;
964        let sel = parse_select(q).unwrap();
965        assert_eq!(sel.project, vec!["x", "z"]);
966        match &sel.plan {
967            Plan::Bgp(p) => assert_eq!(p.len(), 2),
968            other => panic!("expected a BGP plan, got {other:?}"),
969        }
970    }
971
972    #[test]
973    fn evaluates_two_hop_select() {
974        let bytes = rete_from(&[
975            ("<http://ex/Alice>", "<http://ex/knows>", "<http://ex/Bob>"),
976            ("<http://ex/Bob>", "<http://ex/knows>", "<http://ex/Carol>"),
977        ]);
978        let rete = Rete::open(&bytes).unwrap();
979        let q = r#"PREFIX ex: <http://ex/>
980                   SELECT ?x ?z WHERE { ?x ex:knows ?y . ?y ex:knows ?z }"#;
981        let (proj, sols) = eval_sparql(&rete, q).unwrap();
982        assert_eq!(proj, vec!["x", "z"]);
983        assert_eq!(sols.len(), 1);
984        assert_eq!(sols[0]["x"], "<http://ex/Alice>");
985        assert_eq!(sols[0]["z"], "<http://ex/Carol>");
986    }
987
988    #[test]
989    fn union_returns_both_sides() {
990        let bytes = rete_from(&[
991            ("<http://ex/Alice>", "<http://ex/likes>", "<http://ex/Tea>"),
992            ("<http://ex/Bob>", "<http://ex/hates>", "<http://ex/Tea>"),
993        ]);
994        let rete = Rete::open(&bytes).unwrap();
995        let q = "PREFIX ex: <http://ex/> SELECT ?p WHERE { \
996                 { ?p ex:likes ex:Tea } UNION { ?p ex:hates ex:Tea } }";
997        let (_, sols) = eval_sparql(&rete, q).unwrap();
998        let mut who: Vec<&str> = sols.iter().map(|b| b["p"].as_str()).collect();
999        who.sort();
1000        assert_eq!(who, vec!["<http://ex/Alice>", "<http://ex/Bob>"]);
1001    }
1002
1003    #[test]
1004    fn optional_keeps_left_when_right_absent() {
1005        // Alice has an email, Bob doesn't. OPTIONAL email keeps both people.
1006        let bytes = rete_from(&[
1007            ("<http://ex/Alice>", "<http://ex/name>", "\"Alice\""),
1008            ("<http://ex/Bob>", "<http://ex/name>", "\"Bob\""),
1009            ("<http://ex/Alice>", "<http://ex/email>", "\"a@ex\""),
1010        ]);
1011        let rete = Rete::open(&bytes).unwrap();
1012        let q = "PREFIX ex: <http://ex/> SELECT ?p ?e WHERE { \
1013                 ?p ex:name ?n . OPTIONAL { ?p ex:email ?e } }";
1014        let (_, sols) = eval_sparql(&rete, q).unwrap();
1015        assert_eq!(sols.len(), 2, "both people present");
1016        let alice = sols.iter().find(|b| b["p"] == "<http://ex/Alice>").unwrap();
1017        assert_eq!(alice["e"], "\"a@ex\"");
1018        let bob = sols.iter().find(|b| b["p"] == "<http://ex/Bob>").unwrap();
1019        assert!(!bob.contains_key("e"), "Bob has no email binding");
1020    }
1021
1022    #[test]
1023    fn numeric_filter_on_typed_literal() {
1024        // ages 30 and 25; FILTER(?age > 27) keeps only Alice.
1025        let xsd = "<http://www.w3.org/2001/XMLSchema#integer>";
1026        let bytes = rete_from(&[
1027            (
1028                "<http://ex/Alice>",
1029                "<http://ex/age>",
1030                &format!("\"30\"^^{xsd}"),
1031            ),
1032            (
1033                "<http://ex/Bob>",
1034                "<http://ex/age>",
1035                &format!("\"25\"^^{xsd}"),
1036            ),
1037        ]);
1038        let rete = Rete::open(&bytes).unwrap();
1039        let q = "PREFIX ex: <http://ex/> \
1040                 SELECT ?p WHERE { ?p ex:age ?age . FILTER(?age > 27) }";
1041        let (_, sols) = eval_sparql(&rete, q).unwrap();
1042        assert_eq!(sols.len(), 1);
1043        assert_eq!(sols[0]["p"], "<http://ex/Alice>");
1044    }
1045
1046    #[test]
1047    fn filter_equality_and_boolean_logic() {
1048        let bytes = rete_from(&[
1049            ("<http://ex/Alice>", "<http://ex/city>", "<http://ex/NYC>"),
1050            ("<http://ex/Bob>", "<http://ex/city>", "<http://ex/LA>"),
1051            ("<http://ex/Carol>", "<http://ex/city>", "<http://ex/NYC>"),
1052        ]);
1053        let rete = Rete::open(&bytes).unwrap();
1054        let q = "PREFIX ex: <http://ex/> \
1055                 SELECT ?p WHERE { ?p ex:city ?c . FILTER(?c = ex:NYC && ?p != ex:Carol) }";
1056        let (_, sols) = eval_sparql(&rete, q).unwrap();
1057        assert_eq!(sols.len(), 1);
1058        assert_eq!(sols[0]["p"], "<http://ex/Alice>");
1059    }
1060
1061    #[test]
1062    fn distinct_collapses_duplicate_projections() {
1063        // Dave is reachable in two hops via both Bob and Carol.
1064        let bytes = rete_from(&[
1065            ("<http://ex/Alice>", "<http://ex/knows>", "<http://ex/Bob>"),
1066            (
1067                "<http://ex/Alice>",
1068                "<http://ex/knows>",
1069                "<http://ex/Carol>",
1070            ),
1071            ("<http://ex/Bob>", "<http://ex/knows>", "<http://ex/Dave>"),
1072            ("<http://ex/Carol>", "<http://ex/knows>", "<http://ex/Dave>"),
1073        ]);
1074        let rete = Rete::open(&bytes).unwrap();
1075        let base = "PREFIX ex: <http://ex/> SELECT {} ?z WHERE { ?x ex:knows ?y . ?y ex:knows ?z }";
1076
1077        let (_, non_distinct) = eval_sparql(&rete, &base.replace("{}", "")).unwrap();
1078        assert_eq!(non_distinct.len(), 2); // {z:Dave} twice
1079
1080        let (proj, distinct) = eval_sparql(&rete, &base.replace("{}", "DISTINCT")).unwrap();
1081        assert_eq!(proj, vec!["z"]);
1082        assert_eq!(distinct.len(), 1);
1083        assert_eq!(distinct[0]["z"], "<http://ex/Dave>");
1084        // Projection dropped ?x and ?y.
1085        assert!(!distinct[0].contains_key("x"));
1086    }
1087
1088    #[test]
1089    fn transitive_path_reachability() {
1090        // A -> B -> C -> D chain.
1091        let bytes = rete_from(&[
1092            ("<http://ex/A>", "<http://ex/k>", "<http://ex/B>"),
1093            ("<http://ex/B>", "<http://ex/k>", "<http://ex/C>"),
1094            ("<http://ex/C>", "<http://ex/k>", "<http://ex/D>"),
1095        ]);
1096        let rete = Rete::open(&bytes).unwrap();
1097
1098        // A k+ ?y  → B, C, D (one or more hops).
1099        let q = "PREFIX ex: <http://ex/> SELECT ?y WHERE { ex:A ex:k+ ?y }";
1100        let (_, sols) = eval_sparql(&rete, q).unwrap();
1101        let mut ys: Vec<&str> = sols.iter().map(|b| b["y"].as_str()).collect();
1102        ys.sort();
1103        assert_eq!(ys, vec!["<http://ex/B>", "<http://ex/C>", "<http://ex/D>"]);
1104
1105        // A k* ?y includes A itself (zero-length).
1106        let q0 = "PREFIX ex: <http://ex/> SELECT ?y WHERE { ex:A ex:k* ?y }";
1107        let (_, s0) = eval_sparql(&rete, q0).unwrap();
1108        assert!(s0.iter().any(|b| b["y"] == "<http://ex/A>"));
1109        assert_eq!(s0.len(), 4); // A, B, C, D
1110    }
1111
1112    #[test]
1113    fn sequence_and_alternative_paths() {
1114        // Alice -parent-> Bob -parent-> Carol; Alice -stepparent-> Dave.
1115        let bytes = rete_from(&[
1116            ("<http://ex/Alice>", "<http://ex/parent>", "<http://ex/Bob>"),
1117            ("<http://ex/Bob>", "<http://ex/parent>", "<http://ex/Carol>"),
1118            (
1119                "<http://ex/Alice>",
1120                "<http://ex/stepparent>",
1121                "<http://ex/Dave>",
1122            ),
1123        ]);
1124        let rete = Rete::open(&bytes).unwrap();
1125
1126        // grandparent = parent/parent : Alice -> Carol.
1127        let q = "PREFIX ex: <http://ex/> SELECT ?g WHERE { ex:Alice ex:parent/ex:parent ?g }";
1128        let (_, sols) = eval_sparql(&rete, q).unwrap();
1129        assert_eq!(sols.len(), 1);
1130        assert_eq!(sols[0]["g"], "<http://ex/Carol>");
1131
1132        // any-parent = parent|stepparent : Alice -> {Bob, Dave}.
1133        let qa = "PREFIX ex: <http://ex/> \
1134                  SELECT ?p WHERE { ex:Alice ex:parent|ex:stepparent ?p }";
1135        let (_, sa) = eval_sparql(&rete, qa).unwrap();
1136        let mut ps: Vec<&str> = sa.iter().map(|b| b["p"].as_str()).collect();
1137        ps.sort();
1138        assert_eq!(ps, vec!["<http://ex/Bob>", "<http://ex/Dave>"]);
1139    }
1140
1141    #[test]
1142    fn group_concat_aggregate() {
1143        let bytes = rete_from(&[
1144            ("<http://ex/Alice>", "<http://ex/knows>", "<http://ex/Bob>"),
1145            (
1146                "<http://ex/Alice>",
1147                "<http://ex/knows>",
1148                "<http://ex/Carol>",
1149            ),
1150        ]);
1151        let rete = Rete::open(&bytes).unwrap();
1152        let q = "PREFIX ex: <http://ex/> \
1153                 SELECT (GROUP_CONCAT(?f; SEPARATOR=\"|\") AS ?fs) WHERE { ex:Alice ex:knows ?f }";
1154        let (_, sols) = eval_sparql(&rete, q).unwrap();
1155        assert_eq!(sols.len(), 1);
1156        // GROUP_CONCAT yields a simple literal — strip the quotes before splitting.
1157        let fs = sols[0]["fs"].trim_matches('"');
1158        let mut parts: Vec<&str> = fs.split('|').collect();
1159        parts.sort();
1160        assert_eq!(parts, vec!["<http://ex/Bob>", "<http://ex/Carol>"]);
1161    }
1162
1163    #[test]
1164    fn group_by_having() {
1165        // Alice knows 2, Bob knows 1.
1166        let bytes = rete_from(&[
1167            ("<http://ex/Alice>", "<http://ex/knows>", "<http://ex/Bob>"),
1168            (
1169                "<http://ex/Alice>",
1170                "<http://ex/knows>",
1171                "<http://ex/Carol>",
1172            ),
1173            ("<http://ex/Bob>", "<http://ex/knows>", "<http://ex/Carol>"),
1174        ]);
1175        let rete = Rete::open(&bytes).unwrap();
1176        // Only people who know more than one person → Alice.
1177        let q = "PREFIX ex: <http://ex/> SELECT ?p (COUNT(?f) AS ?n) \
1178                 WHERE { ?p ex:knows ?f } GROUP BY ?p HAVING (COUNT(?f) > 1)";
1179        let (_, sols) = eval_sparql(&rete, q).unwrap();
1180        assert_eq!(sols.len(), 1);
1181        assert_eq!(sols[0]["p"], "<http://ex/Alice>");
1182        assert_eq!(
1183            sols[0]["n"],
1184            "\"2\"^^<http://www.w3.org/2001/XMLSchema#integer>"
1185        );
1186    }
1187
1188    #[test]
1189    fn count_group_by() {
1190        // Alice knows Bob & Carol (2); Bob knows Carol (1).
1191        let bytes = rete_from(&[
1192            ("<http://ex/Alice>", "<http://ex/knows>", "<http://ex/Bob>"),
1193            (
1194                "<http://ex/Alice>",
1195                "<http://ex/knows>",
1196                "<http://ex/Carol>",
1197            ),
1198            ("<http://ex/Bob>", "<http://ex/knows>", "<http://ex/Carol>"),
1199        ]);
1200        let rete = Rete::open(&bytes).unwrap();
1201        let q = "PREFIX ex: <http://ex/> \
1202                 SELECT ?p (COUNT(?f) AS ?n) WHERE { ?p ex:knows ?f } GROUP BY ?p";
1203        let (_, sols) = eval_sparql(&rete, q).unwrap();
1204        let mut counts: Vec<(String, String)> = sols
1205            .iter()
1206            .map(|b| (b["p"].clone(), b["n"].clone()))
1207            .collect();
1208        counts.sort();
1209        assert_eq!(
1210            counts,
1211            vec![
1212                (
1213                    "<http://ex/Alice>".into(),
1214                    "\"2\"^^<http://www.w3.org/2001/XMLSchema#integer>".into()
1215                ),
1216                (
1217                    "<http://ex/Bob>".into(),
1218                    "\"1\"^^<http://www.w3.org/2001/XMLSchema#integer>".into()
1219                ),
1220            ]
1221        );
1222    }
1223
1224    #[test]
1225    fn global_count_star() {
1226        let bytes = rete_from(&[
1227            ("<http://ex/a>", "<http://ex/p>", "<http://ex/1>"),
1228            ("<http://ex/b>", "<http://ex/p>", "<http://ex/2>"),
1229        ]);
1230        let rete = Rete::open(&bytes).unwrap();
1231        let (_, sols) = eval_sparql(&rete, "SELECT (COUNT(*) AS ?n) WHERE { ?s ?p ?o }").unwrap();
1232        assert_eq!(sols.len(), 1);
1233        assert_eq!(
1234            sols[0]["n"],
1235            "\"2\"^^<http://www.w3.org/2001/XMLSchema#integer>"
1236        );
1237    }
1238
1239    #[test]
1240    fn summary_query_shape_classifies_only_exact_predicate_totals() {
1241        let count = summary_query_shape(
1242            "PREFIX ex: <http://ex/> SELECT (COUNT(*) AS ?n) WHERE { ?s ex:p ?o }",
1243        )
1244        .unwrap();
1245        assert_eq!(
1246            count,
1247            Some(SummaryQueryShape::PredicateCount {
1248                predicate: "<http://ex/p>".into(),
1249                variable: "n".into(),
1250            })
1251        );
1252
1253        let total = summary_query_shape("SELECT (COUNT(*) AS ?n) WHERE { ?s ?p ?o }").unwrap();
1254        assert_eq!(
1255            total,
1256            Some(SummaryQueryShape::TripleCount {
1257                variable: "n".into(),
1258            })
1259        );
1260
1261        let by_pred =
1262            summary_query_shape("SELECT ?p (COUNT(*) AS ?n) WHERE { ?s ?p ?o } GROUP BY ?p")
1263                .unwrap();
1264        assert_eq!(
1265            by_pred,
1266            Some(SummaryQueryShape::PredicateTotals {
1267                predicate_variable: "p".into(),
1268                count_variable: "n".into(),
1269            })
1270        );
1271
1272        let predicates = summary_query_shape("SELECT DISTINCT ?p WHERE { ?s ?p ?o }").unwrap();
1273        assert_eq!(
1274            predicates,
1275            Some(SummaryQueryShape::PredicateList {
1276                variable: "p".into(),
1277            })
1278        );
1279
1280        let predicate_count =
1281            summary_query_shape("SELECT (COUNT(DISTINCT ?p) AS ?n) WHERE { ?s ?p ?o }").unwrap();
1282        assert_eq!(
1283            predicate_count,
1284            Some(SummaryQueryShape::PredicateDistinctCount {
1285                variable: "n".into(),
1286            })
1287        );
1288
1289        let ask = summary_query_shape("PREFIX ex: <http://ex/> ASK { ?s ex:p ?o }").unwrap();
1290        assert_eq!(
1291            ask,
1292            Some(SummaryQueryShape::PredicateExists {
1293                predicate: "<http://ex/p>".into(),
1294            })
1295        );
1296
1297        let any_ask = summary_query_shape("ASK { ?s ?p ?o }").unwrap();
1298        assert_eq!(any_ask, Some(SummaryQueryShape::TripleExists));
1299
1300        let constrained =
1301            summary_query_shape("PREFIX ex: <http://ex/> ASK { ex:a ex:p ?o }").unwrap();
1302        assert_eq!(constrained, None);
1303
1304        let filtered =
1305            summary_query_shape("PREFIX ex: <http://ex/> ASK { ?s ex:p ?o FILTER(?s = ?o) }")
1306                .unwrap();
1307        assert_eq!(filtered, None);
1308    }
1309
1310    #[test]
1311    fn filter_exists_and_not_exists() {
1312        // Alice knows Bob & Carol; Bob knows Dave; Carol knows nobody.
1313        let bytes = rete_from(&[
1314            ("<http://ex/Alice>", "<http://ex/knows>", "<http://ex/Bob>"),
1315            (
1316                "<http://ex/Alice>",
1317                "<http://ex/knows>",
1318                "<http://ex/Carol>",
1319            ),
1320            ("<http://ex/Bob>", "<http://ex/knows>", "<http://ex/Dave>"),
1321        ]);
1322        let rete = Rete::open(&bytes).unwrap();
1323
1324        // Friends of Alice who themselves know someone → Bob.
1325        let q_exists = "PREFIX ex: <http://ex/> SELECT ?f WHERE { \
1326            ex:Alice ex:knows ?f . FILTER EXISTS { ?f ex:knows ?x } }";
1327        let (_, e) = eval_sparql(&rete, q_exists).unwrap();
1328        assert_eq!(
1329            e.iter().map(|b| b["f"].as_str()).collect::<Vec<_>>(),
1330            vec!["<http://ex/Bob>"]
1331        );
1332
1333        // Friends of Alice who know nobody → Carol.
1334        let q_not = "PREFIX ex: <http://ex/> SELECT ?f WHERE { \
1335            ex:Alice ex:knows ?f . FILTER NOT EXISTS { ?f ex:knows ?x } }";
1336        let (_, n) = eval_sparql(&rete, q_not).unwrap();
1337        assert_eq!(
1338            n.iter().map(|b| b["f"].as_str()).collect::<Vec<_>>(),
1339            vec!["<http://ex/Carol>"]
1340        );
1341    }
1342
1343    #[test]
1344    fn ask_repeated_variable_pattern() {
1345        // ASK's fast path must NOT take the single-pattern scan shortcut when a
1346        // variable repeats across positions (`?x knows ?x`) — only Bob self-knows.
1347        let yes = rete_from(&[
1348            ("<http://ex/Alice>", "<http://ex/knows>", "<http://ex/Bob>"),
1349            ("<http://ex/Bob>", "<http://ex/knows>", "<http://ex/Bob>"),
1350        ]);
1351        let rete = Rete::open(&yes).unwrap();
1352        match eval_query(&rete, "PREFIX ex: <http://ex/> ASK { ?x ex:knows ?x }").unwrap() {
1353            QueryOutput::Ask(b) => assert!(b, "Bob knows himself"),
1354            other => panic!("expected Ask, got {other:?}"),
1355        }
1356        // With no self-edge, the index still has a `knows` triple, so a naive
1357        // first-match probe would wrongly say true; the guard must reject it.
1358        let no = rete_from(&[("<http://ex/Alice>", "<http://ex/knows>", "<http://ex/Bob>")]);
1359        let rete = Rete::open(&no).unwrap();
1360        match eval_query(&rete, "PREFIX ex: <http://ex/> ASK { ?x ex:knows ?x }").unwrap() {
1361            QueryOutput::Ask(b) => assert!(!b, "nobody knows themselves"),
1362            other => panic!("expected Ask, got {other:?}"),
1363        }
1364    }
1365
1366    #[test]
1367    fn minus_excludes_compatible() {
1368        // Alice knows Bob and Carol; Bob knows Carol; Carol knows no one.
1369        let bytes = rete_from(&[
1370            ("<http://ex/Alice>", "<http://ex/knows>", "<http://ex/Bob>"),
1371            (
1372                "<http://ex/Alice>",
1373                "<http://ex/knows>",
1374                "<http://ex/Carol>",
1375            ),
1376            ("<http://ex/Bob>", "<http://ex/knows>", "<http://ex/Carol>"),
1377        ]);
1378        let rete = Rete::open(&bytes).unwrap();
1379        // People Alice knows who themselves know nobody → Carol.
1380        let q = "PREFIX ex: <http://ex/> SELECT ?f WHERE { \
1381                 ex:Alice ex:knows ?f . MINUS { ?f ex:knows ?x } }";
1382        let (_, sols) = eval_sparql(&rete, q).unwrap();
1383        let fs: Vec<&str> = sols.iter().map(|b| b["f"].as_str()).collect();
1384        assert_eq!(fs, vec!["<http://ex/Carol>"]);
1385    }
1386
1387    #[test]
1388    fn filter_exists_disjoint_variable() {
1389        // EXISTS over a sub-pattern that shares NO variable with the outer row is
1390        // true iff the sub-pattern has any solution — so NOT EXISTS removes ALL
1391        // rows (the exact case where NOT EXISTS differs from MINUS). Guards the
1392        // semi-join index's empty-`jvars` path.
1393        let bytes = rete_from(&[
1394            ("<http://ex/Alice>", "<http://ex/a>", "<http://ex/Person>"),
1395            ("<http://ex/Bob>", "<http://ex/a>", "<http://ex/Person>"),
1396            ("<http://ex/Tea>", "<http://ex/a>", "<http://ex/Drink>"),
1397        ]);
1398        let rete = Rete::open(&bytes).unwrap();
1399        let q_none = "PREFIX ex: <http://ex/> SELECT ?x WHERE { \
1400            ?x ex:a ex:Person FILTER NOT EXISTS { ?y ex:a ex:Drink } }";
1401        assert!(eval_sparql(&rete, q_none).unwrap().1.is_empty());
1402        let q_all = "PREFIX ex: <http://ex/> SELECT ?x WHERE { \
1403            ?x ex:a ex:Person FILTER EXISTS { ?y ex:a ex:Drink } }";
1404        assert_eq!(eval_sparql(&rete, q_all).unwrap().1.len(), 2);
1405    }
1406
1407    #[test]
1408    fn minus_disjoint_domain_keeps_all() {
1409        // MINUS with no shared variable must remove nothing (SPARQL semantics) —
1410        // the hash anti-join's `jv.is_empty()` guard. Alice/Bob both kept even
1411        // though the right pattern has solutions.
1412        let bytes = rete_from(&[
1413            ("<http://ex/Alice>", "<http://ex/a>", "<http://ex/Person>"),
1414            ("<http://ex/Bob>", "<http://ex/a>", "<http://ex/Person>"),
1415            ("<http://ex/Tea>", "<http://ex/a>", "<http://ex/Drink>"),
1416        ]);
1417        let rete = Rete::open(&bytes).unwrap();
1418        let q = "PREFIX ex: <http://ex/> SELECT ?x WHERE { \
1419                 ?x ex:a ex:Person MINUS { ?y ex:a ex:Drink } }";
1420        let (_, sols) = eval_sparql(&rete, q).unwrap();
1421        let mut xs: Vec<&str> = sols.iter().map(|b| b["x"].as_str()).collect();
1422        xs.sort();
1423        assert_eq!(xs, vec!["<http://ex/Alice>", "<http://ex/Bob>"]);
1424    }
1425
1426    #[test]
1427    fn values_pushdown_selects_subset() {
1428        // VALUES with several rows pushes each into the scan; the result must be
1429        // exactly the union (here: two of three disciplines).
1430        let bytes = rete_from(&[
1431            ("<http://ex/a>", "<http://ex/d>", "<http://ex/Bio>"),
1432            ("<http://ex/b>", "<http://ex/d>", "<http://ex/Phys>"),
1433            ("<http://ex/c>", "<http://ex/d>", "<http://ex/Chem>"),
1434        ]);
1435        let rete = Rete::open(&bytes).unwrap();
1436        let q = "PREFIX ex: <http://ex/> SELECT ?p ?disc WHERE { \
1437            VALUES ?disc { ex:Bio ex:Phys } ?p ex:d ?disc }";
1438        let (_, sols) = eval_sparql(&rete, q).unwrap();
1439        let mut got: Vec<(String, String)> = sols
1440            .iter()
1441            .map(|b| (b["p"].clone(), b["disc"].clone()))
1442            .collect();
1443        got.sort();
1444        assert_eq!(
1445            got,
1446            vec![
1447                ("<http://ex/a>".into(), "<http://ex/Bio>".into()),
1448                ("<http://ex/b>".into(), "<http://ex/Phys>".into()),
1449            ]
1450        );
1451    }
1452
1453    #[test]
1454    fn values_inline_data_joins() {
1455        let bytes = rete_from(&[
1456            ("<http://ex/Alice>", "<http://ex/knows>", "<http://ex/Bob>"),
1457            (
1458                "<http://ex/Alice>",
1459                "<http://ex/knows>",
1460                "<http://ex/Carol>",
1461            ),
1462            ("<http://ex/Dave>", "<http://ex/knows>", "<http://ex/Eve>"),
1463        ]);
1464        let rete = Rete::open(&bytes).unwrap();
1465        // Restrict to Alice's friends via VALUES.
1466        let q = "PREFIX ex: <http://ex/> \
1467                 SELECT ?f WHERE { VALUES ?p { ex:Alice } ?p ex:knows ?f }";
1468        let (_, sols) = eval_sparql(&rete, q).unwrap();
1469        let mut fs: Vec<&str> = sols.iter().map(|b| b["f"].as_str()).collect();
1470        fs.sort();
1471        assert_eq!(fs, vec!["<http://ex/Bob>", "<http://ex/Carol>"]);
1472    }
1473
1474    #[test]
1475    fn graph_queries_over_named_graphs() {
1476        use crate::write_dataset;
1477        // Shared dict; social graph has knows edges, profile graph has ages.
1478        let mut db = DictionaryBuilder::new();
1479        let edges = [
1480            ("<http://ex/Alice>", "<http://ex/knows>", "<http://ex/Bob>"),
1481            ("<http://ex/Bob>", "<http://ex/age>", "\"25\""),
1482        ];
1483        for (s, p, o) in edges {
1484            db.observe(s, p, o);
1485        }
1486        let dict = db.build();
1487        let mut social = GraphIndexBuilder::new();
1488        social.push(dict.encode(edges[0].0, edges[0].1, edges[0].2).unwrap());
1489        let mut profile = GraphIndexBuilder::new();
1490        profile.push(dict.encode(edges[1].0, edges[1].1, edges[1].2).unwrap());
1491        let named = vec![
1492            ("<http://ex/social>".to_string(), social.build()),
1493            ("<http://ex/profile>".to_string(), profile.build()),
1494        ];
1495        let bytes = write_dataset(
1496            &dict,
1497            &GraphIndexBuilder::new().build(),
1498            &named,
1499            true,
1500            &[],
1501            0,
1502        );
1503        let rete = Rete::open(&bytes).unwrap();
1504
1505        // GRAPH <iri>: knows edge only in the social graph.
1506        let q = "PREFIX ex: <http://ex/> \
1507                 SELECT ?f WHERE { GRAPH ex:social { ex:Alice ex:knows ?f } }";
1508        let (_, s) = eval_sparql(&rete, q).unwrap();
1509        assert_eq!(s.len(), 1);
1510        assert_eq!(s[0]["f"], "<http://ex/Bob>");
1511
1512        // GRAPH ?g: which graph holds the age triple?
1513        let q2 = "PREFIX ex: <http://ex/> \
1514                  SELECT ?g WHERE { GRAPH ?g { ex:Bob ex:age ?a } }";
1515        let (_, s2) = eval_sparql(&rete, q2).unwrap();
1516        assert_eq!(s2.len(), 1);
1517        assert_eq!(s2[0]["g"], "<http://ex/profile>");
1518
1519        // EXISTS inside GRAPH evaluates in that graph: the `age` triple exists in
1520        // the profile graph but NOT in the social graph.
1521        let q3 = "PREFIX ex: <http://ex/> SELECT ?f WHERE { \
1522                  GRAPH ex:profile { ?f ex:age ?a . FILTER EXISTS { ?f ex:age ?a2 } } }";
1523        assert_eq!(eval_sparql(&rete, q3).unwrap().1.len(), 1);
1524        let q4 = "PREFIX ex: <http://ex/> SELECT ?s WHERE { \
1525                  GRAPH ex:social { ?s ex:knows ?o . FILTER NOT EXISTS { ?s ex:age ?a } } }";
1526        // In the social graph, no `age` triples exist → NOT EXISTS keeps the row.
1527        assert_eq!(eval_sparql(&rete, q4).unwrap().1.len(), 1);
1528    }
1529
1530    #[test]
1531    fn from_unions_named_graphs() {
1532        use crate::write_dataset;
1533        // social graph: Alice knows Bob. profile graph: Bob knows Carol.
1534        // A join spanning both only works if FROM merges them into the default.
1535        let mut db = DictionaryBuilder::new();
1536        let t = [
1537            ("<http://ex/Alice>", "<http://ex/knows>", "<http://ex/Bob>"),
1538            ("<http://ex/Bob>", "<http://ex/knows>", "<http://ex/Carol>"),
1539        ];
1540        for (s, p, o) in t {
1541            db.observe(s, p, o);
1542        }
1543        let dict = db.build();
1544        let mut g1 = GraphIndexBuilder::new();
1545        g1.push(dict.encode(t[0].0, t[0].1, t[0].2).unwrap());
1546        let mut g2 = GraphIndexBuilder::new();
1547        g2.push(dict.encode(t[1].0, t[1].1, t[1].2).unwrap());
1548        let named = vec![
1549            ("<http://ex/social>".to_string(), g1.build()),
1550            ("<http://ex/profile>".to_string(), g2.build()),
1551        ];
1552        let bytes = write_dataset(
1553            &dict,
1554            &GraphIndexBuilder::new().build(),
1555            &named,
1556            true,
1557            &[],
1558            0,
1559        );
1560        let rete = Rete::open(&bytes).unwrap();
1561
1562        // Without FROM the default graph is empty → no join.
1563        let q0 = "PREFIX ex: <http://ex/> SELECT ?z WHERE { ?x ex:knows ?y . ?y ex:knows ?z }";
1564        assert!(eval_sparql(&rete, q0).unwrap().1.is_empty());
1565
1566        // FROM both graphs → the cross-graph join (Alice→Bob→Carol) succeeds.
1567        let q = "PREFIX ex: <http://ex/> \
1568                 SELECT ?z FROM ex:social FROM ex:profile \
1569                 WHERE { ?x ex:knows ?y . ?y ex:knows ?z }";
1570        let (_, s) = eval_sparql(&rete, q).unwrap();
1571        assert_eq!(s.len(), 1);
1572        assert_eq!(s[0]["z"], "<http://ex/Carol>");
1573
1574        // FROM NAMED restricts GRAPH ?g to the listed graph only.
1575        let qn = "PREFIX ex: <http://ex/> SELECT ?g FROM NAMED ex:social \
1576                  WHERE { GRAPH ?g { ?x ex:knows ?y } }";
1577        let (_, sn) = eval_sparql(&rete, qn).unwrap();
1578        let gs: Vec<&str> = sn.iter().map(|b| b["g"].as_str()).collect();
1579        assert_eq!(gs, vec!["<http://ex/social>"]); // profile excluded
1580    }
1581
1582    #[test]
1583    fn describe_resource() {
1584        let bytes = rete_from(&[
1585            ("<http://ex/Alice>", "<http://ex/knows>", "<http://ex/Bob>"),
1586            ("<http://ex/Alice>", "<http://ex/age>", "\"30\""),
1587            ("<http://ex/Bob>", "<http://ex/age>", "\"25\""),
1588        ]);
1589        let rete = Rete::open(&bytes).unwrap();
1590        // DESCRIBE <Alice> → Alice's two outgoing triples.
1591        match eval_query(&rete, "DESCRIBE <http://ex/Alice>").unwrap() {
1592            QueryOutput::Construct(t) => assert_eq!(t.len(), 2),
1593            other => panic!("expected Construct, got {other:?}"),
1594        }
1595        // DESCRIBE ?x WHERE { ?x ex:age ?a } → describes Alice and Bob.
1596        let q = "PREFIX ex: <http://ex/> DESCRIBE ?x WHERE { ?x ex:age ?a }";
1597        match eval_query(&rete, q).unwrap() {
1598            QueryOutput::Construct(t) => {
1599                // Alice has 2 triples, Bob has 1 → 3 total.
1600                assert_eq!(t.len(), 3);
1601            }
1602            other => panic!("expected Construct, got {other:?}"),
1603        }
1604    }
1605
1606    #[test]
1607    fn ask_and_construct() {
1608        let bytes = rete_from(&[
1609            ("<http://ex/Alice>", "<http://ex/knows>", "<http://ex/Bob>"),
1610            ("<http://ex/Bob>", "<http://ex/knows>", "<http://ex/Carol>"),
1611        ]);
1612        let rete = Rete::open(&bytes).unwrap();
1613
1614        // ASK: is there any knows edge? yes; a likes edge? no.
1615        match eval_query(&rete, "PREFIX ex: <http://ex/> ASK { ?a ex:knows ?b }").unwrap() {
1616            QueryOutput::Ask(b) => assert!(b),
1617            other => panic!("expected Ask, got {other:?}"),
1618        }
1619        match eval_query(&rete, "PREFIX ex: <http://ex/> ASK { ?a ex:likes ?b }").unwrap() {
1620            QueryOutput::Ask(b) => assert!(!b),
1621            other => panic!("expected Ask, got {other:?}"),
1622        }
1623
1624        // CONSTRUCT a reverse `knownBy` graph.
1625        let q = "PREFIX ex: <http://ex/> \
1626                 CONSTRUCT { ?b ex:knownBy ?a } WHERE { ?a ex:knows ?b }";
1627        match eval_query(&rete, q).unwrap() {
1628            QueryOutput::Construct(mut triples) => {
1629                triples.sort();
1630                assert_eq!(triples.len(), 2);
1631                assert!(triples.contains(&(
1632                    "<http://ex/Bob>".into(),
1633                    "<http://ex/knownBy>".into(),
1634                    "<http://ex/Alice>".into(),
1635                )));
1636            }
1637            other => panic!("expected Construct, got {other:?}"),
1638        }
1639    }
1640
1641    #[test]
1642    fn substr_strbefore_strafter() {
1643        let bytes = rete_from(&[("<http://ex/a>", "<http://ex/name>", "\"Alice Smith\"")]);
1644        let rete = Rete::open(&bytes).unwrap();
1645        let q = "PREFIX ex: <http://ex/> SELECT ?first ?last ?ini WHERE { \
1646            ?p ex:name ?n . \
1647            BIND(STRBEFORE(?n, \" \") AS ?first) \
1648            BIND(STRAFTER(?n, \" \") AS ?last) \
1649            BIND(SUBSTR(?n, 1, 1) AS ?ini) }";
1650        let (_, sols) = eval_sparql(&rete, q).unwrap();
1651        // String built-ins return proper literal terms (quoted), not bare text.
1652        assert_eq!(sols[0]["first"], "\"Alice\"");
1653        assert_eq!(sols[0]["last"], "\"Smith\"");
1654        assert_eq!(sols[0]["ini"], "\"A\"");
1655    }
1656
1657    #[test]
1658    fn concat_and_coalesce() {
1659        // Alice has a nickname, Bob doesn't.
1660        let bytes = rete_from(&[
1661            ("<http://ex/Alice>", "<http://ex/name>", "\"Alice\""),
1662            ("<http://ex/Alice>", "<http://ex/nick>", "\"Al\""),
1663            ("<http://ex/Bob>", "<http://ex/name>", "\"Bob\""),
1664        ]);
1665        let rete = Rete::open(&bytes).unwrap();
1666        // COALESCE falls back to name when nick is unbound; CONCAT builds a label.
1667        let q = "PREFIX ex: <http://ex/> SELECT ?label WHERE { \
1668            ?p ex:name ?name . OPTIONAL { ?p ex:nick ?nick } \
1669            BIND(CONCAT(\"@\", COALESCE(?nick, ?name)) AS ?label) }";
1670        let (_, sols) = eval_sparql(&rete, q).unwrap();
1671        let mut labels: Vec<&str> = sols.iter().map(|b| b["label"].as_str()).collect();
1672        labels.sort();
1673        assert_eq!(labels, vec!["\"@Al\"", "\"@Bob\""]);
1674    }
1675
1676    #[test]
1677    fn builtin_functions() {
1678        let bytes = rete_from(&[
1679            ("<http://ex/Alice>", "<http://ex/name>", "\"Alice Smith\""),
1680            ("<http://ex/Bob>", "<http://ex/name>", "\"Bob Jones\""),
1681        ]);
1682        let rete = Rete::open(&bytes).unwrap();
1683        // CONTAINS on the literal value, and STRLEN as a computed value.
1684        let q = "PREFIX ex: <http://ex/> SELECT ?p ?len WHERE { \
1685            ?p ex:name ?n . FILTER(CONTAINS(?n, \"Smith\")) BIND(STRLEN(?n) AS ?len) }";
1686        let (_, sols) = eval_sparql(&rete, q).unwrap();
1687        assert_eq!(sols.len(), 1);
1688        assert_eq!(sols[0]["p"], "<http://ex/Alice>");
1689        assert_eq!(
1690            sols[0]["len"],
1691            "\"11\"^^<http://www.w3.org/2001/XMLSchema#integer>"
1692        ); // "Alice Smith"
1693    }
1694
1695    #[test]
1696    fn geosparql_filter_and_functions() {
1697        let wkt = "\"POLYGON((0 0,10 0,10 10,0 10,0 0))\"^^\
1698            <http://www.opengis.net/ont/geosparql#wktLiteral>";
1699        let bytes = rete_from(&[
1700            (
1701                "<http://ex/f>",
1702                "<http://www.opengis.net/ont/geosparql#hasGeometry>",
1703                "<http://ex/f/g>",
1704            ),
1705            (
1706                "<http://ex/f/g>",
1707                "<http://www.opengis.net/ont/geosparql#asWKT>",
1708                wkt,
1709            ),
1710            (
1711                "<http://ex/f>",
1712                "<http://ex/year>",
1713                "\"1815\"^^<http://www.w3.org/2001/XMLSchema#integer>",
1714            ),
1715        ]);
1716        let rete = Rete::open(&bytes).unwrap();
1717        let pre = "PREFIX geo: <http://www.opengis.net/ont/geosparql#> \
1718            PREFIX geof: <http://www.opengis.net/def/function/geosparql/> \
1719            PREFIX uom: <http://www.opengis.net/def/uom/OGC/1.0/> PREFIX ex: <http://ex/> ";
1720
1721        // Headline: temporal FILTER + spatial point-in-polygon compose.
1722        let (_, s) = eval_sparql(
1723            &rete,
1724            &format!(
1725                "{pre}SELECT ?f WHERE {{ ?f ex:year ?y ; \
1726            geo:hasGeometry/geo:asWKT ?w . \
1727            FILTER(?y = 1815 && geof:sfContains(?w, \"POINT(5 5)\"^^geo:wktLiteral)) }}"
1728            ),
1729        )
1730        .unwrap();
1731        assert_eq!(s.len(), 1, "point inside the polygon in year 1815");
1732        assert_eq!(s[0]["f"], "<http://ex/f>");
1733
1734        // Point outside → no rows.
1735        let (_, s) = eval_sparql(
1736            &rete,
1737            &format!(
1738                "{pre}SELECT ?f WHERE {{ \
1739            ?f geo:hasGeometry/geo:asWKT ?w . \
1740            FILTER(geof:sfContains(?w, \"POINT(50 50)\"^^geo:wktLiteral)) }}"
1741            ),
1742        )
1743        .unwrap();
1744        assert!(s.is_empty());
1745
1746        // sfWithin is the argument-swapped relation.
1747        let (_, s) = eval_sparql(
1748            &rete,
1749            &format!(
1750                "{pre}SELECT ?f WHERE {{ \
1751            ?f geo:hasGeometry/geo:asWKT ?w . \
1752            FILTER(geof:sfWithin(\"POINT(5 5)\"^^geo:wktLiteral, ?w)) }}"
1753            ),
1754        )
1755        .unwrap();
1756        assert_eq!(s.len(), 1);
1757
1758        // Malformed WKT is a type error → 0 rows, but the query must NOT error.
1759        let (_, s) = eval_sparql(
1760            &rete,
1761            &format!(
1762                "{pre}SELECT ?f WHERE {{ \
1763            ?f geo:hasGeometry/geo:asWKT ?w . \
1764            FILTER(geof:sfContains(?w, \"garbage\"^^geo:wktLiteral)) }}"
1765            ),
1766        )
1767        .unwrap();
1768        assert!(s.is_empty());
1769
1770        // Relation in BIND (value position) → typed xsd:boolean.
1771        let (_, s) = eval_sparql(
1772            &rete,
1773            &format!(
1774                "{pre}SELECT ?hit WHERE {{ \
1775            ?f geo:hasGeometry/geo:asWKT ?w . \
1776            BIND(geof:sfContains(?w, \"POINT(5 5)\"^^geo:wktLiteral) AS ?hit) }}"
1777            ),
1778        )
1779        .unwrap();
1780        assert_eq!(
1781            s[0]["hit"],
1782            "\"true\"^^<http://www.w3.org/2001/XMLSchema#boolean>"
1783        );
1784
1785        // distance → xsd:double; envelope → geo:wktLiteral.
1786        let (_, s) = eval_sparql(
1787            &rete,
1788            &format!(
1789                "{pre}SELECT ?d WHERE {{ BIND(geof:distance(\
1790            \"POINT(0 0)\"^^geo:wktLiteral, \"POINT(0 1)\"^^geo:wktLiteral, uom:metre) AS ?d) }}"
1791            ),
1792        )
1793        .unwrap();
1794        assert!(
1795            s[0]["d"].ends_with("XMLSchema#double>"),
1796            "distance is xsd:double: {}",
1797            s[0]["d"]
1798        );
1799        let (_, s) = eval_sparql(
1800            &rete,
1801            &format!(
1802                "{pre}SELECT ?e WHERE {{ \
1803            ?f geo:hasGeometry/geo:asWKT ?w . BIND(geof:envelope(?w) AS ?e) }}"
1804            ),
1805        )
1806        .unwrap();
1807        assert!(s[0]["e"].contains("wktLiteral") && s[0]["e"].contains("POLYGON"));
1808
1809        // An unsupported geof: function is rejected cleanly at parse/lower time.
1810        assert!(eval_sparql(
1811            &rete,
1812            &format!(
1813                "{pre}SELECT ?x WHERE {{ \
1814            BIND(geof:buffer(\"POINT(0 0)\"^^geo:wktLiteral, 1) AS ?x) }}"
1815            )
1816        )
1817        .is_err());
1818    }
1819
1820    #[test]
1821    fn bind_arithmetic() {
1822        let xsd = "<http://www.w3.org/2001/XMLSchema#integer>";
1823        let bytes = rete_from(&[(
1824            "<http://ex/a>",
1825            "<http://ex/age>",
1826            &format!("\"30\"^^{xsd}"),
1827        )]);
1828        let rete = Rete::open(&bytes).unwrap();
1829        // BIND a computed value, and FILTER on arithmetic.
1830        let q = "PREFIX ex: <http://ex/> \
1831                 SELECT ?next WHERE { ?p ex:age ?age . BIND(?age + 1 AS ?next) FILTER(?age * 2 > 50) }";
1832        let (_, sols) = eval_sparql(&rete, q).unwrap();
1833        assert_eq!(sols.len(), 1);
1834        assert_eq!(
1835            sols[0]["next"],
1836            "\"31\"^^<http://www.w3.org/2001/XMLSchema#integer>"
1837        );
1838    }
1839
1840    #[test]
1841    fn bind_value_is_visible_to_a_following_filter_and_join() {
1842        // A BIND inside the WHERE pattern must be evaluated before a *following*
1843        // FILTER (and join) can reference it — the bound var is in-tree, not a
1844        // projection-time alias.
1845        let xsd = "<http://www.w3.org/2001/XMLSchema#integer>";
1846        let n = |v: i32| format!("\"{v}\"^^{xsd}");
1847        let bytes = rete_from(&[
1848            ("<http://ex/a>", "<http://ex/v>", &n(1)),
1849            ("<http://ex/b>", "<http://ex/v>", &n(2)),
1850            ("<http://ex/c>", "<http://ex/v>", &n(3)),
1851            // a node whose :v equals b's value+1, for the join case
1852            ("<http://ex/x>", "<http://ex/v>", &n(3)),
1853        ]);
1854        let rete = Rete::open(&bytes).unwrap();
1855
1856        // FILTER references the BIND'd ?z.
1857        let q1 = "PREFIX ex: <http://ex/> SELECT ?s WHERE { \
1858            ?s ex:v ?o . BIND(?o + 1 AS ?z) FILTER(?z = 3) }";
1859        let (_, s1) = eval_sparql(&rete, q1).unwrap();
1860        assert_eq!(s1.len(), 1, "only b (2+1=3) passes");
1861        assert_eq!(s1[0]["s"], "<http://ex/b>");
1862
1863        // A following triple pattern joins on the BIND'd ?z.
1864        let q2 = "PREFIX ex: <http://ex/> SELECT ?s ?s2 WHERE { \
1865            ?s ex:v ?o . BIND(?o + 1 AS ?z) ?s2 ex:v ?z }";
1866        let (_, s2) = eval_sparql(&rete, q2).unwrap();
1867        // a (1→2) joins b (:v 2); b (2→3) joins c and x (:v 3); c (3→4) no match.
1868        let mut pairs: Vec<(String, String)> = s2
1869            .iter()
1870            .map(|b| (b["s"].clone(), b["s2"].clone()))
1871            .collect();
1872        pairs.sort();
1873        assert_eq!(
1874            pairs,
1875            vec![
1876                ("<http://ex/a>".to_string(), "<http://ex/b>".to_string()),
1877                ("<http://ex/b>".to_string(), "<http://ex/c>".to_string()),
1878                ("<http://ex/b>".to_string(), "<http://ex/x>".to_string()),
1879            ]
1880        );
1881    }
1882
1883    #[test]
1884    fn order_by_numeric_desc_then_limit() {
1885        let xsd = "<http://www.w3.org/2001/XMLSchema#integer>";
1886        let bytes = rete_from(&[
1887            (
1888                "<http://ex/a>",
1889                "<http://ex/age>",
1890                &format!("\"30\"^^{xsd}"),
1891            ),
1892            (
1893                "<http://ex/b>",
1894                "<http://ex/age>",
1895                &format!("\"25\"^^{xsd}"),
1896            ),
1897            (
1898                "<http://ex/c>",
1899                "<http://ex/age>",
1900                &format!("\"40\"^^{xsd}"),
1901            ),
1902        ]);
1903        let rete = Rete::open(&bytes).unwrap();
1904        // Oldest two, descending by age.
1905        let q = "PREFIX ex: <http://ex/> \
1906                 SELECT ?p WHERE { ?p ex:age ?age } ORDER BY DESC(?age) LIMIT 2";
1907        let (_, sols) = eval_sparql(&rete, q).unwrap();
1908        let ps: Vec<&str> = sols.iter().map(|b| b["p"].as_str()).collect();
1909        assert_eq!(ps, vec!["<http://ex/c>", "<http://ex/a>"]); // 40, 30
1910    }
1911
1912    #[test]
1913    fn limit_early_out_two_hop_join() {
1914        // A→B, B→C, B→D, C→E. Two-hop join (?x k ?y . ?y k ?z) has 3 solutions:
1915        // (A,B,C), (A,B,D), (B,C,E). The LIMIT early-out must preserve the count
1916        // contract and only ever yield genuine solutions.
1917        let bytes = rete_from(&[
1918            ("<http://ex/A>", "<http://ex/k>", "<http://ex/B>"),
1919            ("<http://ex/B>", "<http://ex/k>", "<http://ex/C>"),
1920            ("<http://ex/B>", "<http://ex/k>", "<http://ex/D>"),
1921            ("<http://ex/C>", "<http://ex/k>", "<http://ex/E>"),
1922        ]);
1923        let rete = Rete::open(&bytes).unwrap();
1924        let q = "PREFIX ex: <http://ex/> SELECT ?x ?z WHERE { ?x ex:k ?y . ?y ex:k ?z }";
1925        let (_, full) = eval_sparql(&rete, q).unwrap();
1926        assert_eq!(full.len(), 3);
1927
1928        let (_, one) = eval_sparql(&rete, &format!("{q} LIMIT 1")).unwrap();
1929        assert_eq!(one.len(), 1);
1930        // The early-out row must be a real solution of the full query.
1931        assert!(one.iter().all(|r| full.contains(r)));
1932
1933        // LIMIT above the total returns everything; OFFSET composes.
1934        let (_, all) = eval_sparql(&rete, &format!("{q} LIMIT 100")).unwrap();
1935        assert_eq!(all.len(), 3);
1936        let (_, off) = eval_sparql(&rete, &format!("{q} LIMIT 100 OFFSET 2")).unwrap();
1937        assert_eq!(off.len(), 1);
1938    }
1939
1940    #[test]
1941    fn limit_early_out_filter_over_bgp() {
1942        // FILTER over a BGP under LIMIT streams and stops early; the result must
1943        // match the unlimited filtered query's prefix.
1944        let xsd = "<http://www.w3.org/2001/XMLSchema#integer>";
1945        let bytes = rete_from(&[
1946            ("<http://ex/a>", "<http://ex/n>", &format!("\"10\"^^{xsd}")),
1947            ("<http://ex/b>", "<http://ex/n>", &format!("\"20\"^^{xsd}")),
1948            ("<http://ex/c>", "<http://ex/n>", &format!("\"30\"^^{xsd}")),
1949            ("<http://ex/d>", "<http://ex/n>", &format!("\"40\"^^{xsd}")),
1950        ]);
1951        let rete = Rete::open(&bytes).unwrap();
1952        let q = "PREFIX ex: <http://ex/> SELECT ?p WHERE { ?p ex:n ?v FILTER(?v > 15) }";
1953        let (_, full) = eval_sparql(&rete, q).unwrap();
1954        assert_eq!(full.len(), 3); // b, c, d
1955        let (_, two) = eval_sparql(&rete, &format!("{q} LIMIT 2")).unwrap();
1956        assert_eq!(two.len(), 2);
1957        assert!(two.iter().all(|r| full.contains(r)));
1958    }
1959
1960    #[test]
1961    fn distinct_bgp_fast_matches_general() {
1962        // a/b both →x, b→y, c→z. The integer-DISTINCT fast path must collapse on
1963        // the projection and apply OFFSET/LIMIT after dedup.
1964        let bytes = rete_from(&[
1965            ("<http://ex/a>", "<http://ex/p>", "<http://ex/x>"),
1966            ("<http://ex/b>", "<http://ex/p>", "<http://ex/x>"),
1967            ("<http://ex/b>", "<http://ex/p>", "<http://ex/y>"),
1968            ("<http://ex/c>", "<http://ex/p>", "<http://ex/z>"),
1969        ]);
1970        let rete = Rete::open(&bytes).unwrap();
1971        // DISTINCT ?o → {x, y, z} = 3 (the two ?s→x rows collapse).
1972        let q = "PREFIX ex: <http://ex/> SELECT DISTINCT ?o WHERE { ?s ex:p ?o }";
1973        let (proj, sols) = eval_sparql(&rete, q).unwrap();
1974        assert_eq!(proj, vec!["o"]);
1975        let mut os: Vec<&str> = sols.iter().map(|b| b["o"].as_str()).collect();
1976        os.sort();
1977        assert_eq!(os, vec!["<http://ex/x>", "<http://ex/y>", "<http://ex/z>"]);
1978        // LIMIT applies after dedup.
1979        assert_eq!(
1980            eval_sparql(&rete, &format!("{q} LIMIT 2")).unwrap().1.len(),
1981            2
1982        );
1983        // A 2-var DISTINCT keeps every (s, o) pair → 4 rows.
1984        let q2 = "PREFIX ex: <http://ex/> SELECT DISTINCT ?s ?o WHERE { ?s ex:p ?o }";
1985        assert_eq!(eval_sparql(&rete, q2).unwrap().1.len(), 4);
1986    }
1987
1988    #[test]
1989    fn limit_caps_solutions() {
1990        let bytes = rete_from(&[
1991            ("<http://ex/a>", "<http://ex/p>", "<http://ex/1>"),
1992            ("<http://ex/b>", "<http://ex/p>", "<http://ex/2>"),
1993            ("<http://ex/c>", "<http://ex/p>", "<http://ex/3>"),
1994        ]);
1995        let rete = Rete::open(&bytes).unwrap();
1996        let (_, sols) =
1997            eval_sparql(&rete, "SELECT ?x WHERE { ?x <http://ex/p> ?y } LIMIT 2").unwrap();
1998        assert_eq!(sols.len(), 2);
1999    }
2000}