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