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