use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec;
use core::hash::Hash;
use crate::IndexableValues;
type UpdateValues = Vec<(MaybeValue<String, Vec<u8>>, MaybeValue<String, Vec<u8>>)>;
type ParsedValues = (Vec<MaybeValue<String, Vec<u8>>>, usize);
use crate::builders::{ChangesetFormat, DiffSet, DiffSetBuilder, Operation, PatchsetFormat};
use crate::encoding::{MaybeValue, Value, decode_value, markers, op_codes};
use crate::schema::{DynTable, SchemaWithPK};
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum ParseError {
#[error("Unexpected end of input at position {0}")]
UnexpectedEof(usize),
#[error("Invalid table marker 0x{0:02x} at position {1}")]
InvalidTableMarker(u8, usize),
#[error("Invalid operation code 0x{0:02x} at position {1}")]
InvalidOpCode(u8, usize),
#[error("Invalid UTF-8 in table name at position {0}")]
InvalidTableName(usize),
#[error("Failed to decode value at position {0}")]
InvalidValue(usize),
#[error("Table name not null-terminated")]
UnterminatedTableName,
#[error("Mixed format markers: expected {expected:?}, found {found:?} at position {position}")]
MixedFormats {
expected: FormatMarker,
found: FormatMarker,
position: usize,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FormatMarker {
Changeset,
Patchset,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct TableSchema<S> {
name: S,
column_count: usize,
pk_flags: Vec<u8>,
}
impl<S> TableSchema<S> {
#[inline]
#[must_use]
pub fn new(name: S, column_count: usize, pk_flags: Vec<u8>) -> Self {
debug_assert_eq!(pk_flags.len(), column_count);
Self {
name,
column_count,
pk_flags,
}
}
#[inline]
#[must_use]
pub fn name(&self) -> &S {
&self.name
}
#[inline]
#[must_use]
pub fn pk_flags(&self) -> &[u8] {
&self.pk_flags
}
#[must_use]
pub(crate) fn pk_indices(&self) -> Vec<usize> {
let mut pk_cols: Vec<(usize, u8)> = self
.pk_flags
.iter()
.enumerate()
.filter_map(|(i, &pk_ordinal)| {
if pk_ordinal > 0 {
Some((i, pk_ordinal))
} else {
None
}
})
.collect();
pk_cols.sort_by_key(|(_, ordinal)| *ordinal);
pk_cols.into_iter().map(|(idx, _)| idx).collect()
}
}
impl<S: AsRef<str> + Clone + Eq + core::fmt::Debug> DynTable for TableSchema<S> {
#[inline]
fn name(&self) -> &str {
self.name.as_ref()
}
#[inline]
fn number_of_columns(&self) -> usize {
self.column_count
}
#[inline]
fn write_pk_flags(&self, buf: &mut [u8]) {
assert_eq!(buf.len(), self.column_count);
buf.copy_from_slice(&self.pk_flags);
}
}
impl<N: AsRef<str> + Clone + core::hash::Hash + Eq + core::fmt::Debug> SchemaWithPK
for TableSchema<N>
{
fn number_of_primary_keys(&self) -> usize {
self.pk_flags.iter().filter(|&&b| b > 0).count()
}
fn primary_key_index(&self, col_idx: usize) -> Option<usize> {
self.pk_flags.get(col_idx).and_then(|&pk_ordinal| {
if pk_ordinal > 0 {
Some(usize::from(pk_ordinal - 1))
} else {
None
}
})
}
fn extract_pk<S, B>(
&self,
values: &impl IndexableValues<Text = S, Binary = B>,
) -> alloc::vec::Vec<Value<S, B>>
where
S: Clone,
B: Clone,
{
self.pk_indices()
.into_iter()
.map(|i| {
values
.get(i)
.expect("primary key column index out of bounds, values shorter than schema")
})
.collect()
}
}
#[derive(Debug, Clone, Eq)]
pub enum ParsedDiffSet {
Changeset(DiffSet<ChangesetFormat, TableSchema<String>, String, Vec<u8>>),
Patchset(DiffSet<PatchsetFormat, TableSchema<String>, String, Vec<u8>>),
}
impl PartialEq for ParsedDiffSet {
fn eq(&self, other: &Self) -> bool {
let self_empty = match self {
ParsedDiffSet::Changeset(d) => d.is_empty(),
ParsedDiffSet::Patchset(d) => d.is_empty(),
};
let other_empty = match other {
ParsedDiffSet::Changeset(d) => d.is_empty(),
ParsedDiffSet::Patchset(d) => d.is_empty(),
};
if self_empty && other_empty {
return true;
}
match (self, other) {
(ParsedDiffSet::Changeset(a), ParsedDiffSet::Changeset(b)) => a == b,
(ParsedDiffSet::Patchset(a), ParsedDiffSet::Patchset(b)) => a == b,
_ => false,
}
}
}
impl TryFrom<&[u8]> for ParsedDiffSet {
type Error = ParseError;
fn try_from(data: &[u8]) -> Result<Self, Self::Error> {
Self::parse(data)
}
}
impl From<ParsedDiffSet> for Vec<u8> {
fn from(diffset: ParsedDiffSet) -> Self {
match diffset {
ParsedDiffSet::Changeset(d) => d.into(),
ParsedDiffSet::Patchset(d) => d.into(),
}
}
}
impl ParsedDiffSet {
pub fn parse(data: &[u8]) -> Result<Self, ParseError> {
if data.is_empty() {
return Ok(ParsedDiffSet::Changeset(DiffSet::default()));
}
match data[0] {
markers::CHANGESET => {
let diffset = parse_as_changeset(data)?;
Ok(ParsedDiffSet::Changeset(diffset))
}
markers::PATCHSET => {
let diffset = parse_as_patchset(data)?;
Ok(ParsedDiffSet::Patchset(diffset))
}
b => Err(ParseError::InvalidTableMarker(b, 0)),
}
}
#[must_use]
pub fn is_changeset(&self) -> bool {
matches!(self, ParsedDiffSet::Changeset(_))
}
#[must_use]
pub fn is_patchset(&self) -> bool {
matches!(self, ParsedDiffSet::Patchset(_))
}
#[must_use]
pub fn table_schemas(&self) -> Vec<&TableSchema<String>> {
match self {
ParsedDiffSet::Changeset(d) => d
.tables
.iter()
.filter(|(_, ops)| !ops.is_empty())
.map(|(schema, _)| schema)
.collect(),
ParsedDiffSet::Patchset(d) => d
.tables
.iter()
.filter(|(_, ops)| !ops.is_empty())
.map(|(schema, _)| schema)
.collect(),
}
}
}
fn parse_as_changeset(
data: &[u8],
) -> Result<DiffSet<ChangesetFormat, TableSchema<String>, String, Vec<u8>>, ParseError> {
let mut builder: DiffSetBuilder<ChangesetFormat, TableSchema<String>, String, Vec<u8>> =
DiffSetBuilder::new();
let mut pos = 0;
while pos < data.len() {
let (schema, format, header_len) = parse_table_header(&data[pos..], pos)?;
if format != FormatMarker::Changeset {
return Err(ParseError::MixedFormats {
expected: FormatMarker::Changeset,
found: format,
position: pos,
});
}
pos += header_len;
while pos < data.len() {
let byte = data[pos];
if byte == markers::CHANGESET || byte == markers::PATCHSET {
break;
}
let op_len = parse_changeset_operation(&data[pos..], pos, &schema, &mut builder)?;
pos += op_len;
}
}
Ok(builder.into())
}
fn parse_as_patchset(
data: &[u8],
) -> Result<DiffSet<PatchsetFormat, TableSchema<String>, String, Vec<u8>>, ParseError> {
let mut builder: DiffSetBuilder<PatchsetFormat, TableSchema<String>, String, Vec<u8>> =
DiffSetBuilder::new();
let mut pos = 0;
while pos < data.len() {
let (schema, format, header_len) = parse_table_header(&data[pos..], pos)?;
if format != FormatMarker::Patchset {
return Err(ParseError::MixedFormats {
expected: FormatMarker::Patchset,
found: format,
position: pos,
});
}
pos += header_len;
while pos < data.len() {
let byte = data[pos];
if byte == markers::CHANGESET || byte == markers::PATCHSET {
break;
}
let op_len = parse_patchset_operation(&data[pos..], pos, &schema, &mut builder)?;
pos += op_len;
}
}
Ok(builder.into())
}
fn parse_table_header(
data: &[u8],
base_pos: usize,
) -> Result<(TableSchema<String>, FormatMarker, usize), ParseError> {
let mut pos = 0;
if data.is_empty() {
return Err(ParseError::UnexpectedEof(base_pos));
}
let format = match data[pos] {
markers::CHANGESET => FormatMarker::Changeset,
markers::PATCHSET => FormatMarker::Patchset,
b => return Err(ParseError::InvalidTableMarker(b, base_pos + pos)),
};
pos += 1;
if pos >= data.len() {
return Err(ParseError::UnexpectedEof(base_pos + pos));
}
let column_count = data[pos] as usize;
pos += 1;
if pos + column_count > data.len() {
return Err(ParseError::UnexpectedEof(base_pos + pos));
}
let pk_flags: Vec<u8> = data[pos..pos + column_count].to_vec();
pos += column_count;
let name_start = pos;
while pos < data.len() && data[pos] != 0 {
pos += 1;
}
if pos >= data.len() {
return Err(ParseError::UnterminatedTableName);
}
let name = String::from_utf8(data[name_start..pos].to_vec())
.map_err(|_| ParseError::InvalidTableName(base_pos + name_start))?;
pos += 1;
Ok((TableSchema::new(name, column_count, pk_flags), format, pos))
}
fn parse_operation_header(data: &[u8], base_pos: usize) -> Result<(u8, bool, usize), ParseError> {
if data.len() < 2 {
return Err(ParseError::UnexpectedEof(base_pos));
}
Ok((data[0], data[1] != 0, 2))
}
fn parse_changeset_operation(
data: &[u8],
base_pos: usize,
schema: &TableSchema<String>,
builder: &mut DiffSetBuilder<ChangesetFormat, TableSchema<String>, String, Vec<u8>>,
) -> Result<usize, ParseError> {
let (op_code, indirect, mut pos) = parse_operation_header(data, base_pos)?;
match op_code {
op_codes::INSERT => {
let (values, len) = parse_values(&data[pos..], base_pos + pos, schema.column_count)?;
pos += len;
let values: Vec<Value<String, Vec<u8>>> = values
.into_iter()
.map(|v| v.unwrap_or(Value::Null))
.collect();
let pk = schema.extract_pk(&values);
builder.add_operation(schema, pk, Operation::Insert { values, indirect });
}
op_codes::DELETE => {
let (values, len) = parse_values(&data[pos..], base_pos + pos, schema.column_count)?;
pos += len;
let values: Vec<Value<String, Vec<u8>>> = values
.into_iter()
.map(|v| v.unwrap_or(Value::Null))
.collect();
let pk = schema.extract_pk(&values);
builder.add_operation(
schema,
pk,
Operation::Delete {
data: values,
indirect,
},
);
}
op_codes::UPDATE => {
let (old_values, old_len) =
parse_values(&data[pos..], base_pos + pos, schema.column_count)?;
pos += old_len;
let (new_values, new_len) =
parse_values(&data[pos..], base_pos + pos, schema.column_count)?;
pos += new_len;
let pk_values: Vec<Value<String, Vec<u8>>> = old_values
.iter()
.map(|v| v.clone().unwrap_or(Value::Null))
.collect();
let pk = schema.extract_pk(&pk_values);
let values: UpdateValues = old_values.into_iter().zip(new_values).collect();
builder.add_operation(schema, pk, Operation::Update { values, indirect });
}
_ => return Err(ParseError::InvalidOpCode(op_code, base_pos)),
}
Ok(pos)
}
fn parse_patchset_operation(
data: &[u8],
base_pos: usize,
schema: &TableSchema<String>,
builder: &mut DiffSetBuilder<PatchsetFormat, TableSchema<String>, String, Vec<u8>>,
) -> Result<usize, ParseError> {
let (op_code, indirect, mut pos) = parse_operation_header(data, base_pos)?;
match op_code {
op_codes::INSERT => {
let (values, len) = parse_values(&data[pos..], base_pos + pos, schema.column_count)?;
pos += len;
let values: Vec<Value<String, Vec<u8>>> = values
.into_iter()
.map(|v| v.unwrap_or(Value::Null))
.collect();
let pk = schema.extract_pk(&values);
builder.add_operation(schema, pk, Operation::Insert { values, indirect });
}
op_codes::DELETE => {
let pk_count = schema.pk_flags.iter().filter(|&&b| b > 0).count();
let (pk_values, len) = parse_values(&data[pos..], base_pos + pos, pk_count)?;
pos += len;
let full_values = expand_pk_values(&schema.pk_flags, pk_values, schema.column_count);
let full_values_concrete: Vec<Value<String, Vec<u8>>> = full_values
.into_iter()
.map(|v| v.unwrap_or(Value::Null))
.collect();
let pk = schema.extract_pk(&full_values_concrete);
builder.add_operation(schema, pk, Operation::Delete { data: (), indirect });
}
op_codes::UPDATE => {
let pk_count = schema.pk_flags.iter().filter(|&&b| b > 0).count();
let non_pk_count = schema.column_count.saturating_sub(pk_count);
let (old_pk_values, old_len) = parse_values(&data[pos..], base_pos + pos, pk_count)?;
pos += old_len;
let (new_non_pk_values, new_len) =
parse_values(&data[pos..], base_pos + pos, non_pk_count)?;
pos += new_len;
let mut values: Vec<((), MaybeValue<String, Vec<u8>>)> =
alloc::vec![((), None); schema.column_count];
let mut old_iter = old_pk_values.into_iter();
let mut new_iter = new_non_pk_values.into_iter();
for (col_idx, &pk_flag) in schema.pk_flags.iter().enumerate() {
if pk_flag > 0 {
let old = old_iter.next().flatten().unwrap_or(Value::Null);
values[col_idx] = ((), Some(old));
} else {
values[col_idx] = ((), new_iter.next().flatten());
}
}
let pk = schema.extract_pk(&values);
builder.add_operation(schema, pk, Operation::Update { values, indirect });
}
_ => return Err(ParseError::InvalidOpCode(op_code, base_pos)),
}
Ok(pos)
}
fn expand_pk_values(
pk_flags: &[u8],
pk_values: Vec<MaybeValue<String, Vec<u8>>>,
column_count: usize,
) -> Vec<MaybeValue<String, Vec<u8>>> {
let mut full: Vec<MaybeValue<String, Vec<u8>>> = vec![None; column_count];
let mut pk_iter = pk_values.into_iter();
for (i, &pk_ordinal) in pk_flags.iter().enumerate() {
if pk_ordinal > 0
&& let Some(v) = pk_iter.next()
{
full[i] = v;
}
}
full
}
fn parse_values(data: &[u8], base_pos: usize, count: usize) -> Result<ParsedValues, ParseError> {
let mut values = Vec::with_capacity(count);
let mut pos = 0;
for _ in 0..count {
let (value, value_len) =
decode_value(&data[pos..]).ok_or(ParseError::InvalidValue(base_pos + pos))?;
values.push(value);
pos += value_len;
}
Ok((values, pos))
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
#[test]
fn test_parse_empty() {
let result = ParsedDiffSet::parse(&[]);
assert!(result.is_ok());
assert!(result.unwrap().is_changeset());
}
#[test]
fn test_parse_table_header() {
let data = [b'T', 2, 1, 0, b't', 0];
let (schema, format, len) = parse_table_header(&data, 0).unwrap();
assert_eq!(format, FormatMarker::Changeset);
assert_eq!(schema.column_count, 2);
assert_eq!(schema.pk_flags, vec![1, 0]); assert_eq!(schema.name, "t");
assert_eq!(len, 6);
}
#[test]
fn test_parse_insert_changeset() {
let mut data = vec![b'T', 2, 1, 0, b't', 0];
data.push(op_codes::INSERT);
data.push(0);
data.push(0x01);
data.extend(&1i64.to_be_bytes());
data.push(0x03);
data.push(1);
data.push(b'a');
let parsed = ParsedDiffSet::parse(&data).unwrap();
assert!(parsed.is_changeset());
}
#[test]
fn test_parse_delete_changeset() {
let mut data = vec![b'T', 2, 1, 0, b't', 0];
data.push(op_codes::DELETE);
data.push(0);
data.push(0x01);
data.extend(&1i64.to_be_bytes());
data.push(0x03);
data.push(1);
data.push(b'a');
let parsed = ParsedDiffSet::parse(&data).unwrap();
assert!(parsed.is_changeset());
}
#[test]
fn test_parse_delete_patchset() {
let mut data = vec![b'P', 2, 1, 0, b't', 0];
data.push(op_codes::DELETE);
data.push(0);
data.push(0x01);
data.extend(&1i64.to_be_bytes());
let parsed = ParsedDiffSet::parse(&data).unwrap();
assert!(parsed.is_patchset());
}
#[test]
fn test_parse_update_changeset() {
let mut data = vec![b'T', 2, 1, 0, b't', 0];
data.push(op_codes::UPDATE);
data.push(0);
data.push(0x01);
data.extend(&1i64.to_be_bytes());
data.push(0x03);
data.push(1);
data.push(b'a');
data.push(0x01);
data.extend(&1i64.to_be_bytes());
data.push(0x03);
data.push(1);
data.push(b'b');
let parsed = ParsedDiffSet::parse(&data).unwrap();
assert!(parsed.is_changeset());
}
#[test]
fn test_is_changeset() {
let data = vec![b'T', 1, 1, b't', 0];
let parsed = ParsedDiffSet::parse(&data).unwrap();
assert!(parsed.is_changeset());
assert!(!parsed.is_patchset());
}
#[test]
fn test_is_patchset() {
let data = vec![b'P', 1, 1, b't', 0];
let parsed = ParsedDiffSet::parse(&data).unwrap();
assert!(parsed.is_patchset());
assert!(!parsed.is_changeset());
}
#[test]
fn test_parsed_table_schema_dyn_table() {
let schema: TableSchema<String> = TableSchema::new("users".into(), 3, vec![1, 0, 0]);
assert_eq!(schema.name(), "users");
assert_eq!(schema.number_of_columns(), 3);
let mut buf = [0u8; 3];
schema.write_pk_flags(&mut buf);
assert_eq!(buf, [1, 0, 0]);
}
#[test]
fn test_parsed_table_schema_extract_pk() {
let schema: TableSchema<String> = TableSchema::new("users".into(), 3, vec![1, 0, 2]);
let values: Vec<Value<String, Vec<u8>>> = vec![
Value::Integer(1),
Value::Text("alice".into()),
Value::Integer(100),
];
let pk = schema.extract_pk(&values);
let expected: Vec<Value<String, Vec<u8>>> = vec![Value::Integer(1), Value::Integer(100)];
assert_eq!(pk, expected);
}
#[test]
fn test_parse_invalid_table_marker() {
let data = [0xFFu8, 1, 1, b't', 0];
let err = ParsedDiffSet::parse(&data).unwrap_err();
assert!(
matches!(err, ParseError::InvalidTableMarker(0xFF, 0)),
"got {err:?}"
);
}
#[test]
fn test_parse_unexpected_eof_in_table_header() {
let data = *b"T";
let err = ParsedDiffSet::parse(&data).unwrap_err();
assert!(matches!(err, ParseError::UnexpectedEof(_)), "got {err:?}");
}
#[test]
fn test_parse_unexpected_eof_in_pk_flags() {
let data = [b'T', 3, 1];
let err = ParsedDiffSet::parse(&data).unwrap_err();
assert!(matches!(err, ParseError::UnexpectedEof(_)), "got {err:?}");
}
#[test]
fn test_parse_unterminated_table_name() {
let data = [b'T', 1, 1, b'a', b'b', b'c'];
let err = ParsedDiffSet::parse(&data).unwrap_err();
assert!(
matches!(err, ParseError::UnterminatedTableName),
"got {err:?}"
);
}
#[test]
fn test_parse_invalid_utf8_in_table_name() {
let data = [b'T', 1, 1, 0xFF, 0];
let err = ParsedDiffSet::parse(&data).unwrap_err();
assert!(
matches!(err, ParseError::InvalidTableName(_)),
"got {err:?}"
);
}
#[test]
fn test_parse_mixed_formats_changeset_then_patchset() {
let mut data = vec![b'T', 1, 1, b'a', 0];
data.extend_from_slice(&[b'P', 1, 1, b'b', 0]);
let err = ParsedDiffSet::parse(&data).unwrap_err();
assert!(
matches!(
err,
ParseError::MixedFormats {
expected: FormatMarker::Changeset,
found: FormatMarker::Patchset,
..
}
),
"got {err:?}"
);
}
#[test]
fn test_parse_mixed_formats_patchset_then_changeset() {
let mut data = vec![b'P', 1, 1, b'a', 0];
data.extend_from_slice(&[b'T', 1, 1, b'b', 0]);
let err = ParsedDiffSet::parse(&data).unwrap_err();
assert!(
matches!(
err,
ParseError::MixedFormats {
expected: FormatMarker::Patchset,
found: FormatMarker::Changeset,
..
}
),
"got {err:?}"
);
}
fn make_insert_with_indirect(indirect_byte: u8) -> Vec<u8> {
let mut data = vec![b'T', 1, 1, b't', 0];
data.push(op_codes::INSERT);
data.push(indirect_byte);
data.push(0x01);
data.extend(&1i64.to_be_bytes());
data
}
fn first_op_indirect_changeset(data: &[u8]) -> bool {
let parsed = ParsedDiffSet::parse(data).unwrap();
let ParsedDiffSet::Changeset(set) = parsed else {
panic!("expected Changeset");
};
set.tables
.iter()
.find_map(|(_schema, rows)| rows.first().map(|(_, op)| op.indirect()))
.expect("expected at least one op")
}
#[test]
fn test_parse_changeset_indirect_flag_set() {
let data = make_insert_with_indirect(1);
assert!(first_op_indirect_changeset(&data));
}
#[test]
fn test_parse_changeset_indirect_flag_clear() {
let data = make_insert_with_indirect(0);
assert!(!first_op_indirect_changeset(&data));
}
#[test]
fn test_parse_indirect_nonzero_treated_as_true() {
let data = make_insert_with_indirect(0x42);
assert!(first_op_indirect_changeset(&data));
}
#[test]
fn test_parsed_diffset_variant_mismatch_partial_eq() {
let changeset = ParsedDiffSet::parse(&[b'T', 1, 1, b't', 0]).unwrap();
let patchset = ParsedDiffSet::parse(&[b'P', 1, 1, b't', 0]).unwrap();
let mut full_changeset = vec![b'T', 1, 1, b't', 0];
full_changeset.push(op_codes::INSERT);
full_changeset.push(0);
full_changeset.push(0x01);
full_changeset.extend(&1i64.to_be_bytes());
let cs = ParsedDiffSet::parse(&full_changeset).unwrap();
let mut full_patchset = vec![b'P', 1, 1, b't', 0];
full_patchset.push(op_codes::INSERT);
full_patchset.push(0);
full_patchset.push(0x01);
full_patchset.extend(&1i64.to_be_bytes());
let ps = ParsedDiffSet::parse(&full_patchset).unwrap();
assert_ne!(cs, ps);
assert_eq!(changeset, patchset);
}
#[test]
fn test_parse_unexpected_eof_in_operation_header() {
let data = [b'T', 1, 1, b't', 0, op_codes::INSERT];
let err = ParsedDiffSet::parse(&data).unwrap_err();
assert!(matches!(err, ParseError::UnexpectedEof(_)), "got {err:?}");
}
#[test]
fn test_parse_patchset_indirect_flag_set() {
let mut data = vec![b'P', 1, 1, b't', 0];
data.push(op_codes::INSERT);
data.push(1);
data.push(0x01);
data.extend(&1i64.to_be_bytes());
let parsed = ParsedDiffSet::parse(&data).unwrap();
let ParsedDiffSet::Patchset(set) = parsed else {
panic!("expected Patchset");
};
let indirect = set
.tables
.iter()
.find_map(|(_schema, rows)| rows.first().map(|(_, op)| op.indirect()))
.expect("expected at least one op");
assert!(indirect);
}
fn assert_patchset_update_roundtrip(
data: &[u8],
check: impl FnOnce(
&TableSchema<String>,
&[Value<String, Vec<u8>>],
&[((), MaybeValue<String, Vec<u8>>)],
bool,
),
) {
let parsed = ParsedDiffSet::parse(data).expect("SQLite patchset UPDATE must parse");
let ParsedDiffSet::Patchset(set) = parsed else {
panic!("expected Patchset, got {parsed:?}");
};
let (schema, rows) = set.tables.first().expect("expected one table");
assert_eq!(rows.len(), 1, "expected exactly one row");
let (pk, op) = rows.first().expect("row map non-empty");
let Operation::Update { values, indirect } = op else {
panic!("expected Update, got {op:?}");
};
check(schema, pk.as_slice(), values.as_slice(), *indirect);
let serialized: Vec<u8> = set.into();
assert_eq!(serialized, data, "roundtrip must match SQLite output");
}
#[test]
fn test_parse_patchset_update_sqlite_wire_layout_single_pk() {
let data: [u8; 33] = [
0x50, 0x03, 0x01, 0x00, 0x00, b'o', b'r', b'd', b'e', b'r', b's', 0x00, 0x17, 0x00,
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x07, b's', b'h',
b'i', b'p', b'p', b'e', b'd',
];
assert_patchset_update_roundtrip(&data, |schema, pk, values, indirect| {
assert_eq!(schema.name, "orders");
assert_eq!(schema.column_count, 3);
assert_eq!(schema.pk_flags, vec![1, 0, 0]);
assert_eq!(pk, &[Value::Integer(5)]);
assert!(!indirect);
assert_eq!(values.len(), 3);
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())));
});
}
#[test]
fn test_parse_patchset_update_sqlite_wire_layout_composite_pk() {
let data: [u8; 35] = [
0x50, 0x03, 0x01, 0x02, 0x00, b'i', b't', b'e', b'm', b's', 0x00, 0x17, 0x00, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x02, 0x03, 0x02, b'v', b'2',
];
assert_patchset_update_roundtrip(&data, |schema, pk, values, _indirect| {
assert_eq!(schema.name, "items");
assert_eq!(schema.pk_flags, vec![1, 2, 0]);
assert_eq!(pk, &[Value::Integer(1), Value::Integer(2)]);
assert_eq!(values.len(), 3);
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())));
});
}
#[test]
fn test_parse_patchset_update_all_non_pk_changed() {
let data: [u8; 41] = [
0x50, 0x03, 0x01, 0x00, 0x00, b'o', b'r', b'd', b'e', b'r', b's', 0x00, 0x17, 0x00,
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0xc8, 0x03, 0x07, b's', b'h', b'i', b'p', b'p', b'e', b'd',
];
assert_patchset_update_roundtrip(&data, |_schema, _pk, values, _indirect| {
assert_eq!(values[0].1, Some(Value::Integer(5)));
assert_eq!(values[1].1, Some(Value::Integer(200)));
assert_eq!(values[2].1, Some(Value::Text("shipped".into())));
});
}
}