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