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    /// Rename table sections in place.
326    ///
327    /// The callback receives each section name and returns a new name, or
328    /// `None` to leave it unchanged. Returns the number of sections renamed.
329    /// Only the name changes. Columns, primary-key flags, and operations are
330    /// untouched, and two sections mapped to the same name stay separate.
331    pub fn rename_tables<F>(&mut self, mut rename: F) -> usize
332    where
333        F: FnMut(&str) -> Option<String>,
334    {
335        fn rename_in<Fmt, F>(tables: &mut [(TableSchema<String>, Fmt)], rename: &mut F) -> usize
336        where
337            F: FnMut(&str) -> Option<String>,
338        {
339            let mut renamed = 0;
340            for (schema, _) in tables.iter_mut() {
341                if let Some(new_name) = rename(schema.name.as_str()) {
342                    schema.name = new_name;
343                    renamed += 1;
344                }
345            }
346            renamed
347        }
348
349        match self {
350            ParsedDiffSet::Changeset(d) => rename_in(&mut d.tables, &mut rename),
351            ParsedDiffSet::Patchset(d) => rename_in(&mut d.tables, &mut rename),
352        }
353    }
354}
355
356/// Parse binary data as a changeset.
357///
358/// # Errors
359///
360/// Returns a `ParseError` if the data is malformed or not a valid changeset.
361fn parse_as_changeset(
362    data: &[u8],
363) -> Result<DiffSet<ChangesetFormat, TableSchema<String>, String, Vec<u8>>, ParseError> {
364    let mut builder: DiffSetBuilder<ChangesetFormat, TableSchema<String>, String, Vec<u8>> =
365        DiffSetBuilder::new();
366    let mut pos = 0;
367
368    while pos < data.len() {
369        let (schema, format, header_len) = parse_table_header(&data[pos..], pos)?;
370        if format != FormatMarker::Changeset {
371            return Err(ParseError::MixedFormats {
372                expected: FormatMarker::Changeset,
373                found: format,
374                position: pos,
375            });
376        }
377        pos += header_len;
378
379        while pos < data.len() {
380            let byte = data[pos];
381            if byte == markers::CHANGESET || byte == markers::PATCHSET {
382                break;
383            }
384            let op_len = parse_changeset_operation(&data[pos..], pos, &schema, &mut builder)?;
385            pos += op_len;
386        }
387    }
388
389    Ok(builder.into())
390}
391
392/// Parse binary data as a patchset.
393///
394/// # Errors
395///
396/// Returns a `ParseError` if the data is malformed or not a valid patchset.
397fn parse_as_patchset(
398    data: &[u8],
399) -> Result<DiffSet<PatchsetFormat, TableSchema<String>, String, Vec<u8>>, ParseError> {
400    let mut builder: DiffSetBuilder<PatchsetFormat, TableSchema<String>, String, Vec<u8>> =
401        DiffSetBuilder::new();
402    let mut pos = 0;
403
404    while pos < data.len() {
405        let (schema, format, header_len) = parse_table_header(&data[pos..], pos)?;
406        if format != FormatMarker::Patchset {
407            return Err(ParseError::MixedFormats {
408                expected: FormatMarker::Patchset,
409                found: format,
410                position: pos,
411            });
412        }
413        pos += header_len;
414
415        while pos < data.len() {
416            let byte = data[pos];
417            if byte == markers::CHANGESET || byte == markers::PATCHSET {
418                break;
419            }
420            let op_len = parse_patchset_operation(&data[pos..], pos, &schema, &mut builder)?;
421            pos += op_len;
422        }
423    }
424
425    Ok(builder.into())
426}
427
428/// Parse a table header and return the schema.
429fn parse_table_header(
430    data: &[u8],
431    base_pos: usize,
432) -> Result<(TableSchema<String>, FormatMarker, usize), ParseError> {
433    let mut pos = 0;
434
435    if data.is_empty() {
436        return Err(ParseError::UnexpectedEof(base_pos));
437    }
438    let format = match data[pos] {
439        markers::CHANGESET => FormatMarker::Changeset,
440        markers::PATCHSET => FormatMarker::Patchset,
441        b => return Err(ParseError::InvalidTableMarker(b, base_pos + pos)),
442    };
443    pos += 1;
444
445    if pos >= data.len() {
446        return Err(ParseError::UnexpectedEof(base_pos + pos));
447    }
448    let column_count = data[pos] as usize;
449    pos += 1;
450
451    if pos + column_count > data.len() {
452        return Err(ParseError::UnexpectedEof(base_pos + pos));
453    }
454    let pk_flags: Vec<u8> = data[pos..pos + column_count].to_vec();
455    pos += column_count;
456
457    let name_start = pos;
458    while pos < data.len() && data[pos] != 0 {
459        pos += 1;
460    }
461    if pos >= data.len() {
462        return Err(ParseError::UnterminatedTableName);
463    }
464    let name = String::from_utf8(data[name_start..pos].to_vec())
465        .map_err(|_| ParseError::InvalidTableName(base_pos + name_start))?;
466    pos += 1;
467
468    Ok((TableSchema::new(name, column_count, pk_flags), format, pos))
469}
470
471/// Parse operation header (`op_code` + indirect flag).
472///
473/// Returns `(op_code, indirect, bytes_consumed)`. Any non-zero indirect byte
474/// parses as `true` to match SQLite's permissive treatment of the flag.
475fn parse_operation_header(data: &[u8], base_pos: usize) -> Result<(u8, bool, usize), ParseError> {
476    if data.len() < 2 {
477        return Err(ParseError::UnexpectedEof(base_pos));
478    }
479    Ok((data[0], data[1] != 0, 2))
480}
481
482/// Parse a changeset operation.
483fn parse_changeset_operation(
484    data: &[u8],
485    base_pos: usize,
486    schema: &TableSchema<String>,
487    builder: &mut DiffSetBuilder<ChangesetFormat, TableSchema<String>, String, Vec<u8>>,
488) -> Result<usize, ParseError> {
489    let (op_code, indirect, mut pos) = parse_operation_header(data, base_pos)?;
490
491    match op_code {
492        op_codes::INSERT => {
493            let (values, len) = parse_values(&data[pos..], base_pos + pos, schema.column_count)?;
494            pos += len;
495            let values: Vec<Value<String, Vec<u8>>> = values
496                .into_iter()
497                .map(|v| v.unwrap_or(Value::Null))
498                .collect();
499            let pk = schema.extract_pk(&values);
500            builder.add_operation(schema, pk, Operation::Insert { values, indirect });
501        }
502        op_codes::DELETE => {
503            let (values, len) = parse_values(&data[pos..], base_pos + pos, schema.column_count)?;
504            pos += len;
505            let values: Vec<Value<String, Vec<u8>>> = values
506                .into_iter()
507                .map(|v| v.unwrap_or(Value::Null))
508                .collect();
509            let pk = schema.extract_pk(&values);
510            builder.add_operation(
511                schema,
512                pk,
513                Operation::Delete {
514                    data: values,
515                    indirect,
516                },
517            );
518        }
519        op_codes::UPDATE => {
520            let (old_values, old_len) =
521                parse_values(&data[pos..], base_pos + pos, schema.column_count)?;
522            pos += old_len;
523            let (new_values, new_len) =
524                parse_values(&data[pos..], base_pos + pos, schema.column_count)?;
525            pos += new_len;
526            // Extract PK using old values (convert None to Null)
527            let pk_values: Vec<Value<String, Vec<u8>>> = old_values
528                .iter()
529                .map(|v| v.clone().unwrap_or(Value::Null))
530                .collect();
531            let pk = schema.extract_pk(&pk_values);
532            let values: UpdateValues = old_values.into_iter().zip(new_values).collect();
533            builder.add_operation(schema, pk, Operation::Update { values, indirect });
534        }
535        _ => return Err(ParseError::InvalidOpCode(op_code, base_pos)),
536    }
537
538    Ok(pos)
539}
540
541/// Parse a patchset operation.
542fn parse_patchset_operation(
543    data: &[u8],
544    base_pos: usize,
545    schema: &TableSchema<String>,
546    builder: &mut DiffSetBuilder<PatchsetFormat, TableSchema<String>, String, Vec<u8>>,
547) -> Result<usize, ParseError> {
548    let (op_code, indirect, mut pos) = parse_operation_header(data, base_pos)?;
549
550    match op_code {
551        op_codes::INSERT => {
552            let (values, len) = parse_values(&data[pos..], base_pos + pos, schema.column_count)?;
553            pos += len;
554            let values: Vec<Value<String, Vec<u8>>> = values
555                .into_iter()
556                .map(|v| v.unwrap_or(Value::Null))
557                .collect();
558            let pk = schema.extract_pk(&values);
559            builder.add_operation(schema, pk, Operation::Insert { values, indirect });
560        }
561        op_codes::DELETE => {
562            // Patchset DELETE: only PK values in column order
563            let pk_count = schema.pk_flags.iter().filter(|&&b| b > 0).count();
564            let (pk_values, len) = parse_values(&data[pos..], base_pos + pos, pk_count)?;
565            pos += len;
566            // Expand PK values to full row, then extract_pk to get ordinal-sorted PK.
567            // This is needed because the binary format stores PKs in column order,
568            // but the builder stores them sorted by pk_ordinal (matching the serializer).
569            let full_values = expand_pk_values(&schema.pk_flags, pk_values, schema.column_count);
570            // Convert MaybeValue to Value for extract_pk (PK values should always be defined)
571            let full_values_concrete: Vec<Value<String, Vec<u8>>> = full_values
572                .into_iter()
573                .map(|v| v.unwrap_or(Value::Null))
574                .collect();
575            let pk = schema.extract_pk(&full_values_concrete);
576            builder.add_operation(schema, pk, Operation::Delete { data: (), indirect });
577        }
578        op_codes::UPDATE => {
579            // Patchset UPDATE wire layout (matching SQLite's session extension):
580            //   old side: PK-only values, in column order, exactly `pk_count` entries
581            //            (no padding / undefined markers for non-PK columns).
582            //   new side: non-PK columns, in column order, exactly
583            //            `column_count - pk_count` entries; each entry is either the
584            //            new column value or `0x00` (undefined) if that non-PK column
585            //            did not change.
586            //
587            // Internally we rehydrate the operation into a full-width
588            // `Vec<((), MaybeValue)>` of length `column_count` so downstream code
589            // (`extract_pk`, `sql_output`, consolidation, reversal) keeps working
590            // uniformly: PK slots hold `Some(pk_value)`, non-PK slots hold either
591            // `Some(new_value)` or `None` for undefined.
592            let pk_count = schema.pk_flags.iter().filter(|&&b| b > 0).count();
593            let non_pk_count = schema.column_count.saturating_sub(pk_count);
594
595            let (old_pk_values, old_len) = parse_values(&data[pos..], base_pos + pos, pk_count)?;
596            pos += old_len;
597            let (new_non_pk_values, new_len) =
598                parse_values(&data[pos..], base_pos + pos, non_pk_count)?;
599            pos += new_len;
600
601            let mut values: Vec<((), MaybeValue<String, Vec<u8>>)> =
602                alloc::vec![((), None); schema.column_count];
603            let mut old_iter = old_pk_values.into_iter();
604            let mut new_iter = new_non_pk_values.into_iter();
605            for (col_idx, &pk_flag) in schema.pk_flags.iter().enumerate() {
606                if pk_flag > 0 {
607                    // PK columns always carry a defined value on the old side; a
608                    // stray undefined marker is normalized to Null to stay lenient
609                    // for fuzz-generated inputs (matches `expand_pk_values` in the
610                    // DELETE path).
611                    let old = old_iter.next().flatten().unwrap_or(Value::Null);
612                    values[col_idx] = ((), Some(old));
613                } else {
614                    values[col_idx] = ((), new_iter.next().flatten());
615                }
616            }
617
618            let pk = schema.extract_pk(&values);
619            builder.add_operation(schema, pk, Operation::Update { values, indirect });
620        }
621        _ => return Err(ParseError::InvalidOpCode(op_code, base_pos)),
622    }
623
624    Ok(pos)
625}
626
627/// Expand PK-only values to full row with None (undefined) for non-PK columns.
628///
629/// The `pk_flags` are raw bytes where non-zero means the column is part of the PK.
630/// PK values are expected in the order they appear in `pk_flags` (not sorted by ordinal).
631fn expand_pk_values(
632    pk_flags: &[u8],
633    pk_values: Vec<MaybeValue<String, Vec<u8>>>,
634    column_count: usize,
635) -> Vec<MaybeValue<String, Vec<u8>>> {
636    let mut full: Vec<MaybeValue<String, Vec<u8>>> = vec![None; column_count];
637    let mut pk_iter = pk_values.into_iter();
638    for (i, &pk_ordinal) in pk_flags.iter().enumerate() {
639        if pk_ordinal > 0
640            && let Some(v) = pk_iter.next()
641        {
642            full[i] = v;
643        }
644    }
645    full
646}
647
648/// Parse a sequence of values.
649fn parse_values(data: &[u8], base_pos: usize, count: usize) -> Result<ParsedValues, ParseError> {
650    let mut values = Vec::with_capacity(count);
651    let mut pos = 0;
652
653    for _ in 0..count {
654        let (value, value_len) =
655            decode_value(&data[pos..]).ok_or(ParseError::InvalidValue(base_pos + pos))?;
656        values.push(value);
657        pos += value_len;
658    }
659
660    Ok((values, pos))
661}
662
663#[cfg(test)]
664mod tests {
665    use super::*;
666    use crate::SimpleTable;
667    use alloc::vec;
668
669    #[test]
670    fn test_parse_empty() {
671        let result = ParsedDiffSet::parse(&[]);
672        assert!(result.is_ok());
673        assert!(result.unwrap().is_changeset());
674    }
675
676    #[test]
677    fn test_parse_table_header() {
678        // 'T', 2 columns, pk_flags [1, 0], table name "t\0"
679        let data = [b'T', 2, 1, 0, b't', 0];
680        let (schema, format, len) = parse_table_header(&data, 0).unwrap();
681
682        assert_eq!(format, FormatMarker::Changeset);
683        assert_eq!(schema.column_count, 2);
684        assert_eq!(schema.pk_flags, vec![1, 0]); // Raw bytes: 1 = first PK column, 0 = not PK
685        assert_eq!(schema.name, "t");
686        assert_eq!(len, 6);
687    }
688
689    #[test]
690    fn test_parse_insert_changeset() {
691        // Table header + INSERT with integer 1 and text "a"
692        let mut data = vec![b'T', 2, 1, 0, b't', 0];
693        // INSERT opcode, indirect=0
694        data.push(op_codes::INSERT);
695        data.push(0);
696        // Integer 1 (type 1, 8 bytes)
697        data.push(0x01);
698        data.extend(&1i64.to_be_bytes());
699        // Text "a" (type 3, length 1, "a")
700        data.push(0x03);
701        data.push(1);
702        data.push(b'a');
703
704        let parsed = ParsedDiffSet::parse(&data).unwrap();
705        assert!(parsed.is_changeset());
706    }
707
708    #[test]
709    fn test_parse_delete_changeset() {
710        let mut data = vec![b'T', 2, 1, 0, b't', 0];
711        data.push(op_codes::DELETE);
712        data.push(0);
713        // Integer 1
714        data.push(0x01);
715        data.extend(&1i64.to_be_bytes());
716        // Text "a"
717        data.push(0x03);
718        data.push(1);
719        data.push(b'a');
720
721        let parsed = ParsedDiffSet::parse(&data).unwrap();
722        assert!(parsed.is_changeset());
723    }
724
725    #[test]
726    fn test_parse_delete_patchset() {
727        // Patchset DELETE only has PK values
728        let mut data = vec![b'P', 2, 1, 0, b't', 0];
729        data.push(op_codes::DELETE);
730        data.push(0);
731        // Only PK value (integer 1)
732        data.push(0x01);
733        data.extend(&1i64.to_be_bytes());
734
735        let parsed = ParsedDiffSet::parse(&data).unwrap();
736        assert!(parsed.is_patchset());
737    }
738
739    #[test]
740    fn test_parse_update_changeset() {
741        let mut data = vec![b'T', 2, 1, 0, b't', 0];
742        data.push(op_codes::UPDATE);
743        data.push(0);
744        // Old values: integer 1, text "a"
745        data.push(0x01);
746        data.extend(&1i64.to_be_bytes());
747        data.push(0x03);
748        data.push(1);
749        data.push(b'a');
750        // New values: integer 1, text "b"
751        data.push(0x01);
752        data.extend(&1i64.to_be_bytes());
753        data.push(0x03);
754        data.push(1);
755        data.push(b'b');
756
757        let parsed = ParsedDiffSet::parse(&data).unwrap();
758        assert!(parsed.is_changeset());
759    }
760
761    #[test]
762    fn test_is_changeset() {
763        let data = vec![b'T', 1, 1, b't', 0];
764        let parsed = ParsedDiffSet::parse(&data).unwrap();
765        assert!(parsed.is_changeset());
766        assert!(!parsed.is_patchset());
767    }
768
769    #[test]
770    fn test_is_patchset() {
771        let data = vec![b'P', 1, 1, b't', 0];
772        let parsed = ParsedDiffSet::parse(&data).unwrap();
773        assert!(parsed.is_patchset());
774        assert!(!parsed.is_changeset());
775    }
776
777    #[test]
778    fn test_parsed_table_schema_dyn_table() {
779        let schema: TableSchema<String> = TableSchema::new("users".into(), 3, vec![1, 0, 0]);
780        assert_eq!(schema.name(), "users");
781        assert_eq!(schema.number_of_columns(), 3);
782
783        let mut buf = [0u8; 3];
784        schema.write_pk_flags(&mut buf);
785        assert_eq!(buf, [1, 0, 0]);
786    }
787
788    #[test]
789    fn test_parsed_table_schema_extract_pk() {
790        let schema: TableSchema<String> = TableSchema::new("users".into(), 3, vec![1, 0, 2]);
791        let values: Vec<Value<String, Vec<u8>>> = vec![
792            Value::Integer(1),
793            Value::Text("alice".into()),
794            Value::Integer(100),
795        ];
796        let pk = schema.extract_pk(&values);
797        let expected: Vec<Value<String, Vec<u8>>> = vec![Value::Integer(1), Value::Integer(100)];
798        assert_eq!(pk, expected);
799    }
800
801    // ---- Error path tests ----
802
803    #[test]
804    fn test_parse_invalid_table_marker() {
805        let data = [0xFFu8, 1, 1, b't', 0];
806        let err = ParsedDiffSet::parse(&data).unwrap_err();
807        assert!(
808            matches!(err, ParseError::InvalidTableMarker(0xFF, 0)),
809            "got {err:?}"
810        );
811    }
812
813    #[test]
814    fn test_parse_unexpected_eof_in_table_header() {
815        // 'T' marker but no column count
816        let data = *b"T";
817        let err = ParsedDiffSet::parse(&data).unwrap_err();
818        assert!(matches!(err, ParseError::UnexpectedEof(_)), "got {err:?}");
819    }
820
821    #[test]
822    fn test_parse_unexpected_eof_in_pk_flags() {
823        // 'T', column count 3, but only 1 PK flag byte
824        let data = [b'T', 3, 1];
825        let err = ParsedDiffSet::parse(&data).unwrap_err();
826        assert!(matches!(err, ParseError::UnexpectedEof(_)), "got {err:?}");
827    }
828
829    #[test]
830    fn test_parse_unterminated_table_name() {
831        // 'T', 1 column, pk_flags [1], then "abc" with no null terminator
832        let data = [b'T', 1, 1, b'a', b'b', b'c'];
833        let err = ParsedDiffSet::parse(&data).unwrap_err();
834        assert!(
835            matches!(err, ParseError::UnterminatedTableName),
836            "got {err:?}"
837        );
838    }
839
840    #[test]
841    fn test_parse_invalid_utf8_in_table_name() {
842        // 'T', 1 column, pk_flags [1], then 0xFF (invalid UTF-8), then null
843        let data = [b'T', 1, 1, 0xFF, 0];
844        let err = ParsedDiffSet::parse(&data).unwrap_err();
845        assert!(
846            matches!(err, ParseError::InvalidTableName(_)),
847            "got {err:?}"
848        );
849    }
850
851    #[test]
852    fn test_parse_mixed_formats_changeset_then_patchset() {
853        // First table 'T' (changeset), then second table 'P' (patchset)
854        let mut data = vec![b'T', 1, 1, b'a', 0];
855        // Now a 'P' table header without preceding operations
856        data.extend_from_slice(&[b'P', 1, 1, b'b', 0]);
857        let err = ParsedDiffSet::parse(&data).unwrap_err();
858        assert!(
859            matches!(
860                err,
861                ParseError::MixedFormats {
862                    expected: FormatMarker::Changeset,
863                    found: FormatMarker::Patchset,
864                    ..
865                }
866            ),
867            "got {err:?}"
868        );
869    }
870
871    #[test]
872    fn test_parse_mixed_formats_patchset_then_changeset() {
873        let mut data = vec![b'P', 1, 1, b'a', 0];
874        data.extend_from_slice(&[b'T', 1, 1, b'b', 0]);
875        let err = ParsedDiffSet::parse(&data).unwrap_err();
876        assert!(
877            matches!(
878                err,
879                ParseError::MixedFormats {
880                    expected: FormatMarker::Patchset,
881                    found: FormatMarker::Changeset,
882                    ..
883                }
884            ),
885            "got {err:?}"
886        );
887    }
888
889    /// Build the operation header bytes followed by a single integer payload.
890    fn make_insert_with_indirect(indirect_byte: u8) -> Vec<u8> {
891        let mut data = vec![b'T', 1, 1, b't', 0];
892        data.push(op_codes::INSERT);
893        data.push(indirect_byte);
894        // Integer 1
895        data.push(0x01);
896        data.extend(&1i64.to_be_bytes());
897        data
898    }
899
900    fn first_op_indirect_changeset(data: &[u8]) -> bool {
901        let parsed = ParsedDiffSet::parse(data).unwrap();
902        let ParsedDiffSet::Changeset(set) = parsed else {
903            panic!("expected Changeset");
904        };
905        set.tables
906            .iter()
907            .find_map(|(_schema, rows)| rows.first().map(|(_, op)| op.indirect()))
908            .expect("expected at least one op")
909    }
910
911    #[test]
912    fn test_parse_changeset_indirect_flag_set() {
913        let data = make_insert_with_indirect(1);
914        assert!(first_op_indirect_changeset(&data));
915    }
916
917    #[test]
918    fn test_parse_changeset_indirect_flag_clear() {
919        let data = make_insert_with_indirect(0);
920        assert!(!first_op_indirect_changeset(&data));
921    }
922
923    #[test]
924    fn test_parse_indirect_nonzero_treated_as_true() {
925        // Any non-zero byte must parse as indirect = true.
926        let data = make_insert_with_indirect(0x42);
927        assert!(first_op_indirect_changeset(&data));
928    }
929
930    #[test]
931    fn test_parsed_diffset_variant_mismatch_partial_eq() {
932        let changeset = ParsedDiffSet::parse(&[b'T', 1, 1, b't', 0]).unwrap();
933        let patchset = ParsedDiffSet::parse(&[b'P', 1, 1, b't', 0]).unwrap();
934        // Both are empty so PartialEq short-circuits to true. Add a real op
935        // to each so the variant-mismatch arm in the `match` is reached.
936        let mut full_changeset = vec![b'T', 1, 1, b't', 0];
937        full_changeset.push(op_codes::INSERT);
938        full_changeset.push(0);
939        full_changeset.push(0x01);
940        full_changeset.extend(&1i64.to_be_bytes());
941        let cs = ParsedDiffSet::parse(&full_changeset).unwrap();
942
943        let mut full_patchset = vec![b'P', 1, 1, b't', 0];
944        full_patchset.push(op_codes::INSERT);
945        full_patchset.push(0);
946        full_patchset.push(0x01);
947        full_patchset.extend(&1i64.to_be_bytes());
948        let ps = ParsedDiffSet::parse(&full_patchset).unwrap();
949
950        assert_ne!(cs, ps);
951        // Empty/empty still equal regardless of variant.
952        assert_eq!(changeset, patchset);
953    }
954
955    #[test]
956    fn test_parse_unexpected_eof_in_operation_header() {
957        // Valid changeset header followed by a single byte (op_code only,
958        // no indirect byte) — parse_operation_header must return UnexpectedEof.
959        let data = [b'T', 1, 1, b't', 0, op_codes::INSERT];
960        let err = ParsedDiffSet::parse(&data).unwrap_err();
961        assert!(matches!(err, ParseError::UnexpectedEof(_)), "got {err:?}");
962    }
963
964    #[test]
965    fn test_parse_patchset_indirect_flag_set() {
966        // Patchset INSERT carries full row values, same header layout.
967        let mut data = vec![b'P', 1, 1, b't', 0];
968        data.push(op_codes::INSERT);
969        data.push(1);
970        data.push(0x01);
971        data.extend(&1i64.to_be_bytes());
972
973        let parsed = ParsedDiffSet::parse(&data).unwrap();
974        let ParsedDiffSet::Patchset(set) = parsed else {
975            panic!("expected Patchset");
976        };
977        let indirect = set
978            .tables
979            .iter()
980            .find_map(|(_schema, rows)| rows.first().map(|(_, op)| op.indirect()))
981            .expect("expected at least one op");
982        assert!(indirect);
983    }
984
985    /// Assert a real SQLite patchset UPDATE byte string parses to a single
986    /// UPDATE operation, run caller-supplied checks against the destructured
987    /// state, and confirm the parsed value re-serializes byte-identically.
988    ///
989    /// Centralizing the destructures here keeps the individual scenario tests
990    /// focused on their assertions and folds the defensive `panic!` arms into
991    /// one place. Each test below feeds bytes captured from a real
992    /// `Session::patchset_strm` call, so the checker sees the exact wire
993    /// layout the parser now targets.
994    fn assert_patchset_update_roundtrip(
995        data: &[u8],
996        check: impl FnOnce(
997            &TableSchema<String>,
998            &[Value<String, Vec<u8>>],
999            &[((), MaybeValue<String, Vec<u8>>)],
1000            bool,
1001        ),
1002    ) {
1003        let parsed = ParsedDiffSet::parse(data).expect("SQLite patchset UPDATE must parse");
1004        let ParsedDiffSet::Patchset(set) = parsed else {
1005            panic!("expected Patchset, got {parsed:?}");
1006        };
1007        let (schema, rows) = set.tables.first().expect("expected one table");
1008        assert_eq!(rows.len(), 1, "expected exactly one row");
1009        let (pk, op) = rows.first().expect("row map non-empty");
1010        let Operation::Update { values, indirect } = op else {
1011            panic!("expected Update, got {op:?}");
1012        };
1013        check(schema, pk.as_slice(), values.as_slice(), *indirect);
1014        let serialized: Vec<u8> = set.into();
1015        assert_eq!(serialized, data, "roundtrip must match SQLite output");
1016    }
1017
1018    /// Real SQLite session output for a standalone patchset UPDATE against a
1019    /// pre-existing row on a single-column PK table:
1020    ///
1021    /// ```text
1022    /// CREATE TABLE orders (id INTEGER PRIMARY KEY, amount INTEGER, status TEXT);
1023    /// INSERT INTO orders VALUES (5, 100, 'pending'); -- before session.attach()
1024    /// UPDATE orders SET status = 'shipped' WHERE id = 5; -- after attach, tracked
1025    /// ```
1026    ///
1027    /// Wire layout:
1028    /// - 12 bytes header ('P', 3, [1,0,0], "orders\0")
1029    /// - 2 bytes op header (UPDATE, indirect=0)
1030    /// - 9 bytes old side: INTEGER 5 (only the PK column)
1031    /// - 10 bytes new side: undefined (amount unchanged) + TEXT 'shipped'
1032    ///
1033    /// Total 33 bytes. Historically the parser expected `column_count` values on
1034    /// each side (padded with undefined) and returned `InvalidValue` mid-buffer.
1035    #[test]
1036    fn test_parse_patchset_update_sqlite_wire_layout_single_pk() {
1037        let data: [u8; 33] = [
1038            0x50, 0x03, 0x01, 0x00, 0x00, b'o', b'r', b'd', b'e', b'r', b's', 0x00, 0x17, 0x00,
1039            0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x07, b's', b'h',
1040            b'i', b'p', b'p', b'e', b'd',
1041        ];
1042        assert_patchset_update_roundtrip(&data, |schema, pk, values, indirect| {
1043            assert_eq!(schema.name, "orders");
1044            assert_eq!(schema.column_count, 3);
1045            assert_eq!(schema.pk_flags, vec![1, 0, 0]);
1046            assert_eq!(pk, &[Value::Integer(5)]);
1047            assert!(!indirect);
1048            assert_eq!(values.len(), 3);
1049            assert_eq!(values[0].1, Some(Value::Integer(5))); // PK preserved
1050            assert_eq!(values[1].1, None); // amount unchanged
1051            assert_eq!(values[2].1, Some(Value::Text("shipped".into())));
1052        });
1053    }
1054
1055    /// Real SQLite output for a composite PK, `PRIMARY KEY(a, b)`:
1056    ///
1057    /// ```text
1058    /// CREATE TABLE items (a INTEGER NOT NULL, b INTEGER NOT NULL, val TEXT, PRIMARY KEY(a, b));
1059    /// INSERT INTO items VALUES (1, 2, 'v1'); -- before attach
1060    /// UPDATE items SET val = 'v2' WHERE a = 1 AND b = 2;
1061    /// ```
1062    ///
1063    /// Wire layout: two PK values on the old side (INTEGER 1, INTEGER 2), one
1064    /// non-PK value on the new side (TEXT 'v2'). 35 bytes total.
1065    #[test]
1066    fn test_parse_patchset_update_sqlite_wire_layout_composite_pk() {
1067        let data: [u8; 35] = [
1068            0x50, 0x03, 0x01, 0x02, 0x00, b'i', b't', b'e', b'm', b's', 0x00, 0x17, 0x00, 0x01,
1069            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
1070            0x00, 0x00, 0x02, 0x03, 0x02, b'v', b'2',
1071        ];
1072        assert_patchset_update_roundtrip(&data, |schema, pk, values, _indirect| {
1073            assert_eq!(schema.name, "items");
1074            assert_eq!(schema.pk_flags, vec![1, 2, 0]);
1075            // `extract_pk` returns values sorted by PK ordinal. With PK(a, b) and
1076            // pk_flags [1, 2, 0], ordinal 1 is column `a`, ordinal 2 is column `b`.
1077            assert_eq!(pk, &[Value::Integer(1), Value::Integer(2)]);
1078            assert_eq!(values.len(), 3);
1079            assert_eq!(values[0].1, Some(Value::Integer(1))); // a (PK)
1080            assert_eq!(values[1].1, Some(Value::Integer(2))); // b (PK)
1081            assert_eq!(values[2].1, Some(Value::Text("v2".into())));
1082        });
1083    }
1084
1085    /// Every non-PK column is present on the new side, in column order, either
1086    /// as its new value or as the undefined marker `0x00` when unchanged.
1087    ///
1088    /// ```text
1089    /// UPDATE orders SET amount = 200, status = 'shipped' WHERE id = 5;
1090    /// ```
1091    ///
1092    /// Two non-PK columns changed, so both are defined values (no undefined
1093    /// markers). 41 bytes total.
1094    #[test]
1095    fn test_parse_patchset_update_all_non_pk_changed() {
1096        let data: [u8; 41] = [
1097            0x50, 0x03, 0x01, 0x00, 0x00, b'o', b'r', b'd', b'e', b'r', b's', 0x00, 0x17, 0x00,
1098            0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00,
1099            0x00, 0x00, 0x00, 0xc8, 0x03, 0x07, b's', b'h', b'i', b'p', b'p', b'e', b'd',
1100        ];
1101        assert_patchset_update_roundtrip(&data, |_schema, _pk, values, _indirect| {
1102            assert_eq!(values[0].1, Some(Value::Integer(5)));
1103            assert_eq!(values[1].1, Some(Value::Integer(200)));
1104            assert_eq!(values[2].1, Some(Value::Text("shipped".into())));
1105        });
1106    }
1107
1108    /// Assert a parsed [`TableSchema`] and a [`SimpleTable`] of the same shape
1109    /// agree on every [`SchemaWithPK`] accessor, so the read and build sides
1110    /// are symmetric.
1111    fn assert_schema_pk_parity(
1112        parsed: &TableSchema<String>,
1113        simple: &SimpleTable,
1114        row: &[Value<String, Vec<u8>>],
1115    ) {
1116        assert_eq!(
1117            parsed.number_of_primary_keys(),
1118            simple.number_of_primary_keys(),
1119            "number_of_primary_keys",
1120        );
1121        for col in 0..simple.number_of_columns() {
1122            assert_eq!(
1123                parsed.primary_key_index(col),
1124                simple.primary_key_index(col),
1125                "primary_key_index at col {col}",
1126            );
1127        }
1128        assert_eq!(
1129            parsed.primary_key_columns(),
1130            simple.primary_key_columns(),
1131            "primary_key_columns",
1132        );
1133        assert_eq!(
1134            parsed.extract_pk(&row),
1135            simple.extract_pk(&row),
1136            "extract_pk"
1137        );
1138    }
1139
1140    #[test]
1141    fn test_parsed_schema_pk_parity_single_key() {
1142        // Changeset over `kv(id, val)` with single-column key `id` (flags [1, 0]).
1143        let mut data = vec![b'T', 2, 1, 0, b'k', b'v', 0];
1144        data.push(op_codes::INSERT);
1145        data.push(0);
1146        data.push(0x01);
1147        data.extend(&1i64.to_be_bytes());
1148        data.push(0x03);
1149        data.push(1);
1150        data.push(b'x');
1151
1152        let ParsedDiffSet::Changeset(set) = ParsedDiffSet::parse(&data).unwrap() else {
1153            panic!("expected changeset");
1154        };
1155        let (parsed, _rows) = set.tables.first().expect("one table");
1156        assert_eq!(parsed.pk_flags(), &[1, 0]);
1157
1158        let simple = SimpleTable::new("kv", &["id", "val"], &[0]);
1159        let row: Vec<Value<String, Vec<u8>>> = vec![Value::Integer(1), Value::Text("x".into())];
1160        assert_schema_pk_parity(parsed, &simple, &row);
1161        assert_eq!(parsed.primary_key_columns(), vec![0]);
1162    }
1163
1164    #[test]
1165    fn test_parsed_schema_pk_parity_composite_reordered_key() {
1166        // Changeset over `abc(a, b, c)` whose key is `(b, a)`, so the key column
1167        // order differs from table order. Flags are [2, 1, 0].
1168        let mut data = vec![b'T', 3, 2, 1, 0, b'a', b'b', b'c', 0];
1169        data.push(op_codes::INSERT);
1170        data.push(0);
1171        data.push(0x01);
1172        data.extend(&10i64.to_be_bytes());
1173        data.push(0x01);
1174        data.extend(&20i64.to_be_bytes());
1175        data.push(0x03);
1176        data.push(1);
1177        data.push(b'z');
1178
1179        let ParsedDiffSet::Changeset(set) = ParsedDiffSet::parse(&data).unwrap() else {
1180            panic!("expected changeset");
1181        };
1182        let (parsed, _rows) = set.tables.first().expect("one table");
1183        assert_eq!(parsed.pk_flags(), &[2, 1, 0]);
1184
1185        let simple = SimpleTable::new("abc", &["a", "b", "c"], &[1, 0]);
1186        let row: Vec<Value<String, Vec<u8>>> = vec![
1187            Value::Integer(10),
1188            Value::Integer(20),
1189            Value::Text("z".into()),
1190        ];
1191        assert_schema_pk_parity(parsed, &simple, &row);
1192        // Key order is (b, a): column 1 first, column 0 second.
1193        assert_eq!(parsed.primary_key_columns(), vec![1, 0]);
1194        // `extract_pk` follows key order: b's value, then a's value.
1195        assert_eq!(
1196            parsed.extract_pk(&row),
1197            vec![Value::Integer(20), Value::Integer(10)]
1198        );
1199    }
1200}