Skip to main content

oxilite_core/compiler/
expr.rs

1//! SPARQL expressions → SQL.
2//!
3//! Every SPARQL value is represented by a [`V`]: a bundle of SQL fragments giving its kind,
4//! lexical form, datatype, language, numeric value, timestamp and boolean value. For a stored
5//! term these fragments are derived from its id (inline values decode arithmetically, hashed
6//! ones through correlated `terms` lookups that SQLite evaluates as early as possible). SQL
7//! `NULL` plays the role of the SPARQL error / unbound value; SQL three-valued logic then
8//! matches SPARQL's error semantics for `&&`, `||` and `!`.
9//!
10// @lat: [[architecture#SPARQL to SQL compiler#Expressions]]
11
12use super::{Binding, Block, Compiler};
13use crate::encoding::{
14    encode_literal, numeric_rank, numeric_type, Tag, INT_OFFSET, PAYLOAD_BITS, PAYLOAD_MASK,
15};
16use crate::error::{Error, Result};
17use crate::sql::{sql_f64, sql_str};
18use oxrdf::vocab::{rdf, xsd};
19use oxrdf::{Literal, NamedNode, Term, Variable};
20use spargebra::algebra::{Expression, Function};
21use std::collections::BTreeMap;
22
23/// What is statically known about a value.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub(crate) enum Stat {
26    Any,
27    Numeric,
28    String,
29    LangString,
30    Bool,
31    DateTime,
32    Iri,
33}
34
35/// A SPARQL value as SQL fragments.
36#[derive(Debug, Clone)]
37pub(crate) struct V {
38    /// Term id, when it is defined whenever the value is.
39    pub id: Option<String>,
40    pub kind: String,
41    pub lex: String,
42    pub dt: String,
43    pub lang: String,
44    pub num: String,
45    pub nt: String,
46    pub ts: String,
47    pub boolv: String,
48    pub stat: Stat,
49    /// `lex` is only an approximation of the canonical form (computed numbers).
50    pub computed_num: bool,
51    /// `id` decodes without the query's constants (a stored term or an inline value).
52    pub decodable: bool,
53    /// Auxiliary value: the value key of a triple term, or `"avg:sum/count"` of an integer
54    /// AVG for exact decoding.
55    pub aux: String,
56    /// Datatype and timezone presence of date/time values (`dt|flag`), when cheaply known.
57    pub tz: String,
58}
59
60impl V {
61    /// Total SQL text of the fields (how much inlining this value costs).
62    pub(crate) fn size(&self) -> usize {
63        self.id.as_ref().map_or(0, String::len)
64            + self.kind.len()
65            + self.lex.len()
66            + self.dt.len()
67            + self.lang.len()
68            + self.num.len()
69            + self.nt.len()
70            + self.ts.len()
71            + self.boolv.len()
72            + self.aux.len()
73            + self.tz.len()
74    }
75}
76
77pub(crate) const K_IRI: i64 = Tag::Iri as i64;
78pub(crate) const K_BNODE: i64 = Tag::BlankNode as i64;
79pub(crate) const K_STRING: i64 = Tag::String as i64;
80pub(crate) const K_LANG: i64 = Tag::LangString as i64;
81pub(crate) const K_TYPED: i64 = Tag::Typed as i64;
82pub(crate) const K_INT: i64 = Tag::Integer as i64;
83pub(crate) const K_BOOL: i64 = Tag::Boolean as i64;
84pub(crate) const K_TRIPLE: i64 = Tag::Triple as i64;
85pub(crate) const K_DIRLANG: i64 = Tag::DirLangString as i64;
86
87/// `id` of the inline integer `x` is `INT_BASE + x`.
88pub(crate) const INT_BASE: i64 = Tag::Integer.base() + INT_OFFSET;
89pub(crate) const BOOL_BASE: i64 = Tag::Boolean.base();
90
91fn xsd_str(n: oxrdf::NamedNodeRef<'_>) -> String {
92    sql_str(n.as_str())
93}
94
95impl V {
96    pub(crate) fn null() -> Self {
97        Self {
98            id: Some("NULL".into()),
99            kind: "NULL".into(),
100            lex: "NULL".into(),
101            dt: "NULL".into(),
102            lang: "NULL".into(),
103            num: "NULL".into(),
104            nt: "NULL".into(),
105            ts: "NULL".into(),
106            boolv: "NULL".into(),
107            stat: Stat::Any,
108            computed_num: false,
109            decodable: true,
110            aux: "NULL".into(),
111            tz: "NULL".into(),
112        }
113    }
114
115    /// Value of a stored term given the SQL expression of its id.
116    pub(crate) fn from_id(x: &str) -> Self {
117        let k = format!("(({x}) >> {PAYLOAD_BITS})");
118        let payload = format!("(({x}) & {PAYLOAD_MASK})");
119        Self {
120            id: Some(x.into()),
121            lex: format!(
122                "CASE {k} WHEN {K_INT} THEN CAST({payload} - {INT_OFFSET} AS TEXT) WHEN {K_BOOL} THEN CASE {payload} WHEN 1 THEN 'true' ELSE 'false' END ELSE (SELECT lex FROM terms WHERE id = {x}) END"
123            ),
124            dt: format!(
125                "CASE {k} WHEN {K_STRING} THEN {} WHEN {K_LANG} THEN {} WHEN {K_DIRLANG} THEN {} WHEN {K_INT} THEN {} WHEN {K_BOOL} THEN {} WHEN {K_TYPED} THEN (SELECT dt FROM terms WHERE id = {x}) END",
126                xsd_str(xsd::STRING),
127                xsd_str(rdf::LANG_STRING),
128                sql_str("http://www.w3.org/1999/02/22-rdf-syntax-ns#dirLangString"),
129                xsd_str(xsd::INTEGER),
130                xsd_str(xsd::BOOLEAN),
131            ),
132            lang: format!(
133                "CASE {k} WHEN {K_LANG} THEN (SELECT lang FROM terms WHERE id = {x}) WHEN {K_DIRLANG} THEN (SELECT lang || '--' || CASE dir WHEN 2 THEN 'rtl' ELSE 'ltr' END FROM terms WHERE id = {x}) END"
134            ),
135            num: format!(
136                "CASE {k} WHEN {K_INT} THEN {payload} - {INT_OFFSET} WHEN {K_TYPED} THEN (SELECT num FROM terms WHERE id = {x} AND nt IS NOT NULL) END"
137            ),
138            nt: format!(
139                "CASE {k} WHEN {K_INT} THEN 1 WHEN {K_TYPED} THEN (SELECT nt FROM terms WHERE id = {x}) END"
140            ),
141            ts: format!("CASE WHEN {k} = {K_TYPED} THEN (SELECT ts FROM terms WHERE id = {x}) END"),
142            boolv: format!(
143                "CASE {k} WHEN {K_BOOL} THEN {payload} WHEN {K_TYPED} THEN (SELECT num FROM terms WHERE id = {x} AND dt = {}) END",
144                xsd_str(xsd::BOOLEAN)
145            ),
146            aux: format!("CASE WHEN {k} = {K_TRIPLE} THEN (SELECT vk FROM triple_terms WHERE id = {x}) END"),
147            tz: format!("CASE WHEN {k} = {K_TYPED} THEN (SELECT dt || '|' || dir FROM terms WHERE id = {x} AND ts IS NOT NULL) END"),
148            kind: k,
149            stat: Stat::Any,
150            computed_num: false,
151            decodable: true,
152        }
153    }
154
155    /// Like [`Self::from_id`], with the stored term's row joined once as `t` (`LEFT JOIN terms t
156    /// ON t.id = x`): every field reads that row instead of running its own lookup, which
157    /// matters when expressions (casts, arithmetic, aggregates) read many fields per row.
158    pub(crate) fn from_joined(x: &str, t: &str) -> Self {
159        let k = format!("(({x}) >> {PAYLOAD_BITS})");
160        let payload = format!("(({x}) & {PAYLOAD_MASK})");
161        let mut v = Self::from_id(x);
162        v.lex = format!(
163            "CASE {k} WHEN {K_INT} THEN CAST({payload} - {INT_OFFSET} AS TEXT) WHEN {K_BOOL} THEN CASE {payload} WHEN 1 THEN 'true' ELSE 'false' END ELSE {t}.lex END"
164        );
165        v.dt = format!(
166            "CASE {k} WHEN {K_STRING} THEN {} WHEN {K_LANG} THEN {} WHEN {K_DIRLANG} THEN {} WHEN {K_INT} THEN {} WHEN {K_BOOL} THEN {} WHEN {K_TYPED} THEN {t}.dt END",
167            xsd_str(xsd::STRING),
168            xsd_str(rdf::LANG_STRING),
169            sql_str("http://www.w3.org/1999/02/22-rdf-syntax-ns#dirLangString"),
170            xsd_str(xsd::INTEGER),
171            xsd_str(xsd::BOOLEAN),
172        );
173        v.lang = format!(
174            "CASE {k} WHEN {K_LANG} THEN {t}.lang WHEN {K_DIRLANG} THEN {t}.lang || '--' || CASE {t}.dir WHEN 2 THEN 'rtl' ELSE 'ltr' END END"
175        );
176        v.num = format!(
177            "CASE {k} WHEN {K_INT} THEN {payload} - {INT_OFFSET} WHEN {K_TYPED} THEN CASE WHEN {t}.nt IS NOT NULL THEN {t}.num END END"
178        );
179        v.nt = format!("CASE {k} WHEN {K_INT} THEN 1 WHEN {K_TYPED} THEN {t}.nt END");
180        v.ts = format!("CASE WHEN {k} = {K_TYPED} THEN {t}.ts END");
181        v.boolv = format!(
182            "CASE {k} WHEN {K_BOOL} THEN {payload} WHEN {K_TYPED} THEN CASE WHEN {t}.dt = {} THEN {t}.num END END",
183            xsd_str(xsd::BOOLEAN)
184        );
185        v.tz = format!(
186            "CASE WHEN {k} = {K_TYPED} AND {t}.ts IS NOT NULL THEN {t}.dt || '|' || {t}.dir END"
187        );
188        v
189    }
190
191    /// Value of a constant term.
192    pub(crate) fn from_term(term: &Term, id: i64) -> Result<Self> {
193        let mut v = Self::null();
194        v.id = Some(id.to_string());
195        v.decodable = false;
196        match term {
197            Term::NamedNode(n) => {
198                v.kind = K_IRI.to_string();
199                v.lex = sql_str(n.as_str());
200                v.stat = Stat::Iri;
201            }
202            Term::BlankNode(b) => {
203                v.kind = K_BNODE.to_string();
204                v.lex = sql_str(b.as_str());
205            }
206            Term::Literal(l) => {
207                let (lid, row) = encode_literal(l.as_ref());
208                debug_assert_eq!(lid, id);
209                let tag = crate::encoding::tag_of(id).unwrap_or(Tag::Typed);
210                v.kind = (tag as i64).to_string();
211                v.lex = sql_str(l.value());
212                v.dt = sql_str(l.datatype().as_str());
213                v.lang = match (l.language(), l.direction()) {
214                    (Some(lang), Some(oxrdf::BaseDirection::Ltr)) => {
215                        sql_str(&format!("{lang}--ltr"))
216                    }
217                    (Some(lang), Some(oxrdf::BaseDirection::Rtl)) => {
218                        sql_str(&format!("{lang}--rtl"))
219                    }
220                    (Some(lang), None) => sql_str(lang),
221                    (None, _) => "NULL".into(),
222                };
223                match tag {
224                    Tag::Integer => {
225                        v.num = l.value().to_string();
226                        v.nt = "1".into();
227                        v.stat = Stat::Numeric;
228                    }
229                    Tag::Boolean => {
230                        v.boolv = if l.value() == "true" { "1" } else { "0" }.into();
231                        v.stat = Stat::Bool;
232                    }
233                    Tag::String => v.stat = Stat::String,
234                    Tag::LangString | Tag::DirLangString => v.stat = Stat::LangString,
235                    _ => {
236                        if let Some(row) = row {
237                            if let (Some(num), Some(nt)) = (row.num, row.nt) {
238                                v.num = sql_f64(num);
239                                v.nt = nt.to_string();
240                                v.stat = Stat::Numeric;
241                            } else if l.datatype() == xsd::BOOLEAN {
242                                v.boolv = row
243                                    .num
244                                    .map_or_else(|| "NULL".into(), |b| (b as i64).to_string());
245                                v.stat = Stat::Bool;
246                            }
247                            if let Some(ts) = row.ts {
248                                v.ts = sql_f64(ts);
249                                v.tz = sql_str(&format!(
250                                    "{}|{}",
251                                    l.datatype().as_str(),
252                                    row.dir.unwrap_or(0)
253                                ));
254                                v.stat = Stat::DateTime;
255                            }
256                        }
257                    }
258                }
259            }
260            Term::Triple(_) => {
261                v.kind = K_TRIPLE.to_string();
262                v.aux = sql_str(&crate::encoding::value_key(term.as_ref()));
263            }
264        }
265        Ok(v)
266    }
267
268    /// A computed simple (or language-tagged, following `like`) string.
269    fn string(lex: String, like: Option<&V>) -> Self {
270        let mut v = Self::null();
271        v.id = None;
272        match like {
273            Some(l) if l.stat != Stat::String => {
274                v.kind = format!(
275                    "CASE WHEN ({lex}) IS NOT NULL THEN CASE WHEN {} IN ({K_LANG}, {K_DIRLANG}) THEN {K_LANG} ELSE {K_STRING} END END",
276                    l.kind
277                );
278                v.lang = format!(
279                    "CASE WHEN {} IN ({K_LANG}, {K_DIRLANG}) THEN {} END",
280                    l.kind, l.lang
281                );
282                v.dt = format!(
283                    "CASE WHEN {} IN ({K_LANG}, {K_DIRLANG}) THEN {} ELSE {} END",
284                    l.kind,
285                    xsd_str(rdf::LANG_STRING),
286                    xsd_str(xsd::STRING)
287                );
288                v.stat = Stat::Any;
289            }
290            _ => {
291                v.kind = format!("CASE WHEN ({lex}) IS NOT NULL THEN {K_STRING} END");
292                v.dt = xsd_str(xsd::STRING);
293                v.stat = Stat::String;
294            }
295        }
296        v.lex = lex;
297        v
298    }
299
300    /// A computed numeric value.
301    pub(crate) fn numeric(num: String, nt: String) -> Self {
302        let mut v = Self::null();
303        v.id = None;
304        v.kind = format!(
305            "CASE WHEN ({num}) IS NULL THEN NULL WHEN ({nt}) = 1 THEN {K_INT} WHEN ({nt}) IS NOT NULL THEN {K_TYPED} END"
306        );
307        v.dt = format!(
308            "CASE ({nt}) WHEN 1 THEN {} WHEN 2 THEN {} WHEN 3 THEN {} WHEN 4 THEN {} END",
309            xsd_str(xsd::INTEGER),
310            xsd_str(xsd::DECIMAL),
311            xsd_str(xsd::FLOAT),
312            xsd_str(xsd::DOUBLE)
313        );
314        v.lex = format!(
315            "CASE WHEN ({nt}) = 1 THEN CAST(CAST({num} AS INTEGER) AS TEXT) ELSE CAST({num} AS TEXT) END"
316        );
317        v.num = num;
318        v.nt = nt;
319        v.stat = Stat::Numeric;
320        v.computed_num = true;
321        v
322    }
323
324    /// A computed integer (always `xsd:integer`).
325    pub(crate) fn integer(num: String) -> Self {
326        let mut v = Self::numeric(num.clone(), "1".into());
327        v.id = Some(format!("({INT_BASE} + CAST({num} AS INTEGER))"));
328        v.kind = format!("CASE WHEN ({num}) IS NOT NULL THEN {K_INT} END");
329        v.dt = xsd_str(xsd::INTEGER);
330        v.lex = format!("CAST({num} AS TEXT)");
331        v.computed_num = false;
332        v.decodable = true;
333        v
334    }
335
336    /// A computed boolean.
337    pub(crate) fn boolean(b: &str) -> Self {
338        let mut v = Self::null();
339        v.id = Some(format!("({BOOL_BASE} + CAST({b} AS INTEGER))"));
340        v.kind = format!("CASE WHEN ({b}) IS NOT NULL THEN {K_BOOL} END");
341        v.lex = format!("CASE ({b}) WHEN 1 THEN 'true' WHEN 0 THEN 'false' END");
342        v.dt = xsd_str(xsd::BOOLEAN);
343        v.boolv = format!("({b})");
344        v.stat = Stat::Bool;
345        v.decodable = true;
346        v
347    }
348
349    /// A computed IRI.
350    fn iri(lex: String) -> Self {
351        let mut v = Self::null();
352        v.id = None;
353        v.kind = format!("CASE WHEN ({lex}) IS NOT NULL THEN {K_IRI} END");
354        v.lex = lex;
355        v.stat = Stat::Iri;
356        v
357    }
358
359    /// A computed typed literal with a statically known datatype.
360    fn typed(lex: String, dt: &str) -> Self {
361        let mut v = Self::null();
362        v.id = None;
363        v.kind = format!("CASE WHEN ({lex}) IS NOT NULL THEN {K_TYPED} END");
364        v.dt = sql_str(dt);
365        v.lex = lex;
366        v
367    }
368
369    fn is_string_like(&self) -> String {
370        match self.stat {
371            Stat::String | Stat::LangString => "1".into(),
372            _ => format!("{} IN ({K_STRING}, {K_LANG}, {K_DIRLANG})", self.kind),
373        }
374    }
375
376    /// Effective boolean value.
377    pub(crate) fn ebv(&self) -> String {
378        match self.stat {
379            Stat::Bool => format!("({})", self.boolv),
380            Stat::Numeric => format!("(({}) <> 0)", self.num),
381            Stat::String => format!("(({}) <> '')", self.lex),
382            Stat::Iri | Stat::LangString | Stat::DateTime => "NULL".into(),
383            Stat::Any => format!(
384                "(CASE WHEN ({b}) IS NOT NULL THEN ({b}) WHEN ({n}) IS NOT NULL THEN (({n}) <> 0) WHEN {k} = {K_STRING} THEN (({l}) <> '') END)",
385                b = self.boolv,
386                n = self.num,
387                k = self.kind,
388                l = self.lex
389            ),
390        }
391    }
392}
393
394/// A compiled expression: a SQL boolean (0/1/NULL) or a value.
395pub(crate) enum E {
396    B(String),
397    T(V),
398}
399
400impl E {
401    pub(crate) fn bool_sql(self) -> String {
402        match self {
403            Self::B(b) => b,
404            Self::T(v) => v.ebv(),
405        }
406    }
407
408    pub(crate) fn term(self) -> V {
409        match self {
410            Self::B(b) => V::boolean(&b),
411            Self::T(v) => v,
412        }
413    }
414}
415
416#[derive(Clone, Copy, PartialEq, Eq)]
417enum Cmp {
418    Eq,
419    Lt,
420    Le,
421    Gt,
422    Ge,
423}
424
425impl Cmp {
426    fn op(self) -> &'static str {
427        match self {
428            Self::Eq => "=",
429            Self::Lt => "<",
430            Self::Le => "<=",
431            Self::Gt => ">",
432            Self::Ge => ">=",
433        }
434    }
435}
436
437/// Datatype plus timezone presence: date/time values only compare within the same key.
438fn tsk(v: &V) -> String {
439    if v.tz != "NULL" {
440        return format!("({})", v.tz);
441    }
442    format!(
443        "(({dt}) || CASE WHEN substr({l}, -1) = 'Z' OR (substr({l}, -6, 1) IN ('+', '-') AND substr({l}, -3, 1) = ':') THEN 'Z' ELSE '' END)",
444        dt = v.dt,
445        l = v.lex
446    )
447}
448
449fn may(s: Stat, want: Stat) -> bool {
450    s == Stat::Any || s == want
451}
452
453pub(crate) fn same_term(a: &V, b: &V) -> String {
454    match (&a.id, &b.id) {
455        (Some(x), Some(y)) => format!("(({x}) = ({y}))"),
456        _ => format!(
457            "(({ak}) = ({bk}) AND ({al}) = ({bl}) AND ({ad}) IS ({bd}) AND ({ag}) IS ({bg}))",
458            ak = a.kind,
459            bk = b.kind,
460            al = a.lex,
461            bl = b.lex,
462            ad = a.dt,
463            bd = b.dt,
464            ag = a.lang,
465            bg = b.lang
466        ),
467    }
468}
469
470fn compare(a: &V, b: &V, cmp: Cmp) -> String {
471    compare_inner(a, b, cmp, true)
472}
473
474fn compare_inner(a: &V, b: &V, cmp: Cmp, triples: bool) -> String {
475    let op = cmp.op();
476    let mut branches = Vec::new();
477    if cmp == Cmp::Eq {
478        if let (Some(x), Some(y)) = (&a.id, &b.id) {
479            branches.push(format!("WHEN ({x}) = ({y}) THEN 1"));
480        }
481    }
482    if may(a.stat, Stat::Numeric) && may(b.stat, Stat::Numeric) {
483        if a.stat == Stat::Numeric && b.stat == Stat::Numeric {
484            return format!("(({}) {op} ({}))", a.num, b.num);
485        }
486        branches.push(format!(
487            "WHEN ({an}) IS NOT NULL AND ({bn}) IS NOT NULL THEN ({an}) {op} ({bn})",
488            an = a.num,
489            bn = b.num
490        ));
491    }
492    if may(a.stat, Stat::String) && may(b.stat, Stat::String) {
493        if a.stat == Stat::String && b.stat == Stat::String {
494            return format!("(({}) {op} ({}))", a.lex, b.lex);
495        }
496        branches.push(format!(
497            "WHEN ({ak}) = {K_STRING} AND ({bk}) = {K_STRING} THEN ({al}) {op} ({bl})",
498            ak = a.kind,
499            bk = b.kind,
500            al = a.lex,
501            bl = b.lex
502        ));
503    }
504    if may(a.stat, Stat::DateTime) && may(b.stat, Stat::DateTime) {
505        // Same datatype: compare timestamps. Timezone vs no timezone follows XSD 1.1: the
506        // result is determinate only outside the ±14h window, otherwise an error.
507        let (at, bt) = (&a.ts, &b.ts);
508        let mixed = match cmp {
509            Cmp::Lt | Cmp::Le => format!(
510                "CASE WHEN ({at}) + 50400 < ({bt}) THEN 1 WHEN ({at}) - 50400 > ({bt}) THEN 0 END"
511            ),
512            Cmp::Gt | Cmp::Ge => format!(
513                "CASE WHEN ({at}) - 50400 > ({bt}) THEN 1 WHEN ({at}) + 50400 < ({bt}) THEN 0 END"
514            ),
515            Cmp::Eq => format!("CASE WHEN abs(({at}) - ({bt})) > 50400 THEN 0 END"),
516        };
517        branches.push(format!(
518            "WHEN ({at}) IS NOT NULL AND ({bt}) IS NOT NULL AND ({ad}) = ({bd}) THEN CASE WHEN {ak} = {bk} THEN ({at}) {op} ({bt}) ELSE {mixed} END",
519            ad = a.dt,
520            bd = b.dt,
521            ak = tsk(a),
522            bk = tsk(b)
523        ));
524    }
525    if may(a.stat, Stat::Bool) && may(b.stat, Stat::Bool) {
526        branches.push(format!(
527            "WHEN ({ab}) IS NOT NULL AND ({bb}) IS NOT NULL THEN ({ab}) {op} ({bb})",
528            ab = a.boolv,
529            bb = b.boolv
530        ));
531    }
532    if cmp != Cmp::Eq && may(a.stat, Stat::LangString) && may(b.stat, Stat::LangString) {
533        // Oxigraph extension: language-tagged strings with the same tag compare by value.
534        branches.push(format!(
535            "WHEN ({ak}) = {K_LANG} AND ({bk}) = {K_LANG} AND ({ag}) = ({bg}) THEN ({al}) {op} ({bl})",
536            ak = a.kind,
537            bk = b.kind,
538            ag = a.lang,
539            bg = b.lang,
540            al = a.lex,
541            bl = b.lex
542        ));
543    }
544    if cmp == Cmp::Eq {
545        if a.id.is_none() || b.id.is_none() {
546            branches.push(format!("WHEN {} THEN 1", same_term(a, b)));
547        }
548        if may(a.stat, Stat::LangString) && may(b.stat, Stat::LangString) {
549            branches.push(format!(
550                "WHEN ({ak}) = {K_LANG} AND ({bk}) = {K_LANG} THEN (({al}) = ({bl}) AND ({ag}) = ({bg}))",
551                ak = a.kind,
552                bk = b.kind,
553                al = a.lex,
554                bl = b.lex,
555                ag = a.lang,
556                bg = b.lang
557            ));
558        }
559        if triples && a.aux != "NULL" && b.aux != "NULL" {
560            // Triple terms: value keys computed at write time (components by value).
561            branches.push(format!(
562                "WHEN ({}) = {K_TRIPLE} AND ({}) = {K_TRIPLE} THEN ({}) = ({})",
563                a.kind, b.kind, a.aux, b.aux
564            ));
565        }
566        // IRIs, blank nodes and triple terms are only equal to themselves.
567        branches.push(format!(
568            "WHEN ({}) IN ({K_IRI}, {K_BNODE}, {K_TRIPLE}) OR ({}) IN ({K_IRI}, {K_BNODE}, {K_TRIPLE}) THEN 0",
569            a.kind, b.kind
570        ));
571    }
572    if branches.is_empty() {
573        return "NULL".into();
574    }
575    format!("(CASE {} END)", branches.join(" "))
576}
577
578fn arith(a: &V, b: &V, op: char) -> V {
579    let nt = if op == '/' {
580        format!("max(2, max({}, {}))", a.nt, b.nt)
581    } else {
582        format!("max({}, {})", a.nt, b.nt)
583    };
584    let num = match op {
585        '/' => format!(
586            "(CASE WHEN ({bn}) = 0 AND max({ant}, {bnt}) < 3 THEN NULL ELSE CAST({an} AS REAL) / ({bn}) END)",
587            an = a.num,
588            bn = b.num,
589            ant = a.nt,
590            bnt = b.nt
591        ),
592        _ => format!("(({}) {op} ({}))", a.num, b.num),
593    };
594    V::numeric(num, nt)
595}
596
597/// Is a regex made only of literal characters (plus optional `^` / `$` anchors)?
598fn simple_regex(pattern: &str) -> Option<(bool, String, bool)> {
599    let mut chars = pattern.chars().peekable();
600    let starts = chars.peek() == Some(&'^');
601    if starts {
602        chars.next();
603    }
604    let mut lit = String::new();
605    let mut ends = false;
606    while let Some(c) = chars.next() {
607        match c {
608            '\\' => match chars.next() {
609                Some(e) if ".*+?()[]{}|\\^$/-".contains(e) => lit.push(e),
610                _ => return None,
611            },
612            '$' if chars.peek().is_none() => ends = true,
613            '.' | '*' | '+' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '^' | '$' => {
614                return None
615            }
616            c => lit.push(c),
617        }
618    }
619    Some((starts, lit, ends))
620}
621
622impl Compiler<'_> {
623    /// Resolves a variable against a block's bindings and the outer (EXISTS) scopes.
624    pub(crate) fn var_value(&mut self, v: &Variable, cols: &BTreeMap<usize, Binding>) -> V {
625        let idx = self.var(v);
626        if let Some(b) = cols.get(&idx) {
627            let mut value = b.col.value();
628            if value.stat == Stat::Any {
629                if let Some(t) = self.options.var_types.get(v.as_str()) {
630                    value.stat = match t {
631                        super::ValueType::Numeric => Stat::Numeric,
632                        super::ValueType::String => Stat::String,
633                        super::ValueType::Boolean => Stat::Bool,
634                    };
635                }
636            }
637            return value;
638        }
639        for scope in self.outer.iter().rev() {
640            if let Some(b) = scope.get(&idx) {
641                return b.col.value();
642            }
643        }
644        V::null()
645    }
646
647    fn constant(&mut self, t: Term) -> Result<V> {
648        let id = self.constant_id(&t)?;
649        V::from_term(&t, id)
650    }
651
652    pub(crate) fn expr_term(
653        &mut self,
654        e: &Expression,
655        cols: &BTreeMap<usize, Binding>,
656    ) -> Result<V> {
657        Ok(self.expr(e, cols)?.term())
658    }
659
660    pub(crate) fn expr_bool(
661        &mut self,
662        e: &Expression,
663        cols: &BTreeMap<usize, Binding>,
664    ) -> Result<String> {
665        Ok(self.expr(e, cols)?.bool_sql())
666    }
667
668    pub(crate) fn expr(&mut self, e: &Expression, cols: &BTreeMap<usize, Binding>) -> Result<E> {
669        Ok(match e {
670            Expression::NamedNode(n) => E::T(self.constant(n.clone().into())?),
671            Expression::Literal(l) => E::T(self.constant(l.clone().into())?),
672            Expression::Variable(v) => E::T(self.var_value(v, cols)),
673            Expression::Or(a, b) => E::B(format!(
674                "({} OR {})",
675                self.expr_bool(a, cols)?,
676                self.expr_bool(b, cols)?
677            )),
678            Expression::And(a, b) => E::B(format!(
679                "({} AND {})",
680                self.expr_bool(a, cols)?,
681                self.expr_bool(b, cols)?
682            )),
683            Expression::Not(a) => E::B(format!("(NOT {})", self.expr_bool(a, cols)?)),
684            Expression::Equal(a, b) => {
685                let (a, b) = (self.expr_term(a, cols)?, self.expr_term(b, cols)?);
686                E::B(compare(&a, &b, Cmp::Eq))
687            }
688            Expression::SameTerm(a, b) => {
689                let (a, b) = (self.expr_term(a, cols)?, self.expr_term(b, cols)?);
690                E::B(same_term(&a, &b))
691            }
692            Expression::Greater(a, b) => self.cmp(a, b, Cmp::Gt, cols)?,
693            Expression::GreaterOrEqual(a, b) => self.cmp(a, b, Cmp::Ge, cols)?,
694            Expression::Less(a, b) => self.cmp(a, b, Cmp::Lt, cols)?,
695            Expression::LessOrEqual(a, b) => self.cmp(a, b, Cmp::Le, cols)?,
696            Expression::In(a, list) => {
697                let a = self.expr_term(a, cols)?;
698                if list.is_empty() {
699                    // Evaluating the left operand may still fail (e.g. unbound).
700                    return Ok(E::B(format!(
701                        "(CASE WHEN ({}) IS NOT NULL THEN 0 END)",
702                        a.kind
703                    )));
704                }
705                let mut parts = Vec::new();
706                for b in list {
707                    let b = self.expr_term(b, cols)?;
708                    parts.push(compare(&a, &b, Cmp::Eq));
709                }
710                E::B(format!("({})", parts.join(" OR ")))
711            }
712            Expression::Add(a, b) => self.arith(a, b, '+', cols)?,
713            Expression::Subtract(a, b) => self.arith(a, b, '-', cols)?,
714            Expression::Multiply(a, b) => self.arith(a, b, '*', cols)?,
715            Expression::Divide(a, b) => self.arith(a, b, '/', cols)?,
716            Expression::UnaryPlus(a) => {
717                let a = self.expr_term(a, cols)?;
718                E::T(V::numeric(a.num.clone(), a.nt.clone()))
719            }
720            Expression::UnaryMinus(a) => {
721                let a = self.expr_term(a, cols)?;
722                E::T(V::numeric(format!("(-({}))", a.num), a.nt.clone()))
723            }
724            Expression::Bound(v) => {
725                let v = self.var_value(v, cols);
726                E::B(format!("(({}) IS NOT NULL)", v.kind))
727            }
728            Expression::If(c, a, b) => {
729                let c = self.expr_bool(c, cols)?;
730                let a = self.expr_term(a, cols)?;
731                let b = self.expr_term(b, cols)?;
732                E::T(Self::choose(&c, &a, &b))
733            }
734            Expression::Coalesce(list) => {
735                let mut vals = Vec::new();
736                for e in list {
737                    vals.push(self.expr_term(e, cols)?);
738                }
739                let Some(mut acc) = vals.pop() else {
740                    return Ok(E::T(V::null()));
741                };
742                while let Some(v) = vals.pop() {
743                    let c = format!("(({}) IS NOT NULL)", v.kind);
744                    acc = Self::choose(&c, &v, &acc);
745                }
746                E::T(acc)
747            }
748            Expression::Exists(p) => E::B(self.exists(p, cols)?),
749            Expression::FunctionCall(f, args) => self.function(f, args, cols)?,
750        })
751    }
752
753    pub(crate) fn choose(c: &str, a: &V, b: &V) -> V {
754        let pick = |x: &str, y: &str| {
755            format!("(CASE WHEN ({c}) IS NULL THEN NULL WHEN ({c}) THEN {x} ELSE {y} END)")
756        };
757        V {
758            id: match (&a.id, &b.id) {
759                (Some(x), Some(y)) => Some(pick(x, y)),
760                _ => None,
761            },
762            kind: pick(&a.kind, &b.kind),
763            lex: pick(&a.lex, &b.lex),
764            dt: pick(&a.dt, &b.dt),
765            lang: pick(&a.lang, &b.lang),
766            num: pick(&a.num, &b.num),
767            nt: pick(&a.nt, &b.nt),
768            ts: pick(&a.ts, &b.ts),
769            boolv: pick(&a.boolv, &b.boolv),
770            stat: if a.stat == b.stat { a.stat } else { Stat::Any },
771            computed_num: a.computed_num || b.computed_num,
772            decodable: a.decodable && b.decodable,
773            aux: pick(&a.aux, &b.aux),
774            tz: pick(&a.tz, &b.tz),
775        }
776    }
777
778    fn cmp(
779        &mut self,
780        a: &Expression,
781        b: &Expression,
782        c: Cmp,
783        cols: &BTreeMap<usize, Binding>,
784    ) -> Result<E> {
785        let (a, b) = (self.expr_term(a, cols)?, self.expr_term(b, cols)?);
786        Ok(E::B(compare(&a, &b, c)))
787    }
788
789    fn arith(
790        &mut self,
791        a: &Expression,
792        b: &Expression,
793        op: char,
794        cols: &BTreeMap<usize, Binding>,
795    ) -> Result<E> {
796        let (a, b) = (self.expr_term(a, cols)?, self.expr_term(b, cols)?);
797        Ok(E::T(arith(&a, &b, op)))
798    }
799
800    fn exists(
801        &mut self,
802        p: &spargebra::algebra::GraphPattern,
803        cols: &BTreeMap<usize, Binding>,
804    ) -> Result<String> {
805        self.outer.push(cols.clone());
806        let inner = self.pattern(p);
807        self.outer.pop();
808        let mut inner = inner?;
809        if !inner.is_plain() || inner.from.is_empty() {
810            inner = self.seal(inner);
811        }
812        // Variables bound on both sides must agree (substitution semantics).
813        let mut conds = inner.wheres.clone();
814        for (idx, b) in &inner.cols {
815            if let Some(outer) = cols.get(idx) {
816                if b.correlated {
817                    continue;
818                }
819                conds.push(Self::unify(b, outer).0);
820            }
821        }
822        Ok(format!(
823            "EXISTS (SELECT 1 FROM {}{})",
824            Block::render_from(&inner.from),
825            if conds.is_empty() {
826                String::new()
827            } else {
828                format!(" WHERE {}", conds.join(" AND "))
829            }
830        ))
831    }
832
833    fn args(&mut self, args: &[Expression], cols: &BTreeMap<usize, Binding>) -> Result<Vec<V>> {
834        args.iter().map(|a| self.expr_term(a, cols)).collect()
835    }
836
837    fn function(
838        &mut self,
839        f: &Function,
840        args: &[Expression],
841        cols: &BTreeMap<usize, Binding>,
842    ) -> Result<E> {
843        let udf = self.caps.udf;
844        Ok(match f {
845            Function::Str => {
846                let a = self.expr_term(&args[0], cols)?;
847                let lex = format!(
848                    "CASE WHEN ({}) IN ({K_IRI}, {K_STRING}, {K_LANG}, {K_TYPED}, {K_INT}, {K_BOOL}, {K_DIRLANG}) THEN {} END",
849                    a.kind, a.lex
850                );
851                if a.computed_num {
852                    // String form of a computed number is only approximately canonical.
853                    return Err(Error::unsupported("STR() of a computed number"));
854                }
855                E::T(V::string(lex, None))
856            }
857            Function::Lang => {
858                let a = self.expr_term(&args[0], cols)?;
859                E::T(V::string(
860                    format!(
861                        "CASE WHEN ({k}) = {K_LANG} THEN ({l}) WHEN ({k}) = {K_DIRLANG} THEN substr({l}, 1, instr({l}, '--') - 1) WHEN ({k}) IN ({K_STRING}, {K_TYPED}, {K_INT}, {K_BOOL}) THEN '' END",
862                        k = a.kind,
863                        l = a.lang
864                    ),
865                    None,
866                ))
867            }
868            Function::Datatype => {
869                let a = self.expr_term(&args[0], cols)?;
870                // The datatype of an error (e.g. a failed cast) is an error.
871                E::T(V::iri(format!(
872                    "CASE WHEN ({}) IS NOT NULL THEN {} END",
873                    a.kind, a.dt
874                )))
875            }
876            Function::IsIri => {
877                let a = self.expr_term(&args[0], cols)?;
878                E::B(format!("(({}) = {K_IRI})", a.kind))
879            }
880            Function::IsBlank => {
881                let a = self.expr_term(&args[0], cols)?;
882                E::B(format!("(({}) = {K_BNODE})", a.kind))
883            }
884            Function::IsLiteral => {
885                let a = self.expr_term(&args[0], cols)?;
886                E::B(format!(
887                    "(({}) IN ({K_STRING}, {K_LANG}, {K_TYPED}, {K_INT}, {K_BOOL}, {K_DIRLANG}))",
888                    a.kind
889                ))
890            }
891            Function::IsNumeric => {
892                let a = self.expr_term(&args[0], cols)?;
893                E::B(format!(
894                    "(CASE WHEN ({}) IS NULL THEN NULL ELSE ({}) IS NOT NULL END)",
895                    a.kind, a.nt
896                ))
897            }
898            Function::IsTriple => {
899                let a = self.expr_term(&args[0], cols)?;
900                E::B(format!("(({}) = {K_TRIPLE})", a.kind))
901            }
902            Function::StrLen => {
903                let a = self.expr_term(&args[0], cols)?;
904                E::T(V::integer(format!(
905                    "CASE WHEN {} THEN length({}) END",
906                    a.is_string_like(),
907                    a.lex
908                )))
909            }
910            Function::UCase | Function::LCase => {
911                let a = self.expr_term(&args[0], cols)?;
912                let func = if matches!(f, Function::UCase) {
913                    "upper"
914                } else {
915                    "lower"
916                };
917                E::T(V::string(
918                    format!(
919                        "CASE WHEN {} THEN {func}({}) END",
920                        a.is_string_like(),
921                        a.lex
922                    ),
923                    Some(&a),
924                ))
925            }
926            Function::SubStr => {
927                let a = self.args(args, cols)?;
928                let start = format!("CAST(round({}) AS INTEGER)", a[1].num);
929                let lex = if let Some(len) = a.get(2) {
930                    format!(
931                        "CASE WHEN {} THEN substr({}, {start}, CAST(round({}) AS INTEGER)) END",
932                        a[0].is_string_like(),
933                        a[0].lex,
934                        len.num
935                    )
936                } else {
937                    format!(
938                        "CASE WHEN {} THEN substr({}, {start}) END",
939                        a[0].is_string_like(),
940                        a[0].lex
941                    )
942                };
943                E::T(V::string(lex, Some(&a[0])))
944            }
945            Function::Concat => {
946                let a = self.args(args, cols)?;
947                if a.is_empty() {
948                    E::T(V::string("''".into(), None))
949                } else {
950                    let parts: Vec<String> = a
951                        .iter()
952                        .map(|v| format!("(CASE WHEN {} THEN {} END)", v.is_string_like(), v.lex))
953                        .collect();
954                    let lex = format!("({})", parts.join(" || "));
955                    // The language tag (and direction) is kept only if all arguments share it.
956                    let same_lang = a
957                        .iter()
958                        .map(|v| {
959                            format!(
960                                "({k}) IN ({K_LANG}, {K_DIRLANG}) AND ({l}) = ({l0})",
961                                k = v.kind,
962                                l = v.lang,
963                                l0 = a[0].lang
964                            )
965                        })
966                        .collect::<Vec<_>>()
967                        .join(" AND ");
968                    let mut v = V::string(lex.clone(), None);
969                    v.kind = format!(
970                        "CASE WHEN ({lex}) IS NULL THEN NULL WHEN {same_lang} THEN ({k0}) ELSE {K_STRING} END",
971                        k0 = a[0].kind
972                    );
973                    v.lang = format!("CASE WHEN {same_lang} THEN ({}) END", a[0].lang);
974                    v.dt = format!(
975                        "CASE WHEN {same_lang} THEN ({}) ELSE {} END",
976                        a[0].dt,
977                        xsd_str(xsd::STRING)
978                    );
979                    v.stat = Stat::Any;
980                    E::T(v)
981                }
982            }
983            Function::Contains | Function::StrStarts | Function::StrEnds => {
984                let a = self.args(args, cols)?;
985                let (x, y) = (&a[0].lex, &a[1].lex);
986                let test = match f {
987                    Function::Contains => format!("instr({x}, {y}) > 0"),
988                    Function::StrStarts => format!("substr({x}, 1, length({y})) = {y}"),
989                    _ => format!("(length({y}) = 0 OR substr({x}, -length({y})) = {y})"),
990                };
991                E::B(format!(
992                    "(CASE WHEN {} AND {} THEN {test} END)",
993                    a[0].is_string_like(),
994                    a[1].is_string_like()
995                ))
996            }
997            Function::StrBefore | Function::StrAfter => {
998                let a = self.args(args, cols)?;
999                let (x, y) = (&a[0].lex, &a[1].lex);
1000                // Arguments are compatible if arg2 is a simple string or has arg1's language.
1001                let compatible = format!(
1002                    "({k0}) IN ({K_STRING}, {K_LANG}, {K_DIRLANG}) AND (({k1}) = {K_STRING} OR (({k1}) IN ({K_LANG}, {K_DIRLANG}) AND ({l1}) = ({l0})))",
1003                    k0 = a[0].kind,
1004                    k1 = a[1].kind,
1005                    l0 = a[0].lang,
1006                    l1 = a[1].lang
1007                );
1008                let found = format!("(length({y}) = 0 OR instr({x}, {y}) > 0)");
1009                let lex = if matches!(f, Function::StrBefore) {
1010                    format!("CASE WHEN length({y}) = 0 THEN '' WHEN instr({x}, {y}) > 0 THEN substr({x}, 1, instr({x}, {y}) - 1) ELSE '' END")
1011                } else {
1012                    format!("CASE WHEN length({y}) = 0 THEN {x} WHEN instr({x}, {y}) > 0 THEN substr({x}, instr({x}, {y}) + length({y})) ELSE '' END")
1013                };
1014                let lex = format!("CASE WHEN {compatible} THEN {lex} END");
1015                let mut v = V::string(lex.clone(), None);
1016                // A match keeps arg1's language tag; no match gives a simple empty string.
1017                v.kind = format!(
1018                    "CASE WHEN ({lex}) IS NULL THEN NULL WHEN {found} THEN ({k0}) ELSE {K_STRING} END",
1019                    k0 = a[0].kind
1020                );
1021                v.lang = format!("CASE WHEN {found} THEN ({}) END", a[0].lang);
1022                v.dt = format!(
1023                    "CASE WHEN {found} THEN ({}) ELSE {} END",
1024                    a[0].dt,
1025                    xsd_str(xsd::STRING)
1026                );
1027                v.stat = Stat::Any;
1028                E::T(v)
1029            }
1030            Function::LangMatches => {
1031                let a = self.args(args, cols)?;
1032                let (tag, range) = (&a[0].lex, &a[1].lex);
1033                E::B(format!(
1034                    "(CASE WHEN ({range}) = '*' THEN ({tag}) <> '' ELSE lower({tag}) = lower({range}) OR lower({tag}) LIKE lower({range}) || '-%' END)"
1035                ))
1036            }
1037            Function::Regex => self.regex(args, cols)?,
1038            Function::Replace if udf => {
1039                let a = self.args(args, cols)?;
1040                let flags = a.get(3).map_or_else(|| "''".into(), |f| f.lex.clone());
1041                E::T(V::string(
1042                    format!(
1043                        "CASE WHEN {} THEN oxilite_replace({}, {}, {}, {flags}) END",
1044                        a[0].is_string_like(),
1045                        a[0].lex,
1046                        a[1].lex,
1047                        a[2].lex
1048                    ),
1049                    Some(&a[0]),
1050                ))
1051            }
1052            Function::EncodeForUri if udf => {
1053                let a = self.expr_term(&args[0], cols)?;
1054                E::T(V::string(
1055                    format!("oxilite_encode_for_uri({})", a.lex),
1056                    None,
1057                ))
1058            }
1059            Function::Md5
1060            | Function::Sha1
1061            | Function::Sha256
1062            | Function::Sha384
1063            | Function::Sha512
1064                if udf =>
1065            {
1066                let a = self.expr_term(&args[0], cols)?;
1067                let algo = match f {
1068                    Function::Md5 => "md5",
1069                    Function::Sha1 => "sha1",
1070                    Function::Sha256 => "sha256",
1071                    Function::Sha384 => "sha384",
1072                    _ => "sha512",
1073                };
1074                E::T(V::string(
1075                    format!(
1076                        "CASE WHEN ({}) = {K_STRING} THEN oxilite_hash('{algo}', {}) END",
1077                        a.kind, a.lex
1078                    ),
1079                    None,
1080                ))
1081            }
1082            Function::Abs | Function::Ceil | Function::Floor | Function::Round => {
1083                let a = self.expr_term(&args[0], cols)?;
1084                let n = &a.num;
1085                let floor = format!("(CAST({n} AS INTEGER) - (({n}) < CAST({n} AS INTEGER)))");
1086                let num = match f {
1087                    Function::Abs => format!("abs({n})"),
1088                    Function::Floor => floor,
1089                    Function::Ceil => format!(
1090                        "(-(CAST(-({n}) AS INTEGER) - ((-({n})) < CAST(-({n}) AS INTEGER))))"
1091                    ),
1092                    _ => {
1093                        let m = format!("(({n}) + 0.5)");
1094                        format!("(CAST({m} AS INTEGER) - (({m}) < CAST({m} AS INTEGER)))")
1095                    }
1096                };
1097                E::T(V::numeric(num, a.nt.clone()))
1098            }
1099            Function::Year
1100            | Function::Month
1101            | Function::Day
1102            | Function::Hours
1103            | Function::Minutes => {
1104                let a = self.expr_term(&args[0], cols)?;
1105                let (start, len) = match f {
1106                    Function::Year => (1, 4),
1107                    Function::Month => (6, 2),
1108                    Function::Day => (9, 2),
1109                    Function::Hours => (12, 2),
1110                    _ => (15, 2),
1111                };
1112                let neg = if matches!(f, Function::Year) {
1113                    // Negative years: skip the sign.
1114                    format!(
1115                        "CASE WHEN substr({l}, 1, 1) = '-' THEN -CAST(substr({l}, 2, 4) AS INTEGER) ELSE CAST(substr({l}, 1, 4) AS INTEGER) END",
1116                        l = a.lex
1117                    )
1118                } else {
1119                    format!("CAST(substr({}, {start}, {len}) AS INTEGER)", a.lex)
1120                };
1121                E::T(V::integer(format!(
1122                    "CASE WHEN ({}) IS NOT NULL THEN {neg} END",
1123                    a.ts
1124                )))
1125            }
1126            Function::Seconds => {
1127                let a = self.expr_term(&args[0], cols)?;
1128                // Seconds start at offset 18 of the lexical form; CAST keeps the numeric prefix.
1129                E::T(V::numeric(
1130                    format!(
1131                        "CASE WHEN ({}) IS NOT NULL AND ({}) = {} THEN CAST(substr({l}, 18) AS REAL) END",
1132                        a.ts,
1133                        a.dt,
1134                        xsd_str(xsd::DATE_TIME),
1135                        l = a.lex
1136                    ),
1137                    "2".into(),
1138                ))
1139            }
1140            Function::Tz => {
1141                let a = self.expr_term(&args[0], cols)?;
1142                let l = &a.lex;
1143                E::T(V::string(
1144                    format!(
1145                        "CASE WHEN ({}) IS NULL THEN NULL WHEN substr({l}, -1) = 'Z' THEN 'Z' WHEN substr({l}, -6, 1) IN ('+', '-') AND substr({l}, -3, 1) = ':' THEN substr({l}, -6) ELSE '' END",
1146                        a.ts
1147                    ),
1148                    None,
1149                ))
1150            }
1151            Function::Now => {
1152                let now = self.now.clone();
1153                E::T(self.constant(now.into())?)
1154            }
1155            Function::Rand => {
1156                let mut v = V::numeric(
1157                    "((random() / 18446744073709551616.0) + 0.5)".into(),
1158                    numeric_type::DOUBLE.to_string(),
1159                );
1160                v.computed_num = true;
1161                E::T(v)
1162            }
1163            Function::StrUuid | Function::Uuid => {
1164                let uuid = "lower(hex(randomblob(4)) || '-' || hex(randomblob(2)) || '-4' || substr(hex(randomblob(2)), 2) || '-' || substr('89ab', 1 + (abs(random()) % 4), 1) || substr(hex(randomblob(2)), 2) || '-' || hex(randomblob(6)))";
1165                if matches!(f, Function::Uuid) {
1166                    E::T(V::iri(format!("('urn:uuid:' || {uuid})")))
1167                } else {
1168                    E::T(V::string(uuid.into(), None))
1169                }
1170            }
1171            Function::Iri => match &args[0] {
1172                Expression::NamedNode(n) => E::T(self.constant(n.clone().into())?),
1173                Expression::Literal(l) if l.datatype() == xsd::STRING => {
1174                    let iri = match &self.base_iri {
1175                        Some(base) => oxiri::Iri::parse(base.as_str())
1176                            .and_then(|b| b.resolve(l.value()))
1177                            .map(oxiri::Iri::into_inner),
1178                        None => {
1179                            oxiri::Iri::parse(l.value().to_string()).map(oxiri::Iri::into_inner)
1180                        }
1181                    };
1182                    match iri {
1183                        Ok(i) => E::T(self.constant(NamedNode::new_unchecked(i).into())?),
1184                        Err(_) => E::T(V::null()),
1185                    }
1186                }
1187                _ if self.base_iri.is_some() => {
1188                    return Err(Error::unsupported("IRI() of a non-constant with a BASE"))
1189                }
1190                _ => {
1191                    let a = self.expr_term(&args[0], cols)?;
1192                    E::T(V::iri(format!(
1193                            "CASE WHEN ({k}) = {K_IRI} OR (({k}) = {K_STRING} AND instr({l}, ':') > 1) THEN {l} END",
1194                            k = a.kind,
1195                            l = a.lex
1196                        )))
1197                }
1198            },
1199            Function::StrDt => {
1200                let a = self.args(args, cols)?;
1201                let Expression::NamedNode(dt) = &args[1] else {
1202                    return Err(Error::unsupported("STRDT with a non-constant datatype"));
1203                };
1204                let lex = format!(
1205                    "CASE WHEN ({}) = {K_STRING} THEN {} END",
1206                    a[0].kind, a[0].lex
1207                );
1208                if dt.as_ref() == xsd::STRING {
1209                    E::T(V::string(lex, None))
1210                } else {
1211                    let mut v = V::typed(lex.clone(), dt.as_str());
1212                    if let Some(rank) = numeric_rank(dt.as_str()) {
1213                        v.num = format!("CASE WHEN trim({lex}) GLOB '*[0-9]*' THEN CAST(trim({lex}) AS REAL) END");
1214                        v.nt = rank.to_string();
1215                        v.stat = Stat::Numeric;
1216                    }
1217                    E::T(v)
1218                }
1219            }
1220            Function::StrLang => {
1221                let a = self.args(args, cols)?;
1222                let mut v = V::string(
1223                    format!(
1224                        "CASE WHEN ({}) = {K_STRING} THEN {} END",
1225                        a[0].kind, a[0].lex
1226                    ),
1227                    None,
1228                );
1229                v.kind = format!("CASE WHEN ({}) = {K_STRING} THEN {K_LANG} END", a[0].kind);
1230                v.lang = format!("lower({})", a[1].lex);
1231                v.dt = xsd_str(rdf::LANG_STRING);
1232                v.stat = Stat::LangString;
1233                E::T(v)
1234            }
1235            Function::Custom(name) if name.as_str() == crate::text::TEXT_MATCH => {
1236                let [value, Expression::Literal(query)] = args else {
1237                    return Err(Error::unsupported("textMatch with a non-constant query"));
1238                };
1239                if !self.stats.text_index {
1240                    return Err(Error::unsupported(
1241                        "textMatch without the text index (StoreOptions::text_index)",
1242                    ));
1243                }
1244                let v = self.expr_term(value, cols)?;
1245                let Some(id) = v.id.clone() else {
1246                    return Err(Error::unsupported("textMatch on a computed value"));
1247                };
1248                E::B(format!(
1249                    "(({id}) IN (SELECT rowid FROM terms_fts WHERE terms_fts MATCH {}))",
1250                    crate::sql::sql_str(query.value())
1251                ))
1252            }
1253            Function::Custom(name) => {
1254                let a = self.args(args, cols)?;
1255                if a.len() == 1
1256                    && name
1257                        .as_str()
1258                        .starts_with("http://www.w3.org/2001/XMLSchema#")
1259                {
1260                    E::T(cast(&a[0], name)?)
1261                } else {
1262                    return Err(Error::unsupported(format!("custom function {name}")));
1263                }
1264            }
1265            Function::HasLang => {
1266                let a = self.expr_term(&args[0], cols)?;
1267                E::B(format!(
1268                    "(CASE WHEN ({k}) IS NOT NULL THEN ({k}) IN ({K_LANG}, {K_DIRLANG}) END)",
1269                    k = a.kind
1270                ))
1271            }
1272            Function::HasLangDir => {
1273                let a = self.expr_term(&args[0], cols)?;
1274                E::B(format!(
1275                    "(CASE WHEN ({k}) IS NOT NULL THEN ({k}) = {K_DIRLANG} END)",
1276                    k = a.kind
1277                ))
1278            }
1279            Function::LangDir => {
1280                let a = self.expr_term(&args[0], cols)?;
1281                E::T(V::string(
1282                    format!(
1283                        "CASE WHEN ({k}) = {K_DIRLANG} THEN substr({l}, instr({l}, '--') + 2) WHEN ({k}) IN ({K_STRING}, {K_LANG}, {K_TYPED}, {K_INT}, {K_BOOL}) THEN '' END",
1284                        k = a.kind,
1285                        l = a.lang
1286                    ),
1287                    None,
1288                ))
1289            }
1290            Function::StrLangDir => {
1291                let a = self.args(args, cols)?;
1292                let mut v = V::string(
1293                    format!(
1294                        "CASE WHEN ({}) = {K_STRING} AND ({}) = {K_STRING} AND ({d}) IN ('ltr', 'rtl') THEN {} END",
1295                        a[0].kind,
1296                        a[1].kind,
1297                        a[0].lex,
1298                        d = a[2].lex
1299                    ),
1300                    None,
1301                );
1302                v.kind = format!("CASE WHEN ({}) IS NOT NULL THEN {K_DIRLANG} END", v.lex);
1303                v.lang = format!("(lower({}) || '--' || ({}))", a[1].lex, a[2].lex);
1304                v.dt = sql_str("http://www.w3.org/1999/02/22-rdf-syntax-ns#dirLangString");
1305                v.stat = Stat::LangString;
1306                E::T(v)
1307            }
1308            Function::Subject | Function::Predicate | Function::Object => {
1309                let a = self.expr_term(&args[0], cols)?;
1310                let (Some(id), true) = (&a.id, a.decodable) else {
1311                    return Err(Error::unsupported(
1312                        "SUBJECT/PREDICATE/OBJECT of a computed triple",
1313                    ));
1314                };
1315                let c = match f {
1316                    Function::Subject => "s",
1317                    Function::Predicate => "p",
1318                    _ => "o",
1319                };
1320                E::T(V::from_id(&format!(
1321                    "(CASE WHEN ({}) = {K_TRIPLE} THEN (SELECT {c} FROM triple_terms WHERE id = {id}) END)",
1322                    a.kind
1323                )))
1324            }
1325            Function::Timezone => {
1326                let a = self.expr_term(&args[0], cols)?;
1327                let l = &a.lex;
1328                // dayTimeDuration of the timezone suffix: Z → PT0S, +05:30 → PT5H30M, -08:00 → -PT8H.
1329                let h = format!("CAST(substr({l}, -5, 2) AS INTEGER)");
1330                let m = format!("CAST(substr({l}, -2, 2) AS INTEGER)");
1331                let lex = format!(
1332                    "CASE WHEN ({ts}) IS NULL THEN NULL WHEN substr({l}, -1) = 'Z' THEN 'PT0S' WHEN substr({l}, -6, 1) IN ('+', '-') AND substr({l}, -3, 1) = ':' THEN (CASE WHEN substr({l}, -6, 1) = '-' AND ({h} > 0 OR {m} > 0) THEN '-' ELSE '' END) || 'PT' || CASE WHEN {h} = 0 AND {m} = 0 THEN '0S' ELSE (CASE WHEN {h} > 0 THEN {h} || 'H' ELSE '' END) || (CASE WHEN {m} > 0 THEN {m} || 'M' ELSE '' END) END END",
1333                    ts = a.ts
1334                );
1335                E::T(V::typed(
1336                    lex,
1337                    "http://www.w3.org/2001/XMLSchema#dayTimeDuration",
1338                ))
1339            }
1340            other => return Err(Error::unsupported(format!("SPARQL function {other}"))),
1341        })
1342    }
1343
1344    fn regex(&mut self, args: &[Expression], cols: &BTreeMap<usize, Binding>) -> Result<E> {
1345        let text = self.expr_term(&args[0], cols)?;
1346        let flags = match args.get(2) {
1347            None => Some(String::new()),
1348            Some(Expression::Literal(l)) => Some(l.value().to_string()),
1349            Some(_) => None,
1350        };
1351        let guard = text.is_string_like();
1352        if let (Expression::Literal(p), Some(flags)) = (&args[1], &flags) {
1353            let ci = flags == "i";
1354            if flags.is_empty() || ci {
1355                if let Some((starts, lit, ends)) = simple_regex(p.value()) {
1356                    let (t, l) = if ci {
1357                        (format!("lower({})", text.lex), sql_str(&lit.to_lowercase()))
1358                    } else {
1359                        (text.lex.clone(), sql_str(&lit))
1360                    };
1361                    let test = match (starts, ends) {
1362                        (true, true) => format!("{t} = {l}"),
1363                        (true, false) => format!("substr({t}, 1, length({l})) = {l}"),
1364                        (false, true) => {
1365                            format!("(length({l}) = 0 OR substr({t}, -length({l})) = {l})")
1366                        }
1367                        (false, false) => format!("instr({t}, {l}) > 0"),
1368                    };
1369                    return Ok(E::B(format!("(CASE WHEN {guard} THEN {test} END)")));
1370                }
1371            }
1372        }
1373        if self.caps.udf {
1374            let pattern = self.expr_term(&args[1], cols)?;
1375            let flags = match args.get(2) {
1376                Some(f) => self.expr_term(f, cols)?.lex,
1377                None => "''".into(),
1378            };
1379            return Ok(E::B(format!(
1380                "(CASE WHEN {guard} THEN oxilite_regex({}, {}, {flags}) END)",
1381                text.lex, pattern.lex
1382            )));
1383        }
1384        Err(Error::unsupported(
1385            "REGEX with a non-literal pattern (no regex UDF on this backend)",
1386        ))
1387    }
1388}
1389
1390/// XSD whitespace collapse (spaces, tabs, line breaks) around a lexical form.
1391fn collapse(lex: &str) -> String {
1392    format!("trim({lex}, ' ' || char(9) || char(10) || char(13))")
1393}
1394
1395/// Evaluates `body` with the (trimmed) lexical form bound once as `lx`: a correlated scalar
1396/// subquery over a one-row FROM subquery is SQL's only per-row `let`. Validity checks read the
1397/// lexical form a dozen times, so inlining it would multiply its SQL (and its evaluation).
1398fn let_lex(t: &str, body: impl Fn(&str) -> String) -> String {
1399    format!("(SELECT {} FROM (SELECT {t} AS lx))", body("lx"))
1400}
1401
1402/// `t` is a valid `xsd:integer` lexical form.
1403fn int_lex(t: &str) -> String {
1404    let u = format!("(CASE WHEN substr({t}, 1, 1) IN ('+', '-') THEN substr({t}, 2) ELSE {t} END)");
1405    format!("({u} <> '' AND {u} NOT GLOB '*[^0-9]*')")
1406}
1407
1408/// `t` is a valid `xsd:decimal` lexical form.
1409fn dec_lex(t: &str) -> String {
1410    let u = format!("(CASE WHEN substr({t}, 1, 1) IN ('+', '-') THEN substr({t}, 2) ELSE {t} END)");
1411    format!(
1412        "({u} <> '' AND {u} <> '.' AND {u} NOT GLOB '*[^0-9.]*' AND length({u}) - length(replace({u}, '.', '')) <= 1)"
1413    )
1414}
1415
1416/// `t` is a valid `xsd:double` / `xsd:float` lexical form.
1417fn dbl_lex(t: &str) -> String {
1418    let e = format!("instr(lower({t}), 'e')");
1419    let mantissa = format!("substr({t}, 1, {e} - 1)");
1420    let exponent = format!("substr({t}, {e} + 1)");
1421    format!(
1422        "({t} IN ('INF', '+INF', '-INF') OR {} OR ({e} > 0 AND {} AND {}))",
1423        dec_lex(t),
1424        dec_lex(&mantissa),
1425        int_lex(&exponent)
1426    )
1427}
1428
1429/// XSD casts (`xsd:integer(?x)` …) following the SPARQL 1.1 cast table; results are
1430/// canonicalized by the decoder like Oxigraph's.
1431fn cast(a: &V, dt: &NamedNode) -> Result<V> {
1432    let t = collapse(&a.lex);
1433    let is_str = format!("({}) = {K_STRING}", a.kind);
1434    let is_num = format!("({}) IS NOT NULL", a.nt);
1435    let is_bool = format!("({}) IS NOT NULL", a.boolv);
1436    if a.computed_num {
1437        return Err(Error::unsupported("cast of a computed number"));
1438    }
1439    let dts = dt.as_str();
1440    Ok(match dts {
1441        "http://www.w3.org/2001/XMLSchema#string" => {
1442            // Canonical lexical forms, like Oxigraph (which stores values, not lexical forms).
1443            let n = &a.num;
1444            V::string(
1445                format!(
1446                    "CASE WHEN ({k}) IN ({K_IRI}, {K_STRING}) THEN {l} WHEN {is_bool} THEN CASE ({b}) WHEN 1 THEN 'true' ELSE 'false' END WHEN ({n}) IS NOT NULL AND ({n}) = CAST({n} AS INTEGER) AND abs({n}) < 1e15 THEN CAST(CAST({n} AS INTEGER) AS TEXT) WHEN ({n}) IS NOT NULL THEN CAST({n} AS TEXT) WHEN ({k}) = {K_TYPED} THEN {l} END",
1447                    k = a.kind,
1448                    l = a.lex,
1449                    b = a.boolv
1450                ),
1451                None,
1452            )
1453        }
1454        "http://www.w3.org/2001/XMLSchema#boolean" => V::boolean(&format!(
1455            "CASE WHEN {is_bool} THEN ({b}) WHEN {is_num} THEN (({n}) IS NOT NULL AND ({n}) <> 0) WHEN {is_str} THEN CASE WHEN {t} IN ('true', '1') THEN 1 WHEN {t} IN ('false', '0') THEN 0 END END",
1456            b = a.boolv,
1457            n = a.num
1458        )),
1459        "http://www.w3.org/2001/XMLSchema#integer" => V::integer(format!(
1460            "CASE WHEN {is_bool} THEN ({b}) WHEN {is_num} THEN CASE WHEN abs({n}) < 9.2e18 THEN CAST({n} AS INTEGER) END WHEN {is_str} THEN {parse} END",
1461            b = a.boolv,
1462            n = a.num,
1463            parse = let_lex(&t, |x| format!("CASE WHEN {} THEN CAST({x} AS INTEGER) END", int_lex(x)))
1464        )),
1465        "http://www.w3.org/2001/XMLSchema#decimal" => V::numeric(
1466            format!(
1467                "CASE WHEN {is_bool} THEN ({b}) * 1.0 WHEN {is_num} THEN CASE WHEN abs({n}) < 9e999 THEN CAST({n} AS REAL) END WHEN {is_str} THEN {parse} END",
1468                b = a.boolv,
1469                n = a.num,
1470                parse = let_lex(&t, |x| format!("CASE WHEN {} THEN CAST({x} AS REAL) END", dec_lex(x)))
1471            ),
1472            numeric_type::DECIMAL.to_string(),
1473        ),
1474        "http://www.w3.org/2001/XMLSchema#float" | "http://www.w3.org/2001/XMLSchema#double" => V::numeric(
1475            format!(
1476                "CASE WHEN {is_bool} THEN ({b}) * 1.0 WHEN {is_num} THEN CAST({n} AS REAL) WHEN {is_str} THEN {parse} END",
1477                b = a.boolv,
1478                n = a.num,
1479                parse = let_lex(&t, |x| format!(
1480                    "CASE WHEN {} THEN CASE WHEN {x} IN ('INF', '+INF') THEN 9e999 WHEN {x} = '-INF' THEN -9e999 ELSE CAST({x} AS REAL) END END",
1481                    dbl_lex(x)
1482                ))
1483            ),
1484            if dts.ends_with("float") { numeric_type::FLOAT } else { numeric_type::DOUBLE }.to_string(),
1485        ),
1486        "http://www.w3.org/2001/XMLSchema#dateTime" => {
1487            let valid = format!(
1488                "(({dt}) = {dts_sql} OR ({is_str} AND substr({t}, 1, 10) GLOB '[0-9][0-9][0-9][0-9]-[0-1][0-9]-[0-3][0-9]' AND substr({t}, 11, 9) GLOB 'T[0-2][0-9]:[0-5][0-9]:[0-6][0-9]' AND julianday({t}) IS NOT NULL))",
1489                dt = a.dt,
1490                dts_sql = sql_str(dts)
1491            );
1492            let mut v = V::typed(format!("CASE WHEN {valid} THEN {t} END"), dts);
1493            v.ts = format!("CASE WHEN {valid} THEN (julianday({t}) - 2440587.5) * 86400.0 END");
1494            v.stat = Stat::DateTime;
1495            v
1496        }
1497        _ => return Err(Error::unsupported(format!("XSD cast {dt}"))),
1498    })
1499}
1500
1501/// Canonical lexical form of a computed numeric result, used by the decoder.
1502pub(crate) fn format_number(num: f64, dt: &str) -> Literal {
1503    match numeric_rank(dt) {
1504        Some(numeric_type::INTEGER) => Literal::from(num as i64),
1505        Some(numeric_type::DECIMAL) => {
1506            // Decimals are computed as IEEE doubles: round to 15 significant digits so that
1507            // 1.1 + 10 is 11.1, not 11.100000000000001.
1508            let digits = if num == 0.0 {
1509                0
1510            } else {
1511                15 - (num.abs().log10().floor() as i32) - 1
1512            };
1513            let text = format!("{:.*}", digits.clamp(0, 30) as usize, num);
1514            let d = std::str::FromStr::from_str(&text)
1515                .or_else(|_| oxsdatatypes::Decimal::try_from(oxsdatatypes::Double::from(num)))
1516                .unwrap_or_default();
1517            Literal::from(d)
1518        }
1519        Some(numeric_type::FLOAT) => Literal::from(oxsdatatypes::Float::from(num as f32)),
1520        _ => Literal::from(oxsdatatypes::Double::from(num)),
1521    }
1522}
1523
1524#[cfg(test)]
1525mod tests {
1526    use super::simple_regex;
1527
1528    #[test]
1529    fn simple_regexes() {
1530        assert_eq!(simple_regex("^abc$"), Some((true, "abc".into(), true)));
1531        assert_eq!(simple_regex("a\\.b"), Some((false, "a.b".into(), false)));
1532        assert_eq!(simple_regex("a.b"), None);
1533        assert_eq!(simple_regex("(x|y)"), None);
1534    }
1535}