Skip to main content

teaql_core/
value.rs

1use std::collections::BTreeMap;
2use std::str::FromStr;
3
4use chrono::{DateTime, NaiveDate, Utc};
5pub use rust_decimal::Decimal;
6use rust_decimal::prelude::{FromPrimitive, ToPrimitive};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum DataType {
10    Bool,
11    I64,
12    U64,
13    F64,
14    Decimal,
15    Text,
16    LargeText,
17    Json,
18    Date,
19    Timestamp,
20}
21
22#[derive(Debug, Clone, PartialEq)]
23pub enum Value {
24    Null,
25    Bool(bool),
26    I64(i64),
27    U64(u64),
28    F64(f64),
29    Decimal(Decimal),
30    Text(String),
31    Json(serde_json::Value),
32    Date(NaiveDate),
33    Timestamp(crate::time::Timestamp),
34    Object(BTreeMap<String, Value>),
35    List(Vec<Value>),
36    TypedNull(DataType),
37}
38
39impl From<&str> for Value {
40    fn from(value: &str) -> Self {
41        Self::Text(value.to_owned())
42    }
43}
44
45impl From<String> for Value {
46    fn from(value: String) -> Self {
47        Self::Text(value)
48    }
49}
50
51impl From<i64> for Value {
52    fn from(value: i64) -> Self {
53        Self::I64(value)
54    }
55}
56
57impl From<i32> for Value {
58    fn from(value: i32) -> Self {
59        Self::I64(i64::from(value))
60    }
61}
62
63impl From<i16> for Value {
64    fn from(value: i16) -> Self {
65        Self::I64(i64::from(value))
66    }
67}
68
69impl From<u64> for Value {
70    fn from(value: u64) -> Self {
71        Self::U64(value)
72    }
73}
74
75impl From<u32> for Value {
76    fn from(value: u32) -> Self {
77        Self::U64(u64::from(value))
78    }
79}
80
81impl From<u16> for Value {
82    fn from(value: u16) -> Self {
83        Self::U64(u64::from(value))
84    }
85}
86
87impl From<f64> for Value {
88    fn from(value: f64) -> Self {
89        Self::F64(value)
90    }
91}
92
93impl From<f32> for Value {
94    fn from(value: f32) -> Self {
95        Self::F64(f64::from(value))
96    }
97}
98
99impl From<bool> for Value {
100    fn from(value: bool) -> Self {
101        Self::Bool(value)
102    }
103}
104
105impl From<Decimal> for Value {
106    fn from(value: Decimal) -> Self {
107        Self::Decimal(value)
108    }
109}
110
111impl From<serde_json::Value> for Value {
112    fn from(value: serde_json::Value) -> Self {
113        Self::Json(value)
114    }
115}
116
117impl From<NaiveDate> for Value {
118    fn from(value: NaiveDate) -> Self {
119        Self::Date(value)
120    }
121}
122
123impl From<crate::time::Timestamp> for Value {
124    fn from(value: crate::time::Timestamp) -> Self {
125        Self::Timestamp(value)
126    }
127}
128
129impl From<DateTime<Utc>> for Value {
130    fn from(value: DateTime<Utc>) -> Self {
131        Self::Timestamp(crate::time::Timestamp(value.timestamp_millis()))
132    }
133}
134
135impl Value {
136    pub fn object(values: std::collections::BTreeMap<String, Value>) -> Self {
137        Self::Object(values)
138    }
139
140    pub fn try_i64(&self) -> Option<i64> {
141        match self {
142            Self::I64(value) => Some(*value),
143            Self::U64(value) => i64::try_from(*value).ok(),
144            Self::Decimal(value) => value.to_i64(),
145            _ => None,
146        }
147    }
148
149    pub fn try_u64(&self) -> Option<u64> {
150        match self {
151            Self::U64(value) => Some(*value),
152            Self::I64(value) => u64::try_from(*value).ok(),
153            Self::Decimal(value) => value.to_u64(),
154            _ => None,
155        }
156    }
157
158    pub fn try_decimal(&self) -> Option<Decimal> {
159        match self {
160            Self::Decimal(value) => Some(*value),
161            Self::I64(value) => Some(Decimal::from(*value)),
162            Self::U64(value) => Some(Decimal::from(*value)),
163            // SQL drivers commonly return NUMERIC values as f64. Convert using
164            // the shortest human decimal representation instead of retaining
165            // the binary floating-point expansion (for example 129.95 must not
166            // become 129.949999999999988...).
167            Self::F64(value) if value.is_finite() => Decimal::from_f64(*value),
168            Self::Text(value) => Decimal::from_str(value).ok(),
169            _ => None,
170        }
171    }
172
173    pub fn try_f64(&self) -> Option<f64> {
174        match self {
175            Self::F64(value) => Some(*value),
176            Self::I64(value) => Some(*value as f64),
177            Self::U64(value) => Some(*value as f64),
178            Self::Decimal(value) => value.to_f64(),
179            _ => None,
180        }
181    }
182
183    pub fn try_text(&self) -> Option<&str> {
184        match self {
185            Self::Text(value) => Some(value),
186            _ => None,
187        }
188    }
189
190    pub fn try_bool(&self) -> Option<bool> {
191        match self {
192            Self::Bool(value) => Some(*value),
193            _ => None,
194        }
195    }
196
197    pub fn try_date(&self) -> Option<NaiveDate> {
198        match self {
199            Self::Date(value) => Some(*value),
200            Self::Text(value) => {
201                if let Ok(nd) = chrono::NaiveDate::parse_from_str(value, "%Y-%m-%d") {
202                    return Some(nd);
203                }
204                None
205            }
206            Self::I64(value) => {
207                chrono::DateTime::from_timestamp_millis(*value).map(|dt| dt.naive_utc().date())
208            }
209            Self::U64(value) => i64::try_from(*value)
210                .ok()
211                .and_then(chrono::DateTime::from_timestamp_millis)
212                .map(|dt| dt.naive_utc().date()),
213            _ => None,
214        }
215    }
216
217    pub fn try_timestamp(&self) -> Option<crate::time::Timestamp> {
218        match self {
219            Self::Timestamp(value) => Some(*value),
220            Self::Text(value) => {
221                if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(value) {
222                    return Some(crate::time::Timestamp(dt.timestamp_millis()));
223                }
224                if let Ok(ndt) = chrono::NaiveDateTime::parse_from_str(value, "%Y-%m-%d %H:%M:%S") {
225                    return Some(crate::time::Timestamp(
226                        chrono::DateTime::<Utc>::from_naive_utc_and_offset(ndt, chrono::Utc)
227                            .timestamp_millis(),
228                    ));
229                }
230                if let Ok(nd) = chrono::NaiveDate::parse_from_str(value, "%Y-%m-%d") {
231                    let ndt = nd.and_hms_opt(0, 0, 0)?;
232                    return Some(crate::time::Timestamp(
233                        chrono::DateTime::<Utc>::from_naive_utc_and_offset(ndt, chrono::Utc)
234                            .timestamp_millis(),
235                    ));
236                }
237                None
238            }
239            Self::I64(value) => Some(crate::time::Timestamp(*value)),
240            Self::U64(value) => i64::try_from(*value).ok().map(crate::time::Timestamp),
241            _ => None,
242        }
243    }
244
245    pub fn to_json_value(&self) -> serde_json::Value {
246        match self {
247            Self::Null => serde_json::Value::Null,
248            Self::Bool(value) => serde_json::Value::Bool(*value),
249            Self::I64(value) => serde_json::Value::from(*value),
250            Self::U64(value) => serde_json::Value::from(*value),
251            Self::F64(value) => serde_json::Number::from_f64(*value)
252                .map(serde_json::Value::Number)
253                .unwrap_or(serde_json::Value::Null),
254            Self::Decimal(value) => serde_json::Value::String(value.to_string()),
255            Self::Text(value) => serde_json::Value::String(value.clone()),
256            Self::Json(value) => value.clone(),
257            Self::Date(value) => serde_json::Value::String(value.to_string()),
258            Self::Timestamp(value) => serde_json::Value::from(value.0),
259            Self::Object(record) => crate::record_to_json_value(record),
260            Self::List(values) => {
261                serde_json::Value::Array(values.iter().map(Value::to_json_value).collect())
262            }
263            Self::TypedNull(_) => serde_json::Value::Null,
264        }
265    }
266}
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271
272    #[test]
273    fn value_try_i64_accepts_representable_numeric_variants() {
274        assert_eq!(Value::I64(i64::MIN).try_i64(), Some(i64::MIN));
275        assert_eq!(Value::I64(i64::MAX).try_i64(), Some(i64::MAX));
276        assert_eq!(Value::U64(i64::MAX as u64).try_i64(), Some(i64::MAX));
277        assert_eq!(Value::Decimal(Decimal::from(-42)).try_i64(), Some(-42));
278    }
279
280    #[test]
281    fn value_try_i64_rejects_unsigned_overflow_and_unrelated_variants() {
282        assert_eq!(Value::U64(i64::MAX as u64 + 1).try_i64(), None);
283        assert_eq!(Value::U64(u64::MAX).try_i64(), None);
284        assert_eq!(Value::F64(42.0).try_i64(), None);
285        assert_eq!(Value::Text("42".to_owned()).try_i64(), None);
286        assert_eq!(Value::Null.try_i64(), None);
287    }
288
289    #[test]
290    fn value_try_u64_accepts_representable_numeric_variants() {
291        assert_eq!(Value::U64(0).try_u64(), Some(0));
292        assert_eq!(Value::U64(u64::MAX).try_u64(), Some(u64::MAX));
293        assert_eq!(Value::I64(i64::MAX).try_u64(), Some(i64::MAX as u64));
294        assert_eq!(Value::Decimal(Decimal::from(42)).try_u64(), Some(42));
295    }
296
297    #[test]
298    fn value_try_u64_rejects_negative_and_unrelated_variants() {
299        assert_eq!(Value::I64(-1).try_u64(), None);
300        assert_eq!(Value::Decimal(Decimal::from(-1)).try_u64(), None);
301        assert_eq!(Value::F64(42.0).try_u64(), None);
302        assert_eq!(Value::Text("42".to_owned()).try_u64(), None);
303        assert_eq!(Value::Null.try_u64(), None);
304    }
305
306    #[test]
307    fn value_try_decimal_accepts_decimal_numeric_and_text_variants() {
308        let decimal = Decimal::from_str("123.450").expect("valid decimal");
309
310        assert_eq!(Value::Decimal(decimal).try_decimal(), Some(decimal));
311        assert_eq!(
312            Value::I64(i64::MIN).try_decimal(),
313            Some(Decimal::from(i64::MIN))
314        );
315        assert_eq!(
316            Value::U64(u64::MAX).try_decimal(),
317            Some(Decimal::from(u64::MAX))
318        );
319        assert_eq!(
320            Value::Text("123.450".to_owned()).try_decimal(),
321            Some(decimal)
322        );
323        assert_eq!(
324            Value::F64(136.25).try_decimal(),
325            Decimal::from_f64_retain(136.25)
326        );
327    }
328
329    #[test]
330    fn value_try_decimal_rejects_invalid_text_and_unrelated_variants() {
331        assert_eq!(Value::Text("not-a-decimal".to_owned()).try_decimal(), None);
332        assert_eq!(Value::Bool(true).try_decimal(), None);
333        assert_eq!(Value::F64(f64::NAN).try_decimal(), None);
334        assert_eq!(Value::Null.try_decimal(), None);
335    }
336
337    #[test]
338    fn value_try_decimal_uses_human_decimal_for_f64() {
339        assert_eq!(
340            Value::F64(129.95).try_decimal(),
341            Some(Decimal::from_str("129.95").unwrap())
342        );
343    }
344
345    #[test]
346    fn value_try_f64_accepts_supported_numeric_variants() {
347        assert_eq!(Value::F64(1.25).try_f64(), Some(1.25));
348        assert_eq!(Value::I64(-2).try_f64(), Some(-2.0));
349        assert_eq!(Value::U64(2).try_f64(), Some(2.0));
350        assert_eq!(
351            Value::Decimal(Decimal::from_str("1.5").expect("valid decimal")).try_f64(),
352            Some(1.5)
353        );
354    }
355
356    #[test]
357    fn value_try_f64_rejects_unrelated_variants() {
358        assert_eq!(Value::Text("1.5".to_owned()).try_f64(), None);
359        assert_eq!(Value::Bool(true).try_f64(), None);
360        assert_eq!(Value::Null.try_f64(), None);
361    }
362
363    #[test]
364    fn value_try_date_accepts_date_and_iso_date_text() {
365        let leap_day = NaiveDate::from_ymd_opt(2024, 2, 29).expect("valid leap day");
366
367        assert_eq!(Value::Date(leap_day).try_date(), Some(leap_day));
368        assert_eq!(
369            Value::Text("2024-02-29".to_owned()).try_date(),
370            Some(leap_day)
371        );
372        let millis = leap_day
373            .and_hms_opt(0, 0, 0)
374            .unwrap()
375            .and_utc()
376            .timestamp_millis();
377        assert_eq!(Value::I64(millis).try_date(), Some(leap_day));
378        assert_eq!(Value::U64(millis as u64).try_date(), Some(leap_day));
379    }
380
381    #[test]
382    fn value_try_date_rejects_invalid_dates_and_unrelated_variants() {
383        assert_eq!(Value::Text("2023-02-29".to_owned()).try_date(), None);
384        assert_eq!(
385            Value::Text("2024-02-29T00:00:00Z".to_owned()).try_date(),
386            None
387        );
388        assert_eq!(Value::Null.try_date(), None);
389    }
390
391    #[test]
392    fn value_try_timestamp_accepts_timestamp_and_supported_text_formats() {
393        let utc_timestamp = crate::time::Timestamp(
394            DateTime::parse_from_rfc3339("2024-01-02T03:04:05Z")
395                .expect("valid RFC 3339 timestamp")
396                .with_timezone(&Utc)
397                .timestamp_millis(),
398        );
399        let offset_timestamp = crate::time::Timestamp(
400            DateTime::parse_from_rfc3339("2024-01-02T03:04:05+08:00")
401                .expect("valid RFC 3339 timestamp")
402                .with_timezone(&Utc)
403                .timestamp_millis(),
404        );
405        let naive_timestamp = NaiveDate::from_ymd_opt(2024, 1, 2)
406            .expect("valid date")
407            .and_hms_opt(3, 4, 5)
408            .expect("valid time");
409        let midnight = NaiveDate::from_ymd_opt(2024, 1, 2)
410            .expect("valid date")
411            .and_hms_opt(0, 0, 0)
412            .expect("valid time");
413
414        assert_eq!(
415            Value::Timestamp(utc_timestamp).try_timestamp(),
416            Some(utc_timestamp)
417        );
418        assert_eq!(
419            Value::Text("2024-01-02T03:04:05+08:00".to_owned()).try_timestamp(),
420            Some(offset_timestamp)
421        );
422        assert_eq!(
423            Value::Text("2024-01-02 03:04:05".to_owned()).try_timestamp(),
424            Some(crate::time::Timestamp(
425                DateTime::<Utc>::from_naive_utc_and_offset(naive_timestamp, Utc).timestamp_millis()
426            ))
427        );
428        assert_eq!(
429            Value::Text("2024-01-02".to_owned()).try_timestamp(),
430            Some(crate::time::Timestamp(
431                DateTime::<Utc>::from_naive_utc_and_offset(midnight, Utc).timestamp_millis()
432            ))
433        );
434
435        let millis = utc_timestamp.0;
436        assert_eq!(Value::I64(millis).try_timestamp(), Some(utc_timestamp));
437        assert_eq!(
438            Value::U64(millis as u64).try_timestamp(),
439            Some(utc_timestamp)
440        );
441    }
442
443    #[test]
444    fn value_try_timestamp_normalizes_offsets_and_rejects_invalid_input() {
445        let expected_utc = crate::time::Timestamp(
446            DateTime::parse_from_rfc3339("2024-01-01T19:04:05Z")
447                .expect("valid RFC 3339 timestamp")
448                .with_timezone(&Utc)
449                .timestamp_millis(),
450        );
451
452        assert_eq!(
453            Value::Text("2024-01-02T03:04:05+08:00".to_owned()).try_timestamp(),
454            Some(expected_utc)
455        );
456        assert_eq!(
457            Value::Text("2024-13-40 25:61:61".to_owned()).try_timestamp(),
458            None
459        );
460        assert_eq!(Value::Bool(true).try_timestamp(), None);
461        assert_eq!(Value::Null.try_timestamp(), None);
462    }
463}