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