Skip to main content

akar_common/
types.rs

1//! Core type system: LogicalType, PhysicalType, Value, InternalID, date/time types.
2
3use serde::{Deserialize, Serialize};
4
5/// Logical type identifiers for Akar's type system.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
7#[repr(u8)]
8pub enum LogicalTypeID {
9    Any = 0,
10    Node = 10,
11    Rel = 11,
12    RecursiveRel = 12,
13    Serial = 13,
14    Bool = 22,
15    Int64 = 23,
16    Int32 = 24,
17    Int16 = 25,
18    Int8 = 26,
19    UInt64 = 27,
20    UInt32 = 28,
21    UInt16 = 29,
22    UInt8 = 30,
23    Int128 = 31,
24    Double = 32,
25    Float = 33,
26    Date = 34,
27    Timestamp = 35,
28    TimestampSec = 36,
29    TimestampMs = 37,
30    TimestampNs = 38,
31    TimestampTz = 39,
32    Interval = 40,
33    Decimal = 41,
34    InternalID = 42,
35    UInt128 = 43,
36    Json = 44,
37    Time = 45,
38    String = 50,
39    Blob = 51,
40    List = 52,
41    Array = 53,
42    Struct = 54,
43    Map = 55,
44    Union = 56,
45    Uuid = 59,
46}
47
48/// Physical type identifiers for in-memory representation.
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
50#[repr(u8)]
51pub enum PhysicalTypeID {
52    Any = 0,
53    Bool = 1,
54    Int64 = 2,
55    Int32 = 3,
56    Int16 = 4,
57    Int8 = 5,
58    UInt64 = 6,
59    UInt32 = 7,
60    UInt16 = 8,
61    UInt8 = 9,
62    Int128 = 10,
63    Double = 11,
64    Float = 12,
65    Interval = 13,
66    String = 14,
67    Struct = 15,
68    List = 16,
69    Array = 17,
70    Blob = 20,
71}
72
73/// A 4-byte aligned, 8-byte internal node/rel identifier.
74#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
75pub struct InternalID {
76    pub table_id: u64,
77    pub offset: u64,
78}
79
80/// Date representation (days since epoch).
81#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
82pub struct Date(pub i32);
83
84/// Timestamp representation (microseconds since epoch).
85#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
86pub struct Timestamp(pub i64);
87
88/// Timestamp with timezone.
89#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
90pub struct TimestampTZ(pub i64);
91
92/// Interval (duration).
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
94pub struct Interval {
95    pub months: i32,
96    pub days: i32,
97    pub micros: i64,
98}
99
100/// A Akar value — the runtime representation of any data type.
101#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
102pub enum Value {
103    Null,
104    Bool(bool),
105    Int64(i64),
106    Int32(i32),
107    Int16(i16),
108    Int8(i8),
109    UInt64(u64),
110    UInt32(u32),
111    UInt16(u16),
112    UInt8(u8),
113    Int128(i128),
114    Double(f64),
115    Float(f32),
116    String(String),
117    Blob(Vec<u8>),
118    Date(Date),
119    Timestamp(Timestamp),
120    TimestampTz(TimestampTZ),
121    TimestampNs(Timestamp),
122    TimestampMs(Timestamp),
123    TimestampSec(Timestamp),
124    Interval(Interval),
125    InternalID(InternalID),
126    UInt128(u128),
127    Json(serde_json::Value),
128    DTime(i64),
129    Union(String, Box<Value>),
130    List(Vec<Value>),
131    Map(Vec<(Value, Value)>),
132    Struct(Vec<(String, Value)>),
133}
134
135// --- From implementations for Value ---
136
137impl From<bool> for Value {
138    #[inline(always)]
139    fn from(v: bool) -> Self {
140        Value::Bool(v)
141    }
142}
143impl From<i64> for Value {
144    #[inline(always)]
145    fn from(v: i64) -> Self {
146        Value::Int64(v)
147    }
148}
149impl From<i32> for Value {
150    #[inline(always)]
151    fn from(v: i32) -> Self {
152        Value::Int32(v)
153    }
154}
155impl From<i16> for Value {
156    #[inline(always)]
157    fn from(v: i16) -> Self {
158        Value::Int16(v)
159    }
160}
161impl From<i8> for Value {
162    #[inline(always)]
163    fn from(v: i8) -> Self {
164        Value::Int8(v)
165    }
166}
167impl From<u64> for Value {
168    #[inline(always)]
169    fn from(v: u64) -> Self {
170        Value::UInt64(v)
171    }
172}
173impl From<u32> for Value {
174    #[inline(always)]
175    fn from(v: u32) -> Self {
176        Value::UInt32(v)
177    }
178}
179impl From<u16> for Value {
180    #[inline(always)]
181    fn from(v: u16) -> Self {
182        Value::UInt16(v)
183    }
184}
185impl From<u8> for Value {
186    #[inline(always)]
187    fn from(v: u8) -> Self {
188        Value::UInt8(v)
189    }
190}
191impl From<f64> for Value {
192    #[inline(always)]
193    fn from(v: f64) -> Self {
194        Value::Double(v)
195    }
196}
197impl From<f32> for Value {
198    #[inline(always)]
199    fn from(v: f32) -> Self {
200        Value::Float(v)
201    }
202}
203impl From<String> for Value {
204    #[inline(always)]
205    fn from(v: String) -> Self {
206        Value::String(v)
207    }
208}
209impl From<&str> for Value {
210    #[inline(always)]
211    fn from(v: &str) -> Self {
212        Value::String(v.to_string())
213    }
214}
215impl From<Date> for Value {
216    #[inline(always)]
217    fn from(v: Date) -> Self {
218        Value::Date(v)
219    }
220}
221impl From<Timestamp> for Value {
222    #[inline(always)]
223    fn from(v: Timestamp) -> Self {
224        Value::Timestamp(v)
225    }
226}
227impl From<Interval> for Value {
228    #[inline(always)]
229    fn from(v: Interval) -> Self {
230        Value::Interval(v)
231    }
232}
233impl From<InternalID> for Value {
234    #[inline(always)]
235    fn from(v: InternalID) -> Self {
236        Value::InternalID(v)
237    }
238}
239impl From<u128> for Value {
240    #[inline(always)]
241    fn from(v: u128) -> Self {
242        Value::UInt128(v)
243    }
244}
245impl From<serde_json::Value> for Value {
246    #[inline(always)]
247    fn from(v: serde_json::Value) -> Self {
248        Value::Json(v)
249    }
250}
251
252impl Value {
253    /// Get the LogicalTypeID corresponding to this Value.
254    pub fn logical_type(&self) -> LogicalTypeID {
255        match self {
256            Value::Null => LogicalTypeID::Any,
257            Value::Bool(_) => LogicalTypeID::Bool,
258            Value::Int64(_) => LogicalTypeID::Int64,
259            Value::Int32(_) => LogicalTypeID::Int32,
260            Value::Int16(_) => LogicalTypeID::Int16,
261            Value::Int8(_) => LogicalTypeID::Int8,
262            Value::UInt64(_) => LogicalTypeID::UInt64,
263            Value::UInt32(_) => LogicalTypeID::UInt32,
264            Value::UInt16(_) => LogicalTypeID::UInt16,
265            Value::UInt8(_) => LogicalTypeID::UInt8,
266            Value::Double(_) => LogicalTypeID::Double,
267            Value::Float(_) => LogicalTypeID::Float,
268            Value::String(_) => LogicalTypeID::String,
269            Value::Blob(_) => LogicalTypeID::Blob,
270            Value::Date(_) => LogicalTypeID::Date,
271            Value::Timestamp(_) => LogicalTypeID::Timestamp,
272            Value::Interval(_) => LogicalTypeID::Interval,
273            Value::InternalID(_) => LogicalTypeID::InternalID,
274            Value::UInt128(_) => LogicalTypeID::UInt128,
275            Value::Json(_) => LogicalTypeID::Json,
276            Value::DTime(_) => LogicalTypeID::Time,
277            Value::Union(_, _) => LogicalTypeID::Union,
278            Value::List(_) => LogicalTypeID::List,
279            Value::Map(_) => LogicalTypeID::Map,
280            Value::Struct(_) => LogicalTypeID::Struct,
281            Value::Int128(_) => LogicalTypeID::Int128,
282            Value::TimestampTz(_) => LogicalTypeID::TimestampTz,
283            Value::TimestampNs(_) => LogicalTypeID::TimestampNs,
284            Value::TimestampMs(_) => LogicalTypeID::TimestampMs,
285            Value::TimestampSec(_) => LogicalTypeID::TimestampSec,
286        }
287    }
288
289    /// Get the PhysicalTypeID for this Value's logical type.
290    #[inline(always)]
291    pub fn physical_type(&self) -> PhysicalTypeID {
292        physical_type_from_logical(self.logical_type())
293    }
294}
295
296/// Map a LogicalTypeID to its corresponding PhysicalTypeID.
297#[inline]
298pub const fn physical_type_from_logical(logical: LogicalTypeID) -> PhysicalTypeID {
299    match logical {
300        LogicalTypeID::Any => PhysicalTypeID::Any,
301        LogicalTypeID::Bool => PhysicalTypeID::Bool,
302        LogicalTypeID::Int64 | LogicalTypeID::Serial => PhysicalTypeID::Int64,
303        LogicalTypeID::Int32 => PhysicalTypeID::Int32,
304        LogicalTypeID::Int16 => PhysicalTypeID::Int16,
305        LogicalTypeID::Int8 => PhysicalTypeID::Int8,
306        LogicalTypeID::UInt64 => PhysicalTypeID::UInt64,
307        LogicalTypeID::UInt32 => PhysicalTypeID::UInt32,
308        LogicalTypeID::UInt16 => PhysicalTypeID::UInt16,
309        LogicalTypeID::UInt8 => PhysicalTypeID::UInt8,
310        LogicalTypeID::Double => PhysicalTypeID::Double,
311        LogicalTypeID::Float => PhysicalTypeID::Float,
312        LogicalTypeID::Int128 | LogicalTypeID::Decimal | LogicalTypeID::UInt128 => PhysicalTypeID::Int128,
313        LogicalTypeID::Date
314        | LogicalTypeID::Timestamp
315        | LogicalTypeID::TimestampSec
316        | LogicalTypeID::TimestampMs
317        | LogicalTypeID::TimestampNs
318        | LogicalTypeID::TimestampTz
319        | LogicalTypeID::Time => PhysicalTypeID::Int64,
320        LogicalTypeID::Interval => PhysicalTypeID::Interval,
321        LogicalTypeID::String | LogicalTypeID::Blob | LogicalTypeID::Uuid | LogicalTypeID::Json => {
322            PhysicalTypeID::String
323        }
324        LogicalTypeID::InternalID => PhysicalTypeID::Struct,
325        LogicalTypeID::List | LogicalTypeID::Array => PhysicalTypeID::List,
326        LogicalTypeID::Map | LogicalTypeID::Struct | LogicalTypeID::Union => PhysicalTypeID::Struct,
327        LogicalTypeID::Node | LogicalTypeID::Rel | LogicalTypeID::RecursiveRel => PhysicalTypeID::Struct,
328    }
329}
330
331impl Date {
332    /// Create a Date from epoch days.
333    #[inline(always)]
334    pub fn from_days_since_epoch(days: i32) -> Self {
335        Date(days)
336    }
337
338    /// Get the days since epoch.
339    #[inline(always)]
340    pub fn days_since_epoch(&self) -> i32 {
341        self.0
342    }
343}
344
345impl Timestamp {
346    /// Create a Timestamp from epoch microseconds.
347    #[inline(always)]
348    pub fn from_micros_since_epoch(micros: i64) -> Self {
349        Timestamp(micros)
350    }
351
352    /// Get the microseconds since epoch.
353    #[inline(always)]
354    pub fn micros_since_epoch(&self) -> i64 {
355        self.0
356    }
357}
358
359impl Interval {
360    pub fn new(months: i32, days: i32, micros: i64) -> Self {
361        Interval { months, days, micros }
362    }
363}
364
365/// Extract a `Vec<f64>` from a `Value` (expects `Value::List` of numbers).
366///
367/// Numeric list items (Double/Int64/Int32/Float) are coerced to `f64`; any
368/// other item or a non-List value produces an error. Shared by the vector
369/// extension and the vector-index write path (DRY, P51.41).
370pub fn extract_f64_list(val: &Value) -> Result<Vec<f64>, String> {
371    match val {
372        Value::List(items) => {
373            let mut result = Vec::with_capacity(items.len());
374            for item in items {
375                match item {
376                    Value::Double(d) => result.push(*d),
377                    Value::Int64(i) => result.push(*i as f64),
378                    Value::Int32(i) => result.push(*i as f64),
379                    Value::Float(f) => result.push(*f as f64),
380                    other => {
381                        return Err(format!("Expected numeric value in vector list, got {:?}", other));
382                    }
383                }
384            }
385            Ok(result)
386        }
387        other => Err(format!("Expected List value for vector, got {:?}", other)),
388    }
389}
390
391/// Render a [`Value`] as the canonical string key used by hash indexes on
392/// primary keys.
393///
394/// Single source of truth shared by `akar-storage` (index build, lookup,
395/// delete) and `akar-main` (DDL/DML lookup paths) so every layer produces
396/// byte-identical keys. Non-scalar values fall back to their `Debug` text.
397pub fn pk_value_to_string(v: &Value) -> String {
398    match v {
399        Value::Null => "null".to_string(),
400        Value::Bool(b) => b.to_string(),
401        Value::Int64(i) => i.to_string(),
402        Value::Int32(i) => i.to_string(),
403        Value::Int16(i) => i.to_string(),
404        Value::Int8(i) => i.to_string(),
405        Value::UInt64(u) => u.to_string(),
406        Value::UInt32(u) => u.to_string(),
407        Value::UInt16(u) => u.to_string(),
408        Value::UInt8(u) => u.to_string(),
409        Value::Double(f) => f.to_string(),
410        Value::Float(f) => f.to_string(),
411        Value::String(s) => s.clone(),
412        Value::Date(d) => format!("Date({})", d.0),
413        Value::Timestamp(ts) => format!("Timestamp({})", ts.0),
414        other => format!("{other:?}"),
415    }
416}
417
418/// Render a [`Value`] as a CSV cell: `Null` becomes an empty field, anything
419/// else uses the canonical scalar rendering ([`pk_value_to_string`]).
420pub fn value_to_csv_string(v: &Value) -> String {
421    match v {
422        Value::Null => String::new(),
423        other => pk_value_to_string(other),
424    }
425}
426
427#[cfg(test)]
428mod tests {
429    use super::*;
430
431    #[test]
432    fn test_value_from_primitives() {
433        assert_eq!(Value::from(true), Value::Bool(true));
434        assert_eq!(Value::from(42i64), Value::Int64(42));
435        assert_eq!(Value::from(42i32), Value::Int32(42));
436        assert_eq!(Value::from(std::f64::consts::PI), Value::Double(std::f64::consts::PI));
437        assert_eq!(Value::from("hello"), Value::String("hello".into()));
438        assert_eq!(Value::from(Date(100)), Value::Date(Date(100)));
439    }
440
441    #[test]
442    fn test_value_logical_type() {
443        assert_eq!(Value::Null.logical_type(), LogicalTypeID::Any);
444        assert_eq!(Value::Bool(true).logical_type(), LogicalTypeID::Bool);
445        assert_eq!(Value::Int64(1).logical_type(), LogicalTypeID::Int64);
446        assert_eq!(Value::String("a".into()).logical_type(), LogicalTypeID::String);
447        assert_eq!(Value::List(vec![]).logical_type(), LogicalTypeID::List);
448    }
449
450    #[test]
451    fn test_extract_f64_list() {
452        assert_eq!(
453            extract_f64_list(&Value::List(vec![
454                Value::Double(1.5),
455                Value::Int64(2),
456                Value::Int32(3),
457                Value::Float(4.0),
458            ]))
459            .unwrap(),
460            vec![1.5, 2.0, 3.0, 4.0]
461        );
462        assert!(extract_f64_list(&Value::String("x".into())).is_err());
463        assert!(extract_f64_list(&Value::List(vec![Value::Bool(true)])).is_err());
464        assert_eq!(extract_f64_list(&Value::List(vec![])).unwrap(), Vec::<f64>::new());
465    }
466
467    #[test]
468    fn test_physical_type_from_logical() {
469        assert_eq!(physical_type_from_logical(LogicalTypeID::Bool), PhysicalTypeID::Bool);
470        assert_eq!(physical_type_from_logical(LogicalTypeID::Int64), PhysicalTypeID::Int64);
471        assert_eq!(
472            physical_type_from_logical(LogicalTypeID::String),
473            PhysicalTypeID::String
474        );
475        assert_eq!(physical_type_from_logical(LogicalTypeID::Date), PhysicalTypeID::Int64);
476        assert_eq!(physical_type_from_logical(LogicalTypeID::List), PhysicalTypeID::List);
477    }
478
479    #[test]
480    fn test_date_roundtrip() {
481        let d = Date::from_days_since_epoch(20000);
482        assert_eq!(d.days_since_epoch(), 20000);
483    }
484
485    #[test]
486    fn test_timestamp_roundtrip() {
487        let ts = Timestamp::from_micros_since_epoch(1_700_000_000_000_000);
488        assert_eq!(ts.micros_since_epoch(), 1_700_000_000_000_000);
489    }
490
491    #[test]
492    fn test_internal_id() {
493        let id = InternalID {
494            table_id: 5,
495            offset: 100,
496        };
497        assert_eq!(id.table_id, 5);
498        assert_eq!(id.offset, 100);
499    }
500
501    #[test]
502    fn test_value_from_list() {
503        let list = Value::List(vec![Value::Int64(1), Value::Int64(2)]);
504        assert_eq!(list.logical_type(), LogicalTypeID::List);
505    }
506
507    #[test]
508    fn test_value_physical_type() {
509        let v: Value = 42i64.into();
510        assert_eq!(v.physical_type(), PhysicalTypeID::Int64);
511        let v: Value = std::f64::consts::PI.into();
512        assert_eq!(v.physical_type(), PhysicalTypeID::Double);
513        let v: Value = "test".into();
514        assert_eq!(v.physical_type(), PhysicalTypeID::String);
515    }
516
517    #[test]
518    fn test_pk_value_to_string_canonical_keys() {
519        assert_eq!(pk_value_to_string(&Value::Null), "null");
520        assert_eq!(pk_value_to_string(&Value::Bool(true)), "true");
521        assert_eq!(pk_value_to_string(&Value::Int64(-7)), "-7");
522        assert_eq!(pk_value_to_string(&Value::Int32(9)), "9");
523        // Narrow integer/float widths must render as bare numerics (not
524        // Debug text) so keys built here match lookups in every layer.
525        assert_eq!(pk_value_to_string(&Value::Int16(5)), "5");
526        assert_eq!(pk_value_to_string(&Value::Int8(3)), "3");
527        assert_eq!(pk_value_to_string(&Value::UInt64(u64::MAX)), u64::MAX.to_string());
528        assert_eq!(pk_value_to_string(&Value::UInt32(11)), "11");
529        assert_eq!(pk_value_to_string(&Value::UInt16(12)), "12");
530        assert_eq!(pk_value_to_string(&Value::UInt8(13)), "13");
531        assert_eq!(pk_value_to_string(&Value::Double(1.5)), "1.5");
532        assert_eq!(pk_value_to_string(&Value::Float(0.25)), "0.25");
533        assert_eq!(pk_value_to_string(&Value::String("k".into())), "k");
534        assert_eq!(
535            pk_value_to_string(&Value::Date(Date(20000))),
536            format!("Date({})", 20000)
537        );
538        assert_eq!(
539            pk_value_to_string(&Value::Timestamp(Timestamp(1_700_000_000_000_000))),
540            "Timestamp(1700000000000000)"
541        );
542        // Non-scalars fall back to Debug text.
543        assert_eq!(
544            pk_value_to_string(&Value::List(vec![Value::Int64(1)])),
545            "List([Int64(1)])"
546        );
547    }
548
549    #[test]
550    fn test_value_to_csv_string_null_is_empty_cell() {
551        assert_eq!(value_to_csv_string(&Value::Null), "");
552        assert_eq!(value_to_csv_string(&Value::Bool(false)), "false");
553        assert_eq!(value_to_csv_string(&Value::Int64(42)), "42");
554        assert_eq!(value_to_csv_string(&Value::String("a,b".into())), "a,b");
555        assert_eq!(value_to_csv_string(&Value::UInt8(7)), "7");
556    }
557}