1use nodedb_types::columnar::{ColumnType, SchemaOps, StrictSchema};
10
11use crate::encode::{FORMAT_VERSION, MAGIC};
12use nodedb_types::datetime::NdbDateTime;
13use nodedb_types::value::Value;
14
15use crate::error::StrictError;
16
17pub struct TupleDecoder {
22 schema: StrictSchema,
23 fixed_offsets: Vec<Option<usize>>,
26 fixed_section_size: usize,
28 var_table_index: Vec<Option<usize>>,
31 var_count: usize,
33 header_size: usize,
35}
36
37impl TupleDecoder {
38 pub fn new(schema: &StrictSchema) -> Self {
40 let mut fixed_offsets = Vec::with_capacity(schema.columns.len());
41 let mut var_table_index = Vec::with_capacity(schema.columns.len());
42 let mut fixed_offset = 0usize;
43 let mut var_idx = 0usize;
44
45 for col in &schema.columns {
46 if let Some(size) = col.column_type.fixed_size() {
47 fixed_offsets.push(Some(fixed_offset));
48 var_table_index.push(None);
49 fixed_offset += size;
50 } else {
51 fixed_offsets.push(None);
52 var_table_index.push(Some(var_idx));
53 var_idx += 1;
54 }
55 }
56
57 let header_size = 9 + schema.null_bitmap_size();
59
60 Self {
61 schema: schema.clone(),
62 fixed_offsets,
63 fixed_section_size: fixed_offset,
64 var_table_index,
65 var_count: var_idx,
66 header_size,
67 }
68 }
69
70 pub fn schema_version(&self, tuple: &[u8]) -> Result<u32, StrictError> {
75 if tuple.len() < 9 {
76 return Err(StrictError::TruncatedTuple {
77 expected: 9,
78 got: tuple.len(),
79 });
80 }
81 let got_magic = u32::from_le_bytes([tuple[0], tuple[1], tuple[2], tuple[3]]);
82 if got_magic != MAGIC {
83 return Err(StrictError::InvalidMagic {
84 expected: MAGIC,
85 got: got_magic,
86 });
87 }
88 let got_version = tuple[4];
89 if got_version != FORMAT_VERSION {
90 return Err(StrictError::InvalidFormatVersion {
91 expected: FORMAT_VERSION,
92 got: got_version,
93 });
94 }
95 Ok(u32::from_le_bytes([tuple[5], tuple[6], tuple[7], tuple[8]]))
96 }
97
98 pub fn is_null(&self, tuple: &[u8], col_idx: usize) -> Result<bool, StrictError> {
100 self.check_bounds(col_idx)?;
101 self.check_min_size(tuple)?;
102
103 let bitmap_byte = tuple[9 + col_idx / 8];
104 Ok(bitmap_byte & (1 << (col_idx % 8)) != 0)
105 }
106
107 pub fn extract_fixed_raw<'a>(
111 &self,
112 tuple: &'a [u8],
113 col_idx: usize,
114 ) -> Result<Option<&'a [u8]>, StrictError> {
115 self.check_bounds(col_idx)?;
116 self.check_min_size(tuple)?;
117
118 if self.is_null_unchecked(tuple, col_idx) {
119 return Ok(None);
120 }
121
122 let offset = self.fixed_offsets[col_idx].ok_or(StrictError::TypeMismatch {
123 column: self.schema.columns[col_idx].name.clone(),
124 expected: self.schema.columns[col_idx].column_type,
125 })?;
126
127 let size = self.schema.columns[col_idx]
128 .column_type
129 .fixed_size()
130 .ok_or(StrictError::TypeMismatch {
131 column: self.schema.columns[col_idx].name.clone(),
132 expected: self.schema.columns[col_idx].column_type,
133 })?;
134 let start = self.header_size + offset;
135 let end = start + size;
136
137 if end > tuple.len() {
138 return Err(StrictError::TruncatedTuple {
139 expected: end,
140 got: tuple.len(),
141 });
142 }
143
144 Ok(Some(&tuple[start..end]))
145 }
146
147 pub fn extract_variable_raw<'a>(
151 &self,
152 tuple: &'a [u8],
153 col_idx: usize,
154 ) -> Result<Option<&'a [u8]>, StrictError> {
155 self.check_bounds(col_idx)?;
156 self.check_min_size(tuple)?;
157
158 if self.is_null_unchecked(tuple, col_idx) {
159 return Ok(None);
160 }
161
162 let var_idx = self.var_table_index[col_idx].ok_or(StrictError::TypeMismatch {
163 column: self.schema.columns[col_idx].name.clone(),
164 expected: self.schema.columns[col_idx].column_type,
165 })?;
166
167 let table_start = self.header_size + self.fixed_section_size;
168 let entry_pos = table_start + var_idx * 4;
169 let next_pos = entry_pos + 4;
170
171 if next_pos + 4 > tuple.len() {
172 return Err(StrictError::TruncatedTuple {
173 expected: next_pos + 4,
174 got: tuple.len(),
175 });
176 }
177
178 let offset = u32::from_le_bytes(
180 tuple[entry_pos..entry_pos + 4]
181 .try_into()
182 .expect("4-byte slice from bounds-checked range"),
183 );
184 let next_offset = u32::from_le_bytes(
185 tuple[next_pos..next_pos + 4]
186 .try_into()
187 .expect("4-byte slice from bounds-checked range"),
188 );
189
190 let var_data_start = table_start + (self.var_count + 1) * 4;
191 let abs_start = var_data_start + offset as usize;
192 let abs_end = var_data_start + next_offset as usize;
193
194 if abs_end > tuple.len() {
195 return Err(StrictError::CorruptOffset {
196 offset: next_offset,
197 len: tuple.len(),
198 });
199 }
200
201 Ok(Some(&tuple[abs_start..abs_end]))
202 }
203
204 pub fn extract_value(&self, tuple: &[u8], col_idx: usize) -> Result<Value, StrictError> {
209 self.check_bounds(col_idx)?;
210
211 if self.is_null(tuple, col_idx)? {
212 return Ok(Value::Null);
213 }
214
215 let col = &self.schema.columns[col_idx];
216
217 if col.column_type.fixed_size().is_some() {
218 let raw = self
219 .extract_fixed_raw(tuple, col_idx)?
220 .ok_or(StrictError::TypeMismatch {
221 column: col.name.clone(),
222 expected: col.column_type,
223 })?;
224 Ok(decode_fixed_value(&col.column_type, raw))
225 } else {
226 let raw =
227 self.extract_variable_raw(tuple, col_idx)?
228 .ok_or(StrictError::TypeMismatch {
229 column: col.name.clone(),
230 expected: col.column_type,
231 })?;
232 Ok(decode_variable_value(&col.column_type, raw))
233 }
234 }
235
236 pub fn extract_all(&self, tuple: &[u8]) -> Result<Vec<Value>, StrictError> {
238 let mut values = Vec::with_capacity(self.schema.columns.len());
239 for i in 0..self.schema.columns.len() {
240 values.push(self.extract_value(tuple, i)?);
241 }
242 Ok(values)
243 }
244
245 pub fn extract_by_name(&self, tuple: &[u8], name: &str) -> Result<Value, StrictError> {
247 let idx = self
248 .schema
249 .column_index(name)
250 .ok_or(StrictError::ColumnOutOfRange {
251 index: usize::MAX,
252 count: self.schema.columns.len(),
253 })?;
254 self.extract_value(tuple, idx)
255 }
256
257 pub fn extract_value_versioned(
272 &self,
273 tuple: &[u8],
274 col_idx: usize,
275 old_col_count: usize,
276 ) -> Result<Value, StrictError> {
277 self.check_bounds(col_idx)?;
278
279 if col_idx >= old_col_count {
280 let col = &self.schema.columns[col_idx];
283 let value = col
284 .default
285 .as_deref()
286 .map(nodedb_types::columnar::StrictSchema::parse_default_literal)
287 .unwrap_or(Value::Null);
288 return Ok(value);
289 }
290
291 self.extract_value(tuple, col_idx)
292 }
293
294 pub fn schema(&self) -> &StrictSchema {
296 &self.schema
297 }
298
299 pub fn extract_bitemporal_timestamps(
303 &self,
304 tuple: &[u8],
305 ) -> Result<(i64, i64, i64), StrictError> {
306 if !self.schema.bitemporal {
307 return Err(StrictError::ColumnOutOfRange {
308 index: 0,
309 count: self.schema.columns.len(),
310 });
311 }
312 let sys = extract_i64(self, tuple, 0)?;
313 let vf = extract_i64(self, tuple, 1)?;
314 let vu = extract_i64(self, tuple, 2)?;
315 Ok((sys, vf, vu))
316 }
317
318 pub fn fixed_section_start(&self) -> usize {
320 self.header_size
321 }
322
323 pub fn offset_table_start(&self) -> usize {
325 self.header_size + self.fixed_section_size
326 }
327
328 pub fn var_data_start(&self) -> usize {
330 self.offset_table_start() + (self.var_count + 1) * 4
331 }
332
333 pub fn var_count(&self) -> usize {
335 self.var_count
336 }
337
338 pub fn fixed_field_location(&self, col_idx: usize) -> Option<(usize, usize)> {
341 let offset = self.fixed_offsets.get(col_idx).copied().flatten()?;
342 let size = self.schema.columns[col_idx].column_type.fixed_size()?;
343 Some((self.header_size + offset, size))
344 }
345
346 pub fn var_field_index(&self, col_idx: usize) -> Option<usize> {
349 self.var_table_index.get(col_idx).copied().flatten()
350 }
351
352 fn check_bounds(&self, col_idx: usize) -> Result<(), StrictError> {
355 if col_idx >= self.schema.columns.len() {
356 Err(StrictError::ColumnOutOfRange {
357 index: col_idx,
358 count: self.schema.columns.len(),
359 })
360 } else {
361 Ok(())
362 }
363 }
364
365 fn check_min_size(&self, tuple: &[u8]) -> Result<(), StrictError> {
366 let min = self.header_size;
367 if tuple.len() < min {
368 Err(StrictError::TruncatedTuple {
369 expected: min,
370 got: tuple.len(),
371 })
372 } else {
373 Ok(())
374 }
375 }
376
377 fn is_null_unchecked(&self, tuple: &[u8], col_idx: usize) -> bool {
378 let bitmap_byte = tuple[9 + col_idx / 8];
379 bitmap_byte & (1 << (col_idx % 8)) != 0
380 }
381}
382
383fn extract_i64(decoder: &TupleDecoder, tuple: &[u8], col_idx: usize) -> Result<i64, StrictError> {
385 let raw = decoder
386 .extract_fixed_raw(tuple, col_idx)?
387 .ok_or(StrictError::TypeMismatch {
388 column: decoder.schema.columns[col_idx].name.clone(),
389 expected: ColumnType::Int64,
390 })?;
391 Ok(i64::from_le_bytes([
392 raw[0], raw[1], raw[2], raw[3], raw[4], raw[5], raw[6], raw[7],
393 ]))
394}
395
396fn decode_fixed_value(col_type: &ColumnType, raw: &[u8]) -> Value {
398 match col_type {
399 ColumnType::Int64 => Value::Integer(i64::from_le_bytes([
400 raw[0], raw[1], raw[2], raw[3], raw[4], raw[5], raw[6], raw[7],
401 ])),
402 ColumnType::Float64 => Value::Float(f64::from_le_bytes([
403 raw[0], raw[1], raw[2], raw[3], raw[4], raw[5], raw[6], raw[7],
404 ])),
405 ColumnType::Bool => Value::Bool(raw[0] != 0),
406 ColumnType::Timestamp => {
407 let micros = i64::from_le_bytes([
408 raw[0], raw[1], raw[2], raw[3], raw[4], raw[5], raw[6], raw[7],
409 ]);
410 Value::NaiveDateTime(NdbDateTime::from_micros(micros))
411 }
412 ColumnType::Timestamptz => {
413 let micros = i64::from_le_bytes([
414 raw[0], raw[1], raw[2], raw[3], raw[4], raw[5], raw[6], raw[7],
415 ]);
416 Value::DateTime(NdbDateTime::from_micros(micros))
417 }
418 ColumnType::Decimal { .. } => {
419 let mut bytes = [0u8; 16];
420 bytes.copy_from_slice(&raw[..16]);
421 Value::Decimal(rust_decimal::Decimal::deserialize(bytes))
422 }
423 ColumnType::Uuid => {
424 let mut bytes = [0u8; 16];
425 bytes.copy_from_slice(&raw[..16]);
426 let parsed = uuid::Uuid::from_bytes(bytes);
427 Value::Uuid(parsed.to_string())
428 }
429 ColumnType::Vector(dim) => {
430 let d = *dim as usize;
431 let mut floats = Vec::with_capacity(d);
432 for i in 0..d {
433 let off = i * 4;
434 let bytes = [raw[off], raw[off + 1], raw[off + 2], raw[off + 3]];
435 let f = f32::from_le_bytes(bytes);
436 floats.push(Value::Float(f as f64));
437 }
438 Value::Array(floats)
439 }
440 _ => Value::Null, }
442}
443
444fn decode_variable_value(col_type: &ColumnType, raw: &[u8]) -> Value {
446 match col_type {
447 ColumnType::String => {
448 Value::String(std::str::from_utf8(raw).unwrap_or_default().to_string())
449 }
450 ColumnType::Bytes => Value::Bytes(raw.to_vec()),
451 ColumnType::Geometry => {
452 if let Ok(geom) = sonic_rs::from_slice::<nodedb_types::geometry::Geometry>(raw) {
454 Value::Geometry(geom)
455 } else {
456 Value::String(std::str::from_utf8(raw).unwrap_or_default().to_string())
457 }
458 }
459 ColumnType::Json => {
460 match nodedb_types::value_from_msgpack(raw) {
462 Ok(val) => val,
463 Err(e) => {
464 tracing::warn!(len = raw.len(), error = %e, "corrupted JSON msgpack in tuple");
465 Value::Null
466 }
467 }
468 }
469 ColumnType::SparseVector => {
472 Value::String(std::str::from_utf8(raw).unwrap_or_default().to_string())
473 }
474 _ => Value::Null,
475 }
476}
477
478#[cfg(test)]
479mod tests {
480 use nodedb_types::columnar::ColumnDef;
481
482 use super::*;
483 use crate::encode::TupleEncoder;
484
485 fn crm_schema() -> StrictSchema {
486 StrictSchema::new(vec![
487 ColumnDef::required("id", ColumnType::Int64).with_primary_key(),
488 ColumnDef::required("name", ColumnType::String),
489 ColumnDef::nullable("email", ColumnType::String),
490 ColumnDef::required(
491 "balance",
492 ColumnType::Decimal {
493 precision: 18,
494 scale: 4,
495 },
496 ),
497 ColumnDef::nullable("active", ColumnType::Bool),
498 ])
499 .unwrap()
500 }
501
502 fn encode_crm_row(values: &[Value]) -> Vec<u8> {
503 let schema = crm_schema();
504 TupleEncoder::new(&schema).encode(values).unwrap()
505 }
506
507 #[test]
508 fn roundtrip_all_fields() {
509 let schema = crm_schema();
510 let encoder = TupleEncoder::new(&schema);
511 let decoder = TupleDecoder::new(&schema);
512
513 let values = vec![
514 Value::Integer(42),
515 Value::String("Alice".into()),
516 Value::String("alice@example.com".into()),
517 Value::Decimal(rust_decimal::Decimal::new(5000, 2)),
518 Value::Bool(true),
519 ];
520
521 let tuple = encoder.encode(&values).unwrap();
522 let decoded = decoder.extract_all(&tuple).unwrap();
523
524 assert_eq!(decoded[0], Value::Integer(42));
525 assert_eq!(decoded[1], Value::String("Alice".into()));
526 assert_eq!(decoded[2], Value::String("alice@example.com".into()));
527 assert_eq!(
528 decoded[3],
529 Value::Decimal(rust_decimal::Decimal::new(5000, 2))
530 );
531 assert_eq!(decoded[4], Value::Bool(true));
532 }
533
534 #[test]
535 fn roundtrip_with_nulls() {
536 let schema = crm_schema();
537 let encoder = TupleEncoder::new(&schema);
538 let decoder = TupleDecoder::new(&schema);
539
540 let values = vec![
541 Value::Integer(1),
542 Value::String("Bob".into()),
543 Value::Null,
544 Value::Decimal(rust_decimal::Decimal::ZERO),
545 Value::Null,
546 ];
547
548 let tuple = encoder.encode(&values).unwrap();
549 let decoded = decoder.extract_all(&tuple).unwrap();
550
551 assert_eq!(decoded[0], Value::Integer(1));
552 assert_eq!(decoded[1], Value::String("Bob".into()));
553 assert_eq!(decoded[2], Value::Null);
554 assert_eq!(decoded[3], Value::Decimal(rust_decimal::Decimal::ZERO));
555 assert_eq!(decoded[4], Value::Null);
556 }
557
558 #[test]
559 fn o1_extraction_single_field() {
560 let schema = crm_schema();
561 let decoder = TupleDecoder::new(&schema);
562
563 let tuple = encode_crm_row(&[
564 Value::Integer(99),
565 Value::String("Charlie".into()),
566 Value::String("charlie@co.com".into()),
567 Value::Decimal(rust_decimal::Decimal::new(12345, 0)),
568 Value::Bool(false),
569 ]);
570
571 let balance = decoder.extract_value(&tuple, 3).unwrap();
573 assert_eq!(
574 balance,
575 Value::Decimal(rust_decimal::Decimal::new(12345, 0))
576 );
577
578 let name = decoder.extract_value(&tuple, 1).unwrap();
580 assert_eq!(name, Value::String("Charlie".into()));
581 }
582
583 #[test]
584 fn extract_by_name() {
585 let schema = crm_schema();
586 let decoder = TupleDecoder::new(&schema);
587
588 let tuple = encode_crm_row(&[
589 Value::Integer(7),
590 Value::String("Dana".into()),
591 Value::Null,
592 Value::Decimal(rust_decimal::Decimal::new(999, 1)),
593 Value::Bool(true),
594 ]);
595
596 assert_eq!(
597 decoder.extract_by_name(&tuple, "name").unwrap(),
598 Value::String("Dana".into())
599 );
600 assert_eq!(
601 decoder.extract_by_name(&tuple, "email").unwrap(),
602 Value::Null
603 );
604 }
605
606 #[test]
607 fn null_bitmap_check() {
608 let schema = crm_schema();
609 let decoder = TupleDecoder::new(&schema);
610
611 let tuple = encode_crm_row(&[
612 Value::Integer(1),
613 Value::String("x".into()),
614 Value::Null,
615 Value::Decimal(rust_decimal::Decimal::ZERO),
616 Value::Null,
617 ]);
618
619 assert!(!decoder.is_null(&tuple, 0).unwrap()); assert!(!decoder.is_null(&tuple, 1).unwrap()); assert!(decoder.is_null(&tuple, 2).unwrap()); assert!(!decoder.is_null(&tuple, 3).unwrap()); assert!(decoder.is_null(&tuple, 4).unwrap()); }
625
626 #[test]
627 fn column_out_of_range() {
628 let schema = crm_schema();
629 let decoder = TupleDecoder::new(&schema);
630 let tuple = encode_crm_row(&[
631 Value::Integer(1),
632 Value::String("x".into()),
633 Value::Null,
634 Value::Decimal(rust_decimal::Decimal::ZERO),
635 Value::Null,
636 ]);
637
638 let err = decoder.extract_value(&tuple, 99).unwrap_err();
639 assert!(matches!(
640 err,
641 StrictError::ColumnOutOfRange { index: 99, .. }
642 ));
643 }
644
645 #[test]
646 fn schema_version_read() {
647 let schema = crm_schema();
648 let decoder = TupleDecoder::new(&schema);
649 let tuple = encode_crm_row(&[
650 Value::Integer(1),
651 Value::String("x".into()),
652 Value::Null,
653 Value::Decimal(rust_decimal::Decimal::ZERO),
654 Value::Null,
655 ]);
656
657 assert_eq!(decoder.schema_version(&tuple).unwrap(), 1);
658 }
659
660 #[test]
661 fn schema_version_u32_no_truncation() {
662 let mut schema = crm_schema();
665 schema.version = 0x0001_0000;
666 let encoder = TupleEncoder::new(&schema);
667 let decoder = TupleDecoder::new(&schema);
668
669 let tuple = encoder
670 .encode(&[
671 Value::Integer(1),
672 Value::String("test".into()),
673 Value::Null,
674 Value::Decimal(rust_decimal::Decimal::ZERO),
675 Value::Null,
676 ])
677 .unwrap();
678
679 let decoded_version = decoder.schema_version(&tuple).unwrap();
680 assert_eq!(
681 decoded_version, 0x0001_0000u32,
682 "schema_version must not truncate to u16"
683 );
684 }
685
686 #[test]
687 fn versioned_extraction_new_column_returns_null() {
688 let schema = crm_schema();
689 let decoder = TupleDecoder::new(&schema);
690
691 let old_schema = StrictSchema::new(vec![
693 ColumnDef::required("id", ColumnType::Int64).with_primary_key(),
694 ColumnDef::required("name", ColumnType::String),
695 ColumnDef::nullable("email", ColumnType::String),
696 ])
697 .unwrap();
698 let old_encoder = TupleEncoder::new(&old_schema);
699 let tuple = old_encoder
700 .encode(&[Value::Integer(1), Value::String("x".into()), Value::Null])
701 .unwrap();
702
703 let balance = decoder.extract_value_versioned(&tuple, 3, 3).unwrap();
705 assert_eq!(balance, Value::Null);
706
707 let active = decoder.extract_value_versioned(&tuple, 4, 3).unwrap();
708 assert_eq!(active, Value::Null);
709
710 let id = decoder.extract_value_versioned(&tuple, 0, 3).unwrap();
712 assert_eq!(id, Value::Integer(1));
713 }
714
715 #[test]
716 fn raw_fixed_extraction() {
717 let schema = StrictSchema::new(vec![
718 ColumnDef::required("a", ColumnType::Int64),
719 ColumnDef::required("b", ColumnType::Float64),
720 ColumnDef::required("c", ColumnType::Bool),
721 ])
722 .unwrap();
723 let encoder = TupleEncoder::new(&schema);
724 let decoder = TupleDecoder::new(&schema);
725
726 let tuple = encoder
727 .encode(&[Value::Integer(42), Value::Float(0.75), Value::Bool(true)])
728 .unwrap();
729
730 let a_raw = decoder.extract_fixed_raw(&tuple, 0).unwrap().unwrap();
731 assert_eq!(i64::from_le_bytes(a_raw.try_into().unwrap()), 42);
732
733 let b_raw = decoder.extract_fixed_raw(&tuple, 1).unwrap().unwrap();
734 assert_eq!(f64::from_le_bytes(b_raw.try_into().unwrap()), 0.75);
735
736 let c_raw = decoder.extract_fixed_raw(&tuple, 2).unwrap().unwrap();
737 assert_eq!(c_raw[0], 1);
738 }
739
740 #[test]
741 fn raw_variable_extraction() {
742 let schema = StrictSchema::new(vec![
743 ColumnDef::required("id", ColumnType::Int64),
744 ColumnDef::required("name", ColumnType::String),
745 ColumnDef::nullable("bio", ColumnType::String),
746 ])
747 .unwrap();
748 let encoder = TupleEncoder::new(&schema);
749 let decoder = TupleDecoder::new(&schema);
750
751 let tuple = encoder
752 .encode(&[
753 Value::Integer(1),
754 Value::String("hello".into()),
755 Value::String("world".into()),
756 ])
757 .unwrap();
758
759 let name_raw = decoder.extract_variable_raw(&tuple, 1).unwrap().unwrap();
760 assert_eq!(std::str::from_utf8(name_raw).unwrap(), "hello");
761
762 let bio_raw = decoder.extract_variable_raw(&tuple, 2).unwrap().unwrap();
763 assert_eq!(std::str::from_utf8(bio_raw).unwrap(), "world");
764 }
765
766 #[test]
767 fn all_types_roundtrip() {
768 let schema = StrictSchema::new(vec![
769 ColumnDef::required("i", ColumnType::Int64),
770 ColumnDef::required("f", ColumnType::Float64),
771 ColumnDef::required("s", ColumnType::String),
772 ColumnDef::required("b", ColumnType::Bool),
773 ColumnDef::required("raw", ColumnType::Bytes),
774 ColumnDef::required("ts", ColumnType::Timestamp),
775 ColumnDef::required("tstz", ColumnType::Timestamptz),
776 ColumnDef::required(
777 "dec",
778 ColumnType::Decimal {
779 precision: 18,
780 scale: 4,
781 },
782 ),
783 ColumnDef::required("uid", ColumnType::Uuid),
784 ColumnDef::required("vec", ColumnType::Vector(2)),
785 ])
786 .unwrap();
787 let encoder = TupleEncoder::new(&schema);
788 let decoder = TupleDecoder::new(&schema);
789
790 let uuid_str = "550e8400-e29b-41d4-a716-446655440000";
791 let values = vec![
792 Value::Integer(-100),
793 Value::Float(0.5),
794 Value::String("test string".into()),
795 Value::Bool(false),
796 Value::Bytes(vec![0xDE, 0xAD, 0xBE, 0xEF]),
797 Value::NaiveDateTime(NdbDateTime::from_micros(1_000_000)),
798 Value::DateTime(NdbDateTime::from_micros(2_000_000)),
799 Value::Decimal(rust_decimal::Decimal::new(314159, 5)),
800 Value::Uuid(uuid_str.into()),
801 Value::Array(vec![Value::Float(1.5), Value::Float(2.5)]),
802 ];
803
804 let tuple = encoder.encode(&values).unwrap();
805 let decoded = decoder.extract_all(&tuple).unwrap();
806
807 assert_eq!(decoded[0], Value::Integer(-100));
808 assert_eq!(decoded[1], Value::Float(0.5));
809 assert_eq!(decoded[2], Value::String("test string".into()));
810 assert_eq!(decoded[3], Value::Bool(false));
811 assert_eq!(decoded[4], Value::Bytes(vec![0xDE, 0xAD, 0xBE, 0xEF]));
812 assert_eq!(
813 decoded[5],
814 Value::NaiveDateTime(NdbDateTime::from_micros(1_000_000))
815 );
816 assert_eq!(
817 decoded[6],
818 Value::DateTime(NdbDateTime::from_micros(2_000_000))
819 );
820 assert_eq!(
821 decoded[7],
822 Value::Decimal(rust_decimal::Decimal::new(314159, 5))
823 );
824 assert_eq!(decoded[8], Value::Uuid(uuid_str.into()));
825 if let Value::Array(ref arr) = decoded[9] {
827 assert_eq!(arr.len(), 2);
828 if let Value::Float(v) = arr[0] {
829 assert!((v - 1.5).abs() < 0.001);
830 }
831 } else {
832 panic!("expected array");
833 }
834 }
835
836 fn base_schema() -> StrictSchema {
840 StrictSchema::new(vec![
841 ColumnDef::required("id", ColumnType::Int64).with_primary_key(),
842 ColumnDef::required("name", ColumnType::String),
843 ])
844 .unwrap()
845 }
846
847 fn base_tuple() -> Vec<u8> {
848 TupleEncoder::new(&base_schema())
849 .encode(&[Value::Integer(7), Value::String("Alice".into())])
850 .unwrap()
851 }
852
853 #[test]
854 fn versioned_int_default_zero_not_null() {
855 let mut schema = base_schema();
857 let mut col = ColumnDef::required("score", ColumnType::Int64).with_default("0");
858 col.added_at_version = 2;
859 schema.columns.push(col);
860 schema.version = 2;
861
862 let decoder = TupleDecoder::new(&schema);
863 let tuple = base_tuple();
864
865 let val = decoder.extract_value_versioned(&tuple, 2, 2).unwrap();
867 assert_eq!(val, Value::Integer(0), "expected default 0, not null");
868 }
869
870 #[test]
871 fn versioned_text_default_pending_not_null() {
872 let mut schema = base_schema();
874 let mut col = ColumnDef::required("status", ColumnType::String).with_default("'pending'");
875 col.added_at_version = 2;
876 schema.columns.push(col);
877 schema.version = 2;
878
879 let decoder = TupleDecoder::new(&schema);
880 let tuple = base_tuple();
881
882 let val = decoder.extract_value_versioned(&tuple, 2, 2).unwrap();
883 assert_eq!(
884 val,
885 Value::String("pending".into()),
886 "expected default 'pending', not null"
887 );
888 }
889
890 #[test]
891 fn versioned_new_row_written_at_new_schema_no_double_default() {
892 let mut schema = base_schema();
895 let mut col = ColumnDef::required("score", ColumnType::Int64).with_default("0");
896 col.added_at_version = 2;
897 schema.columns.push(col);
898 schema.version = 2;
899
900 let encoder = TupleEncoder::new(&schema);
901 let tuple = encoder
902 .encode(&[
903 Value::Integer(42),
904 Value::String("Bob".into()),
905 Value::Integer(99),
906 ])
907 .unwrap();
908
909 let decoder = TupleDecoder::new(&schema);
910 let val = decoder.extract_value_versioned(&tuple, 2, 3).unwrap();
912 assert_eq!(
913 val,
914 Value::Integer(99),
915 "must read encoded value, not default"
916 );
917 }
918
919 #[test]
920 fn versioned_multiple_alters_accumulate() {
921 let mut schema = base_schema();
924
925 let mut col_a = ColumnDef::required("a", ColumnType::Int64).with_default("10");
926 col_a.added_at_version = 2;
927 schema.columns.push(col_a);
928 schema.version = 2;
929
930 let mut col_b = ColumnDef::required("b", ColumnType::String).with_default("'x'");
931 col_b.added_at_version = 3;
932 schema.columns.push(col_b);
933 schema.version = 3;
934
935 let decoder = TupleDecoder::new(&schema);
936 let tuple = base_tuple(); let a = decoder.extract_value_versioned(&tuple, 2, 2).unwrap();
939 assert_eq!(a, Value::Integer(10), "a default must be 10");
940
941 let b = decoder.extract_value_versioned(&tuple, 3, 2).unwrap();
942 assert_eq!(b, Value::String("x".into()), "b default must be 'x'");
943
944 let id = decoder.extract_value_versioned(&tuple, 0, 2).unwrap();
946 assert_eq!(id, Value::Integer(7));
947 }
948
949 #[test]
950 fn versioned_nullable_column_no_default_returns_null() {
951 let mut schema = base_schema();
953 let mut col = ColumnDef::nullable("note", ColumnType::String);
954 col.added_at_version = 2;
955 schema.columns.push(col);
956 schema.version = 2;
957
958 let decoder = TupleDecoder::new(&schema);
959 let tuple = base_tuple();
960
961 let val = decoder.extract_value_versioned(&tuple, 2, 2).unwrap();
962 assert_eq!(
963 val,
964 Value::Null,
965 "nullable column without default must be null"
966 );
967 }
968}