Skip to main content

oxilite_core/compiler/
mod.rs

1//! SPARQL algebra → SQL.
2//!
3//! Each graph pattern compiles to a [`Block`]: FROM items, WHERE conditions, variable
4//! bindings and solution-modifier state. Blocks stay "plain" (a single SELECT-FROM-WHERE)
5//! for as long as possible so that SQLite sees one flat join; they are sealed into
6//! subqueries only when SQL semantics require it.
7//!
8// @lat: [[architecture#SPARQL to SQL compiler]]
9
10pub mod expr;
11mod ops;
12pub mod plan;
13mod vector;
14
15use crate::encoding::{encode_literal, named_node_id, term_id, EncodedRows, DEFAULT_GRAPH_ID};
16use crate::error::{Error, Result};
17use crate::reason::{Entailment, GraphFilter};
18use crate::sql::Capabilities;
19use crate::stats::Stats;
20use expr::V;
21use oxrdf::{Literal, Term, Variable};
22use plan::Pos;
23use spargebra::algebra::{Expression, GraphPattern, OrderExpression, QueryDataset};
24use spargebra::term::{GroundTerm, NamedNodePattern, TermPattern, TriplePattern};
25use std::collections::{BTreeMap, HashMap, HashSet};
26use std::fmt::Write;
27
28/// Per-query options.
29#[derive(Debug, Clone, PartialEq, Eq)]
30#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
31#[cfg_attr(feature = "serde", serde(default, rename_all = "camelCase"))]
32pub struct QueryOptions {
33    /// Treat the default graph as the union of all graphs (Oxigraph's `union_default_graph`).
34    pub union_default_graph: bool,
35    /// Let SQLite choose the join order instead of the oxilite planner (for benchmarking).
36    pub sqlite_planner: bool,
37    /// Overrides the default graph with the merge of these graphs (term ids, 0 = default
38    /// graph), like Oxigraph's `default_graph` option.
39    pub default_graph: Option<Vec<i64>>,
40    /// Overrides the available named graphs (term ids).
41    pub named_graphs: Option<Vec<i64>>,
42    /// Entailment regime (default: none, like Oxigraph).
43    pub reasoning: crate::reason::Reasoning,
44    /// Also match materialized inferences (`quads_inf`, see `materialize()`).
45    pub include_inferred: bool,
46    /// Match triples of the registered schema graphs (ontologies, shapes; see
47    /// `oxilite_core::registry`). Default `true`: the dataset is the dataset. Set to `false`
48    /// to query the data without the axioms and shapes that describe it.
49    pub include_schema_graphs: bool,
50    /// Value types known for variables (e.g. from a schema): comparisons on them compile to
51    /// one typed comparison instead of a comparison per possible type. A value of another
52    /// type then compares as unknown (false in a filter).
53    pub var_types: BTreeMap<String, ValueType>,
54    /// Read the store as it was at this version (`HEAD~2`, `#42`, `@2026-09-01T00:00:00Z`; see
55    /// `version::VersionRef`). Needs a store with a change log; the store resolves it to
56    /// [`Self::as_of_tick`] before compiling.
57    pub as_of: Option<String>,
58    /// The resolved tick of [`Self::as_of`].
59    pub as_of_tick: Option<i64>,
60    /// Resolved ticks of the versions named by `SERVICE <oxilite:version/REF>` (by `REF`).
61    pub versions: BTreeMap<String, i64>,
62    /// Host functions callable by IRI (see [`crate::functions`]). A call to one is evaluated
63    /// by the fallback evaluator; the rest of the query still compiles to SQL.
64    #[cfg_attr(feature = "serde", serde(skip))]
65    pub functions: crate::functions::Functions,
66}
67
68/// The IRI prefix naming a version of the store in `SERVICE <oxilite:version/REF> { … }`.
69pub const VERSION_SERVICE: &str = "oxilite:version/";
70
71/// The id offset of inline integers (a tick `t` is the integer id `t + expr_int_base()`).
72pub fn expr_int_base() -> i64 {
73    expr::INT_BASE
74}
75
76/// The graph of the store's history: commits (PROV-O activities, identified by their tick as an
77/// `xsd:integer`) and their changes (`oxl:added` / `oxl:removed` triple terms).
78pub const HISTORY_GRAPH: &str = "oxilite:history";
79
80impl Default for QueryOptions {
81    fn default() -> Self {
82        Self {
83            union_default_graph: false,
84            sqlite_planner: false,
85            default_graph: None,
86            named_graphs: None,
87            reasoning: crate::reason::Reasoning::default(),
88            include_inferred: false,
89            // The dataset is the dataset: schema graphs are matched unless asked otherwise.
90            include_schema_graphs: true,
91            var_types: BTreeMap::new(),
92            as_of: None,
93            as_of_tick: None,
94            versions: BTreeMap::new(),
95            functions: crate::functions::Functions::default(),
96        }
97    }
98}
99
100/// A static value type for [`QueryOptions::var_types`].
101#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
102#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
103#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
104pub enum ValueType {
105    /// Numbers (`xsd:integer`, `xsd:decimal`, `xsd:double`… and derived types).
106    Numeric,
107    /// Simple literals (`xsd:string`).
108    String,
109    Boolean,
110}
111
112/// How a variable is represented in SQL.
113#[derive(Debug, Clone)]
114pub(crate) enum Col {
115    /// A term id column.
116    Id(String),
117    /// A computed value (see [`V`]).
118    Val(Box<V>),
119}
120
121impl Col {
122    /// SQL of the term id usable as a join key.
123    pub(crate) fn key(&self) -> Option<&str> {
124        match self {
125            Self::Id(x) => Some(x),
126            Self::Val(v) => v.id.as_deref(),
127        }
128    }
129
130    pub(crate) fn value(&self) -> V {
131        match self {
132            Self::Id(x) => V::from_id(x),
133            Self::Val(v) => (**v).clone(),
134        }
135    }
136}
137
138#[derive(Debug, Clone)]
139pub(crate) struct Binding {
140    pub col: Col,
141    pub nullable: bool,
142    /// The column is an expression (not a plain column of a FROM item); such bindings must
143    /// be sealed before being placed on the right of a LEFT JOIN.
144    #[allow(dead_code)]
145    pub computed: bool,
146    /// Already equated with an outer (EXISTS) binding.
147    pub correlated: bool,
148}
149
150impl Binding {
151    fn id(sql: impl Into<String>) -> Self {
152        Self {
153            col: Col::Id(sql.into()),
154            nullable: false,
155            computed: false,
156            correlated: false,
157        }
158    }
159}
160
161#[derive(Debug, Clone, PartialEq, Eq)]
162pub(crate) enum Join {
163    First,
164    Cross,
165    Inner,
166    /// `LEFT JOIN … ON (condition)` (OPTIONAL).
167    #[allow(dead_code)]
168    Left(String),
169}
170
171#[derive(Debug, Clone)]
172pub(crate) struct FromItem {
173    pub join: Join,
174    pub item: String,
175}
176
177#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)]
178pub(crate) enum Stage {
179    #[default]
180    Plain,
181    Grouped,
182    Ordered,
183    Distinct,
184    Sliced,
185}
186
187#[derive(Debug, Clone, Default)]
188pub(crate) struct Block {
189    pub from: Vec<FromItem>,
190    pub wheres: Vec<String>,
191    pub cols: BTreeMap<usize, Binding>,
192    pub group_by: Option<Vec<String>>,
193    pub order_by: Vec<String>,
194    pub distinct: bool,
195    pub limit: Option<usize>,
196    pub offset: usize,
197    pub stage: Stage,
198    /// Extra select-list entries (e.g. the aggregate that makes a bare column meaningful).
199    pub extra_select: Vec<String>,
200    /// Render `LIMIT -1` so that SQLite does not flatten this subquery into an aggregate
201    /// query (flattening restriction 21): its computed columns are then evaluated once per row
202    /// instead of being inlined into every aggregate that reads them.
203    pub no_flatten: bool,
204}
205
206pub(crate) const VAL_FIELDS: [&str; 10] = ["i", "k", "l", "d", "g", "n", "t", "s", "b", "x"];
207
208pub(crate) fn val_fields(v: &V) -> [String; 10] {
209    [
210        v.id.clone().unwrap_or_else(|| "NULL".into()),
211        v.kind.clone(),
212        if v.computed_num {
213            "NULL".into()
214        } else {
215            v.lex.clone()
216        },
217        v.dt.clone(),
218        v.lang.clone(),
219        v.num.clone(),
220        v.nt.clone(),
221        v.ts.clone(),
222        v.boolv.clone(),
223        v.aux.clone(),
224    ]
225}
226
227impl Block {
228    pub(crate) fn is_plain(&self) -> bool {
229        self.stage == Stage::Plain
230    }
231
232    pub(crate) fn is_unit(&self) -> bool {
233        self.from.is_empty() && self.wheres.is_empty() && self.cols.is_empty()
234    }
235
236    pub(crate) fn render_from(items: &[FromItem]) -> String {
237        let mut out = String::new();
238        for (i, f) in items.iter().enumerate() {
239            if i == 0 {
240                out.push_str(&f.item);
241                continue;
242            }
243            match &f.join {
244                Join::First | Join::Inner => {
245                    let _ = write!(out, " JOIN {}", f.item);
246                }
247                Join::Cross => {
248                    let _ = write!(out, " CROSS JOIN {}", f.item);
249                }
250                Join::Left(on) => {
251                    let _ = write!(out, " LEFT JOIN {} ON ({on})", f.item);
252                }
253            }
254        }
255        out
256    }
257
258    /// Renders the select list entries of the given variables.
259    fn select_list(&self, vars: &[usize], text_ids: bool) -> Vec<String> {
260        let mut sel = Vec::new();
261        for idx in vars {
262            match self.cols.get(idx).map(|b| &b.col) {
263                Some(Col::Id(x)) => sel.push(if text_ids {
264                    format!("CAST({x} AS TEXT) AS v{idx}")
265                } else {
266                    format!("{x} AS v{idx}")
267                }),
268                Some(Col::Val(v)) => {
269                    for (suffix, field) in VAL_FIELDS.iter().zip(val_fields(v)) {
270                        if text_ids && *suffix == "i" {
271                            sel.push(format!("CAST({field} AS TEXT) AS v{idx}_{suffix}"));
272                        } else {
273                            sel.push(format!("{field} AS v{idx}_{suffix}"));
274                        }
275                    }
276                }
277                None => sel.push(format!("NULL AS v{idx}")),
278            }
279        }
280        sel
281    }
282
283    /// Renders everything after the select list: FROM, WHERE, GROUP BY, ORDER BY, LIMIT.
284    pub(crate) fn tail(&self) -> String {
285        let mut sql = String::new();
286        if !self.from.is_empty() {
287            sql.push_str(" FROM ");
288            sql.push_str(&Self::render_from(&self.from));
289        }
290        if !self.wheres.is_empty() {
291            sql.push_str(" WHERE ");
292            sql.push_str(&self.wheres.join(" AND "));
293        }
294        if let Some(g) = &self.group_by {
295            if !g.is_empty() {
296                sql.push_str(" GROUP BY ");
297                sql.push_str(&g.join(", "));
298            }
299        }
300        if !self.order_by.is_empty() {
301            sql.push_str(" ORDER BY ");
302            sql.push_str(&self.order_by.join(", "));
303        }
304        if self.limit.is_some() || self.offset > 0 || self.no_flatten {
305            let _ = write!(
306                sql,
307                " LIMIT {}",
308                self.limit.map_or_else(|| "-1".into(), |l| l.to_string())
309            );
310            if self.offset > 0 {
311                let _ = write!(sql, " OFFSET {}", self.offset);
312            }
313        }
314        sql
315    }
316
317    /// Renders the block as a SELECT statement.
318    pub(crate) fn to_select(&self, vars: Option<&[usize]>, text_ids: bool) -> String {
319        let all: Vec<usize> = self.cols.keys().copied().collect();
320        let mut sel = self.select_list(vars.unwrap_or(&all), text_ids);
321        sel.extend(self.extra_select.iter().cloned());
322        if sel.is_empty() {
323            sel.push("1 AS _u".into());
324        }
325        format!(
326            "SELECT {}{}{}",
327            if self.distinct { "DISTINCT " } else { "" },
328            sel.join(", "),
329            self.tail()
330        )
331    }
332}
333
334#[derive(Debug, Clone)]
335enum DefaultGraph {
336    Zero,
337    Union,
338    List(Vec<i64>),
339}
340
341#[derive(Debug, Clone)]
342struct Dataset {
343    default: DefaultGraph,
344    named: Option<Vec<i64>>,
345}
346
347#[derive(Debug, Clone, Copy)]
348enum GraphScope {
349    Default,
350    Fixed(i64),
351    Var(usize),
352    /// `GRAPH <oxilite:history>`: the store's commits and changes (see `version::history_sql`).
353    History,
354}
355
356/// The SPARQL → SQL compiler state for one query or update.
357pub(crate) struct Compiler<'a> {
358    pub stats: &'a Stats,
359    pub caps: &'a Capabilities,
360    pub options: &'a QueryOptions,
361    vars: HashMap<Variable, usize>,
362    pub var_names: Vec<Variable>,
363    aliases: usize,
364    /// Constants appearing in the query: decodable without a lookup.
365    pub constants: HashMap<i64, Term>,
366    /// Bindings visible from enclosing EXISTS scopes.
367    pub outer: Vec<BTreeMap<usize, Binding>>,
368    pub now: Literal,
369    pub base_iri: Option<String>,
370    dataset: Dataset,
371    scope: GraphScope,
372    /// Rows needed by constants that end up stored (update templates).
373    pub rows: EncodedRows,
374    /// Planner decisions and warnings, for `explain()`.
375    pub notes: Vec<String>,
376    /// Triple-term patterns awaiting a `triple_terms` join (variable, pattern).
377    pending_triples: Vec<(usize, TriplePattern)>,
378    /// Variables bound by an enclosing join (e.g. the left side of OPTIONAL): the planner
379    /// orders patterns as if they were bound, since SQLite evaluates the right side per row.
380    pub(crate) plan_hint: Vec<HashSet<usize>>,
381    /// The tick the quads are read at (`as_of`, or the version of an enclosing
382    /// `SERVICE <oxilite:version/…>`); `None` reads the current store.
383    pub(crate) as_of: Option<i64>,
384}
385
386impl<'a> Compiler<'a> {
387    pub(crate) fn new(
388        stats: &'a Stats,
389        caps: &'a Capabilities,
390        options: &'a QueryOptions,
391        dataset: Option<&QueryDataset>,
392        base_iri: Option<String>,
393    ) -> Self {
394        let mut dataset = match dataset {
395            Some(ds) => Dataset {
396                default: DefaultGraph::List(
397                    ds.default
398                        .iter()
399                        .map(|g| named_node_id(g.as_str()))
400                        .collect(),
401                ),
402                // `None` (e.g. `WITH`) leaves every named graph available, as in spareval.
403                named: ds
404                    .named
405                    .as_ref()
406                    .map(|n| n.iter().map(|g| named_node_id(g.as_str())).collect()),
407            },
408            None => Dataset {
409                default: if options.union_default_graph {
410                    DefaultGraph::Union
411                } else {
412                    DefaultGraph::Zero
413                },
414                named: None,
415            },
416        };
417        if let Some(d) = &options.default_graph {
418            dataset.default = DefaultGraph::List(d.clone());
419        }
420        if let Some(n) = &options.named_graphs {
421            dataset.named = Some(n.clone());
422        }
423        Self {
424            stats,
425            caps,
426            options,
427            vars: HashMap::new(),
428            var_names: Vec::new(),
429            aliases: 0,
430            constants: HashMap::new(),
431            outer: Vec::new(),
432            now: Literal::from(oxsdatatypes::DateTime::now()),
433            base_iri,
434            dataset,
435            scope: GraphScope::Default,
436            rows: EncodedRows::default(),
437            notes: Vec::new(),
438            pending_triples: Vec::new(),
439            plan_hint: Vec::new(),
440            as_of: options.as_of_tick,
441        }
442    }
443
444    /// Joins `terms` once per stored-term variable an expression reads, so that its fields
445    /// come from one row instead of one correlated lookup each (see [`V::from_joined`]).
446    /// Returns the bindings to restore once the expression is compiled: other operators keep
447    /// seeing plain id columns.
448    pub(crate) fn join_terms(
449        &mut self,
450        b: &mut Block,
451        e: &Expression,
452    ) -> Vec<(usize, Binding, String)> {
453        if matches!(e, Expression::Variable(_) | Expression::Bound(_)) {
454            return Vec::new();
455        }
456        let mut vars = Vec::new();
457        expression_variables(e, &mut vars);
458        self.join_term_vars(b, &vars)
459    }
460
461    /// [`Self::join_terms`] for a list of variables.
462    pub(crate) fn join_term_vars(
463        &mut self,
464        b: &mut Block,
465        vars: &[Variable],
466    ) -> Vec<(usize, Binding, String)> {
467        let mut restore = Vec::new();
468        if b.stage >= Stage::Grouped || b.from.is_empty() || !b.is_plain() {
469            return restore;
470        }
471        for v in vars {
472            let Some(&idx) = self.vars.get(v) else {
473                continue;
474            };
475            let Some(bind) = b.cols.get(&idx) else {
476                continue;
477            };
478            let Col::Id(x) = &bind.col else {
479                continue;
480            };
481            if bind.correlated || bind.computed {
482                continue;
483            }
484            let x = x.clone();
485            let t = self.alias("t");
486            b.from.push(FromItem {
487                join: Join::Left(format!("{t}.id = {x}")),
488                item: format!("terms {t}"),
489            });
490            let mut joined = bind.clone();
491            let v = V::from_joined(&x, &t);
492            let marker = v.lex.clone();
493            joined.col = Col::Val(Box::new(v));
494            restore.push((idx, bind.clone(), marker));
495            b.cols.insert(idx, joined);
496        }
497        restore
498    }
499
500    /// Puts back the id bindings replaced by [`Self::join_terms`], unless the block was sealed
501    /// meanwhile (its bindings then name the subquery's columns).
502    pub(crate) fn restore_terms(b: &mut Block, restore: Vec<(usize, Binding, String)>) {
503        for (i, bind, marker) in restore {
504            if let Some(cur) = b.cols.get_mut(&i) {
505                if matches!(&cur.col, Col::Val(v) if v.lex == marker) {
506                    *cur = bind;
507                }
508            }
509        }
510    }
511
512    /// Keeps SQL shallow for nested function calls: a large argument (e.g. the canonical string
513    /// of a decimal inside `xsd:float(xsd:string(?price))`) is computed once as a column of a
514    /// sealed subquery, instead of being inlined everywhere the outer function uses it, which
515    /// multiplies SQL size (and per-row work) at each nesting level.
516    pub(crate) fn shallow(&mut self, b: Block, e: &Expression) -> Result<(Block, Expression)> {
517        use Expression as E;
518        let two = |me: &mut Self, b: Block, x: &E, y: &E| -> Result<(Block, E, E)> {
519            let (b, x) = me.shallow_arg(b, x)?;
520            let (b, y) = me.shallow_arg(b, y)?;
521            Ok((b, x, y))
522        };
523        Ok(match e {
524            E::FunctionCall(f, args) => {
525                let mut b = b;
526                let mut out = Vec::with_capacity(args.len());
527                for a in args {
528                    let (nb, a) = self.shallow_arg(b, a)?;
529                    b = nb;
530                    out.push(a);
531                }
532                (b, E::FunctionCall(f.clone(), out))
533            }
534            E::Equal(x, y) => {
535                let (b, x, y) = two(self, b, x, y)?;
536                (b, E::Equal(Box::new(x), Box::new(y)))
537            }
538            E::Less(x, y) => {
539                let (b, x, y) = two(self, b, x, y)?;
540                (b, E::Less(Box::new(x), Box::new(y)))
541            }
542            E::LessOrEqual(x, y) => {
543                let (b, x, y) = two(self, b, x, y)?;
544                (b, E::LessOrEqual(Box::new(x), Box::new(y)))
545            }
546            E::Greater(x, y) => {
547                let (b, x, y) = two(self, b, x, y)?;
548                (b, E::Greater(Box::new(x), Box::new(y)))
549            }
550            E::GreaterOrEqual(x, y) => {
551                let (b, x, y) = two(self, b, x, y)?;
552                (b, E::GreaterOrEqual(Box::new(x), Box::new(y)))
553            }
554            E::Add(x, y) => {
555                let (b, x, y) = two(self, b, x, y)?;
556                (b, E::Add(Box::new(x), Box::new(y)))
557            }
558            E::Subtract(x, y) => {
559                let (b, x, y) = two(self, b, x, y)?;
560                (b, E::Subtract(Box::new(x), Box::new(y)))
561            }
562            E::Multiply(x, y) => {
563                let (b, x, y) = two(self, b, x, y)?;
564                (b, E::Multiply(Box::new(x), Box::new(y)))
565            }
566            E::Divide(x, y) => {
567                let (b, x, y) = two(self, b, x, y)?;
568                (b, E::Divide(Box::new(x), Box::new(y)))
569            }
570            E::And(x, y) => {
571                let (b, x) = self.shallow(b, x)?;
572                let (b, y) = self.shallow(b, y)?;
573                (b, E::And(Box::new(x), Box::new(y)))
574            }
575            E::Or(x, y) => {
576                let (b, x) = self.shallow(b, x)?;
577                let (b, y) = self.shallow(b, y)?;
578                (b, E::Or(Box::new(x), Box::new(y)))
579            }
580            E::Not(x) => {
581                let (b, x) = self.shallow(b, x)?;
582                (b, E::Not(Box::new(x)))
583            }
584            _ => (b, e.clone()),
585        })
586    }
587
588    /// [`Self::shallow`] for an operand: after lifting inside it, a function call whose SQL is
589    /// large becomes a column of a sealed subquery.
590    fn shallow_arg(&mut self, b: Block, a: &Expression) -> Result<(Block, Expression)> {
591        const LIMIT: usize = 1_500;
592        let (mut b, a) = self.shallow(b, a)?;
593        if !matches!(a, Expression::FunctionCall(..)) {
594            return Ok((b, a));
595        }
596        let v = self.expr_term(&a, &b.cols)?;
597        if v.size() <= LIMIT {
598            return Ok((b, a));
599        }
600        let hidden = self.fresh_var("arg");
601        b.cols.insert(
602            hidden,
603            Binding {
604                col: Col::Val(Box::new(v)),
605                nullable: true,
606                computed: true,
607                correlated: false,
608            },
609        );
610        b = self.seal(b);
611        Ok((b, Expression::Variable(self.var_names[hidden].clone())))
612    }
613
614    pub(crate) fn var(&mut self, v: &Variable) -> usize {
615        if let Some(i) = self.vars.get(v) {
616            return *i;
617        }
618        let i = self.var_names.len();
619        self.vars.insert(v.clone(), i);
620        self.var_names.push(v.clone());
621        i
622    }
623
624    pub(crate) fn fresh_var(&mut self, hint: &str) -> usize {
625        let v = Variable::new_unchecked(format!("\u{1}{hint}{}", self.var_names.len()));
626        self.var(&v)
627    }
628
629    pub(crate) fn alias(&mut self, prefix: &str) -> String {
630        self.aliases += 1;
631        format!("{prefix}{}", self.aliases)
632    }
633
634    /// Encodes a constant term and remembers it for decoding.
635    pub(crate) fn constant_id(&mut self, t: &Term) -> Result<i64> {
636        let id = match t {
637            Term::Triple(tr) => {
638                let id = self.rows.triple(tr.as_ref().as_ref());
639                self.constants.insert(id, t.clone());
640                // Components are decodable too.
641                self.constants.insert(
642                    term_id(tr.subject.as_ref().into()),
643                    tr.subject.clone().into(),
644                );
645                self.constants.insert(
646                    named_node_id(tr.predicate.as_str()),
647                    tr.predicate.clone().into(),
648                );
649                self.constants
650                    .insert(term_id(tr.object.as_ref()), tr.object.clone());
651                return Ok(id);
652            }
653            Term::Literal(l) => encode_literal(l.as_ref()).0,
654            _ => term_id(t.as_ref()),
655        };
656        self.constants.insert(id, t.clone());
657        Ok(id)
658    }
659
660    fn outer_binding(&self, idx: usize) -> Option<&Binding> {
661        self.outer.iter().rev().find_map(|s| s.get(&idx))
662    }
663
664    /// Wraps a block into a subquery.
665    pub(crate) fn seal(&mut self, b: Block) -> Block {
666        let alias = self.alias("s");
667        let sql = b.to_select(None, false);
668        let mut cols = BTreeMap::new();
669        for (idx, bind) in &b.cols {
670            let col = match &bind.col {
671                Col::Id(_) => Col::Id(format!("{alias}.v{idx}")),
672                Col::Val(v) => Col::Val(Box::new(V {
673                    id: v.id.as_ref().map(|_| format!("{alias}.v{idx}_i")),
674                    kind: format!("{alias}.v{idx}_k"),
675                    lex: format!("{alias}.v{idx}_l"),
676                    dt: format!("{alias}.v{idx}_d"),
677                    lang: format!("{alias}.v{idx}_g"),
678                    num: format!("{alias}.v{idx}_n"),
679                    nt: format!("{alias}.v{idx}_t"),
680                    ts: format!("{alias}.v{idx}_s"),
681                    boolv: format!("{alias}.v{idx}_b"),
682                    stat: v.stat,
683                    computed_num: false,
684                    decodable: v.decodable,
685                    aux: format!("{alias}.v{idx}_x"),
686                    tz: "NULL".into(),
687                })),
688            };
689            cols.insert(
690                *idx,
691                Binding {
692                    col,
693                    nullable: bind.nullable,
694                    computed: false,
695                    correlated: false,
696                },
697            );
698        }
699        // A sealed computed number keeps a NULL lexical form: re-derive it from num/nt.
700        for (idx, bind) in &b.cols {
701            if let Col::Val(v) = &bind.col {
702                if v.computed_num {
703                    if let Some(Binding {
704                        col: Col::Val(sv), ..
705                    }) = cols.get_mut(idx)
706                    {
707                        let n = V::numeric(sv.num.clone(), sv.nt.clone());
708                        sv.lex = n.lex;
709                        sv.computed_num = true;
710                    }
711                }
712            }
713        }
714        Block {
715            from: vec![FromItem {
716                join: Join::First,
717                item: format!("({sql}) AS {alias}"),
718            }],
719            cols,
720            ..Block::default()
721        }
722    }
723
724    fn plain(&mut self, b: Block) -> Block {
725        if b.is_plain() {
726            b
727        } else {
728            self.seal(b)
729        }
730    }
731
732    /// Compiles a graph pattern.
733    pub(crate) fn pattern(&mut self, p: &GraphPattern) -> Result<Block> {
734        match p {
735            GraphPattern::Bgp { patterns } => self.bgp(patterns),
736            GraphPattern::Join { left, right } => {
737                let a = self.pattern(left)?;
738                if let GraphPattern::Path {
739                    subject,
740                    path,
741                    object,
742                } = right.as_ref()
743                {
744                    if let Some(b) = self.seeded_path(&a, subject, path, object)? {
745                        return self.join(a, b);
746                    }
747                }
748                let b = self.pattern(right)?;
749                self.join(a, b)
750            }
751            GraphPattern::Filter { expr, inner } => {
752                let b = self.pattern(inner)?;
753                let b = self.plain(b);
754                let (b, expr) = self.shallow(b, expr)?;
755                let mut b = b;
756                let conds = self.filter_conditions(&expr, &b.cols)?;
757                b.wheres.extend(conds);
758                Ok(b)
759            }
760            GraphPattern::Graph { name, inner } => {
761                let saved = self.scope;
762                self.scope = match name {
763                    NamedNodePattern::NamedNode(n) if n.as_str() == HISTORY_GRAPH => {
764                        GraphScope::History
765                    }
766                    NamedNodePattern::NamedNode(n) => {
767                        let id = self.constant_id(&n.clone().into())?;
768                        GraphScope::Fixed(id)
769                    }
770                    NamedNodePattern::Variable(v) => GraphScope::Var(self.var(v)),
771                };
772                let accesses = self.aliases;
773                let r = self.pattern(inner);
774                let scope = self.scope;
775                self.scope = saved;
776                let mut b = r?;
777                match scope {
778                    GraphScope::Var(v) => {
779                        if !b.cols.contains_key(&v) {
780                            // No quad access binds the graph: enumerate named graphs.
781                            let g = self.graph_list_block(v);
782                            b = self.join(b, g)?;
783                        }
784                    }
785                    GraphScope::Fixed(id) => {
786                        if self.aliases == accesses || !has_quad_access(p) {
787                            // GRAPH <g> {} only matches if the graph exists.
788                            b = self.plain(b);
789                            b.wheres
790                                .push(format!("EXISTS (SELECT 1 FROM graphs WHERE id = {id})"));
791                        }
792                    }
793                    GraphScope::Default | GraphScope::History => {}
794                }
795                Ok(b)
796            }
797            GraphPattern::Extend {
798                inner,
799                variable,
800                expression,
801            } => {
802                let b = self.pattern(inner)?;
803                let mut b = self.plain(b);
804                let restore = self.join_terms(&mut b, expression);
805                let (b, expression) = self.shallow(b, expression)?;
806                let mut b = b;
807                let expression = &expression;
808                let v = self.expr_term(expression, &b.cols)?;
809                Self::restore_terms(&mut b, restore);
810                let idx = self.var(variable);
811                let (nullable, computed) = match expression {
812                    Expression::Variable(x) => {
813                        let xi = self.var(x);
814                        (b.cols.get(&xi).is_none_or(|b| b.nullable), false)
815                    }
816                    Expression::NamedNode(_) | Expression::Literal(_) => (false, true),
817                    _ => (true, true),
818                };
819                let col = match &v.id {
820                    Some(id) if v.decodable => Col::Id(id.clone()),
821                    _ => Col::Val(Box::new(v)),
822                };
823                b.cols.insert(
824                    idx,
825                    Binding {
826                        col,
827                        nullable,
828                        computed,
829                        correlated: false,
830                    },
831                );
832                Ok(b)
833            }
834            GraphPattern::Values {
835                variables,
836                bindings,
837            } => self.values(variables, bindings),
838            GraphPattern::OrderBy { inner, expression } => {
839                let mut b = self.pattern(inner)?;
840                // Sort keys over an aggregate of this block would put the aggregate inside
841                // the keys' scalar subqueries (a SQLite error): sort a sealed block instead.
842                let reads_aggregate = b.stage == Stage::Grouped
843                    && expression.iter().any(|oe| {
844                        let (OrderExpression::Asc(e) | OrderExpression::Desc(e)) = oe;
845                        expr_mentions(e, &mut |v| {
846                            self.vars
847                                .get(v)
848                                .and_then(|i| b.cols.get(i))
849                                .is_some_and(|c| c.computed)
850                        })
851                    });
852                if b.stage > Stage::Grouped || reads_aggregate {
853                    b = self.seal(b);
854                }
855                for oe in expression {
856                    let (e, desc) = match oe {
857                        OrderExpression::Asc(e) => (e, false),
858                        OrderExpression::Desc(e) => (e, true),
859                    };
860                    let v = self.expr_term(e, &b.cols)?;
861                    for k in order_keys(&v) {
862                        b.order_by.push(if desc { format!("{k} DESC") } else { k });
863                    }
864                }
865                b.stage = Stage::Ordered;
866                Ok(b)
867            }
868            GraphPattern::Project { inner, variables } => {
869                // Inside `GRAPH ?g`, a subquery's own ?g (if not projected) is a different
870                // variable: bind the graph to a hidden variable and expose it as ?g after
871                // projection.
872                let graph_var = match self.scope {
873                    GraphScope::Var(g) => Some(g),
874                    _ => None,
875                };
876                let hidden = graph_var.map(|_| self.fresh_var("g"));
877                let saved = self.scope;
878                if let Some(h) = hidden {
879                    self.scope = GraphScope::Var(h);
880                }
881                let r = self.pattern(inner);
882                self.scope = saved;
883                let mut b = r?;
884                if b.stage >= Stage::Distinct {
885                    b = self.seal(b);
886                }
887                let hidden_binding = hidden.and_then(|h| b.cols.get(&h).cloned());
888                let mut cols = BTreeMap::new();
889                for v in variables {
890                    let idx = self.var(v);
891                    let bind = b.cols.remove(&idx).unwrap_or(Binding {
892                        col: Col::Id("NULL".into()),
893                        nullable: true,
894                        computed: true,
895                        correlated: false,
896                    });
897                    cols.insert(idx, bind);
898                }
899                b.cols = cols;
900                if let (Some(g), Some(h)) = (graph_var, hidden_binding) {
901                    match b.cols.get(&g).and_then(|x| x.col.key().map(str::to_string)) {
902                        Some(inner_g) => {
903                            let hk = h.col.key().unwrap_or("NULL").to_string();
904                            b.wheres.push(format!("{inner_g} = {hk}"));
905                        }
906                        None => {
907                            b.cols.insert(g, h);
908                        }
909                    }
910                }
911                Ok(b)
912            }
913            GraphPattern::Distinct { inner } => {
914                let mut b = self.pattern(inner)?;
915                if b.stage >= Stage::Distinct {
916                    b = self.seal(b);
917                }
918                b.distinct = true;
919                b.stage = Stage::Distinct;
920                Ok(b)
921            }
922            // REDUCED may drop any duplicates; Oxigraph drops them all, so do we.
923            GraphPattern::Reduced { inner } => self.pattern(&GraphPattern::Distinct {
924                inner: inner.clone(),
925            }),
926            GraphPattern::Slice {
927                inner,
928                start,
929                length,
930            } => {
931                let mut b = self.pattern(inner)?;
932                if b.stage == Stage::Sliced {
933                    b = self.seal(b);
934                }
935                b.offset = *start;
936                b.limit = *length;
937                b.stage = Stage::Sliced;
938                Ok(b)
939            }
940            other => self.pattern_ext(other),
941        }
942    }
943
944    /// OPTIONAL, UNION, MINUS, GROUP BY and property paths live in `ops.rs`.
945    fn pattern_ext(&mut self, p: &GraphPattern) -> Result<Block> {
946        self.pattern_m2(p)
947    }
948
949    /// Condition that two bindings of the same variable agree (SPARQL compatibility), and the
950    /// binding that results from merging them.
951    pub(crate) fn unify(a: &Binding, b: &Binding) -> (String, Binding) {
952        let eq = match (a.col.key(), b.col.key()) {
953            (Some(x), Some(y)) => format!("{x} = {y}"),
954            _ => expr::same_term(&a.col.value(), &b.col.value()),
955        };
956        if !a.nullable && !b.nullable {
957            return (eq, a.clone());
958        }
959        let is_null = |x: &Binding| match &x.col {
960            Col::Id(i) => format!("{i} IS NULL"),
961            Col::Val(v) => format!("({}) IS NULL", v.kind),
962        };
963        let cond = format!("({eq} OR {} OR {})", is_null(a), is_null(b));
964        let col = match (&a.col, &b.col) {
965            (Col::Id(x), Col::Id(y)) => Col::Id(format!("COALESCE({x}, {y})")),
966            _ => {
967                let (av, bv) = (a.col.value(), b.col.value());
968                Col::Val(Box::new(Self::choose(
969                    &format!("(({}) IS NOT NULL)", av.kind),
970                    &av,
971                    &bv,
972                )))
973            }
974        };
975        (
976            cond,
977            Binding {
978                col,
979                nullable: a.nullable && b.nullable,
980                computed: true,
981                correlated: a.correlated || b.correlated,
982            },
983        )
984    }
985
986    fn graph_list_block(&mut self, v: usize) -> Block {
987        let a = self.alias("gr");
988        let mut b = Block::default();
989        b.from.push(FromItem {
990            join: Join::First,
991            item: format!("graphs {a}"),
992        });
993        if let Some(named) = &self.dataset.named {
994            b.wheres.push(in_list(&format!("{a}.id"), named));
995        }
996        b.cols.insert(v, Binding::id(format!("{a}.id")));
997        b
998    }
999
1000    fn pos(&mut self, t: &TermPattern, bnodes: &mut HashMap<String, usize>) -> Result<Pos> {
1001        Ok(match t {
1002            TermPattern::NamedNode(n) => Pos::Const(self.constant_id(&n.clone().into())?),
1003            TermPattern::Literal(l) => Pos::Const(self.constant_id(&l.clone().into())?),
1004            TermPattern::Variable(v) => Pos::Var(self.var(v)),
1005            TermPattern::BlankNode(b) => {
1006                // Blank nodes act as variables; the parser gives them query-unique labels, so
1007                // one hidden variable per label keeps them joined across paths and BGPs.
1008                let _ = bnodes;
1009                Pos::Var(self.var(&Variable::new_unchecked(format!("\u{1}b{}", b.as_str()))))
1010            }
1011            TermPattern::Triple(tp) => {
1012                if let Some(t) = ground_triple(tp) {
1013                    Pos::Const(self.constant_id(&t.into())?)
1014                } else {
1015                    let v = self.fresh_var("t");
1016                    self.pending_triples.push((v, (**tp).clone()));
1017                    Pos::Var(v)
1018                }
1019            }
1020        })
1021    }
1022
1023    fn pos_nn(&mut self, p: &NamedNodePattern) -> Result<Pos> {
1024        Ok(match p {
1025            NamedNodePattern::NamedNode(n) => Pos::Const(self.constant_id(&n.clone().into())?),
1026            NamedNodePattern::Variable(v) => Pos::Var(self.var(v)),
1027        })
1028    }
1029
1030    /// Binds a SQL column to a pattern position.
1031    pub(crate) fn bind_pos(&mut self, b: &mut Block, colsql: &str, pos: Pos) -> Result<()> {
1032        match pos {
1033            Pos::Const(id) => b.wheres.push(format!("{colsql} = {id}")),
1034            Pos::Var(v) => {
1035                if let Some(existing) = b.cols.get(&v) {
1036                    let cond = match existing.col.key() {
1037                        Some(x) => format!("{colsql} = {x}"),
1038                        None => expr::same_term(&V::from_id(colsql), &existing.col.value()),
1039                    };
1040                    b.wheres.push(if existing.nullable {
1041                        let null = match &existing.col {
1042                            Col::Id(x) => format!("{x} IS NULL"),
1043                            Col::Val(v) => format!("({}) IS NULL", v.kind),
1044                        };
1045                        format!("({null} OR {cond})")
1046                    } else {
1047                        cond
1048                    });
1049                    return Ok(());
1050                }
1051                let mut correlated = false;
1052                if let Some(outer) = self.outer_binding(v).cloned() {
1053                    let (cond, _) = Self::unify(&Binding::id(colsql), &outer);
1054                    b.wheres.push(cond);
1055                    correlated = true;
1056                }
1057                b.cols.insert(
1058                    v,
1059                    Binding {
1060                        correlated,
1061                        ..Binding::id(colsql)
1062                    },
1063                );
1064            }
1065        }
1066        Ok(())
1067    }
1068
1069    /// Adds graph conditions for a quad alias according to the current scope and dataset.
1070    ///
1071    /// When another position is bound (`selective`), the graph equality is written as
1072    /// `+q.g = …`: the unary plus keeps SQLite from choosing the `gspo` index on a `g`
1073    /// equality that nearly every quad satisfies, so it uses the s/p/o permutation instead.
1074    pub(crate) fn graph_pos(&mut self, b: &mut Block, q: &str, selective: bool) -> Result<()> {
1075        let plus = if selective { "+" } else { "" };
1076        match self.scope {
1077            GraphScope::Default => match self.dataset.default.clone() {
1078                DefaultGraph::Zero => b.wheres.push(format!("{plus}{q}.g = {DEFAULT_GRAPH_ID}")),
1079                DefaultGraph::List(l) if l.is_empty() => b.wheres.push("0".into()),
1080                DefaultGraph::List(l) if l.len() == 1 => {
1081                    b.wheres.push(format!("{plus}{q}.g = {}", l[0]))
1082                }
1083                DefaultGraph::List(l) => {
1084                    b.wheres.push(in_list(&format!("{q}.g"), &l));
1085                    let d = self.alias("d");
1086                    b.wheres.push(format!(
1087                        "NOT EXISTS (SELECT 1 FROM {} {d} WHERE {d}.s = {q}.s AND {d}.p = {q}.p AND {d}.o = {q}.o AND {d}.g < {q}.g AND {})",
1088                        self.entailment().base(),
1089                        in_list(&format!("{d}.g"), &l)
1090                    ));
1091                }
1092                DefaultGraph::Union => {
1093                    let d = self.alias("d");
1094                    let base = self.entailment().base();
1095                    b.wheres.push(format!(
1096                        "NOT EXISTS (SELECT 1 FROM {base} {d} WHERE {d}.s = {q}.s AND {d}.p = {q}.p AND {d}.o = {q}.o AND {d}.g < {q}.g)"
1097                    ));
1098                }
1099            },
1100            GraphScope::Fixed(id) => {
1101                if self
1102                    .dataset
1103                    .named
1104                    .as_ref()
1105                    .is_some_and(|n| !n.contains(&id))
1106                {
1107                    b.wheres.push("0".into());
1108                }
1109                b.wheres.push(format!("{plus}{q}.g = {id}"));
1110            }
1111            GraphScope::Var(v) => {
1112                b.wheres.push(format!("{q}.g <> {DEFAULT_GRAPH_ID}"));
1113                if let Some(named) = self.dataset.named.clone() {
1114                    b.wheres.push(in_list(&format!("{q}.g"), &named));
1115                }
1116                self.bind_pos(b, &format!("{q}.g"), Pos::Var(v))?;
1117            }
1118            GraphScope::History => {}
1119        }
1120        Ok(())
1121    }
1122
1123    /// `?c oxl:added <<( s p o )>>` / `oxl:removed` in the history graph: one row of the change
1124    /// log, the commit bound to its tick and the changed triple to the logged columns.
1125    fn change_access(&mut self, b: &mut Block, tp: &TriplePattern, added: bool) -> Result<()> {
1126        if self.stats.version.history == crate::version::History::None {
1127            return Err(Error::Other(
1128                "this store keeps no change log: raise its versioning level to `log` to read changes".into(),
1129            ));
1130        }
1131        let TermPattern::Triple(inner) = &tp.object else {
1132            return Err(Error::Other(
1133                "oxl:added and oxl:removed take a triple term: ?c oxl:added <<( ?s ?p ?o )>>"
1134                    .into(),
1135            ));
1136        };
1137        if matches!(inner.subject, TermPattern::Triple(_))
1138            || matches!(inner.object, TermPattern::Triple(_))
1139        {
1140            return Err(Error::unsupported(
1141                "nested triple terms in a change pattern",
1142            ));
1143        }
1144        let mut bnodes = HashMap::new();
1145        let c = self.pos(&tp.subject, &mut bnodes)?;
1146        let (s, p, o) = (
1147            self.pos(&inner.subject, &mut bnodes)?,
1148            self.pos_nn(&inner.predicate)?,
1149            self.pos(&inner.object, &mut bnodes)?,
1150        );
1151        let l = self.alias("hl");
1152        b.from.push(FromItem {
1153            join: if b.from.is_empty() {
1154                Join::First
1155            } else {
1156                Join::Inner
1157            },
1158            item: format!("quad_log {l}"),
1159        });
1160        b.wheres.push(format!("{l}.op = {}", u8::from(added)));
1161        self.bind_pos(b, &format!("({l}.tx + {})", expr::INT_BASE), c)?;
1162        self.bind_pos(b, &format!("{l}.s"), s)?;
1163        self.bind_pos(b, &format!("{l}.p"), p)?;
1164        self.bind_pos(b, &format!("{l}.o"), o)?;
1165        Ok(())
1166    }
1167
1168    /// Builds entailed-triple sources for this query's reasoning options.
1169    pub(crate) fn entailment(&self) -> Entailment<'a> {
1170        Entailment {
1171            reasoning: self.options.reasoning,
1172            inferred: self.options.include_inferred,
1173            hide_schema: !self.options.include_schema_graphs,
1174            transitive: &self.stats.transitive,
1175            scopes: &self.stats.schema_scopes,
1176            max_compound: self.caps.max_compound_select,
1177            as_of: self.as_of,
1178        }
1179    }
1180
1181    /// Adds a `quads` alias bound to the three positions (in the current graph scope).
1182    pub(crate) fn quad_access(
1183        &mut self,
1184        b: &mut Block,
1185        join: Join,
1186        s: Pos,
1187        p: Pos,
1188        o: Pos,
1189    ) -> Result<String> {
1190        let q = self.alias("q");
1191        let bound = |pos: Pos, b: &Block, me: &Self| match pos {
1192            Pos::Const(_) => true,
1193            Pos::Var(v) => b.cols.contains_key(&v) || me.outer_binding(v).is_some(),
1194        };
1195        let selective = bound(s, b, self) || bound(p, b, self) || bound(o, b, self);
1196        let ent = self.entailment();
1197        // With reasoning, a merged default graph is merged inside the derived table (so an
1198        // entailed triple appears once), which then only exposes graph 0.
1199        let merge = match (&self.scope, &self.dataset.default) {
1200            (GraphScope::Default, DefaultGraph::Union) if ent.active() => {
1201                Some(GraphFilter::Merge(None))
1202            }
1203            (GraphScope::Default, DefaultGraph::List(l)) if ent.active() && l.len() > 1 => {
1204                Some(GraphFilter::Merge(Some(l.clone())))
1205            }
1206            _ => None,
1207        };
1208        let source = if matches!(self.scope, GraphScope::History) {
1209            crate::version::history_sql(&self.stats.version)?
1210        } else if ent.active() {
1211            let c = |p: Pos| match p {
1212                Pos::Const(id) => Some(id),
1213                Pos::Var(_) => None,
1214            };
1215            ent.source(
1216                c(s),
1217                c(p),
1218                c(o),
1219                merge.as_ref().unwrap_or(&GraphFilter::Keep),
1220            )
1221        } else {
1222            ent.base()
1223        };
1224        b.from.push(FromItem {
1225            join: if b.from.is_empty() { Join::First } else { join },
1226            item: format!("{source} {q}"),
1227        });
1228        self.bind_pos(b, &format!("{q}.s"), s)?;
1229        self.bind_pos(b, &format!("{q}.p"), p)?;
1230        self.bind_pos(b, &format!("{q}.o"), o)?;
1231        if merge.is_none() {
1232            self.graph_pos(b, &q, selective)?;
1233        }
1234        Ok(q)
1235    }
1236
1237    fn bgp(&mut self, patterns: &[TriplePattern]) -> Result<Block> {
1238        if patterns.is_empty() {
1239            return Ok(Block::default());
1240        }
1241        if matches!(self.scope, GraphScope::History) {
1242            let change = |tp: &TriplePattern| match &tp.predicate {
1243                NamedNodePattern::NamedNode(n) if n.as_str() == crate::version::vocab::ADDED => {
1244                    Some(true)
1245                }
1246                NamedNodePattern::NamedNode(n) if n.as_str() == crate::version::vocab::REMOVED => {
1247                    Some(false)
1248                }
1249                _ => None,
1250            };
1251            if patterns.iter().any(|tp| change(tp).is_some()) {
1252                let rest: Vec<TriplePattern> = patterns
1253                    .iter()
1254                    .filter(|tp| change(tp).is_none())
1255                    .cloned()
1256                    .collect();
1257                let mut b = self.bgp(&rest)?;
1258                for tp in patterns {
1259                    if let Some(added) = change(tp) {
1260                        self.change_access(&mut b, tp, added)?;
1261                    }
1262                }
1263                return Ok(b);
1264            }
1265        }
1266        let mut bnodes = HashMap::new();
1267        let mut enc = Vec::with_capacity(patterns.len());
1268        for tp in patterns {
1269            enc.push([
1270                self.pos(&tp.subject, &mut bnodes)?,
1271                self.pos_nn(&tp.predicate)?,
1272                self.pos(&tp.object, &mut bnodes)?,
1273            ]);
1274        }
1275        let pre: HashSet<usize> = self
1276            .outer
1277            .iter()
1278            .flat_map(|m| m.keys().copied())
1279            .chain(self.plan_hint.iter().flatten().copied())
1280            .collect();
1281        let ord: Vec<usize> = if self.options.sqlite_planner {
1282            (0..enc.len()).collect()
1283        } else {
1284            plan::order(&enc, &pre, self.stats)
1285        };
1286        let join = if self.options.sqlite_planner {
1287            Join::Inner
1288        } else {
1289            Join::Cross
1290        };
1291        // Record the plan for explain(): order, estimated rows, Cartesian products.
1292        {
1293            let mut bound = pre.clone();
1294            let mut steps = Vec::new();
1295            for (n, i) in ord.iter().enumerate() {
1296                let est = plan::estimate(&enc[*i], &bound, self.stats);
1297                let vars: Vec<usize> = enc[*i]
1298                    .iter()
1299                    .filter_map(|p| match p {
1300                        Pos::Var(v) => Some(*v),
1301                        Pos::Const(_) => None,
1302                    })
1303                    .collect();
1304                if n > 0 && !vars.iter().any(|v| bound.contains(v)) {
1305                    self.notes.push(format!(
1306                        "warning: Cartesian product: triple pattern {} shares no variable with the patterns before it",
1307                        patterns[*i]
1308                    ));
1309                }
1310                steps.push(format!("{} (~{est:.0} rows)", patterns[*i]));
1311                bound.extend(vars);
1312            }
1313            self.notes.push(format!(
1314                "join order ({}): {}",
1315                if self.options.sqlite_planner {
1316                    "SQLite planner"
1317                } else if self.stats.available {
1318                    "statistics"
1319                } else {
1320                    "heuristics, run optimize() for statistics"
1321                },
1322                steps.join(" → ")
1323            ));
1324        }
1325        let mut b = Block::default();
1326        for i in ord {
1327            let [s, p, o] = enc[i];
1328            self.quad_access(&mut b, join.clone(), s, p, o)?;
1329        }
1330        // RDF 1.2 triple-term patterns: join `triple_terms` on the term's id and match its
1331        // components (possibly nested).
1332        while let Some((v, tp)) = self.pending_triples.pop() {
1333            let Some(key) = b.cols.get(&v).and_then(|x| x.col.key().map(str::to_string)) else {
1334                return Err(Error::unsupported("unbound triple term pattern"));
1335            };
1336            let t = self.alias("tt");
1337            b.from.push(FromItem {
1338                join: Join::Inner,
1339                item: format!("triple_terms {t}"),
1340            });
1341            b.wheres.push(format!("{t}.id = {key}"));
1342            let sp = self.pos(&tp.subject, &mut bnodes)?;
1343            let pp = self.pos_nn(&tp.predicate)?;
1344            let op = self.pos(&tp.object, &mut bnodes)?;
1345            self.bind_pos(&mut b, &format!("{t}.s"), sp)?;
1346            self.bind_pos(&mut b, &format!("{t}.p"), pp)?;
1347            self.bind_pos(&mut b, &format!("{t}.o"), op)?;
1348        }
1349        Ok(b)
1350    }
1351
1352    /// Joins two blocks (SPARQL Join).
1353    pub(crate) fn join(&mut self, a: Block, b: Block) -> Result<Block> {
1354        let mut a = self.plain(a);
1355        let b = self.plain(b);
1356        if a.is_unit() {
1357            return Ok(b);
1358        }
1359        if b.is_unit() {
1360            return Ok(a);
1361        }
1362        for (idx, bb) in b.cols {
1363            match a.cols.get(&idx).cloned() {
1364                None => {
1365                    a.cols.insert(idx, bb);
1366                }
1367                Some(ab) => {
1368                    let (cond, merged) = Self::unify(&ab, &bb);
1369                    a.wheres.push(cond);
1370                    a.cols.insert(idx, merged);
1371                }
1372            }
1373        }
1374        for mut item in b.from {
1375            if a.from.is_empty() {
1376                item.join = Join::First;
1377            } else if item.join == Join::First {
1378                item.join = Join::Inner;
1379            }
1380            a.from.push(item);
1381        }
1382        a.wheres.extend(b.wheres);
1383        Ok(a)
1384    }
1385
1386    fn values(
1387        &mut self,
1388        variables: &[Variable],
1389        bindings: &[Vec<Option<GroundTerm>>],
1390    ) -> Result<Block> {
1391        let mut b = Block::default();
1392        if variables.is_empty() {
1393            match bindings.len() {
1394                0 => b.wheres.push("0".into()),
1395                1 => {}
1396                n => b.from.push(FromItem {
1397                    join: Join::First,
1398                    item: format!(
1399                        "(VALUES {}) AS {}",
1400                        vec!["(1)"; n].join(","),
1401                        self.alias("u")
1402                    ),
1403                }),
1404            }
1405            return Ok(b);
1406        }
1407        let idxs: Vec<usize> = variables.iter().map(|v| self.var(v)).collect();
1408        if bindings.is_empty() {
1409            b.wheres.push("0".into());
1410            for i in idxs {
1411                b.cols.insert(
1412                    i,
1413                    Binding {
1414                        nullable: true,
1415                        computed: true,
1416                        ..Binding::id("NULL")
1417                    },
1418                );
1419            }
1420            return Ok(b);
1421        }
1422        // Constants are not necessarily stored: each variable carries its full value (all
1423        // `V` fields) so no dictionary lookup is needed; the id remains the join key.
1424        let mut rows = Vec::new();
1425        let mut nullable = vec![false; idxs.len()];
1426        for row in bindings {
1427            let mut vals = Vec::new();
1428            for (j, t) in row.iter().enumerate() {
1429                let fields = match t {
1430                    None => {
1431                        nullable[j] = true;
1432                        val_fields(&V::null())
1433                    }
1434                    Some(t) => {
1435                        let term = ground_to_term(t);
1436                        let id = self.constant_id(&term)?;
1437                        val_fields(&V::from_term(&term, id)?)
1438                    }
1439                };
1440                vals.extend(fields);
1441            }
1442            rows.push(format!("({})", vals.join(",")));
1443        }
1444        let a = self.alias("vals");
1445        b.from.push(FromItem {
1446            join: Join::First,
1447            item: format!("(VALUES {}) AS {a}", rows.join(",")),
1448        });
1449        let n = VAL_FIELDS.len();
1450        for (j, i) in idxs.into_iter().enumerate() {
1451            let c = |k: usize| format!("{a}.column{}", j * n + k + 1);
1452            let v = V {
1453                id: Some(c(0)),
1454                kind: c(1),
1455                lex: c(2),
1456                dt: c(3),
1457                lang: c(4),
1458                num: c(5),
1459                nt: c(6),
1460                ts: c(7),
1461                boolv: c(8),
1462                stat: expr::Stat::Any,
1463                computed_num: false,
1464                decodable: false,
1465                aux: c(9),
1466                tz: "NULL".into(),
1467            };
1468            b.cols.insert(
1469                i,
1470                Binding {
1471                    col: Col::Val(Box::new(v)),
1472                    nullable: nullable[j],
1473                    computed: false,
1474                    correlated: false,
1475                },
1476            );
1477        }
1478        Ok(b)
1479    }
1480
1481    /// Compiles a FILTER into WHERE conditions, keeping sargable equalities index-friendly.
1482    pub(crate) fn filter_conditions(
1483        &mut self,
1484        e: &Expression,
1485        cols: &BTreeMap<usize, Binding>,
1486    ) -> Result<Vec<String>> {
1487        if let Expression::And(a, b) = e {
1488            let mut out = self.filter_conditions(a, cols)?;
1489            out.extend(self.filter_conditions(b, cols)?);
1490            return Ok(out);
1491        }
1492        if let Expression::Equal(a, b) | Expression::SameTerm(a, b) = e {
1493            let pair = match (a.as_ref(), b.as_ref()) {
1494                (Expression::Variable(v), c) | (c, Expression::Variable(v)) => Some((v, c)),
1495                _ => None,
1496            };
1497            if let Some((v, c)) = pair {
1498                let t: Option<Term> = match c {
1499                    Expression::NamedNode(n) => Some(n.clone().into()),
1500                    Expression::Literal(l)
1501                        if matches!(e, Expression::SameTerm(..))
1502                            || l.language().is_some()
1503                            || l.datatype() == oxrdf::vocab::xsd::STRING =>
1504                    {
1505                        Some(l.clone().into())
1506                    }
1507                    _ => None,
1508                };
1509                if let Some(t) = t {
1510                    let idx = self.var(v);
1511                    let binding = cols.get(&idx).or_else(|| self.outer_binding(idx)).cloned();
1512                    if let Some(Binding {
1513                        col: Col::Id(x), ..
1514                    }) = binding
1515                    {
1516                        // In a FILTER an error and `false` both reject, so term equality is exact.
1517                        let id = self.constant_id(&t)?;
1518                        return Ok(vec![format!("{x} = {id}")]);
1519                    }
1520                }
1521            }
1522        }
1523        Ok(vec![self.expr_bool(e, cols)?])
1524    }
1525}
1526
1527/// The triple term of a pattern without variables or blank nodes.
1528fn ground_triple(tp: &TriplePattern) -> Option<oxrdf::Triple> {
1529    fn term(t: &TermPattern) -> Option<Term> {
1530        Some(match t {
1531            TermPattern::NamedNode(n) => n.clone().into(),
1532            TermPattern::Literal(l) => l.clone().into(),
1533            TermPattern::Triple(tp) => ground_triple(tp)?.into(),
1534            TermPattern::BlankNode(_) | TermPattern::Variable(_) => return None,
1535        })
1536    }
1537    let s = match term(&tp.subject)? {
1538        Term::NamedNode(n) => oxrdf::NamedOrBlankNode::from(n),
1539        _ => return None,
1540    };
1541    let NamedNodePattern::NamedNode(p) = &tp.predicate else {
1542        return None;
1543    };
1544    Some(oxrdf::Triple::new(s, p.clone(), term(&tp.object)?))
1545}
1546
1547fn has_quad_access(p: &GraphPattern) -> bool {
1548    match p {
1549        GraphPattern::Bgp { patterns } => !patterns.is_empty(),
1550        GraphPattern::Path { .. } => true,
1551        GraphPattern::Graph { .. } | GraphPattern::Values { .. } => false,
1552        GraphPattern::Join { left, right }
1553        | GraphPattern::LeftJoin { left, right, .. }
1554        | GraphPattern::Union { left, right }
1555        | GraphPattern::Minus { left, right } => has_quad_access(left) || has_quad_access(right),
1556        GraphPattern::Filter { inner, .. }
1557        | GraphPattern::Extend { inner, .. }
1558        | GraphPattern::OrderBy { inner, .. }
1559        | GraphPattern::Project { inner, .. }
1560        | GraphPattern::Distinct { inner }
1561        | GraphPattern::Reduced { inner }
1562        | GraphPattern::Slice { inner, .. }
1563        | GraphPattern::Group { inner, .. } => has_quad_access(inner),
1564        _ => true,
1565    }
1566}
1567
1568/// Variables an expression reads (outside EXISTS patterns), each once.
1569fn expression_variables(e: &Expression, out: &mut Vec<Variable>) {
1570    match e {
1571        Expression::Variable(v) | Expression::Bound(v) if !out.contains(v) => out.push(v.clone()),
1572        Expression::Or(a, b)
1573        | Expression::And(a, b)
1574        | Expression::Equal(a, b)
1575        | Expression::SameTerm(a, b)
1576        | Expression::Greater(a, b)
1577        | Expression::GreaterOrEqual(a, b)
1578        | Expression::Less(a, b)
1579        | Expression::LessOrEqual(a, b)
1580        | Expression::Add(a, b)
1581        | Expression::Subtract(a, b)
1582        | Expression::Multiply(a, b)
1583        | Expression::Divide(a, b) => {
1584            expression_variables(a, out);
1585            expression_variables(b, out);
1586        }
1587        Expression::UnaryPlus(a) | Expression::UnaryMinus(a) | Expression::Not(a) => {
1588            expression_variables(a, out)
1589        }
1590        Expression::In(a, l) => {
1591            expression_variables(a, out);
1592            for x in l {
1593                expression_variables(x, out);
1594            }
1595        }
1596        Expression::If(a, b, c) => {
1597            for x in [a, b, c] {
1598                expression_variables(x, out);
1599            }
1600        }
1601        Expression::Coalesce(l) | Expression::FunctionCall(_, l) => {
1602            for x in l {
1603                expression_variables(x, out);
1604            }
1605        }
1606        _ => {}
1607    }
1608}
1609
1610pub(crate) fn in_list(col: &str, ids: &[i64]) -> String {
1611    if ids.is_empty() {
1612        return "0".into();
1613    }
1614    format!(
1615        "{col} IN ({})",
1616        ids.iter()
1617            .map(ToString::to_string)
1618            .collect::<Vec<_>>()
1619            .join(",")
1620    )
1621}
1622
1623pub(crate) fn ground_to_term(t: &GroundTerm) -> Term {
1624    match t {
1625        GroundTerm::NamedNode(n) => n.clone().into(),
1626        GroundTerm::Literal(l) => l.clone().into(),
1627        GroundTerm::Triple(t) => oxrdf::Triple::new(
1628            t.subject.clone(),
1629            t.predicate.clone(),
1630            ground_to_term(&t.object),
1631        )
1632        .into(),
1633    }
1634}
1635
1636/// SQL ORDER BY keys implementing Oxigraph's ordering: unbound < blank nodes < IRIs <
1637/// literals < triple terms; numeric literals first by value, other literals by
1638/// (lexical form, datatype, language).
1639pub(crate) fn order_keys(v: &V) -> Vec<String> {
1640    use expr::{Stat, K_BNODE, K_IRI, K_TRIPLE};
1641    match v.stat {
1642        Stat::Numeric => vec![format!("({}) IS NOT NULL", v.kind), v.num.clone()],
1643        Stat::String => vec![format!("({}) IS NOT NULL", v.kind), v.lex.clone()],
1644        _ => vec![
1645            format!(
1646                "CASE WHEN ({k}) IS NULL THEN 0 WHEN ({k}) = {K_BNODE} THEN 1 WHEN ({k}) = {K_IRI} THEN 2 WHEN ({k}) = {K_TRIPLE} THEN 4 ELSE 3 END",
1647                k = v.kind
1648            ),
1649            format!("({}) IS NULL", v.num),
1650            v.num.clone(),
1651            v.lex.clone(),
1652            v.dt.clone(),
1653            v.lang.clone(),
1654        ]
1655        .into_iter()
1656        .chain(triple_order_keys(v))
1657        .collect(),
1658    }
1659}
1660
1661/// Ordering key of triple terms: the sort key computed at write time.
1662fn triple_order_keys(v: &V) -> Vec<String> {
1663    use expr::K_TRIPLE;
1664    let (Some(id), true) = (&v.id, v.decodable) else {
1665        return Vec::new();
1666    };
1667    vec![format!(
1668        "(CASE WHEN ({}) = {K_TRIPLE} THEN (SELECT sk FROM triple_terms WHERE id = {id}) END)",
1669        v.kind
1670    )]
1671}
1672
1673/// Whether `e` mentions a variable for which `f` holds (`EXISTS` counts as mentioning).
1674fn expr_mentions(e: &Expression, f: &mut impl FnMut(&Variable) -> bool) -> bool {
1675    match e {
1676        Expression::NamedNode(_) | Expression::Literal(_) => false,
1677        Expression::Variable(v) | Expression::Bound(v) => f(v),
1678        Expression::Or(a, b)
1679        | Expression::And(a, b)
1680        | Expression::Equal(a, b)
1681        | Expression::SameTerm(a, b)
1682        | Expression::Greater(a, b)
1683        | Expression::GreaterOrEqual(a, b)
1684        | Expression::Less(a, b)
1685        | Expression::LessOrEqual(a, b)
1686        | Expression::Add(a, b)
1687        | Expression::Subtract(a, b)
1688        | Expression::Multiply(a, b)
1689        | Expression::Divide(a, b) => expr_mentions(a, f) || expr_mentions(b, f),
1690        Expression::UnaryPlus(a) | Expression::UnaryMinus(a) | Expression::Not(a) => {
1691            expr_mentions(a, f)
1692        }
1693        Expression::In(a, l) => expr_mentions(a, f) || l.iter().any(|x| expr_mentions(x, f)),
1694        Expression::If(a, b, c) => {
1695            expr_mentions(a, f) || expr_mentions(b, f) || expr_mentions(c, f)
1696        }
1697        Expression::Coalesce(l) | Expression::FunctionCall(_, l) => {
1698            l.iter().any(|x| expr_mentions(x, f))
1699        }
1700        Expression::Exists(_) => true,
1701    }
1702}