Skip to main content

alopex_sql/storage/
value.rs

1use std::cmp::Ordering;
2use std::convert::TryFrom;
3
4use crate::ast::ddl::VectorMetric;
5use crate::planner::ResolvedType;
6use serde::{Deserialize, Serialize};
7
8use super::error::{Result, StorageError};
9
10/// Canonical native JSON value. JSON and JSONB share this representation.
11#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
12pub struct JsonValue(String);
13
14impl JsonValue {
15    pub fn parse(input: &str) -> std::result::Result<Self, serde_json::Error> {
16        let mut value: serde_json::Value = serde_json::from_str(input)?;
17        sort_json_keys(&mut value);
18        Ok(Self(
19            serde_json::to_string(&value).expect("JSON serialization cannot fail"),
20        ))
21    }
22
23    pub fn from_value(mut value: serde_json::Value) -> Self {
24        sort_json_keys(&mut value);
25        Self(serde_json::to_string(&value).expect("JSON serialization cannot fail"))
26    }
27
28    pub fn as_str(&self) -> &str {
29        &self.0
30    }
31
32    pub fn to_value(&self) -> serde_json::Value {
33        serde_json::from_str(&self.0).expect("JsonValue is validated on construction")
34    }
35}
36
37impl std::fmt::Display for JsonValue {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        f.write_str(&self.0)
40    }
41}
42
43fn sort_json_keys(value: &mut serde_json::Value) {
44    match value {
45        serde_json::Value::Array(values) => values.iter_mut().for_each(sort_json_keys),
46        serde_json::Value::Object(values) => {
47            let mut entries: Vec<_> = std::mem::take(values).into_iter().collect();
48            entries.sort_unstable_by(|left, right| left.0.cmp(&right.0));
49            for (key, mut value) in entries {
50                sort_json_keys(&mut value);
51                values.insert(key, value);
52            }
53        }
54        _ => {}
55    }
56}
57
58/// Exact decimal value represented as a signed coefficient and base-10 scale.
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
60pub struct DecimalValue {
61    pub coefficient: i128,
62    pub scale: u8,
63}
64
65impl DecimalValue {
66    pub fn new(coefficient: i128, scale: u8) -> Self {
67        Self { coefficient, scale }
68    }
69
70    pub fn parse(value: &str) -> Option<Self> {
71        let value = value.trim();
72        let (negative, unsigned) = match value.as_bytes().first() {
73            Some(b'-') => (true, &value[1..]),
74            Some(b'+') => (false, &value[1..]),
75            _ => (false, value),
76        };
77        let mut parts = unsigned.split('.');
78        let whole = parts.next()?;
79        let fraction = parts.next().unwrap_or("");
80        if parts.next().is_some()
81            || (whole.is_empty() && fraction.is_empty())
82            || !whole.bytes().all(|byte| byte.is_ascii_digit())
83            || !fraction.bytes().all(|byte| byte.is_ascii_digit())
84            || fraction.len() > 38
85        {
86            return None;
87        }
88        let digits = format!("{whole}{fraction}");
89        let coefficient = digits.parse::<i128>().ok()?;
90        Some(Self {
91            coefficient: if negative {
92                coefficient.checked_neg()?
93            } else {
94                coefficient
95            },
96            scale: fraction.len() as u8,
97        })
98    }
99
100    pub fn rescale(self, target_scale: u8) -> Option<Self> {
101        if target_scale == self.scale {
102            return Some(self);
103        }
104        if target_scale > self.scale {
105            return Some(Self::new(
106                self.coefficient
107                    .checked_mul(decimal_power(target_scale - self.scale)?)?,
108                target_scale,
109            ));
110        }
111        let divisor = decimal_power(self.scale - target_scale)?;
112        let quotient = self.coefficient / divisor;
113        let remainder = self.coefficient % divisor;
114        let rounded = if remainder.abs().checked_mul(2)? >= divisor {
115            quotient.checked_add(self.coefficient.signum())?
116        } else {
117            quotient
118        };
119        Some(Self::new(rounded, target_scale))
120    }
121
122    pub fn fits_precision(self, precision: u8) -> bool {
123        decimal_digits(self.coefficient) <= usize::from(precision)
124    }
125
126    pub(crate) fn cmp_numeric(self, other: Self) -> Ordering {
127        if self.coefficient.signum() != other.coefficient.signum() {
128            return self.coefficient.signum().cmp(&other.coefficient.signum());
129        }
130        let negative = self.coefficient < 0;
131        let mut left = self.coefficient.unsigned_abs().to_string();
132        let mut right = other.coefficient.unsigned_abs().to_string();
133        let left_integer_digits = left.len() as i16 - i16::from(self.scale);
134        let right_integer_digits = right.len() as i16 - i16::from(other.scale);
135        let ordering = left_integer_digits
136            .cmp(&right_integer_digits)
137            .then_with(|| {
138                let scale = self.scale.max(other.scale);
139                left.extend(std::iter::repeat_n('0', usize::from(scale - self.scale)));
140                right.extend(std::iter::repeat_n('0', usize::from(scale - other.scale)));
141                left.cmp(&right)
142            });
143        if negative {
144            ordering.reverse()
145        } else {
146            ordering
147        }
148    }
149}
150
151pub(crate) fn decimal_power(scale: u8) -> Option<i128> {
152    10_i128.checked_pow(u32::from(scale))
153}
154
155fn decimal_digits(value: i128) -> usize {
156    value.unsigned_abs().to_string().len()
157}
158
159impl std::fmt::Display for DecimalValue {
160    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161        let negative = self.coefficient < 0;
162        let digits = self.coefficient.unsigned_abs().to_string();
163        if self.scale == 0 {
164            return write!(f, "{}{}", if negative { "-" } else { "" }, digits);
165        }
166        let scale = usize::from(self.scale);
167        if digits.len() <= scale {
168            write!(
169                f,
170                "{}0.{:0>width$}",
171                if negative { "-" } else { "" },
172                digits,
173                width = scale
174            )
175        } else {
176            let split = digits.len() - scale;
177            write!(
178                f,
179                "{}{}.{}",
180                if negative { "-" } else { "" },
181                &digits[..split],
182                &digits[split..]
183            )
184        }
185    }
186}
187
188/// Runtime value representation for the SQL storage layer.
189#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
190pub enum SqlValue {
191    Null,
192    Integer(i32),
193    BigInt(i64),
194    Float(f32),
195    Double(f64),
196    Text(String),
197    Blob(Vec<u8>),
198    Boolean(bool),
199    Timestamp(i64), // microseconds since epoch
200    Vector(Vec<f32>),
201    Date(i32), // days since 1970-01-01
202    Time(i64), // microseconds since midnight
203    Interval { months: i32, days: i32, micros: i64 },
204    Decimal(DecimalValue),
205    Json(JsonValue),
206    Array(Vec<SqlValue>),
207    Map(Vec<(SqlValue, SqlValue)>),
208    Struct(Vec<(String, SqlValue)>),
209}
210
211impl SqlValue {
212    /// Canonical JSON mapping used by public surfaces for nested values.
213    pub fn nested_json_text(&self) -> Option<String> {
214        matches!(self, Self::Array(_) | Self::Map(_) | Self::Struct(_))
215            .then(|| nested_json_value(self))?
216            .and_then(|value| serde_json::to_string(&value).ok())
217    }
218
219    /// Formats native temporal values for text-oriented public surfaces.
220    pub fn temporal_text(&self) -> Option<String> {
221        match self {
222            Self::Date(days) => {
223                let epoch = chrono::NaiveDate::from_ymd_opt(1970, 1, 1).expect("valid epoch");
224                Some(
225                    epoch
226                        .checked_add_signed(chrono::Duration::days(i64::from(*days)))
227                        .map(|date| date.format("%Y-%m-%d").to_string())
228                        .unwrap_or_else(|| format!("{days} days since 1970-01-01")),
229                )
230            }
231            Self::Time(micros) => {
232                let formatted = u32::try_from(micros.div_euclid(1_000_000))
233                    .ok()
234                    .zip(u32::try_from(micros.rem_euclid(1_000_000)).ok())
235                    .and_then(|(seconds, micros)| {
236                        chrono::NaiveTime::from_num_seconds_from_midnight_opt(
237                            seconds,
238                            micros * 1_000,
239                        )
240                    })
241                    .map(|time| time.format("%H:%M:%S%.6f").to_string())
242                    .unwrap_or_else(|| format!("{micros} microseconds after midnight"));
243                Some(formatted)
244            }
245            Self::Interval {
246                months,
247                days,
248                micros,
249            } => Some(format!("{months} months {days} days {micros} microseconds")),
250            _ => None,
251        }
252    }
253
254    /// Returns the type tag for serialization (see design type tags).
255    pub fn type_tag(&self) -> u8 {
256        match self {
257            SqlValue::Null => 0x00,
258            SqlValue::Integer(_) => 0x01,
259            SqlValue::BigInt(_) => 0x02,
260            SqlValue::Float(_) => 0x03,
261            SqlValue::Double(_) => 0x04,
262            SqlValue::Text(_) => 0x05,
263            SqlValue::Blob(_) => 0x06,
264            SqlValue::Boolean(_) => 0x07,
265            SqlValue::Timestamp(_) => 0x08,
266            SqlValue::Vector(_) => 0x09,
267            SqlValue::Date(_) => 0x0a,
268            SqlValue::Time(_) => 0x0b,
269            SqlValue::Interval { .. } => 0x0c,
270            SqlValue::Decimal(_) => 0x0d,
271            SqlValue::Json(_) => 0x0e,
272            SqlValue::Array(_) => 0x0f,
273            SqlValue::Map(_) => 0x10,
274            SqlValue::Struct(_) => 0x11,
275        }
276    }
277
278    /// Returns true if the value is Null.
279    pub fn is_null(&self) -> bool {
280        matches!(self, SqlValue::Null)
281    }
282
283    /// Human-readable type name for error reporting.
284    pub fn type_name(&self) -> &'static str {
285        match self {
286            SqlValue::Null => "Null",
287            SqlValue::Integer(_) => "Integer",
288            SqlValue::BigInt(_) => "BigInt",
289            SqlValue::Float(_) => "Float",
290            SqlValue::Double(_) => "Double",
291            SqlValue::Text(_) => "Text",
292            SqlValue::Blob(_) => "Blob",
293            SqlValue::Boolean(_) => "Boolean",
294            SqlValue::Timestamp(_) => "Timestamp",
295            SqlValue::Vector(_) => "Vector",
296            SqlValue::Date(_) => "Date",
297            SqlValue::Time(_) => "Time",
298            SqlValue::Interval { .. } => "Interval",
299            SqlValue::Decimal(_) => "Decimal",
300            SqlValue::Json(_) => "Json",
301            SqlValue::Array(_) => "Array",
302            SqlValue::Map(_) => "Map",
303            SqlValue::Struct(_) => "Struct",
304        }
305    }
306
307    /// Returns the corresponding ResolvedType for this value.
308    pub fn resolved_type(&self) -> ResolvedType {
309        match self {
310            SqlValue::Null => ResolvedType::Null,
311            SqlValue::Integer(_) => ResolvedType::Integer,
312            SqlValue::BigInt(_) => ResolvedType::BigInt,
313            SqlValue::Float(_) => ResolvedType::Float,
314            SqlValue::Double(_) => ResolvedType::Double,
315            SqlValue::Text(_) => ResolvedType::Text,
316            SqlValue::Blob(_) => ResolvedType::Blob,
317            SqlValue::Boolean(_) => ResolvedType::Boolean,
318            SqlValue::Timestamp(_) => ResolvedType::Timestamp,
319            SqlValue::Vector(v) => ResolvedType::Vector {
320                dimension: v.len() as u32,
321                metric: VectorMetric::Cosine,
322            },
323            SqlValue::Date(_) => ResolvedType::Date,
324            SqlValue::Time(_) => ResolvedType::Time,
325            SqlValue::Interval { .. } => ResolvedType::Interval,
326            SqlValue::Decimal(value) => ResolvedType::Decimal {
327                precision: 38,
328                scale: value.scale,
329            },
330            SqlValue::Json(_) => ResolvedType::Json,
331            SqlValue::Array(values) => ResolvedType::Array(Box::new(
332                values
333                    .iter()
334                    .find(|value| !value.is_null())
335                    .map(SqlValue::resolved_type)
336                    .unwrap_or(ResolvedType::Null),
337            )),
338            SqlValue::Map(values) => ResolvedType::Map {
339                key: Box::new(
340                    values
341                        .iter()
342                        .find(|(key, _)| !key.is_null())
343                        .map(|(key, _)| key.resolved_type())
344                        .unwrap_or(ResolvedType::Null),
345                ),
346                value: Box::new(
347                    values
348                        .iter()
349                        .find(|(_, value)| !value.is_null())
350                        .map(|(_, value)| value.resolved_type())
351                        .unwrap_or(ResolvedType::Null),
352                ),
353            },
354            SqlValue::Struct(values) => ResolvedType::Struct(
355                values
356                    .iter()
357                    .map(|(name, value)| (name.clone(), value.resolved_type()))
358                    .collect(),
359            ),
360        }
361    }
362}
363
364fn nested_json_value(value: &SqlValue) -> Option<serde_json::Value> {
365    use serde_json::{Map, Number, Value};
366    Some(match value {
367        SqlValue::Null => Value::Null,
368        SqlValue::Integer(value) => Value::Number(Number::from(*value)),
369        SqlValue::BigInt(value) => Value::Number(Number::from(*value)),
370        SqlValue::Float(value) => Value::Number(Number::from_f64(f64::from(*value))?),
371        SqlValue::Double(value) => Value::Number(Number::from_f64(*value)?),
372        SqlValue::Text(value) => Value::String(value.clone()),
373        SqlValue::Boolean(value) => Value::Bool(*value),
374        SqlValue::Decimal(value) => serde_json::from_str(&value.to_string()).ok()?,
375        SqlValue::Json(value) => value.to_value(),
376        SqlValue::Array(values) => Value::Array(
377            values
378                .iter()
379                .map(nested_json_value)
380                .collect::<Option<Vec<_>>>()?,
381        ),
382        SqlValue::Map(values) => {
383            if values
384                .iter()
385                .all(|(key, _)| matches!(key, SqlValue::Text(_)))
386            {
387                let mut output = Map::new();
388                for (key, value) in values {
389                    let SqlValue::Text(key) = key else {
390                        unreachable!()
391                    };
392                    output.insert(key.clone(), nested_json_value(value)?);
393                }
394                Value::Object(output)
395            } else {
396                Value::Array(
397                    values
398                        .iter()
399                        .map(|(key, value)| {
400                            Some(Value::Array(vec![
401                                nested_json_value(key)?,
402                                nested_json_value(value)?,
403                            ]))
404                        })
405                        .collect::<Option<Vec<_>>>()?,
406                )
407            }
408        }
409        SqlValue::Struct(values) => Value::Object(
410            values
411                .iter()
412                .map(|(name, value)| Some((name.clone(), nested_json_value(value)?)))
413                .collect::<Option<Map<_, _>>>()?,
414        ),
415        SqlValue::Date(_) | SqlValue::Time(_) | SqlValue::Interval { .. } => {
416            Value::String(value.temporal_text()?)
417        }
418        SqlValue::Timestamp(value) => Value::Number(Number::from(*value)),
419        SqlValue::Vector(values) => Value::Array(
420            values
421                .iter()
422                .map(|value| Number::from_f64(f64::from(*value)).map(Value::Number))
423                .collect::<Option<Vec<_>>>()?,
424        ),
425        SqlValue::Blob(_) => return None,
426    })
427}
428
429impl PartialOrd for SqlValue {
430    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
431        use SqlValue::*;
432        match (self, other) {
433            (Null, _) | (_, Null) => None,
434            (Integer(a), Integer(b)) => Some(a.cmp(b)),
435            (BigInt(a), BigInt(b)) => Some(a.cmp(b)),
436            (Float(a), Float(b)) => a.partial_cmp(b),
437            (Double(a), Double(b)) => a.partial_cmp(b),
438            (Text(a), Text(b)) => Some(a.cmp(b)),
439            (Blob(a), Blob(b)) => Some(a.cmp(b)),
440            (Boolean(a), Boolean(b)) => Some(a.cmp(b)),
441            (Timestamp(a), Timestamp(b)) => Some(a.cmp(b)),
442            (Date(a), Date(b)) => Some(a.cmp(b)),
443            (Time(a), Time(b)) => Some(a.cmp(b)),
444            (
445                Interval {
446                    months: am,
447                    days: ad,
448                    micros: au,
449                },
450                Interval {
451                    months: bm,
452                    days: bd,
453                    micros: bu,
454                },
455            ) => Some((am, ad, au).cmp(&(bm, bd, bu))),
456            (Decimal(a), Decimal(b)) => Some(a.cmp_numeric(*b)),
457            (Json(a), Json(b)) if a == b => Some(Ordering::Equal),
458            (Json(_), Json(_)) => None,
459            // Vector ordering is undefined for now.
460            (Vector(_), Vector(_)) => None,
461            _ => None,
462        }
463    }
464}
465
466macro_rules! impl_from_sqlvalue_try {
467    ($target:ty, $variant:ident) => {
468        impl TryFrom<SqlValue> for $target {
469            type Error = StorageError;
470            fn try_from(value: SqlValue) -> Result<Self> {
471                if let SqlValue::$variant(v) = value {
472                    Ok(v.into())
473                } else {
474                    Err(StorageError::TypeMismatch {
475                        expected: stringify!($variant).to_string(),
476                        actual: value.type_name().to_string(),
477                    })
478                }
479            }
480        }
481    };
482}
483
484macro_rules! impl_from_primitive {
485    ($source:ty, $variant:ident) => {
486        impl From<$source> for SqlValue {
487            fn from(value: $source) -> Self {
488                SqlValue::$variant(value.into())
489            }
490        }
491    };
492}
493
494impl_from_primitive!(i32, Integer);
495impl_from_primitive!(i64, BigInt);
496impl_from_primitive!(f32, Float);
497impl_from_primitive!(f64, Double);
498impl_from_primitive!(bool, Boolean);
499impl_from_primitive!(String, Text);
500impl_from_primitive!(&str, Text);
501impl_from_primitive!(Vec<u8>, Blob);
502impl From<&[u8]> for SqlValue {
503    fn from(value: &[u8]) -> Self {
504        SqlValue::Blob(value.to_vec())
505    }
506}
507impl From<Vec<f32>> for SqlValue {
508    fn from(value: Vec<f32>) -> Self {
509        SqlValue::Vector(value)
510    }
511}
512
513impl_from_sqlvalue_try!(i32, Integer);
514impl_from_sqlvalue_try!(f32, Float);
515impl_from_sqlvalue_try!(f64, Double);
516impl_from_sqlvalue_try!(bool, Boolean);
517impl_from_sqlvalue_try!(String, Text);
518impl_from_sqlvalue_try!(Vec<u8>, Blob);
519
520impl TryFrom<SqlValue> for i64 {
521    type Error = StorageError;
522    fn try_from(value: SqlValue) -> Result<Self> {
523        match value {
524            SqlValue::BigInt(v) | SqlValue::Timestamp(v) => Ok(v),
525            other => Err(StorageError::TypeMismatch {
526                expected: "BigInt/Timestamp".to_string(),
527                actual: other.type_name().to_string(),
528            }),
529        }
530    }
531}
532
533impl TryFrom<SqlValue> for Vec<f32> {
534    type Error = StorageError;
535    fn try_from(value: SqlValue) -> Result<Self> {
536        if let SqlValue::Vector(v) = value {
537            Ok(v)
538        } else {
539            Err(StorageError::TypeMismatch {
540                expected: "Vector".to_string(),
541                actual: value.type_name().to_string(),
542            })
543        }
544    }
545}
546
547impl TryFrom<&SqlValue> for ResolvedType {
548    type Error = StorageError;
549    fn try_from(value: &SqlValue) -> Result<Self> {
550        Ok(value.resolved_type())
551    }
552}
553
554#[cfg(test)]
555mod tests {
556    use super::*;
557    use proptest::prelude::*;
558
559    #[test]
560    fn null_comparisons_return_none() {
561        assert!(SqlValue::Null.partial_cmp(&SqlValue::Null).is_none());
562        assert!(SqlValue::Null.partial_cmp(&SqlValue::Integer(1)).is_none());
563    }
564
565    #[test]
566    fn heterogeneous_comparisons_return_none() {
567        assert!(
568            SqlValue::Integer(1)
569                .partial_cmp(&SqlValue::Double(1.0))
570                .is_none()
571        );
572        assert!(
573            SqlValue::Text("a".into())
574                .partial_cmp(&SqlValue::Blob(vec![0x61]))
575                .is_none()
576        );
577    }
578
579    #[test]
580    fn temporal_text_preserves_out_of_range_public_values() {
581        assert_eq!(
582            SqlValue::Date(i32::MAX).temporal_text().as_deref(),
583            Some("2147483647 days since 1970-01-01")
584        );
585        assert_eq!(
586            SqlValue::Time(-1).temporal_text().as_deref(),
587            Some("-1 microseconds after midnight")
588        );
589    }
590
591    #[test]
592    fn same_type_comparisons_work() {
593        assert_eq!(
594            SqlValue::Integer(1).partial_cmp(&SqlValue::Integer(2)),
595            Some(Ordering::Less)
596        );
597        assert_eq!(
598            SqlValue::Text("b".into()).partial_cmp(&SqlValue::Text("a".into())),
599            Some(Ordering::Greater)
600        );
601        assert_eq!(
602            SqlValue::Boolean(false).partial_cmp(&SqlValue::Boolean(true)),
603            Some(Ordering::Less)
604        );
605    }
606
607    proptest! {
608        #[test]
609        fn integer_roundtrip(v in any::<i32>()) {
610            let sql: SqlValue = v.into();
611            let back = i32::try_from(sql.clone()).unwrap();
612            prop_assert_eq!(back, v);
613            prop_assert_eq!(sql.partial_cmp(&SqlValue::Integer(v)), Some(Ordering::Equal));
614        }
615
616        #[test]
617        fn bigint_roundtrip(v in any::<i64>()) {
618            let sql: SqlValue = SqlValue::BigInt(v);
619            let back = i64::try_from(sql.clone()).unwrap();
620            prop_assert_eq!(back, v);
621        }
622
623        #[test]
624        fn float_roundtrip_non_nan(v in any::<f32>().prop_filter("no NaN", |f| f.is_finite())) {
625            let sql: SqlValue = SqlValue::Float(v);
626            let back = f32::try_from(sql.clone()).unwrap();
627            prop_assert_eq!(back, v);
628            prop_assert_eq!(sql.partial_cmp(&SqlValue::Float(v)), Some(Ordering::Equal));
629        }
630
631        #[test]
632        fn text_roundtrip(s in ".*") {
633            let sql: SqlValue = SqlValue::Text(s.clone());
634            let back = String::try_from(sql.clone()).unwrap();
635            prop_assert_eq!(back, s.clone());
636            prop_assert_eq!(sql.partial_cmp(&SqlValue::Text(s)), Some(Ordering::Equal));
637        }
638
639        #[test]
640        fn blob_roundtrip(data in proptest::collection::vec(any::<u8>(), 0..64)) {
641            let sql: SqlValue = SqlValue::Blob(data.clone());
642            let back = Vec::<u8>::try_from(sql.clone()).unwrap();
643            prop_assert_eq!(back, data.clone());
644            prop_assert_eq!(sql.partial_cmp(&SqlValue::Blob(data)), Some(Ordering::Equal));
645        }
646    }
647}