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