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