Skip to main content

kyu_types/
value.rs

1use kyu_common::InternalId;
2use smol_str::SmolStr;
3
4use crate::interval::Interval;
5use crate::logical_type::LogicalType;
6
7/// A typed value used during parsing, binding, and planning.
8///
9/// This is the ergonomic representation. The storage-optimized 32-byte
10/// untagged `Value` union for the executor is introduced in Phase 3
11/// when the storage layer and vectorized execution need it.
12#[derive(Clone, Debug)]
13pub enum TypedValue {
14    Null,
15    Bool(bool),
16    Int8(i8),
17    Int16(i16),
18    Int32(i32),
19    Int64(i64),
20    Int128(i128),
21    UInt8(u8),
22    UInt16(u16),
23    UInt32(u32),
24    UInt64(u64),
25    Float(f32),
26    Double(f64),
27    Date(i64),
28    Timestamp(i64),
29    TimestampSec(i64),
30    TimestampMs(i64),
31    TimestampNs(i64),
32    TimestampTz(i64),
33    Interval(Interval),
34    String(SmolStr),
35    Blob(Vec<u8>),
36    Uuid(SmolStr),
37    InternalId(InternalId),
38    Serial(i64),
39    List(Vec<TypedValue>),
40    Array(Vec<TypedValue>),
41    Struct(Vec<(SmolStr, TypedValue)>),
42    Map(Vec<(TypedValue, TypedValue)>),
43}
44
45// Manual PartialEq: use to_bits() for floats to get total ordering.
46impl PartialEq for TypedValue {
47    fn eq(&self, other: &Self) -> bool {
48        match (self, other) {
49            (Self::Null, Self::Null) => true,
50            (Self::Bool(a), Self::Bool(b)) => a == b,
51            (Self::Int8(a), Self::Int8(b)) => a == b,
52            (Self::Int16(a), Self::Int16(b)) => a == b,
53            (Self::Int32(a), Self::Int32(b)) => a == b,
54            (Self::Int64(a), Self::Int64(b)) => a == b,
55            (Self::Int128(a), Self::Int128(b)) => a == b,
56            (Self::UInt8(a), Self::UInt8(b)) => a == b,
57            (Self::UInt16(a), Self::UInt16(b)) => a == b,
58            (Self::UInt32(a), Self::UInt32(b)) => a == b,
59            (Self::UInt64(a), Self::UInt64(b)) => a == b,
60            (Self::Float(a), Self::Float(b)) => a.to_bits() == b.to_bits(),
61            (Self::Double(a), Self::Double(b)) => a.to_bits() == b.to_bits(),
62            (Self::Date(a), Self::Date(b)) => a == b,
63            (Self::Timestamp(a), Self::Timestamp(b)) => a == b,
64            (Self::TimestampSec(a), Self::TimestampSec(b)) => a == b,
65            (Self::TimestampMs(a), Self::TimestampMs(b)) => a == b,
66            (Self::TimestampNs(a), Self::TimestampNs(b)) => a == b,
67            (Self::TimestampTz(a), Self::TimestampTz(b)) => a == b,
68            (Self::Interval(a), Self::Interval(b)) => a == b,
69            (Self::String(a), Self::String(b)) => a == b,
70            (Self::Blob(a), Self::Blob(b)) => a == b,
71            (Self::Uuid(a), Self::Uuid(b)) => a == b,
72            (Self::InternalId(a), Self::InternalId(b)) => a == b,
73            (Self::Serial(a), Self::Serial(b)) => a == b,
74            (Self::List(a), Self::List(b)) => a == b,
75            (Self::Array(a), Self::Array(b)) => a == b,
76            (Self::Struct(a), Self::Struct(b)) => a == b,
77            (Self::Map(a), Self::Map(b)) => a == b,
78            _ => false,
79        }
80    }
81}
82
83impl Eq for TypedValue {}
84
85impl std::hash::Hash for TypedValue {
86    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
87        std::mem::discriminant(self).hash(state);
88        match self {
89            Self::Null => {}
90            Self::Bool(v) => v.hash(state),
91            Self::Int8(v) => v.hash(state),
92            Self::Int16(v) => v.hash(state),
93            Self::Int32(v) => v.hash(state),
94            Self::Int64(v) => v.hash(state),
95            Self::Int128(v) => v.hash(state),
96            Self::UInt8(v) => v.hash(state),
97            Self::UInt16(v) => v.hash(state),
98            Self::UInt32(v) => v.hash(state),
99            Self::UInt64(v) => v.hash(state),
100            Self::Float(v) => v.to_bits().hash(state),
101            Self::Double(v) => v.to_bits().hash(state),
102            Self::Date(v) => v.hash(state),
103            Self::Timestamp(v) => v.hash(state),
104            Self::TimestampSec(v) => v.hash(state),
105            Self::TimestampMs(v) => v.hash(state),
106            Self::TimestampNs(v) => v.hash(state),
107            Self::TimestampTz(v) => v.hash(state),
108            Self::Interval(v) => v.hash(state),
109            Self::String(v) => v.hash(state),
110            Self::Blob(v) => v.hash(state),
111            Self::Uuid(v) => v.hash(state),
112            Self::InternalId(v) => v.hash(state),
113            Self::Serial(v) => v.hash(state),
114            Self::List(v) => v.hash(state),
115            Self::Array(v) => v.hash(state),
116            Self::Struct(v) => v.hash(state),
117            Self::Map(v) => v.hash(state),
118        }
119    }
120}
121
122impl TypedValue {
123    /// Infer the LogicalType from the runtime value.
124    pub fn logical_type(&self) -> LogicalType {
125        match self {
126            Self::Null => LogicalType::Any,
127            Self::Bool(_) => LogicalType::Bool,
128            Self::Int8(_) => LogicalType::Int8,
129            Self::Int16(_) => LogicalType::Int16,
130            Self::Int32(_) => LogicalType::Int32,
131            Self::Int64(_) => LogicalType::Int64,
132            Self::Int128(_) => LogicalType::Int128,
133            Self::UInt8(_) => LogicalType::UInt8,
134            Self::UInt16(_) => LogicalType::UInt16,
135            Self::UInt32(_) => LogicalType::UInt32,
136            Self::UInt64(_) => LogicalType::UInt64,
137            Self::Float(_) => LogicalType::Float,
138            Self::Double(_) => LogicalType::Double,
139            Self::Date(_) => LogicalType::Date,
140            Self::Timestamp(_) => LogicalType::Timestamp,
141            Self::TimestampSec(_) => LogicalType::TimestampSec,
142            Self::TimestampMs(_) => LogicalType::TimestampMs,
143            Self::TimestampNs(_) => LogicalType::TimestampNs,
144            Self::TimestampTz(_) => LogicalType::TimestampTz,
145            Self::Interval(_) => LogicalType::Interval,
146            Self::String(_) => LogicalType::String,
147            Self::Blob(_) => LogicalType::Blob,
148            Self::Uuid(_) => LogicalType::Uuid,
149            Self::InternalId(_) => LogicalType::InternalId,
150            Self::Serial(_) => LogicalType::Serial,
151            Self::List(_) => LogicalType::List(Box::new(LogicalType::Any)),
152            Self::Array(_) => LogicalType::List(Box::new(LogicalType::Any)),
153            Self::Struct(_) => LogicalType::Any,
154            Self::Map(_) => LogicalType::Map {
155                key: Box::new(LogicalType::Any),
156                value: Box::new(LogicalType::Any),
157            },
158        }
159    }
160
161    pub fn is_null(&self) -> bool {
162        matches!(self, Self::Null)
163    }
164
165    /// Try to extract a boolean.
166    pub fn as_bool(&self) -> Option<bool> {
167        match self {
168            Self::Bool(v) => Some(*v),
169            _ => None,
170        }
171    }
172
173    /// Try to extract an i64 (works for all integer types that fit).
174    pub fn as_i64(&self) -> Option<i64> {
175        match self {
176            Self::Int8(v) => Some(*v as i64),
177            Self::Int16(v) => Some(*v as i64),
178            Self::Int32(v) => Some(*v as i64),
179            Self::Int64(v) | Self::Date(v) | Self::Timestamp(v) | Self::Serial(v) => Some(*v),
180            Self::UInt8(v) => Some(*v as i64),
181            Self::UInt16(v) => Some(*v as i64),
182            Self::UInt32(v) => Some(*v as i64),
183            _ => None,
184        }
185    }
186
187    /// Try to extract an f64 (works for Float and Double).
188    pub fn as_f64(&self) -> Option<f64> {
189        match self {
190            Self::Float(v) => Some(*v as f64),
191            Self::Double(v) => Some(*v),
192            _ => None,
193        }
194    }
195
196    /// Try to extract a string reference.
197    pub fn as_str(&self) -> Option<&str> {
198        match self {
199            Self::String(s) | Self::Uuid(s) => Some(s.as_str()),
200            _ => None,
201        }
202    }
203
204    /// Try to extract an InternalId.
205    pub fn as_internal_id(&self) -> Option<InternalId> {
206        match self {
207            Self::InternalId(id) => Some(*id),
208            _ => None,
209        }
210    }
211}
212
213impl std::fmt::Display for TypedValue {
214    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
215        match self {
216            Self::Null => write!(f, "NULL"),
217            Self::Bool(v) => write!(f, "{v}"),
218            Self::Int8(v) => write!(f, "{v}"),
219            Self::Int16(v) => write!(f, "{v}"),
220            Self::Int32(v) => write!(f, "{v}"),
221            Self::Int64(v) | Self::Serial(v) => write!(f, "{v}"),
222            Self::Int128(v) => write!(f, "{v}"),
223            Self::UInt8(v) => write!(f, "{v}"),
224            Self::UInt16(v) => write!(f, "{v}"),
225            Self::UInt32(v) => write!(f, "{v}"),
226            Self::UInt64(v) => write!(f, "{v}"),
227            Self::Float(v) => write!(f, "{v}"),
228            Self::Double(v) => write!(f, "{v}"),
229            Self::Date(v) => write!(f, "{v}"),
230            Self::Timestamp(v)
231            | Self::TimestampSec(v)
232            | Self::TimestampMs(v)
233            | Self::TimestampNs(v)
234            | Self::TimestampTz(v) => write!(f, "{v}"),
235            Self::Interval(iv) => write!(f, "{iv}"),
236            Self::String(s) | Self::Uuid(s) => write!(f, "{s}"),
237            Self::Blob(b) => write!(f, "\\x{}", hex_encode(b)),
238            Self::InternalId(id) => write!(f, "{id}"),
239            Self::List(items) | Self::Array(items) => {
240                write!(f, "[")?;
241                for (i, item) in items.iter().enumerate() {
242                    if i > 0 {
243                        write!(f, ",")?;
244                    }
245                    write!(f, "{item}")?;
246                }
247                write!(f, "]")
248            }
249            Self::Struct(fields) => {
250                write!(f, "{{")?;
251                for (i, (name, val)) in fields.iter().enumerate() {
252                    if i > 0 {
253                        write!(f, ",")?;
254                    }
255                    write!(f, "{name}: {val}")?;
256                }
257                write!(f, "}}")
258            }
259            Self::Map(entries) => {
260                write!(f, "{{")?;
261                for (i, (k, v)) in entries.iter().enumerate() {
262                    if i > 0 {
263                        write!(f, ",")?;
264                    }
265                    write!(f, "{k}={v}")?;
266                }
267                write!(f, "}}")
268            }
269        }
270    }
271}
272
273fn hex_encode(bytes: &[u8]) -> String {
274    bytes.iter().map(|b| format!("{b:02x}")).collect()
275}
276
277// ---------------------------------------------------------------------------
278// serde_json conversions
279// ---------------------------------------------------------------------------
280
281impl From<serde_json::Value> for TypedValue {
282    fn from(v: serde_json::Value) -> Self {
283        match v {
284            serde_json::Value::Null => TypedValue::Null,
285            serde_json::Value::Bool(b) => TypedValue::Bool(b),
286            serde_json::Value::Number(n) => {
287                if let Some(i) = n.as_i64() {
288                    TypedValue::Int64(i)
289                } else if let Some(u) = n.as_u64() {
290                    TypedValue::UInt64(u)
291                } else if let Some(f) = n.as_f64() {
292                    TypedValue::Double(f)
293                } else {
294                    TypedValue::Null
295                }
296            }
297            serde_json::Value::String(s) => TypedValue::String(SmolStr::new(s)),
298            serde_json::Value::Array(arr) => {
299                TypedValue::List(arr.into_iter().map(TypedValue::from).collect())
300            }
301            serde_json::Value::Object(map) => TypedValue::Struct(
302                map.into_iter()
303                    .map(|(k, v)| (SmolStr::new(k), TypedValue::from(v)))
304                    .collect(),
305            ),
306        }
307    }
308}
309
310impl From<TypedValue> for serde_json::Value {
311    fn from(v: TypedValue) -> Self {
312        match v {
313            TypedValue::Null => serde_json::Value::Null,
314            TypedValue::Bool(b) => serde_json::Value::Bool(b),
315            TypedValue::Int8(n) => serde_json::Value::from(n),
316            TypedValue::Int16(n) => serde_json::Value::from(n),
317            TypedValue::Int32(n) => serde_json::Value::from(n),
318            TypedValue::Int64(n)
319            | TypedValue::Date(n)
320            | TypedValue::Timestamp(n)
321            | TypedValue::TimestampSec(n)
322            | TypedValue::TimestampMs(n)
323            | TypedValue::TimestampNs(n)
324            | TypedValue::TimestampTz(n)
325            | TypedValue::Serial(n) => serde_json::Value::from(n),
326            TypedValue::Int128(n) => serde_json::Value::from(n.to_string()),
327            TypedValue::UInt8(n) => serde_json::Value::from(n),
328            TypedValue::UInt16(n) => serde_json::Value::from(n),
329            TypedValue::UInt32(n) => serde_json::Value::from(n),
330            TypedValue::UInt64(n) => serde_json::Value::from(n),
331            TypedValue::Float(f) => serde_json::Number::from_f64(f as f64)
332                .map(serde_json::Value::Number)
333                .unwrap_or(serde_json::Value::Null),
334            TypedValue::Double(f) => serde_json::Number::from_f64(f)
335                .map(serde_json::Value::Number)
336                .unwrap_or(serde_json::Value::Null),
337            TypedValue::Interval(iv) => serde_json::Value::from(iv.to_string()),
338            TypedValue::String(s) | TypedValue::Uuid(s) => serde_json::Value::String(s.to_string()),
339            TypedValue::Blob(b) => serde_json::Value::String(format!("\\x{}", hex_encode(&b))),
340            TypedValue::InternalId(id) => serde_json::Value::from(id.to_string()),
341            TypedValue::List(items) | TypedValue::Array(items) => {
342                serde_json::Value::Array(items.into_iter().map(serde_json::Value::from).collect())
343            }
344            TypedValue::Struct(fields) => {
345                let map: serde_json::Map<String, serde_json::Value> = fields
346                    .into_iter()
347                    .map(|(k, v)| (k.to_string(), serde_json::Value::from(v)))
348                    .collect();
349                serde_json::Value::Object(map)
350            }
351            TypedValue::Map(entries) => {
352                let map: serde_json::Map<String, serde_json::Value> = entries
353                    .into_iter()
354                    .map(|(k, v)| (k.to_string(), serde_json::Value::from(v)))
355                    .collect();
356                serde_json::Value::Object(map)
357            }
358        }
359    }
360}
361
362/// Convert a flat JSON object into a `HashMap<SmolStr, TypedValue>`.
363///
364/// Only top-level keys are extracted. Non-object values return an empty map.
365pub fn json_object_to_map(
366    value: serde_json::Value,
367) -> std::collections::HashMap<SmolStr, TypedValue> {
368    match value {
369        serde_json::Value::Object(map) => map
370            .into_iter()
371            .map(|(k, v)| (SmolStr::new(k), TypedValue::from(v)))
372            .collect(),
373        _ => std::collections::HashMap::new(),
374    }
375}
376
377/// Parse a JSON string into a `HashMap<SmolStr, TypedValue>`.
378///
379/// Returns `Err` if the string is not valid JSON or is not an object.
380pub fn json_str_to_map(
381    s: &str,
382) -> Result<std::collections::HashMap<SmolStr, TypedValue>, serde_json::Error> {
383    let value: serde_json::Value = serde_json::from_str(s)?;
384    Ok(json_object_to_map(value))
385}
386
387#[cfg(test)]
388mod tests {
389    use super::*;
390
391    #[test]
392    fn null() {
393        let v = TypedValue::Null;
394        assert!(v.is_null());
395        assert_eq!(v.to_string(), "NULL");
396    }
397
398    #[test]
399    fn bool_round_trip() {
400        let v = TypedValue::Bool(true);
401        assert_eq!(v.as_bool(), Some(true));
402        assert!(!v.is_null());
403    }
404
405    #[test]
406    fn integer_round_trips() {
407        assert_eq!(TypedValue::Int8(42).as_i64(), Some(42));
408        assert_eq!(TypedValue::Int16(-1000).as_i64(), Some(-1000));
409        assert_eq!(TypedValue::Int32(100_000).as_i64(), Some(100_000));
410        assert_eq!(TypedValue::Int64(i64::MAX).as_i64(), Some(i64::MAX));
411        assert_eq!(TypedValue::UInt8(255).as_i64(), Some(255));
412        assert_eq!(TypedValue::UInt16(65535).as_i64(), Some(65535));
413        assert_eq!(TypedValue::UInt32(u32::MAX).as_i64(), Some(u32::MAX as i64));
414    }
415
416    #[test]
417    fn float_round_trips() {
418        let f = TypedValue::Float(3.14);
419        assert!((f.as_f64().unwrap() - 3.14f32 as f64).abs() < 1e-6);
420
421        let d = TypedValue::Double(2.718281828);
422        assert!((d.as_f64().unwrap() - 2.718281828).abs() < 1e-9);
423    }
424
425    #[test]
426    fn string_round_trip() {
427        let v = TypedValue::String(SmolStr::new("hello"));
428        assert_eq!(v.as_str(), Some("hello"));
429        assert_eq!(v.to_string(), "hello");
430    }
431
432    #[test]
433    fn internal_id_round_trip() {
434        let id = InternalId::new(1, 42);
435        let v = TypedValue::InternalId(id);
436        assert_eq!(v.as_internal_id(), Some(id));
437    }
438
439    #[test]
440    fn list_display() {
441        let v = TypedValue::List(vec![
442            TypedValue::Int64(1),
443            TypedValue::Int64(2),
444            TypedValue::Int64(3),
445        ]);
446        assert_eq!(v.to_string(), "[1,2,3]");
447    }
448
449    #[test]
450    fn struct_display() {
451        let v = TypedValue::Struct(vec![
452            (
453                SmolStr::new("name"),
454                TypedValue::String(SmolStr::new("Alice")),
455            ),
456            (SmolStr::new("age"), TypedValue::Int64(30)),
457        ]);
458        assert_eq!(v.to_string(), "{name: Alice,age: 30}");
459    }
460
461    #[test]
462    fn logical_type_inference() {
463        assert_eq!(TypedValue::Null.logical_type(), LogicalType::Any);
464        assert_eq!(TypedValue::Bool(true).logical_type(), LogicalType::Bool);
465        assert_eq!(TypedValue::Int8(1).logical_type(), LogicalType::Int8);
466        assert_eq!(TypedValue::Int16(1).logical_type(), LogicalType::Int16);
467        assert_eq!(TypedValue::Int32(1).logical_type(), LogicalType::Int32);
468        assert_eq!(TypedValue::Int64(42).logical_type(), LogicalType::Int64);
469        assert_eq!(TypedValue::Float(1.0).logical_type(), LogicalType::Float);
470        assert_eq!(TypedValue::Double(3.14).logical_type(), LogicalType::Double);
471        assert_eq!(
472            TypedValue::String(SmolStr::new("hi")).logical_type(),
473            LogicalType::String
474        );
475        assert_eq!(TypedValue::Serial(1).logical_type(), LogicalType::Serial);
476    }
477
478    #[test]
479    fn wrong_type_returns_none() {
480        let v = TypedValue::String(SmolStr::new("hello"));
481        assert_eq!(v.as_bool(), None);
482        assert_eq!(v.as_i64(), None);
483        assert_eq!(v.as_f64(), None);
484    }
485
486    #[test]
487    fn clone_and_eq() {
488        let a = TypedValue::List(vec![TypedValue::Int64(1)]);
489        let b = a.clone();
490        assert_eq!(a, b);
491    }
492
493    // ---- JSON conversion tests ----
494
495    #[test]
496    fn json_null() {
497        let v: TypedValue = serde_json::Value::Null.into();
498        assert_eq!(v, TypedValue::Null);
499    }
500
501    #[test]
502    fn json_bool() {
503        let v: TypedValue = serde_json::json!(true).into();
504        assert_eq!(v, TypedValue::Bool(true));
505    }
506
507    #[test]
508    fn json_integer() {
509        let v: TypedValue = serde_json::json!(42).into();
510        assert_eq!(v, TypedValue::Int64(42));
511    }
512
513    #[test]
514    fn json_negative_integer() {
515        let v: TypedValue = serde_json::json!(-10).into();
516        assert_eq!(v, TypedValue::Int64(-10));
517    }
518
519    #[test]
520    fn json_float() {
521        let v: TypedValue = serde_json::json!(3.14).into();
522        assert_eq!(v, TypedValue::Double(3.14));
523    }
524
525    #[test]
526    fn json_string() {
527        let v: TypedValue = serde_json::json!("hello").into();
528        assert_eq!(v, TypedValue::String(SmolStr::new("hello")));
529    }
530
531    #[test]
532    fn json_array() {
533        let v: TypedValue = serde_json::json!([1, "two", true]).into();
534        assert_eq!(
535            v,
536            TypedValue::List(vec![
537                TypedValue::Int64(1),
538                TypedValue::String(SmolStr::new("two")),
539                TypedValue::Bool(true),
540            ])
541        );
542    }
543
544    #[test]
545    fn json_object() {
546        let v: TypedValue = serde_json::json!({"name": "Alice", "age": 30}).into();
547        if let TypedValue::Struct(fields) = &v {
548            assert_eq!(fields.len(), 2);
549        } else {
550            panic!("expected Struct");
551        }
552    }
553
554    #[test]
555    fn json_roundtrip() {
556        let original = serde_json::json!({"x": 42, "y": "hello", "z": [1, 2]});
557        let typed: TypedValue = original.clone().into();
558        let back: serde_json::Value = typed.into();
559        assert_eq!(original, back);
560    }
561
562    #[test]
563    fn json_object_to_map_flat() {
564        let map = super::json_object_to_map(serde_json::json!({
565            "DATA_DIR": "/data",
566            "PORT": 8080,
567            "DEBUG": true
568        }));
569        assert_eq!(map.len(), 3);
570        assert_eq!(
571            map.get("DATA_DIR"),
572            Some(&TypedValue::String(SmolStr::new("/data")))
573        );
574        assert_eq!(map.get("PORT"), Some(&TypedValue::Int64(8080)));
575        assert_eq!(map.get("DEBUG"), Some(&TypedValue::Bool(true)));
576    }
577
578    #[test]
579    fn json_str_to_map_valid() {
580        let map = super::json_str_to_map(r#"{"key": "value", "num": 42}"#).unwrap();
581        assert_eq!(map.len(), 2);
582        assert_eq!(
583            map.get("key"),
584            Some(&TypedValue::String(SmolStr::new("value")))
585        );
586        assert_eq!(map.get("num"), Some(&TypedValue::Int64(42)));
587    }
588
589    #[test]
590    fn json_str_to_map_invalid() {
591        assert!(super::json_str_to_map("not json").is_err());
592    }
593
594    #[test]
595    fn json_str_to_map_non_object() {
596        // Arrays and scalars return an empty map.
597        let map = super::json_str_to_map("[1,2,3]").unwrap();
598        assert!(map.is_empty());
599    }
600}