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