1use alloc::string::String;
25use alloc::vec;
26use alloc::vec::Vec;
27use core::hash::Hash;
28
29use crate::IndexableValues;
30
31type UpdateValues = Vec<(MaybeValue<String, Vec<u8>>, MaybeValue<String, Vec<u8>>)>;
33
34type ParsedValues = (Vec<MaybeValue<String, Vec<u8>>>, usize);
36use crate::builders::{ChangesetFormat, DiffSet, DiffSetBuilder, Operation, PatchsetFormat};
37use crate::encoding::{MaybeValue, Value, decode_value, markers, op_codes};
38use crate::schema::{DynTable, SchemaWithPK};
39
40#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
42pub enum ParseError {
43 #[error("Unexpected end of input at position {0}")]
45 UnexpectedEof(usize),
46
47 #[error("Invalid table marker 0x{0:02x} at position {1}")]
49 InvalidTableMarker(u8, usize),
50
51 #[error("Invalid operation code 0x{0:02x} at position {1}")]
53 InvalidOpCode(u8, usize),
54
55 #[error("Invalid UTF-8 in table name at position {0}")]
57 InvalidTableName(usize),
58
59 #[error("Failed to decode value at position {0}")]
61 InvalidValue(usize),
62
63 #[error("Table name not null-terminated")]
65 UnterminatedTableName,
66
67 #[error("Mixed format markers: expected {expected:?}, found {found:?} at position {position}")]
69 MixedFormats {
70 expected: FormatMarker,
72 found: FormatMarker,
74 position: usize,
76 },
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub enum FormatMarker {
82 Changeset,
84 Patchset,
86}
87
88#[derive(Debug, Clone, PartialEq, Eq, Hash)]
93pub struct TableSchema<S> {
94 name: S,
96 column_count: usize,
98 pk_flags: Vec<u8>,
105}
106
107impl<S> TableSchema<S> {
108 #[inline]
110 #[must_use]
111 pub fn new(name: S, column_count: usize, pk_flags: Vec<u8>) -> Self {
112 debug_assert_eq!(pk_flags.len(), column_count);
113 Self {
114 name,
115 column_count,
116 pk_flags,
117 }
118 }
119
120 #[inline]
122 #[must_use]
123 pub fn name(&self) -> &S {
124 &self.name
125 }
126
127 #[inline]
132 #[must_use]
133 pub fn pk_flags(&self) -> &[u8] {
134 &self.pk_flags
135 }
136
137 #[must_use]
139 pub(crate) fn pk_indices(&self) -> Vec<usize> {
140 let mut pk_cols: Vec<(usize, u8)> = self
142 .pk_flags
143 .iter()
144 .enumerate()
145 .filter_map(|(i, &pk_ordinal)| {
146 if pk_ordinal > 0 {
147 Some((i, pk_ordinal))
148 } else {
149 None
150 }
151 })
152 .collect();
153 pk_cols.sort_by_key(|(_, ordinal)| *ordinal);
155 pk_cols.into_iter().map(|(idx, _)| idx).collect()
156 }
157}
158
159impl<S: AsRef<str> + Clone + Eq + core::fmt::Debug> DynTable for TableSchema<S> {
160 #[inline]
161 fn name(&self) -> &str {
162 self.name.as_ref()
163 }
164
165 #[inline]
166 fn number_of_columns(&self) -> usize {
167 self.column_count
168 }
169
170 #[inline]
171 fn write_pk_flags(&self, buf: &mut [u8]) {
172 assert_eq!(buf.len(), self.column_count);
173 buf.copy_from_slice(&self.pk_flags);
174 }
175}
176
177impl<N: AsRef<str> + Clone + core::hash::Hash + Eq + core::fmt::Debug> SchemaWithPK
178 for TableSchema<N>
179{
180 fn number_of_primary_keys(&self) -> usize {
181 self.pk_flags.iter().filter(|&&b| b > 0).count()
182 }
183
184 fn primary_key_index(&self, col_idx: usize) -> Option<usize> {
185 self.pk_flags.get(col_idx).and_then(|&pk_ordinal| {
186 if pk_ordinal > 0 {
187 Some(usize::from(pk_ordinal - 1))
188 } else {
189 None
190 }
191 })
192 }
193
194 fn extract_pk<S, B>(
195 &self,
196 values: &impl IndexableValues<Text = S, Binary = B>,
197 ) -> alloc::vec::Vec<Value<S, B>>
198 where
199 S: Clone,
200 B: Clone,
201 {
202 self.pk_indices()
203 .into_iter()
204 .map(|i| {
205 values
206 .get(i)
207 .expect("primary key column index out of bounds, values shorter than schema")
208 })
209 .collect()
210 }
211}
212
213#[derive(Debug, Clone, Eq)]
218pub enum ParsedDiffSet {
219 Changeset(DiffSet<ChangesetFormat, TableSchema<String>, String, Vec<u8>>),
221 Patchset(DiffSet<PatchsetFormat, TableSchema<String>, String, Vec<u8>>),
223}
224
225impl PartialEq for ParsedDiffSet {
226 fn eq(&self, other: &Self) -> bool {
227 let self_empty = match self {
228 ParsedDiffSet::Changeset(d) => d.is_empty(),
229 ParsedDiffSet::Patchset(d) => d.is_empty(),
230 };
231 let other_empty = match other {
232 ParsedDiffSet::Changeset(d) => d.is_empty(),
233 ParsedDiffSet::Patchset(d) => d.is_empty(),
234 };
235
236 if self_empty && other_empty {
237 return true;
238 }
239
240 match (self, other) {
242 (ParsedDiffSet::Changeset(a), ParsedDiffSet::Changeset(b)) => a == b,
243 (ParsedDiffSet::Patchset(a), ParsedDiffSet::Patchset(b)) => a == b,
244 _ => false,
245 }
246 }
247}
248
249impl TryFrom<&[u8]> for ParsedDiffSet {
250 type Error = ParseError;
251
252 fn try_from(data: &[u8]) -> Result<Self, Self::Error> {
253 Self::parse(data)
254 }
255}
256
257impl From<ParsedDiffSet> for Vec<u8> {
258 fn from(diffset: ParsedDiffSet) -> Self {
259 match diffset {
260 ParsedDiffSet::Changeset(d) => d.into(),
261 ParsedDiffSet::Patchset(d) => d.into(),
262 }
263 }
264}
265
266impl ParsedDiffSet {
267 pub fn parse(data: &[u8]) -> Result<Self, ParseError> {
275 if data.is_empty() {
276 return Ok(ParsedDiffSet::Changeset(DiffSet::default()));
278 }
279
280 match data[0] {
282 markers::CHANGESET => {
283 let diffset = parse_as_changeset(data)?;
284 Ok(ParsedDiffSet::Changeset(diffset))
285 }
286 markers::PATCHSET => {
287 let diffset = parse_as_patchset(data)?;
288 Ok(ParsedDiffSet::Patchset(diffset))
289 }
290 b => Err(ParseError::InvalidTableMarker(b, 0)),
291 }
292 }
293
294 #[must_use]
296 pub fn is_changeset(&self) -> bool {
297 matches!(self, ParsedDiffSet::Changeset(_))
298 }
299
300 #[must_use]
302 pub fn is_patchset(&self) -> bool {
303 matches!(self, ParsedDiffSet::Patchset(_))
304 }
305
306 #[must_use]
308 pub fn table_schemas(&self) -> Vec<&TableSchema<String>> {
309 match self {
310 ParsedDiffSet::Changeset(d) => d
311 .tables
312 .iter()
313 .filter(|(_, ops)| !ops.is_empty())
314 .map(|(schema, _)| schema)
315 .collect(),
316 ParsedDiffSet::Patchset(d) => d
317 .tables
318 .iter()
319 .filter(|(_, ops)| !ops.is_empty())
320 .map(|(schema, _)| schema)
321 .collect(),
322 }
323 }
324}
325
326fn parse_as_changeset(
332 data: &[u8],
333) -> Result<DiffSet<ChangesetFormat, TableSchema<String>, String, Vec<u8>>, ParseError> {
334 let mut builder: DiffSetBuilder<ChangesetFormat, TableSchema<String>, String, Vec<u8>> =
335 DiffSetBuilder::new();
336 let mut pos = 0;
337
338 while pos < data.len() {
339 let (schema, format, header_len) = parse_table_header(&data[pos..], pos)?;
340 if format != FormatMarker::Changeset {
341 return Err(ParseError::MixedFormats {
342 expected: FormatMarker::Changeset,
343 found: format,
344 position: pos,
345 });
346 }
347 pos += header_len;
348
349 while pos < data.len() {
350 let byte = data[pos];
351 if byte == markers::CHANGESET || byte == markers::PATCHSET {
352 break;
353 }
354 let op_len = parse_changeset_operation(&data[pos..], pos, &schema, &mut builder)?;
355 pos += op_len;
356 }
357 }
358
359 Ok(builder.into())
360}
361
362fn parse_as_patchset(
368 data: &[u8],
369) -> Result<DiffSet<PatchsetFormat, TableSchema<String>, String, Vec<u8>>, ParseError> {
370 let mut builder: DiffSetBuilder<PatchsetFormat, TableSchema<String>, String, Vec<u8>> =
371 DiffSetBuilder::new();
372 let mut pos = 0;
373
374 while pos < data.len() {
375 let (schema, format, header_len) = parse_table_header(&data[pos..], pos)?;
376 if format != FormatMarker::Patchset {
377 return Err(ParseError::MixedFormats {
378 expected: FormatMarker::Patchset,
379 found: format,
380 position: pos,
381 });
382 }
383 pos += header_len;
384
385 while pos < data.len() {
386 let byte = data[pos];
387 if byte == markers::CHANGESET || byte == markers::PATCHSET {
388 break;
389 }
390 let op_len = parse_patchset_operation(&data[pos..], pos, &schema, &mut builder)?;
391 pos += op_len;
392 }
393 }
394
395 Ok(builder.into())
396}
397
398fn parse_table_header(
400 data: &[u8],
401 base_pos: usize,
402) -> Result<(TableSchema<String>, FormatMarker, usize), ParseError> {
403 let mut pos = 0;
404
405 if data.is_empty() {
406 return Err(ParseError::UnexpectedEof(base_pos));
407 }
408 let format = match data[pos] {
409 markers::CHANGESET => FormatMarker::Changeset,
410 markers::PATCHSET => FormatMarker::Patchset,
411 b => return Err(ParseError::InvalidTableMarker(b, base_pos + pos)),
412 };
413 pos += 1;
414
415 if pos >= data.len() {
416 return Err(ParseError::UnexpectedEof(base_pos + pos));
417 }
418 let column_count = data[pos] as usize;
419 pos += 1;
420
421 if pos + column_count > data.len() {
422 return Err(ParseError::UnexpectedEof(base_pos + pos));
423 }
424 let pk_flags: Vec<u8> = data[pos..pos + column_count].to_vec();
425 pos += column_count;
426
427 let name_start = pos;
428 while pos < data.len() && data[pos] != 0 {
429 pos += 1;
430 }
431 if pos >= data.len() {
432 return Err(ParseError::UnterminatedTableName);
433 }
434 let name = String::from_utf8(data[name_start..pos].to_vec())
435 .map_err(|_| ParseError::InvalidTableName(base_pos + name_start))?;
436 pos += 1;
437
438 Ok((TableSchema::new(name, column_count, pk_flags), format, pos))
439}
440
441fn parse_operation_header(data: &[u8], base_pos: usize) -> Result<(u8, bool, usize), ParseError> {
446 if data.len() < 2 {
447 return Err(ParseError::UnexpectedEof(base_pos));
448 }
449 Ok((data[0], data[1] != 0, 2))
450}
451
452fn parse_changeset_operation(
454 data: &[u8],
455 base_pos: usize,
456 schema: &TableSchema<String>,
457 builder: &mut DiffSetBuilder<ChangesetFormat, TableSchema<String>, String, Vec<u8>>,
458) -> Result<usize, ParseError> {
459 let (op_code, indirect, mut pos) = parse_operation_header(data, base_pos)?;
460
461 match op_code {
462 op_codes::INSERT => {
463 let (values, len) = parse_values(&data[pos..], base_pos + pos, schema.column_count)?;
464 pos += len;
465 let values: Vec<Value<String, Vec<u8>>> = values
466 .into_iter()
467 .map(|v| v.unwrap_or(Value::Null))
468 .collect();
469 let pk = schema.extract_pk(&values);
470 builder.add_operation(schema, pk, Operation::Insert { values, indirect });
471 }
472 op_codes::DELETE => {
473 let (values, len) = parse_values(&data[pos..], base_pos + pos, schema.column_count)?;
474 pos += len;
475 let values: Vec<Value<String, Vec<u8>>> = values
476 .into_iter()
477 .map(|v| v.unwrap_or(Value::Null))
478 .collect();
479 let pk = schema.extract_pk(&values);
480 builder.add_operation(
481 schema,
482 pk,
483 Operation::Delete {
484 data: values,
485 indirect,
486 },
487 );
488 }
489 op_codes::UPDATE => {
490 let (old_values, old_len) =
491 parse_values(&data[pos..], base_pos + pos, schema.column_count)?;
492 pos += old_len;
493 let (new_values, new_len) =
494 parse_values(&data[pos..], base_pos + pos, schema.column_count)?;
495 pos += new_len;
496 let pk_values: Vec<Value<String, Vec<u8>>> = old_values
498 .iter()
499 .map(|v| v.clone().unwrap_or(Value::Null))
500 .collect();
501 let pk = schema.extract_pk(&pk_values);
502 let values: UpdateValues = old_values.into_iter().zip(new_values).collect();
503 builder.add_operation(schema, pk, Operation::Update { values, indirect });
504 }
505 _ => return Err(ParseError::InvalidOpCode(op_code, base_pos)),
506 }
507
508 Ok(pos)
509}
510
511fn parse_patchset_operation(
513 data: &[u8],
514 base_pos: usize,
515 schema: &TableSchema<String>,
516 builder: &mut DiffSetBuilder<PatchsetFormat, TableSchema<String>, String, Vec<u8>>,
517) -> Result<usize, ParseError> {
518 let (op_code, indirect, mut pos) = parse_operation_header(data, base_pos)?;
519
520 match op_code {
521 op_codes::INSERT => {
522 let (values, len) = parse_values(&data[pos..], base_pos + pos, schema.column_count)?;
523 pos += len;
524 let values: Vec<Value<String, Vec<u8>>> = values
525 .into_iter()
526 .map(|v| v.unwrap_or(Value::Null))
527 .collect();
528 let pk = schema.extract_pk(&values);
529 builder.add_operation(schema, pk, Operation::Insert { values, indirect });
530 }
531 op_codes::DELETE => {
532 let pk_count = schema.pk_flags.iter().filter(|&&b| b > 0).count();
534 let (pk_values, len) = parse_values(&data[pos..], base_pos + pos, pk_count)?;
535 pos += len;
536 let full_values = expand_pk_values(&schema.pk_flags, pk_values, schema.column_count);
540 let full_values_concrete: Vec<Value<String, Vec<u8>>> = full_values
542 .into_iter()
543 .map(|v| v.unwrap_or(Value::Null))
544 .collect();
545 let pk = schema.extract_pk(&full_values_concrete);
546 builder.add_operation(schema, pk, Operation::Delete { data: (), indirect });
547 }
548 op_codes::UPDATE => {
549 let pk_count = schema.pk_flags.iter().filter(|&&b| b > 0).count();
563 let non_pk_count = schema.column_count.saturating_sub(pk_count);
564
565 let (old_pk_values, old_len) = parse_values(&data[pos..], base_pos + pos, pk_count)?;
566 pos += old_len;
567 let (new_non_pk_values, new_len) =
568 parse_values(&data[pos..], base_pos + pos, non_pk_count)?;
569 pos += new_len;
570
571 let mut values: Vec<((), MaybeValue<String, Vec<u8>>)> =
572 alloc::vec![((), None); schema.column_count];
573 let mut old_iter = old_pk_values.into_iter();
574 let mut new_iter = new_non_pk_values.into_iter();
575 for (col_idx, &pk_flag) in schema.pk_flags.iter().enumerate() {
576 if pk_flag > 0 {
577 let old = old_iter.next().flatten().unwrap_or(Value::Null);
582 values[col_idx] = ((), Some(old));
583 } else {
584 values[col_idx] = ((), new_iter.next().flatten());
585 }
586 }
587
588 let pk = schema.extract_pk(&values);
589 builder.add_operation(schema, pk, Operation::Update { values, indirect });
590 }
591 _ => return Err(ParseError::InvalidOpCode(op_code, base_pos)),
592 }
593
594 Ok(pos)
595}
596
597fn expand_pk_values(
602 pk_flags: &[u8],
603 pk_values: Vec<MaybeValue<String, Vec<u8>>>,
604 column_count: usize,
605) -> Vec<MaybeValue<String, Vec<u8>>> {
606 let mut full: Vec<MaybeValue<String, Vec<u8>>> = vec![None; column_count];
607 let mut pk_iter = pk_values.into_iter();
608 for (i, &pk_ordinal) in pk_flags.iter().enumerate() {
609 if pk_ordinal > 0
610 && let Some(v) = pk_iter.next()
611 {
612 full[i] = v;
613 }
614 }
615 full
616}
617
618fn parse_values(data: &[u8], base_pos: usize, count: usize) -> Result<ParsedValues, ParseError> {
620 let mut values = Vec::with_capacity(count);
621 let mut pos = 0;
622
623 for _ in 0..count {
624 let (value, value_len) =
625 decode_value(&data[pos..]).ok_or(ParseError::InvalidValue(base_pos + pos))?;
626 values.push(value);
627 pos += value_len;
628 }
629
630 Ok((values, pos))
631}
632
633#[cfg(test)]
634mod tests {
635 use super::*;
636 use alloc::vec;
637
638 #[test]
639 fn test_parse_empty() {
640 let result = ParsedDiffSet::parse(&[]);
641 assert!(result.is_ok());
642 assert!(result.unwrap().is_changeset());
643 }
644
645 #[test]
646 fn test_parse_table_header() {
647 let data = [b'T', 2, 1, 0, b't', 0];
649 let (schema, format, len) = parse_table_header(&data, 0).unwrap();
650
651 assert_eq!(format, FormatMarker::Changeset);
652 assert_eq!(schema.column_count, 2);
653 assert_eq!(schema.pk_flags, vec![1, 0]); assert_eq!(schema.name, "t");
655 assert_eq!(len, 6);
656 }
657
658 #[test]
659 fn test_parse_insert_changeset() {
660 let mut data = vec![b'T', 2, 1, 0, b't', 0];
662 data.push(op_codes::INSERT);
664 data.push(0);
665 data.push(0x01);
667 data.extend(&1i64.to_be_bytes());
668 data.push(0x03);
670 data.push(1);
671 data.push(b'a');
672
673 let parsed = ParsedDiffSet::parse(&data).unwrap();
674 assert!(parsed.is_changeset());
675 }
676
677 #[test]
678 fn test_parse_delete_changeset() {
679 let mut data = vec![b'T', 2, 1, 0, b't', 0];
680 data.push(op_codes::DELETE);
681 data.push(0);
682 data.push(0x01);
684 data.extend(&1i64.to_be_bytes());
685 data.push(0x03);
687 data.push(1);
688 data.push(b'a');
689
690 let parsed = ParsedDiffSet::parse(&data).unwrap();
691 assert!(parsed.is_changeset());
692 }
693
694 #[test]
695 fn test_parse_delete_patchset() {
696 let mut data = vec![b'P', 2, 1, 0, b't', 0];
698 data.push(op_codes::DELETE);
699 data.push(0);
700 data.push(0x01);
702 data.extend(&1i64.to_be_bytes());
703
704 let parsed = ParsedDiffSet::parse(&data).unwrap();
705 assert!(parsed.is_patchset());
706 }
707
708 #[test]
709 fn test_parse_update_changeset() {
710 let mut data = vec![b'T', 2, 1, 0, b't', 0];
711 data.push(op_codes::UPDATE);
712 data.push(0);
713 data.push(0x01);
715 data.extend(&1i64.to_be_bytes());
716 data.push(0x03);
717 data.push(1);
718 data.push(b'a');
719 data.push(0x01);
721 data.extend(&1i64.to_be_bytes());
722 data.push(0x03);
723 data.push(1);
724 data.push(b'b');
725
726 let parsed = ParsedDiffSet::parse(&data).unwrap();
727 assert!(parsed.is_changeset());
728 }
729
730 #[test]
731 fn test_is_changeset() {
732 let data = vec![b'T', 1, 1, b't', 0];
733 let parsed = ParsedDiffSet::parse(&data).unwrap();
734 assert!(parsed.is_changeset());
735 assert!(!parsed.is_patchset());
736 }
737
738 #[test]
739 fn test_is_patchset() {
740 let data = vec![b'P', 1, 1, b't', 0];
741 let parsed = ParsedDiffSet::parse(&data).unwrap();
742 assert!(parsed.is_patchset());
743 assert!(!parsed.is_changeset());
744 }
745
746 #[test]
747 fn test_parsed_table_schema_dyn_table() {
748 let schema: TableSchema<String> = TableSchema::new("users".into(), 3, vec![1, 0, 0]);
749 assert_eq!(schema.name(), "users");
750 assert_eq!(schema.number_of_columns(), 3);
751
752 let mut buf = [0u8; 3];
753 schema.write_pk_flags(&mut buf);
754 assert_eq!(buf, [1, 0, 0]);
755 }
756
757 #[test]
758 fn test_parsed_table_schema_extract_pk() {
759 let schema: TableSchema<String> = TableSchema::new("users".into(), 3, vec![1, 0, 2]);
760 let values: Vec<Value<String, Vec<u8>>> = vec![
761 Value::Integer(1),
762 Value::Text("alice".into()),
763 Value::Integer(100),
764 ];
765 let pk = schema.extract_pk(&values);
766 let expected: Vec<Value<String, Vec<u8>>> = vec![Value::Integer(1), Value::Integer(100)];
767 assert_eq!(pk, expected);
768 }
769
770 #[test]
773 fn test_parse_invalid_table_marker() {
774 let data = [0xFFu8, 1, 1, b't', 0];
775 let err = ParsedDiffSet::parse(&data).unwrap_err();
776 assert!(
777 matches!(err, ParseError::InvalidTableMarker(0xFF, 0)),
778 "got {err:?}"
779 );
780 }
781
782 #[test]
783 fn test_parse_unexpected_eof_in_table_header() {
784 let data = *b"T";
786 let err = ParsedDiffSet::parse(&data).unwrap_err();
787 assert!(matches!(err, ParseError::UnexpectedEof(_)), "got {err:?}");
788 }
789
790 #[test]
791 fn test_parse_unexpected_eof_in_pk_flags() {
792 let data = [b'T', 3, 1];
794 let err = ParsedDiffSet::parse(&data).unwrap_err();
795 assert!(matches!(err, ParseError::UnexpectedEof(_)), "got {err:?}");
796 }
797
798 #[test]
799 fn test_parse_unterminated_table_name() {
800 let data = [b'T', 1, 1, b'a', b'b', b'c'];
802 let err = ParsedDiffSet::parse(&data).unwrap_err();
803 assert!(
804 matches!(err, ParseError::UnterminatedTableName),
805 "got {err:?}"
806 );
807 }
808
809 #[test]
810 fn test_parse_invalid_utf8_in_table_name() {
811 let data = [b'T', 1, 1, 0xFF, 0];
813 let err = ParsedDiffSet::parse(&data).unwrap_err();
814 assert!(
815 matches!(err, ParseError::InvalidTableName(_)),
816 "got {err:?}"
817 );
818 }
819
820 #[test]
821 fn test_parse_mixed_formats_changeset_then_patchset() {
822 let mut data = vec![b'T', 1, 1, b'a', 0];
824 data.extend_from_slice(&[b'P', 1, 1, b'b', 0]);
826 let err = ParsedDiffSet::parse(&data).unwrap_err();
827 assert!(
828 matches!(
829 err,
830 ParseError::MixedFormats {
831 expected: FormatMarker::Changeset,
832 found: FormatMarker::Patchset,
833 ..
834 }
835 ),
836 "got {err:?}"
837 );
838 }
839
840 #[test]
841 fn test_parse_mixed_formats_patchset_then_changeset() {
842 let mut data = vec![b'P', 1, 1, b'a', 0];
843 data.extend_from_slice(&[b'T', 1, 1, b'b', 0]);
844 let err = ParsedDiffSet::parse(&data).unwrap_err();
845 assert!(
846 matches!(
847 err,
848 ParseError::MixedFormats {
849 expected: FormatMarker::Patchset,
850 found: FormatMarker::Changeset,
851 ..
852 }
853 ),
854 "got {err:?}"
855 );
856 }
857
858 fn make_insert_with_indirect(indirect_byte: u8) -> Vec<u8> {
860 let mut data = vec![b'T', 1, 1, b't', 0];
861 data.push(op_codes::INSERT);
862 data.push(indirect_byte);
863 data.push(0x01);
865 data.extend(&1i64.to_be_bytes());
866 data
867 }
868
869 fn first_op_indirect_changeset(data: &[u8]) -> bool {
870 let parsed = ParsedDiffSet::parse(data).unwrap();
871 let ParsedDiffSet::Changeset(set) = parsed else {
872 panic!("expected Changeset");
873 };
874 set.tables
875 .iter()
876 .find_map(|(_schema, rows)| rows.first().map(|(_, op)| op.indirect()))
877 .expect("expected at least one op")
878 }
879
880 #[test]
881 fn test_parse_changeset_indirect_flag_set() {
882 let data = make_insert_with_indirect(1);
883 assert!(first_op_indirect_changeset(&data));
884 }
885
886 #[test]
887 fn test_parse_changeset_indirect_flag_clear() {
888 let data = make_insert_with_indirect(0);
889 assert!(!first_op_indirect_changeset(&data));
890 }
891
892 #[test]
893 fn test_parse_indirect_nonzero_treated_as_true() {
894 let data = make_insert_with_indirect(0x42);
896 assert!(first_op_indirect_changeset(&data));
897 }
898
899 #[test]
900 fn test_parsed_diffset_variant_mismatch_partial_eq() {
901 let changeset = ParsedDiffSet::parse(&[b'T', 1, 1, b't', 0]).unwrap();
902 let patchset = ParsedDiffSet::parse(&[b'P', 1, 1, b't', 0]).unwrap();
903 let mut full_changeset = vec![b'T', 1, 1, b't', 0];
906 full_changeset.push(op_codes::INSERT);
907 full_changeset.push(0);
908 full_changeset.push(0x01);
909 full_changeset.extend(&1i64.to_be_bytes());
910 let cs = ParsedDiffSet::parse(&full_changeset).unwrap();
911
912 let mut full_patchset = vec![b'P', 1, 1, b't', 0];
913 full_patchset.push(op_codes::INSERT);
914 full_patchset.push(0);
915 full_patchset.push(0x01);
916 full_patchset.extend(&1i64.to_be_bytes());
917 let ps = ParsedDiffSet::parse(&full_patchset).unwrap();
918
919 assert_ne!(cs, ps);
920 assert_eq!(changeset, patchset);
922 }
923
924 #[test]
925 fn test_parse_unexpected_eof_in_operation_header() {
926 let data = [b'T', 1, 1, b't', 0, op_codes::INSERT];
929 let err = ParsedDiffSet::parse(&data).unwrap_err();
930 assert!(matches!(err, ParseError::UnexpectedEof(_)), "got {err:?}");
931 }
932
933 #[test]
934 fn test_parse_patchset_indirect_flag_set() {
935 let mut data = vec![b'P', 1, 1, b't', 0];
937 data.push(op_codes::INSERT);
938 data.push(1);
939 data.push(0x01);
940 data.extend(&1i64.to_be_bytes());
941
942 let parsed = ParsedDiffSet::parse(&data).unwrap();
943 let ParsedDiffSet::Patchset(set) = parsed else {
944 panic!("expected Patchset");
945 };
946 let indirect = set
947 .tables
948 .iter()
949 .find_map(|(_schema, rows)| rows.first().map(|(_, op)| op.indirect()))
950 .expect("expected at least one op");
951 assert!(indirect);
952 }
953
954 fn assert_patchset_update_roundtrip(
964 data: &[u8],
965 check: impl FnOnce(
966 &TableSchema<String>,
967 &[Value<String, Vec<u8>>],
968 &[((), MaybeValue<String, Vec<u8>>)],
969 bool,
970 ),
971 ) {
972 let parsed = ParsedDiffSet::parse(data).expect("SQLite patchset UPDATE must parse");
973 let ParsedDiffSet::Patchset(set) = parsed else {
974 panic!("expected Patchset, got {parsed:?}");
975 };
976 let (schema, rows) = set.tables.first().expect("expected one table");
977 assert_eq!(rows.len(), 1, "expected exactly one row");
978 let (pk, op) = rows.first().expect("row map non-empty");
979 let Operation::Update { values, indirect } = op else {
980 panic!("expected Update, got {op:?}");
981 };
982 check(schema, pk.as_slice(), values.as_slice(), *indirect);
983 let serialized: Vec<u8> = set.into();
984 assert_eq!(serialized, data, "roundtrip must match SQLite output");
985 }
986
987 #[test]
1005 fn test_parse_patchset_update_sqlite_wire_layout_single_pk() {
1006 let data: [u8; 33] = [
1007 0x50, 0x03, 0x01, 0x00, 0x00, b'o', b'r', b'd', b'e', b'r', b's', 0x00, 0x17, 0x00,
1008 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x07, b's', b'h',
1009 b'i', b'p', b'p', b'e', b'd',
1010 ];
1011 assert_patchset_update_roundtrip(&data, |schema, pk, values, indirect| {
1012 assert_eq!(schema.name, "orders");
1013 assert_eq!(schema.column_count, 3);
1014 assert_eq!(schema.pk_flags, vec![1, 0, 0]);
1015 assert_eq!(pk, &[Value::Integer(5)]);
1016 assert!(!indirect);
1017 assert_eq!(values.len(), 3);
1018 assert_eq!(values[0].1, Some(Value::Integer(5))); assert_eq!(values[1].1, None); assert_eq!(values[2].1, Some(Value::Text("shipped".into())));
1021 });
1022 }
1023
1024 #[test]
1035 fn test_parse_patchset_update_sqlite_wire_layout_composite_pk() {
1036 let data: [u8; 35] = [
1037 0x50, 0x03, 0x01, 0x02, 0x00, b'i', b't', b'e', b'm', b's', 0x00, 0x17, 0x00, 0x01,
1038 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
1039 0x00, 0x00, 0x02, 0x03, 0x02, b'v', b'2',
1040 ];
1041 assert_patchset_update_roundtrip(&data, |schema, pk, values, _indirect| {
1042 assert_eq!(schema.name, "items");
1043 assert_eq!(schema.pk_flags, vec![1, 2, 0]);
1044 assert_eq!(pk, &[Value::Integer(1), Value::Integer(2)]);
1047 assert_eq!(values.len(), 3);
1048 assert_eq!(values[0].1, Some(Value::Integer(1))); assert_eq!(values[1].1, Some(Value::Integer(2))); assert_eq!(values[2].1, Some(Value::Text("v2".into())));
1051 });
1052 }
1053
1054 #[test]
1064 fn test_parse_patchset_update_all_non_pk_changed() {
1065 let data: [u8; 41] = [
1066 0x50, 0x03, 0x01, 0x00, 0x00, b'o', b'r', b'd', b'e', b'r', b's', 0x00, 0x17, 0x00,
1067 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00,
1068 0x00, 0x00, 0x00, 0xc8, 0x03, 0x07, b's', b'h', b'i', b'p', b'p', b'e', b'd',
1069 ];
1070 assert_patchset_update_roundtrip(&data, |_schema, _pk, values, _indirect| {
1071 assert_eq!(values[0].1, Some(Value::Integer(5)));
1072 assert_eq!(values[1].1, Some(Value::Integer(200)));
1073 assert_eq!(values[2].1, Some(Value::Text("shipped".into())));
1074 });
1075 }
1076}