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