Skip to main content

fsqlite_types/
value.rs

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