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