Skip to main content

graphitesql/exec/
func.rs

1//! Built-in scalar functions.
2//!
3//! Aggregate functions (`count`, `sum`, …) are handled by the executor, which
4//! folds over rows; this module covers the per-row scalar functions. The set is
5//! a useful core and grows toward SQLite's full library (`func.c`, `date.c`).
6
7use super::eval::{self, EvalCtx};
8use crate::error::{Error, Result};
9use crate::sql::ast::{Expr, Literal};
10use crate::value::Value;
11use alloc::string::String;
12use alloc::vec::Vec;
13
14/// SQLite's default `SQLITE_MAX_LENGTH`: the largest string/blob a single value
15/// may hold. `zeroblob`/`randomblob` error with "string or blob too big" past it
16/// rather than attempting a multi-gigabyte allocation.
17const MAX_BLOB_LEN: usize = 1_000_000_000;
18
19/// Names that *can* be aggregates (used for catalog/name checks).
20pub fn is_aggregate(name: &str) -> bool {
21    matches!(
22        name.to_ascii_lowercase().as_str(),
23        "count" | "sum" | "total" | "avg" | "min" | "max" | "group_concat" | "geopoly_group_bbox"
24    )
25}
26
27/// Whether a *specific call* is an aggregate. `min`/`max` are scalar with 2+
28/// arguments and aggregate with exactly one (or `*`), matching SQLite.
29pub fn is_aggregate_call(name: &str, nargs: usize, star: bool) -> bool {
30    match name.to_ascii_lowercase().as_str() {
31        "count" | "sum" | "total" | "avg" | "group_concat" | "string_agg" => true,
32        // The JSON group aggregates have no scalar counterpart, so they are
33        // aggregates at any argument count — a wrong count must reach the
34        // aggregate arity guard ("wrong number of arguments"), not fall through
35        // to scalar dispatch ("no such function").
36        "json_group_array" | "jsonb_group_array" | "json_group_object" | "jsonb_group_object" => {
37            true
38        }
39        // geopoly_group_bbox has no scalar counterpart, so it is an aggregate at
40        // any argument count (the arity guard reports a wrong count).
41        "geopoly_group_bbox" => true,
42        "min" | "max" => star || nargs == 1,
43        _ => false,
44    }
45}
46
47/// The searchable `(column name, text)` pairs an FTS5 `MATCH` operand refers to,
48/// or `None` if the operand is not a column or table-name reference (so `MATCH`
49/// is not a full-text query in this context). A reference to an indexed column
50/// yields just that column; an unqualified reference to the table's own name
51/// yields every column (SQLite's table-wide `MATCH`, where `col:token` filters
52/// pick out individual columns).
53#[cfg(feature = "fts5")]
54fn fts5_match_columns(
55    operand: &Expr,
56    ctx: &EvalCtx,
57) -> Option<(Vec<(String, String)>, crate::vtab::Fts5Tok)> {
58    let (table, column) = match operand {
59        Expr::Column { table, column, .. } => (table.as_deref(), column.as_str()),
60        Expr::Paren(e) => return fts5_match_columns(e, ctx),
61        _ => return None,
62    };
63    // The table's tokenizer config (Porter stemming + `remove_diacritics` level),
64    // so the query folds exactly like the indexed documents.
65    let tok = |t: &str| {
66        ctx.subqueries
67            .map_or_else(crate::vtab::Fts5Tok::default, |s| s.fts5_tok(t))
68    };
69    // A reference to a specific indexed column searches only that column. An
70    // `UNINDEXED` column matches nothing (it carries no full-text index).
71    if let Some(i) = ctx.columns.iter().position(|c| {
72        c.name.eq_ignore_ascii_case(column) && table.is_none_or(|t| c.table.eq_ignore_ascii_case(t))
73    }) {
74        let c = &ctx.columns[i];
75        let unindexed = ctx
76            .subqueries
77            .and_then(|s| s.fts5_indexed_columns(&c.table))
78            .is_some_and(|cols| !cols.iter().any(|n| n.eq_ignore_ascii_case(&c.name)));
79        if unindexed {
80            return Some((Vec::new(), crate::vtab::Fts5Tok::default()));
81        }
82        return Some((
83            alloc::vec![(c.name.clone(), eval::to_text(&ctx.row[i]))],
84            tok(&c.table),
85        ));
86    }
87    // An unqualified reference to the table itself searches across every indexed
88    // column (`UNINDEXED` columns are stored but excluded from the full-text index).
89    if table.is_none() {
90        let indexed = ctx.subqueries.and_then(|s| s.fts5_indexed_columns(column));
91        let cols: Vec<(String, String)> = ctx
92            .columns
93            .iter()
94            .enumerate()
95            .filter(|(_, c)| c.table.eq_ignore_ascii_case(column))
96            .filter(|(_, c)| {
97                indexed
98                    .as_ref()
99                    .is_none_or(|cols| cols.iter().any(|n| n.eq_ignore_ascii_case(&c.name)))
100            })
101            .map(|(i, c)| (c.name.clone(), eval::to_text(&ctx.row[i])))
102            .collect();
103        if !cols.is_empty() {
104            return Some((cols, tok(column)));
105        }
106    }
107    None
108}
109
110/// The fts5 table a `MATCH` operand refers to: the operand `t` in `t MATCH q` (a
111/// bare table reference) or the owning table of `t.col MATCH q`. Resolves the name
112/// through the current row's column set (so an alias maps to the real table name),
113/// falling back to the operand's own identifier. `None` when the operand is not a
114/// column/table reference.
115#[cfg(feature = "fts5")]
116fn fts5_match_operand_table(operand: &Expr, ctx: &EvalCtx) -> Option<alloc::string::String> {
117    let (table, column) = match operand {
118        Expr::Column { table, column, .. } => (table.as_deref(), column.as_str()),
119        Expr::Paren(e) => return fts5_match_operand_table(e, ctx),
120        _ => return None,
121    };
122    // `t.col` → the table owning that column; bare `t` → the table named `t` in the
123    // row's column set (its `table` field holds the resolved base-table name).
124    if let Some(t) = table {
125        return Some(alloc::string::String::from(t));
126    }
127    ctx.columns
128        .iter()
129        .find(|c| c.table.eq_ignore_ascii_case(column))
130        .map(|c| c.table.clone())
131        .or_else(|| Some(alloc::string::String::from(column)))
132}
133
134/// Whether a `highlight`/`snippet` first argument names a contentless fts5 table.
135#[cfg(feature = "fts5")]
136fn fts5_operand_is_contentless(operand: &Expr, ctx: &EvalCtx) -> bool {
137    fts5_match_operand_table(operand, ctx)
138        .zip(ctx.subqueries)
139        .is_some_and(|(t, s)| s.fts5_is_contentless_table(&t))
140}
141
142/// Evaluate a scalar function call.
143pub fn eval_scalar(name: &str, args: &[Expr], star: bool, ctx: &EvalCtx) -> Result<Value> {
144    let lname = name.to_ascii_lowercase();
145    if is_aggregate_call(&lname, args.len(), star) {
146        // Reaching the scalar evaluator means this aggregate sits in a position
147        // that forbids one — most commonly nested inside another aggregate's
148        // argument or FILTER (`sum(count(a))`), where the inner call is evaluated
149        // per row as a scalar. SQLite reports this as `misuse of aggregate
150        // function NAME()`, naming the inner call. (Row-filtering positions —
151        // WHERE, join ON, ORDER BY of a non-aggregate query, UPDATE/DELETE
152        // expressions — are intercepted earlier at prepare time so they error even
153        // over an empty table; see `reject_misused_aggregate`.)
154        return Err(Error::Error(alloc::format!(
155            "misuse of aggregate function {name}()"
156        )));
157    }
158    if star {
159        return Err(Error::Error(alloc::format!(
160            "{name}(*) is not a scalar call"
161        )));
162    }
163
164    // Connection-state functions: read counters off the subquery handler.
165    match lname.as_str() {
166        "last_insert_rowid" | "changes" | "total_changes" => {
167            arity(&lname, args, 0)?;
168            let n = ctx.subqueries.map_or(0, |s| match lname.as_str() {
169                "last_insert_rowid" => s.last_insert_rowid(),
170                "changes" => s.changes(),
171                _ => s.total_changes(),
172            });
173            return Ok(Value::Integer(n));
174        }
175        "random" => {
176            arity(&lname, args, 0)?;
177            return Ok(Value::Integer(
178                ctx.subqueries.map_or(0, |s| s.next_random()),
179            ));
180        }
181        "randomblob" => {
182            arity(&lname, args, 1)?;
183            // SQLite coerces the argument to an integer (NULL/non-numeric text
184            // become 0, reals truncate) and clamps N < 1 to a single byte — so
185            // randomblob(NULL) is a 1-byte blob, not NULL.
186            let n = eval::to_int_value(&eval::eval(&args[0], ctx)?);
187            let len = if n < 1 { 1 } else { n as usize };
188            if len > MAX_BLOB_LEN {
189                return Err(Error::Error("string or blob too big".into()));
190            }
191            let mut bytes = Vec::new();
192            if let Some(s) = ctx.subqueries {
193                while bytes.len() < len {
194                    bytes.extend_from_slice(&s.next_random().to_le_bytes());
195                }
196                bytes.truncate(len);
197            } else {
198                bytes.resize(len, 0);
199            }
200            return Ok(Value::Blob(bytes));
201        }
202        // FTS5 `MATCH`: `x MATCH 'query'` parsed to `match('query', x)`. When the
203        // operand `x` references an indexed column (or the table itself), run the
204        // full-text query against that document. When it is not a column/table
205        // reference (e.g. a literal), fall through so a user-registered `match`
206        // function — or the "no such function" error — applies, matching SQLite,
207        // where a bare `MATCH` outside a virtual-table context is an error.
208        #[cfg(feature = "fts5")]
209        "match" if args.len() == 2 => {
210            if let Some((cols, tok)) = fts5_match_columns(&args[1], ctx) {
211                let pattern = eval::eval(&args[0], ctx)?;
212                return Ok(match pattern {
213                    Value::Null => Value::Null,
214                    p => {
215                        let q = eval::to_text(&p);
216                        // A contentless table keeps no column text, so `MATCH` can't
217                        // be re-checked from the (NULL) row — consult the index by
218                        // rowid instead. `fts5_match_operand_table` names the table
219                        // the operand refers to; the connection resolves whether it
220                        // is contentless and, if so, evaluates against the index.
221                        if let (Some(table), Some(rowid)) =
222                            (fts5_match_operand_table(&args[1], ctx), ctx.rowid)
223                        {
224                            if let Some(m) = ctx
225                                .subqueries
226                                .and_then(|s| s.fts5_contentless_match(&table, &q, rowid))
227                            {
228                                return Ok(Value::Integer(m as i64));
229                            }
230                        }
231                        Value::Integer(crate::vtab::fts5_query_matches(&q, &cols, tok) as i64)
232                    }
233                });
234            }
235        }
236        // FTS5 `bm25(<table>[, w1, w2, …])`: the relevance score of the current row
237        // (optionally with per-column weights), computed by `run_core` for a `MATCH`
238        // query over an `fts5` table. Falls through when no such score is in scope
239        // (so `bm25()` elsewhere is the usual unknown name).
240        #[cfg(feature = "fts5")]
241        "bm25" if !args.is_empty() && ctx.rowid.is_some() => {
242            let weights: Vec<f64> = args[1..]
243                .iter()
244                .map(|a| Ok(eval::to_f64(&eval::eval(a, ctx)?)))
245                .collect::<Result<_>>()?;
246            if let Some(score) = ctx
247                .rowid
248                .and_then(|r| ctx.subqueries?.fts5_bm25(r, &weights))
249            {
250                return Ok(Value::Real(score));
251            }
252        }
253        // FTS5 `highlight(<table>, col, open, close)`: column `col`'s text with the
254        // matched tokens wrapped, in scope for a `MATCH` query over an `fts5` table.
255        #[cfg(feature = "fts5")]
256        "highlight" if args.len() == 4 => {
257            // A contentless table stores no text, so there is nothing to render:
258            // `highlight`/`snippet` return NULL (matching SQLite).
259            if fts5_operand_is_contentless(&args[0], ctx) {
260                return Ok(Value::Null);
261            }
262            let col = eval::to_int_value(&eval::eval(&args[1], ctx)?);
263            let open = eval::to_text(&eval::eval(&args[2], ctx)?);
264            let close = eval::to_text(&eval::eval(&args[3], ctx)?);
265            // The column text is the current row's value for that column.
266            if let Ok(col) = usize::try_from(col) {
267                let text = ctx.row.get(col).map(eval::to_text).unwrap_or_default();
268                if let Some(s) = ctx
269                    .subqueries
270                    .and_then(|s| s.fts5_highlight(col, &text, &open, &close))
271                {
272                    return Ok(Value::Text(s));
273                }
274            }
275        }
276        // FTS5 `snippet(<table>, col, open, close, ellipsis, n)`: a window of up
277        // to `n` tokens from column `col`, matched tokens wrapped, ellipsis at any
278        // trimmed end. In scope for a `MATCH` query over an `fts5` table.
279        #[cfg(feature = "fts5")]
280        "snippet" if args.len() == 6 => {
281            if fts5_operand_is_contentless(&args[0], ctx) {
282                return Ok(Value::Null);
283            }
284            let col = eval::to_int_value(&eval::eval(&args[1], ctx)?);
285            let open = eval::to_text(&eval::eval(&args[2], ctx)?);
286            let close = eval::to_text(&eval::eval(&args[3], ctx)?);
287            let ellipsis = eval::to_text(&eval::eval(&args[4], ctx)?);
288            let ntokens = eval::to_int_value(&eval::eval(&args[5], ctx)?);
289            // A negative `col` auto-selects the best column, so pass every column's
290            // text; the module bounds the slice by the table's declared columns.
291            if let Ok(ntokens) = usize::try_from(ntokens) {
292                let cols: Vec<alloc::string::String> = ctx.row.iter().map(eval::to_text).collect();
293                if let Some(s) = ctx
294                    .subqueries
295                    .and_then(|s| s.fts5_snippet(col, &cols, &open, &close, &ellipsis, ntokens))
296                {
297                    return Ok(Value::Text(s));
298                }
299            }
300        }
301        _ => {}
302    }
303
304    // Functions whose NULL-handling is special are done before arg evaluation.
305    match lname.as_str() {
306        "coalesce" => {
307            // SQLite requires at least two arguments.
308            if args.len() < 2 {
309                return Err(wrong_arg_count("coalesce"));
310            }
311            for a in args {
312                let v = eval::eval(a, ctx)?;
313                if !matches!(v, Value::Null) {
314                    return Ok(v);
315                }
316            }
317            return Ok(Value::Null);
318        }
319        "ifnull" => {
320            arity(&lname, args, 2)?;
321            let a = eval::eval(&args[0], ctx)?;
322            return if matches!(a, Value::Null) {
323                eval::eval(&args[1], ctx)
324            } else {
325                Ok(a)
326            };
327        }
328        // `json_quote(X)` returns a JSON-subtyped argument as-is (it is already
329        // JSON text), and only quotes a value that is *not* JSON. graphite has no
330        // value subtypes, so it approximates the subtype syntactically: an arg
331        // that is itself a JSON-producing call (`json`, `json_array`, …) carries
332        // the subtype. Anything else falls through to the quoting arm below.
333        "json_quote" if args.len() == 1 && produces_json(&args[0]) => {
334            let val = eval::eval(&args[0], ctx)?;
335            // A JSONB blob (e.g. `json_quote(jsonb('[1,2]'))`) renders as its JSON
336            // text; any other blob is rejected.
337            if let Value::Blob(b) = &val {
338                let j = super::json::Json::from_jsonb(b)
339                    .ok_or_else(|| Error::Error("JSON cannot hold BLOB values".into()))?;
340                return Ok(Value::Text(j.quote()));
341            }
342            return Ok(val);
343        }
344        _ => {}
345    }
346
347    let v: Vec<Value> = args
348        .iter()
349        .map(|a| eval::eval(a, ctx))
350        .collect::<Result<_>>()?;
351
352    Ok(match lname.as_str() {
353        "abs" => {
354            arity(&lname, args, 1)?;
355            match &v[0] {
356                Value::Null => Value::Null,
357                // `abs(-9223372036854775808)` has no i64 result — SQLite errors.
358                Value::Integer(i) => match i.checked_abs() {
359                    Some(a) => Value::Integer(a),
360                    None => return Err(Error::Error("integer overflow".into())),
361                },
362                // `+ 0.0` normalises a negative-zero result to `0.0`, matching
363                // SQLite (`abs(-0.0)` is `0.0`, not `-0.0`).
364                Value::Real(r) => Value::Real(crate::util::float::abs(*r) + 0.0),
365                // A text/blob argument is coerced to a real (SQLite gives
366                // `abs('5')` = 5.0, not 5).
367                other => Value::Real(crate::util::float::abs(eval::to_f64(other)) + 0.0),
368            }
369        }
370        "length" => {
371            arity(&lname, args, 1)?;
372            match &v[0] {
373                Value::Null => Value::Null,
374                Value::Blob(b) => Value::Integer(b.len() as i64),
375                // For a string, SQLite counts characters up to (not including)
376                // the first NUL — `length('A'||char(0)||'B')` is 1, not 3.
377                // Numbers stringify without NULs, so they are unaffected.
378                other => {
379                    let t = eval::to_text(other);
380                    let n = match t.find('\0') {
381                        Some(i) => t[..i].chars().count(),
382                        None => t.chars().count(),
383                    };
384                    Value::Integer(n as i64)
385                }
386            }
387        }
388        "octet_length" => {
389            arity(&lname, args, 1)?;
390            // Number of bytes in the value's encoding: blobs as-is, everything
391            // else as the UTF-8 length of its text representation.
392            match &v[0] {
393                Value::Null => Value::Null,
394                Value::Blob(b) => Value::Integer(b.len() as i64),
395                other => Value::Integer(eval::to_text(other).len() as i64),
396            }
397        }
398        "glob" => {
399            // glob(pattern, text) is the function form of `text GLOB pattern`.
400            arity(&lname, args, 2)?;
401            if v.iter().take(2).any(|x| matches!(x, Value::Null)) {
402                Value::Null
403            } else {
404                let m = eval::glob_match(&eval::to_text(&v[0]), &eval::to_text(&v[1]));
405                Value::Integer(m as i64)
406            }
407        }
408        "lower" => {
409            arity(&lname, args, 1)?;
410            // Stock sqlite3's lower()/upper() fold ASCII A–Z/a–z only; the optional
411            // `unicode` feature switches to full Unicode case-folding
412            // (`CAFÉ` → `café`), like a sqlite3 built with the ICU extension.
413            // `str::to_lowercase` provides it in core+alloc, so no dependency is
414            // needed. Default (feature off) stays byte-for-byte stock-sqlite3.
415            #[cfg(feature = "unicode")]
416            {
417                str_map(&v[0], |s| s.to_lowercase())
418            }
419            #[cfg(not(feature = "unicode"))]
420            {
421                str_map(&v[0], |s| s.to_ascii_lowercase())
422            }
423        }
424        "upper" => {
425            arity(&lname, args, 1)?;
426            #[cfg(feature = "unicode")]
427            {
428                str_map(&v[0], |s| s.to_uppercase())
429            }
430            #[cfg(not(feature = "unicode"))]
431            {
432                str_map(&v[0], |s| s.to_ascii_uppercase())
433            }
434        }
435        "trim" | "ltrim" | "rtrim" => {
436            // SQLite's trim family takes 1 (string) or 2 (string, chars) arguments.
437            if args.is_empty() || args.len() > 2 {
438                return Err(wrong_arg_count(&lname));
439            }
440            let (left, right) = match lname.as_str() {
441                "ltrim" => (true, false),
442                "rtrim" => (false, true),
443                _ => (true, true),
444            };
445            trim_fn(&v, left, right)
446        }
447        "soundex" => {
448            arity(&lname, args, 1)?;
449            // NULL/non-alpha input yields "?000" (SQLite does not propagate NULL).
450            Value::Text(soundex(&c_text(&v[0])))
451        }
452        "typeof" => {
453            arity(&lname, args, 1)?;
454            Value::Text(String::from(type_name(&v[0])))
455        }
456        "nullif" => {
457            arity(&lname, args, 2)?;
458            // The comparison follows the standard binary-comparison collation
459            // rule: an explicit `COLLATE` on either operand wins (left-preferred),
460            // else a column's declared collation, else BINARY — so
461            // `NULLIF('a','A' COLLATE NOCASE)` is NULL.
462            let coll = eval::resolve_collation(&args[0], &args[1], ctx);
463            if crate::value::cmp_values_coll(&v[0], &v[1], coll) == core::cmp::Ordering::Equal {
464                Value::Null
465            } else {
466                v[0].clone()
467            }
468        }
469        "n/a" => unreachable!(),
470        "substr" | "substring" => substr(&v)?,
471        "instr" => instr(&v)?,
472        "replace" => replace(&v)?,
473        "round" => round(&v)?,
474        "min" => scalar_min_max(&v, true)?,
475        "max" => scalar_min_max(&v, false)?,
476        "hex" => {
477            arity(&lname, args, 1)?;
478            Value::Text(hex_encode(&v[0]))
479        }
480        "char" => char_fn(&v),
481        "unicode" => {
482            arity(&lname, args, 1)?;
483            match &v[0] {
484                Value::Null => Value::Null,
485                other => eval::to_text(other)
486                    .chars()
487                    .next()
488                    .map(|c| Value::Integer(c as i64))
489                    .unwrap_or(Value::Null),
490            }
491        }
492        // `if` is SQLite's alias for `iif`. The parser desugars the scalar form
493        // (>= 2 args) into a CASE expression so it short-circuits, so this arm is
494        // normally reached only for the <2-arg arity error; it still evaluates the
495        // CASE-form semantics (`(when, then)` pairs with an optional trailing
496        // ELSE) for any non-desugared call, for robustness.
497        "iif" | "if" => {
498            if args.len() < 2 {
499                return Err(wrong_arg_count(&lname));
500            }
501            let n = v.len();
502            let mut out = if n % 2 == 1 {
503                v[n - 1].clone()
504            } else {
505                Value::Null
506            };
507            let mut i = 0;
508            while i + 1 < n {
509                if eval::truth(&v[i]) == Some(true) {
510                    out = v[i + 1].clone();
511                    break;
512                }
513                i += 2;
514            }
515            out
516        }
517        // The SQLite release graphitesql tracks and writes into new file headers
518        // (`SQLITE_VERSION_NUMBER` 3_053_002 = 3.53.2).
519        "sqlite_version" => {
520            arity(&lname, args, 0)?;
521            Value::Text(crate::TARGET_SQLITE_VERSION.into())
522        }
523        // Independent reimplementation: this is graphitesql's own identifier in
524        // SQLite's `YYYY-MM-DD HH:MM:SS <hash>` shape, not a C build's id.
525        "sqlite_source_id" => {
526            arity(&lname, args, 0)?;
527            Value::Text(crate::TARGET_SQLITE_SOURCE_ID.into())
528        }
529        "zeroblob" => {
530            arity(&lname, args, 1)?;
531            // SQLite reads the length via `sqlite3_value_int64`, which maps NULL to
532            // 0 — so `zeroblob(NULL)` is an empty blob, not NULL.
533            let n = eval::to_int_value(&v[0]).max(0) as usize;
534            if n > MAX_BLOB_LEN {
535                return Err(Error::Error("string or blob too big".into()));
536            }
537            Value::Blob(alloc::vec![0u8; n])
538        }
539        "quote" => {
540            arity(&lname, args, 1)?;
541            Value::Text(quote_value(&v[0]))
542        }
543        "unistr" => {
544            // Decode `\uXXXX` / `\UXXXXXXXX` / `\\` escapes in the argument's
545            // text. NULL passes through; any other escape errors, as in SQLite.
546            arity(&lname, args, 1)?;
547            match &v[0] {
548                Value::Null => Value::Null,
549                other => Value::Text(unistr_decode(&eval::to_text(other))?),
550            }
551        }
552        "unistr_quote" => {
553            // Like quote(), except a text value containing a control character
554            // (< U+0020) is rendered as `unistr('…')` with those characters
555            // escaped `\uXXXX`. Non-text values match quote() exactly.
556            arity(&lname, args, 1)?;
557            match &v[0] {
558                Value::Text(s) if s.chars().any(|c| (c as u32) < 0x20) => {
559                    Value::Text(unistr_quote_text(s))
560                }
561                other => Value::Text(quote_value(other)),
562            }
563        }
564        "subtype" => {
565            // The value's subtype. graphite tracks no subtypes, so this is
566            // always 0 (SQLite also returns 0 for ordinary, non-JSON values).
567            arity(&lname, args, 1)?;
568            Value::Integer(0)
569        }
570        "sign" => {
571            arity(&lname, args, 1)?;
572            // Numeric (or losslessly-numeric text) only; otherwise NULL.
573            let num = match &v[0] {
574                Value::Integer(i) => Some(*i as f64),
575                Value::Real(r) => Some(*r),
576                Value::Text(s) => eval::parse_decimal_f64(s.trim()),
577                _ => None,
578            };
579            match num {
580                Some(r) if r > 0.0 => Value::Integer(1),
581                Some(r) if r < 0.0 => Value::Integer(-1),
582                Some(_) => Value::Integer(0),
583                None => Value::Null,
584            }
585        }
586        "concat" => {
587            // SQLite 3.44+: concatenate all args, treating NULL as empty.
588            // At least one argument is required.
589            if v.is_empty() {
590                return Err(wrong_arg_count("concat"));
591            }
592            let mut s = String::new();
593            for x in &v {
594                if !matches!(x, Value::Null) {
595                    s.push_str(&eval::to_text(x));
596                }
597            }
598            Value::Text(s)
599        }
600        "concat_ws" => {
601            // A separator plus at least one value argument are required.
602            if v.len() < 2 {
603                return Err(wrong_arg_count("concat_ws"));
604            }
605            if matches!(v[0], Value::Null) {
606                Value::Null
607            } else {
608                let sep = eval::to_text(&v[0]);
609                let parts: alloc::vec::Vec<String> = v[1..]
610                    .iter()
611                    .filter(|x| !matches!(x, Value::Null))
612                    .map(eval::to_text)
613                    .collect();
614                Value::Text(parts.join(&sep))
615            }
616        }
617        "like" => {
618            // like(pattern, text[, escape]) — the function form of `text LIKE
619            // pattern`. NULL operand → NULL.
620            if v.len() < 2 || v.len() > 3 {
621                return Err(wrong_arg_count("like"));
622            }
623            // A NULL escape, like a NULL pattern/text, yields NULL.
624            if v.iter().any(|x| matches!(x, Value::Null)) {
625                Value::Null
626            } else {
627                // An explicit ESCAPE must be exactly one character (SQLite
628                // raises "ESCAPE expression must be a single character" for
629                // empty or multi-character escapes).
630                let escape = match v.get(2) {
631                    Some(e) => {
632                        let s = eval::to_text(e);
633                        let mut it = s.chars();
634                        match (it.next(), it.next()) {
635                            (Some(c), None) => Some(c),
636                            _ => {
637                                return Err(Error::Error(
638                                    "ESCAPE expression must be a single character".into(),
639                                ));
640                            }
641                        }
642                    }
643                    None => None,
644                };
645                let m = eval::like_match_escape(
646                    &eval::to_text(&v[0]),
647                    &eval::to_text(&v[1]),
648                    escape,
649                    ctx.subqueries.is_some_and(|s| s.case_sensitive_like()),
650                );
651                Value::Integer(m as i64)
652            }
653        }
654        // Optimizer hints that are no-ops at the value level (return the operand).
655        "likely" | "unlikely" => {
656            arity(&lname, args, 1)?;
657            v[0].clone()
658        }
659        "likelihood" => {
660            arity(&lname, args, 2)?;
661            // SQLite requires the second argument to be a floating-point *literal*
662            // in the range 0.0..=1.0, checked against the parsed AST rather than
663            // the runtime value: an integer literal (`0`, `1`, `2`), a negative,
664            // a string, an expression (`0.5+0.1`), or a column reference are all
665            // rejected, while `0.5`, `.5`, `1e0` and a parenthesized `(0.5)` are
666            // accepted (`exprProbability` in SQLite's `expr.c`).
667            if !likelihood_prob_is_valid(&args[1]) {
668                return Err(Error::Error(
669                    "second argument to likelihood() must be a constant between 0.0 and 1.0".into(),
670                ));
671            }
672            v[0].clone()
673        }
674        "unhex" => {
675            if v.is_empty() || v.len() > 2 {
676                return Err(wrong_arg_count("unhex"));
677            }
678            // A NULL hex string or NULL ignore-set both yield NULL.
679            let ignore = match v.get(1) {
680                Some(Value::Null) => return Ok(Value::Null),
681                // The ignore set is itself coerced as a C string (NUL-terminated).
682                Some(set) => Some(c_text(set)),
683                None => None,
684            };
685            match &v[0] {
686                Value::Null => Value::Null,
687                // The hex input is read as a NUL-terminated C string, matching
688                // SQLite's `unhexFunc` (`sqlite3_value_text`).
689                other => match unhex(&c_text(other), ignore.as_deref()) {
690                    Some(b) => Value::Blob(b),
691                    None => Value::Null,
692                },
693            }
694        }
695        // Math functions (SQLite's `-DSQLITE_ENABLE_MATH_FUNCTIONS` set; the CLI
696        // ships with these enabled). Each coerces its argument(s) to a real and
697        // returns NULL when an argument is NULL or the result is not finite,
698        // matching SQLite.
699        "pi" => {
700            arity(&lname, args, 0)?;
701            Value::Real(crate::util::float::PI)
702        }
703        "ceil" | "ceiling" => math_round_to_int(&lname, &v, crate::util::float::ceil)?,
704        "floor" => math_round_to_int(&lname, &v, crate::util::float::floor)?,
705        "trunc" => math_round_to_int(&lname, &v, crate::util::float::trunc)?,
706        "sqrt" => math1(&lname, &v, crate::util::float::sqrt)?,
707        "exp" => math1(&lname, &v, crate::util::float::exp)?,
708        "ln" => math1(&lname, &v, crate::util::float::ln)?,
709        "log2" => math1(&lname, &v, crate::util::float::log2)?,
710        "sin" => math1(&lname, &v, crate::util::float::sin)?,
711        "cos" => math1(&lname, &v, crate::util::float::cos)?,
712        "tan" => math1(&lname, &v, crate::util::float::tan)?,
713        "asin" => math1(&lname, &v, crate::util::float::asin)?,
714        "acos" => math1(&lname, &v, crate::util::float::acos)?,
715        "atan" => math1(&lname, &v, crate::util::float::atan)?,
716        "sinh" => math1(&lname, &v, crate::util::float::sinh)?,
717        "cosh" => math1(&lname, &v, crate::util::float::cosh)?,
718        "tanh" => math1(&lname, &v, crate::util::float::tanh)?,
719        "asinh" => math1(&lname, &v, crate::util::float::asinh)?,
720        "acosh" => math1(&lname, &v, crate::util::float::acosh)?,
721        "atanh" => math1(&lname, &v, crate::util::float::atanh)?,
722        "degrees" => math1(&lname, &v, crate::util::float::degrees)?,
723        "radians" => math1(&lname, &v, crate::util::float::radians)?,
724        // `log(X)` is base-10; `log(B, X)` is base B. `log10` is base-10.
725        "log10" => math1(&lname, &v, crate::util::float::log10)?,
726        "log" => {
727            if v.len() == 1 {
728                math_finite(real_arg(&v[0]).map(crate::util::float::log10))
729            } else {
730                arity(&lname, args, 2)?;
731                match (real_arg(&v[0]), real_arg(&v[1])) {
732                    // `log(B, X)` is NULL unless `B > 0`, `B != 1`, and `X > 0`
733                    // (SQLite). A base of 1 makes `ln(B) == 0`, which the bare
734                    // division would turn into ±Inf instead of NULL, so guard it.
735                    (Some(1.0), Some(_)) => Value::Null,
736                    (Some(b), Some(x)) => {
737                        math_finite(Some(crate::util::float::ln(x) / crate::util::float::ln(b)))
738                    }
739                    _ => Value::Null,
740                }
741            }
742        }
743        "pow" | "power" => {
744            arity(&lname, args, 2)?;
745            match (real_arg(&v[0]), real_arg(&v[1])) {
746                (Some(b), Some(e)) => math_finite(Some(crate::util::float::pow(b, e))),
747                _ => Value::Null,
748            }
749        }
750        "atan2" => {
751            arity(&lname, args, 2)?;
752            match (real_arg(&v[0]), real_arg(&v[1])) {
753                (Some(y), Some(x)) => math_finite(Some(crate::util::float::atan2(y, x))),
754                _ => Value::Null,
755            }
756        }
757        "mod" => {
758            arity(&lname, args, 2)?;
759            match (real_arg(&v[0]), real_arg(&v[1])) {
760                (Some(x), Some(y)) => math_finite(Some(crate::util::float::fmod(x, y))),
761                _ => Value::Null,
762            }
763        }
764        // JSON functions (see `super::json`).
765        "json" => {
766            arity(&lname, args, 1)?;
767            match json_root(&v[0])? {
768                None => Value::Null,
769                Some(j) => Value::Text(j.serialize()),
770            }
771        }
772        // `jsonb(X)` — the JSONB (binary) form of `json(X)`: parse the JSON/JSON5
773        // text (or pass a JSONB blob through) and return its JSONB encoding.
774        "jsonb" => {
775            arity(&lname, args, 1)?;
776            match json_root(&v[0])? {
777                None => Value::Null,
778                Some(j) => Value::Blob(j.to_jsonb()),
779            }
780        }
781        "json_valid" => {
782            if v.is_empty() || v.len() > 2 {
783                return Err(Error::Error(
784                    "wrong number of arguments to function json_valid()".into(),
785                ));
786            }
787            // The optional flags select which well-formedness checks count, and
788            // must be 1..=15 (sqlite errors otherwise): 0x01 = strict RFC-8259
789            // JSON text, 0x02 = JSON5 text, 0x04 = a BLOB that looks like JSONB,
790            // 0x08 = a BLOB that is fully-valid JSONB. The 1-argument form is 0x01
791            // (strict JSON only) — JSON5 acceptance needs the explicit flag.
792            let flags = match v.get(1) {
793                None => 1,
794                Some(f) => {
795                    let n = eval::to_int_value(f);
796                    if !(1..=15).contains(&n) {
797                        return Err(Error::Error(
798                            "FLAGS parameter to json_valid() must be between 1 and 15".into(),
799                        ));
800                    }
801                    n
802                }
803            };
804            match &v[0] {
805                Value::Null => Value::Null,
806                // A BLOB is only ever judged against the JSONB flag bits; text bits
807                // do not apply (and vice versa for a text value).
808                Value::Blob(b) => {
809                    let ok = flags & 0x0c != 0 && super::json::Json::from_jsonb(b).is_some();
810                    Value::Integer(ok as i64)
811                }
812                other => {
813                    let text = eval::to_text(other);
814                    let ok = (flags & 0x01 != 0 && super::json::is_strict_json(&text))
815                        || (flags & 0x02 != 0 && super::json::parse(&text).is_some());
816                    Value::Integer(ok as i64)
817                }
818            }
819        }
820        // `json_error_position(X)` — the 1-based byte position of the first JSON
821        // syntax error in X, or 0 when X is well-formed JSON. NULL yields NULL,
822        // matching sqlite3.
823        "json_error_position" => {
824            arity(&lname, args, 1)?;
825            match &v[0] {
826                Value::Null => Value::Null,
827                other => {
828                    let pos = match super::json::parse_with_error_position(&eval::to_text(other)) {
829                        Ok(_) => 0,
830                        Err(off) => off as i64 + 1,
831                    };
832                    Value::Integer(pos)
833                }
834            }
835        }
836        // `json_pretty(X [, indent])` — reformat with indentation (default 4
837        // spaces). Empty arrays/objects and scalars stay compact, like SQLite.
838        "json_pretty" => {
839            if v.is_empty() || v.len() > 2 {
840                return Err(wrong_arg_count("json_pretty"));
841            }
842            match &v[0] {
843                Value::Null => Value::Null,
844                other => {
845                    let indent = match v.get(1) {
846                        Some(Value::Null) | None => alloc::string::String::from("    "),
847                        Some(iv) => eval::to_text(iv),
848                    };
849                    let _ = other;
850                    match json_root(&v[0])? {
851                        None => Value::Null,
852                        Some(j) => Value::Text(j.pretty(&indent)),
853                    }
854                }
855            }
856        }
857        "json_quote" => {
858            arity(&lname, args, 1)?;
859            // A BLOB that decodes as JSONB renders as its JSON text (so
860            // `json_quote(x'01')` → `true`, `json_quote(jsonb('[1,2]'))` →
861            // `[1,2]`); any other BLOB is rejected. graphite has no value
862            // subtypes, so it falls back to "does it parse as JSONB", matching
863            // sqlite.
864            let j = match &v[0] {
865                Value::Blob(b) => super::json::Json::from_jsonb(b)
866                    .ok_or_else(|| Error::Error("JSON cannot hold BLOB values".into()))?,
867                other => super::json::value_to_json(other),
868            };
869            Value::Text(j.quote())
870        }
871        "json_type" => {
872            if v.is_empty() || v.len() > 2 {
873                return Err(wrong_arg_count("json_type"));
874            }
875            match json_root(&v[0])? {
876                None => Value::Null,
877                Some(root) => {
878                    let target = if v.len() == 2 {
879                        check_path(&v[1])?;
880                        super::json::navigate(&root, &eval::to_text(&v[1]))
881                    } else {
882                        Some(&root)
883                    };
884                    match target {
885                        Some(j) => Value::Text(String::from(j.type_name())),
886                        None => Value::Null,
887                    }
888                }
889            }
890        }
891        "json_array_length" => {
892            if v.is_empty() || v.len() > 2 {
893                return Err(wrong_arg_count("json_array_length"));
894            }
895            match json_root(&v[0])? {
896                None => Value::Null,
897                Some(root) => {
898                    let target = if v.len() == 2 {
899                        check_path(&v[1])?;
900                        super::json::navigate(&root, &eval::to_text(&v[1]))
901                    } else {
902                        Some(&root)
903                    };
904                    match target {
905                        Some(super::json::Json::Array(items)) => Value::Integer(items.len() as i64),
906                        Some(_) => Value::Integer(0),
907                        None => Value::Null,
908                    }
909                }
910            }
911        }
912        "json_extract" | "jsonb_extract" => {
913            // SQLite's json_extract is variadic: with fewer than two arguments
914            // it returns NULL without even parsing the document (so an invalid
915            // or absent first argument is NOT an error here).
916            if v.len() < 2 {
917                return Ok(Value::Null);
918            }
919            match json_root(&v[0])? {
920                None => Value::Null,
921                Some(root) => json_extract(&root, &v[1..], lname.starts_with("jsonb"))?,
922            }
923        }
924        "json_array" | "jsonb_array" => {
925            let mut items = Vec::with_capacity(v.len());
926            for (i, val) in v.iter().enumerate() {
927                items.push(json_value_arg(val, args.get(i))?);
928            }
929            json_doc_result(&lname, &super::json::Json::Array(items))
930        }
931        "json_object" | "jsonb_object" => {
932            if !v.len().is_multiple_of(2) {
933                return Err(Error::Error(
934                    "json_object() requires an even number of arguments".into(),
935                ));
936            }
937            let mut members = Vec::with_capacity(v.len() / 2);
938            for pair in v.chunks(2).enumerate() {
939                let (i, kv) = pair;
940                // SQLite requires object labels to be TEXT (a NULL, numeric, or
941                // BLOB key is an error), and rejects BLOB values.
942                let Value::Text(key) = &kv[0] else {
943                    return Err(Error::Error("json_object() labels must be TEXT".into()));
944                };
945                let val = json_value_arg(&kv[1], args.get(2 * i + 1))?;
946                // A key built from a SQL TEXT arg carries no escape provenance.
947                members.push((key.clone(), None, val));
948            }
949            json_doc_result(&lname, &super::json::Json::Object(members))
950        }
951        "json_set" | "json_insert" | "json_replace" | "jsonb_set" | "jsonb_insert"
952        | "jsonb_replace" => {
953            // sqlite: zero args → NULL; an even arg count is a hard error (the
954            // message always names the text-output `json_*` form, even for the
955            // `jsonb_*` blob variants); an odd count is the document followed by
956            // zero or more (path, value) pairs (a bare document is a no-op).
957            if v.is_empty() {
958                return Ok(Value::Null);
959            }
960            if v.len().is_multiple_of(2) {
961                let report = lname
962                    .strip_prefix("jsonb_")
963                    .map_or_else(|| lname.clone(), |rest| alloc::format!("json_{rest}"));
964                return Err(Error::Error(alloc::format!(
965                    "{report}() needs an odd number of arguments"
966                )));
967            }
968            let mode = if lname.ends_with("set") {
969                super::json::SetMode::Set
970            } else if lname.ends_with("insert") {
971                super::json::SetMode::Insert
972            } else {
973                super::json::SetMode::Replace
974            };
975            match json_root(&v[0])? {
976                None => Value::Null,
977                Some(mut root) => {
978                    let mut i = 1;
979                    while i + 1 < v.len() {
980                        check_path(&v[i])?;
981                        let path = eval::to_text(&v[i]);
982                        let val = json_value_arg(&v[i + 1], args.get(i + 1))?;
983                        super::json::set_path(&mut root, &path, val, mode);
984                        i += 2;
985                    }
986                    json_doc_result(&lname, &root)
987                }
988            }
989        }
990        "json_remove" | "jsonb_remove" => {
991            if v.is_empty() {
992                return Err(Error::Error("json_remove() requires a document".into()));
993            }
994            match json_root(&v[0])? {
995                None => Value::Null,
996                Some(mut root) => {
997                    let mut removed = Value::Text(String::new());
998                    for p in &v[1..] {
999                        // A NULL path collapses the whole call to NULL (scanning
1000                        // left to right, so a malformed path *before* it still
1001                        // errors via check_path first), discarding any removals
1002                        // already applied — matching sqlite's json_remove.
1003                        if matches!(p, Value::Null) {
1004                            removed = Value::Null;
1005                            break;
1006                        }
1007                        check_path(p)?;
1008                        // Removing the whole document (`$`) yields SQL NULL.
1009                        if matches!(p, Value::Text(s) if s == "$") {
1010                            removed = Value::Null;
1011                            continue;
1012                        }
1013                        super::json::remove_path(&mut root, &eval::to_text(p));
1014                    }
1015                    if matches!(removed, Value::Null) {
1016                        Value::Null
1017                    } else {
1018                        json_doc_result(&lname, &root)
1019                    }
1020                }
1021            }
1022        }
1023        "json_patch" | "jsonb_patch" => {
1024            arity(&lname, args, 2)?;
1025            match (json_root(&v[0])?, json_root(&v[1])?) {
1026                (Some(mut root), Some(patch)) => {
1027                    super::json::merge_patch(&mut root, &patch);
1028                    json_doc_result(&lname, &root)
1029                }
1030                _ => Value::Null,
1031            }
1032        }
1033        // Date/time functions (see `super::datetime`).
1034        "date" => super::datetime::date(&v),
1035        "time" => super::datetime::time(&v),
1036        "datetime" => super::datetime::datetime(&v),
1037        "julianday" => super::datetime::julianday(&v),
1038        "unixepoch" => super::datetime::unixepoch(&v),
1039        "strftime" => super::datetime::strftime(&v),
1040        "timediff" => {
1041            arity(&lname, args, 2)?;
1042            super::datetime::timediff(&v[0], &v[1])
1043        }
1044        "geopoly_json" => {
1045            arity(&lname, args, 1)?;
1046            match crate::geopoly::parse_value(&v[0]) {
1047                Some(p) => Value::Text(p.to_json()),
1048                None => Value::Null,
1049            }
1050        }
1051        "geopoly_blob" => {
1052            arity(&lname, args, 1)?;
1053            match crate::geopoly::parse_value(&v[0]) {
1054                Some(p) => Value::Blob(p.to_blob()),
1055                None => Value::Null,
1056            }
1057        }
1058        "geopoly_area" => {
1059            arity(&lname, args, 1)?;
1060            match crate::geopoly::parse_value(&v[0]) {
1061                Some(p) => Value::Real(p.area()),
1062                None => Value::Null,
1063            }
1064        }
1065        "geopoly_bbox" => {
1066            arity(&lname, args, 1)?;
1067            match crate::geopoly::parse_value(&v[0]) {
1068                Some(p) => Value::Blob(p.bbox().to_blob()),
1069                None => Value::Null,
1070            }
1071        }
1072        "geopoly_ccw" => {
1073            arity(&lname, args, 1)?;
1074            match crate::geopoly::parse_value(&v[0]) {
1075                Some(p) => Value::Blob(p.ccw().to_blob()),
1076                None => Value::Null,
1077            }
1078        }
1079        "geopoly_regular" => {
1080            arity(&lname, args, 4)?;
1081            // A NULL argument yields NULL (SQLite's `sqlite3_value_double`/`int`
1082            // treat NULL as 0, but n<3 or r<=0 then returns NULL anyway; an
1083            // explicit NULL check keeps the common cases NULL-clean).
1084            if v.iter().any(|x| matches!(x, Value::Null)) {
1085                Value::Null
1086            } else {
1087                let cx = eval::to_f64(&v[0]);
1088                let cy = eval::to_f64(&v[1]);
1089                let r = eval::to_f64(&v[2]);
1090                let n = eval::to_int_value(&v[3]);
1091                match crate::geopoly::regular(cx, cy, r, n) {
1092                    Some(p) => Value::Blob(p.to_blob()),
1093                    None => Value::Null,
1094                }
1095            }
1096        }
1097        "geopoly_contains_point" => {
1098            arity(&lname, args, 3)?;
1099            match crate::geopoly::parse_value(&v[0]) {
1100                Some(p) => {
1101                    Value::Integer(p.contains_point(eval::to_f64(&v[1]), eval::to_f64(&v[2])))
1102                }
1103                None => Value::Null,
1104            }
1105        }
1106        "geopoly_overlap" => {
1107            arity(&lname, args, 2)?;
1108            match (
1109                crate::geopoly::parse_value(&v[0]),
1110                crate::geopoly::parse_value(&v[1]),
1111            ) {
1112                (Some(p1), Some(p2)) => Value::Integer(crate::geopoly::overlap(&p1, &p2)),
1113                _ => Value::Null,
1114            }
1115        }
1116        "geopoly_within" => {
1117            arity(&lname, args, 2)?;
1118            match (
1119                crate::geopoly::parse_value(&v[0]),
1120                crate::geopoly::parse_value(&v[1]),
1121            ) {
1122                (Some(p1), Some(p2)) => Value::Integer(crate::geopoly::within(&p1, &p2)),
1123                _ => Value::Null,
1124            }
1125        }
1126        "geopoly_svg" => {
1127            // geopoly_svg(X, ...) is variadic. With no arguments SQLite's
1128            // implementation simply returns NULL (`if(argc<1) return;`) rather
1129            // than raising an arity error.
1130            if args.is_empty() {
1131                return Ok(Value::Null);
1132            }
1133            match crate::geopoly::parse_value(&v[0]) {
1134                Some(p) => {
1135                    let extra: Vec<Option<String>> = v[1..]
1136                        .iter()
1137                        .map(|x| match x {
1138                            Value::Null => None,
1139                            other => Some(eval::to_text(other)),
1140                        })
1141                        .collect();
1142                    Value::Text(p.to_svg(&extra))
1143                }
1144                None => Value::Null,
1145            }
1146        }
1147        "geopoly_xform" => {
1148            arity(&lname, args, 7)?;
1149            match crate::geopoly::parse_value(&v[0]) {
1150                Some(p) => Value::Blob(
1151                    p.xform(
1152                        eval::to_f64(&v[1]),
1153                        eval::to_f64(&v[2]),
1154                        eval::to_f64(&v[3]),
1155                        eval::to_f64(&v[4]),
1156                        eval::to_f64(&v[5]),
1157                        eval::to_f64(&v[6]),
1158                    )
1159                    .to_blob(),
1160                ),
1161                None => Value::Null,
1162            }
1163        }
1164        "printf" | "format" => super::datetime::printf(&v),
1165        _ => {
1166            // A user-defined function registered via `register_function`. Builtins
1167            // above take precedence; this fires only for an otherwise-unknown name.
1168            if let Some(result) = ctx.subqueries.and_then(|s| s.call_udf(&lname, &v)) {
1169                return result;
1170            }
1171            // A built-in window/ranking function used outside an `OVER` clause is
1172            // "misuse of window function NAME()" in SQLite, not "no such function".
1173            if is_window_function_name(&lname) {
1174                return Err(Error::Error(alloc::format!(
1175                    "misuse of window function {lname}()"
1176                )));
1177            }
1178            // `RAISE(...)` parses into a canonical `raise(...)` call. A real
1179            // trigger program intercepts it before evaluation (see `fire_raise`);
1180            // reaching scalar dispatch means it was used outside a trigger, which
1181            // SQLite rejects with this dedicated message rather than "no such
1182            // function".
1183            if lname == "raise" {
1184                return Err(Error::Error(alloc::string::String::from(
1185                    "RAISE() may only be used within a trigger-program",
1186                )));
1187            }
1188            return Err(Error::Error(alloc::format!("no such function: {name}")));
1189        }
1190    })
1191}
1192
1193/// Render a value as a SQL literal, like SQLite's `quote()`.
1194fn quote_value(v: &Value) -> String {
1195    match v {
1196        Value::Null => String::from("NULL"),
1197        Value::Integer(i) => alloc::format!("{i}"),
1198        // `quote()` renders an infinity as `±9.0e+999` (unlike plain text output,
1199        // which prints `Inf`).
1200        Value::Real(r) if !r.is_finite() => {
1201            String::from(if *r < 0.0 { "-9.0e+999" } else { "9.0e+999" })
1202        }
1203        // `quote()` renders a finite real at round-trip precision (`%!0.15g`,
1204        // falling back to `%!0.20e`), unlike the 15-significant-digit column
1205        // rendering `format_real` produces — so a dumped real reparses exactly.
1206        Value::Real(r) => crate::util::fpdecode::quote_real(*r),
1207        Value::Text(s) => {
1208            // SQLite's `quote()` reads the text through `sqlite3_value_text`, a
1209            // NUL-terminated C string, so an embedded NUL truncates the rendered
1210            // literal (the stored value keeps its full bytes). Match that.
1211            let s = s.split('\0').next().unwrap_or("");
1212            alloc::format!("'{}'", s.replace('\'', "''"))
1213        }
1214        Value::Blob(b) => {
1215            // SQLite renders blob literals as `X'ABCD'` — uppercase `X` and
1216            // uppercase hex digits.
1217            let mut s = String::from("X'");
1218            for byte in b {
1219                s.push_str(&alloc::format!("{byte:02X}"));
1220            }
1221            s.push('\'');
1222            s
1223        }
1224    }
1225}
1226
1227/// Decode SQLite `unistr()` escapes: `\uXXXX` (4 hex), `\UXXXXXXXX` (8 hex), and
1228/// `\\` (a literal backslash). Any other backslash sequence — including a `\u`/
1229/// `\U` with too few hex digits or a trailing `\` — is an error, matching
1230/// SQLite's "invalid Unicode escape". A code point Rust cannot represent (a lone
1231/// surrogate or one past U+10FFFF) becomes the replacement char `U+FFFD`; SQLite
1232/// emits raw WTF-8 there, an extreme edge graphite cannot store in a `String`.
1233fn unistr_decode(s: &str) -> Result<String> {
1234    let cs: alloc::vec::Vec<char> = s.chars().collect();
1235    let mut out = String::with_capacity(s.len());
1236    let invalid = || Error::Error(String::from("invalid Unicode escape"));
1237    let hex_char = |cs: &[char], at: usize, n: usize| -> Option<u32> {
1238        let slice = cs.get(at..at + n)?;
1239        if !slice.iter().all(|c| c.is_ascii_hexdigit()) {
1240            return None;
1241        }
1242        let s: String = slice.iter().collect();
1243        u32::from_str_radix(&s, 16).ok()
1244    };
1245    let mut i = 0;
1246    while i < cs.len() {
1247        if cs[i] != '\\' {
1248            out.push(cs[i]);
1249            i += 1;
1250            continue;
1251        }
1252        match cs.get(i + 1) {
1253            Some('\\') => {
1254                out.push('\\');
1255                i += 2;
1256            }
1257            Some('u') => {
1258                let cp = hex_char(&cs, i + 2, 4).ok_or_else(invalid)?;
1259                out.push(char::from_u32(cp).unwrap_or('\u{FFFD}'));
1260                i += 6;
1261            }
1262            Some('U') => {
1263                let cp = hex_char(&cs, i + 2, 8).ok_or_else(invalid)?;
1264                out.push(char::from_u32(cp).unwrap_or('\u{FFFD}'));
1265                i += 10;
1266            }
1267            _ => return Err(invalid()),
1268        }
1269    }
1270    Ok(out)
1271}
1272
1273/// Render a control-character-bearing text value as SQLite's `unistr('…')`: each
1274/// character below U+0020 becomes `\uXXXX`, a backslash doubles, a single quote
1275/// doubles, everything else (including non-ASCII) is kept literal.
1276fn unistr_quote_text(s: &str) -> String {
1277    let mut out = String::from("unistr('");
1278    for c in s.chars() {
1279        let cp = c as u32;
1280        if cp < 0x20 {
1281            out.push_str(&alloc::format!("\\u{cp:04x}"));
1282        } else if c == '\\' {
1283            out.push_str("\\\\");
1284        } else if c == '\'' {
1285            out.push_str("''");
1286        } else {
1287            out.push(c);
1288        }
1289    }
1290    out.push_str("')");
1291    out
1292}
1293
1294/// Decode a hex string to bytes, returning `None` on malformed input. A faithful
1295/// port of SQLite's `unhexFunc`:
1296///
1297/// - with no ignore set (`ignore` is `None`), the input must be an even number of
1298///   hex digits and nothing else;
1299/// - with an ignore set (the 2-argument form), characters from `ignore` may
1300///   appear *before*, *between*, and *after* complete byte pairs, but never
1301///   *within* a pair — so `unhex('AB CD', ' ')` is `X'ABCD'` while
1302///   `unhex('A BCD', ' ')` is `NULL`. Any character that is neither a hex digit
1303///   nor in the ignore set fails the whole decode.
1304fn unhex(s: &str, ignore: Option<&str>) -> Option<alloc::vec::Vec<u8>> {
1305    let hexval = |c: char| -> Option<u8> {
1306        match c {
1307            '0'..='9' => Some(c as u8 - b'0'),
1308            'a'..='f' => Some(c as u8 - b'a' + 10),
1309            'A'..='F' => Some(c as u8 - b'A' + 10),
1310            _ => None,
1311        }
1312    };
1313    let ignored = |c: char| -> bool { ignore.is_some_and(|set| set.contains(c)) };
1314    let mut out = alloc::vec::Vec::new();
1315    let mut it = s.chars();
1316    loop {
1317        // Skip any leading/inter-pair ignore characters. A non-ignored,
1318        // non-hex-digit character (or running out of input) ends this phase.
1319        let hi = loop {
1320            match it.next() {
1321                None => return Some(out),
1322                Some(c) if hexval(c).is_some() => break c,
1323                Some(c) if ignored(c) => continue,
1324                Some(_) => return None,
1325            }
1326        };
1327        // The second nibble must immediately follow the first.
1328        let lo = it.next()?;
1329        out.push((hexval(hi)? << 4) | hexval(lo)?);
1330    }
1331}
1332
1333/// Whether `e` is an acceptable `likelihood()` probability: a floating-point
1334/// literal in `0.0..=1.0`, after peeling any redundant parentheses. SQLite's
1335/// `exprProbability` only accepts a bare `TK_FLOAT` token in range, so an
1336/// integer literal, a negated float, or any compound expression is rejected.
1337pub(crate) fn likelihood_prob_is_valid(e: &Expr) -> bool {
1338    match e {
1339        Expr::Paren(inner) => likelihood_prob_is_valid(inner),
1340        Expr::Literal(Literal::Real(r)) => (0.0..=1.0).contains(r),
1341        _ => false,
1342    }
1343}
1344
1345fn arity(name: &str, args: &[Expr], n: usize) -> Result<()> {
1346    if args.len() == n {
1347        Ok(())
1348    } else {
1349        Err(wrong_arg_count(name))
1350    }
1351}
1352
1353/// SQLite's universal arity-error message: `wrong number of arguments to
1354/// function NAME()` — no "(want N, got M)" suffix, for every built-in.
1355fn wrong_arg_count(name: &str) -> Error {
1356    Error::Error(alloc::format!(
1357        "wrong number of arguments to function {name}()"
1358    ))
1359}
1360
1361/// The built-in ranking/value window functions, which are only valid inside an
1362/// `OVER` clause. Called as a plain scalar, SQLite reports "misuse of window
1363/// function NAME()". `name` must already be lowercased.
1364fn is_window_function_name(name: &str) -> bool {
1365    matches!(
1366        name,
1367        "row_number"
1368            | "rank"
1369            | "dense_rank"
1370            | "percent_rank"
1371            | "cume_dist"
1372            | "ntile"
1373            | "first_value"
1374            | "last_value"
1375            | "nth_value"
1376            | "lag"
1377            | "lead"
1378    )
1379}
1380
1381/// SQLite's `soundex(X)`: the phonetic code of the first word in `X` — the first
1382/// letter followed by up to three digits, padded with `0`. Input with no letters
1383/// (including NULL, which `to_text` maps to "") yields `"?000"`. Faithful port of
1384/// `soundexFunc`: each letter maps to a digit; a digit is emitted only when it is
1385/// nonzero and differs from the previous letter's code; a zero-code character
1386/// (vowel, `H`/`W`/`Y`, or non-letter) resets the running code.
1387fn soundex(s: &str) -> String {
1388    // Code for a-z (index `c.to_ascii_lowercase() - b'a'`); 0 = not coded.
1389    const CODE: [u8; 26] = [
1390        0, 1, 2, 3, 0, 1, 2, 0, 0, 2, 2, 4, 5, 5, 0, 1, 2, 6, 2, 3, 0, 1, 0, 2, 0, 2,
1391    ];
1392    let code_of = |c: u8| -> u8 {
1393        if c.is_ascii_alphabetic() {
1394            CODE[(c.to_ascii_lowercase() - b'a') as usize]
1395        } else {
1396            0
1397        }
1398    };
1399    let b = s.as_bytes();
1400    let mut i = 0;
1401    while i < b.len() && !b[i].is_ascii_alphabetic() {
1402        i += 1;
1403    }
1404    if i >= b.len() {
1405        return String::from("?000");
1406    }
1407    let mut out = String::with_capacity(4);
1408    out.push(b[i].to_ascii_uppercase() as char);
1409    let mut prev = code_of(b[i]);
1410    let mut j = 1;
1411    while j < 4 && i < b.len() {
1412        let code = code_of(b[i]);
1413        if code > 0 {
1414            if code != prev {
1415                prev = code;
1416                out.push((b'0' + code) as char);
1417                j += 1;
1418            }
1419        } else {
1420            prev = 0;
1421        }
1422        i += 1;
1423    }
1424    while j < 4 {
1425        out.push('0');
1426        j += 1;
1427    }
1428    out
1429}
1430
1431/// Coerce a value to text for the string functions (`trim`/`upper`/`lower`/
1432/// `replace`/`substr`/`soundex`).
1433///
1434/// When the value is a BLOB, SQLite reads it as a NUL-terminated C string
1435/// (`sqlite3_value_text`), so an embedded `NUL` byte truncates the coerced
1436/// text: `trim(X'00200041…')` is `''`, not `'   A   '`. We reproduce that for
1437/// the blob path. A genuine TEXT value keeps its embedded NULs: this engine's
1438/// TEXT model is NUL-preserving (e.g. the JSON5 `\0` escape stores a real NUL
1439/// character — see `tests/json5.rs`), and `length`/`unicode` count through it,
1440/// so truncating TEXT here would be inconsistent with the rest of the engine.
1441fn c_text(v: &Value) -> String {
1442    match v {
1443        Value::Blob(b) => {
1444            let end = b.iter().position(|&x| x == 0).unwrap_or(b.len());
1445            String::from_utf8_lossy(&b[..end]).into_owned()
1446        }
1447        other => eval::to_text(other),
1448    }
1449}
1450
1451fn str_map(v: &Value, f: impl Fn(&str) -> String) -> Value {
1452    match v {
1453        Value::Null => Value::Null,
1454        other => Value::Text(f(&c_text(other))),
1455    }
1456}
1457
1458/// A numeric argument to a math function: `None` for SQL `NULL`, else the value
1459/// coerced to `f64` (SQLite applies REAL affinity to math-function arguments).
1460/// Coerce a math-function argument to `f64` using SQLite's
1461/// `sqlite3_value_numeric_type` rule: an INTEGER / REAL — or a text that is wholly
1462/// numeric (`'4'`, `'  9  '`, `'2e2'`) — yields its numeric value, while a
1463/// non-numeric text, a blob, or NULL yields `None` (so the caller returns SQL
1464/// NULL). This is *not* the lax `to_f64`, which would turn `'abc'` or a blob into
1465/// `0.0` and silently feed it to `sqrt`/`log`/`pow`/…
1466fn real_arg(v: &Value) -> Option<f64> {
1467    eval::to_number_strict(v).map(|n| eval::to_f64(&n))
1468}
1469
1470/// Wrap a computed math result, matching SQLite's split between a *domain error*
1471/// and an *overflow*:
1472///
1473/// - a missing argument or a `NaN` result (`sqrt(-1)`, `ln(0)`, `acos(2)`, …)
1474///   becomes SQL `NULL`;
1475/// - a `±∞` result is kept as a `REAL` — both genuine overflow (`exp(710)`,
1476///   `pow(2,2000)`) and poles (`pow(0,-1)`, `atanh(1)`) render as `Inf`/`-Inf`,
1477///   exactly as SQLite reports them.
1478///
1479/// Each underlying `float` routine is responsible for returning `NaN` (not `±∞`)
1480/// on a domain error, so that the NULL-vs-Inf decision is made consistently here.
1481fn math_finite(r: Option<f64>) -> Value {
1482    match r {
1483        Some(x) if x.is_nan() => Value::Null,
1484        Some(x) => Value::Real(x),
1485        None => Value::Null,
1486    }
1487}
1488
1489/// A one-argument math function: arity-check, coerce, apply, finiteness-guard.
1490fn math1(name: &str, v: &[Value], f: impl Fn(f64) -> f64) -> Result<Value> {
1491    if v.len() != 1 {
1492        return Err(Error::Error(alloc::format!(
1493            "wrong number of arguments to function {name}()"
1494        )));
1495    }
1496    Ok(math_finite(real_arg(&v[0]).map(f)))
1497}
1498
1499/// `ceil`/`ceiling`/`floor`/`trunc`: SQLite dispatches on `sqlite3_value_numeric_type`
1500/// — an INTEGER argument is returned UNCHANGED (so its exact value, incl. i64
1501/// extremes, survives), a REAL is rounded to a REAL, and a non-numeric text / blob
1502/// / NULL argument yields NULL. This differs from the other `math1` functions,
1503/// which always coerce to a real (and read a blob's bytes as a number).
1504fn math_round_to_int(name: &str, v: &[Value], f: impl Fn(f64) -> f64) -> Result<Value> {
1505    if v.len() != 1 {
1506        return Err(Error::Error(alloc::format!(
1507            "wrong number of arguments to function {name}()"
1508        )));
1509    }
1510    Ok(match eval::to_number_strict(&v[0]) {
1511        Some(Value::Integer(i)) => Value::Integer(i),
1512        Some(Value::Real(r)) => math_finite(Some(f(r))),
1513        _ => Value::Null,
1514    })
1515}
1516
1517/// Parse the first argument of a JSON function as a document: `NULL` → `None`;
1518/// malformed JSON is an error (matching SQLite).
1519/// Render a JSON document result as text (`json_*`) or as a JSONB blob
1520/// (`jsonb_*`), chosen by the function name.
1521fn json_doc_result(lname: &str, j: &super::json::Json) -> Value {
1522    if lname.starts_with("jsonb") {
1523        Value::Blob(j.to_jsonb())
1524    } else {
1525        Value::Text(j.serialize())
1526    }
1527}
1528
1529fn json_root(v: &Value) -> Result<Option<super::json::Json>> {
1530    match v {
1531        Value::Null => Ok(None),
1532        // A BLOB document is JSONB (SQLite's binary JSON); text is JSON/JSON5.
1533        Value::Blob(b) => match super::json::Json::from_jsonb(b) {
1534            Some(j) => Ok(Some(j)),
1535            None => Err(Error::Error("malformed JSON".into())),
1536        },
1537        other => match super::json::parse(&eval::to_text(other)) {
1538            Some(j) => Ok(Some(j)),
1539            None => Err(Error::Error("malformed JSON".into())),
1540        },
1541    }
1542}
1543
1544/// Validate a JSON path argument, raising SQLite's `bad JSON path: '<path>'`
1545/// error if it is malformed. A `NULL` path is left for the caller to treat as a
1546/// missing lookup (SQLite returns NULL rather than erroring on a NULL path).
1547fn check_path(p: &Value) -> Result<()> {
1548    // A NULL path is not validated — callers treat it as a missing lookup
1549    // (SQLite returns NULL rather than erroring). Every other type is coerced to
1550    // text first (SQLite applies its usual text conversion before parsing the
1551    // path), so an integer/real/blob argument is validated as the path it spells
1552    // — e.g. `json_extract(j, 1)` raises `bad JSON path: '1'`, just like sqlite.
1553    if matches!(p, Value::Null) {
1554        return Ok(());
1555    }
1556    let s = eval::to_text(p);
1557    if !super::json::path_is_valid(&s) {
1558        return Err(Error::Error(alloc::format!("bad JSON path: '{s}'")));
1559    }
1560    Ok(())
1561}
1562
1563/// Convert a *value* argument of a JSON/JSONB constructor or mutator to JSON. A
1564/// BLOB is embedded as its JSON when it decodes as JSONB (so `jsonb_*` results
1565/// compose, e.g. `jsonb_object('a', jsonb_array(1,2))`) and otherwise rejected —
1566/// graphite has no value subtypes, so it falls back to "does it parse as JSONB".
1567fn json_value_arg(val: &Value, expr: Option<&Expr>) -> Result<super::json::Json> {
1568    if let Value::Blob(b) = val {
1569        return super::json::Json::from_jsonb(b)
1570            .ok_or_else(|| Error::Error("JSON cannot hold BLOB values".into()));
1571    }
1572    Ok(arg_to_json(val, expr))
1573}
1574
1575/// `json_extract`: one path returns the SQL value at that path (objects/arrays as
1576/// minified JSON text); multiple paths return a JSON array of the extracted
1577/// elements (missing paths become JSON `null`). `jsonb_extract` is the same but
1578/// returns object/array results (and the multi-path array) as JSONB blobs.
1579fn json_extract(root: &super::json::Json, paths: &[Value], jsonb: bool) -> Result<Value> {
1580    // SQLite scans the paths left to right: a NULL path collapses the whole
1581    // result to NULL (even when a *later* path is malformed), but a malformed
1582    // non-NULL path that comes *first* still errors before the NULL is reached.
1583    for p in paths {
1584        if matches!(p, Value::Null) {
1585            return Ok(Value::Null);
1586        }
1587        check_path(p)?;
1588    }
1589    // A non-scalar single-path result is JSONB under jsonb_extract; scalars are
1590    // returned as their SQL value either way.
1591    let scalar_or_doc = |j: &super::json::Json| -> Value {
1592        match j {
1593            super::json::Json::Array(_) | super::json::Json::Object(_) if jsonb => {
1594                Value::Blob(j.to_jsonb())
1595            }
1596            _ => j.to_sql(),
1597        }
1598    };
1599    if paths.len() == 1 {
1600        return Ok(
1601            match super::json::navigate(root, &eval::to_text(&paths[0])) {
1602                Some(j) => scalar_or_doc(j),
1603                None => Value::Null,
1604            },
1605        );
1606    }
1607    let items = paths
1608        .iter()
1609        .map(|p| match super::json::navigate(root, &eval::to_text(p)) {
1610            Some(j) => j.clone(),
1611            None => super::json::Json::Null,
1612        })
1613        .collect();
1614    let arr = super::json::Json::Array(items);
1615    Ok(if jsonb {
1616        Value::Blob(arr.to_jsonb())
1617    } else {
1618        Value::Text(arr.serialize())
1619    })
1620}
1621
1622/// Convert a constructor argument to JSON. If the source expression is itself a
1623/// JSON-producing call (`json`, `json_array`, `json_object`), its text value is
1624/// embedded as parsed JSON — mirroring SQLite's JSON subtype propagation — rather
1625/// than quoted as a string.
1626pub(crate) fn arg_to_json(val: &Value, expr: Option<&Expr>) -> super::json::Json {
1627    if let (Value::Text(s), Some(e)) = (val, expr) {
1628        if produces_json(e) {
1629            if let Some(j) = super::json::parse(s) {
1630                return j;
1631            }
1632        }
1633    }
1634    super::json::value_to_json(val)
1635}
1636
1637/// Whether an expression *statically* yields a value carrying SQLite's JSON
1638/// subtype. Functions that always emit a JSON structure (including the
1639/// `json_group_array`/`json_group_object` aggregates) qualify; `json_extract`
1640/// only with two or more paths (then its result is always a JSON array — a
1641/// single path's subtype is value-dependent and needs the runtime subtype, not
1642/// modelled here); the `->` operator always carries the subtype (`->>` does not).
1643fn produces_json(e: &Expr) -> bool {
1644    match e {
1645        Expr::Function { name, args, .. } => {
1646            let lname = name.to_ascii_lowercase();
1647            match lname.as_str() {
1648                "json" | "json_array" | "json_object" | "json_insert" | "json_replace"
1649                | "json_set" | "json_patch" | "json_remove" | "json_group_array"
1650                | "json_group_object" => true,
1651                // Multiple paths → a JSON array (subtype). One path's subtype is
1652                // value-dependent (structure vs scalar), so don't claim it here.
1653                "json_extract" => args.len() >= 3,
1654                _ => false,
1655            }
1656        }
1657        // `->` (JsonExtract) carries the JSON subtype; `->>` (JsonExtractText) does not.
1658        Expr::Binary {
1659            op: crate::sql::ast::BinaryOp::JsonExtract,
1660            ..
1661        } => true,
1662        Expr::Paren(inner) => produces_json(inner),
1663        _ => false,
1664    }
1665}
1666
1667fn type_name(v: &Value) -> &'static str {
1668    match v {
1669        Value::Null => "null",
1670        Value::Integer(_) => "integer",
1671        Value::Real(_) => "real",
1672        Value::Text(_) => "text",
1673        Value::Blob(_) => "blob",
1674    }
1675}
1676
1677fn trim_fn(v: &[Value], left: bool, right: bool) -> Value {
1678    if v.is_empty() || matches!(v[0], Value::Null) {
1679        return Value::Null;
1680    }
1681    // A NULL trim-set yields NULL, like any other NULL argument (sqlite:
1682    // `trim(X, NULL)` / `ltrim` / `rtrim` are all NULL).
1683    if v.len() >= 2 && matches!(v[1], Value::Null) {
1684        return Value::Null;
1685    }
1686    let s = c_text(&v[0]);
1687    let trim_chars: Vec<char> = if v.len() >= 2 {
1688        c_text(&v[1]).chars().collect()
1689    } else {
1690        alloc::vec![' ']
1691    };
1692    let is_trim = |c: char| trim_chars.contains(&c);
1693    let chars: Vec<char> = s.chars().collect();
1694    let mut start = 0;
1695    let mut end = chars.len();
1696    if left {
1697        while start < end && is_trim(chars[start]) {
1698            start += 1;
1699        }
1700    }
1701    if right {
1702        while end > start && is_trim(chars[end - 1]) {
1703            end -= 1;
1704        }
1705    }
1706    Value::Text(chars[start..end].iter().collect())
1707}
1708
1709fn substr(v: &[Value]) -> Result<Value> {
1710    if v.len() < 2 || v.len() > 3 {
1711        return Err(wrong_arg_count("substr"));
1712    }
1713    if matches!(v[0], Value::Null) {
1714        return Ok(Value::Null);
1715    }
1716    // `substr` of a blob slices bytes and returns a blob; otherwise it slices
1717    // characters of the text form and returns text.
1718    let blob = matches!(v[0], Value::Blob(_));
1719    let units: alloc::vec::Vec<char> = if blob {
1720        match &v[0] {
1721            Value::Blob(b) => b.iter().map(|&x| x as char).collect(),
1722            _ => unreachable!(),
1723        }
1724    } else {
1725        eval::to_text(&v[0]).chars().collect()
1726    };
1727    // `substr(x, NULL [, …])` returns NULL — a NULL start position propagates
1728    // like any other NULL argument (the length already does, below).
1729    if matches!(v[1], Value::Null) {
1730        return Ok(Value::Null);
1731    }
1732    let len = units.len() as i64;
1733    // Faithful port of SQLite's `substrFunc` (src/func.c): `p1` is a 1-based
1734    // start (negative counts from the end), `p2` a signed length (negative
1735    // means "the |p2| units ending at p1"). The default `p2` for the 2-arg form
1736    // is the LENGTH limit (1e9), which we clamp to `len` below. All arithmetic
1737    // saturates so pathological i64 inputs (e.g. i64::MIN) cannot overflow,
1738    // matching SQLite's behaviour where the window collapses to empty or the
1739    // whole string.
1740    let mut p1 = eval::to_int_value(&v[1]);
1741    let mut p2 = if v.len() == 3 {
1742        // `substr(x, p1, NULL)` returns NULL (the length argument is required
1743        // to be non-NULL to produce a value).
1744        if matches!(v[2], Value::Null) {
1745            return Ok(Value::Null);
1746        }
1747        eval::to_int_value(&v[2])
1748    } else {
1749        1_000_000_000
1750    };
1751    if p1 < 0 {
1752        p1 = p1.saturating_add(len);
1753        if p1 < 0 {
1754            if p2 < 0 {
1755                p2 = 0;
1756            } else {
1757                p2 = p2.saturating_add(p1);
1758            }
1759            p1 = 0;
1760        }
1761    } else if p1 > 0 {
1762        p1 -= 1;
1763    } else if p2 > 0 {
1764        p2 -= 1;
1765    }
1766    if p2 < 0 {
1767        if p2 < -p1 {
1768            p2 = p1;
1769        } else {
1770            p2 = -p2;
1771        }
1772        p1 = p1.saturating_sub(p2);
1773    }
1774    // `p1 >= 0 && p2 >= 0` now holds. Clamp the window to the available units.
1775    let start = (p1.max(0) as usize).min(units.len());
1776    let take = (p2.max(0) as usize).min(units.len() - start);
1777    let slice = &units[start..start + take];
1778    if blob {
1779        Ok(Value::Blob(slice.iter().map(|&c| c as u8).collect()))
1780    } else {
1781        Ok(Value::Text(slice.iter().collect()))
1782    }
1783}
1784
1785fn instr(v: &[Value]) -> Result<Value> {
1786    if v.len() != 2 {
1787        return Err(wrong_arg_count("instr"));
1788    }
1789    if matches!(v[0], Value::Null) || matches!(v[1], Value::Null) {
1790        return Ok(Value::Null);
1791    }
1792    let hay = eval::to_text(&v[0]);
1793    let needle = eval::to_text(&v[1]);
1794    // SQLite returns a 1-based character index, 0 if not found.
1795    match hay.find(&needle) {
1796        None => Ok(Value::Integer(0)),
1797        Some(byte_idx) => {
1798            let char_idx = hay[..byte_idx].chars().count();
1799            Ok(Value::Integer(char_idx as i64 + 1))
1800        }
1801    }
1802}
1803
1804fn replace(v: &[Value]) -> Result<Value> {
1805    if v.len() != 3 {
1806        return Err(wrong_arg_count("replace"));
1807    }
1808    // SQLite short-circuits in a specific order: a NULL subject or a NULL pattern
1809    // yields NULL, but an EMPTY pattern returns the subject (converted to text)
1810    // *before* the replacement argument is examined — so `replace('ab','',NULL)`
1811    // is `'ab'`, not NULL. Checking all three for NULL up front got this wrong.
1812    if matches!(v[0], Value::Null) || matches!(v[1], Value::Null) {
1813        return Ok(Value::Null);
1814    }
1815    let s = c_text(&v[0]);
1816    let from = c_text(&v[1]);
1817    if from.is_empty() {
1818        return Ok(Value::Text(s));
1819    }
1820    if matches!(v[2], Value::Null) {
1821        return Ok(Value::Null);
1822    }
1823    let to = c_text(&v[2]);
1824    Ok(Value::Text(s.replace(&from, &to)))
1825}
1826
1827fn round(v: &[Value]) -> Result<Value> {
1828    if v.is_empty() || v.len() > 2 {
1829        return Err(wrong_arg_count("round"));
1830    }
1831    // A NULL value or NULL precision both yield NULL.
1832    if matches!(v[0], Value::Null) || matches!(v.get(1), Some(Value::Null)) {
1833        return Ok(Value::Null);
1834    }
1835    let x = eval::to_f64(&v[0]);
1836    let digits = if v.len() == 2 {
1837        eval::to_int_value(&v[1]).clamp(0, 30) as u32
1838    } else {
1839        0
1840    };
1841    let r = round_half_away(x, digits);
1842    // SQLite normalises a negative-zero result to positive zero
1843    // (`round(-0.4)` is `0.0`, not `-0.0`).
1844    Ok(Value::Real(if r == 0.0 { 0.0 } else { r }))
1845}
1846
1847/// Round `x` to `n` decimal places, half away from zero, matching SQLite. Instead
1848/// of `round(x * 10^n) / 10^n` — which loses precision (e.g. `2.675 * 100` rounds
1849/// *up* to exactly `267.5` in f64, giving 2.68 where SQLite gives 2.67) — this
1850/// formats `x` to high fixed precision (exposing the true decimal digits) and
1851/// rounds the digit string, so it sees that `2.675` is really `2.67499…`.
1852pub(crate) fn round_half_away(x: f64, n: u32) -> f64 {
1853    if !x.is_finite() || x == 0.0 {
1854        return x;
1855    }
1856    // Values at or beyond 2^52 have no fractional part in f64; return unchanged
1857    // (this also bounds the formatted string length).
1858    if crate::util::float::abs(x) >= 4_503_599_627_370_496.0 {
1859        return x;
1860    }
1861    let neg = x < 0.0;
1862    let ax = crate::util::float::abs(x);
1863    let prec = n as usize + 25;
1864    let s = alloc::format!("{ax:.prec$}");
1865    let dot = s.find('.').unwrap_or(s.len());
1866    let frac = if dot < s.len() { &s[dot + 1..] } else { "" };
1867    // Round up when the first dropped digit (position `n`) is >= 5.
1868    let round_up = frac.as_bytes().get(n as usize).is_some_and(|&d| d >= b'5');
1869    // Kept digits: the integer part followed by the first `n` fractional digits.
1870    let mut digits: alloc::vec::Vec<u8> = alloc::vec::Vec::new();
1871    digits.extend_from_slice(&s.as_bytes()[..dot]);
1872    if n > 0 {
1873        let take = (n as usize).min(frac.len());
1874        digits.extend_from_slice(&frac.as_bytes()[..take]);
1875        // Pad with zeros if the formatting produced fewer than n fraction digits.
1876        digits.resize(dot + n as usize, b'0');
1877    }
1878    if round_up {
1879        let mut i = digits.len();
1880        loop {
1881            if i == 0 {
1882                digits.insert(0, b'1');
1883                break;
1884            }
1885            i -= 1;
1886            if digits[i] == b'9' {
1887                digits[i] = b'0';
1888            } else {
1889                digits[i] += 1;
1890                break;
1891            }
1892        }
1893    }
1894    // Reassemble with the decimal point `n` digits from the right and parse back.
1895    let nn = n as usize;
1896    let s2 = if nn == 0 {
1897        alloc::string::String::from_utf8(digits).unwrap_or_default()
1898    } else {
1899        let point = digits.len() - nn;
1900        let mut out = alloc::string::String::new();
1901        out.push_str(core::str::from_utf8(&digits[..point]).unwrap_or("0"));
1902        out.push('.');
1903        out.push_str(core::str::from_utf8(&digits[point..]).unwrap_or("0"));
1904        out
1905    };
1906    let mag: f64 = s2.parse().unwrap_or(ax);
1907    if neg {
1908        -mag
1909    } else {
1910        mag
1911    }
1912}
1913
1914fn scalar_min_max(v: &[Value], want_min: bool) -> Result<Value> {
1915    // Scalar min()/max() take 2+ args (the 1-arg/`*` forms are aggregates,
1916    // routed elsewhere); 0 args is an error in SQLite.
1917    if v.is_empty() {
1918        let name = if want_min { "min" } else { "max" };
1919        return Err(Error::Error(alloc::format!(
1920            "wrong number of arguments to function {name}()"
1921        )));
1922    }
1923    // NULL if any arg is NULL.
1924    if v.iter().any(|x| matches!(x, Value::Null)) {
1925        return Ok(Value::Null);
1926    }
1927    // Mirror SQLite's `minmaxFunc`: scan left-to-right keeping the best so far.
1928    // For `min`, replace whenever `best >= candidate` (so on a tie the *later*
1929    // argument wins — `min(1.0,1)` is the integer `1`); for `max`, replace only
1930    // when `best < candidate` (so on a tie the *earlier* argument wins —
1931    // `max(1.0,1)` is the real `1.0`). This preserves the storage class of the
1932    // exact argument SQLite would return.
1933    let mut best = v[0].clone();
1934    for x in &v[1..] {
1935        let ord = eval::compare(&best, x);
1936        let take = if want_min {
1937            ord != core::cmp::Ordering::Less
1938        } else {
1939            ord == core::cmp::Ordering::Less
1940        };
1941        if take {
1942            best = x.clone();
1943        }
1944    }
1945    Ok(best)
1946}
1947
1948fn hex_encode(v: &Value) -> String {
1949    let bytes = match v {
1950        Value::Blob(b) => b.clone(),
1951        other => eval::to_text(other).into_bytes(),
1952    };
1953    let mut s = String::with_capacity(bytes.len() * 2);
1954    for b in bytes {
1955        s.push(nibble(b >> 4));
1956        s.push(nibble(b & 0xf));
1957    }
1958    s
1959}
1960
1961fn nibble(n: u8) -> char {
1962    match n {
1963        0..=9 => (b'0' + n) as char,
1964        _ => (b'A' + n - 10) as char,
1965    }
1966}
1967
1968fn char_fn(v: &[Value]) -> Value {
1969    let mut s = String::new();
1970    for x in v {
1971        // SQLite's `charFunc` reads each argument as a code point and substitutes
1972        // U+FFFD for an out-of-range value (negative or > U+10FFFF) rather than
1973        // dropping the character. Surrogates (U+D800..=U+DFFF) cannot be held in
1974        // Rust's UTF-8 `String`, so they too fall back to U+FFFD (SQLite emits
1975        // their raw 3-byte encoding — a faithful match would need invalid UTF-8,
1976        // which this engine's TEXT representation forbids).
1977        let cp = eval::to_int_value(x);
1978        let c = u32::try_from(cp)
1979            .ok()
1980            .and_then(char::from_u32)
1981            .unwrap_or('\u{FFFD}');
1982        s.push(c);
1983    }
1984    Value::Text(s)
1985}