Skip to main content

fsqlite_types/
value.rs

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