Skip to main content

fsqlite_func/
builtins.rs

1//! Built-in core scalar functions (§13.1).
2//!
3//! Implements 60+ SQLite scalar functions with exact NULL-propagation
4//! semantics. The connection-state helpers `changes()`, `total_changes()`,
5//! and `last_insert_rowid()` are projected through thread-local connection
6//! state. `sqlite_offset()` remains unwired.
7#![allow(
8    clippy::unnecessary_literal_bound,
9    clippy::too_many_lines,
10    clippy::cast_possible_truncation,
11    clippy::cast_possible_wrap,
12    clippy::cast_sign_loss,
13    clippy::fn_params_excessive_bools,
14    clippy::items_after_statements,
15    clippy::match_same_arms,
16    clippy::single_match_else,
17    clippy::manual_let_else,
18    clippy::comparison_chain,
19    clippy::suboptimal_flops,
20    clippy::unnecessary_wraps,
21    clippy::useless_let_if_seq,
22    clippy::redundant_closure_for_method_calls,
23    clippy::manual_ignore_case_cmp
24)]
25
26use std::borrow::Cow;
27use std::fmt::Write as _;
28use std::sync::Arc;
29
30use fsqlite_error::{FrankenError, Result};
31use fsqlite_types::value::{format_sqlite_float, sql_like_cased};
32use fsqlite_types::{SmallText, SqliteValue};
33
34use crate::agg_builtins::register_aggregate_builtins;
35use crate::datetime::register_datetime_builtins;
36use crate::math::register_math_builtins;
37use crate::{FunctionRegistry, ScalarFunction};
38
39// Thread-local storage for connection state that scalar functions need access to.
40// Set by the Connection during DML operations; read by stub functions like
41// last_insert_rowid(), changes(), total_changes().
42thread_local! {
43    static LAST_INSERT_ROWID: std::cell::Cell<i64> = const { std::cell::Cell::new(0) };
44    static LAST_CHANGES: std::cell::Cell<i64> = const { std::cell::Cell::new(0) };
45    static TOTAL_CHANGES: std::cell::Cell<i64> = const { std::cell::Cell::new(0) };
46    /// `PRAGMA case_sensitive_like` for the connection whose statement is
47    /// currently executing on this thread. `false` (the default) folds ASCII
48    /// case in LIKE; `true` makes LIKE byte-exact. Set by the Connection before
49    /// each statement (see `sync_change_tracking_context`); read by `LikeFunc`
50    /// and other LIKE evaluation paths so the pragma never has to be threaded
51    /// through every call site.
52    static CASE_SENSITIVE_LIKE: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
53}
54
55/// Set the active `case_sensitive_like` flag for LIKE evaluation on this thread
56/// (called by the Connection before executing a statement).
57pub fn set_case_sensitive_like(case_sensitive: bool) {
58    CASE_SENSITIVE_LIKE.set(case_sensitive);
59}
60
61/// Read the active `case_sensitive_like` flag for LIKE evaluation on this thread.
62#[must_use]
63pub fn case_sensitive_like_active() -> bool {
64    CASE_SENSITIVE_LIKE.get()
65}
66
67/// Connection-scoped change-tracking state projected into builtin execution context.
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub struct ChangeTrackingState {
70    pub last_insert_rowid: i64,
71    pub last_changes: i64,
72    pub total_changes: i64,
73}
74
75/// Replace the full builtin change-tracking context.
76pub fn set_change_tracking_state(state: ChangeTrackingState) {
77    LAST_INSERT_ROWID.set(state.last_insert_rowid);
78    LAST_CHANGES.set(state.last_changes);
79    TOTAL_CHANGES.set(state.total_changes);
80}
81
82/// Read the current builtin change-tracking context for this thread.
83#[must_use]
84pub fn get_change_tracking_state() -> ChangeTrackingState {
85    ChangeTrackingState {
86        last_insert_rowid: LAST_INSERT_ROWID.get(),
87        last_changes: LAST_CHANGES.get(),
88        total_changes: TOTAL_CHANGES.get(),
89    }
90}
91
92/// Set the last insert rowid (called by Connection after INSERT).
93pub fn set_last_insert_rowid(rowid: i64) {
94    LAST_INSERT_ROWID.set(rowid);
95}
96
97/// Get the current last insert rowid.
98pub fn get_last_insert_rowid() -> i64 {
99    LAST_INSERT_ROWID.get()
100}
101
102/// Set the last changes count (called by Connection after DML).
103///
104/// Also accumulates into the cumulative `total_changes` counter.
105pub fn set_last_changes(count: i64) {
106    LAST_CHANGES.set(count);
107    TOTAL_CHANGES.set(TOTAL_CHANGES.get().saturating_add(count));
108}
109
110/// Get the current last changes count.
111pub fn get_last_changes() -> i64 {
112    LAST_CHANGES.get()
113}
114
115/// Get the cumulative total changes since the connection was opened.
116pub fn get_total_changes() -> i64 {
117    TOTAL_CHANGES.get()
118}
119
120/// Reset the cumulative total changes counter (called on new connection open).
121pub fn reset_total_changes() {
122    TOTAL_CHANGES.set(0);
123}
124
125const SQLITE_COMPILE_OPTIONS: &[&str] = &[
126    "COMPILER=rustc",
127    #[cfg(feature = "ext-fts5")]
128    "ENABLE_FTS5",
129    #[cfg(feature = "ext-geopoly")]
130    "ENABLE_GEOPOLY",
131    #[cfg(feature = "ext-icu")]
132    "ENABLE_ICU",
133    #[cfg(feature = "ext-json")]
134    "ENABLE_JSON1",
135    #[cfg(feature = "ext-rtree")]
136    "ENABLE_RTREE",
137    "FRANKENSQLITE",
138    "OMIT_LOAD_EXTENSION",
139    "THREADSAFE=1",
140];
141
142/// Return the canonical compile-option surface exposed by FrankenSQLite.
143#[must_use]
144pub fn sqlite_compile_options() -> &'static [&'static str] {
145    SQLITE_COMPILE_OPTIONS
146}
147
148fn is_sqlite_compile_option_match(query: &str, option: &str) -> bool {
149    let trimmed = query.trim();
150    let normalized = if trimmed
151        .get(..7)
152        .is_some_and(|prefix| prefix.eq_ignore_ascii_case("SQLITE_"))
153    {
154        &trimmed[7..]
155    } else {
156        trimmed
157    };
158    if normalized.is_empty() {
159        return false;
160    }
161    if option.eq_ignore_ascii_case(normalized) {
162        return true;
163    }
164    option
165        .get(..normalized.len())
166        .is_some_and(|prefix| prefix.eq_ignore_ascii_case(normalized))
167        && option
168            .as_bytes()
169            .get(normalized.len())
170            .is_none_or(|next| !next.is_ascii_alphanumeric() && *next != b'_')
171}
172
173/// Report whether the given SQLite-style compile-option query matches the
174/// current FrankenSQLite build surface.
175#[must_use]
176pub fn sqlite_compileoption_used(query: &str) -> bool {
177    sqlite_compile_options()
178        .iter()
179        .any(|option| is_sqlite_compile_option_match(query, option))
180}
181
182// ── Helpers ───────────────────────────────────────────────────────────────
183
184/// Standard NULL propagation: if any arg is NULL, return NULL.
185fn null_propagate(args: &[SqliteValue]) -> Option<SqliteValue> {
186    if args.iter().any(SqliteValue::is_null) {
187        Some(SqliteValue::Null)
188    } else {
189        None
190    }
191}
192
193// ── abs(X) ────────────────────────────────────────────────────────────────
194
195pub struct AbsFunc;
196
197impl ScalarFunction for AbsFunc {
198    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
199        if args[0].is_null() {
200            return Ok(SqliteValue::Null);
201        }
202        match &args[0] {
203            SqliteValue::Integer(i) => {
204                if *i == i64::MIN {
205                    return Err(FrankenError::IntegerOverflow);
206                }
207                Ok(SqliteValue::Integer(i.abs()))
208            }
209            other => {
210                let f = other.to_float();
211                // Match C SQLite: abs uses `x < 0 ? -x : x`.
212                // IEEE 754: -0.0 < 0.0 is false, so abs(-0.0) == -0.0.
213                Ok(SqliteValue::Float(if f < 0.0 { -f } else { f }))
214            }
215        }
216    }
217
218    fn num_args(&self) -> i32 {
219        1
220    }
221
222    fn name(&self) -> &str {
223        "abs"
224    }
225}
226
227// ── char(X1, X2, ...) ────────────────────────────────────────────────────
228
229pub struct CharFunc;
230
231impl ScalarFunction for CharFunc {
232    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
233        let mut result = String::new();
234        for arg in args {
235            // C SQLite: sqlite3_value_int(NULL) returns 0, so NULL → U+0000.
236            let ch = u32::try_from(arg.to_integer())
237                .ok()
238                .and_then(char::from_u32)
239                .unwrap_or(char::REPLACEMENT_CHARACTER);
240            result.push(ch);
241        }
242        Ok(SqliteValue::Text(SmallText::from_string(result)))
243    }
244
245    fn is_deterministic(&self) -> bool {
246        true
247    }
248
249    fn num_args(&self) -> i32 {
250        -1 // variadic
251    }
252
253    fn name(&self) -> &str {
254        "char"
255    }
256}
257
258// ── coalesce(X, Y, ...) ─────────────────────────────────────────────────
259
260pub struct CoalesceFunc;
261
262impl ScalarFunction for CoalesceFunc {
263    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
264        // Return first non-NULL argument.
265        // NOTE: Real short-circuit evaluation happens at the VDBE level.
266        // At the scalar level, all args are already evaluated.
267        for arg in args {
268            if !arg.is_null() {
269                return Ok(arg.clone());
270            }
271        }
272        Ok(SqliteValue::Null)
273    }
274
275    fn num_args(&self) -> i32 {
276        -1
277    }
278
279    fn min_args(&self) -> i32 {
280        2
281    }
282
283    fn name(&self) -> &str {
284        "coalesce"
285    }
286}
287
288// ── concat(X, Y, ...) ───────────────────────────────────────────────────
289
290pub struct ConcatFunc;
291
292impl ScalarFunction for ConcatFunc {
293    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
294        let mut result = String::new();
295        for arg in args {
296            // concat treats NULL as empty string (unlike ||)
297            if !arg.is_null() {
298                result.push_str(text_arg(arg).as_ref());
299            }
300        }
301        Ok(SqliteValue::Text(SmallText::from_string(result)))
302    }
303
304    fn num_args(&self) -> i32 {
305        -1
306    }
307
308    fn min_args(&self) -> i32 {
309        1
310    }
311
312    fn name(&self) -> &str {
313        "concat"
314    }
315}
316
317// ── concat_ws(SEP, X, Y, ...) ───────────────────────────────────────────
318
319pub struct ConcatWsFunc;
320
321impl ScalarFunction for ConcatWsFunc {
322    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
323        if args.is_empty() {
324            return Ok(SqliteValue::Text(SmallText::new("")));
325        }
326        // C SQLite: concat_ws(NULL, ...) returns NULL when separator is NULL.
327        if args[0].is_null() {
328            return Ok(SqliteValue::Null);
329        }
330        let sep = text_arg(&args[0]);
331        let mut result = String::new();
332        let mut has_part = false;
333        for arg in &args[1..] {
334            // C SQLite skips only NULL value arguments. Empty text is still a
335            // value: `concat_ws('|','','x')` yields `'|x'`.
336            if arg.is_null() {
337                continue;
338            }
339            let part = text_arg(arg);
340            if has_part {
341                result.push_str(sep.as_ref());
342            }
343            result.push_str(part.as_ref());
344            has_part = true;
345        }
346        Ok(SqliteValue::Text(SmallText::from_string(result)))
347    }
348
349    fn num_args(&self) -> i32 {
350        -1
351    }
352
353    fn min_args(&self) -> i32 {
354        2
355    }
356
357    fn name(&self) -> &str {
358        "concat_ws"
359    }
360}
361
362// ── hex(X) ───────────────────────────────────────────────────────────────
363
364pub struct HexFunc;
365
366impl ScalarFunction for HexFunc {
367    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
368        // C SQLite hex() calls sqlite3_value_blob(arg) + sqlite3_value_bytes(arg).
369        // For NULL: blob returns NULL ptr, bytes returns 0, producing "" (empty string).
370        // This has been consistent across all SQLite versions including 3.52.0.
371        if args[0].is_null() {
372            return Ok(SqliteValue::Text(SmallText::new("")));
373        }
374        let bytes: Cow<'_, [u8]> = match &args[0] {
375            SqliteValue::Blob(b) => Cow::Borrowed(b.as_ref()),
376            SqliteValue::Text(text) => Cow::Borrowed(text.as_bytes_direct()),
377            // For non-blob: convert to text first, then hex-encode UTF-8 bytes.
378            other => Cow::Owned(other.to_text().into_bytes()),
379        };
380        let mut hex = String::with_capacity(bytes.len() * 2);
381        for b in bytes.as_ref() {
382            let _ = write!(hex, "{b:02X}");
383        }
384        Ok(SqliteValue::Text(SmallText::from_string(hex)))
385    }
386
387    fn num_args(&self) -> i32 {
388        1
389    }
390
391    fn name(&self) -> &str {
392        "hex"
393    }
394}
395
396// ── ifnull(X, Y) ────────────────────────────────────────────────────────
397
398pub struct IfnullFunc;
399
400impl ScalarFunction for IfnullFunc {
401    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
402        if args[0].is_null() {
403            Ok(args[1].clone())
404        } else {
405            Ok(args[0].clone())
406        }
407    }
408
409    fn num_args(&self) -> i32 {
410        2
411    }
412
413    fn name(&self) -> &str {
414        "ifnull"
415    }
416}
417
418// ── iif(COND, TRUE_VAL, FALSE_VAL) ──────────────────────────────────────
419
420pub struct IifFunc;
421
422impl ScalarFunction for IifFunc {
423    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
424        let cond = &args[0];
425        // C SQLite evaluates IIF condition with sqlite3VdbeRealValue != 0.0,
426        // so 0.5 is truthy (non-zero real).
427        let is_true = match cond {
428            SqliteValue::Null => false,
429            SqliteValue::Integer(n) => *n != 0,
430            SqliteValue::Float(f) => *f != 0.0,
431            SqliteValue::Text(_) | SqliteValue::Blob(_) => {
432                let i = cond.to_integer();
433                if i != 0 { true } else { cond.to_float() != 0.0 }
434            }
435        };
436        if is_true {
437            Ok(args[1].clone())
438        } else {
439            Ok(args[2].clone())
440        }
441    }
442
443    fn num_args(&self) -> i32 {
444        3
445    }
446
447    fn name(&self) -> &str {
448        "iif"
449    }
450}
451
452// ── instr(X, Y) ─────────────────────────────────────────────────────────
453
454pub struct InstrFunc;
455
456impl ScalarFunction for InstrFunc {
457    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
458        if let Some(null) = null_propagate(args) {
459            return Ok(null);
460        }
461        match (&args[0], &args[1]) {
462            (SqliteValue::Blob(haystack), SqliteValue::Blob(needle)) => {
463                // SQLite: empty needle returns 1, empty haystack with non-empty needle returns 0.
464                if needle.is_empty() {
465                    return Ok(SqliteValue::Integer(1));
466                }
467                if haystack.is_empty() {
468                    return Ok(SqliteValue::Integer(0));
469                }
470                let pos = find_bytes(haystack, needle).map_or(0, |p| p + 1);
471                Ok(SqliteValue::Integer(i64::try_from(pos).unwrap_or(0)))
472            }
473            _ => {
474                // Text: character-level search.
475                // SQLite: empty needle returns 1, empty haystack with non-empty needle returns 0.
476                let haystack = text_arg(&args[0]);
477                let needle = text_arg(&args[1]);
478                let haystack = haystack.as_ref();
479                let needle = needle.as_ref();
480                if needle.is_empty() {
481                    return Ok(SqliteValue::Integer(1));
482                }
483                if haystack.is_empty() {
484                    return Ok(SqliteValue::Integer(0));
485                }
486                let pos = haystack
487                    .find(needle)
488                    .map_or(0, |byte_pos| haystack[..byte_pos].chars().count() + 1);
489                Ok(SqliteValue::Integer(i64::try_from(pos).unwrap_or(0)))
490            }
491        }
492    }
493
494    fn num_args(&self) -> i32 {
495        2
496    }
497
498    fn name(&self) -> &str {
499        "instr"
500    }
501}
502
503fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option<usize> {
504    if needle.is_empty() {
505        return Some(0);
506    }
507    haystack.windows(needle.len()).position(|w| w == needle)
508}
509
510fn sqlite_text_until_nul(text: &str) -> &str {
511    text.split_once('\0').map_or(text, |(prefix, _)| prefix)
512}
513
514// ── length(X) ────────────────────────────────────────────────────────────
515
516pub struct LengthFunc;
517
518impl ScalarFunction for LengthFunc {
519    #[allow(clippy::cast_possible_wrap)]
520    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
521        if args[0].is_null() {
522            return Ok(SqliteValue::Null);
523        }
524        let len = match &args[0] {
525            SqliteValue::Text(s) => {
526                let text = sqlite_text_until_nul(s.as_str());
527                if text.is_ascii() {
528                    text.len()
529                } else {
530                    text.chars().count()
531                }
532            }
533            SqliteValue::Blob(b) => b.len(),
534            other => {
535                // Numbers: length of text representation.
536                let text = other.to_text();
537                let text = sqlite_text_until_nul(&text);
538                if text.is_ascii() {
539                    text.len()
540                } else {
541                    text.chars().count()
542                }
543            }
544        };
545        Ok(SqliteValue::Integer(len as i64))
546    }
547
548    fn num_args(&self) -> i32 {
549        1
550    }
551
552    fn name(&self) -> &str {
553        "length"
554    }
555}
556
557// ── octet_length(X) ─────────────────────────────────────────────────────
558
559pub struct OctetLengthFunc;
560
561impl ScalarFunction for OctetLengthFunc {
562    #[allow(clippy::cast_possible_wrap)]
563    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
564        if args[0].is_null() {
565            return Ok(SqliteValue::Null);
566        }
567        let len = match &args[0] {
568            SqliteValue::Text(s) => s.len(),
569            SqliteValue::Blob(b) => b.len(),
570            other => other.to_text().len(),
571        };
572        Ok(SqliteValue::Integer(len as i64))
573    }
574
575    fn num_args(&self) -> i32 {
576        1
577    }
578
579    fn name(&self) -> &str {
580        "octet_length"
581    }
582}
583
584// ── lower(X) / upper(X) ─────────────────────────────────────────────────
585
586pub struct LowerFunc;
587
588impl ScalarFunction for LowerFunc {
589    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
590        if args[0].is_null() {
591            return Ok(SqliteValue::Null);
592        }
593        let lowered = text_arg(&args[0]).as_ref().to_ascii_lowercase();
594        Ok(SqliteValue::Text(SmallText::from_string(lowered)))
595    }
596
597    fn num_args(&self) -> i32 {
598        1
599    }
600
601    fn name(&self) -> &str {
602        "lower"
603    }
604}
605
606pub struct UpperFunc;
607
608impl ScalarFunction for UpperFunc {
609    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
610        if args[0].is_null() {
611            return Ok(SqliteValue::Null);
612        }
613        let upper = text_arg(&args[0]).as_ref().to_ascii_uppercase();
614        Ok(SqliteValue::Text(SmallText::from_string(upper)))
615    }
616
617    fn num_args(&self) -> i32 {
618        1
619    }
620
621    fn name(&self) -> &str {
622        "upper"
623    }
624}
625
626// ── trim/ltrim/rtrim ────────────────────────────────────────────────────
627
628pub struct TrimFunc;
629pub struct LtrimFunc;
630pub struct RtrimFunc;
631
632fn trim_chars(s: &str, chars: &str) -> String {
633    let char_set: Vec<char> = chars.chars().collect();
634    s.trim_matches(|c: char| char_set.contains(&c)).to_owned()
635}
636
637fn ltrim_chars(s: &str, chars: &str) -> String {
638    let char_set: Vec<char> = chars.chars().collect();
639    s.trim_start_matches(|c: char| char_set.contains(&c))
640        .to_owned()
641}
642
643fn rtrim_chars(s: &str, chars: &str) -> String {
644    let char_set: Vec<char> = chars.chars().collect();
645    s.trim_end_matches(|c: char| char_set.contains(&c))
646        .to_owned()
647}
648
649impl ScalarFunction for TrimFunc {
650    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
651        if args[0].is_null() {
652            return Ok(SqliteValue::Null);
653        }
654        let s = text_arg(&args[0]);
655        let chars = if args.len() > 1 && !args[1].is_null() {
656            text_arg(&args[1])
657        } else {
658            Cow::Borrowed(" ")
659        };
660        Ok(SqliteValue::Text(SmallText::new(
661            trim_chars(s.as_ref(), chars.as_ref()).as_str(),
662        )))
663    }
664
665    fn num_args(&self) -> i32 {
666        -1 // 1 or 2 args
667    }
668
669    fn min_args(&self) -> i32 {
670        1
671    }
672
673    fn max_args(&self) -> Option<i32> {
674        Some(2)
675    }
676
677    fn name(&self) -> &str {
678        "trim"
679    }
680}
681
682impl ScalarFunction for LtrimFunc {
683    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
684        if args[0].is_null() {
685            return Ok(SqliteValue::Null);
686        }
687        let s = text_arg(&args[0]);
688        let chars = if args.len() > 1 && !args[1].is_null() {
689            text_arg(&args[1])
690        } else {
691            Cow::Borrowed(" ")
692        };
693        Ok(SqliteValue::Text(SmallText::new(
694            ltrim_chars(s.as_ref(), chars.as_ref()).as_str(),
695        )))
696    }
697
698    fn num_args(&self) -> i32 {
699        -1
700    }
701
702    fn min_args(&self) -> i32 {
703        1
704    }
705
706    fn max_args(&self) -> Option<i32> {
707        Some(2)
708    }
709
710    fn name(&self) -> &str {
711        "ltrim"
712    }
713}
714
715impl ScalarFunction for RtrimFunc {
716    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
717        if args[0].is_null() {
718            return Ok(SqliteValue::Null);
719        }
720        let s = text_arg(&args[0]);
721        let chars = if args.len() > 1 && !args[1].is_null() {
722            text_arg(&args[1])
723        } else {
724            Cow::Borrowed(" ")
725        };
726        Ok(SqliteValue::Text(SmallText::new(
727            rtrim_chars(s.as_ref(), chars.as_ref()).as_str(),
728        )))
729    }
730
731    fn num_args(&self) -> i32 {
732        -1
733    }
734
735    fn min_args(&self) -> i32 {
736        1
737    }
738
739    fn max_args(&self) -> Option<i32> {
740        Some(2)
741    }
742
743    fn name(&self) -> &str {
744        "rtrim"
745    }
746}
747
748// ── nullif(X, Y) ────────────────────────────────────────────────────────
749
750pub struct NullifFunc;
751
752impl ScalarFunction for NullifFunc {
753    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
754        if args[0] == args[1] {
755            Ok(SqliteValue::Null)
756        } else {
757            Ok(args[0].clone())
758        }
759    }
760
761    fn num_args(&self) -> i32 {
762        2
763    }
764
765    fn name(&self) -> &str {
766        "nullif"
767    }
768}
769
770// ── typeof(X) ────────────────────────────────────────────────────────────
771
772pub struct TypeofFunc;
773
774impl ScalarFunction for TypeofFunc {
775    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
776        let type_name = match &args[0] {
777            SqliteValue::Null => "null",
778            SqliteValue::Integer(_) => "integer",
779            SqliteValue::Float(_) => "real",
780            SqliteValue::Text(_) => "text",
781            SqliteValue::Blob(_) => "blob",
782        };
783        Ok(SqliteValue::Text(SmallText::new(type_name)))
784    }
785
786    fn num_args(&self) -> i32 {
787        1
788    }
789
790    fn name(&self) -> &str {
791        "typeof"
792    }
793}
794
795// ── subtype(X) ───────────────────────────────────────────────────────────
796
797pub struct SubtypeFunc;
798
799impl ScalarFunction for SubtypeFunc {
800    fn invoke(&self, _args: &[SqliteValue]) -> Result<SqliteValue> {
801        // subtype(NULL) = 0 (does NOT propagate NULL)
802        // Without subtype tags in SqliteValue, always return 0.
803        Ok(SqliteValue::Integer(0))
804    }
805
806    fn num_args(&self) -> i32 {
807        1
808    }
809
810    fn name(&self) -> &str {
811        "subtype"
812    }
813}
814
815// ── replace(X, Y, Z) ────────────────────────────────────────────────────
816
817pub struct ReplaceFunc;
818
819impl ScalarFunction for ReplaceFunc {
820    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
821        if let Some(null) = null_propagate(args) {
822            return Ok(null);
823        }
824        let x = text_arg(&args[0]);
825        let y = text_arg(&args[1]);
826        let z = text_arg(&args[2]);
827        if y.is_empty() {
828            return Ok(SqliteValue::Text(SmallText::from_string(x)));
829        }
830
831        // Prevent OOM from massive string expansion
832        if z.len() > y.len() {
833            let occurrences = x.matches(y.as_ref()).count();
834            let final_len = x.len() + occurrences * (z.len() - y.len());
835            if final_len > 1_000_000_000 {
836                return Err(FrankenError::TooBig);
837            }
838        }
839
840        Ok(SqliteValue::Text(SmallText::from_string(
841            x.replace(y.as_ref(), z.as_ref()),
842        )))
843    }
844
845    fn num_args(&self) -> i32 {
846        3
847    }
848
849    fn name(&self) -> &str {
850        "replace"
851    }
852}
853
854// ── round(X [, N]) ──────────────────────────────────────────────────────
855
856pub struct RoundFunc;
857
858impl ScalarFunction for RoundFunc {
859    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
860        if args[0].is_null() {
861            return Ok(SqliteValue::Null);
862        }
863        // C SQLite: a NULL precision argument makes the whole call NULL
864        // (`round(123.4, NULL)` → NULL), not a default of 0.
865        if args.len() > 1 && args[1].is_null() {
866            return Ok(SqliteValue::Null);
867        }
868        let x = args[0].to_float();
869        // Clamp N to [0, 30] matching SQLite behavior.
870        let n = if args.len() > 1 {
871            args[1].to_integer().clamp(0, 30)
872        } else {
873            0
874        };
875        // Values beyond 2^52 have no fractional part — return unchanged
876        if !(-4_503_599_627_370_496.0..=4_503_599_627_370_496.0).contains(&x) {
877            return Ok(SqliteValue::Float(x));
878        }
879        // SQLite uses "round half away from zero" via its custom printf.
880        // Rust's format! uses "round half to even" (IEEE 754 default).
881        // They agree on all cases except exact ties (digit at n+1 is
882        // precisely 5 with no further non-zero digits). For ties, we
883        // detect and adjust to match SQLite.
884        #[allow(clippy::cast_possible_truncation)]
885        let rounded = {
886            let prec = (n as usize) + 15;
887            let full = format!("{x:.prec$}");
888            let dot = full.find('.').unwrap_or(full.len());
889            let rd_idx = dot + 1 + n as usize;
890            if rd_idx >= full.len() {
891                format!("{x:.prec$}", prec = n as usize)
892                    .parse::<f64>()
893                    .unwrap_or(x)
894            } else {
895                let rd = full.as_bytes()[rd_idx] - b'0';
896                if rd != 5 || !full[rd_idx + 1..].bytes().all(|b| b == b'0') {
897                    // Not an exact tie — format!'s default rounding is correct
898                    format!("{x:.prec$}", prec = n as usize)
899                        .parse::<f64>()
900                        .unwrap_or(x)
901                } else {
902                    // Exact tie — round half away from zero by incrementing
903                    // the truncated string's last digit.
904                    let mut trunc = full.as_bytes()[..rd_idx].to_vec();
905                    // Strip trailing '.' for n==0
906                    if trunc.last() == Some(&b'.') {
907                        trunc.pop();
908                    }
909                    let start = usize::from(trunc.first() == Some(&b'-'));
910                    let mut carry = true;
911                    for b in trunc[start..].iter_mut().rev() {
912                        if *b == b'.' {
913                            continue;
914                        }
915                        if carry {
916                            if *b == b'9' {
917                                *b = b'0';
918                            } else {
919                                *b += 1;
920                                carry = false;
921                                break;
922                            }
923                        }
924                    }
925                    if carry {
926                        trunc.insert(start, b'1');
927                    }
928                    String::from_utf8(trunc)
929                        .ok()
930                        .and_then(|s| s.parse::<f64>().ok())
931                        .unwrap_or(x)
932                }
933            }
934        };
935        Ok(SqliteValue::Float(rounded))
936    }
937
938    fn num_args(&self) -> i32 {
939        -1 // 1 or 2 args
940    }
941
942    fn min_args(&self) -> i32 {
943        1
944    }
945
946    fn max_args(&self) -> Option<i32> {
947        Some(2)
948    }
949
950    fn name(&self) -> &str {
951        "round"
952    }
953}
954
955// ── sign(X) ──────────────────────────────────────────────────────────────
956
957pub struct SignFunc;
958
959impl ScalarFunction for SignFunc {
960    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
961        if args[0].is_null() {
962            return Ok(SqliteValue::Null);
963        }
964        match &args[0] {
965            SqliteValue::Null => Ok(SqliteValue::Null),
966            SqliteValue::Integer(i) => Ok(SqliteValue::Integer(i.signum())),
967            SqliteValue::Float(f) => {
968                if f.is_nan() {
969                    Ok(SqliteValue::Null)
970                } else if *f > 0.0 {
971                    Ok(SqliteValue::Integer(1))
972                } else if *f < 0.0 {
973                    Ok(SqliteValue::Integer(-1))
974                } else {
975                    Ok(SqliteValue::Integer(0))
976                }
977            }
978            SqliteValue::Text(s) => {
979                // C SQLite sign() uses sqlite3AtoF — returns NULL for non-numeric text.
980                let trimmed = s.trim_matches(|ch: char| ch.is_ascii_whitespace());
981                if trimmed.is_empty() {
982                    return Ok(SqliteValue::Null);
983                }
984
985                // Reject literal NaN/inf/infinity keywords (case-insensitive,
986                // with optional leading sign). Rust's f64::parse accepts these
987                // but C SQLite's sqlite3AtoF does not. Note: numeric overflow
988                // strings like "1e999" that parse to infinity ARE valid — C
989                // SQLite recognises those as numeric and sign() returns 1/-1.
990                let stripped = trimmed.strip_prefix(['+', '-']).unwrap_or(trimmed);
991                if stripped.eq_ignore_ascii_case("nan")
992                    || stripped.eq_ignore_ascii_case("inf")
993                    || stripped.eq_ignore_ascii_case("infinity")
994                {
995                    return Ok(SqliteValue::Null);
996                }
997
998                // Try parsing as a number. If the string isn't a valid numeric
999                // representation, return NULL (matching C SQLite behavior).
1000                if let Ok(f) = trimmed.parse::<f64>() {
1001                    // Use the already-parsed value (avoids a redundant double-parse).
1002                    if f > 0.0 {
1003                        Ok(SqliteValue::Integer(1))
1004                    } else if f < 0.0 {
1005                        Ok(SqliteValue::Integer(-1))
1006                    } else {
1007                        Ok(SqliteValue::Integer(0))
1008                    }
1009                } else if let Ok(i) = trimmed.parse::<i64>() {
1010                    // Handles integers that f64 can't represent exactly but i64 can.
1011                    Ok(SqliteValue::Integer(i.signum()))
1012                } else {
1013                    Ok(SqliteValue::Null)
1014                }
1015            }
1016            SqliteValue::Blob(_) => Ok(SqliteValue::Null),
1017        }
1018    }
1019
1020    fn num_args(&self) -> i32 {
1021        1
1022    }
1023
1024    fn name(&self) -> &str {
1025        "sign"
1026    }
1027}
1028
1029// ── random() ─────────────────────────────────────────────────────────────
1030
1031pub struct RandomFunc;
1032
1033impl ScalarFunction for RandomFunc {
1034    fn invoke(&self, _args: &[SqliteValue]) -> Result<SqliteValue> {
1035        // Simple PRNG using thread_rng is fine for SQLite's random()
1036        // which is explicitly non-cryptographic.
1037        let val = simple_random_i64();
1038        Ok(SqliteValue::Integer(val))
1039    }
1040
1041    fn is_deterministic(&self) -> bool {
1042        false
1043    }
1044
1045    fn num_args(&self) -> i32 {
1046        0
1047    }
1048
1049    fn name(&self) -> &str {
1050        "random"
1051    }
1052}
1053
1054/// Simple deterministic-enough PRNG for SQLite's random().
1055fn simple_random_i64() -> i64 {
1056    // Deterministic per-process PRNG (no ambient authority).
1057    // Not cryptographic, matching SQLite's random()/randomblob() semantics.
1058    //
1059    // splitmix64: fast, decent statistical properties, and requires only a u64 state.
1060    use std::sync::atomic::{AtomicU64, Ordering};
1061
1062    static STATE: AtomicU64 = AtomicU64::new(0xD1B5_4A32_D192_ED03);
1063    let mut x = STATE.fetch_add(0x9E37_79B9_7F4A_7C15, Ordering::Relaxed);
1064    x ^= x >> 30;
1065    x = x.wrapping_mul(0xBF58_476D_1CE4_E5B9);
1066    x ^= x >> 27;
1067    x = x.wrapping_mul(0x94D0_49BB_1331_11EB);
1068    x ^= x >> 31;
1069    x as i64
1070}
1071
1072// ── randomblob(N) ────────────────────────────────────────────────────────
1073
1074pub struct RandomblobFunc;
1075
1076impl ScalarFunction for RandomblobFunc {
1077    #[allow(clippy::cast_sign_loss)]
1078    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1079        // C SQLite returns a one-byte blob for NULL and for all lengths below
1080        // one. `zeroblob()` uses different empty-blob semantics, so keep this
1081        // rule local to randomblob().
1082        let n_i64 = if args[0].is_null() {
1083            1
1084        } else {
1085            args[0].to_integer().max(1)
1086        };
1087        if n_i64 > 1_000_000_000 {
1088            return Err(FrankenError::TooBig);
1089        }
1090        let n = n_i64 as usize;
1091        let mut buf = vec![0u8; n];
1092        let mut i = 0;
1093        while i < n {
1094            let rnd = simple_random_i64().to_ne_bytes();
1095            let to_copy = (n - i).min(8);
1096            buf[i..i + to_copy].copy_from_slice(&rnd[..to_copy]);
1097            i += to_copy;
1098        }
1099        Ok(SqliteValue::Blob(Arc::from(buf.as_slice())))
1100    }
1101
1102    fn is_deterministic(&self) -> bool {
1103        false
1104    }
1105
1106    fn num_args(&self) -> i32 {
1107        1
1108    }
1109
1110    fn name(&self) -> &str {
1111        "randomblob"
1112    }
1113}
1114
1115// ── zeroblob(N) ──────────────────────────────────────────────────────────
1116
1117pub struct ZeroblobFunc;
1118
1119impl ScalarFunction for ZeroblobFunc {
1120    #[allow(clippy::cast_sign_loss)]
1121    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1122        // C SQLite: zeroblob(NULL) returns x'' (empty blob), not NULL.
1123        if args[0].is_null() {
1124            return Ok(SqliteValue::Blob(Arc::from([] as [u8; 0])));
1125        }
1126        let n_i64 = args[0].to_integer().max(0);
1127        if n_i64 > 1_000_000_000 {
1128            return Err(FrankenError::TooBig);
1129        }
1130        let n = n_i64 as usize;
1131        Ok(SqliteValue::Blob(Arc::from(vec![0u8; n].as_slice())))
1132    }
1133
1134    fn num_args(&self) -> i32 {
1135        1
1136    }
1137
1138    fn name(&self) -> &str {
1139        "zeroblob"
1140    }
1141}
1142
1143// ── quote(X) ─────────────────────────────────────────────────────────────
1144
1145pub struct QuoteFunc;
1146
1147impl ScalarFunction for QuoteFunc {
1148    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1149        let result = quote_sql_value(&args[0], false);
1150        Ok(SqliteValue::Text(SmallText::from_string(result)))
1151    }
1152
1153    fn num_args(&self) -> i32 {
1154        1
1155    }
1156
1157    fn name(&self) -> &str {
1158        "quote"
1159    }
1160}
1161
1162// ── unistr_quote(X) ───────────────────────────────────────────────────────
1163
1164pub struct UnistrQuoteFunc;
1165
1166impl ScalarFunction for UnistrQuoteFunc {
1167    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1168        let result = quote_sql_value(&args[0], true);
1169        Ok(SqliteValue::Text(SmallText::from_string(result)))
1170    }
1171
1172    fn num_args(&self) -> i32 {
1173        1
1174    }
1175
1176    fn name(&self) -> &str {
1177        "unistr_quote"
1178    }
1179}
1180
1181fn quote_sql_value(value: &SqliteValue, use_unistr_quote: bool) -> String {
1182    match value {
1183        SqliteValue::Null => "NULL".to_owned(),
1184        SqliteValue::Integer(i) => i.to_string(),
1185        SqliteValue::Float(f) => format_sqlite_float(*f),
1186        SqliteValue::Text(s) => quote_sql_text_literal(s.as_str(), use_unistr_quote),
1187        SqliteValue::Blob(b) => {
1188            let mut hex = String::with_capacity(3 + b.len() * 2);
1189            hex.push_str("X'");
1190            for byte in b.iter() {
1191                let _ = write!(hex, "{byte:02X}");
1192            }
1193            hex.push('\'');
1194            hex
1195        }
1196    }
1197}
1198
1199fn quote_sql_text_literal(text: &str, use_unistr_quote: bool) -> String {
1200    let text = sqlite_text_until_nul(text);
1201    if use_unistr_quote && text.chars().any(is_unistr_control_char) {
1202        return unistr_quote_sql_text_literal(text);
1203    }
1204
1205    let mut quoted = String::with_capacity(text.len() + 2);
1206    quoted.push('\'');
1207    append_sql_string_literal_body(&mut quoted, text);
1208    quoted.push('\'');
1209    quoted
1210}
1211
1212fn unistr_quote_sql_text_literal(text: &str) -> String {
1213    let mut quoted = String::with_capacity(text.len() + 12);
1214    quoted.push_str("unistr('");
1215    for ch in text.chars() {
1216        match ch {
1217            '\'' => quoted.push_str("''"),
1218            '\\' => quoted.push_str("\\\\"),
1219            _ if is_unistr_control_char(ch) => {
1220                let _ = write!(quoted, "\\u{:04x}", ch as u32);
1221            }
1222            _ => quoted.push(ch),
1223        }
1224    }
1225    quoted.push_str("')");
1226    quoted
1227}
1228
1229fn append_sql_string_literal_body(out: &mut String, text: &str) {
1230    for ch in text.chars() {
1231        if ch == '\'' {
1232            out.push_str("''");
1233        } else {
1234            out.push(ch);
1235        }
1236    }
1237}
1238
1239fn is_unistr_control_char(ch: char) -> bool {
1240    matches!(ch, '\u{0001}'..='\u{001F}')
1241}
1242
1243// ── unhex(X [, Y]) ──────────────────────────────────────────────────────
1244
1245pub struct UnhexFunc;
1246
1247impl ScalarFunction for UnhexFunc {
1248    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1249        if args[0].is_null() {
1250            return Ok(SqliteValue::Null);
1251        }
1252        if args.len() > 1 && args[1].is_null() {
1253            return Ok(SqliteValue::Null);
1254        }
1255        let input = text_arg(&args[0]);
1256        let ignore_chars: Vec<char> = if args.len() > 1 {
1257            text_arg(&args[1])
1258                .chars()
1259                .filter(|&c| hex_digit(c).is_none())
1260                .collect()
1261        } else {
1262            Vec::new()
1263        };
1264
1265        let mut bytes = Vec::with_capacity(input.len() / 2);
1266        let mut hi_nibble = None;
1267        for c in input.as_ref().chars() {
1268            if ignore_chars.contains(&c) {
1269                if hi_nibble.is_some() {
1270                    return Ok(SqliteValue::Null);
1271                }
1272                continue;
1273            }
1274            let digit = match hex_digit(c) {
1275                Some(v) => v,
1276                None => return Ok(SqliteValue::Null),
1277            };
1278            if let Some(hi) = hi_nibble.take() {
1279                bytes.push(hi << 4 | digit);
1280            } else {
1281                hi_nibble = Some(digit);
1282            }
1283        }
1284        if hi_nibble.is_some() {
1285            return Ok(SqliteValue::Null);
1286        }
1287        Ok(SqliteValue::Blob(Arc::from(bytes.as_slice())))
1288    }
1289
1290    fn num_args(&self) -> i32 {
1291        -1 // 1 or 2 args
1292    }
1293
1294    fn min_args(&self) -> i32 {
1295        1
1296    }
1297
1298    fn max_args(&self) -> Option<i32> {
1299        Some(2)
1300    }
1301
1302    fn name(&self) -> &str {
1303        "unhex"
1304    }
1305}
1306
1307fn hex_digit(c: char) -> Option<u8> {
1308    match c {
1309        '0'..='9' => Some(c as u8 - b'0'),
1310        'a'..='f' => Some(c as u8 - b'a' + 10),
1311        'A'..='F' => Some(c as u8 - b'A' + 10),
1312        _ => None,
1313    }
1314}
1315
1316// ── unicode(X) ───────────────────────────────────────────────────────────
1317
1318pub struct UnicodeFunc;
1319
1320impl ScalarFunction for UnicodeFunc {
1321    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1322        if args[0].is_null() {
1323            return Ok(SqliteValue::Null);
1324        }
1325        if let SqliteValue::Blob(bytes) = &args[0] {
1326            return Ok(
1327                sqlite_blob_first_codepoint(bytes).map_or(SqliteValue::Null, SqliteValue::Integer)
1328            );
1329        }
1330        let s = text_arg(&args[0]);
1331        match sqlite_text_until_nul(s.as_ref()).chars().next() {
1332            Some(c) => Ok(SqliteValue::Integer(i64::from(c as u32))),
1333            None => Ok(SqliteValue::Null),
1334        }
1335    }
1336
1337    fn num_args(&self) -> i32 {
1338        1
1339    }
1340
1341    fn name(&self) -> &str {
1342        "unicode"
1343    }
1344}
1345
1346fn sqlite_blob_first_codepoint(bytes: &[u8]) -> Option<i64> {
1347    let first = *bytes.first()?;
1348    if first == 0 {
1349        return None;
1350    }
1351    let mut codepoint = match first {
1352        0x00..=0xBF => u32::from(first),
1353        0xC0..=0xDF => u32::from(first & 0x1F),
1354        0xE0..=0xEF => u32::from(first & 0x0F),
1355        0xF0..=0xF7 => u32::from(first & 0x07),
1356        _ => 0xFFFD,
1357    };
1358
1359    if first >= 0xC0 && first <= 0xF7 {
1360        for byte in bytes
1361            .iter()
1362            .copied()
1363            .skip(1)
1364            .take_while(|byte| byte & 0xC0 == 0x80)
1365        {
1366            codepoint = codepoint
1367                .wrapping_shl(6)
1368                .wrapping_add(u32::from(byte & 0x3F));
1369        }
1370        if codepoint < 0x80
1371            || (codepoint & 0xFFFF_F800) == 0xD800
1372            || (codepoint & 0xFFFF_FFFE) == 0xFFFE
1373        {
1374            codepoint = 0xFFFD;
1375        }
1376    }
1377
1378    Some(i64::from(codepoint))
1379}
1380
1381// ── substr(X, START [, LENGTH]) / substring() ───────────────────────────
1382
1383pub struct SubstrFunc;
1384
1385impl ScalarFunction for SubstrFunc {
1386    #[allow(clippy::cast_sign_loss, clippy::cast_possible_wrap)]
1387    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1388        if args[0].is_null() || args[1].is_null() {
1389            return Ok(SqliteValue::Null);
1390        }
1391        let is_blob = matches!(&args[0], SqliteValue::Blob(_));
1392        if is_blob {
1393            return self.invoke_blob(args);
1394        }
1395
1396        let text = text_arg(&args[0]);
1397        let s = text.as_ref();
1398        let ascii_fast_path = s.is_ascii();
1399        let len = if ascii_fast_path {
1400            s.len() as i64
1401        } else {
1402            s.chars().count() as i64
1403        };
1404        let has_length = args.len() > 2 && !args[2].is_null();
1405
1406        let mut p1 = args[1].to_integer();
1407        let mut p2 = if has_length {
1408            args[2].to_integer()
1409        } else {
1410            1_000_000_000
1411        };
1412
1413        // Match C SQLite's 2-phase substr algorithm exactly:
1414        // Phase 1: remember if length was negative, make it positive
1415        // Use saturating_neg to avoid panic on i64::MIN.
1416        let neg_p2 = p2 < 0;
1417        if neg_p2 {
1418            p2 = p2.saturating_neg();
1419        }
1420
1421        // Phase 2: resolve start position (1-based to 0-based)
1422        if p1 < 0 {
1423            p1 = p1.saturating_add(len);
1424            if p1 < 0 {
1425                p2 = p2.saturating_add(p1);
1426                p1 = 0;
1427            }
1428        } else if p1 > 0 {
1429            p1 -= 1;
1430        } else if p2 > 0 {
1431            p2 -= 1; // start=0 quirk
1432        }
1433
1434        // Phase 3: apply negative-length shift (move start backward)
1435        if neg_p2 {
1436            p1 = p1.saturating_sub(p2);
1437            if p1 < 0 {
1438                p2 = p2.saturating_add(p1);
1439                p1 = 0;
1440            }
1441        }
1442
1443        if p1.saturating_add(p2) > len {
1444            p2 = len.saturating_sub(p1);
1445        }
1446        if p2 <= 0 {
1447            return Ok(SqliteValue::Text(SmallText::new("")));
1448        }
1449
1450        if ascii_fast_path {
1451            let start = p1 as usize;
1452            let end = (p1 + p2) as usize;
1453            return Ok(SqliteValue::Text(SmallText::new(&s[start..end])));
1454        }
1455
1456        let chars: Vec<char> = s.chars().collect();
1457        let result: String = chars[p1 as usize..(p1 + p2) as usize].iter().collect();
1458        Ok(SqliteValue::Text(SmallText::from_string(result)))
1459    }
1460
1461    fn num_args(&self) -> i32 {
1462        -1 // 2 or 3 args
1463    }
1464
1465    fn min_args(&self) -> i32 {
1466        2
1467    }
1468
1469    fn max_args(&self) -> Option<i32> {
1470        Some(3)
1471    }
1472
1473    fn name(&self) -> &str {
1474        "substr"
1475    }
1476}
1477
1478impl SubstrFunc {
1479    #[allow(
1480        clippy::unused_self,
1481        clippy::cast_sign_loss,
1482        clippy::cast_possible_wrap
1483    )]
1484    fn invoke_blob(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1485        let blob = match &args[0] {
1486            SqliteValue::Blob(b) => b,
1487            _ => return Ok(SqliteValue::Null),
1488        };
1489        let len = blob.len() as i64;
1490        let has_length = args.len() > 2 && !args[2].is_null();
1491
1492        let mut p1 = args[1].to_integer();
1493        let mut p2 = if has_length {
1494            args[2].to_integer()
1495        } else {
1496            1_000_000_000
1497        };
1498
1499        let neg_p2 = p2 < 0;
1500        if neg_p2 {
1501            p2 = p2.saturating_neg();
1502        }
1503
1504        if p1 < 0 {
1505            p1 = p1.saturating_add(len);
1506            if p1 < 0 {
1507                p2 = p2.saturating_add(p1);
1508                p1 = 0;
1509            }
1510        } else if p1 > 0 {
1511            p1 -= 1;
1512        } else if p2 > 0 {
1513            p2 -= 1;
1514        }
1515
1516        if neg_p2 {
1517            p1 = p1.saturating_sub(p2);
1518            if p1 < 0 {
1519                p2 = p2.saturating_add(p1);
1520                p1 = 0;
1521            }
1522        }
1523
1524        if p1.saturating_add(p2) > len {
1525            p2 = len.saturating_sub(p1);
1526        }
1527        if p2 <= 0 {
1528            return Ok(SqliteValue::Blob(Arc::from([] as [u8; 0])));
1529        }
1530
1531        Ok(SqliteValue::Blob(Arc::from(
1532            &blob[p1 as usize..(p1 + p2) as usize],
1533        )))
1534    }
1535}
1536
1537// ── soundex(X) ───────────────────────────────────────────────────────────
1538
1539pub struct SoundexFunc;
1540
1541impl ScalarFunction for SoundexFunc {
1542    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1543        if args[0].is_null() {
1544            // SQLite returns "?000" for SOUNDEX(NULL), not NULL.
1545            return Ok(SqliteValue::Text(SmallText::new("?000")));
1546        }
1547        let s = text_arg(&args[0]);
1548        Ok(SqliteValue::Text(SmallText::from_string(soundex(
1549            s.as_ref(),
1550        ))))
1551    }
1552
1553    fn num_args(&self) -> i32 {
1554        1
1555    }
1556
1557    fn name(&self) -> &str {
1558        "soundex"
1559    }
1560}
1561
1562fn soundex(s: &str) -> String {
1563    let mut chars = s.chars().filter(|c| c.is_ascii_alphabetic());
1564    let first = match chars.next() {
1565        Some(c) => c.to_ascii_uppercase(),
1566        None => return "?000".to_owned(),
1567    };
1568
1569    let code = |c: char| -> Option<char> {
1570        match c.to_ascii_uppercase() {
1571            'B' | 'F' | 'P' | 'V' => Some('1'),
1572            'C' | 'G' | 'J' | 'K' | 'Q' | 'S' | 'X' | 'Z' => Some('2'),
1573            'D' | 'T' => Some('3'),
1574            'L' => Some('4'),
1575            'M' | 'N' => Some('5'),
1576            'R' => Some('6'),
1577            _ => None, // A, E, I, O, U, H, W, Y
1578        }
1579    };
1580
1581    let mut result = String::with_capacity(4);
1582    result.push(first);
1583    let mut last_code = code(first);
1584
1585    for c in chars {
1586        if result.len() >= 4 {
1587            break;
1588        }
1589        let current = code(c);
1590        if let Some(digit) = current {
1591            if current != last_code {
1592                result.push(digit);
1593            }
1594        }
1595        last_code = current;
1596    }
1597
1598    while result.len() < 4 {
1599        result.push('0');
1600    }
1601    result
1602}
1603
1604// ── scalar max(X, Y, ...) ───────────────────────────────────────────────
1605
1606pub struct ScalarMaxFunc;
1607
1608impl ScalarFunction for ScalarMaxFunc {
1609    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1610        // Scalar max: if ANY argument is NULL, returns NULL
1611        if let Some(null) = null_propagate(args) {
1612            return Ok(null);
1613        }
1614        let mut max = &args[0];
1615        for arg in &args[1..] {
1616            if arg.partial_cmp(max) == Some(std::cmp::Ordering::Greater) {
1617                max = arg;
1618            }
1619        }
1620        Ok(max.clone())
1621    }
1622
1623    fn num_args(&self) -> i32 {
1624        -1
1625    }
1626
1627    fn min_args(&self) -> i32 {
1628        1
1629    }
1630
1631    fn name(&self) -> &str {
1632        "max"
1633    }
1634}
1635
1636// ── scalar min(X, Y, ...) ───────────────────────────────────────────────
1637
1638pub struct ScalarMinFunc;
1639
1640impl ScalarFunction for ScalarMinFunc {
1641    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1642        // Scalar min: if ANY argument is NULL, returns NULL
1643        if let Some(null) = null_propagate(args) {
1644            return Ok(null);
1645        }
1646        let mut min = &args[0];
1647        for arg in &args[1..] {
1648            if arg.partial_cmp(min) == Some(std::cmp::Ordering::Less) {
1649                min = arg;
1650            }
1651        }
1652        Ok(min.clone())
1653    }
1654
1655    fn num_args(&self) -> i32 {
1656        -1
1657    }
1658
1659    fn min_args(&self) -> i32 {
1660        1
1661    }
1662
1663    fn name(&self) -> &str {
1664        "min"
1665    }
1666}
1667
1668// ── likelihood/likely/unlikely ──────────────────────────────────────────
1669
1670pub struct LikelihoodFunc;
1671
1672impl ScalarFunction for LikelihoodFunc {
1673    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1674        // Returns X unchanged; P is a planner hint (ignored at runtime).
1675        Ok(args[0].clone())
1676    }
1677
1678    fn num_args(&self) -> i32 {
1679        2
1680    }
1681
1682    fn name(&self) -> &str {
1683        "likelihood"
1684    }
1685}
1686
1687pub struct LikelyFunc;
1688
1689impl ScalarFunction for LikelyFunc {
1690    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1691        Ok(args[0].clone())
1692    }
1693
1694    fn num_args(&self) -> i32 {
1695        1
1696    }
1697
1698    fn name(&self) -> &str {
1699        "likely"
1700    }
1701}
1702
1703pub struct UnlikelyFunc;
1704
1705impl ScalarFunction for UnlikelyFunc {
1706    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1707        Ok(args[0].clone())
1708    }
1709
1710    fn num_args(&self) -> i32 {
1711        1
1712    }
1713
1714    fn name(&self) -> &str {
1715        "unlikely"
1716    }
1717}
1718
1719// ── sqlite_version() ────────────────────────────────────────────────────
1720
1721pub struct SqliteVersionFunc;
1722
1723impl ScalarFunction for SqliteVersionFunc {
1724    fn invoke(&self, _args: &[SqliteValue]) -> Result<SqliteValue> {
1725        Ok(SqliteValue::Text(SmallText::new(
1726            fsqlite_types::FRANKENSQLITE_SQLITE_VERSION,
1727        )))
1728    }
1729
1730    fn num_args(&self) -> i32 {
1731        0
1732    }
1733
1734    fn name(&self) -> &str {
1735        "sqlite_version"
1736    }
1737}
1738
1739// ── sqlite_source_id() ──────────────────────────────────────────────────
1740
1741pub struct SqliteSourceIdFunc;
1742
1743impl ScalarFunction for SqliteSourceIdFunc {
1744    fn invoke(&self, _args: &[SqliteValue]) -> Result<SqliteValue> {
1745        Ok(SqliteValue::Text(SmallText::new(
1746            fsqlite_types::FRANKENSQLITE_SOURCE_ID,
1747        )))
1748    }
1749
1750    fn num_args(&self) -> i32 {
1751        0
1752    }
1753
1754    fn name(&self) -> &str {
1755        "sqlite_source_id"
1756    }
1757}
1758
1759// ── sqlite_compileoption_used(X) ────────────────────────────────────────
1760
1761pub struct SqliteCompileoptionUsedFunc;
1762
1763impl ScalarFunction for SqliteCompileoptionUsedFunc {
1764    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1765        if args[0].is_null() {
1766            return Ok(SqliteValue::Null);
1767        }
1768        let query = text_arg(&args[0]);
1769        Ok(SqliteValue::Integer(i64::from(sqlite_compileoption_used(
1770            query.as_ref(),
1771        ))))
1772    }
1773
1774    fn num_args(&self) -> i32 {
1775        1
1776    }
1777
1778    fn name(&self) -> &str {
1779        "sqlite_compileoption_used"
1780    }
1781}
1782
1783// ── sqlite_compileoption_get(N) ─────────────────────────────────────────
1784
1785pub struct SqliteCompileoptionGetFunc;
1786
1787impl ScalarFunction for SqliteCompileoptionGetFunc {
1788    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1789        if args[0].is_null() {
1790            return Ok(SqliteValue::Null);
1791        }
1792        let n = args[0].to_integer();
1793        #[allow(clippy::cast_sign_loss)]
1794        match sqlite_compile_options().get(n as usize) {
1795            Some(opt) => Ok(SqliteValue::Text(SmallText::new(opt))),
1796            None => Ok(SqliteValue::Null),
1797        }
1798    }
1799
1800    fn num_args(&self) -> i32 {
1801        1
1802    }
1803
1804    fn name(&self) -> &str {
1805        "sqlite_compileoption_get"
1806    }
1807}
1808
1809// ── like(PATTERN, STRING [, ESCAPE]) ────────────────────────────────────
1810
1811pub struct LikeFunc;
1812
1813impl ScalarFunction for LikeFunc {
1814    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1815        if let Some(null) = null_propagate(args) {
1816            return Ok(null);
1817        }
1818        let pattern = text_arg(&args[0]);
1819        let string = text_arg(&args[1]);
1820        let escape = if args.len() > 2 && !args[2].is_null() {
1821            Some(single_char_escape(text_arg(&args[2]).as_ref())?)
1822        } else {
1823            None
1824        };
1825        let matched = like_match(pattern.as_ref(), string.as_ref(), escape);
1826        Ok(SqliteValue::Integer(i64::from(matched)))
1827    }
1828
1829    fn num_args(&self) -> i32 {
1830        -1 // 2 or 3 args
1831    }
1832
1833    fn name(&self) -> &str {
1834        "like"
1835    }
1836}
1837
1838#[cfg(test)]
1839mod like_func_pragma_tests {
1840    use super::{LikeFunc, case_sensitive_like_active, set_case_sensitive_like};
1841    use crate::ScalarFunction;
1842    use fsqlite_types::SqliteValue;
1843
1844    fn like(pattern: &str, text: &str) -> i64 {
1845        match LikeFunc
1846            .invoke(&[
1847                SqliteValue::Text(pattern.into()),
1848                SqliteValue::Text(text.into()),
1849            ])
1850            .unwrap()
1851        {
1852            SqliteValue::Integer(n) => n,
1853            other => panic!("expected integer, got {other:?}"),
1854        }
1855    }
1856
1857    #[test]
1858    fn like_honors_case_sensitive_like_thread_local() {
1859        // Default: ASCII-case-insensitive.
1860        set_case_sensitive_like(false);
1861        assert_eq!(like("a", "A"), 1);
1862        assert_eq!(like("A%", "apple"), 1);
1863        // ON: byte-exact.
1864        set_case_sensitive_like(true);
1865        assert!(case_sensitive_like_active());
1866        assert_eq!(like("a", "A"), 0);
1867        assert_eq!(like("A%", "apple"), 0);
1868        assert_eq!(like("A%", "Apple"), 1);
1869        // Restore so other tests on this thread see the default.
1870        set_case_sensitive_like(false);
1871    }
1872}
1873
1874fn single_char_escape(escape: &str) -> Result<char> {
1875    let mut chars = escape.chars();
1876    match (chars.next(), chars.next()) {
1877        (Some(ch), None) => Ok(ch),
1878        _ => Err(FrankenError::function_error(
1879            "ESCAPE expression must be a single character",
1880        )),
1881    }
1882}
1883
1884/// LIKE pattern matching. ASCII-case-insensitive by default; byte-exact when
1885/// the connection has `PRAGMA case_sensitive_like = ON` (read from the
1886/// thread-local set by the Connection before statement execution).
1887fn like_match(pattern: &str, string: &str, escape: Option<char>) -> bool {
1888    sql_like_cased(pattern, string, escape, case_sensitive_like_active())
1889}
1890
1891// ── glob(PATTERN, STRING) ───────────────────────────────────────────────
1892
1893pub struct GlobFunc;
1894
1895impl ScalarFunction for GlobFunc {
1896    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1897        if let Some(null) = null_propagate(args) {
1898            return Ok(null);
1899        }
1900        let pattern = text_arg(&args[0]);
1901        let string = text_arg(&args[1]);
1902        let matched = glob_match(pattern.as_ref(), string.as_ref());
1903        Ok(SqliteValue::Integer(i64::from(matched)))
1904    }
1905
1906    fn num_args(&self) -> i32 {
1907        2
1908    }
1909
1910    fn name(&self) -> &str {
1911        "glob"
1912    }
1913}
1914
1915/// GLOB pattern matching (case-sensitive, * and ? wildcards).
1916fn glob_match(pattern: &str, string: &str) -> bool {
1917    let pat: Vec<char> = pattern.chars().collect();
1918    let txt: Vec<char> = string.chars().collect();
1919    glob_match_inner(&pat, &txt, 0, 0)
1920}
1921
1922fn text_arg(value: &SqliteValue) -> Cow<'_, str> {
1923    match value.as_text_str() {
1924        Some(text) => Cow::Borrowed(text),
1925        None => Cow::Owned(value.to_text()),
1926    }
1927}
1928
1929fn glob_match_inner(pat: &[char], txt: &[char], mut pi: usize, mut ti: usize) -> bool {
1930    while pi < pat.len() {
1931        match pat[pi] {
1932            '*' => {
1933                while pi < pat.len() && pat[pi] == '*' {
1934                    pi += 1;
1935                }
1936                if pi >= pat.len() {
1937                    return true;
1938                }
1939                for start in ti..=txt.len() {
1940                    if glob_match_inner(pat, txt, pi, start) {
1941                        return true;
1942                    }
1943                }
1944                return false;
1945            }
1946            '?' => {
1947                if ti >= txt.len() {
1948                    return false;
1949                }
1950                pi += 1;
1951                ti += 1;
1952            }
1953            '[' => {
1954                if ti >= txt.len() {
1955                    return false;
1956                }
1957                pi += 1;
1958                let negate = pi < pat.len() && pat[pi] == '^';
1959                if negate {
1960                    pi += 1;
1961                }
1962                let mut found = false;
1963                let mut first = true;
1964                while pi < pat.len() && (first || pat[pi] != ']') {
1965                    first = false;
1966                    if pi + 2 < pat.len() && pat[pi + 1] == '-' {
1967                        let lo = pat[pi];
1968                        let hi = pat[pi + 2];
1969                        if txt[ti] >= lo && txt[ti] <= hi {
1970                            found = true;
1971                        }
1972                        pi += 3;
1973                    } else {
1974                        if txt[ti] == pat[pi] {
1975                            found = true;
1976                        }
1977                        pi += 1;
1978                    }
1979                }
1980                if pi < pat.len() && pat[pi] == ']' {
1981                    pi += 1;
1982                }
1983                if found == negate {
1984                    return false;
1985                }
1986                ti += 1;
1987            }
1988            c => {
1989                if ti >= txt.len() || txt[ti] != c {
1990                    return false;
1991                }
1992                pi += 1;
1993                ti += 1;
1994            }
1995        }
1996    }
1997    ti >= txt.len()
1998}
1999
2000// ── unistr(X) ───────────────────────────────────────────────────────────
2001
2002pub struct UnistrFunc;
2003
2004const INVALID_UNISTR_ESCAPE: &str = "invalid Unicode escape";
2005
2006fn decode_unistr_escape(chars: &mut std::str::Chars<'_>, digits: usize) -> Result<char> {
2007    let mut lookahead = chars.clone();
2008    let mut codepoint = 0u32;
2009    for _ in 0..digits {
2010        let Some(ch) = lookahead.next() else {
2011            return Err(FrankenError::function_error(INVALID_UNISTR_ESCAPE));
2012        };
2013        let Some(digit) = hex_digit(ch) else {
2014            return Err(FrankenError::function_error(INVALID_UNISTR_ESCAPE));
2015        };
2016        codepoint = (codepoint << 4) | u32::from(digit);
2017    }
2018    for _ in 0..digits {
2019        let _digit = chars.next();
2020    }
2021    char::from_u32(codepoint).ok_or_else(|| FrankenError::function_error(INVALID_UNISTR_ESCAPE))
2022}
2023
2024impl ScalarFunction for UnistrFunc {
2025    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
2026        if args[0].is_null() {
2027            return Ok(SqliteValue::Null);
2028        }
2029        let input = text_arg(&args[0]);
2030        let mut result = String::with_capacity(input.len());
2031        let mut chars = input.as_ref().chars();
2032        while let Some(ch) = chars.next() {
2033            if ch == '\\' {
2034                // C SQLite: \\ is an escaped backslash literal.
2035                if chars.as_str().starts_with('\\') {
2036                    let _ = chars.next();
2037                    result.push('\\');
2038                    continue;
2039                }
2040                let digits = if chars.as_str().starts_with('+') {
2041                    // \+XXXXXX
2042                    let _plus = chars.next();
2043                    6
2044                } else if chars.as_str().starts_with('u') {
2045                    // \uXXXX
2046                    let _marker = chars.next();
2047                    4
2048                } else if chars.as_str().starts_with('U') {
2049                    // \UXXXXXXXX
2050                    let _marker = chars.next();
2051                    8
2052                } else {
2053                    // \XXXX
2054                    4
2055                };
2056                result.push(decode_unistr_escape(&mut chars, digits)?);
2057                continue;
2058            }
2059            result.push(ch);
2060        }
2061        Ok(SqliteValue::Text(SmallText::from_string(result)))
2062    }
2063
2064    fn num_args(&self) -> i32 {
2065        1
2066    }
2067
2068    fn name(&self) -> &str {
2069        "unistr"
2070    }
2071}
2072
2073// ── Connection-state helpers ────────────────────────────────────────────
2074// These functions reflect connection-local counters projected into this
2075// thread by the connection layer around statement execution.
2076
2077pub struct ChangesFunc;
2078
2079impl ScalarFunction for ChangesFunc {
2080    fn invoke(&self, _args: &[SqliteValue]) -> Result<SqliteValue> {
2081        Ok(SqliteValue::Integer(LAST_CHANGES.get()))
2082    }
2083
2084    fn is_deterministic(&self) -> bool {
2085        false
2086    }
2087
2088    fn num_args(&self) -> i32 {
2089        0
2090    }
2091
2092    fn name(&self) -> &str {
2093        "changes"
2094    }
2095}
2096
2097pub struct TotalChangesFunc;
2098
2099impl ScalarFunction for TotalChangesFunc {
2100    fn invoke(&self, _args: &[SqliteValue]) -> Result<SqliteValue> {
2101        Ok(SqliteValue::Integer(TOTAL_CHANGES.get()))
2102    }
2103
2104    fn is_deterministic(&self) -> bool {
2105        false
2106    }
2107
2108    fn num_args(&self) -> i32 {
2109        0
2110    }
2111
2112    fn name(&self) -> &str {
2113        "total_changes"
2114    }
2115}
2116
2117pub struct LastInsertRowidFunc;
2118
2119impl ScalarFunction for LastInsertRowidFunc {
2120    fn invoke(&self, _args: &[SqliteValue]) -> Result<SqliteValue> {
2121        Ok(SqliteValue::Integer(LAST_INSERT_ROWID.get()))
2122    }
2123
2124    fn is_deterministic(&self) -> bool {
2125        false
2126    }
2127
2128    fn num_args(&self) -> i32 {
2129        0
2130    }
2131
2132    fn name(&self) -> &str {
2133        "last_insert_rowid"
2134    }
2135}
2136
2137// ── Register all built-ins ──────────────────────────────────────────────
2138
2139/// Register all core built-in scalar functions into the given registry.
2140#[allow(clippy::too_many_lines)]
2141pub fn register_builtins(registry: &mut FunctionRegistry) {
2142    // Math
2143    registry.register_scalar(AbsFunc);
2144    registry.register_scalar(SignFunc);
2145    registry.register_scalar(RoundFunc);
2146    registry.register_scalar(RandomFunc);
2147    registry.register_scalar(RandomblobFunc);
2148    registry.register_scalar(ZeroblobFunc);
2149
2150    // String
2151    registry.register_scalar(LowerFunc);
2152    registry.register_scalar(UpperFunc);
2153    registry.register_scalar(LengthFunc);
2154    registry.register_scalar(OctetLengthFunc);
2155    registry.register_scalar(TrimFunc);
2156    registry.register_scalar(LtrimFunc);
2157    registry.register_scalar(RtrimFunc);
2158    registry.register_scalar(ReplaceFunc);
2159    registry.register_scalar(SubstrFunc);
2160    registry.register_scalar(InstrFunc);
2161    registry.register_scalar(CharFunc);
2162    registry.register_scalar(UnicodeFunc);
2163    registry.register_scalar(UnistrFunc);
2164    registry.register_scalar(HexFunc);
2165    registry.register_scalar(UnhexFunc);
2166    registry.register_scalar(QuoteFunc);
2167    registry.register_scalar(UnistrQuoteFunc);
2168    registry.register_scalar(SoundexFunc);
2169
2170    // Type
2171    registry.register_scalar(TypeofFunc);
2172    registry.register_scalar(SubtypeFunc);
2173
2174    // Conditional
2175    registry.register_scalar(CoalesceFunc);
2176    registry.register_scalar(IfnullFunc);
2177    registry.register_scalar(NullifFunc);
2178    registry.register_scalar(IifFunc);
2179
2180    // Multi-value
2181    registry.register_scalar(ConcatFunc);
2182    registry.register_scalar(ConcatWsFunc);
2183    registry.register_scalar(ScalarMaxFunc);
2184    registry.register_scalar(ScalarMinFunc);
2185
2186    // Planner hints
2187    registry.register_scalar(LikelihoodFunc);
2188    registry.register_scalar(LikelyFunc);
2189    registry.register_scalar(UnlikelyFunc);
2190
2191    // Pattern matching
2192    registry.register_scalar(LikeFunc);
2193    registry.register_scalar(GlobFunc);
2194
2195    // Meta
2196    registry.register_scalar(SqliteVersionFunc);
2197    registry.register_scalar(SqliteSourceIdFunc);
2198    registry.register_scalar(SqliteCompileoptionUsedFunc);
2199    registry.register_scalar(SqliteCompileoptionGetFunc);
2200
2201    // Connection-state stubs
2202    registry.register_scalar(ChangesFunc);
2203    registry.register_scalar(TotalChangesFunc);
2204    registry.register_scalar(LastInsertRowidFunc);
2205
2206    // "if" is an alias for "iif" (3.48+)
2207    // Register same function under alternate name
2208    struct IfFunc;
2209    impl ScalarFunction for IfFunc {
2210        fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
2211            IifFunc.invoke(args)
2212        }
2213
2214        fn num_args(&self) -> i32 {
2215            3
2216        }
2217
2218        fn name(&self) -> &str {
2219            "if"
2220        }
2221    }
2222    registry.register_scalar(IfFunc);
2223
2224    // "substring" is an alias for "substr"
2225    struct SubstringFunc;
2226    impl ScalarFunction for SubstringFunc {
2227        fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
2228            SubstrFunc.invoke(args)
2229        }
2230
2231        fn num_args(&self) -> i32 {
2232            -1
2233        }
2234
2235        fn min_args(&self) -> i32 {
2236            2
2237        }
2238
2239        fn max_args(&self) -> Option<i32> {
2240            Some(3)
2241        }
2242
2243        fn name(&self) -> &str {
2244            "substring"
2245        }
2246    }
2247    registry.register_scalar(SubstringFunc);
2248
2249    // "printf" is an alias for "format".
2250    struct PrintfFunc;
2251    impl ScalarFunction for PrintfFunc {
2252        fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
2253            FormatFunc.invoke(args)
2254        }
2255
2256        fn num_args(&self) -> i32 {
2257            -1
2258        }
2259
2260        fn name(&self) -> &str {
2261            "printf"
2262        }
2263    }
2264    registry.register_scalar(FormatFunc);
2265    registry.register_scalar(PrintfFunc);
2266
2267    // §13.2 Math functions (acos, asin, atan, ceil, floor, log, pow, sqrt, etc.)
2268    register_math_builtins(registry);
2269
2270    // §13.3 Date/time functions (date, time, datetime, julianday, unixepoch, strftime, timediff)
2271    register_datetime_builtins(registry);
2272
2273    // §13.4 Aggregate functions (avg, count, group_concat, max, min, sum, total, etc.)
2274    register_aggregate_builtins(registry);
2275}
2276
2277// ── format(FORMAT, ...) / printf(FORMAT, ...) ───────────────────────────
2278
2279pub struct FormatFunc;
2280
2281impl ScalarFunction for FormatFunc {
2282    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
2283        if args.is_empty() || args[0].is_null() {
2284            return Ok(SqliteValue::Null);
2285        }
2286        let fmt_str = args[0].to_text();
2287        // SQLite returns NULL (not empty text) when the format string is empty:
2288        // an empty format never appends to the StrAccum, so its result buffer
2289        // stays NULL. A non-empty format that renders to nothing (e.g.
2290        // printf('%s', NULL)) still yields empty TEXT, so only gate on the
2291        // format string being empty here.
2292        if fmt_str.is_empty() {
2293            return Ok(SqliteValue::Null);
2294        }
2295        let params = &args[1..];
2296        let result = sqlite_format(&fmt_str, params)?;
2297        Ok(SqliteValue::Text(SmallText::from_string(result)))
2298    }
2299
2300    fn num_args(&self) -> i32 {
2301        -1
2302    }
2303
2304    fn name(&self) -> &str {
2305        "format"
2306    }
2307}
2308
2309/// Simplified SQLite format/printf implementation.
2310/// Supports: %d, %f, %e, %g, %s, %q, %Q, %w, %%, %n (no-op).
2311fn sqlite_format(fmt: &str, params: &[SqliteValue]) -> Result<String> {
2312    let mut result = String::new();
2313    let chars: Vec<char> = fmt.chars().collect();
2314    let mut i = 0;
2315    let mut param_idx = 0;
2316
2317    while i < chars.len() {
2318        if chars[i] != '%' {
2319            result.push(chars[i]);
2320            i += 1;
2321            continue;
2322        }
2323        i += 1;
2324        if i >= chars.len() {
2325            break;
2326        }
2327
2328        // Parse flags
2329        let mut left_align = false;
2330        let mut show_sign = false;
2331        let mut space_sign = false;
2332        let mut zero_pad = false;
2333        let mut alt_form = false;
2334        loop {
2335            if i >= chars.len() {
2336                break;
2337            }
2338            match chars[i] {
2339                '-' => left_align = true,
2340                '+' => show_sign = true,
2341                ' ' => space_sign = true,
2342                '0' => zero_pad = true,
2343                '#' => alt_form = true,
2344                _ => break,
2345            }
2346            i += 1;
2347        }
2348
2349        // Parse width: a literal number, or `*` to take the width from the next
2350        // argument (bd-jvnwt). A negative dynamic width means left-justify with
2351        // its absolute value, matching C printf.
2352        let mut width = 0usize;
2353        if i < chars.len() && chars[i] == '*' {
2354            i += 1;
2355            let w = params.get(param_idx).map_or(0, SqliteValue::to_integer);
2356            param_idx += 1;
2357            if w < 0 {
2358                left_align = true;
2359                width = usize::try_from(w.unsigned_abs())
2360                    .unwrap_or(0)
2361                    .min(100_000_000);
2362            } else {
2363                width = usize::try_from(w).unwrap_or(0).min(100_000_000);
2364            }
2365        } else {
2366            while i < chars.len() && chars[i].is_ascii_digit() {
2367                width = width
2368                    .saturating_mul(10)
2369                    .saturating_add(chars[i] as usize - '0' as usize)
2370                    .min(100_000_000); // Prevent OOM from malicious formats
2371                i += 1;
2372            }
2373        }
2374
2375        // Parse precision: a literal number, or `*` to take the precision from
2376        // the next argument (like width above). A negative dynamic precision
2377        // means "no precision", matching C printf.
2378        let mut precision = None;
2379        if i < chars.len() && chars[i] == '.' {
2380            i += 1;
2381            if i < chars.len() && chars[i] == '*' {
2382                i += 1;
2383                let p = params.get(param_idx).map_or(0, SqliteValue::to_integer);
2384                param_idx += 1;
2385                if p >= 0 {
2386                    precision = Some(usize::try_from(p).unwrap_or(0).min(100_000_000));
2387                }
2388            } else {
2389                let mut prec = 0usize;
2390                while i < chars.len() && chars[i].is_ascii_digit() {
2391                    prec = prec
2392                        .saturating_mul(10)
2393                        .saturating_add(chars[i] as usize - '0' as usize)
2394                        .min(100_000_000); // Prevent OOM from malicious formats
2395                    i += 1;
2396                }
2397                precision = Some(prec);
2398            }
2399        }
2400
2401        if i >= chars.len() {
2402            break;
2403        }
2404
2405        let spec = chars[i];
2406        i += 1;
2407
2408        match spec {
2409            '%' => result.push('%'),
2410            'n' => {} // no-op (security: never writes to memory)
2411            'd' | 'i' => {
2412                let val = params.get(param_idx).map_or(0, SqliteValue::to_integer);
2413                param_idx += 1;
2414                let formatted =
2415                    format_integer(val, width, left_align, show_sign, space_sign, zero_pad);
2416                result.push_str(&formatted);
2417            }
2418            'u' => {
2419                // Unsigned decimal (bd-jvnwt): reinterpret the i64 bit pattern as
2420                // u64, matching C/SQLite %u.
2421                let val = params.get(param_idx).map_or(0, SqliteValue::to_integer);
2422                param_idx += 1;
2423                #[allow(clippy::cast_sign_loss)]
2424                let digits = (val as u64).to_string();
2425                let padded = if zero_pad && width > digits.len() {
2426                    format!("{}{}", "0".repeat(width - digits.len()), digits)
2427                } else {
2428                    pad_string(&digits, width, left_align)
2429                };
2430                result.push_str(&padded);
2431            }
2432            'f' => {
2433                let val = params.get(param_idx).map_or(0.0, SqliteValue::to_float);
2434                param_idx += 1;
2435                let prec = precision.unwrap_or(6);
2436                let formatted = format_float_f(
2437                    val, prec, width, left_align, show_sign, space_sign, zero_pad,
2438                );
2439                result.push_str(&formatted);
2440            }
2441            'e' | 'E' => {
2442                let val = params.get(param_idx).map_or(0.0, SqliteValue::to_float);
2443                param_idx += 1;
2444                let prec = precision.unwrap_or(6);
2445                if let Some(s) = nonfinite_float_str(val, width, left_align, show_sign, space_sign)
2446                {
2447                    result.push_str(&s);
2448                } else {
2449                    let raw = if spec == 'e' {
2450                        format!("{val:.prec$e}")
2451                    } else {
2452                        format!("{val:.prec$E}")
2453                    };
2454                    // C printf always uses explicit sign and minimum 2-digit exponent
2455                    let formatted = normalize_exponent(&raw);
2456                    result.push_str(&finish_float_padding(
2457                        &formatted, width, left_align, show_sign, space_sign, zero_pad,
2458                    ));
2459                }
2460            }
2461            'g' | 'G' => {
2462                let val = params.get(param_idx).map_or(0.0, SqliteValue::to_float);
2463                param_idx += 1;
2464                let prec = precision.unwrap_or(6);
2465                let sig = prec.max(1);
2466                if let Some(s) = nonfinite_float_str(val, width, left_align, show_sign, space_sign)
2467                {
2468                    result.push_str(&s);
2469                } else {
2470                    let formatted = format_float_g(val, sig, spec == 'G');
2471                    result.push_str(&finish_float_padding(
2472                        &formatted, width, left_align, show_sign, space_sign, zero_pad,
2473                    ));
2474                }
2475            }
2476            's' | 'z' => {
2477                let param = params.get(param_idx);
2478                param_idx += 1;
2479                let val = match param {
2480                    // SQLite: printf('%s', NULL) returns empty string
2481                    Some(SqliteValue::Null) | None => String::new(),
2482                    Some(v) => v.to_text(),
2483                };
2484                // C SQLite counts %s precision in BYTES. It will emit a bare
2485                // partial code point; we floor to the previous char boundary
2486                // instead (Rust strings must stay valid UTF-8), which matches
2487                // C SQLite whenever the cut lands on a boundary.
2488                let truncated = if let Some(prec) = precision {
2489                    if val.len() > prec {
2490                        let mut end = prec;
2491                        while end > 0 && !val.is_char_boundary(end) {
2492                            end -= 1;
2493                        }
2494                        val[..end].to_owned()
2495                    } else {
2496                        val
2497                    }
2498                } else {
2499                    val
2500                };
2501                result.push_str(&pad_string(&truncated, width, left_align));
2502            }
2503            'q' => {
2504                // Single-quote escaping; C SQLite emits nothing for %q with NULL
2505                let param = params.get(param_idx);
2506                param_idx += 1;
2507                match param {
2508                    // SQLite: printf('%q', NULL) returns literal "(NULL)"
2509                    Some(SqliteValue::Null) | None => {
2510                        result.push_str("(NULL)");
2511                    }
2512                    Some(v) => {
2513                        let val = v.to_text();
2514                        let escaped = val.replace('\'', "''");
2515                        result.push_str(&escaped);
2516                    }
2517                }
2518            }
2519            'Q' => {
2520                // Like %q but wrapped in quotes, NULL -> "NULL"
2521                let param = params.get(param_idx);
2522                param_idx += 1;
2523                match param {
2524                    Some(SqliteValue::Null) | None => result.push_str("NULL"),
2525                    Some(v) => {
2526                        let val = v.to_text();
2527                        let escaped = val.replace('\'', "''");
2528                        result.push('\'');
2529                        result.push_str(&escaped);
2530                        result.push('\'');
2531                    }
2532                }
2533            }
2534            'w' => {
2535                // Double-quote escaping for identifiers; NULL → empty.
2536                // C SQLite %w with NULL produces nothing (empty string),
2537                // and only escapes internal double quotes (no surrounding quotes).
2538                let param = params.get(param_idx);
2539                param_idx += 1;
2540                if matches!(param, Some(SqliteValue::Null) | None) {
2541                    // NULL: produce nothing (matches C SQLite).
2542                } else {
2543                    let val = param.map(SqliteValue::to_text).unwrap_or_default();
2544                    let escaped = val.replace('"', "\"\"");
2545                    result.push_str(&escaped);
2546                }
2547            }
2548            'x' | 'X' => {
2549                let val = params.get(param_idx).map_or(0, SqliteValue::to_integer);
2550                param_idx += 1;
2551                #[allow(clippy::cast_sign_loss)]
2552                let digits = if spec == 'x' {
2553                    format!("{:x}", val as u64)
2554                } else {
2555                    format!("{:X}", val as u64)
2556                };
2557                // Alternate form (`#`) prefixes a nonzero value with 0x / 0X.
2558                let prefix = if alt_form && val != 0 {
2559                    if spec == 'x' { "0x" } else { "0X" }
2560                } else {
2561                    ""
2562                };
2563                // SQLite's printf zero-pads whenever the `0` flag is present,
2564                // even alongside `-` (it does NOT let `-` override `0` the way C
2565                // does). The digits are zero-padded to `width`; the prefix sits
2566                // outside that pad.
2567                let padded = if zero_pad && width > digits.len() {
2568                    let pad = "0".repeat(width - digits.len());
2569                    format!("{prefix}{pad}{digits}")
2570                } else {
2571                    pad_string(&format!("{prefix}{digits}"), width, left_align)
2572                };
2573                result.push_str(&padded);
2574            }
2575            'o' => {
2576                let val = params.get(param_idx).map_or(0, SqliteValue::to_integer);
2577                param_idx += 1;
2578                #[allow(clippy::cast_sign_loss)]
2579                let digits = format!("{:o}", val as u64);
2580                // Alternate form (`#`) prefixes a nonzero value with a leading 0.
2581                let prefix = if alt_form && val != 0 { "0" } else { "" };
2582                // As with %x, SQLite zero-pads whenever the `0` flag is present
2583                // (even with `-`).
2584                let padded = if zero_pad && width > digits.len() {
2585                    let pad = "0".repeat(width - digits.len());
2586                    format!("{prefix}{pad}{digits}")
2587                } else {
2588                    pad_string(&format!("{prefix}{digits}"), width, left_align)
2589                };
2590                result.push_str(&padded);
2591            }
2592            'c' => {
2593                let param = params.get(param_idx);
2594                param_idx += 1;
2595                // SQLite's printf %c renders the argument to its text form and
2596                // emits the first character — it does NOT interpret an integer
2597                // as a Unicode codepoint like C printf does (bd-47mu0). So
2598                // printf('%c', 65) yields '6' (first char of "65"), not 'A'.
2599                let text = match param {
2600                    Some(SqliteValue::Null) | None => String::new(),
2601                    Some(v) => v.to_text(),
2602                };
2603                if let Some(c) = text.chars().next() {
2604                    result.push(c);
2605                }
2606            }
2607            _ => {
2608                // Unknown specifier: output literally
2609                result.push('%');
2610                result.push(spec);
2611            }
2612        }
2613        // Suppress unused warnings
2614        let _ = (left_align, show_sign, space_sign, zero_pad);
2615    }
2616    Ok(result)
2617}
2618
2619fn format_integer(
2620    val: i64,
2621    width: usize,
2622    left_align: bool,
2623    show_sign: bool,
2624    space_sign: bool,
2625    zero_pad: bool,
2626) -> String {
2627    let sign = if val < 0 {
2628        "-".to_owned()
2629    } else if show_sign {
2630        "+".to_owned()
2631    } else if space_sign {
2632        " ".to_owned()
2633    } else {
2634        String::new()
2635    };
2636    let digits = format!("{}", val.unsigned_abs());
2637    let body = format!("{sign}{digits}");
2638    if body.len() >= width {
2639        return body;
2640    }
2641    let pad = width - body.len();
2642    if left_align {
2643        format!("{body}{}", " ".repeat(pad))
2644    } else if zero_pad {
2645        format!("{sign}{}{digits}", "0".repeat(pad))
2646    } else {
2647        format!("{}{body}", " ".repeat(pad))
2648    }
2649}
2650
2651/// C SQLite renders non-finite floats in printf as `Inf` / `-Inf` / `NaN`
2652/// (sign flags honored for infinities, space-padded to width, never
2653/// zero-padded). Returns `None` for finite values.
2654fn nonfinite_float_str(
2655    val: f64,
2656    width: usize,
2657    left_align: bool,
2658    show_sign: bool,
2659    space_sign: bool,
2660) -> Option<String> {
2661    let body = if val.is_nan() {
2662        "NaN".to_owned()
2663    } else if val.is_infinite() {
2664        let sign = if val < 0.0 {
2665            "-"
2666        } else if show_sign {
2667            "+"
2668        } else if space_sign {
2669            " "
2670        } else {
2671            ""
2672        };
2673        format!("{sign}Inf")
2674    } else {
2675        return None;
2676    };
2677    Some(pad_string(&body, width, left_align))
2678}
2679
2680/// Apply printf sign flags and width padding to an already-formatted float
2681/// body (which may carry a leading `-`). Zero padding is inserted between the
2682/// sign and the digits, matching C printf.
2683fn finish_float_padding(
2684    body: &str,
2685    width: usize,
2686    left_align: bool,
2687    show_sign: bool,
2688    space_sign: bool,
2689    zero_pad: bool,
2690) -> String {
2691    let (sign, digits) = if let Some(rest) = body.strip_prefix('-') {
2692        ("-", rest)
2693    } else if show_sign {
2694        ("+", body)
2695    } else if space_sign {
2696        (" ", body)
2697    } else {
2698        ("", body)
2699    };
2700    let full_len = sign.len() + digits.len();
2701    if full_len >= width {
2702        return format!("{sign}{digits}");
2703    }
2704    let pad = width - full_len;
2705    if left_align {
2706        format!("{sign}{digits}{}", " ".repeat(pad))
2707    } else if zero_pad {
2708        format!("{sign}{}{digits}", "0".repeat(pad))
2709    } else {
2710        format!("{}{sign}{digits}", " ".repeat(pad))
2711    }
2712}
2713
2714fn format_float_f(
2715    val: f64,
2716    prec: usize,
2717    width: usize,
2718    left_align: bool,
2719    show_sign: bool,
2720    space_sign: bool,
2721    zero_pad: bool,
2722) -> String {
2723    if let Some(s) = nonfinite_float_str(val, width, left_align, show_sign, space_sign) {
2724        return s;
2725    }
2726    // Use is_sign_negative() to detect -0.0 (IEEE 754: -0.0 < 0.0 is false).
2727    let sign = if val.is_sign_negative() {
2728        "-".to_owned()
2729    } else if show_sign {
2730        "+".to_owned()
2731    } else if space_sign {
2732        " ".to_owned()
2733    } else {
2734        String::new()
2735    };
2736    let digits = format!("{:.prec$}", val.abs());
2737    let body = format!("{sign}{digits}");
2738    if body.len() >= width {
2739        return body;
2740    }
2741    let pad = width - body.len();
2742    if left_align {
2743        format!("{body}{}", " ".repeat(pad))
2744    } else if zero_pad {
2745        format!("{sign}{}{digits}", "0".repeat(pad))
2746    } else {
2747        format!("{}{body}", " ".repeat(pad))
2748    }
2749}
2750
2751fn pad_string(s: &str, width: usize, left_align: bool) -> String {
2752    if s.len() >= width {
2753        return s.to_owned();
2754    }
2755    let pad = width - s.len();
2756    if left_align {
2757        format!("{s}{}", " ".repeat(pad))
2758    } else {
2759        format!("{}{s}", " ".repeat(pad))
2760    }
2761}
2762
2763/// Normalize an exponent string to match C printf: explicit sign and
2764/// minimum two digits (e.g. `"1.23e6"` → `"1.23e+06"`).
2765fn normalize_exponent(s: &str) -> String {
2766    let (prefix, e_char, exp_part) = if let Some(pos) = s.find('e') {
2767        (&s[..pos], 'e', &s[pos + 1..])
2768    } else if let Some(pos) = s.find('E') {
2769        (&s[..pos], 'E', &s[pos + 1..])
2770    } else {
2771        return s.to_owned();
2772    };
2773    let (sign, digits) = if let Some(rest) = exp_part.strip_prefix('-') {
2774        ("-", rest)
2775    } else if let Some(rest) = exp_part.strip_prefix('+') {
2776        ("+", rest)
2777    } else {
2778        ("+", exp_part)
2779    };
2780    let padded = if digits.len() < 2 {
2781        format!("0{digits}")
2782    } else {
2783        digits.to_owned()
2784    };
2785    format!("{prefix}{e_char}{sign}{padded}")
2786}
2787
2788/// Format a float using `%g`/`%G` semantics.
2789fn format_float_g(val: f64, sig: usize, upper: bool) -> String {
2790    if !val.is_finite() {
2791        return format!("{val}");
2792    }
2793    let e_str = format!("{val:.prec$e}", prec = sig.saturating_sub(1));
2794    let exp: i32 = e_str
2795        .rsplit_once('e')
2796        .and_then(|(_, e)| e.parse().ok())
2797        .unwrap_or(0);
2798    #[allow(clippy::cast_possible_wrap)]
2799    let formatted = if exp < -4 || exp >= sig as i32 {
2800        let s = format!("{val:.prec$e}", prec = sig.saturating_sub(1));
2801        let s = if upper { s.replace('e', "E") } else { s };
2802        // Strip trailing zeros from mantissa, then normalize the exponent.
2803        let trimmed = if s.contains('.') {
2804            if let Some(e_pos) = s.find('e').or_else(|| s.find('E')) {
2805                let mantissa = s[..e_pos].trim_end_matches('0').trim_end_matches('.');
2806                format!("{mantissa}{}", &s[e_pos..])
2807            } else {
2808                s.trim_end_matches('0').trim_end_matches('.').to_owned()
2809            }
2810        } else {
2811            s
2812        };
2813        normalize_exponent(&trimmed)
2814    } else {
2815        let decimal_places = if exp >= 0 {
2816            sig.saturating_sub((exp + 1) as usize)
2817        } else {
2818            sig + exp.unsigned_abs() as usize - 1
2819        };
2820        let s = format!("{val:.decimal_places$}");
2821        s.trim_end_matches('0').trim_end_matches('.').to_owned()
2822    };
2823    formatted
2824}
2825
2826#[cfg(test)]
2827#[allow(clippy::too_many_lines)]
2828mod tests {
2829    use super::*;
2830
2831    fn invoke1(f: &dyn ScalarFunction, v: SqliteValue) -> Result<SqliteValue> {
2832        f.invoke(&[v])
2833    }
2834
2835    fn invoke2(f: &dyn ScalarFunction, a: SqliteValue, b: SqliteValue) -> Result<SqliteValue> {
2836        f.invoke(&[a, b])
2837    }
2838
2839    fn assert_wrong_arg_count(registry: &FunctionRegistry, name: &str, arity: i32) {
2840        let function = registry
2841            .find_scalar(name, arity)
2842            .expect("known scalar name with bad arity returns erroring scalar");
2843        let args = vec![SqliteValue::Null; arity.max(0) as usize];
2844        let err = function
2845            .invoke(&args)
2846            .expect_err("wrong arity should return function error");
2847        let expected = format!("wrong number of arguments to function {name}()");
2848        assert!(
2849            matches!(&err, FrankenError::FunctionError(message) if message == &expected),
2850            "expected {expected:?}, got {err:?}"
2851        );
2852    }
2853
2854    #[test]
2855    fn test_get_change_tracking_state_returns_thread_local_snapshot() {
2856        let original = get_change_tracking_state();
2857        let expected = ChangeTrackingState {
2858            last_insert_rowid: 17,
2859            last_changes: 23,
2860            total_changes: 42,
2861        };
2862
2863        set_change_tracking_state(expected);
2864        assert_eq!(get_change_tracking_state(), expected);
2865
2866        set_change_tracking_state(original);
2867    }
2868
2869    // ── abs ──────────────────────────────────────────────────────────────
2870
2871    #[test]
2872    fn test_abs_positive() {
2873        assert_eq!(
2874            invoke1(&AbsFunc, SqliteValue::Integer(42)).unwrap(),
2875            SqliteValue::Integer(42)
2876        );
2877    }
2878
2879    #[test]
2880    fn test_abs_negative() {
2881        assert_eq!(
2882            invoke1(&AbsFunc, SqliteValue::Integer(-42)).unwrap(),
2883            SqliteValue::Integer(42)
2884        );
2885    }
2886
2887    #[test]
2888    fn test_abs_null() {
2889        assert_eq!(
2890            invoke1(&AbsFunc, SqliteValue::Null).unwrap(),
2891            SqliteValue::Null
2892        );
2893    }
2894
2895    #[test]
2896    fn test_abs_min_i64_overflow() {
2897        let err = invoke1(&AbsFunc, SqliteValue::Integer(i64::MIN)).unwrap_err();
2898        assert!(matches!(err, FrankenError::IntegerOverflow));
2899    }
2900
2901    #[test]
2902    fn test_abs_string_coercion() {
2903        assert_eq!(
2904            invoke1(&AbsFunc, SqliteValue::Text(SmallText::from_string("-7.5"))).unwrap(),
2905            SqliteValue::Float(7.5)
2906        );
2907    }
2908
2909    #[test]
2910    fn test_abs_whitespace_padded_text() {
2911        // SQLite's abs() casts non-integers to REAL, even if they parse cleanly as integers
2912        assert_eq!(
2913            invoke1(
2914                &AbsFunc,
2915                SqliteValue::Text(SmallText::from_string("  42  "))
2916            )
2917            .unwrap(),
2918            SqliteValue::Float(42.0)
2919        );
2920        assert_eq!(
2921            invoke1(
2922                &AbsFunc,
2923                SqliteValue::Text(SmallText::from_string("  -7.5  "))
2924            )
2925            .unwrap(),
2926            SqliteValue::Float(7.5)
2927        );
2928        assert_eq!(
2929            invoke1(&AbsFunc, SqliteValue::Text(SmallText::from_string("abc"))).unwrap(),
2930            SqliteValue::Float(0.0)
2931        );
2932    }
2933
2934    #[test]
2935    #[allow(clippy::approx_constant)]
2936    fn test_abs_float() {
2937        assert_eq!(
2938            invoke1(&AbsFunc, SqliteValue::Float(-3.14)).unwrap(),
2939            SqliteValue::Float(3.14)
2940        );
2941    }
2942
2943    // ── char ─────────────────────────────────────────────────────────────
2944
2945    #[test]
2946    fn test_char_basic() {
2947        let f = CharFunc;
2948        let result = f
2949            .invoke(&[
2950                SqliteValue::Integer(72),
2951                SqliteValue::Integer(101),
2952                SqliteValue::Integer(108),
2953                SqliteValue::Integer(108),
2954                SqliteValue::Integer(111),
2955            ])
2956            .unwrap();
2957        assert_eq!(result, SqliteValue::Text(SmallText::from_string("Hello")));
2958    }
2959
2960    #[test]
2961    fn test_char_null_skipped() {
2962        let f = CharFunc;
2963        // C SQLite: NULL → sqlite3_value_int()=0 → U+0000 (NUL byte).
2964        let result = f
2965            .invoke(&[
2966                SqliteValue::Integer(65),
2967                SqliteValue::Null,
2968                SqliteValue::Integer(66),
2969            ])
2970            .unwrap();
2971        assert_eq!(result, SqliteValue::Text(SmallText::from_string("A\0B")));
2972    }
2973
2974    #[test]
2975    fn test_char_invalid_scalar_values_use_replacement_character() {
2976        let f = CharFunc;
2977        let result = f
2978            .invoke(&[
2979                SqliteValue::Integer(-1),
2980                SqliteValue::Integer(65),
2981                SqliteValue::Integer(1_114_112),
2982            ])
2983            .unwrap();
2984        assert_eq!(
2985            result,
2986            SqliteValue::Text(SmallText::from_string("\u{fffd}A\u{fffd}"))
2987        );
2988    }
2989
2990    // ── coalesce ─────────────────────────────────────────────────────────
2991
2992    #[test]
2993    fn test_coalesce_first_non_null() {
2994        let f = CoalesceFunc;
2995        let result = f
2996            .invoke(&[
2997                SqliteValue::Null,
2998                SqliteValue::Null,
2999                SqliteValue::Integer(3),
3000                SqliteValue::Integer(4),
3001            ])
3002            .unwrap();
3003        assert_eq!(result, SqliteValue::Integer(3));
3004    }
3005
3006    // ── concat ───────────────────────────────────────────────────────────
3007
3008    #[test]
3009    fn test_concat_null_as_empty() {
3010        let f = ConcatFunc;
3011        let result = f
3012            .invoke(&[
3013                SqliteValue::Null,
3014                SqliteValue::Text(SmallText::from_string("hello")),
3015                SqliteValue::Null,
3016            ])
3017            .unwrap();
3018        assert_eq!(result, SqliteValue::Text(SmallText::from_string("hello")));
3019    }
3020
3021    #[test]
3022    #[ignore = "perf-only benchmark"]
3023    fn perf_concat_text_args() {
3024        use std::hint::black_box;
3025        use std::time::Instant;
3026
3027        const TEXT_ARGS: usize = 24;
3028        const INVOCATIONS: usize = 50_000;
3029        const REPEATS: usize = 5;
3030
3031        let f = ConcatFunc;
3032        let mut args = Vec::with_capacity(TEXT_ARGS);
3033        for _ in 0..TEXT_ARGS {
3034            args.push(SqliteValue::Text(SmallText::from_string("payload")));
3035        }
3036
3037        let mut best_ns = u128::MAX;
3038        let mut result_len = 0usize;
3039        for _ in 0..REPEATS {
3040            let started = Instant::now();
3041            for _ in 0..INVOCATIONS {
3042                let result = black_box(
3043                    f.invoke(black_box(args.as_slice()))
3044                        .expect("concat benchmark invocation must succeed"),
3045                );
3046                result_len = match result {
3047                    SqliteValue::Text(text) => text.len(),
3048                    SqliteValue::Null
3049                    | SqliteValue::Integer(_)
3050                    | SqliteValue::Float(_)
3051                    | SqliteValue::Blob(_) => 0,
3052                };
3053            }
3054            best_ns = best_ns.min(started.elapsed().as_nanos());
3055        }
3056
3057        println!(
3058            "concat_text_args text_args={TEXT_ARGS} invocations={INVOCATIONS} repeats={REPEATS} best_ns={best_ns} result_len={result_len}"
3059        );
3060    }
3061
3062    // ── concat_ws ────────────────────────────────────────────────────────
3063
3064    #[test]
3065    fn test_concat_ws_null_skipped() {
3066        let f = ConcatWsFunc;
3067        let result = f
3068            .invoke(&[
3069                SqliteValue::Text(SmallText::from_string(",")),
3070                SqliteValue::Text(SmallText::from_string("a")),
3071                SqliteValue::Null,
3072                SqliteValue::Text(SmallText::from_string("b")),
3073            ])
3074            .unwrap();
3075        assert_eq!(result, SqliteValue::Text(SmallText::from_string("a,b")));
3076    }
3077
3078    #[test]
3079    fn test_concat_ws_empty_string_is_not_skipped() {
3080        let f = ConcatWsFunc;
3081        let result = f
3082            .invoke(&[
3083                SqliteValue::Text(SmallText::from_string("|")),
3084                SqliteValue::Text(SmallText::new("")),
3085                SqliteValue::Text(SmallText::from_string("x")),
3086            ])
3087            .unwrap();
3088        assert_eq!(result, SqliteValue::Text(SmallText::from_string("|x")));
3089    }
3090
3091    #[test]
3092    #[ignore = "perf-only benchmark"]
3093    fn perf_concat_ws_text_args() {
3094        use std::hint::black_box;
3095        use std::time::Instant;
3096
3097        const TEXT_ARGS: usize = 24;
3098        const INVOCATIONS: usize = 50_000;
3099        const REPEATS: usize = 5;
3100
3101        let f = ConcatWsFunc;
3102        let mut args = Vec::with_capacity(TEXT_ARGS + 1);
3103        args.push(SqliteValue::Text(SmallText::from_string(",")));
3104        for _ in 0..TEXT_ARGS {
3105            args.push(SqliteValue::Text(SmallText::from_string("payload")));
3106        }
3107
3108        let mut best_ns = u128::MAX;
3109        let mut result_len = 0usize;
3110        for _ in 0..REPEATS {
3111            let started = Instant::now();
3112            for _ in 0..INVOCATIONS {
3113                let result = black_box(
3114                    f.invoke(black_box(args.as_slice()))
3115                        .expect("concat_ws benchmark invocation must succeed"),
3116                );
3117                result_len = match result {
3118                    SqliteValue::Text(text) => text.len(),
3119                    SqliteValue::Null
3120                    | SqliteValue::Integer(_)
3121                    | SqliteValue::Float(_)
3122                    | SqliteValue::Blob(_) => 0,
3123                };
3124            }
3125            best_ns = best_ns.min(started.elapsed().as_nanos());
3126        }
3127
3128        println!(
3129            "concat_ws_text_args text_args={TEXT_ARGS} invocations={INVOCATIONS} repeats={REPEATS} best_ns={best_ns} result_len={result_len}"
3130        );
3131    }
3132
3133    // ── hex ──────────────────────────────────────────────────────────────
3134
3135    #[test]
3136    fn test_hex_blob() {
3137        let result = invoke1(
3138            &HexFunc,
3139            SqliteValue::Blob(Arc::from([0xDE, 0xAD, 0xBE, 0xEF].as_slice())),
3140        )
3141        .unwrap();
3142        assert_eq!(
3143            result,
3144            SqliteValue::Text(SmallText::from_string("DEADBEEF"))
3145        );
3146    }
3147
3148    #[test]
3149    fn test_hex_number_via_text() {
3150        // hex(42) encodes '42' as UTF-8 hex, not raw bits
3151        let result = invoke1(&HexFunc, SqliteValue::Integer(42)).unwrap();
3152        assert_eq!(result, SqliteValue::Text(SmallText::from_string("3432")));
3153    }
3154
3155    #[test]
3156    #[ignore = "perf-only benchmark"]
3157    fn perf_hex_text_blob_args() {
3158        use std::hint::black_box;
3159        use std::time::Instant;
3160
3161        const BYTES: usize = 24;
3162        const INVOCATIONS: usize = 100_000;
3163        const REPEATS: usize = 5;
3164
3165        let f = HexFunc;
3166        let text_args = [SqliteValue::Text(SmallText::from_string(
3167            "payload payload sentinel",
3168        ))];
3169        let blob_args = [SqliteValue::Blob(Arc::from([0xAB; BYTES].as_slice()))];
3170
3171        let mut text_best_ns = u128::MAX;
3172        let mut blob_best_ns = u128::MAX;
3173        let mut text_result_len = 0usize;
3174        let mut blob_result_len = 0usize;
3175        for _ in 0..REPEATS {
3176            let started = Instant::now();
3177            for _ in 0..INVOCATIONS {
3178                let result = black_box(
3179                    f.invoke(black_box(text_args.as_slice()))
3180                        .expect("hex text benchmark invocation must succeed"),
3181                );
3182                text_result_len = match result {
3183                    SqliteValue::Text(text) => text.len(),
3184                    SqliteValue::Null
3185                    | SqliteValue::Integer(_)
3186                    | SqliteValue::Float(_)
3187                    | SqliteValue::Blob(_) => 0,
3188                };
3189            }
3190            text_best_ns = text_best_ns.min(started.elapsed().as_nanos());
3191
3192            let started = Instant::now();
3193            for _ in 0..INVOCATIONS {
3194                let result = black_box(
3195                    f.invoke(black_box(blob_args.as_slice()))
3196                        .expect("hex blob benchmark invocation must succeed"),
3197                );
3198                blob_result_len = match result {
3199                    SqliteValue::Text(text) => text.len(),
3200                    SqliteValue::Null
3201                    | SqliteValue::Integer(_)
3202                    | SqliteValue::Float(_)
3203                    | SqliteValue::Blob(_) => 0,
3204                };
3205            }
3206            blob_best_ns = blob_best_ns.min(started.elapsed().as_nanos());
3207        }
3208
3209        println!(
3210            "hex_text_blob_args bytes={BYTES} invocations={INVOCATIONS} repeats={REPEATS} text_best_ns={text_best_ns} blob_best_ns={blob_best_ns} text_result_len={text_result_len} blob_result_len={blob_result_len}"
3211        );
3212    }
3213
3214    // ── iif ──────────────────────────────────────────────────────────────
3215
3216    #[test]
3217    fn test_iif_true() {
3218        let f = IifFunc;
3219        let result = f
3220            .invoke(&[
3221                SqliteValue::Integer(1),
3222                SqliteValue::Text(SmallText::from_string("yes")),
3223                SqliteValue::Text(SmallText::from_string("no")),
3224            ])
3225            .unwrap();
3226        assert_eq!(result, SqliteValue::Text(SmallText::from_string("yes")));
3227    }
3228
3229    #[test]
3230    fn test_iif_false() {
3231        let f = IifFunc;
3232        let result = f
3233            .invoke(&[
3234                SqliteValue::Integer(0),
3235                SqliteValue::Text(SmallText::from_string("yes")),
3236                SqliteValue::Text(SmallText::from_string("no")),
3237            ])
3238            .unwrap();
3239        assert_eq!(result, SqliteValue::Text(SmallText::from_string("no")));
3240    }
3241
3242    #[test]
3243    fn test_iif_whitespace_padded_text_truthy() {
3244        // Regression: IIF('  5  ', 'yes', 'no') must return 'yes'
3245        // because SQLite trims text before numeric coercion.
3246        let f = IifFunc;
3247        let result = f
3248            .invoke(&[
3249                SqliteValue::Text(SmallText::from_string("  5  ")),
3250                SqliteValue::Text(SmallText::from_string("yes")),
3251                SqliteValue::Text(SmallText::from_string("no")),
3252            ])
3253            .unwrap();
3254        assert_eq!(result, SqliteValue::Text(SmallText::from_string("yes")));
3255    }
3256
3257    // ── ifnull ───────────────────────────────────────────────────────────
3258
3259    #[test]
3260    fn test_ifnull_non_null() {
3261        assert_eq!(
3262            invoke2(
3263                &IfnullFunc,
3264                SqliteValue::Integer(5),
3265                SqliteValue::Integer(10)
3266            )
3267            .unwrap(),
3268            SqliteValue::Integer(5)
3269        );
3270    }
3271
3272    #[test]
3273    fn test_ifnull_null() {
3274        assert_eq!(
3275            invoke2(&IfnullFunc, SqliteValue::Null, SqliteValue::Integer(10)).unwrap(),
3276            SqliteValue::Integer(10)
3277        );
3278    }
3279
3280    // ── instr ────────────────────────────────────────────────────────────
3281
3282    #[test]
3283    fn test_instr_found() {
3284        assert_eq!(
3285            invoke2(
3286                &InstrFunc,
3287                SqliteValue::Text(SmallText::from_string("hello world")),
3288                SqliteValue::Text(SmallText::from_string("world"))
3289            )
3290            .unwrap(),
3291            SqliteValue::Integer(7)
3292        );
3293    }
3294
3295    #[test]
3296    fn test_instr_not_found() {
3297        assert_eq!(
3298            invoke2(
3299                &InstrFunc,
3300                SqliteValue::Text(SmallText::from_string("hello")),
3301                SqliteValue::Text(SmallText::from_string("xyz"))
3302            )
3303            .unwrap(),
3304            SqliteValue::Integer(0)
3305        );
3306    }
3307
3308    #[test]
3309    fn test_instr_empty_needle_returns_one() {
3310        // SQLite: instr(X, '') returns 1 (empty string found at position 1).
3311        assert_eq!(
3312            invoke2(
3313                &InstrFunc,
3314                SqliteValue::Text(SmallText::from_string("hello")),
3315                SqliteValue::Text(SmallText::new(""))
3316            )
3317            .unwrap(),
3318            SqliteValue::Integer(1)
3319        );
3320    }
3321
3322    #[test]
3323    fn test_instr_empty_haystack_returns_zero() {
3324        assert_eq!(
3325            invoke2(
3326                &InstrFunc,
3327                SqliteValue::Text(SmallText::new("")),
3328                SqliteValue::Text(SmallText::from_string("x"))
3329            )
3330            .unwrap(),
3331            SqliteValue::Integer(0)
3332        );
3333    }
3334
3335    #[test]
3336    fn test_instr_blob_empty_needle_returns_one() {
3337        // SQLite: instr(X, x'') returns 1 (empty blob found at position 1).
3338        assert_eq!(
3339            invoke2(
3340                &InstrFunc,
3341                SqliteValue::Blob(Arc::from([1, 2, 3].as_slice())),
3342                SqliteValue::Blob(Arc::from([].as_slice()))
3343            )
3344            .unwrap(),
3345            SqliteValue::Integer(1)
3346        );
3347    }
3348
3349    #[test]
3350    #[ignore = "perf-only benchmark"]
3351    fn perf_instr_text_args() {
3352        use std::hint::black_box;
3353        use std::time::Instant;
3354
3355        const INVOCATIONS: usize = 100_000;
3356        const REPEATS: usize = 5;
3357
3358        let f = InstrFunc;
3359        let args = [
3360            SqliteValue::Text(SmallText::from_string("payload payload sentinel")),
3361            SqliteValue::Text(SmallText::from_string("sentinel")),
3362        ];
3363
3364        let mut best_ns = u128::MAX;
3365        let mut result_value = 0i64;
3366        for _ in 0..REPEATS {
3367            let started = Instant::now();
3368            for _ in 0..INVOCATIONS {
3369                let result = black_box(
3370                    f.invoke(black_box(args.as_slice()))
3371                        .expect("instr benchmark invocation must succeed"),
3372                );
3373                result_value = match result {
3374                    SqliteValue::Integer(value) => value,
3375                    SqliteValue::Null
3376                    | SqliteValue::Float(_)
3377                    | SqliteValue::Text(_)
3378                    | SqliteValue::Blob(_) => 0,
3379                };
3380            }
3381            best_ns = best_ns.min(started.elapsed().as_nanos());
3382        }
3383
3384        println!(
3385            "instr_text_args invocations={INVOCATIONS} repeats={REPEATS} best_ns={best_ns} result_value={result_value}"
3386        );
3387    }
3388
3389    // ── length ───────────────────────────────────────────────────────────
3390
3391    #[test]
3392    fn test_length_text_chars() {
3393        // café is 4 characters, 5 bytes
3394        assert_eq!(
3395            invoke1(
3396                &LengthFunc,
3397                SqliteValue::Text(SmallText::from_string("café"))
3398            )
3399            .unwrap(),
3400            SqliteValue::Integer(4)
3401        );
3402    }
3403
3404    #[test]
3405    fn test_length_text_stops_at_nul() {
3406        assert_eq!(
3407            invoke1(
3408                &LengthFunc,
3409                SqliteValue::Text(SmallText::from_string("A\0B"))
3410            )
3411            .unwrap(),
3412            SqliteValue::Integer(1)
3413        );
3414        assert_eq!(
3415            invoke1(
3416                &LengthFunc,
3417                SqliteValue::Text(SmallText::from_string("\0A"))
3418            )
3419            .unwrap(),
3420            SqliteValue::Integer(0)
3421        );
3422    }
3423
3424    #[test]
3425    fn test_length_blob_bytes() {
3426        assert_eq!(
3427            invoke1(&LengthFunc, SqliteValue::Blob(Arc::from([1, 2].as_slice()))).unwrap(),
3428            SqliteValue::Integer(2)
3429        );
3430    }
3431
3432    // ── octet_length ─────────────────────────────────────────────────────
3433
3434    #[test]
3435    fn test_octet_length_multibyte() {
3436        // café: 'c'=1, 'a'=1, 'f'=1, 'é'=2 bytes = 5 bytes total
3437        assert_eq!(
3438            invoke1(
3439                &OctetLengthFunc,
3440                SqliteValue::Text(SmallText::from_string("café"))
3441            )
3442            .unwrap(),
3443            SqliteValue::Integer(5)
3444        );
3445    }
3446
3447    // ── lower/upper ──────────────────────────────────────────────────────
3448
3449    #[test]
3450    fn test_lower_ascii() {
3451        assert_eq!(
3452            invoke1(
3453                &LowerFunc,
3454                SqliteValue::Text(SmallText::from_string("HELLO"))
3455            )
3456            .unwrap(),
3457            SqliteValue::Text(SmallText::from_string("hello"))
3458        );
3459    }
3460
3461    #[test]
3462    fn test_upper_ascii() {
3463        assert_eq!(
3464            invoke1(
3465                &UpperFunc,
3466                SqliteValue::Text(SmallText::from_string("hello"))
3467            )
3468            .unwrap(),
3469            SqliteValue::Text(SmallText::from_string("HELLO"))
3470        );
3471    }
3472
3473    // ── trim/ltrim/rtrim ─────────────────────────────────────────────────
3474
3475    #[test]
3476    fn test_trim_default() {
3477        let f = TrimFunc;
3478        assert_eq!(
3479            f.invoke(&[SqliteValue::Text(SmallText::from_string("  hello  "))])
3480                .unwrap(),
3481            SqliteValue::Text(SmallText::from_string("hello"))
3482        );
3483    }
3484
3485    #[test]
3486    fn test_ltrim_default() {
3487        let f = LtrimFunc;
3488        assert_eq!(
3489            f.invoke(&[SqliteValue::Text(SmallText::from_string("  hello"))])
3490                .unwrap(),
3491            SqliteValue::Text(SmallText::from_string("hello"))
3492        );
3493    }
3494
3495    #[test]
3496    fn test_ltrim_custom() {
3497        let f = LtrimFunc;
3498        assert_eq!(
3499            f.invoke(&[
3500                SqliteValue::Text(SmallText::from_string("xxhello")),
3501                SqliteValue::Text(SmallText::from_string("x")),
3502            ])
3503            .unwrap(),
3504            SqliteValue::Text(SmallText::from_string("hello"))
3505        );
3506    }
3507
3508    #[test]
3509    #[ignore = "perf-only benchmark"]
3510    fn perf_trim_text_args() {
3511        use std::hint::black_box;
3512        use std::time::Instant;
3513
3514        const INVOCATIONS: usize = 100_000;
3515        const REPEATS: usize = 5;
3516
3517        let trim = TrimFunc;
3518        let ltrim = LtrimFunc;
3519        let rtrim = RtrimFunc;
3520        let default_args = [SqliteValue::Text(SmallText::from_string("   payload   "))];
3521        let custom_args = [
3522            SqliteValue::Text(SmallText::from_string("xxxpayloadxxx")),
3523            SqliteValue::Text(SmallText::from_string("x")),
3524        ];
3525
3526        let mut trim_best_ns = u128::MAX;
3527        let mut ltrim_best_ns = u128::MAX;
3528        let mut rtrim_best_ns = u128::MAX;
3529        let mut custom_best_ns = u128::MAX;
3530        let mut result_len = 0usize;
3531
3532        for _ in 0..REPEATS {
3533            let started = Instant::now();
3534            for _ in 0..INVOCATIONS {
3535                let result = black_box(
3536                    trim.invoke(black_box(default_args.as_slice()))
3537                        .expect("trim benchmark invocation must succeed"),
3538                );
3539                result_len = match result {
3540                    SqliteValue::Text(text) => text.len(),
3541                    SqliteValue::Null
3542                    | SqliteValue::Integer(_)
3543                    | SqliteValue::Float(_)
3544                    | SqliteValue::Blob(_) => 0,
3545                };
3546            }
3547            trim_best_ns = trim_best_ns.min(started.elapsed().as_nanos());
3548
3549            let started = Instant::now();
3550            for _ in 0..INVOCATIONS {
3551                let result = black_box(
3552                    ltrim
3553                        .invoke(black_box(default_args.as_slice()))
3554                        .expect("ltrim benchmark invocation must succeed"),
3555                );
3556                result_len = match result {
3557                    SqliteValue::Text(text) => text.len(),
3558                    SqliteValue::Null
3559                    | SqliteValue::Integer(_)
3560                    | SqliteValue::Float(_)
3561                    | SqliteValue::Blob(_) => 0,
3562                };
3563            }
3564            ltrim_best_ns = ltrim_best_ns.min(started.elapsed().as_nanos());
3565
3566            let started = Instant::now();
3567            for _ in 0..INVOCATIONS {
3568                let result = black_box(
3569                    rtrim
3570                        .invoke(black_box(default_args.as_slice()))
3571                        .expect("rtrim benchmark invocation must succeed"),
3572                );
3573                result_len = match result {
3574                    SqliteValue::Text(text) => text.len(),
3575                    SqliteValue::Null
3576                    | SqliteValue::Integer(_)
3577                    | SqliteValue::Float(_)
3578                    | SqliteValue::Blob(_) => 0,
3579                };
3580            }
3581            rtrim_best_ns = rtrim_best_ns.min(started.elapsed().as_nanos());
3582
3583            let started = Instant::now();
3584            for _ in 0..INVOCATIONS {
3585                let result = black_box(
3586                    trim.invoke(black_box(custom_args.as_slice()))
3587                        .expect("custom trim benchmark invocation must succeed"),
3588                );
3589                result_len = match result {
3590                    SqliteValue::Text(text) => text.len(),
3591                    SqliteValue::Null
3592                    | SqliteValue::Integer(_)
3593                    | SqliteValue::Float(_)
3594                    | SqliteValue::Blob(_) => 0,
3595                };
3596            }
3597            custom_best_ns = custom_best_ns.min(started.elapsed().as_nanos());
3598        }
3599
3600        println!(
3601            "trim_text_args invocations={INVOCATIONS} repeats={REPEATS} trim_best_ns={trim_best_ns} ltrim_best_ns={ltrim_best_ns} rtrim_best_ns={rtrim_best_ns} custom_best_ns={custom_best_ns} result_len={result_len}"
3602        );
3603    }
3604
3605    // ── nullif ───────────────────────────────────────────────────────────
3606
3607    #[test]
3608    fn test_nullif_equal() {
3609        assert_eq!(
3610            invoke2(
3611                &NullifFunc,
3612                SqliteValue::Integer(5),
3613                SqliteValue::Integer(5)
3614            )
3615            .unwrap(),
3616            SqliteValue::Null
3617        );
3618    }
3619
3620    #[test]
3621    fn test_nullif_different() {
3622        assert_eq!(
3623            invoke2(
3624                &NullifFunc,
3625                SqliteValue::Integer(5),
3626                SqliteValue::Integer(3)
3627            )
3628            .unwrap(),
3629            SqliteValue::Integer(5)
3630        );
3631    }
3632
3633    // ── typeof ───────────────────────────────────────────────────────────
3634
3635    #[test]
3636    fn test_typeof_each() {
3637        assert_eq!(
3638            invoke1(&TypeofFunc, SqliteValue::Null).unwrap(),
3639            SqliteValue::Text(SmallText::from_string("null"))
3640        );
3641        assert_eq!(
3642            invoke1(&TypeofFunc, SqliteValue::Integer(1)).unwrap(),
3643            SqliteValue::Text(SmallText::from_string("integer"))
3644        );
3645        assert_eq!(
3646            invoke1(&TypeofFunc, SqliteValue::Float(1.0)).unwrap(),
3647            SqliteValue::Text(SmallText::from_string("real"))
3648        );
3649        assert_eq!(
3650            invoke1(&TypeofFunc, SqliteValue::Text(SmallText::from_string("x"))).unwrap(),
3651            SqliteValue::Text(SmallText::from_string("text"))
3652        );
3653        assert_eq!(
3654            invoke1(&TypeofFunc, SqliteValue::Blob(Arc::from([0].as_slice()))).unwrap(),
3655            SqliteValue::Text(SmallText::from_string("blob"))
3656        );
3657    }
3658
3659    // ── subtype ──────────────────────────────────────────────────────────
3660
3661    #[test]
3662    fn test_subtype_null_returns_zero() {
3663        assert_eq!(
3664            invoke1(&SubtypeFunc, SqliteValue::Null).unwrap(),
3665            SqliteValue::Integer(0)
3666        );
3667    }
3668
3669    // ── replace ──────────────────────────────────────────────────────────
3670
3671    #[test]
3672    fn test_replace_basic() {
3673        let f = ReplaceFunc;
3674        assert_eq!(
3675            f.invoke(&[
3676                SqliteValue::Text(SmallText::from_string("hello world")),
3677                SqliteValue::Text(SmallText::from_string("world")),
3678                SqliteValue::Text(SmallText::from_string("earth")),
3679            ])
3680            .unwrap(),
3681            SqliteValue::Text(SmallText::from_string("hello earth"))
3682        );
3683    }
3684
3685    #[test]
3686    fn test_replace_empty_y() {
3687        let f = ReplaceFunc;
3688        assert_eq!(
3689            f.invoke(&[
3690                SqliteValue::Text(SmallText::from_string("hello")),
3691                SqliteValue::Text(SmallText::new("")),
3692                SqliteValue::Text(SmallText::from_string("x")),
3693            ])
3694            .unwrap(),
3695            SqliteValue::Text(SmallText::from_string("hello"))
3696        );
3697    }
3698
3699    #[test]
3700    #[ignore = "perf-only benchmark"]
3701    fn perf_replace_text_args() {
3702        use std::hint::black_box;
3703        use std::time::Instant;
3704
3705        const INVOCATIONS: usize = 100_000;
3706        const REPEATS: usize = 5;
3707
3708        let f = ReplaceFunc;
3709        let args = [
3710            SqliteValue::Text(SmallText::from_string("payload payload payload")),
3711            SqliteValue::Text(SmallText::from_string("zz")),
3712            SqliteValue::Text(SmallText::from_string("replacement")),
3713        ];
3714
3715        let mut best_ns = u128::MAX;
3716        let mut result_len = 0usize;
3717        for _ in 0..REPEATS {
3718            let started = Instant::now();
3719            for _ in 0..INVOCATIONS {
3720                let result = black_box(
3721                    f.invoke(black_box(args.as_slice()))
3722                        .expect("replace benchmark invocation must succeed"),
3723                );
3724                result_len = match result {
3725                    SqliteValue::Text(text) => text.len(),
3726                    SqliteValue::Null
3727                    | SqliteValue::Integer(_)
3728                    | SqliteValue::Float(_)
3729                    | SqliteValue::Blob(_) => 0,
3730                };
3731            }
3732            best_ns = best_ns.min(started.elapsed().as_nanos());
3733        }
3734
3735        println!(
3736            "replace_text_args invocations={INVOCATIONS} repeats={REPEATS} best_ns={best_ns} result_len={result_len}"
3737        );
3738    }
3739
3740    // ── round ────────────────────────────────────────────────────────────
3741
3742    #[test]
3743    #[allow(clippy::float_cmp)]
3744    fn test_round_half_away() {
3745        // round(2.5) = 3.0, round(-2.5) = -3.0
3746        assert_eq!(
3747            RoundFunc.invoke(&[SqliteValue::Float(2.5)]).unwrap(),
3748            SqliteValue::Float(3.0)
3749        );
3750        assert_eq!(
3751            RoundFunc.invoke(&[SqliteValue::Float(-2.5)]).unwrap(),
3752            SqliteValue::Float(-3.0)
3753        );
3754    }
3755
3756    #[test]
3757    #[allow(clippy::float_cmp, clippy::approx_constant)]
3758    fn test_round_precision() {
3759        assert_eq!(
3760            RoundFunc
3761                .invoke(&[SqliteValue::Float(3.14159), SqliteValue::Integer(2)])
3762                .unwrap(),
3763            SqliteValue::Float(3.14)
3764        );
3765    }
3766
3767    #[test]
3768    #[allow(clippy::float_cmp)]
3769    fn test_round_extreme_n_clamped() {
3770        // N > 30 is clamped to 30 (matches C SQLite)
3771        assert_eq!(
3772            RoundFunc
3773                .invoke(&[SqliteValue::Float(1.5), SqliteValue::Integer(400)])
3774                .unwrap(),
3775            RoundFunc
3776                .invoke(&[SqliteValue::Float(1.5), SqliteValue::Integer(30)])
3777                .unwrap(),
3778        );
3779        // Negative N is clamped to 0 (matches C SQLite)
3780        assert_eq!(
3781            RoundFunc
3782                .invoke(&[SqliteValue::Float(2.5), SqliteValue::Integer(-5)])
3783                .unwrap(),
3784            SqliteValue::Float(3.0)
3785        );
3786        // i64::MAX is clamped to 30
3787        let result = RoundFunc
3788            .invoke(&[SqliteValue::Float(1.5), SqliteValue::Integer(i64::MAX)])
3789            .unwrap();
3790        if let SqliteValue::Float(v) = result {
3791            assert!(!v.is_nan(), "round must never return NaN");
3792        }
3793    }
3794
3795    #[test]
3796    #[allow(clippy::float_cmp)]
3797    fn test_round_large_value_no_fractional() {
3798        // Values beyond 2^52 have no fractional part — returned unchanged
3799        let big = 9_007_199_254_740_993.0_f64;
3800        assert_eq!(
3801            RoundFunc.invoke(&[SqliteValue::Float(big)]).unwrap(),
3802            SqliteValue::Float(big)
3803        );
3804        assert_eq!(
3805            RoundFunc.invoke(&[SqliteValue::Float(-big)]).unwrap(),
3806            SqliteValue::Float(-big)
3807        );
3808    }
3809
3810    // ── sign ─────────────────────────────────────────────────────────────
3811
3812    #[test]
3813    fn test_sign_positive() {
3814        assert_eq!(
3815            invoke1(&SignFunc, SqliteValue::Integer(42)).unwrap(),
3816            SqliteValue::Integer(1)
3817        );
3818    }
3819
3820    #[test]
3821    fn test_sign_negative() {
3822        assert_eq!(
3823            invoke1(&SignFunc, SqliteValue::Integer(-42)).unwrap(),
3824            SqliteValue::Integer(-1)
3825        );
3826    }
3827
3828    #[test]
3829    fn test_sign_zero() {
3830        assert_eq!(
3831            invoke1(&SignFunc, SqliteValue::Integer(0)).unwrap(),
3832            SqliteValue::Integer(0)
3833        );
3834    }
3835
3836    #[test]
3837    fn test_sign_null() {
3838        assert_eq!(
3839            invoke1(&SignFunc, SqliteValue::Null).unwrap(),
3840            SqliteValue::Null
3841        );
3842    }
3843
3844    #[test]
3845    fn test_sign_non_numeric() {
3846        // C SQLite: math functions return NULL for strings that cannot be parsed as numeric.
3847        assert_eq!(
3848            invoke1(&SignFunc, SqliteValue::Text(SmallText::from_string("abc"))).unwrap(),
3849            SqliteValue::Null
3850        );
3851    }
3852
3853    #[test]
3854    fn test_sign_whitespace_padded_text() {
3855        // Regression: SIGN('  5  ') must return 1, not NULL.
3856        // SQLite trims ASCII whitespace before numeric parsing.
3857        assert_eq!(
3858            invoke1(
3859                &SignFunc,
3860                SqliteValue::Text(SmallText::from_string("  5  "))
3861            )
3862            .unwrap(),
3863            SqliteValue::Integer(1)
3864        );
3865        assert_eq!(
3866            invoke1(
3867                &SignFunc,
3868                SqliteValue::Text(SmallText::from_string("  -3.14  "))
3869            )
3870            .unwrap(),
3871            SqliteValue::Integer(-1)
3872        );
3873    }
3874
3875    #[test]
3876    fn test_sign_unicode_space_and_blob_return_null() {
3877        assert_eq!(
3878            invoke1(
3879                &SignFunc,
3880                SqliteValue::Text(SmallText::from_string("\u{00a0}123"))
3881            )
3882            .unwrap(),
3883            SqliteValue::Null
3884        );
3885        assert_eq!(
3886            invoke1(&SignFunc, SqliteValue::Blob(Arc::from(b"123".as_slice()))).unwrap(),
3887            SqliteValue::Null
3888        );
3889    }
3890
3891    #[test]
3892    fn test_sign_nan_inf_text_returns_null() {
3893        // C SQLite doesn't recognise "NaN", "inf", "Infinity" etc. as numeric —
3894        // sign() must return NULL for these, matching the C oracle.
3895        for s in &[
3896            "NaN",
3897            "nan",
3898            "inf",
3899            "-inf",
3900            "Infinity",
3901            "-Infinity",
3902            "INF",
3903            "+nan",
3904            "+inf",
3905        ] {
3906            assert_eq!(
3907                invoke1(&SignFunc, SqliteValue::Text(SmallText::from_string(*s))).unwrap(),
3908                SqliteValue::Null,
3909                "sign('{s}') should be NULL"
3910            );
3911        }
3912    }
3913
3914    #[test]
3915    fn test_sign_numeric_overflow_to_infinity() {
3916        // "1e999" overflows to +inf in both Rust and C. C SQLite's sqlite3AtoF
3917        // accepts it as numeric, so sign() must return 1 (not NULL).
3918        assert_eq!(
3919            invoke1(
3920                &SignFunc,
3921                SqliteValue::Text(SmallText::from_string("1e999"))
3922            )
3923            .unwrap(),
3924            SqliteValue::Integer(1)
3925        );
3926        assert_eq!(
3927            invoke1(
3928                &SignFunc,
3929                SqliteValue::Text(SmallText::from_string("-1e999"))
3930            )
3931            .unwrap(),
3932            SqliteValue::Integer(-1)
3933        );
3934        // Underflow to zero
3935        assert_eq!(
3936            invoke1(
3937                &SignFunc,
3938                SqliteValue::Text(SmallText::from_string("1e-999"))
3939            )
3940            .unwrap(),
3941            SqliteValue::Integer(0)
3942        );
3943    }
3944
3945    #[test]
3946    fn test_sign_float_nan_returns_null() {
3947        // C SQLite: sign(0.0/0.0) = NULL. Float NaN must not return 0.
3948        assert_eq!(
3949            invoke1(&SignFunc, SqliteValue::Float(f64::NAN)).unwrap(),
3950            SqliteValue::Null
3951        );
3952    }
3953
3954    // ── scalar max/min ───────────────────────────────────────────────────
3955
3956    #[test]
3957    fn test_scalar_max_null() {
3958        let f = ScalarMaxFunc;
3959        let result = f
3960            .invoke(&[
3961                SqliteValue::Integer(1),
3962                SqliteValue::Null,
3963                SqliteValue::Integer(3),
3964            ])
3965            .unwrap();
3966        assert_eq!(result, SqliteValue::Null);
3967    }
3968
3969    #[test]
3970    fn test_scalar_max_values() {
3971        let f = ScalarMaxFunc;
3972        let result = f
3973            .invoke(&[
3974                SqliteValue::Integer(3),
3975                SqliteValue::Integer(1),
3976                SqliteValue::Integer(2),
3977            ])
3978            .unwrap();
3979        assert_eq!(result, SqliteValue::Integer(3));
3980    }
3981
3982    #[test]
3983    fn test_scalar_min_null() {
3984        let f = ScalarMinFunc;
3985        let result = f
3986            .invoke(&[
3987                SqliteValue::Integer(1),
3988                SqliteValue::Null,
3989                SqliteValue::Integer(3),
3990            ])
3991            .unwrap();
3992        assert_eq!(result, SqliteValue::Null);
3993    }
3994
3995    // ── quote ────────────────────────────────────────────────────────────
3996
3997    #[test]
3998    fn test_quote_text() {
3999        assert_eq!(
4000            invoke1(
4001                &QuoteFunc,
4002                SqliteValue::Text(SmallText::from_string("it's"))
4003            )
4004            .unwrap(),
4005            SqliteValue::Text(SmallText::from_string("'it''s'"))
4006        );
4007    }
4008
4009    #[test]
4010    fn test_quote_null() {
4011        assert_eq!(
4012            invoke1(&QuoteFunc, SqliteValue::Null).unwrap(),
4013            SqliteValue::Text(SmallText::from_string("NULL"))
4014        );
4015    }
4016
4017    #[test]
4018    fn test_quote_blob() {
4019        assert_eq!(
4020            invoke1(&QuoteFunc, SqliteValue::Blob(Arc::from([0xAB].as_slice()))).unwrap(),
4021            SqliteValue::Text(SmallText::from_string("X'AB'"))
4022        );
4023    }
4024
4025    #[test]
4026    fn test_quote_text_truncates_at_first_nul() {
4027        assert_eq!(
4028            invoke1(
4029                &QuoteFunc,
4030                SqliteValue::Text(SmallText::from_string("A\0B"))
4031            )
4032            .unwrap(),
4033            SqliteValue::Text(SmallText::from_string("'A'"))
4034        );
4035    }
4036
4037    #[test]
4038    fn test_unistr_quote_plain_text_matches_quote() {
4039        assert_eq!(
4040            invoke1(
4041                &UnistrQuoteFunc,
4042                SqliteValue::Text(SmallText::from_string("it's"))
4043            )
4044            .unwrap(),
4045            SqliteValue::Text(SmallText::from_string("'it''s'"))
4046        );
4047    }
4048
4049    #[test]
4050    fn test_unistr_quote_escapes_control_chars_and_backslashes() {
4051        assert_eq!(
4052            invoke1(
4053                &UnistrQuoteFunc,
4054                SqliteValue::Text(SmallText::from_string("a\nb\\c\x01d"))
4055            )
4056            .unwrap(),
4057            SqliteValue::Text(SmallText::from_string("unistr('a\\u000ab\\\\c\\u0001d')"))
4058        );
4059    }
4060
4061    #[test]
4062    fn test_unistr_quote_truncates_at_first_nul_before_wrapping() {
4063        assert_eq!(
4064            invoke1(
4065                &UnistrQuoteFunc,
4066                SqliteValue::Text(SmallText::from_string("A\0\nB"))
4067            )
4068            .unwrap(),
4069            SqliteValue::Text(SmallText::from_string("'A'"))
4070        );
4071    }
4072
4073    #[test]
4074    fn test_unistr_decodes_backslash_and_unicode_escapes() {
4075        assert_eq!(
4076            invoke1(
4077                &UnistrFunc,
4078                SqliteValue::Text(SmallText::from_string(
4079                    "a\\\\b\\u0020\\U0001f600\\0041\\+000042"
4080                ))
4081            )
4082            .unwrap(),
4083            SqliteValue::Text(SmallText::from_string("a\\b \u{1f600}AB"))
4084        );
4085    }
4086
4087    #[test]
4088    fn test_unistr_invalid_escape_returns_error() {
4089        for input in [
4090            "\\u12xz",
4091            "\\12xz",
4092            "\\+00xz",
4093            "\\",
4094            "\\x",
4095            "\\U00110000",
4096            "\\D800",
4097        ] {
4098            let err = invoke1(
4099                &UnistrFunc,
4100                SqliteValue::Text(SmallText::from_string(input)),
4101            )
4102            .unwrap_err();
4103            assert_eq!(err.to_string(), INVALID_UNISTR_ESCAPE);
4104        }
4105    }
4106
4107    #[test]
4108    #[ignore = "perf-only benchmark"]
4109    fn perf_unistr_text_args() {
4110        use std::hint::black_box;
4111        use std::time::Instant;
4112
4113        const INVOCATIONS: usize = 500_000;
4114        const REPEATS: usize = 7;
4115
4116        let f = UnistrFunc;
4117        let plain_args = [SqliteValue::Text(SmallText::from_string(
4118            "plain unicode payload",
4119        ))];
4120        let escaped_args = [SqliteValue::Text(SmallText::from_string(
4121            "a\\\\b\\u0020\\u0048\\u0069\\U0001f600",
4122        ))];
4123
4124        let mut plain_best_ns = u128::MAX;
4125        let mut escaped_best_ns = u128::MAX;
4126        let mut checksum = 0usize;
4127        for _ in 0..REPEATS {
4128            let started = Instant::now();
4129            for _ in 0..INVOCATIONS {
4130                let result = black_box(
4131                    f.invoke(black_box(plain_args.as_slice()))
4132                        .expect("unistr plain benchmark invocation must succeed"),
4133                );
4134                if let SqliteValue::Text(text) = result {
4135                    checksum = checksum.wrapping_add(text.len());
4136                }
4137            }
4138            plain_best_ns = plain_best_ns.min(started.elapsed().as_nanos());
4139
4140            let started = Instant::now();
4141            for _ in 0..INVOCATIONS {
4142                let result = black_box(
4143                    f.invoke(black_box(escaped_args.as_slice()))
4144                        .expect("unistr escaped benchmark invocation must succeed"),
4145                );
4146                if let SqliteValue::Text(text) = result {
4147                    checksum = checksum.wrapping_add(text.len());
4148                }
4149            }
4150            escaped_best_ns = escaped_best_ns.min(started.elapsed().as_nanos());
4151        }
4152
4153        println!(
4154            "unistr_text_args invocations={INVOCATIONS} repeats={REPEATS} plain_best_ns={plain_best_ns} escaped_best_ns={escaped_best_ns} checksum={checksum}"
4155        );
4156    }
4157
4158    // ── random ───────────────────────────────────────────────────────────
4159
4160    #[test]
4161    fn test_random_range() {
4162        let f = RandomFunc;
4163        let result = f.invoke(&[]).unwrap();
4164        assert!(matches!(result, SqliteValue::Integer(_)));
4165    }
4166
4167    // ── randomblob ───────────────────────────────────────────────────────
4168
4169    #[test]
4170    fn test_randomblob_length() {
4171        let result = invoke1(&RandomblobFunc, SqliteValue::Integer(16)).unwrap();
4172        match result {
4173            SqliteValue::Blob(b) => assert_eq!(b.len(), 16),
4174            other => unreachable!("expected blob, got {other:?}"),
4175        }
4176    }
4177
4178    #[test]
4179    fn test_randomblob_null_zero_and_negative_lengths_are_one_byte() {
4180        for arg in [
4181            SqliteValue::Null,
4182            SqliteValue::Integer(0),
4183            SqliteValue::Integer(-5),
4184        ] {
4185            let result = invoke1(&RandomblobFunc, arg).unwrap();
4186            match result {
4187                SqliteValue::Blob(b) => assert_eq!(b.len(), 1),
4188                other => unreachable!("expected one-byte blob, got {other:?}"),
4189            }
4190        }
4191    }
4192
4193    // ── zeroblob ─────────────────────────────────────────────────────────
4194
4195    #[test]
4196    fn test_zeroblob_length() {
4197        let result = invoke1(&ZeroblobFunc, SqliteValue::Integer(100)).unwrap();
4198        match result {
4199            SqliteValue::Blob(b) => {
4200                assert_eq!(b.len(), 100);
4201                assert!(b.iter().all(|&x| x == 0));
4202            }
4203            other => unreachable!("expected blob, got {other:?}"),
4204        }
4205    }
4206
4207    // ── unhex ────────────────────────────────────────────────────────────
4208
4209    #[test]
4210    fn test_unhex_valid() {
4211        let result = invoke1(
4212            &UnhexFunc,
4213            SqliteValue::Text(SmallText::from_string("48656C6C6F")),
4214        )
4215        .unwrap();
4216        assert_eq!(result, SqliteValue::Blob(Arc::from(b"Hello".as_slice())));
4217    }
4218
4219    #[test]
4220    fn test_unhex_invalid() {
4221        let result = invoke1(
4222            &UnhexFunc,
4223            SqliteValue::Text(SmallText::from_string("ZZZZ")),
4224        )
4225        .unwrap();
4226        assert_eq!(result, SqliteValue::Null);
4227    }
4228
4229    #[test]
4230    fn test_unhex_ignore_chars() {
4231        let f = UnhexFunc;
4232        let result = f
4233            .invoke(&[
4234                SqliteValue::Text(SmallText::from_string("48-65-6C")),
4235                SqliteValue::Text(SmallText::from_string("-")),
4236            ])
4237            .unwrap();
4238        assert_eq!(result, SqliteValue::Blob(Arc::from(b"Hel".as_slice())));
4239    }
4240
4241    #[test]
4242    fn test_unhex_ignore_chars_only_between_byte_pairs() {
4243        let f = UnhexFunc;
4244        let result = f
4245            .invoke(&[
4246                SqliteValue::Text(SmallText::from_string("AB CD")),
4247                SqliteValue::Text(SmallText::from_string(" ")),
4248            ])
4249            .unwrap();
4250        assert_eq!(result, SqliteValue::Blob(Arc::from([0xAB, 0xCD])));
4251
4252        let result = f
4253            .invoke(&[
4254                SqliteValue::Text(SmallText::from_string("A BCD")),
4255                SqliteValue::Text(SmallText::from_string(" ")),
4256            ])
4257            .unwrap();
4258        assert_eq!(result, SqliteValue::Null);
4259    }
4260
4261    #[test]
4262    fn test_unhex_null_ignore_argument_returns_null() {
4263        let f = UnhexFunc;
4264        let result = f
4265            .invoke(&[
4266                SqliteValue::Text(SmallText::from_string("41")),
4267                SqliteValue::Null,
4268            ])
4269            .unwrap();
4270        assert_eq!(result, SqliteValue::Null);
4271    }
4272
4273    #[test]
4274    fn test_unhex_hex_digits_in_ignore_argument_do_not_ignore_digits() {
4275        let f = UnhexFunc;
4276        let result = f
4277            .invoke(&[
4278                SqliteValue::Text(SmallText::from_string("41")),
4279                SqliteValue::Text(SmallText::from_string("4")),
4280            ])
4281            .unwrap();
4282        assert_eq!(result, SqliteValue::Blob(Arc::from(b"A".as_slice())));
4283    }
4284
4285    #[test]
4286    #[ignore = "perf-only benchmark"]
4287    fn perf_unhex_text_args() {
4288        use std::hint::black_box;
4289        use std::time::Instant;
4290
4291        const INVOCATIONS: usize = 300_000;
4292        const REPEATS: usize = 7;
4293
4294        let f = UnhexFunc;
4295        let plain_args = [SqliteValue::Text(SmallText::from_string(
4296            "48656C6C6F776F726C64",
4297        ))];
4298        let ignore_args = [
4299            SqliteValue::Text(SmallText::from_string("48-65-6C-6C-6F")),
4300            SqliteValue::Text(SmallText::from_string("-")),
4301        ];
4302        let mut plain_best_ns = u128::MAX;
4303        let mut ignore_best_ns = u128::MAX;
4304        let mut checksum = 0usize;
4305
4306        for _ in 0..REPEATS {
4307            let started = Instant::now();
4308            for _ in 0..INVOCATIONS {
4309                let result = black_box(
4310                    f.invoke(black_box(plain_args.as_slice()))
4311                        .expect("unhex benchmark invocation must succeed"),
4312                );
4313                if let SqliteValue::Blob(blob) = result {
4314                    checksum = checksum.wrapping_add(blob.len());
4315                }
4316            }
4317            plain_best_ns = plain_best_ns.min(started.elapsed().as_nanos());
4318
4319            let started = Instant::now();
4320            for _ in 0..INVOCATIONS {
4321                let result = black_box(
4322                    f.invoke(black_box(ignore_args.as_slice()))
4323                        .expect("unhex ignore benchmark invocation must succeed"),
4324                );
4325                if let SqliteValue::Blob(blob) = result {
4326                    checksum = checksum.wrapping_add(blob.len());
4327                }
4328            }
4329            ignore_best_ns = ignore_best_ns.min(started.elapsed().as_nanos());
4330        }
4331
4332        println!(
4333            "unhex_text_args invocations={INVOCATIONS} repeats={REPEATS} plain_best_ns={plain_best_ns} ignore_best_ns={ignore_best_ns} checksum={checksum}"
4334        );
4335    }
4336
4337    // ── unicode ──────────────────────────────────────────────────────────
4338
4339    #[test]
4340    fn test_unicode_first_char() {
4341        assert_eq!(
4342            invoke1(&UnicodeFunc, SqliteValue::Text(SmallText::from_string("A"))).unwrap(),
4343            SqliteValue::Integer(65)
4344        );
4345    }
4346
4347    #[test]
4348    fn test_unicode_text_stops_at_nul() {
4349        assert_eq!(
4350            invoke1(
4351                &UnicodeFunc,
4352                SqliteValue::Text(SmallText::from_string("\0A"))
4353            )
4354            .unwrap(),
4355            SqliteValue::Null
4356        );
4357        assert_eq!(
4358            invoke1(
4359                &UnicodeFunc,
4360                SqliteValue::Text(SmallText::from_string("A\0"))
4361            )
4362            .unwrap(),
4363            SqliteValue::Integer(65)
4364        );
4365    }
4366
4367    #[test]
4368    fn test_unicode_blob_uses_sqlite_utf8_reader() {
4369        let cases: &[(&[u8], SqliteValue)] = &[
4370            (&[0x00, 0x41], SqliteValue::Null),
4371            (&[0x80], SqliteValue::Integer(128)),
4372            (&[0xC2, 0x80], SqliteValue::Integer(128)),
4373            (&[0xC2, 0x80, 0x80], SqliteValue::Integer(8192)),
4374            (&[0xED, 0xA0, 0x80], SqliteValue::Integer(65_533)),
4375            (&[0xF4, 0x90, 0x80, 0x80], SqliteValue::Integer(1_114_112)),
4376        ];
4377
4378        for (bytes, expected) in cases {
4379            assert_eq!(
4380                invoke1(&UnicodeFunc, SqliteValue::Blob(Arc::from(*bytes))).unwrap(),
4381                expected.clone()
4382            );
4383        }
4384    }
4385
4386    #[test]
4387    #[ignore = "perf-only benchmark"]
4388    fn perf_unicode_text_arg() {
4389        use std::hint::black_box;
4390        use std::time::Instant;
4391
4392        const INVOCATIONS: usize = 1_000_000;
4393        const REPEATS: usize = 7;
4394
4395        let f = UnicodeFunc;
4396        let args = [SqliteValue::Text(SmallText::from_string("Alphabet soup"))];
4397        let mut text_best_ns = u128::MAX;
4398        let mut checksum = 0i64;
4399
4400        for _ in 0..REPEATS {
4401            let started = Instant::now();
4402            for _ in 0..INVOCATIONS {
4403                let result = black_box(
4404                    f.invoke(black_box(args.as_slice()))
4405                        .expect("unicode benchmark invocation must succeed"),
4406                );
4407                if let SqliteValue::Integer(codepoint) = result {
4408                    checksum = checksum.wrapping_add(codepoint);
4409                }
4410            }
4411            text_best_ns = text_best_ns.min(started.elapsed().as_nanos());
4412        }
4413
4414        println!(
4415            "unicode_text_arg invocations={INVOCATIONS} repeats={REPEATS} text_best_ns={text_best_ns} checksum={checksum}"
4416        );
4417    }
4418
4419    // ── soundex ──────────────────────────────────────────────────────────
4420
4421    #[test]
4422    fn test_soundex_basic() {
4423        assert_eq!(
4424            invoke1(
4425                &SoundexFunc,
4426                SqliteValue::Text(SmallText::from_string("Robert"))
4427            )
4428            .unwrap(),
4429            SqliteValue::Text(SmallText::from_string("R163"))
4430        );
4431    }
4432
4433    #[test]
4434    #[ignore = "perf-only benchmark"]
4435    fn perf_soundex_text_arg() {
4436        use std::hint::black_box;
4437        use std::time::Instant;
4438
4439        const INVOCATIONS: usize = 1_000_000;
4440        const REPEATS: usize = 7;
4441
4442        let f = SoundexFunc;
4443        let args = [SqliteValue::Text(SmallText::from_string("Robert"))];
4444        let mut text_best_ns = u128::MAX;
4445        let mut checksum = 0usize;
4446
4447        for _ in 0..REPEATS {
4448            let started = Instant::now();
4449            for _ in 0..INVOCATIONS {
4450                let result = black_box(
4451                    f.invoke(black_box(args.as_slice()))
4452                        .expect("soundex benchmark invocation must succeed"),
4453                );
4454                if let SqliteValue::Text(text) = result {
4455                    checksum = checksum.wrapping_add(text.len());
4456                }
4457            }
4458            text_best_ns = text_best_ns.min(started.elapsed().as_nanos());
4459        }
4460
4461        println!(
4462            "soundex_text_arg invocations={INVOCATIONS} repeats={REPEATS} text_best_ns={text_best_ns} checksum={checksum}"
4463        );
4464    }
4465
4466    // ── substr ───────────────────────────────────────────────────────────
4467
4468    #[test]
4469    fn test_substr_basic() {
4470        let f = SubstrFunc;
4471        assert_eq!(
4472            f.invoke(&[
4473                SqliteValue::Text(SmallText::from_string("hello")),
4474                SqliteValue::Integer(2),
4475                SqliteValue::Integer(3),
4476            ])
4477            .unwrap(),
4478            SqliteValue::Text(SmallText::from_string("ell"))
4479        );
4480    }
4481
4482    #[test]
4483    fn test_substr_start_zero_quirk() {
4484        // substr('hello', 0, 3) returns 2 chars from start
4485        let f = SubstrFunc;
4486        let result = f
4487            .invoke(&[
4488                SqliteValue::Text(SmallText::from_string("hello")),
4489                SqliteValue::Integer(0),
4490                SqliteValue::Integer(3),
4491            ])
4492            .unwrap();
4493        assert_eq!(result, SqliteValue::Text(SmallText::from_string("he")));
4494    }
4495
4496    #[test]
4497    fn test_substr_negative_start() {
4498        // substr('hello', -2) = 'lo'
4499        let f = SubstrFunc;
4500        let result = f
4501            .invoke(&[
4502                SqliteValue::Text(SmallText::from_string("hello")),
4503                SqliteValue::Integer(-2),
4504            ])
4505            .unwrap();
4506        assert_eq!(result, SqliteValue::Text(SmallText::from_string("lo")));
4507    }
4508
4509    #[test]
4510    fn test_substr_negative_length() {
4511        let f = SubstrFunc;
4512        let t = |s: &str| SqliteValue::Text(SmallText::from_string(s));
4513        let i = SqliteValue::Integer;
4514        // SUBSTR('hello', 3, -2) => 'he' (2 chars before position 3)
4515        assert_eq!(f.invoke(&[t("hello"), i(3), i(-2)]).unwrap(), t("he"));
4516        // SUBSTR('hello', 3, -5) => 'he' (clamped at start)
4517        assert_eq!(f.invoke(&[t("hello"), i(3), i(-5)]).unwrap(), t("he"));
4518        // SUBSTR('hello', 1, -1) => '' (nothing before position 1)
4519        assert_eq!(f.invoke(&[t("hello"), i(1), i(-1)]).unwrap(), t(""));
4520    }
4521
4522    #[test]
4523    fn test_substr_negative_start_negative_length() {
4524        let f = SubstrFunc;
4525        let t = |s: &str| SqliteValue::Text(SmallText::from_string(s));
4526        let i = SqliteValue::Integer;
4527        // SUBSTR('hello', -2, -2) => 'el' (C SQLite confirmed)
4528        assert_eq!(f.invoke(&[t("hello"), i(-2), i(-2)]).unwrap(), t("el"));
4529    }
4530
4531    #[test]
4532    fn test_substr_edge_cases() {
4533        let f = SubstrFunc;
4534        let t = |s: &str| SqliteValue::Text(SmallText::from_string(s));
4535        let i = SqliteValue::Integer;
4536        // Past end
4537        assert_eq!(f.invoke(&[t("hello"), i(6), i(2)]).unwrap(), t(""));
4538        // Way before start
4539        assert_eq!(f.invoke(&[t("hello"), i(-10), i(3)]).unwrap(), t(""));
4540        // Negative start covering entire string
4541        assert_eq!(f.invoke(&[t("hello"), i(-5), i(6)]).unwrap(), t("hello"));
4542        // start=0, length=1 => '' (quirk)
4543        assert_eq!(f.invoke(&[t("hello"), i(0), i(1)]).unwrap(), t(""));
4544        // start=0, negative length
4545        assert_eq!(f.invoke(&[t("hello"), i(0), i(-1)]).unwrap(), t(""));
4546        // Empty string
4547        assert_eq!(f.invoke(&[t(""), i(1), i(1)]).unwrap(), t(""));
4548    }
4549
4550    #[test]
4551    fn test_substr_blob_negative_length() {
4552        let f = SubstrFunc;
4553        let i = SqliteValue::Integer;
4554        let blob = SqliteValue::Blob(Arc::from([1, 2, 3, 4, 5].as_slice()));
4555        // SUBSTR(X'0102030405', -2, -2) => X'0203' (matches text behavior)
4556        assert_eq!(
4557            f.invoke(&[blob, i(-2), i(-2)]).unwrap(),
4558            SqliteValue::Blob(Arc::from([2, 3].as_slice()))
4559        );
4560    }
4561
4562    // ── like ─────────────────────────────────────────────────────────────
4563
4564    #[test]
4565    fn test_like_case_insensitive() {
4566        assert_eq!(
4567            invoke2(
4568                &LikeFunc,
4569                SqliteValue::Text(SmallText::from_string("ABC")),
4570                SqliteValue::Text(SmallText::from_string("abc"))
4571            )
4572            .unwrap(),
4573            SqliteValue::Integer(1)
4574        );
4575    }
4576
4577    #[test]
4578    fn test_like_escape() {
4579        let f = LikeFunc;
4580        let result = f
4581            .invoke(&[
4582                SqliteValue::Text(SmallText::from_string("10\\%")),
4583                SqliteValue::Text(SmallText::from_string("10%")),
4584                SqliteValue::Text(SmallText::from_string("\\")),
4585            ])
4586            .unwrap();
4587        assert_eq!(result, SqliteValue::Integer(1));
4588    }
4589
4590    #[test]
4591    fn test_like_escape_rejects_empty_string() {
4592        let err = LikeFunc
4593            .invoke(&[
4594                SqliteValue::Text(SmallText::from_string("a")),
4595                SqliteValue::Text(SmallText::from_string("a")),
4596                SqliteValue::Text(SmallText::new("")),
4597            ])
4598            .unwrap_err();
4599        assert!(
4600            err.to_string()
4601                .contains("ESCAPE expression must be a single character")
4602        );
4603    }
4604
4605    #[test]
4606    fn test_like_escape_rejects_multi_character_string() {
4607        let err = LikeFunc
4608            .invoke(&[
4609                SqliteValue::Text(SmallText::from_string("a")),
4610                SqliteValue::Text(SmallText::from_string("a")),
4611                SqliteValue::Text(SmallText::from_string("xx")),
4612            ])
4613            .unwrap_err();
4614        assert!(
4615            err.to_string()
4616                .contains("ESCAPE expression must be a single character")
4617        );
4618    }
4619
4620    #[test]
4621    fn test_like_percent() {
4622        assert_eq!(
4623            invoke2(
4624                &LikeFunc,
4625                SqliteValue::Text(SmallText::from_string("%ell%")),
4626                SqliteValue::Text(SmallText::from_string("Hello"))
4627            )
4628            .unwrap(),
4629            SqliteValue::Integer(1)
4630        );
4631    }
4632
4633    // ── glob ─────────────────────────────────────────────────────────────
4634
4635    #[test]
4636    fn test_glob_star() {
4637        assert_eq!(
4638            invoke2(
4639                &GlobFunc,
4640                SqliteValue::Text(SmallText::from_string("*.txt")),
4641                SqliteValue::Text(SmallText::from_string("file.txt"))
4642            )
4643            .unwrap(),
4644            SqliteValue::Integer(1)
4645        );
4646    }
4647
4648    #[test]
4649    fn test_glob_case_sensitive() {
4650        assert_eq!(
4651            invoke2(
4652                &GlobFunc,
4653                SqliteValue::Text(SmallText::from_string("ABC")),
4654                SqliteValue::Text(SmallText::from_string("abc"))
4655            )
4656            .unwrap(),
4657            SqliteValue::Integer(0)
4658        );
4659    }
4660
4661    // ── format ───────────────────────────────────────────────────────────
4662
4663    #[test]
4664    fn test_format_specifiers() {
4665        let f = FormatFunc;
4666        let result = f
4667            .invoke(&[
4668                SqliteValue::Text(SmallText::from_string("%d %s")),
4669                SqliteValue::Integer(42),
4670                SqliteValue::Text(SmallText::from_string("hello")),
4671            ])
4672            .unwrap();
4673        assert_eq!(
4674            result,
4675            SqliteValue::Text(SmallText::from_string("42 hello"))
4676        );
4677    }
4678
4679    #[test]
4680    fn test_format_n_noop() {
4681        let f = FormatFunc;
4682        // %n should not crash or do anything
4683        let result = f
4684            .invoke(&[SqliteValue::Text(SmallText::from_string("before%nafter"))])
4685            .unwrap();
4686        assert_eq!(
4687            result,
4688            SqliteValue::Text(SmallText::from_string("beforeafter"))
4689        );
4690    }
4691
4692    #[test]
4693    fn test_format_alternate_form_hex_octal() {
4694        // bd-w54bm: `#` flag prefixes 0x/0X (hex) or 0 (octal) for nonzero values.
4695        let cases: &[(&str, i64, &str)] = &[
4696            ("%#x", 255, "0xff"),
4697            ("%#X", 255, "0XFF"),
4698            ("%#o", 64, "0100"),
4699            ("%#x", 0, "0"),        // zero gets no prefix
4700            ("%#o", 0, "0"),        // zero gets no prefix
4701            ("%#5x", 255, " 0xff"), // prefix counts toward space pad
4702            ("%#8x", 255, "    0xff"),
4703            ("%#08x", 255, "0x000000ff"), // zero pad pads digits, prefix outside
4704            ("%-#8x", 255, "0xff    "),   // '-' (no '0') -> space pad, left aligned
4705            ("%-08x", 255, "000000ff"),   // '-' does NOT override '0' in SQLite
4706            ("%#08o", 64, "000000100"),
4707            ("%#x", -1, "0xffffffffffffffff"),
4708        ];
4709        for (fmt, arg, want) in cases {
4710            let f = FormatFunc;
4711            let result = f
4712                .invoke(&[
4713                    SqliteValue::Text(SmallText::from_string(*fmt)),
4714                    SqliteValue::Integer(*arg),
4715                ])
4716                .unwrap();
4717            assert_eq!(
4718                result,
4719                SqliteValue::Text(SmallText::from_string((*want).to_owned())),
4720                "format({fmt:?}, {arg})"
4721            );
4722        }
4723    }
4724
4725    #[test]
4726    fn test_format_empty_string_is_null() {
4727        // bd-13ivh: an empty format string yields NULL (the StrAccum is never
4728        // touched), while a non-empty format that renders to nothing still
4729        // yields empty TEXT.
4730        let f = FormatFunc;
4731        assert_eq!(
4732            f.invoke(&[SqliteValue::Text(SmallText::from_string(""))])
4733                .unwrap(),
4734            SqliteValue::Null
4735        );
4736        // Non-empty format rendering to empty output is still TEXT, not NULL.
4737        assert_eq!(
4738            f.invoke(&[
4739                SqliteValue::Text(SmallText::from_string("%s")),
4740                SqliteValue::Null,
4741            ])
4742            .unwrap(),
4743            SqliteValue::Text(SmallText::from_string(String::new()))
4744        );
4745    }
4746
4747    // ── sqlite_version ───────────────────────────────────────────────────
4748
4749    #[test]
4750    fn test_sqlite_version_format() {
4751        let result = SqliteVersionFunc.invoke(&[]).unwrap();
4752        match result {
4753            SqliteValue::Text(v) => {
4754                assert_eq!(v.split('.').count(), 3, "version must be N.N.N format");
4755            }
4756            other => unreachable!("expected text, got {other:?}"),
4757        }
4758    }
4759
4760    #[test]
4761    fn test_sqlite_compileoption_used_matches_sqlite_prefix_and_value_options() {
4762        let func = SqliteCompileoptionUsedFunc;
4763        assert_eq!(
4764            invoke1(
4765                &func,
4766                SqliteValue::Text(SmallText::from_string("THREADSAFE"))
4767            )
4768            .unwrap(),
4769            SqliteValue::Integer(1)
4770        );
4771        let expected_icu_enabled = i64::from(cfg!(feature = "ext-icu"));
4772        assert_eq!(
4773            invoke1(
4774                &func,
4775                SqliteValue::Text(SmallText::from_string("SQLITE_ENABLE_ICU"))
4776            )
4777            .unwrap(),
4778            SqliteValue::Integer(expected_icu_enabled)
4779        );
4780        assert_eq!(
4781            invoke1(
4782                &func,
4783                SqliteValue::Text(SmallText::from_string("sqlite_enable_icu"))
4784            )
4785            .unwrap(),
4786            SqliteValue::Integer(expected_icu_enabled)
4787        );
4788        assert_eq!(
4789            invoke1(
4790                &func,
4791                SqliteValue::Text(SmallText::from_string("OMIT_LOAD_EXTENSION"))
4792            )
4793            .unwrap(),
4794            SqliteValue::Integer(1)
4795        );
4796        assert_eq!(
4797            invoke1(
4798                &func,
4799                SqliteValue::Text(SmallText::from_string("ENABLE_FTS3"))
4800            )
4801            .unwrap(),
4802            SqliteValue::Integer(0)
4803        );
4804        assert_eq!(
4805            invoke1(&func, SqliteValue::Null).unwrap(),
4806            SqliteValue::Null
4807        );
4808    }
4809
4810    #[test]
4811    #[ignore = "perf-only benchmark"]
4812    fn perf_compileoption_used_text_args() {
4813        use std::hint::black_box;
4814        use std::time::Instant;
4815
4816        const INVOCATIONS: usize = 1_000_000;
4817        const REPEATS: usize = 7;
4818
4819        let f = SqliteCompileoptionUsedFunc;
4820        let present_args = [SqliteValue::Text(SmallText::from_string(
4821            "SQLITE_ENABLE_ICU",
4822        ))];
4823        let absent_args = [SqliteValue::Text(SmallText::from_string(
4824            "ENABLE_NOT_PRESENT",
4825        ))];
4826
4827        let mut present_best_ns = u128::MAX;
4828        let mut absent_best_ns = u128::MAX;
4829        let mut checksum = 0i64;
4830        for _ in 0..REPEATS {
4831            let started = Instant::now();
4832            for _ in 0..INVOCATIONS {
4833                let result = black_box(
4834                    f.invoke(black_box(present_args.as_slice()))
4835                        .expect("compileoption present benchmark invocation must succeed"),
4836                );
4837                if let SqliteValue::Integer(value) = result {
4838                    checksum = checksum.wrapping_add(value);
4839                }
4840            }
4841            present_best_ns = present_best_ns.min(started.elapsed().as_nanos());
4842
4843            let started = Instant::now();
4844            for _ in 0..INVOCATIONS {
4845                let result = black_box(
4846                    f.invoke(black_box(absent_args.as_slice()))
4847                        .expect("compileoption absent benchmark invocation must succeed"),
4848                );
4849                if let SqliteValue::Integer(value) = result {
4850                    checksum = checksum.wrapping_add(value);
4851                }
4852            }
4853            absent_best_ns = absent_best_ns.min(started.elapsed().as_nanos());
4854        }
4855
4856        println!(
4857            "compileoption_used_text_args invocations={INVOCATIONS} repeats={REPEATS} present_best_ns={present_best_ns} absent_best_ns={absent_best_ns} checksum={checksum}"
4858        );
4859    }
4860
4861    #[test]
4862    fn test_sqlite_compileoption_get_enumerates_canonical_option_list() {
4863        let func = SqliteCompileoptionGetFunc;
4864        for (index, option) in sqlite_compile_options().iter().enumerate() {
4865            assert_eq!(
4866                invoke1(&func, SqliteValue::Integer(index as i64)).unwrap(),
4867                SqliteValue::Text(SmallText::new(option))
4868            );
4869        }
4870        assert_eq!(
4871            invoke1(&func, SqliteValue::Integer(-1)).unwrap(),
4872            SqliteValue::Null
4873        );
4874        assert_eq!(
4875            invoke1(
4876                &func,
4877                SqliteValue::Integer(sqlite_compile_options().len() as i64)
4878            )
4879            .unwrap(),
4880            SqliteValue::Null
4881        );
4882    }
4883
4884    // ── register_builtins ────────────────────────────────────────────────
4885
4886    #[test]
4887    fn test_register_builtins_all_present() {
4888        let mut registry = FunctionRegistry::new();
4889        register_builtins(&mut registry);
4890
4891        // Spot-check key functions are registered
4892        assert!(registry.find_scalar("abs", 1).is_some());
4893        assert!(registry.find_scalar("typeof", 1).is_some());
4894        assert!(registry.find_scalar("length", 1).is_some());
4895        assert!(registry.find_scalar("lower", 1).is_some());
4896        assert!(registry.find_scalar("upper", 1).is_some());
4897        assert!(registry.find_scalar("hex", 1).is_some());
4898        assert!(registry.find_scalar("coalesce", 3).is_some());
4899        assert!(registry.find_scalar("concat", 2).is_some());
4900        assert!(registry.find_scalar("like", 2).is_some());
4901        assert!(registry.find_scalar("glob", 2).is_some());
4902        assert!(registry.find_scalar("round", 1).is_some());
4903        assert!(registry.find_scalar("substr", 2).is_some());
4904        assert!(registry.find_scalar("substring", 3).is_some());
4905        assert!(registry.find_scalar("sqlite_version", 0).is_some());
4906        assert!(registry.find_scalar("iif", 3).is_some());
4907        assert!(registry.find_scalar("if", 3).is_some());
4908        assert!(registry.find_scalar("format", 1).is_some());
4909        assert!(registry.find_scalar("printf", 1).is_some());
4910        assert!(registry.find_scalar("max", 2).is_some());
4911        assert!(registry.find_scalar("min", 2).is_some());
4912        assert!(registry.find_scalar("sign", 1).is_some());
4913        assert!(registry.find_scalar("random", 0).is_some());
4914
4915        // Newer SQLite scalar functions (3.41+)
4916        assert!(registry.find_scalar("concat_ws", 3).is_some());
4917        assert!(registry.find_scalar("octet_length", 1).is_some());
4918        assert!(registry.find_scalar("unhex", 1).is_some());
4919        assert!(registry.find_scalar("timediff", 2).is_some());
4920        assert!(registry.find_scalar("unistr", 1).is_some());
4921        assert!(registry.find_scalar("unistr_quote", 1).is_some());
4922
4923        // Percentile family enabled by default.
4924        assert!(registry.find_aggregate("median", 1).is_some());
4925        assert!(registry.find_aggregate("percentile", 2).is_some());
4926        assert!(registry.find_aggregate("percentile_cont", 2).is_some());
4927        assert!(registry.find_aggregate("percentile_disc", 2).is_some());
4928
4929        // Loadable extensions are not exposed as SQL function by default.
4930        assert!(registry.find_scalar("load_extension", 1).is_none());
4931        assert!(registry.find_scalar("load_extension", 2).is_none());
4932    }
4933
4934    #[test]
4935    fn test_register_builtins_rejects_invalid_variadic_arities() {
4936        let mut registry = FunctionRegistry::new();
4937        register_builtins(&mut registry);
4938
4939        for (name, too_few, valid, too_many) in [
4940            ("coalesce", 1, 2, None),
4941            ("concat", 0, 1, None),
4942            ("concat_ws", 1, 2, None),
4943            ("trim", 0, 1, Some(3)),
4944            ("ltrim", 0, 1, Some(3)),
4945            ("rtrim", 0, 1, Some(3)),
4946            ("round", 0, 1, Some(3)),
4947            ("unhex", 0, 1, Some(3)),
4948            ("substr", 1, 2, Some(4)),
4949            ("substring", 1, 2, Some(4)),
4950            ("max", 0, 1, None),
4951            ("min", 0, 1, None),
4952        ] {
4953            assert_wrong_arg_count(&registry, name, too_few);
4954            assert!(
4955                registry.find_scalar(name, valid).is_some(),
4956                "{name}/{valid} should resolve"
4957            );
4958            if let Some(arity) = too_many {
4959                assert_wrong_arg_count(&registry, name, arity);
4960            }
4961        }
4962
4963        assert!(registry.find_scalar("char", 0).is_some());
4964        assert!(registry.find_scalar("format", 0).is_some());
4965        assert!(registry.find_scalar("printf", 0).is_some());
4966    }
4967
4968    #[test]
4969    fn test_e2e_registry_invoke_through_lookup() {
4970        let mut registry = FunctionRegistry::new();
4971        register_builtins(&mut registry);
4972
4973        // Look up abs, invoke it
4974        let abs = registry.find_scalar("ABS", 1).unwrap();
4975        assert_eq!(
4976            abs.invoke(&[SqliteValue::Integer(-42)]).unwrap(),
4977            SqliteValue::Integer(42)
4978        );
4979
4980        // Look up typeof, invoke it
4981        let typeof_fn = registry.find_scalar("typeof", 1).unwrap();
4982        assert_eq!(
4983            typeof_fn
4984                .invoke(&[SqliteValue::Text(SmallText::from_string("hello"))])
4985                .unwrap(),
4986            SqliteValue::Text(SmallText::from_string("text"))
4987        );
4988
4989        // Look up coalesce (variadic), invoke with 4 args
4990        let coalesce = registry.find_scalar("COALESCE", 4).unwrap();
4991        assert_eq!(
4992            coalesce
4993                .invoke(&[
4994                    SqliteValue::Null,
4995                    SqliteValue::Null,
4996                    SqliteValue::Integer(42),
4997                    SqliteValue::Integer(99),
4998                ])
4999                .unwrap(),
5000            SqliteValue::Integer(42)
5001        );
5002    }
5003
5004    // ── bd-13r.8: Non-Deterministic Function Evaluation Semantics ──
5005
5006    #[test]
5007    fn test_nondeterministic_functions_flagged() {
5008        // These functions MUST be marked non-deterministic to prevent
5009        // unsafe planner optimizations (hoisting, CSE).
5010        assert!(!RandomFunc.is_deterministic());
5011        assert!(!RandomblobFunc.is_deterministic());
5012        assert!(!ChangesFunc.is_deterministic());
5013        assert!(!TotalChangesFunc.is_deterministic());
5014        assert!(!LastInsertRowidFunc.is_deterministic());
5015    }
5016
5017    #[test]
5018    fn test_deterministic_functions_flagged() {
5019        // Deterministic functions are safe for constant folding/CSE.
5020        assert!(AbsFunc.is_deterministic());
5021        assert!(LengthFunc.is_deterministic());
5022        assert!(TypeofFunc.is_deterministic());
5023        assert!(UpperFunc.is_deterministic());
5024        assert!(LowerFunc.is_deterministic());
5025        assert!(HexFunc.is_deterministic());
5026        assert!(CoalesceFunc.is_deterministic());
5027        assert!(IifFunc.is_deterministic());
5028    }
5029
5030    #[test]
5031    fn test_random_produces_different_values() {
5032        // random() should produce different values on successive calls
5033        // (verifying per-call evaluation, not constant folding).
5034        let a = RandomFunc.invoke(&[]).unwrap();
5035        let b = RandomFunc.invoke(&[]).unwrap();
5036        // With overwhelming probability, two random i64 values differ.
5037        // If they're ever equal, it's a 1-in-2^64 coincidence.
5038        assert_ne!(a.as_integer(), b.as_integer());
5039    }
5040
5041    #[test]
5042    fn test_registry_nondeterministic_lookup() {
5043        let mut registry = FunctionRegistry::default();
5044        register_builtins(&mut registry);
5045
5046        // Non-deterministic functions should be findable and flagged.
5047        let random = registry.find_scalar("random", 0).unwrap();
5048        assert!(!random.is_deterministic());
5049
5050        let changes = registry.find_scalar("changes", 0).unwrap();
5051        assert!(!changes.is_deterministic());
5052
5053        let lir = registry.find_scalar("last_insert_rowid", 0).unwrap();
5054        assert!(!lir.is_deterministic());
5055
5056        // Deterministic function check.
5057        let abs = registry.find_scalar("abs", 1).unwrap();
5058        assert!(abs.is_deterministic());
5059    }
5060}