Skip to main content

fsqlite_func/
builtins.rs

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