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