Skip to main content

fsqlite_func/
builtins.rs

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