Skip to main content

fsqlite_func/
builtins.rs

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