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::borrow::Cow;
13use alloc::string::String;
14use alloc::vec::Vec;
15use core::cmp::Ordering;
16
17/// A SQL text value — a byte string that is *usually* UTF-8.
18///
19/// SQLite text is a sequence of bytes (compared with `memcmp` under `BINARY`),
20/// and the vast majority of values are valid UTF-8. But a few operations produce
21/// text whose bytes are not — `x'ff' || x'00'` (concatenation coerces to text),
22/// `CAST(<blob> AS TEXT)`, or `char(…)` with lone surrogates. Storing the raw
23/// bytes (rather than a Rust `String`) lets those round-trip and report
24/// `typeof() = 'text'` exactly as SQLite does, instead of falling back to a blob.
25///
26/// [`Deref`](core::ops::Deref) gives ergonomic `&str` access for the common valid
27/// case; non-UTF-8 bytes read as the empty string through that path (a further
28/// niche edge), so anything that must be byte-exact — record encoding, its byte
29/// length, and comparison — uses [`as_bytes`](Text::as_bytes) /
30/// [`byte_len`](Text::byte_len) instead.
31#[derive(Clone, Default, Eq)]
32pub struct Text(Vec<u8>);
33
34impl Text {
35 /// The raw bytes (byte-exact; used for comparison and record encoding).
36 pub fn as_bytes(&self) -> &[u8] {
37 &self.0
38 }
39 /// The number of bytes (the record body length — not the character count).
40 pub fn byte_len(&self) -> usize {
41 self.0.len()
42 }
43 /// Whether the text is empty.
44 pub fn is_empty(&self) -> bool {
45 self.0.is_empty()
46 }
47 /// A `&str` view: the exact text for valid UTF-8, else the empty string.
48 pub fn as_str(&self) -> &str {
49 core::str::from_utf8(&self.0).unwrap_or("")
50 }
51 /// A lossy `&str` view replacing invalid bytes with U+FFFD (for display).
52 pub fn to_str_lossy(&self) -> Cow<'_, str> {
53 String::from_utf8_lossy(&self.0)
54 }
55 /// Take ownership of the underlying bytes.
56 pub fn into_bytes(self) -> Vec<u8> {
57 self.0
58 }
59 /// Wrap raw bytes as text without a UTF-8 check (the bytes may be non-UTF-8).
60 pub fn from_bytes(bytes: Vec<u8>) -> Self {
61 Text(bytes)
62 }
63}
64
65impl core::ops::Deref for Text {
66 type Target = str;
67 fn deref(&self) -> &str {
68 self.as_str()
69 }
70}
71
72impl core::fmt::Debug for Text {
73 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
74 // Preserve the `Text("abc")`-style rendering for valid UTF-8 (lossy for
75 // the rare non-UTF-8 value) so `{:?}` output stays stable.
76 core::fmt::Debug::fmt(&self.to_str_lossy(), f)
77 }
78}
79
80impl core::fmt::Display for Text {
81 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
82 f.write_str(&self.to_str_lossy())
83 }
84}
85
86impl From<String> for Text {
87 fn from(s: String) -> Self {
88 Text(s.into_bytes())
89 }
90}
91impl From<&str> for Text {
92 fn from(s: &str) -> Self {
93 Text(s.as_bytes().to_vec())
94 }
95}
96impl From<Cow<'_, str>> for Text {
97 fn from(s: Cow<'_, str>) -> Self {
98 Text(s.into_owned().into_bytes())
99 }
100}
101impl PartialEq for Text {
102 fn eq(&self, other: &Self) -> bool {
103 self.0 == other.0
104 }
105}
106impl PartialEq<str> for Text {
107 fn eq(&self, other: &str) -> bool {
108 self.0 == other.as_bytes()
109 }
110}
111impl PartialEq<&str> for Text {
112 fn eq(&self, other: &&str) -> bool {
113 self.0 == other.as_bytes()
114 }
115}
116impl PartialEq<String> for Text {
117 fn eq(&self, other: &String) -> bool {
118 self.0 == other.as_bytes()
119 }
120}
121impl PartialEq<Text> for String {
122 fn eq(&self, other: &Text) -> bool {
123 self.as_bytes() == other.as_bytes()
124 }
125}
126impl PartialEq<Text> for str {
127 fn eq(&self, other: &Text) -> bool {
128 self.as_bytes() == other.as_bytes()
129 }
130}
131
132/// A value's storage class, owning its data.
133///
134/// These are the five SQLite storage classes. Note that SQLite stores `BOOLEAN`
135/// as integers and has no separate date/time class — those are conventions on
136/// top of these five.
137#[derive(Debug, Clone, PartialEq)]
138pub enum Value {
139 /// SQL `NULL`.
140 Null,
141 /// A signed 64-bit integer.
142 Integer(i64),
143 /// An IEEE-754 double.
144 Real(f64),
145 /// A text value — usually UTF-8, but any byte string (see [`Text`]).
146 Text(Text),
147 /// A binary blob.
148 Blob(Vec<u8>),
149}
150
151/// A borrowed view of a [`Value`], used on hot decode paths to avoid copying.
152#[derive(Debug, Clone, Copy, PartialEq)]
153pub enum ValueRef<'a> {
154 /// SQL `NULL`.
155 Null,
156 /// A signed 64-bit integer.
157 Integer(i64),
158 /// An IEEE-754 double.
159 Real(f64),
160 /// Borrowed UTF-8 text.
161 Text(&'a str),
162 /// Borrowed binary blob.
163 Blob(&'a [u8]),
164}
165
166impl ValueRef<'_> {
167 /// Copy this borrowed value into an owned [`Value`].
168 pub fn to_owned(&self) -> Value {
169 match *self {
170 ValueRef::Null => Value::Null,
171 ValueRef::Integer(i) => Value::Integer(i),
172 ValueRef::Real(r) => Value::Real(r),
173 ValueRef::Text(s) => Value::Text(String::from(s).into()),
174 ValueRef::Blob(b) => Value::Blob(Vec::from(b)),
175 }
176 }
177}
178
179/// A text collating sequence. `BINARY` (the default) compares bytes; `NOCASE`
180/// folds ASCII letters; `RTRIM` ignores trailing spaces. `Custom` is an
181/// application-registered sequence (see [`Connection::register_collation`](crate::Connection::register_collation)), identified by a
182/// small id into a process-global registry so this enum stays `Copy`/`Send`/`Sync`.
183/// Collations only affect text-vs-text comparison; storage-class ordering is
184/// unchanged.
185#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
186pub enum Collation {
187 /// `BINARY` — `memcmp` on the UTF-8 bytes.
188 #[default]
189 Binary,
190 /// `NOCASE` — ASCII case-insensitive.
191 NoCase,
192 /// `RTRIM` — like `BINARY` but trailing spaces are ignored.
193 RTrim,
194 /// A user-registered collating sequence, by registry id. Only ever
195 /// constructed on `std` builds (via [`Connection::register_collation`](crate::Connection::register_collation)).
196 Custom(u32),
197}
198
199impl Collation {
200 /// Parse a *built-in* collation name (`BINARY`/`NOCASE`/`RTRIM`,
201 /// case-insensitive). Does not resolve custom collations — use the
202 /// crate-internal `resolve_collation_name` for that.
203 pub fn parse(name: &str) -> Option<Collation> {
204 match name.to_ascii_lowercase().as_str() {
205 "binary" => Some(Collation::Binary),
206 "nocase" => Some(Collation::NoCase),
207 "rtrim" => Some(Collation::RTrim),
208 _ => None,
209 }
210 }
211}
212
213/// Resolve a collation name to a [`Collation`], including application-registered
214/// custom collations. Returns `None` for an unknown name (the caller reports the
215/// usual `no such collation sequence` error). Built-ins resolve on every build;
216/// custom names resolve only on `std` builds.
217pub fn resolve_collation_name(name: &str) -> Option<Collation> {
218 if let Some(c) = Collation::parse(name) {
219 return Some(c);
220 }
221 #[cfg(feature = "std")]
222 {
223 registry::resolve_name(name).map(Collation::Custom)
224 }
225 #[cfg(not(feature = "std"))]
226 {
227 None
228 }
229}
230
231/// The name of a collation for schema/EXPLAIN reprinting (`BINARY`/`NOCASE`/
232/// `RTRIM`, or a custom sequence's registered name).
233pub fn collation_name(coll: Collation) -> alloc::string::String {
234 use alloc::string::ToString;
235 match coll {
236 Collation::Binary => "BINARY".to_string(),
237 Collation::NoCase => "NOCASE".to_string(),
238 Collation::RTrim => "RTRIM".to_string(),
239 Collation::Custom(id) => {
240 #[cfg(feature = "std")]
241 {
242 registry::name_of(id).unwrap_or_else(|| "BINARY".to_string())
243 }
244 #[cfg(not(feature = "std"))]
245 {
246 let _ = id;
247 "BINARY".to_string()
248 }
249 }
250 }
251}
252
253/// Register (or replace) a custom collating sequence `name`, callable as
254/// `COLLATE <name>` in SQL. `cmp` compares two text values. Requires `std`.
255///
256/// Re-registering an existing name replaces its comparison function. The
257/// registry is process-global and its entries are never reclaimed, so register
258/// each collation once at startup.
259#[cfg(feature = "std")]
260#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
261pub fn register_collation<F>(name: &str, cmp: F) -> u32
262where
263 F: Fn(&str, &str) -> Ordering + Send + 'static,
264{
265 registry::register(name, alloc::boxed::Box::new(cmp))
266}
267
268/// The process-global custom-collation registry. `std`-only (needs a global
269/// `Mutex`). Ids index `fns`; `by_name` maps a name to its id.
270#[cfg(feature = "std")]
271mod registry {
272 use super::Ordering;
273 use alloc::boxed::Box;
274 use alloc::collections::BTreeMap;
275 use alloc::string::{String, ToString};
276 use alloc::vec::Vec;
277 use std::sync::{Mutex, OnceLock};
278
279 type CollFn = Box<dyn Fn(&str, &str) -> Ordering + Send>;
280
281 struct Registry {
282 /// Lowercased name → id (collation names are case-insensitive).
283 by_name: BTreeMap<String, u32>,
284 /// id → original-case name (for schema/EXPLAIN reprinting).
285 names: Vec<String>,
286 /// id → comparison function.
287 fns: Vec<CollFn>,
288 }
289
290 fn registry() -> &'static Mutex<Registry> {
291 static REG: OnceLock<Mutex<Registry>> = OnceLock::new();
292 REG.get_or_init(|| {
293 Mutex::new(Registry {
294 by_name: BTreeMap::new(),
295 names: Vec::new(),
296 fns: Vec::new(),
297 })
298 })
299 }
300
301 pub(super) fn register(name: &str, f: CollFn) -> u32 {
302 let mut reg = registry().lock().unwrap();
303 let key = name.to_ascii_lowercase();
304 if let Some(&id) = reg.by_name.get(&key) {
305 reg.fns[id as usize] = f;
306 reg.names[id as usize] = name.to_string();
307 return id;
308 }
309 let id = reg.fns.len() as u32;
310 reg.fns.push(f);
311 reg.names.push(name.to_string());
312 reg.by_name.insert(key, id);
313 id
314 }
315
316 pub(super) fn resolve_name(name: &str) -> Option<u32> {
317 registry()
318 .lock()
319 .unwrap()
320 .by_name
321 .get(&name.to_ascii_lowercase())
322 .copied()
323 }
324
325 pub(super) fn name_of(id: u32) -> Option<String> {
326 registry().lock().unwrap().names.get(id as usize).cloned()
327 }
328
329 pub(super) fn compare(id: u32, x: &str, y: &str) -> Ordering {
330 let reg = registry().lock().unwrap();
331 match reg.fns.get(id as usize) {
332 Some(f) => f(x, y),
333 // Unregistered id (cannot happen via the public API): fall back to BINARY.
334 None => x.as_bytes().cmp(y.as_bytes()),
335 }
336 }
337}
338
339/// Compare two text strings under `coll`.
340pub fn cmp_text(x: &str, y: &str, coll: Collation) -> Ordering {
341 match coll {
342 Collation::Binary => x.as_bytes().cmp(y.as_bytes()),
343 // NOCASE folds ASCII letters to *lower* case, matching SQLite's
344 // `sqlite3UpperToLower[]` (used by `nocaseCollatingFunc`): only bytes
345 // 'A'..='Z' change (`c | 0x20`); everything else — including the
346 // punctuation bytes 0x5B..=0x60 (`[ \ ] ^ _ ` `) — is left as-is.
347 // Rust's `u8::to_ascii_lowercase` is exactly this ASCII table. Folding
348 // to *upper* case (as an earlier version did) inverted the order of
349 // keys mixing letters with those punctuation bytes, and — because
350 // NOCASE builds on-disk index keys — wrote NOCASE indexes in an order
351 // sqlite considers malformed. Equality is unaffected either way.
352 Collation::NoCase => x
353 .bytes()
354 .map(|b| b.to_ascii_lowercase())
355 .cmp(y.bytes().map(|b| b.to_ascii_lowercase())),
356 Collation::RTrim => x
357 .trim_end_matches(' ')
358 .as_bytes()
359 .cmp(y.trim_end_matches(' ').as_bytes()),
360 Collation::Custom(_id) => {
361 #[cfg(feature = "std")]
362 {
363 registry::compare(_id, x, y)
364 }
365 // In `no_std` builds a `Custom` collation can never be constructed
366 // (there is no registry), so this arm is unreachable; fall back to
367 // BINARY to keep the match exhaustive.
368 #[cfg(not(feature = "std"))]
369 {
370 x.as_bytes().cmp(y.as_bytes())
371 }
372 }
373 }
374}
375
376/// Like [`cmp_values`] but applying `coll` to text-vs-text comparison.
377pub fn cmp_values_coll(a: &Value, b: &Value, coll: Collation) -> Ordering {
378 match (a, b) {
379 (Value::Text(x), Value::Text(y)) => cmp_text(x, y, coll),
380 _ => cmp_values(a, b),
381 }
382}
383
384/// Compare two values in SQLite's total ordering: `NULL` < numbers < text <
385/// blobs; numbers compared numerically, text by byte (the `BINARY` collation),
386/// blobs by `memcmp`. This is the order used for index keys, `ORDER BY`, and
387/// comparisons (collation refinements are layered on top elsewhere).
388pub fn cmp_values(a: &Value, b: &Value) -> Ordering {
389 fn class(v: &Value) -> u8 {
390 match v {
391 Value::Null => 0,
392 Value::Integer(_) | Value::Real(_) => 1,
393 Value::Text(_) => 2,
394 Value::Blob(_) => 3,
395 }
396 }
397 match (a, b) {
398 (Value::Null, Value::Null) => Ordering::Equal,
399 // Two integers compare exactly as `i64`; coercing both through `f64`
400 // (as this used to) collapses values above 2^53 — e.g. `10^16` and
401 // `10^16 + 1` would wrongly read equal.
402 (Value::Integer(x), Value::Integer(y)) => x.cmp(y),
403 (Value::Real(x), Value::Real(y)) => x.partial_cmp(y).unwrap_or(Ordering::Equal),
404 // A mixed integer/real comparison uses SQLite's exact algorithm, which
405 // never loses the integer's low bits to a lossy `f64` round-trip.
406 (Value::Integer(i), Value::Real(r)) => int_float_cmp(*i, *r),
407 (Value::Real(r), Value::Integer(i)) => int_float_cmp(*i, *r).reverse(),
408 (Value::Text(x), Value::Text(y)) => x.as_bytes().cmp(y.as_bytes()),
409 (Value::Blob(x), Value::Blob(y)) => x.cmp(y),
410 _ => class(a).cmp(&class(b)),
411 }
412}
413
414/// Compare an `i64` with an `f64` exactly, mirroring SQLite's
415/// `sqlite3IntFloatCompare` (the 8-byte-`double` branch). Returns the ordering
416/// of `i` relative to `r`. The naive `i as f64` comparison loses precision once
417/// `|i| > 2^53`; this truncates the real toward zero, compares integer parts
418/// first, then disambiguates an equal-integer-part tie by the real's fraction.
419fn int_float_cmp(i: i64, r: f64) -> Ordering {
420 if r.is_nan() {
421 // SQLite never stores a NaN (it becomes NULL); match the prior
422 // `partial_cmp(..).unwrap_or(Equal)` fallback defensively.
423 return Ordering::Equal;
424 }
425 // `r` entirely outside the `i64` range: any finite integer is on the near
426 // side. (`2^63` is not representable as `i64`, so the upper bound is `>=`.)
427 if r < -9_223_372_036_854_775_808.0 {
428 return Ordering::Greater;
429 }
430 if r >= 9_223_372_036_854_775_808.0 {
431 return Ordering::Less;
432 }
433 let y = r as i64; // truncates toward zero; exact since `r` is in range
434 match i.cmp(&y) {
435 Ordering::Equal => (i as f64).partial_cmp(&r).unwrap_or(Ordering::Equal),
436 other => other,
437 }
438}
439
440/// A SQLite record serial type code.
441///
442/// The mapping from code to meaning (file-format spec, "Serial Type Codes Of
443/// The Record Format"):
444///
445/// | code | meaning | body bytes |
446/// |------|---------|------------|
447/// | 0 | NULL | 0 |
448/// | 1 | int, big-endian | 1 |
449/// | 2 | int | 2 |
450/// | 3 | int | 3 |
451/// | 4 | int | 4 |
452/// | 5 | int | 6 |
453/// | 6 | int | 8 |
454/// | 7 | IEEE-754 float | 8 |
455/// | 8 | integer 0 | 0 |
456/// | 9 | integer 1 | 0 |
457/// | 10, 11 | reserved | — |
458/// | N≥12 even | BLOB | (N-12)/2 |
459/// | N≥13 odd | TEXT | (N-13)/2 |
460#[derive(Debug, Clone, Copy, PartialEq, Eq)]
461pub struct SerialType(pub u64);
462
463impl SerialType {
464 /// Number of bytes this serial type occupies in the record body, or `None`
465 /// for the reserved codes 10 and 11.
466 pub fn content_len(self) -> Option<usize> {
467 Some(match self.0 {
468 0 | 8 | 9 => 0,
469 1 => 1,
470 2 => 2,
471 3 => 3,
472 4 => 4,
473 5 => 6,
474 6 | 7 => 8,
475 10 | 11 => return None,
476 n if n % 2 == 0 => ((n - 12) / 2) as usize,
477 n => ((n - 13) / 2) as usize,
478 })
479 }
480
481 /// The smallest serial type that can losslessly represent `value`.
482 ///
483 /// This matches SQLite's choice: small integers collapse to the 0/1 literals
484 /// (codes 8/9) and otherwise to the narrowest of the 1/2/3/4/6/8-byte forms.
485 pub fn for_value(value: &Value) -> SerialType {
486 SerialType(match value {
487 Value::Null => 0,
488 Value::Integer(0) => 8,
489 Value::Integer(1) => 9,
490 Value::Integer(i) => {
491 let i = *i;
492 if (-0x80..=0x7f).contains(&i) {
493 1
494 } else if (-0x8000..=0x7fff).contains(&i) {
495 2
496 } else if (-0x80_0000..=0x7f_ffff).contains(&i) {
497 3
498 } else if (-0x8000_0000..=0x7fff_ffff).contains(&i) {
499 4
500 } else if (-0x8000_0000_0000..=0x7fff_ffff_ffff).contains(&i) {
501 5
502 } else {
503 6
504 }
505 }
506 Value::Real(_) => 7,
507 Value::Blob(b) => 12 + 2 * b.len() as u64,
508 Value::Text(s) => 13 + 2 * s.byte_len() as u64,
509 })
510 }
511}
512
513#[cfg(test)]
514mod tests {
515 use super::*;
516 use alloc::string::ToString;
517 use alloc::vec;
518
519 #[test]
520 fn content_lengths() {
521 assert_eq!(SerialType(0).content_len(), Some(0));
522 assert_eq!(SerialType(1).content_len(), Some(1));
523 assert_eq!(SerialType(5).content_len(), Some(6));
524 assert_eq!(SerialType(6).content_len(), Some(8));
525 assert_eq!(SerialType(7).content_len(), Some(8));
526 assert_eq!(SerialType(8).content_len(), Some(0));
527 assert_eq!(SerialType(9).content_len(), Some(0));
528 assert_eq!(SerialType(10).content_len(), None);
529 assert_eq!(SerialType(11).content_len(), None);
530 // BLOB of 4 bytes -> 12 + 2*4 = 20.
531 assert_eq!(SerialType(20).content_len(), Some(4));
532 // TEXT of 5 bytes -> 13 + 2*5 = 23.
533 assert_eq!(SerialType(23).content_len(), Some(5));
534 }
535
536 #[test]
537 fn serial_type_selection_matches_sqlite() {
538 assert_eq!(SerialType::for_value(&Value::Null), SerialType(0));
539 assert_eq!(SerialType::for_value(&Value::Integer(0)), SerialType(8));
540 assert_eq!(SerialType::for_value(&Value::Integer(1)), SerialType(9));
541 assert_eq!(SerialType::for_value(&Value::Integer(2)), SerialType(1));
542 assert_eq!(SerialType::for_value(&Value::Integer(127)), SerialType(1));
543 assert_eq!(SerialType::for_value(&Value::Integer(128)), SerialType(2));
544 assert_eq!(SerialType::for_value(&Value::Integer(-1)), SerialType(1));
545 assert_eq!(
546 SerialType::for_value(&Value::Integer(i64::MAX)),
547 SerialType(6)
548 );
549 assert_eq!(SerialType::for_value(&Value::Real(1.5)), SerialType(7));
550 assert_eq!(
551 SerialType::for_value(&Value::Text("abc".to_string().into())),
552 SerialType(19) // 13 + 2*3
553 );
554 assert_eq!(
555 SerialType::for_value(&Value::Blob(vec![0u8; 4])),
556 SerialType(20) // 12 + 2*4
557 );
558 }
559
560 #[test]
561 fn nocase_folds_to_lowercase_like_sqlite() {
562 use core::cmp::Ordering;
563 // SQLite's NOCASE (`sqlite3UpperToLower[]`) folds letters to lower
564 // case: 'A' -> 0x61, which sorts *after* the punctuation bytes
565 // 0x5B..=0x60. Folding to upper case would put 'A' (0x41) *before*
566 // them — the bug this guards against.
567 assert_eq!(cmp_text("A", "[", Collation::NoCase), Ordering::Greater);
568 assert_eq!(cmp_text("A", "_", Collation::NoCase), Ordering::Greater);
569 assert_eq!(cmp_text("A", "`", Collation::NoCase), Ordering::Greater);
570 // '9' (0x39) < '[' (0x5B) < 'a'/'A' either way.
571 assert_eq!(cmp_text("9", "[", Collation::NoCase), Ordering::Less);
572 // Case-insensitive equality is unaffected.
573 assert_eq!(
574 cmp_text("Apple", "apple", Collation::NoCase),
575 Ordering::Equal
576 );
577 assert_eq!(cmp_text("Z", "z", Collation::NoCase), Ordering::Equal);
578 // Non-letter bytes never fold.
579 assert_eq!(cmp_text("[", "[", Collation::NoCase), Ordering::Equal);
580 }
581
582 #[test]
583 fn value_ref_round_trips() {
584 assert_eq!(ValueRef::Integer(5).to_owned(), Value::Integer(5));
585 assert_eq!(
586 ValueRef::Text("x").to_owned(),
587 Value::Text("x".to_string().into())
588 );
589 }
590}