1use std::convert::TryFrom;
2
3use crate::planner::ResolvedType;
4
5use super::error::{Result, StorageError};
6use super::value::SqlValue;
7
8const MAX_INLINE_BYTES: usize = 16 * 1024 * 1024; const MAX_VECTOR_LEN: usize = 4 * 1024 * 1024; const MAX_NESTED_ELEMENTS: usize = 100_000;
11const MAX_NESTED_DEPTH: usize = 16;
12
13pub struct RowCodec;
24
25impl RowCodec {
26 pub fn encode(row: &[SqlValue]) -> Vec<u8> {
28 let column_count =
29 u16::try_from(row.len()).expect("row column count exceeds u16::MAX (design limit)");
30 let null_bytes = (column_count as usize).div_ceil(8);
31
32 let mut buf = Vec::with_capacity(2 + null_bytes + row.len() * 8);
34 buf.extend_from_slice(&column_count.to_le_bytes());
35
36 let mut null_bitmap = vec![0u8; null_bytes];
37 for (idx, val) in row.iter().enumerate() {
38 if val.is_null() {
39 null_bitmap[idx / 8] |= 1 << (idx % 8);
40 }
41 }
42 buf.extend_from_slice(&null_bitmap);
43
44 for value in row {
45 if value.is_null() {
46 continue;
47 }
48 buf.push(value.type_tag());
49 encode_value(value, &mut buf);
50 }
51
52 buf
53 }
54
55 pub fn decode(bytes: &[u8]) -> Result<Vec<SqlValue>> {
57 let mut cursor = 0;
58 if bytes.len() < 2 {
59 return Err(StorageError::CorruptedData {
60 reason: "missing column count".into(),
61 });
62 }
63
64 let column_count =
65 u16::from_le_bytes(bytes[cursor..cursor + 2].try_into().unwrap()) as usize;
66 cursor += 2;
67
68 let null_bytes = column_count.div_ceil(8);
69 if bytes.len() < cursor + null_bytes {
70 return Err(StorageError::CorruptedData {
71 reason: "missing null bitmap".into(),
72 });
73 }
74 let null_bitmap = &bytes[cursor..cursor + null_bytes];
75 cursor += null_bytes;
76
77 let mut values = Vec::with_capacity(column_count);
78 for idx in 0..column_count {
79 let is_null = (null_bitmap[idx / 8] & (1 << (idx % 8))) != 0;
80 if is_null {
81 values.push(SqlValue::Null);
82 continue;
83 }
84
85 if cursor >= bytes.len() {
86 return Err(StorageError::CorruptedData {
87 reason: "missing type tag".into(),
88 });
89 }
90 let tag = bytes[cursor];
91 cursor += 1;
92
93 let value = decode_value(tag, bytes, &mut cursor)?;
94 values.push(value);
95 }
96
97 if cursor != bytes.len() {
98 return Err(StorageError::CorruptedData {
99 reason: "trailing bytes after decoding row".into(),
100 });
101 }
102
103 Ok(values)
104 }
105
106 pub fn decode_with_schema(bytes: &[u8], schema: &[ResolvedType]) -> Result<Vec<SqlValue>> {
108 let values = Self::decode(bytes)?;
109
110 if values.len() != schema.len() {
111 return Err(StorageError::CorruptedData {
112 reason: format!(
113 "column count mismatch: encoded={}, expected={}",
114 values.len(),
115 schema.len()
116 ),
117 });
118 }
119
120 values
121 .into_iter()
122 .zip(schema.iter())
123 .map(|(value, ty)| ensure_type(value, ty))
124 .collect()
125 }
126}
127
128fn encode_value(value: &SqlValue, buf: &mut Vec<u8>) {
129 match value {
130 SqlValue::Null => {}
131 SqlValue::Integer(v) => buf.extend_from_slice(&v.to_le_bytes()),
132 SqlValue::BigInt(v) => buf.extend_from_slice(&v.to_le_bytes()),
133 SqlValue::Float(v) => buf.extend_from_slice(&v.to_bits().to_le_bytes()),
134 SqlValue::Double(v) => buf.extend_from_slice(&v.to_bits().to_le_bytes()),
135 SqlValue::Text(s) => {
136 let len = u32::try_from(s.len())
137 .expect("text length exceeds u32::MAX (design limit for row encoding)");
138 buf.extend_from_slice(&len.to_le_bytes());
139 buf.extend_from_slice(s.as_bytes());
140 }
141 SqlValue::Blob(bytes) => {
142 let len = u32::try_from(bytes.len())
143 .expect("blob length exceeds u32::MAX (design limit for row encoding)");
144 buf.extend_from_slice(&len.to_le_bytes());
145 buf.extend_from_slice(bytes);
146 }
147 SqlValue::Boolean(b) => buf.push(u8::from(*b)),
148 SqlValue::Timestamp(v) => buf.extend_from_slice(&v.to_le_bytes()),
149 SqlValue::Vector(values) => {
150 let len = u32::try_from(values.len())
151 .expect("vector length exceeds u32::MAX (design limit for row encoding)");
152 buf.extend_from_slice(&len.to_le_bytes());
153 for f in values {
154 buf.extend_from_slice(&f.to_bits().to_le_bytes());
155 }
156 }
157 SqlValue::Date(v) => buf.extend_from_slice(&v.to_le_bytes()),
158 SqlValue::Time(v) => buf.extend_from_slice(&v.to_le_bytes()),
159 SqlValue::Interval {
160 months,
161 days,
162 micros,
163 } => {
164 buf.extend_from_slice(&months.to_le_bytes());
165 buf.extend_from_slice(&days.to_le_bytes());
166 buf.extend_from_slice(µs.to_le_bytes());
167 }
168 SqlValue::Decimal(value) => {
169 buf.extend_from_slice(&value.coefficient.to_le_bytes());
170 buf.push(value.scale);
171 }
172 SqlValue::Json(value) => {
173 let bytes = value.as_str().as_bytes();
174 let len = u32::try_from(bytes.len())
175 .expect("JSON length exceeds u32::MAX (design limit for row encoding)");
176 buf.extend_from_slice(&len.to_le_bytes());
177 buf.extend_from_slice(bytes);
178 }
179 SqlValue::Array(values) => {
180 buf.extend_from_slice(&(values.len() as u32).to_le_bytes());
181 for value in values {
182 encode_nested(value, buf);
183 }
184 }
185 SqlValue::Map(values) => {
186 buf.extend_from_slice(&(values.len() as u32).to_le_bytes());
187 for (key, value) in values {
188 encode_nested(key, buf);
189 encode_nested(value, buf);
190 }
191 }
192 SqlValue::Struct(values) => {
193 buf.extend_from_slice(&(values.len() as u32).to_le_bytes());
194 for (name, value) in values {
195 buf.extend_from_slice(&(name.len() as u32).to_le_bytes());
196 buf.extend_from_slice(name.as_bytes());
197 encode_nested(value, buf);
198 }
199 }
200 }
201}
202
203fn decode_value(tag: u8, bytes: &[u8], cursor: &mut usize) -> Result<SqlValue> {
204 decode_value_depth(tag, bytes, cursor, 0)
205}
206
207fn encode_nested(value: &SqlValue, buf: &mut Vec<u8>) {
208 let mut encoded = vec![value.type_tag()];
209 encode_value(value, &mut encoded);
210 buf.extend_from_slice(&(encoded.len() as u32).to_le_bytes());
211 buf.extend_from_slice(&encoded);
212}
213
214fn decode_nested(bytes: &[u8], depth: usize) -> Result<SqlValue> {
215 let Some((&tag, payload)) = bytes.split_first() else {
216 return Err(StorageError::CorruptedData {
217 reason: "missing nested type tag".into(),
218 });
219 };
220 let mut cursor = 0;
221 let value = decode_value_depth(tag, payload, &mut cursor, depth)?;
222 if cursor != payload.len() {
223 return Err(StorageError::CorruptedData {
224 reason: "trailing nested value bytes".into(),
225 });
226 }
227 Ok(value)
228}
229
230fn decode_value_depth(tag: u8, bytes: &[u8], cursor: &mut usize, depth: usize) -> Result<SqlValue> {
231 if depth > MAX_NESTED_DEPTH {
232 return Err(StorageError::CorruptedData {
233 reason: "nested value exceeds depth 16".into(),
234 });
235 }
236 let mut take = |len: usize, reason: &'static str| -> Result<&[u8]> {
237 let end = cursor
238 .checked_add(len)
239 .ok_or_else(|| StorageError::CorruptedData {
240 reason: reason.to_string(),
241 })?;
242 if end > bytes.len() {
243 return Err(StorageError::CorruptedData {
244 reason: reason.to_string(),
245 });
246 }
247 let slice = &bytes[*cursor..end];
248 *cursor = end;
249 Ok(slice)
250 };
251
252 match tag {
253 0x00 => Ok(SqlValue::Null),
254 0x01 => {
255 let raw = take(4, "truncated Integer value")?;
256 Ok(SqlValue::Integer(i32::from_le_bytes(
257 raw.try_into().unwrap(),
258 )))
259 }
260 0x02 => {
261 let raw = take(8, "truncated BigInt value")?;
262 Ok(SqlValue::BigInt(i64::from_le_bytes(
263 raw.try_into().unwrap(),
264 )))
265 }
266 0x03 => {
267 let raw = take(4, "truncated Float value")?;
268 Ok(SqlValue::Float(f32::from_bits(u32::from_le_bytes(
269 raw.try_into().unwrap(),
270 ))))
271 }
272 0x04 => {
273 let raw = take(8, "truncated Double value")?;
274 Ok(SqlValue::Double(f64::from_bits(u64::from_le_bytes(
275 raw.try_into().unwrap(),
276 ))))
277 }
278 0x05 => {
279 let len_bytes = take(4, "truncated Text length")?;
280 let len = u32::from_le_bytes(len_bytes.try_into().unwrap()) as usize;
281 if len > MAX_INLINE_BYTES {
282 return Err(StorageError::CorruptedData {
283 reason: format!("text length exceeds limit: {len}"),
284 });
285 }
286 let raw = take(len, "truncated Text payload")?;
287 let s = String::from_utf8(raw.to_vec()).map_err(|_| StorageError::CorruptedData {
288 reason: "invalid UTF-8 in Text".into(),
289 })?;
290 Ok(SqlValue::Text(s))
291 }
292 0x06 => {
293 let len_bytes = take(4, "truncated Blob length")?;
294 let len = u32::from_le_bytes(len_bytes.try_into().unwrap()) as usize;
295 if len > MAX_INLINE_BYTES {
296 return Err(StorageError::CorruptedData {
297 reason: format!("blob length exceeds limit: {len}"),
298 });
299 }
300 let raw = take(len, "truncated Blob payload")?;
301 Ok(SqlValue::Blob(raw.to_vec()))
302 }
303 0x07 => {
304 let raw = take(1, "truncated Boolean")?[0];
305 match raw {
306 0 => Ok(SqlValue::Boolean(false)),
307 1 => Ok(SqlValue::Boolean(true)),
308 other => Err(StorageError::CorruptedData {
309 reason: format!("invalid boolean value: {}", other),
310 }),
311 }
312 }
313 0x08 => {
314 let raw = take(8, "truncated Timestamp value")?;
315 Ok(SqlValue::Timestamp(i64::from_le_bytes(
316 raw.try_into().unwrap(),
317 )))
318 }
319 0x09 => {
320 let len_bytes = take(4, "truncated Vector length")?;
321 let len = u32::from_le_bytes(len_bytes.try_into().unwrap()) as usize;
322 if len > MAX_VECTOR_LEN {
323 return Err(StorageError::CorruptedData {
324 reason: format!("vector length exceeds limit: {len}"),
325 });
326 }
327 let total = len
328 .checked_mul(4)
329 .ok_or_else(|| StorageError::CorruptedData {
330 reason: "vector length overflow".into(),
331 })?;
332 let raw = take(total, "truncated Vector payload")?;
333
334 let mut values = Vec::with_capacity(len);
335 for chunk in raw.as_chunks::<4>().0 {
336 values.push(f32::from_bits(u32::from_le_bytes(*chunk)));
337 }
338 Ok(SqlValue::Vector(values))
339 }
340 0x0a => Ok(SqlValue::Date(i32::from_le_bytes(
341 take(4, "truncated Date value")?.try_into().unwrap(),
342 ))),
343 0x0b => Ok(SqlValue::Time(i64::from_le_bytes(
344 take(8, "truncated Time value")?.try_into().unwrap(),
345 ))),
346 0x0c => Ok(SqlValue::Interval {
347 months: i32::from_le_bytes(take(4, "truncated Interval months")?.try_into().unwrap()),
348 days: i32::from_le_bytes(take(4, "truncated Interval days")?.try_into().unwrap()),
349 micros: i64::from_le_bytes(take(8, "truncated Interval micros")?.try_into().unwrap()),
350 }),
351 0x0d => Ok(SqlValue::Decimal(super::DecimalValue::new(
352 i128::from_le_bytes(
353 take(16, "truncated Decimal coefficient")?
354 .try_into()
355 .unwrap(),
356 ),
357 take(1, "truncated Decimal scale")?[0],
358 ))),
359 0x0e => {
360 let len =
361 u32::from_le_bytes(take(4, "truncated JSON length")?.try_into().unwrap()) as usize;
362 if len > MAX_INLINE_BYTES {
363 return Err(StorageError::CorruptedData {
364 reason: format!("JSON length exceeds limit: {len}"),
365 });
366 }
367 let text = std::str::from_utf8(take(len, "truncated JSON payload")?).map_err(|_| {
368 StorageError::CorruptedData {
369 reason: "invalid UTF-8 in JSON".into(),
370 }
371 })?;
372 Ok(SqlValue::Json(super::JsonValue::parse(text).map_err(
373 |_| StorageError::CorruptedData {
374 reason: "invalid JSON payload".into(),
375 },
376 )?))
377 }
378 0x0f => {
379 let len =
380 u32::from_le_bytes(take(4, "truncated Array length")?.try_into().unwrap()) as usize;
381 if len > MAX_NESTED_ELEMENTS {
382 return Err(StorageError::CorruptedData {
383 reason: format!("array length exceeds limit: {len}"),
384 });
385 }
386 let mut values = Vec::with_capacity(len);
387 for _ in 0..len {
388 let size = u32::from_le_bytes(
389 take(4, "truncated nested value length")?
390 .try_into()
391 .unwrap(),
392 ) as usize;
393 values.push(decode_nested(
394 take(size, "truncated nested value")?,
395 depth + 1,
396 )?);
397 }
398 Ok(SqlValue::Array(values))
399 }
400 0x10 => {
401 let len =
402 u32::from_le_bytes(take(4, "truncated Map length")?.try_into().unwrap()) as usize;
403 if len > MAX_NESTED_ELEMENTS {
404 return Err(StorageError::CorruptedData {
405 reason: format!("map length exceeds limit: {len}"),
406 });
407 }
408 let mut values = Vec::with_capacity(len);
409 for _ in 0..len {
410 let key_size =
411 u32::from_le_bytes(take(4, "truncated map key length")?.try_into().unwrap())
412 as usize;
413 let key = decode_nested(take(key_size, "truncated map key")?, depth + 1)?;
414 let value_size =
415 u32::from_le_bytes(take(4, "truncated map value length")?.try_into().unwrap())
416 as usize;
417 let value = decode_nested(take(value_size, "truncated map value")?, depth + 1)?;
418 values.push((key, value));
419 }
420 Ok(SqlValue::Map(values))
421 }
422 0x11 => {
423 let len = u32::from_le_bytes(take(4, "truncated Struct length")?.try_into().unwrap())
424 as usize;
425 if len > MAX_NESTED_ELEMENTS {
426 return Err(StorageError::CorruptedData {
427 reason: format!("struct length exceeds limit: {len}"),
428 });
429 }
430 let mut values = Vec::with_capacity(len);
431 for _ in 0..len {
432 let name_len = u32::from_le_bytes(
433 take(4, "truncated struct field name length")?
434 .try_into()
435 .unwrap(),
436 ) as usize;
437 let name = std::str::from_utf8(take(name_len, "truncated struct field name")?)
438 .map_err(|_| StorageError::CorruptedData {
439 reason: "invalid UTF-8 in struct field name".into(),
440 })?
441 .to_string();
442 let value_size = u32::from_le_bytes(
443 take(4, "truncated struct field length")?
444 .try_into()
445 .unwrap(),
446 ) as usize;
447 let value = decode_nested(take(value_size, "truncated struct field")?, depth + 1)?;
448 values.push((name, value));
449 }
450 Ok(SqlValue::Struct(values))
451 }
452 other => Err(StorageError::CorruptedData {
453 reason: format!("unknown type tag: 0x{other:02x}"),
454 }),
455 }
456}
457
458fn ensure_type(value: SqlValue, expected: &ResolvedType) -> Result<SqlValue> {
459 use ResolvedType::*;
460 match (expected, value) {
461 (_, SqlValue::Null) => Ok(SqlValue::Null),
462 (Integer, SqlValue::Integer(v)) => Ok(SqlValue::Integer(v)),
463 (BigInt, SqlValue::BigInt(v)) => Ok(SqlValue::BigInt(v)),
464 (Float, SqlValue::Float(v)) => Ok(SqlValue::Float(v)),
465 (Double, SqlValue::Double(v)) => Ok(SqlValue::Double(v)),
466 (Text, SqlValue::Text(s)) => Ok(SqlValue::Text(s)),
467 (Blob, SqlValue::Blob(b)) => Ok(SqlValue::Blob(b)),
468 (Boolean, SqlValue::Boolean(v)) => Ok(SqlValue::Boolean(v)),
469 (Timestamp, SqlValue::Timestamp(v)) => Ok(SqlValue::Timestamp(v)),
470 (Date, SqlValue::Date(v)) => Ok(SqlValue::Date(v)),
471 (Time, SqlValue::Time(v)) => Ok(SqlValue::Time(v)),
472 (Interval, value @ SqlValue::Interval { .. }) => Ok(value),
473 (Decimal { precision, scale }, SqlValue::Decimal(value)) => {
474 let value = value
475 .rescale(*scale)
476 .ok_or_else(|| StorageError::TypeMismatch {
477 expected: format!("Decimal({precision},{scale})"),
478 actual: "Decimal overflow".into(),
479 })?;
480 if value.fits_precision(*precision) {
481 Ok(SqlValue::Decimal(value))
482 } else {
483 Err(StorageError::TypeMismatch {
484 expected: format!("Decimal({precision},{scale})"),
485 actual: value.to_string(),
486 })
487 }
488 }
489 (Json, SqlValue::Json(value)) => Ok(SqlValue::Json(value)),
490 (Array(element), SqlValue::Array(values)) => values
491 .into_iter()
492 .map(|value| ensure_type(value, element))
493 .collect::<Result<Vec<_>>>()
494 .map(SqlValue::Array),
495 (
496 Map {
497 key: key_type,
498 value: value_type,
499 },
500 SqlValue::Map(values),
501 ) => values
502 .into_iter()
503 .map(|(key, value)| {
504 if key.is_null() {
505 return Err(StorageError::TypeMismatch {
506 expected: "non-NULL map key".into(),
507 actual: "Null".into(),
508 });
509 }
510 Ok((ensure_type(key, key_type)?, ensure_type(value, value_type)?))
511 })
512 .collect::<Result<Vec<_>>>()
513 .map(SqlValue::Map),
514 (Struct(fields), SqlValue::Struct(values)) if fields.len() == values.len() => values
515 .into_iter()
516 .zip(fields)
517 .map(|((name, value), (expected_name, expected_type))| {
518 if name != *expected_name {
519 return Err(StorageError::TypeMismatch {
520 expected: expected_name.clone(),
521 actual: name,
522 });
523 }
524 Ok((name, ensure_type(value, expected_type)?))
525 })
526 .collect::<Result<Vec<_>>>()
527 .map(SqlValue::Struct),
528 (Vector { dimension, .. }, SqlValue::Vector(values)) => {
529 if values.len() as u32 == *dimension {
530 Ok(SqlValue::Vector(values))
531 } else {
532 Err(StorageError::TypeMismatch {
533 expected: format!("Vector(dim={})", dimension),
534 actual: format!("Vector(dim={})", values.len()),
535 })
536 }
537 }
538 (expected_ty, actual) => Err(StorageError::TypeMismatch {
539 expected: expected_ty.type_name().to_string(),
540 actual: actual.type_name().to_string(),
541 }),
542 }
543}
544
545#[cfg(test)]
546mod tests {
547 use super::*;
548 use proptest::prelude::*;
549
550 fn values_equal(a: &SqlValue, b: &SqlValue) -> bool {
551 match (a, b) {
552 (SqlValue::Float(x), SqlValue::Float(y)) => x.to_bits() == y.to_bits(),
553 (SqlValue::Double(x), SqlValue::Double(y)) => x.to_bits() == y.to_bits(),
554 (SqlValue::Vector(xs), SqlValue::Vector(ys)) => {
555 xs.len() == ys.len()
556 && xs
557 .iter()
558 .zip(ys.iter())
559 .all(|(x, y)| x.to_bits() == y.to_bits())
560 }
561 _ => a == b,
562 }
563 }
564
565 fn row_equal(a: &[SqlValue], b: &[SqlValue]) -> bool {
566 a.len() == b.len()
567 && a.iter()
568 .zip(b.iter())
569 .all(|(lhs, rhs)| values_equal(lhs, rhs))
570 }
571
572 fn sql_value_strategy() -> impl Strategy<Value = SqlValue> {
573 let finite_f32 = any::<f32>();
574 let finite_f64 = any::<f64>();
575 let decimal_max = crate::storage::value::decimal_power(38).unwrap() - 1;
576 prop_oneof![
577 Just(SqlValue::Null),
578 any::<i32>().prop_map(SqlValue::Integer),
579 any::<i64>().prop_map(SqlValue::BigInt),
580 finite_f32.prop_map(SqlValue::Float),
581 finite_f64.prop_map(SqlValue::Double),
582 ".*".prop_map(SqlValue::Text),
583 proptest::collection::vec(any::<u8>(), 0..32).prop_map(SqlValue::Blob),
584 any::<bool>().prop_map(SqlValue::Boolean),
585 any::<i64>().prop_map(SqlValue::Timestamp),
586 proptest::collection::vec(any::<f32>(), 0..8).prop_map(SqlValue::Vector),
587 any::<i32>().prop_map(SqlValue::Date),
588 any::<i64>().prop_map(SqlValue::Time),
589 (any::<i32>(), any::<i32>(), any::<i64>()).prop_map(|(months, days, micros)| {
590 SqlValue::Interval {
591 months,
592 days,
593 micros,
594 }
595 }),
596 (-decimal_max..=decimal_max, 0_u8..=38).prop_map(|(coefficient, scale)| {
597 SqlValue::Decimal(crate::storage::DecimalValue::new(coefficient, scale))
598 }),
599 ]
600 }
601
602 #[test]
603 fn roundtrip_preserves_all_types() {
604 let row = vec![
605 SqlValue::Null,
606 SqlValue::Integer(42),
607 SqlValue::BigInt(-42),
608 SqlValue::Float(1.5),
609 SqlValue::Double(-2.5),
610 SqlValue::Text("hello".into()),
611 SqlValue::Blob(vec![0x01, 0x02]),
612 SqlValue::Boolean(true),
613 SqlValue::Timestamp(1_700_000_000),
614 SqlValue::Vector(vec![0.1, 0.2, 0.3]),
615 SqlValue::Date(19_782),
616 SqlValue::Time(86_399_123_456),
617 SqlValue::Interval {
618 months: -1,
619 days: 2,
620 micros: 3,
621 },
622 SqlValue::Decimal(crate::storage::DecimalValue::new(-12345, 2)),
623 SqlValue::Array(vec![SqlValue::Integer(1), SqlValue::Null]),
624 SqlValue::Map(vec![(SqlValue::Text("a".into()), SqlValue::Integer(1))]),
625 SqlValue::Struct(vec![(
626 "items".into(),
627 SqlValue::Array(vec![SqlValue::Text("x".into())]),
628 )]),
629 ];
630
631 let encoded = RowCodec::encode(&row);
632 let decoded = RowCodec::decode(&encoded).unwrap();
633
634 assert!(row_equal(&row, &decoded));
635 }
636
637 #[test]
638 fn pre_temporal_row_bytes_remain_readable() {
639 let bytes = [
640 2, 0, 0, 0x01, 42, 0, 0, 0, 0x08, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, ];
645
646 assert_eq!(
647 RowCodec::decode(&bytes).unwrap(),
648 vec![SqlValue::Integer(42), SqlValue::Timestamp(-1)]
649 );
650 }
651
652 #[test]
653 fn null_bitmap_is_respected() {
654 let row = vec![SqlValue::Integer(1), SqlValue::Null, SqlValue::Integer(2)];
655 let encoded = RowCodec::encode(&row);
656 let decoded = RowCodec::decode(&encoded).unwrap();
657 assert!(matches!(decoded[1], SqlValue::Null));
658 }
659
660 #[test]
661 fn corruption_is_detected_for_truncated_payload() {
662 let row = vec![SqlValue::Text("abc".into())];
663 let mut encoded = RowCodec::encode(&row);
664 encoded.pop(); let err = RowCodec::decode(&encoded).unwrap_err();
666 assert!(matches!(err, StorageError::CorruptedData { .. }));
667 }
668
669 #[test]
670 fn corruption_is_detected_for_unknown_tag() {
671 let bytes = vec![1, 0, 0, 0xFF];
673 let err = RowCodec::decode(&bytes).unwrap_err();
674 assert!(matches!(err, StorageError::CorruptedData { .. }));
675 }
676
677 #[test]
678 fn oversized_lengths_are_rejected() {
679 let mut bytes = Vec::new();
681 bytes.extend_from_slice(&(1u16).to_le_bytes()); bytes.push(0); bytes.push(0x05); let too_large = (super::MAX_INLINE_BYTES as u32) + 1;
685 bytes.extend_from_slice(&too_large.to_le_bytes());
686 let err = RowCodec::decode(&bytes).unwrap_err();
687 assert!(matches!(err, StorageError::CorruptedData { .. }));
688 }
689
690 #[test]
691 fn oversized_vector_is_rejected() {
692 let mut bytes = Vec::new();
693 bytes.extend_from_slice(&(1u16).to_le_bytes()); bytes.push(0); bytes.push(0x09); let too_large = (super::MAX_VECTOR_LEN as u32) + 1;
697 bytes.extend_from_slice(&too_large.to_le_bytes());
698 let err = RowCodec::decode(&bytes).unwrap_err();
699 assert!(matches!(err, StorageError::CorruptedData { .. }));
700 }
701
702 #[test]
703 fn decode_with_schema_validates_types() {
704 let row = vec![SqlValue::Vector(vec![1.0, 2.0])];
705 let encoded = RowCodec::encode(&row);
706 let schema = vec![ResolvedType::Vector {
707 dimension: 3,
708 metric: crate::ast::ddl::VectorMetric::Cosine,
709 }];
710 let err = RowCodec::decode_with_schema(&encoded, &schema).unwrap_err();
711 assert!(matches!(err, StorageError::TypeMismatch { .. }));
712 }
713
714 proptest! {
715 #[test]
716 fn proptest_roundtrip(row in proptest::collection::vec(sql_value_strategy(), 0..16)) {
717 let encoded = RowCodec::encode(&row);
718 let decoded = RowCodec::decode(&encoded).unwrap();
719 prop_assert!(row_equal(&row, &decoded));
720 }
721
722 #[test]
723 fn decode_with_schema_matches_lengths(row in proptest::collection::vec(sql_value_strategy(), 1..5)) {
724 let schema: Vec<ResolvedType> = row.iter().map(|v| v.resolved_type()).collect();
725 let encoded = RowCodec::encode(&row);
726 let decoded = RowCodec::decode_with_schema(&encoded, &schema).unwrap();
727 prop_assert!(row_equal(&row, &decoded));
728 }
729 }
730}