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