Skip to main content

graphitesql/
value.rs

1//! The dynamic value model and SQLite "serial types".
2//!
3//! Every cell in a SQLite table or index stores a *record*: a header of serial
4//! type codes (varints) followed by the concatenated value bodies. This module
5//! models the five SQLite storage classes ([`Value`]) and the serial-type codes
6//! ([`SerialType`]) that describe how each value is laid out on disk.
7//!
8//! Record (de)serialization itself lives in the `format` module and builds on
9//! these types; keeping the value model here lets the rest of the engine reason
10//! about values without pulling in disk-format details.
11
12use alloc::string::String;
13use alloc::vec::Vec;
14use core::cmp::Ordering;
15
16/// A value's storage class, owning its data.
17///
18/// These are the five SQLite storage classes. Note that SQLite stores `BOOLEAN`
19/// as integers and has no separate date/time class — those are conventions on
20/// top of these five.
21#[derive(Debug, Clone, PartialEq)]
22pub enum Value {
23    /// SQL `NULL`.
24    Null,
25    /// A signed 64-bit integer.
26    Integer(i64),
27    /// An IEEE-754 double.
28    Real(f64),
29    /// A UTF-8 text value. (SQLite also supports UTF-16; graphitesql stores
30    /// text as UTF-8 internally and converts at the boundary.)
31    Text(String),
32    /// A binary blob.
33    Blob(Vec<u8>),
34}
35
36/// A borrowed view of a [`Value`], used on hot decode paths to avoid copying.
37#[derive(Debug, Clone, Copy, PartialEq)]
38pub enum ValueRef<'a> {
39    /// SQL `NULL`.
40    Null,
41    /// A signed 64-bit integer.
42    Integer(i64),
43    /// An IEEE-754 double.
44    Real(f64),
45    /// Borrowed UTF-8 text.
46    Text(&'a str),
47    /// Borrowed binary blob.
48    Blob(&'a [u8]),
49}
50
51impl ValueRef<'_> {
52    /// Copy this borrowed value into an owned [`Value`].
53    pub fn to_owned(&self) -> Value {
54        match *self {
55            ValueRef::Null => Value::Null,
56            ValueRef::Integer(i) => Value::Integer(i),
57            ValueRef::Real(r) => Value::Real(r),
58            ValueRef::Text(s) => Value::Text(String::from(s)),
59            ValueRef::Blob(b) => Value::Blob(Vec::from(b)),
60        }
61    }
62}
63
64/// A text collating sequence. `BINARY` (the default) compares bytes; `NOCASE`
65/// folds ASCII letters; `RTRIM` ignores trailing spaces. Collations only affect
66/// text-vs-text comparison; storage-class ordering is unchanged.
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
68pub enum Collation {
69    /// `BINARY` — `memcmp` on the UTF-8 bytes.
70    #[default]
71    Binary,
72    /// `NOCASE` — ASCII case-insensitive.
73    NoCase,
74    /// `RTRIM` — like `BINARY` but trailing spaces are ignored.
75    RTrim,
76}
77
78impl Collation {
79    /// Parse a collation name (`BINARY`/`NOCASE`/`RTRIM`, case-insensitive).
80    pub fn parse(name: &str) -> Option<Collation> {
81        match name.to_ascii_lowercase().as_str() {
82            "binary" => Some(Collation::Binary),
83            "nocase" => Some(Collation::NoCase),
84            "rtrim" => Some(Collation::RTrim),
85            _ => None,
86        }
87    }
88}
89
90/// Compare two text strings under `coll`.
91pub fn cmp_text(x: &str, y: &str, coll: Collation) -> Ordering {
92    match coll {
93        Collation::Binary => x.as_bytes().cmp(y.as_bytes()),
94        Collation::NoCase => x
95            .bytes()
96            .map(|b| b.to_ascii_uppercase())
97            .cmp(y.bytes().map(|b| b.to_ascii_uppercase())),
98        Collation::RTrim => x
99            .trim_end_matches(' ')
100            .as_bytes()
101            .cmp(y.trim_end_matches(' ').as_bytes()),
102    }
103}
104
105/// Like [`cmp_values`] but applying `coll` to text-vs-text comparison.
106pub fn cmp_values_coll(a: &Value, b: &Value, coll: Collation) -> Ordering {
107    match (a, b) {
108        (Value::Text(x), Value::Text(y)) => cmp_text(x, y, coll),
109        _ => cmp_values(a, b),
110    }
111}
112
113/// Compare two values in SQLite's total ordering: `NULL` < numbers < text <
114/// blobs; numbers compared numerically, text by byte (the `BINARY` collation),
115/// blobs by `memcmp`. This is the order used for index keys, `ORDER BY`, and
116/// comparisons (collation refinements are layered on top elsewhere).
117pub fn cmp_values(a: &Value, b: &Value) -> Ordering {
118    fn class(v: &Value) -> u8 {
119        match v {
120            Value::Null => 0,
121            Value::Integer(_) | Value::Real(_) => 1,
122            Value::Text(_) => 2,
123            Value::Blob(_) => 3,
124        }
125    }
126    match (a, b) {
127        (Value::Null, Value::Null) => Ordering::Equal,
128        // Two integers compare exactly as `i64`; coercing both through `f64`
129        // (as this used to) collapses values above 2^53 — e.g. `10^16` and
130        // `10^16 + 1` would wrongly read equal.
131        (Value::Integer(x), Value::Integer(y)) => x.cmp(y),
132        (Value::Real(x), Value::Real(y)) => x.partial_cmp(y).unwrap_or(Ordering::Equal),
133        // A mixed integer/real comparison uses SQLite's exact algorithm, which
134        // never loses the integer's low bits to a lossy `f64` round-trip.
135        (Value::Integer(i), Value::Real(r)) => int_float_cmp(*i, *r),
136        (Value::Real(r), Value::Integer(i)) => int_float_cmp(*i, *r).reverse(),
137        (Value::Text(x), Value::Text(y)) => x.as_bytes().cmp(y.as_bytes()),
138        (Value::Blob(x), Value::Blob(y)) => x.cmp(y),
139        _ => class(a).cmp(&class(b)),
140    }
141}
142
143/// Compare an `i64` with an `f64` exactly, mirroring SQLite's
144/// `sqlite3IntFloatCompare` (the 8-byte-`double` branch). Returns the ordering
145/// of `i` relative to `r`. The naive `i as f64` comparison loses precision once
146/// `|i| > 2^53`; this truncates the real toward zero, compares integer parts
147/// first, then disambiguates an equal-integer-part tie by the real's fraction.
148fn int_float_cmp(i: i64, r: f64) -> Ordering {
149    if r.is_nan() {
150        // SQLite never stores a NaN (it becomes NULL); match the prior
151        // `partial_cmp(..).unwrap_or(Equal)` fallback defensively.
152        return Ordering::Equal;
153    }
154    // `r` entirely outside the `i64` range: any finite integer is on the near
155    // side. (`2^63` is not representable as `i64`, so the upper bound is `>=`.)
156    if r < -9_223_372_036_854_775_808.0 {
157        return Ordering::Greater;
158    }
159    if r >= 9_223_372_036_854_775_808.0 {
160        return Ordering::Less;
161    }
162    let y = r as i64; // truncates toward zero; exact since `r` is in range
163    match i.cmp(&y) {
164        Ordering::Equal => (i as f64).partial_cmp(&r).unwrap_or(Ordering::Equal),
165        other => other,
166    }
167}
168
169/// A SQLite record serial type code.
170///
171/// The mapping from code to meaning (file-format spec, "Serial Type Codes Of
172/// The Record Format"):
173///
174/// | code | meaning | body bytes |
175/// |------|---------|------------|
176/// | 0 | NULL | 0 |
177/// | 1 | int, big-endian | 1 |
178/// | 2 | int | 2 |
179/// | 3 | int | 3 |
180/// | 4 | int | 4 |
181/// | 5 | int | 6 |
182/// | 6 | int | 8 |
183/// | 7 | IEEE-754 float | 8 |
184/// | 8 | integer 0 | 0 |
185/// | 9 | integer 1 | 0 |
186/// | 10, 11 | reserved | — |
187/// | N≥12 even | BLOB | (N-12)/2 |
188/// | N≥13 odd | TEXT | (N-13)/2 |
189#[derive(Debug, Clone, Copy, PartialEq, Eq)]
190pub struct SerialType(pub u64);
191
192impl SerialType {
193    /// Number of bytes this serial type occupies in the record body, or `None`
194    /// for the reserved codes 10 and 11.
195    pub fn content_len(self) -> Option<usize> {
196        Some(match self.0 {
197            0 | 8 | 9 => 0,
198            1 => 1,
199            2 => 2,
200            3 => 3,
201            4 => 4,
202            5 => 6,
203            6 | 7 => 8,
204            10 | 11 => return None,
205            n if n % 2 == 0 => ((n - 12) / 2) as usize,
206            n => ((n - 13) / 2) as usize,
207        })
208    }
209
210    /// The smallest serial type that can losslessly represent `value`.
211    ///
212    /// This matches SQLite's choice: small integers collapse to the 0/1 literals
213    /// (codes 8/9) and otherwise to the narrowest of the 1/2/3/4/6/8-byte forms.
214    pub fn for_value(value: &Value) -> SerialType {
215        SerialType(match value {
216            Value::Null => 0,
217            Value::Integer(0) => 8,
218            Value::Integer(1) => 9,
219            Value::Integer(i) => {
220                let i = *i;
221                if (-0x80..=0x7f).contains(&i) {
222                    1
223                } else if (-0x8000..=0x7fff).contains(&i) {
224                    2
225                } else if (-0x80_0000..=0x7f_ffff).contains(&i) {
226                    3
227                } else if (-0x8000_0000..=0x7fff_ffff).contains(&i) {
228                    4
229                } else if (-0x8000_0000_0000..=0x7fff_ffff_ffff).contains(&i) {
230                    5
231                } else {
232                    6
233                }
234            }
235            Value::Real(_) => 7,
236            Value::Blob(b) => 12 + 2 * b.len() as u64,
237            Value::Text(s) => 13 + 2 * s.len() as u64,
238        })
239    }
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245    use alloc::string::ToString;
246    use alloc::vec;
247
248    #[test]
249    fn content_lengths() {
250        assert_eq!(SerialType(0).content_len(), Some(0));
251        assert_eq!(SerialType(1).content_len(), Some(1));
252        assert_eq!(SerialType(5).content_len(), Some(6));
253        assert_eq!(SerialType(6).content_len(), Some(8));
254        assert_eq!(SerialType(7).content_len(), Some(8));
255        assert_eq!(SerialType(8).content_len(), Some(0));
256        assert_eq!(SerialType(9).content_len(), Some(0));
257        assert_eq!(SerialType(10).content_len(), None);
258        assert_eq!(SerialType(11).content_len(), None);
259        // BLOB of 4 bytes -> 12 + 2*4 = 20.
260        assert_eq!(SerialType(20).content_len(), Some(4));
261        // TEXT of 5 bytes -> 13 + 2*5 = 23.
262        assert_eq!(SerialType(23).content_len(), Some(5));
263    }
264
265    #[test]
266    fn serial_type_selection_matches_sqlite() {
267        assert_eq!(SerialType::for_value(&Value::Null), SerialType(0));
268        assert_eq!(SerialType::for_value(&Value::Integer(0)), SerialType(8));
269        assert_eq!(SerialType::for_value(&Value::Integer(1)), SerialType(9));
270        assert_eq!(SerialType::for_value(&Value::Integer(2)), SerialType(1));
271        assert_eq!(SerialType::for_value(&Value::Integer(127)), SerialType(1));
272        assert_eq!(SerialType::for_value(&Value::Integer(128)), SerialType(2));
273        assert_eq!(SerialType::for_value(&Value::Integer(-1)), SerialType(1));
274        assert_eq!(
275            SerialType::for_value(&Value::Integer(i64::MAX)),
276            SerialType(6)
277        );
278        assert_eq!(SerialType::for_value(&Value::Real(1.5)), SerialType(7));
279        assert_eq!(
280            SerialType::for_value(&Value::Text("abc".to_string())),
281            SerialType(19) // 13 + 2*3
282        );
283        assert_eq!(
284            SerialType::for_value(&Value::Blob(vec![0u8; 4])),
285            SerialType(20) // 12 + 2*4
286        );
287    }
288
289    #[test]
290    fn value_ref_round_trips() {
291        assert_eq!(ValueRef::Integer(5).to_owned(), Value::Integer(5));
292        assert_eq!(ValueRef::Text("x").to_owned(), Value::Text("x".to_string()));
293    }
294}