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