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