Skip to main content

rete_core/
sparql.rs

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