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