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, sqlite_float_altform2_digits};
32use fsqlite_types::{SmallText, SqliteValue, TextEncoding};
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    /// The database TEXT encoding for the connection whose statement is
63    /// currently executing on this thread (bd-iubwb). `octet_length(X)` must
64    /// report the byte length of X's TEXT representation *in the database
65    /// encoding*, so a UTF-16 database counts each code unit as two bytes.
66    /// Defaults to `Utf8` (the common case, byte-identical to today), and is
67    /// projected by the Connection before each statement (see
68    /// `sync_change_tracking_context`) and by the VDBE engine before every
69    /// scalar-function invocation, mirroring how `CASE_SENSITIVE_LIKE` is
70    /// threaded so the encoding never has to be passed through every call site.
71    static STATEMENT_TEXT_ENCODING: std::cell::Cell<TextEncoding> =
72        const { std::cell::Cell::new(TextEncoding::Utf8) };
73}
74
75/// Reset the captured statement `'now'` (called by the Connection at each
76/// statement start so the next statement re-reads the wall clock).
77pub fn reset_statement_now() {
78    STATEMENT_NOW.set(None);
79}
80
81/// The `'now'` value already captured for the current statement, if any.
82#[must_use]
83pub fn statement_now() -> Option<f64> {
84    STATEMENT_NOW.with(std::cell::Cell::get)
85}
86
87/// Record the statement `'now'` captured on its first use this statement.
88pub fn set_statement_now(now_jdn: f64) {
89    STATEMENT_NOW.set(Some(now_jdn));
90}
91
92/// Set the active `case_sensitive_like` flag for LIKE evaluation on this thread
93/// (called by the Connection before executing a statement).
94pub fn set_case_sensitive_like(case_sensitive: bool) {
95    CASE_SENSITIVE_LIKE.set(case_sensitive);
96}
97
98/// Read the active `case_sensitive_like` flag for LIKE evaluation on this thread.
99#[must_use]
100pub fn case_sensitive_like_active() -> bool {
101    CASE_SENSITIVE_LIKE.get()
102}
103
104/// Set the active database TEXT encoding for the statement on this thread.
105///
106/// Called by the Connection before executing a statement and by the VDBE engine
107/// before invoking a scalar function. Read by `octet_length()`.
108pub fn set_statement_text_encoding(encoding: TextEncoding) {
109    STATEMENT_TEXT_ENCODING.set(encoding);
110}
111
112/// Read the active database TEXT encoding for the current statement's thread.
113/// Defaults to [`TextEncoding::Utf8`] when nothing has been projected.
114#[must_use]
115pub fn statement_text_encoding() -> TextEncoding {
116    STATEMENT_TEXT_ENCODING.get()
117}
118
119/// Byte length of `text` when serialized in the database `encoding`. UTF-8 is
120/// the string's own byte length; UTF-16 (either endianness) is two bytes per
121/// UTF-16 code unit, which counts a non-BMP scalar (a surrogate pair) as four
122/// bytes exactly as SQLite does.
123#[must_use]
124fn text_octet_length(text: &str, encoding: TextEncoding) -> usize {
125    match encoding {
126        TextEncoding::Utf8 => text.len(),
127        TextEncoding::Utf16le | TextEncoding::Utf16be => 2 * text.encode_utf16().count(),
128    }
129}
130
131/// Connection-scoped change-tracking state projected into builtin execution context.
132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133pub struct ChangeTrackingState {
134    pub last_insert_rowid: i64,
135    pub last_changes: i64,
136    pub total_changes: i64,
137}
138
139/// Replace the full builtin change-tracking context.
140pub fn set_change_tracking_state(state: ChangeTrackingState) {
141    LAST_INSERT_ROWID.set(state.last_insert_rowid);
142    LAST_CHANGES.set(state.last_changes);
143    TOTAL_CHANGES.set(state.total_changes);
144}
145
146/// Read the current builtin change-tracking context for this thread.
147#[must_use]
148pub fn get_change_tracking_state() -> ChangeTrackingState {
149    ChangeTrackingState {
150        last_insert_rowid: LAST_INSERT_ROWID.get(),
151        last_changes: LAST_CHANGES.get(),
152        total_changes: TOTAL_CHANGES.get(),
153    }
154}
155
156/// Set the last insert rowid (called by Connection after INSERT).
157pub fn set_last_insert_rowid(rowid: i64) {
158    LAST_INSERT_ROWID.set(rowid);
159}
160
161/// Get the current last insert rowid.
162pub fn get_last_insert_rowid() -> i64 {
163    LAST_INSERT_ROWID.get()
164}
165
166/// Set the last changes count (called by Connection after DML).
167///
168/// Also accumulates into the cumulative `total_changes` counter.
169pub fn set_last_changes(count: i64) {
170    LAST_CHANGES.set(count);
171    TOTAL_CHANGES.set(TOTAL_CHANGES.get().saturating_add(count));
172}
173
174/// Get the current last changes count.
175pub fn get_last_changes() -> i64 {
176    LAST_CHANGES.get()
177}
178
179/// Get the cumulative total changes since the connection was opened.
180pub fn get_total_changes() -> i64 {
181    TOTAL_CHANGES.get()
182}
183
184/// Reset the cumulative total changes counter (called on new connection open).
185pub fn reset_total_changes() {
186    TOTAL_CHANGES.set(0);
187}
188
189const SQLITE_COMPILE_OPTIONS: &[&str] = &[
190    "COMPILER=rustc",
191    #[cfg(feature = "ext-fts5")]
192    "ENABLE_FTS5",
193    #[cfg(feature = "ext-geopoly")]
194    "ENABLE_GEOPOLY",
195    #[cfg(feature = "ext-icu")]
196    "ENABLE_ICU",
197    #[cfg(feature = "ext-json")]
198    "ENABLE_JSON1",
199    #[cfg(feature = "ext-rtree")]
200    "ENABLE_RTREE",
201    "FRANKENSQLITE",
202    "OMIT_LOAD_EXTENSION",
203    "THREADSAFE=1",
204];
205
206/// Return the canonical compile-option surface exposed by FrankenSQLite.
207#[must_use]
208pub fn sqlite_compile_options() -> &'static [&'static str] {
209    SQLITE_COMPILE_OPTIONS
210}
211
212fn is_sqlite_compile_option_match(query: &str, option: &str) -> bool {
213    let trimmed = query.trim();
214    let normalized = if trimmed
215        .get(..7)
216        .is_some_and(|prefix| prefix.eq_ignore_ascii_case("SQLITE_"))
217    {
218        &trimmed[7..]
219    } else {
220        trimmed
221    };
222    if normalized.is_empty() {
223        return false;
224    }
225    if option.eq_ignore_ascii_case(normalized) {
226        return true;
227    }
228    option
229        .get(..normalized.len())
230        .is_some_and(|prefix| prefix.eq_ignore_ascii_case(normalized))
231        && option
232            .as_bytes()
233            .get(normalized.len())
234            .is_none_or(|next| !next.is_ascii_alphanumeric() && *next != b'_')
235}
236
237/// Report whether the given SQLite-style compile-option query matches the
238/// current FrankenSQLite build surface.
239#[must_use]
240pub fn sqlite_compileoption_used(query: &str) -> bool {
241    sqlite_compile_options()
242        .iter()
243        .any(|option| is_sqlite_compile_option_match(query, option))
244}
245
246// ── Helpers ───────────────────────────────────────────────────────────────
247
248/// Standard NULL propagation: if any arg is NULL, return NULL.
249fn null_propagate(args: &[SqliteValue]) -> Option<SqliteValue> {
250    if args.iter().any(SqliteValue::is_null) {
251        Some(SqliteValue::Null)
252    } else {
253        None
254    }
255}
256
257// ── abs(X) ────────────────────────────────────────────────────────────────
258
259pub struct AbsFunc;
260
261impl ScalarFunction for AbsFunc {
262    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
263        if args[0].is_null() {
264            return Ok(SqliteValue::Null);
265        }
266        match &args[0] {
267            SqliteValue::Integer(i) => {
268                if *i == i64::MIN {
269                    return Err(FrankenError::IntegerOverflow);
270                }
271                Ok(SqliteValue::Integer(i.abs()))
272            }
273            other => {
274                let f = other.to_float();
275                // Match C SQLite: abs uses `x < 0 ? -x : x`.
276                // IEEE 754: -0.0 < 0.0 is false, so abs(-0.0) == -0.0.
277                Ok(SqliteValue::Float(if f < 0.0 { -f } else { f }))
278            }
279        }
280    }
281
282    fn num_args(&self) -> i32 {
283        1
284    }
285
286    fn name(&self) -> &str {
287        "abs"
288    }
289}
290
291// ── char(X1, X2, ...) ────────────────────────────────────────────────────
292
293pub struct CharFunc;
294
295impl ScalarFunction for CharFunc {
296    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
297        let mut result = String::new();
298        for arg in args {
299            // C SQLite: sqlite3_value_int(NULL) returns 0, so NULL → U+0000.
300            let ch = u32::try_from(arg.to_integer())
301                .ok()
302                .and_then(char::from_u32)
303                .unwrap_or(char::REPLACEMENT_CHARACTER);
304            result.push(ch);
305        }
306        Ok(SqliteValue::Text(SmallText::from_string(result)))
307    }
308
309    fn is_deterministic(&self) -> bool {
310        true
311    }
312
313    fn num_args(&self) -> i32 {
314        -1 // variadic
315    }
316
317    fn name(&self) -> &str {
318        "char"
319    }
320}
321
322// ── coalesce(X, Y, ...) ─────────────────────────────────────────────────
323
324pub struct CoalesceFunc;
325
326impl ScalarFunction for CoalesceFunc {
327    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
328        // Return first non-NULL argument.
329        // NOTE: Real short-circuit evaluation happens at the VDBE level.
330        // At the scalar level, all args are already evaluated.
331        for arg in args {
332            if !arg.is_null() {
333                return Ok(arg.clone());
334            }
335        }
336        Ok(SqliteValue::Null)
337    }
338
339    fn num_args(&self) -> i32 {
340        -1
341    }
342
343    fn min_args(&self) -> i32 {
344        2
345    }
346
347    fn name(&self) -> &str {
348        "coalesce"
349    }
350}
351
352// ── concat(X, Y, ...) ───────────────────────────────────────────────────
353
354pub struct ConcatFunc;
355
356impl ScalarFunction for ConcatFunc {
357    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
358        let mut result = String::new();
359        for arg in args {
360            // concat treats NULL as empty string (unlike ||)
361            if !arg.is_null() {
362                result.push_str(text_arg(arg).as_ref());
363            }
364        }
365        Ok(SqliteValue::Text(SmallText::from_string(result)))
366    }
367
368    fn num_args(&self) -> i32 {
369        -1
370    }
371
372    fn min_args(&self) -> i32 {
373        1
374    }
375
376    fn name(&self) -> &str {
377        "concat"
378    }
379}
380
381// ── concat_ws(SEP, X, Y, ...) ───────────────────────────────────────────
382
383pub struct ConcatWsFunc;
384
385impl ScalarFunction for ConcatWsFunc {
386    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
387        if args.is_empty() {
388            return Ok(SqliteValue::Text(SmallText::new("")));
389        }
390        // C SQLite: concat_ws(NULL, ...) returns NULL when separator is NULL.
391        if args[0].is_null() {
392            return Ok(SqliteValue::Null);
393        }
394        let sep = text_arg(&args[0]);
395        let mut result = String::new();
396        let mut has_part = false;
397        for arg in &args[1..] {
398            // C SQLite skips only NULL value arguments. Empty text is still a
399            // value: `concat_ws('|','','x')` yields `'|x'`.
400            if arg.is_null() {
401                continue;
402            }
403            let part = text_arg(arg);
404            if has_part {
405                result.push_str(sep.as_ref());
406            }
407            result.push_str(part.as_ref());
408            has_part = true;
409        }
410        Ok(SqliteValue::Text(SmallText::from_string(result)))
411    }
412
413    fn num_args(&self) -> i32 {
414        -1
415    }
416
417    fn min_args(&self) -> i32 {
418        2
419    }
420
421    fn name(&self) -> &str {
422        "concat_ws"
423    }
424}
425
426// ── hex(X) ───────────────────────────────────────────────────────────────
427
428pub struct HexFunc;
429
430impl ScalarFunction for HexFunc {
431    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
432        // C SQLite hex() calls sqlite3_value_blob(arg) + sqlite3_value_bytes(arg).
433        // For NULL: blob returns NULL ptr, bytes returns 0, producing "" (empty string).
434        // This has been consistent across all SQLite versions including 3.52.0.
435        if args[0].is_null() {
436            return Ok(SqliteValue::Text(SmallText::new("")));
437        }
438        let bytes: Cow<'_, [u8]> = match &args[0] {
439            SqliteValue::Blob(b) => Cow::Borrowed(b.as_ref()),
440            SqliteValue::Text(text) => Cow::Borrowed(text.as_bytes_direct()),
441            // For non-blob: convert to text first, then hex-encode UTF-8 bytes.
442            other => Cow::Owned(other.to_text().into_bytes()),
443        };
444        let mut hex = String::with_capacity(bytes.len() * 2);
445        for b in bytes.as_ref() {
446            let _ = write!(hex, "{b:02X}");
447        }
448        Ok(SqliteValue::Text(SmallText::from_string(hex)))
449    }
450
451    fn num_args(&self) -> i32 {
452        1
453    }
454
455    fn name(&self) -> &str {
456        "hex"
457    }
458}
459
460// ── ifnull(X, Y) ────────────────────────────────────────────────────────
461
462pub struct IfnullFunc;
463
464impl ScalarFunction for IfnullFunc {
465    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
466        if args[0].is_null() {
467            Ok(args[1].clone())
468        } else {
469            Ok(args[0].clone())
470        }
471    }
472
473    fn num_args(&self) -> i32 {
474        2
475    }
476
477    fn name(&self) -> &str {
478        "ifnull"
479    }
480}
481
482// ── iif(COND, TRUE_VAL, FALSE_VAL) ──────────────────────────────────────
483
484pub struct IifFunc;
485
486impl ScalarFunction for IifFunc {
487    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
488        let cond = &args[0];
489        // C SQLite evaluates IIF condition with sqlite3VdbeRealValue != 0.0,
490        // so 0.5 is truthy (non-zero real).
491        let is_true = match cond {
492            SqliteValue::Null => false,
493            SqliteValue::Integer(n) => *n != 0,
494            SqliteValue::Float(f) => *f != 0.0,
495            SqliteValue::Text(_) | SqliteValue::Blob(_) => {
496                let i = cond.to_integer();
497                if i != 0 { true } else { cond.to_float() != 0.0 }
498            }
499        };
500        if is_true {
501            Ok(args[1].clone())
502        } else if args.len() >= 3 {
503            Ok(args[2].clone())
504        } else {
505            // Two-argument form iif(X,Y) is shorthand for iif(X,Y,NULL),
506            // i.e. CASE WHEN X THEN Y END (SQLite 3.48+).
507            Ok(SqliteValue::Null)
508        }
509    }
510
511    fn num_args(&self) -> i32 {
512        -1 // 2 or 3 args
513    }
514
515    fn min_args(&self) -> i32 {
516        2
517    }
518
519    fn max_args(&self) -> Option<i32> {
520        Some(3)
521    }
522
523    fn name(&self) -> &str {
524        "iif"
525    }
526}
527
528// ── instr(X, Y) ─────────────────────────────────────────────────────────
529
530pub struct InstrFunc;
531
532impl ScalarFunction for InstrFunc {
533    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
534        if let Some(null) = null_propagate(args) {
535            return Ok(null);
536        }
537        match (&args[0], &args[1]) {
538            (SqliteValue::Blob(haystack), SqliteValue::Blob(needle)) => {
539                // SQLite: empty needle returns 1, empty haystack with non-empty needle returns 0.
540                if needle.is_empty() {
541                    return Ok(SqliteValue::Integer(1));
542                }
543                if haystack.is_empty() {
544                    return Ok(SqliteValue::Integer(0));
545                }
546                let pos = find_bytes(haystack, needle).map_or(0, |p| p + 1);
547                Ok(SqliteValue::Integer(i64::try_from(pos).unwrap_or(0)))
548            }
549            _ => {
550                // Text: character-level search.
551                // SQLite: empty needle returns 1, empty haystack with non-empty needle returns 0.
552                let haystack = text_arg(&args[0]);
553                let needle = text_arg(&args[1]);
554                let haystack = haystack.as_ref();
555                let needle = needle.as_ref();
556                if needle.is_empty() {
557                    return Ok(SqliteValue::Integer(1));
558                }
559                if haystack.is_empty() {
560                    return Ok(SqliteValue::Integer(0));
561                }
562                let pos = haystack
563                    .find(needle)
564                    .map_or(0, |byte_pos| haystack[..byte_pos].chars().count() + 1);
565                Ok(SqliteValue::Integer(i64::try_from(pos).unwrap_or(0)))
566            }
567        }
568    }
569
570    fn num_args(&self) -> i32 {
571        2
572    }
573
574    fn name(&self) -> &str {
575        "instr"
576    }
577}
578
579fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option<usize> {
580    if needle.is_empty() {
581        return Some(0);
582    }
583    haystack.windows(needle.len()).position(|w| w == needle)
584}
585
586fn sqlite_text_until_nul(text: &str) -> &str {
587    text.split_once('\0').map_or(text, |(prefix, _)| prefix)
588}
589
590// ── length(X) ────────────────────────────────────────────────────────────
591
592pub struct LengthFunc;
593
594impl ScalarFunction for LengthFunc {
595    #[allow(clippy::cast_possible_wrap)]
596    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
597        if args[0].is_null() {
598            return Ok(SqliteValue::Null);
599        }
600        let len = match &args[0] {
601            SqliteValue::Text(s) => {
602                let text = sqlite_text_until_nul(s.as_str());
603                if text.is_ascii() {
604                    text.len()
605                } else {
606                    text.chars().count()
607                }
608            }
609            SqliteValue::Blob(b) => b.len(),
610            other => {
611                // Numbers: length of text representation.
612                let text = other.to_text();
613                let text = sqlite_text_until_nul(&text);
614                if text.is_ascii() {
615                    text.len()
616                } else {
617                    text.chars().count()
618                }
619            }
620        };
621        Ok(SqliteValue::Integer(len as i64))
622    }
623
624    fn num_args(&self) -> i32 {
625        1
626    }
627
628    fn name(&self) -> &str {
629        "length"
630    }
631}
632
633// ── octet_length(X) ─────────────────────────────────────────────────────
634
635pub struct OctetLengthFunc;
636
637impl ScalarFunction for OctetLengthFunc {
638    #[allow(clippy::cast_possible_wrap)]
639    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
640        if args[0].is_null() {
641            return Ok(SqliteValue::Null);
642        }
643        // bd-iubwb: octet_length reports the byte length of X's TEXT rendering
644        // in the DATABASE text encoding (projected via the thread-local), so a
645        // UTF-16 database counts two bytes per code unit. BLOB is raw bytes,
646        // regardless of encoding.
647        let encoding = statement_text_encoding();
648        let len = match &args[0] {
649            SqliteValue::Text(s) => text_octet_length(s.as_str(), encoding),
650            SqliteValue::Blob(b) => b.len(),
651            other => text_octet_length(&other.to_text(), encoding),
652        };
653        Ok(SqliteValue::Integer(len as i64))
654    }
655
656    fn num_args(&self) -> i32 {
657        1
658    }
659
660    fn name(&self) -> &str {
661        "octet_length"
662    }
663}
664
665// ── lower(X) / upper(X) ─────────────────────────────────────────────────
666
667pub struct LowerFunc;
668
669impl ScalarFunction for LowerFunc {
670    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
671        if args[0].is_null() {
672            return Ok(SqliteValue::Null);
673        }
674        let lowered = text_arg(&args[0]).as_ref().to_ascii_lowercase();
675        Ok(SqliteValue::Text(SmallText::from_string(lowered)))
676    }
677
678    fn num_args(&self) -> i32 {
679        1
680    }
681
682    fn name(&self) -> &str {
683        "lower"
684    }
685}
686
687pub struct UpperFunc;
688
689impl ScalarFunction for UpperFunc {
690    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
691        if args[0].is_null() {
692            return Ok(SqliteValue::Null);
693        }
694        let upper = text_arg(&args[0]).as_ref().to_ascii_uppercase();
695        Ok(SqliteValue::Text(SmallText::from_string(upper)))
696    }
697
698    fn num_args(&self) -> i32 {
699        1
700    }
701
702    fn name(&self) -> &str {
703        "upper"
704    }
705}
706
707// ── trim/ltrim/rtrim ────────────────────────────────────────────────────
708
709pub struct TrimFunc;
710pub struct LtrimFunc;
711pub struct RtrimFunc;
712
713fn trim_chars<'a>(s: &'a str, chars: &str) -> &'a str {
714    let char_set: Vec<char> = chars.chars().collect();
715    s.trim_matches(|c: char| char_set.contains(&c))
716}
717
718fn ltrim_chars<'a>(s: &'a str, chars: &str) -> &'a str {
719    let char_set: Vec<char> = chars.chars().collect();
720    s.trim_start_matches(|c: char| char_set.contains(&c))
721}
722
723fn rtrim_chars<'a>(s: &'a str, chars: &str) -> &'a str {
724    let char_set: Vec<char> = chars.chars().collect();
725    s.trim_end_matches(|c: char| char_set.contains(&c))
726}
727
728impl ScalarFunction for TrimFunc {
729    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
730        if args[0].is_null() {
731            return Ok(SqliteValue::Null);
732        }
733        let s = text_arg(&args[0]);
734        let chars = if args.len() > 1 && !args[1].is_null() {
735            text_arg(&args[1])
736        } else {
737            Cow::Borrowed(" ")
738        };
739        Ok(SqliteValue::Text(SmallText::new(trim_chars(
740            s.as_ref(),
741            chars.as_ref(),
742        ))))
743    }
744
745    fn num_args(&self) -> i32 {
746        -1 // 1 or 2 args
747    }
748
749    fn min_args(&self) -> i32 {
750        1
751    }
752
753    fn max_args(&self) -> Option<i32> {
754        Some(2)
755    }
756
757    fn name(&self) -> &str {
758        "trim"
759    }
760}
761
762impl ScalarFunction for LtrimFunc {
763    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
764        if args[0].is_null() {
765            return Ok(SqliteValue::Null);
766        }
767        let s = text_arg(&args[0]);
768        let chars = if args.len() > 1 && !args[1].is_null() {
769            text_arg(&args[1])
770        } else {
771            Cow::Borrowed(" ")
772        };
773        Ok(SqliteValue::Text(SmallText::new(ltrim_chars(
774            s.as_ref(),
775            chars.as_ref(),
776        ))))
777    }
778
779    fn num_args(&self) -> i32 {
780        -1
781    }
782
783    fn min_args(&self) -> i32 {
784        1
785    }
786
787    fn max_args(&self) -> Option<i32> {
788        Some(2)
789    }
790
791    fn name(&self) -> &str {
792        "ltrim"
793    }
794}
795
796impl ScalarFunction for RtrimFunc {
797    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
798        if args[0].is_null() {
799            return Ok(SqliteValue::Null);
800        }
801        let s = text_arg(&args[0]);
802        let chars = if args.len() > 1 && !args[1].is_null() {
803            text_arg(&args[1])
804        } else {
805            Cow::Borrowed(" ")
806        };
807        Ok(SqliteValue::Text(SmallText::new(rtrim_chars(
808            s.as_ref(),
809            chars.as_ref(),
810        ))))
811    }
812
813    fn num_args(&self) -> i32 {
814        -1
815    }
816
817    fn min_args(&self) -> i32 {
818        1
819    }
820
821    fn max_args(&self) -> Option<i32> {
822        Some(2)
823    }
824
825    fn name(&self) -> &str {
826        "rtrim"
827    }
828}
829
830// ── nullif(X, Y) ────────────────────────────────────────────────────────
831
832pub struct NullifFunc;
833
834impl ScalarFunction for NullifFunc {
835    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
836        self.invoke_with_collation(args, None)
837    }
838
839    fn consumes_argument_collation(&self) -> bool {
840        true
841    }
842
843    fn invoke_with_collation(
844        &self,
845        args: &[SqliteValue],
846        collation: Option<&dyn crate::collation::CollationFunction>,
847    ) -> Result<SqliteValue> {
848        let equal = match (&args[0], &args[1], collation) {
849            (SqliteValue::Text(left), SqliteValue::Text(right), Some(collation)) => {
850                collation.compare(left.as_bytes(), right.as_bytes()) == std::cmp::Ordering::Equal
851            }
852            _ => args[0] == args[1],
853        };
854        if equal {
855            Ok(SqliteValue::Null)
856        } else {
857            Ok(args[0].clone())
858        }
859    }
860
861    fn num_args(&self) -> i32 {
862        2
863    }
864
865    fn name(&self) -> &str {
866        "nullif"
867    }
868}
869
870// ── typeof(X) ────────────────────────────────────────────────────────────
871
872pub struct TypeofFunc;
873
874impl ScalarFunction for TypeofFunc {
875    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
876        let type_name = match &args[0] {
877            SqliteValue::Null => "null",
878            SqliteValue::Integer(_) => "integer",
879            SqliteValue::Float(_) => "real",
880            SqliteValue::Text(_) => "text",
881            SqliteValue::Blob(_) => "blob",
882        };
883        Ok(SqliteValue::Text(SmallText::new(type_name)))
884    }
885
886    fn num_args(&self) -> i32 {
887        1
888    }
889
890    fn name(&self) -> &str {
891        "typeof"
892    }
893}
894
895// ── subtype(X) ───────────────────────────────────────────────────────────
896
897pub struct SubtypeFunc;
898
899impl ScalarFunction for SubtypeFunc {
900    fn invoke(&self, _args: &[SqliteValue]) -> Result<SqliteValue> {
901        // subtype(NULL) = 0 (does NOT propagate NULL)
902        // Without subtype tags in SqliteValue, always return 0.
903        Ok(SqliteValue::Integer(0))
904    }
905
906    fn num_args(&self) -> i32 {
907        1
908    }
909
910    fn name(&self) -> &str {
911        "subtype"
912    }
913}
914
915// ── replace(X, Y, Z) ────────────────────────────────────────────────────
916
917pub struct ReplaceFunc;
918
919impl ScalarFunction for ReplaceFunc {
920    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
921        if let Some(null) = null_propagate(args) {
922            return Ok(null);
923        }
924        let x = text_arg(&args[0]);
925        let y = text_arg(&args[1]);
926        let z = text_arg(&args[2]);
927        if y.is_empty() {
928            return Ok(SqliteValue::Text(SmallText::from_string(x)));
929        }
930
931        // Prevent OOM from massive string expansion
932        if z.len() > y.len() {
933            let occurrences = x.matches(y.as_ref()).count();
934            let final_len = x.len() + occurrences * (z.len() - y.len());
935            if final_len > 1_000_000_000 {
936                return Err(FrankenError::TooBig);
937            }
938        }
939
940        Ok(SqliteValue::Text(SmallText::from_string(
941            x.replace(y.as_ref(), z.as_ref()),
942        )))
943    }
944
945    fn num_args(&self) -> i32 {
946        3
947    }
948
949    fn name(&self) -> &str {
950        "replace"
951    }
952}
953
954// ── round(X [, N]) ──────────────────────────────────────────────────────
955
956pub struct RoundFunc;
957
958impl ScalarFunction for RoundFunc {
959    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
960        if args[0].is_null() {
961            return Ok(SqliteValue::Null);
962        }
963        // C SQLite: a NULL precision argument makes the whole call NULL
964        // (`round(123.4, NULL)` → NULL), not a default of 0.
965        if args.len() > 1 && args[1].is_null() {
966            return Ok(SqliteValue::Null);
967        }
968        let x = args[0].to_float();
969        // Clamp N to [0, 30] matching SQLite behavior.
970        let n = if args.len() > 1 {
971            args[1].to_integer().clamp(0, 30)
972        } else {
973            0
974        };
975        // Values beyond 2^52 have no fractional part — return unchanged
976        if !(-4_503_599_627_370_496.0..=4_503_599_627_370_496.0).contains(&x) {
977            return Ok(SqliteValue::Float(x));
978        }
979        // SQLite uses "round half away from zero" via its custom printf, while
980        // Rust's format! uses "round half to even" (IEEE 754 default). They
981        // agree on every value except an exact binary tie, which the shared
982        // fixed-notation helper detects and adjusts to match SQLite.
983        let rounded = format_fixed_round_half_away(x, n as usize)
984            .parse::<f64>()
985            .unwrap_or(x);
986        Ok(SqliteValue::Float(rounded))
987    }
988
989    fn num_args(&self) -> i32 {
990        -1 // 1 or 2 args
991    }
992
993    fn min_args(&self) -> i32 {
994        1
995    }
996
997    fn max_args(&self) -> Option<i32> {
998        Some(2)
999    }
1000
1001    fn name(&self) -> &str {
1002        "round"
1003    }
1004}
1005
1006// ── sign(X) ──────────────────────────────────────────────────────────────
1007
1008pub struct SignFunc;
1009
1010impl ScalarFunction for SignFunc {
1011    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1012        if args[0].is_null() {
1013            return Ok(SqliteValue::Null);
1014        }
1015        match &args[0] {
1016            SqliteValue::Null => Ok(SqliteValue::Null),
1017            SqliteValue::Integer(i) => Ok(SqliteValue::Integer(i.signum())),
1018            SqliteValue::Float(f) => {
1019                if f.is_nan() {
1020                    Ok(SqliteValue::Null)
1021                } else if *f > 0.0 {
1022                    Ok(SqliteValue::Integer(1))
1023                } else if *f < 0.0 {
1024                    Ok(SqliteValue::Integer(-1))
1025                } else {
1026                    Ok(SqliteValue::Integer(0))
1027                }
1028            }
1029            SqliteValue::Text(s) => {
1030                // C SQLite sign() uses sqlite3AtoF — returns NULL for non-numeric text.
1031                let trimmed = s.trim_matches(|ch: char| ch.is_ascii_whitespace());
1032                if trimmed.is_empty() {
1033                    return Ok(SqliteValue::Null);
1034                }
1035
1036                // Reject literal NaN/inf/infinity keywords (case-insensitive,
1037                // with optional leading sign). Rust's f64::parse accepts these
1038                // but C SQLite's sqlite3AtoF does not. Note: numeric overflow
1039                // strings like "1e999" that parse to infinity ARE valid — C
1040                // SQLite recognises those as numeric and sign() returns 1/-1.
1041                let stripped = trimmed.strip_prefix(['+', '-']).unwrap_or(trimmed);
1042                if stripped.eq_ignore_ascii_case("nan")
1043                    || stripped.eq_ignore_ascii_case("inf")
1044                    || stripped.eq_ignore_ascii_case("infinity")
1045                {
1046                    return Ok(SqliteValue::Null);
1047                }
1048
1049                // Try parsing as a number. If the string isn't a valid numeric
1050                // representation, return NULL (matching C SQLite behavior).
1051                if let Ok(f) = trimmed.parse::<f64>() {
1052                    // Use the already-parsed value (avoids a redundant double-parse).
1053                    if f > 0.0 {
1054                        Ok(SqliteValue::Integer(1))
1055                    } else if f < 0.0 {
1056                        Ok(SqliteValue::Integer(-1))
1057                    } else {
1058                        Ok(SqliteValue::Integer(0))
1059                    }
1060                } else if let Ok(i) = trimmed.parse::<i64>() {
1061                    // Handles integers that f64 can't represent exactly but i64 can.
1062                    Ok(SqliteValue::Integer(i.signum()))
1063                } else {
1064                    Ok(SqliteValue::Null)
1065                }
1066            }
1067            SqliteValue::Blob(_) => Ok(SqliteValue::Null),
1068        }
1069    }
1070
1071    fn num_args(&self) -> i32 {
1072        1
1073    }
1074
1075    fn name(&self) -> &str {
1076        "sign"
1077    }
1078}
1079
1080// ── random() ─────────────────────────────────────────────────────────────
1081
1082pub struct RandomFunc;
1083
1084impl ScalarFunction for RandomFunc {
1085    fn invoke(&self, _args: &[SqliteValue]) -> Result<SqliteValue> {
1086        // Simple PRNG using thread_rng is fine for SQLite's random()
1087        // which is explicitly non-cryptographic.
1088        let val = simple_random_i64();
1089        Ok(SqliteValue::Integer(val))
1090    }
1091
1092    fn is_deterministic(&self) -> bool {
1093        false
1094    }
1095
1096    fn num_args(&self) -> i32 {
1097        0
1098    }
1099
1100    fn name(&self) -> &str {
1101        "random"
1102    }
1103}
1104
1105/// Simple deterministic-enough PRNG for SQLite's random().
1106fn simple_random_i64() -> i64 {
1107    // Deterministic per-process PRNG (no ambient authority).
1108    // Not cryptographic, matching SQLite's random()/randomblob() semantics.
1109    //
1110    // splitmix64: fast, decent statistical properties, and requires only a u64 state.
1111    use std::sync::atomic::{AtomicU64, Ordering};
1112
1113    static STATE: AtomicU64 = AtomicU64::new(0xD1B5_4A32_D192_ED03);
1114    let mut x = STATE.fetch_add(0x9E37_79B9_7F4A_7C15, Ordering::Relaxed);
1115    x ^= x >> 30;
1116    x = x.wrapping_mul(0xBF58_476D_1CE4_E5B9);
1117    x ^= x >> 27;
1118    x = x.wrapping_mul(0x94D0_49BB_1331_11EB);
1119    x ^= x >> 31;
1120    x as i64
1121}
1122
1123// ── randomblob(N) ────────────────────────────────────────────────────────
1124
1125pub struct RandomblobFunc;
1126
1127impl ScalarFunction for RandomblobFunc {
1128    #[allow(clippy::cast_sign_loss)]
1129    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1130        // C SQLite returns a one-byte blob for NULL and for all lengths below
1131        // one. `zeroblob()` uses different empty-blob semantics, so keep this
1132        // rule local to randomblob().
1133        let n_i64 = if args[0].is_null() {
1134            1
1135        } else {
1136            args[0].to_integer().max(1)
1137        };
1138        if n_i64 > 1_000_000_000 {
1139            return Err(FrankenError::TooBig);
1140        }
1141        let n = n_i64 as usize;
1142        let mut buf = vec![0u8; n];
1143        let mut i = 0;
1144        while i < n {
1145            let rnd = simple_random_i64().to_ne_bytes();
1146            let to_copy = (n - i).min(8);
1147            buf[i..i + to_copy].copy_from_slice(&rnd[..to_copy]);
1148            i += to_copy;
1149        }
1150        Ok(SqliteValue::Blob(Arc::from(buf.as_slice())))
1151    }
1152
1153    fn is_deterministic(&self) -> bool {
1154        false
1155    }
1156
1157    fn num_args(&self) -> i32 {
1158        1
1159    }
1160
1161    fn name(&self) -> &str {
1162        "randomblob"
1163    }
1164}
1165
1166// ── zeroblob(N) ──────────────────────────────────────────────────────────
1167
1168pub struct ZeroblobFunc;
1169
1170impl ScalarFunction for ZeroblobFunc {
1171    #[allow(clippy::cast_sign_loss)]
1172    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1173        // C SQLite: zeroblob(NULL) returns x'' (empty blob), not NULL.
1174        if args[0].is_null() {
1175            return Ok(SqliteValue::Blob(Arc::from([] as [u8; 0])));
1176        }
1177        let n_i64 = args[0].to_integer().max(0);
1178        if n_i64 > 1_000_000_000 {
1179            return Err(FrankenError::TooBig);
1180        }
1181        let n = n_i64 as usize;
1182        Ok(SqliteValue::Blob(Arc::from(vec![0u8; n].as_slice())))
1183    }
1184
1185    fn num_args(&self) -> i32 {
1186        1
1187    }
1188
1189    fn name(&self) -> &str {
1190        "zeroblob"
1191    }
1192}
1193
1194// ── quote(X) ─────────────────────────────────────────────────────────────
1195
1196pub struct QuoteFunc;
1197
1198impl ScalarFunction for QuoteFunc {
1199    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1200        let result = quote_sql_value(&args[0], false);
1201        Ok(SqliteValue::Text(SmallText::from_string(result)))
1202    }
1203
1204    fn num_args(&self) -> i32 {
1205        1
1206    }
1207
1208    fn name(&self) -> &str {
1209        "quote"
1210    }
1211}
1212
1213// ── unistr_quote(X) ───────────────────────────────────────────────────────
1214
1215pub struct UnistrQuoteFunc;
1216
1217impl ScalarFunction for UnistrQuoteFunc {
1218    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1219        let result = quote_sql_value(&args[0], true);
1220        Ok(SqliteValue::Text(SmallText::from_string(result)))
1221    }
1222
1223    fn num_args(&self) -> i32 {
1224        1
1225    }
1226
1227    fn name(&self) -> &str {
1228        "unistr_quote"
1229    }
1230}
1231
1232fn quote_sql_value(value: &SqliteValue, use_unistr_quote: bool) -> String {
1233    match value {
1234        SqliteValue::Null => "NULL".to_owned(),
1235        SqliteValue::Integer(i) => i.to_string(),
1236        SqliteValue::Float(f) => format_sqlite_float(*f),
1237        SqliteValue::Text(s) => quote_sql_text_literal(s.as_str(), use_unistr_quote),
1238        SqliteValue::Blob(b) => {
1239            let mut hex = String::with_capacity(3 + b.len() * 2);
1240            hex.push_str("X'");
1241            for byte in b.iter() {
1242                let _ = write!(hex, "{byte:02X}");
1243            }
1244            hex.push('\'');
1245            hex
1246        }
1247    }
1248}
1249
1250fn quote_sql_text_literal(text: &str, use_unistr_quote: bool) -> String {
1251    let text = sqlite_text_until_nul(text);
1252    if use_unistr_quote && text.chars().any(is_unistr_control_char) {
1253        return unistr_quote_sql_text_literal(text);
1254    }
1255
1256    let mut quoted = String::with_capacity(text.len() + 2);
1257    quoted.push('\'');
1258    append_sql_string_literal_body(&mut quoted, text);
1259    quoted.push('\'');
1260    quoted
1261}
1262
1263fn unistr_quote_sql_text_literal(text: &str) -> String {
1264    let mut quoted = String::with_capacity(text.len() + 12);
1265    quoted.push_str("unistr('");
1266    for ch in text.chars() {
1267        match ch {
1268            '\'' => quoted.push_str("''"),
1269            '\\' => quoted.push_str("\\\\"),
1270            _ if is_unistr_control_char(ch) => {
1271                let _ = write!(quoted, "\\u{:04x}", ch as u32);
1272            }
1273            _ => quoted.push(ch),
1274        }
1275    }
1276    quoted.push_str("')");
1277    quoted
1278}
1279
1280fn append_sql_string_literal_body(out: &mut String, text: &str) {
1281    for ch in text.chars() {
1282        if ch == '\'' {
1283            out.push_str("''");
1284        } else {
1285            out.push(ch);
1286        }
1287    }
1288}
1289
1290fn is_unistr_control_char(ch: char) -> bool {
1291    matches!(ch, '\u{0001}'..='\u{001F}')
1292}
1293
1294// ── unhex(X [, Y]) ──────────────────────────────────────────────────────
1295
1296pub struct UnhexFunc;
1297
1298impl ScalarFunction for UnhexFunc {
1299    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1300        if args[0].is_null() {
1301            return Ok(SqliteValue::Null);
1302        }
1303        if args.len() > 1 && args[1].is_null() {
1304            return Ok(SqliteValue::Null);
1305        }
1306        let input = text_arg(&args[0]);
1307        let ignore_chars: Vec<char> = if args.len() > 1 {
1308            text_arg(&args[1])
1309                .chars()
1310                .filter(|&c| hex_digit(c).is_none())
1311                .collect()
1312        } else {
1313            Vec::new()
1314        };
1315
1316        let mut bytes = Vec::with_capacity(input.len() / 2);
1317        let mut hi_nibble = None;
1318        for c in input.as_ref().chars() {
1319            if ignore_chars.contains(&c) {
1320                if hi_nibble.is_some() {
1321                    return Ok(SqliteValue::Null);
1322                }
1323                continue;
1324            }
1325            let digit = match hex_digit(c) {
1326                Some(v) => v,
1327                None => return Ok(SqliteValue::Null),
1328            };
1329            if let Some(hi) = hi_nibble.take() {
1330                bytes.push(hi << 4 | digit);
1331            } else {
1332                hi_nibble = Some(digit);
1333            }
1334        }
1335        if hi_nibble.is_some() {
1336            return Ok(SqliteValue::Null);
1337        }
1338        Ok(SqliteValue::Blob(Arc::from(bytes.as_slice())))
1339    }
1340
1341    fn num_args(&self) -> i32 {
1342        -1 // 1 or 2 args
1343    }
1344
1345    fn min_args(&self) -> i32 {
1346        1
1347    }
1348
1349    fn max_args(&self) -> Option<i32> {
1350        Some(2)
1351    }
1352
1353    fn name(&self) -> &str {
1354        "unhex"
1355    }
1356}
1357
1358fn hex_digit(c: char) -> Option<u8> {
1359    match c {
1360        '0'..='9' => Some(c as u8 - b'0'),
1361        'a'..='f' => Some(c as u8 - b'a' + 10),
1362        'A'..='F' => Some(c as u8 - b'A' + 10),
1363        _ => None,
1364    }
1365}
1366
1367// ── unicode(X) ───────────────────────────────────────────────────────────
1368
1369pub struct UnicodeFunc;
1370
1371impl ScalarFunction for UnicodeFunc {
1372    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1373        if args[0].is_null() {
1374            return Ok(SqliteValue::Null);
1375        }
1376        if let SqliteValue::Blob(bytes) = &args[0] {
1377            return Ok(
1378                sqlite_blob_first_codepoint(bytes).map_or(SqliteValue::Null, SqliteValue::Integer)
1379            );
1380        }
1381        let s = text_arg(&args[0]);
1382        match sqlite_text_until_nul(s.as_ref()).chars().next() {
1383            Some(c) => Ok(SqliteValue::Integer(i64::from(c as u32))),
1384            None => Ok(SqliteValue::Null),
1385        }
1386    }
1387
1388    fn num_args(&self) -> i32 {
1389        1
1390    }
1391
1392    fn name(&self) -> &str {
1393        "unicode"
1394    }
1395}
1396
1397fn sqlite_blob_first_codepoint(bytes: &[u8]) -> Option<i64> {
1398    let first = *bytes.first()?;
1399    if first == 0 {
1400        return None;
1401    }
1402    let mut codepoint = match first {
1403        0x00..=0xBF => u32::from(first),
1404        0xC0..=0xDF => u32::from(first & 0x1F),
1405        0xE0..=0xEF => u32::from(first & 0x0F),
1406        0xF0..=0xF7 => u32::from(first & 0x07),
1407        _ => 0xFFFD,
1408    };
1409
1410    if first >= 0xC0 && first <= 0xF7 {
1411        for byte in bytes
1412            .iter()
1413            .copied()
1414            .skip(1)
1415            .take_while(|byte| byte & 0xC0 == 0x80)
1416        {
1417            codepoint = codepoint
1418                .wrapping_shl(6)
1419                .wrapping_add(u32::from(byte & 0x3F));
1420        }
1421        if codepoint < 0x80
1422            || (codepoint & 0xFFFF_F800) == 0xD800
1423            || (codepoint & 0xFFFF_FFFE) == 0xFFFE
1424        {
1425            codepoint = 0xFFFD;
1426        }
1427    }
1428
1429    Some(i64::from(codepoint))
1430}
1431
1432// ── substr(X, START [, LENGTH]) / substring() ───────────────────────────
1433
1434pub struct SubstrFunc;
1435
1436impl ScalarFunction for SubstrFunc {
1437    #[allow(clippy::cast_sign_loss, clippy::cast_possible_wrap)]
1438    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1439        if args[0].is_null() || args[1].is_null() {
1440            return Ok(SqliteValue::Null);
1441        }
1442        let is_blob = matches!(&args[0], SqliteValue::Blob(_));
1443        if is_blob {
1444            return self.invoke_blob(args);
1445        }
1446
1447        let text = text_arg(&args[0]);
1448        // bd-7c6g7 #2: SQLite treats an embedded NUL as a string terminator for
1449        // text functions (`length('a'||char(0)||'bc')` == 1), so substr operates
1450        // on the prefix before the first NUL — the is_ascii() fast path (NUL is
1451        // ASCII) would otherwise index into bytes past the terminator.
1452        let full = text.as_ref();
1453        let s = full.split_once('\0').map_or(full, |(prefix, _)| prefix);
1454        let ascii_fast_path = s.is_ascii();
1455        let len = if ascii_fast_path {
1456            s.len() as i64
1457        } else {
1458            s.chars().count() as i64
1459        };
1460        let has_length = args.len() > 2 && !args[2].is_null();
1461
1462        let mut p1 = args[1].to_integer();
1463        let mut p2 = if has_length {
1464            args[2].to_integer()
1465        } else {
1466            1_000_000_000
1467        };
1468
1469        // Match C SQLite's 2-phase substr algorithm exactly:
1470        // Phase 1: remember if length was negative, make it positive
1471        // Use saturating_neg to avoid panic on i64::MIN.
1472        let neg_p2 = p2 < 0;
1473        if neg_p2 {
1474            p2 = p2.saturating_neg();
1475        }
1476
1477        // Phase 2: resolve start position (1-based to 0-based)
1478        if p1 < 0 {
1479            p1 = p1.saturating_add(len);
1480            if p1 < 0 {
1481                p2 = p2.saturating_add(p1);
1482                p1 = 0;
1483            }
1484        } else if p1 > 0 {
1485            p1 -= 1;
1486        } else if p2 > 0 {
1487            p2 -= 1; // start=0 quirk
1488        }
1489
1490        // Phase 3: apply negative-length shift (move start backward)
1491        if neg_p2 {
1492            p1 = p1.saturating_sub(p2);
1493            if p1 < 0 {
1494                p2 = p2.saturating_add(p1);
1495                p1 = 0;
1496            }
1497        }
1498
1499        if p1.saturating_add(p2) > len {
1500            p2 = len.saturating_sub(p1);
1501        }
1502        if p2 <= 0 {
1503            return Ok(SqliteValue::Text(SmallText::new("")));
1504        }
1505
1506        if ascii_fast_path {
1507            let start = p1 as usize;
1508            let end = (p1 + p2) as usize;
1509            return Ok(SqliteValue::Text(SmallText::new(&s[start..end])));
1510        }
1511
1512        let chars: Vec<char> = s.chars().collect();
1513        let result: String = chars[p1 as usize..(p1 + p2) as usize].iter().collect();
1514        Ok(SqliteValue::Text(SmallText::from_string(result)))
1515    }
1516
1517    fn num_args(&self) -> i32 {
1518        -1 // 2 or 3 args
1519    }
1520
1521    fn min_args(&self) -> i32 {
1522        2
1523    }
1524
1525    fn max_args(&self) -> Option<i32> {
1526        Some(3)
1527    }
1528
1529    fn name(&self) -> &str {
1530        "substr"
1531    }
1532}
1533
1534impl SubstrFunc {
1535    #[allow(
1536        clippy::unused_self,
1537        clippy::cast_sign_loss,
1538        clippy::cast_possible_wrap
1539    )]
1540    fn invoke_blob(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1541        let blob = match &args[0] {
1542            SqliteValue::Blob(b) => b,
1543            _ => return Ok(SqliteValue::Null),
1544        };
1545        let len = blob.len() as i64;
1546        let has_length = args.len() > 2 && !args[2].is_null();
1547
1548        let mut p1 = args[1].to_integer();
1549        let mut p2 = if has_length {
1550            args[2].to_integer()
1551        } else {
1552            1_000_000_000
1553        };
1554
1555        let neg_p2 = p2 < 0;
1556        if neg_p2 {
1557            p2 = p2.saturating_neg();
1558        }
1559
1560        if p1 < 0 {
1561            p1 = p1.saturating_add(len);
1562            if p1 < 0 {
1563                p2 = p2.saturating_add(p1);
1564                p1 = 0;
1565            }
1566        } else if p1 > 0 {
1567            p1 -= 1;
1568        } else if p2 > 0 {
1569            p2 -= 1;
1570        }
1571
1572        if neg_p2 {
1573            p1 = p1.saturating_sub(p2);
1574            if p1 < 0 {
1575                p2 = p2.saturating_add(p1);
1576                p1 = 0;
1577            }
1578        }
1579
1580        if p1.saturating_add(p2) > len {
1581            p2 = len.saturating_sub(p1);
1582        }
1583        if p2 <= 0 {
1584            return Ok(SqliteValue::Blob(Arc::from([] as [u8; 0])));
1585        }
1586
1587        Ok(SqliteValue::Blob(Arc::from(
1588            &blob[p1 as usize..(p1 + p2) as usize],
1589        )))
1590    }
1591}
1592
1593// ── soundex(X) ───────────────────────────────────────────────────────────
1594
1595pub struct SoundexFunc;
1596
1597impl ScalarFunction for SoundexFunc {
1598    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1599        if args[0].is_null() {
1600            // SQLite returns "?000" for SOUNDEX(NULL), not NULL.
1601            return Ok(SqliteValue::Text(SmallText::new("?000")));
1602        }
1603        let s = text_arg(&args[0]);
1604        let code = soundex(s.as_ref());
1605        let text = std::str::from_utf8(&code).expect("Soundex output must be ASCII");
1606        Ok(SqliteValue::Text(SmallText::new(text)))
1607    }
1608
1609    fn num_args(&self) -> i32 {
1610        1
1611    }
1612
1613    fn name(&self) -> &str {
1614        "soundex"
1615    }
1616}
1617
1618fn soundex(s: &str) -> [u8; 4] {
1619    let mut chars = s.chars().filter(|c| c.is_ascii_alphabetic());
1620    let first = match chars.next() {
1621        Some(c) => c.to_ascii_uppercase(),
1622        None => return *b"?000",
1623    };
1624
1625    let code = |c: char| -> Option<u8> {
1626        match c.to_ascii_uppercase() {
1627            'B' | 'F' | 'P' | 'V' => Some(b'1'),
1628            'C' | 'G' | 'J' | 'K' | 'Q' | 'S' | 'X' | 'Z' => Some(b'2'),
1629            'D' | 'T' => Some(b'3'),
1630            'L' => Some(b'4'),
1631            'M' | 'N' => Some(b'5'),
1632            'R' => Some(b'6'),
1633            _ => None, // A, E, I, O, U, H, W, Y
1634        }
1635    };
1636
1637    let mut result = *b"0000";
1638    result[0] = first as u8;
1639    let mut result_len = 1;
1640    let mut last_code = code(first);
1641
1642    for c in chars {
1643        if result_len >= result.len() {
1644            break;
1645        }
1646        let current = code(c);
1647        if let Some(digit) = current
1648            && current != last_code
1649        {
1650            result[result_len] = digit;
1651            result_len += 1;
1652        }
1653        last_code = current;
1654    }
1655
1656    result
1657}
1658
1659// ── scalar max(X, Y, ...) ───────────────────────────────────────────────
1660
1661pub struct ScalarMaxFunc;
1662
1663impl ScalarFunction for ScalarMaxFunc {
1664    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1665        self.invoke_with_collation(args, None)
1666    }
1667
1668    fn consumes_argument_collation(&self) -> bool {
1669        true
1670    }
1671
1672    fn invoke_with_collation(
1673        &self,
1674        args: &[SqliteValue],
1675        collation: Option<&dyn crate::collation::CollationFunction>,
1676    ) -> Result<SqliteValue> {
1677        // Scalar max: if ANY argument is NULL, returns NULL
1678        if let Some(null) = null_propagate(args) {
1679            return Ok(null);
1680        }
1681        let mut max = &args[0];
1682        for arg in &args[1..] {
1683            let ordering = match (arg, max, collation) {
1684                (SqliteValue::Text(left), SqliteValue::Text(right), Some(collation)) => {
1685                    Some(collation.compare(left.as_bytes(), right.as_bytes()))
1686                }
1687                _ => arg.partial_cmp(max),
1688            };
1689            if ordering == Some(std::cmp::Ordering::Greater) {
1690                max = arg;
1691            }
1692        }
1693        Ok(max.clone())
1694    }
1695
1696    fn num_args(&self) -> i32 {
1697        -1
1698    }
1699
1700    fn min_args(&self) -> i32 {
1701        1
1702    }
1703
1704    fn name(&self) -> &str {
1705        "max"
1706    }
1707}
1708
1709// ── scalar min(X, Y, ...) ───────────────────────────────────────────────
1710
1711pub struct ScalarMinFunc;
1712
1713impl ScalarFunction for ScalarMinFunc {
1714    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1715        self.invoke_with_collation(args, None)
1716    }
1717
1718    fn consumes_argument_collation(&self) -> bool {
1719        true
1720    }
1721
1722    fn invoke_with_collation(
1723        &self,
1724        args: &[SqliteValue],
1725        collation: Option<&dyn crate::collation::CollationFunction>,
1726    ) -> Result<SqliteValue> {
1727        // Scalar min: if ANY argument is NULL, returns NULL
1728        if let Some(null) = null_propagate(args) {
1729            return Ok(null);
1730        }
1731        let mut min = &args[0];
1732        for arg in &args[1..] {
1733            let ordering = match (arg, min, collation) {
1734                (SqliteValue::Text(left), SqliteValue::Text(right), Some(collation)) => {
1735                    Some(collation.compare(left.as_bytes(), right.as_bytes()))
1736                }
1737                _ => arg.partial_cmp(min),
1738            };
1739            // SQLite's scalar min() selects the later argument on a tie. This
1740            // is observable when equal numeric values use different storage
1741            // classes or a collation considers distinct text values equal.
1742            if matches!(
1743                ordering,
1744                Some(std::cmp::Ordering::Less | std::cmp::Ordering::Equal)
1745            ) {
1746                min = arg;
1747            }
1748        }
1749        Ok(min.clone())
1750    }
1751
1752    fn num_args(&self) -> i32 {
1753        -1
1754    }
1755
1756    fn min_args(&self) -> i32 {
1757        1
1758    }
1759
1760    fn name(&self) -> &str {
1761        "min"
1762    }
1763}
1764
1765// ── likelihood/likely/unlikely ──────────────────────────────────────────
1766
1767pub struct LikelihoodFunc;
1768
1769impl ScalarFunction for LikelihoodFunc {
1770    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1771        // Returns X unchanged; P is a planner hint (ignored at runtime).
1772        Ok(args[0].clone())
1773    }
1774
1775    fn num_args(&self) -> i32 {
1776        2
1777    }
1778
1779    fn name(&self) -> &str {
1780        "likelihood"
1781    }
1782}
1783
1784pub struct LikelyFunc;
1785
1786impl ScalarFunction for LikelyFunc {
1787    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1788        Ok(args[0].clone())
1789    }
1790
1791    fn num_args(&self) -> i32 {
1792        1
1793    }
1794
1795    fn name(&self) -> &str {
1796        "likely"
1797    }
1798}
1799
1800pub struct UnlikelyFunc;
1801
1802impl ScalarFunction for UnlikelyFunc {
1803    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1804        Ok(args[0].clone())
1805    }
1806
1807    fn num_args(&self) -> i32 {
1808        1
1809    }
1810
1811    fn name(&self) -> &str {
1812        "unlikely"
1813    }
1814}
1815
1816// ── sqlite_version() ────────────────────────────────────────────────────
1817
1818pub struct SqliteVersionFunc;
1819
1820impl ScalarFunction for SqliteVersionFunc {
1821    fn invoke(&self, _args: &[SqliteValue]) -> Result<SqliteValue> {
1822        Ok(SqliteValue::Text(SmallText::new(
1823            fsqlite_types::FRANKENSQLITE_SQLITE_VERSION,
1824        )))
1825    }
1826
1827    fn is_deterministic(&self) -> bool {
1828        false
1829    }
1830
1831    fn num_args(&self) -> i32 {
1832        0
1833    }
1834
1835    fn name(&self) -> &str {
1836        "sqlite_version"
1837    }
1838}
1839
1840// ── sqlite_source_id() ──────────────────────────────────────────────────
1841
1842pub struct SqliteSourceIdFunc;
1843
1844impl ScalarFunction for SqliteSourceIdFunc {
1845    fn invoke(&self, _args: &[SqliteValue]) -> Result<SqliteValue> {
1846        Ok(SqliteValue::Text(SmallText::new(
1847            fsqlite_types::FRANKENSQLITE_SOURCE_ID,
1848        )))
1849    }
1850
1851    fn is_deterministic(&self) -> bool {
1852        false
1853    }
1854
1855    fn num_args(&self) -> i32 {
1856        0
1857    }
1858
1859    fn name(&self) -> &str {
1860        "sqlite_source_id"
1861    }
1862}
1863
1864// ── sqlite_compileoption_used(X) ────────────────────────────────────────
1865
1866pub struct SqliteCompileoptionUsedFunc;
1867
1868impl ScalarFunction for SqliteCompileoptionUsedFunc {
1869    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1870        if args[0].is_null() {
1871            return Ok(SqliteValue::Null);
1872        }
1873        let query = text_arg(&args[0]);
1874        Ok(SqliteValue::Integer(i64::from(sqlite_compileoption_used(
1875            query.as_ref(),
1876        ))))
1877    }
1878
1879    fn is_deterministic(&self) -> bool {
1880        false
1881    }
1882
1883    fn num_args(&self) -> i32 {
1884        1
1885    }
1886
1887    fn name(&self) -> &str {
1888        "sqlite_compileoption_used"
1889    }
1890}
1891
1892// ── sqlite_compileoption_get(N) ─────────────────────────────────────────
1893
1894pub struct SqliteCompileoptionGetFunc;
1895
1896impl ScalarFunction for SqliteCompileoptionGetFunc {
1897    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1898        if args[0].is_null() {
1899            return Ok(SqliteValue::Null);
1900        }
1901        let n = args[0].to_integer();
1902        #[allow(clippy::cast_sign_loss)]
1903        match sqlite_compile_options().get(n as usize) {
1904            Some(opt) => Ok(SqliteValue::Text(SmallText::new(opt))),
1905            None => Ok(SqliteValue::Null),
1906        }
1907    }
1908
1909    fn is_deterministic(&self) -> bool {
1910        false
1911    }
1912
1913    fn num_args(&self) -> i32 {
1914        1
1915    }
1916
1917    fn name(&self) -> &str {
1918        "sqlite_compileoption_get"
1919    }
1920}
1921
1922// ── like(PATTERN, STRING [, ESCAPE]) ────────────────────────────────────
1923
1924pub struct LikeFunc;
1925
1926impl ScalarFunction for LikeFunc {
1927    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
1928        if let Some(null) = null_propagate(args) {
1929            return Ok(null);
1930        }
1931        let pattern = text_arg(&args[0]);
1932        let string = text_arg(&args[1]);
1933        let escape = if args.len() > 2 && !args[2].is_null() {
1934            Some(single_char_escape(text_arg(&args[2]).as_ref())?)
1935        } else {
1936            None
1937        };
1938        let matched = like_match(pattern.as_ref(), string.as_ref(), escape);
1939        Ok(SqliteValue::Integer(i64::from(matched)))
1940    }
1941
1942    fn num_args(&self) -> i32 {
1943        -1 // 2 or 3 args
1944    }
1945
1946    fn min_args(&self) -> i32 {
1947        2
1948    }
1949
1950    fn max_args(&self) -> Option<i32> {
1951        Some(3)
1952    }
1953
1954    fn name(&self) -> &str {
1955        "like"
1956    }
1957}
1958
1959#[cfg(test)]
1960mod like_func_pragma_tests {
1961    use super::{LikeFunc, case_sensitive_like_active, set_case_sensitive_like};
1962    use crate::ScalarFunction;
1963    use fsqlite_types::SqliteValue;
1964
1965    fn like(pattern: &str, text: &str) -> i64 {
1966        match LikeFunc
1967            .invoke(&[
1968                SqliteValue::Text(pattern.into()),
1969                SqliteValue::Text(text.into()),
1970            ])
1971            .unwrap()
1972        {
1973            SqliteValue::Integer(n) => n,
1974            other => panic!("expected integer, got {other:?}"),
1975        }
1976    }
1977
1978    #[test]
1979    fn like_honors_case_sensitive_like_thread_local() {
1980        // Default: ASCII-case-insensitive.
1981        set_case_sensitive_like(false);
1982        assert_eq!(like("a", "A"), 1);
1983        assert_eq!(like("A%", "apple"), 1);
1984        // ON: byte-exact.
1985        set_case_sensitive_like(true);
1986        assert!(case_sensitive_like_active());
1987        assert_eq!(like("a", "A"), 0);
1988        assert_eq!(like("A%", "apple"), 0);
1989        assert_eq!(like("A%", "Apple"), 1);
1990        // Restore so other tests on this thread see the default.
1991        set_case_sensitive_like(false);
1992    }
1993}
1994
1995fn single_char_escape(escape: &str) -> Result<char> {
1996    let mut chars = escape.chars();
1997    match (chars.next(), chars.next()) {
1998        (Some(ch), None) => Ok(ch),
1999        _ => Err(FrankenError::function_error(
2000            "ESCAPE expression must be a single character",
2001        )),
2002    }
2003}
2004
2005/// LIKE pattern matching. ASCII-case-insensitive by default; byte-exact when
2006/// the connection has `PRAGMA case_sensitive_like = ON` (read from the
2007/// thread-local set by the Connection before statement execution).
2008fn like_match(pattern: &str, string: &str, escape: Option<char>) -> bool {
2009    sql_like_cased(pattern, string, escape, case_sensitive_like_active())
2010}
2011
2012// ── glob(PATTERN, STRING) ───────────────────────────────────────────────
2013
2014pub struct GlobFunc;
2015
2016impl ScalarFunction for GlobFunc {
2017    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
2018        if let Some(null) = null_propagate(args) {
2019            return Ok(null);
2020        }
2021        let pattern = text_arg(&args[0]);
2022        let string = text_arg(&args[1]);
2023        let matched = glob_match(pattern.as_ref(), string.as_ref());
2024        Ok(SqliteValue::Integer(i64::from(matched)))
2025    }
2026
2027    fn num_args(&self) -> i32 {
2028        2
2029    }
2030
2031    fn name(&self) -> &str {
2032        "glob"
2033    }
2034}
2035
2036/// GLOB pattern matching (case-sensitive, * and ? wildcards).
2037fn glob_match(pattern: &str, string: &str) -> bool {
2038    let pat: Vec<char> = pattern.chars().collect();
2039    let txt: Vec<char> = string.chars().collect();
2040    glob_match_inner(&pat, &txt, 0, 0)
2041}
2042
2043fn text_arg(value: &SqliteValue) -> Cow<'_, str> {
2044    match value.as_text_str() {
2045        Some(text) => Cow::Borrowed(text),
2046        None => Cow::Owned(value.to_text()),
2047    }
2048}
2049
2050fn glob_match_inner(pat: &[char], txt: &[char], mut pi: usize, mut ti: usize) -> bool {
2051    while pi < pat.len() {
2052        match pat[pi] {
2053            '*' => {
2054                while pi < pat.len() && pat[pi] == '*' {
2055                    pi += 1;
2056                }
2057                if pi >= pat.len() {
2058                    return true;
2059                }
2060                for start in ti..=txt.len() {
2061                    if glob_match_inner(pat, txt, pi, start) {
2062                        return true;
2063                    }
2064                }
2065                return false;
2066            }
2067            '?' => {
2068                if ti >= txt.len() {
2069                    return false;
2070                }
2071                pi += 1;
2072                ti += 1;
2073            }
2074            '[' => {
2075                if ti >= txt.len() {
2076                    return false;
2077                }
2078                pi += 1;
2079                let negate = pi < pat.len() && pat[pi] == '^';
2080                if negate {
2081                    pi += 1;
2082                }
2083                let mut found = false;
2084                let mut first = true;
2085                while pi < pat.len() && (first || pat[pi] != ']') {
2086                    first = false;
2087                    // `X-Y` is a range only when Y is not the closing
2088                    // bracket: C SQLite's patternCompare treats a `-`
2089                    // immediately before `]` as a literal dash, so
2090                    // `[a-c-]` is {a..c, '-'} and `[^A-Za-z0-9._:-]`
2091                    // ends with a literal '-' rather than a `:-]` range
2092                    // that would swallow the class terminator.
2093                    if pi + 2 < pat.len() && pat[pi + 1] == '-' && pat[pi + 2] != ']' {
2094                        let lo = pat[pi];
2095                        let hi = pat[pi + 2];
2096                        if txt[ti] >= lo && txt[ti] <= hi {
2097                            found = true;
2098                        }
2099                        pi += 3;
2100                    } else {
2101                        if txt[ti] == pat[pi] {
2102                            found = true;
2103                        }
2104                        pi += 1;
2105                    }
2106                }
2107                if pi < pat.len() && pat[pi] == ']' {
2108                    pi += 1;
2109                } else {
2110                    // Unterminated character class: the pattern ran off the end
2111                    // before a closing ']'. C SQLite's patternCompare returns 0
2112                    // (no match) in this case, so `'a' GLOB '[a'` must be false.
2113                    return false;
2114                }
2115                if found == negate {
2116                    return false;
2117                }
2118                ti += 1;
2119            }
2120            c => {
2121                if ti >= txt.len() || txt[ti] != c {
2122                    return false;
2123                }
2124                pi += 1;
2125                ti += 1;
2126            }
2127        }
2128    }
2129    ti >= txt.len()
2130}
2131
2132// ── unistr(X) ───────────────────────────────────────────────────────────
2133
2134pub struct UnistrFunc;
2135
2136const INVALID_UNISTR_ESCAPE: &str = "invalid Unicode escape";
2137
2138fn decode_unistr_escape(chars: &mut std::str::Chars<'_>, digits: usize) -> Result<char> {
2139    let mut lookahead = chars.clone();
2140    let mut codepoint = 0u32;
2141    for _ in 0..digits {
2142        let Some(ch) = lookahead.next() else {
2143            return Err(FrankenError::function_error(INVALID_UNISTR_ESCAPE));
2144        };
2145        let Some(digit) = hex_digit(ch) else {
2146            return Err(FrankenError::function_error(INVALID_UNISTR_ESCAPE));
2147        };
2148        codepoint = (codepoint << 4) | u32::from(digit);
2149    }
2150    for _ in 0..digits {
2151        let _digit = chars.next();
2152    }
2153    char::from_u32(codepoint).ok_or_else(|| FrankenError::function_error(INVALID_UNISTR_ESCAPE))
2154}
2155
2156impl ScalarFunction for UnistrFunc {
2157    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
2158        if args[0].is_null() {
2159            return Ok(SqliteValue::Null);
2160        }
2161        let input = text_arg(&args[0]);
2162        let mut result = String::with_capacity(input.len());
2163        let mut chars = input.as_ref().chars();
2164        while let Some(ch) = chars.next() {
2165            if ch == '\\' {
2166                // C SQLite: \\ is an escaped backslash literal.
2167                if chars.as_str().starts_with('\\') {
2168                    let _ = chars.next();
2169                    result.push('\\');
2170                    continue;
2171                }
2172                let digits = if chars.as_str().starts_with('+') {
2173                    // \+XXXXXX
2174                    let _plus = chars.next();
2175                    6
2176                } else if chars.as_str().starts_with('u') {
2177                    // \uXXXX
2178                    let _marker = chars.next();
2179                    4
2180                } else if chars.as_str().starts_with('U') {
2181                    // \UXXXXXXXX
2182                    let _marker = chars.next();
2183                    8
2184                } else {
2185                    // \XXXX
2186                    4
2187                };
2188                result.push(decode_unistr_escape(&mut chars, digits)?);
2189                continue;
2190            }
2191            result.push(ch);
2192        }
2193        Ok(SqliteValue::Text(SmallText::from_string(result)))
2194    }
2195
2196    fn num_args(&self) -> i32 {
2197        1
2198    }
2199
2200    fn name(&self) -> &str {
2201        "unistr"
2202    }
2203}
2204
2205// ── Connection-state helpers ────────────────────────────────────────────
2206// These functions reflect connection-local counters projected into this
2207// thread by the connection layer around statement execution.
2208
2209pub struct ChangesFunc;
2210
2211impl ScalarFunction for ChangesFunc {
2212    fn invoke(&self, _args: &[SqliteValue]) -> Result<SqliteValue> {
2213        Ok(SqliteValue::Integer(LAST_CHANGES.get()))
2214    }
2215
2216    fn is_deterministic(&self) -> bool {
2217        false
2218    }
2219
2220    fn num_args(&self) -> i32 {
2221        0
2222    }
2223
2224    fn name(&self) -> &str {
2225        "changes"
2226    }
2227}
2228
2229pub struct TotalChangesFunc;
2230
2231impl ScalarFunction for TotalChangesFunc {
2232    fn invoke(&self, _args: &[SqliteValue]) -> Result<SqliteValue> {
2233        Ok(SqliteValue::Integer(TOTAL_CHANGES.get()))
2234    }
2235
2236    fn is_deterministic(&self) -> bool {
2237        false
2238    }
2239
2240    fn num_args(&self) -> i32 {
2241        0
2242    }
2243
2244    fn name(&self) -> &str {
2245        "total_changes"
2246    }
2247}
2248
2249pub struct LastInsertRowidFunc;
2250
2251impl ScalarFunction for LastInsertRowidFunc {
2252    fn invoke(&self, _args: &[SqliteValue]) -> Result<SqliteValue> {
2253        Ok(SqliteValue::Integer(LAST_INSERT_ROWID.get()))
2254    }
2255
2256    fn is_deterministic(&self) -> bool {
2257        false
2258    }
2259
2260    fn num_args(&self) -> i32 {
2261        0
2262    }
2263
2264    fn name(&self) -> &str {
2265        "last_insert_rowid"
2266    }
2267}
2268
2269// ── Register all built-ins ──────────────────────────────────────────────
2270
2271/// Register all core built-in scalar functions into the given registry.
2272#[allow(clippy::too_many_lines)]
2273pub fn register_builtins(registry: &mut FunctionRegistry) {
2274    // Math
2275    registry.register_scalar(AbsFunc);
2276    registry.register_scalar(SignFunc);
2277    registry.register_scalar(RoundFunc);
2278    registry.register_scalar(RandomFunc);
2279    registry.register_scalar(RandomblobFunc);
2280    registry.register_scalar(ZeroblobFunc);
2281
2282    // String
2283    registry.register_scalar(LowerFunc);
2284    registry.register_scalar(UpperFunc);
2285    registry.register_scalar(LengthFunc);
2286    registry.register_scalar(OctetLengthFunc);
2287    registry.register_scalar(TrimFunc);
2288    registry.register_scalar(LtrimFunc);
2289    registry.register_scalar(RtrimFunc);
2290    registry.register_scalar(ReplaceFunc);
2291    registry.register_scalar(SubstrFunc);
2292    registry.register_scalar(InstrFunc);
2293    registry.register_scalar(CharFunc);
2294    registry.register_scalar(UnicodeFunc);
2295    registry.register_scalar(UnistrFunc);
2296    registry.register_scalar(HexFunc);
2297    registry.register_scalar(UnhexFunc);
2298    registry.register_scalar(QuoteFunc);
2299    registry.register_scalar(UnistrQuoteFunc);
2300    registry.register_scalar(SoundexFunc);
2301
2302    // Type
2303    registry.register_scalar(TypeofFunc);
2304    registry.register_scalar(SubtypeFunc);
2305
2306    // Conditional
2307    registry.register_scalar(CoalesceFunc);
2308    registry.register_scalar(IfnullFunc);
2309    registry.register_scalar(NullifFunc);
2310    registry.register_scalar(IifFunc);
2311
2312    // Multi-value
2313    registry.register_scalar(ConcatFunc);
2314    registry.register_scalar(ConcatWsFunc);
2315    registry.register_scalar(ScalarMaxFunc);
2316    registry.register_scalar(ScalarMinFunc);
2317
2318    // Planner hints
2319    registry.register_scalar(LikelihoodFunc);
2320    registry.register_scalar(LikelyFunc);
2321    registry.register_scalar(UnlikelyFunc);
2322
2323    // Pattern matching
2324    registry.register_scalar(LikeFunc);
2325    registry.register_scalar(GlobFunc);
2326
2327    // Meta
2328    registry.register_slow_changing_scalar(SqliteVersionFunc);
2329    registry.register_slow_changing_scalar(SqliteSourceIdFunc);
2330    registry.register_slow_changing_scalar(SqliteCompileoptionUsedFunc);
2331    registry.register_slow_changing_scalar(SqliteCompileoptionGetFunc);
2332
2333    // Connection-state stubs
2334    registry.register_scalar(ChangesFunc);
2335    registry.register_scalar(TotalChangesFunc);
2336    registry.register_scalar(LastInsertRowidFunc);
2337
2338    // "if" is an alias for "iif" (3.48+)
2339    // Register same function under alternate name
2340    struct IfFunc;
2341    impl ScalarFunction for IfFunc {
2342        fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
2343            IifFunc.invoke(args)
2344        }
2345
2346        fn num_args(&self) -> i32 {
2347            -1 // 2 or 3 args, mirroring iif
2348        }
2349
2350        fn min_args(&self) -> i32 {
2351            2
2352        }
2353
2354        fn max_args(&self) -> Option<i32> {
2355            Some(3)
2356        }
2357
2358        fn name(&self) -> &str {
2359            "if"
2360        }
2361    }
2362    registry.register_scalar(IfFunc);
2363
2364    // "substring" is an alias for "substr"
2365    struct SubstringFunc;
2366    impl ScalarFunction for SubstringFunc {
2367        fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
2368            SubstrFunc.invoke(args)
2369        }
2370
2371        fn num_args(&self) -> i32 {
2372            -1
2373        }
2374
2375        fn min_args(&self) -> i32 {
2376            2
2377        }
2378
2379        fn max_args(&self) -> Option<i32> {
2380            Some(3)
2381        }
2382
2383        fn name(&self) -> &str {
2384            "substring"
2385        }
2386    }
2387    registry.register_scalar(SubstringFunc);
2388
2389    // "printf" is an alias for "format".
2390    struct PrintfFunc;
2391    impl ScalarFunction for PrintfFunc {
2392        fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
2393            FormatFunc.invoke(args)
2394        }
2395
2396        fn num_args(&self) -> i32 {
2397            -1
2398        }
2399
2400        fn name(&self) -> &str {
2401            "printf"
2402        }
2403    }
2404    registry.register_scalar(FormatFunc);
2405    registry.register_scalar(PrintfFunc);
2406
2407    // §13.2 Math functions (acos, asin, atan, ceil, floor, log, pow, sqrt, etc.)
2408    register_math_builtins(registry);
2409
2410    // §13.3 Date/time functions (date, time, datetime, julianday, unixepoch, strftime, timediff)
2411    register_datetime_builtins(registry);
2412
2413    // §13.4 Aggregate functions (avg, count, group_concat, max, min, sum, total, etc.)
2414    register_aggregate_builtins(registry);
2415}
2416
2417// ── format(FORMAT, ...) / printf(FORMAT, ...) ───────────────────────────
2418
2419pub struct FormatFunc;
2420
2421impl ScalarFunction for FormatFunc {
2422    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
2423        if args.is_empty() || args[0].is_null() {
2424            return Ok(SqliteValue::Null);
2425        }
2426        let fmt_str = args[0].to_text();
2427        // SQLite returns NULL (not empty text) when the format string is empty:
2428        // an empty format never appends to the StrAccum, so its result buffer
2429        // stays NULL. A non-empty format that renders to nothing (e.g.
2430        // printf('%s', NULL)) still yields empty TEXT, so only gate on the
2431        // format string being empty here.
2432        if fmt_str.is_empty() {
2433            return Ok(SqliteValue::Null);
2434        }
2435        let params = &args[1..];
2436        let result = sqlite_format(&fmt_str, params)?;
2437        Ok(SqliteValue::Text(SmallText::from_string(result)))
2438    }
2439
2440    fn num_args(&self) -> i32 {
2441        -1
2442    }
2443
2444    fn name(&self) -> &str {
2445        "format"
2446    }
2447}
2448
2449/// Simplified SQLite format/printf implementation.
2450/// Supports: %d, %f, %e, %g, %s, %q, %Q, %w, %%, %n (no-op).
2451fn sqlite_format(fmt: &str, params: &[SqliteValue]) -> Result<String> {
2452    let mut result = String::new();
2453    let chars: Vec<char> = fmt.chars().collect();
2454    let mut i = 0;
2455    let mut param_idx = 0;
2456
2457    while i < chars.len() {
2458        if chars[i] != '%' {
2459            result.push(chars[i]);
2460            i += 1;
2461            continue;
2462        }
2463        i += 1;
2464        if i >= chars.len() {
2465            break;
2466        }
2467
2468        // Parse flags
2469        let mut left_align = false;
2470        let mut show_sign = false;
2471        let mut space_sign = false;
2472        let mut zero_pad = false;
2473        let mut alt_form = false;
2474        let mut alt_form2 = false;
2475        let mut comma_group = 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                // SQLite's comma flag: group the integer digits into
2490                // thousands separated by commas. Applies to the decimal
2491                // conversions %d/%i/%u and the integer part of %f; it is
2492                // accepted-but-inert for %e/%g/%x/%o (matches C SQLite).
2493                ',' => comma_group = true,
2494                _ => break,
2495            }
2496            i += 1;
2497        }
2498
2499        // Parse width: a literal number, or `*` to take the width from the next
2500        // argument (bd-jvnwt). A negative dynamic width means left-justify with
2501        // its absolute value, matching C printf.
2502        let mut width = 0usize;
2503        if i < chars.len() && chars[i] == '*' {
2504            i += 1;
2505            let w = params.get(param_idx).map_or(0, SqliteValue::to_integer);
2506            param_idx += 1;
2507            if w < 0 {
2508                left_align = true;
2509                width = usize::try_from(w.unsigned_abs())
2510                    .unwrap_or(0)
2511                    .min(100_000_000);
2512            } else {
2513                width = usize::try_from(w).unwrap_or(0).min(100_000_000);
2514            }
2515        } else {
2516            while i < chars.len() && chars[i].is_ascii_digit() {
2517                width = width
2518                    .saturating_mul(10)
2519                    .saturating_add(chars[i] as usize - '0' as usize)
2520                    .min(100_000_000); // Prevent OOM from malicious formats
2521                i += 1;
2522            }
2523        }
2524
2525        // Parse precision: a literal number, or `*` to take the precision from
2526        // the next argument (like width above). A negative dynamic precision
2527        // means "no precision", matching C printf.
2528        let mut precision = None;
2529        if i < chars.len() && chars[i] == '.' {
2530            i += 1;
2531            if i < chars.len() && chars[i] == '*' {
2532                i += 1;
2533                let p = params.get(param_idx).map_or(0, SqliteValue::to_integer);
2534                param_idx += 1;
2535                // bd-9zzr0 L1 / bd-77dkj: SQLite takes the ABSOLUTE value of a
2536                // negative dynamic precision (unlike C's "no precision") —
2537                // printf('%.*d', -3, 42) == printf('%.3d', 42) == '042'. Stock
2538                // first CASTS the precision arg to i32, so a huge i64 like
2539                // -4294967293 becomes i32 3 ('042'), NOT a 100M-digit zero-pad
2540                // blowup; and INT32_MIN (no positive counterpart) escapes to
2541                // "no precision".
2542                let p32 = p as i32;
2543                precision = if p32 == i32::MIN {
2544                    None
2545                } else {
2546                    Some(
2547                        usize::try_from(p32.unsigned_abs())
2548                            .unwrap_or(usize::MAX)
2549                            .min(100_000_000),
2550                    )
2551                };
2552            } else {
2553                let mut prec = 0usize;
2554                while i < chars.len() && chars[i].is_ascii_digit() {
2555                    prec = prec
2556                        .saturating_mul(10)
2557                        .saturating_add(chars[i] as usize - '0' as usize)
2558                        .min(100_000_000); // Prevent OOM from malicious formats
2559                    i += 1;
2560                }
2561                precision = Some(prec);
2562            }
2563        }
2564
2565        if i >= chars.len() {
2566            break;
2567        }
2568
2569        let spec = chars[i];
2570        i += 1;
2571
2572        match spec {
2573            // A literal `%` honors the field width like any other conversion
2574            // (space-padded, right/left-justified): `%5%` -> "    %", `%-5%` ->
2575            // "%    ". The `0` flag pads with spaces (`%` is not numeric), so
2576            // pad_string is correct (bd-g27fn).
2577            '%' => result.push_str(&pad_string("%", width, left_align)),
2578            'n' => {} // no-op (security: never writes to memory)
2579            'd' | 'i' => {
2580                let val = params.get(param_idx).map_or(0, SqliteValue::to_integer);
2581                param_idx += 1;
2582                let formatted = format_integer(
2583                    val,
2584                    width,
2585                    left_align,
2586                    show_sign,
2587                    space_sign,
2588                    zero_pad,
2589                    comma_group,
2590                    precision,
2591                );
2592                result.push_str(&formatted);
2593            }
2594            'u' => {
2595                // Unsigned decimal (bd-jvnwt): reinterpret the i64 bit pattern as
2596                // u64, matching C/SQLite %u.
2597                let val = params.get(param_idx).map_or(0, SqliteValue::to_integer);
2598                param_idx += 1;
2599                #[allow(clippy::cast_sign_loss)]
2600                let digits = apply_int_precision(&(val as u64).to_string(), precision);
2601                let padded = if comma_group {
2602                    // Zero-pad the raw digits to the field width before grouping,
2603                    // so the padding zeros participate in comma grouping.
2604                    let base = if zero_pad && width > digits.len() {
2605                        format!("{}{}", "0".repeat(width - digits.len()), digits)
2606                    } else {
2607                        digits
2608                    };
2609                    let grouped = group_thousands(&base);
2610                    if zero_pad {
2611                        grouped
2612                    } else {
2613                        pad_string(&grouped, width, left_align)
2614                    }
2615                } else if zero_pad && width > digits.len() {
2616                    format!("{}{}", "0".repeat(width - digits.len()), digits)
2617                } else {
2618                    pad_string(&digits, width, left_align)
2619                };
2620                result.push_str(&padded);
2621            }
2622            'f' => {
2623                let val = params.get(param_idx).map_or(0.0, SqliteValue::to_float);
2624                param_idx += 1;
2625                // C SQLite normalizes signed zero for %f/%e/%g: -0.0 renders as
2626                // 0 (no minus), and any sign flag then applies to +0.0
2627                // (bd-gh-printf-negative-zero-era4w). `-0.0 == 0.0` is true.
2628                let val = if val == 0.0 { 0.0 } else { val };
2629                let formatted = if let Some(s) =
2630                    nonfinite_float_str(val, width, left_align, show_sign, space_sign)
2631                {
2632                    s
2633                } else {
2634                    // Build the unsigned magnitude, honoring precision. Alt-form-2
2635                    // (`!`) applies the requested precision FIRST and then strips
2636                    // trailing fractional zeros (keeping >=1 digit, forcing ".0"
2637                    // when precision is 0) — matches C SQLite, e.g. '%!5.2f' 3.14159
2638                    // -> "3.14", '%!.3f' 1.5 -> "1.5", '%!f' 0.1 -> "0.1".
2639                    let prec = precision.unwrap_or(6);
2640                    // Round exact binary ties away from zero (C SQLite) rather
2641                    // than Rust's round-half-to-even, e.g. printf('%.0f', 2.5)
2642                    // -> "3" not "2" (bd-o1tu1).
2643                    // C SQLite's dtoa caps at 16 significant digits of the true
2644                    // value, then pads zeros (bd-o8m86). Round the naive
2645                    // fixed-point rendering to that cap; a no-op when prec stays
2646                    // within the value's meaningful precision. Alt-form-2 (`!`)
2647                    // has its own shortest-round-trip rendering (which keeps the
2648                    // exact integer of a large whole number), so it is not capped.
2649                    let mut mag = if alt_form2 {
2650                        // bd-ixizz: alt-form-2 caps significant digits at the
2651                        // exact double's value-dependent cap (18, or 19 for
2652                        // |val| >= 1e18), TRUNCATING there. Below the cap, Rust's
2653                        // exact fixed rounding already reproduces stock's digits.
2654                        if val == 0.0 {
2655                            altform2_trim_float("0")
2656                        } else {
2657                            let (cap_digits, sci_exp) = sqlite_float_altform2_digits(val);
2658                            // Significant digits a `prec`-fractional render shows.
2659                            let want =
2660                                i64::from(sci_exp) + i64::try_from(prec).unwrap_or(i64::MAX) + 1;
2661                            let cap = i64::try_from(cap_digits.len()).unwrap_or(i64::MAX);
2662                            if want >= cap {
2663                                altform2_render_fixed(&cap_digits, sci_exp, prec)
2664                            } else {
2665                                altform2_trim_float(&format_fixed_round_half_away(val.abs(), prec))
2666                            }
2667                        }
2668                    } else {
2669                        round_positional_to_sig(
2670                            &format_fixed_round_half_away(val.abs(), prec),
2671                            FLOAT_SIG_DIGITS,
2672                        )
2673                    };
2674                    // Alternate form (`#`) forces a decimal point, e.g. '%#.0f' 3
2675                    // -> "3.".
2676                    if alt_form && !mag.contains('.') {
2677                        mag.push('.');
2678                    }
2679                    if comma_group {
2680                        mag = group_float_integer_part(&mag);
2681                    }
2682                    let body = if val.is_sign_negative() {
2683                        format!("-{mag}")
2684                    } else {
2685                        mag
2686                    };
2687                    finish_float_padding(&body, width, left_align, show_sign, space_sign, zero_pad)
2688                };
2689                result.push_str(&formatted);
2690            }
2691            'e' | 'E' => {
2692                let val = params.get(param_idx).map_or(0.0, SqliteValue::to_float);
2693                param_idx += 1;
2694                // Normalize signed zero (bd-gh-printf-negative-zero-era4w).
2695                let val = if val == 0.0 { 0.0 } else { val };
2696                let prec = precision.unwrap_or(6);
2697                if let Some(s) = nonfinite_float_str(val, width, left_align, show_sign, space_sign)
2698                {
2699                    result.push_str(&s);
2700                } else if alt_form2 {
2701                    // bd-ixizz: alt-form-2 emits min(prec+1, cap) significant
2702                    // digits — ROUNDED below the exact double's value-dependent
2703                    // cap (18, or 19 for |val| >= 1e18), exact-TRUNCATED at it —
2704                    // then strips trailing zeros. e.g. '%!.40e' 2.0/3.0 ->
2705                    // "6.66666666666666629e-01", '%!e' 5.0 -> "5.0e+00". The
2706                    // mantissa always carries a decimal point, so `#` is a no-op.
2707                    let (digits, exp) = altform2_sig_digits(val, prec + 1);
2708                    let mut formatted = altform2_render_exp(&digits, exp, spec == 'E');
2709                    if val.is_sign_negative() {
2710                        formatted = format!("-{formatted}");
2711                    }
2712                    result.push_str(&finish_float_padding(
2713                        &formatted, width, left_align, show_sign, space_sign, zero_pad,
2714                    ));
2715                } else {
2716                    // Round the mantissa's exact ties away from zero (C SQLite)
2717                    // instead of Rust's round-half-to-even, e.g.
2718                    // printf('%.0e', 2.5) -> "3e+00" not "2e+00" (bd-o1tu1).
2719                    // C SQLite's dtoa caps the mantissa at 16 significant digits
2720                    // of the true value, then pads zeros beyond (bd-o8m86). Round
2721                    // the mantissa to <=16 sig figs, then pad zeros to `prec`.
2722                    let mant_prec = prec.min(FLOAT_SIG_DIGITS - 1);
2723                    let raw = format_sci_round_half_away(val, mant_prec, spec == 'E');
2724                    let mut formatted = normalize_exponent(&raw);
2725                    if prec > mant_prec
2726                        && let Some(e_pos) = formatted.find(['e', 'E'])
2727                    {
2728                        let (mant, exp_part) = formatted.split_at(e_pos);
2729                        formatted = format!("{mant}{}{exp_part}", "0".repeat(prec - mant_prec));
2730                    }
2731                    // Alternate form (`#`) forces a decimal point in the mantissa,
2732                    // e.g. '%#.0e' 3 -> "3.e+00".
2733                    if alt_form && let Some(e_pos) = formatted.find(['e', 'E']) {
2734                        let (mantissa, exp_part) = formatted.split_at(e_pos);
2735                        if !mantissa.contains('.') {
2736                            formatted = format!("{mantissa}.{exp_part}");
2737                        }
2738                    }
2739                    result.push_str(&finish_float_padding(
2740                        &formatted, width, left_align, show_sign, space_sign, zero_pad,
2741                    ));
2742                }
2743            }
2744            'g' | 'G' => {
2745                let val = params.get(param_idx).map_or(0.0, SqliteValue::to_float);
2746                param_idx += 1;
2747                // Normalize signed zero, incl. the alt-form path that bypasses
2748                // format_float_g (bd-gh-printf-negative-zero-era4w).
2749                let val = if val == 0.0 { 0.0 } else { val };
2750                let prec = precision.unwrap_or(6);
2751                let sig = prec.max(1);
2752                // C SQLite's dtoa caps at 16 significant digits of the true value
2753                // then pads/omits beyond (bd-o8m86); the fixed-vs-exponential
2754                // choice still uses the requested `sig`. format_float_g rounds
2755                // (not truncates) to `min(sig, max_sig)` figs, matching the cap.
2756                let max_sig = FLOAT_SIG_DIGITS;
2757                if let Some(s) = nonfinite_float_str(val, width, left_align, show_sign, space_sign)
2758                {
2759                    result.push_str(&s);
2760                } else if alt_form2 {
2761                    // bd-ixizz: alt-form-2 (`!`) on %g emits min(sig, cap)
2762                    // significant digits — ROUNDED below the exact double's
2763                    // value-dependent cap (18, or 19 for |val| >= 1e18), exact-
2764                    // TRUNCATED at it — then applies %g's fixed-vs-exponential
2765                    // choice (honoring precision — 0 means 1 sig fig) and forces a
2766                    // decimal point with >= 1 fractional digit: '%!.0g' 12345 ->
2767                    // "1.0e+04", '%!.3g' 12345 -> "1.23e+04", '%!g' 100 -> "100.0",
2768                    // '%!.18g' 2.0/3.0 -> "0.666666666666666629".
2769                    let (digits, exp) = altform2_sig_digits(val, sig);
2770                    let use_exp_form =
2771                        exp < -4 || i64::from(exp) >= i64::try_from(sig).unwrap_or(i64::MAX);
2772                    let mut alt = if use_exp_form {
2773                        altform2_render_exp(&digits, exp, spec == 'G')
2774                    } else {
2775                        altform2_render_fixed(&digits, exp, usize::MAX)
2776                    };
2777                    if val.is_sign_negative() {
2778                        alt = format!("-{alt}");
2779                    }
2780                    result.push_str(&finish_float_padding(
2781                        &alt, width, left_align, show_sign, space_sign, zero_pad,
2782                    ));
2783                } else {
2784                    let mut formatted = format_float_g(val, sig, spec == 'G', alt_form, max_sig);
2785                    // The `,` flag groups the integer part only when %g renders in
2786                    // fixed (non-exponential) form; SQLite leaves exponential
2787                    // output ungrouped ('%,g' 1234.5 -> "1,234.5"; '%,g' 1e6 ->
2788                    // "1e+06").
2789                    if comma_group && !formatted.contains(['e', 'E']) {
2790                        formatted = group_signed_decimal_integer_part(&formatted);
2791                    }
2792                    result.push_str(&finish_float_padding(
2793                        &formatted, width, left_align, show_sign, space_sign, zero_pad,
2794                    ));
2795                }
2796            }
2797            's' | 'z' => {
2798                let param = params.get(param_idx);
2799                param_idx += 1;
2800                let val = match param {
2801                    // SQLite: printf('%s', NULL) returns empty string
2802                    Some(SqliteValue::Null) | None => String::new(),
2803                    Some(v) => v.to_text(),
2804                };
2805                // C SQLite counts %s precision in BYTES. It will emit a bare
2806                // partial code point; we floor to the previous char boundary
2807                // instead (Rust strings must stay valid UTF-8), which matches
2808                // C SQLite whenever the cut lands on a boundary.
2809                let truncated = if let Some(prec) = precision {
2810                    if val.len() > prec {
2811                        let mut end = prec;
2812                        while end > 0 && !val.is_char_boundary(end) {
2813                            end -= 1;
2814                        }
2815                        val[..end].to_owned()
2816                    } else {
2817                        val
2818                    }
2819                } else {
2820                    val
2821                };
2822                result.push_str(&pad_string(&truncated, width, left_align));
2823            }
2824            'q' => {
2825                // Single-quote escaping; C SQLite emits "(NULL)" for %q with NULL.
2826                // Field width applies (byte-counted, like %s), space-padded and
2827                // right/left-justified, including the "(NULL)" case (bd-8959m).
2828                let param = params.get(param_idx);
2829                param_idx += 1;
2830                // bd-9zzr0 L3: precision truncates the raw text BEFORE escaping.
2831                let escaped = match param {
2832                    // SQLite: printf('%q', NULL) returns literal "(NULL)"
2833                    Some(SqliteValue::Null) | None => "(NULL)".to_owned(),
2834                    Some(v) => {
2835                        let text = v.to_text();
2836                        truncate_str_precision(&text, precision).replace('\'', "''")
2837                    }
2838                };
2839                result.push_str(&pad_string(&escaped, width, left_align));
2840            }
2841            'Q' => {
2842                // Like %q but wrapped in quotes, NULL -> "NULL". Field width
2843                // applies to the whole rendered token (bd-8959m). Precision
2844                // truncates the raw text before escaping (bd-9zzr0 L3).
2845                let param = params.get(param_idx);
2846                param_idx += 1;
2847                let rendered = match param {
2848                    Some(SqliteValue::Null) | None => "NULL".to_owned(),
2849                    Some(v) => {
2850                        let text = v.to_text();
2851                        format!(
2852                            "'{}'",
2853                            truncate_str_precision(&text, precision).replace('\'', "''")
2854                        )
2855                    }
2856                };
2857                result.push_str(&pad_string(&rendered, width, left_align));
2858            }
2859            'w' => {
2860                // Double-quote escaping for identifiers. bd-9zzr0 L2: C SQLite
2861                // renders NULL as the literal "(NULL)" (not empty). bd-9zzr0 L3:
2862                // precision truncates the raw text before escaping. Field width
2863                // applies to the whole rendered token (bd-8959m).
2864                let param = params.get(param_idx);
2865                param_idx += 1;
2866                let rendered = match param {
2867                    Some(SqliteValue::Null) | None => "(NULL)".to_owned(),
2868                    Some(v) => {
2869                        let text = v.to_text();
2870                        truncate_str_precision(&text, precision).replace('"', "\"\"")
2871                    }
2872                };
2873                result.push_str(&pad_string(&rendered, width, left_align));
2874            }
2875            'x' | 'X' => {
2876                let val = params.get(param_idx).map_or(0, SqliteValue::to_integer);
2877                param_idx += 1;
2878                #[allow(clippy::cast_sign_loss)]
2879                let digits = apply_int_precision(
2880                    &if spec == 'x' {
2881                        format!("{:x}", val as u64)
2882                    } else {
2883                        format!("{:X}", val as u64)
2884                    },
2885                    precision,
2886                );
2887                // Alternate form (`#`) prefixes a nonzero value with 0x / 0X.
2888                let prefix = if alt_form && val != 0 {
2889                    if spec == 'x' { "0x" } else { "0X" }
2890                } else {
2891                    ""
2892                };
2893                // SQLite's printf zero-pads whenever the `0` flag is present,
2894                // even alongside `-` (it does NOT let `-` override `0` the way C
2895                // does). The digits are zero-padded to `width`; the prefix sits
2896                // outside that pad.
2897                let padded = if zero_pad && width > digits.len() {
2898                    let pad = "0".repeat(width - digits.len());
2899                    format!("{prefix}{pad}{digits}")
2900                } else {
2901                    pad_string(&format!("{prefix}{digits}"), width, left_align)
2902                };
2903                result.push_str(&padded);
2904            }
2905            'o' => {
2906                let val = params.get(param_idx).map_or(0, SqliteValue::to_integer);
2907                param_idx += 1;
2908                #[allow(clippy::cast_sign_loss)]
2909                let digits = apply_int_precision(&format!("{:o}", val as u64), precision);
2910                // Alternate form (`#`) prefixes a nonzero value with a leading 0.
2911                let prefix = if alt_form && val != 0 { "0" } else { "" };
2912                // As with %x, SQLite zero-pads whenever the `0` flag is present
2913                // (even with `-`).
2914                let padded = if zero_pad && width > digits.len() {
2915                    let pad = "0".repeat(width - digits.len());
2916                    format!("{prefix}{pad}{digits}")
2917                } else {
2918                    pad_string(&format!("{prefix}{digits}"), width, left_align)
2919                };
2920                result.push_str(&padded);
2921            }
2922            'c' => {
2923                let param = params.get(param_idx);
2924                param_idx += 1;
2925                // SQLite's printf %c renders the argument to its text form and
2926                // emits the first character — it does NOT interpret an integer
2927                // as a Unicode codepoint like C printf does (bd-47mu0). So
2928                // printf('%c', 65) yields '6' (first char of "65"), not 'A'.
2929                let text = match param {
2930                    Some(SqliteValue::Null) | None => String::new(),
2931                    Some(v) => v.to_text(),
2932                };
2933                // bd-9zzr0 M9: %c precision is a REPEAT count for the emitted
2934                // char — printf('%.5c', 'A') == "AAAAA". Without precision the
2935                // char is emitted once; an empty argument emits none. bd-77dkj:
2936                // precision 0 (and 1) still emits the char ONCE — stock clamps
2937                // the repeat count to a minimum of 1 (printf('%.0c','A') == 'A'),
2938                // it does NOT drop the char like a naive repeat(0) would.
2939                // Field width applies, counted in CHARACTERS (the '0' flag is
2940                // ignored — padding is always spaces, right- or left-justified;
2941                // bd-ul4c0/bd-47mu0).
2942                let content = match text.chars().next() {
2943                    Some(c) => c.to_string().repeat(precision.map_or(1, |p| p.max(1))),
2944                    None => String::new(),
2945                };
2946                let pad = width.saturating_sub(content.chars().count());
2947                if !left_align {
2948                    for _ in 0..pad {
2949                        result.push(' ');
2950                    }
2951                }
2952                result.push_str(&content);
2953                if left_align {
2954                    for _ in 0..pad {
2955                        result.push(' ');
2956                    }
2957                }
2958            }
2959            _ => {
2960                // Unknown specifier: output literally
2961                result.push('%');
2962                result.push(spec);
2963            }
2964        }
2965        // Suppress unused warnings
2966        let _ = (left_align, show_sign, space_sign, zero_pad);
2967    }
2968    Ok(result)
2969}
2970
2971/// Truncate `val` to `precision` BYTES, flooring to the previous char boundary
2972/// so the slice stays valid UTF-8 — matching C SQLite's byte-counted string
2973/// precision. Shared by %s/%z and (bd-9zzr0 L3) the pre-escape text of %q/%Q/%w.
2974fn truncate_str_precision(val: &str, precision: Option<usize>) -> &str {
2975    match precision {
2976        Some(prec) if val.len() > prec => {
2977            let mut end = prec;
2978            while end > 0 && !val.is_char_boundary(end) {
2979                end -= 1;
2980            }
2981            &val[..end]
2982        }
2983        _ => val,
2984    }
2985}
2986
2987// A printf integer conversion carries several independent, non-groupable flags
2988// (justification, sign mode, zero-pad, comma grouping) plus width and precision;
2989// bundling them behind a struct would only add indirection for a single caller.
2990#[allow(clippy::too_many_arguments)]
2991fn format_integer(
2992    val: i64,
2993    width: usize,
2994    left_align: bool,
2995    show_sign: bool,
2996    space_sign: bool,
2997    zero_pad: bool,
2998    comma_group: bool,
2999    precision: Option<usize>,
3000) -> String {
3001    let sign = if val < 0 {
3002        "-".to_owned()
3003    } else if show_sign {
3004        "+".to_owned()
3005    } else if space_sign {
3006        " ".to_owned()
3007    } else {
3008        String::new()
3009    };
3010    let digits = apply_int_precision(&format!("{}", val.unsigned_abs()), precision);
3011    if comma_group {
3012        // SQLite zero-pads the raw digits up to the field width BEFORE inserting
3013        // the grouping commas, so the padding zeros are themselves grouped
3014        // (e.g. '%,08d' 1234 -> "00,001,234"). Space padding, by contrast, is
3015        // applied to the already-grouped value ('%,10d' 1234567 -> " 1,234,567").
3016        let padded_digits = if zero_pad && width > sign.len() + digits.len() {
3017            format!("{}{digits}", "0".repeat(width - sign.len() - digits.len()))
3018        } else {
3019            digits
3020        };
3021        let body = format!("{sign}{}", group_thousands(&padded_digits));
3022        if zero_pad || body.len() >= width {
3023            return body;
3024        }
3025        let pad = width - body.len();
3026        return if left_align {
3027            format!("{body}{}", " ".repeat(pad))
3028        } else {
3029            format!("{}{body}", " ".repeat(pad))
3030        };
3031    }
3032    let body = format!("{sign}{digits}");
3033    if body.len() >= width {
3034        return body;
3035    }
3036    let pad = width - body.len();
3037    // bd-9zzr0 M10: SQLite's `0` flag WINS over `-` for integer conversions
3038    // (unlike C, where `-` disables `0`): printf('%-05d', 42) == '00042',
3039    // matching the %u/%x/%o paths. So zero-pad is checked before left-align.
3040    if zero_pad {
3041        format!("{sign}{}{digits}", "0".repeat(pad))
3042    } else if left_align {
3043        format!("{body}{}", " ".repeat(pad))
3044    } else {
3045        format!("{}{body}", " ".repeat(pad))
3046    }
3047}
3048
3049/// Insert a comma every three digits, counting from the right, into a string of
3050/// ASCII digits — SQLite's printf `,` grouping flag. Input with fewer than four
3051/// digits (or any non-digit byte) is returned unchanged.
3052fn group_thousands(digits: &str) -> String {
3053    if digits.len() <= 3 || !digits.bytes().all(|b| b.is_ascii_digit()) {
3054        return digits.to_owned();
3055    }
3056    let lead = digits.len() % 3;
3057    let mut out = String::with_capacity(digits.len() + digits.len() / 3);
3058    if lead > 0 {
3059        out.push_str(&digits[..lead]);
3060    }
3061    let mut idx = lead;
3062    while idx < digits.len() {
3063        if !out.is_empty() {
3064            out.push(',');
3065        }
3066        out.push_str(&digits[idx..idx + 3]);
3067        idx += 3;
3068    }
3069    out
3070}
3071
3072/// Alternate-form-2 (`!`) trailing-zero trim for a `%f` magnitude string: strip
3073/// trailing fractional zeros but keep at least one digit after the decimal
3074/// point, and add a ".0" when there is no point at all (precision 0). Matches
3075/// C SQLite: "5.000000" -> "5.0", "1.500" -> "1.5", "6" -> "6.0", "3.14" -> "3.14".
3076fn altform2_trim_float(mag: &str) -> String {
3077    if mag.contains('.') {
3078        let trimmed = mag.trim_end_matches('0');
3079        if trimmed.ends_with('.') {
3080            format!("{trimmed}0")
3081        } else {
3082            trimmed.to_owned()
3083        }
3084    } else {
3085        format!("{mag}.0")
3086    }
3087}
3088
3089/// Apply the `,` thousands grouping to the integer part of an unsigned `%f`
3090/// magnitude string, e.g. "1234.500000" -> "1,234.500000".
3091fn group_float_integer_part(mag: &str) -> String {
3092    if let Some(dot) = mag.find('.') {
3093        format!("{}{}", group_thousands(&mag[..dot]), &mag[dot..])
3094    } else {
3095        group_thousands(mag)
3096    }
3097}
3098
3099/// Like [`group_float_integer_part`] but for a possibly signed decimal string
3100/// (e.g. a `%g` fixed-form result such as "-1234.5" -> "-1,234.5").
3101fn group_signed_decimal_integer_part(s: &str) -> String {
3102    if let Some(rest) = s.strip_prefix('-') {
3103        format!("-{}", group_float_integer_part(rest))
3104    } else {
3105        group_float_integer_part(s)
3106    }
3107}
3108
3109/// Apply an integer conversion's precision (SQLite/C printf: precision is the
3110/// MINIMUM number of digits — left-pad with zeros to reach it). Applies to
3111/// %d/%i/%u/%x/%X/%o. `None` (no precision) leaves the digits untouched. SQLite
3112/// keeps a single "0" for `%.0d` of 0 (unlike C, which yields ""), which falls
3113/// out naturally because the "0" digit already satisfies precision 0.
3114fn apply_int_precision(digits: &str, precision: Option<usize>) -> String {
3115    match precision {
3116        Some(p) if digits.len() < p => {
3117            format!("{}{digits}", "0".repeat(p - digits.len()))
3118        }
3119        _ => digits.to_owned(),
3120    }
3121}
3122
3123/// The significant digits SQLite's `%!` (alt-form-2) float rendering emits for a
3124/// requested count of `want` significant digits (bd-ixizz).
3125///
3126/// Draws from the exact double's value-dependent cap
3127/// ([`sqlite_float_altform2_digits`], 18 sig digits, or 19 when `|val| >= 1e18`):
3128/// ROUNDS half-away when `want` is below the cap, and returns the exact TRUNCATED
3129/// digits at/above it. Returns `(digit_bytes, exponent)` with `exponent` the
3130/// power-of-ten of the first digit (adjusted for any rounding carry). `want` must
3131/// be >= 1.
3132fn altform2_sig_digits(val: f64, want: usize) -> (Vec<u8>, i32) {
3133    let (cap_digits, sci_exp) = sqlite_float_altform2_digits(val);
3134    if want >= cap_digits.len() {
3135        return (cap_digits, sci_exp);
3136    }
3137    let mut digits = cap_digits[..want].to_vec();
3138    let mut exp = sci_exp;
3139    // Round half away from zero when the first dropped digit is >= 5. (The digits
3140    // up to the cap are exact, so this matches rounding the true value.)
3141    if cap_digits[want] >= b'5' {
3142        let mut idx = want;
3143        loop {
3144            if idx == 0 {
3145                // Carried out of the leading digit: "99..9" -> "10..0", exp + 1.
3146                digits.fill(b'0');
3147                digits[0] = b'1';
3148                exp += 1;
3149                break;
3150            }
3151            idx -= 1;
3152            if digits[idx] == b'9' {
3153                digits[idx] = b'0';
3154            } else {
3155                digits[idx] += 1;
3156                break;
3157            }
3158        }
3159    }
3160    (digits, exp)
3161}
3162
3163/// Render alt-form-2 significant `digits` as a `%e`/`%E` mantissa+exponent.
3164///
3165/// The first digit is at 10^`exp`. Emits `d.ddd` with trailing zeros stripped
3166/// (>= 1 fractional digit kept) and a signed, >= 2-digit exponent. Unsigned — the
3167/// caller prepends any `-`. e.g. `("666…629", -1)` -> "6.66…629e-01".
3168fn altform2_render_exp(digits: &[u8], exp: i32, upper: bool) -> String {
3169    let e_char = if upper { 'E' } else { 'e' };
3170    let mut mant = String::with_capacity(digits.len() + 2);
3171    mant.push(char::from(digits[0]));
3172    mant.push('.');
3173    if digits.len() > 1 {
3174        mant.extend(digits[1..].iter().map(|&b| char::from(b)));
3175    } else {
3176        mant.push('0');
3177    }
3178    let mant = altform2_trim_float(&mant);
3179    let sign = if exp < 0 { '-' } else { '+' };
3180    format!("{mant}{e_char}{sign}{:02}", exp.unsigned_abs())
3181}
3182
3183/// Render alt-form-2 significant `digits` in fixed (`%f`/`%g`) form.
3184///
3185/// The first digit is at 10^`exp`; place the digits about the decimal point, keep
3186/// at most `max_frac` fractional slots, strip trailing zeros (forcing >= 1
3187/// fractional digit). `max_frac == usize::MAX` shows every significant digit
3188/// (`%g`). Unsigned — the caller prepends any `-`.
3189fn altform2_render_fixed(digits: &[u8], exp: i32, max_frac: usize) -> String {
3190    let n = digits.len();
3191    let mut out = String::new();
3192    if exp >= 0 {
3193        let int_len = usize::try_from(exp).unwrap_or(0) + 1;
3194        if int_len >= n {
3195            // All significant digits are integer digits; zero-fill to `int_len`.
3196            out.extend(digits.iter().map(|&b| char::from(b)));
3197            out.push_str(&"0".repeat(int_len - n));
3198        } else {
3199            out.extend(digits[..int_len].iter().map(|&b| char::from(b)));
3200            out.push('.');
3201            let frac = &digits[int_len..];
3202            let take = frac.len().min(max_frac);
3203            out.extend(frac[..take].iter().map(|&b| char::from(b)));
3204        }
3205    } else {
3206        out.push_str("0.");
3207        let lead_zeros = usize::try_from(-exp - 1).unwrap_or(0);
3208        let take_zeros = lead_zeros.min(max_frac);
3209        out.push_str(&"0".repeat(take_zeros));
3210        let remaining = max_frac.saturating_sub(take_zeros);
3211        let take = n.min(remaining);
3212        out.extend(digits[..take].iter().map(|&b| char::from(b)));
3213    }
3214    altform2_trim_float(&out)
3215}
3216
3217/// C SQLite renders non-finite floats in printf as `Inf` / `-Inf` / `NaN`
3218/// (sign flags honored for infinities, space-padded to width, never
3219/// zero-padded). Returns `None` for finite values.
3220fn nonfinite_float_str(
3221    val: f64,
3222    width: usize,
3223    left_align: bool,
3224    show_sign: bool,
3225    space_sign: bool,
3226) -> Option<String> {
3227    let body = if val.is_nan() {
3228        "NaN".to_owned()
3229    } else if val.is_infinite() {
3230        let sign = if val < 0.0 {
3231            "-"
3232        } else if show_sign {
3233            "+"
3234        } else if space_sign {
3235            " "
3236        } else {
3237            ""
3238        };
3239        format!("{sign}Inf")
3240    } else {
3241        return None;
3242    };
3243    Some(pad_string(&body, width, left_align))
3244}
3245
3246/// Apply printf sign flags and width padding to an already-formatted float
3247/// body (which may carry a leading `-`). Zero padding is inserted between the
3248/// sign and the digits, matching C printf.
3249fn finish_float_padding(
3250    body: &str,
3251    width: usize,
3252    left_align: bool,
3253    show_sign: bool,
3254    space_sign: bool,
3255    zero_pad: bool,
3256) -> String {
3257    let (sign, digits) = if let Some(rest) = body.strip_prefix('-') {
3258        ("-", rest)
3259    } else if show_sign {
3260        ("+", body)
3261    } else if space_sign {
3262        (" ", body)
3263    } else {
3264        ("", body)
3265    };
3266    let full_len = sign.len() + digits.len();
3267    if full_len >= width {
3268        return format!("{sign}{digits}");
3269    }
3270    let pad = width - full_len;
3271    if left_align {
3272        format!("{sign}{digits}{}", " ".repeat(pad))
3273    } else if zero_pad {
3274        format!("{sign}{}{digits}", "0".repeat(pad))
3275    } else {
3276        format!("{}{sign}{digits}", " ".repeat(pad))
3277    }
3278}
3279
3280fn pad_string(s: &str, width: usize, left_align: bool) -> String {
3281    if s.len() >= width {
3282        return s.to_owned();
3283    }
3284    let pad = width - s.len();
3285    if left_align {
3286        format!("{s}{}", " ".repeat(pad))
3287    } else {
3288        format!("{}{s}", " ".repeat(pad))
3289    }
3290}
3291
3292/// Normalize an exponent string to match C printf: explicit sign and
3293/// minimum two digits (e.g. `"1.23e6"` → `"1.23e+06"`).
3294fn normalize_exponent(s: &str) -> String {
3295    let (prefix, e_char, exp_part) = if let Some(pos) = s.find('e') {
3296        (&s[..pos], 'e', &s[pos + 1..])
3297    } else if let Some(pos) = s.find('E') {
3298        (&s[..pos], 'E', &s[pos + 1..])
3299    } else {
3300        return s.to_owned();
3301    };
3302    let (sign, digits) = if let Some(rest) = exp_part.strip_prefix('-') {
3303        ("-", rest)
3304    } else if let Some(rest) = exp_part.strip_prefix('+') {
3305        ("+", rest)
3306    } else {
3307        ("+", exp_part)
3308    };
3309    let padded = if digits.len() < 2 {
3310        format!("0{digits}")
3311    } else {
3312        digits.to_owned()
3313    };
3314    format!("{prefix}{e_char}{sign}{padded}")
3315}
3316
3317/// A finite `f64` has at most 1074 fractional decimal digits (the smallest
3318/// positive subnormal, `2^-1074`). Formatting with this many guard digits
3319/// therefore reproduces the value's EXACT decimal expansion, so a trailing
3320/// "…5000…0" is a genuine binary half-tie rather than a rounding artifact.
3321const MAX_F64_FRACTIONAL_DIGITS: usize = 1074;
3322
3323/// Increment a non-negative decimal magnitude given as ASCII digit bytes (may
3324/// contain a single `.`, never a sign) by one unit in its last place,
3325/// propagating carry and prepending `1` on overflow, e.g. `"2"` → `"3"`,
3326/// `"9"` → `"10"`, `"9.9"` → `"10.0"`.
3327fn increment_decimal_digits(digits: &mut Vec<u8>) {
3328    let mut carry = true;
3329    for b in digits.iter_mut().rev() {
3330        if *b == b'.' {
3331            continue;
3332        }
3333        if carry {
3334            if *b == b'9' {
3335                *b = b'0';
3336            } else {
3337                *b += 1;
3338                carry = false;
3339                break;
3340            }
3341        }
3342    }
3343    if carry {
3344        digits.insert(0, b'1');
3345    }
3346}
3347
3348/// True iff the non-negative magnitude `mag` is an EXACT binary half-tie at
3349/// `prec` fractional digits — its exact decimal expansion has digit `prec + 1`
3350/// equal to `5` with only zeros afterward. Rust's `format!` rounds ties to
3351/// even while C SQLite rounds them away from zero, so ONLY exact ties diverge;
3352/// every other value is already correctly rounded by `format!`.
3353///
3354/// A real tie shows the "…5000" pattern at any guard length, so a cheap guard
3355/// is checked first; it is confirmed against the full exact expansion only to
3356/// reject near-ties whose long 9-/0-runs a short guard would round into a
3357/// spurious "…5000" (e.g. `0.15` is really `0.14999…`, not a tie).
3358fn is_exact_decimal_tie(mag: f64, prec: usize) -> bool {
3359    fn looks_like_tie(mag: f64, prec: usize, guard: usize) -> bool {
3360        let full = format!("{mag:.guard$}");
3361        let Some(dot) = full.find('.') else {
3362            return false;
3363        };
3364        let rd_idx = dot + 1 + prec;
3365        let bytes = full.as_bytes();
3366        rd_idx < bytes.len()
3367            && bytes[rd_idx] == b'5'
3368            && full[rd_idx + 1..].bytes().all(|b| b == b'0')
3369    }
3370    looks_like_tie(mag, prec, prec + 18)
3371        && looks_like_tie(mag, prec, prec + MAX_F64_FRACTIONAL_DIGITS)
3372}
3373
3374/// Scientific-notation analogue of [`is_exact_decimal_tie`]: true iff the
3375/// mantissa of `mag` (normalized to `[1, 10)`) has an exact `5` with only
3376/// trailing zeros at fractional digit `prec + 1`.
3377fn is_exact_sci_tie(mag: f64, prec: usize) -> bool {
3378    fn looks_like_tie(mag: f64, prec: usize, guard: usize) -> bool {
3379        let s = format!("{mag:.guard$e}");
3380        let Some((mant, _)) = s.split_once('e') else {
3381            return false;
3382        };
3383        let Some(dot) = mant.find('.') else {
3384            return false;
3385        };
3386        let rd_idx = dot + 1 + prec;
3387        let bytes = mant.as_bytes();
3388        rd_idx < bytes.len()
3389            && bytes[rd_idx] == b'5'
3390            && mant[rd_idx + 1..].bytes().all(|b| b == b'0')
3391    }
3392    looks_like_tie(mag, prec, prec + 18)
3393        && looks_like_tie(mag, prec, prec + MAX_F64_FRACTIONAL_DIGITS)
3394}
3395
3396/// Format a finite float in fixed notation with `prec` fractional digits,
3397/// rounding exact binary half-ties AWAY FROM ZERO (matching C SQLite's
3398/// `printf`/`round`) instead of Rust's round-half-to-even. Non-tie values are
3399/// left to `format!`, which already rounds them exactly as SQLite does; only a
3400/// confirmed exact tie is adjusted. A leading `-` is preserved for negatives.
3401fn format_fixed_round_half_away(val: f64, prec: usize) -> String {
3402    let base = format!("{val:.prec$}");
3403    let mag = val.abs();
3404    if !is_exact_decimal_tie(mag, prec) {
3405        return base;
3406    }
3407    // Exact tie: round the magnitude up (away from zero) by incrementing the
3408    // truncated digit string, then reattach the sign.
3409    let src = format!("{mag:.p$}", p = prec + 2);
3410    let dot = src.find('.').unwrap_or(src.len());
3411    let rd_idx = dot + 1 + prec;
3412    let mut digits = src.as_bytes()[..rd_idx].to_vec();
3413    if digits.last() == Some(&b'.') {
3414        digits.pop();
3415    }
3416    increment_decimal_digits(&mut digits);
3417    let Ok(body) = String::from_utf8(digits) else {
3418        return base;
3419    };
3420    if val.is_sign_negative() {
3421        format!("-{body}")
3422    } else {
3423        body
3424    }
3425}
3426
3427/// Format a finite float in `%e`/`%E` scientific notation with `prec`
3428/// fractional mantissa digits, rounding an exact mantissa half-tie AWAY FROM
3429/// ZERO. A carry out of `[1, 10)` renormalizes the mantissa (e.g. `9.5` at
3430/// precision 0 → `1e+01`) and bumps the exponent. Returns an un-normalized
3431/// `d.ddde{exp}` string (sign included, exponent not zero-padded) that mirrors
3432/// Rust's `{:e}`/`{:E}` output, so callers post-process it with
3433/// `normalize_exponent` exactly as before.
3434fn format_sci_round_half_away(val: f64, prec: usize, upper: bool) -> String {
3435    let base = if upper {
3436        format!("{val:.prec$E}")
3437    } else {
3438        format!("{val:.prec$e}")
3439    };
3440    let mag = val.abs();
3441    if mag == 0.0 || !is_exact_sci_tie(mag, prec) {
3442        return base;
3443    }
3444    let e_char = if upper { 'E' } else { 'e' };
3445    let src = format!("{mag:.p$e}", p = prec + 2);
3446    let Some((mant, exp_str)) = src.split_once('e') else {
3447        return base;
3448    };
3449    let mut exp: i64 = exp_str.parse().unwrap_or(0);
3450    let dot = mant.find('.').unwrap_or(mant.len());
3451    let rd_idx = dot + 1 + prec;
3452    let mut digits = mant.as_bytes()[..rd_idx].to_vec();
3453    if digits.last() == Some(&b'.') {
3454        digits.pop();
3455    }
3456    increment_decimal_digits(&mut digits);
3457    let Ok(mut mantissa) = String::from_utf8(digits) else {
3458        return base;
3459    };
3460    // A carry out of the `[1, 10)` mantissa produces a two-digit integer part
3461    // ("10" or "10.0…0"); renormalize to "1.0…0" and bump the exponent.
3462    let int_len = mantissa.find('.').unwrap_or(mantissa.len());
3463    if int_len == 2 {
3464        mantissa = if prec > 0 {
3465            format!("1.{}", "0".repeat(prec))
3466        } else {
3467            "1".to_owned()
3468        };
3469        exp += 1;
3470    }
3471    let sign = if val.is_sign_negative() { "-" } else { "" };
3472    format!("{sign}{mantissa}{e_char}{exp}")
3473}
3474
3475/// Format a float using `%g`/`%G` semantics.
3476fn format_float_g(val: f64, sig: usize, upper: bool, alt_form: bool, max_sig: usize) -> String {
3477    if !val.is_finite() {
3478        return format!("{val}");
3479    }
3480    // C SQLite canonicalizes signed zero for %g: both +0.0 and -0.0 render as
3481    // "0" (no minus sign). `-0.0 == 0.0` is true, so this maps -0.0 to +0.0.
3482    let val = if val == 0.0 { 0.0 } else { val };
3483    // The fixed-vs-exponential CHOICE uses the requested `sig`, but the actual
3484    // digit count never exceeds `max_sig` — C SQLite renders from the shortest
3485    // round-trip decimal (~16-17 figs) and pads/omits beyond it (bd-o8m86).
3486    // Rounding to `sig_digits` (not truncating) reproduces the dtoa digits.
3487    let sig_digits = sig.min(max_sig).max(1);
3488    // Round to `sig_digits` significant digits half-away, then read the resulting
3489    // exponent. The rounding may carry across a power of ten (e.g. `9.5` at 1
3490    // significant digit → `1e1`), and that rounded exponent — not the raw one —
3491    // selects fixed vs. exponential form below.
3492    let sci = format_sci_round_half_away(val, sig_digits.saturating_sub(1), false);
3493    let exp: i32 = sci
3494        .rsplit_once('e')
3495        .and_then(|(_, e)| e.parse().ok())
3496        .unwrap_or(0);
3497    #[allow(clippy::cast_possible_wrap)]
3498    let formatted = if exp < -4 || exp >= sig as i32 {
3499        let s = if upper { sci.replace('e', "E") } else { sci };
3500        // Alternate form (`#`) keeps every significant digit (no trailing-zero
3501        // strip) and forces a decimal point in the mantissa; otherwise strip
3502        // trailing zeros. The exponent is normalized afterwards.
3503        let trimmed = if alt_form {
3504            if let Some(e_pos) = s.find(['e', 'E']) {
3505                let (mantissa, exp_part) = s.split_at(e_pos);
3506                if mantissa.contains('.') {
3507                    s.clone()
3508                } else {
3509                    format!("{mantissa}.{exp_part}")
3510                }
3511            } else if s.contains('.') {
3512                s.clone()
3513            } else {
3514                format!("{s}.")
3515            }
3516        } else if s.contains('.') {
3517            if let Some(e_pos) = s.find('e').or_else(|| s.find('E')) {
3518                let mantissa = s[..e_pos].trim_end_matches('0').trim_end_matches('.');
3519                format!("{mantissa}{}", &s[e_pos..])
3520            } else {
3521                s.trim_end_matches('0').trim_end_matches('.').to_owned()
3522            }
3523        } else {
3524            s
3525        };
3526        normalize_exponent(&trimmed)
3527    } else {
3528        let decimal_places = if exp >= 0 {
3529            sig_digits.saturating_sub((exp + 1) as usize)
3530        } else {
3531            sig_digits + exp.unsigned_abs() as usize - 1
3532        };
3533        let s = format_fixed_round_half_away(val, decimal_places);
3534        // Alternate form (`#`) keeps all digits and forces a decimal point.
3535        if alt_form {
3536            if s.contains('.') { s } else { format!("{s}.") }
3537        }
3538        // Only strip trailing zeros when there is a fractional part. When
3539        // decimal_places == 0 (e.g. `%g` of 100000.0 -> exp 5, sig 6), `s` is
3540        // "100000" with no '.', and an unconditional trim would strip the
3541        // significant integer zeros down to "1". Mirror the exponential branch's
3542        // `if s.contains('.')` guard above. (C/SQLite %g never drops integer digits.)
3543        else if s.contains('.') {
3544            s.trim_end_matches('0').trim_end_matches('.').to_owned()
3545        } else {
3546            s
3547        }
3548    };
3549    formatted
3550}
3551
3552/// C SQLite's printf renders floats via a dtoa that emits at most this many
3553/// significant digits of the true value, then pads with zeros (%f/%e) or omits
3554/// (%g). Matching it caps frank's digit count at the same limit (bd-o8m86).
3555const FLOAT_SIG_DIGITS: usize = 16;
3556
3557/// Round a non-negative positional decimal string to `max_sig` significant
3558/// figures (half away from zero, with carry), zeroing digit positions beyond the
3559/// cut while preserving the decimal-place count. Reproduces C SQLite's dtoa cap
3560/// for `%f` (bd-o8m86): `%.20f` 0.1 -> "0.1" + zeros, `%.2f` 123456789012345.67
3561/// -> "...45.70", `%f` 6.022e23 caps the integer digits. Input has no sign.
3562fn round_positional_to_sig(s: &str, max_sig: usize) -> String {
3563    let bytes = s.as_bytes();
3564    let mut sig = 0usize;
3565    let mut started = false;
3566    let mut cut = None;
3567    for (i, &b) in bytes.iter().enumerate() {
3568        if b.is_ascii_digit() && (b != b'0' || started) {
3569            started = true;
3570            sig += 1;
3571            if sig == max_sig {
3572                cut = Some(i);
3573                break;
3574            }
3575        }
3576    }
3577    let Some(cut) = cut else { return s.to_owned() };
3578    // Nothing significant to drop after the cut.
3579    if bytes[cut + 1..].iter().all(|b| !b.is_ascii_digit()) {
3580        return s.to_owned();
3581    }
3582    let round_up = bytes[cut + 1..]
3583        .iter()
3584        .find(|b| b.is_ascii_digit())
3585        .is_some_and(|&b| b >= b'5');
3586    let mut kept: Vec<u8> = bytes[..=cut].to_vec();
3587    if round_up {
3588        increment_decimal_digits(&mut kept);
3589    }
3590    let mut tail = String::new();
3591    for &b in &bytes[cut + 1..] {
3592        tail.push(if b == b'.' { '.' } else { '0' });
3593    }
3594    format!("{}{tail}", String::from_utf8_lossy(&kept))
3595}
3596
3597#[cfg(test)]
3598#[allow(clippy::too_many_lines)]
3599mod tests {
3600    use super::*;
3601
3602    fn invoke1(f: &dyn ScalarFunction, v: SqliteValue) -> Result<SqliteValue> {
3603        f.invoke(&[v])
3604    }
3605
3606    fn invoke2(f: &dyn ScalarFunction, a: SqliteValue, b: SqliteValue) -> Result<SqliteValue> {
3607        f.invoke(&[a, b])
3608    }
3609
3610    fn assert_wrong_arg_count(registry: &FunctionRegistry, name: &str, arity: i32) {
3611        let function = registry
3612            .find_scalar(name, arity)
3613            .expect("known scalar name with bad arity returns erroring scalar");
3614        let args = vec![SqliteValue::Null; arity.max(0) as usize];
3615        let err = function
3616            .invoke(&args)
3617            .expect_err("wrong arity should return function error");
3618        let expected = format!("wrong number of arguments to function {name}()");
3619        assert!(
3620            matches!(&err, FrankenError::FunctionError(message) if message == &expected),
3621            "expected {expected:?}, got {err:?}"
3622        );
3623    }
3624
3625    #[test]
3626    fn test_get_change_tracking_state_returns_thread_local_snapshot() {
3627        let original = get_change_tracking_state();
3628        let expected = ChangeTrackingState {
3629            last_insert_rowid: 17,
3630            last_changes: 23,
3631            total_changes: 42,
3632        };
3633
3634        set_change_tracking_state(expected);
3635        assert_eq!(get_change_tracking_state(), expected);
3636
3637        set_change_tracking_state(original);
3638    }
3639
3640    // ── abs ──────────────────────────────────────────────────────────────
3641
3642    #[test]
3643    fn test_abs_positive() {
3644        assert_eq!(
3645            invoke1(&AbsFunc, SqliteValue::Integer(42)).unwrap(),
3646            SqliteValue::Integer(42)
3647        );
3648    }
3649
3650    #[test]
3651    fn test_abs_negative() {
3652        assert_eq!(
3653            invoke1(&AbsFunc, SqliteValue::Integer(-42)).unwrap(),
3654            SqliteValue::Integer(42)
3655        );
3656    }
3657
3658    #[test]
3659    fn test_abs_null() {
3660        assert_eq!(
3661            invoke1(&AbsFunc, SqliteValue::Null).unwrap(),
3662            SqliteValue::Null
3663        );
3664    }
3665
3666    #[test]
3667    fn test_abs_min_i64_overflow() {
3668        let err = invoke1(&AbsFunc, SqliteValue::Integer(i64::MIN)).unwrap_err();
3669        assert!(matches!(err, FrankenError::IntegerOverflow));
3670    }
3671
3672    #[test]
3673    fn test_abs_string_coercion() {
3674        assert_eq!(
3675            invoke1(&AbsFunc, SqliteValue::Text(SmallText::from_string("-7.5"))).unwrap(),
3676            SqliteValue::Float(7.5)
3677        );
3678    }
3679
3680    #[test]
3681    fn test_abs_whitespace_padded_text() {
3682        // SQLite's abs() casts non-integers to REAL, even if they parse cleanly as integers
3683        assert_eq!(
3684            invoke1(
3685                &AbsFunc,
3686                SqliteValue::Text(SmallText::from_string("  42  "))
3687            )
3688            .unwrap(),
3689            SqliteValue::Float(42.0)
3690        );
3691        assert_eq!(
3692            invoke1(
3693                &AbsFunc,
3694                SqliteValue::Text(SmallText::from_string("  -7.5  "))
3695            )
3696            .unwrap(),
3697            SqliteValue::Float(7.5)
3698        );
3699        assert_eq!(
3700            invoke1(&AbsFunc, SqliteValue::Text(SmallText::from_string("abc"))).unwrap(),
3701            SqliteValue::Float(0.0)
3702        );
3703    }
3704
3705    #[test]
3706    #[allow(clippy::approx_constant)]
3707    fn test_abs_float() {
3708        assert_eq!(
3709            invoke1(&AbsFunc, SqliteValue::Float(-3.14)).unwrap(),
3710            SqliteValue::Float(3.14)
3711        );
3712    }
3713
3714    // ── char ─────────────────────────────────────────────────────────────
3715
3716    #[test]
3717    fn test_char_basic() {
3718        let f = CharFunc;
3719        let result = f
3720            .invoke(&[
3721                SqliteValue::Integer(72),
3722                SqliteValue::Integer(101),
3723                SqliteValue::Integer(108),
3724                SqliteValue::Integer(108),
3725                SqliteValue::Integer(111),
3726            ])
3727            .unwrap();
3728        assert_eq!(result, SqliteValue::Text(SmallText::from_string("Hello")));
3729    }
3730
3731    #[test]
3732    fn test_char_null_skipped() {
3733        let f = CharFunc;
3734        // C SQLite: NULL → sqlite3_value_int()=0 → U+0000 (NUL byte).
3735        let result = f
3736            .invoke(&[
3737                SqliteValue::Integer(65),
3738                SqliteValue::Null,
3739                SqliteValue::Integer(66),
3740            ])
3741            .unwrap();
3742        assert_eq!(result, SqliteValue::Text(SmallText::from_string("A\0B")));
3743    }
3744
3745    #[test]
3746    fn test_char_invalid_scalar_values_use_replacement_character() {
3747        let f = CharFunc;
3748        let result = f
3749            .invoke(&[
3750                SqliteValue::Integer(-1),
3751                SqliteValue::Integer(65),
3752                SqliteValue::Integer(1_114_112),
3753            ])
3754            .unwrap();
3755        assert_eq!(
3756            result,
3757            SqliteValue::Text(SmallText::from_string("\u{fffd}A\u{fffd}"))
3758        );
3759    }
3760
3761    // ── coalesce ─────────────────────────────────────────────────────────
3762
3763    #[test]
3764    fn test_coalesce_first_non_null() {
3765        let f = CoalesceFunc;
3766        let result = f
3767            .invoke(&[
3768                SqliteValue::Null,
3769                SqliteValue::Null,
3770                SqliteValue::Integer(3),
3771                SqliteValue::Integer(4),
3772            ])
3773            .unwrap();
3774        assert_eq!(result, SqliteValue::Integer(3));
3775    }
3776
3777    // ── concat ───────────────────────────────────────────────────────────
3778
3779    #[test]
3780    fn test_concat_null_as_empty() {
3781        let f = ConcatFunc;
3782        let result = f
3783            .invoke(&[
3784                SqliteValue::Null,
3785                SqliteValue::Text(SmallText::from_string("hello")),
3786                SqliteValue::Null,
3787            ])
3788            .unwrap();
3789        assert_eq!(result, SqliteValue::Text(SmallText::from_string("hello")));
3790    }
3791
3792    #[test]
3793    #[ignore = "perf-only benchmark"]
3794    fn perf_concat_text_args() {
3795        use std::hint::black_box;
3796        use std::time::Instant;
3797
3798        const TEXT_ARGS: usize = 24;
3799        const INVOCATIONS: usize = 50_000;
3800        const REPEATS: usize = 5;
3801
3802        let f = ConcatFunc;
3803        let mut args = Vec::with_capacity(TEXT_ARGS);
3804        for _ in 0..TEXT_ARGS {
3805            args.push(SqliteValue::Text(SmallText::from_string("payload")));
3806        }
3807
3808        let mut best_ns = u128::MAX;
3809        let mut result_len = 0usize;
3810        for _ in 0..REPEATS {
3811            let started = Instant::now();
3812            for _ in 0..INVOCATIONS {
3813                let result = black_box(
3814                    f.invoke(black_box(args.as_slice()))
3815                        .expect("concat benchmark invocation must succeed"),
3816                );
3817                result_len = match result {
3818                    SqliteValue::Text(text) => text.len(),
3819                    SqliteValue::Null
3820                    | SqliteValue::Integer(_)
3821                    | SqliteValue::Float(_)
3822                    | SqliteValue::Blob(_) => 0,
3823                };
3824            }
3825            best_ns = best_ns.min(started.elapsed().as_nanos());
3826        }
3827
3828        println!(
3829            "concat_text_args text_args={TEXT_ARGS} invocations={INVOCATIONS} repeats={REPEATS} best_ns={best_ns} result_len={result_len}"
3830        );
3831    }
3832
3833    // ── concat_ws ────────────────────────────────────────────────────────
3834
3835    #[test]
3836    fn test_concat_ws_null_skipped() {
3837        let f = ConcatWsFunc;
3838        let result = f
3839            .invoke(&[
3840                SqliteValue::Text(SmallText::from_string(",")),
3841                SqliteValue::Text(SmallText::from_string("a")),
3842                SqliteValue::Null,
3843                SqliteValue::Text(SmallText::from_string("b")),
3844            ])
3845            .unwrap();
3846        assert_eq!(result, SqliteValue::Text(SmallText::from_string("a,b")));
3847    }
3848
3849    #[test]
3850    fn test_concat_ws_empty_string_is_not_skipped() {
3851        let f = ConcatWsFunc;
3852        let result = f
3853            .invoke(&[
3854                SqliteValue::Text(SmallText::from_string("|")),
3855                SqliteValue::Text(SmallText::new("")),
3856                SqliteValue::Text(SmallText::from_string("x")),
3857            ])
3858            .unwrap();
3859        assert_eq!(result, SqliteValue::Text(SmallText::from_string("|x")));
3860    }
3861
3862    #[test]
3863    #[ignore = "perf-only benchmark"]
3864    fn perf_concat_ws_text_args() {
3865        use std::hint::black_box;
3866        use std::time::Instant;
3867
3868        const TEXT_ARGS: usize = 24;
3869        const INVOCATIONS: usize = 50_000;
3870        const REPEATS: usize = 5;
3871
3872        let f = ConcatWsFunc;
3873        let mut args = Vec::with_capacity(TEXT_ARGS + 1);
3874        args.push(SqliteValue::Text(SmallText::from_string(",")));
3875        for _ in 0..TEXT_ARGS {
3876            args.push(SqliteValue::Text(SmallText::from_string("payload")));
3877        }
3878
3879        let mut best_ns = u128::MAX;
3880        let mut result_len = 0usize;
3881        for _ in 0..REPEATS {
3882            let started = Instant::now();
3883            for _ in 0..INVOCATIONS {
3884                let result = black_box(
3885                    f.invoke(black_box(args.as_slice()))
3886                        .expect("concat_ws benchmark invocation must succeed"),
3887                );
3888                result_len = match result {
3889                    SqliteValue::Text(text) => text.len(),
3890                    SqliteValue::Null
3891                    | SqliteValue::Integer(_)
3892                    | SqliteValue::Float(_)
3893                    | SqliteValue::Blob(_) => 0,
3894                };
3895            }
3896            best_ns = best_ns.min(started.elapsed().as_nanos());
3897        }
3898
3899        println!(
3900            "concat_ws_text_args text_args={TEXT_ARGS} invocations={INVOCATIONS} repeats={REPEATS} best_ns={best_ns} result_len={result_len}"
3901        );
3902    }
3903
3904    // ── hex ──────────────────────────────────────────────────────────────
3905
3906    #[test]
3907    fn test_hex_blob() {
3908        let result = invoke1(
3909            &HexFunc,
3910            SqliteValue::Blob(Arc::from([0xDE, 0xAD, 0xBE, 0xEF].as_slice())),
3911        )
3912        .unwrap();
3913        assert_eq!(
3914            result,
3915            SqliteValue::Text(SmallText::from_string("DEADBEEF"))
3916        );
3917    }
3918
3919    #[test]
3920    fn test_hex_number_via_text() {
3921        // hex(42) encodes '42' as UTF-8 hex, not raw bits
3922        let result = invoke1(&HexFunc, SqliteValue::Integer(42)).unwrap();
3923        assert_eq!(result, SqliteValue::Text(SmallText::from_string("3432")));
3924    }
3925
3926    #[test]
3927    #[ignore = "perf-only benchmark"]
3928    fn perf_hex_text_blob_args() {
3929        use std::hint::black_box;
3930        use std::time::Instant;
3931
3932        const BYTES: usize = 24;
3933        const INVOCATIONS: usize = 100_000;
3934        const REPEATS: usize = 5;
3935
3936        let f = HexFunc;
3937        let text_args = [SqliteValue::Text(SmallText::from_string(
3938            "payload payload sentinel",
3939        ))];
3940        let blob_args = [SqliteValue::Blob(Arc::from([0xAB; BYTES].as_slice()))];
3941
3942        let mut text_best_ns = u128::MAX;
3943        let mut blob_best_ns = u128::MAX;
3944        let mut text_result_len = 0usize;
3945        let mut blob_result_len = 0usize;
3946        for _ in 0..REPEATS {
3947            let started = Instant::now();
3948            for _ in 0..INVOCATIONS {
3949                let result = black_box(
3950                    f.invoke(black_box(text_args.as_slice()))
3951                        .expect("hex text benchmark invocation must succeed"),
3952                );
3953                text_result_len = match result {
3954                    SqliteValue::Text(text) => text.len(),
3955                    SqliteValue::Null
3956                    | SqliteValue::Integer(_)
3957                    | SqliteValue::Float(_)
3958                    | SqliteValue::Blob(_) => 0,
3959                };
3960            }
3961            text_best_ns = text_best_ns.min(started.elapsed().as_nanos());
3962
3963            let started = Instant::now();
3964            for _ in 0..INVOCATIONS {
3965                let result = black_box(
3966                    f.invoke(black_box(blob_args.as_slice()))
3967                        .expect("hex blob benchmark invocation must succeed"),
3968                );
3969                blob_result_len = match result {
3970                    SqliteValue::Text(text) => text.len(),
3971                    SqliteValue::Null
3972                    | SqliteValue::Integer(_)
3973                    | SqliteValue::Float(_)
3974                    | SqliteValue::Blob(_) => 0,
3975                };
3976            }
3977            blob_best_ns = blob_best_ns.min(started.elapsed().as_nanos());
3978        }
3979
3980        println!(
3981            "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}"
3982        );
3983    }
3984
3985    // ── iif ──────────────────────────────────────────────────────────────
3986
3987    #[test]
3988    fn test_iif_true() {
3989        let f = IifFunc;
3990        let result = f
3991            .invoke(&[
3992                SqliteValue::Integer(1),
3993                SqliteValue::Text(SmallText::from_string("yes")),
3994                SqliteValue::Text(SmallText::from_string("no")),
3995            ])
3996            .unwrap();
3997        assert_eq!(result, SqliteValue::Text(SmallText::from_string("yes")));
3998    }
3999
4000    #[test]
4001    fn test_iif_false() {
4002        let f = IifFunc;
4003        let result = f
4004            .invoke(&[
4005                SqliteValue::Integer(0),
4006                SqliteValue::Text(SmallText::from_string("yes")),
4007                SqliteValue::Text(SmallText::from_string("no")),
4008            ])
4009            .unwrap();
4010        assert_eq!(result, SqliteValue::Text(SmallText::from_string("no")));
4011    }
4012
4013    #[test]
4014    fn test_iif_whitespace_padded_text_truthy() {
4015        // Regression: IIF('  5  ', 'yes', 'no') must return 'yes'
4016        // because SQLite trims text before numeric coercion.
4017        let f = IifFunc;
4018        let result = f
4019            .invoke(&[
4020                SqliteValue::Text(SmallText::from_string("  5  ")),
4021                SqliteValue::Text(SmallText::from_string("yes")),
4022                SqliteValue::Text(SmallText::from_string("no")),
4023            ])
4024            .unwrap();
4025        assert_eq!(result, SqliteValue::Text(SmallText::from_string("yes")));
4026    }
4027
4028    // ── ifnull ───────────────────────────────────────────────────────────
4029
4030    #[test]
4031    fn test_ifnull_non_null() {
4032        assert_eq!(
4033            invoke2(
4034                &IfnullFunc,
4035                SqliteValue::Integer(5),
4036                SqliteValue::Integer(10)
4037            )
4038            .unwrap(),
4039            SqliteValue::Integer(5)
4040        );
4041    }
4042
4043    #[test]
4044    fn test_ifnull_null() {
4045        assert_eq!(
4046            invoke2(&IfnullFunc, SqliteValue::Null, SqliteValue::Integer(10)).unwrap(),
4047            SqliteValue::Integer(10)
4048        );
4049    }
4050
4051    // ── instr ────────────────────────────────────────────────────────────
4052
4053    #[test]
4054    fn test_instr_found() {
4055        assert_eq!(
4056            invoke2(
4057                &InstrFunc,
4058                SqliteValue::Text(SmallText::from_string("hello world")),
4059                SqliteValue::Text(SmallText::from_string("world"))
4060            )
4061            .unwrap(),
4062            SqliteValue::Integer(7)
4063        );
4064    }
4065
4066    #[test]
4067    fn test_instr_not_found() {
4068        assert_eq!(
4069            invoke2(
4070                &InstrFunc,
4071                SqliteValue::Text(SmallText::from_string("hello")),
4072                SqliteValue::Text(SmallText::from_string("xyz"))
4073            )
4074            .unwrap(),
4075            SqliteValue::Integer(0)
4076        );
4077    }
4078
4079    #[test]
4080    fn test_instr_empty_needle_returns_one() {
4081        // SQLite: instr(X, '') returns 1 (empty string found at position 1).
4082        assert_eq!(
4083            invoke2(
4084                &InstrFunc,
4085                SqliteValue::Text(SmallText::from_string("hello")),
4086                SqliteValue::Text(SmallText::new(""))
4087            )
4088            .unwrap(),
4089            SqliteValue::Integer(1)
4090        );
4091    }
4092
4093    #[test]
4094    fn test_instr_empty_haystack_returns_zero() {
4095        assert_eq!(
4096            invoke2(
4097                &InstrFunc,
4098                SqliteValue::Text(SmallText::new("")),
4099                SqliteValue::Text(SmallText::from_string("x"))
4100            )
4101            .unwrap(),
4102            SqliteValue::Integer(0)
4103        );
4104    }
4105
4106    #[test]
4107    fn test_instr_blob_empty_needle_returns_one() {
4108        // SQLite: instr(X, x'') returns 1 (empty blob found at position 1).
4109        assert_eq!(
4110            invoke2(
4111                &InstrFunc,
4112                SqliteValue::Blob(Arc::from([1, 2, 3].as_slice())),
4113                SqliteValue::Blob(Arc::from([].as_slice()))
4114            )
4115            .unwrap(),
4116            SqliteValue::Integer(1)
4117        );
4118    }
4119
4120    #[test]
4121    #[ignore = "perf-only benchmark"]
4122    fn perf_instr_text_args() {
4123        use std::hint::black_box;
4124        use std::time::Instant;
4125
4126        const INVOCATIONS: usize = 100_000;
4127        const REPEATS: usize = 5;
4128
4129        let f = InstrFunc;
4130        let args = [
4131            SqliteValue::Text(SmallText::from_string("payload payload sentinel")),
4132            SqliteValue::Text(SmallText::from_string("sentinel")),
4133        ];
4134
4135        let mut best_ns = u128::MAX;
4136        let mut result_value = 0i64;
4137        for _ in 0..REPEATS {
4138            let started = Instant::now();
4139            for _ in 0..INVOCATIONS {
4140                let result = black_box(
4141                    f.invoke(black_box(args.as_slice()))
4142                        .expect("instr benchmark invocation must succeed"),
4143                );
4144                result_value = match result {
4145                    SqliteValue::Integer(value) => value,
4146                    SqliteValue::Null
4147                    | SqliteValue::Float(_)
4148                    | SqliteValue::Text(_)
4149                    | SqliteValue::Blob(_) => 0,
4150                };
4151            }
4152            best_ns = best_ns.min(started.elapsed().as_nanos());
4153        }
4154
4155        println!(
4156            "instr_text_args invocations={INVOCATIONS} repeats={REPEATS} best_ns={best_ns} result_value={result_value}"
4157        );
4158    }
4159
4160    // ── length ───────────────────────────────────────────────────────────
4161
4162    #[test]
4163    fn test_length_text_chars() {
4164        // café is 4 characters, 5 bytes
4165        assert_eq!(
4166            invoke1(
4167                &LengthFunc,
4168                SqliteValue::Text(SmallText::from_string("café"))
4169            )
4170            .unwrap(),
4171            SqliteValue::Integer(4)
4172        );
4173    }
4174
4175    #[test]
4176    fn test_length_text_stops_at_nul() {
4177        assert_eq!(
4178            invoke1(
4179                &LengthFunc,
4180                SqliteValue::Text(SmallText::from_string("A\0B"))
4181            )
4182            .unwrap(),
4183            SqliteValue::Integer(1)
4184        );
4185        assert_eq!(
4186            invoke1(
4187                &LengthFunc,
4188                SqliteValue::Text(SmallText::from_string("\0A"))
4189            )
4190            .unwrap(),
4191            SqliteValue::Integer(0)
4192        );
4193    }
4194
4195    #[test]
4196    fn test_length_blob_bytes() {
4197        assert_eq!(
4198            invoke1(&LengthFunc, SqliteValue::Blob(Arc::from([1, 2].as_slice()))).unwrap(),
4199            SqliteValue::Integer(2)
4200        );
4201    }
4202
4203    // ── octet_length ─────────────────────────────────────────────────────
4204
4205    #[test]
4206    fn test_octet_length_multibyte() {
4207        // café: 'c'=1, 'a'=1, 'f'=1, 'é'=2 bytes = 5 bytes total
4208        assert_eq!(
4209            invoke1(
4210                &OctetLengthFunc,
4211                SqliteValue::Text(SmallText::from_string("café"))
4212            )
4213            .unwrap(),
4214            SqliteValue::Integer(5)
4215        );
4216    }
4217
4218    #[test]
4219    fn test_octet_length_honors_statement_text_encoding() {
4220        // Default (UTF-8): byte length is the string's own byte length.
4221        set_statement_text_encoding(TextEncoding::Utf8);
4222        assert_eq!(
4223            invoke1(
4224                &OctetLengthFunc,
4225                SqliteValue::Text(SmallText::from_string("abc"))
4226            )
4227            .unwrap(),
4228            SqliteValue::Integer(3)
4229        );
4230        assert_eq!(
4231            invoke1(&OctetLengthFunc, SqliteValue::Integer(12345)).unwrap(),
4232            SqliteValue::Integer(5)
4233        );
4234
4235        // UTF-16le: two bytes per code unit for TEXT and rendered numerics.
4236        set_statement_text_encoding(TextEncoding::Utf16le);
4237        assert_eq!(statement_text_encoding(), TextEncoding::Utf16le);
4238        assert_eq!(
4239            invoke1(
4240                &OctetLengthFunc,
4241                SqliteValue::Text(SmallText::from_string("abc"))
4242            )
4243            .unwrap(),
4244            SqliteValue::Integer(6)
4245        );
4246        assert_eq!(
4247            invoke1(&OctetLengthFunc, SqliteValue::Integer(12345)).unwrap(),
4248            SqliteValue::Integer(10)
4249        );
4250        // A non-BMP scalar is a surrogate pair = two code units = four bytes.
4251        assert_eq!(
4252            invoke1(
4253                &OctetLengthFunc,
4254                SqliteValue::Text(SmallText::from_string("\u{1F600}"))
4255            )
4256            .unwrap(),
4257            SqliteValue::Integer(4)
4258        );
4259
4260        // UTF-16be counts identically to UTF-16le.
4261        set_statement_text_encoding(TextEncoding::Utf16be);
4262        assert_eq!(
4263            invoke1(
4264                &OctetLengthFunc,
4265                SqliteValue::Text(SmallText::from_string("abc"))
4266            )
4267            .unwrap(),
4268            SqliteValue::Integer(6)
4269        );
4270
4271        // BLOB stays raw bytes regardless of the database encoding.
4272        assert_eq!(
4273            invoke1(&OctetLengthFunc, SqliteValue::Blob(vec![1, 2, 3].into())).unwrap(),
4274            SqliteValue::Integer(3)
4275        );
4276
4277        // Restore the default so other tests on this thread are unaffected.
4278        set_statement_text_encoding(TextEncoding::Utf8);
4279    }
4280
4281    // ── lower/upper ──────────────────────────────────────────────────────
4282
4283    #[test]
4284    fn test_lower_ascii() {
4285        assert_eq!(
4286            invoke1(
4287                &LowerFunc,
4288                SqliteValue::Text(SmallText::from_string("HELLO"))
4289            )
4290            .unwrap(),
4291            SqliteValue::Text(SmallText::from_string("hello"))
4292        );
4293    }
4294
4295    #[test]
4296    fn test_upper_ascii() {
4297        assert_eq!(
4298            invoke1(
4299                &UpperFunc,
4300                SqliteValue::Text(SmallText::from_string("hello"))
4301            )
4302            .unwrap(),
4303            SqliteValue::Text(SmallText::from_string("HELLO"))
4304        );
4305    }
4306
4307    // ── trim/ltrim/rtrim ─────────────────────────────────────────────────
4308
4309    #[test]
4310    fn test_trim_default() {
4311        let f = TrimFunc;
4312        assert_eq!(
4313            f.invoke(&[SqliteValue::Text(SmallText::from_string("  hello  "))])
4314                .unwrap(),
4315            SqliteValue::Text(SmallText::from_string("hello"))
4316        );
4317    }
4318
4319    #[test]
4320    fn test_ltrim_default() {
4321        let f = LtrimFunc;
4322        assert_eq!(
4323            f.invoke(&[SqliteValue::Text(SmallText::from_string("  hello"))])
4324                .unwrap(),
4325            SqliteValue::Text(SmallText::from_string("hello"))
4326        );
4327    }
4328
4329    #[test]
4330    fn test_ltrim_custom() {
4331        let f = LtrimFunc;
4332        assert_eq!(
4333            f.invoke(&[
4334                SqliteValue::Text(SmallText::from_string("xxhello")),
4335                SqliteValue::Text(SmallText::from_string("x")),
4336            ])
4337            .unwrap(),
4338            SqliteValue::Text(SmallText::from_string("hello"))
4339        );
4340    }
4341
4342    #[test]
4343    #[ignore = "perf-only benchmark"]
4344    fn perf_trim_text_args() {
4345        use std::hint::black_box;
4346        use std::time::Instant;
4347
4348        const INVOCATIONS: usize = 100_000;
4349        const REPEATS: usize = 5;
4350
4351        let trim = TrimFunc;
4352        let ltrim = LtrimFunc;
4353        let rtrim = RtrimFunc;
4354        let default_args = [SqliteValue::Text(SmallText::from_string("   payload   "))];
4355        let custom_args = [
4356            SqliteValue::Text(SmallText::from_string("xxxpayloadxxx")),
4357            SqliteValue::Text(SmallText::from_string("x")),
4358        ];
4359
4360        let mut trim_best_ns = u128::MAX;
4361        let mut ltrim_best_ns = u128::MAX;
4362        let mut rtrim_best_ns = u128::MAX;
4363        let mut custom_best_ns = u128::MAX;
4364        let mut result_len = 0usize;
4365
4366        for _ in 0..REPEATS {
4367            let started = Instant::now();
4368            for _ in 0..INVOCATIONS {
4369                let result = black_box(
4370                    trim.invoke(black_box(default_args.as_slice()))
4371                        .expect("trim benchmark invocation must succeed"),
4372                );
4373                result_len = match result {
4374                    SqliteValue::Text(text) => text.len(),
4375                    SqliteValue::Null
4376                    | SqliteValue::Integer(_)
4377                    | SqliteValue::Float(_)
4378                    | SqliteValue::Blob(_) => 0,
4379                };
4380            }
4381            trim_best_ns = trim_best_ns.min(started.elapsed().as_nanos());
4382
4383            let started = Instant::now();
4384            for _ in 0..INVOCATIONS {
4385                let result = black_box(
4386                    ltrim
4387                        .invoke(black_box(default_args.as_slice()))
4388                        .expect("ltrim benchmark invocation must succeed"),
4389                );
4390                result_len = match result {
4391                    SqliteValue::Text(text) => text.len(),
4392                    SqliteValue::Null
4393                    | SqliteValue::Integer(_)
4394                    | SqliteValue::Float(_)
4395                    | SqliteValue::Blob(_) => 0,
4396                };
4397            }
4398            ltrim_best_ns = ltrim_best_ns.min(started.elapsed().as_nanos());
4399
4400            let started = Instant::now();
4401            for _ in 0..INVOCATIONS {
4402                let result = black_box(
4403                    rtrim
4404                        .invoke(black_box(default_args.as_slice()))
4405                        .expect("rtrim benchmark invocation must succeed"),
4406                );
4407                result_len = match result {
4408                    SqliteValue::Text(text) => text.len(),
4409                    SqliteValue::Null
4410                    | SqliteValue::Integer(_)
4411                    | SqliteValue::Float(_)
4412                    | SqliteValue::Blob(_) => 0,
4413                };
4414            }
4415            rtrim_best_ns = rtrim_best_ns.min(started.elapsed().as_nanos());
4416
4417            let started = Instant::now();
4418            for _ in 0..INVOCATIONS {
4419                let result = black_box(
4420                    trim.invoke(black_box(custom_args.as_slice()))
4421                        .expect("custom trim benchmark invocation must succeed"),
4422                );
4423                result_len = match result {
4424                    SqliteValue::Text(text) => text.len(),
4425                    SqliteValue::Null
4426                    | SqliteValue::Integer(_)
4427                    | SqliteValue::Float(_)
4428                    | SqliteValue::Blob(_) => 0,
4429                };
4430            }
4431            custom_best_ns = custom_best_ns.min(started.elapsed().as_nanos());
4432        }
4433
4434        println!(
4435            "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}"
4436        );
4437    }
4438
4439    // ── nullif ───────────────────────────────────────────────────────────
4440
4441    #[test]
4442    fn test_nullif_equal() {
4443        assert_eq!(
4444            invoke2(
4445                &NullifFunc,
4446                SqliteValue::Integer(5),
4447                SqliteValue::Integer(5)
4448            )
4449            .unwrap(),
4450            SqliteValue::Null
4451        );
4452    }
4453
4454    #[test]
4455    fn test_nullif_different() {
4456        assert_eq!(
4457            invoke2(
4458                &NullifFunc,
4459                SqliteValue::Integer(5),
4460                SqliteValue::Integer(3)
4461            )
4462            .unwrap(),
4463            SqliteValue::Integer(5)
4464        );
4465    }
4466
4467    // ── typeof ───────────────────────────────────────────────────────────
4468
4469    #[test]
4470    fn test_typeof_each() {
4471        assert_eq!(
4472            invoke1(&TypeofFunc, SqliteValue::Null).unwrap(),
4473            SqliteValue::Text(SmallText::from_string("null"))
4474        );
4475        assert_eq!(
4476            invoke1(&TypeofFunc, SqliteValue::Integer(1)).unwrap(),
4477            SqliteValue::Text(SmallText::from_string("integer"))
4478        );
4479        assert_eq!(
4480            invoke1(&TypeofFunc, SqliteValue::Float(1.0)).unwrap(),
4481            SqliteValue::Text(SmallText::from_string("real"))
4482        );
4483        assert_eq!(
4484            invoke1(&TypeofFunc, SqliteValue::Text(SmallText::from_string("x"))).unwrap(),
4485            SqliteValue::Text(SmallText::from_string("text"))
4486        );
4487        assert_eq!(
4488            invoke1(&TypeofFunc, SqliteValue::Blob(Arc::from([0].as_slice()))).unwrap(),
4489            SqliteValue::Text(SmallText::from_string("blob"))
4490        );
4491    }
4492
4493    // ── subtype ──────────────────────────────────────────────────────────
4494
4495    #[test]
4496    fn test_subtype_null_returns_zero() {
4497        assert_eq!(
4498            invoke1(&SubtypeFunc, SqliteValue::Null).unwrap(),
4499            SqliteValue::Integer(0)
4500        );
4501    }
4502
4503    // ── replace ──────────────────────────────────────────────────────────
4504
4505    #[test]
4506    fn test_replace_basic() {
4507        let f = ReplaceFunc;
4508        assert_eq!(
4509            f.invoke(&[
4510                SqliteValue::Text(SmallText::from_string("hello world")),
4511                SqliteValue::Text(SmallText::from_string("world")),
4512                SqliteValue::Text(SmallText::from_string("earth")),
4513            ])
4514            .unwrap(),
4515            SqliteValue::Text(SmallText::from_string("hello earth"))
4516        );
4517    }
4518
4519    #[test]
4520    fn test_replace_empty_y() {
4521        let f = ReplaceFunc;
4522        assert_eq!(
4523            f.invoke(&[
4524                SqliteValue::Text(SmallText::from_string("hello")),
4525                SqliteValue::Text(SmallText::new("")),
4526                SqliteValue::Text(SmallText::from_string("x")),
4527            ])
4528            .unwrap(),
4529            SqliteValue::Text(SmallText::from_string("hello"))
4530        );
4531    }
4532
4533    #[test]
4534    #[ignore = "perf-only benchmark"]
4535    fn perf_replace_text_args() {
4536        use std::hint::black_box;
4537        use std::time::Instant;
4538
4539        const INVOCATIONS: usize = 100_000;
4540        const REPEATS: usize = 5;
4541
4542        let f = ReplaceFunc;
4543        let args = [
4544            SqliteValue::Text(SmallText::from_string("payload payload payload")),
4545            SqliteValue::Text(SmallText::from_string("zz")),
4546            SqliteValue::Text(SmallText::from_string("replacement")),
4547        ];
4548
4549        let mut best_ns = u128::MAX;
4550        let mut result_len = 0usize;
4551        for _ in 0..REPEATS {
4552            let started = Instant::now();
4553            for _ in 0..INVOCATIONS {
4554                let result = black_box(
4555                    f.invoke(black_box(args.as_slice()))
4556                        .expect("replace benchmark invocation must succeed"),
4557                );
4558                result_len = match result {
4559                    SqliteValue::Text(text) => text.len(),
4560                    SqliteValue::Null
4561                    | SqliteValue::Integer(_)
4562                    | SqliteValue::Float(_)
4563                    | SqliteValue::Blob(_) => 0,
4564                };
4565            }
4566            best_ns = best_ns.min(started.elapsed().as_nanos());
4567        }
4568
4569        println!(
4570            "replace_text_args invocations={INVOCATIONS} repeats={REPEATS} best_ns={best_ns} result_len={result_len}"
4571        );
4572    }
4573
4574    // ── round ────────────────────────────────────────────────────────────
4575
4576    #[test]
4577    #[allow(clippy::float_cmp)]
4578    fn test_round_half_away() {
4579        // round(2.5) = 3.0, round(-2.5) = -3.0
4580        assert_eq!(
4581            RoundFunc.invoke(&[SqliteValue::Float(2.5)]).unwrap(),
4582            SqliteValue::Float(3.0)
4583        );
4584        assert_eq!(
4585            RoundFunc.invoke(&[SqliteValue::Float(-2.5)]).unwrap(),
4586            SqliteValue::Float(-3.0)
4587        );
4588    }
4589
4590    #[test]
4591    #[allow(clippy::float_cmp, clippy::approx_constant)]
4592    fn test_round_precision() {
4593        assert_eq!(
4594            RoundFunc
4595                .invoke(&[SqliteValue::Float(3.14159), SqliteValue::Integer(2)])
4596                .unwrap(),
4597            SqliteValue::Float(3.14)
4598        );
4599    }
4600
4601    #[test]
4602    #[allow(clippy::float_cmp)]
4603    fn test_round_extreme_n_clamped() {
4604        // N > 30 is clamped to 30 (matches C SQLite)
4605        assert_eq!(
4606            RoundFunc
4607                .invoke(&[SqliteValue::Float(1.5), SqliteValue::Integer(400)])
4608                .unwrap(),
4609            RoundFunc
4610                .invoke(&[SqliteValue::Float(1.5), SqliteValue::Integer(30)])
4611                .unwrap(),
4612        );
4613        // Negative N is clamped to 0 (matches C SQLite)
4614        assert_eq!(
4615            RoundFunc
4616                .invoke(&[SqliteValue::Float(2.5), SqliteValue::Integer(-5)])
4617                .unwrap(),
4618            SqliteValue::Float(3.0)
4619        );
4620        // i64::MAX is clamped to 30
4621        let result = RoundFunc
4622            .invoke(&[SqliteValue::Float(1.5), SqliteValue::Integer(i64::MAX)])
4623            .unwrap();
4624        if let SqliteValue::Float(v) = result {
4625            assert!(!v.is_nan(), "round must never return NaN");
4626        }
4627    }
4628
4629    #[test]
4630    #[allow(clippy::float_cmp)]
4631    fn test_round_large_value_no_fractional() {
4632        // Values beyond 2^52 have no fractional part — returned unchanged
4633        let big = 9_007_199_254_740_993.0_f64;
4634        assert_eq!(
4635            RoundFunc.invoke(&[SqliteValue::Float(big)]).unwrap(),
4636            SqliteValue::Float(big)
4637        );
4638        assert_eq!(
4639            RoundFunc.invoke(&[SqliteValue::Float(-big)]).unwrap(),
4640            SqliteValue::Float(-big)
4641        );
4642    }
4643
4644    // ── sign ─────────────────────────────────────────────────────────────
4645
4646    #[test]
4647    fn test_sign_positive() {
4648        assert_eq!(
4649            invoke1(&SignFunc, SqliteValue::Integer(42)).unwrap(),
4650            SqliteValue::Integer(1)
4651        );
4652    }
4653
4654    #[test]
4655    fn test_sign_negative() {
4656        assert_eq!(
4657            invoke1(&SignFunc, SqliteValue::Integer(-42)).unwrap(),
4658            SqliteValue::Integer(-1)
4659        );
4660    }
4661
4662    #[test]
4663    fn test_sign_zero() {
4664        assert_eq!(
4665            invoke1(&SignFunc, SqliteValue::Integer(0)).unwrap(),
4666            SqliteValue::Integer(0)
4667        );
4668    }
4669
4670    #[test]
4671    fn test_sign_null() {
4672        assert_eq!(
4673            invoke1(&SignFunc, SqliteValue::Null).unwrap(),
4674            SqliteValue::Null
4675        );
4676    }
4677
4678    #[test]
4679    fn test_sign_non_numeric() {
4680        // C SQLite: math functions return NULL for strings that cannot be parsed as numeric.
4681        assert_eq!(
4682            invoke1(&SignFunc, SqliteValue::Text(SmallText::from_string("abc"))).unwrap(),
4683            SqliteValue::Null
4684        );
4685    }
4686
4687    #[test]
4688    fn test_sign_whitespace_padded_text() {
4689        // Regression: SIGN('  5  ') must return 1, not NULL.
4690        // SQLite trims ASCII whitespace before numeric parsing.
4691        assert_eq!(
4692            invoke1(
4693                &SignFunc,
4694                SqliteValue::Text(SmallText::from_string("  5  "))
4695            )
4696            .unwrap(),
4697            SqliteValue::Integer(1)
4698        );
4699        assert_eq!(
4700            invoke1(
4701                &SignFunc,
4702                SqliteValue::Text(SmallText::from_string("  -3.14  "))
4703            )
4704            .unwrap(),
4705            SqliteValue::Integer(-1)
4706        );
4707    }
4708
4709    #[test]
4710    fn test_sign_unicode_space_and_blob_return_null() {
4711        assert_eq!(
4712            invoke1(
4713                &SignFunc,
4714                SqliteValue::Text(SmallText::from_string("\u{00a0}123"))
4715            )
4716            .unwrap(),
4717            SqliteValue::Null
4718        );
4719        assert_eq!(
4720            invoke1(&SignFunc, SqliteValue::Blob(Arc::from(b"123".as_slice()))).unwrap(),
4721            SqliteValue::Null
4722        );
4723    }
4724
4725    #[test]
4726    fn test_sign_nan_inf_text_returns_null() {
4727        // C SQLite doesn't recognise "NaN", "inf", "Infinity" etc. as numeric —
4728        // sign() must return NULL for these, matching the C oracle.
4729        for s in &[
4730            "NaN",
4731            "nan",
4732            "inf",
4733            "-inf",
4734            "Infinity",
4735            "-Infinity",
4736            "INF",
4737            "+nan",
4738            "+inf",
4739        ] {
4740            assert_eq!(
4741                invoke1(&SignFunc, SqliteValue::Text(SmallText::from_string(*s))).unwrap(),
4742                SqliteValue::Null,
4743                "sign('{s}') should be NULL"
4744            );
4745        }
4746    }
4747
4748    #[test]
4749    fn test_sign_numeric_overflow_to_infinity() {
4750        // "1e999" overflows to +inf in both Rust and C. C SQLite's sqlite3AtoF
4751        // accepts it as numeric, so sign() must return 1 (not NULL).
4752        assert_eq!(
4753            invoke1(
4754                &SignFunc,
4755                SqliteValue::Text(SmallText::from_string("1e999"))
4756            )
4757            .unwrap(),
4758            SqliteValue::Integer(1)
4759        );
4760        assert_eq!(
4761            invoke1(
4762                &SignFunc,
4763                SqliteValue::Text(SmallText::from_string("-1e999"))
4764            )
4765            .unwrap(),
4766            SqliteValue::Integer(-1)
4767        );
4768        // Underflow to zero
4769        assert_eq!(
4770            invoke1(
4771                &SignFunc,
4772                SqliteValue::Text(SmallText::from_string("1e-999"))
4773            )
4774            .unwrap(),
4775            SqliteValue::Integer(0)
4776        );
4777    }
4778
4779    #[test]
4780    fn test_sign_float_nan_returns_null() {
4781        // C SQLite: sign(0.0/0.0) = NULL. Float NaN must not return 0.
4782        assert_eq!(
4783            invoke1(&SignFunc, SqliteValue::Float(f64::NAN)).unwrap(),
4784            SqliteValue::Null
4785        );
4786    }
4787
4788    // ── scalar max/min ───────────────────────────────────────────────────
4789
4790    #[test]
4791    fn test_scalar_max_null() {
4792        let f = ScalarMaxFunc;
4793        let result = f
4794            .invoke(&[
4795                SqliteValue::Integer(1),
4796                SqliteValue::Null,
4797                SqliteValue::Integer(3),
4798            ])
4799            .unwrap();
4800        assert_eq!(result, SqliteValue::Null);
4801    }
4802
4803    #[test]
4804    fn test_scalar_max_values() {
4805        let f = ScalarMaxFunc;
4806        let result = f
4807            .invoke(&[
4808                SqliteValue::Integer(3),
4809                SqliteValue::Integer(1),
4810                SqliteValue::Integer(2),
4811            ])
4812            .unwrap();
4813        assert_eq!(result, SqliteValue::Integer(3));
4814    }
4815
4816    #[test]
4817    fn test_scalar_min_null() {
4818        let f = ScalarMinFunc;
4819        let result = f
4820            .invoke(&[
4821                SqliteValue::Integer(1),
4822                SqliteValue::Null,
4823                SqliteValue::Integer(3),
4824            ])
4825            .unwrap();
4826        assert_eq!(result, SqliteValue::Null);
4827    }
4828
4829    #[test]
4830    fn test_scalar_min_selects_later_equal_value_while_max_keeps_first() {
4831        let min = ScalarMinFunc;
4832        let max = ScalarMaxFunc;
4833        let numeric = [SqliteValue::Integer(1), SqliteValue::Float(1.0)];
4834        assert!(matches!(
4835            min.invoke(&numeric).unwrap(),
4836            SqliteValue::Float(value) if value == 1.0
4837        ));
4838        assert_eq!(max.invoke(&numeric).unwrap(), SqliteValue::Integer(1));
4839
4840        let text = [
4841            SqliteValue::Text(SmallText::new("a")),
4842            SqliteValue::Text(SmallText::new("A")),
4843        ];
4844        let nocase = crate::collation::NoCaseCollation;
4845        assert_eq!(
4846            min.invoke_with_collation(&text, Some(&nocase)).unwrap(),
4847            SqliteValue::Text(SmallText::new("A"))
4848        );
4849        assert_eq!(
4850            max.invoke_with_collation(&text, Some(&nocase)).unwrap(),
4851            SqliteValue::Text(SmallText::new("a"))
4852        );
4853    }
4854
4855    // ── quote ────────────────────────────────────────────────────────────
4856
4857    #[test]
4858    fn test_quote_text() {
4859        assert_eq!(
4860            invoke1(
4861                &QuoteFunc,
4862                SqliteValue::Text(SmallText::from_string("it's"))
4863            )
4864            .unwrap(),
4865            SqliteValue::Text(SmallText::from_string("'it''s'"))
4866        );
4867    }
4868
4869    #[test]
4870    fn test_quote_null() {
4871        assert_eq!(
4872            invoke1(&QuoteFunc, SqliteValue::Null).unwrap(),
4873            SqliteValue::Text(SmallText::from_string("NULL"))
4874        );
4875    }
4876
4877    #[test]
4878    fn test_quote_blob() {
4879        assert_eq!(
4880            invoke1(&QuoteFunc, SqliteValue::Blob(Arc::from([0xAB].as_slice()))).unwrap(),
4881            SqliteValue::Text(SmallText::from_string("X'AB'"))
4882        );
4883    }
4884
4885    #[test]
4886    fn test_quote_text_truncates_at_first_nul() {
4887        assert_eq!(
4888            invoke1(
4889                &QuoteFunc,
4890                SqliteValue::Text(SmallText::from_string("A\0B"))
4891            )
4892            .unwrap(),
4893            SqliteValue::Text(SmallText::from_string("'A'"))
4894        );
4895    }
4896
4897    #[test]
4898    fn test_unistr_quote_plain_text_matches_quote() {
4899        assert_eq!(
4900            invoke1(
4901                &UnistrQuoteFunc,
4902                SqliteValue::Text(SmallText::from_string("it's"))
4903            )
4904            .unwrap(),
4905            SqliteValue::Text(SmallText::from_string("'it''s'"))
4906        );
4907    }
4908
4909    #[test]
4910    fn test_unistr_quote_escapes_control_chars_and_backslashes() {
4911        assert_eq!(
4912            invoke1(
4913                &UnistrQuoteFunc,
4914                SqliteValue::Text(SmallText::from_string("a\nb\\c\x01d"))
4915            )
4916            .unwrap(),
4917            SqliteValue::Text(SmallText::from_string("unistr('a\\u000ab\\\\c\\u0001d')"))
4918        );
4919    }
4920
4921    #[test]
4922    fn test_unistr_quote_truncates_at_first_nul_before_wrapping() {
4923        assert_eq!(
4924            invoke1(
4925                &UnistrQuoteFunc,
4926                SqliteValue::Text(SmallText::from_string("A\0\nB"))
4927            )
4928            .unwrap(),
4929            SqliteValue::Text(SmallText::from_string("'A'"))
4930        );
4931    }
4932
4933    #[test]
4934    fn test_unistr_decodes_backslash_and_unicode_escapes() {
4935        assert_eq!(
4936            invoke1(
4937                &UnistrFunc,
4938                SqliteValue::Text(SmallText::from_string(
4939                    "a\\\\b\\u0020\\U0001f600\\0041\\+000042"
4940                ))
4941            )
4942            .unwrap(),
4943            SqliteValue::Text(SmallText::from_string("a\\b \u{1f600}AB"))
4944        );
4945    }
4946
4947    #[test]
4948    fn test_unistr_invalid_escape_returns_error() {
4949        for input in [
4950            "\\u12xz",
4951            "\\12xz",
4952            "\\+00xz",
4953            "\\",
4954            "\\x",
4955            "\\U00110000",
4956            "\\D800",
4957        ] {
4958            let err = invoke1(
4959                &UnistrFunc,
4960                SqliteValue::Text(SmallText::from_string(input)),
4961            )
4962            .unwrap_err();
4963            assert_eq!(err.to_string(), INVALID_UNISTR_ESCAPE);
4964        }
4965    }
4966
4967    #[test]
4968    #[ignore = "perf-only benchmark"]
4969    fn perf_unistr_text_args() {
4970        use std::hint::black_box;
4971        use std::time::Instant;
4972
4973        const INVOCATIONS: usize = 500_000;
4974        const REPEATS: usize = 7;
4975
4976        let f = UnistrFunc;
4977        let plain_args = [SqliteValue::Text(SmallText::from_string(
4978            "plain unicode payload",
4979        ))];
4980        let escaped_args = [SqliteValue::Text(SmallText::from_string(
4981            "a\\\\b\\u0020\\u0048\\u0069\\U0001f600",
4982        ))];
4983
4984        let mut plain_best_ns = u128::MAX;
4985        let mut escaped_best_ns = u128::MAX;
4986        let mut checksum = 0usize;
4987        for _ in 0..REPEATS {
4988            let started = Instant::now();
4989            for _ in 0..INVOCATIONS {
4990                let result = black_box(
4991                    f.invoke(black_box(plain_args.as_slice()))
4992                        .expect("unistr plain benchmark invocation must succeed"),
4993                );
4994                if let SqliteValue::Text(text) = result {
4995                    checksum = checksum.wrapping_add(text.len());
4996                }
4997            }
4998            plain_best_ns = plain_best_ns.min(started.elapsed().as_nanos());
4999
5000            let started = Instant::now();
5001            for _ in 0..INVOCATIONS {
5002                let result = black_box(
5003                    f.invoke(black_box(escaped_args.as_slice()))
5004                        .expect("unistr escaped benchmark invocation must succeed"),
5005                );
5006                if let SqliteValue::Text(text) = result {
5007                    checksum = checksum.wrapping_add(text.len());
5008                }
5009            }
5010            escaped_best_ns = escaped_best_ns.min(started.elapsed().as_nanos());
5011        }
5012
5013        println!(
5014            "unistr_text_args invocations={INVOCATIONS} repeats={REPEATS} plain_best_ns={plain_best_ns} escaped_best_ns={escaped_best_ns} checksum={checksum}"
5015        );
5016    }
5017
5018    // ── random ───────────────────────────────────────────────────────────
5019
5020    #[test]
5021    fn test_random_range() {
5022        let f = RandomFunc;
5023        let result = f.invoke(&[]).unwrap();
5024        assert!(matches!(result, SqliteValue::Integer(_)));
5025    }
5026
5027    // ── randomblob ───────────────────────────────────────────────────────
5028
5029    #[test]
5030    fn test_randomblob_length() {
5031        let result = invoke1(&RandomblobFunc, SqliteValue::Integer(16)).unwrap();
5032        match result {
5033            SqliteValue::Blob(b) => assert_eq!(b.len(), 16),
5034            other => unreachable!("expected blob, got {other:?}"),
5035        }
5036    }
5037
5038    #[test]
5039    fn test_randomblob_null_zero_and_negative_lengths_are_one_byte() {
5040        for arg in [
5041            SqliteValue::Null,
5042            SqliteValue::Integer(0),
5043            SqliteValue::Integer(-5),
5044        ] {
5045            let result = invoke1(&RandomblobFunc, arg).unwrap();
5046            match result {
5047                SqliteValue::Blob(b) => assert_eq!(b.len(), 1),
5048                other => unreachable!("expected one-byte blob, got {other:?}"),
5049            }
5050        }
5051    }
5052
5053    // ── zeroblob ─────────────────────────────────────────────────────────
5054
5055    #[test]
5056    fn test_zeroblob_length() {
5057        let result = invoke1(&ZeroblobFunc, SqliteValue::Integer(100)).unwrap();
5058        match result {
5059            SqliteValue::Blob(b) => {
5060                assert_eq!(b.len(), 100);
5061                assert!(b.iter().all(|&x| x == 0));
5062            }
5063            other => unreachable!("expected blob, got {other:?}"),
5064        }
5065    }
5066
5067    // ── unhex ────────────────────────────────────────────────────────────
5068
5069    #[test]
5070    fn test_unhex_valid() {
5071        let result = invoke1(
5072            &UnhexFunc,
5073            SqliteValue::Text(SmallText::from_string("48656C6C6F")),
5074        )
5075        .unwrap();
5076        assert_eq!(result, SqliteValue::Blob(Arc::from(b"Hello".as_slice())));
5077    }
5078
5079    #[test]
5080    fn test_unhex_invalid() {
5081        let result = invoke1(
5082            &UnhexFunc,
5083            SqliteValue::Text(SmallText::from_string("ZZZZ")),
5084        )
5085        .unwrap();
5086        assert_eq!(result, SqliteValue::Null);
5087    }
5088
5089    #[test]
5090    fn test_unhex_ignore_chars() {
5091        let f = UnhexFunc;
5092        let result = f
5093            .invoke(&[
5094                SqliteValue::Text(SmallText::from_string("48-65-6C")),
5095                SqliteValue::Text(SmallText::from_string("-")),
5096            ])
5097            .unwrap();
5098        assert_eq!(result, SqliteValue::Blob(Arc::from(b"Hel".as_slice())));
5099    }
5100
5101    #[test]
5102    fn test_unhex_ignore_chars_only_between_byte_pairs() {
5103        let f = UnhexFunc;
5104        let result = f
5105            .invoke(&[
5106                SqliteValue::Text(SmallText::from_string("AB CD")),
5107                SqliteValue::Text(SmallText::from_string(" ")),
5108            ])
5109            .unwrap();
5110        assert_eq!(result, SqliteValue::Blob(Arc::from([0xAB, 0xCD])));
5111
5112        let result = f
5113            .invoke(&[
5114                SqliteValue::Text(SmallText::from_string("A BCD")),
5115                SqliteValue::Text(SmallText::from_string(" ")),
5116            ])
5117            .unwrap();
5118        assert_eq!(result, SqliteValue::Null);
5119    }
5120
5121    #[test]
5122    fn test_unhex_null_ignore_argument_returns_null() {
5123        let f = UnhexFunc;
5124        let result = f
5125            .invoke(&[
5126                SqliteValue::Text(SmallText::from_string("41")),
5127                SqliteValue::Null,
5128            ])
5129            .unwrap();
5130        assert_eq!(result, SqliteValue::Null);
5131    }
5132
5133    #[test]
5134    fn test_unhex_hex_digits_in_ignore_argument_do_not_ignore_digits() {
5135        let f = UnhexFunc;
5136        let result = f
5137            .invoke(&[
5138                SqliteValue::Text(SmallText::from_string("41")),
5139                SqliteValue::Text(SmallText::from_string("4")),
5140            ])
5141            .unwrap();
5142        assert_eq!(result, SqliteValue::Blob(Arc::from(b"A".as_slice())));
5143    }
5144
5145    #[test]
5146    #[ignore = "perf-only benchmark"]
5147    fn perf_unhex_text_args() {
5148        use std::hint::black_box;
5149        use std::time::Instant;
5150
5151        const INVOCATIONS: usize = 300_000;
5152        const REPEATS: usize = 7;
5153
5154        let f = UnhexFunc;
5155        let plain_args = [SqliteValue::Text(SmallText::from_string(
5156            "48656C6C6F776F726C64",
5157        ))];
5158        let ignore_args = [
5159            SqliteValue::Text(SmallText::from_string("48-65-6C-6C-6F")),
5160            SqliteValue::Text(SmallText::from_string("-")),
5161        ];
5162        let mut plain_best_ns = u128::MAX;
5163        let mut ignore_best_ns = u128::MAX;
5164        let mut checksum = 0usize;
5165
5166        for _ in 0..REPEATS {
5167            let started = Instant::now();
5168            for _ in 0..INVOCATIONS {
5169                let result = black_box(
5170                    f.invoke(black_box(plain_args.as_slice()))
5171                        .expect("unhex benchmark invocation must succeed"),
5172                );
5173                if let SqliteValue::Blob(blob) = result {
5174                    checksum = checksum.wrapping_add(blob.len());
5175                }
5176            }
5177            plain_best_ns = plain_best_ns.min(started.elapsed().as_nanos());
5178
5179            let started = Instant::now();
5180            for _ in 0..INVOCATIONS {
5181                let result = black_box(
5182                    f.invoke(black_box(ignore_args.as_slice()))
5183                        .expect("unhex ignore benchmark invocation must succeed"),
5184                );
5185                if let SqliteValue::Blob(blob) = result {
5186                    checksum = checksum.wrapping_add(blob.len());
5187                }
5188            }
5189            ignore_best_ns = ignore_best_ns.min(started.elapsed().as_nanos());
5190        }
5191
5192        println!(
5193            "unhex_text_args invocations={INVOCATIONS} repeats={REPEATS} plain_best_ns={plain_best_ns} ignore_best_ns={ignore_best_ns} checksum={checksum}"
5194        );
5195    }
5196
5197    // ── unicode ──────────────────────────────────────────────────────────
5198
5199    #[test]
5200    fn test_unicode_first_char() {
5201        assert_eq!(
5202            invoke1(&UnicodeFunc, SqliteValue::Text(SmallText::from_string("A"))).unwrap(),
5203            SqliteValue::Integer(65)
5204        );
5205    }
5206
5207    #[test]
5208    fn test_unicode_text_stops_at_nul() {
5209        assert_eq!(
5210            invoke1(
5211                &UnicodeFunc,
5212                SqliteValue::Text(SmallText::from_string("\0A"))
5213            )
5214            .unwrap(),
5215            SqliteValue::Null
5216        );
5217        assert_eq!(
5218            invoke1(
5219                &UnicodeFunc,
5220                SqliteValue::Text(SmallText::from_string("A\0"))
5221            )
5222            .unwrap(),
5223            SqliteValue::Integer(65)
5224        );
5225    }
5226
5227    #[test]
5228    fn test_unicode_blob_uses_sqlite_utf8_reader() {
5229        let cases: &[(&[u8], SqliteValue)] = &[
5230            (&[0x00, 0x41], SqliteValue::Null),
5231            (&[0x80], SqliteValue::Integer(128)),
5232            (&[0xC2, 0x80], SqliteValue::Integer(128)),
5233            (&[0xC2, 0x80, 0x80], SqliteValue::Integer(8192)),
5234            (&[0xED, 0xA0, 0x80], SqliteValue::Integer(65_533)),
5235            (&[0xF4, 0x90, 0x80, 0x80], SqliteValue::Integer(1_114_112)),
5236        ];
5237
5238        for (bytes, expected) in cases {
5239            assert_eq!(
5240                invoke1(&UnicodeFunc, SqliteValue::Blob(Arc::from(*bytes))).unwrap(),
5241                expected.clone()
5242            );
5243        }
5244    }
5245
5246    #[test]
5247    #[ignore = "perf-only benchmark"]
5248    fn perf_unicode_text_arg() {
5249        use std::hint::black_box;
5250        use std::time::Instant;
5251
5252        const INVOCATIONS: usize = 1_000_000;
5253        const REPEATS: usize = 7;
5254
5255        let f = UnicodeFunc;
5256        let args = [SqliteValue::Text(SmallText::from_string("Alphabet soup"))];
5257        let mut text_best_ns = u128::MAX;
5258        let mut checksum = 0i64;
5259
5260        for _ in 0..REPEATS {
5261            let started = Instant::now();
5262            for _ in 0..INVOCATIONS {
5263                let result = black_box(
5264                    f.invoke(black_box(args.as_slice()))
5265                        .expect("unicode benchmark invocation must succeed"),
5266                );
5267                if let SqliteValue::Integer(codepoint) = result {
5268                    checksum = checksum.wrapping_add(codepoint);
5269                }
5270            }
5271            text_best_ns = text_best_ns.min(started.elapsed().as_nanos());
5272        }
5273
5274        println!(
5275            "unicode_text_arg invocations={INVOCATIONS} repeats={REPEATS} text_best_ns={text_best_ns} checksum={checksum}"
5276        );
5277    }
5278
5279    // ── soundex ──────────────────────────────────────────────────────────
5280
5281    #[test]
5282    fn test_soundex_basic() {
5283        assert_eq!(
5284            invoke1(
5285                &SoundexFunc,
5286                SqliteValue::Text(SmallText::from_string("Robert"))
5287            )
5288            .unwrap(),
5289            SqliteValue::Text(SmallText::from_string("R163"))
5290        );
5291    }
5292
5293    #[test]
5294    #[ignore = "perf-only benchmark"]
5295    fn perf_soundex_text_arg() {
5296        use std::hint::black_box;
5297        use std::time::Instant;
5298
5299        const INVOCATIONS: usize = 1_000_000;
5300        const REPEATS: usize = 7;
5301
5302        let f = SoundexFunc;
5303        let args = [SqliteValue::Text(SmallText::from_string("Robert"))];
5304        let mut text_best_ns = u128::MAX;
5305        let mut checksum = 0usize;
5306
5307        for _ in 0..REPEATS {
5308            let started = Instant::now();
5309            for _ in 0..INVOCATIONS {
5310                let result = black_box(
5311                    f.invoke(black_box(args.as_slice()))
5312                        .expect("soundex benchmark invocation must succeed"),
5313                );
5314                if let SqliteValue::Text(text) = result {
5315                    checksum = checksum.wrapping_add(text.len());
5316                }
5317            }
5318            text_best_ns = text_best_ns.min(started.elapsed().as_nanos());
5319        }
5320
5321        println!(
5322            "soundex_text_arg invocations={INVOCATIONS} repeats={REPEATS} text_best_ns={text_best_ns} checksum={checksum}"
5323        );
5324    }
5325
5326    // ── substr ───────────────────────────────────────────────────────────
5327
5328    #[test]
5329    fn test_substr_basic() {
5330        let f = SubstrFunc;
5331        assert_eq!(
5332            f.invoke(&[
5333                SqliteValue::Text(SmallText::from_string("hello")),
5334                SqliteValue::Integer(2),
5335                SqliteValue::Integer(3),
5336            ])
5337            .unwrap(),
5338            SqliteValue::Text(SmallText::from_string("ell"))
5339        );
5340    }
5341
5342    #[test]
5343    fn test_substr_truncates_at_embedded_nul() {
5344        // bd-7c6g7 #2: SQLite treats an embedded NUL as a string terminator for
5345        // text functions, so 'a\0bc' behaves as the 1-character string 'a'
5346        // (matching `length('a'||char(0)||'bc')` == 1 and substr(...,1,4) == 'a').
5347        let f = SubstrFunc;
5348        let s = SqliteValue::Text(SmallText::from_string("a\u{0}bc"));
5349        assert_eq!(
5350            f.invoke(&[s.clone(), SqliteValue::Integer(1), SqliteValue::Integer(4)])
5351                .unwrap(),
5352            SqliteValue::Text(SmallText::from_string("a"))
5353        );
5354        assert_eq!(
5355            f.invoke(&[s, SqliteValue::Integer(3)]).unwrap(),
5356            SqliteValue::Text(SmallText::from_string(""))
5357        );
5358    }
5359
5360    #[test]
5361    fn test_substr_start_zero_quirk() {
5362        // substr('hello', 0, 3) returns 2 chars from start
5363        let f = SubstrFunc;
5364        let result = f
5365            .invoke(&[
5366                SqliteValue::Text(SmallText::from_string("hello")),
5367                SqliteValue::Integer(0),
5368                SqliteValue::Integer(3),
5369            ])
5370            .unwrap();
5371        assert_eq!(result, SqliteValue::Text(SmallText::from_string("he")));
5372    }
5373
5374    #[test]
5375    fn test_substr_negative_start() {
5376        // substr('hello', -2) = 'lo'
5377        let f = SubstrFunc;
5378        let result = f
5379            .invoke(&[
5380                SqliteValue::Text(SmallText::from_string("hello")),
5381                SqliteValue::Integer(-2),
5382            ])
5383            .unwrap();
5384        assert_eq!(result, SqliteValue::Text(SmallText::from_string("lo")));
5385    }
5386
5387    #[test]
5388    fn test_substr_negative_length() {
5389        let f = SubstrFunc;
5390        let t = |s: &str| SqliteValue::Text(SmallText::from_string(s));
5391        let i = SqliteValue::Integer;
5392        // SUBSTR('hello', 3, -2) => 'he' (2 chars before position 3)
5393        assert_eq!(f.invoke(&[t("hello"), i(3), i(-2)]).unwrap(), t("he"));
5394        // SUBSTR('hello', 3, -5) => 'he' (clamped at start)
5395        assert_eq!(f.invoke(&[t("hello"), i(3), i(-5)]).unwrap(), t("he"));
5396        // SUBSTR('hello', 1, -1) => '' (nothing before position 1)
5397        assert_eq!(f.invoke(&[t("hello"), i(1), i(-1)]).unwrap(), t(""));
5398    }
5399
5400    #[test]
5401    fn test_substr_negative_start_negative_length() {
5402        let f = SubstrFunc;
5403        let t = |s: &str| SqliteValue::Text(SmallText::from_string(s));
5404        let i = SqliteValue::Integer;
5405        // SUBSTR('hello', -2, -2) => 'el' (C SQLite confirmed)
5406        assert_eq!(f.invoke(&[t("hello"), i(-2), i(-2)]).unwrap(), t("el"));
5407    }
5408
5409    #[test]
5410    fn test_substr_edge_cases() {
5411        let f = SubstrFunc;
5412        let t = |s: &str| SqliteValue::Text(SmallText::from_string(s));
5413        let i = SqliteValue::Integer;
5414        // Past end
5415        assert_eq!(f.invoke(&[t("hello"), i(6), i(2)]).unwrap(), t(""));
5416        // Way before start
5417        assert_eq!(f.invoke(&[t("hello"), i(-10), i(3)]).unwrap(), t(""));
5418        // Negative start covering entire string
5419        assert_eq!(f.invoke(&[t("hello"), i(-5), i(6)]).unwrap(), t("hello"));
5420        // start=0, length=1 => '' (quirk)
5421        assert_eq!(f.invoke(&[t("hello"), i(0), i(1)]).unwrap(), t(""));
5422        // start=0, negative length
5423        assert_eq!(f.invoke(&[t("hello"), i(0), i(-1)]).unwrap(), t(""));
5424        // Empty string
5425        assert_eq!(f.invoke(&[t(""), i(1), i(1)]).unwrap(), t(""));
5426    }
5427
5428    #[test]
5429    fn test_substr_blob_negative_length() {
5430        let f = SubstrFunc;
5431        let i = SqliteValue::Integer;
5432        let blob = SqliteValue::Blob(Arc::from([1, 2, 3, 4, 5].as_slice()));
5433        // SUBSTR(X'0102030405', -2, -2) => X'0203' (matches text behavior)
5434        assert_eq!(
5435            f.invoke(&[blob, i(-2), i(-2)]).unwrap(),
5436            SqliteValue::Blob(Arc::from([2, 3].as_slice()))
5437        );
5438    }
5439
5440    // ── like ─────────────────────────────────────────────────────────────
5441
5442    #[test]
5443    fn test_like_case_insensitive() {
5444        assert_eq!(
5445            invoke2(
5446                &LikeFunc,
5447                SqliteValue::Text(SmallText::from_string("ABC")),
5448                SqliteValue::Text(SmallText::from_string("abc"))
5449            )
5450            .unwrap(),
5451            SqliteValue::Integer(1)
5452        );
5453    }
5454
5455    #[test]
5456    fn test_like_escape() {
5457        let f = LikeFunc;
5458        let result = f
5459            .invoke(&[
5460                SqliteValue::Text(SmallText::from_string("10\\%")),
5461                SqliteValue::Text(SmallText::from_string("10%")),
5462                SqliteValue::Text(SmallText::from_string("\\")),
5463            ])
5464            .unwrap();
5465        assert_eq!(result, SqliteValue::Integer(1));
5466    }
5467
5468    #[test]
5469    fn test_like_escape_rejects_empty_string() {
5470        let err = LikeFunc
5471            .invoke(&[
5472                SqliteValue::Text(SmallText::from_string("a")),
5473                SqliteValue::Text(SmallText::from_string("a")),
5474                SqliteValue::Text(SmallText::new("")),
5475            ])
5476            .unwrap_err();
5477        assert!(
5478            err.to_string()
5479                .contains("ESCAPE expression must be a single character")
5480        );
5481    }
5482
5483    #[test]
5484    fn test_like_escape_rejects_multi_character_string() {
5485        let err = LikeFunc
5486            .invoke(&[
5487                SqliteValue::Text(SmallText::from_string("a")),
5488                SqliteValue::Text(SmallText::from_string("a")),
5489                SqliteValue::Text(SmallText::from_string("xx")),
5490            ])
5491            .unwrap_err();
5492        assert!(
5493            err.to_string()
5494                .contains("ESCAPE expression must be a single character")
5495        );
5496    }
5497
5498    #[test]
5499    fn test_like_percent() {
5500        assert_eq!(
5501            invoke2(
5502                &LikeFunc,
5503                SqliteValue::Text(SmallText::from_string("%ell%")),
5504                SqliteValue::Text(SmallText::from_string("Hello"))
5505            )
5506            .unwrap(),
5507            SqliteValue::Integer(1)
5508        );
5509    }
5510
5511    // ── glob ─────────────────────────────────────────────────────────────
5512
5513    #[test]
5514    fn test_glob_star() {
5515        assert_eq!(
5516            invoke2(
5517                &GlobFunc,
5518                SqliteValue::Text(SmallText::from_string("*.txt")),
5519                SqliteValue::Text(SmallText::from_string("file.txt"))
5520            )
5521            .unwrap(),
5522            SqliteValue::Integer(1)
5523        );
5524    }
5525
5526    #[test]
5527    fn test_glob_case_sensitive() {
5528        assert_eq!(
5529            invoke2(
5530                &GlobFunc,
5531                SqliteValue::Text(SmallText::from_string("ABC")),
5532                SqliteValue::Text(SmallText::from_string("abc"))
5533            )
5534            .unwrap(),
5535            SqliteValue::Integer(0)
5536        );
5537    }
5538
5539    #[test]
5540    fn test_glob_unterminated_character_class_does_not_match() {
5541        // Regression (#257): an unterminated '[' character class never matches,
5542        // matching C SQLite's patternCompare which returns 0 at end-of-pattern.
5543        assert_eq!(
5544            invoke2(
5545                &GlobFunc,
5546                SqliteValue::Text(SmallText::from_string("[a")),
5547                SqliteValue::Text(SmallText::from_string("a"))
5548            )
5549            .unwrap(),
5550            SqliteValue::Integer(0)
5551        );
5552        // The properly-closed form still matches.
5553        assert_eq!(
5554            invoke2(
5555                &GlobFunc,
5556                SqliteValue::Text(SmallText::from_string("[a]")),
5557                SqliteValue::Text(SmallText::from_string("a"))
5558            )
5559            .unwrap(),
5560            SqliteValue::Integer(1)
5561        );
5562    }
5563
5564    #[test]
5565    fn test_glob_trailing_dash_in_character_class_is_literal() {
5566        // Regression (found via eidetic_engine_cli bd-1eeyw): C SQLite's
5567        // patternCompare treats a `-` immediately before `]` as a literal
5568        // class member, never as a range opener. The old parser consumed
5569        // `:-]` in `[^A-Za-z0-9._:-]` as a range from ':' to ']', swallowed
5570        // the class terminator, and derailed the rest of the pattern — so
5571        // `peer_abc/123 GLOB '*[^A-Za-z0-9._:-]*'` returned 0 and a
5572        // NOT-GLOB CHECK constraint admitted invalid identifiers.
5573        let glob = |pattern: &str, text: &str| {
5574            invoke2(
5575                &GlobFunc,
5576                SqliteValue::Text(SmallText::from_string(pattern)),
5577                SqliteValue::Text(SmallText::from_string(text)),
5578            )
5579            .unwrap()
5580        };
5581        // '/' is outside the allowed set: the negated class must match it.
5582        assert_eq!(
5583            glob("*[^A-Za-z0-9._:-]*", "peer_abc/123"),
5584            SqliteValue::Integer(1)
5585        );
5586        // Every allowed byte class: no negated-class match anywhere.
5587        assert_eq!(
5588            glob("*[^A-Za-z0-9._:-]*", "peer_a.b:c-"),
5589            SqliteValue::Integer(0)
5590        );
5591        // Positive class: trailing dash is a literal member.
5592        assert_eq!(glob("[a-c-]", "-"), SqliteValue::Integer(1));
5593        assert_eq!(glob("[a-c-]", "b"), SqliteValue::Integer(1));
5594        assert_eq!(glob("[a-c-]", "d"), SqliteValue::Integer(0));
5595        // A dash as the very first member is likewise literal.
5596        assert_eq!(glob("[-a]", "-"), SqliteValue::Integer(1));
5597        assert_eq!(glob("[-a]", "b"), SqliteValue::Integer(0));
5598    }
5599
5600    #[test]
5601    fn test_iif_two_argument_form() {
5602        // Regression (#183): iif(X, Y) is shorthand for iif(X, Y, NULL) (3.48+).
5603        let f = IifFunc;
5604        assert_eq!(
5605            f.invoke(&[
5606                SqliteValue::Integer(1),
5607                SqliteValue::Text(SmallText::from_string("y")),
5608            ])
5609            .unwrap(),
5610            SqliteValue::Text(SmallText::from_string("y"))
5611        );
5612        assert_eq!(
5613            f.invoke(&[
5614                SqliteValue::Integer(0),
5615                SqliteValue::Text(SmallText::from_string("y")),
5616            ])
5617            .unwrap(),
5618            SqliteValue::Null
5619        );
5620    }
5621
5622    #[test]
5623    fn test_format_g_negative_zero() {
5624        // Regression (#258): printf('%g', -0.0) canonicalizes to '0' (no minus).
5625        let f = FormatFunc;
5626        assert_eq!(
5627            f.invoke(&[
5628                SqliteValue::Text(SmallText::from_string("%g")),
5629                SqliteValue::Float(-0.0),
5630            ])
5631            .unwrap(),
5632            SqliteValue::Text(SmallText::from_string("0"))
5633        );
5634    }
5635
5636    #[test]
5637    fn test_format_signed_zero_all_specs() {
5638        // bd-gh-printf-negative-zero-era4w (#258): -0.0 normalizes to 0 for
5639        // %f/%e/%g and every sub-path (sign flags, width, alt-form), matching
5640        // C SQLite 3.46.1; real negatives keep the minus sign.
5641        let f = FormatFunc;
5642        let fmt = |spec: &str, v: f64| -> String {
5643            match f
5644                .invoke(&[
5645                    SqliteValue::Text(SmallText::from_string(spec)),
5646                    SqliteValue::Float(v),
5647                ])
5648                .unwrap()
5649            {
5650                SqliteValue::Text(s) => s.as_str().to_owned(),
5651                other => panic!("expected text, got {other:?}"),
5652            }
5653        };
5654        // Negative zero -> canonical zero across specifiers.
5655        assert_eq!(fmt("%f", -0.0), "0.000000");
5656        assert_eq!(fmt("%e", -0.0), "0.000000e+00");
5657        assert_eq!(fmt("%E", -0.0), "0.000000E+00");
5658        assert_eq!(fmt("%G", -0.0), "0");
5659        // Sign flags apply to the normalized +0.0.
5660        assert_eq!(fmt("%+g", -0.0), "+0");
5661        assert_eq!(fmt("% g", -0.0), " 0");
5662        assert_eq!(fmt("%+f", -0.0), "+0.000000");
5663        // Width/precision.
5664        assert_eq!(fmt("%8.2f", -0.0), "    0.00");
5665        // Alt-form (`!`) path also normalizes.
5666        assert_eq!(fmt("%!g", -0.0), "0.0");
5667        // Arithmetic-produced negative zero (underflow) normalizes too.
5668        assert_eq!(fmt("%g", -1e-320 * 1e-10), "0");
5669        // Real negatives are UNCHANGED (regression guard).
5670        assert_eq!(fmt("%g", -1.5), "-1.5");
5671        assert_eq!(fmt("%f", -2.25), "-2.250000");
5672        assert_eq!(fmt("%+g", -1.5), "-1.5");
5673    }
5674
5675    #[test]
5676    fn test_format_g_integer_trailing_zeros() {
5677        // bd-v4ujl: %g must not strip significant integer trailing zeros when the
5678        // value rounds to an integer (decimal_places == 0). C/SQLite 3.46.1:
5679        // printf('%g', 100000.0) -> "100000", never "1". Expected values below
5680        // are oracle-verified against sqlite3 3.46.1.
5681        let f = FormatFunc;
5682        let fmt = |spec: &str, v: f64| -> String {
5683            match f
5684                .invoke(&[
5685                    SqliteValue::Text(SmallText::from_string(spec)),
5686                    SqliteValue::Float(v),
5687                ])
5688                .unwrap()
5689            {
5690                SqliteValue::Text(s) => s.as_str().to_owned(),
5691                other => panic!("expected text, got {other:?}"),
5692            }
5693        };
5694        // The bug: integer-valued %g stripped its trailing zeros to a single digit.
5695        assert_eq!(fmt("%g", 100000.0), "100000");
5696        assert_eq!(fmt("%g", 120000.0), "120000");
5697        assert_eq!(fmt("%g", 250000.0), "250000");
5698        assert_eq!(fmt("%g", 100.0), "100");
5699        assert_eq!(fmt("%g", 999999.0), "999999");
5700        assert_eq!(fmt("%G", 100000.0), "100000");
5701        // Fractional %g still trims trailing zeros (regression guard).
5702        assert_eq!(fmt("%g", 0.5), "0.5");
5703        assert_eq!(fmt("%g", 1.5), "1.5");
5704        // Exponential branch (exp >= sig) is unaffected by the guard.
5705        assert_eq!(fmt("%g", 1000000.0), "1e+06");
5706        assert_eq!(fmt("%g", 1234560.0), "1.23456e+06");
5707        assert_eq!(fmt("%G", 1000000.0), "1E+06");
5708    }
5709
5710    #[test]
5711    fn test_format_c_field_width_bd_ul4c0() {
5712        // bd-ul4c0: printf %c honors field width, counted in CHARACTERS — the
5713        // single emitted char is one width unit regardless of byte length
5714        // (unlike %s, which counts bytes) — padded with spaces (the '0' flag is
5715        // ignored), right- or left-justified. %c still emits the FIRST char of
5716        // the argument's text form (bd-47mu0). Oracle-verified vs sqlite3 3.46.1.
5717        let f = FormatFunc;
5718        let fmt = |spec: &str, v: SqliteValue| -> String {
5719            match f
5720                .invoke(&[SqliteValue::Text(SmallText::from_string(spec)), v])
5721                .unwrap()
5722            {
5723                SqliteValue::Text(s) => s.as_str().to_owned(),
5724                other => panic!("expected text, got {other:?}"),
5725            }
5726        };
5727        let txt = |s: &str| SqliteValue::Text(SmallText::from_string(s));
5728        // The bug: %c emitted the char but ignored field width entirely.
5729        assert_eq!(fmt(">%3c<", SqliteValue::Integer(65)), ">  6<");
5730        assert_eq!(fmt(">%-3c<", SqliteValue::Integer(65)), ">6  <");
5731        // First char of the text form, then width padding.
5732        assert_eq!(fmt(">%5c<", txt("abc")), ">    a<");
5733        assert_eq!(fmt(">%-5c<", txt("abc")), ">a    <");
5734        // The '0' flag does NOT zero-pad %c; padding stays spaces.
5735        assert_eq!(fmt(">%03c<", SqliteValue::Integer(65)), ">  6<");
5736        // Width counts CHARACTERS, not bytes: 'é' (2 UTF-8 bytes) is one unit.
5737        assert_eq!(fmt(">%3c<", txt("é")), ">  é<");
5738        // No width => just the first char (regression guard for bd-47mu0).
5739        assert_eq!(fmt(">%c<", SqliteValue::Integer(65)), ">6<");
5740        assert_eq!(fmt(">%c<", txt("abc")), ">a<");
5741    }
5742
5743    #[test]
5744    fn test_format_quote_specifiers_field_width_bd_8959m() {
5745        // bd-8959m: printf %q/%Q/%w honor field width (byte-counted like %s,
5746        // space-padded, right/left-justified). %q NULL renders "(NULL)" and %Q
5747        // NULL renders "NULL", both padded; %w NULL stays empty. Width is a
5748        // minimum (never truncates). Oracle-verified vs sqlite3 3.46.1.
5749        let f = FormatFunc;
5750        let fmt = |spec: &str, v: SqliteValue| -> String {
5751            match f
5752                .invoke(&[SqliteValue::Text(SmallText::from_string(spec)), v])
5753                .unwrap()
5754            {
5755                SqliteValue::Text(s) => s.as_str().to_owned(),
5756                other => panic!("expected text, got {other:?}"),
5757            }
5758        };
5759        let txt = |s: &str| SqliteValue::Text(SmallText::from_string(s));
5760        // %q width (the bug: width was ignored) + escaping + NULL.
5761        assert_eq!(fmt(">%6q<", txt("ab")), ">    ab<");
5762        assert_eq!(fmt(">%-6q<", txt("ab")), ">ab    <");
5763        assert_eq!(fmt(">%8q<", SqliteValue::Null), ">  (NULL)<");
5764        assert_eq!(fmt(">%8q<", txt("a'b")), ">    a''b<");
5765        assert_eq!(fmt(">%3q<", txt("abcde")), ">abcde<"); // width is a minimum
5766        // %Q: quote-wrapped, width applies to the whole token.
5767        assert_eq!(fmt(">%6Q<", txt("ab")), ">  'ab'<");
5768        assert_eq!(fmt(">%-6Q<", txt("ab")), ">'ab'  <");
5769        assert_eq!(fmt(">%6Q<", SqliteValue::Null), ">  NULL<");
5770        // %w: identifier escaping, width on the non-NULL rendering.
5771        assert_eq!(fmt(">%6w<", txt("ab")), ">    ab<");
5772        assert_eq!(fmt(">%-6w<", txt("ab")), ">ab    <");
5773        // Byte-counted width (matches %s): 'é' is two UTF-8 bytes.
5774        assert_eq!(fmt(">%4q<", txt("é")), ">  é<");
5775        // No width => unchanged (regression guard).
5776        assert_eq!(fmt(">%q<", txt("ab")), ">ab<");
5777        assert_eq!(fmt(">%Q<", txt("ab")), ">'ab'<");
5778    }
5779
5780    #[test]
5781    fn test_format_round_half_away_from_zero_bd_o1tu1() {
5782        // bd-o1tu1: printf/format float conversions %f/%e/%g must round exact
5783        // binary half-ties AWAY FROM ZERO (C SQLite) rather than Rust's
5784        // round-half-to-even. Non-tie values (e.g. 0.135, 1.005, 2.675, 0.15)
5785        // are NOT exact binary ties and MUST stay on their correctly-rounded
5786        // value. Every expected string below was produced by running
5787        // `sqlite3 :memory: "SELECT printf('<spec>', <val>);"` against stock
5788        // sqlite3 3.46.1 — assert exactly that, never a guess.
5789        let f = FormatFunc;
5790        let fmt = |spec: &str, v: f64| -> String {
5791            match f
5792                .invoke(&[
5793                    SqliteValue::Text(SmallText::from_string(spec)),
5794                    SqliteValue::Float(v),
5795                ])
5796                .unwrap()
5797            {
5798                SqliteValue::Text(s) => s.as_str().to_owned(),
5799                other => panic!("expected text, got {other:?}"),
5800            }
5801        };
5802        let cases: &[(&str, f64, &str)] = &[
5803            // %f exact ties -> away from zero.
5804            ("%.0f", 2.5, "3"),
5805            ("%.0f", 0.5, "1"),
5806            ("%.0f", -2.5, "-3"),
5807            ("%.0f", 3.5, "4"),
5808            ("%.0f", -0.5, "-1"),
5809            ("%.0f", -3.5, "-4"),
5810            ("%.0f", 1.5, "2"),
5811            ("%.2f", 0.125, "0.13"),
5812            ("%.2f", 0.375, "0.38"),
5813            ("%.2f", 0.625, "0.63"),
5814            ("%.2f", 2.125, "2.13"),
5815            ("%.2f", -0.125, "-0.13"),
5816            ("%.1f", 0.25, "0.3"),
5817            ("%.1f", 0.75, "0.8"),
5818            ("%.1f", 2.25, "2.3"),
5819            ("%.1f", -0.25, "-0.3"),
5820            ("%.1f", 0.05, "0.1"),
5821            ("%.0f", 12.5, "13"),
5822            ("%.2f", 12.5, "12.50"),
5823            // %f non-ties -> unchanged (correctly-rounded true value).
5824            ("%.2f", 0.135, "0.14"),
5825            ("%.2f", 0.35, "0.35"),
5826            ("%.2f", 0.15, "0.15"),
5827            ("%.2f", 0.85, "0.85"),
5828            ("%.2f", 0.95, "0.95"),
5829            ("%.2f", 1.005, "1.00"),
5830            ("%.2f", 2.675, "2.67"),
5831            ("%.2f", 0.005, "0.01"),
5832            ("%.2f", 0.015, "0.01"),
5833            ("%.2f", 0.025, "0.03"),
5834            ("%.1f", 0.35, "0.3"),
5835            ("%.1f", 0.15, "0.1"),
5836            ("%.1f", 0.135, "0.1"),
5837            ("%.0f", 2.675, "3"),
5838            ("%.0f", 0.49999, "0"),
5839            // Sign / width / uppercase interplay applied AFTER rounding.
5840            ("%+.0f", 2.5, "+3"),
5841            ("%8.0f", 2.5, "       3"),
5842            // %e exact mantissa ties -> away (carry may bump the exponent).
5843            ("%.0e", 2.5, "3e+00"),
5844            ("%.0e", 9.5, "1e+01"),
5845            ("%.0e", 1.5, "2e+00"),
5846            ("%.0e", 250.0, "3e+02"),
5847            ("%.0e", 0.25, "3e-01"),
5848            ("%.1e", 1.25, "1.3e+00"),
5849            ("%.1e", 12.5, "1.3e+01"),
5850            ("%.0E", 2.5, "3E+00"),
5851            // %e non-ties -> unchanged.
5852            ("%.1e", 0.5, "5.0e-01"),
5853            ("%.1e", 9.95, "9.9e+00"),
5854            ("%.1e", 1.005, "1.0e+00"),
5855            ("%.1e", 2.675, "2.7e+00"),
5856            ("%.1e", 1.35, "1.4e+00"),
5857            ("%.0e", 0.5, "5e-01"),
5858            ("%.0e", 9.95, "1e+01"),
5859            ("%.0e", 1.005, "1e+00"),
5860            // %g exact ties (precision is significant digits) -> away.
5861            ("%.1g", 0.25, "0.3"),
5862            ("%.1g", 2.5, "3"),
5863            ("%.1g", 25.0, "3e+01"),
5864            ("%.2g", 0.125, "0.13"),
5865            ("%.2g", 1.25, "1.3"),
5866            ("%.2g", 12.5, "13"),
5867            // %g non-ties -> unchanged.
5868            ("%.1g", 0.35, "0.3"),
5869            ("%.1g", 0.15, "0.1"),
5870            ("%.1g", 0.45, "0.5"),
5871            ("%.1g", 0.125, "0.1"),
5872            ("%.2g", 0.135, "0.14"),
5873            ("%.2g", 1.005, "1"),
5874            ("%.2g", 2.675, "2.7"),
5875        ];
5876        for (spec, v, want) in cases {
5877            assert_eq!(fmt(spec, *v), *want, "spec={spec} v={v}");
5878        }
5879    }
5880
5881    #[test]
5882    fn test_round_half_away_near_ties_match_oracle_bd_o1tu1() {
5883        // bd-o1tu1: round() shares the fixed-notation half-away helper, so its
5884        // exact-tie detection must NOT misfire on near-ties whose double is not
5885        // a true binary half (a small guard would round e.g. 0.15's 0.14999…
5886        // into a spurious 0.1500…). Values below are stock sqlite3 3.46.1
5887        // `SELECT round(v, 1);` results.
5888        #[allow(clippy::float_cmp)]
5889        fn round1(v: f64) -> f64 {
5890            match RoundFunc
5891                .invoke(&[SqliteValue::Float(v), SqliteValue::Integer(1)])
5892                .unwrap()
5893            {
5894                SqliteValue::Float(x) => x,
5895                other => panic!("expected float, got {other:?}"),
5896            }
5897        }
5898        let cases: &[(f64, f64)] = &[
5899            (0.15, 0.1),
5900            (0.35, 0.3),
5901            (0.85, 0.8),
5902            (0.95, 0.9),
5903            (0.135, 0.1),
5904            (1.005, 1.0),
5905            (2.675, 2.7),
5906            // 0.25 is a genuine exact tie -> away from zero (0.3); 0.45's
5907            // double is 0.45000…111 so it rounds up on its true value; 2.5 has
5908            // no digit past precision 1 and is returned unchanged.
5909            (0.25, 0.3),
5910            (0.45, 0.5),
5911            (2.5, 2.5),
5912        ];
5913        for (v, want) in cases {
5914            #[allow(clippy::float_cmp)]
5915            let got = round1(*v);
5916            assert_eq!(got, *want, "round({v}, 1)");
5917        }
5918    }
5919
5920    #[test]
5921    fn test_format_altform2_flag() {
5922        // Regression (#176): the '!' (alternate-form-2) flag is accepted. For
5923        // string/int conversions the value formats normally; for %f it selects
5924        // the shortest round-trip form with a decimal point.
5925        let f = FormatFunc;
5926        assert_eq!(
5927            f.invoke(&[
5928                SqliteValue::Text(SmallText::from_string("%!5s")),
5929                SqliteValue::Text(SmallText::from_string("ab")),
5930            ])
5931            .unwrap(),
5932            SqliteValue::Text(SmallText::from_string("   ab"))
5933        );
5934        assert_eq!(
5935            f.invoke(&[
5936                SqliteValue::Text(SmallText::from_string("%!d")),
5937                SqliteValue::Integer(3),
5938            ])
5939            .unwrap(),
5940            SqliteValue::Text(SmallText::from_string("3"))
5941        );
5942        assert_eq!(
5943            f.invoke(&[
5944                SqliteValue::Text(SmallText::from_string("%!f")),
5945                SqliteValue::Float(0.1),
5946            ])
5947            .unwrap(),
5948            SqliteValue::Text(SmallText::from_string("0.1"))
5949        );
5950    }
5951
5952    #[test]
5953    fn test_format_altform2_precision_and_width() {
5954        // The '!' (alternate-form-2) flag on %f applies the requested precision
5955        // FIRST and then strips trailing fractional zeros (keeping >= 1 digit;
5956        // ".0" is forced at precision 0), with width/sign flags applied last.
5957        // Frank previously used Rust's shortest round-trip form and ignored an
5958        // explicit precision, so '%!5.2f' 3.14159 rendered "3.14159" instead of
5959        // " 3.14" (probe-found divergence). Oracle: sqlite3 3.46.1.
5960        let f = FormatFunc;
5961        let fmt = |spec: &str, v: f64| -> String {
5962            match f
5963                .invoke(&[
5964                    SqliteValue::Text(SmallText::from_string(spec)),
5965                    SqliteValue::Float(v),
5966                ])
5967                .unwrap()
5968            {
5969                SqliteValue::Text(s) => s.as_str().to_owned(),
5970                other => panic!("expected text, got {other:?}"),
5971            }
5972        };
5973        let cases: &[(&str, f64, &str)] = &[
5974            ("%!f", 0.1, "0.1"),
5975            ("%!5.2f", 3.14159, " 3.14"),
5976            ("%!.3f", 1.5, "1.5"),
5977            ("%!f", 3.14159, "3.14159"),
5978            ("%!f", 5.0, "5.0"),
5979            ("%!f", 5.5, "5.5"),
5980            ("%!.0f", 5.5, "6.0"),
5981            ("%!f", -0.5, "-0.5"),
5982            ("%+!f", 0.5, "+0.5"),
5983            ("%!f", 100.0, "100.0"),
5984            ("%!8.2f", 3.14159, "    3.14"),
5985            ("%!08.3f", 1.5, "000001.5"),
5986            ("%!10.2f", 3.14159, "      3.14"),
5987        ];
5988        for (spec, v, want) in cases {
5989            assert_eq!(fmt(spec, *v), *want, "spec={spec} v={v}");
5990        }
5991    }
5992
5993    #[test]
5994    fn test_format_comma_grouping_flag() {
5995        // SQLite's `,` printf flag groups the integer digits into thousands.
5996        // Applies to %d/%i/%u and the integer part of %f; it is accepted but
5997        // inert for %e/%g/%x. Frank previously did not recognize `,` as a flag
5998        // and emitted the spec verbatim ('%,d' 1234567 -> "%,d"; probe-found).
5999        // Zero padding pads the raw digits BEFORE grouping ('%,08d' 1234 ->
6000        // "00,001,234"); space padding is applied AFTER grouping. Oracle:
6001        // sqlite3 3.46.1.
6002        let f = FormatFunc;
6003        let fmt = |spec: &str, v: SqliteValue| -> String {
6004            match f
6005                .invoke(&[SqliteValue::Text(SmallText::from_string(spec)), v])
6006                .unwrap()
6007            {
6008                SqliteValue::Text(s) => s.as_str().to_owned(),
6009                other => panic!("expected text, got {other:?}"),
6010            }
6011        };
6012        let int_cases: &[(&str, i64, &str)] = &[
6013            ("%,d", 1234567, "1,234,567"),
6014            ("%,d", -1234567, "-1,234,567"),
6015            ("%,d", 123, "123"),
6016            ("%,d", 1000, "1,000"),
6017            ("%,d", 0, "0"),
6018            ("%,d", -100, "-100"),
6019            ("%,d", 1000000, "1,000,000"),
6020            ("%,10d", 1234567, " 1,234,567"),
6021            ("%,08d", 1234, "00,001,234"),
6022            ("%+,d", 1234567, "+1,234,567"),
6023            ("%, d", 1234567, " 1,234,567"),
6024            ("%-,12d", 1234567, "1,234,567   "),
6025            ("%,i", 1234567, "1,234,567"),
6026            ("%,u", 1234567, "1,234,567"),
6027            ("%,x", 1234567, "12d687"),
6028        ];
6029        for (spec, v, want) in int_cases {
6030            assert_eq!(
6031                fmt(spec, SqliteValue::Integer(*v)),
6032                *want,
6033                "spec={spec} v={v}"
6034            );
6035        }
6036        let float_cases: &[(&str, f64, &str)] = &[
6037            ("%,f", 1234567.5, "1,234,567.500000"),
6038            ("%,.2f", 1234567.891, "1,234,567.89"),
6039            ("%,f", -1234.5, "-1,234.500000"),
6040            ("%,e", 1234.5, "1.234500e+03"),
6041            // %g is grouped only in fixed (non-exponential) form.
6042            ("%,g", 1234.5, "1,234.5"),
6043            ("%,g", 12.0, "12"),
6044            ("%,g", 1234567.0, "1.23457e+06"),
6045            ("%,g", 1000000.0, "1e+06"),
6046            ("%,.2g", 1234.5, "1.2e+03"),
6047        ];
6048        for (spec, v, want) in float_cases {
6049            assert_eq!(
6050                fmt(spec, SqliteValue::Float(*v)),
6051                *want,
6052                "spec={spec} v={v}"
6053            );
6054        }
6055    }
6056
6057    #[test]
6058    fn test_format_integer_precision() {
6059        // Integer precision (%.Nd) is the MINIMUM digit count: the digits are
6060        // zero-padded to N, with sign/width applied outside. Applies to
6061        // %d/%i/%u/%x/%X/%o. Frank previously ignored precision on integers
6062        // ('%.3d' 5 -> "5"; probe-found divergence). Oracle: sqlite3 3.46.1.
6063        let f = FormatFunc;
6064        let fmt = |spec: &str, v: i64| -> String {
6065            match f
6066                .invoke(&[
6067                    SqliteValue::Text(SmallText::from_string(spec)),
6068                    SqliteValue::Integer(v),
6069                ])
6070                .unwrap()
6071            {
6072                SqliteValue::Text(s) => s.as_str().to_owned(),
6073                other => panic!("expected text, got {other:?}"),
6074            }
6075        };
6076        let cases: &[(&str, i64, &str)] = &[
6077            ("%.3d", 5, "005"),
6078            ("%.3d", -5, "-005"),
6079            ("%.0d", 0, "0"),
6080            ("%.0d", 5, "5"),
6081            ("%5.3d", 42, "  042"),
6082            ("%-5.3d", 42, "042  "),
6083            ("%.3d", 12345, "12345"),
6084            ("%+.3d", 5, "+005"),
6085            ("% .3d", 5, " 005"),
6086            ("%08.3d", 42, "00000042"),
6087            ("%.3i", 9, "009"),
6088            ("%.3u", 7, "007"),
6089            ("%.3x", 10, "00a"),
6090            ("%.3o", 8, "010"),
6091        ];
6092        for (spec, v, want) in cases {
6093            assert_eq!(fmt(spec, *v), *want, "spec={spec} v={v}");
6094        }
6095    }
6096
6097    #[test]
6098    fn test_format_altform2_exponential() {
6099        // The '!' flag on %e/%E strips trailing zeros from the mantissa (keeping
6100        // >= 1 fractional digit), then reattaches the exponent. Oracle: sqlite3
6101        // 3.46.1.
6102        let f = FormatFunc;
6103        let fmt = |spec: &str, v: f64| -> String {
6104            match f
6105                .invoke(&[
6106                    SqliteValue::Text(SmallText::from_string(spec)),
6107                    SqliteValue::Float(v),
6108                ])
6109                .unwrap()
6110            {
6111                SqliteValue::Text(s) => s.as_str().to_owned(),
6112                other => panic!("expected text, got {other:?}"),
6113            }
6114        };
6115        let cases: &[(&str, f64, &str)] = &[
6116            ("%!e", 3.14159, "3.14159e+00"),
6117            ("%!E", 3.14159, "3.14159E+00"),
6118            ("%!e", 5.0, "5.0e+00"),
6119            ("%!.2e", 3.14159, "3.14e+00"),
6120            ("%!.0e", 3.0, "3.0e+00"),
6121        ];
6122        for (spec, v, want) in cases {
6123            assert_eq!(fmt(spec, *v), *want, "spec={spec} v={v}");
6124        }
6125    }
6126
6127    #[test]
6128    fn test_format_altform2_g_honors_precision() {
6129        // bd-g7pfx: '!' on %g formats at the requested significant digits
6130        // (precision 0 => 1 sig fig, so the fixed/exponential choice is honored),
6131        // then ensures a decimal point with >= 1 fractional digit. Previously the
6132        // path used shortest-round-trip and ignored precision. Oracle: sqlite3
6133        // 3.46.1.
6134        let f = FormatFunc;
6135        let fmt = |spec: &str, v: f64| -> String {
6136            match f
6137                .invoke(&[
6138                    SqliteValue::Text(SmallText::from_string(spec)),
6139                    SqliteValue::Float(v),
6140                ])
6141                .unwrap()
6142            {
6143                SqliteValue::Text(s) => s.as_str().to_owned(),
6144                other => panic!("expected text, got {other:?}"),
6145            }
6146        };
6147        let cases: &[(&str, f64, &str)] = &[
6148            ("%!g", 12345.0, "12345.0"),
6149            ("%!.0g", 12345.0, "1.0e+04"),
6150            ("%!.1g", 12345.0, "1.0e+04"),
6151            ("%!.3g", 12345.0, "1.23e+04"),
6152            ("%!.2g", 0.000123, "0.00012"),
6153            ("%!g", 100.0, "100.0"),
6154            ("%!.0g", 5.0, "5.0"),
6155            ("%!g", 0.1, "0.1"),
6156            ("%!G", 12345.0, "12345.0"),
6157            ("%!.0G", 12345.0, "1.0E+04"),
6158        ];
6159        for (spec, v, want) in cases {
6160            assert_eq!(fmt(spec, *v), *want, "spec={spec} v={v}");
6161        }
6162    }
6163
6164    #[test]
6165    #[allow(clippy::excessive_precision, clippy::unreadable_literal)]
6166    fn test_format_altform2_high_precision_bd_ixizz() {
6167        // bd-ixizz: at HIGH precision the '!' (alt-form-2) float paths must emit
6168        // the exact double's value-dependent significant-digit cap — 18 figs, or
6169        // 19 when |val| >= 1e18 — TRUNCATED (not rounded to the 16-fig dtoa cap
6170        // the plain conversions use), then strip trailing zeros. Below the cap
6171        // they round as before. Oracle: sqlite3 3.46.1 (float->text is
6172        // oracle-version-sensitive, but these are all sqlite3-3.46.1 %! outputs).
6173        let f = FormatFunc;
6174        let fmt = |spec: &str, v: f64| -> String {
6175            match f
6176                .invoke(&[
6177                    SqliteValue::Text(SmallText::from_string(spec)),
6178                    SqliteValue::Float(v),
6179                ])
6180                .unwrap()
6181            {
6182                SqliteValue::Text(s) => s.as_str().to_owned(),
6183                other => panic!("expected text, got {other:?}"),
6184            }
6185        };
6186        let third = 1.0 / 3.0;
6187        let two_thirds = 2.0 / 3.0;
6188        let seventh = 1.0 / 7.0;
6189        let cases: &[(&str, f64, &str)] = &[
6190            // %!e: 18-fig cap (19 for 1e300), truncated then trailing-stripped.
6191            ("%!.40e", two_thirds, "6.66666666666666629e-01"),
6192            ("%!.40e", third, "3.33333333333333314e-01"),
6193            ("%!.40e", 0.1, "1.00000000000000005e-01"),
6194            ("%!.40e", seventh, "1.42857142857142849e-01"),
6195            ("%!.40e", 1e300, "1.000000000000000052e+300"),
6196            ("%!.40E", two_thirds, "6.66666666666666629E-01"),
6197            ("%!.40e", -two_thirds, "-6.66666666666666629e-01"),
6198            // Transition: below the cap rounds, at the cap truncates.
6199            ("%!.16e", two_thirds, "6.6666666666666663e-01"),
6200            ("%!.17e", two_thirds, "6.66666666666666629e-01"),
6201            // %!f: same cap in fixed form.
6202            ("%!.40f", third, "0.333333333333333314"),
6203            ("%!.40f", 0.1, "0.100000000000000005"),
6204            ("%!.18f", third, "0.333333333333333314"),
6205            ("%!.40f", -two_thirds, "-0.666666666666666629"),
6206            // 19-fig cap for a >= 1e18 whole number (extra integer zero-fill).
6207            ("%!.40f", 12345678901234567890.0, "12345678901234567160.0"),
6208            // %!g: honors the fixed/exponential choice at the wider cap.
6209            ("%!.17g", two_thirds, "0.66666666666666663"),
6210            ("%!.18g", two_thirds, "0.666666666666666629"),
6211            ("%!.40g", two_thirds, "0.666666666666666629"),
6212            ("%!.40g", -two_thirds, "-0.666666666666666629"),
6213        ];
6214        for (spec, v, want) in cases {
6215            assert_eq!(fmt(spec, *v), *want, "spec={spec} v={v}");
6216        }
6217    }
6218
6219    #[test]
6220    fn test_format_alt_form_hash_floats() {
6221        // bd-0hgsi: `#` (alt-form) on floats forces a decimal point (%f/%e) and,
6222        // for %g, retains every significant digit (no trailing-zero strip) plus a
6223        // decimal point. Oracle: sqlite3 3.46.1.
6224        let f = FormatFunc;
6225        let fmt = |spec: &str, v: f64| -> String {
6226            match f
6227                .invoke(&[
6228                    SqliteValue::Text(SmallText::from_string(spec)),
6229                    SqliteValue::Float(v),
6230                ])
6231                .unwrap()
6232            {
6233                SqliteValue::Text(s) => s.as_str().to_owned(),
6234                other => panic!("expected text, got {other:?}"),
6235            }
6236        };
6237        let cases: &[(&str, f64, &str)] = &[
6238            ("%#.0f", 3.0, "3."),
6239            ("%#.2f", 3.5, "3.50"),
6240            ("%#.0f", -3.0, "-3."),
6241            ("%#5.0f", 3.0, "   3."),
6242            ("%#.0f", 0.0, "0."),
6243            ("%#.0e", 3.0, "3.e+00"),
6244            ("%#e", 3.0, "3.000000e+00"),
6245            ("%#.0g", 3.0, "3."),
6246            ("%#g", 3.0, "3.00000"),
6247            ("%#.3g", 3.0, "3.00"),
6248            ("%#g", 100000.0, "100000."),
6249            ("%#g", 0.0001, "0.000100000"),
6250            ("%#.1g", 9.9, "1.e+01"),
6251            ("%#g", 1234567.0, "1.23457e+06"),
6252        ];
6253        for (spec, v, want) in cases {
6254            assert_eq!(fmt(spec, *v), *want, "spec={spec} v={v}");
6255        }
6256    }
6257
6258    #[test]
6259    fn test_printf_bd_9zzr0_review_fixes() {
6260        // Oracle: sqlite3 3.46.1. bd-9zzr0 REVIEW-B-printf tail.
6261        let f = FormatFunc;
6262        let run = |args: &[SqliteValue]| -> String {
6263            match f.invoke(args).unwrap() {
6264                SqliteValue::Text(s) => s.as_str().to_owned(),
6265                other => panic!("expected text, got {other:?}"),
6266            }
6267        };
6268        let txt = |s: &str| SqliteValue::Text(SmallText::from_string(s));
6269        let int = SqliteValue::Integer;
6270
6271        // M9: %c precision repeats the char.
6272        assert_eq!(run(&[txt("[%.5c]"), txt("A")]), "[AAAAA]");
6273        assert_eq!(run(&[txt("[%c]"), txt("A")]), "[A]");
6274        assert_eq!(run(&[txt("[%3c]"), txt("B")]), "[  B]");
6275        // M10: the `0` flag wins over `-` for integers (matches %x/%o/%u).
6276        assert_eq!(run(&[txt("[%-05d]"), int(42)]), "[00042]");
6277        assert_eq!(run(&[txt("[%05d]"), int(42)]), "[00042]");
6278        assert_eq!(run(&[txt("[%-5d]"), int(42)]), "[42   ]");
6279        // L1: a negative dynamic precision takes its absolute value.
6280        assert_eq!(run(&[txt("[%.*d]"), int(-3), int(42)]), "[042]");
6281        assert_eq!(run(&[txt("[%.*d]"), int(3), int(42)]), "[042]");
6282        // L2: %w with NULL renders "(NULL)", width-padded.
6283        assert_eq!(run(&[txt("[%w]"), SqliteValue::Null]), "[(NULL)]");
6284        assert_eq!(run(&[txt("[%10w]"), SqliteValue::Null]), "[    (NULL)]");
6285        // L3: %q/%Q/%w precision truncates the raw text BEFORE escaping.
6286        assert_eq!(run(&[txt("[%.3q]"), txt("ab'cdef")]), "[ab'']");
6287        assert_eq!(run(&[txt("[%.3Q]"), txt("ab'cdef")]), "['ab''']");
6288        assert_eq!(run(&[txt("[%.3w]"), txt("a\"bcdef")]), "[a\"\"b]");
6289        // bd-77dkj: %c precision 0 (and 1) still emits the char ONCE (min repeat 1);
6290        // stock printf('%.0c','A') == 'A', not '' (a naive repeat(0) dropped it).
6291        assert_eq!(run(&[txt("[%.0c]"), txt("A")]), "[A]");
6292        assert_eq!(run(&[txt("[%.1c]"), txt("A")]), "[A]");
6293        // bd-77dkj: a dynamic precision is CAST TO i32 first. A huge i64 like
6294        // -4294967293 becomes i32 3 -> "042" (not a 100M-digit zero-pad blowup);
6295        // INT32_MIN escapes to "no precision".
6296        assert_eq!(run(&[txt("[%.*d]"), int(-4_294_967_293), int(42)]), "[042]");
6297        assert_eq!(run(&[txt("[%.*d]"), int(-2_147_483_648), int(42)]), "[42]");
6298        assert_eq!(run(&[txt("[%.*d]"), int(-1), int(42)]), "[42]");
6299    }
6300
6301    #[test]
6302    #[allow(clippy::excessive_precision)] // literals intentionally name specific f64s
6303    fn test_format_high_precision_shortest_round_trip() {
6304        // bd-o8m86: beyond the shortest round-trip decimal (~16-17 sig figs) C
6305        // SQLite pads with zeros rather than showing the true f64 tail. Frank
6306        // must match. Normal-precision cases (< shortest length) are unchanged.
6307        // Oracle: sqlite3 3.46.1.
6308        let f = FormatFunc;
6309        let fmt = |spec: &str, v: f64| -> String {
6310            match f
6311                .invoke(&[
6312                    SqliteValue::Text(SmallText::from_string(spec)),
6313                    SqliteValue::Float(v),
6314                ])
6315                .unwrap()
6316            {
6317                SqliteValue::Text(s) => s.as_str().to_owned(),
6318                other => panic!("expected text, got {other:?}"),
6319            }
6320        };
6321        let third = 1.0 / 3.0;
6322        let pi = std::f64::consts::PI;
6323        let cases: &[(&str, f64, &str)] = &[
6324            // --- beyond shortest: pad zeros, don't show true tail ---
6325            ("%.20f", 0.1, "0.10000000000000000000"),
6326            ("%.18f", 0.1, "0.100000000000000000"),
6327            ("%.30f", 1.5, "1.500000000000000000000000000000"),
6328            ("%.25f", 1.5, "1.5000000000000000000000000"),
6329            ("%.17f", third, "0.33333333333333330"),
6330            ("%.18f", third, "0.333333333333333300"),
6331            ("%.17g", 0.1, "0.1"),
6332            ("%.25g", 0.1, "0.1"),
6333            ("%.17g", third, "0.3333333333333333"),
6334            ("%.18g", third, "0.3333333333333333"),
6335            ("%.17g", pi, "3.141592653589793"),
6336            ("%.17e", 0.1, "1.00000000000000000e-01"),
6337            ("%.16e", 0.1, "1.0000000000000000e-01"),
6338            ("%.19e", third, "3.3333333333333330000e-01"),
6339            ("%.17e", 2.675, "2.67500000000000000e+00"),
6340            // --- 16-sig-fig dtoa cap: value whose true 16 figs are NOT its
6341            // minimal-shortest ("1e-20"); C SQLite shows "9.999...e-21" ---
6342            ("%.17g", 1e-20, "9.999999999999999e-21"),
6343            ("%.20g", 1e-20, "9.999999999999999e-21"),
6344            ("%.17e", 1e-20, "9.99999999999999900e-21"),
6345            ("%.30g", 1.0 / 7.0, "0.1428571428571428"),
6346            // --- %f 16-fig cap on high-integer-digit + huge-integer values ---
6347            ("%.2f", 123_456_789_012_345.678, "123456789012345.70"),
6348            ("%.6f", 123_456_789_012_345.678, "123456789012345.700000"),
6349            ("%.17g", 123_456_789_012_345.678, "123456789012345.7"),
6350            ("%f", 6.022e23, "602200000000000000000000.000000"),
6351            // --- carry through the 16-fig cap + negatives ---
6352            ("%.17f", 2.675, "2.67500000000000000"),
6353            ("%.20f", -0.1, "-0.10000000000000000000"),
6354            ("%.17f", -1.0 / 3.0, "-0.33333333333333330"),
6355            // --- %!f (alt-form-2) has its own rendering, NOT the 16-cap ---
6356            ("%!f", 0.1, "0.1"),
6357            ("%!f", 1.5, "1.5"),
6358            // --- normal precision (< shortest length): unchanged, must match ---
6359            ("%.2f", 0.1, "0.10"),
6360            ("%.6f", 0.1, "0.100000"),
6361            ("%.15f", 0.1, "0.100000000000000"),
6362            ("%.1f", 0.15, "0.1"),
6363            ("%.2f", 2.675, "2.67"),
6364            ("%.0f", 2.5, "3"),
6365            ("%g", third, "0.333333"),
6366            ("%.6e", 0.1, "1.000000e-01"),
6367            ("%f", 1.5, "1.500000"),
6368        ];
6369        for (spec, v, want) in cases {
6370            assert_eq!(fmt(spec, *v), *want, "spec={spec} v={v}");
6371        }
6372    }
6373
6374    // ── format ───────────────────────────────────────────────────────────
6375
6376    #[test]
6377    fn test_format_specifiers() {
6378        let f = FormatFunc;
6379        let result = f
6380            .invoke(&[
6381                SqliteValue::Text(SmallText::from_string("%d %s")),
6382                SqliteValue::Integer(42),
6383                SqliteValue::Text(SmallText::from_string("hello")),
6384            ])
6385            .unwrap();
6386        assert_eq!(
6387            result,
6388            SqliteValue::Text(SmallText::from_string("42 hello"))
6389        );
6390    }
6391
6392    #[test]
6393    fn test_format_n_noop() {
6394        let f = FormatFunc;
6395        // %n should not crash or do anything
6396        let result = f
6397            .invoke(&[SqliteValue::Text(SmallText::from_string("before%nafter"))])
6398            .unwrap();
6399        assert_eq!(
6400            result,
6401            SqliteValue::Text(SmallText::from_string("beforeafter"))
6402        );
6403    }
6404
6405    #[test]
6406    fn test_format_literal_percent_honors_width() {
6407        // bd-g27fn: a literal `%` conversion honors the field width, space-padded
6408        // and right/left-justified (the `0` flag pads with spaces since `%` is
6409        // non-numeric). Oracle: sqlite3 3.46.1.
6410        let f = FormatFunc;
6411        let fmt = |spec: &str| -> String {
6412            match f
6413                .invoke(&[SqliteValue::Text(SmallText::from_string(spec))])
6414                .unwrap()
6415            {
6416                SqliteValue::Text(s) => s.as_str().to_owned(),
6417                other => panic!("expected text, got {other:?}"),
6418            }
6419        };
6420        assert_eq!(fmt("%%"), "%");
6421        assert_eq!(fmt("%5%"), "    %");
6422        assert_eq!(fmt("%-5%"), "%    ");
6423        assert_eq!(fmt("%05%"), "    %");
6424        assert_eq!(fmt("[%3%]"), "[  %]");
6425    }
6426
6427    #[test]
6428    fn test_format_alternate_form_hex_octal() {
6429        // bd-w54bm: `#` flag prefixes 0x/0X (hex) or 0 (octal) for nonzero values.
6430        let cases: &[(&str, i64, &str)] = &[
6431            ("%#x", 255, "0xff"),
6432            ("%#X", 255, "0XFF"),
6433            ("%#o", 64, "0100"),
6434            ("%#x", 0, "0"),        // zero gets no prefix
6435            ("%#o", 0, "0"),        // zero gets no prefix
6436            ("%#5x", 255, " 0xff"), // prefix counts toward space pad
6437            ("%#8x", 255, "    0xff"),
6438            ("%#08x", 255, "0x000000ff"), // zero pad pads digits, prefix outside
6439            ("%-#8x", 255, "0xff    "),   // '-' (no '0') -> space pad, left aligned
6440            ("%-08x", 255, "000000ff"),   // '-' does NOT override '0' in SQLite
6441            ("%#08o", 64, "000000100"),
6442            ("%#x", -1, "0xffffffffffffffff"),
6443        ];
6444        for (fmt, arg, want) in cases {
6445            let f = FormatFunc;
6446            let result = f
6447                .invoke(&[
6448                    SqliteValue::Text(SmallText::from_string(*fmt)),
6449                    SqliteValue::Integer(*arg),
6450                ])
6451                .unwrap();
6452            assert_eq!(
6453                result,
6454                SqliteValue::Text(SmallText::from_string((*want).to_owned())),
6455                "format({fmt:?}, {arg})"
6456            );
6457        }
6458    }
6459
6460    #[test]
6461    fn test_format_empty_string_is_null() {
6462        // bd-13ivh: an empty format string yields NULL (the StrAccum is never
6463        // touched), while a non-empty format that renders to nothing still
6464        // yields empty TEXT.
6465        let f = FormatFunc;
6466        assert_eq!(
6467            f.invoke(&[SqliteValue::Text(SmallText::from_string(""))])
6468                .unwrap(),
6469            SqliteValue::Null
6470        );
6471        // Non-empty format rendering to empty output is still TEXT, not NULL.
6472        assert_eq!(
6473            f.invoke(&[
6474                SqliteValue::Text(SmallText::from_string("%s")),
6475                SqliteValue::Null,
6476            ])
6477            .unwrap(),
6478            SqliteValue::Text(SmallText::from_string(String::new()))
6479        );
6480    }
6481
6482    // ── sqlite_version ───────────────────────────────────────────────────
6483
6484    #[test]
6485    fn test_sqlite_version_format() {
6486        let result = SqliteVersionFunc.invoke(&[]).unwrap();
6487        match result {
6488            SqliteValue::Text(v) => {
6489                assert_eq!(v.split('.').count(), 3, "version must be N.N.N format");
6490            }
6491            other => unreachable!("expected text, got {other:?}"),
6492        }
6493    }
6494
6495    #[test]
6496    fn test_sqlite_compileoption_used_matches_sqlite_prefix_and_value_options() {
6497        let func = SqliteCompileoptionUsedFunc;
6498        assert_eq!(
6499            invoke1(
6500                &func,
6501                SqliteValue::Text(SmallText::from_string("THREADSAFE"))
6502            )
6503            .unwrap(),
6504            SqliteValue::Integer(1)
6505        );
6506        let expected_icu_enabled = i64::from(cfg!(feature = "ext-icu"));
6507        assert_eq!(
6508            invoke1(
6509                &func,
6510                SqliteValue::Text(SmallText::from_string("SQLITE_ENABLE_ICU"))
6511            )
6512            .unwrap(),
6513            SqliteValue::Integer(expected_icu_enabled)
6514        );
6515        assert_eq!(
6516            invoke1(
6517                &func,
6518                SqliteValue::Text(SmallText::from_string("sqlite_enable_icu"))
6519            )
6520            .unwrap(),
6521            SqliteValue::Integer(expected_icu_enabled)
6522        );
6523        assert_eq!(
6524            invoke1(
6525                &func,
6526                SqliteValue::Text(SmallText::from_string("OMIT_LOAD_EXTENSION"))
6527            )
6528            .unwrap(),
6529            SqliteValue::Integer(1)
6530        );
6531        assert_eq!(
6532            invoke1(
6533                &func,
6534                SqliteValue::Text(SmallText::from_string("ENABLE_FTS3"))
6535            )
6536            .unwrap(),
6537            SqliteValue::Integer(0)
6538        );
6539        assert_eq!(
6540            invoke1(&func, SqliteValue::Null).unwrap(),
6541            SqliteValue::Null
6542        );
6543    }
6544
6545    #[test]
6546    #[ignore = "perf-only benchmark"]
6547    fn perf_compileoption_used_text_args() {
6548        use std::hint::black_box;
6549        use std::time::Instant;
6550
6551        const INVOCATIONS: usize = 1_000_000;
6552        const REPEATS: usize = 7;
6553
6554        let f = SqliteCompileoptionUsedFunc;
6555        let present_args = [SqliteValue::Text(SmallText::from_string(
6556            "SQLITE_ENABLE_ICU",
6557        ))];
6558        let absent_args = [SqliteValue::Text(SmallText::from_string(
6559            "ENABLE_NOT_PRESENT",
6560        ))];
6561
6562        let mut present_best_ns = u128::MAX;
6563        let mut absent_best_ns = u128::MAX;
6564        let mut checksum = 0i64;
6565        for _ in 0..REPEATS {
6566            let started = Instant::now();
6567            for _ in 0..INVOCATIONS {
6568                let result = black_box(
6569                    f.invoke(black_box(present_args.as_slice()))
6570                        .expect("compileoption present benchmark invocation must succeed"),
6571                );
6572                if let SqliteValue::Integer(value) = result {
6573                    checksum = checksum.wrapping_add(value);
6574                }
6575            }
6576            present_best_ns = present_best_ns.min(started.elapsed().as_nanos());
6577
6578            let started = Instant::now();
6579            for _ in 0..INVOCATIONS {
6580                let result = black_box(
6581                    f.invoke(black_box(absent_args.as_slice()))
6582                        .expect("compileoption absent benchmark invocation must succeed"),
6583                );
6584                if let SqliteValue::Integer(value) = result {
6585                    checksum = checksum.wrapping_add(value);
6586                }
6587            }
6588            absent_best_ns = absent_best_ns.min(started.elapsed().as_nanos());
6589        }
6590
6591        println!(
6592            "compileoption_used_text_args invocations={INVOCATIONS} repeats={REPEATS} present_best_ns={present_best_ns} absent_best_ns={absent_best_ns} checksum={checksum}"
6593        );
6594    }
6595
6596    #[test]
6597    fn test_sqlite_compileoption_get_enumerates_canonical_option_list() {
6598        let func = SqliteCompileoptionGetFunc;
6599        for (index, option) in sqlite_compile_options().iter().enumerate() {
6600            assert_eq!(
6601                invoke1(&func, SqliteValue::Integer(index as i64)).unwrap(),
6602                SqliteValue::Text(SmallText::new(option))
6603            );
6604        }
6605        assert_eq!(
6606            invoke1(&func, SqliteValue::Integer(-1)).unwrap(),
6607            SqliteValue::Null
6608        );
6609        assert_eq!(
6610            invoke1(
6611                &func,
6612                SqliteValue::Integer(sqlite_compile_options().len() as i64)
6613            )
6614            .unwrap(),
6615            SqliteValue::Null
6616        );
6617    }
6618
6619    // ── register_builtins ────────────────────────────────────────────────
6620
6621    #[test]
6622    fn test_register_builtins_all_present() {
6623        let mut registry = FunctionRegistry::new();
6624        register_builtins(&mut registry);
6625
6626        // Spot-check key functions are registered
6627        assert!(registry.find_scalar("abs", 1).is_some());
6628        assert!(registry.find_scalar("typeof", 1).is_some());
6629        assert!(registry.find_scalar("length", 1).is_some());
6630        assert!(registry.find_scalar("lower", 1).is_some());
6631        assert!(registry.find_scalar("upper", 1).is_some());
6632        assert!(registry.find_scalar("hex", 1).is_some());
6633        assert!(registry.find_scalar("coalesce", 3).is_some());
6634        assert!(registry.find_scalar("concat", 2).is_some());
6635        assert!(registry.find_scalar("like", 2).is_some());
6636        assert!(registry.find_scalar("glob", 2).is_some());
6637        assert!(registry.find_scalar("round", 1).is_some());
6638        assert!(registry.find_scalar("substr", 2).is_some());
6639        assert!(registry.find_scalar("substring", 3).is_some());
6640        assert!(registry.find_scalar("sqlite_version", 0).is_some());
6641        assert!(registry.find_scalar("iif", 3).is_some());
6642        assert!(registry.find_scalar("if", 3).is_some());
6643        assert!(registry.find_scalar("format", 1).is_some());
6644        assert!(registry.find_scalar("printf", 1).is_some());
6645        assert!(registry.find_scalar("max", 2).is_some());
6646        assert!(registry.find_scalar("min", 2).is_some());
6647        assert!(registry.find_scalar("sign", 1).is_some());
6648        assert!(registry.find_scalar("random", 0).is_some());
6649
6650        // Newer SQLite scalar functions (3.41+)
6651        assert!(registry.find_scalar("concat_ws", 3).is_some());
6652        assert!(registry.find_scalar("octet_length", 1).is_some());
6653        assert!(registry.find_scalar("unhex", 1).is_some());
6654        assert!(registry.find_scalar("timediff", 2).is_some());
6655        assert!(registry.find_scalar("unistr", 1).is_some());
6656        assert!(registry.find_scalar("unistr_quote", 1).is_some());
6657
6658        // Percentile family enabled by default.
6659        assert!(registry.find_aggregate("median", 1).is_some());
6660        assert!(registry.find_aggregate("percentile", 2).is_some());
6661        assert!(registry.find_aggregate("percentile_cont", 2).is_some());
6662        assert!(registry.find_aggregate("percentile_disc", 2).is_some());
6663
6664        // Loadable extensions are not exposed as SQL function by default.
6665        assert!(registry.find_scalar("load_extension", 1).is_none());
6666        assert!(registry.find_scalar("load_extension", 2).is_none());
6667    }
6668
6669    #[test]
6670    fn test_register_builtins_rejects_invalid_variadic_arities() {
6671        let mut registry = FunctionRegistry::new();
6672        register_builtins(&mut registry);
6673
6674        for (name, too_few, valid, too_many) in [
6675            ("coalesce", 1, 2, None),
6676            ("concat", 0, 1, None),
6677            ("concat_ws", 1, 2, None),
6678            ("trim", 0, 1, Some(3)),
6679            ("ltrim", 0, 1, Some(3)),
6680            ("rtrim", 0, 1, Some(3)),
6681            ("round", 0, 1, Some(3)),
6682            ("unhex", 0, 1, Some(3)),
6683            ("substr", 1, 2, Some(4)),
6684            ("substring", 1, 2, Some(4)),
6685            ("max", 0, 1, None),
6686            ("min", 0, 1, None),
6687        ] {
6688            assert_wrong_arg_count(&registry, name, too_few);
6689            assert!(
6690                registry.find_scalar(name, valid).is_some(),
6691                "{name}/{valid} should resolve"
6692            );
6693            if let Some(arity) = too_many {
6694                assert_wrong_arg_count(&registry, name, arity);
6695            }
6696        }
6697
6698        assert!(registry.find_scalar("char", 0).is_some());
6699        assert!(registry.find_scalar("format", 0).is_some());
6700        assert!(registry.find_scalar("printf", 0).is_some());
6701    }
6702
6703    #[test]
6704    fn test_e2e_registry_invoke_through_lookup() {
6705        let mut registry = FunctionRegistry::new();
6706        register_builtins(&mut registry);
6707
6708        // Look up abs, invoke it
6709        let abs = registry.find_scalar("ABS", 1).unwrap();
6710        assert_eq!(
6711            abs.invoke(&[SqliteValue::Integer(-42)]).unwrap(),
6712            SqliteValue::Integer(42)
6713        );
6714
6715        // Look up typeof, invoke it
6716        let typeof_fn = registry.find_scalar("typeof", 1).unwrap();
6717        assert_eq!(
6718            typeof_fn
6719                .invoke(&[SqliteValue::Text(SmallText::from_string("hello"))])
6720                .unwrap(),
6721            SqliteValue::Text(SmallText::from_string("text"))
6722        );
6723
6724        // Look up coalesce (variadic), invoke with 4 args
6725        let coalesce = registry.find_scalar("COALESCE", 4).unwrap();
6726        assert_eq!(
6727            coalesce
6728                .invoke(&[
6729                    SqliteValue::Null,
6730                    SqliteValue::Null,
6731                    SqliteValue::Integer(42),
6732                    SqliteValue::Integer(99),
6733                ])
6734                .unwrap(),
6735            SqliteValue::Integer(42)
6736        );
6737    }
6738
6739    // ── bd-13r.8: Non-Deterministic Function Evaluation Semantics ──
6740
6741    #[test]
6742    fn test_nondeterministic_functions_flagged() {
6743        // These functions MUST be marked non-deterministic to prevent
6744        // unsafe planner optimizations (hoisting, CSE).
6745        assert!(!RandomFunc.is_deterministic());
6746        assert!(!RandomblobFunc.is_deterministic());
6747        assert!(!ChangesFunc.is_deterministic());
6748        assert!(!TotalChangesFunc.is_deterministic());
6749        assert!(!LastInsertRowidFunc.is_deterministic());
6750        assert!(!SqliteVersionFunc.is_deterministic());
6751        assert!(!SqliteSourceIdFunc.is_deterministic());
6752        assert!(!SqliteCompileoptionUsedFunc.is_deterministic());
6753        assert!(!SqliteCompileoptionGetFunc.is_deterministic());
6754    }
6755
6756    #[test]
6757    fn test_deterministic_functions_flagged() {
6758        // Deterministic functions are safe for constant folding/CSE.
6759        assert!(AbsFunc.is_deterministic());
6760        assert!(LengthFunc.is_deterministic());
6761        assert!(TypeofFunc.is_deterministic());
6762        assert!(UpperFunc.is_deterministic());
6763        assert!(LowerFunc.is_deterministic());
6764        assert!(HexFunc.is_deterministic());
6765        assert!(CoalesceFunc.is_deterministic());
6766        assert!(IifFunc.is_deterministic());
6767    }
6768
6769    #[test]
6770    fn test_random_produces_different_values() {
6771        // random() should produce different values on successive calls
6772        // (verifying per-call evaluation, not constant folding).
6773        let a = RandomFunc.invoke(&[]).unwrap();
6774        let b = RandomFunc.invoke(&[]).unwrap();
6775        // With overwhelming probability, two random i64 values differ.
6776        // If they're ever equal, it's a 1-in-2^64 coincidence.
6777        assert_ne!(a.as_integer(), b.as_integer());
6778    }
6779
6780    #[test]
6781    fn test_registry_nondeterministic_lookup() {
6782        let mut registry = FunctionRegistry::default();
6783        register_builtins(&mut registry);
6784
6785        // Non-deterministic functions should be findable and flagged.
6786        let random = registry.find_scalar("random", 0).unwrap();
6787        assert!(!random.is_deterministic());
6788
6789        let changes = registry.find_scalar("changes", 0).unwrap();
6790        assert!(!changes.is_deterministic());
6791
6792        let lir = registry.find_scalar("last_insert_rowid", 0).unwrap();
6793        assert!(!lir.is_deterministic());
6794
6795        for (name, num_args) in [
6796            ("sqlite_version", 0),
6797            ("sqlite_source_id", 0),
6798            ("sqlite_compileoption_used", 1),
6799            ("sqlite_compileoption_get", 1),
6800        ] {
6801            assert_eq!(
6802                registry.scalar_is_deterministic(name, num_args),
6803                Some(false),
6804                "{name} must publish non-deterministic registry metadata"
6805            );
6806        }
6807
6808        // Deterministic function check.
6809        let abs = registry.find_scalar("abs", 1).unwrap();
6810        assert!(abs.is_deterministic());
6811    }
6812
6813    #[test]
6814    fn test_registry_builtin_query_constancy_metadata() {
6815        use crate::{ScalarQueryConstancy, ScalarSchemaSafety};
6816
6817        let mut registry = FunctionRegistry::default();
6818        register_builtins(&mut registry);
6819
6820        for (name, num_args) in [
6821            ("sqlite_version", 0),
6822            ("sqlite_source_id", 0),
6823            ("sqlite_compileoption_used", 1),
6824            ("sqlite_compileoption_get", 1),
6825        ] {
6826            let resolved = registry.resolve_scalar(name, num_args).unwrap();
6827            assert_eq!(resolved.schema_safety(), ScalarSchemaSafety::Never);
6828            assert_eq!(
6829                resolved.query_constancy(),
6830                ScalarQueryConstancy::SlowChanging,
6831                "{name}/{num_args} must match SQLite's slow-changing metadata"
6832            );
6833        }
6834
6835        for (name, num_args) in [
6836            ("date", 0),
6837            ("time", 0),
6838            ("datetime", 0),
6839            ("julianday", 0),
6840            ("unixepoch", 0),
6841            ("strftime", 1),
6842            ("timediff", 2),
6843        ] {
6844            let resolved = registry.resolve_scalar(name, num_args).unwrap();
6845            assert_eq!(
6846                resolved.schema_safety(),
6847                ScalarSchemaSafety::DateTimeConditional
6848            );
6849            assert_eq!(
6850                resolved.query_constancy(),
6851                ScalarQueryConstancy::SlowChanging,
6852                "{name}/{num_args} must be query-constant despite conditional schema safety"
6853            );
6854        }
6855
6856        for (name, num_args) in [
6857            ("random", 0),
6858            ("randomblob", 1),
6859            ("changes", 0),
6860            ("total_changes", 0),
6861            ("last_insert_rowid", 0),
6862        ] {
6863            assert_eq!(
6864                registry
6865                    .resolve_scalar(name, num_args)
6866                    .unwrap()
6867                    .query_constancy(),
6868                ScalarQueryConstancy::Volatile,
6869                "{name}/{num_args} must remain volatile"
6870            );
6871        }
6872
6873        for (name, num_args) in [("abs", 1), ("like", 2), ("like", 3), ("glob", 2)] {
6874            assert_eq!(
6875                registry
6876                    .resolve_scalar(name, num_args)
6877                    .unwrap()
6878                    .query_constancy(),
6879                ScalarQueryConstancy::Constant,
6880                "{name}/{num_args} must remain constant"
6881            );
6882        }
6883
6884        for (name, num_args) in [
6885            ("sqlite_version", 1),
6886            ("sqlite_compileoption_used", 0),
6887            ("like", 1),
6888            ("like", 4),
6889            ("glob", 1),
6890        ] {
6891            assert_eq!(
6892                registry
6893                    .resolve_scalar(name, num_args)
6894                    .unwrap()
6895                    .query_constancy(),
6896                ScalarQueryConstancy::Volatile,
6897                "{name}/{num_args} wrong-arity sentinel must fail closed"
6898            );
6899        }
6900    }
6901}