Skip to main content

basalt/
types.rs

1/// Basalt's scalar value system.
2use std::fmt;
3
4#[derive(Debug, Clone, PartialEq)]
5pub enum Value {
6    Null,
7    Integer(i64),
8    Real(f64),
9    Text(String),
10    Boolean(bool),
11}
12
13impl Value {
14    pub fn type_name(&self) -> &'static str {
15        match self {
16            Value::Null => "NULL",
17            Value::Integer(_) => "INTEGER",
18            Value::Real(_) => "REAL",
19            Value::Text(_) => "TEXT",
20            Value::Boolean(_) => "BOOLEAN",
21        }
22    }
23
24    /// Total ordering used by comparisons and B-trees. NULL sorts lowest.
25    pub fn cmp_value(&self, other: &Value) -> std::cmp::Ordering {
26        use std::cmp::Ordering;
27        fn rank(v: &Value) -> u8 {
28            match v {
29                Value::Null => 0,
30                Value::Boolean(_) => 1,
31                Value::Integer(_) | Value::Real(_) => 2,
32                Value::Text(_) => 3,
33            }
34        }
35        match (self, other) {
36            (Value::Null, Value::Null) => Ordering::Equal,
37            (Value::Boolean(a), Value::Boolean(b)) => a.cmp(b),
38            (Value::Integer(a), Value::Integer(b)) => a.cmp(b),
39            (Value::Real(a), Value::Real(b)) => cmp_real(*a, *b),
40            (Value::Integer(a), Value::Real(b)) => cmp_real(*a as f64, *b),
41            (Value::Real(a), Value::Integer(b)) => cmp_real(*a, *b as f64),
42            (Value::Text(a), Value::Text(b)) => a.cmp(b),
43            _ => rank(self).cmp(&rank(other)),
44        }
45    }
46
47    /// Truthiness per SQL-ish rules: nonzero numbers and their own values.
48    pub fn is_truthy(&self) -> Option<bool> {
49        match self {
50            Value::Null => None,
51            Value::Boolean(b) => Some(*b),
52            Value::Integer(i) => Some(*i != 0),
53            Value::Real(f) => Some(*f != 0.0),
54            Value::Text(_) => None,
55        }
56    }
57
58    pub fn coerce_to(&self, ty: &ColumnType) -> Result<Value, String> {
59        match (ty, self) {
60            (ColumnType::Null | ColumnType::Any, _) => Ok(self.clone()),
61            (ColumnType::Integer, Value::Integer(_)) | (ColumnType::Integer, Value::Null) => {
62                Ok(self.clone())
63            }
64            (ColumnType::Integer, Value::Boolean(b)) => Ok(Value::Integer(*b as i64)),
65            (ColumnType::Integer, Value::Real(f)) => {
66                if !f.is_finite() || *f < i64::MIN as f64 || *f >= i64::MAX as f64 {
67                    Err("cannot convert non-finite or out-of-range REAL to INTEGER".to_string())
68                } else {
69                    Ok(Value::Integer(*f as i64))
70                }
71            }
72            (ColumnType::Integer, Value::Text(t)) => t
73                .parse::<i64>()
74                .map(Value::Integer)
75                .map_err(|_| format!("cannot convert {t:?} to INTEGER")),
76            (ColumnType::Real, Value::Real(f)) if f.is_finite() => Ok(self.clone()),
77            (ColumnType::Real, Value::Real(_)) => Err("cannot store a non-finite REAL".to_string()),
78            (ColumnType::Real, Value::Null) => Ok(self.clone()),
79            (ColumnType::Real, Value::Integer(i)) => Ok(Value::Real(*i as f64)),
80            (ColumnType::Real, Value::Boolean(_)) => {
81                Err("cannot convert BOOLEAN to REAL".to_string())
82            }
83            (ColumnType::Real, Value::Text(t)) => {
84                let value = t
85                    .parse::<f64>()
86                    .map_err(|_| format!("cannot convert {t:?} to REAL"))?;
87                if value.is_finite() {
88                    Ok(Value::Real(value))
89                } else {
90                    Err(format!("cannot convert {t:?} to REAL"))
91                }
92            }
93            (ColumnType::Text, Value::Text(_)) | (ColumnType::Text, Value::Null) => {
94                Ok(self.clone())
95            }
96            (ColumnType::Text, v) => Ok(Value::Text(v.to_string())),
97            (ColumnType::Boolean, Value::Boolean(_)) | (ColumnType::Boolean, Value::Null) => {
98                Ok(self.clone())
99            }
100            (ColumnType::Boolean, Value::Integer(i)) => Ok(Value::Boolean(*i != 0)),
101            (ColumnType::Boolean, Value::Real(_)) | (ColumnType::Boolean, Value::Text(_)) => {
102                Err(format!("cannot convert {} to BOOLEAN", self.type_name()))
103            }
104        }
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn rejects_non_finite_and_out_of_range_real_conversions() {
114        assert!(
115            Value::Real(f64::INFINITY)
116                .coerce_to(&ColumnType::Real)
117                .is_err()
118        );
119        assert!(
120            Value::Text("NaN".into())
121                .coerce_to(&ColumnType::Real)
122                .is_err()
123        );
124        assert!(
125            Value::Real(i64::MAX as f64)
126                .coerce_to(&ColumnType::Integer)
127                .is_err()
128        );
129        assert_eq!(
130            Value::Real(i64::MIN as f64)
131                .coerce_to(&ColumnType::Integer)
132                .unwrap(),
133            Value::Integer(i64::MIN)
134        );
135    }
136}
137
138fn cmp_real(left: f64, right: f64) -> std::cmp::Ordering {
139    left.partial_cmp(&right)
140        .unwrap_or_else(|| left.total_cmp(&right))
141}
142
143impl fmt::Display for Value {
144    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145        match self {
146            Value::Null => write!(f, "NULL"),
147            Value::Integer(i) => write!(f, "{i}"),
148            Value::Real(x) => write!(f, "{x}"),
149            Value::Text(s) => write!(f, "'{s}'"),
150            Value::Boolean(b) => write!(f, "{b}"),
151        }
152    }
153}
154
155#[derive(Debug, Clone, PartialEq, Eq)]
156pub enum ColumnType {
157    Integer,
158    Real,
159    Text,
160    Boolean,
161    /// Untyped (expression results)
162    Any,
163    /// Internal: NULL literal type
164    Null,
165}
166
167impl ColumnType {
168    pub fn parse(name: &str) -> Option<ColumnType> {
169        match name.to_uppercase().as_str() {
170            "INTEGER" | "INT" | "BIGINT" | "SMALLINT" => Some(ColumnType::Integer),
171            "REAL" | "FLOAT" | "DOUBLE" => Some(ColumnType::Real),
172            "TEXT" | "VARCHAR" | "CHAR" | "STRING" => Some(ColumnType::Text),
173            "BOOLEAN" | "BOOL" => Some(ColumnType::Boolean),
174            _ => None,
175        }
176    }
177}