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