Skip to main content

modelvault_core/record/
row_value.rs

1//! Row values for record payload v2: primitives, optionals, lists, objects, enums (as strings).
2
3use std::collections::BTreeMap;
4
5use crate::error::{DbError, FormatError};
6use crate::record::scalar::{
7    decode_tagged_scalar, decode_tagged_string, encode_tagged_scalar, Cursor, ScalarValue,
8};
9use crate::schema::{FieldDef, Type};
10
11/// In-memory value for a row field (including nested structures).
12#[derive(Debug, Clone, PartialEq)]
13pub enum RowValue {
14    Bool(bool),
15    Int64(i64),
16    Uint64(u64),
17    Float64(f64),
18    String(String),
19    Bytes(Vec<u8>),
20    Uuid([u8; 16]),
21    Timestamp(i64),
22    /// Absent `Optional<T>` (omitted key or explicit null).
23    None,
24    List(Vec<RowValue>),
25    Object(BTreeMap<String, RowValue>),
26}
27
28impl RowValue {
29    /// Convert a primitive [`ScalarValue`] to a row value (for PK and v1 interop).
30    pub fn from_scalar(s: ScalarValue) -> Self {
31        match s {
32            ScalarValue::Bool(b) => RowValue::Bool(b),
33            ScalarValue::Int64(n) => RowValue::Int64(n),
34            ScalarValue::Uint64(n) => RowValue::Uint64(n),
35            ScalarValue::Float64(n) => RowValue::Float64(n),
36            ScalarValue::String(x) => RowValue::String(x),
37            ScalarValue::Bytes(b) => RowValue::Bytes(b),
38            ScalarValue::Uuid(u) => RowValue::Uuid(u),
39            ScalarValue::Timestamp(t) => RowValue::Timestamp(t),
40        }
41    }
42
43    /// If this row value is a primitive, return its scalar form (for PK encoding).
44    pub fn as_scalar(&self) -> Option<ScalarValue> {
45        Some(match self {
46            RowValue::Bool(b) => ScalarValue::Bool(*b),
47            RowValue::Int64(n) => ScalarValue::Int64(*n),
48            RowValue::Uint64(n) => ScalarValue::Uint64(*n),
49            RowValue::Float64(n) => ScalarValue::Float64(*n),
50            RowValue::String(s) => ScalarValue::String(s.clone()),
51            RowValue::Bytes(b) => ScalarValue::Bytes(b.clone()),
52            RowValue::Uuid(u) => ScalarValue::Uuid(*u),
53            RowValue::Timestamp(t) => ScalarValue::Timestamp(*t),
54            _ => return None,
55        })
56    }
57
58    /// Require a primitive scalar (for `get` / PK lookup parameters).
59    pub fn into_scalar(self) -> Result<ScalarValue, DbError> {
60        self.as_scalar()
61            .ok_or(DbError::Format(FormatError::RecordPayloadTypeMismatch))
62    }
63
64    #[inline]
65    pub(crate) fn as_object_map(&self) -> Option<&BTreeMap<String, RowValue>> {
66        match self {
67            RowValue::Object(m) => Some(m),
68            _ => None,
69        }
70    }
71}
72
73/// Encode a row value according to `ty` (record payload v2).
74pub fn encode_row_value(out: &mut Vec<u8>, v: &RowValue, ty: &Type) -> Result<(), DbError> {
75    match ty {
76        Type::Bool
77        | Type::Int64
78        | Type::Uint64
79        | Type::Float64
80        | Type::String
81        | Type::Bytes
82        | Type::Uuid
83        | Type::Timestamp => {
84            let s = v
85                .as_scalar()
86                .ok_or(DbError::Format(FormatError::RecordPayloadTypeMismatch))?;
87            encode_tagged_scalar(out, &s, ty)
88        }
89        Type::Optional(inner) => {
90            if matches!(v, RowValue::None) {
91                out.push(0);
92                Ok(())
93            } else {
94                out.push(1);
95                encode_row_value(out, v, inner)
96            }
97        }
98        Type::List(inner) => {
99            let RowValue::List(items) = v else {
100                return Err(DbError::Format(FormatError::RecordPayloadTypeMismatch));
101            };
102            out.extend_from_slice(&(items.len() as u32).to_le_bytes());
103            for item in items {
104                encode_row_value(out, item, inner)?;
105            }
106            Ok(())
107        }
108        Type::Object(fields) => {
109            let RowValue::Object(map) = v else {
110                return Err(DbError::Format(FormatError::RecordPayloadTypeMismatch));
111            };
112            for def in fields {
113                let key = def.path.0[0].as_ref();
114                let fv = map
115                    .get(key)
116                    .ok_or(DbError::Format(FormatError::TruncatedRecordPayload))?;
117                encode_row_value(out, fv, &def.ty)?;
118            }
119            Ok(())
120        }
121        Type::Enum(_) => {
122            let RowValue::String(s) = v else {
123                return Err(DbError::Format(FormatError::RecordPayloadTypeMismatch));
124            };
125            encode_tagged_scalar(out, &ScalarValue::String(s.clone()), &Type::String)
126        }
127    }
128}
129
130/// Decode a row value according to `ty` (record payload v2).
131pub fn decode_row_value(cur: &mut Cursor<'_>, ty: &Type) -> Result<RowValue, DbError> {
132    Ok(match ty {
133        Type::Bool
134        | Type::Int64
135        | Type::Uint64
136        | Type::Float64
137        | Type::String
138        | Type::Bytes
139        | Type::Uuid
140        | Type::Timestamp => RowValue::from_scalar(decode_tagged_scalar(cur, ty)?),
141        Type::Optional(inner) => {
142            let pres = cur.take_u8()?;
143            match pres {
144                0 => RowValue::None,
145                1 => decode_row_value(cur, inner)?,
146                _ => return Err(DbError::Format(FormatError::RecordPayloadTypeMismatch)),
147            }
148        }
149        Type::List(inner) => {
150            let n = cur.take_u32()? as usize;
151            let mut items = Vec::with_capacity(n.min(1_048_576));
152            for _ in 0..n {
153                items.push(decode_row_value(cur, inner)?);
154            }
155            RowValue::List(items)
156        }
157        Type::Object(fields) => {
158            let mut map = BTreeMap::new();
159            for def in fields {
160                let key = def.path.0[0].to_string();
161                let val = decode_row_value(cur, &def.ty)?;
162                map.insert(key, val);
163            }
164            RowValue::Object(map)
165        }
166        Type::Enum(_) => RowValue::String(decode_tagged_string(cur)?),
167    })
168}
169
170/// Ordered non-PK field definitions (schema order, excluding primary key column).
171pub fn non_pk_defs_in_order<'a>(fields: &'a [FieldDef], pk_name: &str) -> Vec<&'a FieldDef> {
172    fields
173        .iter()
174        .filter(|f| f.path.0.len() == 1 && f.path.0[0] != pk_name)
175        .collect()
176}