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()).unwrap_or(0).min(100_000_000);
2360            } else {
2361                width = usize::try_from(w).unwrap_or(0).min(100_000_000);
2362            }
2363        } else {
2364            while i < chars.len() && chars[i].is_ascii_digit() {
2365                width = width
2366                    .saturating_mul(10)
2367                    .saturating_add(chars[i] as usize - '0' as usize)
2368                    .min(100_000_000); // Prevent OOM from malicious formats
2369                i += 1;
2370            }
2371        }
2372
2373        // Parse precision
2374        let mut precision = None;
2375        if i < chars.len() && chars[i] == '.' {
2376            i += 1;
2377            let mut prec = 0usize;
2378            while i < chars.len() && chars[i].is_ascii_digit() {
2379                prec = prec
2380                    .saturating_mul(10)
2381                    .saturating_add(chars[i] as usize - '0' as usize)
2382                    .min(100_000_000); // Prevent OOM from malicious formats
2383                i += 1;
2384            }
2385            precision = Some(prec);
2386        }
2387
2388        if i >= chars.len() {
2389            break;
2390        }
2391
2392        let spec = chars[i];
2393        i += 1;
2394
2395        match spec {
2396            '%' => result.push('%'),
2397            'n' => {} // no-op (security: never writes to memory)
2398            'd' | 'i' => {
2399                let val = params.get(param_idx).map_or(0, SqliteValue::to_integer);
2400                param_idx += 1;
2401                let formatted =
2402                    format_integer(val, width, left_align, show_sign, space_sign, zero_pad);
2403                result.push_str(&formatted);
2404            }
2405            'u' => {
2406                // Unsigned decimal (bd-jvnwt): reinterpret the i64 bit pattern as
2407                // u64, matching C/SQLite %u.
2408                let val = params.get(param_idx).map_or(0, SqliteValue::to_integer);
2409                param_idx += 1;
2410                #[allow(clippy::cast_sign_loss)]
2411                let digits = (val as u64).to_string();
2412                let padded = if zero_pad && width > digits.len() {
2413                    format!("{}{}", "0".repeat(width - digits.len()), digits)
2414                } else {
2415                    pad_string(&digits, width, left_align)
2416                };
2417                result.push_str(&padded);
2418            }
2419            'f' => {
2420                let val = params.get(param_idx).map_or(0.0, SqliteValue::to_float);
2421                param_idx += 1;
2422                let prec = precision.unwrap_or(6);
2423                let formatted = format_float_f(
2424                    val, prec, width, left_align, show_sign, space_sign, zero_pad,
2425                );
2426                result.push_str(&formatted);
2427            }
2428            'e' | 'E' => {
2429                let val = params.get(param_idx).map_or(0.0, SqliteValue::to_float);
2430                param_idx += 1;
2431                let prec = precision.unwrap_or(6);
2432                let raw = if spec == 'e' {
2433                    format!("{val:.prec$e}")
2434                } else {
2435                    format!("{val:.prec$E}")
2436                };
2437                // C printf always uses explicit sign and minimum 2-digit exponent
2438                let formatted = normalize_exponent(&raw);
2439                result.push_str(&pad_string(&formatted, width, left_align));
2440            }
2441            'g' | 'G' => {
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                let sig = prec.max(1);
2446                let formatted = format_float_g(val, sig, spec == 'G');
2447                result.push_str(&pad_string(&formatted, width, left_align));
2448            }
2449            's' | 'z' => {
2450                let param = params.get(param_idx);
2451                param_idx += 1;
2452                let val = match param {
2453                    // SQLite: printf('%s', NULL) returns empty string
2454                    Some(SqliteValue::Null) | None => String::new(),
2455                    Some(v) => v.to_text(),
2456                };
2457                let truncated = if let Some(prec) = precision {
2458                    val.chars().take(prec).collect::<String>()
2459                } else {
2460                    val
2461                };
2462                result.push_str(&pad_string(&truncated, width, left_align));
2463            }
2464            'q' => {
2465                // Single-quote escaping; C SQLite emits nothing for %q with NULL
2466                let param = params.get(param_idx);
2467                param_idx += 1;
2468                match param {
2469                    // SQLite: printf('%q', NULL) returns literal "(NULL)"
2470                    Some(SqliteValue::Null) | None => {
2471                        result.push_str("(NULL)");
2472                    }
2473                    Some(v) => {
2474                        let val = v.to_text();
2475                        let escaped = val.replace('\'', "''");
2476                        result.push_str(&escaped);
2477                    }
2478                }
2479            }
2480            'Q' => {
2481                // Like %q but wrapped in quotes, NULL -> "NULL"
2482                let param = params.get(param_idx);
2483                param_idx += 1;
2484                match param {
2485                    Some(SqliteValue::Null) | None => result.push_str("NULL"),
2486                    Some(v) => {
2487                        let val = v.to_text();
2488                        let escaped = val.replace('\'', "''");
2489                        result.push('\'');
2490                        result.push_str(&escaped);
2491                        result.push('\'');
2492                    }
2493                }
2494            }
2495            'w' => {
2496                // Double-quote escaping for identifiers; NULL → empty.
2497                // C SQLite %w with NULL produces nothing (empty string),
2498                // and only escapes internal double quotes (no surrounding quotes).
2499                let param = params.get(param_idx);
2500                param_idx += 1;
2501                if matches!(param, Some(SqliteValue::Null) | None) {
2502                    // NULL: produce nothing (matches C SQLite).
2503                } else {
2504                    let val = param.map(SqliteValue::to_text).unwrap_or_default();
2505                    let escaped = val.replace('"', "\"\"");
2506                    result.push_str(&escaped);
2507                }
2508            }
2509            'x' | 'X' => {
2510                let val = params.get(param_idx).map_or(0, SqliteValue::to_integer);
2511                param_idx += 1;
2512                #[allow(clippy::cast_sign_loss)]
2513                let digits = if spec == 'x' {
2514                    format!("{:x}", val as u64)
2515                } else {
2516                    format!("{:X}", val as u64)
2517                };
2518                // Alternate form (`#`) prefixes a nonzero value with 0x / 0X.
2519                let prefix = if alt_form && val != 0 {
2520                    if spec == 'x' { "0x" } else { "0X" }
2521                } else {
2522                    ""
2523                };
2524                // SQLite's printf zero-pads whenever the `0` flag is present,
2525                // even alongside `-` (it does NOT let `-` override `0` the way C
2526                // does). The digits are zero-padded to `width`; the prefix sits
2527                // outside that pad.
2528                let padded = if zero_pad && width > digits.len() {
2529                    let pad = "0".repeat(width - digits.len());
2530                    format!("{prefix}{pad}{digits}")
2531                } else {
2532                    pad_string(&format!("{prefix}{digits}"), width, left_align)
2533                };
2534                result.push_str(&padded);
2535            }
2536            'o' => {
2537                let val = params.get(param_idx).map_or(0, SqliteValue::to_integer);
2538                param_idx += 1;
2539                #[allow(clippy::cast_sign_loss)]
2540                let digits = format!("{:o}", val as u64);
2541                // Alternate form (`#`) prefixes a nonzero value with a leading 0.
2542                let prefix = if alt_form && val != 0 { "0" } else { "" };
2543                // As with %x, SQLite zero-pads whenever the `0` flag is present
2544                // (even with `-`).
2545                let padded = if zero_pad && width > digits.len() {
2546                    let pad = "0".repeat(width - digits.len());
2547                    format!("{prefix}{pad}{digits}")
2548                } else {
2549                    pad_string(&format!("{prefix}{digits}"), width, left_align)
2550                };
2551                result.push_str(&padded);
2552            }
2553            'c' => {
2554                let param = params.get(param_idx);
2555                param_idx += 1;
2556                // SQLite's printf %c renders the argument to its text form and
2557                // emits the first character — it does NOT interpret an integer
2558                // as a Unicode codepoint like C printf does (bd-47mu0). So
2559                // printf('%c', 65) yields '6' (first char of "65"), not 'A'.
2560                let text = match param {
2561                    Some(SqliteValue::Null) | None => String::new(),
2562                    Some(v) => v.to_text(),
2563                };
2564                if let Some(c) = text.chars().next() {
2565                    result.push(c);
2566                }
2567            }
2568            _ => {
2569                // Unknown specifier: output literally
2570                result.push('%');
2571                result.push(spec);
2572            }
2573        }
2574        // Suppress unused warnings
2575        let _ = (left_align, show_sign, space_sign, zero_pad);
2576    }
2577    Ok(result)
2578}
2579
2580fn format_integer(
2581    val: i64,
2582    width: usize,
2583    left_align: bool,
2584    show_sign: bool,
2585    space_sign: bool,
2586    zero_pad: bool,
2587) -> String {
2588    let sign = if val < 0 {
2589        "-".to_owned()
2590    } else if show_sign {
2591        "+".to_owned()
2592    } else if space_sign {
2593        " ".to_owned()
2594    } else {
2595        String::new()
2596    };
2597    let digits = format!("{}", val.unsigned_abs());
2598    let body = format!("{sign}{digits}");
2599    if body.len() >= width {
2600        return body;
2601    }
2602    let pad = width - body.len();
2603    if left_align {
2604        format!("{body}{}", " ".repeat(pad))
2605    } else if zero_pad {
2606        format!("{sign}{}{digits}", "0".repeat(pad))
2607    } else {
2608        format!("{}{body}", " ".repeat(pad))
2609    }
2610}
2611
2612fn format_float_f(
2613    val: f64,
2614    prec: usize,
2615    width: usize,
2616    left_align: bool,
2617    show_sign: bool,
2618    space_sign: bool,
2619    zero_pad: bool,
2620) -> String {
2621    // Use is_sign_negative() to detect -0.0 (IEEE 754: -0.0 < 0.0 is false).
2622    let sign = if val.is_sign_negative() {
2623        "-".to_owned()
2624    } else if show_sign {
2625        "+".to_owned()
2626    } else if space_sign {
2627        " ".to_owned()
2628    } else {
2629        String::new()
2630    };
2631    let digits = format!("{:.prec$}", val.abs());
2632    let body = format!("{sign}{digits}");
2633    if body.len() >= width {
2634        return body;
2635    }
2636    let pad = width - body.len();
2637    if left_align {
2638        format!("{body}{}", " ".repeat(pad))
2639    } else if zero_pad {
2640        format!("{sign}{}{digits}", "0".repeat(pad))
2641    } else {
2642        format!("{}{body}", " ".repeat(pad))
2643    }
2644}
2645
2646fn pad_string(s: &str, width: usize, left_align: bool) -> String {
2647    if s.len() >= width {
2648        return s.to_owned();
2649    }
2650    let pad = width - s.len();
2651    if left_align {
2652        format!("{s}{}", " ".repeat(pad))
2653    } else {
2654        format!("{}{s}", " ".repeat(pad))
2655    }
2656}
2657
2658/// Normalize an exponent string to match C printf: explicit sign and
2659/// minimum two digits (e.g. `"1.23e6"` → `"1.23e+06"`).
2660fn normalize_exponent(s: &str) -> String {
2661    let (prefix, e_char, exp_part) = if let Some(pos) = s.find('e') {
2662        (&s[..pos], 'e', &s[pos + 1..])
2663    } else if let Some(pos) = s.find('E') {
2664        (&s[..pos], 'E', &s[pos + 1..])
2665    } else {
2666        return s.to_owned();
2667    };
2668    let (sign, digits) = if let Some(rest) = exp_part.strip_prefix('-') {
2669        ("-", rest)
2670    } else if let Some(rest) = exp_part.strip_prefix('+') {
2671        ("+", rest)
2672    } else {
2673        ("+", exp_part)
2674    };
2675    let padded = if digits.len() < 2 {
2676        format!("0{digits}")
2677    } else {
2678        digits.to_owned()
2679    };
2680    format!("{prefix}{e_char}{sign}{padded}")
2681}
2682
2683/// Format a float using `%g`/`%G` semantics.
2684fn format_float_g(val: f64, sig: usize, upper: bool) -> String {
2685    if !val.is_finite() {
2686        return format!("{val}");
2687    }
2688    let e_str = format!("{val:.prec$e}", prec = sig.saturating_sub(1));
2689    let exp: i32 = e_str
2690        .rsplit_once('e')
2691        .and_then(|(_, e)| e.parse().ok())
2692        .unwrap_or(0);
2693    #[allow(clippy::cast_possible_wrap)]
2694    let formatted = if exp < -4 || exp >= sig as i32 {
2695        let s = format!("{val:.prec$e}", prec = sig.saturating_sub(1));
2696        let s = if upper { s.replace('e', "E") } else { s };
2697        // Strip trailing zeros from mantissa, then normalize the exponent.
2698        let trimmed = if s.contains('.') {
2699            if let Some(e_pos) = s.find('e').or_else(|| s.find('E')) {
2700                let mantissa = s[..e_pos].trim_end_matches('0').trim_end_matches('.');
2701                format!("{mantissa}{}", &s[e_pos..])
2702            } else {
2703                s.trim_end_matches('0').trim_end_matches('.').to_owned()
2704            }
2705        } else {
2706            s
2707        };
2708        normalize_exponent(&trimmed)
2709    } else {
2710        let decimal_places = if exp >= 0 {
2711            sig.saturating_sub((exp + 1) as usize)
2712        } else {
2713            sig + exp.unsigned_abs() as usize - 1
2714        };
2715        let s = format!("{val:.decimal_places$}");
2716        s.trim_end_matches('0').trim_end_matches('.').to_owned()
2717    };
2718    formatted
2719}
2720
2721#[cfg(test)]
2722#[allow(clippy::too_many_lines)]
2723mod tests {
2724    use super::*;
2725
2726    fn invoke1(f: &dyn ScalarFunction, v: SqliteValue) -> Result<SqliteValue> {
2727        f.invoke(&[v])
2728    }
2729
2730    fn invoke2(f: &dyn ScalarFunction, a: SqliteValue, b: SqliteValue) -> Result<SqliteValue> {
2731        f.invoke(&[a, b])
2732    }
2733
2734    fn assert_wrong_arg_count(registry: &FunctionRegistry, name: &str, arity: i32) {
2735        let function = registry
2736            .find_scalar(name, arity)
2737            .expect("known scalar name with bad arity returns erroring scalar");
2738        let args = vec![SqliteValue::Null; arity.max(0) as usize];
2739        let err = function
2740            .invoke(&args)
2741            .expect_err("wrong arity should return function error");
2742        let expected = format!("wrong number of arguments to function {name}()");
2743        assert!(
2744            matches!(&err, FrankenError::FunctionError(message) if message == &expected),
2745            "expected {expected:?}, got {err:?}"
2746        );
2747    }
2748
2749    #[test]
2750    fn test_get_change_tracking_state_returns_thread_local_snapshot() {
2751        let original = get_change_tracking_state();
2752        let expected = ChangeTrackingState {
2753            last_insert_rowid: 17,
2754            last_changes: 23,
2755            total_changes: 42,
2756        };
2757
2758        set_change_tracking_state(expected);
2759        assert_eq!(get_change_tracking_state(), expected);
2760
2761        set_change_tracking_state(original);
2762    }
2763
2764    // ── abs ──────────────────────────────────────────────────────────────
2765
2766    #[test]
2767    fn test_abs_positive() {
2768        assert_eq!(
2769            invoke1(&AbsFunc, SqliteValue::Integer(42)).unwrap(),
2770            SqliteValue::Integer(42)
2771        );
2772    }
2773
2774    #[test]
2775    fn test_abs_negative() {
2776        assert_eq!(
2777            invoke1(&AbsFunc, SqliteValue::Integer(-42)).unwrap(),
2778            SqliteValue::Integer(42)
2779        );
2780    }
2781
2782    #[test]
2783    fn test_abs_null() {
2784        assert_eq!(
2785            invoke1(&AbsFunc, SqliteValue::Null).unwrap(),
2786            SqliteValue::Null
2787        );
2788    }
2789
2790    #[test]
2791    fn test_abs_min_i64_overflow() {
2792        let err = invoke1(&AbsFunc, SqliteValue::Integer(i64::MIN)).unwrap_err();
2793        assert!(matches!(err, FrankenError::IntegerOverflow));
2794    }
2795
2796    #[test]
2797    fn test_abs_string_coercion() {
2798        assert_eq!(
2799            invoke1(&AbsFunc, SqliteValue::Text(SmallText::from_string("-7.5"))).unwrap(),
2800            SqliteValue::Float(7.5)
2801        );
2802    }
2803
2804    #[test]
2805    fn test_abs_whitespace_padded_text() {
2806        // SQLite's abs() casts non-integers to REAL, even if they parse cleanly as integers
2807        assert_eq!(
2808            invoke1(
2809                &AbsFunc,
2810                SqliteValue::Text(SmallText::from_string("  42  "))
2811            )
2812            .unwrap(),
2813            SqliteValue::Float(42.0)
2814        );
2815        assert_eq!(
2816            invoke1(
2817                &AbsFunc,
2818                SqliteValue::Text(SmallText::from_string("  -7.5  "))
2819            )
2820            .unwrap(),
2821            SqliteValue::Float(7.5)
2822        );
2823        assert_eq!(
2824            invoke1(&AbsFunc, SqliteValue::Text(SmallText::from_string("abc"))).unwrap(),
2825            SqliteValue::Float(0.0)
2826        );
2827    }
2828
2829    #[test]
2830    #[allow(clippy::approx_constant)]
2831    fn test_abs_float() {
2832        assert_eq!(
2833            invoke1(&AbsFunc, SqliteValue::Float(-3.14)).unwrap(),
2834            SqliteValue::Float(3.14)
2835        );
2836    }
2837
2838    // ── char ─────────────────────────────────────────────────────────────
2839
2840    #[test]
2841    fn test_char_basic() {
2842        let f = CharFunc;
2843        let result = f
2844            .invoke(&[
2845                SqliteValue::Integer(72),
2846                SqliteValue::Integer(101),
2847                SqliteValue::Integer(108),
2848                SqliteValue::Integer(108),
2849                SqliteValue::Integer(111),
2850            ])
2851            .unwrap();
2852        assert_eq!(result, SqliteValue::Text(SmallText::from_string("Hello")));
2853    }
2854
2855    #[test]
2856    fn test_char_null_skipped() {
2857        let f = CharFunc;
2858        // C SQLite: NULL → sqlite3_value_int()=0 → U+0000 (NUL byte).
2859        let result = f
2860            .invoke(&[
2861                SqliteValue::Integer(65),
2862                SqliteValue::Null,
2863                SqliteValue::Integer(66),
2864            ])
2865            .unwrap();
2866        assert_eq!(result, SqliteValue::Text(SmallText::from_string("A\0B")));
2867    }
2868
2869    #[test]
2870    fn test_char_invalid_scalar_values_use_replacement_character() {
2871        let f = CharFunc;
2872        let result = f
2873            .invoke(&[
2874                SqliteValue::Integer(-1),
2875                SqliteValue::Integer(65),
2876                SqliteValue::Integer(1_114_112),
2877            ])
2878            .unwrap();
2879        assert_eq!(
2880            result,
2881            SqliteValue::Text(SmallText::from_string("\u{fffd}A\u{fffd}"))
2882        );
2883    }
2884
2885    // ── coalesce ─────────────────────────────────────────────────────────
2886
2887    #[test]
2888    fn test_coalesce_first_non_null() {
2889        let f = CoalesceFunc;
2890        let result = f
2891            .invoke(&[
2892                SqliteValue::Null,
2893                SqliteValue::Null,
2894                SqliteValue::Integer(3),
2895                SqliteValue::Integer(4),
2896            ])
2897            .unwrap();
2898        assert_eq!(result, SqliteValue::Integer(3));
2899    }
2900
2901    // ── concat ───────────────────────────────────────────────────────────
2902
2903    #[test]
2904    fn test_concat_null_as_empty() {
2905        let f = ConcatFunc;
2906        let result = f
2907            .invoke(&[
2908                SqliteValue::Null,
2909                SqliteValue::Text(SmallText::from_string("hello")),
2910                SqliteValue::Null,
2911            ])
2912            .unwrap();
2913        assert_eq!(result, SqliteValue::Text(SmallText::from_string("hello")));
2914    }
2915
2916    #[test]
2917    #[ignore = "perf-only benchmark"]
2918    fn perf_concat_text_args() {
2919        use std::hint::black_box;
2920        use std::time::Instant;
2921
2922        const TEXT_ARGS: usize = 24;
2923        const INVOCATIONS: usize = 50_000;
2924        const REPEATS: usize = 5;
2925
2926        let f = ConcatFunc;
2927        let mut args = Vec::with_capacity(TEXT_ARGS);
2928        for _ in 0..TEXT_ARGS {
2929            args.push(SqliteValue::Text(SmallText::from_string("payload")));
2930        }
2931
2932        let mut best_ns = u128::MAX;
2933        let mut result_len = 0usize;
2934        for _ in 0..REPEATS {
2935            let started = Instant::now();
2936            for _ in 0..INVOCATIONS {
2937                let result = black_box(
2938                    f.invoke(black_box(args.as_slice()))
2939                        .expect("concat benchmark invocation must succeed"),
2940                );
2941                result_len = match result {
2942                    SqliteValue::Text(text) => text.len(),
2943                    SqliteValue::Null
2944                    | SqliteValue::Integer(_)
2945                    | SqliteValue::Float(_)
2946                    | SqliteValue::Blob(_) => 0,
2947                };
2948            }
2949            best_ns = best_ns.min(started.elapsed().as_nanos());
2950        }
2951
2952        println!(
2953            "concat_text_args text_args={TEXT_ARGS} invocations={INVOCATIONS} repeats={REPEATS} best_ns={best_ns} result_len={result_len}"
2954        );
2955    }
2956
2957    // ── concat_ws ────────────────────────────────────────────────────────
2958
2959    #[test]
2960    fn test_concat_ws_null_skipped() {
2961        let f = ConcatWsFunc;
2962        let result = f
2963            .invoke(&[
2964                SqliteValue::Text(SmallText::from_string(",")),
2965                SqliteValue::Text(SmallText::from_string("a")),
2966                SqliteValue::Null,
2967                SqliteValue::Text(SmallText::from_string("b")),
2968            ])
2969            .unwrap();
2970        assert_eq!(result, SqliteValue::Text(SmallText::from_string("a,b")));
2971    }
2972
2973    #[test]
2974    fn test_concat_ws_empty_string_is_not_skipped() {
2975        let f = ConcatWsFunc;
2976        let result = f
2977            .invoke(&[
2978                SqliteValue::Text(SmallText::from_string("|")),
2979                SqliteValue::Text(SmallText::new("")),
2980                SqliteValue::Text(SmallText::from_string("x")),
2981            ])
2982            .unwrap();
2983        assert_eq!(result, SqliteValue::Text(SmallText::from_string("|x")));
2984    }
2985
2986    #[test]
2987    #[ignore = "perf-only benchmark"]
2988    fn perf_concat_ws_text_args() {
2989        use std::hint::black_box;
2990        use std::time::Instant;
2991
2992        const TEXT_ARGS: usize = 24;
2993        const INVOCATIONS: usize = 50_000;
2994        const REPEATS: usize = 5;
2995
2996        let f = ConcatWsFunc;
2997        let mut args = Vec::with_capacity(TEXT_ARGS + 1);
2998        args.push(SqliteValue::Text(SmallText::from_string(",")));
2999        for _ in 0..TEXT_ARGS {
3000            args.push(SqliteValue::Text(SmallText::from_string("payload")));
3001        }
3002
3003        let mut best_ns = u128::MAX;
3004        let mut result_len = 0usize;
3005        for _ in 0..REPEATS {
3006            let started = Instant::now();
3007            for _ in 0..INVOCATIONS {
3008                let result = black_box(
3009                    f.invoke(black_box(args.as_slice()))
3010                        .expect("concat_ws benchmark invocation must succeed"),
3011                );
3012                result_len = match result {
3013                    SqliteValue::Text(text) => text.len(),
3014                    SqliteValue::Null
3015                    | SqliteValue::Integer(_)
3016                    | SqliteValue::Float(_)
3017                    | SqliteValue::Blob(_) => 0,
3018                };
3019            }
3020            best_ns = best_ns.min(started.elapsed().as_nanos());
3021        }
3022
3023        println!(
3024            "concat_ws_text_args text_args={TEXT_ARGS} invocations={INVOCATIONS} repeats={REPEATS} best_ns={best_ns} result_len={result_len}"
3025        );
3026    }
3027
3028    // ── hex ──────────────────────────────────────────────────────────────
3029
3030    #[test]
3031    fn test_hex_blob() {
3032        let result = invoke1(
3033            &HexFunc,
3034            SqliteValue::Blob(Arc::from([0xDE, 0xAD, 0xBE, 0xEF].as_slice())),
3035        )
3036        .unwrap();
3037        assert_eq!(
3038            result,
3039            SqliteValue::Text(SmallText::from_string("DEADBEEF"))
3040        );
3041    }
3042
3043    #[test]
3044    fn test_hex_number_via_text() {
3045        // hex(42) encodes '42' as UTF-8 hex, not raw bits
3046        let result = invoke1(&HexFunc, SqliteValue::Integer(42)).unwrap();
3047        assert_eq!(result, SqliteValue::Text(SmallText::from_string("3432")));
3048    }
3049
3050    #[test]
3051    #[ignore = "perf-only benchmark"]
3052    fn perf_hex_text_blob_args() {
3053        use std::hint::black_box;
3054        use std::time::Instant;
3055
3056        const BYTES: usize = 24;
3057        const INVOCATIONS: usize = 100_000;
3058        const REPEATS: usize = 5;
3059
3060        let f = HexFunc;
3061        let text_args = [SqliteValue::Text(SmallText::from_string(
3062            "payload payload sentinel",
3063        ))];
3064        let blob_args = [SqliteValue::Blob(Arc::from([0xAB; BYTES].as_slice()))];
3065
3066        let mut text_best_ns = u128::MAX;
3067        let mut blob_best_ns = u128::MAX;
3068        let mut text_result_len = 0usize;
3069        let mut blob_result_len = 0usize;
3070        for _ in 0..REPEATS {
3071            let started = Instant::now();
3072            for _ in 0..INVOCATIONS {
3073                let result = black_box(
3074                    f.invoke(black_box(text_args.as_slice()))
3075                        .expect("hex text benchmark invocation must succeed"),
3076                );
3077                text_result_len = match result {
3078                    SqliteValue::Text(text) => text.len(),
3079                    SqliteValue::Null
3080                    | SqliteValue::Integer(_)
3081                    | SqliteValue::Float(_)
3082                    | SqliteValue::Blob(_) => 0,
3083                };
3084            }
3085            text_best_ns = text_best_ns.min(started.elapsed().as_nanos());
3086
3087            let started = Instant::now();
3088            for _ in 0..INVOCATIONS {
3089                let result = black_box(
3090                    f.invoke(black_box(blob_args.as_slice()))
3091                        .expect("hex blob benchmark invocation must succeed"),
3092                );
3093                blob_result_len = match result {
3094                    SqliteValue::Text(text) => text.len(),
3095                    SqliteValue::Null
3096                    | SqliteValue::Integer(_)
3097                    | SqliteValue::Float(_)
3098                    | SqliteValue::Blob(_) => 0,
3099                };
3100            }
3101            blob_best_ns = blob_best_ns.min(started.elapsed().as_nanos());
3102        }
3103
3104        println!(
3105            "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}"
3106        );
3107    }
3108
3109    // ── iif ──────────────────────────────────────────────────────────────
3110
3111    #[test]
3112    fn test_iif_true() {
3113        let f = IifFunc;
3114        let result = f
3115            .invoke(&[
3116                SqliteValue::Integer(1),
3117                SqliteValue::Text(SmallText::from_string("yes")),
3118                SqliteValue::Text(SmallText::from_string("no")),
3119            ])
3120            .unwrap();
3121        assert_eq!(result, SqliteValue::Text(SmallText::from_string("yes")));
3122    }
3123
3124    #[test]
3125    fn test_iif_false() {
3126        let f = IifFunc;
3127        let result = f
3128            .invoke(&[
3129                SqliteValue::Integer(0),
3130                SqliteValue::Text(SmallText::from_string("yes")),
3131                SqliteValue::Text(SmallText::from_string("no")),
3132            ])
3133            .unwrap();
3134        assert_eq!(result, SqliteValue::Text(SmallText::from_string("no")));
3135    }
3136
3137    #[test]
3138    fn test_iif_whitespace_padded_text_truthy() {
3139        // Regression: IIF('  5  ', 'yes', 'no') must return 'yes'
3140        // because SQLite trims text before numeric coercion.
3141        let f = IifFunc;
3142        let result = f
3143            .invoke(&[
3144                SqliteValue::Text(SmallText::from_string("  5  ")),
3145                SqliteValue::Text(SmallText::from_string("yes")),
3146                SqliteValue::Text(SmallText::from_string("no")),
3147            ])
3148            .unwrap();
3149        assert_eq!(result, SqliteValue::Text(SmallText::from_string("yes")));
3150    }
3151
3152    // ── ifnull ───────────────────────────────────────────────────────────
3153
3154    #[test]
3155    fn test_ifnull_non_null() {
3156        assert_eq!(
3157            invoke2(
3158                &IfnullFunc,
3159                SqliteValue::Integer(5),
3160                SqliteValue::Integer(10)
3161            )
3162            .unwrap(),
3163            SqliteValue::Integer(5)
3164        );
3165    }
3166
3167    #[test]
3168    fn test_ifnull_null() {
3169        assert_eq!(
3170            invoke2(&IfnullFunc, SqliteValue::Null, SqliteValue::Integer(10)).unwrap(),
3171            SqliteValue::Integer(10)
3172        );
3173    }
3174
3175    // ── instr ────────────────────────────────────────────────────────────
3176
3177    #[test]
3178    fn test_instr_found() {
3179        assert_eq!(
3180            invoke2(
3181                &InstrFunc,
3182                SqliteValue::Text(SmallText::from_string("hello world")),
3183                SqliteValue::Text(SmallText::from_string("world"))
3184            )
3185            .unwrap(),
3186            SqliteValue::Integer(7)
3187        );
3188    }
3189
3190    #[test]
3191    fn test_instr_not_found() {
3192        assert_eq!(
3193            invoke2(
3194                &InstrFunc,
3195                SqliteValue::Text(SmallText::from_string("hello")),
3196                SqliteValue::Text(SmallText::from_string("xyz"))
3197            )
3198            .unwrap(),
3199            SqliteValue::Integer(0)
3200        );
3201    }
3202
3203    #[test]
3204    fn test_instr_empty_needle_returns_one() {
3205        // SQLite: instr(X, '') returns 1 (empty string found at position 1).
3206        assert_eq!(
3207            invoke2(
3208                &InstrFunc,
3209                SqliteValue::Text(SmallText::from_string("hello")),
3210                SqliteValue::Text(SmallText::new(""))
3211            )
3212            .unwrap(),
3213            SqliteValue::Integer(1)
3214        );
3215    }
3216
3217    #[test]
3218    fn test_instr_empty_haystack_returns_zero() {
3219        assert_eq!(
3220            invoke2(
3221                &InstrFunc,
3222                SqliteValue::Text(SmallText::new("")),
3223                SqliteValue::Text(SmallText::from_string("x"))
3224            )
3225            .unwrap(),
3226            SqliteValue::Integer(0)
3227        );
3228    }
3229
3230    #[test]
3231    fn test_instr_blob_empty_needle_returns_one() {
3232        // SQLite: instr(X, x'') returns 1 (empty blob found at position 1).
3233        assert_eq!(
3234            invoke2(
3235                &InstrFunc,
3236                SqliteValue::Blob(Arc::from([1, 2, 3].as_slice())),
3237                SqliteValue::Blob(Arc::from([].as_slice()))
3238            )
3239            .unwrap(),
3240            SqliteValue::Integer(1)
3241        );
3242    }
3243
3244    #[test]
3245    #[ignore = "perf-only benchmark"]
3246    fn perf_instr_text_args() {
3247        use std::hint::black_box;
3248        use std::time::Instant;
3249
3250        const INVOCATIONS: usize = 100_000;
3251        const REPEATS: usize = 5;
3252
3253        let f = InstrFunc;
3254        let args = [
3255            SqliteValue::Text(SmallText::from_string("payload payload sentinel")),
3256            SqliteValue::Text(SmallText::from_string("sentinel")),
3257        ];
3258
3259        let mut best_ns = u128::MAX;
3260        let mut result_value = 0i64;
3261        for _ in 0..REPEATS {
3262            let started = Instant::now();
3263            for _ in 0..INVOCATIONS {
3264                let result = black_box(
3265                    f.invoke(black_box(args.as_slice()))
3266                        .expect("instr benchmark invocation must succeed"),
3267                );
3268                result_value = match result {
3269                    SqliteValue::Integer(value) => value,
3270                    SqliteValue::Null
3271                    | SqliteValue::Float(_)
3272                    | SqliteValue::Text(_)
3273                    | SqliteValue::Blob(_) => 0,
3274                };
3275            }
3276            best_ns = best_ns.min(started.elapsed().as_nanos());
3277        }
3278
3279        println!(
3280            "instr_text_args invocations={INVOCATIONS} repeats={REPEATS} best_ns={best_ns} result_value={result_value}"
3281        );
3282    }
3283
3284    // ── length ───────────────────────────────────────────────────────────
3285
3286    #[test]
3287    fn test_length_text_chars() {
3288        // café is 4 characters, 5 bytes
3289        assert_eq!(
3290            invoke1(
3291                &LengthFunc,
3292                SqliteValue::Text(SmallText::from_string("café"))
3293            )
3294            .unwrap(),
3295            SqliteValue::Integer(4)
3296        );
3297    }
3298
3299    #[test]
3300    fn test_length_text_stops_at_nul() {
3301        assert_eq!(
3302            invoke1(
3303                &LengthFunc,
3304                SqliteValue::Text(SmallText::from_string("A\0B"))
3305            )
3306            .unwrap(),
3307            SqliteValue::Integer(1)
3308        );
3309        assert_eq!(
3310            invoke1(
3311                &LengthFunc,
3312                SqliteValue::Text(SmallText::from_string("\0A"))
3313            )
3314            .unwrap(),
3315            SqliteValue::Integer(0)
3316        );
3317    }
3318
3319    #[test]
3320    fn test_length_blob_bytes() {
3321        assert_eq!(
3322            invoke1(&LengthFunc, SqliteValue::Blob(Arc::from([1, 2].as_slice()))).unwrap(),
3323            SqliteValue::Integer(2)
3324        );
3325    }
3326
3327    // ── octet_length ─────────────────────────────────────────────────────
3328
3329    #[test]
3330    fn test_octet_length_multibyte() {
3331        // café: 'c'=1, 'a'=1, 'f'=1, 'é'=2 bytes = 5 bytes total
3332        assert_eq!(
3333            invoke1(
3334                &OctetLengthFunc,
3335                SqliteValue::Text(SmallText::from_string("café"))
3336            )
3337            .unwrap(),
3338            SqliteValue::Integer(5)
3339        );
3340    }
3341
3342    // ── lower/upper ──────────────────────────────────────────────────────
3343
3344    #[test]
3345    fn test_lower_ascii() {
3346        assert_eq!(
3347            invoke1(
3348                &LowerFunc,
3349                SqliteValue::Text(SmallText::from_string("HELLO"))
3350            )
3351            .unwrap(),
3352            SqliteValue::Text(SmallText::from_string("hello"))
3353        );
3354    }
3355
3356    #[test]
3357    fn test_upper_ascii() {
3358        assert_eq!(
3359            invoke1(
3360                &UpperFunc,
3361                SqliteValue::Text(SmallText::from_string("hello"))
3362            )
3363            .unwrap(),
3364            SqliteValue::Text(SmallText::from_string("HELLO"))
3365        );
3366    }
3367
3368    // ── trim/ltrim/rtrim ─────────────────────────────────────────────────
3369
3370    #[test]
3371    fn test_trim_default() {
3372        let f = TrimFunc;
3373        assert_eq!(
3374            f.invoke(&[SqliteValue::Text(SmallText::from_string("  hello  "))])
3375                .unwrap(),
3376            SqliteValue::Text(SmallText::from_string("hello"))
3377        );
3378    }
3379
3380    #[test]
3381    fn test_ltrim_default() {
3382        let f = LtrimFunc;
3383        assert_eq!(
3384            f.invoke(&[SqliteValue::Text(SmallText::from_string("  hello"))])
3385                .unwrap(),
3386            SqliteValue::Text(SmallText::from_string("hello"))
3387        );
3388    }
3389
3390    #[test]
3391    fn test_ltrim_custom() {
3392        let f = LtrimFunc;
3393        assert_eq!(
3394            f.invoke(&[
3395                SqliteValue::Text(SmallText::from_string("xxhello")),
3396                SqliteValue::Text(SmallText::from_string("x")),
3397            ])
3398            .unwrap(),
3399            SqliteValue::Text(SmallText::from_string("hello"))
3400        );
3401    }
3402
3403    #[test]
3404    #[ignore = "perf-only benchmark"]
3405    fn perf_trim_text_args() {
3406        use std::hint::black_box;
3407        use std::time::Instant;
3408
3409        const INVOCATIONS: usize = 100_000;
3410        const REPEATS: usize = 5;
3411
3412        let trim = TrimFunc;
3413        let ltrim = LtrimFunc;
3414        let rtrim = RtrimFunc;
3415        let default_args = [SqliteValue::Text(SmallText::from_string("   payload   "))];
3416        let custom_args = [
3417            SqliteValue::Text(SmallText::from_string("xxxpayloadxxx")),
3418            SqliteValue::Text(SmallText::from_string("x")),
3419        ];
3420
3421        let mut trim_best_ns = u128::MAX;
3422        let mut ltrim_best_ns = u128::MAX;
3423        let mut rtrim_best_ns = u128::MAX;
3424        let mut custom_best_ns = u128::MAX;
3425        let mut result_len = 0usize;
3426
3427        for _ in 0..REPEATS {
3428            let started = Instant::now();
3429            for _ in 0..INVOCATIONS {
3430                let result = black_box(
3431                    trim.invoke(black_box(default_args.as_slice()))
3432                        .expect("trim benchmark invocation must succeed"),
3433                );
3434                result_len = match result {
3435                    SqliteValue::Text(text) => text.len(),
3436                    SqliteValue::Null
3437                    | SqliteValue::Integer(_)
3438                    | SqliteValue::Float(_)
3439                    | SqliteValue::Blob(_) => 0,
3440                };
3441            }
3442            trim_best_ns = trim_best_ns.min(started.elapsed().as_nanos());
3443
3444            let started = Instant::now();
3445            for _ in 0..INVOCATIONS {
3446                let result = black_box(
3447                    ltrim
3448                        .invoke(black_box(default_args.as_slice()))
3449                        .expect("ltrim benchmark invocation must succeed"),
3450                );
3451                result_len = match result {
3452                    SqliteValue::Text(text) => text.len(),
3453                    SqliteValue::Null
3454                    | SqliteValue::Integer(_)
3455                    | SqliteValue::Float(_)
3456                    | SqliteValue::Blob(_) => 0,
3457                };
3458            }
3459            ltrim_best_ns = ltrim_best_ns.min(started.elapsed().as_nanos());
3460
3461            let started = Instant::now();
3462            for _ in 0..INVOCATIONS {
3463                let result = black_box(
3464                    rtrim
3465                        .invoke(black_box(default_args.as_slice()))
3466                        .expect("rtrim benchmark invocation must succeed"),
3467                );
3468                result_len = match result {
3469                    SqliteValue::Text(text) => text.len(),
3470                    SqliteValue::Null
3471                    | SqliteValue::Integer(_)
3472                    | SqliteValue::Float(_)
3473                    | SqliteValue::Blob(_) => 0,
3474                };
3475            }
3476            rtrim_best_ns = rtrim_best_ns.min(started.elapsed().as_nanos());
3477
3478            let started = Instant::now();
3479            for _ in 0..INVOCATIONS {
3480                let result = black_box(
3481                    trim.invoke(black_box(custom_args.as_slice()))
3482                        .expect("custom trim benchmark invocation must succeed"),
3483                );
3484                result_len = match result {
3485                    SqliteValue::Text(text) => text.len(),
3486                    SqliteValue::Null
3487                    | SqliteValue::Integer(_)
3488                    | SqliteValue::Float(_)
3489                    | SqliteValue::Blob(_) => 0,
3490                };
3491            }
3492            custom_best_ns = custom_best_ns.min(started.elapsed().as_nanos());
3493        }
3494
3495        println!(
3496            "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}"
3497        );
3498    }
3499
3500    // ── nullif ───────────────────────────────────────────────────────────
3501
3502    #[test]
3503    fn test_nullif_equal() {
3504        assert_eq!(
3505            invoke2(
3506                &NullifFunc,
3507                SqliteValue::Integer(5),
3508                SqliteValue::Integer(5)
3509            )
3510            .unwrap(),
3511            SqliteValue::Null
3512        );
3513    }
3514
3515    #[test]
3516    fn test_nullif_different() {
3517        assert_eq!(
3518            invoke2(
3519                &NullifFunc,
3520                SqliteValue::Integer(5),
3521                SqliteValue::Integer(3)
3522            )
3523            .unwrap(),
3524            SqliteValue::Integer(5)
3525        );
3526    }
3527
3528    // ── typeof ───────────────────────────────────────────────────────────
3529
3530    #[test]
3531    fn test_typeof_each() {
3532        assert_eq!(
3533            invoke1(&TypeofFunc, SqliteValue::Null).unwrap(),
3534            SqliteValue::Text(SmallText::from_string("null"))
3535        );
3536        assert_eq!(
3537            invoke1(&TypeofFunc, SqliteValue::Integer(1)).unwrap(),
3538            SqliteValue::Text(SmallText::from_string("integer"))
3539        );
3540        assert_eq!(
3541            invoke1(&TypeofFunc, SqliteValue::Float(1.0)).unwrap(),
3542            SqliteValue::Text(SmallText::from_string("real"))
3543        );
3544        assert_eq!(
3545            invoke1(&TypeofFunc, SqliteValue::Text(SmallText::from_string("x"))).unwrap(),
3546            SqliteValue::Text(SmallText::from_string("text"))
3547        );
3548        assert_eq!(
3549            invoke1(&TypeofFunc, SqliteValue::Blob(Arc::from([0].as_slice()))).unwrap(),
3550            SqliteValue::Text(SmallText::from_string("blob"))
3551        );
3552    }
3553
3554    // ── subtype ──────────────────────────────────────────────────────────
3555
3556    #[test]
3557    fn test_subtype_null_returns_zero() {
3558        assert_eq!(
3559            invoke1(&SubtypeFunc, SqliteValue::Null).unwrap(),
3560            SqliteValue::Integer(0)
3561        );
3562    }
3563
3564    // ── replace ──────────────────────────────────────────────────────────
3565
3566    #[test]
3567    fn test_replace_basic() {
3568        let f = ReplaceFunc;
3569        assert_eq!(
3570            f.invoke(&[
3571                SqliteValue::Text(SmallText::from_string("hello world")),
3572                SqliteValue::Text(SmallText::from_string("world")),
3573                SqliteValue::Text(SmallText::from_string("earth")),
3574            ])
3575            .unwrap(),
3576            SqliteValue::Text(SmallText::from_string("hello earth"))
3577        );
3578    }
3579
3580    #[test]
3581    fn test_replace_empty_y() {
3582        let f = ReplaceFunc;
3583        assert_eq!(
3584            f.invoke(&[
3585                SqliteValue::Text(SmallText::from_string("hello")),
3586                SqliteValue::Text(SmallText::new("")),
3587                SqliteValue::Text(SmallText::from_string("x")),
3588            ])
3589            .unwrap(),
3590            SqliteValue::Text(SmallText::from_string("hello"))
3591        );
3592    }
3593
3594    #[test]
3595    #[ignore = "perf-only benchmark"]
3596    fn perf_replace_text_args() {
3597        use std::hint::black_box;
3598        use std::time::Instant;
3599
3600        const INVOCATIONS: usize = 100_000;
3601        const REPEATS: usize = 5;
3602
3603        let f = ReplaceFunc;
3604        let args = [
3605            SqliteValue::Text(SmallText::from_string("payload payload payload")),
3606            SqliteValue::Text(SmallText::from_string("zz")),
3607            SqliteValue::Text(SmallText::from_string("replacement")),
3608        ];
3609
3610        let mut best_ns = u128::MAX;
3611        let mut result_len = 0usize;
3612        for _ in 0..REPEATS {
3613            let started = Instant::now();
3614            for _ in 0..INVOCATIONS {
3615                let result = black_box(
3616                    f.invoke(black_box(args.as_slice()))
3617                        .expect("replace benchmark invocation must succeed"),
3618                );
3619                result_len = match result {
3620                    SqliteValue::Text(text) => text.len(),
3621                    SqliteValue::Null
3622                    | SqliteValue::Integer(_)
3623                    | SqliteValue::Float(_)
3624                    | SqliteValue::Blob(_) => 0,
3625                };
3626            }
3627            best_ns = best_ns.min(started.elapsed().as_nanos());
3628        }
3629
3630        println!(
3631            "replace_text_args invocations={INVOCATIONS} repeats={REPEATS} best_ns={best_ns} result_len={result_len}"
3632        );
3633    }
3634
3635    // ── round ────────────────────────────────────────────────────────────
3636
3637    #[test]
3638    #[allow(clippy::float_cmp)]
3639    fn test_round_half_away() {
3640        // round(2.5) = 3.0, round(-2.5) = -3.0
3641        assert_eq!(
3642            RoundFunc.invoke(&[SqliteValue::Float(2.5)]).unwrap(),
3643            SqliteValue::Float(3.0)
3644        );
3645        assert_eq!(
3646            RoundFunc.invoke(&[SqliteValue::Float(-2.5)]).unwrap(),
3647            SqliteValue::Float(-3.0)
3648        );
3649    }
3650
3651    #[test]
3652    #[allow(clippy::float_cmp, clippy::approx_constant)]
3653    fn test_round_precision() {
3654        assert_eq!(
3655            RoundFunc
3656                .invoke(&[SqliteValue::Float(3.14159), SqliteValue::Integer(2)])
3657                .unwrap(),
3658            SqliteValue::Float(3.14)
3659        );
3660    }
3661
3662    #[test]
3663    #[allow(clippy::float_cmp)]
3664    fn test_round_extreme_n_clamped() {
3665        // N > 30 is clamped to 30 (matches C SQLite)
3666        assert_eq!(
3667            RoundFunc
3668                .invoke(&[SqliteValue::Float(1.5), SqliteValue::Integer(400)])
3669                .unwrap(),
3670            RoundFunc
3671                .invoke(&[SqliteValue::Float(1.5), SqliteValue::Integer(30)])
3672                .unwrap(),
3673        );
3674        // Negative N is clamped to 0 (matches C SQLite)
3675        assert_eq!(
3676            RoundFunc
3677                .invoke(&[SqliteValue::Float(2.5), SqliteValue::Integer(-5)])
3678                .unwrap(),
3679            SqliteValue::Float(3.0)
3680        );
3681        // i64::MAX is clamped to 30
3682        let result = RoundFunc
3683            .invoke(&[SqliteValue::Float(1.5), SqliteValue::Integer(i64::MAX)])
3684            .unwrap();
3685        if let SqliteValue::Float(v) = result {
3686            assert!(!v.is_nan(), "round must never return NaN");
3687        }
3688    }
3689
3690    #[test]
3691    #[allow(clippy::float_cmp)]
3692    fn test_round_large_value_no_fractional() {
3693        // Values beyond 2^52 have no fractional part — returned unchanged
3694        let big = 9_007_199_254_740_993.0_f64;
3695        assert_eq!(
3696            RoundFunc.invoke(&[SqliteValue::Float(big)]).unwrap(),
3697            SqliteValue::Float(big)
3698        );
3699        assert_eq!(
3700            RoundFunc.invoke(&[SqliteValue::Float(-big)]).unwrap(),
3701            SqliteValue::Float(-big)
3702        );
3703    }
3704
3705    // ── sign ─────────────────────────────────────────────────────────────
3706
3707    #[test]
3708    fn test_sign_positive() {
3709        assert_eq!(
3710            invoke1(&SignFunc, SqliteValue::Integer(42)).unwrap(),
3711            SqliteValue::Integer(1)
3712        );
3713    }
3714
3715    #[test]
3716    fn test_sign_negative() {
3717        assert_eq!(
3718            invoke1(&SignFunc, SqliteValue::Integer(-42)).unwrap(),
3719            SqliteValue::Integer(-1)
3720        );
3721    }
3722
3723    #[test]
3724    fn test_sign_zero() {
3725        assert_eq!(
3726            invoke1(&SignFunc, SqliteValue::Integer(0)).unwrap(),
3727            SqliteValue::Integer(0)
3728        );
3729    }
3730
3731    #[test]
3732    fn test_sign_null() {
3733        assert_eq!(
3734            invoke1(&SignFunc, SqliteValue::Null).unwrap(),
3735            SqliteValue::Null
3736        );
3737    }
3738
3739    #[test]
3740    fn test_sign_non_numeric() {
3741        // C SQLite: math functions return NULL for strings that cannot be parsed as numeric.
3742        assert_eq!(
3743            invoke1(&SignFunc, SqliteValue::Text(SmallText::from_string("abc"))).unwrap(),
3744            SqliteValue::Null
3745        );
3746    }
3747
3748    #[test]
3749    fn test_sign_whitespace_padded_text() {
3750        // Regression: SIGN('  5  ') must return 1, not NULL.
3751        // SQLite trims ASCII whitespace before numeric parsing.
3752        assert_eq!(
3753            invoke1(
3754                &SignFunc,
3755                SqliteValue::Text(SmallText::from_string("  5  "))
3756            )
3757            .unwrap(),
3758            SqliteValue::Integer(1)
3759        );
3760        assert_eq!(
3761            invoke1(
3762                &SignFunc,
3763                SqliteValue::Text(SmallText::from_string("  -3.14  "))
3764            )
3765            .unwrap(),
3766            SqliteValue::Integer(-1)
3767        );
3768    }
3769
3770    #[test]
3771    fn test_sign_unicode_space_and_blob_return_null() {
3772        assert_eq!(
3773            invoke1(
3774                &SignFunc,
3775                SqliteValue::Text(SmallText::from_string("\u{00a0}123"))
3776            )
3777            .unwrap(),
3778            SqliteValue::Null
3779        );
3780        assert_eq!(
3781            invoke1(&SignFunc, SqliteValue::Blob(Arc::from(b"123".as_slice()))).unwrap(),
3782            SqliteValue::Null
3783        );
3784    }
3785
3786    #[test]
3787    fn test_sign_nan_inf_text_returns_null() {
3788        // C SQLite doesn't recognise "NaN", "inf", "Infinity" etc. as numeric —
3789        // sign() must return NULL for these, matching the C oracle.
3790        for s in &[
3791            "NaN",
3792            "nan",
3793            "inf",
3794            "-inf",
3795            "Infinity",
3796            "-Infinity",
3797            "INF",
3798            "+nan",
3799            "+inf",
3800        ] {
3801            assert_eq!(
3802                invoke1(&SignFunc, SqliteValue::Text(SmallText::from_string(*s))).unwrap(),
3803                SqliteValue::Null,
3804                "sign('{s}') should be NULL"
3805            );
3806        }
3807    }
3808
3809    #[test]
3810    fn test_sign_numeric_overflow_to_infinity() {
3811        // "1e999" overflows to +inf in both Rust and C. C SQLite's sqlite3AtoF
3812        // accepts it as numeric, so sign() must return 1 (not NULL).
3813        assert_eq!(
3814            invoke1(
3815                &SignFunc,
3816                SqliteValue::Text(SmallText::from_string("1e999"))
3817            )
3818            .unwrap(),
3819            SqliteValue::Integer(1)
3820        );
3821        assert_eq!(
3822            invoke1(
3823                &SignFunc,
3824                SqliteValue::Text(SmallText::from_string("-1e999"))
3825            )
3826            .unwrap(),
3827            SqliteValue::Integer(-1)
3828        );
3829        // Underflow to zero
3830        assert_eq!(
3831            invoke1(
3832                &SignFunc,
3833                SqliteValue::Text(SmallText::from_string("1e-999"))
3834            )
3835            .unwrap(),
3836            SqliteValue::Integer(0)
3837        );
3838    }
3839
3840    #[test]
3841    fn test_sign_float_nan_returns_null() {
3842        // C SQLite: sign(0.0/0.0) = NULL. Float NaN must not return 0.
3843        assert_eq!(
3844            invoke1(&SignFunc, SqliteValue::Float(f64::NAN)).unwrap(),
3845            SqliteValue::Null
3846        );
3847    }
3848
3849    // ── scalar max/min ───────────────────────────────────────────────────
3850
3851    #[test]
3852    fn test_scalar_max_null() {
3853        let f = ScalarMaxFunc;
3854        let result = f
3855            .invoke(&[
3856                SqliteValue::Integer(1),
3857                SqliteValue::Null,
3858                SqliteValue::Integer(3),
3859            ])
3860            .unwrap();
3861        assert_eq!(result, SqliteValue::Null);
3862    }
3863
3864    #[test]
3865    fn test_scalar_max_values() {
3866        let f = ScalarMaxFunc;
3867        let result = f
3868            .invoke(&[
3869                SqliteValue::Integer(3),
3870                SqliteValue::Integer(1),
3871                SqliteValue::Integer(2),
3872            ])
3873            .unwrap();
3874        assert_eq!(result, SqliteValue::Integer(3));
3875    }
3876
3877    #[test]
3878    fn test_scalar_min_null() {
3879        let f = ScalarMinFunc;
3880        let result = f
3881            .invoke(&[
3882                SqliteValue::Integer(1),
3883                SqliteValue::Null,
3884                SqliteValue::Integer(3),
3885            ])
3886            .unwrap();
3887        assert_eq!(result, SqliteValue::Null);
3888    }
3889
3890    // ── quote ────────────────────────────────────────────────────────────
3891
3892    #[test]
3893    fn test_quote_text() {
3894        assert_eq!(
3895            invoke1(
3896                &QuoteFunc,
3897                SqliteValue::Text(SmallText::from_string("it's"))
3898            )
3899            .unwrap(),
3900            SqliteValue::Text(SmallText::from_string("'it''s'"))
3901        );
3902    }
3903
3904    #[test]
3905    fn test_quote_null() {
3906        assert_eq!(
3907            invoke1(&QuoteFunc, SqliteValue::Null).unwrap(),
3908            SqliteValue::Text(SmallText::from_string("NULL"))
3909        );
3910    }
3911
3912    #[test]
3913    fn test_quote_blob() {
3914        assert_eq!(
3915            invoke1(&QuoteFunc, SqliteValue::Blob(Arc::from([0xAB].as_slice()))).unwrap(),
3916            SqliteValue::Text(SmallText::from_string("X'AB'"))
3917        );
3918    }
3919
3920    #[test]
3921    fn test_quote_text_truncates_at_first_nul() {
3922        assert_eq!(
3923            invoke1(
3924                &QuoteFunc,
3925                SqliteValue::Text(SmallText::from_string("A\0B"))
3926            )
3927            .unwrap(),
3928            SqliteValue::Text(SmallText::from_string("'A'"))
3929        );
3930    }
3931
3932    #[test]
3933    fn test_unistr_quote_plain_text_matches_quote() {
3934        assert_eq!(
3935            invoke1(
3936                &UnistrQuoteFunc,
3937                SqliteValue::Text(SmallText::from_string("it's"))
3938            )
3939            .unwrap(),
3940            SqliteValue::Text(SmallText::from_string("'it''s'"))
3941        );
3942    }
3943
3944    #[test]
3945    fn test_unistr_quote_escapes_control_chars_and_backslashes() {
3946        assert_eq!(
3947            invoke1(
3948                &UnistrQuoteFunc,
3949                SqliteValue::Text(SmallText::from_string("a\nb\\c\x01d"))
3950            )
3951            .unwrap(),
3952            SqliteValue::Text(SmallText::from_string("unistr('a\\u000ab\\\\c\\u0001d')"))
3953        );
3954    }
3955
3956    #[test]
3957    fn test_unistr_quote_truncates_at_first_nul_before_wrapping() {
3958        assert_eq!(
3959            invoke1(
3960                &UnistrQuoteFunc,
3961                SqliteValue::Text(SmallText::from_string("A\0\nB"))
3962            )
3963            .unwrap(),
3964            SqliteValue::Text(SmallText::from_string("'A'"))
3965        );
3966    }
3967
3968    #[test]
3969    fn test_unistr_decodes_backslash_and_unicode_escapes() {
3970        assert_eq!(
3971            invoke1(
3972                &UnistrFunc,
3973                SqliteValue::Text(SmallText::from_string(
3974                    "a\\\\b\\u0020\\U0001f600\\0041\\+000042"
3975                ))
3976            )
3977            .unwrap(),
3978            SqliteValue::Text(SmallText::from_string("a\\b \u{1f600}AB"))
3979        );
3980    }
3981
3982    #[test]
3983    fn test_unistr_invalid_escape_returns_error() {
3984        for input in [
3985            "\\u12xz",
3986            "\\12xz",
3987            "\\+00xz",
3988            "\\",
3989            "\\x",
3990            "\\U00110000",
3991            "\\D800",
3992        ] {
3993            let err = invoke1(
3994                &UnistrFunc,
3995                SqliteValue::Text(SmallText::from_string(input)),
3996            )
3997            .unwrap_err();
3998            assert_eq!(err.to_string(), INVALID_UNISTR_ESCAPE);
3999        }
4000    }
4001
4002    #[test]
4003    #[ignore = "perf-only benchmark"]
4004    fn perf_unistr_text_args() {
4005        use std::hint::black_box;
4006        use std::time::Instant;
4007
4008        const INVOCATIONS: usize = 500_000;
4009        const REPEATS: usize = 7;
4010
4011        let f = UnistrFunc;
4012        let plain_args = [SqliteValue::Text(SmallText::from_string(
4013            "plain unicode payload",
4014        ))];
4015        let escaped_args = [SqliteValue::Text(SmallText::from_string(
4016            "a\\\\b\\u0020\\u0048\\u0069\\U0001f600",
4017        ))];
4018
4019        let mut plain_best_ns = u128::MAX;
4020        let mut escaped_best_ns = u128::MAX;
4021        let mut checksum = 0usize;
4022        for _ in 0..REPEATS {
4023            let started = Instant::now();
4024            for _ in 0..INVOCATIONS {
4025                let result = black_box(
4026                    f.invoke(black_box(plain_args.as_slice()))
4027                        .expect("unistr plain benchmark invocation must succeed"),
4028                );
4029                if let SqliteValue::Text(text) = result {
4030                    checksum = checksum.wrapping_add(text.len());
4031                }
4032            }
4033            plain_best_ns = plain_best_ns.min(started.elapsed().as_nanos());
4034
4035            let started = Instant::now();
4036            for _ in 0..INVOCATIONS {
4037                let result = black_box(
4038                    f.invoke(black_box(escaped_args.as_slice()))
4039                        .expect("unistr escaped benchmark invocation must succeed"),
4040                );
4041                if let SqliteValue::Text(text) = result {
4042                    checksum = checksum.wrapping_add(text.len());
4043                }
4044            }
4045            escaped_best_ns = escaped_best_ns.min(started.elapsed().as_nanos());
4046        }
4047
4048        println!(
4049            "unistr_text_args invocations={INVOCATIONS} repeats={REPEATS} plain_best_ns={plain_best_ns} escaped_best_ns={escaped_best_ns} checksum={checksum}"
4050        );
4051    }
4052
4053    // ── random ───────────────────────────────────────────────────────────
4054
4055    #[test]
4056    fn test_random_range() {
4057        let f = RandomFunc;
4058        let result = f.invoke(&[]).unwrap();
4059        assert!(matches!(result, SqliteValue::Integer(_)));
4060    }
4061
4062    // ── randomblob ───────────────────────────────────────────────────────
4063
4064    #[test]
4065    fn test_randomblob_length() {
4066        let result = invoke1(&RandomblobFunc, SqliteValue::Integer(16)).unwrap();
4067        match result {
4068            SqliteValue::Blob(b) => assert_eq!(b.len(), 16),
4069            other => unreachable!("expected blob, got {other:?}"),
4070        }
4071    }
4072
4073    #[test]
4074    fn test_randomblob_null_zero_and_negative_lengths_are_one_byte() {
4075        for arg in [
4076            SqliteValue::Null,
4077            SqliteValue::Integer(0),
4078            SqliteValue::Integer(-5),
4079        ] {
4080            let result = invoke1(&RandomblobFunc, arg).unwrap();
4081            match result {
4082                SqliteValue::Blob(b) => assert_eq!(b.len(), 1),
4083                other => unreachable!("expected one-byte blob, got {other:?}"),
4084            }
4085        }
4086    }
4087
4088    // ── zeroblob ─────────────────────────────────────────────────────────
4089
4090    #[test]
4091    fn test_zeroblob_length() {
4092        let result = invoke1(&ZeroblobFunc, SqliteValue::Integer(100)).unwrap();
4093        match result {
4094            SqliteValue::Blob(b) => {
4095                assert_eq!(b.len(), 100);
4096                assert!(b.iter().all(|&x| x == 0));
4097            }
4098            other => unreachable!("expected blob, got {other:?}"),
4099        }
4100    }
4101
4102    // ── unhex ────────────────────────────────────────────────────────────
4103
4104    #[test]
4105    fn test_unhex_valid() {
4106        let result = invoke1(
4107            &UnhexFunc,
4108            SqliteValue::Text(SmallText::from_string("48656C6C6F")),
4109        )
4110        .unwrap();
4111        assert_eq!(result, SqliteValue::Blob(Arc::from(b"Hello".as_slice())));
4112    }
4113
4114    #[test]
4115    fn test_unhex_invalid() {
4116        let result = invoke1(
4117            &UnhexFunc,
4118            SqliteValue::Text(SmallText::from_string("ZZZZ")),
4119        )
4120        .unwrap();
4121        assert_eq!(result, SqliteValue::Null);
4122    }
4123
4124    #[test]
4125    fn test_unhex_ignore_chars() {
4126        let f = UnhexFunc;
4127        let result = f
4128            .invoke(&[
4129                SqliteValue::Text(SmallText::from_string("48-65-6C")),
4130                SqliteValue::Text(SmallText::from_string("-")),
4131            ])
4132            .unwrap();
4133        assert_eq!(result, SqliteValue::Blob(Arc::from(b"Hel".as_slice())));
4134    }
4135
4136    #[test]
4137    fn test_unhex_ignore_chars_only_between_byte_pairs() {
4138        let f = UnhexFunc;
4139        let result = f
4140            .invoke(&[
4141                SqliteValue::Text(SmallText::from_string("AB CD")),
4142                SqliteValue::Text(SmallText::from_string(" ")),
4143            ])
4144            .unwrap();
4145        assert_eq!(result, SqliteValue::Blob(Arc::from([0xAB, 0xCD])));
4146
4147        let result = f
4148            .invoke(&[
4149                SqliteValue::Text(SmallText::from_string("A BCD")),
4150                SqliteValue::Text(SmallText::from_string(" ")),
4151            ])
4152            .unwrap();
4153        assert_eq!(result, SqliteValue::Null);
4154    }
4155
4156    #[test]
4157    fn test_unhex_null_ignore_argument_returns_null() {
4158        let f = UnhexFunc;
4159        let result = f
4160            .invoke(&[
4161                SqliteValue::Text(SmallText::from_string("41")),
4162                SqliteValue::Null,
4163            ])
4164            .unwrap();
4165        assert_eq!(result, SqliteValue::Null);
4166    }
4167
4168    #[test]
4169    fn test_unhex_hex_digits_in_ignore_argument_do_not_ignore_digits() {
4170        let f = UnhexFunc;
4171        let result = f
4172            .invoke(&[
4173                SqliteValue::Text(SmallText::from_string("41")),
4174                SqliteValue::Text(SmallText::from_string("4")),
4175            ])
4176            .unwrap();
4177        assert_eq!(result, SqliteValue::Blob(Arc::from(b"A".as_slice())));
4178    }
4179
4180    #[test]
4181    #[ignore = "perf-only benchmark"]
4182    fn perf_unhex_text_args() {
4183        use std::hint::black_box;
4184        use std::time::Instant;
4185
4186        const INVOCATIONS: usize = 300_000;
4187        const REPEATS: usize = 7;
4188
4189        let f = UnhexFunc;
4190        let plain_args = [SqliteValue::Text(SmallText::from_string(
4191            "48656C6C6F776F726C64",
4192        ))];
4193        let ignore_args = [
4194            SqliteValue::Text(SmallText::from_string("48-65-6C-6C-6F")),
4195            SqliteValue::Text(SmallText::from_string("-")),
4196        ];
4197        let mut plain_best_ns = u128::MAX;
4198        let mut ignore_best_ns = u128::MAX;
4199        let mut checksum = 0usize;
4200
4201        for _ in 0..REPEATS {
4202            let started = Instant::now();
4203            for _ in 0..INVOCATIONS {
4204                let result = black_box(
4205                    f.invoke(black_box(plain_args.as_slice()))
4206                        .expect("unhex benchmark invocation must succeed"),
4207                );
4208                if let SqliteValue::Blob(blob) = result {
4209                    checksum = checksum.wrapping_add(blob.len());
4210                }
4211            }
4212            plain_best_ns = plain_best_ns.min(started.elapsed().as_nanos());
4213
4214            let started = Instant::now();
4215            for _ in 0..INVOCATIONS {
4216                let result = black_box(
4217                    f.invoke(black_box(ignore_args.as_slice()))
4218                        .expect("unhex ignore benchmark invocation must succeed"),
4219                );
4220                if let SqliteValue::Blob(blob) = result {
4221                    checksum = checksum.wrapping_add(blob.len());
4222                }
4223            }
4224            ignore_best_ns = ignore_best_ns.min(started.elapsed().as_nanos());
4225        }
4226
4227        println!(
4228            "unhex_text_args invocations={INVOCATIONS} repeats={REPEATS} plain_best_ns={plain_best_ns} ignore_best_ns={ignore_best_ns} checksum={checksum}"
4229        );
4230    }
4231
4232    // ── unicode ──────────────────────────────────────────────────────────
4233
4234    #[test]
4235    fn test_unicode_first_char() {
4236        assert_eq!(
4237            invoke1(&UnicodeFunc, SqliteValue::Text(SmallText::from_string("A"))).unwrap(),
4238            SqliteValue::Integer(65)
4239        );
4240    }
4241
4242    #[test]
4243    fn test_unicode_text_stops_at_nul() {
4244        assert_eq!(
4245            invoke1(
4246                &UnicodeFunc,
4247                SqliteValue::Text(SmallText::from_string("\0A"))
4248            )
4249            .unwrap(),
4250            SqliteValue::Null
4251        );
4252        assert_eq!(
4253            invoke1(
4254                &UnicodeFunc,
4255                SqliteValue::Text(SmallText::from_string("A\0"))
4256            )
4257            .unwrap(),
4258            SqliteValue::Integer(65)
4259        );
4260    }
4261
4262    #[test]
4263    fn test_unicode_blob_uses_sqlite_utf8_reader() {
4264        let cases: &[(&[u8], SqliteValue)] = &[
4265            (&[0x00, 0x41], SqliteValue::Null),
4266            (&[0x80], SqliteValue::Integer(128)),
4267            (&[0xC2, 0x80], SqliteValue::Integer(128)),
4268            (&[0xC2, 0x80, 0x80], SqliteValue::Integer(8192)),
4269            (&[0xED, 0xA0, 0x80], SqliteValue::Integer(65_533)),
4270            (&[0xF4, 0x90, 0x80, 0x80], SqliteValue::Integer(1_114_112)),
4271        ];
4272
4273        for (bytes, expected) in cases {
4274            assert_eq!(
4275                invoke1(&UnicodeFunc, SqliteValue::Blob(Arc::from(*bytes))).unwrap(),
4276                expected.clone()
4277            );
4278        }
4279    }
4280
4281    #[test]
4282    #[ignore = "perf-only benchmark"]
4283    fn perf_unicode_text_arg() {
4284        use std::hint::black_box;
4285        use std::time::Instant;
4286
4287        const INVOCATIONS: usize = 1_000_000;
4288        const REPEATS: usize = 7;
4289
4290        let f = UnicodeFunc;
4291        let args = [SqliteValue::Text(SmallText::from_string("Alphabet soup"))];
4292        let mut text_best_ns = u128::MAX;
4293        let mut checksum = 0i64;
4294
4295        for _ in 0..REPEATS {
4296            let started = Instant::now();
4297            for _ in 0..INVOCATIONS {
4298                let result = black_box(
4299                    f.invoke(black_box(args.as_slice()))
4300                        .expect("unicode benchmark invocation must succeed"),
4301                );
4302                if let SqliteValue::Integer(codepoint) = result {
4303                    checksum = checksum.wrapping_add(codepoint);
4304                }
4305            }
4306            text_best_ns = text_best_ns.min(started.elapsed().as_nanos());
4307        }
4308
4309        println!(
4310            "unicode_text_arg invocations={INVOCATIONS} repeats={REPEATS} text_best_ns={text_best_ns} checksum={checksum}"
4311        );
4312    }
4313
4314    // ── soundex ──────────────────────────────────────────────────────────
4315
4316    #[test]
4317    fn test_soundex_basic() {
4318        assert_eq!(
4319            invoke1(
4320                &SoundexFunc,
4321                SqliteValue::Text(SmallText::from_string("Robert"))
4322            )
4323            .unwrap(),
4324            SqliteValue::Text(SmallText::from_string("R163"))
4325        );
4326    }
4327
4328    #[test]
4329    #[ignore = "perf-only benchmark"]
4330    fn perf_soundex_text_arg() {
4331        use std::hint::black_box;
4332        use std::time::Instant;
4333
4334        const INVOCATIONS: usize = 1_000_000;
4335        const REPEATS: usize = 7;
4336
4337        let f = SoundexFunc;
4338        let args = [SqliteValue::Text(SmallText::from_string("Robert"))];
4339        let mut text_best_ns = u128::MAX;
4340        let mut checksum = 0usize;
4341
4342        for _ in 0..REPEATS {
4343            let started = Instant::now();
4344            for _ in 0..INVOCATIONS {
4345                let result = black_box(
4346                    f.invoke(black_box(args.as_slice()))
4347                        .expect("soundex benchmark invocation must succeed"),
4348                );
4349                if let SqliteValue::Text(text) = result {
4350                    checksum = checksum.wrapping_add(text.len());
4351                }
4352            }
4353            text_best_ns = text_best_ns.min(started.elapsed().as_nanos());
4354        }
4355
4356        println!(
4357            "soundex_text_arg invocations={INVOCATIONS} repeats={REPEATS} text_best_ns={text_best_ns} checksum={checksum}"
4358        );
4359    }
4360
4361    // ── substr ───────────────────────────────────────────────────────────
4362
4363    #[test]
4364    fn test_substr_basic() {
4365        let f = SubstrFunc;
4366        assert_eq!(
4367            f.invoke(&[
4368                SqliteValue::Text(SmallText::from_string("hello")),
4369                SqliteValue::Integer(2),
4370                SqliteValue::Integer(3),
4371            ])
4372            .unwrap(),
4373            SqliteValue::Text(SmallText::from_string("ell"))
4374        );
4375    }
4376
4377    #[test]
4378    fn test_substr_start_zero_quirk() {
4379        // substr('hello', 0, 3) returns 2 chars from start
4380        let f = SubstrFunc;
4381        let result = f
4382            .invoke(&[
4383                SqliteValue::Text(SmallText::from_string("hello")),
4384                SqliteValue::Integer(0),
4385                SqliteValue::Integer(3),
4386            ])
4387            .unwrap();
4388        assert_eq!(result, SqliteValue::Text(SmallText::from_string("he")));
4389    }
4390
4391    #[test]
4392    fn test_substr_negative_start() {
4393        // substr('hello', -2) = 'lo'
4394        let f = SubstrFunc;
4395        let result = f
4396            .invoke(&[
4397                SqliteValue::Text(SmallText::from_string("hello")),
4398                SqliteValue::Integer(-2),
4399            ])
4400            .unwrap();
4401        assert_eq!(result, SqliteValue::Text(SmallText::from_string("lo")));
4402    }
4403
4404    #[test]
4405    fn test_substr_negative_length() {
4406        let f = SubstrFunc;
4407        let t = |s: &str| SqliteValue::Text(SmallText::from_string(s));
4408        let i = SqliteValue::Integer;
4409        // SUBSTR('hello', 3, -2) => 'he' (2 chars before position 3)
4410        assert_eq!(f.invoke(&[t("hello"), i(3), i(-2)]).unwrap(), t("he"));
4411        // SUBSTR('hello', 3, -5) => 'he' (clamped at start)
4412        assert_eq!(f.invoke(&[t("hello"), i(3), i(-5)]).unwrap(), t("he"));
4413        // SUBSTR('hello', 1, -1) => '' (nothing before position 1)
4414        assert_eq!(f.invoke(&[t("hello"), i(1), i(-1)]).unwrap(), t(""));
4415    }
4416
4417    #[test]
4418    fn test_substr_negative_start_negative_length() {
4419        let f = SubstrFunc;
4420        let t = |s: &str| SqliteValue::Text(SmallText::from_string(s));
4421        let i = SqliteValue::Integer;
4422        // SUBSTR('hello', -2, -2) => 'el' (C SQLite confirmed)
4423        assert_eq!(f.invoke(&[t("hello"), i(-2), i(-2)]).unwrap(), t("el"));
4424    }
4425
4426    #[test]
4427    fn test_substr_edge_cases() {
4428        let f = SubstrFunc;
4429        let t = |s: &str| SqliteValue::Text(SmallText::from_string(s));
4430        let i = SqliteValue::Integer;
4431        // Past end
4432        assert_eq!(f.invoke(&[t("hello"), i(6), i(2)]).unwrap(), t(""));
4433        // Way before start
4434        assert_eq!(f.invoke(&[t("hello"), i(-10), i(3)]).unwrap(), t(""));
4435        // Negative start covering entire string
4436        assert_eq!(f.invoke(&[t("hello"), i(-5), i(6)]).unwrap(), t("hello"));
4437        // start=0, length=1 => '' (quirk)
4438        assert_eq!(f.invoke(&[t("hello"), i(0), i(1)]).unwrap(), t(""));
4439        // start=0, negative length
4440        assert_eq!(f.invoke(&[t("hello"), i(0), i(-1)]).unwrap(), t(""));
4441        // Empty string
4442        assert_eq!(f.invoke(&[t(""), i(1), i(1)]).unwrap(), t(""));
4443    }
4444
4445    #[test]
4446    fn test_substr_blob_negative_length() {
4447        let f = SubstrFunc;
4448        let i = SqliteValue::Integer;
4449        let blob = SqliteValue::Blob(Arc::from([1, 2, 3, 4, 5].as_slice()));
4450        // SUBSTR(X'0102030405', -2, -2) => X'0203' (matches text behavior)
4451        assert_eq!(
4452            f.invoke(&[blob, i(-2), i(-2)]).unwrap(),
4453            SqliteValue::Blob(Arc::from([2, 3].as_slice()))
4454        );
4455    }
4456
4457    // ── like ─────────────────────────────────────────────────────────────
4458
4459    #[test]
4460    fn test_like_case_insensitive() {
4461        assert_eq!(
4462            invoke2(
4463                &LikeFunc,
4464                SqliteValue::Text(SmallText::from_string("ABC")),
4465                SqliteValue::Text(SmallText::from_string("abc"))
4466            )
4467            .unwrap(),
4468            SqliteValue::Integer(1)
4469        );
4470    }
4471
4472    #[test]
4473    fn test_like_escape() {
4474        let f = LikeFunc;
4475        let result = f
4476            .invoke(&[
4477                SqliteValue::Text(SmallText::from_string("10\\%")),
4478                SqliteValue::Text(SmallText::from_string("10%")),
4479                SqliteValue::Text(SmallText::from_string("\\")),
4480            ])
4481            .unwrap();
4482        assert_eq!(result, SqliteValue::Integer(1));
4483    }
4484
4485    #[test]
4486    fn test_like_escape_rejects_empty_string() {
4487        let err = LikeFunc
4488            .invoke(&[
4489                SqliteValue::Text(SmallText::from_string("a")),
4490                SqliteValue::Text(SmallText::from_string("a")),
4491                SqliteValue::Text(SmallText::new("")),
4492            ])
4493            .unwrap_err();
4494        assert!(
4495            err.to_string()
4496                .contains("ESCAPE expression must be a single character")
4497        );
4498    }
4499
4500    #[test]
4501    fn test_like_escape_rejects_multi_character_string() {
4502        let err = LikeFunc
4503            .invoke(&[
4504                SqliteValue::Text(SmallText::from_string("a")),
4505                SqliteValue::Text(SmallText::from_string("a")),
4506                SqliteValue::Text(SmallText::from_string("xx")),
4507            ])
4508            .unwrap_err();
4509        assert!(
4510            err.to_string()
4511                .contains("ESCAPE expression must be a single character")
4512        );
4513    }
4514
4515    #[test]
4516    fn test_like_percent() {
4517        assert_eq!(
4518            invoke2(
4519                &LikeFunc,
4520                SqliteValue::Text(SmallText::from_string("%ell%")),
4521                SqliteValue::Text(SmallText::from_string("Hello"))
4522            )
4523            .unwrap(),
4524            SqliteValue::Integer(1)
4525        );
4526    }
4527
4528    // ── glob ─────────────────────────────────────────────────────────────
4529
4530    #[test]
4531    fn test_glob_star() {
4532        assert_eq!(
4533            invoke2(
4534                &GlobFunc,
4535                SqliteValue::Text(SmallText::from_string("*.txt")),
4536                SqliteValue::Text(SmallText::from_string("file.txt"))
4537            )
4538            .unwrap(),
4539            SqliteValue::Integer(1)
4540        );
4541    }
4542
4543    #[test]
4544    fn test_glob_case_sensitive() {
4545        assert_eq!(
4546            invoke2(
4547                &GlobFunc,
4548                SqliteValue::Text(SmallText::from_string("ABC")),
4549                SqliteValue::Text(SmallText::from_string("abc"))
4550            )
4551            .unwrap(),
4552            SqliteValue::Integer(0)
4553        );
4554    }
4555
4556    // ── format ───────────────────────────────────────────────────────────
4557
4558    #[test]
4559    fn test_format_specifiers() {
4560        let f = FormatFunc;
4561        let result = f
4562            .invoke(&[
4563                SqliteValue::Text(SmallText::from_string("%d %s")),
4564                SqliteValue::Integer(42),
4565                SqliteValue::Text(SmallText::from_string("hello")),
4566            ])
4567            .unwrap();
4568        assert_eq!(
4569            result,
4570            SqliteValue::Text(SmallText::from_string("42 hello"))
4571        );
4572    }
4573
4574    #[test]
4575    fn test_format_n_noop() {
4576        let f = FormatFunc;
4577        // %n should not crash or do anything
4578        let result = f
4579            .invoke(&[SqliteValue::Text(SmallText::from_string("before%nafter"))])
4580            .unwrap();
4581        assert_eq!(
4582            result,
4583            SqliteValue::Text(SmallText::from_string("beforeafter"))
4584        );
4585    }
4586
4587    #[test]
4588    fn test_format_alternate_form_hex_octal() {
4589        // bd-w54bm: `#` flag prefixes 0x/0X (hex) or 0 (octal) for nonzero values.
4590        let cases: &[(&str, i64, &str)] = &[
4591            ("%#x", 255, "0xff"),
4592            ("%#X", 255, "0XFF"),
4593            ("%#o", 64, "0100"),
4594            ("%#x", 0, "0"),        // zero gets no prefix
4595            ("%#o", 0, "0"),        // zero gets no prefix
4596            ("%#5x", 255, " 0xff"), // prefix counts toward space pad
4597            ("%#8x", 255, "    0xff"),
4598            ("%#08x", 255, "0x000000ff"), // zero pad pads digits, prefix outside
4599            ("%-#8x", 255, "0xff    "),   // '-' (no '0') -> space pad, left aligned
4600            ("%-08x", 255, "000000ff"),   // '-' does NOT override '0' in SQLite
4601            ("%#08o", 64, "000000100"),
4602            ("%#x", -1, "0xffffffffffffffff"),
4603        ];
4604        for (fmt, arg, want) in cases {
4605            let f = FormatFunc;
4606            let result = f
4607                .invoke(&[
4608                    SqliteValue::Text(SmallText::from_string(*fmt)),
4609                    SqliteValue::Integer(*arg),
4610                ])
4611                .unwrap();
4612            assert_eq!(
4613                result,
4614                SqliteValue::Text(SmallText::from_string((*want).to_owned())),
4615                "format({fmt:?}, {arg})"
4616            );
4617        }
4618    }
4619
4620    #[test]
4621    fn test_format_empty_string_is_null() {
4622        // bd-13ivh: an empty format string yields NULL (the StrAccum is never
4623        // touched), while a non-empty format that renders to nothing still
4624        // yields empty TEXT.
4625        let f = FormatFunc;
4626        assert_eq!(
4627            f.invoke(&[SqliteValue::Text(SmallText::from_string(""))])
4628                .unwrap(),
4629            SqliteValue::Null
4630        );
4631        // Non-empty format rendering to empty output is still TEXT, not NULL.
4632        assert_eq!(
4633            f.invoke(&[
4634                SqliteValue::Text(SmallText::from_string("%s")),
4635                SqliteValue::Null,
4636            ])
4637            .unwrap(),
4638            SqliteValue::Text(SmallText::from_string(String::new()))
4639        );
4640    }
4641
4642    // ── sqlite_version ───────────────────────────────────────────────────
4643
4644    #[test]
4645    fn test_sqlite_version_format() {
4646        let result = SqliteVersionFunc.invoke(&[]).unwrap();
4647        match result {
4648            SqliteValue::Text(v) => {
4649                assert_eq!(v.split('.').count(), 3, "version must be N.N.N format");
4650            }
4651            other => unreachable!("expected text, got {other:?}"),
4652        }
4653    }
4654
4655    #[test]
4656    fn test_sqlite_compileoption_used_matches_sqlite_prefix_and_value_options() {
4657        let func = SqliteCompileoptionUsedFunc;
4658        assert_eq!(
4659            invoke1(
4660                &func,
4661                SqliteValue::Text(SmallText::from_string("THREADSAFE"))
4662            )
4663            .unwrap(),
4664            SqliteValue::Integer(1)
4665        );
4666        let expected_icu_enabled = i64::from(cfg!(feature = "ext-icu"));
4667        assert_eq!(
4668            invoke1(
4669                &func,
4670                SqliteValue::Text(SmallText::from_string("SQLITE_ENABLE_ICU"))
4671            )
4672            .unwrap(),
4673            SqliteValue::Integer(expected_icu_enabled)
4674        );
4675        assert_eq!(
4676            invoke1(
4677                &func,
4678                SqliteValue::Text(SmallText::from_string("sqlite_enable_icu"))
4679            )
4680            .unwrap(),
4681            SqliteValue::Integer(expected_icu_enabled)
4682        );
4683        assert_eq!(
4684            invoke1(
4685                &func,
4686                SqliteValue::Text(SmallText::from_string("OMIT_LOAD_EXTENSION"))
4687            )
4688            .unwrap(),
4689            SqliteValue::Integer(1)
4690        );
4691        assert_eq!(
4692            invoke1(
4693                &func,
4694                SqliteValue::Text(SmallText::from_string("ENABLE_FTS3"))
4695            )
4696            .unwrap(),
4697            SqliteValue::Integer(0)
4698        );
4699        assert_eq!(
4700            invoke1(&func, SqliteValue::Null).unwrap(),
4701            SqliteValue::Null
4702        );
4703    }
4704
4705    #[test]
4706    #[ignore = "perf-only benchmark"]
4707    fn perf_compileoption_used_text_args() {
4708        use std::hint::black_box;
4709        use std::time::Instant;
4710
4711        const INVOCATIONS: usize = 1_000_000;
4712        const REPEATS: usize = 7;
4713
4714        let f = SqliteCompileoptionUsedFunc;
4715        let present_args = [SqliteValue::Text(SmallText::from_string(
4716            "SQLITE_ENABLE_ICU",
4717        ))];
4718        let absent_args = [SqliteValue::Text(SmallText::from_string(
4719            "ENABLE_NOT_PRESENT",
4720        ))];
4721
4722        let mut present_best_ns = u128::MAX;
4723        let mut absent_best_ns = u128::MAX;
4724        let mut checksum = 0i64;
4725        for _ in 0..REPEATS {
4726            let started = Instant::now();
4727            for _ in 0..INVOCATIONS {
4728                let result = black_box(
4729                    f.invoke(black_box(present_args.as_slice()))
4730                        .expect("compileoption present benchmark invocation must succeed"),
4731                );
4732                if let SqliteValue::Integer(value) = result {
4733                    checksum = checksum.wrapping_add(value);
4734                }
4735            }
4736            present_best_ns = present_best_ns.min(started.elapsed().as_nanos());
4737
4738            let started = Instant::now();
4739            for _ in 0..INVOCATIONS {
4740                let result = black_box(
4741                    f.invoke(black_box(absent_args.as_slice()))
4742                        .expect("compileoption absent benchmark invocation must succeed"),
4743                );
4744                if let SqliteValue::Integer(value) = result {
4745                    checksum = checksum.wrapping_add(value);
4746                }
4747            }
4748            absent_best_ns = absent_best_ns.min(started.elapsed().as_nanos());
4749        }
4750
4751        println!(
4752            "compileoption_used_text_args invocations={INVOCATIONS} repeats={REPEATS} present_best_ns={present_best_ns} absent_best_ns={absent_best_ns} checksum={checksum}"
4753        );
4754    }
4755
4756    #[test]
4757    fn test_sqlite_compileoption_get_enumerates_canonical_option_list() {
4758        let func = SqliteCompileoptionGetFunc;
4759        for (index, option) in sqlite_compile_options().iter().enumerate() {
4760            assert_eq!(
4761                invoke1(&func, SqliteValue::Integer(index as i64)).unwrap(),
4762                SqliteValue::Text(SmallText::new(option))
4763            );
4764        }
4765        assert_eq!(
4766            invoke1(&func, SqliteValue::Integer(-1)).unwrap(),
4767            SqliteValue::Null
4768        );
4769        assert_eq!(
4770            invoke1(
4771                &func,
4772                SqliteValue::Integer(sqlite_compile_options().len() as i64)
4773            )
4774            .unwrap(),
4775            SqliteValue::Null
4776        );
4777    }
4778
4779    // ── register_builtins ────────────────────────────────────────────────
4780
4781    #[test]
4782    fn test_register_builtins_all_present() {
4783        let mut registry = FunctionRegistry::new();
4784        register_builtins(&mut registry);
4785
4786        // Spot-check key functions are registered
4787        assert!(registry.find_scalar("abs", 1).is_some());
4788        assert!(registry.find_scalar("typeof", 1).is_some());
4789        assert!(registry.find_scalar("length", 1).is_some());
4790        assert!(registry.find_scalar("lower", 1).is_some());
4791        assert!(registry.find_scalar("upper", 1).is_some());
4792        assert!(registry.find_scalar("hex", 1).is_some());
4793        assert!(registry.find_scalar("coalesce", 3).is_some());
4794        assert!(registry.find_scalar("concat", 2).is_some());
4795        assert!(registry.find_scalar("like", 2).is_some());
4796        assert!(registry.find_scalar("glob", 2).is_some());
4797        assert!(registry.find_scalar("round", 1).is_some());
4798        assert!(registry.find_scalar("substr", 2).is_some());
4799        assert!(registry.find_scalar("substring", 3).is_some());
4800        assert!(registry.find_scalar("sqlite_version", 0).is_some());
4801        assert!(registry.find_scalar("iif", 3).is_some());
4802        assert!(registry.find_scalar("if", 3).is_some());
4803        assert!(registry.find_scalar("format", 1).is_some());
4804        assert!(registry.find_scalar("printf", 1).is_some());
4805        assert!(registry.find_scalar("max", 2).is_some());
4806        assert!(registry.find_scalar("min", 2).is_some());
4807        assert!(registry.find_scalar("sign", 1).is_some());
4808        assert!(registry.find_scalar("random", 0).is_some());
4809
4810        // Newer SQLite scalar functions (3.41+)
4811        assert!(registry.find_scalar("concat_ws", 3).is_some());
4812        assert!(registry.find_scalar("octet_length", 1).is_some());
4813        assert!(registry.find_scalar("unhex", 1).is_some());
4814        assert!(registry.find_scalar("timediff", 2).is_some());
4815        assert!(registry.find_scalar("unistr", 1).is_some());
4816        assert!(registry.find_scalar("unistr_quote", 1).is_some());
4817
4818        // Percentile family enabled by default.
4819        assert!(registry.find_aggregate("median", 1).is_some());
4820        assert!(registry.find_aggregate("percentile", 2).is_some());
4821        assert!(registry.find_aggregate("percentile_cont", 2).is_some());
4822        assert!(registry.find_aggregate("percentile_disc", 2).is_some());
4823
4824        // Loadable extensions are not exposed as SQL function by default.
4825        assert!(registry.find_scalar("load_extension", 1).is_none());
4826        assert!(registry.find_scalar("load_extension", 2).is_none());
4827    }
4828
4829    #[test]
4830    fn test_register_builtins_rejects_invalid_variadic_arities() {
4831        let mut registry = FunctionRegistry::new();
4832        register_builtins(&mut registry);
4833
4834        for (name, too_few, valid, too_many) in [
4835            ("coalesce", 1, 2, None),
4836            ("concat", 0, 1, None),
4837            ("concat_ws", 1, 2, None),
4838            ("trim", 0, 1, Some(3)),
4839            ("ltrim", 0, 1, Some(3)),
4840            ("rtrim", 0, 1, Some(3)),
4841            ("round", 0, 1, Some(3)),
4842            ("unhex", 0, 1, Some(3)),
4843            ("substr", 1, 2, Some(4)),
4844            ("substring", 1, 2, Some(4)),
4845            ("max", 0, 1, None),
4846            ("min", 0, 1, None),
4847        ] {
4848            assert_wrong_arg_count(&registry, name, too_few);
4849            assert!(
4850                registry.find_scalar(name, valid).is_some(),
4851                "{name}/{valid} should resolve"
4852            );
4853            if let Some(arity) = too_many {
4854                assert_wrong_arg_count(&registry, name, arity);
4855            }
4856        }
4857
4858        assert!(registry.find_scalar("char", 0).is_some());
4859        assert!(registry.find_scalar("format", 0).is_some());
4860        assert!(registry.find_scalar("printf", 0).is_some());
4861    }
4862
4863    #[test]
4864    fn test_e2e_registry_invoke_through_lookup() {
4865        let mut registry = FunctionRegistry::new();
4866        register_builtins(&mut registry);
4867
4868        // Look up abs, invoke it
4869        let abs = registry.find_scalar("ABS", 1).unwrap();
4870        assert_eq!(
4871            abs.invoke(&[SqliteValue::Integer(-42)]).unwrap(),
4872            SqliteValue::Integer(42)
4873        );
4874
4875        // Look up typeof, invoke it
4876        let typeof_fn = registry.find_scalar("typeof", 1).unwrap();
4877        assert_eq!(
4878            typeof_fn
4879                .invoke(&[SqliteValue::Text(SmallText::from_string("hello"))])
4880                .unwrap(),
4881            SqliteValue::Text(SmallText::from_string("text"))
4882        );
4883
4884        // Look up coalesce (variadic), invoke with 4 args
4885        let coalesce = registry.find_scalar("COALESCE", 4).unwrap();
4886        assert_eq!(
4887            coalesce
4888                .invoke(&[
4889                    SqliteValue::Null,
4890                    SqliteValue::Null,
4891                    SqliteValue::Integer(42),
4892                    SqliteValue::Integer(99),
4893                ])
4894                .unwrap(),
4895            SqliteValue::Integer(42)
4896        );
4897    }
4898
4899    // ── bd-13r.8: Non-Deterministic Function Evaluation Semantics ──
4900
4901    #[test]
4902    fn test_nondeterministic_functions_flagged() {
4903        // These functions MUST be marked non-deterministic to prevent
4904        // unsafe planner optimizations (hoisting, CSE).
4905        assert!(!RandomFunc.is_deterministic());
4906        assert!(!RandomblobFunc.is_deterministic());
4907        assert!(!ChangesFunc.is_deterministic());
4908        assert!(!TotalChangesFunc.is_deterministic());
4909        assert!(!LastInsertRowidFunc.is_deterministic());
4910    }
4911
4912    #[test]
4913    fn test_deterministic_functions_flagged() {
4914        // Deterministic functions are safe for constant folding/CSE.
4915        assert!(AbsFunc.is_deterministic());
4916        assert!(LengthFunc.is_deterministic());
4917        assert!(TypeofFunc.is_deterministic());
4918        assert!(UpperFunc.is_deterministic());
4919        assert!(LowerFunc.is_deterministic());
4920        assert!(HexFunc.is_deterministic());
4921        assert!(CoalesceFunc.is_deterministic());
4922        assert!(IifFunc.is_deterministic());
4923    }
4924
4925    #[test]
4926    fn test_random_produces_different_values() {
4927        // random() should produce different values on successive calls
4928        // (verifying per-call evaluation, not constant folding).
4929        let a = RandomFunc.invoke(&[]).unwrap();
4930        let b = RandomFunc.invoke(&[]).unwrap();
4931        // With overwhelming probability, two random i64 values differ.
4932        // If they're ever equal, it's a 1-in-2^64 coincidence.
4933        assert_ne!(a.as_integer(), b.as_integer());
4934    }
4935
4936    #[test]
4937    fn test_registry_nondeterministic_lookup() {
4938        let mut registry = FunctionRegistry::default();
4939        register_builtins(&mut registry);
4940
4941        // Non-deterministic functions should be findable and flagged.
4942        let random = registry.find_scalar("random", 0).unwrap();
4943        assert!(!random.is_deterministic());
4944
4945        let changes = registry.find_scalar("changes", 0).unwrap();
4946        assert!(!changes.is_deterministic());
4947
4948        let lir = registry.find_scalar("last_insert_rowid", 0).unwrap();
4949        assert!(!lir.is_deterministic());
4950
4951        // Deterministic function check.
4952        let abs = registry.find_scalar("abs", 1).unwrap();
4953        assert!(abs.is_deterministic());
4954    }
4955}