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