Skip to main content

fsqlite_types/
value.rs

1use std::cmp::Ordering;
2use std::fmt;
3
4use crate::{StorageClass, StrictColumnType, StrictTypeError, TypeAffinity};
5
6/// A dynamically-typed SQLite value.
7///
8/// Corresponds to C SQLite's `sqlite3_value` / `Mem` type. SQLite has five
9/// fundamental storage classes: NULL, INTEGER, REAL, TEXT, and BLOB.
10#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
11pub enum SqliteValue {
12    /// SQL NULL.
13    Null,
14    /// A 64-bit signed integer.
15    Integer(i64),
16    /// A 64-bit IEEE 754 floating-point number.
17    Float(f64),
18    /// A UTF-8 text string.
19    Text(String),
20    /// A binary large object.
21    Blob(Vec<u8>),
22}
23
24impl SqliteValue {
25    /// Returns the type affinity that best describes this value.
26    pub const fn affinity(&self) -> TypeAffinity {
27        match self {
28            Self::Null | Self::Blob(_) => TypeAffinity::Blob,
29            Self::Integer(_) => TypeAffinity::Integer,
30            Self::Float(_) => TypeAffinity::Real,
31            Self::Text(_) => TypeAffinity::Text,
32        }
33    }
34
35    /// Returns the storage class of this value.
36    pub const fn storage_class(&self) -> StorageClass {
37        match self {
38            Self::Null => StorageClass::Null,
39            Self::Integer(_) => StorageClass::Integer,
40            Self::Float(_) => StorageClass::Real,
41            Self::Text(_) => StorageClass::Text,
42            Self::Blob(_) => StorageClass::Blob,
43        }
44    }
45
46    /// Apply column type affinity coercion (advisory mode).
47    ///
48    /// In non-STRICT tables, affinity is advisory: values are coerced when
49    /// possible but never rejected. Follows SQLite §3.4 rules from
50    /// <https://www.sqlite.org/datatype3.html#type_affinity_of_a_column>.
51    ///
52    /// - TEXT affinity: numeric values converted to text before storing.
53    /// - NUMERIC affinity: text parsed as integer/real if well-formed.
54    /// - INTEGER affinity: like NUMERIC, plus exact-integer reals become integer.
55    /// - REAL affinity: like NUMERIC, plus integers forced to float.
56    /// - BLOB affinity: no conversion.
57    #[must_use]
58    #[allow(
59        clippy::cast_possible_truncation,
60        clippy::cast_precision_loss,
61        clippy::float_cmp
62    )]
63    pub fn apply_affinity(self, affinity: TypeAffinity) -> Self {
64        match affinity {
65            TypeAffinity::Blob => self,
66            TypeAffinity::Text => match self {
67                Self::Null | Self::Text(_) | Self::Blob(_) => self,
68                Self::Integer(_) | Self::Float(_) => {
69                    let t = self.to_text();
70                    Self::Text(t)
71                }
72            },
73            TypeAffinity::Numeric => match &self {
74                Self::Text(s) => try_coerce_text_to_numeric(s).unwrap_or(self),
75                _ => self,
76            },
77            TypeAffinity::Integer => match &self {
78                Self::Text(s) => try_coerce_text_to_numeric(s).unwrap_or(self),
79                Self::Float(f) => {
80                    if *f >= -9_223_372_036_854_775_808.0 && *f < 9_223_372_036_854_775_808.0 {
81                        let i = *f as i64;
82                        if (i as f64) == *f {
83                            return Self::Integer(i);
84                        }
85                    }
86                    self
87                }
88                _ => self,
89            },
90            TypeAffinity::Real => match &self {
91                Self::Text(s) => try_coerce_text_to_numeric(s)
92                    .map(|v| match v {
93                        Self::Integer(i) => Self::Float(i as f64),
94                        other => other,
95                    })
96                    .unwrap_or(self),
97                Self::Integer(i) => Self::Float(*i as f64),
98                _ => self,
99            },
100        }
101    }
102
103    /// Validate a value against a STRICT table column type.
104    ///
105    /// NULL is always accepted (nullability is enforced separately via NOT NULL).
106    /// Returns `Ok(value)` with possible implicit coercion (REAL columns accept
107    /// integers, converting them to float), or `Err` if the storage class is
108    /// incompatible.
109    #[allow(clippy::cast_precision_loss)]
110    pub fn validate_strict(self, col_type: StrictColumnType) -> Result<Self, StrictTypeError> {
111        if matches!(self, Self::Null) {
112            return Ok(self);
113        }
114        match col_type {
115            StrictColumnType::Any => Ok(self),
116            StrictColumnType::Integer => match self {
117                Self::Integer(_) => Ok(self),
118                other => Err(StrictTypeError {
119                    expected: col_type,
120                    actual: other.storage_class(),
121                }),
122            },
123            StrictColumnType::Real => match self {
124                Self::Float(_) => Ok(self),
125                Self::Integer(i) => Ok(Self::Float(i as f64)),
126                other => Err(StrictTypeError {
127                    expected: col_type,
128                    actual: other.storage_class(),
129                }),
130            },
131            StrictColumnType::Text => match self {
132                Self::Text(_) => Ok(self),
133                other => Err(StrictTypeError {
134                    expected: col_type,
135                    actual: other.storage_class(),
136                }),
137            },
138            StrictColumnType::Blob => match self {
139                Self::Blob(_) => Ok(self),
140                other => Err(StrictTypeError {
141                    expected: col_type,
142                    actual: other.storage_class(),
143                }),
144            },
145        }
146    }
147
148    /// Returns true if this is a NULL value.
149    pub const fn is_null(&self) -> bool {
150        matches!(self, Self::Null)
151    }
152
153    /// Try to extract an integer value.
154    pub const fn as_integer(&self) -> Option<i64> {
155        match self {
156            Self::Integer(i) => Some(*i),
157            _ => None,
158        }
159    }
160
161    /// Try to extract a float value.
162    pub fn as_float(&self) -> Option<f64> {
163        match self {
164            Self::Float(f) => Some(*f),
165            _ => None,
166        }
167    }
168
169    /// Try to extract a text reference.
170    pub fn as_text(&self) -> Option<&str> {
171        match self {
172            Self::Text(s) => Some(s),
173            _ => None,
174        }
175    }
176
177    /// Try to extract a blob reference.
178    pub fn as_blob(&self) -> Option<&[u8]> {
179        match self {
180            Self::Blob(b) => Some(b),
181            _ => None,
182        }
183    }
184
185    /// Convert to an integer following SQLite's type coercion rules.
186    ///
187    /// - NULL -> 0
188    /// - Integer -> itself
189    /// - Float -> truncated to i64
190    /// - Text -> attempt to parse, 0 on failure
191    /// - Blob -> 0
192    #[allow(clippy::cast_possible_truncation)]
193    pub fn to_integer(&self) -> i64 {
194        match self {
195            Self::Null | Self::Blob(_) => 0,
196            Self::Integer(i) => *i,
197            Self::Float(f) => *f as i64,
198            Self::Text(s) => s.trim().parse::<i64>().unwrap_or_else(|_| {
199                // Try parsing as float first, then truncate
200                s.trim().parse::<f64>().map_or(0, |f| f as i64)
201            }),
202        }
203    }
204
205    /// Convert to a float following SQLite's type coercion rules.
206    ///
207    /// - NULL -> 0.0
208    /// - Integer -> as f64
209    /// - Float -> itself
210    /// - Text -> attempt to parse, 0.0 on failure
211    /// - Blob -> 0.0
212    #[allow(clippy::cast_precision_loss)]
213    pub fn to_float(&self) -> f64 {
214        match self {
215            Self::Null | Self::Blob(_) => 0.0,
216            Self::Integer(i) => *i as f64,
217            Self::Float(f) => *f,
218            Self::Text(s) => s.trim().parse::<f64>().unwrap_or(0.0),
219        }
220    }
221
222    /// Convert to text following SQLite's type coercion rules.
223    pub fn to_text(&self) -> String {
224        match self {
225            Self::Null => String::new(),
226            Self::Integer(i) => i.to_string(),
227            Self::Float(f) => format!("{f}"),
228            Self::Text(s) => s.clone(),
229            Self::Blob(b) => {
230                use std::fmt::Write;
231                let mut hex = String::with_capacity(2 + b.len() * 2);
232                hex.push_str("X'");
233                for byte in b {
234                    let _ = write!(hex, "{byte:02X}");
235                }
236                hex.push('\'');
237                hex
238            }
239        }
240    }
241
242    /// Returns the SQLite `typeof()` string for this value.
243    ///
244    /// Matches C sqlite3: "null", "integer", "real", "text", or "blob".
245    pub const fn typeof_str(&self) -> &'static str {
246        match self {
247            Self::Null => "null",
248            Self::Integer(_) => "integer",
249            Self::Float(_) => "real",
250            Self::Text(_) => "text",
251            Self::Blob(_) => "blob",
252        }
253    }
254
255    /// Returns the SQLite `length()` result for this value.
256    ///
257    /// - NULL → NULL (represented as None)
258    /// - TEXT → character count
259    /// - BLOB → byte count
260    /// - INTEGER/REAL → character count of text representation
261    pub fn sql_length(&self) -> Option<i64> {
262        match self {
263            Self::Null => None,
264            Self::Text(s) => Some(i64::try_from(s.chars().count()).unwrap_or(i64::MAX)),
265            Self::Blob(b) => Some(i64::try_from(b.len()).unwrap_or(i64::MAX)),
266            Self::Integer(_) | Self::Float(_) => {
267                let t = self.to_text();
268                Some(i64::try_from(t.chars().count()).unwrap_or(i64::MAX))
269            }
270        }
271    }
272
273    /// Check equality for UNIQUE constraint purposes.
274    ///
275    /// In SQLite, NULL != NULL for uniqueness: if either value is NULL, the
276    /// result is `false` (they are never considered duplicates). Non-NULL values
277    /// compare by storage class ordering (same as `PartialEq`).
278    pub fn unique_eq(&self, other: &Self) -> bool {
279        if self.is_null() || other.is_null() {
280            return false;
281        }
282        matches!(self.partial_cmp(other), Some(Ordering::Equal))
283    }
284
285    /// Convert a floating-point arithmetic result into a SQLite value.
286    ///
287    /// SQLite does not surface NaN; NaN is normalized to NULL while ±Inf remain REAL.
288    fn float_result_or_null(result: f64) -> Self {
289        if result.is_nan() {
290            Self::Null
291        } else {
292            Self::Float(result)
293        }
294    }
295
296    /// Add two values following SQLite's overflow semantics.
297    ///
298    /// - Integer + Integer: checked add; overflows promote to REAL.
299    /// - Any REAL operand: float addition.
300    /// - NULL propagates (NULL + x = NULL).
301    /// - Non-numeric types coerced to numeric first.
302    #[must_use]
303    #[allow(clippy::cast_precision_loss)]
304    pub fn sql_add(&self, other: &Self) -> Self {
305        match (self, other) {
306            (Self::Null, _) | (_, Self::Null) => Self::Null,
307            (Self::Integer(a), Self::Integer(b)) => match a.checked_add(*b) {
308                Some(result) => Self::Integer(result),
309                None => Self::float_result_or_null(*a as f64 + *b as f64),
310            },
311            _ => Self::float_result_or_null(self.to_float() + other.to_float()),
312        }
313    }
314
315    /// Subtract two values following SQLite's overflow semantics.
316    ///
317    /// Integer - Integer with overflow promotes to REAL.
318    #[must_use]
319    #[allow(clippy::cast_precision_loss)]
320    pub fn sql_sub(&self, other: &Self) -> Self {
321        match (self, other) {
322            (Self::Null, _) | (_, Self::Null) => Self::Null,
323            (Self::Integer(a), Self::Integer(b)) => match a.checked_sub(*b) {
324                Some(result) => Self::Integer(result),
325                None => Self::float_result_or_null(*a as f64 - *b as f64),
326            },
327            _ => Self::float_result_or_null(self.to_float() - other.to_float()),
328        }
329    }
330
331    /// Multiply two values following SQLite's overflow semantics.
332    ///
333    /// Integer * Integer with overflow promotes to REAL.
334    #[must_use]
335    #[allow(clippy::cast_precision_loss)]
336    pub fn sql_mul(&self, other: &Self) -> Self {
337        match (self, other) {
338            (Self::Null, _) | (_, Self::Null) => Self::Null,
339            (Self::Integer(a), Self::Integer(b)) => match a.checked_mul(*b) {
340                Some(result) => Self::Integer(result),
341                None => Self::float_result_or_null(*a as f64 * *b as f64),
342            },
343            _ => Self::float_result_or_null(self.to_float() * other.to_float()),
344        }
345    }
346
347    /// The sort order key for NULL values (SQLite sorts NULLs first).
348    const fn sort_class(&self) -> u8 {
349        match self {
350            Self::Null => 0,
351            Self::Integer(_) | Self::Float(_) => 1,
352            Self::Text(_) => 2,
353            Self::Blob(_) => 3,
354        }
355    }
356}
357
358/// Check if two composite UNIQUE keys are duplicates (SQLite NULL semantics).
359///
360/// Returns `true` only if ALL corresponding components are non-NULL and equal.
361/// If ANY component in either key is NULL, the keys are NOT duplicates (per
362/// SQLite's NULL != NULL rule for UNIQUE constraints).
363///
364/// Both slices must have the same length (panics otherwise).
365pub fn unique_key_duplicates(a: &[SqliteValue], b: &[SqliteValue]) -> bool {
366    assert_eq!(a.len(), b.len(), "UNIQUE key columns must match");
367    a.iter().zip(b.iter()).all(|(va, vb)| va.unique_eq(vb))
368}
369
370/// Match a string against a SQL LIKE pattern with SQLite semantics.
371///
372/// - `%` matches zero or more characters.
373/// - `_` matches exactly one character.
374/// - Case-insensitive for ASCII A-Z only (no Unicode case folding without ICU).
375/// - `escape` optionally specifies the escape character for literal `%`/`_`.
376pub fn sql_like(pattern: &str, text: &str, escape: Option<char>) -> bool {
377    sql_like_inner(
378        &pattern.chars().collect::<Vec<_>>(),
379        &text.chars().collect::<Vec<_>>(),
380        escape,
381        0,
382        0,
383    )
384}
385
386fn sql_like_inner(
387    pattern: &[char],
388    text: &[char],
389    escape: Option<char>,
390    pi: usize,
391    ti: usize,
392) -> bool {
393    let mut pi = pi;
394    let mut ti = ti;
395
396    while pi < pattern.len() {
397        let pc = pattern[pi];
398
399        // Handle escape character.
400        if Some(pc) == escape {
401            pi += 1;
402            if pi >= pattern.len() {
403                return false; // Trailing escape is malformed.
404            }
405            // Match the escaped character literally.
406            if ti >= text.len() || !ascii_ci_eq(pattern[pi], text[ti]) {
407                return false;
408            }
409            pi += 1;
410            ti += 1;
411            continue;
412        }
413
414        match pc {
415            '%' => {
416                // Skip consecutive % wildcards.
417                while pi < pattern.len() && pattern[pi] == '%' {
418                    pi += 1;
419                }
420                // If % is at end of pattern, matches everything.
421                if pi >= pattern.len() {
422                    return true;
423                }
424                // Try matching rest of pattern at each position.
425                for start in ti..=text.len() {
426                    if sql_like_inner(pattern, text, escape, pi, start) {
427                        return true;
428                    }
429                }
430                return false;
431            }
432            '_' => {
433                if ti >= text.len() {
434                    return false;
435                }
436                pi += 1;
437                ti += 1;
438            }
439            _ => {
440                if ti >= text.len() || !ascii_ci_eq(pc, text[ti]) {
441                    return false;
442                }
443                pi += 1;
444                ti += 1;
445            }
446        }
447    }
448    ti >= text.len()
449}
450
451/// ASCII-only case-insensitive character comparison (SQLite LIKE semantics).
452fn ascii_ci_eq(a: char, b: char) -> bool {
453    if a == b {
454        return true;
455    }
456    // Only fold ASCII A-Z / a-z.
457    a.is_ascii() && b.is_ascii() && a.eq_ignore_ascii_case(&b)
458}
459
460/// Accumulator for SQL `sum()` aggregate with SQLite overflow semantics.
461///
462/// Unlike expression arithmetic (which promotes to REAL on overflow), `sum()`
463/// raises an error on integer overflow. This matches C sqlite3 behavior.
464#[derive(Debug, Clone)]
465pub struct SumAccumulator {
466    /// Running integer sum (if still in integer mode).
467    int_sum: i64,
468    /// Running float sum (if promoted to float mode).
469    float_sum: f64,
470    /// Whether we've seen any non-NULL value.
471    has_value: bool,
472    /// Whether we're in float mode (any REAL input or integer overflow).
473    is_float: bool,
474    /// Whether an integer overflow occurred (error condition).
475    overflow: bool,
476}
477
478impl Default for SumAccumulator {
479    fn default() -> Self {
480        Self::new()
481    }
482}
483
484impl SumAccumulator {
485    /// Create a new accumulator.
486    pub const fn new() -> Self {
487        Self {
488            int_sum: 0,
489            float_sum: 0.0,
490            has_value: false,
491            is_float: false,
492            overflow: false,
493        }
494    }
495
496    /// Add a value to the running sum.
497    #[allow(clippy::cast_precision_loss)]
498    pub fn accumulate(&mut self, val: &SqliteValue) {
499        match val {
500            SqliteValue::Null => {}
501            SqliteValue::Integer(i) => {
502                self.has_value = true;
503                if self.is_float {
504                    self.float_sum += *i as f64;
505                } else {
506                    match self.int_sum.checked_add(*i) {
507                        Some(result) => self.int_sum = result,
508                        None => self.overflow = true,
509                    }
510                }
511            }
512            SqliteValue::Float(f) => {
513                self.has_value = true;
514                if !self.is_float {
515                    self.float_sum = self.int_sum as f64;
516                    self.is_float = true;
517                }
518                self.float_sum += f;
519            }
520            other => {
521                // TEXT/BLOB coerced to numeric.
522                self.has_value = true;
523                let n = other.to_float();
524                if !self.is_float {
525                    self.float_sum = self.int_sum as f64;
526                    self.is_float = true;
527                }
528                self.float_sum += n;
529            }
530        }
531    }
532
533    /// Finalize the sum. Returns `Err` if integer overflow occurred,
534    /// `Ok(NULL)` if no non-NULL values were seen, or the sum value.
535    pub fn finish(&self) -> Result<SqliteValue, SumOverflowError> {
536        if self.overflow {
537            return Err(SumOverflowError);
538        }
539        if !self.has_value {
540            return Ok(SqliteValue::Null);
541        }
542        if self.is_float {
543            Ok(SqliteValue::Float(self.float_sum))
544        } else {
545            Ok(SqliteValue::Integer(self.int_sum))
546        }
547    }
548}
549
550/// Error returned when `sum()` encounters integer overflow.
551#[derive(Debug, Clone, PartialEq, Eq)]
552pub struct SumOverflowError;
553
554impl fmt::Display for SumOverflowError {
555    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
556        f.write_str("integer overflow in sum()")
557    }
558}
559
560impl fmt::Display for SqliteValue {
561    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
562        match self {
563            Self::Null => f.write_str("NULL"),
564            Self::Integer(i) => write!(f, "{i}"),
565            Self::Float(v) => write!(f, "{v}"),
566            Self::Text(s) => write!(f, "'{s}'"),
567            Self::Blob(b) => {
568                f.write_str("X'")?;
569                for byte in b {
570                    write!(f, "{byte:02X}")?;
571                }
572                f.write_str("'")
573            }
574        }
575    }
576}
577
578impl PartialEq for SqliteValue {
579    fn eq(&self, other: &Self) -> bool {
580        matches!(self.partial_cmp(other), Some(Ordering::Equal))
581    }
582}
583
584impl PartialOrd for SqliteValue {
585    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
586        // SQLite sort order: NULL < numeric < text < blob
587        let class_a = self.sort_class();
588        let class_b = other.sort_class();
589
590        if class_a != class_b {
591            return Some(class_a.cmp(&class_b));
592        }
593
594        match (self, other) {
595            (Self::Null, Self::Null) => Some(Ordering::Equal),
596            (Self::Integer(a), Self::Integer(b)) => Some(a.cmp(b)),
597            (Self::Float(a), Self::Float(b)) => a.partial_cmp(b),
598            (Self::Integer(a), Self::Float(b)) => Some(int_float_cmp(*a, *b)),
599            (Self::Float(a), Self::Integer(b)) => Some(int_float_cmp(*b, *a).reverse()),
600            (Self::Text(a), Self::Text(b)) => Some(a.cmp(b)),
601            (Self::Blob(a), Self::Blob(b)) => Some(a.cmp(b)),
602            _ => None,
603        }
604    }
605}
606
607impl From<i64> for SqliteValue {
608    fn from(i: i64) -> Self {
609        Self::Integer(i)
610    }
611}
612
613impl From<i32> for SqliteValue {
614    fn from(i: i32) -> Self {
615        Self::Integer(i64::from(i))
616    }
617}
618
619impl From<f64> for SqliteValue {
620    fn from(f: f64) -> Self {
621        Self::float_result_or_null(f)
622    }
623}
624
625impl From<String> for SqliteValue {
626    fn from(s: String) -> Self {
627        Self::Text(s)
628    }
629}
630
631impl From<&str> for SqliteValue {
632    fn from(s: &str) -> Self {
633        Self::Text(s.to_owned())
634    }
635}
636
637impl From<Vec<u8>> for SqliteValue {
638    fn from(b: Vec<u8>) -> Self {
639        Self::Blob(b)
640    }
641}
642
643impl From<&[u8]> for SqliteValue {
644    fn from(b: &[u8]) -> Self {
645        Self::Blob(b.to_vec())
646    }
647}
648
649impl<T: Into<Self>> From<Option<T>> for SqliteValue {
650    fn from(opt: Option<T>) -> Self {
651        match opt {
652            Some(v) => v.into(),
653            None => Self::Null,
654        }
655    }
656}
657
658/// Try to coerce a text string to INTEGER or REAL following SQLite NUMERIC
659/// affinity rules. Returns `None` if the text is not a well-formed numeric
660/// literal.
661#[allow(
662    clippy::cast_possible_truncation,
663    clippy::cast_precision_loss,
664    clippy::float_cmp
665)]
666fn try_coerce_text_to_numeric(s: &str) -> Option<SqliteValue> {
667    let trimmed = s.trim();
668    if trimmed.is_empty() {
669        return None;
670    }
671    // Try integer first (preferred for NUMERIC affinity).
672    if let Ok(i) = trimmed.parse::<i64>() {
673        return Some(SqliteValue::Integer(i));
674    }
675    // Try float. Reject non-finite results (NaN, Infinity) since SQLite
676    // does not recognise "nan", "inf", or "infinity" as numeric literals.
677    if let Ok(f) = trimmed.parse::<f64>() {
678        if !f.is_finite() {
679            return None;
680        }
681        // If the float is an exact integer value within bounds, store as integer.
682        // Checking bounds prevents incorrect saturation for values >= 2^63.
683        if (-9_223_372_036_854_775_808.0..9_223_372_036_854_775_808.0).contains(&f) {
684            #[allow(clippy::cast_possible_truncation)]
685            let i = f as i64;
686            #[allow(clippy::cast_precision_loss)]
687            if (i as f64) == f {
688                return Some(SqliteValue::Integer(i));
689            }
690        }
691        return Some(SqliteValue::Float(f));
692    }
693    None
694}
695
696/// Compare an integer with a float, preserving precision for large i64 values.
697///
698/// Matches C SQLite's `sqlite3IntFloatCompare` algorithm. The naive
699/// `(i as f64).partial_cmp(&r)` loses precision for |i| > 2^53.
700#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
701fn int_float_cmp(i: i64, r: f64) -> Ordering {
702    if r.is_nan() {
703        // SQLite treats NaN as NULL, and all integers are greater than NULL.
704        return Ordering::Greater;
705    }
706    // If r is out of i64 range, the answer is obvious.
707    if r < -9_223_372_036_854_775_808.0 {
708        return Ordering::Greater;
709    }
710    if r >= 9_223_372_036_854_775_808.0 {
711        return Ordering::Less;
712    }
713    // Truncate float to integer and compare integer parts.
714    let y = r as i64;
715    match i.cmp(&y) {
716        Ordering::Less => Ordering::Less,
717        Ordering::Greater => Ordering::Greater,
718        // Integer parts equal — use float comparison as tiebreaker.
719        Ordering::Equal => {
720            let s = i as f64;
721            s.partial_cmp(&r).unwrap_or(Ordering::Equal)
722        }
723    }
724}
725
726#[cfg(test)]
727#[allow(clippy::float_cmp, clippy::approx_constant)]
728mod tests {
729    use super::*;
730
731    #[test]
732    fn null_properties() {
733        let v = SqliteValue::Null;
734        assert!(v.is_null());
735        assert_eq!(v.to_integer(), 0);
736        assert_eq!(v.to_float(), 0.0);
737        assert_eq!(v.to_text(), "");
738        assert_eq!(v.to_string(), "NULL");
739    }
740
741    #[test]
742    fn integer_properties() {
743        let v = SqliteValue::Integer(42);
744        assert!(!v.is_null());
745        assert_eq!(v.as_integer(), Some(42));
746        assert_eq!(v.to_integer(), 42);
747        assert_eq!(v.to_float(), 42.0);
748        assert_eq!(v.to_text(), "42");
749    }
750
751    #[test]
752    fn float_properties() {
753        let v = SqliteValue::Float(3.14);
754        assert_eq!(v.as_float(), Some(3.14));
755        assert_eq!(v.to_integer(), 3);
756        assert_eq!(v.to_text(), "3.14");
757    }
758
759    #[test]
760    fn text_properties() {
761        let v = SqliteValue::Text("hello".to_owned());
762        assert_eq!(v.as_text(), Some("hello"));
763        assert_eq!(v.to_integer(), 0);
764        assert_eq!(v.to_float(), 0.0);
765    }
766
767    #[test]
768    fn text_numeric_coercion() {
769        let v = SqliteValue::Text("123".to_owned());
770        assert_eq!(v.to_integer(), 123);
771        assert_eq!(v.to_float(), 123.0);
772
773        let v = SqliteValue::Text("3.14".to_owned());
774        assert_eq!(v.to_integer(), 3);
775        assert_eq!(v.to_float(), 3.14);
776    }
777
778    #[test]
779    fn test_sqlite_value_integer_real_comparison_equal() {
780        let int_value = SqliteValue::Integer(3);
781        let real_value = SqliteValue::Float(3.0);
782        assert_eq!(int_value.partial_cmp(&real_value), Some(Ordering::Equal));
783        assert_eq!(real_value.partial_cmp(&int_value), Some(Ordering::Equal));
784    }
785
786    #[test]
787    fn test_sqlite_value_text_to_integer_coercion() {
788        let text_value = SqliteValue::Text("123".to_owned());
789        let coerced = text_value.apply_affinity(TypeAffinity::Integer);
790        assert_eq!(coerced, SqliteValue::Integer(123));
791    }
792
793    #[test]
794    fn blob_properties() {
795        let v = SqliteValue::Blob(vec![0xDE, 0xAD]);
796        assert_eq!(v.as_blob(), Some(&[0xDE, 0xAD][..]));
797        assert_eq!(v.to_integer(), 0);
798        assert_eq!(v.to_float(), 0.0);
799        assert_eq!(v.to_text(), "X'DEAD'");
800    }
801
802    #[test]
803    fn display_formatting() {
804        assert_eq!(SqliteValue::Null.to_string(), "NULL");
805        assert_eq!(SqliteValue::Integer(42).to_string(), "42");
806        assert_eq!(SqliteValue::Integer(-1).to_string(), "-1");
807        assert_eq!(SqliteValue::Float(1.5).to_string(), "1.5");
808        assert_eq!(SqliteValue::Text("hi".to_owned()).to_string(), "'hi'");
809        assert_eq!(SqliteValue::Blob(vec![0xCA, 0xFE]).to_string(), "X'CAFE'");
810    }
811
812    #[test]
813    fn sort_order_null_first() {
814        let null = SqliteValue::Null;
815        let int = SqliteValue::Integer(0);
816        let text = SqliteValue::Text(String::new());
817        let blob = SqliteValue::Blob(vec![]);
818
819        assert!(null < int);
820        assert!(int < text);
821        assert!(text < blob);
822    }
823
824    #[test]
825    fn sort_order_integers() {
826        let a = SqliteValue::Integer(1);
827        let b = SqliteValue::Integer(2);
828        assert!(a < b);
829        assert_eq!(a.partial_cmp(&a), Some(Ordering::Equal));
830    }
831
832    #[test]
833    fn sort_order_mixed_numeric() {
834        let int = SqliteValue::Integer(1);
835        let float = SqliteValue::Float(1.5);
836        assert!(int < float);
837
838        let int = SqliteValue::Integer(2);
839        assert!(int > float);
840    }
841
842    #[test]
843    fn test_int_float_precision_at_i64_boundary() {
844        // i64::MAX cast to f64 rounds UP to 9223372036854775808.0.
845        // The naive (i as f64) comparison would say Equal, but C SQLite
846        // correctly reports i64::MAX < 9223372036854775808.0.
847        let imax = SqliteValue::Integer(i64::MAX);
848        let fmax = SqliteValue::Float(9_223_372_036_854_775_808.0);
849        assert_eq!(
850            imax.partial_cmp(&fmax),
851            Some(Ordering::Less),
852            "i64::MAX must be Less than 9223372036854775808.0"
853        );
854
855        // Two distinct large integers that map to the same f64.
856        let a = SqliteValue::Integer(i64::MAX);
857        let b = SqliteValue::Integer(i64::MAX - 1);
858        let f = SqliteValue::Float(i64::MAX as f64);
859        // a > b, but both should compare consistently vs the float.
860        assert_eq!(a.partial_cmp(&b), Some(Ordering::Greater));
861        // Both are less than the rounded-up float.
862        assert_eq!(a.partial_cmp(&f), Some(Ordering::Less));
863        assert_eq!(b.partial_cmp(&f), Some(Ordering::Less));
864    }
865
866    #[test]
867    fn test_int_float_precision_symmetric() {
868        // Float-vs-Integer should be the reverse of Integer-vs-Float.
869        let i = SqliteValue::Integer(i64::MAX);
870        let f = SqliteValue::Float(9_223_372_036_854_775_808.0);
871        assert_eq!(f.partial_cmp(&i), Some(Ordering::Greater));
872    }
873
874    #[test]
875    fn test_int_float_exact_representation() {
876        // For exactly representable values, equality still works.
877        let i = SqliteValue::Integer(42);
878        let f = SqliteValue::Float(42.0);
879        assert_eq!(i.partial_cmp(&f), Some(Ordering::Equal));
880        assert_eq!(f.partial_cmp(&i), Some(Ordering::Equal));
881
882        // Integer 3 vs Float 3.5 — Integer is less.
883        let i = SqliteValue::Integer(3);
884        let f = SqliteValue::Float(3.5);
885        assert_eq!(i.partial_cmp(&f), Some(Ordering::Less));
886        assert_eq!(f.partial_cmp(&i), Some(Ordering::Greater));
887    }
888
889    #[test]
890    fn from_conversions() {
891        assert_eq!(SqliteValue::from(42i64).as_integer(), Some(42));
892        assert_eq!(SqliteValue::from(42i32).as_integer(), Some(42));
893        assert_eq!(SqliteValue::from(1.5f64).as_float(), Some(1.5));
894        assert_eq!(SqliteValue::from("hello").as_text(), Some("hello"));
895        assert_eq!(
896            SqliteValue::from(String::from("world")).as_text(),
897            Some("world")
898        );
899        assert_eq!(SqliteValue::from(vec![1u8, 2]).as_blob(), Some(&[1, 2][..]));
900        assert!(SqliteValue::from(None::<i64>).is_null());
901        assert_eq!(SqliteValue::from(Some(42i64)).as_integer(), Some(42));
902    }
903
904    #[test]
905    fn affinity() {
906        assert_eq!(SqliteValue::Null.affinity(), TypeAffinity::Blob);
907        assert_eq!(SqliteValue::Integer(0).affinity(), TypeAffinity::Integer);
908        assert_eq!(SqliteValue::Float(0.0).affinity(), TypeAffinity::Real);
909        assert_eq!(
910            SqliteValue::Text(String::new()).affinity(),
911            TypeAffinity::Text
912        );
913        assert_eq!(SqliteValue::Blob(vec![]).affinity(), TypeAffinity::Blob);
914    }
915
916    #[test]
917    fn null_equality() {
918        // In SQLite, NULL == NULL is false, but for sorting they are equal
919        let a = SqliteValue::Null;
920        let b = SqliteValue::Null;
921        assert_eq!(a.partial_cmp(&b), Some(Ordering::Equal));
922    }
923
924    // ── bd-13r.1: Type Affinity Advisory + STRICT Enforcement ──
925
926    #[test]
927    fn test_storage_class_variants() {
928        assert_eq!(SqliteValue::Null.storage_class(), StorageClass::Null);
929        assert_eq!(
930            SqliteValue::Integer(42).storage_class(),
931            StorageClass::Integer
932        );
933        assert_eq!(SqliteValue::Float(3.14).storage_class(), StorageClass::Real);
934        assert_eq!(
935            SqliteValue::Text("hi".into()).storage_class(),
936            StorageClass::Text
937        );
938        assert_eq!(
939            SqliteValue::Blob(vec![1]).storage_class(),
940            StorageClass::Blob
941        );
942    }
943
944    #[test]
945    fn test_type_affinity_advisory_text_into_integer_ok() {
946        // INSERT TEXT "hello" into INTEGER-affinity column: text stays as text
947        // (not a well-formed numeric literal).
948        let val = SqliteValue::Text("hello".into());
949        let coerced = val.apply_affinity(TypeAffinity::Integer);
950        assert!(coerced.as_text().is_some());
951        assert_eq!(coerced.as_text().unwrap(), "hello");
952
953        // INSERT TEXT "42" into INTEGER-affinity column: coerced to integer.
954        let val = SqliteValue::Text("42".into());
955        let coerced = val.apply_affinity(TypeAffinity::Integer);
956        assert_eq!(coerced.as_integer(), Some(42));
957    }
958
959    #[test]
960    fn test_type_affinity_advisory_integer_into_text_ok() {
961        // INSERT INTEGER 42 into TEXT-affinity column: coerced to text "42".
962        let val = SqliteValue::Integer(42);
963        let coerced = val.apply_affinity(TypeAffinity::Text);
964        assert_eq!(coerced.as_text(), Some("42"));
965    }
966
967    #[test]
968    fn test_type_affinity_comparison_coercion_matches_oracle() {
969        // NUMERIC affinity coerces text "123" to integer.
970        let val = SqliteValue::Text("123".into());
971        let coerced = val.apply_affinity(TypeAffinity::Numeric);
972        assert_eq!(coerced.as_integer(), Some(123));
973
974        // NUMERIC affinity coerces text "3.14" to real.
975        let val = SqliteValue::Text("3.14".into());
976        let coerced = val.apply_affinity(TypeAffinity::Numeric);
977        assert_eq!(coerced.as_float(), Some(3.14));
978
979        // NUMERIC affinity leaves text "hello" as text.
980        let val = SqliteValue::Text("hello".into());
981        let coerced = val.apply_affinity(TypeAffinity::Numeric);
982        assert!(coerced.as_text().is_some());
983
984        // BLOB affinity never converts anything.
985        let val = SqliteValue::Integer(42);
986        let coerced = val.apply_affinity(TypeAffinity::Blob);
987        assert_eq!(coerced.as_integer(), Some(42));
988
989        // INTEGER affinity converts exact-integer floats to integer.
990        let val = SqliteValue::Float(5.0);
991        let coerced = val.apply_affinity(TypeAffinity::Integer);
992        assert_eq!(coerced.as_integer(), Some(5));
993
994        // INTEGER affinity keeps non-exact floats as float.
995        let val = SqliteValue::Float(5.5);
996        let coerced = val.apply_affinity(TypeAffinity::Integer);
997        assert_eq!(coerced.as_float(), Some(5.5));
998
999        // REAL affinity forces integers to float.
1000        let val = SqliteValue::Integer(7);
1001        let coerced = val.apply_affinity(TypeAffinity::Real);
1002        assert_eq!(coerced.as_float(), Some(7.0));
1003
1004        // REAL affinity coerces text "9" to float 9.0.
1005        let val = SqliteValue::Text("9".into());
1006        let coerced = val.apply_affinity(TypeAffinity::Real);
1007        assert_eq!(coerced.as_float(), Some(9.0));
1008    }
1009
1010    #[test]
1011    fn test_strict_table_rejects_text_into_integer() {
1012        let val = SqliteValue::Text("hello".into());
1013        let result = val.validate_strict(StrictColumnType::Integer);
1014        assert!(result.is_err());
1015        let err = result.unwrap_err();
1016        assert_eq!(err.expected, StrictColumnType::Integer);
1017        assert_eq!(err.actual, StorageClass::Text);
1018    }
1019
1020    #[test]
1021    fn test_strict_table_allows_exact_type() {
1022        // INTEGER into INTEGER column: ok.
1023        let val = SqliteValue::Integer(42);
1024        assert!(val.validate_strict(StrictColumnType::Integer).is_ok());
1025
1026        // REAL into REAL column: ok.
1027        let val = SqliteValue::Float(3.14);
1028        assert!(val.validate_strict(StrictColumnType::Real).is_ok());
1029
1030        // TEXT into TEXT column: ok.
1031        let val = SqliteValue::Text("hello".into());
1032        assert!(val.validate_strict(StrictColumnType::Text).is_ok());
1033
1034        // BLOB into BLOB column: ok.
1035        let val = SqliteValue::Blob(vec![1, 2, 3]);
1036        assert!(val.validate_strict(StrictColumnType::Blob).is_ok());
1037
1038        // NULL into any STRICT column: ok (nullability enforced separately).
1039        assert!(
1040            SqliteValue::Null
1041                .validate_strict(StrictColumnType::Integer)
1042                .is_ok()
1043        );
1044        assert!(
1045            SqliteValue::Null
1046                .validate_strict(StrictColumnType::Text)
1047                .is_ok()
1048        );
1049
1050        // ANY accepts everything.
1051        let val = SqliteValue::Integer(42);
1052        assert!(val.validate_strict(StrictColumnType::Any).is_ok());
1053        let val = SqliteValue::Text("hi".into());
1054        assert!(val.validate_strict(StrictColumnType::Any).is_ok());
1055    }
1056
1057    #[test]
1058    fn test_strict_real_accepts_integer_with_coercion() {
1059        // STRICT REAL column accepts INTEGER and coerces to float.
1060        let val = SqliteValue::Integer(42);
1061        let result = val.validate_strict(StrictColumnType::Real).unwrap();
1062        assert_eq!(result.as_float(), Some(42.0));
1063    }
1064
1065    #[test]
1066    fn test_strict_rejects_wrong_storage_classes() {
1067        // REAL into INTEGER column: rejected.
1068        assert!(
1069            SqliteValue::Float(3.14)
1070                .validate_strict(StrictColumnType::Integer)
1071                .is_err()
1072        );
1073
1074        // BLOB into TEXT column: rejected.
1075        assert!(
1076            SqliteValue::Blob(vec![1])
1077                .validate_strict(StrictColumnType::Text)
1078                .is_err()
1079        );
1080
1081        // INTEGER into TEXT column: rejected.
1082        assert!(
1083            SqliteValue::Integer(1)
1084                .validate_strict(StrictColumnType::Text)
1085                .is_err()
1086        );
1087
1088        // TEXT into BLOB column: rejected.
1089        assert!(
1090            SqliteValue::Text("x".into())
1091                .validate_strict(StrictColumnType::Blob)
1092                .is_err()
1093        );
1094    }
1095
1096    #[test]
1097    fn test_strict_column_type_parsing() {
1098        assert_eq!(
1099            StrictColumnType::from_type_name("INT"),
1100            Some(StrictColumnType::Integer)
1101        );
1102        assert_eq!(
1103            StrictColumnType::from_type_name("INTEGER"),
1104            Some(StrictColumnType::Integer)
1105        );
1106        assert_eq!(
1107            StrictColumnType::from_type_name("REAL"),
1108            Some(StrictColumnType::Real)
1109        );
1110        assert_eq!(
1111            StrictColumnType::from_type_name("TEXT"),
1112            Some(StrictColumnType::Text)
1113        );
1114        assert_eq!(
1115            StrictColumnType::from_type_name("BLOB"),
1116            Some(StrictColumnType::Blob)
1117        );
1118        assert_eq!(
1119            StrictColumnType::from_type_name("ANY"),
1120            Some(StrictColumnType::Any)
1121        );
1122        // Invalid type name in STRICT mode.
1123        assert_eq!(StrictColumnType::from_type_name("VARCHAR(255)"), None);
1124        assert_eq!(StrictColumnType::from_type_name("NUMERIC"), None);
1125    }
1126
1127    #[test]
1128    fn test_affinity_advisory_never_rejects() {
1129        // Advisory affinity NEVER rejects a value. All combinations must succeed.
1130        let values = vec![
1131            SqliteValue::Null,
1132            SqliteValue::Integer(42),
1133            SqliteValue::Float(3.14),
1134            SqliteValue::Text("hello".into()),
1135            SqliteValue::Blob(vec![0xDE, 0xAD]),
1136        ];
1137        let affinities = [
1138            TypeAffinity::Integer,
1139            TypeAffinity::Text,
1140            TypeAffinity::Blob,
1141            TypeAffinity::Real,
1142            TypeAffinity::Numeric,
1143        ];
1144        for val in &values {
1145            for aff in &affinities {
1146                // apply_affinity is infallible - it always returns a value.
1147                let _ = val.clone().apply_affinity(*aff);
1148            }
1149        }
1150    }
1151
1152    // ── bd-13r.2: UNIQUE NULL Semantics (NULL != NULL) ──
1153
1154    #[test]
1155    fn test_unique_allows_multiple_nulls_single_column() {
1156        // In UNIQUE columns, NULL != NULL: two NULLs are never duplicates.
1157        let a = SqliteValue::Null;
1158        let b = SqliteValue::Null;
1159        assert!(!a.unique_eq(&b));
1160    }
1161
1162    #[test]
1163    fn test_unique_allows_multiple_nulls_multi_column_partial_null() {
1164        // UNIQUE(a,b): (NULL,1) and (NULL,1) are NOT duplicates because
1165        // any NULL component makes the whole key non-duplicate.
1166        let row_a = [SqliteValue::Null, SqliteValue::Integer(1)];
1167        let row_b = [SqliteValue::Null, SqliteValue::Integer(1)];
1168        assert!(!unique_key_duplicates(&row_a, &row_b));
1169
1170        // UNIQUE(a,b): (1,NULL) and (1,NULL) are NOT duplicates.
1171        let row_a = [SqliteValue::Integer(1), SqliteValue::Null];
1172        let row_b = [SqliteValue::Integer(1), SqliteValue::Null];
1173        assert!(!unique_key_duplicates(&row_a, &row_b));
1174
1175        // UNIQUE(a,b): (NULL,NULL) and (NULL,NULL) are NOT duplicates.
1176        let row_a = [SqliteValue::Null, SqliteValue::Null];
1177        let row_b = [SqliteValue::Null, SqliteValue::Null];
1178        assert!(!unique_key_duplicates(&row_a, &row_b));
1179    }
1180
1181    #[test]
1182    fn test_unique_rejects_duplicate_non_null() {
1183        // Two identical non-NULL values ARE duplicates.
1184        let a = SqliteValue::Integer(42);
1185        let b = SqliteValue::Integer(42);
1186        assert!(a.unique_eq(&b));
1187
1188        // Composite: (1, "hello") and (1, "hello") ARE duplicates.
1189        let row_a = [SqliteValue::Integer(1), SqliteValue::Text("hello".into())];
1190        let row_b = [SqliteValue::Integer(1), SqliteValue::Text("hello".into())];
1191        assert!(unique_key_duplicates(&row_a, &row_b));
1192
1193        // Different values are NOT duplicates.
1194        let row_a = [SqliteValue::Integer(1), SqliteValue::Text("hello".into())];
1195        let row_b = [SqliteValue::Integer(1), SqliteValue::Text("world".into())];
1196        assert!(!unique_key_duplicates(&row_a, &row_b));
1197    }
1198
1199    #[test]
1200    fn test_unique_null_vs_non_null_distinct() {
1201        // NULL and a non-NULL value are never duplicates.
1202        let a = SqliteValue::Null;
1203        let b = SqliteValue::Integer(1);
1204        assert!(!a.unique_eq(&b));
1205        assert!(!b.unique_eq(&a));
1206
1207        // Composite: (NULL, 1) and (2, 1) are not duplicates (different first element).
1208        let row_a = [SqliteValue::Null, SqliteValue::Integer(1)];
1209        let row_b = [SqliteValue::Integer(2), SqliteValue::Integer(1)];
1210        assert!(!unique_key_duplicates(&row_a, &row_b));
1211    }
1212
1213    // ── bd-13r.4: Integer Overflow Semantics (Expr vs sum()) ──
1214
1215    #[test]
1216    #[allow(clippy::cast_precision_loss)]
1217    fn test_integer_overflow_promotes_real_expr_add() {
1218        let max = SqliteValue::Integer(i64::MAX);
1219        let one = SqliteValue::Integer(1);
1220        let result = max.sql_add(&one);
1221        // Overflow promotes to REAL (not integer).
1222        assert!(result.as_integer().is_none());
1223        assert!(result.as_float().is_some());
1224        // The float value is approximately i64::MAX + 1.
1225        assert!(result.as_float().unwrap() >= i64::MAX as f64);
1226    }
1227
1228    #[test]
1229    fn test_integer_overflow_promotes_real_expr_mul() {
1230        let max = SqliteValue::Integer(i64::MAX);
1231        let two = SqliteValue::Integer(2);
1232        let result = max.sql_mul(&two);
1233        // Overflow promotes to REAL.
1234        assert!(result.as_float().is_some());
1235    }
1236
1237    #[test]
1238    fn test_integer_overflow_promotes_real_expr_sub() {
1239        let min = SqliteValue::Integer(i64::MIN);
1240        let one = SqliteValue::Integer(1);
1241        let result = min.sql_sub(&one);
1242        // Underflow promotes to REAL.
1243        assert!(result.as_float().is_some());
1244    }
1245
1246    #[test]
1247    fn test_sum_overflow_errors() {
1248        let mut acc = SumAccumulator::new();
1249        acc.accumulate(&SqliteValue::Integer(i64::MAX));
1250        acc.accumulate(&SqliteValue::Integer(1));
1251        let result = acc.finish();
1252        assert!(result.is_err());
1253    }
1254
1255    #[test]
1256    fn test_no_overflow_stays_integer() {
1257        // Non-overflow addition stays INTEGER.
1258        let a = SqliteValue::Integer(100);
1259        let b = SqliteValue::Integer(200);
1260        let result = a.sql_add(&b);
1261        assert_eq!(result.as_integer(), Some(300));
1262
1263        // Non-overflow multiplication stays INTEGER.
1264        let result = SqliteValue::Integer(7).sql_mul(&SqliteValue::Integer(6));
1265        assert_eq!(result.as_integer(), Some(42));
1266
1267        // Non-overflow subtraction stays INTEGER.
1268        let result = SqliteValue::Integer(50).sql_sub(&SqliteValue::Integer(8));
1269        assert_eq!(result.as_integer(), Some(42));
1270    }
1271
1272    #[test]
1273    fn test_sum_null_only_returns_null() {
1274        let mut acc = SumAccumulator::new();
1275        acc.accumulate(&SqliteValue::Null);
1276        acc.accumulate(&SqliteValue::Null);
1277        let result = acc.finish().unwrap();
1278        assert!(result.is_null());
1279    }
1280
1281    #[test]
1282    fn test_sum_mixed_int_float() {
1283        let mut acc = SumAccumulator::new();
1284        acc.accumulate(&SqliteValue::Integer(10));
1285        acc.accumulate(&SqliteValue::Float(2.5));
1286        acc.accumulate(&SqliteValue::Integer(3));
1287        let result = acc.finish().unwrap();
1288        // Once float is seen, result is float.
1289        assert_eq!(result.as_float(), Some(15.5));
1290    }
1291
1292    #[test]
1293    fn test_sum_integer_only() {
1294        let mut acc = SumAccumulator::new();
1295        acc.accumulate(&SqliteValue::Integer(10));
1296        acc.accumulate(&SqliteValue::Integer(20));
1297        acc.accumulate(&SqliteValue::Integer(30));
1298        let result = acc.finish().unwrap();
1299        assert_eq!(result.as_integer(), Some(60));
1300    }
1301
1302    #[test]
1303    fn test_sql_arithmetic_null_propagation() {
1304        let n = SqliteValue::Null;
1305        let i = SqliteValue::Integer(42);
1306        assert!(n.sql_add(&i).is_null());
1307        assert!(i.sql_add(&n).is_null());
1308        assert!(n.sql_sub(&i).is_null());
1309        assert!(n.sql_mul(&i).is_null());
1310    }
1311
1312    #[test]
1313    fn test_sql_inf_arithmetic_nan_normalized_to_null() {
1314        // +Inf + (-Inf) is NaN in IEEE-754 and must be normalized to NULL.
1315        let pos_inf = SqliteValue::Float(f64::INFINITY);
1316        let neg_inf = SqliteValue::Float(f64::NEG_INFINITY);
1317        assert!(pos_inf.sql_add(&neg_inf).is_null());
1318
1319        // +Inf - +Inf is also NaN and must normalize to NULL.
1320        assert!(pos_inf.sql_sub(&pos_inf).is_null());
1321    }
1322
1323    #[test]
1324    fn test_sql_mul_zero_times_inf_normalized_to_null() {
1325        // 0 * +Inf is NaN in IEEE-754 and must be normalized to NULL.
1326        let zero = SqliteValue::Float(0.0);
1327        let pos_inf = SqliteValue::Float(f64::INFINITY);
1328        assert!(zero.sql_mul(&pos_inf).is_null());
1329    }
1330
1331    #[test]
1332    fn test_sql_inf_propagates_when_not_nan() {
1333        let pos_inf = SqliteValue::Float(f64::INFINITY);
1334        let one = SqliteValue::Integer(1);
1335        let add_result = pos_inf.sql_add(&one);
1336        assert!(
1337            matches!(add_result, SqliteValue::Float(v) if v.is_infinite() && v.is_sign_positive()),
1338            "expected +Inf propagation, got {add_result:?}"
1339        );
1340
1341        let neg_inf = SqliteValue::Float(f64::NEG_INFINITY);
1342        let sub_result = neg_inf.sql_sub(&one);
1343        assert!(
1344            matches!(sub_result, SqliteValue::Float(v) if v.is_infinite() && v.is_sign_negative()),
1345            "expected -Inf propagation, got {sub_result:?}"
1346        );
1347    }
1348
1349    #[test]
1350    fn test_from_f64_nan_normalizes_to_null() {
1351        let value = SqliteValue::from(f64::NAN);
1352        assert!(value.is_null());
1353    }
1354
1355    #[test]
1356    fn test_inf_comparisons_against_finite_values() {
1357        let pos_inf = SqliteValue::Float(f64::INFINITY);
1358        let neg_inf = SqliteValue::Float(f64::NEG_INFINITY);
1359        let finite_hi = SqliteValue::Float(1.0e308);
1360        let finite_lo = SqliteValue::Float(-1.0e308);
1361
1362        assert_eq!(pos_inf.partial_cmp(&finite_hi), Some(Ordering::Greater));
1363        assert_eq!(neg_inf.partial_cmp(&finite_lo), Some(Ordering::Less));
1364    }
1365
1366    // ── bd-13r.7: Empty String vs NULL Semantics ──
1367
1368    #[test]
1369    fn test_empty_string_is_not_null() {
1370        let empty = SqliteValue::Text(String::new());
1371        // '' IS NULL → false.
1372        assert!(!empty.is_null());
1373        // '' IS NOT NULL → true (expressed as !is_null).
1374        assert!(!empty.is_null());
1375        // NULL IS NULL → true.
1376        assert!(SqliteValue::Null.is_null());
1377    }
1378
1379    #[test]
1380    fn test_length_empty_string_zero() {
1381        let empty = SqliteValue::Text(String::new());
1382        assert_eq!(empty.sql_length(), Some(0));
1383    }
1384
1385    #[test]
1386    fn test_typeof_empty_string_text() {
1387        let empty = SqliteValue::Text(String::new());
1388        assert_eq!(empty.typeof_str(), "text");
1389        // NULL has typeof "null".
1390        assert_eq!(SqliteValue::Null.typeof_str(), "null");
1391    }
1392
1393    #[test]
1394    fn test_empty_string_comparisons() {
1395        let empty1 = SqliteValue::Text(String::new());
1396        let empty2 = SqliteValue::Text(String::new());
1397        // '' = '' → true.
1398        assert_eq!(empty1.partial_cmp(&empty2), Some(std::cmp::Ordering::Equal));
1399
1400        // '' = NULL → NULL (comparison with NULL yields None/unknown).
1401        // In our PartialOrd, NULL and TEXT are different sort classes,
1402        // so NULL < TEXT (they are not equal).
1403        let null = SqliteValue::Null;
1404        assert_ne!(empty1.partial_cmp(&null), Some(std::cmp::Ordering::Equal));
1405    }
1406
1407    #[test]
1408    fn test_typeof_all_variants() {
1409        assert_eq!(SqliteValue::Null.typeof_str(), "null");
1410        assert_eq!(SqliteValue::Integer(0).typeof_str(), "integer");
1411        assert_eq!(SqliteValue::Float(0.0).typeof_str(), "real");
1412        assert_eq!(SqliteValue::Text("x".into()).typeof_str(), "text");
1413        assert_eq!(SqliteValue::Blob(vec![]).typeof_str(), "blob");
1414    }
1415
1416    #[test]
1417    fn test_sql_length_all_types() {
1418        // NULL → NULL (None).
1419        assert_eq!(SqliteValue::Null.sql_length(), None);
1420        // TEXT → character count.
1421        assert_eq!(SqliteValue::Text("hello".into()).sql_length(), Some(5));
1422        assert_eq!(SqliteValue::Text(String::new()).sql_length(), Some(0));
1423        // BLOB → byte count.
1424        assert_eq!(SqliteValue::Blob(vec![1, 2, 3]).sql_length(), Some(3));
1425        // INTEGER → length of text representation.
1426        assert_eq!(SqliteValue::Integer(42).sql_length(), Some(2));
1427        // REAL → length of text representation.
1428        assert_eq!(SqliteValue::Float(3.14).sql_length(), Some(4)); // "3.14"
1429    }
1430
1431    // ── bd-13r.6: LIKE Semantics (ASCII-only case folding) ──
1432
1433    #[test]
1434    fn test_like_ascii_case_insensitive() {
1435        assert!(sql_like("A", "a", None));
1436        assert!(sql_like("a", "A", None));
1437        assert!(sql_like("hello", "HELLO", None));
1438        assert!(sql_like("HELLO", "hello", None));
1439        assert!(sql_like("HeLLo", "hEllO", None));
1440    }
1441
1442    #[test]
1443    fn test_like_unicode_case_sensitive_without_icu() {
1444        // Without ICU, Unicode case folding does NOT occur.
1445        assert!(!sql_like("ä", "Ä", None));
1446        assert!(!sql_like("Ä", "ä", None));
1447        // But exact match works.
1448        assert!(sql_like("ä", "ä", None));
1449    }
1450
1451    #[test]
1452    fn test_like_escape_handling() {
1453        // Escape literal % with backslash.
1454        assert!(sql_like("100\\%", "100%", Some('\\')));
1455        assert!(!sql_like("100\\%", "100x", Some('\\')));
1456
1457        // Escape literal _.
1458        assert!(sql_like("a\\_b", "a_b", Some('\\')));
1459        assert!(!sql_like("a\\_b", "axb", Some('\\')));
1460    }
1461
1462    #[test]
1463    fn test_like_wildcards_basic() {
1464        // % matches zero or more characters.
1465        assert!(sql_like("%", "", None));
1466        assert!(sql_like("%", "anything", None));
1467        assert!(sql_like("a%", "abc", None));
1468        assert!(sql_like("%c", "abc", None));
1469        assert!(sql_like("a%c", "abc", None));
1470        assert!(sql_like("a%c", "aXYZc", None));
1471        assert!(!sql_like("a%c", "abd", None));
1472
1473        // _ matches exactly one character.
1474        assert!(sql_like("_", "x", None));
1475        assert!(!sql_like("_", "", None));
1476        assert!(!sql_like("_", "xy", None));
1477        assert!(sql_like("a_c", "abc", None));
1478        assert!(!sql_like("a_c", "abbc", None));
1479    }
1480
1481    #[test]
1482    fn test_like_combined_wildcards() {
1483        assert!(sql_like("%_", "a", None));
1484        assert!(!sql_like("%_", "", None));
1485        assert!(sql_like("_%_", "ab", None));
1486        assert!(!sql_like("_%_", "a", None));
1487        assert!(sql_like("%a%b%", "xaybz", None));
1488        assert!(!sql_like("%a%b%", "xyz", None));
1489    }
1490
1491    #[test]
1492    fn test_like_exact_match() {
1493        assert!(sql_like("hello", "hello", None));
1494        assert!(!sql_like("hello", "world", None));
1495        assert!(sql_like("", "", None));
1496        assert!(!sql_like("a", "", None));
1497        assert!(!sql_like("", "a", None));
1498    }
1499}