Skip to main content

fsqlite_types/
value.rs

1use std::cell::RefCell;
2use std::cmp::Ordering;
3use std::fmt;
4use std::hash::{Hash, Hasher};
5use std::sync::{Arc, OnceLock};
6
7use memchr::{memchr, memchr2, memmem};
8
9use crate::{StorageClass, StrictColumnType, StrictTypeError, TypeAffinity};
10
11// ============================================================================
12// Thread-Local Value Slab Allocator
13// ============================================================================
14
15/// Maximum number of values to keep in the thread-local pool.
16///
17/// 256 is chosen as a balance: large enough to cover typical row batch sizes,
18/// small enough to avoid unbounded memory retention per thread.
19const VALUE_POOL_CAP: usize = 256;
20
21thread_local! {
22    /// Thread-local pool of reusable `SqliteValue` objects.
23    ///
24    /// During hot-path execution (MakeRecord, Column decode), values are
25    /// acquired from this pool instead of allocating fresh, then returned
26    /// when the register is overwritten or the row changes.
27    static VALUE_POOL: RefCell<Vec<SqliteValue>> = const { RefCell::new(Vec::new()) };
28}
29
30#[cfg(test)]
31#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
32struct ValuePoolStats {
33    slab_alloc_count: usize,
34    slab_return_count: usize,
35    global_alloc_fallback_count: usize,
36    slab_high_water_mark: usize,
37}
38
39#[cfg(test)]
40impl ValuePoolStats {
41    const fn new() -> Self {
42        Self {
43            slab_alloc_count: 0,
44            slab_return_count: 0,
45            global_alloc_fallback_count: 0,
46            slab_high_water_mark: 0,
47        }
48    }
49}
50
51#[cfg(test)]
52thread_local! {
53    static VALUE_POOL_TEST_STATS: RefCell<ValuePoolStats> =
54        const { RefCell::new(ValuePoolStats::new()) };
55}
56
57#[cfg(test)]
58fn reset_value_pool_test_stats() {
59    VALUE_POOL_TEST_STATS.with(|stats| *stats.borrow_mut() = ValuePoolStats::new());
60}
61
62#[cfg(test)]
63fn value_pool_test_stats_snapshot() -> ValuePoolStats {
64    VALUE_POOL_TEST_STATS.with(|stats| *stats.borrow())
65}
66
67#[cfg(test)]
68fn record_value_pool_acquire(hit: bool) {
69    VALUE_POOL_TEST_STATS.with(|stats| {
70        let mut stats = stats.borrow_mut();
71        if hit {
72            stats.slab_alloc_count += 1;
73        } else {
74            stats.global_alloc_fallback_count += 1;
75        }
76    });
77}
78
79#[cfg(test)]
80fn record_value_pool_return(pool_len: usize) {
81    VALUE_POOL_TEST_STATS.with(|stats| {
82        let mut stats = stats.borrow_mut();
83        stats.slab_return_count += 1;
84        stats.slab_high_water_mark = stats.slab_high_water_mark.max(pool_len);
85    });
86}
87
88/// Acquire a value from the thread-local pool, if available.
89///
90/// Returns `Some(value)` if a pooled value was available, `None` otherwise.
91/// The returned value may contain stale data and should be overwritten
92/// with the desired variant before use.
93///
94/// # Example
95/// ```ignore
96/// let value = pool_acquire().unwrap_or(SqliteValue::Null);
97/// // Overwrite with actual data
98/// value = SqliteValue::Integer(42);
99/// ```
100#[inline]
101pub fn pool_acquire() -> Option<SqliteValue> {
102    let value = VALUE_POOL.with(|pool| pool.borrow_mut().pop());
103    #[cfg(test)]
104    record_value_pool_acquire(value.is_some());
105    value
106}
107
108/// Return a value to the thread-local pool for reuse.
109///
110/// Values are only pooled if the pool has capacity (max 256 entries).
111/// Excess values are dropped normally, preventing unbounded memory growth.
112///
113/// For best effect, return values just before they would be dropped,
114/// allowing future `pool_acquire` calls to skip allocation.
115#[inline]
116pub fn pool_return(value: SqliteValue) {
117    VALUE_POOL.with(|pool| {
118        let mut pool = pool.borrow_mut();
119        if pool.len() < VALUE_POOL_CAP {
120            pool.push(value);
121            #[cfg(test)]
122            record_value_pool_return(pool.len());
123        }
124        // If pool is full, value is dropped normally
125    });
126}
127
128/// Return a heap-carrying value to the thread-local pool when preserving its
129/// backing allocation is likely to pay off on the next decode/write.
130#[inline]
131pub fn pool_return_reusable(value: SqliteValue) {
132    if value_preserves_reusable_heap_storage(&value) {
133        pool_return(value);
134    }
135}
136
137/// Clear the thread-local value pool.
138///
139/// Use this to release memory when a thread's workload is complete,
140/// or in test teardown to ensure deterministic behavior.
141#[inline]
142pub fn pool_clear() {
143    VALUE_POOL.with(|pool| pool.borrow_mut().clear());
144}
145
146/// Returns the current number of values in the thread-local pool.
147///
148/// Useful for testing and diagnostics.
149#[inline]
150pub fn pool_len() -> usize {
151    VALUE_POOL.with(|pool| pool.borrow().len())
152}
153
154#[inline]
155fn value_preserves_reusable_heap_storage(value: &SqliteValue) -> bool {
156    match value {
157        SqliteValue::Text(text) => matches!(&text.repr, SmallTextRepr::HeapOwned { .. }),
158        SqliteValue::Blob(bytes) => Arc::strong_count(bytes) == 1,
159        _ => false,
160    }
161}
162
163/// Maximum inline string length for `SmallText`.
164///
165/// Strings up to this length are stored inline (on the stack/in the struct)
166/// without heap allocation. Longer strings fall back to `Arc<str>`.
167///
168/// 23 bytes inline + 1 byte for length/tag = 24 bytes total, which aligns
169/// with common cache line fractions and matches Arc<str>'s pointer size.
170const SMALL_TEXT_INLINE_CAP: usize = 23;
171
172#[cfg(feature = "bench-internals")]
173static SMALL_TEXT_DIRECT_TRAITS_FOR_BENCH: std::sync::atomic::AtomicBool =
174    std::sync::atomic::AtomicBool::new(false);
175#[cfg(feature = "bench-internals")]
176static SMALL_TEXT_DIRECT_TRAIT_HITS_FOR_BENCH: std::sync::atomic::AtomicU64 =
177    std::sync::atomic::AtomicU64::new(0);
178
179/// Select the historical direct-byte `SmallText` trait candidate.
180#[cfg(feature = "bench-internals")]
181#[doc(hidden)]
182pub fn set_small_text_direct_traits_for_bench(enabled: bool) {
183    SMALL_TEXT_DIRECT_TRAITS_FOR_BENCH.store(enabled, std::sync::atomic::Ordering::Relaxed);
184}
185
186/// Reset the exact trait-call counter for the direct-byte candidate.
187#[cfg(feature = "bench-internals")]
188#[doc(hidden)]
189pub fn reset_small_text_direct_trait_hits_for_bench() {
190    SMALL_TEXT_DIRECT_TRAIT_HITS_FOR_BENCH.store(0, std::sync::atomic::Ordering::Relaxed);
191}
192
193/// Return the exact number of candidate direct-byte trait calls.
194#[cfg(feature = "bench-internals")]
195#[doc(hidden)]
196#[must_use]
197pub fn small_text_direct_trait_hits_for_bench() -> u64 {
198    SMALL_TEXT_DIRECT_TRAIT_HITS_FOR_BENCH.load(std::sync::atomic::Ordering::Relaxed)
199}
200
201/// A small-string-optimized SQLite TEXT value.
202///
203/// Stores strings ≤ 23 bytes inline without heap allocation. Longer strings
204/// stay owned until the first clone, then lazily promote to `Arc<str>` for
205/// shared O(1) cloning. SQLite also permits TEXT values whose byte payload is
206/// not valid UTF-8; those use a byte-preserving raw representation plus a
207/// cached lossy view for Rust APIs that require `&str`.
208pub struct SmallText {
209    /// Representation: either inline bytes or a lazily shared heap string.
210    repr: SmallTextRepr,
211}
212
213/// Internal representation for SmallText.
214enum SmallTextRepr {
215    /// Inline storage: length followed by up to 23 UTF-8 bytes.
216    Inline {
217        len: u8,
218        buf: [u8; SMALL_TEXT_INLINE_CAP],
219    },
220    /// Heap storage before the first clone.
221    ///
222    /// The `Arc<str>` is materialized lazily on demand so a single-owner value
223    /// pays no refcount cost until it is actually shared.
224    HeapOwned {
225        text: String,
226        shared: OnceLock<Arc<str>>,
227    },
228    /// Heap storage after the text has been shared.
229    HeapShared(Arc<str>),
230    /// Byte-preserving storage for SQLite TEXT that is not valid UTF-8.
231    ///
232    /// `lossy` exists only to satisfy Rust-facing string APIs. Storage,
233    /// equality, ordering, hashing, and CAST-to-BLOB use `bytes`.
234    Raw { bytes: Arc<[u8]>, lossy: Arc<str> },
235}
236
237impl Clone for SmallText {
238    fn clone(&self) -> Self {
239        Self {
240            repr: self.repr.clone(),
241        }
242    }
243}
244
245impl Clone for SmallTextRepr {
246    fn clone(&self) -> Self {
247        match self {
248            Self::Inline { len, buf } => Self::Inline {
249                len: *len,
250                buf: *buf,
251            },
252            Self::HeapOwned { text, shared } => {
253                let shared = Arc::clone(shared.get_or_init(|| Arc::from(text.as_str())));
254                Self::HeapShared(shared)
255            }
256            Self::HeapShared(text) => Self::HeapShared(Arc::clone(text)),
257            Self::Raw { bytes, lossy } => Self::Raw {
258                bytes: Arc::clone(bytes),
259                lossy: Arc::clone(lossy),
260            },
261        }
262    }
263}
264
265impl SmallText {
266    /// Create a new SmallText from a string slice.
267    #[inline]
268    pub fn new(s: &str) -> Self {
269        if s.len() <= SMALL_TEXT_INLINE_CAP {
270            let mut buf = [0u8; SMALL_TEXT_INLINE_CAP];
271            buf[..s.len()].copy_from_slice(s.as_bytes());
272            Self {
273                repr: SmallTextRepr::Inline {
274                    len: s.len() as u8,
275                    buf,
276                },
277            }
278        } else {
279            Self {
280                repr: SmallTextRepr::HeapOwned {
281                    text: s.to_owned(),
282                    shared: OnceLock::new(),
283                },
284            }
285        }
286    }
287
288    /// Create from an owned String, potentially reusing its allocation.
289    #[inline]
290    pub fn from_string<S>(s: S) -> Self
291    where
292        S: Into<String> + AsRef<str>,
293    {
294        if s.as_ref().len() <= SMALL_TEXT_INLINE_CAP {
295            Self::new(s.as_ref())
296        } else {
297            Self {
298                repr: SmallTextRepr::HeapOwned {
299                    text: s.into(),
300                    shared: OnceLock::new(),
301                },
302            }
303        }
304    }
305
306    /// Create from an `Arc<str>`, avoiding re-allocation if already heap.
307    #[inline]
308    pub fn from_arc(arc: Arc<str>) -> Self {
309        if arc.len() <= SMALL_TEXT_INLINE_CAP {
310            Self::new(&arc)
311        } else {
312            Self {
313                repr: SmallTextRepr::HeapShared(arc),
314            }
315        }
316    }
317
318    /// Create SQLite TEXT from its exact byte payload.
319    ///
320    /// Valid UTF-8 retains the usual inline/owned fast paths. Invalid UTF-8 is
321    /// kept byte-for-byte and exposed through [`Self::as_bytes_direct`].
322    #[inline]
323    pub fn from_bytes(bytes: &[u8]) -> Self {
324        match simdutf8::basic::from_utf8(bytes) {
325            Ok(text) => Self::new(text),
326            Err(_) => Self::from_raw_bytes(Arc::from(bytes)),
327        }
328    }
329
330    /// Create SQLite TEXT from shared exact bytes, retaining the allocation
331    /// when the payload is not valid UTF-8.
332    #[inline]
333    pub fn from_arc_bytes(bytes: Arc<[u8]>) -> Self {
334        match simdutf8::basic::from_utf8(&bytes) {
335            Ok(text) => Self::new(text),
336            Err(_) => Self::from_raw_bytes(bytes),
337        }
338    }
339
340    #[inline]
341    fn from_raw_bytes(bytes: Arc<[u8]>) -> Self {
342        debug_assert!(simdutf8::basic::from_utf8(&bytes).is_err());
343        let lossy: Arc<str> = Arc::from(String::from_utf8_lossy(&bytes).into_owned());
344        Self {
345            repr: SmallTextRepr::Raw { bytes, lossy },
346        }
347    }
348
349    /// Overwrite this string, reusing the existing heap allocation when the
350    /// value is still single-owner.
351    #[inline]
352    pub fn overwrite(&mut self, s: &str) {
353        if s.len() <= SMALL_TEXT_INLINE_CAP {
354            let mut buf = [0u8; SMALL_TEXT_INLINE_CAP];
355            buf[..s.len()].copy_from_slice(s.as_bytes());
356            self.repr = SmallTextRepr::Inline {
357                len: s.len() as u8,
358                buf,
359            };
360            return;
361        }
362
363        match &mut self.repr {
364            SmallTextRepr::HeapOwned { text, shared } => {
365                text.clear();
366                text.push_str(s);
367                if shared.get().is_some() {
368                    *shared = OnceLock::new();
369                }
370            }
371            _ => {
372                self.repr = SmallTextRepr::HeapOwned {
373                    text: s.to_owned(),
374                    shared: OnceLock::new(),
375                };
376            }
377        }
378    }
379
380    /// Overwrite this value from an exact SQLite TEXT byte payload.
381    #[inline]
382    pub fn overwrite_bytes(&mut self, bytes: &[u8]) {
383        match simdutf8::basic::from_utf8(bytes) {
384            Ok(text) => self.overwrite(text),
385            Err(_) => *self = Self::from_raw_bytes(Arc::from(bytes)),
386        }
387    }
388
389    /// Get the string as a slice.
390    ///
391    /// OPT-UTF8: the inline buffer is always valid UTF-8 by construction (see
392    /// the constructors and [`Self::overwrite`]), but because
393    /// `forbid(unsafe_code)` prevents `from_utf8_unchecked` we must run a
394    /// validator. `simdutf8::basic::from_utf8` is a drop-in for
395    /// `std::str::from_utf8` that uses runtime-dispatched SIMD and is
396    /// ~3-10x faster on the ASCII-dominant TEXT payloads that make up the
397    /// majority of real SQL workloads.
398    #[inline]
399    pub fn as_str(&self) -> &str {
400        match &self.repr {
401            SmallTextRepr::Inline { len, buf } => simdutf8::basic::from_utf8(&buf[..*len as usize])
402                .expect("SmallText inline representation must always contain valid UTF-8"),
403            SmallTextRepr::HeapOwned { text, .. } => text.as_str(),
404            SmallTextRepr::HeapShared(text) => text,
405            SmallTextRepr::Raw { lossy, .. } => lossy,
406        }
407    }
408
409    /// Return a borrowed string only when the exact TEXT payload is UTF-8.
410    #[inline]
411    #[must_use]
412    pub fn as_str_checked(&self) -> Option<&str> {
413        match &self.repr {
414            SmallTextRepr::Raw { .. } => None,
415            _ => Some(self.as_str()),
416        }
417    }
418
419    /// Whether the exact TEXT payload is valid UTF-8.
420    #[inline]
421    #[must_use]
422    pub fn is_valid_utf8(&self) -> bool {
423        !matches!(&self.repr, SmallTextRepr::Raw { .. })
424    }
425
426    /// Get the raw bytes of this text value without going through `&str`.
427    ///
428    /// Unlike [`Self::as_str`] (which returns a cached lossy view for a raw
429    /// payload), this directly returns the exact stored bytes. The slice can
430    /// therefore be invalid UTF-8 when the value came from SQLite record bytes
431    /// or a BLOB-to-TEXT cast.
432    ///
433    /// This is useful on hot paths where a byte-wise equality check is the
434    /// only operation performed — for example, the record-decode fast path
435    /// that reuses an existing slot when incoming bytes match what is already
436    /// there. Skipping the internal `from_utf8` of `as_str` measurably
437    /// reduces per-column decode cost on INSERT/SELECT workloads.
438    #[inline]
439    #[must_use]
440    pub fn as_bytes_direct(&self) -> &[u8] {
441        match &self.repr {
442            SmallTextRepr::Inline { len, buf } => &buf[..*len as usize],
443            SmallTextRepr::HeapOwned { text, .. } => text.as_bytes(),
444            SmallTextRepr::HeapShared(text) => text.as_bytes(),
445            SmallTextRepr::Raw { bytes, .. } => bytes,
446        }
447    }
448
449    /// Get the length in bytes.
450    #[inline]
451    pub fn len(&self) -> usize {
452        match &self.repr {
453            SmallTextRepr::Inline { len, .. } => *len as usize,
454            SmallTextRepr::HeapOwned { text, .. } => text.len(),
455            SmallTextRepr::HeapShared(text) => text.len(),
456            SmallTextRepr::Raw { bytes, .. } => bytes.len(),
457        }
458    }
459
460    /// Check if empty.
461    #[inline]
462    pub fn is_empty(&self) -> bool {
463        self.len() == 0
464    }
465
466    /// Check if stored inline (no heap allocation).
467    #[inline]
468    pub fn is_inline(&self) -> bool {
469        matches!(&self.repr, SmallTextRepr::Inline { .. })
470    }
471
472    /// Convert to `Arc<str>`, potentially allocating if currently inline.
473    #[inline]
474    pub fn into_arc(self) -> Arc<str> {
475        match self.repr {
476            SmallTextRepr::Inline { len, buf } => {
477                // See `as_str` for the simdutf8 rationale.
478                let s = simdutf8::basic::from_utf8(&buf[..len as usize])
479                    .expect("SmallText inline representation must always contain valid UTF-8");
480                Arc::from(s)
481            }
482            SmallTextRepr::HeapOwned { text, shared } => shared
483                .into_inner()
484                .unwrap_or_else(|| Arc::<str>::from(text)),
485            SmallTextRepr::HeapShared(text) => text,
486            SmallTextRepr::Raw { lossy, .. } => lossy,
487        }
488    }
489}
490
491impl Default for SmallText {
492    #[inline]
493    fn default() -> Self {
494        Self::new("")
495    }
496}
497
498impl fmt::Debug for SmallText {
499    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
500        fmt::Debug::fmt(self.as_str(), f)
501    }
502}
503
504impl fmt::Display for SmallText {
505    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
506        fmt::Display::fmt(self.as_str(), f)
507    }
508}
509
510impl PartialEq for SmallText {
511    #[inline]
512    fn eq(&self, other: &Self) -> bool {
513        #[cfg(feature = "bench-internals")]
514        if SMALL_TEXT_DIRECT_TRAITS_FOR_BENCH.load(std::sync::atomic::Ordering::Relaxed) {
515            SMALL_TEXT_DIRECT_TRAIT_HITS_FOR_BENCH
516                .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
517            return self.as_bytes_direct() == other.as_bytes_direct();
518        }
519        match (self.as_str_checked(), other.as_str_checked()) {
520            (Some(left), Some(right)) => left == right,
521            _ => self.as_bytes_direct() == other.as_bytes_direct(),
522        }
523    }
524}
525
526impl Eq for SmallText {}
527
528impl PartialOrd for SmallText {
529    #[inline]
530    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
531        Some(self.cmp(other))
532    }
533}
534
535impl Ord for SmallText {
536    #[inline]
537    fn cmp(&self, other: &Self) -> Ordering {
538        #[cfg(feature = "bench-internals")]
539        if SMALL_TEXT_DIRECT_TRAITS_FOR_BENCH.load(std::sync::atomic::Ordering::Relaxed) {
540            SMALL_TEXT_DIRECT_TRAIT_HITS_FOR_BENCH
541                .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
542            return self.as_bytes_direct().cmp(other.as_bytes_direct());
543        }
544        match (self.as_str_checked(), other.as_str_checked()) {
545            (Some(left), Some(right)) => left.cmp(right),
546            _ => self.as_bytes_direct().cmp(other.as_bytes_direct()),
547        }
548    }
549}
550
551impl Hash for SmallText {
552    #[inline]
553    fn hash<H: Hasher>(&self, state: &mut H) {
554        #[cfg(feature = "bench-internals")]
555        if SMALL_TEXT_DIRECT_TRAITS_FOR_BENCH.load(std::sync::atomic::Ordering::Relaxed) {
556            SMALL_TEXT_DIRECT_TRAIT_HITS_FOR_BENCH
557                .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
558            state.write(self.as_bytes_direct());
559            state.write_u8(0xff);
560            return;
561        }
562        if let Some(text) = self.as_str_checked() {
563            text.hash(state);
564        } else {
565            state.write(self.as_bytes_direct());
566            state.write_u8(0xff);
567        }
568    }
569}
570
571impl From<&str> for SmallText {
572    #[inline]
573    fn from(s: &str) -> Self {
574        Self::new(s)
575    }
576}
577
578impl From<String> for SmallText {
579    #[inline]
580    fn from(s: String) -> Self {
581        Self::from_string(s)
582    }
583}
584
585impl From<Arc<str>> for SmallText {
586    #[inline]
587    fn from(arc: Arc<str>) -> Self {
588        Self::from_arc(arc)
589    }
590}
591
592impl AsRef<str> for SmallText {
593    #[inline]
594    fn as_ref(&self) -> &str {
595        self.as_str()
596    }
597}
598
599impl std::ops::Deref for SmallText {
600    type Target = str;
601
602    #[inline]
603    fn deref(&self) -> &Self::Target {
604        self.as_str()
605    }
606}
607
608impl std::borrow::Borrow<str> for SmallText {
609    #[inline]
610    fn borrow(&self) -> &str {
611        self.as_str()
612    }
613}
614
615// Serde implementations for SmallText
616impl serde::Serialize for SmallText {
617    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
618    where
619        S: serde::Serializer,
620    {
621        let Some(text) = self.as_str_checked() else {
622            return Err(serde::ser::Error::custom(
623                "SQLite TEXT containing invalid UTF-8 cannot be serialized as a Rust string",
624            ));
625        };
626        serializer.serialize_str(text)
627    }
628}
629
630impl<'de> serde::Deserialize<'de> for SmallText {
631    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
632    where
633        D: serde::Deserializer<'de>,
634    {
635        let s = String::deserialize(deserializer)?;
636        Ok(Self::from_string(s))
637    }
638}
639
640/// Scan the longest SQLite numeric prefix from a byte slice.
641///
642/// Recognises `[+-]? [0-9]* ('.' [0-9]*)? ([eE] [+-]? [0-9]+)?`.
643/// Returns the byte offset where the prefix ends, or 0 if no numeric prefix
644/// is present.
645fn scan_numeric_prefix(bytes: &[u8]) -> usize {
646    if bytes.is_empty() {
647        return 0;
648    }
649
650    let mut i = 0usize;
651    if bytes[i] == b'+' || bytes[i] == b'-' {
652        i += 1;
653    }
654
655    let mut has_digit = false;
656    while i < bytes.len() && bytes[i].is_ascii_digit() {
657        has_digit = true;
658        i += 1;
659    }
660
661    if i < bytes.len() && bytes[i] == b'.' {
662        i += 1;
663        while i < bytes.len() && bytes[i].is_ascii_digit() {
664            has_digit = true;
665            i += 1;
666        }
667    }
668
669    if !has_digit {
670        return 0;
671    }
672
673    if i < bytes.len() && (bytes[i] == b'e' || bytes[i] == b'E') {
674        let exp_start = i;
675        i += 1;
676        if i < bytes.len() && (bytes[i] == b'+' || bytes[i] == b'-') {
677            i += 1;
678        }
679        if i < bytes.len() && bytes[i].is_ascii_digit() {
680            while i < bytes.len() && bytes[i].is_ascii_digit() {
681                i += 1;
682            }
683        } else {
684            i = exp_start;
685        }
686    }
687
688    i
689}
690
691/// Parse the longest numeric prefix of `b` as an integer.
692#[allow(clippy::cast_possible_truncation)]
693fn parse_integer_prefix_bytes(b: &[u8]) -> i64 {
694    let mut start = 0;
695    while start < b.len() && b[start].is_ascii_whitespace() {
696        start += 1;
697    }
698    let trimmed = &b[start..];
699    let end = scan_numeric_prefix(trimmed);
700    if end == 0 {
701        return 0;
702    }
703    // SAFETY: scan_numeric_prefix only advances over ASCII bytes (digits, +, -, ., e, E),
704    // so the slice is always valid UTF-8.
705    let s = std::str::from_utf8(&trimmed[..end]).unwrap_or("");
706    let f = s.parse::<f64>().unwrap_or(0.0);
707    #[allow(clippy::manual_clamp)]
708    if f >= i64::MAX as f64 {
709        i64::MAX
710    } else if f <= i64::MIN as f64 {
711        i64::MIN
712    } else {
713        f as i64
714    }
715}
716
717/// Parse the longest numeric prefix of `s` as an integer.
718#[allow(clippy::cast_possible_truncation)]
719fn parse_integer_prefix(s: &str) -> i64 {
720    parse_integer_prefix_bytes(s.as_bytes())
721}
722
723/// Parse the longest numeric prefix of `b` as a float.
724fn parse_float_prefix_bytes(b: &[u8]) -> f64 {
725    let mut start = 0;
726    while start < b.len() && b[start].is_ascii_whitespace() {
727        start += 1;
728    }
729    let trimmed = &b[start..];
730    let end = scan_numeric_prefix(trimmed);
731    if end == 0 {
732        return 0.0;
733    }
734    // SAFETY: scan_numeric_prefix only advances over ASCII bytes (digits, +, -, ., e, E),
735    // so the slice is always valid UTF-8.
736    let s = std::str::from_utf8(&trimmed[..end]).unwrap_or("");
737    s.parse::<f64>().unwrap_or(0.0)
738}
739
740/// Parse the longest numeric prefix of `s` as a float.
741fn parse_float_prefix(s: &str) -> f64 {
742    parse_float_prefix_bytes(s.as_bytes())
743}
744
745fn trim_sqlite_ascii_whitespace(s: &str) -> &str {
746    s.trim_matches(|ch: char| ch.is_ascii_whitespace())
747}
748
749fn cast_text_prefix_to_numeric(s: &str) -> SqliteValue {
750    let trimmed = trim_sqlite_ascii_whitespace(s);
751    let end = scan_numeric_prefix(trimmed.as_bytes());
752    if end == 0 {
753        return SqliteValue::Integer(0);
754    }
755
756    let prefix = &trimmed[..end];
757    let is_integer_syntax = !prefix
758        .as_bytes()
759        .iter()
760        .any(|byte| matches!(*byte, b'.' | b'e' | b'E'));
761
762    if is_integer_syntax && let Ok(value) = prefix.parse::<i64>() {
763        return SqliteValue::Integer(value);
764    }
765
766    if let Ok(value) = prefix.parse::<f64>() {
767        if value.is_finite()
768            && (-9_223_372_036_854_775_808.0..9_223_372_036_854_775_808.0).contains(&value)
769        {
770            #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
771            let truncated = value as i64;
772            #[allow(clippy::float_cmp, clippy::cast_precision_loss)]
773            if truncated as f64 == value {
774                return SqliteValue::Integer(truncated);
775            }
776        }
777        return SqliteValue::Float(value);
778    }
779
780    SqliteValue::Integer(0)
781}
782
783/// A dynamically-typed SQLite value.
784///
785/// Corresponds to C SQLite's `sqlite3_value` / `Mem` type. SQLite has five
786/// fundamental storage classes: NULL, INTEGER, REAL, TEXT, and BLOB.
787#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
788pub enum SqliteValue {
789    /// A NULL value.
790    Null,
791    /// A signed 64-bit integer.
792    Integer(i64),
793    /// A 64-bit IEEE floating point number.
794    Float(f64),
795    /// A SQLite TEXT value.
796    ///
797    /// Uses `SmallText` for small-string optimization: strings ≤ 23 bytes
798    /// are stored inline without heap allocation. Longer valid UTF-8 strings
799    /// use shared string storage; invalid UTF-8 remains exact raw bytes.
800    Text(SmallText),
801    /// A binary large object.
802    ///
803    /// Uses `Arc<[u8]>` for the same O(1)-clone benefit as `Text`.
804    Blob(Arc<[u8]>),
805}
806
807impl SqliteValue {
808    /// Returns the type affinity that best describes this value.
809    pub const fn affinity(&self) -> TypeAffinity {
810        match self {
811            Self::Null | Self::Blob(_) => TypeAffinity::Blob,
812            Self::Integer(_) => TypeAffinity::Integer,
813            Self::Float(_) => TypeAffinity::Real,
814            Self::Text(_) => TypeAffinity::Text,
815        }
816    }
817
818    /// Returns the storage class of this value.
819    pub const fn storage_class(&self) -> StorageClass {
820        match self {
821            Self::Null => StorageClass::Null,
822            Self::Integer(_) => StorageClass::Integer,
823            Self::Float(_) => StorageClass::Real,
824            Self::Text(_) => StorageClass::Text,
825            Self::Blob(_) => StorageClass::Blob,
826        }
827    }
828
829    /// Apply column type affinity coercion (advisory mode).
830    ///
831    /// In non-STRICT tables, affinity is advisory: values are coerced when
832    /// possible but never rejected. Follows SQLite §3.4 rules from
833    /// <https://www.sqlite.org/datatype3.html#type_affinity_of_a_column>.
834    ///
835    /// - TEXT affinity: numeric values converted to text before storing.
836    /// - NUMERIC affinity: text parsed as integer/real if well-formed; exact-integer reals become integer.
837    /// - INTEGER affinity: identical to NUMERIC for storage/comparison coercion (differ only in CAST).
838    /// - REAL affinity: like NUMERIC, plus integers forced to float.
839    /// - BLOB affinity: no conversion.
840    #[must_use]
841    #[allow(
842        clippy::cast_possible_truncation,
843        clippy::cast_precision_loss,
844        clippy::float_cmp
845    )]
846    pub fn apply_affinity(self, affinity: TypeAffinity) -> Self {
847        match affinity {
848            TypeAffinity::Blob => self,
849            TypeAffinity::Text => match self {
850                Self::Null | Self::Text(_) | Self::Blob(_) => self,
851                Self::Integer(_) | Self::Float(_) => {
852                    let t = self.to_text();
853                    Self::Text(SmallText::from_string(t))
854                }
855            },
856            TypeAffinity::Numeric | TypeAffinity::Integer => match &self {
857                Self::Text(s) => try_coerce_text_to_numeric(s.as_str()).unwrap_or(self),
858                Self::Float(f) => {
859                    if *f >= -9_223_372_036_854_775_808.0 && *f < 9_223_372_036_854_775_808.0 {
860                        let i = *f as i64;
861                        if (i as f64) == *f {
862                            return Self::Integer(i);
863                        }
864                    }
865                    self
866                }
867                _ => self,
868            },
869            TypeAffinity::Real => match &self {
870                Self::Text(s) => try_coerce_text_to_numeric(s.as_str())
871                    .map(|v| match v {
872                        Self::Integer(i) => Self::Float(i as f64),
873                        other => other,
874                    })
875                    .unwrap_or(self),
876                Self::Integer(i) => Self::Float(*i as f64),
877                _ => self,
878            },
879        }
880    }
881
882    /// Validate a value against a STRICT table column type.
883    ///
884    /// NULL is always accepted (nullability is enforced separately via NOT NULL).
885    /// Returns `Ok(value)` with possible implicit coercion (REAL columns accept
886    /// integers, converting them to float), or `Err` if the storage class is
887    /// incompatible.
888    #[allow(clippy::cast_precision_loss)]
889    pub fn validate_strict(self, col_type: StrictColumnType) -> Result<Self, StrictTypeError> {
890        if matches!(self, Self::Null) {
891            return Ok(self);
892        }
893        match col_type {
894            StrictColumnType::Any => Ok(self),
895            StrictColumnType::Integer => match self {
896                Self::Integer(_) => Ok(self),
897                other => Err(StrictTypeError {
898                    expected: col_type,
899                    actual: other.storage_class(),
900                }),
901            },
902            StrictColumnType::Real => match self {
903                Self::Float(_) => Ok(self),
904                Self::Integer(i) => Ok(Self::Float(i as f64)),
905                other => Err(StrictTypeError {
906                    expected: col_type,
907                    actual: other.storage_class(),
908                }),
909            },
910            StrictColumnType::Text => match self {
911                Self::Text(_) => Ok(self),
912                other => Err(StrictTypeError {
913                    expected: col_type,
914                    actual: other.storage_class(),
915                }),
916            },
917            StrictColumnType::Blob => match self {
918                Self::Blob(_) => Ok(self),
919                other => Err(StrictTypeError {
920                    expected: col_type,
921                    actual: other.storage_class(),
922                }),
923            },
924        }
925    }
926
927    /// Returns true if this is a NULL value.
928    #[inline(always)]
929    #[allow(clippy::inline_always)]
930    pub const fn is_null(&self) -> bool {
931        matches!(self, Self::Null)
932    }
933
934    /// Try to extract an integer value.
935    #[inline]
936    pub const fn as_integer(&self) -> Option<i64> {
937        match self {
938            Self::Integer(i) => Some(*i),
939            _ => None,
940        }
941    }
942
943    /// Try to extract a float value.
944    #[inline]
945    pub fn as_float(&self) -> Option<f64> {
946        match self {
947            Self::Float(f) => Some(*f),
948            _ => None,
949        }
950    }
951
952    /// Try to extract a text reference.
953    #[inline]
954    pub fn as_text(&self) -> Option<&str> {
955        match self {
956            Self::Text(s) => Some(s),
957            _ => None,
958        }
959    }
960
961    /// Try to extract a blob reference.
962    #[inline]
963    pub fn as_blob(&self) -> Option<&[u8]> {
964        match self {
965            Self::Blob(b) => Some(b),
966            _ => None,
967        }
968    }
969
970    /// Convert to an integer following SQLite's type coercion rules.
971    ///
972    /// - NULL -> 0
973    /// - Integer -> itself
974    /// - Float -> truncated to i64
975    /// - Text -> attempt to parse, 0 on failure
976    /// - Blob -> parse bytes as numeric string, 0 on failure
977    #[inline(always)]
978    #[allow(clippy::inline_always)]
979    #[allow(clippy::cast_possible_truncation)]
980    pub fn to_integer(&self) -> i64 {
981        match self {
982            Self::Null => 0,
983            Self::Integer(i) => *i,
984            Self::Float(f) => *f as i64,
985            Self::Text(s) => parse_integer_prefix(s),
986            Self::Blob(b) => parse_integer_prefix_bytes(b),
987        }
988    }
989
990    /// Convert to a float following SQLite's type coercion rules.
991    ///
992    /// - NULL -> 0.0
993    /// - Integer -> as f64
994    /// - Float -> itself
995    /// - Text -> attempt to parse, 0.0 on failure
996    /// - Blob -> parse bytes as numeric string, 0.0 on failure
997    #[inline(always)]
998    #[allow(clippy::inline_always)]
999    #[allow(clippy::cast_precision_loss)]
1000    pub fn to_float(&self) -> f64 {
1001        match self {
1002            Self::Null => 0.0,
1003            Self::Integer(i) => *i as f64,
1004            Self::Float(f) => *f,
1005            Self::Text(s) => parse_float_prefix(s),
1006            Self::Blob(b) => parse_float_prefix_bytes(b),
1007        }
1008    }
1009
1010    /// Coerce a value for SQLite `sum()` accumulation.
1011    ///
1012    /// `sum()` keeps integer accumulation only for INTEGER values and text that
1013    /// is entirely a signed 64-bit integer literal after trimming SQLite ASCII
1014    /// whitespace. Other text and all blobs participate through the REAL
1015    /// accumulator, even when their numeric prefix is integer-looking.
1016    #[must_use]
1017    pub fn to_sum_numeric_value(&self) -> Self {
1018        match self {
1019            Self::Null => Self::Null,
1020            Self::Integer(i) => Self::Integer(*i),
1021            Self::Float(f) => Self::Float(*f),
1022            Self::Text(s) => {
1023                let trimmed = trim_sqlite_ascii_whitespace(s.as_str());
1024                if let Ok(integer) = trimmed.parse::<i64>() {
1025                    Self::Integer(integer)
1026                } else {
1027                    Self::Float(parse_float_prefix(s))
1028                }
1029            }
1030            Self::Blob(b) => Self::Float(parse_float_prefix_bytes(b)),
1031        }
1032    }
1033
1034    /// Borrow the inner text string without allocating.
1035    ///
1036    /// Returns `Some(&str)` for `Text` values, `None` otherwise.
1037    /// Use this in comparisons, LIKE patterns, and WHERE clause
1038    /// evaluation to avoid the clone that `to_text()` incurs.
1039    #[inline]
1040    #[must_use]
1041    pub fn as_text_str(&self) -> Option<&str> {
1042        match self {
1043            Self::Text(s) => Some(s),
1044            _ => None,
1045        }
1046    }
1047
1048    /// Borrow the inner blob bytes without allocating.
1049    #[inline]
1050    #[must_use]
1051    pub fn as_blob_bytes(&self) -> Option<&[u8]> {
1052        match self {
1053            Self::Blob(b) => Some(b),
1054            _ => None,
1055        }
1056    }
1057
1058    /// Convert to a Rust `String` representation of this value.
1059    ///
1060    /// This boundary is necessarily lossy for invalid UTF-8 BLOB/TEXT bytes.
1061    /// SQL CAST paths use `SmallText::from_arc_bytes` instead so SQLite TEXT
1062    /// remains byte-preserving. For a SQL-literal hex format (`X'...'`), use
1063    /// the `Display` impl.
1064    pub fn to_text(&self) -> String {
1065        match self {
1066            Self::Null => String::new(),
1067            Self::Integer(i) => i.to_string(),
1068            Self::Float(f) => format_sqlite_float(*f),
1069            Self::Text(s) => s.to_string(),
1070            Self::Blob(b) => String::from_utf8_lossy(b).into_owned(),
1071        }
1072    }
1073
1074    /// Convert to NUMERIC using SQLite CAST semantics rather than affinity.
1075    ///
1076    /// Unlike NUMERIC affinity, CAST always produces a numeric storage class for
1077    /// text/blob input, using the longest leading numeric prefix or `0` when no
1078    /// numeric prefix exists.
1079    #[must_use]
1080    pub fn cast_to_numeric(&self) -> Self {
1081        match self {
1082            Self::Null => Self::Null,
1083            Self::Integer(i) => Self::Integer(*i),
1084            Self::Float(f) => Self::Float(*f),
1085            Self::Text(s) => cast_text_prefix_to_numeric(s),
1086            Self::Blob(b) => cast_text_prefix_to_numeric(&String::from_utf8_lossy(b)),
1087        }
1088    }
1089
1090    /// Returns the SQLite `typeof()` string for this value.
1091    ///
1092    /// Matches C sqlite3: "null", "integer", "real", "text", or "blob".
1093    pub const fn typeof_str(&self) -> &'static str {
1094        match self {
1095            Self::Null => "null",
1096            Self::Integer(_) => "integer",
1097            Self::Float(_) => "real",
1098            Self::Text(_) => "text",
1099            Self::Blob(_) => "blob",
1100        }
1101    }
1102
1103    /// Returns the SQLite `length()` result for this value.
1104    ///
1105    /// - NULL → NULL (represented as None)
1106    /// - TEXT → character count
1107    /// - BLOB → byte count
1108    /// - INTEGER/REAL → character count of text representation
1109    pub fn sql_length(&self) -> Option<i64> {
1110        match self {
1111            Self::Null => None,
1112            Self::Text(s) => Some(i64::try_from(s.chars().count()).unwrap_or(i64::MAX)),
1113            Self::Blob(b) => Some(i64::try_from(b.len()).unwrap_or(i64::MAX)),
1114            Self::Integer(_) | Self::Float(_) => {
1115                let t = self.to_text();
1116                Some(i64::try_from(t.chars().count()).unwrap_or(i64::MAX))
1117            }
1118        }
1119    }
1120
1121    /// Check equality for UNIQUE constraint purposes.
1122    ///
1123    /// In SQLite, NULL != NULL for uniqueness: if either value is NULL, the
1124    /// result is `false` (they are never considered duplicates). Non-NULL values
1125    /// compare by storage class ordering (same as `PartialEq`).
1126    pub fn unique_eq(&self, other: &Self) -> bool {
1127        if self.is_null() || other.is_null() {
1128            return false;
1129        }
1130        matches!(self.partial_cmp(other), Some(Ordering::Equal))
1131    }
1132
1133    /// Convert a floating-point arithmetic result into a SQLite value.
1134    ///
1135    /// SQLite does not surface NaN; NaN is normalized to NULL while ±Inf remain REAL.
1136    fn float_result_or_null(result: f64) -> Self {
1137        if result.is_nan() {
1138            Self::Null
1139        } else {
1140            Self::Float(result)
1141        }
1142    }
1143
1144    /// Mirrors C SQLite's `numericType()` (SQLite VDBE:496): returns true if this
1145    /// value should be treated as an integer for arithmetic purposes.
1146    ///
1147    /// Integer values are obviously integer-typed. Text/Blob values that parse
1148    /// as i64 are also integer-typed. Float and Null are not.
1149    #[inline]
1150    pub fn is_integer_numeric_type(&self) -> bool {
1151        fn text_is_integer_numeric_type(s: &str) -> bool {
1152            let trimmed = s.trim_start();
1153            let end = scan_numeric_prefix(trimmed.as_bytes());
1154            end > 0
1155                && !trimmed.as_bytes()[..end]
1156                    .iter()
1157                    .any(|byte| matches!(*byte, b'.' | b'e' | b'E'))
1158        }
1159
1160        match self {
1161            Self::Integer(_) => true,
1162            Self::Float(_) | Self::Null => false,
1163            Self::Text(s) => text_is_integer_numeric_type(s),
1164            Self::Blob(b) => text_is_integer_numeric_type(&String::from_utf8_lossy(b)),
1165        }
1166    }
1167
1168    /// Returns true if this value should be treated as a float for arithmetic.
1169    /// A value is "float numeric type" only if it has a numeric prefix
1170    /// containing '.', 'e', or 'E'. Non-numeric text/blob is NOT float
1171    /// (it coerces to integer 0 in C SQLite's OP_Add/Sub/Mul).
1172    #[inline]
1173    fn is_float_numeric_type(&self) -> bool {
1174        fn text_is_float(s: &str) -> bool {
1175            let trimmed = s.trim_start();
1176            let end = scan_numeric_prefix(trimmed.as_bytes());
1177            end > 0
1178                && trimmed.as_bytes()[..end]
1179                    .iter()
1180                    .any(|byte| matches!(*byte, b'.' | b'e' | b'E'))
1181        }
1182        match self {
1183            Self::Float(_) => true,
1184            Self::Integer(_) | Self::Null => false,
1185            Self::Text(s) => text_is_float(s),
1186            Self::Blob(b) => text_is_float(&String::from_utf8_lossy(b)),
1187        }
1188    }
1189
1190    /// Add two values following SQLite's overflow semantics.
1191    ///
1192    /// - Integer + Integer: checked add; overflows promote to REAL.
1193    /// - Any REAL operand: float addition.
1194    /// - NULL propagates (NULL + x = NULL).
1195    /// - Text/Blob coerced via `numericType()`: if both parse as integer,
1196    ///   integer math is used (SQLite VDBE:1932-1934).
1197    #[inline(always)]
1198    #[allow(clippy::inline_always)]
1199    #[must_use]
1200    #[allow(clippy::cast_precision_loss)]
1201    pub fn sql_add(&self, other: &Self) -> Self {
1202        match (self, other) {
1203            (Self::Null, _) | (_, Self::Null) => Self::Null,
1204            (Self::Integer(a), Self::Integer(b)) => match a.checked_add(*b) {
1205                Some(result) => Self::Integer(result),
1206                None => Self::float_result_or_null(*a as f64 + *b as f64),
1207            },
1208            // If neither operand is a float-type (i.e. both are integer,
1209            // integer-text, or non-numeric text/blob), use integer arithmetic.
1210            // Non-numeric text like "hello" coerces to integer 0, not float 0.0.
1211            _ if !self.is_float_numeric_type() && !other.is_float_numeric_type() => {
1212                let a = self.to_integer();
1213                let b = other.to_integer();
1214                match a.checked_add(b) {
1215                    Some(result) => Self::Integer(result),
1216                    None => Self::float_result_or_null(a as f64 + b as f64),
1217                }
1218            }
1219            _ => Self::float_result_or_null(self.to_float() + other.to_float()),
1220        }
1221    }
1222
1223    /// Subtract two values following SQLite's overflow semantics.
1224    ///
1225    /// Integer - Integer with overflow promotes to REAL.
1226    #[inline(always)]
1227    #[allow(clippy::inline_always)]
1228    #[must_use]
1229    #[allow(clippy::cast_precision_loss)]
1230    pub fn sql_sub(&self, other: &Self) -> Self {
1231        match (self, other) {
1232            (Self::Null, _) | (_, Self::Null) => Self::Null,
1233            (Self::Integer(a), Self::Integer(b)) => match a.checked_sub(*b) {
1234                Some(result) => Self::Integer(result),
1235                None => Self::float_result_or_null(*a as f64 - *b as f64),
1236            },
1237            _ if !self.is_float_numeric_type() && !other.is_float_numeric_type() => {
1238                let a = self.to_integer();
1239                let b = other.to_integer();
1240                match a.checked_sub(b) {
1241                    Some(result) => Self::Integer(result),
1242                    None => Self::float_result_or_null(a as f64 - b as f64),
1243                }
1244            }
1245            _ => Self::float_result_or_null(self.to_float() - other.to_float()),
1246        }
1247    }
1248
1249    /// Multiply two values following SQLite's overflow semantics.
1250    ///
1251    /// Integer * Integer with overflow promotes to REAL.
1252    #[inline(always)]
1253    #[allow(clippy::inline_always)]
1254    #[must_use]
1255    #[allow(clippy::cast_precision_loss)]
1256    pub fn sql_mul(&self, other: &Self) -> Self {
1257        match (self, other) {
1258            (Self::Null, _) | (_, Self::Null) => Self::Null,
1259            (Self::Integer(a), Self::Integer(b)) => match a.checked_mul(*b) {
1260                Some(result) => Self::Integer(result),
1261                None => Self::float_result_or_null(*a as f64 * *b as f64),
1262            },
1263            (Self::Integer(a), Self::Float(b)) => Self::float_result_or_null(*a as f64 * *b),
1264            (Self::Float(a), Self::Integer(b)) => Self::float_result_or_null(*a * *b as f64),
1265            (Self::Float(a), Self::Float(b)) => Self::float_result_or_null(*a * *b),
1266            _ if !self.is_float_numeric_type() && !other.is_float_numeric_type() => {
1267                let a = self.to_integer();
1268                let b = other.to_integer();
1269                match a.checked_mul(b) {
1270                    Some(result) => Self::Integer(result),
1271                    None => Self::float_result_or_null(a as f64 * b as f64),
1272                }
1273            }
1274            _ => Self::float_result_or_null(self.to_float() * other.to_float()),
1275        }
1276    }
1277
1278    /// The sort order key for NULL values (SQLite sorts NULLs first).
1279    const fn sort_class(&self) -> u8 {
1280        match self {
1281            Self::Null => 0,
1282            Self::Integer(_) | Self::Float(_) => 1,
1283            Self::Text(_) => 2,
1284            Self::Blob(_) => 3,
1285        }
1286    }
1287}
1288
1289/// Check if two composite UNIQUE keys are duplicates (SQLite NULL semantics).
1290///
1291/// Returns `true` only if ALL corresponding components are non-NULL and equal.
1292/// If ANY component in either key is NULL, the keys are NOT duplicates (per
1293/// SQLite's NULL != NULL rule for UNIQUE constraints).
1294///
1295/// Both slices must have the same length (panics otherwise).
1296pub fn unique_key_duplicates(a: &[SqliteValue], b: &[SqliteValue]) -> bool {
1297    assert_eq!(a.len(), b.len(), "UNIQUE key columns must match");
1298    a.iter().zip(b.iter()).all(|(va, vb)| va.unique_eq(vb))
1299}
1300
1301/// Match a string against a SQL LIKE pattern with SQLite semantics.
1302///
1303/// - `%` matches zero or more characters.
1304/// - `_` matches exactly one character.
1305/// - Case-insensitive for ASCII A-Z only (no Unicode case folding without ICU).
1306/// - `escape` optionally specifies the escape character for literal `%`/`_`.
1307pub fn sql_like(pattern: &str, text: &str, escape: Option<char>) -> bool {
1308    sql_like_cased(pattern, text, escape, false)
1309}
1310
1311/// Match a string against a SQL LIKE pattern, honoring the connection's
1312/// `PRAGMA case_sensitive_like` setting.
1313///
1314/// When `case_sensitive` is `false` (the default) this is identical to
1315/// [`sql_like`]: ASCII case is folded. When `case_sensitive` is `true`
1316/// (`PRAGMA case_sensitive_like = ON`) the literal portions of the pattern are
1317/// matched byte-exact — wildcards (`%`, `_`) still behave the same.
1318#[must_use]
1319pub fn sql_like_cased(
1320    pattern: &str,
1321    text: &str,
1322    escape: Option<char>,
1323    case_sensitive: bool,
1324) -> bool {
1325    if let Some((kind, literal)) = classify_sql_like_fast_path(pattern, escape) {
1326        return sql_like_fast_path_matches_cased(kind, literal, text, case_sensitive);
1327    }
1328
1329    sql_like_inner(
1330        &pattern.chars().collect::<Vec<_>>(),
1331        &text.chars().collect::<Vec<_>>(),
1332        escape,
1333        0,
1334        0,
1335        case_sensitive,
1336    )
1337}
1338
1339#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1340pub enum SqlLikeFastPathKind {
1341    MatchAll,
1342    Exact,
1343    Prefix,
1344    Suffix,
1345    Contains,
1346}
1347
1348impl SqlLikeFastPathKind {
1349    #[must_use]
1350    pub const fn opcode_tag(self) -> i32 {
1351        match self {
1352            Self::MatchAll => 0,
1353            Self::Exact => 1,
1354            Self::Prefix => 2,
1355            Self::Suffix => 3,
1356            Self::Contains => 4,
1357        }
1358    }
1359
1360    #[must_use]
1361    pub const fn from_opcode_tag(tag: i32) -> Option<Self> {
1362        match tag {
1363            0 => Some(Self::MatchAll),
1364            1 => Some(Self::Exact),
1365            2 => Some(Self::Prefix),
1366            3 => Some(Self::Suffix),
1367            4 => Some(Self::Contains),
1368            _ => None,
1369        }
1370    }
1371}
1372
1373#[must_use]
1374pub fn sql_like_fast_path_matches(kind: SqlLikeFastPathKind, literal: &str, text: &str) -> bool {
1375    sql_like_fast_path_matches_cased(kind, literal, text, false)
1376}
1377
1378/// Like [`sql_like_fast_path_matches`] but honoring `case_sensitive`.
1379///
1380/// When `case_sensitive` is `true` the literal is compared byte-exact
1381/// (`PRAGMA case_sensitive_like = ON`); otherwise ASCII case is folded.
1382#[must_use]
1383pub fn sql_like_fast_path_matches_cased(
1384    kind: SqlLikeFastPathKind,
1385    literal: &str,
1386    text: &str,
1387    case_sensitive: bool,
1388) -> bool {
1389    match kind {
1390        SqlLikeFastPathKind::MatchAll => true,
1391        SqlLikeFastPathKind::Exact => {
1392            if case_sensitive {
1393                literal.as_bytes() == text.as_bytes()
1394            } else {
1395                ascii_ci_eq_bytes(literal.as_bytes(), text.as_bytes())
1396            }
1397        }
1398        SqlLikeFastPathKind::Prefix => {
1399            if case_sensitive {
1400                text.as_bytes().starts_with(literal.as_bytes())
1401            } else {
1402                ascii_ci_starts_with(text, literal)
1403            }
1404        }
1405        SqlLikeFastPathKind::Suffix => {
1406            if case_sensitive {
1407                text.as_bytes().ends_with(literal.as_bytes())
1408            } else {
1409                ascii_ci_ends_with(text, literal)
1410            }
1411        }
1412        SqlLikeFastPathKind::Contains => {
1413            if case_sensitive {
1414                literal.is_empty() || memmem::find(text.as_bytes(), literal.as_bytes()).is_some()
1415            } else {
1416                ascii_ci_contains(text, literal)
1417            }
1418        }
1419    }
1420}
1421
1422/// Reusable matcher for simple LIKE patterns classified by `classify_sql_like_fast_path`.
1423pub struct SqlLikeFastPathMatcher<'a> {
1424    kind: SqlLikeFastPathKind,
1425    literal: &'a str,
1426    contains_finder: Option<memmem::Finder<'a>>,
1427    case_sensitive: bool,
1428}
1429
1430impl<'a> SqlLikeFastPathMatcher<'a> {
1431    #[must_use]
1432    pub fn new(kind: SqlLikeFastPathKind, literal: &'a str) -> Self {
1433        Self::new_cased(kind, literal, false)
1434    }
1435
1436    /// Build a matcher honoring `case_sensitive` (`PRAGMA case_sensitive_like`).
1437    #[must_use]
1438    pub fn new_cased(kind: SqlLikeFastPathKind, literal: &'a str, case_sensitive: bool) -> Self {
1439        let contains_finder = (kind == SqlLikeFastPathKind::Contains && !literal.is_empty())
1440            .then(|| memmem::Finder::new(literal.as_bytes()));
1441        Self {
1442            kind,
1443            literal,
1444            contains_finder,
1445            case_sensitive,
1446        }
1447    }
1448
1449    #[must_use]
1450    pub fn matches(&self, text: &str) -> bool {
1451        if let (SqlLikeFastPathKind::Contains, Some(finder)) = (self.kind, &self.contains_finder) {
1452            let text_bytes = text.as_bytes();
1453            let needle_bytes = self.literal.as_bytes();
1454            if needle_bytes.len() > text_bytes.len() {
1455                return false;
1456            }
1457            if finder.find(text_bytes).is_some() {
1458                return true;
1459            }
1460            // A byte-exact substring (the finder above) is the *only* match when
1461            // the connection's LIKE is case-sensitive; otherwise fall back to the
1462            // ASCII-case-folded scan.
1463            if self.case_sensitive {
1464                return false;
1465            }
1466            return ascii_ci_contains_folded_scan(text_bytes, needle_bytes);
1467        }
1468        sql_like_fast_path_matches_cased(self.kind, self.literal, text, self.case_sensitive)
1469    }
1470}
1471
1472#[must_use]
1473pub fn classify_sql_like_fast_path(
1474    pattern: &str,
1475    escape: Option<char>,
1476) -> Option<(SqlLikeFastPathKind, &str)> {
1477    if escape.is_some() || pattern.contains('_') {
1478        return None;
1479    }
1480    if !pattern.contains('%') {
1481        return Some((SqlLikeFastPathKind::Exact, pattern));
1482    }
1483    if pattern.chars().all(|ch| ch == '%') {
1484        return Some((SqlLikeFastPathKind::MatchAll, ""));
1485    }
1486
1487    let trimmed_start = pattern.trim_start_matches('%');
1488    let trimmed_end = pattern.trim_end_matches('%');
1489    if pattern.starts_with('%') && pattern.ends_with('%') {
1490        let core = trimmed_start.trim_end_matches('%');
1491        if core.is_empty() {
1492            return Some((SqlLikeFastPathKind::MatchAll, ""));
1493        }
1494        if !core.contains('%') {
1495            return Some((SqlLikeFastPathKind::Contains, core));
1496        }
1497    }
1498    if !pattern.starts_with('%') && trimmed_end.len() < pattern.len() && !trimmed_end.contains('%')
1499    {
1500        return Some((SqlLikeFastPathKind::Prefix, trimmed_end));
1501    }
1502    if !pattern.ends_with('%')
1503        && trimmed_start.len() < pattern.len()
1504        && !trimmed_start.contains('%')
1505    {
1506        return Some((SqlLikeFastPathKind::Suffix, trimmed_start));
1507    }
1508    None
1509}
1510
1511fn sql_like_inner(
1512    pattern: &[char],
1513    text: &[char],
1514    escape: Option<char>,
1515    pi: usize,
1516    ti: usize,
1517    case_sensitive: bool,
1518) -> bool {
1519    let mut pi = pi;
1520    let mut ti = ti;
1521
1522    while pi < pattern.len() {
1523        let pc = pattern[pi];
1524
1525        // Handle escape character.
1526        if Some(pc) == escape {
1527            pi += 1;
1528            if pi >= pattern.len() {
1529                return false; // Trailing escape is malformed.
1530            }
1531            // Match the escaped character literally.
1532            if ti >= text.len() || !chars_eq(pattern[pi], text[ti], case_sensitive) {
1533                return false;
1534            }
1535            pi += 1;
1536            ti += 1;
1537            continue;
1538        }
1539
1540        match pc {
1541            '%' => {
1542                // Skip consecutive % wildcards.
1543                while pi < pattern.len() && pattern[pi] == '%' {
1544                    pi += 1;
1545                }
1546                // If % is at end of pattern, matches everything.
1547                if pi >= pattern.len() {
1548                    return true;
1549                }
1550                // Try matching rest of pattern at each position.
1551                for start in ti..=text.len() {
1552                    if sql_like_inner(pattern, text, escape, pi, start, case_sensitive) {
1553                        return true;
1554                    }
1555                }
1556                return false;
1557            }
1558            '_' => {
1559                if ti >= text.len() {
1560                    return false;
1561                }
1562                pi += 1;
1563                ti += 1;
1564            }
1565            _ => {
1566                if ti >= text.len() || !chars_eq(pc, text[ti], case_sensitive) {
1567                    return false;
1568                }
1569                pi += 1;
1570                ti += 1;
1571            }
1572        }
1573    }
1574    ti >= text.len()
1575}
1576
1577/// LIKE literal-character comparison: byte-exact when `case_sensitive`
1578/// (`PRAGMA case_sensitive_like = ON`), otherwise ASCII case-folded (default).
1579#[inline]
1580fn chars_eq(a: char, b: char, case_sensitive: bool) -> bool {
1581    if case_sensitive {
1582        a == b
1583    } else {
1584        ascii_ci_eq(a, b)
1585    }
1586}
1587
1588/// ASCII-only case-insensitive character comparison (SQLite LIKE semantics).
1589fn ascii_ci_eq(a: char, b: char) -> bool {
1590    if a == b {
1591        return true;
1592    }
1593    // Only fold ASCII A-Z / a-z.
1594    a.is_ascii() && b.is_ascii() && a.eq_ignore_ascii_case(&b)
1595}
1596
1597#[inline]
1598fn ascii_fold_byte(byte: u8) -> u8 {
1599    byte.to_ascii_lowercase()
1600}
1601
1602#[inline]
1603fn ascii_ci_eq_byte(left: u8, right: u8) -> bool {
1604    left == right || ((left ^ right) == 0x20 && left.is_ascii_alphabetic())
1605}
1606
1607fn ascii_ci_eq_bytes(left: &[u8], right: &[u8]) -> bool {
1608    if left.len() != right.len() {
1609        return false;
1610    }
1611    let mut idx = 0;
1612    while idx < left.len() {
1613        if !ascii_ci_eq_byte(left[idx], right[idx]) {
1614            return false;
1615        }
1616        idx += 1;
1617    }
1618    true
1619}
1620
1621fn ascii_ci_starts_with(text: &str, prefix: &str) -> bool {
1622    let text = text.as_bytes();
1623    let prefix = prefix.as_bytes();
1624    text.len() >= prefix.len() && ascii_ci_eq_bytes(&text[..prefix.len()], prefix)
1625}
1626
1627fn ascii_ci_ends_with(text: &str, suffix: &str) -> bool {
1628    let text = text.as_bytes();
1629    let suffix = suffix.as_bytes();
1630    text.len() >= suffix.len() && ascii_ci_eq_bytes(&text[text.len() - suffix.len()..], suffix)
1631}
1632
1633fn ascii_ci_contains(text: &str, needle: &str) -> bool {
1634    let text = text.as_bytes();
1635    let needle = needle.as_bytes();
1636    if needle.is_empty() {
1637        return true;
1638    }
1639    if needle.len() > text.len() {
1640        return false;
1641    }
1642    if memmem::find(text, needle).is_some() {
1643        return true;
1644    }
1645
1646    ascii_ci_contains_folded_scan(text, needle)
1647}
1648
1649fn ascii_ci_contains_folded_scan(text: &[u8], needle: &[u8]) -> bool {
1650    if needle.is_empty() {
1651        return true;
1652    }
1653    if needle.len() > text.len() {
1654        return false;
1655    }
1656    let max_start = text.len() - needle.len();
1657    let first = needle[0];
1658    let first_folded = ascii_fold_byte(first);
1659    let first_alt = if first.is_ascii_alphabetic() {
1660        first_folded.to_ascii_uppercase()
1661    } else {
1662        first_folded
1663    };
1664    let mut start = 0;
1665    while start <= max_start {
1666        let rel = if first_folded == first_alt {
1667            memchr(first_folded, &text[start..=max_start])
1668        } else {
1669            memchr2(first_folded, first_alt, &text[start..=max_start])
1670        };
1671        let Some(rel) = rel else {
1672            break;
1673        };
1674        start += rel;
1675        if ascii_ci_eq_bytes(&text[start + 1..start + needle.len()], &needle[1..]) {
1676            return true;
1677        }
1678        start += 1;
1679    }
1680    false
1681}
1682
1683/// Accumulator for SQL `sum()` aggregate with SQLite overflow semantics.
1684///
1685/// Unlike expression arithmetic (which promotes to REAL on overflow), `sum()`
1686/// raises an error on integer overflow only if all non-NULL inputs remain in
1687/// the integer accumulator. A later REAL input suppresses the overflow error
1688/// and returns the approximate REAL sum, matching C sqlite3 behavior.
1689#[derive(Debug, Clone)]
1690pub struct SumAccumulator {
1691    /// Running integer sum (if still in integer mode).
1692    int_sum: i64,
1693    /// Running float sum retained in parallel so a later REAL input can fall
1694    /// back without losing an integer that overflowed the exact accumulator.
1695    float_sum: f64,
1696    /// KBN compensation error term.
1697    float_err: f64,
1698    /// Whether we've seen any non-NULL value.
1699    has_value: bool,
1700    /// Whether we're in float mode (any REAL-like input).
1701    is_float: bool,
1702    /// Whether an integer overflow occurred (error condition).
1703    overflow: bool,
1704}
1705
1706impl Default for SumAccumulator {
1707    fn default() -> Self {
1708        Self::new()
1709    }
1710}
1711
1712/// Kahan-Babuska-Neumaier compensated summation step matching upstream
1713/// aggregate precision behavior.
1714#[inline]
1715fn kbn_step(sum: &mut f64, err: &mut f64, value: f64) {
1716    let s = *sum;
1717    let t = s + value;
1718    if s.abs() > value.abs() {
1719        *err += (s - t) + value;
1720    } else {
1721        *err += (value - t) + s;
1722    }
1723    *sum = t;
1724}
1725
1726impl SumAccumulator {
1727    /// Create a new accumulator.
1728    pub const fn new() -> Self {
1729        Self {
1730            int_sum: 0,
1731            float_sum: 0.0,
1732            float_err: 0.0,
1733            has_value: false,
1734            is_float: false,
1735            overflow: false,
1736        }
1737    }
1738
1739    /// Add a value to the running sum.
1740    #[allow(clippy::cast_precision_loss)]
1741    pub fn accumulate(&mut self, val: &SqliteValue) {
1742        match val.to_sum_numeric_value() {
1743            SqliteValue::Null | SqliteValue::Text(_) | SqliteValue::Blob(_) => {}
1744            SqliteValue::Integer(i) => {
1745                self.has_value = true;
1746                if !self.is_float && !self.overflow {
1747                    match self.int_sum.checked_add(i) {
1748                        Some(result) => self.int_sum = result,
1749                        None => self.overflow = true,
1750                    }
1751                }
1752                kbn_step(&mut self.float_sum, &mut self.float_err, i as f64);
1753            }
1754            SqliteValue::Float(f) => {
1755                self.has_value = true;
1756                self.is_float = true;
1757                kbn_step(&mut self.float_sum, &mut self.float_err, f);
1758            }
1759        }
1760    }
1761
1762    /// Finalize the sum. Returns `Err` if integer overflow occurred while the
1763    /// accumulator stayed integer, `Ok(NULL)` if no non-NULL values were seen,
1764    /// or the sum value.
1765    pub fn finish(&self) -> Result<SqliteValue, SumOverflowError> {
1766        if !self.is_float && self.overflow {
1767            return Err(SumOverflowError);
1768        }
1769        if !self.has_value {
1770            return Ok(SqliteValue::Null);
1771        }
1772        if self.is_float {
1773            Ok(SqliteValue::Float(self.float_sum + self.float_err))
1774        } else {
1775            Ok(SqliteValue::Integer(self.int_sum))
1776        }
1777    }
1778}
1779
1780/// Error returned when `sum()` encounters integer overflow.
1781#[derive(Debug, Clone, PartialEq, Eq)]
1782pub struct SumOverflowError;
1783
1784impl fmt::Display for SumOverflowError {
1785    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1786        f.write_str("integer overflow in sum()")
1787    }
1788}
1789
1790impl fmt::Display for SqliteValue {
1791    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1792        match self {
1793            Self::Null => f.write_str("NULL"),
1794            Self::Integer(i) => write!(f, "{i}"),
1795            Self::Float(v) => f.write_str(&format_sqlite_float(*v)),
1796            Self::Text(s) => write!(f, "'{s}'"),
1797            Self::Blob(b) => {
1798                f.write_str("X'")?;
1799                for byte in b.iter() {
1800                    write!(f, "{byte:02X}")?;
1801                }
1802                f.write_str("'")
1803            }
1804        }
1805    }
1806}
1807
1808impl PartialEq for SqliteValue {
1809    fn eq(&self, other: &Self) -> bool {
1810        matches!(self.partial_cmp(other), Some(Ordering::Equal))
1811    }
1812}
1813
1814impl Eq for SqliteValue {}
1815
1816impl PartialOrd for SqliteValue {
1817    #[inline]
1818    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1819        Some(self.cmp(other))
1820    }
1821}
1822
1823impl Ord for SqliteValue {
1824    #[inline]
1825    fn cmp(&self, other: &Self) -> Ordering {
1826        // SQLite sort order: NULL < numeric < text < blob
1827        let class_a = self.sort_class();
1828        let class_b = other.sort_class();
1829
1830        if class_a != class_b {
1831            return class_a.cmp(&class_b);
1832        }
1833
1834        match (self, other) {
1835            (Self::Null, Self::Null) => Ordering::Equal,
1836            (Self::Integer(a), Self::Integer(b)) => a.cmp(b),
1837            (Self::Float(a), Self::Float(b)) => a.partial_cmp(b).unwrap_or_else(|| a.total_cmp(b)),
1838            (Self::Integer(a), Self::Float(b)) => int_float_cmp(*a, *b),
1839            (Self::Float(a), Self::Integer(b)) => int_float_cmp(*b, *a).reverse(),
1840            (Self::Text(a), Self::Text(b)) => a.cmp(b),
1841            (Self::Blob(a), Self::Blob(b)) => a.cmp(b),
1842            _ => unreachable!(),
1843        }
1844    }
1845}
1846
1847impl From<i64> for SqliteValue {
1848    fn from(i: i64) -> Self {
1849        Self::Integer(i)
1850    }
1851}
1852
1853impl From<i32> for SqliteValue {
1854    fn from(i: i32) -> Self {
1855        Self::Integer(i64::from(i))
1856    }
1857}
1858
1859impl From<f64> for SqliteValue {
1860    fn from(f: f64) -> Self {
1861        Self::float_result_or_null(f)
1862    }
1863}
1864
1865impl From<String> for SqliteValue {
1866    fn from(s: String) -> Self {
1867        // SmallText stores strings ≤ 23 bytes inline without heap allocation.
1868        // Longer strings use Arc<str> internally.
1869        Self::Text(SmallText::from_string(s))
1870    }
1871}
1872
1873impl From<&str> for SqliteValue {
1874    fn from(s: &str) -> Self {
1875        Self::Text(SmallText::new(s))
1876    }
1877}
1878
1879impl From<Arc<str>> for SqliteValue {
1880    fn from(s: Arc<str>) -> Self {
1881        Self::Text(SmallText::from_arc(s))
1882    }
1883}
1884
1885impl From<Vec<u8>> for SqliteValue {
1886    fn from(b: Vec<u8>) -> Self {
1887        // Arc::from(Vec<u8>) reuses the Vec's heap buffer via
1888        // Vec → Box<[u8]> → Arc<[u8]>, avoiding a redundant copy.
1889        Self::Blob(Arc::from(b))
1890    }
1891}
1892
1893impl From<&[u8]> for SqliteValue {
1894    fn from(b: &[u8]) -> Self {
1895        Self::Blob(Arc::from(b))
1896    }
1897}
1898
1899impl From<Arc<[u8]>> for SqliteValue {
1900    fn from(b: Arc<[u8]>) -> Self {
1901        Self::Blob(b)
1902    }
1903}
1904
1905impl<T: Into<Self>> From<Option<T>> for SqliteValue {
1906    fn from(opt: Option<T>) -> Self {
1907        match opt {
1908            Some(v) => v.into(),
1909            None => Self::Null,
1910        }
1911    }
1912}
1913
1914/// Try to coerce a text string to INTEGER or REAL following SQLite NUMERIC
1915/// affinity rules. Returns `None` if the text is not a well-formed numeric
1916/// literal.
1917#[allow(
1918    clippy::cast_possible_truncation,
1919    clippy::cast_precision_loss,
1920    clippy::float_cmp
1921)]
1922fn try_coerce_text_to_numeric(s: &str) -> Option<SqliteValue> {
1923    let trimmed = trim_sqlite_ascii_whitespace(s);
1924    if trimmed.is_empty() {
1925        return None;
1926    }
1927    // Try integer first (preferred for NUMERIC affinity).
1928    if let Ok(i) = trimmed.parse::<i64>() {
1929        return Some(SqliteValue::Integer(i));
1930    }
1931    // Try float. Reject non-finite results (NaN, Infinity) since SQLite
1932    // does not recognise "nan", "inf", or "infinity" as numeric literals.
1933    // However, it does recognize literals like "1e999" which evaluate to Inf.
1934    if let Ok(f) = trimmed.parse::<f64>() {
1935        if !f.is_finite() {
1936            let lower = trimmed.to_ascii_lowercase();
1937            if lower.contains("inf") || lower.contains("nan") {
1938                return None;
1939            }
1940        }
1941        // If the float is an exact integer value within bounds, store as integer.
1942        // Checking bounds prevents incorrect saturation for values >= 2^63.
1943        if (-9_223_372_036_854_775_808.0..9_223_372_036_854_775_808.0).contains(&f) {
1944            #[allow(clippy::cast_possible_truncation)]
1945            let i = f as i64;
1946            #[allow(clippy::cast_precision_loss)]
1947            if (i as f64) == f {
1948                return Some(SqliteValue::Integer(i));
1949            }
1950        }
1951        return Some(SqliteValue::Float(f));
1952    }
1953    None
1954}
1955
1956/// Compare an integer with a float, preserving precision for large i64 values.
1957///
1958/// Matches C SQLite's `sqlite3IntFloatCompare` algorithm. The naive
1959/// `(i as f64).partial_cmp(&r)` loses precision for |i| > 2^53.
1960#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
1961pub fn int_float_cmp(i: i64, r: f64) -> Ordering {
1962    if r.is_nan() {
1963        // SQLite treats NaN as NULL, and all integers are greater than NULL.
1964        return Ordering::Greater;
1965    }
1966    // If r is out of i64 range, the answer is obvious.
1967    if r < -9_223_372_036_854_775_808.0 {
1968        return Ordering::Greater;
1969    }
1970    if r >= 9_223_372_036_854_775_808.0 {
1971        return Ordering::Less;
1972    }
1973    // Truncate float to integer and compare integer parts.
1974    let y = r as i64;
1975    match i.cmp(&y) {
1976        Ordering::Less => Ordering::Less,
1977        Ordering::Greater => Ordering::Greater,
1978        // Integer parts equal — use float comparison as tiebreaker.
1979        Ordering::Equal => {
1980            let s = i as f64;
1981            s.partial_cmp(&r).unwrap_or(Ordering::Equal)
1982        }
1983    }
1984}
1985
1986/// Format a floating-point value as text matching SQLite's `%!.*g` behavior.
1987///
1988/// SQLite 3.52 and newer default REAL-to-TEXT conversion to 17 significant
1989/// digits through `sqlite3VdbeMemStringify()`. The `!` flag keeps a decimal
1990/// point so REAL text stays distinct from INTEGER text, for example `120.0`
1991/// rather than `120`.
1992#[must_use]
1993pub fn format_sqlite_float(f: f64) -> String {
1994    if f.is_nan() {
1995        return "NaN".to_owned();
1996    }
1997    if f.is_infinite() {
1998        return if f.is_sign_positive() {
1999            "Inf".to_owned()
2000        } else {
2001            "-Inf".to_owned()
2002        };
2003    }
2004    render_sqlite_float_decode(&sqlite_float_decode(f))
2005}
2006
2007const SQLITE_FLOAT_SIGNIFICANT_DIGITS: usize = 17;
2008const SQLITE_FLOAT_MAX_ROUND_DIGITS: usize = 20;
2009const SQLITE_FLOAT_GENERIC_PRECISION: i32 = 16;
2010const SQLITE_POWERS_OF_TEN_FIRST: i32 = -348;
2011const SQLITE_POWERS_OF_TEN_LAST: i32 = 347;
2012
2013#[derive(Debug)]
2014struct SqliteFloatDecode {
2015    digits: Vec<u8>,
2016    decimal_point: i32,
2017    negative: bool,
2018}
2019
2020fn render_sqlite_float_decode(decoded: &SqliteFloatDecode) -> String {
2021    let exponent = decoded.decimal_point - 1;
2022    if !(-4..=SQLITE_FLOAT_GENERIC_PRECISION).contains(&exponent) {
2023        return render_sqlite_float_exponential(decoded, exponent);
2024    }
2025    render_sqlite_float_fixed(decoded, exponent)
2026}
2027
2028fn render_sqlite_float_fixed(decoded: &SqliteFloatDecode, exponent: i32) -> String {
2029    let mut out = String::with_capacity(decoded.digits.len() + 8);
2030    if decoded.negative {
2031        out.push('-');
2032    }
2033
2034    let mut precision = SQLITE_FLOAT_GENERIC_PRECISION - exponent;
2035    let mut digit_idx = 0usize;
2036    let mut e2 = decoded.decimal_point - 1;
2037
2038    if e2 < 0 {
2039        out.push('0');
2040    } else {
2041        while e2 >= 0 {
2042            if let Some(&digit) = decoded.digits.get(digit_idx) {
2043                out.push(char::from(digit));
2044                digit_idx += 1;
2045            } else {
2046                out.push('0');
2047            }
2048            e2 -= 1;
2049        }
2050    }
2051
2052    out.push('.');
2053
2054    if e2 < -1 && precision > 0 {
2055        let zero_count = (-1 - e2).min(precision);
2056        for _ in 0..zero_count {
2057            out.push('0');
2058        }
2059        precision -= zero_count;
2060    }
2061
2062    if precision > 0 {
2063        let digits_after_decimal =
2064            (decoded.digits.len().saturating_sub(digit_idx)).min(precision as usize);
2065        for &digit in &decoded.digits[digit_idx..digit_idx + digits_after_decimal] {
2066            out.push(char::from(digit));
2067        }
2068    }
2069
2070    trim_sqlite_float_tail(&mut out);
2071    out
2072}
2073
2074fn render_sqlite_float_exponential(decoded: &SqliteFloatDecode, exponent: i32) -> String {
2075    let mut out = String::with_capacity(decoded.digits.len() + 8);
2076    if decoded.negative {
2077        out.push('-');
2078    }
2079
2080    let first = decoded.digits.first().copied().unwrap_or(b'0');
2081    out.push(char::from(first));
2082    out.push('.');
2083    let digits_after_decimal =
2084        (decoded.digits.len().saturating_sub(1)).min(SQLITE_FLOAT_GENERIC_PRECISION as usize);
2085    for &digit in decoded.digits.iter().skip(1).take(digits_after_decimal) {
2086        out.push(char::from(digit));
2087    }
2088    trim_sqlite_float_tail(&mut out);
2089
2090    out.push('e');
2091    let mut abs_exp = exponent;
2092    if abs_exp < 0 {
2093        out.push('-');
2094        abs_exp = -abs_exp;
2095    } else {
2096        out.push('+');
2097    }
2098    if abs_exp >= 100 {
2099        out.push(char::from(b'0' + (abs_exp / 100) as u8));
2100        abs_exp %= 100;
2101    }
2102    out.push(char::from(b'0' + (abs_exp / 10) as u8));
2103    out.push(char::from(b'0' + (abs_exp % 10) as u8));
2104    out
2105}
2106
2107fn trim_sqlite_float_tail(out: &mut String) {
2108    while out.ends_with('0') {
2109        out.pop();
2110    }
2111    if out.ends_with('.') {
2112        out.push('0');
2113    }
2114}
2115
2116fn sqlite_float_decode(f: f64) -> SqliteFloatDecode {
2117    let negative = f < 0.0;
2118    let r = if negative { -f } else { f };
2119    if r == 0.0 {
2120        return SqliteFloatDecode {
2121            digits: vec![b'0'],
2122            decimal_point: 1,
2123            negative: false,
2124        };
2125    }
2126
2127    let bits = r.to_bits();
2128    let raw_exponent = ((bits >> 52) & 0x7ff) as i32;
2129    let mut mantissa = bits & 0x000f_ffff_ffff_ffff;
2130    let binary_exponent = if raw_exponent == 0 {
2131        let leading = mantissa.leading_zeros();
2132        mantissa <<= leading;
2133        -1074 - leading as i32
2134    } else {
2135        mantissa = (mantissa << 11) | (1_u64 << 63);
2136        raw_exponent - 1086
2137    };
2138
2139    let (decimal, decimal_exponent) = sqlite_fp2_convert10(mantissa, binary_exponent, 18);
2140    let mut digits = decimal.to_string().into_bytes();
2141    let mut digit_count = digits.len();
2142    let mut decimal_point = digit_count as i32 + decimal_exponent;
2143    let mut round_at = SQLITE_FLOAT_SIGNIFICANT_DIGITS;
2144
2145    if round_at < digit_count || digit_count > SQLITE_FLOAT_MAX_ROUND_DIGITS {
2146        if round_at == SQLITE_FLOAT_SIGNIFICANT_DIGITS {
2147            round_at = sqlite_adjust_17_digit_rounding(
2148                r,
2149                &digits,
2150                decimal_exponent,
2151                digit_count,
2152                decimal_point,
2153                round_at,
2154            );
2155        }
2156        if digits.get(round_at).copied().unwrap_or(b'0') >= b'5' {
2157            let mut idx = round_at - 1;
2158            loop {
2159                digits[idx] += 1;
2160                if digits[idx] <= b'9' {
2161                    break;
2162                }
2163                digits[idx] = b'0';
2164                if idx == 0 {
2165                    digits.insert(0, b'1');
2166                    round_at += 1;
2167                    decimal_point += 1;
2168                    break;
2169                }
2170                idx -= 1;
2171            }
2172        }
2173        digit_count = round_at;
2174        digits.truncate(digit_count);
2175    }
2176
2177    while digit_count > 1 && digits[digit_count - 1] == b'0' {
2178        digit_count -= 1;
2179    }
2180    digits.truncate(digit_count);
2181
2182    SqliteFloatDecode {
2183        digits,
2184        decimal_point,
2185        negative,
2186    }
2187}
2188
2189fn sqlite_adjust_17_digit_rounding(
2190    r: f64,
2191    digits: &[u8],
2192    decimal_exponent: i32,
2193    digit_count: usize,
2194    decimal_point: i32,
2195    round_at: usize,
2196) -> usize {
2197    if digits.len() <= SQLITE_FLOAT_SIGNIFICANT_DIGITS {
2198        return round_at;
2199    }
2200
2201    if digits[15] == b'9' && digits[14] == b'9' {
2202        let mut keep = 14usize;
2203        while keep > 0 && digits[keep - 1] == b'9' {
2204            keep -= 1;
2205        }
2206        let candidate = if keep == 0 {
2207            1
2208        } else {
2209            decimal_digits_to_u64(&digits[..keep]) + 1
2210        };
2211        if r == sqlite_fp10_convert2(
2212            candidate,
2213            decimal_exponent + digit_count as i32 - keep as i32,
2214        ) {
2215            return keep + 1;
2216        }
2217    } else if decimal_point >= digit_count as i32
2218        || (digits[15] == b'0' && digits[14] == b'0' && digits[13] == b'0')
2219    {
2220        let mut keep = 13usize;
2221        while keep > 0 && digits[keep - 1] == b'0' {
2222            keep -= 1;
2223        }
2224        if keep > 0 {
2225            let candidate = decimal_digits_to_u64(&digits[..keep]);
2226            if r == sqlite_fp10_convert2(
2227                candidate,
2228                decimal_exponent + digit_count as i32 - keep as i32,
2229            ) {
2230                return keep + 1;
2231            }
2232        }
2233    }
2234
2235    round_at
2236}
2237
2238fn decimal_digits_to_u64(digits: &[u8]) -> u64 {
2239    digits
2240        .iter()
2241        .fold(0_u64, |acc, digit| acc * 10 + u64::from(*digit - b'0'))
2242}
2243
2244fn sqlite_fp2_convert10(mantissa: u64, binary_exponent: i32, digits: i32) -> (u64, i32) {
2245    let power = digits - 1 - pwr2_to_10(binary_exponent + 63);
2246    let (power_hi, power_lo) = power_of_ten(power);
2247    let (mut high, _) = sqlite_multiply_128(mantissa, power_hi);
2248    let _ = power_lo;
2249    if digits == 18 {
2250        high >>= -(binary_exponent + pwr10_to_2(power) + 2) as u32;
2251        (high.wrapping_add((high << 1) & 2) >> 1, -power)
2252    } else {
2253        high >>= -(binary_exponent + pwr10_to_2(power) + 1) as u32;
2254        (high, -power)
2255    }
2256}
2257
2258fn sqlite_fp10_convert2(decimal: u64, power: i32) -> f64 {
2259    if power < SQLITE_POWERS_OF_TEN_FIRST {
2260        return 0.0;
2261    }
2262    if power > SQLITE_POWERS_OF_TEN_LAST {
2263        return f64::INFINITY;
2264    }
2265
2266    let bit_width = 64 - decimal.leading_zeros() as i32;
2267    let binary_power = pwr10_to_2(power);
2268    let mut exponent = 53 - bit_width - binary_power;
2269    if exponent > 1074 {
2270        if exponent >= 1130 {
2271            return 0.0;
2272        }
2273        exponent = 1074;
2274    }
2275
2276    let shift = -(exponent - (64 - bit_width) + binary_power + 3);
2277    let shift = shift.clamp(0, 63) as u32;
2278    let (mut power_hi, mut power_lo) = power_of_ten(power);
2279    if power_lo != 0 {
2280        power_hi = power_hi.wrapping_add(1);
2281        power_lo = !power_lo;
2282    }
2283
2284    let shifted_decimal = decimal << (64 - bit_width);
2285    let (mut high, low) = sqlite_multiply_128(shifted_decimal, power_hi);
2286    let mid1 = (low >> 32) as u32;
2287    let mut sticky = 1_u64;
2288    if (high & low_mask(shift)) == 0 {
2289        let (mid2_high, _) = sqlite_multiply_128(shifted_decimal, u64::from(power_lo) << 32);
2290        let mid2 = (mid2_high >> 32) as u32;
2291        sticky = u64::from(mid1.wrapping_sub(mid2) > 1);
2292        high = high.wrapping_sub(u64::from(mid1 < mid2));
2293    }
2294
2295    let mut rounded = (high >> shift) | sticky;
2296    let adjust = u32::from(rounded >= (1_u64 << 55) - 2);
2297    if adjust != 0 {
2298        rounded = (rounded >> adjust) | (rounded & 1);
2299        exponent -= adjust as i32;
2300    }
2301
2302    let mut bits = (rounded + 1 + ((rounded >> 2) & 1)) >> 2;
2303    if exponent <= -972 {
2304        return f64::INFINITY;
2305    }
2306    if (bits & (1_u64 << 52)) != 0 {
2307        bits = (bits & !(1_u64 << 52)) | ((1075 - exponent) as u64) << 52;
2308    }
2309    f64::from_bits(bits)
2310}
2311
2312fn low_mask(bits: u32) -> u64 {
2313    if bits == 0 { 0 } else { (1_u64 << bits) - 1 }
2314}
2315
2316fn sqlite_multiply_128(left: u64, right: u64) -> (u64, u64) {
2317    let product = u128::from(left) * u128::from(right);
2318    ((product >> 64) as u64, product as u64)
2319}
2320
2321fn sqlite_multiply_160(high: u64, low: u32, right: u64) -> (u64, u32) {
2322    let product =
2323        u128::from(high) * u128::from(right) + ((u128::from(low) * u128::from(right)) >> 32);
2324    (
2325        (product >> 64) as u64,
2326        ((product >> 32) & u128::from(u32::MAX)) as u32,
2327    )
2328}
2329
2330fn pwr10_to_2(power: i32) -> i32 {
2331    (power * 108_853) >> 15
2332}
2333
2334fn pwr2_to_10(power: i32) -> i32 {
2335    (power * 78_913) >> 18
2336}
2337
2338fn power_of_ten(power: i32) -> (u64, u32) {
2339    const BASE: [u64; 27] = [
2340        0x8000_0000_0000_0000,
2341        0xa000_0000_0000_0000,
2342        0xc800_0000_0000_0000,
2343        0xfa00_0000_0000_0000,
2344        0x9c40_0000_0000_0000,
2345        0xc350_0000_0000_0000,
2346        0xf424_0000_0000_0000,
2347        0x9896_8000_0000_0000,
2348        0xbebc_2000_0000_0000,
2349        0xee6b_2800_0000_0000,
2350        0x9502_f900_0000_0000,
2351        0xba43_b740_0000_0000,
2352        0xe8d4_a510_0000_0000,
2353        0x9184_e72a_0000_0000,
2354        0xb5e6_20f4_8000_0000,
2355        0xe35f_a931_a000_0000,
2356        0x8e1b_c9bf_0400_0000,
2357        0xb1a2_bc2e_c500_0000,
2358        0xde0b_6b3a_7640_0000,
2359        0x8ac7_2304_89e8_0000,
2360        0xad78_ebc5_ac62_0000,
2361        0xd8d7_26b7_177a_8000,
2362        0x8786_7832_6eac_9000,
2363        0xa968_163f_0a57_b400,
2364        0xd3c2_1bce_cced_a100,
2365        0x8459_5161_4014_84a0,
2366        0xa56f_a5b9_9019_a5c8,
2367    ];
2368    const SCALE: [u64; 26] = [
2369        0x8049_a4ac_0c58_11ae,
2370        0xcf42_894a_5dce_35ea,
2371        0xa76c_5823_38ed_2621,
2372        0x873e_4f75_e222_4e68,
2373        0xda7f_5bf5_9096_6848,
2374        0xb080_392c_c434_9dec,
2375        0x8e93_8662_882a_f53e,
2376        0xe658_29b3_046b_0afa,
2377        0xba12_1a46_50e4_ddeb,
2378        0x964e_858c_91ba_2655,
2379        0xf2d5_6790_ab41_c2a2,
2380        0xc428_d05a_a475_1e4c,
2381        0x9e74_d1b7_91e0_7e48,
2382        0xcccc_cccc_cccc_cccc,
2383        0xcecb_8f27_f420_0f3a,
2384        0xa70c_3c40_a64e_6c51,
2385        0x86f0_ac99_b4e8_dafd,
2386        0xda01_ee64_1a70_8de9,
2387        0xb01a_e745_b101_e9e4,
2388        0x8e41_ade9_fbeb_c27d,
2389        0xe5d3_ef28_2a24_2e81,
2390        0xb9a7_4a06_37ce_2ee1,
2391        0x95f8_3d0a_1fb6_9cd9,
2392        0xf24a_01a7_3cf2_dccf,
2393        0xc3b8_3581_09e8_4f07,
2394        0x9e19_db92_b4e3_1ba9,
2395    ];
2396    const SCALE_LO: [u32; 26] = [
2397        0x205b_896d,
2398        0x5206_4cad,
2399        0xaf2a_f2b8,
2400        0x5a77_44a7,
2401        0xaf39_a475,
2402        0xbd8d_794e,
2403        0x547e_b47b,
2404        0x0cb4_a5a3,
2405        0x92f3_4d62,
2406        0x3a6a_07f9,
2407        0xfae2_7299,
2408        0xaa97_e14c,
2409        0x775e_a265,
2410        0xcccc_cccc,
2411        0x0000_0000,
2412        0x9990_90b6,
2413        0x69a0_28bb,
2414        0xe80e_6f48,
2415        0x5ec0_5dd0,
2416        0x1458_8f14,
2417        0x8f16_68c9,
2418        0x6d95_3e2c,
2419        0x4abd_af10,
2420        0xbc63_3b39,
2421        0x0a86_2f81,
2422        0x6c07_a2c2,
2423    ];
2424
2425    debug_assert!((SQLITE_POWERS_OF_TEN_FIRST..=SQLITE_POWERS_OF_TEN_LAST).contains(&power));
2426
2427    let (group, offset) = if power < 0 {
2428        if power == -1 {
2429            return (SCALE[13], SCALE_LO[13]);
2430        }
2431        let mut group = power / 27;
2432        let mut offset = power % 27;
2433        if offset != 0 {
2434            group -= 1;
2435            offset += 27;
2436        }
2437        (group, offset)
2438    } else if power < 27 {
2439        return (BASE[power as usize], 0);
2440    } else {
2441        (power / 27, power % 27)
2442    };
2443
2444    let scale_idx = (group + 13) as usize;
2445    let mut high = SCALE[scale_idx];
2446    if offset == 0 {
2447        return (high, SCALE_LO[scale_idx]);
2448    }
2449
2450    let (scaled, mut low) = sqlite_multiply_160(high, SCALE_LO[scale_idx], BASE[offset as usize]);
2451    high = scaled;
2452    if (high & (1_u64 << 63)) == 0 {
2453        high = (high << 1) | u64::from(low >> 31);
2454        low = (low << 1) | 1;
2455    }
2456    (high, low)
2457}
2458
2459#[cfg(test)]
2460#[allow(clippy::float_cmp, clippy::approx_constant)]
2461mod tests {
2462    use super::*;
2463
2464    struct ValuePoolTestGuard;
2465
2466    impl ValuePoolTestGuard {
2467        fn new() -> Self {
2468            pool_clear();
2469            reset_value_pool_test_stats();
2470            Self
2471        }
2472    }
2473
2474    impl Drop for ValuePoolTestGuard {
2475        fn drop(&mut self) {
2476            pool_clear();
2477            reset_value_pool_test_stats();
2478        }
2479    }
2480
2481    fn log_value_pool_test_stats(test_name: &str) -> ValuePoolStats {
2482        let stats = value_pool_test_stats_snapshot();
2483        eprintln!(
2484            "bead_id=bd-nsvud test={test_name} slab_alloc_count={} slab_return_count={} global_alloc_fallback_count={} slab_high_water_mark={} pool_len={}",
2485            stats.slab_alloc_count,
2486            stats.slab_return_count,
2487            stats.global_alloc_fallback_count,
2488            stats.slab_high_water_mark,
2489            pool_len(),
2490        );
2491        stats
2492    }
2493
2494    #[test]
2495    fn test_slab_basic_alloc_dealloc() {
2496        let _guard = ValuePoolTestGuard::new();
2497        const ROUND_TRIP_COUNT: usize = 100;
2498
2499        assert_eq!(pool_len(), 0);
2500        assert_eq!(pool_acquire(), None);
2501        assert_eq!(
2502            value_pool_test_stats_snapshot(),
2503            ValuePoolStats {
2504                slab_alloc_count: 0,
2505                slab_return_count: 0,
2506                global_alloc_fallback_count: 1,
2507                slab_high_water_mark: 0,
2508            }
2509        );
2510
2511        reset_value_pool_test_stats();
2512        for value in 0..ROUND_TRIP_COUNT {
2513            pool_return(SqliteValue::Integer(value as i64));
2514        }
2515        assert_eq!(pool_len(), ROUND_TRIP_COUNT);
2516        assert_eq!(
2517            value_pool_test_stats_snapshot(),
2518            ValuePoolStats {
2519                slab_alloc_count: 0,
2520                slab_return_count: ROUND_TRIP_COUNT,
2521                global_alloc_fallback_count: 0,
2522                slab_high_water_mark: ROUND_TRIP_COUNT,
2523            }
2524        );
2525
2526        reset_value_pool_test_stats();
2527        for expected in (0..ROUND_TRIP_COUNT).rev() {
2528            assert_eq!(pool_acquire(), Some(SqliteValue::Integer(expected as i64)));
2529        }
2530        assert_eq!(pool_len(), 0);
2531        assert_eq!(
2532            log_value_pool_test_stats("test_slab_basic_alloc_dealloc"),
2533            ValuePoolStats {
2534                slab_alloc_count: ROUND_TRIP_COUNT,
2535                slab_return_count: 0,
2536                global_alloc_fallback_count: 0,
2537                slab_high_water_mark: 0,
2538            }
2539        );
2540    }
2541
2542    #[test]
2543    fn test_slab_exhaustion_fallback() {
2544        let _guard = ValuePoolTestGuard::new();
2545
2546        for value in 0..=VALUE_POOL_CAP {
2547            pool_return(SqliteValue::Integer(value as i64));
2548        }
2549        assert_eq!(pool_len(), VALUE_POOL_CAP);
2550        assert_eq!(
2551            value_pool_test_stats_snapshot(),
2552            ValuePoolStats {
2553                slab_alloc_count: 0,
2554                slab_return_count: VALUE_POOL_CAP,
2555                global_alloc_fallback_count: 0,
2556                slab_high_water_mark: VALUE_POOL_CAP,
2557            }
2558        );
2559
2560        reset_value_pool_test_stats();
2561        for _ in 0..VALUE_POOL_CAP {
2562            assert!(pool_acquire().is_some());
2563        }
2564        assert_eq!(pool_acquire(), None);
2565        assert_eq!(pool_len(), 0);
2566        assert_eq!(
2567            log_value_pool_test_stats("test_slab_exhaustion_fallback"),
2568            ValuePoolStats {
2569                slab_alloc_count: VALUE_POOL_CAP,
2570                slab_return_count: 0,
2571                global_alloc_fallback_count: 1,
2572                slab_high_water_mark: 0,
2573            }
2574        );
2575    }
2576
2577    #[test]
2578    fn test_slab_no_leak() {
2579        let _guard = ValuePoolTestGuard::new();
2580        const ITERATIONS: usize = 10_000;
2581
2582        let (weak_tx, weak_rx) = std::sync::mpsc::channel();
2583        let (release_tx, release_rx) = std::sync::mpsc::channel();
2584
2585        let worker = std::thread::spawn(move || {
2586            pool_clear();
2587            reset_value_pool_test_stats();
2588
2589            let mut pooled_weak = None;
2590            let mut overflow_weak = None;
2591            for value in 0..ITERATIONS {
2592                let payload: Arc<[u8]> =
2593                    Arc::from(vec![(value % 251) as u8; 64].into_boxed_slice());
2594                if value == 0 {
2595                    pooled_weak = Some(Arc::downgrade(&payload));
2596                } else if value == ITERATIONS - 1 {
2597                    overflow_weak = Some(Arc::downgrade(&payload));
2598                }
2599                pool_return(SqliteValue::Blob(payload));
2600            }
2601
2602            assert_eq!(
2603                pool_len(),
2604                VALUE_POOL_CAP,
2605                "the slab must retain at most VALUE_POOL_CAP entries",
2606            );
2607            weak_tx
2608                .send((
2609                    pooled_weak.expect("capture pooled weak handle"),
2610                    overflow_weak.expect("capture overflow weak handle"),
2611                    log_value_pool_test_stats("test_slab_no_leak"),
2612                ))
2613                .expect("send slab leak stats");
2614            release_rx.recv().expect("wait for release");
2615        });
2616
2617        let (pooled_weak, overflow_weak, stats) =
2618            weak_rx.recv().expect("receive weak blob handles");
2619        assert!(
2620            pooled_weak.upgrade().is_some(),
2621            "pooled blob should remain alive while the owning thread is running"
2622        );
2623        assert!(
2624            overflow_weak.upgrade().is_none(),
2625            "values beyond VALUE_POOL_CAP should fall back to normal drop instead of staying pooled"
2626        );
2627        assert_eq!(
2628            stats,
2629            ValuePoolStats {
2630                slab_alloc_count: 0,
2631                slab_return_count: VALUE_POOL_CAP,
2632                global_alloc_fallback_count: 0,
2633                slab_high_water_mark: VALUE_POOL_CAP,
2634            }
2635        );
2636
2637        release_tx.send(()).expect("release worker thread");
2638        worker.join().expect("join worker");
2639
2640        assert!(
2641            pooled_weak.upgrade().is_none(),
2642            "thread-local slab contents must be dropped when the thread exits"
2643        );
2644    }
2645
2646    #[test]
2647    fn test_slab_thread_local_isolation() {
2648        let _guard = ValuePoolTestGuard::new();
2649
2650        pool_return(SqliteValue::Integer(11));
2651        assert_eq!(pool_len(), 1);
2652
2653        let worker = std::thread::spawn(|| {
2654            pool_clear();
2655            reset_value_pool_test_stats();
2656
2657            assert_eq!(pool_len(), 0, "worker thread must start with an empty slab");
2658            pool_return(SqliteValue::Integer(22));
2659            assert_eq!(pool_len(), 1);
2660            assert_eq!(
2661                value_pool_test_stats_snapshot(),
2662                ValuePoolStats {
2663                    slab_alloc_count: 0,
2664                    slab_return_count: 1,
2665                    global_alloc_fallback_count: 0,
2666                    slab_high_water_mark: 1,
2667                }
2668            );
2669            assert_eq!(pool_acquire(), Some(SqliteValue::Integer(22)));
2670            assert_eq!(pool_len(), 0);
2671        });
2672        worker.join().expect("join worker");
2673
2674        assert_eq!(
2675            pool_len(),
2676            1,
2677            "worker thread slab operations must not affect the caller thread"
2678        );
2679        assert_eq!(pool_acquire(), Some(SqliteValue::Integer(11)));
2680        assert_eq!(pool_len(), 0);
2681        let stats = log_value_pool_test_stats("test_slab_thread_local_isolation");
2682        assert_eq!(
2683            stats,
2684            ValuePoolStats {
2685                slab_alloc_count: 1,
2686                slab_return_count: 1,
2687                global_alloc_fallback_count: 0,
2688                slab_high_water_mark: 1,
2689            }
2690        );
2691    }
2692
2693    #[test]
2694    fn test_slab_zero_malloc_steady_state() {
2695        let _guard = ValuePoolTestGuard::new();
2696        const WARM_POOL_DEPTH: usize = VALUE_POOL_CAP;
2697        const ITERATIONS: usize = 1_000;
2698        const INITIAL_TEXT: &str =
2699            "steady-state pooled string backing store for bd-nsvud warmup payload";
2700        const REUSED_TEXT: &str = "steady-state pooled overwrite stays in-buffer";
2701
2702        assert!(
2703            REUSED_TEXT.len() <= INITIAL_TEXT.len(),
2704            "steady-state overwrite must fit within the warmed heap allocation"
2705        );
2706
2707        for _ in 0..WARM_POOL_DEPTH {
2708            pool_return(SqliteValue::Text(SmallText::new(INITIAL_TEXT)));
2709        }
2710        assert_eq!(pool_len(), WARM_POOL_DEPTH);
2711
2712        reset_value_pool_test_stats();
2713        for _ in 0..ITERATIONS {
2714            let mut reused = pool_acquire().unwrap_or(SqliteValue::Null);
2715            let SqliteValue::Text(existing) = &mut reused else {
2716                panic!("warmed slab entry should remain a text value");
2717            };
2718            let original_ptr = existing.as_str().as_ptr();
2719            existing.overwrite(REUSED_TEXT);
2720            assert_eq!(
2721                existing.as_str().as_ptr(),
2722                original_ptr,
2723                "steady-state overwrite should reuse the warmed heap buffer",
2724            );
2725            assert_eq!(existing.as_str(), REUSED_TEXT);
2726            pool_return(reused);
2727        }
2728
2729        assert_eq!(pool_len(), WARM_POOL_DEPTH);
2730        assert_eq!(
2731            log_value_pool_test_stats("test_slab_zero_malloc_steady_state"),
2732            ValuePoolStats {
2733                slab_alloc_count: ITERATIONS,
2734                slab_return_count: ITERATIONS,
2735                global_alloc_fallback_count: 0,
2736                slab_high_water_mark: WARM_POOL_DEPTH,
2737            }
2738        );
2739    }
2740
2741    #[test]
2742    fn test_small_text_heap_clone_lazily_promotes_to_shared_arc() {
2743        let text = SmallText::new("this string is definitely longer than twenty three bytes");
2744        let SmallTextRepr::HeapOwned { shared, .. } = &text.repr else {
2745            panic!("long text should start in heap-owned mode");
2746        };
2747        assert!(
2748            shared.get().is_none(),
2749            "long text should not allocate Arc eagerly before cloning"
2750        );
2751
2752        let cloned = text.clone();
2753
2754        let SmallTextRepr::HeapOwned { shared, .. } = &text.repr else {
2755            panic!("original text should remain heap-owned after clone");
2756        };
2757        assert!(
2758            shared.get().is_some(),
2759            "first clone should materialize a shared Arc lazily"
2760        );
2761        assert!(
2762            matches!(cloned.repr, SmallTextRepr::HeapShared(_)),
2763            "cloned text should use the shared Arc representation"
2764        );
2765        assert_eq!(text.as_str(), cloned.as_str());
2766    }
2767
2768    #[test]
2769    fn test_small_text_invalid_utf8_clone_compare_and_serde_fail_closed() {
2770        let bytes: &[u8] = &[0x80, 0xC0, 0xAF];
2771        let text = SmallText::from_bytes(bytes);
2772        assert!(!text.is_valid_utf8());
2773        assert_eq!(text.as_str_checked(), None);
2774        assert_eq!(text.as_bytes_direct(), bytes);
2775        assert_eq!(text.len(), bytes.len());
2776        assert!(!text.is_inline());
2777
2778        let cloned = text.clone();
2779        assert_eq!(cloned, text);
2780        assert_eq!(cloned.as_bytes_direct(), bytes);
2781        assert_ne!(
2782            text,
2783            SmallText::new("\u{FFFD}\u{FFFD}\u{FFFD}"),
2784            "lossy display text must not define SQLite TEXT equality"
2785        );
2786
2787        let error = serde_json::to_string(&text)
2788            .expect_err("raw TEXT must not be silently substituted during string serialization");
2789        assert!(
2790            error.to_string().contains("invalid UTF-8"),
2791            "serialization failure must explain the unsupported Rust-string boundary: {error}"
2792        );
2793    }
2794
2795    #[test]
2796    fn test_small_text_overwrite_reuses_unique_heap_buffer() {
2797        let mut text = SmallText::new("this string is definitely longer than twenty three bytes");
2798        let (original_ptr, original_capacity) = match &text.repr {
2799            SmallTextRepr::HeapOwned { text, shared } => {
2800                assert!(shared.get().is_none(), "fresh heap text should be unshared");
2801                (text.as_ptr(), text.capacity())
2802            }
2803            _ => panic!("long text should start in heap-owned mode"),
2804        };
2805
2806        text.overwrite("another long string that still fits the same allocation");
2807
2808        match &text.repr {
2809            SmallTextRepr::HeapOwned { text, shared } => {
2810                assert!(
2811                    shared.get().is_none(),
2812                    "overwrite should keep text single-owner"
2813                );
2814                assert_eq!(text.as_ptr(), original_ptr);
2815                assert_eq!(text.capacity(), original_capacity);
2816                assert_eq!(
2817                    text.as_str(),
2818                    "another long string that still fits the same allocation"
2819                );
2820            }
2821            _ => panic!("overwrite should keep long text in heap-owned mode"),
2822        }
2823    }
2824
2825    #[test]
2826    fn test_small_text_overwrite_detaches_from_shared_arc() {
2827        let original = "this string is definitely longer than twenty three bytes";
2828        let mut text = SmallText::new(original);
2829        let (original_ptr, original_capacity) = match &text.repr {
2830            SmallTextRepr::HeapOwned { text, .. } => (text.as_ptr(), text.capacity()),
2831            _ => panic!("long text should start in heap-owned mode"),
2832        };
2833        let replacement = "replacement text that must not mutate the shared clone";
2834        assert!(
2835            replacement.len() <= original_capacity,
2836            "replacement should fit the original heap allocation for this regression",
2837        );
2838        let clone = text.clone();
2839
2840        text.overwrite(replacement);
2841
2842        assert_eq!(
2843            clone.as_str(),
2844            original,
2845            "existing shared clones must keep the original contents"
2846        );
2847        assert_eq!(text.as_str(), replacement);
2848        match &text.repr {
2849            SmallTextRepr::HeapOwned { text, shared } => {
2850                assert_eq!(
2851                    text.as_ptr(),
2852                    original_ptr,
2853                    "overwriting a cloned long string should keep the owned buffer",
2854                );
2855                assert_eq!(
2856                    text.capacity(),
2857                    original_capacity,
2858                    "detaching from the shared cache should preserve capacity",
2859                );
2860                assert!(
2861                    shared.get().is_none(),
2862                    "overwrite should reset the lazy shared cache after detaching"
2863                );
2864            }
2865            _ => panic!("overwrite should restore heap-owned mode"),
2866        }
2867    }
2868
2869    #[test]
2870    fn test_pool_return_reusable_keeps_only_reusable_heap_storage() {
2871        let _guard = ValuePoolTestGuard::new();
2872
2873        pool_return_reusable(SqliteValue::Text(SmallText::new("tiny")));
2874        assert_eq!(
2875            pool_len(),
2876            0,
2877            "inline text should not occupy reusable slab slots",
2878        );
2879
2880        let owned_text = SmallText::new("this string is definitely longer than twenty three bytes");
2881        let _clone = owned_text.clone();
2882        pool_return_reusable(SqliteValue::Text(owned_text));
2883        assert_eq!(
2884            pool_len(),
2885            1,
2886            "heap-owned text should stay reusable even after serving shared clones",
2887        );
2888        assert!(matches!(pool_acquire(), Some(SqliteValue::Text(_))));
2889        assert_eq!(pool_len(), 0);
2890
2891        let shared_text =
2892            Arc::<str>::from("this string is definitely longer than twenty three bytes");
2893        pool_return_reusable(SqliteValue::Text(SmallText::from_arc(Arc::clone(
2894            &shared_text,
2895        ))));
2896        assert_eq!(
2897            pool_len(),
2898            0,
2899            "arc-backed shared text should not enter the reusable slab",
2900        );
2901
2902        let shared_blob = Arc::<[u8]>::from([0xCA_u8, 0xFE, 0xBA, 0xBE].as_slice());
2903        pool_return_reusable(SqliteValue::Blob(Arc::clone(&shared_blob)));
2904        assert_eq!(
2905            pool_len(),
2906            0,
2907            "shared blob allocations should not displace reusable slab entries",
2908        );
2909
2910        let unique_blob = Arc::<[u8]>::from([1_u8, 2, 3, 4].as_slice());
2911        pool_return_reusable(SqliteValue::Blob(unique_blob));
2912        assert_eq!(
2913            pool_len(),
2914            1,
2915            "unique blob allocations should remain eligible for slab reuse",
2916        );
2917    }
2918
2919    #[test]
2920    fn test_small_text_concurrent_clone_promotion_keeps_contents_stable() {
2921        let text = Arc::new(SmallText::new(
2922            "this string is definitely longer than twenty three bytes",
2923        ));
2924        let expected = text.as_str().to_owned();
2925        let SmallTextRepr::HeapOwned { shared, .. } = &text.repr else {
2926            panic!("long text should start in heap-owned mode");
2927        };
2928        assert!(
2929            shared.get().is_none(),
2930            "shared Arc should still be lazy before concurrent clones"
2931        );
2932
2933        let barrier = Arc::new(std::sync::Barrier::new(5));
2934        let mut workers = Vec::new();
2935        for _ in 0..4 {
2936            let text = Arc::clone(&text);
2937            let barrier = Arc::clone(&barrier);
2938            let expected = expected.clone();
2939            workers.push(std::thread::spawn(move || {
2940                barrier.wait();
2941                for _ in 0..64 {
2942                    let cloned = (*text).clone();
2943                    assert_eq!(cloned.as_str(), expected);
2944                    assert!(
2945                        matches!(cloned.repr, SmallTextRepr::HeapShared(_)),
2946                        "concurrent clone should reuse the shared Arc representation"
2947                    );
2948                }
2949            }));
2950        }
2951
2952        barrier.wait();
2953        for worker in workers {
2954            worker
2955                .join()
2956                .expect("join concurrent small-text clone worker");
2957        }
2958
2959        let SmallTextRepr::HeapOwned { shared, .. } = &text.repr else {
2960            panic!("original text should remain heap-owned after clone promotion");
2961        };
2962        let shared = shared
2963            .get()
2964            .expect("concurrent clones should promote the lazy shared Arc");
2965        assert_eq!(shared.as_ref(), expected);
2966        assert_eq!(text.as_str(), expected);
2967    }
2968
2969    #[test]
2970    fn null_properties() {
2971        let v = SqliteValue::Null;
2972        assert!(v.is_null());
2973        assert_eq!(v.to_integer(), 0);
2974        assert_eq!(v.to_float(), 0.0);
2975        assert_eq!(v.to_text(), "");
2976        assert_eq!(v.to_string(), "NULL");
2977    }
2978
2979    #[test]
2980    fn integer_properties() {
2981        let v = SqliteValue::Integer(42);
2982        assert!(!v.is_null());
2983        assert_eq!(v.as_integer(), Some(42));
2984        assert_eq!(v.to_integer(), 42);
2985        assert_eq!(v.to_float(), 42.0);
2986        assert_eq!(v.to_text(), "42");
2987    }
2988
2989    #[test]
2990    fn float_properties() {
2991        let v = SqliteValue::Float(3.14);
2992        assert_eq!(v.as_float(), Some(3.14));
2993        assert_eq!(v.to_integer(), 3);
2994        assert_eq!(v.to_text(), "3.14");
2995    }
2996
2997    #[test]
2998    fn text_properties() {
2999        let v = SqliteValue::Text(SmallText::new("hello"));
3000        assert_eq!(v.as_text(), Some("hello"));
3001        assert_eq!(v.to_integer(), 0);
3002        assert_eq!(v.to_float(), 0.0);
3003    }
3004
3005    #[test]
3006    fn text_numeric_coercion() {
3007        let v = SqliteValue::Text(SmallText::new("123"));
3008        assert_eq!(v.to_integer(), 123);
3009        assert_eq!(v.to_float(), 123.0);
3010
3011        let v = SqliteValue::Text(SmallText::new("3.14"));
3012        assert_eq!(v.to_integer(), 3);
3013        assert_eq!(v.to_float(), 3.14);
3014    }
3015
3016    #[test]
3017    fn text_numeric_coercion_ignores_hex_text_prefixes() {
3018        let v = SqliteValue::Text(SmallText::new("0x10"));
3019        assert_eq!(v.to_integer(), 0);
3020        assert_eq!(v.to_float(), 0.0);
3021
3022        let v = SqliteValue::Blob(Arc::from(b"0x10".as_slice()));
3023        assert_eq!(v.to_integer(), 0);
3024        assert_eq!(v.to_float(), 0.0);
3025    }
3026
3027    #[test]
3028    fn sum_numeric_value_preserves_sqlite_integer_text_boundary() {
3029        assert_eq!(
3030            SqliteValue::Text(SmallText::new(" +123 ")).to_sum_numeric_value(),
3031            SqliteValue::Integer(123)
3032        );
3033        assert_eq!(
3034            SqliteValue::Text(SmallText::new("\u{00a0}123")).to_sum_numeric_value(),
3035            SqliteValue::Float(0.0)
3036        );
3037        assert_eq!(
3038            SqliteValue::Text(SmallText::new("123\u{00a0}")).to_sum_numeric_value(),
3039            SqliteValue::Float(123.0)
3040        );
3041        assert_eq!(
3042            SqliteValue::Text(SmallText::new("1.0")).to_sum_numeric_value(),
3043            SqliteValue::Float(1.0)
3044        );
3045        assert_eq!(
3046            SqliteValue::Text(SmallText::new("123abc")).to_sum_numeric_value(),
3047            SqliteValue::Float(123.0)
3048        );
3049        assert_eq!(
3050            SqliteValue::Text(SmallText::new("")).to_sum_numeric_value(),
3051            SqliteValue::Float(0.0)
3052        );
3053        assert_eq!(
3054            SqliteValue::Blob(Arc::from(b"123".as_slice())).to_sum_numeric_value(),
3055            SqliteValue::Float(123.0)
3056        );
3057    }
3058
3059    #[test]
3060    fn test_integer_numeric_type_uses_sqlite_prefix_rules() {
3061        assert!(SqliteValue::Text(SmallText::new("123abc")).is_integer_numeric_type());
3062        assert!(SqliteValue::Blob(Arc::from(b"123a".as_slice())).is_integer_numeric_type());
3063        assert!(!SqliteValue::Text(SmallText::new("1.5e2abc")).is_integer_numeric_type());
3064        assert!(!SqliteValue::Text(SmallText::new("abc")).is_integer_numeric_type());
3065    }
3066
3067    #[test]
3068    fn test_sqlite_value_integer_real_comparison_equal() {
3069        let int_value = SqliteValue::Integer(3);
3070        let real_value = SqliteValue::Float(3.0);
3071        assert_eq!(int_value.partial_cmp(&real_value), Some(Ordering::Equal));
3072        assert_eq!(real_value.partial_cmp(&int_value), Some(Ordering::Equal));
3073    }
3074
3075    #[test]
3076    fn test_sqlite_value_text_to_integer_coercion() {
3077        let text_value = SqliteValue::Text(SmallText::new("123"));
3078        let coerced = text_value.apply_affinity(TypeAffinity::Integer);
3079        assert_eq!(coerced, SqliteValue::Integer(123));
3080    }
3081
3082    #[test]
3083    fn blob_properties() {
3084        let v = SqliteValue::Blob(Arc::from([0xDE, 0xAD].as_slice()));
3085        assert_eq!(v.as_blob(), Some(&[0xDE, 0xAD][..]));
3086        assert_eq!(v.to_integer(), 0);
3087        assert_eq!(v.to_float(), 0.0);
3088        // to_text() interprets blob bytes as UTF-8 (matching CAST(blob AS TEXT)).
3089        // 0xDE 0xAD is valid UTF-8 encoding of U+07AD.
3090        assert_eq!(v.to_text(), "\u{07AD}");
3091    }
3092
3093    #[test]
3094    fn display_formatting() {
3095        assert_eq!(SqliteValue::Null.to_string(), "NULL");
3096        assert_eq!(SqliteValue::Integer(42).to_string(), "42");
3097        assert_eq!(SqliteValue::Integer(-1).to_string(), "-1");
3098        assert_eq!(SqliteValue::Float(1.5).to_string(), "1.5");
3099        assert_eq!(SqliteValue::Text(SmallText::new("hi")).to_string(), "'hi'");
3100        assert_eq!(
3101            SqliteValue::Blob(Arc::from([0xCA, 0xFE].as_slice())).to_string(),
3102            "X'CAFE'"
3103        );
3104    }
3105
3106    #[test]
3107    fn sort_order_null_first() {
3108        let null = SqliteValue::Null;
3109        let int = SqliteValue::Integer(0);
3110        let text = SqliteValue::Text(SmallText::new(""));
3111        let blob = SqliteValue::Blob(Arc::from(&[] as &[u8]));
3112
3113        assert!(null < int);
3114        assert!(int < text);
3115        assert!(text < blob);
3116    }
3117
3118    #[test]
3119    fn sort_order_integers() {
3120        let a = SqliteValue::Integer(1);
3121        let b = SqliteValue::Integer(2);
3122        assert!(a < b);
3123        assert_eq!(a.partial_cmp(&a), Some(Ordering::Equal));
3124    }
3125
3126    #[test]
3127    fn sort_order_mixed_numeric() {
3128        let int = SqliteValue::Integer(1);
3129        let float = SqliteValue::Float(1.5);
3130        assert!(int < float);
3131
3132        let int = SqliteValue::Integer(2);
3133        assert!(int > float);
3134    }
3135
3136    #[test]
3137    fn test_int_float_precision_at_i64_boundary() {
3138        // i64::MAX cast to f64 rounds UP to 9223372036854775808.0.
3139        // The naive (i as f64) comparison would say Equal, but C SQLite
3140        // correctly reports i64::MAX < 9223372036854775808.0.
3141        let imax = SqliteValue::Integer(i64::MAX);
3142        let fmax = SqliteValue::Float(9_223_372_036_854_775_808.0);
3143        assert_eq!(
3144            imax.partial_cmp(&fmax),
3145            Some(Ordering::Less),
3146            "i64::MAX must be Less than 9223372036854775808.0"
3147        );
3148
3149        // Two distinct large integers that map to the same f64.
3150        let a = SqliteValue::Integer(i64::MAX);
3151        let b = SqliteValue::Integer(i64::MAX - 1);
3152        let f = SqliteValue::Float(i64::MAX as f64);
3153        // a > b, but both should compare consistently vs the float.
3154        assert_eq!(a.partial_cmp(&b), Some(Ordering::Greater));
3155        // Both are less than the rounded-up float.
3156        assert_eq!(a.partial_cmp(&f), Some(Ordering::Less));
3157        assert_eq!(b.partial_cmp(&f), Some(Ordering::Less));
3158    }
3159
3160    #[test]
3161    fn test_int_float_precision_symmetric() {
3162        // Float-vs-Integer should be the reverse of Integer-vs-Float.
3163        let i = SqliteValue::Integer(i64::MAX);
3164        let f = SqliteValue::Float(9_223_372_036_854_775_808.0);
3165        assert_eq!(f.partial_cmp(&i), Some(Ordering::Greater));
3166    }
3167
3168    #[test]
3169    fn test_int_float_exact_representation() {
3170        // For exactly representable values, equality still works.
3171        let i = SqliteValue::Integer(42);
3172        let f = SqliteValue::Float(42.0);
3173        assert_eq!(i.partial_cmp(&f), Some(Ordering::Equal));
3174        assert_eq!(f.partial_cmp(&i), Some(Ordering::Equal));
3175
3176        // Integer 3 vs Float 3.5 — Integer is less.
3177        let i = SqliteValue::Integer(3);
3178        let f = SqliteValue::Float(3.5);
3179        assert_eq!(i.partial_cmp(&f), Some(Ordering::Less));
3180        assert_eq!(f.partial_cmp(&i), Some(Ordering::Greater));
3181    }
3182
3183    #[test]
3184    fn from_conversions() {
3185        assert_eq!(SqliteValue::from(42i64).as_integer(), Some(42));
3186        assert_eq!(SqliteValue::from(42i32).as_integer(), Some(42));
3187        assert_eq!(SqliteValue::from(1.5f64).as_float(), Some(1.5));
3188        assert_eq!(SqliteValue::from("hello").as_text(), Some("hello"));
3189        assert_eq!(
3190            SqliteValue::from(String::from("world")).as_text(),
3191            Some("world")
3192        );
3193        assert_eq!(SqliteValue::from(vec![1u8, 2]).as_blob(), Some(&[1, 2][..]));
3194        assert!(SqliteValue::from(None::<i64>).is_null());
3195        assert_eq!(SqliteValue::from(Some(42i64)).as_integer(), Some(42));
3196    }
3197
3198    #[test]
3199    fn affinity() {
3200        assert_eq!(SqliteValue::Null.affinity(), TypeAffinity::Blob);
3201        assert_eq!(SqliteValue::Integer(0).affinity(), TypeAffinity::Integer);
3202        assert_eq!(SqliteValue::Float(0.0).affinity(), TypeAffinity::Real);
3203        assert_eq!(
3204            SqliteValue::Text(SmallText::new("")).affinity(),
3205            TypeAffinity::Text
3206        );
3207        assert_eq!(
3208            SqliteValue::Blob(Arc::from(&[] as &[u8])).affinity(),
3209            TypeAffinity::Blob
3210        );
3211    }
3212
3213    #[test]
3214    fn null_equality() {
3215        // In SQLite, NULL == NULL is false, but for sorting they are equal
3216        let a = SqliteValue::Null;
3217        let b = SqliteValue::Null;
3218        assert_eq!(a.partial_cmp(&b), Some(Ordering::Equal));
3219    }
3220
3221    // ── bd-13r.1: Type Affinity Advisory + STRICT Enforcement ──
3222
3223    #[test]
3224    fn test_storage_class_variants() {
3225        assert_eq!(SqliteValue::Null.storage_class(), StorageClass::Null);
3226        assert_eq!(
3227            SqliteValue::Integer(42).storage_class(),
3228            StorageClass::Integer
3229        );
3230        assert_eq!(SqliteValue::Float(3.14).storage_class(), StorageClass::Real);
3231        assert_eq!(
3232            SqliteValue::Text("hi".into()).storage_class(),
3233            StorageClass::Text
3234        );
3235        assert_eq!(
3236            SqliteValue::Blob(Arc::from([1u8].as_slice())).storage_class(),
3237            StorageClass::Blob
3238        );
3239    }
3240
3241    #[test]
3242    fn test_type_affinity_advisory_text_into_integer_ok() {
3243        // INSERT TEXT "hello" into INTEGER-affinity column: text stays as text
3244        // (not a well-formed numeric literal).
3245        let val = SqliteValue::Text("hello".into());
3246        let coerced = val.apply_affinity(TypeAffinity::Integer);
3247        assert!(coerced.as_text().is_some());
3248        assert_eq!(coerced.as_text().unwrap(), "hello");
3249
3250        // INSERT TEXT "42" into INTEGER-affinity column: coerced to integer.
3251        let val = SqliteValue::Text("42".into());
3252        let coerced = val.apply_affinity(TypeAffinity::Integer);
3253        assert_eq!(coerced.as_integer(), Some(42));
3254    }
3255
3256    #[test]
3257    fn test_type_affinity_advisory_integer_into_text_ok() {
3258        // INSERT INTEGER 42 into TEXT-affinity column: coerced to text "42".
3259        let val = SqliteValue::Integer(42);
3260        let coerced = val.apply_affinity(TypeAffinity::Text);
3261        assert_eq!(coerced.as_text(), Some("42"));
3262    }
3263
3264    #[test]
3265    fn test_type_affinity_comparison_coercion_matches_oracle() {
3266        // NUMERIC affinity coerces text "123" to integer.
3267        let val = SqliteValue::Text("123".into());
3268        let coerced = val.apply_affinity(TypeAffinity::Numeric);
3269        assert_eq!(coerced.as_integer(), Some(123));
3270
3271        // NUMERIC affinity coerces text "3.14" to real.
3272        let val = SqliteValue::Text("3.14".into());
3273        let coerced = val.apply_affinity(TypeAffinity::Numeric);
3274        assert_eq!(coerced.as_float(), Some(3.14));
3275
3276        // NUMERIC affinity leaves text "hello" as text.
3277        let val = SqliteValue::Text("hello".into());
3278        let coerced = val.apply_affinity(TypeAffinity::Numeric);
3279        assert!(coerced.as_text().is_some());
3280
3281        // BLOB affinity never converts anything.
3282        let val = SqliteValue::Integer(42);
3283        let coerced = val.apply_affinity(TypeAffinity::Blob);
3284        assert_eq!(coerced.as_integer(), Some(42));
3285
3286        // INTEGER affinity converts exact-integer floats to integer.
3287        let val = SqliteValue::Float(5.0);
3288        let coerced = val.apply_affinity(TypeAffinity::Integer);
3289        assert_eq!(coerced.as_integer(), Some(5));
3290
3291        // INTEGER affinity keeps non-exact floats as float.
3292        let val = SqliteValue::Float(5.5);
3293        let coerced = val.apply_affinity(TypeAffinity::Integer);
3294        assert_eq!(coerced.as_float(), Some(5.5));
3295
3296        // REAL affinity forces integers to float.
3297        let val = SqliteValue::Integer(7);
3298        let coerced = val.apply_affinity(TypeAffinity::Real);
3299        assert_eq!(coerced.as_float(), Some(7.0));
3300
3301        // REAL affinity coerces text "9" to float 9.0.
3302        let val = SqliteValue::Text("9".into());
3303        let coerced = val.apply_affinity(TypeAffinity::Real);
3304        assert_eq!(coerced.as_float(), Some(9.0));
3305    }
3306
3307    #[test]
3308    fn test_cast_to_numeric_uses_sqlite_cast_rules() {
3309        assert_eq!(
3310            SqliteValue::Text(SmallText::new("123abc")).cast_to_numeric(),
3311            SqliteValue::Integer(123)
3312        );
3313        assert_eq!(
3314            SqliteValue::Text(SmallText::new("1.5e2abc")).cast_to_numeric(),
3315            SqliteValue::Integer(150)
3316        );
3317        assert_eq!(
3318            SqliteValue::Text(SmallText::new("abc")).cast_to_numeric(),
3319            SqliteValue::Integer(0)
3320        );
3321        assert_eq!(
3322            SqliteValue::Blob(Arc::from(b"123a".as_slice())).cast_to_numeric(),
3323            SqliteValue::Integer(123)
3324        );
3325
3326        match SqliteValue::Text(SmallText::new("1e999")).cast_to_numeric() {
3327            SqliteValue::Float(value) => assert!(value.is_infinite() && value.is_sign_positive()),
3328            other => panic!("expected +inf REAL from NUMERIC cast, got {other:?}"),
3329        }
3330    }
3331
3332    #[test]
3333    fn test_strict_table_rejects_text_into_integer() {
3334        let val = SqliteValue::Text("hello".into());
3335        let result = val.validate_strict(StrictColumnType::Integer);
3336        assert!(result.is_err());
3337        let err = result.unwrap_err();
3338        assert_eq!(err.expected, StrictColumnType::Integer);
3339        assert_eq!(err.actual, StorageClass::Text);
3340    }
3341
3342    #[test]
3343    fn test_strict_table_allows_exact_type() {
3344        // INTEGER into INTEGER column: ok.
3345        let val = SqliteValue::Integer(42);
3346        assert!(val.validate_strict(StrictColumnType::Integer).is_ok());
3347
3348        // REAL into REAL column: ok.
3349        let val = SqliteValue::Float(3.14);
3350        assert!(val.validate_strict(StrictColumnType::Real).is_ok());
3351
3352        // TEXT into TEXT column: ok.
3353        let val = SqliteValue::Text("hello".into());
3354        assert!(val.validate_strict(StrictColumnType::Text).is_ok());
3355
3356        // BLOB into BLOB column: ok.
3357        let val = SqliteValue::Blob(Arc::from([1u8, 2, 3].as_slice()));
3358        assert!(val.validate_strict(StrictColumnType::Blob).is_ok());
3359
3360        // NULL into any STRICT column: ok (nullability enforced separately).
3361        assert!(
3362            SqliteValue::Null
3363                .validate_strict(StrictColumnType::Integer)
3364                .is_ok()
3365        );
3366        assert!(
3367            SqliteValue::Null
3368                .validate_strict(StrictColumnType::Text)
3369                .is_ok()
3370        );
3371
3372        // ANY accepts everything.
3373        let val = SqliteValue::Integer(42);
3374        assert!(val.validate_strict(StrictColumnType::Any).is_ok());
3375        let val = SqliteValue::Text("hi".into());
3376        assert!(val.validate_strict(StrictColumnType::Any).is_ok());
3377    }
3378
3379    #[test]
3380    fn test_strict_real_accepts_integer_with_coercion() {
3381        // STRICT REAL column accepts INTEGER and coerces to float.
3382        let val = SqliteValue::Integer(42);
3383        let result = val.validate_strict(StrictColumnType::Real).unwrap();
3384        assert_eq!(result.as_float(), Some(42.0));
3385    }
3386
3387    #[test]
3388    fn test_strict_rejects_wrong_storage_classes() {
3389        // REAL into INTEGER column: rejected.
3390        assert!(
3391            SqliteValue::Float(3.14)
3392                .validate_strict(StrictColumnType::Integer)
3393                .is_err()
3394        );
3395
3396        // BLOB into TEXT column: rejected.
3397        assert!(
3398            SqliteValue::Blob(Arc::from([1u8].as_slice()))
3399                .validate_strict(StrictColumnType::Text)
3400                .is_err()
3401        );
3402
3403        // INTEGER into TEXT column: rejected.
3404        assert!(
3405            SqliteValue::Integer(1)
3406                .validate_strict(StrictColumnType::Text)
3407                .is_err()
3408        );
3409
3410        // TEXT into BLOB column: rejected.
3411        assert!(
3412            SqliteValue::Text("x".into())
3413                .validate_strict(StrictColumnType::Blob)
3414                .is_err()
3415        );
3416    }
3417
3418    #[test]
3419    fn test_strict_column_type_parsing() {
3420        assert_eq!(
3421            StrictColumnType::from_type_name("INT"),
3422            Some(StrictColumnType::Integer)
3423        );
3424        assert_eq!(
3425            StrictColumnType::from_type_name("INTEGER"),
3426            Some(StrictColumnType::Integer)
3427        );
3428        assert_eq!(
3429            StrictColumnType::from_type_name("REAL"),
3430            Some(StrictColumnType::Real)
3431        );
3432        assert_eq!(
3433            StrictColumnType::from_type_name("TEXT"),
3434            Some(StrictColumnType::Text)
3435        );
3436        assert_eq!(
3437            StrictColumnType::from_type_name("BLOB"),
3438            Some(StrictColumnType::Blob)
3439        );
3440        assert_eq!(
3441            StrictColumnType::from_type_name("ANY"),
3442            Some(StrictColumnType::Any)
3443        );
3444        // Invalid type name in STRICT mode.
3445        assert_eq!(StrictColumnType::from_type_name("VARCHAR(255)"), None);
3446        assert_eq!(StrictColumnType::from_type_name("NUMERIC"), None);
3447    }
3448
3449    #[test]
3450    fn test_affinity_advisory_never_rejects() {
3451        // Advisory affinity NEVER rejects a value. All combinations must succeed.
3452        let values = vec![
3453            SqliteValue::Null,
3454            SqliteValue::Integer(42),
3455            SqliteValue::Float(3.14),
3456            SqliteValue::Text("hello".into()),
3457            SqliteValue::Blob(Arc::from([0xDE, 0xAD].as_slice())),
3458        ];
3459        let affinities = [
3460            TypeAffinity::Integer,
3461            TypeAffinity::Text,
3462            TypeAffinity::Blob,
3463            TypeAffinity::Real,
3464            TypeAffinity::Numeric,
3465        ];
3466        for val in &values {
3467            for aff in &affinities {
3468                // apply_affinity is infallible - it always returns a value.
3469                let _ = val.clone().apply_affinity(*aff);
3470            }
3471        }
3472    }
3473
3474    // ── bd-13r.2: UNIQUE NULL Semantics (NULL != NULL) ──
3475
3476    #[test]
3477    fn test_unique_allows_multiple_nulls_single_column() {
3478        // In UNIQUE columns, NULL != NULL: two NULLs are never duplicates.
3479        let a = SqliteValue::Null;
3480        let b = SqliteValue::Null;
3481        assert!(!a.unique_eq(&b));
3482    }
3483
3484    #[test]
3485    fn test_unique_allows_multiple_nulls_multi_column_partial_null() {
3486        // UNIQUE(a,b): (NULL,1) and (NULL,1) are NOT duplicates because
3487        // any NULL component makes the whole key non-duplicate.
3488        let row_a = [SqliteValue::Null, SqliteValue::Integer(1)];
3489        let row_b = [SqliteValue::Null, SqliteValue::Integer(1)];
3490        assert!(!unique_key_duplicates(&row_a, &row_b));
3491
3492        // UNIQUE(a,b): (1,NULL) and (1,NULL) are NOT duplicates.
3493        let row_a = [SqliteValue::Integer(1), SqliteValue::Null];
3494        let row_b = [SqliteValue::Integer(1), SqliteValue::Null];
3495        assert!(!unique_key_duplicates(&row_a, &row_b));
3496
3497        // UNIQUE(a,b): (NULL,NULL) and (NULL,NULL) are NOT duplicates.
3498        let row_a = [SqliteValue::Null, SqliteValue::Null];
3499        let row_b = [SqliteValue::Null, SqliteValue::Null];
3500        assert!(!unique_key_duplicates(&row_a, &row_b));
3501    }
3502
3503    #[test]
3504    fn test_unique_rejects_duplicate_non_null() {
3505        // Two identical non-NULL values ARE duplicates.
3506        let a = SqliteValue::Integer(42);
3507        let b = SqliteValue::Integer(42);
3508        assert!(a.unique_eq(&b));
3509
3510        // Composite: (1, "hello") and (1, "hello") ARE duplicates.
3511        let row_a = [SqliteValue::Integer(1), SqliteValue::Text("hello".into())];
3512        let row_b = [SqliteValue::Integer(1), SqliteValue::Text("hello".into())];
3513        assert!(unique_key_duplicates(&row_a, &row_b));
3514
3515        // Different values are NOT duplicates.
3516        let row_a = [SqliteValue::Integer(1), SqliteValue::Text("hello".into())];
3517        let row_b = [SqliteValue::Integer(1), SqliteValue::Text("world".into())];
3518        assert!(!unique_key_duplicates(&row_a, &row_b));
3519    }
3520
3521    #[test]
3522    fn test_unique_null_vs_non_null_distinct() {
3523        // NULL and a non-NULL value are never duplicates.
3524        let a = SqliteValue::Null;
3525        let b = SqliteValue::Integer(1);
3526        assert!(!a.unique_eq(&b));
3527        assert!(!b.unique_eq(&a));
3528
3529        // Composite: (NULL, 1) and (2, 1) are not duplicates (different first element).
3530        let row_a = [SqliteValue::Null, SqliteValue::Integer(1)];
3531        let row_b = [SqliteValue::Integer(2), SqliteValue::Integer(1)];
3532        assert!(!unique_key_duplicates(&row_a, &row_b));
3533    }
3534
3535    // ── bd-13r.4: Integer Overflow Semantics (Expr vs sum()) ──
3536
3537    #[test]
3538    #[allow(clippy::cast_precision_loss)]
3539    fn test_integer_overflow_promotes_real_expr_add() {
3540        let max = SqliteValue::Integer(i64::MAX);
3541        let one = SqliteValue::Integer(1);
3542        let result = max.sql_add(&one);
3543        // Overflow promotes to REAL (not integer).
3544        assert!(result.as_integer().is_none());
3545        assert!(result.as_float().is_some());
3546        // The float value is approximately i64::MAX + 1.
3547        assert!(result.as_float().unwrap() >= i64::MAX as f64);
3548    }
3549
3550    #[test]
3551    fn test_integer_overflow_promotes_real_expr_mul() {
3552        let max = SqliteValue::Integer(i64::MAX);
3553        let two = SqliteValue::Integer(2);
3554        let result = max.sql_mul(&two);
3555        // Overflow promotes to REAL.
3556        assert!(result.as_float().is_some());
3557    }
3558
3559    #[test]
3560    fn test_integer_overflow_promotes_real_expr_sub() {
3561        let min = SqliteValue::Integer(i64::MIN);
3562        let one = SqliteValue::Integer(1);
3563        let result = min.sql_sub(&one);
3564        // Underflow promotes to REAL.
3565        assert!(result.as_float().is_some());
3566    }
3567
3568    #[test]
3569    fn test_sum_overflow_errors() {
3570        let mut acc = SumAccumulator::new();
3571        acc.accumulate(&SqliteValue::Integer(i64::MAX));
3572        acc.accumulate(&SqliteValue::Integer(1));
3573        let result = acc.finish();
3574        assert!(result.is_err());
3575    }
3576
3577    #[test]
3578    fn test_sum_overflow_then_float_returns_real() {
3579        let mut acc = SumAccumulator::new();
3580        acc.accumulate(&SqliteValue::Integer(i64::MAX));
3581        acc.accumulate(&SqliteValue::Integer(1));
3582        acc.accumulate(&SqliteValue::Float(0.5));
3583        let result = acc.finish().unwrap();
3584        assert!(matches!(result, SqliteValue::Float(_)));
3585    }
3586
3587    #[test]
3588    fn test_sum_text_integer_literals_stay_integer() {
3589        let mut acc = SumAccumulator::new();
3590        acc.accumulate(&SqliteValue::Text(SmallText::new("1")));
3591        acc.accumulate(&SqliteValue::Text(SmallText::new("2")));
3592        let result = acc.finish().unwrap();
3593        assert_eq!(result.as_integer(), Some(3));
3594    }
3595
3596    #[test]
3597    fn test_sum_non_numeric_text_returns_real_zero() {
3598        let mut acc = SumAccumulator::new();
3599        acc.accumulate(&SqliteValue::Text(SmallText::new("abc")));
3600        let result = acc.finish().unwrap();
3601        assert_eq!(result.as_float(), Some(0.0));
3602    }
3603
3604    #[test]
3605    fn test_no_overflow_stays_integer() {
3606        // Non-overflow addition stays INTEGER.
3607        let a = SqliteValue::Integer(100);
3608        let b = SqliteValue::Integer(200);
3609        let result = a.sql_add(&b);
3610        assert_eq!(result.as_integer(), Some(300));
3611
3612        // Non-overflow multiplication stays INTEGER.
3613        let result = SqliteValue::Integer(7).sql_mul(&SqliteValue::Integer(6));
3614        assert_eq!(result.as_integer(), Some(42));
3615
3616        // Non-overflow subtraction stays INTEGER.
3617        let result = SqliteValue::Integer(50).sql_sub(&SqliteValue::Integer(8));
3618        assert_eq!(result.as_integer(), Some(42));
3619    }
3620
3621    #[test]
3622    fn test_sum_null_only_returns_null() {
3623        let mut acc = SumAccumulator::new();
3624        acc.accumulate(&SqliteValue::Null);
3625        acc.accumulate(&SqliteValue::Null);
3626        let result = acc.finish().unwrap();
3627        assert!(result.is_null());
3628    }
3629
3630    #[test]
3631    fn test_sum_mixed_int_float() {
3632        let mut acc = SumAccumulator::new();
3633        acc.accumulate(&SqliteValue::Integer(10));
3634        acc.accumulate(&SqliteValue::Float(2.5));
3635        acc.accumulate(&SqliteValue::Integer(3));
3636        let result = acc.finish().unwrap();
3637        // Once float is seen, result is float.
3638        assert_eq!(result.as_float(), Some(15.5));
3639    }
3640
3641    #[test]
3642    fn test_sum_integer_only() {
3643        let mut acc = SumAccumulator::new();
3644        acc.accumulate(&SqliteValue::Integer(10));
3645        acc.accumulate(&SqliteValue::Integer(20));
3646        acc.accumulate(&SqliteValue::Integer(30));
3647        let result = acc.finish().unwrap();
3648        assert_eq!(result.as_integer(), Some(60));
3649    }
3650
3651    #[test]
3652    fn test_sql_arithmetic_null_propagation() {
3653        let n = SqliteValue::Null;
3654        let i = SqliteValue::Integer(42);
3655        assert!(n.sql_add(&i).is_null());
3656        assert!(i.sql_add(&n).is_null());
3657        assert!(n.sql_sub(&i).is_null());
3658        assert!(n.sql_mul(&i).is_null());
3659    }
3660
3661    #[test]
3662    fn test_sql_inf_arithmetic_nan_normalized_to_null() {
3663        // +Inf + (-Inf) is NaN in IEEE-754 and must be normalized to NULL.
3664        let pos_inf = SqliteValue::Float(f64::INFINITY);
3665        let neg_inf = SqliteValue::Float(f64::NEG_INFINITY);
3666        assert!(pos_inf.sql_add(&neg_inf).is_null());
3667
3668        // +Inf - +Inf is also NaN and must normalize to NULL.
3669        assert!(pos_inf.sql_sub(&pos_inf).is_null());
3670    }
3671
3672    #[test]
3673    fn test_sql_mul_zero_times_inf_normalized_to_null() {
3674        // 0 * +Inf is NaN in IEEE-754 and must be normalized to NULL.
3675        let zero = SqliteValue::Float(0.0);
3676        let pos_inf = SqliteValue::Float(f64::INFINITY);
3677        assert!(zero.sql_mul(&pos_inf).is_null());
3678        assert!(
3679            SqliteValue::Integer(0).sql_mul(&pos_inf).is_null(),
3680            "mixed INTEGER/REAL multiplication should preserve NaN-to-NULL semantics"
3681        );
3682    }
3683
3684    #[test]
3685    fn test_sql_mul_mixed_int_float_stays_real() {
3686        let left = SqliteValue::Integer(10);
3687        let right = SqliteValue::Float(0.25);
3688        assert_eq!(left.sql_mul(&right).as_float(), Some(2.5));
3689        assert_eq!(right.sql_mul(&left).as_float(), Some(2.5));
3690    }
3691
3692    #[test]
3693    fn test_sql_inf_propagates_when_not_nan() {
3694        let pos_inf = SqliteValue::Float(f64::INFINITY);
3695        let one = SqliteValue::Integer(1);
3696        let add_result = pos_inf.sql_add(&one);
3697        assert!(
3698            matches!(add_result, SqliteValue::Float(v) if v.is_infinite() && v.is_sign_positive()),
3699            "expected +Inf propagation, got {add_result:?}"
3700        );
3701
3702        let neg_inf = SqliteValue::Float(f64::NEG_INFINITY);
3703        let sub_result = neg_inf.sql_sub(&one);
3704        assert!(
3705            matches!(sub_result, SqliteValue::Float(v) if v.is_infinite() && v.is_sign_negative()),
3706            "expected -Inf propagation, got {sub_result:?}"
3707        );
3708    }
3709
3710    #[test]
3711    fn test_from_f64_nan_normalizes_to_null() {
3712        let value = SqliteValue::from(f64::NAN);
3713        assert!(value.is_null());
3714    }
3715
3716    #[test]
3717    fn test_inf_comparisons_against_finite_values() {
3718        let pos_inf = SqliteValue::Float(f64::INFINITY);
3719        let neg_inf = SqliteValue::Float(f64::NEG_INFINITY);
3720        let finite_hi = SqliteValue::Float(1.0e308);
3721        let finite_lo = SqliteValue::Float(-1.0e308);
3722
3723        assert_eq!(pos_inf.partial_cmp(&finite_hi), Some(Ordering::Greater));
3724        assert_eq!(neg_inf.partial_cmp(&finite_lo), Some(Ordering::Less));
3725    }
3726
3727    // ── bd-13r.7: Empty String vs NULL Semantics ──
3728
3729    #[test]
3730    fn test_empty_string_is_not_null() {
3731        let empty = SqliteValue::Text(SmallText::new(""));
3732        // '' IS NULL → false.
3733        assert!(!empty.is_null());
3734        // '' IS NOT NULL → true (expressed as !is_null).
3735        assert!(!empty.is_null());
3736        // NULL IS NULL → true.
3737        assert!(SqliteValue::Null.is_null());
3738    }
3739
3740    #[test]
3741    fn test_length_empty_string_zero() {
3742        let empty = SqliteValue::Text(SmallText::new(""));
3743        assert_eq!(empty.sql_length(), Some(0));
3744    }
3745
3746    #[test]
3747    fn test_typeof_empty_string_text() {
3748        let empty = SqliteValue::Text(SmallText::new(""));
3749        assert_eq!(empty.typeof_str(), "text");
3750        // NULL has typeof "null".
3751        assert_eq!(SqliteValue::Null.typeof_str(), "null");
3752    }
3753
3754    #[test]
3755    fn test_empty_string_comparisons() {
3756        let empty1 = SqliteValue::Text(SmallText::new(""));
3757        let empty2 = SqliteValue::Text(SmallText::new(""));
3758        // '' = '' → true.
3759        assert_eq!(empty1.partial_cmp(&empty2), Some(std::cmp::Ordering::Equal));
3760
3761        // '' = NULL → NULL (comparison with NULL yields None/unknown).
3762        // In our PartialOrd, NULL and TEXT are different sort classes,
3763        // so NULL < TEXT (they are not equal).
3764        let null = SqliteValue::Null;
3765        assert_ne!(empty1.partial_cmp(&null), Some(std::cmp::Ordering::Equal));
3766    }
3767
3768    #[test]
3769    fn test_typeof_all_variants() {
3770        assert_eq!(SqliteValue::Null.typeof_str(), "null");
3771        assert_eq!(SqliteValue::Integer(0).typeof_str(), "integer");
3772        assert_eq!(SqliteValue::Float(0.0).typeof_str(), "real");
3773        assert_eq!(SqliteValue::Text("x".into()).typeof_str(), "text");
3774        assert_eq!(
3775            SqliteValue::Blob(Arc::from(&[] as &[u8])).typeof_str(),
3776            "blob"
3777        );
3778    }
3779
3780    #[test]
3781    fn test_sql_length_all_types() {
3782        // NULL → NULL (None).
3783        assert_eq!(SqliteValue::Null.sql_length(), None);
3784        // TEXT → character count.
3785        assert_eq!(SqliteValue::Text("hello".into()).sql_length(), Some(5));
3786        assert_eq!(SqliteValue::Text(SmallText::new("")).sql_length(), Some(0));
3787        // BLOB → byte count.
3788        assert_eq!(
3789            SqliteValue::Blob(Arc::from([1u8, 2, 3].as_slice())).sql_length(),
3790            Some(3)
3791        );
3792        // INTEGER → length of text representation.
3793        assert_eq!(SqliteValue::Integer(42).sql_length(), Some(2));
3794        // REAL → length of text representation.
3795        assert_eq!(SqliteValue::Float(3.14).sql_length(), Some(4)); // "3.14"
3796    }
3797
3798    // ── bd-13r.6: LIKE Semantics (ASCII-only case folding) ──
3799
3800    #[test]
3801    fn test_like_ascii_case_insensitive() {
3802        assert!(sql_like("A", "a", None));
3803        assert!(sql_like("a", "A", None));
3804        assert!(sql_like("hello", "HELLO", None));
3805        assert!(sql_like("HELLO", "hello", None));
3806        assert!(sql_like("HeLLo", "hEllO", None));
3807    }
3808
3809    #[test]
3810    fn test_like_unicode_case_sensitive_without_icu() {
3811        // Without ICU, Unicode case folding does NOT occur.
3812        assert!(!sql_like("ä", "Ä", None));
3813        assert!(!sql_like("Ä", "ä", None));
3814        // But exact match works.
3815        assert!(sql_like("ä", "ä", None));
3816    }
3817
3818    #[test]
3819    fn test_like_fast_path_does_not_fold_ascii_punctuation() {
3820        assert!(!sql_like("[", "{", None));
3821        assert!(!sql_like("@", "`", None));
3822    }
3823
3824    #[test]
3825    fn test_like_escape_handling() {
3826        // Escape literal % with backslash.
3827        assert!(sql_like("100\\%", "100%", Some('\\')));
3828        assert!(!sql_like("100\\%", "100x", Some('\\')));
3829
3830        // Escape literal _.
3831        assert!(sql_like("a\\_b", "a_b", Some('\\')));
3832        assert!(!sql_like("a\\_b", "axb", Some('\\')));
3833    }
3834
3835    #[test]
3836    fn test_like_wildcards_basic() {
3837        // % matches zero or more characters.
3838        assert!(sql_like("%", "", None));
3839        assert!(sql_like("%", "anything", None));
3840        assert!(sql_like("a%", "abc", None));
3841        assert!(sql_like("%c", "abc", None));
3842        assert!(sql_like("a%c", "abc", None));
3843        assert!(sql_like("a%c", "aXYZc", None));
3844        assert!(!sql_like("a%c", "abd", None));
3845
3846        // _ matches exactly one character.
3847        assert!(sql_like("_", "x", None));
3848        assert!(!sql_like("_", "", None));
3849        assert!(!sql_like("_", "xy", None));
3850        assert!(sql_like("a_c", "abc", None));
3851        assert!(!sql_like("a_c", "abbc", None));
3852    }
3853
3854    #[test]
3855    fn test_like_combined_wildcards() {
3856        assert!(sql_like("%_", "a", None));
3857        assert!(!sql_like("%_", "", None));
3858        assert!(sql_like("_%_", "ab", None));
3859        assert!(!sql_like("_%_", "a", None));
3860        assert!(sql_like("%a%b%", "xaybz", None));
3861        assert!(!sql_like("%a%b%", "xyz", None));
3862    }
3863
3864    #[test]
3865    fn test_like_exact_match() {
3866        assert!(sql_like("hello", "hello", None));
3867        assert!(!sql_like("hello", "world", None));
3868        assert!(sql_like("", "", None));
3869        assert!(!sql_like("a", "", None));
3870        assert!(!sql_like("", "a", None));
3871    }
3872
3873    #[test]
3874    fn test_like_fast_path_repeated_percent_shapes() {
3875        assert!(sql_like("ab%%", "ABcd", None));
3876        assert!(sql_like("%%cd", "abCD", None));
3877        assert!(sql_like("%%bc%%", "xxBCyy", None));
3878        assert!(sql_like("%%%%", "anything", None));
3879    }
3880
3881    #[test]
3882    fn test_like_fast_path_preserves_mixed_unicode_and_ascii_semantics() {
3883        assert!(sql_like("%éL%", "héllo", None));
3884        assert!(!sql_like("%Él%", "héllo", None));
3885        assert!(sql_like("Stra%", "straße", None));
3886    }
3887
3888    #[test]
3889    fn test_like_contains_fast_path_handles_overlapping_matches() {
3890        assert!(sql_like("%ana%", "bananas", None));
3891        assert!(sql_like("%NAN%", "baNanas", None));
3892        assert!(!sql_like("%ananasx%", "bananas", None));
3893    }
3894
3895    #[test]
3896    fn test_like_contains_fast_path_preserves_non_ascii_byte_matching() {
3897        assert!(sql_like("%ß%", "straße", None));
3898        assert!(!sql_like("%SS%", "straße", None));
3899    }
3900
3901    // ── format_sqlite_float ────────────────────────────────────────────
3902
3903    #[test]
3904    fn test_format_sqlite_float_whole_number() {
3905        assert_eq!(format_sqlite_float(120.0), "120.0");
3906        assert_eq!(format_sqlite_float(0.0), "0.0");
3907        assert_eq!(format_sqlite_float(-42.0), "-42.0");
3908        assert_eq!(format_sqlite_float(1.0), "1.0");
3909    }
3910
3911    #[test]
3912    fn test_format_sqlite_float_fractional() {
3913        assert_eq!(format_sqlite_float(3.14), "3.14");
3914        assert_eq!(format_sqlite_float(0.5), "0.5");
3915        assert_eq!(format_sqlite_float(-0.001), "-0.001");
3916    }
3917
3918    #[test]
3919    fn test_format_sqlite_float_special() {
3920        assert_eq!(format_sqlite_float(f64::NAN), "NaN");
3921        assert_eq!(format_sqlite_float(f64::INFINITY), "Inf");
3922        assert_eq!(format_sqlite_float(f64::NEG_INFINITY), "-Inf");
3923    }
3924
3925    #[test]
3926    fn test_format_sqlite_float_negative_zero() {
3927        // SQLite CAST(... AS TEXT) normalizes both zero signs to "0.0".
3928        assert_eq!(format_sqlite_float(-0.0), "0.0");
3929        assert_eq!(format_sqlite_float(0.0), "0.0");
3930    }
3931
3932    #[test]
3933    fn test_format_sqlite_float_matches_sqlite_17_digit_text_contract() {
3934        assert_eq!(format_sqlite_float(0.1 + 0.2), "0.30000000000000004");
3935        assert_eq!(format_sqlite_float(1.0 / 3.0), "0.33333333333333332");
3936        assert_eq!(format_sqlite_float(2.0 / 3.0), "0.66666666666666663");
3937        assert_eq!(format_sqlite_float(1.5e16), "15000000000000000.0");
3938        assert_eq!(
3939            format_sqlite_float(123_456_789_012_345.6),
3940            "123456789012345.59"
3941        );
3942        assert_eq!(format_sqlite_float(1.0e308), "1.0e+308");
3943        assert_eq!(format_sqlite_float(1.0e-308), "1.0e-308");
3944        assert_eq!(
3945            format_sqlite_float(9.223_372_036_854_776e18),
3946            "9.2233720368547758e+18"
3947        );
3948    }
3949
3950    #[test]
3951    fn test_float_to_text_includes_decimal_point() {
3952        let v = SqliteValue::Float(100.0);
3953        assert_eq!(v.to_text(), "100.0");
3954        let v = SqliteValue::Float(3.14);
3955        assert_eq!(v.to_text(), "3.14");
3956    }
3957
3958    // ── scan_numeric_prefix ──────────────────────────────────────────
3959
3960    #[test]
3961    fn test_scan_numeric_prefix_bare_dot() {
3962        // A bare "." has no digits — not a numeric prefix.
3963        assert_eq!(scan_numeric_prefix(b"."), 0);
3964        assert_eq!(scan_numeric_prefix(b"-."), 0);
3965        assert_eq!(scan_numeric_prefix(b"+."), 0);
3966        assert_eq!(scan_numeric_prefix(b"..1"), 0);
3967    }
3968
3969    #[test]
3970    fn test_scan_numeric_prefix_valid() {
3971        assert_eq!(scan_numeric_prefix(b"123"), 3);
3972        assert_eq!(scan_numeric_prefix(b"3.14"), 4);
3973        assert_eq!(scan_numeric_prefix(b".5"), 2);
3974        assert_eq!(scan_numeric_prefix(b"1e10"), 4);
3975        assert_eq!(scan_numeric_prefix(b"-42abc"), 3);
3976        assert_eq!(scan_numeric_prefix(b"+.5x"), 3);
3977        assert_eq!(scan_numeric_prefix(b"0.0"), 3);
3978    }
3979
3980    #[test]
3981    fn test_scan_numeric_prefix_empty_and_non_numeric() {
3982        assert_eq!(scan_numeric_prefix(b""), 0);
3983        assert_eq!(scan_numeric_prefix(b"abc"), 0);
3984        assert_eq!(scan_numeric_prefix(b"+"), 0);
3985        assert_eq!(scan_numeric_prefix(b"-"), 0);
3986    }
3987}