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}