Skip to main content

fsqlite_types/
value.rs

1use std::cmp::Ordering;
2use std::fmt;
3use std::sync::Arc;
4
5use crate::{StorageClass, StrictColumnType, StrictTypeError, TypeAffinity};
6
7/// Scan the longest SQLite numeric prefix from a byte slice.
8///
9/// Recognises `[+-]? [0-9]* ('.' [0-9]*)? ([eE] [+-]? [0-9]+)?`.
10/// Returns the byte offset where the prefix ends, or 0 if no numeric prefix
11/// is present.
12fn scan_numeric_prefix(bytes: &[u8]) -> usize {
13    if bytes.is_empty() {
14        return 0;
15    }
16
17    let mut i = 0usize;
18    if bytes[i] == b'+' || bytes[i] == b'-' {
19        i += 1;
20    }
21
22    let mut has_digit = false;
23    while i < bytes.len() && bytes[i].is_ascii_digit() {
24        has_digit = true;
25        i += 1;
26    }
27
28    if i < bytes.len() && bytes[i] == b'.' {
29        i += 1;
30        while i < bytes.len() && bytes[i].is_ascii_digit() {
31            has_digit = true;
32            i += 1;
33        }
34    }
35
36    if !has_digit {
37        return 0;
38    }
39
40    if i < bytes.len() && (bytes[i] == b'e' || bytes[i] == b'E') {
41        let exp_start = i;
42        i += 1;
43        if i < bytes.len() && (bytes[i] == b'+' || bytes[i] == b'-') {
44            i += 1;
45        }
46        if i < bytes.len() && bytes[i].is_ascii_digit() {
47            while i < bytes.len() && bytes[i].is_ascii_digit() {
48                i += 1;
49            }
50        } else {
51            i = exp_start;
52        }
53    }
54
55    i
56}
57
58/// Parse the longest numeric prefix of `b` as an integer.
59#[allow(clippy::cast_possible_truncation)]
60fn parse_integer_prefix_bytes(b: &[u8]) -> i64 {
61    let mut start = 0;
62    while start < b.len() && b[start].is_ascii_whitespace() {
63        start += 1;
64    }
65    let trimmed = &b[start..];
66    let end = scan_numeric_prefix(trimmed);
67    if end == 0 {
68        return 0;
69    }
70    // SAFETY: scan_numeric_prefix only advances over ASCII bytes (digits, +, -, ., e, E),
71    // so the slice is always valid UTF-8.
72    let s = std::str::from_utf8(&trimmed[..end]).unwrap_or("");
73    let f = s.parse::<f64>().unwrap_or(0.0);
74    #[allow(clippy::manual_clamp)]
75    if f >= i64::MAX as f64 {
76        i64::MAX
77    } else if f <= i64::MIN as f64 {
78        i64::MIN
79    } else {
80        f as i64
81    }
82}
83
84/// Parse the longest numeric prefix of `s` as an integer.
85#[allow(clippy::cast_possible_truncation)]
86fn parse_integer_prefix(s: &str) -> i64 {
87    parse_integer_prefix_bytes(s.as_bytes())
88}
89
90/// Parse the longest numeric prefix of `b` as a float.
91fn parse_float_prefix_bytes(b: &[u8]) -> f64 {
92    let mut start = 0;
93    while start < b.len() && b[start].is_ascii_whitespace() {
94        start += 1;
95    }
96    let trimmed = &b[start..];
97    let end = scan_numeric_prefix(trimmed);
98    if end == 0 {
99        return 0.0;
100    }
101    // SAFETY: scan_numeric_prefix only advances over ASCII bytes (digits, +, -, ., e, E),
102    // so the slice is always valid UTF-8.
103    let s = std::str::from_utf8(&trimmed[..end]).unwrap_or("");
104    s.parse::<f64>().unwrap_or(0.0)
105}
106
107/// Parse the longest numeric prefix of `s` as a float.
108fn parse_float_prefix(s: &str) -> f64 {
109    parse_float_prefix_bytes(s.as_bytes())
110}
111
112fn cast_text_prefix_to_numeric(s: &str) -> SqliteValue {
113    let trimmed = s.trim();
114    let end = scan_numeric_prefix(trimmed.as_bytes());
115    if end == 0 {
116        return SqliteValue::Integer(0);
117    }
118
119    let prefix = &trimmed[..end];
120    let is_integer_syntax = !prefix
121        .as_bytes()
122        .iter()
123        .any(|byte| matches!(*byte, b'.' | b'e' | b'E'));
124
125    if is_integer_syntax && let Ok(value) = prefix.parse::<i64>() {
126        return SqliteValue::Integer(value);
127    }
128
129    if let Ok(value) = prefix.parse::<f64>() {
130        if value.is_finite()
131            && (-9_223_372_036_854_775_808.0..9_223_372_036_854_775_808.0).contains(&value)
132        {
133            #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
134            let truncated = value as i64;
135            #[allow(clippy::float_cmp, clippy::cast_precision_loss)]
136            if truncated as f64 == value {
137                return SqliteValue::Integer(truncated);
138            }
139        }
140        return SqliteValue::Float(value);
141    }
142
143    SqliteValue::Integer(0)
144}
145
146/// A dynamically-typed SQLite value.
147///
148/// Corresponds to C SQLite's `sqlite3_value` / `Mem` type. SQLite has five
149/// fundamental storage classes: NULL, INTEGER, REAL, TEXT, and BLOB.
150#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
151pub enum SqliteValue {
152    /// A NULL value.
153    Null,
154    /// A signed 64-bit integer.
155    Integer(i64),
156    /// A 64-bit IEEE floating point number.
157    Float(f64),
158    /// A UTF-8 text string.
159    ///
160    /// Uses `Arc<str>` so that register copies (SCopy, Copy, ResultRow) are
161    /// O(1) atomic refcount increments instead of O(n) heap copies.
162    Text(Arc<str>),
163    /// A binary large object.
164    ///
165    /// Uses `Arc<[u8]>` for the same O(1)-clone benefit as `Text`.
166    Blob(Arc<[u8]>),
167}
168
169impl SqliteValue {
170    /// Returns the type affinity that best describes this value.
171    pub const fn affinity(&self) -> TypeAffinity {
172        match self {
173            Self::Null | Self::Blob(_) => TypeAffinity::Blob,
174            Self::Integer(_) => TypeAffinity::Integer,
175            Self::Float(_) => TypeAffinity::Real,
176            Self::Text(_) => TypeAffinity::Text,
177        }
178    }
179
180    /// Returns the storage class of this value.
181    pub const fn storage_class(&self) -> StorageClass {
182        match self {
183            Self::Null => StorageClass::Null,
184            Self::Integer(_) => StorageClass::Integer,
185            Self::Float(_) => StorageClass::Real,
186            Self::Text(_) => StorageClass::Text,
187            Self::Blob(_) => StorageClass::Blob,
188        }
189    }
190
191    /// Apply column type affinity coercion (advisory mode).
192    ///
193    /// In non-STRICT tables, affinity is advisory: values are coerced when
194    /// possible but never rejected. Follows SQLite §3.4 rules from
195    /// <https://www.sqlite.org/datatype3.html#type_affinity_of_a_column>.
196    ///
197    /// - TEXT affinity: numeric values converted to text before storing.
198    /// - NUMERIC affinity: text parsed as integer/real if well-formed.
199    /// - INTEGER affinity: like NUMERIC, plus exact-integer reals become integer.
200    /// - REAL affinity: like NUMERIC, plus integers forced to float.
201    /// - BLOB affinity: no conversion.
202    #[must_use]
203    #[allow(
204        clippy::cast_possible_truncation,
205        clippy::cast_precision_loss,
206        clippy::float_cmp
207    )]
208    pub fn apply_affinity(self, affinity: TypeAffinity) -> Self {
209        match affinity {
210            TypeAffinity::Blob => self,
211            TypeAffinity::Text => match self {
212                Self::Null | Self::Text(_) | Self::Blob(_) => self,
213                Self::Integer(_) | Self::Float(_) => {
214                    let t = self.to_text();
215                    Self::Text(Arc::from(t))
216                }
217            },
218            TypeAffinity::Numeric => match &self {
219                Self::Text(s) => try_coerce_text_to_numeric(s).unwrap_or(self),
220                _ => self,
221            },
222            TypeAffinity::Integer => match &self {
223                Self::Text(s) => try_coerce_text_to_numeric(s).unwrap_or(self),
224                Self::Float(f) => {
225                    if *f >= -9_223_372_036_854_775_808.0 && *f < 9_223_372_036_854_775_808.0 {
226                        let i = *f as i64;
227                        if (i as f64) == *f {
228                            return Self::Integer(i);
229                        }
230                    }
231                    self
232                }
233                _ => self,
234            },
235            TypeAffinity::Real => match &self {
236                Self::Text(s) => try_coerce_text_to_numeric(s)
237                    .map(|v| match v {
238                        Self::Integer(i) => Self::Float(i as f64),
239                        other => other,
240                    })
241                    .unwrap_or(self),
242                Self::Integer(i) => Self::Float(*i as f64),
243                _ => self,
244            },
245        }
246    }
247
248    /// Validate a value against a STRICT table column type.
249    ///
250    /// NULL is always accepted (nullability is enforced separately via NOT NULL).
251    /// Returns `Ok(value)` with possible implicit coercion (REAL columns accept
252    /// integers, converting them to float), or `Err` if the storage class is
253    /// incompatible.
254    #[allow(clippy::cast_precision_loss)]
255    pub fn validate_strict(self, col_type: StrictColumnType) -> Result<Self, StrictTypeError> {
256        if matches!(self, Self::Null) {
257            return Ok(self);
258        }
259        match col_type {
260            StrictColumnType::Any => Ok(self),
261            StrictColumnType::Integer => match self {
262                Self::Integer(_) => Ok(self),
263                other => Err(StrictTypeError {
264                    expected: col_type,
265                    actual: other.storage_class(),
266                }),
267            },
268            StrictColumnType::Real => match self {
269                Self::Float(_) => Ok(self),
270                Self::Integer(i) => Ok(Self::Float(i as f64)),
271                other => Err(StrictTypeError {
272                    expected: col_type,
273                    actual: other.storage_class(),
274                }),
275            },
276            StrictColumnType::Text => match self {
277                Self::Text(_) => Ok(self),
278                other => Err(StrictTypeError {
279                    expected: col_type,
280                    actual: other.storage_class(),
281                }),
282            },
283            StrictColumnType::Blob => match self {
284                Self::Blob(_) => Ok(self),
285                other => Err(StrictTypeError {
286                    expected: col_type,
287                    actual: other.storage_class(),
288                }),
289            },
290        }
291    }
292
293    /// Returns true if this is a NULL value.
294    #[inline(always)]
295    #[allow(clippy::inline_always)]
296    pub const fn is_null(&self) -> bool {
297        matches!(self, Self::Null)
298    }
299
300    /// Try to extract an integer value.
301    #[inline]
302    pub const fn as_integer(&self) -> Option<i64> {
303        match self {
304            Self::Integer(i) => Some(*i),
305            _ => None,
306        }
307    }
308
309    /// Try to extract a float value.
310    #[inline]
311    pub fn as_float(&self) -> Option<f64> {
312        match self {
313            Self::Float(f) => Some(*f),
314            _ => None,
315        }
316    }
317
318    /// Try to extract a text reference.
319    #[inline]
320    pub fn as_text(&self) -> Option<&str> {
321        match self {
322            Self::Text(s) => Some(s),
323            _ => None,
324        }
325    }
326
327    /// Try to extract a blob reference.
328    #[inline]
329    pub fn as_blob(&self) -> Option<&[u8]> {
330        match self {
331            Self::Blob(b) => Some(b),
332            _ => None,
333        }
334    }
335
336    /// Convert to an integer following SQLite's type coercion rules.
337    ///
338    /// - NULL -> 0
339    /// - Integer -> itself
340    /// - Float -> truncated to i64
341    /// - Text -> attempt to parse, 0 on failure
342    /// - Blob -> 0
343    #[inline(always)]
344    #[allow(clippy::inline_always)]
345    #[allow(clippy::cast_possible_truncation)]
346    pub fn to_integer(&self) -> i64 {
347        match self {
348            Self::Null => 0,
349            Self::Integer(i) => *i,
350            Self::Float(f) => *f as i64,
351            Self::Text(s) => parse_integer_prefix(s),
352            Self::Blob(b) => parse_integer_prefix_bytes(b),
353        }
354    }
355
356    /// Convert to a float following SQLite's type coercion rules.
357    ///
358    /// - NULL -> 0.0
359    /// - Integer -> as f64
360    /// - Float -> itself
361    /// - Text -> attempt to parse, 0.0 on failure
362    /// - Blob -> 0.0
363    #[inline(always)]
364    #[allow(clippy::inline_always)]
365    #[allow(clippy::cast_precision_loss)]
366    pub fn to_float(&self) -> f64 {
367        match self {
368            Self::Null => 0.0,
369            Self::Integer(i) => *i as f64,
370            Self::Float(f) => *f,
371            Self::Text(s) => parse_float_prefix(s),
372            Self::Blob(b) => parse_float_prefix_bytes(b),
373        }
374    }
375
376    /// Borrow the inner text string without allocating.
377    ///
378    /// Returns `Some(&str)` for `Text` values, `None` otherwise.
379    /// Use this in comparisons, LIKE patterns, and WHERE clause
380    /// evaluation to avoid the clone that `to_text()` incurs.
381    #[inline]
382    #[must_use]
383    pub fn as_text_str(&self) -> Option<&str> {
384        match self {
385            Self::Text(s) => Some(s),
386            _ => None,
387        }
388    }
389
390    /// Borrow the inner blob bytes without allocating.
391    #[inline]
392    #[must_use]
393    pub fn as_blob_bytes(&self) -> Option<&[u8]> {
394        match self {
395            Self::Blob(b) => Some(b),
396            _ => None,
397        }
398    }
399
400    /// Convert to text following SQLite's CAST(x AS TEXT) coercion rules.
401    ///
402    /// For blobs, this interprets the raw bytes as UTF-8 (with lossy
403    /// replacement for invalid sequences), matching C SQLite behavior.
404    /// For the SQL-literal hex format (`X'...'`), use the `Display` impl.
405    pub fn to_text(&self) -> String {
406        match self {
407            Self::Null => String::new(),
408            Self::Integer(i) => i.to_string(),
409            Self::Float(f) => format_sqlite_float(*f),
410            Self::Text(s) => s.to_string(),
411            Self::Blob(b) => String::from_utf8_lossy(b).into_owned(),
412        }
413    }
414
415    /// Convert to NUMERIC using SQLite CAST semantics rather than affinity.
416    ///
417    /// Unlike NUMERIC affinity, CAST always produces a numeric storage class for
418    /// text/blob input, using the longest leading numeric prefix or `0` when no
419    /// numeric prefix exists.
420    #[must_use]
421    pub fn cast_to_numeric(&self) -> Self {
422        match self {
423            Self::Null => Self::Null,
424            Self::Integer(i) => Self::Integer(*i),
425            Self::Float(f) => Self::Float(*f),
426            Self::Text(s) => cast_text_prefix_to_numeric(s),
427            Self::Blob(b) => cast_text_prefix_to_numeric(&String::from_utf8_lossy(b)),
428        }
429    }
430
431    /// Returns the SQLite `typeof()` string for this value.
432    ///
433    /// Matches C sqlite3: "null", "integer", "real", "text", or "blob".
434    pub const fn typeof_str(&self) -> &'static str {
435        match self {
436            Self::Null => "null",
437            Self::Integer(_) => "integer",
438            Self::Float(_) => "real",
439            Self::Text(_) => "text",
440            Self::Blob(_) => "blob",
441        }
442    }
443
444    /// Returns the SQLite `length()` result for this value.
445    ///
446    /// - NULL → NULL (represented as None)
447    /// - TEXT → character count
448    /// - BLOB → byte count
449    /// - INTEGER/REAL → character count of text representation
450    pub fn sql_length(&self) -> Option<i64> {
451        match self {
452            Self::Null => None,
453            Self::Text(s) => Some(i64::try_from(s.chars().count()).unwrap_or(i64::MAX)),
454            Self::Blob(b) => Some(i64::try_from(b.len()).unwrap_or(i64::MAX)),
455            Self::Integer(_) | Self::Float(_) => {
456                let t = self.to_text();
457                Some(i64::try_from(t.chars().count()).unwrap_or(i64::MAX))
458            }
459        }
460    }
461
462    /// Check equality for UNIQUE constraint purposes.
463    ///
464    /// In SQLite, NULL != NULL for uniqueness: if either value is NULL, the
465    /// result is `false` (they are never considered duplicates). Non-NULL values
466    /// compare by storage class ordering (same as `PartialEq`).
467    pub fn unique_eq(&self, other: &Self) -> bool {
468        if self.is_null() || other.is_null() {
469            return false;
470        }
471        matches!(self.partial_cmp(other), Some(Ordering::Equal))
472    }
473
474    /// Convert a floating-point arithmetic result into a SQLite value.
475    ///
476    /// SQLite does not surface NaN; NaN is normalized to NULL while ±Inf remain REAL.
477    fn float_result_or_null(result: f64) -> Self {
478        if result.is_nan() {
479            Self::Null
480        } else {
481            Self::Float(result)
482        }
483    }
484
485    /// Mirrors C SQLite's `numericType()` (SQLite VDBE:496): returns true if this
486    /// value should be treated as an integer for arithmetic purposes.
487    ///
488    /// Integer values are obviously integer-typed. Text/Blob values that parse
489    /// as i64 are also integer-typed. Float and Null are not.
490    #[inline]
491    pub fn is_integer_numeric_type(&self) -> bool {
492        fn text_is_integer_numeric_type(s: &str) -> bool {
493            let trimmed = s.trim_start();
494            let end = scan_numeric_prefix(trimmed.as_bytes());
495            end > 0
496                && !trimmed.as_bytes()[..end]
497                    .iter()
498                    .any(|byte| matches!(*byte, b'.' | b'e' | b'E'))
499        }
500
501        match self {
502            Self::Integer(_) => true,
503            Self::Float(_) | Self::Null => false,
504            Self::Text(s) => text_is_integer_numeric_type(s),
505            Self::Blob(b) => text_is_integer_numeric_type(&String::from_utf8_lossy(b)),
506        }
507    }
508
509    /// Returns true if this value should be treated as a float for arithmetic.
510    /// A value is "float numeric type" only if it has a numeric prefix
511    /// containing '.', 'e', or 'E'. Non-numeric text/blob is NOT float
512    /// (it coerces to integer 0 in C SQLite's OP_Add/Sub/Mul).
513    #[inline]
514    fn is_float_numeric_type(&self) -> bool {
515        fn text_is_float(s: &str) -> bool {
516            let trimmed = s.trim_start();
517            let end = scan_numeric_prefix(trimmed.as_bytes());
518            end > 0
519                && trimmed.as_bytes()[..end]
520                    .iter()
521                    .any(|byte| matches!(*byte, b'.' | b'e' | b'E'))
522        }
523        match self {
524            Self::Float(_) => true,
525            Self::Integer(_) | Self::Null => false,
526            Self::Text(s) => text_is_float(s),
527            Self::Blob(b) => text_is_float(&String::from_utf8_lossy(b)),
528        }
529    }
530
531    /// Add two values following SQLite's overflow semantics.
532    ///
533    /// - Integer + Integer: checked add; overflows promote to REAL.
534    /// - Any REAL operand: float addition.
535    /// - NULL propagates (NULL + x = NULL).
536    /// - Text/Blob coerced via `numericType()`: if both parse as integer,
537    ///   integer math is used (SQLite VDBE:1932-1934).
538    #[inline(always)]
539    #[allow(clippy::inline_always)]
540    #[must_use]
541    #[allow(clippy::cast_precision_loss)]
542    pub fn sql_add(&self, other: &Self) -> Self {
543        match (self, other) {
544            (Self::Null, _) | (_, Self::Null) => Self::Null,
545            (Self::Integer(a), Self::Integer(b)) => match a.checked_add(*b) {
546                Some(result) => Self::Integer(result),
547                None => Self::float_result_or_null(*a as f64 + *b as f64),
548            },
549            // If neither operand is a float-type (i.e. both are integer,
550            // integer-text, or non-numeric text/blob), use integer arithmetic.
551            // Non-numeric text like "hello" coerces to integer 0, not float 0.0.
552            _ if !self.is_float_numeric_type() && !other.is_float_numeric_type() => {
553                let a = self.to_integer();
554                let b = other.to_integer();
555                match a.checked_add(b) {
556                    Some(result) => Self::Integer(result),
557                    None => Self::float_result_or_null(a as f64 + b as f64),
558                }
559            }
560            _ => Self::float_result_or_null(self.to_float() + other.to_float()),
561        }
562    }
563
564    /// Subtract two values following SQLite's overflow semantics.
565    ///
566    /// Integer - Integer with overflow promotes to REAL.
567    #[inline(always)]
568    #[allow(clippy::inline_always)]
569    #[must_use]
570    #[allow(clippy::cast_precision_loss)]
571    pub fn sql_sub(&self, other: &Self) -> Self {
572        match (self, other) {
573            (Self::Null, _) | (_, Self::Null) => Self::Null,
574            (Self::Integer(a), Self::Integer(b)) => match a.checked_sub(*b) {
575                Some(result) => Self::Integer(result),
576                None => Self::float_result_or_null(*a as f64 - *b as f64),
577            },
578            _ if !self.is_float_numeric_type() && !other.is_float_numeric_type() => {
579                let a = self.to_integer();
580                let b = other.to_integer();
581                match a.checked_sub(b) {
582                    Some(result) => Self::Integer(result),
583                    None => Self::float_result_or_null(a as f64 - b as f64),
584                }
585            }
586            _ => Self::float_result_or_null(self.to_float() - other.to_float()),
587        }
588    }
589
590    /// Multiply two values following SQLite's overflow semantics.
591    ///
592    /// Integer * Integer with overflow promotes to REAL.
593    #[inline(always)]
594    #[allow(clippy::inline_always)]
595    #[must_use]
596    #[allow(clippy::cast_precision_loss)]
597    pub fn sql_mul(&self, other: &Self) -> Self {
598        match (self, other) {
599            (Self::Null, _) | (_, Self::Null) => Self::Null,
600            (Self::Integer(a), Self::Integer(b)) => match a.checked_mul(*b) {
601                Some(result) => Self::Integer(result),
602                None => Self::float_result_or_null(*a as f64 * *b as f64),
603            },
604            _ if !self.is_float_numeric_type() && !other.is_float_numeric_type() => {
605                let a = self.to_integer();
606                let b = other.to_integer();
607                match a.checked_mul(b) {
608                    Some(result) => Self::Integer(result),
609                    None => Self::float_result_or_null(a as f64 * b as f64),
610                }
611            }
612            _ => Self::float_result_or_null(self.to_float() * other.to_float()),
613        }
614    }
615
616    /// The sort order key for NULL values (SQLite sorts NULLs first).
617    const fn sort_class(&self) -> u8 {
618        match self {
619            Self::Null => 0,
620            Self::Integer(_) | Self::Float(_) => 1,
621            Self::Text(_) => 2,
622            Self::Blob(_) => 3,
623        }
624    }
625}
626
627/// Check if two composite UNIQUE keys are duplicates (SQLite NULL semantics).
628///
629/// Returns `true` only if ALL corresponding components are non-NULL and equal.
630/// If ANY component in either key is NULL, the keys are NOT duplicates (per
631/// SQLite's NULL != NULL rule for UNIQUE constraints).
632///
633/// Both slices must have the same length (panics otherwise).
634pub fn unique_key_duplicates(a: &[SqliteValue], b: &[SqliteValue]) -> bool {
635    assert_eq!(a.len(), b.len(), "UNIQUE key columns must match");
636    a.iter().zip(b.iter()).all(|(va, vb)| va.unique_eq(vb))
637}
638
639/// Match a string against a SQL LIKE pattern with SQLite semantics.
640///
641/// - `%` matches zero or more characters.
642/// - `_` matches exactly one character.
643/// - Case-insensitive for ASCII A-Z only (no Unicode case folding without ICU).
644/// - `escape` optionally specifies the escape character for literal `%`/`_`.
645pub fn sql_like(pattern: &str, text: &str, escape: Option<char>) -> bool {
646    sql_like_inner(
647        &pattern.chars().collect::<Vec<_>>(),
648        &text.chars().collect::<Vec<_>>(),
649        escape,
650        0,
651        0,
652    )
653}
654
655fn sql_like_inner(
656    pattern: &[char],
657    text: &[char],
658    escape: Option<char>,
659    pi: usize,
660    ti: usize,
661) -> bool {
662    let mut pi = pi;
663    let mut ti = ti;
664
665    while pi < pattern.len() {
666        let pc = pattern[pi];
667
668        // Handle escape character.
669        if Some(pc) == escape {
670            pi += 1;
671            if pi >= pattern.len() {
672                return false; // Trailing escape is malformed.
673            }
674            // Match the escaped character literally.
675            if ti >= text.len() || !ascii_ci_eq(pattern[pi], text[ti]) {
676                return false;
677            }
678            pi += 1;
679            ti += 1;
680            continue;
681        }
682
683        match pc {
684            '%' => {
685                // Skip consecutive % wildcards.
686                while pi < pattern.len() && pattern[pi] == '%' {
687                    pi += 1;
688                }
689                // If % is at end of pattern, matches everything.
690                if pi >= pattern.len() {
691                    return true;
692                }
693                // Try matching rest of pattern at each position.
694                for start in ti..=text.len() {
695                    if sql_like_inner(pattern, text, escape, pi, start) {
696                        return true;
697                    }
698                }
699                return false;
700            }
701            '_' => {
702                if ti >= text.len() {
703                    return false;
704                }
705                pi += 1;
706                ti += 1;
707            }
708            _ => {
709                if ti >= text.len() || !ascii_ci_eq(pc, text[ti]) {
710                    return false;
711                }
712                pi += 1;
713                ti += 1;
714            }
715        }
716    }
717    ti >= text.len()
718}
719
720/// ASCII-only case-insensitive character comparison (SQLite LIKE semantics).
721fn ascii_ci_eq(a: char, b: char) -> bool {
722    if a == b {
723        return true;
724    }
725    // Only fold ASCII A-Z / a-z.
726    a.is_ascii() && b.is_ascii() && a.eq_ignore_ascii_case(&b)
727}
728
729/// Accumulator for SQL `sum()` aggregate with SQLite overflow semantics.
730///
731/// Unlike expression arithmetic (which promotes to REAL on overflow), `sum()`
732/// raises an error on integer overflow. This matches C sqlite3 behavior.
733#[derive(Debug, Clone)]
734pub struct SumAccumulator {
735    /// Running integer sum (if still in integer mode).
736    int_sum: i64,
737    /// Running float sum (if promoted to float mode).
738    float_sum: f64,
739    /// KBN compensation error term.
740    float_err: f64,
741    /// Whether we've seen any non-NULL value.
742    has_value: bool,
743    /// Whether we're in float mode (any REAL input or integer overflow).
744    is_float: bool,
745    /// Whether an integer overflow occurred (error condition).
746    overflow: bool,
747}
748
749impl Default for SumAccumulator {
750    fn default() -> Self {
751        Self::new()
752    }
753}
754
755/// Kahan-Babuska-Neumaier compensated summation step (matches C SQLite func.c:1871).
756#[inline]
757fn kbn_step(sum: &mut f64, err: &mut f64, value: f64) {
758    let s = *sum;
759    let t = s + value;
760    if s.abs() > value.abs() {
761        *err += (s - t) + value;
762    } else {
763        *err += (value - t) + s;
764    }
765    *sum = t;
766}
767
768impl SumAccumulator {
769    /// Create a new accumulator.
770    pub const fn new() -> Self {
771        Self {
772            int_sum: 0,
773            float_sum: 0.0,
774            float_err: 0.0,
775            has_value: false,
776            is_float: false,
777            overflow: false,
778        }
779    }
780
781    /// Add a value to the running sum.
782    #[allow(clippy::cast_precision_loss)]
783    pub fn accumulate(&mut self, val: &SqliteValue) {
784        match val {
785            SqliteValue::Null => {}
786            SqliteValue::Integer(i) => {
787                self.has_value = true;
788                if self.is_float {
789                    kbn_step(&mut self.float_sum, &mut self.float_err, *i as f64);
790                } else {
791                    match self.int_sum.checked_add(*i) {
792                        Some(result) => self.int_sum = result,
793                        None => self.overflow = true,
794                    }
795                }
796            }
797            SqliteValue::Float(f) => {
798                self.has_value = true;
799                if !self.is_float {
800                    self.float_sum = self.int_sum as f64;
801                    self.float_err = 0.0;
802                    self.is_float = true;
803                }
804                kbn_step(&mut self.float_sum, &mut self.float_err, *f);
805            }
806            other => {
807                // TEXT/BLOB coerced to numeric.
808                self.has_value = true;
809                let n = other.to_float();
810                if !self.is_float {
811                    self.float_sum = self.int_sum as f64;
812                    self.float_err = 0.0;
813                    self.is_float = true;
814                }
815                kbn_step(&mut self.float_sum, &mut self.float_err, n);
816            }
817        }
818    }
819
820    /// Finalize the sum. Returns `Err` if integer overflow occurred,
821    /// `Ok(NULL)` if no non-NULL values were seen, or the sum value.
822    pub fn finish(&self) -> Result<SqliteValue, SumOverflowError> {
823        if self.overflow {
824            return Err(SumOverflowError);
825        }
826        if !self.has_value {
827            return Ok(SqliteValue::Null);
828        }
829        if self.is_float {
830            Ok(SqliteValue::Float(self.float_sum + self.float_err))
831        } else {
832            Ok(SqliteValue::Integer(self.int_sum))
833        }
834    }
835}
836
837/// Error returned when `sum()` encounters integer overflow.
838#[derive(Debug, Clone, PartialEq, Eq)]
839pub struct SumOverflowError;
840
841impl fmt::Display for SumOverflowError {
842    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
843        f.write_str("integer overflow in sum()")
844    }
845}
846
847impl fmt::Display for SqliteValue {
848    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
849        match self {
850            Self::Null => f.write_str("NULL"),
851            Self::Integer(i) => write!(f, "{i}"),
852            Self::Float(v) => f.write_str(&format_sqlite_float(*v)),
853            Self::Text(s) => write!(f, "'{s}'"),
854            Self::Blob(b) => {
855                f.write_str("X'")?;
856                for byte in b.iter() {
857                    write!(f, "{byte:02X}")?;
858                }
859                f.write_str("'")
860            }
861        }
862    }
863}
864
865impl PartialEq for SqliteValue {
866    fn eq(&self, other: &Self) -> bool {
867        matches!(self.partial_cmp(other), Some(Ordering::Equal))
868    }
869}
870
871impl Eq for SqliteValue {}
872
873impl PartialOrd for SqliteValue {
874    #[inline]
875    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
876        Some(self.cmp(other))
877    }
878}
879
880impl Ord for SqliteValue {
881    #[inline]
882    fn cmp(&self, other: &Self) -> Ordering {
883        // SQLite sort order: NULL < numeric < text < blob
884        let class_a = self.sort_class();
885        let class_b = other.sort_class();
886
887        if class_a != class_b {
888            return class_a.cmp(&class_b);
889        }
890
891        match (self, other) {
892            (Self::Null, Self::Null) => Ordering::Equal,
893            (Self::Integer(a), Self::Integer(b)) => a.cmp(b),
894            (Self::Float(a), Self::Float(b)) => a.partial_cmp(b).unwrap_or_else(|| a.total_cmp(b)),
895            (Self::Integer(a), Self::Float(b)) => int_float_cmp(*a, *b),
896            (Self::Float(a), Self::Integer(b)) => int_float_cmp(*b, *a).reverse(),
897            (Self::Text(a), Self::Text(b)) => a.cmp(b),
898            (Self::Blob(a), Self::Blob(b)) => a.cmp(b),
899            _ => unreachable!(),
900        }
901    }
902}
903
904impl From<i64> for SqliteValue {
905    fn from(i: i64) -> Self {
906        Self::Integer(i)
907    }
908}
909
910impl From<i32> for SqliteValue {
911    fn from(i: i32) -> Self {
912        Self::Integer(i64::from(i))
913    }
914}
915
916impl From<f64> for SqliteValue {
917    fn from(f: f64) -> Self {
918        Self::float_result_or_null(f)
919    }
920}
921
922impl From<String> for SqliteValue {
923    fn from(s: String) -> Self {
924        // Arc::from(String) reuses the String's heap buffer via
925        // String → Box<str> → Arc<str>, avoiding a redundant copy.
926        Self::Text(Arc::from(s))
927    }
928}
929
930impl From<&str> for SqliteValue {
931    fn from(s: &str) -> Self {
932        Self::Text(Arc::from(s))
933    }
934}
935
936impl From<Arc<str>> for SqliteValue {
937    fn from(s: Arc<str>) -> Self {
938        Self::Text(s)
939    }
940}
941
942impl From<Vec<u8>> for SqliteValue {
943    fn from(b: Vec<u8>) -> Self {
944        // Arc::from(Vec<u8>) reuses the Vec's heap buffer via
945        // Vec → Box<[u8]> → Arc<[u8]>, avoiding a redundant copy.
946        Self::Blob(Arc::from(b))
947    }
948}
949
950impl From<&[u8]> for SqliteValue {
951    fn from(b: &[u8]) -> Self {
952        Self::Blob(Arc::from(b))
953    }
954}
955
956impl From<Arc<[u8]>> for SqliteValue {
957    fn from(b: Arc<[u8]>) -> Self {
958        Self::Blob(b)
959    }
960}
961
962impl<T: Into<Self>> From<Option<T>> for SqliteValue {
963    fn from(opt: Option<T>) -> Self {
964        match opt {
965            Some(v) => v.into(),
966            None => Self::Null,
967        }
968    }
969}
970
971/// Try to coerce a text string to INTEGER or REAL following SQLite NUMERIC
972/// affinity rules. Returns `None` if the text is not a well-formed numeric
973/// literal.
974#[allow(
975    clippy::cast_possible_truncation,
976    clippy::cast_precision_loss,
977    clippy::float_cmp
978)]
979fn try_coerce_text_to_numeric(s: &str) -> Option<SqliteValue> {
980    let trimmed = s.trim();
981    if trimmed.is_empty() {
982        return None;
983    }
984    // Try integer first (preferred for NUMERIC affinity).
985    if let Ok(i) = trimmed.parse::<i64>() {
986        return Some(SqliteValue::Integer(i));
987    }
988    // Try float. Reject non-finite results (NaN, Infinity) since SQLite
989    // does not recognise "nan", "inf", or "infinity" as numeric literals.
990    // However, it does recognize literals like "1e999" which evaluate to Inf.
991    if let Ok(f) = trimmed.parse::<f64>() {
992        if !f.is_finite() {
993            let lower = trimmed.to_ascii_lowercase();
994            if lower.contains("inf") || lower.contains("nan") {
995                return None;
996            }
997        }
998        // If the float is an exact integer value within bounds, store as integer.
999        // Checking bounds prevents incorrect saturation for values >= 2^63.
1000        if (-9_223_372_036_854_775_808.0..9_223_372_036_854_775_808.0).contains(&f) {
1001            #[allow(clippy::cast_possible_truncation)]
1002            let i = f as i64;
1003            #[allow(clippy::cast_precision_loss)]
1004            if (i as f64) == f {
1005                return Some(SqliteValue::Integer(i));
1006            }
1007        }
1008        return Some(SqliteValue::Float(f));
1009    }
1010    None
1011}
1012
1013/// Compare an integer with a float, preserving precision for large i64 values.
1014///
1015/// Matches C SQLite's `sqlite3IntFloatCompare` algorithm. The naive
1016/// `(i as f64).partial_cmp(&r)` loses precision for |i| > 2^53.
1017#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
1018pub fn int_float_cmp(i: i64, r: f64) -> Ordering {
1019    if r.is_nan() {
1020        // SQLite treats NaN as NULL, and all integers are greater than NULL.
1021        return Ordering::Greater;
1022    }
1023    // If r is out of i64 range, the answer is obvious.
1024    if r < -9_223_372_036_854_775_808.0 {
1025        return Ordering::Greater;
1026    }
1027    if r >= 9_223_372_036_854_775_808.0 {
1028        return Ordering::Less;
1029    }
1030    // Truncate float to integer and compare integer parts.
1031    let y = r as i64;
1032    match i.cmp(&y) {
1033        Ordering::Less => Ordering::Less,
1034        Ordering::Greater => Ordering::Greater,
1035        // Integer parts equal — use float comparison as tiebreaker.
1036        Ordering::Equal => {
1037            let s = i as f64;
1038            s.partial_cmp(&r).unwrap_or(Ordering::Equal)
1039        }
1040    }
1041}
1042
1043/// Format a floating-point value as text matching SQLite's `%!.15g` behavior.
1044///
1045/// SQLite uses `printf("%!.15g", value)` to convert REAL to TEXT. The `!` flag
1046/// ensures the result always contains a decimal point, distinguishing REAL from
1047/// INTEGER in text output (e.g., `120.0` not `120`).
1048#[must_use]
1049pub fn format_sqlite_float(f: f64) -> String {
1050    if f.is_nan() {
1051        return "NaN".to_owned();
1052    }
1053    if f.is_infinite() {
1054        return if f.is_sign_positive() {
1055            "Inf".to_owned()
1056        } else {
1057            "-Inf".to_owned()
1058        };
1059    }
1060    // Emulate C's `printf("%!.15g", f)`:
1061    // - 15 significant digits
1062    // - Use scientific notation if exponent < -4 or >= 15
1063    // - Strip trailing zeros (but keep at least one digit after decimal point)
1064    let abs = f.abs();
1065    let s = if abs == 0.0 {
1066        // Zero: preserve sign for -0.0 (C SQLite: printf("%!.15g", -0.0) → "-0.0")
1067        if f.is_sign_negative() {
1068            "-0.0".to_owned()
1069        } else {
1070            "0.0".to_owned()
1071        }
1072    } else {
1073        // Determine which format is shorter: fixed vs scientific.
1074        // C's %g uses scientific if exponent < -4 or >= precision.
1075        let exp = abs.log10().floor() as i32;
1076        if exp >= 15 || exp < -4 {
1077            // Scientific notation with 14 decimal places (15 sig digits total).
1078            let mut s = format!("{f:.14e}");
1079            // Strip trailing zeros in the mantissa before 'e'.
1080            if let Some(e_pos) = s.find('e') {
1081                let mantissa = &s[..e_pos];
1082                let exp_str = &s[e_pos + 1..]; // after 'e'
1083                let trimmed = mantissa.trim_end_matches('0');
1084                // Ensure decimal point is kept (the `!` flag).
1085                let trimmed = if trimmed.ends_with('.') {
1086                    format!("{trimmed}0")
1087                } else {
1088                    trimmed.to_owned()
1089                };
1090                // Normalize exponent to match C printf: explicit +/- sign
1091                // and at least 2 digits (e.g. "e-05" not "e-5", "e+15" not "e15").
1092                let (exp_sign, exp_digits) = if let Some(rest) = exp_str.strip_prefix('-') {
1093                    ("-", rest)
1094                } else if let Some(rest) = exp_str.strip_prefix('+') {
1095                    ("+", rest)
1096                } else {
1097                    ("+", exp_str)
1098                };
1099                let exp_num: u32 = exp_digits.parse().unwrap_or(0);
1100                s = format!("{trimmed}e{exp_sign}{exp_num:02}");
1101            }
1102            s
1103        } else {
1104            // Fixed notation: number of decimal places = 15 - (exp + 1).
1105            #[allow(clippy::cast_sign_loss)]
1106            let decimal_places = (14 - exp).max(0) as usize;
1107            let mut s = format!("{f:.decimal_places$}");
1108            // Strip trailing zeros but keep at least one digit after decimal.
1109            if s.contains('.') {
1110                let trimmed = s.trim_end_matches('0');
1111                s = if trimmed.ends_with('.') {
1112                    format!("{trimmed}0")
1113                } else {
1114                    trimmed.to_owned()
1115                };
1116            } else {
1117                s.push_str(".0");
1118            }
1119            s
1120        }
1121    };
1122    s
1123}
1124
1125#[cfg(test)]
1126#[allow(clippy::float_cmp, clippy::approx_constant)]
1127mod tests {
1128    use super::*;
1129
1130    #[test]
1131    fn null_properties() {
1132        let v = SqliteValue::Null;
1133        assert!(v.is_null());
1134        assert_eq!(v.to_integer(), 0);
1135        assert_eq!(v.to_float(), 0.0);
1136        assert_eq!(v.to_text(), "");
1137        assert_eq!(v.to_string(), "NULL");
1138    }
1139
1140    #[test]
1141    fn integer_properties() {
1142        let v = SqliteValue::Integer(42);
1143        assert!(!v.is_null());
1144        assert_eq!(v.as_integer(), Some(42));
1145        assert_eq!(v.to_integer(), 42);
1146        assert_eq!(v.to_float(), 42.0);
1147        assert_eq!(v.to_text(), "42");
1148    }
1149
1150    #[test]
1151    fn float_properties() {
1152        let v = SqliteValue::Float(3.14);
1153        assert_eq!(v.as_float(), Some(3.14));
1154        assert_eq!(v.to_integer(), 3);
1155        assert_eq!(v.to_text(), "3.14");
1156    }
1157
1158    #[test]
1159    fn text_properties() {
1160        let v = SqliteValue::Text(Arc::from("hello"));
1161        assert_eq!(v.as_text(), Some("hello"));
1162        assert_eq!(v.to_integer(), 0);
1163        assert_eq!(v.to_float(), 0.0);
1164    }
1165
1166    #[test]
1167    fn text_numeric_coercion() {
1168        let v = SqliteValue::Text(Arc::from("123"));
1169        assert_eq!(v.to_integer(), 123);
1170        assert_eq!(v.to_float(), 123.0);
1171
1172        let v = SqliteValue::Text(Arc::from("3.14"));
1173        assert_eq!(v.to_integer(), 3);
1174        assert_eq!(v.to_float(), 3.14);
1175    }
1176
1177    #[test]
1178    fn text_numeric_coercion_ignores_hex_text_prefixes() {
1179        let v = SqliteValue::Text(Arc::from("0x10"));
1180        assert_eq!(v.to_integer(), 0);
1181        assert_eq!(v.to_float(), 0.0);
1182
1183        let v = SqliteValue::Blob(Arc::from(b"0x10".as_slice()));
1184        assert_eq!(v.to_integer(), 0);
1185        assert_eq!(v.to_float(), 0.0);
1186    }
1187
1188    #[test]
1189    fn test_integer_numeric_type_uses_sqlite_prefix_rules() {
1190        assert!(SqliteValue::Text(Arc::from("123abc")).is_integer_numeric_type());
1191        assert!(SqliteValue::Blob(Arc::from(b"123a".as_slice())).is_integer_numeric_type());
1192        assert!(!SqliteValue::Text(Arc::from("1.5e2abc")).is_integer_numeric_type());
1193        assert!(!SqliteValue::Text(Arc::from("abc")).is_integer_numeric_type());
1194    }
1195
1196    #[test]
1197    fn test_sqlite_value_integer_real_comparison_equal() {
1198        let int_value = SqliteValue::Integer(3);
1199        let real_value = SqliteValue::Float(3.0);
1200        assert_eq!(int_value.partial_cmp(&real_value), Some(Ordering::Equal));
1201        assert_eq!(real_value.partial_cmp(&int_value), Some(Ordering::Equal));
1202    }
1203
1204    #[test]
1205    fn test_sqlite_value_text_to_integer_coercion() {
1206        let text_value = SqliteValue::Text(Arc::from("123"));
1207        let coerced = text_value.apply_affinity(TypeAffinity::Integer);
1208        assert_eq!(coerced, SqliteValue::Integer(123));
1209    }
1210
1211    #[test]
1212    fn blob_properties() {
1213        let v = SqliteValue::Blob(Arc::from([0xDE, 0xAD].as_slice()));
1214        assert_eq!(v.as_blob(), Some(&[0xDE, 0xAD][..]));
1215        assert_eq!(v.to_integer(), 0);
1216        assert_eq!(v.to_float(), 0.0);
1217        // to_text() interprets blob bytes as UTF-8 (matching CAST(blob AS TEXT)).
1218        // 0xDE 0xAD is valid UTF-8 encoding of U+07AD.
1219        assert_eq!(v.to_text(), "\u{07AD}");
1220    }
1221
1222    #[test]
1223    fn display_formatting() {
1224        assert_eq!(SqliteValue::Null.to_string(), "NULL");
1225        assert_eq!(SqliteValue::Integer(42).to_string(), "42");
1226        assert_eq!(SqliteValue::Integer(-1).to_string(), "-1");
1227        assert_eq!(SqliteValue::Float(1.5).to_string(), "1.5");
1228        assert_eq!(SqliteValue::Text(Arc::from("hi")).to_string(), "'hi'");
1229        assert_eq!(
1230            SqliteValue::Blob(Arc::from([0xCA, 0xFE].as_slice())).to_string(),
1231            "X'CAFE'"
1232        );
1233    }
1234
1235    #[test]
1236    fn sort_order_null_first() {
1237        let null = SqliteValue::Null;
1238        let int = SqliteValue::Integer(0);
1239        let text = SqliteValue::Text(Arc::from(""));
1240        let blob = SqliteValue::Blob(Arc::from(&[] as &[u8]));
1241
1242        assert!(null < int);
1243        assert!(int < text);
1244        assert!(text < blob);
1245    }
1246
1247    #[test]
1248    fn sort_order_integers() {
1249        let a = SqliteValue::Integer(1);
1250        let b = SqliteValue::Integer(2);
1251        assert!(a < b);
1252        assert_eq!(a.partial_cmp(&a), Some(Ordering::Equal));
1253    }
1254
1255    #[test]
1256    fn sort_order_mixed_numeric() {
1257        let int = SqliteValue::Integer(1);
1258        let float = SqliteValue::Float(1.5);
1259        assert!(int < float);
1260
1261        let int = SqliteValue::Integer(2);
1262        assert!(int > float);
1263    }
1264
1265    #[test]
1266    fn test_int_float_precision_at_i64_boundary() {
1267        // i64::MAX cast to f64 rounds UP to 9223372036854775808.0.
1268        // The naive (i as f64) comparison would say Equal, but C SQLite
1269        // correctly reports i64::MAX < 9223372036854775808.0.
1270        let imax = SqliteValue::Integer(i64::MAX);
1271        let fmax = SqliteValue::Float(9_223_372_036_854_775_808.0);
1272        assert_eq!(
1273            imax.partial_cmp(&fmax),
1274            Some(Ordering::Less),
1275            "i64::MAX must be Less than 9223372036854775808.0"
1276        );
1277
1278        // Two distinct large integers that map to the same f64.
1279        let a = SqliteValue::Integer(i64::MAX);
1280        let b = SqliteValue::Integer(i64::MAX - 1);
1281        let f = SqliteValue::Float(i64::MAX as f64);
1282        // a > b, but both should compare consistently vs the float.
1283        assert_eq!(a.partial_cmp(&b), Some(Ordering::Greater));
1284        // Both are less than the rounded-up float.
1285        assert_eq!(a.partial_cmp(&f), Some(Ordering::Less));
1286        assert_eq!(b.partial_cmp(&f), Some(Ordering::Less));
1287    }
1288
1289    #[test]
1290    fn test_int_float_precision_symmetric() {
1291        // Float-vs-Integer should be the reverse of Integer-vs-Float.
1292        let i = SqliteValue::Integer(i64::MAX);
1293        let f = SqliteValue::Float(9_223_372_036_854_775_808.0);
1294        assert_eq!(f.partial_cmp(&i), Some(Ordering::Greater));
1295    }
1296
1297    #[test]
1298    fn test_int_float_exact_representation() {
1299        // For exactly representable values, equality still works.
1300        let i = SqliteValue::Integer(42);
1301        let f = SqliteValue::Float(42.0);
1302        assert_eq!(i.partial_cmp(&f), Some(Ordering::Equal));
1303        assert_eq!(f.partial_cmp(&i), Some(Ordering::Equal));
1304
1305        // Integer 3 vs Float 3.5 — Integer is less.
1306        let i = SqliteValue::Integer(3);
1307        let f = SqliteValue::Float(3.5);
1308        assert_eq!(i.partial_cmp(&f), Some(Ordering::Less));
1309        assert_eq!(f.partial_cmp(&i), Some(Ordering::Greater));
1310    }
1311
1312    #[test]
1313    fn from_conversions() {
1314        assert_eq!(SqliteValue::from(42i64).as_integer(), Some(42));
1315        assert_eq!(SqliteValue::from(42i32).as_integer(), Some(42));
1316        assert_eq!(SqliteValue::from(1.5f64).as_float(), Some(1.5));
1317        assert_eq!(SqliteValue::from("hello").as_text(), Some("hello"));
1318        assert_eq!(
1319            SqliteValue::from(String::from("world")).as_text(),
1320            Some("world")
1321        );
1322        assert_eq!(SqliteValue::from(vec![1u8, 2]).as_blob(), Some(&[1, 2][..]));
1323        assert!(SqliteValue::from(None::<i64>).is_null());
1324        assert_eq!(SqliteValue::from(Some(42i64)).as_integer(), Some(42));
1325    }
1326
1327    #[test]
1328    fn affinity() {
1329        assert_eq!(SqliteValue::Null.affinity(), TypeAffinity::Blob);
1330        assert_eq!(SqliteValue::Integer(0).affinity(), TypeAffinity::Integer);
1331        assert_eq!(SqliteValue::Float(0.0).affinity(), TypeAffinity::Real);
1332        assert_eq!(
1333            SqliteValue::Text(Arc::from("")).affinity(),
1334            TypeAffinity::Text
1335        );
1336        assert_eq!(
1337            SqliteValue::Blob(Arc::from(&[] as &[u8])).affinity(),
1338            TypeAffinity::Blob
1339        );
1340    }
1341
1342    #[test]
1343    fn null_equality() {
1344        // In SQLite, NULL == NULL is false, but for sorting they are equal
1345        let a = SqliteValue::Null;
1346        let b = SqliteValue::Null;
1347        assert_eq!(a.partial_cmp(&b), Some(Ordering::Equal));
1348    }
1349
1350    // ── bd-13r.1: Type Affinity Advisory + STRICT Enforcement ──
1351
1352    #[test]
1353    fn test_storage_class_variants() {
1354        assert_eq!(SqliteValue::Null.storage_class(), StorageClass::Null);
1355        assert_eq!(
1356            SqliteValue::Integer(42).storage_class(),
1357            StorageClass::Integer
1358        );
1359        assert_eq!(SqliteValue::Float(3.14).storage_class(), StorageClass::Real);
1360        assert_eq!(
1361            SqliteValue::Text("hi".into()).storage_class(),
1362            StorageClass::Text
1363        );
1364        assert_eq!(
1365            SqliteValue::Blob(Arc::from([1u8].as_slice())).storage_class(),
1366            StorageClass::Blob
1367        );
1368    }
1369
1370    #[test]
1371    fn test_type_affinity_advisory_text_into_integer_ok() {
1372        // INSERT TEXT "hello" into INTEGER-affinity column: text stays as text
1373        // (not a well-formed numeric literal).
1374        let val = SqliteValue::Text("hello".into());
1375        let coerced = val.apply_affinity(TypeAffinity::Integer);
1376        assert!(coerced.as_text().is_some());
1377        assert_eq!(coerced.as_text().unwrap(), "hello");
1378
1379        // INSERT TEXT "42" into INTEGER-affinity column: coerced to integer.
1380        let val = SqliteValue::Text("42".into());
1381        let coerced = val.apply_affinity(TypeAffinity::Integer);
1382        assert_eq!(coerced.as_integer(), Some(42));
1383    }
1384
1385    #[test]
1386    fn test_type_affinity_advisory_integer_into_text_ok() {
1387        // INSERT INTEGER 42 into TEXT-affinity column: coerced to text "42".
1388        let val = SqliteValue::Integer(42);
1389        let coerced = val.apply_affinity(TypeAffinity::Text);
1390        assert_eq!(coerced.as_text(), Some("42"));
1391    }
1392
1393    #[test]
1394    fn test_type_affinity_comparison_coercion_matches_oracle() {
1395        // NUMERIC affinity coerces text "123" to integer.
1396        let val = SqliteValue::Text("123".into());
1397        let coerced = val.apply_affinity(TypeAffinity::Numeric);
1398        assert_eq!(coerced.as_integer(), Some(123));
1399
1400        // NUMERIC affinity coerces text "3.14" to real.
1401        let val = SqliteValue::Text("3.14".into());
1402        let coerced = val.apply_affinity(TypeAffinity::Numeric);
1403        assert_eq!(coerced.as_float(), Some(3.14));
1404
1405        // NUMERIC affinity leaves text "hello" as text.
1406        let val = SqliteValue::Text("hello".into());
1407        let coerced = val.apply_affinity(TypeAffinity::Numeric);
1408        assert!(coerced.as_text().is_some());
1409
1410        // BLOB affinity never converts anything.
1411        let val = SqliteValue::Integer(42);
1412        let coerced = val.apply_affinity(TypeAffinity::Blob);
1413        assert_eq!(coerced.as_integer(), Some(42));
1414
1415        // INTEGER affinity converts exact-integer floats to integer.
1416        let val = SqliteValue::Float(5.0);
1417        let coerced = val.apply_affinity(TypeAffinity::Integer);
1418        assert_eq!(coerced.as_integer(), Some(5));
1419
1420        // INTEGER affinity keeps non-exact floats as float.
1421        let val = SqliteValue::Float(5.5);
1422        let coerced = val.apply_affinity(TypeAffinity::Integer);
1423        assert_eq!(coerced.as_float(), Some(5.5));
1424
1425        // REAL affinity forces integers to float.
1426        let val = SqliteValue::Integer(7);
1427        let coerced = val.apply_affinity(TypeAffinity::Real);
1428        assert_eq!(coerced.as_float(), Some(7.0));
1429
1430        // REAL affinity coerces text "9" to float 9.0.
1431        let val = SqliteValue::Text("9".into());
1432        let coerced = val.apply_affinity(TypeAffinity::Real);
1433        assert_eq!(coerced.as_float(), Some(9.0));
1434    }
1435
1436    #[test]
1437    fn test_cast_to_numeric_uses_sqlite_cast_rules() {
1438        assert_eq!(
1439            SqliteValue::Text(Arc::from("123abc")).cast_to_numeric(),
1440            SqliteValue::Integer(123)
1441        );
1442        assert_eq!(
1443            SqliteValue::Text(Arc::from("1.5e2abc")).cast_to_numeric(),
1444            SqliteValue::Integer(150)
1445        );
1446        assert_eq!(
1447            SqliteValue::Text(Arc::from("abc")).cast_to_numeric(),
1448            SqliteValue::Integer(0)
1449        );
1450        assert_eq!(
1451            SqliteValue::Blob(Arc::from(b"123a".as_slice())).cast_to_numeric(),
1452            SqliteValue::Integer(123)
1453        );
1454
1455        match SqliteValue::Text(Arc::from("1e999")).cast_to_numeric() {
1456            SqliteValue::Float(value) => assert!(value.is_infinite() && value.is_sign_positive()),
1457            other => panic!("expected +inf REAL from NUMERIC cast, got {other:?}"),
1458        }
1459    }
1460
1461    #[test]
1462    fn test_strict_table_rejects_text_into_integer() {
1463        let val = SqliteValue::Text("hello".into());
1464        let result = val.validate_strict(StrictColumnType::Integer);
1465        assert!(result.is_err());
1466        let err = result.unwrap_err();
1467        assert_eq!(err.expected, StrictColumnType::Integer);
1468        assert_eq!(err.actual, StorageClass::Text);
1469    }
1470
1471    #[test]
1472    fn test_strict_table_allows_exact_type() {
1473        // INTEGER into INTEGER column: ok.
1474        let val = SqliteValue::Integer(42);
1475        assert!(val.validate_strict(StrictColumnType::Integer).is_ok());
1476
1477        // REAL into REAL column: ok.
1478        let val = SqliteValue::Float(3.14);
1479        assert!(val.validate_strict(StrictColumnType::Real).is_ok());
1480
1481        // TEXT into TEXT column: ok.
1482        let val = SqliteValue::Text("hello".into());
1483        assert!(val.validate_strict(StrictColumnType::Text).is_ok());
1484
1485        // BLOB into BLOB column: ok.
1486        let val = SqliteValue::Blob(Arc::from([1u8, 2, 3].as_slice()));
1487        assert!(val.validate_strict(StrictColumnType::Blob).is_ok());
1488
1489        // NULL into any STRICT column: ok (nullability enforced separately).
1490        assert!(
1491            SqliteValue::Null
1492                .validate_strict(StrictColumnType::Integer)
1493                .is_ok()
1494        );
1495        assert!(
1496            SqliteValue::Null
1497                .validate_strict(StrictColumnType::Text)
1498                .is_ok()
1499        );
1500
1501        // ANY accepts everything.
1502        let val = SqliteValue::Integer(42);
1503        assert!(val.validate_strict(StrictColumnType::Any).is_ok());
1504        let val = SqliteValue::Text("hi".into());
1505        assert!(val.validate_strict(StrictColumnType::Any).is_ok());
1506    }
1507
1508    #[test]
1509    fn test_strict_real_accepts_integer_with_coercion() {
1510        // STRICT REAL column accepts INTEGER and coerces to float.
1511        let val = SqliteValue::Integer(42);
1512        let result = val.validate_strict(StrictColumnType::Real).unwrap();
1513        assert_eq!(result.as_float(), Some(42.0));
1514    }
1515
1516    #[test]
1517    fn test_strict_rejects_wrong_storage_classes() {
1518        // REAL into INTEGER column: rejected.
1519        assert!(
1520            SqliteValue::Float(3.14)
1521                .validate_strict(StrictColumnType::Integer)
1522                .is_err()
1523        );
1524
1525        // BLOB into TEXT column: rejected.
1526        assert!(
1527            SqliteValue::Blob(Arc::from([1u8].as_slice()))
1528                .validate_strict(StrictColumnType::Text)
1529                .is_err()
1530        );
1531
1532        // INTEGER into TEXT column: rejected.
1533        assert!(
1534            SqliteValue::Integer(1)
1535                .validate_strict(StrictColumnType::Text)
1536                .is_err()
1537        );
1538
1539        // TEXT into BLOB column: rejected.
1540        assert!(
1541            SqliteValue::Text("x".into())
1542                .validate_strict(StrictColumnType::Blob)
1543                .is_err()
1544        );
1545    }
1546
1547    #[test]
1548    fn test_strict_column_type_parsing() {
1549        assert_eq!(
1550            StrictColumnType::from_type_name("INT"),
1551            Some(StrictColumnType::Integer)
1552        );
1553        assert_eq!(
1554            StrictColumnType::from_type_name("INTEGER"),
1555            Some(StrictColumnType::Integer)
1556        );
1557        assert_eq!(
1558            StrictColumnType::from_type_name("REAL"),
1559            Some(StrictColumnType::Real)
1560        );
1561        assert_eq!(
1562            StrictColumnType::from_type_name("TEXT"),
1563            Some(StrictColumnType::Text)
1564        );
1565        assert_eq!(
1566            StrictColumnType::from_type_name("BLOB"),
1567            Some(StrictColumnType::Blob)
1568        );
1569        assert_eq!(
1570            StrictColumnType::from_type_name("ANY"),
1571            Some(StrictColumnType::Any)
1572        );
1573        // Invalid type name in STRICT mode.
1574        assert_eq!(StrictColumnType::from_type_name("VARCHAR(255)"), None);
1575        assert_eq!(StrictColumnType::from_type_name("NUMERIC"), None);
1576    }
1577
1578    #[test]
1579    fn test_affinity_advisory_never_rejects() {
1580        // Advisory affinity NEVER rejects a value. All combinations must succeed.
1581        let values = vec![
1582            SqliteValue::Null,
1583            SqliteValue::Integer(42),
1584            SqliteValue::Float(3.14),
1585            SqliteValue::Text("hello".into()),
1586            SqliteValue::Blob(Arc::from([0xDE, 0xAD].as_slice())),
1587        ];
1588        let affinities = [
1589            TypeAffinity::Integer,
1590            TypeAffinity::Text,
1591            TypeAffinity::Blob,
1592            TypeAffinity::Real,
1593            TypeAffinity::Numeric,
1594        ];
1595        for val in &values {
1596            for aff in &affinities {
1597                // apply_affinity is infallible - it always returns a value.
1598                let _ = val.clone().apply_affinity(*aff);
1599            }
1600        }
1601    }
1602
1603    // ── bd-13r.2: UNIQUE NULL Semantics (NULL != NULL) ──
1604
1605    #[test]
1606    fn test_unique_allows_multiple_nulls_single_column() {
1607        // In UNIQUE columns, NULL != NULL: two NULLs are never duplicates.
1608        let a = SqliteValue::Null;
1609        let b = SqliteValue::Null;
1610        assert!(!a.unique_eq(&b));
1611    }
1612
1613    #[test]
1614    fn test_unique_allows_multiple_nulls_multi_column_partial_null() {
1615        // UNIQUE(a,b): (NULL,1) and (NULL,1) are NOT duplicates because
1616        // any NULL component makes the whole key non-duplicate.
1617        let row_a = [SqliteValue::Null, SqliteValue::Integer(1)];
1618        let row_b = [SqliteValue::Null, SqliteValue::Integer(1)];
1619        assert!(!unique_key_duplicates(&row_a, &row_b));
1620
1621        // UNIQUE(a,b): (1,NULL) and (1,NULL) are NOT duplicates.
1622        let row_a = [SqliteValue::Integer(1), SqliteValue::Null];
1623        let row_b = [SqliteValue::Integer(1), SqliteValue::Null];
1624        assert!(!unique_key_duplicates(&row_a, &row_b));
1625
1626        // UNIQUE(a,b): (NULL,NULL) and (NULL,NULL) are NOT duplicates.
1627        let row_a = [SqliteValue::Null, SqliteValue::Null];
1628        let row_b = [SqliteValue::Null, SqliteValue::Null];
1629        assert!(!unique_key_duplicates(&row_a, &row_b));
1630    }
1631
1632    #[test]
1633    fn test_unique_rejects_duplicate_non_null() {
1634        // Two identical non-NULL values ARE duplicates.
1635        let a = SqliteValue::Integer(42);
1636        let b = SqliteValue::Integer(42);
1637        assert!(a.unique_eq(&b));
1638
1639        // Composite: (1, "hello") and (1, "hello") ARE duplicates.
1640        let row_a = [SqliteValue::Integer(1), SqliteValue::Text("hello".into())];
1641        let row_b = [SqliteValue::Integer(1), SqliteValue::Text("hello".into())];
1642        assert!(unique_key_duplicates(&row_a, &row_b));
1643
1644        // Different values are NOT duplicates.
1645        let row_a = [SqliteValue::Integer(1), SqliteValue::Text("hello".into())];
1646        let row_b = [SqliteValue::Integer(1), SqliteValue::Text("world".into())];
1647        assert!(!unique_key_duplicates(&row_a, &row_b));
1648    }
1649
1650    #[test]
1651    fn test_unique_null_vs_non_null_distinct() {
1652        // NULL and a non-NULL value are never duplicates.
1653        let a = SqliteValue::Null;
1654        let b = SqliteValue::Integer(1);
1655        assert!(!a.unique_eq(&b));
1656        assert!(!b.unique_eq(&a));
1657
1658        // Composite: (NULL, 1) and (2, 1) are not duplicates (different first element).
1659        let row_a = [SqliteValue::Null, SqliteValue::Integer(1)];
1660        let row_b = [SqliteValue::Integer(2), SqliteValue::Integer(1)];
1661        assert!(!unique_key_duplicates(&row_a, &row_b));
1662    }
1663
1664    // ── bd-13r.4: Integer Overflow Semantics (Expr vs sum()) ──
1665
1666    #[test]
1667    #[allow(clippy::cast_precision_loss)]
1668    fn test_integer_overflow_promotes_real_expr_add() {
1669        let max = SqliteValue::Integer(i64::MAX);
1670        let one = SqliteValue::Integer(1);
1671        let result = max.sql_add(&one);
1672        // Overflow promotes to REAL (not integer).
1673        assert!(result.as_integer().is_none());
1674        assert!(result.as_float().is_some());
1675        // The float value is approximately i64::MAX + 1.
1676        assert!(result.as_float().unwrap() >= i64::MAX as f64);
1677    }
1678
1679    #[test]
1680    fn test_integer_overflow_promotes_real_expr_mul() {
1681        let max = SqliteValue::Integer(i64::MAX);
1682        let two = SqliteValue::Integer(2);
1683        let result = max.sql_mul(&two);
1684        // Overflow promotes to REAL.
1685        assert!(result.as_float().is_some());
1686    }
1687
1688    #[test]
1689    fn test_integer_overflow_promotes_real_expr_sub() {
1690        let min = SqliteValue::Integer(i64::MIN);
1691        let one = SqliteValue::Integer(1);
1692        let result = min.sql_sub(&one);
1693        // Underflow promotes to REAL.
1694        assert!(result.as_float().is_some());
1695    }
1696
1697    #[test]
1698    fn test_sum_overflow_errors() {
1699        let mut acc = SumAccumulator::new();
1700        acc.accumulate(&SqliteValue::Integer(i64::MAX));
1701        acc.accumulate(&SqliteValue::Integer(1));
1702        let result = acc.finish();
1703        assert!(result.is_err());
1704    }
1705
1706    #[test]
1707    fn test_no_overflow_stays_integer() {
1708        // Non-overflow addition stays INTEGER.
1709        let a = SqliteValue::Integer(100);
1710        let b = SqliteValue::Integer(200);
1711        let result = a.sql_add(&b);
1712        assert_eq!(result.as_integer(), Some(300));
1713
1714        // Non-overflow multiplication stays INTEGER.
1715        let result = SqliteValue::Integer(7).sql_mul(&SqliteValue::Integer(6));
1716        assert_eq!(result.as_integer(), Some(42));
1717
1718        // Non-overflow subtraction stays INTEGER.
1719        let result = SqliteValue::Integer(50).sql_sub(&SqliteValue::Integer(8));
1720        assert_eq!(result.as_integer(), Some(42));
1721    }
1722
1723    #[test]
1724    fn test_sum_null_only_returns_null() {
1725        let mut acc = SumAccumulator::new();
1726        acc.accumulate(&SqliteValue::Null);
1727        acc.accumulate(&SqliteValue::Null);
1728        let result = acc.finish().unwrap();
1729        assert!(result.is_null());
1730    }
1731
1732    #[test]
1733    fn test_sum_mixed_int_float() {
1734        let mut acc = SumAccumulator::new();
1735        acc.accumulate(&SqliteValue::Integer(10));
1736        acc.accumulate(&SqliteValue::Float(2.5));
1737        acc.accumulate(&SqliteValue::Integer(3));
1738        let result = acc.finish().unwrap();
1739        // Once float is seen, result is float.
1740        assert_eq!(result.as_float(), Some(15.5));
1741    }
1742
1743    #[test]
1744    fn test_sum_integer_only() {
1745        let mut acc = SumAccumulator::new();
1746        acc.accumulate(&SqliteValue::Integer(10));
1747        acc.accumulate(&SqliteValue::Integer(20));
1748        acc.accumulate(&SqliteValue::Integer(30));
1749        let result = acc.finish().unwrap();
1750        assert_eq!(result.as_integer(), Some(60));
1751    }
1752
1753    #[test]
1754    fn test_sql_arithmetic_null_propagation() {
1755        let n = SqliteValue::Null;
1756        let i = SqliteValue::Integer(42);
1757        assert!(n.sql_add(&i).is_null());
1758        assert!(i.sql_add(&n).is_null());
1759        assert!(n.sql_sub(&i).is_null());
1760        assert!(n.sql_mul(&i).is_null());
1761    }
1762
1763    #[test]
1764    fn test_sql_inf_arithmetic_nan_normalized_to_null() {
1765        // +Inf + (-Inf) is NaN in IEEE-754 and must be normalized to NULL.
1766        let pos_inf = SqliteValue::Float(f64::INFINITY);
1767        let neg_inf = SqliteValue::Float(f64::NEG_INFINITY);
1768        assert!(pos_inf.sql_add(&neg_inf).is_null());
1769
1770        // +Inf - +Inf is also NaN and must normalize to NULL.
1771        assert!(pos_inf.sql_sub(&pos_inf).is_null());
1772    }
1773
1774    #[test]
1775    fn test_sql_mul_zero_times_inf_normalized_to_null() {
1776        // 0 * +Inf is NaN in IEEE-754 and must be normalized to NULL.
1777        let zero = SqliteValue::Float(0.0);
1778        let pos_inf = SqliteValue::Float(f64::INFINITY);
1779        assert!(zero.sql_mul(&pos_inf).is_null());
1780    }
1781
1782    #[test]
1783    fn test_sql_inf_propagates_when_not_nan() {
1784        let pos_inf = SqliteValue::Float(f64::INFINITY);
1785        let one = SqliteValue::Integer(1);
1786        let add_result = pos_inf.sql_add(&one);
1787        assert!(
1788            matches!(add_result, SqliteValue::Float(v) if v.is_infinite() && v.is_sign_positive()),
1789            "expected +Inf propagation, got {add_result:?}"
1790        );
1791
1792        let neg_inf = SqliteValue::Float(f64::NEG_INFINITY);
1793        let sub_result = neg_inf.sql_sub(&one);
1794        assert!(
1795            matches!(sub_result, SqliteValue::Float(v) if v.is_infinite() && v.is_sign_negative()),
1796            "expected -Inf propagation, got {sub_result:?}"
1797        );
1798    }
1799
1800    #[test]
1801    fn test_from_f64_nan_normalizes_to_null() {
1802        let value = SqliteValue::from(f64::NAN);
1803        assert!(value.is_null());
1804    }
1805
1806    #[test]
1807    fn test_inf_comparisons_against_finite_values() {
1808        let pos_inf = SqliteValue::Float(f64::INFINITY);
1809        let neg_inf = SqliteValue::Float(f64::NEG_INFINITY);
1810        let finite_hi = SqliteValue::Float(1.0e308);
1811        let finite_lo = SqliteValue::Float(-1.0e308);
1812
1813        assert_eq!(pos_inf.partial_cmp(&finite_hi), Some(Ordering::Greater));
1814        assert_eq!(neg_inf.partial_cmp(&finite_lo), Some(Ordering::Less));
1815    }
1816
1817    // ── bd-13r.7: Empty String vs NULL Semantics ──
1818
1819    #[test]
1820    fn test_empty_string_is_not_null() {
1821        let empty = SqliteValue::Text(Arc::from(""));
1822        // '' IS NULL → false.
1823        assert!(!empty.is_null());
1824        // '' IS NOT NULL → true (expressed as !is_null).
1825        assert!(!empty.is_null());
1826        // NULL IS NULL → true.
1827        assert!(SqliteValue::Null.is_null());
1828    }
1829
1830    #[test]
1831    fn test_length_empty_string_zero() {
1832        let empty = SqliteValue::Text(Arc::from(""));
1833        assert_eq!(empty.sql_length(), Some(0));
1834    }
1835
1836    #[test]
1837    fn test_typeof_empty_string_text() {
1838        let empty = SqliteValue::Text(Arc::from(""));
1839        assert_eq!(empty.typeof_str(), "text");
1840        // NULL has typeof "null".
1841        assert_eq!(SqliteValue::Null.typeof_str(), "null");
1842    }
1843
1844    #[test]
1845    fn test_empty_string_comparisons() {
1846        let empty1 = SqliteValue::Text(Arc::from(""));
1847        let empty2 = SqliteValue::Text(Arc::from(""));
1848        // '' = '' → true.
1849        assert_eq!(empty1.partial_cmp(&empty2), Some(std::cmp::Ordering::Equal));
1850
1851        // '' = NULL → NULL (comparison with NULL yields None/unknown).
1852        // In our PartialOrd, NULL and TEXT are different sort classes,
1853        // so NULL < TEXT (they are not equal).
1854        let null = SqliteValue::Null;
1855        assert_ne!(empty1.partial_cmp(&null), Some(std::cmp::Ordering::Equal));
1856    }
1857
1858    #[test]
1859    fn test_typeof_all_variants() {
1860        assert_eq!(SqliteValue::Null.typeof_str(), "null");
1861        assert_eq!(SqliteValue::Integer(0).typeof_str(), "integer");
1862        assert_eq!(SqliteValue::Float(0.0).typeof_str(), "real");
1863        assert_eq!(SqliteValue::Text("x".into()).typeof_str(), "text");
1864        assert_eq!(
1865            SqliteValue::Blob(Arc::from(&[] as &[u8])).typeof_str(),
1866            "blob"
1867        );
1868    }
1869
1870    #[test]
1871    fn test_sql_length_all_types() {
1872        // NULL → NULL (None).
1873        assert_eq!(SqliteValue::Null.sql_length(), None);
1874        // TEXT → character count.
1875        assert_eq!(SqliteValue::Text("hello".into()).sql_length(), Some(5));
1876        assert_eq!(SqliteValue::Text(Arc::from("")).sql_length(), Some(0));
1877        // BLOB → byte count.
1878        assert_eq!(
1879            SqliteValue::Blob(Arc::from([1u8, 2, 3].as_slice())).sql_length(),
1880            Some(3)
1881        );
1882        // INTEGER → length of text representation.
1883        assert_eq!(SqliteValue::Integer(42).sql_length(), Some(2));
1884        // REAL → length of text representation.
1885        assert_eq!(SqliteValue::Float(3.14).sql_length(), Some(4)); // "3.14"
1886    }
1887
1888    // ── bd-13r.6: LIKE Semantics (ASCII-only case folding) ──
1889
1890    #[test]
1891    fn test_like_ascii_case_insensitive() {
1892        assert!(sql_like("A", "a", None));
1893        assert!(sql_like("a", "A", None));
1894        assert!(sql_like("hello", "HELLO", None));
1895        assert!(sql_like("HELLO", "hello", None));
1896        assert!(sql_like("HeLLo", "hEllO", None));
1897    }
1898
1899    #[test]
1900    fn test_like_unicode_case_sensitive_without_icu() {
1901        // Without ICU, Unicode case folding does NOT occur.
1902        assert!(!sql_like("ä", "Ä", None));
1903        assert!(!sql_like("Ä", "ä", None));
1904        // But exact match works.
1905        assert!(sql_like("ä", "ä", None));
1906    }
1907
1908    #[test]
1909    fn test_like_escape_handling() {
1910        // Escape literal % with backslash.
1911        assert!(sql_like("100\\%", "100%", Some('\\')));
1912        assert!(!sql_like("100\\%", "100x", Some('\\')));
1913
1914        // Escape literal _.
1915        assert!(sql_like("a\\_b", "a_b", Some('\\')));
1916        assert!(!sql_like("a\\_b", "axb", Some('\\')));
1917    }
1918
1919    #[test]
1920    fn test_like_wildcards_basic() {
1921        // % matches zero or more characters.
1922        assert!(sql_like("%", "", None));
1923        assert!(sql_like("%", "anything", None));
1924        assert!(sql_like("a%", "abc", None));
1925        assert!(sql_like("%c", "abc", None));
1926        assert!(sql_like("a%c", "abc", None));
1927        assert!(sql_like("a%c", "aXYZc", None));
1928        assert!(!sql_like("a%c", "abd", None));
1929
1930        // _ matches exactly one character.
1931        assert!(sql_like("_", "x", None));
1932        assert!(!sql_like("_", "", None));
1933        assert!(!sql_like("_", "xy", None));
1934        assert!(sql_like("a_c", "abc", None));
1935        assert!(!sql_like("a_c", "abbc", None));
1936    }
1937
1938    #[test]
1939    fn test_like_combined_wildcards() {
1940        assert!(sql_like("%_", "a", None));
1941        assert!(!sql_like("%_", "", None));
1942        assert!(sql_like("_%_", "ab", None));
1943        assert!(!sql_like("_%_", "a", None));
1944        assert!(sql_like("%a%b%", "xaybz", None));
1945        assert!(!sql_like("%a%b%", "xyz", None));
1946    }
1947
1948    #[test]
1949    fn test_like_exact_match() {
1950        assert!(sql_like("hello", "hello", None));
1951        assert!(!sql_like("hello", "world", None));
1952        assert!(sql_like("", "", None));
1953        assert!(!sql_like("a", "", None));
1954        assert!(!sql_like("", "a", None));
1955    }
1956
1957    // ── format_sqlite_float ────────────────────────────────────────────
1958
1959    #[test]
1960    fn test_format_sqlite_float_whole_number() {
1961        assert_eq!(format_sqlite_float(120.0), "120.0");
1962        assert_eq!(format_sqlite_float(0.0), "0.0");
1963        assert_eq!(format_sqlite_float(-42.0), "-42.0");
1964        assert_eq!(format_sqlite_float(1.0), "1.0");
1965    }
1966
1967    #[test]
1968    fn test_format_sqlite_float_fractional() {
1969        assert_eq!(format_sqlite_float(3.14), "3.14");
1970        assert_eq!(format_sqlite_float(0.5), "0.5");
1971        assert_eq!(format_sqlite_float(-0.001), "-0.001");
1972    }
1973
1974    #[test]
1975    fn test_format_sqlite_float_special() {
1976        assert_eq!(format_sqlite_float(f64::NAN), "NaN");
1977        assert_eq!(format_sqlite_float(f64::INFINITY), "Inf");
1978        assert_eq!(format_sqlite_float(f64::NEG_INFINITY), "-Inf");
1979    }
1980
1981    #[test]
1982    fn test_format_sqlite_float_negative_zero() {
1983        // C SQLite: printf("%!.15g", -0.0) → "-0.0"
1984        assert_eq!(format_sqlite_float(-0.0), "-0.0");
1985        assert_eq!(format_sqlite_float(0.0), "0.0");
1986    }
1987
1988    #[test]
1989    fn test_float_to_text_includes_decimal_point() {
1990        let v = SqliteValue::Float(100.0);
1991        assert_eq!(v.to_text(), "100.0");
1992        let v = SqliteValue::Float(3.14);
1993        assert_eq!(v.to_text(), "3.14");
1994    }
1995
1996    // ── scan_numeric_prefix ──────────────────────────────────────────
1997
1998    #[test]
1999    fn test_scan_numeric_prefix_bare_dot() {
2000        // A bare "." has no digits — not a numeric prefix.
2001        assert_eq!(scan_numeric_prefix(b"."), 0);
2002        assert_eq!(scan_numeric_prefix(b"-."), 0);
2003        assert_eq!(scan_numeric_prefix(b"+."), 0);
2004        assert_eq!(scan_numeric_prefix(b"..1"), 0);
2005    }
2006
2007    #[test]
2008    fn test_scan_numeric_prefix_valid() {
2009        assert_eq!(scan_numeric_prefix(b"123"), 3);
2010        assert_eq!(scan_numeric_prefix(b"3.14"), 4);
2011        assert_eq!(scan_numeric_prefix(b".5"), 2);
2012        assert_eq!(scan_numeric_prefix(b"1e10"), 4);
2013        assert_eq!(scan_numeric_prefix(b"-42abc"), 3);
2014        assert_eq!(scan_numeric_prefix(b"+.5x"), 3);
2015        assert_eq!(scan_numeric_prefix(b"0.0"), 3);
2016    }
2017
2018    #[test]
2019    fn test_scan_numeric_prefix_empty_and_non_numeric() {
2020        assert_eq!(scan_numeric_prefix(b""), 0);
2021        assert_eq!(scan_numeric_prefix(b"abc"), 0);
2022        assert_eq!(scan_numeric_prefix(b"+"), 0);
2023        assert_eq!(scan_numeric_prefix(b"-"), 0);
2024    }
2025}