1use serde::{Deserialize, Serialize};
13use sonic_rs;
14use zerompk::{FromMessagePack, ToMessagePack};
15
16#[derive(Debug, Clone, Serialize, Deserialize, ToMessagePack, FromMessagePack)]
18#[serde(rename_all = "snake_case")]
19#[non_exhaustive]
20pub enum ColumnarWalRecord {
21 #[serde(rename = "insert_row")]
27 InsertRow {
28 collection: String,
29 row_data: Vec<u8>,
33 },
34
35 #[serde(rename = "delete_rows")]
40 DeleteRows {
41 collection: String,
42 segment_id: u64,
43 row_indices: Vec<u32>,
44 },
45
46 #[serde(rename = "compaction_commit")]
54 CompactionCommit {
55 collection: String,
56 old_segment_ids: Vec<u64>,
57 new_segment_ids: Vec<u64>,
58 },
59
60 #[serde(rename = "memtable_flushed")]
66 MemtableFlushed {
67 collection: String,
68 segment_id: u64,
69 row_count: u64,
70 },
71}
72
73impl ColumnarWalRecord {
74 pub fn collection(&self) -> &str {
76 match self {
77 Self::InsertRow { collection, .. }
78 | Self::DeleteRows { collection, .. }
79 | Self::CompactionCommit { collection, .. }
80 | Self::MemtableFlushed { collection, .. } => collection,
81 }
82 }
83
84 pub fn to_bytes(&self) -> Result<Vec<u8>, crate::error::ColumnarError> {
86 zerompk::to_msgpack_vec(self)
87 .map_err(|e| crate::error::ColumnarError::Serialization(e.to_string()))
88 }
89
90 pub fn from_bytes(data: &[u8]) -> Result<Self, crate::error::ColumnarError> {
92 zerompk::from_msgpack(data)
93 .map_err(|e| crate::error::ColumnarError::Serialization(e.to_string()))
94 }
95}
96
97pub fn encode_row_for_wal(
103 values: &[nodedb_types::value::Value],
104) -> Result<Vec<u8>, crate::error::ColumnarError> {
105 use nodedb_types::value::Value;
106
107 let mut buf = Vec::with_capacity(values.len() * 10); for value in values {
110 match value {
111 Value::Null => buf.push(0),
112 Value::Integer(v) => {
113 buf.push(1);
114 buf.extend_from_slice(&v.to_le_bytes());
115 }
116 Value::Float(v) => {
117 buf.push(2);
118 buf.extend_from_slice(&v.to_le_bytes());
119 }
120 Value::Bool(v) => {
121 buf.push(3);
122 buf.push(*v as u8);
123 }
124 Value::String(s) => {
125 buf.push(4);
126 let bytes = s.as_bytes();
127 buf.extend_from_slice(&(bytes.len() as u32).to_le_bytes());
128 buf.extend_from_slice(bytes);
129 }
130 Value::Bytes(b) => {
131 buf.push(5);
132 buf.extend_from_slice(&(b.len() as u32).to_le_bytes());
133 buf.extend_from_slice(b);
134 }
135 Value::DateTime(dt) => {
136 buf.push(6);
137 buf.extend_from_slice(&dt.micros.to_le_bytes());
138 }
139 Value::Decimal(d) => {
140 buf.push(7);
141 buf.extend_from_slice(&d.serialize());
142 }
143 Value::Uuid(s) => {
144 buf.push(8);
145 let bytes = s.as_bytes();
146 buf.extend_from_slice(&(bytes.len() as u32).to_le_bytes());
147 buf.extend_from_slice(bytes);
148 }
149 Value::Array(arr) => {
150 buf.push(9);
152 buf.extend_from_slice(&(arr.len() as u32).to_le_bytes());
153 for v in arr {
154 let f = match v {
155 Value::Float(f) => *f as f32,
156 Value::Integer(n) => *n as f32,
157 _ => 0.0,
158 };
159 buf.extend_from_slice(&f.to_le_bytes());
160 }
161 }
162 _ => {
163 buf.push(10);
165 let json = sonic_rs::to_vec(value).map_err(|e| {
166 crate::error::ColumnarError::Serialization(format!(
167 "failed to serialize value as JSON: {e}"
168 ))
169 })?;
170 buf.extend_from_slice(&(json.len() as u32).to_le_bytes());
171 buf.extend_from_slice(&json);
172 }
173 }
174 }
175
176 Ok(buf)
177}
178
179const MAX_FIELD_LEN: usize = 256 * 1024 * 1024;
182
183fn read_slice<'a>(
186 data: &'a [u8],
187 cursor: &mut usize,
188 n: usize,
189 context: &str,
190) -> Result<&'a [u8], crate::error::ColumnarError> {
191 let end = cursor.checked_add(n).ok_or_else(|| {
192 crate::error::ColumnarError::Serialization(format!("overflow in {context}"))
193 })?;
194 if end > data.len() {
195 return Err(crate::error::ColumnarError::Serialization(format!(
196 "truncated {context}: need {n} bytes at offset {cursor}, have {}",
197 data.len().saturating_sub(*cursor)
198 )));
199 }
200 let slice = &data[*cursor..end];
201 *cursor = end;
202 Ok(slice)
203}
204
205fn read_length_prefixed<'a>(
208 data: &'a [u8],
209 cursor: &mut usize,
210 context: &str,
211) -> Result<&'a [u8], crate::error::ColumnarError> {
212 let len_bytes = read_slice(data, cursor, 4, context)?;
213 let len = u32::from_le_bytes(len_bytes.try_into().map_err(|_| {
214 crate::error::ColumnarError::Serialization(format!("truncated {context} len"))
215 })?) as usize;
216 if len > MAX_FIELD_LEN {
217 return Err(crate::error::ColumnarError::Serialization(format!(
218 "{context} length {len} exceeds maximum {MAX_FIELD_LEN}"
219 )));
220 }
221 read_slice(data, cursor, len, context)
222}
223
224pub fn decode_row_from_wal(
226 data: &[u8],
227) -> Result<Vec<nodedb_types::value::Value>, crate::error::ColumnarError> {
228 use nodedb_types::value::Value;
229
230 let mut values = Vec::new();
231 let mut cursor = 0;
232
233 while cursor < data.len() {
234 let tag_slice = read_slice(data, &mut cursor, 1, "tag")?;
235 let tag = tag_slice[0];
236
237 let value = match tag {
238 0 => Value::Null,
239 1 => {
240 let bytes = read_slice(data, &mut cursor, 8, "i64")?;
241 let v = i64::from_le_bytes(bytes.try_into().map_err(|_| {
242 crate::error::ColumnarError::Serialization("truncated i64".into())
243 })?);
244 Value::Integer(v)
245 }
246 2 => {
247 let bytes = read_slice(data, &mut cursor, 8, "f64")?;
248 let v = f64::from_le_bytes(bytes.try_into().map_err(|_| {
249 crate::error::ColumnarError::Serialization("truncated f64".into())
250 })?);
251 Value::Float(v)
252 }
253 3 => {
254 let bytes = read_slice(data, &mut cursor, 1, "bool")?;
255 Value::Bool(bytes[0] != 0)
256 }
257 4 | 5 | 8 => {
258 let bytes = read_length_prefixed(
259 data,
260 &mut cursor,
261 match tag {
262 4 => "string",
263 5 => "bytes",
264 8 => "uuid",
265 _ => unreachable!(),
266 },
267 )?;
268 match tag {
269 4 => Value::String(String::from_utf8_lossy(bytes).into_owned()),
270 5 => Value::Bytes(bytes.to_vec()),
271 8 => Value::Uuid(String::from_utf8_lossy(bytes).into_owned()),
272 _ => unreachable!(),
273 }
274 }
275 6 => {
276 let bytes = read_slice(data, &mut cursor, 8, "timestamp")?;
277 let micros = i64::from_le_bytes(bytes.try_into().map_err(|_| {
278 crate::error::ColumnarError::Serialization("truncated timestamp".into())
279 })?);
280 Value::DateTime(nodedb_types::datetime::NdbDateTime::from_micros(micros))
281 }
282 7 => {
283 let bytes = read_slice(data, &mut cursor, 16, "decimal")?;
284 let mut arr = [0u8; 16];
285 arr.copy_from_slice(bytes);
286 Value::Decimal(rust_decimal::Decimal::deserialize(arr))
287 }
288 9 => {
289 let count_bytes = read_slice(data, &mut cursor, 4, "vector count")?;
290 let count = u32::from_le_bytes(count_bytes.try_into().map_err(|_| {
291 crate::error::ColumnarError::Serialization("truncated vector count".into())
292 })?) as usize;
293 if count > MAX_FIELD_LEN / 4 {
294 return Err(crate::error::ColumnarError::Serialization(format!(
295 "vector count {count} exceeds maximum {}",
296 MAX_FIELD_LEN / 4
297 )));
298 }
299 let mut arr = Vec::with_capacity(count);
300 for _ in 0..count {
301 let fb = read_slice(data, &mut cursor, 4, "vector f32")?;
302 let f = f32::from_le_bytes(fb.try_into().map_err(|_| {
303 crate::error::ColumnarError::Serialization("truncated f32".into())
304 })?);
305 arr.push(Value::Float(f as f64));
306 }
307 Value::Array(arr)
308 }
309 10 => {
310 let json_bytes = read_length_prefixed(data, &mut cursor, "json")?;
311 sonic_rs::from_slice(json_bytes).unwrap_or(Value::Null)
312 }
313 _ => {
314 return Err(crate::error::ColumnarError::Serialization(format!(
315 "unknown WAL value tag: {tag}"
316 )));
317 }
318 };
319
320 values.push(value);
321 }
322
323 Ok(values)
324}
325
326#[cfg(test)]
327mod tests {
328 use nodedb_types::datetime::NdbDateTime;
329 use nodedb_types::value::Value;
330
331 use super::*;
332
333 #[test]
334 fn wal_record_roundtrip() {
335 let records = vec![
336 ColumnarWalRecord::InsertRow {
337 collection: "test".into(),
338 row_data: vec![1, 2, 3],
339 },
340 ColumnarWalRecord::DeleteRows {
341 collection: "test".into(),
342 segment_id: 0,
343 row_indices: vec![5, 10, 15],
344 },
345 ColumnarWalRecord::CompactionCommit {
346 collection: "test".into(),
347 old_segment_ids: vec![0, 1],
348 new_segment_ids: vec![2],
349 },
350 ColumnarWalRecord::MemtableFlushed {
351 collection: "test".into(),
352 segment_id: 3,
353 row_count: 1024,
354 },
355 ];
356
357 for record in &records {
358 let bytes = record.to_bytes().expect("serialize");
359 let restored = ColumnarWalRecord::from_bytes(&bytes).expect("deserialize");
360 assert_eq!(restored.collection(), record.collection());
361 }
362 }
363
364 #[test]
365 fn row_wire_format_roundtrip() {
366 let values = vec![
367 Value::Integer(42),
368 Value::Float(0.75),
369 Value::Bool(true),
370 Value::String("hello".into()),
371 Value::Bytes(vec![0xDE, 0xAD]),
372 Value::DateTime(NdbDateTime::from_micros(1_700_000_000)),
373 Value::Decimal(rust_decimal::Decimal::new(314, 2)),
374 Value::Uuid("550e8400-e29b-41d4-a716-446655440000".into()),
375 Value::Null,
376 Value::Array(vec![Value::Float(1.0), Value::Float(2.0)]),
377 ];
378
379 let encoded = encode_row_for_wal(&values).expect("encode");
380 let decoded = decode_row_from_wal(&encoded).expect("decode");
381
382 assert_eq!(decoded.len(), values.len());
383 assert_eq!(decoded[0], Value::Integer(42));
384 assert_eq!(decoded[1], Value::Float(0.75));
385 assert_eq!(decoded[2], Value::Bool(true));
386 assert_eq!(decoded[3], Value::String("hello".into()));
387 assert_eq!(decoded[4], Value::Bytes(vec![0xDE, 0xAD]));
388 assert_eq!(
389 decoded[5],
390 Value::DateTime(NdbDateTime::from_micros(1_700_000_000))
391 );
392 assert_eq!(
393 decoded[7],
394 Value::Uuid("550e8400-e29b-41d4-a716-446655440000".into())
395 );
396 assert_eq!(decoded[8], Value::Null);
397 }
398
399 #[test]
400 fn decode_truncated_i64_returns_error() {
401 let result = decode_row_from_wal(&[1]);
406 assert!(
407 result.is_err(),
408 "truncated i64 payload must return Err, not panic"
409 );
410 }
411
412 #[test]
413 fn decode_truncated_string_returns_error() {
414 let input = {
418 let mut v = vec![4u8]; v.extend_from_slice(&255u32.to_le_bytes()); v
422 };
423 let result = decode_row_from_wal(&input);
424 assert!(
425 result.is_err(),
426 "truncated string payload must return Err, not panic"
427 );
428 }
429
430 #[test]
431 fn decode_huge_vector_count_returns_error() {
432 let input = {
437 let mut v = vec![9u8]; v.extend_from_slice(&0x7FFF_FFFFu32.to_le_bytes()); v
441 };
442 let result = decode_row_from_wal(&input);
443 assert!(
444 result.is_err(),
445 "huge vector count with no payload must return Err, not panic or OOM"
446 );
447 }
448
449 #[test]
450 fn decode_truncated_decimal_returns_error() {
451 let input = {
454 let mut v = vec![7u8]; v.extend_from_slice(&[0u8; 4]); v
457 };
458 let result = decode_row_from_wal(&input);
459 assert!(
460 result.is_err(),
461 "truncated decimal payload must return Err, not panic"
462 );
463 }
464}