Skip to main content

sqlite_diff_rs/
parser.rs

1//! Parser for `SQLite` changeset/patchset binary format.
2//!
3//! Parses `SQLite` session extension changesets and patchsets from binary into
4//! [`DiffSetBuilder`] instances.
5//!
6//! # Binary Format
7//!
8//! The format consists of one or more table sections:
9//!
10//! ```text
11//! Table Header:
12//! ├── Marker: 'T' (0x54) for changeset, 'P' (0x50) for patchset
13//! ├── Column count (1 byte)
14//! ├── PK flags (1 byte per column: 0x01 = PK, 0x00 = not)
15//! └── Table name (null-terminated UTF-8)
16//!
17//! Change Records (repeated):
18//! ├── Operation code: INSERT=0x12, DELETE=0x09, UPDATE=0x17
19//! ├── Indirect flag (1 byte, usually 0)
20//! └── Values (encoded per operation type)
21//! ```
22//!
23
24use alloc::string::String;
25use alloc::vec;
26use alloc::vec::Vec;
27use core::hash::Hash;
28
29use crate::IndexableValues;
30
31/// Type alias for update operation values.
32type UpdateValues = Vec<(MaybeValue<String, Vec<u8>>, MaybeValue<String, Vec<u8>>)>;
33
34/// Type alias for parsed values result.
35type 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/// Errors that can occur during parsing.
41#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
42pub enum ParseError {
43    /// Unexpected end of input.
44    #[error("Unexpected end of input at position {0}")]
45    UnexpectedEof(usize),
46
47    /// Invalid table marker (expected 'T' or 'P').
48    #[error("Invalid table marker 0x{0:02x} at position {1}")]
49    InvalidTableMarker(u8, usize),
50
51    /// Invalid operation code.
52    #[error("Invalid operation code 0x{0:02x} at position {1}")]
53    InvalidOpCode(u8, usize),
54
55    /// Invalid UTF-8 in table name.
56    #[error("Invalid UTF-8 in table name at position {0}")]
57    InvalidTableName(usize),
58
59    /// Failed to decode a value.
60    #[error("Failed to decode value at position {0}")]
61    InvalidValue(usize),
62
63    /// Table name not null-terminated.
64    #[error("Table name not null-terminated")]
65    UnterminatedTableName,
66
67    /// Mixed format markers in the same file.
68    #[error("Mixed format markers: expected {expected:?}, found {found:?} at position {position}")]
69    MixedFormats {
70        /// The expected format marker.
71        expected: FormatMarker,
72        /// The found format marker.
73        found: FormatMarker,
74        /// The position where the mismatch occurred.
75        position: usize,
76    },
77}
78
79/// The detected format marker.
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub enum FormatMarker {
82    /// Changeset format ('T' marker).
83    Changeset,
84    /// Patchset format ('P' marker).
85    Patchset,
86}
87
88/// A table schema parsed from binary changeset/patchset data.
89///
90/// This type implements [`DynTable`] and [`SchemaWithPK`], allowing it
91/// to be used with [`DiffSetBuilder`].
92#[derive(Debug, Clone, PartialEq, Eq, Hash)]
93pub struct TableSchema<S> {
94    /// The table name.
95    name: S,
96    /// Number of columns.
97    column_count: usize,
98    /// Primary key flags - raw bytes from the changeset/patchset.
99    ///
100    /// Each byte represents the 1-based ordinal position in the composite PK,
101    /// or 0 if the column is not part of the primary key.
102    /// For example, `[1, 0, 2]` means column 0 is the first PK column,
103    /// column 1 is not a PK column, and column 2 is the second PK column.
104    pk_flags: Vec<u8>,
105}
106
107impl<S> TableSchema<S> {
108    /// Create a new parsed table schema.
109    #[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    /// Returns the name of the table.
121    #[inline]
122    #[must_use]
123    pub fn name(&self) -> &S {
124        &self.name
125    }
126
127    /// Returns the raw primary-key flags. Each byte at index `i`
128    /// represents column `i`: `0` means the column is not part of the
129    /// primary key, and a non-zero value `k` means it is the `k`-th
130    /// column in the composite primary key.
131    #[inline]
132    #[must_use]
133    pub fn pk_flags(&self) -> &[u8] {
134        &self.pk_flags
135    }
136
137    /// Get the indices of primary key columns, in PK order.
138    #[must_use]
139    pub(crate) fn pk_indices(&self) -> Vec<usize> {
140        // Collect (col_idx, pk_ordinal) pairs for non-zero entries
141        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        // Sort by pk_ordinal to get correct PK order
154        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/// A parsed changeset or patchset.
214///
215/// This represents a frozen (immutable) diffset produced by the binary parser.
216/// To modify it, convert it to a [`DiffSetBuilder`] using `Into::into`.
217#[derive(Debug, Clone, Eq)]
218pub enum ParsedDiffSet {
219    /// A parsed changeset.
220    Changeset(DiffSet<ChangesetFormat, TableSchema<String>, String, Vec<u8>>),
221    /// A parsed patchset.
222    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        // Otherwise compare by variant and content
241        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    /// Parse binary data into a frozen [`DiffSet`].
268    ///
269    /// The format (changeset vs patchset) is determined by the first table marker.
270    ///
271    /// # Errors
272    ///
273    /// Returns a `ParseError` if the data is malformed or contains invalid values.
274    pub fn parse(data: &[u8]) -> Result<Self, ParseError> {
275        if data.is_empty() {
276            // Empty data defaults to changeset
277            return Ok(ParsedDiffSet::Changeset(DiffSet::default()));
278        }
279
280        // Peek at the first byte to determine format
281        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    /// Returns true if this is a changeset.
295    #[must_use]
296    pub fn is_changeset(&self) -> bool {
297        matches!(self, ParsedDiffSet::Changeset(_))
298    }
299
300    /// Returns true if this is a patchset.
301    #[must_use]
302    pub fn is_patchset(&self) -> bool {
303        matches!(self, ParsedDiffSet::Patchset(_))
304    }
305
306    /// Returns the table schemas for all tables with non-empty operations.
307    #[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
326/// Parse binary data as a changeset.
327///
328/// # Errors
329///
330/// Returns a `ParseError` if the data is malformed or not a valid changeset.
331fn 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
362/// Parse binary data as a patchset.
363///
364/// # Errors
365///
366/// Returns a `ParseError` if the data is malformed or not a valid patchset.
367fn 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
398/// Parse a table header and return the schema.
399fn 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
441/// Parse operation header (`op_code` + indirect flag).
442///
443/// Returns `(op_code, indirect, bytes_consumed)`. Any non-zero indirect byte
444/// parses as `true` to match SQLite's permissive treatment of the flag.
445fn 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
452/// Parse a changeset operation.
453fn 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            // Extract PK using old values (convert None to Null)
497            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
511/// Parse a patchset operation.
512fn 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            // Patchset DELETE: only PK values in column order
533            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            // Expand PK values to full row, then extract_pk to get ordinal-sorted PK.
537            // This is needed because the binary format stores PKs in column order,
538            // but the builder stores them sorted by pk_ordinal (matching the serializer).
539            let full_values = expand_pk_values(&schema.pk_flags, pk_values, schema.column_count);
540            // Convert MaybeValue to Value for extract_pk (PK values should always be defined)
541            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            // Patchset UPDATE wire layout (matching SQLite's session extension):
550            //   old side: PK-only values, in column order, exactly `pk_count` entries
551            //            (no padding / undefined markers for non-PK columns).
552            //   new side: non-PK columns, in column order, exactly
553            //            `column_count - pk_count` entries; each entry is either the
554            //            new column value or `0x00` (undefined) if that non-PK column
555            //            did not change.
556            //
557            // Internally we rehydrate the operation into a full-width
558            // `Vec<((), MaybeValue)>` of length `column_count` so downstream code
559            // (`extract_pk`, `sql_output`, consolidation, reversal) keeps working
560            // uniformly: PK slots hold `Some(pk_value)`, non-PK slots hold either
561            // `Some(new_value)` or `None` for undefined.
562            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                    // PK columns always carry a defined value on the old side; a
578                    // stray undefined marker is normalized to Null to stay lenient
579                    // for fuzz-generated inputs (matches `expand_pk_values` in the
580                    // DELETE path).
581                    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
597/// Expand PK-only values to full row with None (undefined) for non-PK columns.
598///
599/// The `pk_flags` are raw bytes where non-zero means the column is part of the PK.
600/// PK values are expected in the order they appear in `pk_flags` (not sorted by ordinal).
601fn 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
618/// Parse a sequence of values.
619fn 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 crate::SimpleTable;
637    use alloc::vec;
638
639    #[test]
640    fn test_parse_empty() {
641        let result = ParsedDiffSet::parse(&[]);
642        assert!(result.is_ok());
643        assert!(result.unwrap().is_changeset());
644    }
645
646    #[test]
647    fn test_parse_table_header() {
648        // 'T', 2 columns, pk_flags [1, 0], table name "t\0"
649        let data = [b'T', 2, 1, 0, b't', 0];
650        let (schema, format, len) = parse_table_header(&data, 0).unwrap();
651
652        assert_eq!(format, FormatMarker::Changeset);
653        assert_eq!(schema.column_count, 2);
654        assert_eq!(schema.pk_flags, vec![1, 0]); // Raw bytes: 1 = first PK column, 0 = not PK
655        assert_eq!(schema.name, "t");
656        assert_eq!(len, 6);
657    }
658
659    #[test]
660    fn test_parse_insert_changeset() {
661        // Table header + INSERT with integer 1 and text "a"
662        let mut data = vec![b'T', 2, 1, 0, b't', 0];
663        // INSERT opcode, indirect=0
664        data.push(op_codes::INSERT);
665        data.push(0);
666        // Integer 1 (type 1, 8 bytes)
667        data.push(0x01);
668        data.extend(&1i64.to_be_bytes());
669        // Text "a" (type 3, length 1, "a")
670        data.push(0x03);
671        data.push(1);
672        data.push(b'a');
673
674        let parsed = ParsedDiffSet::parse(&data).unwrap();
675        assert!(parsed.is_changeset());
676    }
677
678    #[test]
679    fn test_parse_delete_changeset() {
680        let mut data = vec![b'T', 2, 1, 0, b't', 0];
681        data.push(op_codes::DELETE);
682        data.push(0);
683        // Integer 1
684        data.push(0x01);
685        data.extend(&1i64.to_be_bytes());
686        // Text "a"
687        data.push(0x03);
688        data.push(1);
689        data.push(b'a');
690
691        let parsed = ParsedDiffSet::parse(&data).unwrap();
692        assert!(parsed.is_changeset());
693    }
694
695    #[test]
696    fn test_parse_delete_patchset() {
697        // Patchset DELETE only has PK values
698        let mut data = vec![b'P', 2, 1, 0, b't', 0];
699        data.push(op_codes::DELETE);
700        data.push(0);
701        // Only PK value (integer 1)
702        data.push(0x01);
703        data.extend(&1i64.to_be_bytes());
704
705        let parsed = ParsedDiffSet::parse(&data).unwrap();
706        assert!(parsed.is_patchset());
707    }
708
709    #[test]
710    fn test_parse_update_changeset() {
711        let mut data = vec![b'T', 2, 1, 0, b't', 0];
712        data.push(op_codes::UPDATE);
713        data.push(0);
714        // Old values: integer 1, text "a"
715        data.push(0x01);
716        data.extend(&1i64.to_be_bytes());
717        data.push(0x03);
718        data.push(1);
719        data.push(b'a');
720        // New values: integer 1, text "b"
721        data.push(0x01);
722        data.extend(&1i64.to_be_bytes());
723        data.push(0x03);
724        data.push(1);
725        data.push(b'b');
726
727        let parsed = ParsedDiffSet::parse(&data).unwrap();
728        assert!(parsed.is_changeset());
729    }
730
731    #[test]
732    fn test_is_changeset() {
733        let data = vec![b'T', 1, 1, b't', 0];
734        let parsed = ParsedDiffSet::parse(&data).unwrap();
735        assert!(parsed.is_changeset());
736        assert!(!parsed.is_patchset());
737    }
738
739    #[test]
740    fn test_is_patchset() {
741        let data = vec![b'P', 1, 1, b't', 0];
742        let parsed = ParsedDiffSet::parse(&data).unwrap();
743        assert!(parsed.is_patchset());
744        assert!(!parsed.is_changeset());
745    }
746
747    #[test]
748    fn test_parsed_table_schema_dyn_table() {
749        let schema: TableSchema<String> = TableSchema::new("users".into(), 3, vec![1, 0, 0]);
750        assert_eq!(schema.name(), "users");
751        assert_eq!(schema.number_of_columns(), 3);
752
753        let mut buf = [0u8; 3];
754        schema.write_pk_flags(&mut buf);
755        assert_eq!(buf, [1, 0, 0]);
756    }
757
758    #[test]
759    fn test_parsed_table_schema_extract_pk() {
760        let schema: TableSchema<String> = TableSchema::new("users".into(), 3, vec![1, 0, 2]);
761        let values: Vec<Value<String, Vec<u8>>> = vec![
762            Value::Integer(1),
763            Value::Text("alice".into()),
764            Value::Integer(100),
765        ];
766        let pk = schema.extract_pk(&values);
767        let expected: Vec<Value<String, Vec<u8>>> = vec![Value::Integer(1), Value::Integer(100)];
768        assert_eq!(pk, expected);
769    }
770
771    // ---- Error path tests ----
772
773    #[test]
774    fn test_parse_invalid_table_marker() {
775        let data = [0xFFu8, 1, 1, b't', 0];
776        let err = ParsedDiffSet::parse(&data).unwrap_err();
777        assert!(
778            matches!(err, ParseError::InvalidTableMarker(0xFF, 0)),
779            "got {err:?}"
780        );
781    }
782
783    #[test]
784    fn test_parse_unexpected_eof_in_table_header() {
785        // 'T' marker but no column count
786        let data = *b"T";
787        let err = ParsedDiffSet::parse(&data).unwrap_err();
788        assert!(matches!(err, ParseError::UnexpectedEof(_)), "got {err:?}");
789    }
790
791    #[test]
792    fn test_parse_unexpected_eof_in_pk_flags() {
793        // 'T', column count 3, but only 1 PK flag byte
794        let data = [b'T', 3, 1];
795        let err = ParsedDiffSet::parse(&data).unwrap_err();
796        assert!(matches!(err, ParseError::UnexpectedEof(_)), "got {err:?}");
797    }
798
799    #[test]
800    fn test_parse_unterminated_table_name() {
801        // 'T', 1 column, pk_flags [1], then "abc" with no null terminator
802        let data = [b'T', 1, 1, b'a', b'b', b'c'];
803        let err = ParsedDiffSet::parse(&data).unwrap_err();
804        assert!(
805            matches!(err, ParseError::UnterminatedTableName),
806            "got {err:?}"
807        );
808    }
809
810    #[test]
811    fn test_parse_invalid_utf8_in_table_name() {
812        // 'T', 1 column, pk_flags [1], then 0xFF (invalid UTF-8), then null
813        let data = [b'T', 1, 1, 0xFF, 0];
814        let err = ParsedDiffSet::parse(&data).unwrap_err();
815        assert!(
816            matches!(err, ParseError::InvalidTableName(_)),
817            "got {err:?}"
818        );
819    }
820
821    #[test]
822    fn test_parse_mixed_formats_changeset_then_patchset() {
823        // First table 'T' (changeset), then second table 'P' (patchset)
824        let mut data = vec![b'T', 1, 1, b'a', 0];
825        // Now a 'P' table header without preceding operations
826        data.extend_from_slice(&[b'P', 1, 1, b'b', 0]);
827        let err = ParsedDiffSet::parse(&data).unwrap_err();
828        assert!(
829            matches!(
830                err,
831                ParseError::MixedFormats {
832                    expected: FormatMarker::Changeset,
833                    found: FormatMarker::Patchset,
834                    ..
835                }
836            ),
837            "got {err:?}"
838        );
839    }
840
841    #[test]
842    fn test_parse_mixed_formats_patchset_then_changeset() {
843        let mut data = vec![b'P', 1, 1, b'a', 0];
844        data.extend_from_slice(&[b'T', 1, 1, b'b', 0]);
845        let err = ParsedDiffSet::parse(&data).unwrap_err();
846        assert!(
847            matches!(
848                err,
849                ParseError::MixedFormats {
850                    expected: FormatMarker::Patchset,
851                    found: FormatMarker::Changeset,
852                    ..
853                }
854            ),
855            "got {err:?}"
856        );
857    }
858
859    /// Build the operation header bytes followed by a single integer payload.
860    fn make_insert_with_indirect(indirect_byte: u8) -> Vec<u8> {
861        let mut data = vec![b'T', 1, 1, b't', 0];
862        data.push(op_codes::INSERT);
863        data.push(indirect_byte);
864        // Integer 1
865        data.push(0x01);
866        data.extend(&1i64.to_be_bytes());
867        data
868    }
869
870    fn first_op_indirect_changeset(data: &[u8]) -> bool {
871        let parsed = ParsedDiffSet::parse(data).unwrap();
872        let ParsedDiffSet::Changeset(set) = parsed else {
873            panic!("expected Changeset");
874        };
875        set.tables
876            .iter()
877            .find_map(|(_schema, rows)| rows.first().map(|(_, op)| op.indirect()))
878            .expect("expected at least one op")
879    }
880
881    #[test]
882    fn test_parse_changeset_indirect_flag_set() {
883        let data = make_insert_with_indirect(1);
884        assert!(first_op_indirect_changeset(&data));
885    }
886
887    #[test]
888    fn test_parse_changeset_indirect_flag_clear() {
889        let data = make_insert_with_indirect(0);
890        assert!(!first_op_indirect_changeset(&data));
891    }
892
893    #[test]
894    fn test_parse_indirect_nonzero_treated_as_true() {
895        // Any non-zero byte must parse as indirect = true.
896        let data = make_insert_with_indirect(0x42);
897        assert!(first_op_indirect_changeset(&data));
898    }
899
900    #[test]
901    fn test_parsed_diffset_variant_mismatch_partial_eq() {
902        let changeset = ParsedDiffSet::parse(&[b'T', 1, 1, b't', 0]).unwrap();
903        let patchset = ParsedDiffSet::parse(&[b'P', 1, 1, b't', 0]).unwrap();
904        // Both are empty so PartialEq short-circuits to true. Add a real op
905        // to each so the variant-mismatch arm in the `match` is reached.
906        let mut full_changeset = vec![b'T', 1, 1, b't', 0];
907        full_changeset.push(op_codes::INSERT);
908        full_changeset.push(0);
909        full_changeset.push(0x01);
910        full_changeset.extend(&1i64.to_be_bytes());
911        let cs = ParsedDiffSet::parse(&full_changeset).unwrap();
912
913        let mut full_patchset = vec![b'P', 1, 1, b't', 0];
914        full_patchset.push(op_codes::INSERT);
915        full_patchset.push(0);
916        full_patchset.push(0x01);
917        full_patchset.extend(&1i64.to_be_bytes());
918        let ps = ParsedDiffSet::parse(&full_patchset).unwrap();
919
920        assert_ne!(cs, ps);
921        // Empty/empty still equal regardless of variant.
922        assert_eq!(changeset, patchset);
923    }
924
925    #[test]
926    fn test_parse_unexpected_eof_in_operation_header() {
927        // Valid changeset header followed by a single byte (op_code only,
928        // no indirect byte) — parse_operation_header must return UnexpectedEof.
929        let data = [b'T', 1, 1, b't', 0, op_codes::INSERT];
930        let err = ParsedDiffSet::parse(&data).unwrap_err();
931        assert!(matches!(err, ParseError::UnexpectedEof(_)), "got {err:?}");
932    }
933
934    #[test]
935    fn test_parse_patchset_indirect_flag_set() {
936        // Patchset INSERT carries full row values, same header layout.
937        let mut data = vec![b'P', 1, 1, b't', 0];
938        data.push(op_codes::INSERT);
939        data.push(1);
940        data.push(0x01);
941        data.extend(&1i64.to_be_bytes());
942
943        let parsed = ParsedDiffSet::parse(&data).unwrap();
944        let ParsedDiffSet::Patchset(set) = parsed else {
945            panic!("expected Patchset");
946        };
947        let indirect = set
948            .tables
949            .iter()
950            .find_map(|(_schema, rows)| rows.first().map(|(_, op)| op.indirect()))
951            .expect("expected at least one op");
952        assert!(indirect);
953    }
954
955    /// Assert a real SQLite patchset UPDATE byte string parses to a single
956    /// UPDATE operation, run caller-supplied checks against the destructured
957    /// state, and confirm the parsed value re-serializes byte-identically.
958    ///
959    /// Centralizing the destructures here keeps the individual scenario tests
960    /// focused on their assertions and folds the defensive `panic!` arms into
961    /// one place. Each test below feeds bytes captured from a real
962    /// `Session::patchset_strm` call, so the checker sees the exact wire
963    /// layout the parser now targets.
964    fn assert_patchset_update_roundtrip(
965        data: &[u8],
966        check: impl FnOnce(
967            &TableSchema<String>,
968            &[Value<String, Vec<u8>>],
969            &[((), MaybeValue<String, Vec<u8>>)],
970            bool,
971        ),
972    ) {
973        let parsed = ParsedDiffSet::parse(data).expect("SQLite patchset UPDATE must parse");
974        let ParsedDiffSet::Patchset(set) = parsed else {
975            panic!("expected Patchset, got {parsed:?}");
976        };
977        let (schema, rows) = set.tables.first().expect("expected one table");
978        assert_eq!(rows.len(), 1, "expected exactly one row");
979        let (pk, op) = rows.first().expect("row map non-empty");
980        let Operation::Update { values, indirect } = op else {
981            panic!("expected Update, got {op:?}");
982        };
983        check(schema, pk.as_slice(), values.as_slice(), *indirect);
984        let serialized: Vec<u8> = set.into();
985        assert_eq!(serialized, data, "roundtrip must match SQLite output");
986    }
987
988    /// Real SQLite session output for a standalone patchset UPDATE against a
989    /// pre-existing row on a single-column PK table:
990    ///
991    /// ```text
992    /// CREATE TABLE orders (id INTEGER PRIMARY KEY, amount INTEGER, status TEXT);
993    /// INSERT INTO orders VALUES (5, 100, 'pending'); -- before session.attach()
994    /// UPDATE orders SET status = 'shipped' WHERE id = 5; -- after attach, tracked
995    /// ```
996    ///
997    /// Wire layout:
998    /// - 12 bytes header ('P', 3, [1,0,0], "orders\0")
999    /// - 2 bytes op header (UPDATE, indirect=0)
1000    /// - 9 bytes old side: INTEGER 5 (only the PK column)
1001    /// - 10 bytes new side: undefined (amount unchanged) + TEXT 'shipped'
1002    ///
1003    /// Total 33 bytes. Historically the parser expected `column_count` values on
1004    /// each side (padded with undefined) and returned `InvalidValue` mid-buffer.
1005    #[test]
1006    fn test_parse_patchset_update_sqlite_wire_layout_single_pk() {
1007        let data: [u8; 33] = [
1008            0x50, 0x03, 0x01, 0x00, 0x00, b'o', b'r', b'd', b'e', b'r', b's', 0x00, 0x17, 0x00,
1009            0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x07, b's', b'h',
1010            b'i', b'p', b'p', b'e', b'd',
1011        ];
1012        assert_patchset_update_roundtrip(&data, |schema, pk, values, indirect| {
1013            assert_eq!(schema.name, "orders");
1014            assert_eq!(schema.column_count, 3);
1015            assert_eq!(schema.pk_flags, vec![1, 0, 0]);
1016            assert_eq!(pk, &[Value::Integer(5)]);
1017            assert!(!indirect);
1018            assert_eq!(values.len(), 3);
1019            assert_eq!(values[0].1, Some(Value::Integer(5))); // PK preserved
1020            assert_eq!(values[1].1, None); // amount unchanged
1021            assert_eq!(values[2].1, Some(Value::Text("shipped".into())));
1022        });
1023    }
1024
1025    /// Real SQLite output for a composite PK, `PRIMARY KEY(a, b)`:
1026    ///
1027    /// ```text
1028    /// CREATE TABLE items (a INTEGER NOT NULL, b INTEGER NOT NULL, val TEXT, PRIMARY KEY(a, b));
1029    /// INSERT INTO items VALUES (1, 2, 'v1'); -- before attach
1030    /// UPDATE items SET val = 'v2' WHERE a = 1 AND b = 2;
1031    /// ```
1032    ///
1033    /// Wire layout: two PK values on the old side (INTEGER 1, INTEGER 2), one
1034    /// non-PK value on the new side (TEXT 'v2'). 35 bytes total.
1035    #[test]
1036    fn test_parse_patchset_update_sqlite_wire_layout_composite_pk() {
1037        let data: [u8; 35] = [
1038            0x50, 0x03, 0x01, 0x02, 0x00, b'i', b't', b'e', b'm', b's', 0x00, 0x17, 0x00, 0x01,
1039            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
1040            0x00, 0x00, 0x02, 0x03, 0x02, b'v', b'2',
1041        ];
1042        assert_patchset_update_roundtrip(&data, |schema, pk, values, _indirect| {
1043            assert_eq!(schema.name, "items");
1044            assert_eq!(schema.pk_flags, vec![1, 2, 0]);
1045            // `extract_pk` returns values sorted by PK ordinal. With PK(a, b) and
1046            // pk_flags [1, 2, 0], ordinal 1 is column `a`, ordinal 2 is column `b`.
1047            assert_eq!(pk, &[Value::Integer(1), Value::Integer(2)]);
1048            assert_eq!(values.len(), 3);
1049            assert_eq!(values[0].1, Some(Value::Integer(1))); // a (PK)
1050            assert_eq!(values[1].1, Some(Value::Integer(2))); // b (PK)
1051            assert_eq!(values[2].1, Some(Value::Text("v2".into())));
1052        });
1053    }
1054
1055    /// Every non-PK column is present on the new side, in column order, either
1056    /// as its new value or as the undefined marker `0x00` when unchanged.
1057    ///
1058    /// ```text
1059    /// UPDATE orders SET amount = 200, status = 'shipped' WHERE id = 5;
1060    /// ```
1061    ///
1062    /// Two non-PK columns changed, so both are defined values (no undefined
1063    /// markers). 41 bytes total.
1064    #[test]
1065    fn test_parse_patchset_update_all_non_pk_changed() {
1066        let data: [u8; 41] = [
1067            0x50, 0x03, 0x01, 0x00, 0x00, b'o', b'r', b'd', b'e', b'r', b's', 0x00, 0x17, 0x00,
1068            0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00,
1069            0x00, 0x00, 0x00, 0xc8, 0x03, 0x07, b's', b'h', b'i', b'p', b'p', b'e', b'd',
1070        ];
1071        assert_patchset_update_roundtrip(&data, |_schema, _pk, values, _indirect| {
1072            assert_eq!(values[0].1, Some(Value::Integer(5)));
1073            assert_eq!(values[1].1, Some(Value::Integer(200)));
1074            assert_eq!(values[2].1, Some(Value::Text("shipped".into())));
1075        });
1076    }
1077
1078    /// Assert a parsed [`TableSchema`] and a [`SimpleTable`] of the same shape
1079    /// agree on every [`SchemaWithPK`] accessor, so the read and build sides
1080    /// are symmetric.
1081    fn assert_schema_pk_parity(
1082        parsed: &TableSchema<String>,
1083        simple: &SimpleTable,
1084        row: &[Value<String, Vec<u8>>],
1085    ) {
1086        assert_eq!(
1087            parsed.number_of_primary_keys(),
1088            simple.number_of_primary_keys(),
1089            "number_of_primary_keys",
1090        );
1091        for col in 0..simple.number_of_columns() {
1092            assert_eq!(
1093                parsed.primary_key_index(col),
1094                simple.primary_key_index(col),
1095                "primary_key_index at col {col}",
1096            );
1097        }
1098        assert_eq!(
1099            parsed.primary_key_columns(),
1100            simple.primary_key_columns(),
1101            "primary_key_columns",
1102        );
1103        assert_eq!(
1104            parsed.extract_pk(&row),
1105            simple.extract_pk(&row),
1106            "extract_pk"
1107        );
1108    }
1109
1110    #[test]
1111    fn test_parsed_schema_pk_parity_single_key() {
1112        // Changeset over `kv(id, val)` with single-column key `id` (flags [1, 0]).
1113        let mut data = vec![b'T', 2, 1, 0, b'k', b'v', 0];
1114        data.push(op_codes::INSERT);
1115        data.push(0);
1116        data.push(0x01);
1117        data.extend(&1i64.to_be_bytes());
1118        data.push(0x03);
1119        data.push(1);
1120        data.push(b'x');
1121
1122        let ParsedDiffSet::Changeset(set) = ParsedDiffSet::parse(&data).unwrap() else {
1123            panic!("expected changeset");
1124        };
1125        let (parsed, _rows) = set.tables.first().expect("one table");
1126        assert_eq!(parsed.pk_flags(), &[1, 0]);
1127
1128        let simple = SimpleTable::new("kv", &["id", "val"], &[0]);
1129        let row: Vec<Value<String, Vec<u8>>> = vec![Value::Integer(1), Value::Text("x".into())];
1130        assert_schema_pk_parity(parsed, &simple, &row);
1131        assert_eq!(parsed.primary_key_columns(), vec![0]);
1132    }
1133
1134    #[test]
1135    fn test_parsed_schema_pk_parity_composite_reordered_key() {
1136        // Changeset over `abc(a, b, c)` whose key is `(b, a)`, so the key column
1137        // order differs from table order. Flags are [2, 1, 0].
1138        let mut data = vec![b'T', 3, 2, 1, 0, b'a', b'b', b'c', 0];
1139        data.push(op_codes::INSERT);
1140        data.push(0);
1141        data.push(0x01);
1142        data.extend(&10i64.to_be_bytes());
1143        data.push(0x01);
1144        data.extend(&20i64.to_be_bytes());
1145        data.push(0x03);
1146        data.push(1);
1147        data.push(b'z');
1148
1149        let ParsedDiffSet::Changeset(set) = ParsedDiffSet::parse(&data).unwrap() else {
1150            panic!("expected changeset");
1151        };
1152        let (parsed, _rows) = set.tables.first().expect("one table");
1153        assert_eq!(parsed.pk_flags(), &[2, 1, 0]);
1154
1155        let simple = SimpleTable::new("abc", &["a", "b", "c"], &[1, 0]);
1156        let row: Vec<Value<String, Vec<u8>>> = vec![
1157            Value::Integer(10),
1158            Value::Integer(20),
1159            Value::Text("z".into()),
1160        ];
1161        assert_schema_pk_parity(parsed, &simple, &row);
1162        // Key order is (b, a): column 1 first, column 0 second.
1163        assert_eq!(parsed.primary_key_columns(), vec![1, 0]);
1164        // `extract_pk` follows key order: b's value, then a's value.
1165        assert_eq!(
1166            parsed.extract_pk(&row),
1167            vec![Value::Integer(20), Value::Integer(10)]
1168        );
1169    }
1170}