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: 0 = not part of the key, k = k-th key column)
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::varint::decode_varint;
38use crate::encoding::{MaybeValue, Value, decode_value, markers, op_codes};
39use crate::schema::{DynTable, SchemaWithPK};
40
41/// Errors that can occur during parsing.
42#[non_exhaustive]
43#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
44pub enum ParseError {
45    /// Unexpected end of input.
46    #[error("Unexpected end of input at position {0}")]
47    UnexpectedEof(usize),
48
49    /// Invalid table marker (expected 'T' or 'P').
50    #[error("Invalid table marker 0x{0:02x} at position {1}")]
51    InvalidTableMarker(u8, usize),
52
53    /// Invalid operation code.
54    #[error("Invalid operation code 0x{0:02x} at position {1}")]
55    InvalidOpCode(u8, usize),
56
57    /// Invalid UTF-8 in table name.
58    #[error("Invalid UTF-8 in table name at position {0}")]
59    InvalidTableName(usize),
60
61    /// Failed to decode a value.
62    #[error("Failed to decode value at position {0}")]
63    InvalidValue(usize),
64
65    /// Table name not null-terminated.
66    #[error("Table name not null-terminated")]
67    UnterminatedTableName,
68
69    /// Mixed format markers in the same file.
70    #[error("Mixed format markers: expected {expected:?}, found {found:?} at position {position}")]
71    MixedFormats {
72        /// The expected format marker.
73        expected: FormatMarker,
74        /// The found format marker.
75        found: FormatMarker,
76        /// The position where the mismatch occurred.
77        position: usize,
78    },
79
80    /// Primary-key flags in a table header are not a unique dense 1-based sequence.
81    ///
82    /// The nonzero bytes must form exactly `{1, 2, ..., n}` where `n` is the count
83    /// of nonzero bytes. All-zero flags (no primary key) are valid.
84    #[error(
85        "Invalid primary-key flags for table {table_name:?} at position {position}: \
86         nonzero bytes must be a unique dense 1-based sequence"
87    )]
88    InvalidPrimaryKeyFlags {
89        /// The table whose header contained invalid flags.
90        table_name: String,
91        /// Byte position of the pk_flags region in the input.
92        position: usize,
93    },
94}
95
96/// The detected format marker.
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98pub enum FormatMarker {
99    /// Changeset format ('T' marker).
100    Changeset,
101    /// Patchset format ('P' marker).
102    Patchset,
103}
104
105/// A table schema parsed from binary changeset/patchset data.
106///
107/// This type implements [`DynTable`] and [`SchemaWithPK`], allowing it
108/// to be used with [`DiffSetBuilder`].
109#[derive(Debug, Clone, PartialEq, Eq, Hash)]
110pub struct TableSchema<S> {
111    /// The table name.
112    name: S,
113    /// Number of columns.
114    column_count: usize,
115    /// Primary key flags - raw bytes from the changeset/patchset.
116    ///
117    /// Each byte represents the 1-based ordinal position in the composite PK,
118    /// or 0 if the column is not part of the primary key.
119    /// For example, `[1, 0, 2]` means column 0 is the first PK column,
120    /// column 1 is not a PK column, and column 2 is the second PK column.
121    pk_flags: Vec<u8>,
122}
123
124impl<S> TableSchema<S> {
125    /// Create a new parsed table schema.
126    ///
127    /// # Panics
128    ///
129    /// Panics if `pk_flags` is not one byte per column, or if its nonzero bytes
130    /// are not the dense key ordinals `1..=n`. Byte input reaching the same
131    /// invariant through the parser is refused with
132    /// [`ParseError::InvalidPrimaryKeyFlags`] instead.
133    #[inline]
134    #[must_use]
135    pub fn new(name: S, column_count: usize, pk_flags: Vec<u8>) -> Self {
136        assert_eq!(pk_flags.len(), column_count);
137        assert!(
138            pk_flags_are_dense_ordinals(&pk_flags),
139            "pk_flags must hold the dense key ordinals 1..=n"
140        );
141        Self {
142            name,
143            column_count,
144            pk_flags,
145        }
146    }
147
148    /// Returns the name of the table.
149    #[inline]
150    #[must_use]
151    pub fn name(&self) -> &S {
152        &self.name
153    }
154
155    /// Returns the raw primary-key flags. Each byte at index `i`
156    /// represents column `i`: `0` means the column is not part of the
157    /// primary key, and a non-zero value `k` means it is the `k`-th
158    /// column in the composite primary key.
159    #[inline]
160    #[must_use]
161    pub fn pk_flags(&self) -> &[u8] {
162        &self.pk_flags
163    }
164}
165
166impl<S: AsRef<str> + Clone + Eq + core::fmt::Debug> DynTable for TableSchema<S> {
167    #[inline]
168    fn name(&self) -> &str {
169        self.name.as_ref()
170    }
171
172    #[inline]
173    fn number_of_columns(&self) -> usize {
174        self.column_count
175    }
176
177    #[inline]
178    fn write_pk_flags(&self, buf: &mut [u8]) {
179        assert_eq!(buf.len(), self.column_count);
180        buf.copy_from_slice(&self.pk_flags);
181    }
182}
183
184impl<N: AsRef<str> + Clone + core::hash::Hash + Eq + core::fmt::Debug> SchemaWithPK
185    for TableSchema<N>
186{
187    fn number_of_primary_keys(&self) -> usize {
188        self.pk_flags.iter().filter(|&&b| b > 0).count()
189    }
190
191    fn primary_key_index(&self, col_idx: usize) -> Option<usize> {
192        self.pk_flags.get(col_idx).and_then(|&pk_ordinal| {
193            if pk_ordinal > 0 {
194                Some(usize::from(pk_ordinal - 1))
195            } else {
196                None
197            }
198        })
199    }
200
201    fn extract_pk<S, B>(
202        &self,
203        values: &impl IndexableValues<Text = S, Binary = B>,
204    ) -> alloc::vec::Vec<Value<S, B>>
205    where
206        S: Clone,
207        B: Clone,
208    {
209        self.primary_key_columns()
210            .map(|i| {
211                values
212                    .get(i)
213                    .expect("primary key column index out of bounds, values shorter than schema")
214            })
215            .collect()
216    }
217}
218
219/// A parsed changeset or patchset.
220///
221/// This represents a frozen (immutable) diffset produced by the binary parser.
222/// To modify it, convert it to a [`DiffSetBuilder`] using `Into::into`.
223#[derive(Debug, Clone, Eq)]
224pub enum ParsedDiffSet {
225    /// A parsed changeset.
226    Changeset(DiffSet<ChangesetFormat, TableSchema<String>, String, Vec<u8>>),
227    /// A parsed patchset.
228    Patchset(DiffSet<PatchsetFormat, TableSchema<String>, String, Vec<u8>>),
229}
230
231impl PartialEq for ParsedDiffSet {
232    fn eq(&self, other: &Self) -> bool {
233        let self_empty = match self {
234            ParsedDiffSet::Changeset(d) => d.is_empty(),
235            ParsedDiffSet::Patchset(d) => d.is_empty(),
236        };
237        let other_empty = match other {
238            ParsedDiffSet::Changeset(d) => d.is_empty(),
239            ParsedDiffSet::Patchset(d) => d.is_empty(),
240        };
241
242        if self_empty && other_empty {
243            return true;
244        }
245
246        // Otherwise compare by variant and content
247        match (self, other) {
248            (ParsedDiffSet::Changeset(a), ParsedDiffSet::Changeset(b)) => a == b,
249            (ParsedDiffSet::Patchset(a), ParsedDiffSet::Patchset(b)) => a == b,
250            _ => false,
251        }
252    }
253}
254
255impl TryFrom<&[u8]> for ParsedDiffSet {
256    type Error = ParseError;
257
258    fn try_from(data: &[u8]) -> Result<Self, Self::Error> {
259        Self::parse(data)
260    }
261}
262
263impl From<ParsedDiffSet> for Vec<u8> {
264    fn from(diffset: ParsedDiffSet) -> Self {
265        match diffset {
266            ParsedDiffSet::Changeset(d) => d.into(),
267            ParsedDiffSet::Patchset(d) => d.into(),
268        }
269    }
270}
271
272impl ParsedDiffSet {
273    /// Parse binary data into a frozen [`DiffSet`].
274    ///
275    /// The format (changeset vs patchset) is determined by the first table marker.
276    ///
277    /// # Errors
278    ///
279    /// Returns a `ParseError` if the data is malformed or contains invalid values.
280    pub fn parse(data: &[u8]) -> Result<Self, ParseError> {
281        if data.is_empty() {
282            // Empty data defaults to changeset
283            return Ok(ParsedDiffSet::Changeset(DiffSet::default()));
284        }
285
286        // Peek at the first byte to determine format
287        match data[0] {
288            markers::CHANGESET => {
289                let diffset = parse_as_changeset(data)?;
290                Ok(ParsedDiffSet::Changeset(diffset))
291            }
292            markers::PATCHSET => {
293                let diffset = parse_as_patchset(data)?;
294                Ok(ParsedDiffSet::Patchset(diffset))
295            }
296            b => Err(ParseError::InvalidTableMarker(b, 0)),
297        }
298    }
299
300    /// Returns true if this is a changeset.
301    #[must_use]
302    pub fn is_changeset(&self) -> bool {
303        matches!(self, ParsedDiffSet::Changeset(_))
304    }
305
306    /// Returns true if this is a patchset.
307    #[must_use]
308    pub fn is_patchset(&self) -> bool {
309        matches!(self, ParsedDiffSet::Patchset(_))
310    }
311
312    /// Returns the table schemas for all tables with non-empty operations.
313    #[must_use]
314    pub fn table_schemas(&self) -> Vec<&TableSchema<String>> {
315        match self {
316            ParsedDiffSet::Changeset(d) => d
317                .tables
318                .iter()
319                .filter(|(_, ops)| !ops.is_empty())
320                .map(|(schema, _)| schema)
321                .collect(),
322            ParsedDiffSet::Patchset(d) => d
323                .tables
324                .iter()
325                .filter(|(_, ops)| !ops.is_empty())
326                .map(|(schema, _)| schema)
327                .collect(),
328        }
329    }
330
331    /// Rename table sections in place.
332    ///
333    /// The callback receives each section name and returns a new name, or
334    /// `None` to leave it unchanged. Returns the number of sections renamed.
335    /// Only the name changes. Columns, primary-key flags, and operations are
336    /// untouched, and two sections mapped to the same name stay separate.
337    pub fn rename_tables<F>(&mut self, mut rename: F) -> usize
338    where
339        F: FnMut(&str) -> Option<String>,
340    {
341        fn rename_in<Fmt, F>(tables: &mut [(TableSchema<String>, Fmt)], rename: &mut F) -> usize
342        where
343            F: FnMut(&str) -> Option<String>,
344        {
345            let mut renamed = 0;
346            for (schema, _) in tables.iter_mut() {
347                if let Some(new_name) = rename(schema.name.as_str()) {
348                    schema.name = new_name;
349                    renamed += 1;
350                }
351            }
352            renamed
353        }
354
355        match self {
356            ParsedDiffSet::Changeset(d) => rename_in(&mut d.tables, &mut rename),
357            ParsedDiffSet::Patchset(d) => rename_in(&mut d.tables, &mut rename),
358        }
359    }
360}
361
362/// Parse binary data as a changeset.
363///
364/// # Errors
365///
366/// Returns a `ParseError` if the data is malformed or not a valid changeset.
367fn parse_as_changeset(
368    data: &[u8],
369) -> Result<DiffSet<ChangesetFormat, TableSchema<String>, String, Vec<u8>>, ParseError> {
370    let mut builder: DiffSetBuilder<ChangesetFormat, 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::Changeset {
377            return Err(ParseError::MixedFormats {
378                expected: FormatMarker::Changeset,
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_changeset_operation(&data[pos..], pos, &schema, &mut builder)?;
391            pos += op_len;
392        }
393    }
394
395    Ok(builder.into())
396}
397
398/// Parse binary data as a patchset.
399///
400/// # Errors
401///
402/// Returns a `ParseError` if the data is malformed or not a valid patchset.
403fn parse_as_patchset(
404    data: &[u8],
405) -> Result<DiffSet<PatchsetFormat, TableSchema<String>, String, Vec<u8>>, ParseError> {
406    let mut builder: DiffSetBuilder<PatchsetFormat, TableSchema<String>, String, Vec<u8>> =
407        DiffSetBuilder::new();
408    let mut pos = 0;
409
410    while pos < data.len() {
411        let (schema, format, header_len) = parse_table_header(&data[pos..], pos)?;
412        if format != FormatMarker::Patchset {
413            return Err(ParseError::MixedFormats {
414                expected: FormatMarker::Patchset,
415                found: format,
416                position: pos,
417            });
418        }
419        pos += header_len;
420
421        while pos < data.len() {
422            let byte = data[pos];
423            if byte == markers::CHANGESET || byte == markers::PATCHSET {
424                break;
425            }
426            let op_len = parse_patchset_operation(&data[pos..], pos, &schema, &mut builder)?;
427            pos += op_len;
428        }
429    }
430
431    Ok(builder.into())
432}
433
434/// Parse a table header and return the schema.
435fn parse_table_header(
436    data: &[u8],
437    base_pos: usize,
438) -> Result<(TableSchema<String>, FormatMarker, usize), ParseError> {
439    let mut pos = 0;
440
441    if data.is_empty() {
442        return Err(ParseError::UnexpectedEof(base_pos));
443    }
444    let format = match data[pos] {
445        markers::CHANGESET => FormatMarker::Changeset,
446        markers::PATCHSET => FormatMarker::Patchset,
447        b => return Err(ParseError::InvalidTableMarker(b, base_pos + pos)),
448    };
449    pos += 1;
450
451    let (column_count, varint_len) = decode_varint(&data[pos..])
452        .ok_or(ParseError::UnexpectedEof(base_pos + pos))
453        .and_then(|(count, len)| {
454            usize::try_from(count)
455                .map(|count| (count, len))
456                .map_err(|_| ParseError::UnexpectedEof(base_pos + pos))
457        })?;
458    pos += varint_len;
459
460    if pos + column_count > data.len() {
461        return Err(ParseError::UnexpectedEof(base_pos + pos));
462    }
463    let pk_flags_pos = base_pos + pos;
464    let pk_flags: Vec<u8> = data[pos..pos + column_count].to_vec();
465    pos += column_count;
466
467    let name_start = pos;
468    while pos < data.len() && data[pos] != 0 {
469        pos += 1;
470    }
471    if pos >= data.len() {
472        return Err(ParseError::UnterminatedTableName);
473    }
474    let name = String::from_utf8(data[name_start..pos].to_vec())
475        .map_err(|_| ParseError::InvalidTableName(base_pos + name_start))?;
476    pos += 1;
477
478    if !pk_flags_are_dense_ordinals(&pk_flags) {
479        return Err(ParseError::InvalidPrimaryKeyFlags {
480            table_name: name,
481            position: pk_flags_pos,
482        });
483    }
484
485    Ok((TableSchema::new(name, column_count, pk_flags), format, pos))
486}
487
488/// Whether the nonzero flag bytes are the dense key ordinals `1..=n`.
489///
490/// All-zero flags describe a table with no primary key and are accepted.
491fn pk_flags_are_dense_ordinals(flags: &[u8]) -> bool {
492    let key_count = flags.iter().filter(|&&flag| flag != 0).count();
493    if key_count > usize::from(u8::MAX) {
494        return false;
495    }
496    let mut seen: [u64; 4] = [0; 4];
497    for &flag in flags {
498        if flag == 0 {
499            continue;
500        }
501        let ordinal = usize::from(flag);
502        if ordinal > key_count {
503            return false;
504        }
505        let mask = 1u64 << (ordinal % 64);
506        if seen[ordinal / 64] & mask != 0 {
507            return false;
508        }
509        seen[ordinal / 64] |= mask;
510    }
511    true
512}
513
514/// Parse operation header (`op_code` + indirect flag).
515///
516/// Returns `(op_code, indirect, bytes_consumed)`. Any non-zero indirect byte
517/// parses as `true` to match SQLite's permissive treatment of the flag.
518fn parse_operation_header(data: &[u8], base_pos: usize) -> Result<(u8, bool, usize), ParseError> {
519    if data.len() < 2 {
520        return Err(ParseError::UnexpectedEof(base_pos));
521    }
522    Ok((data[0], data[1] != 0, 2))
523}
524
525/// Parse a changeset operation.
526fn parse_changeset_operation(
527    data: &[u8],
528    base_pos: usize,
529    schema: &TableSchema<String>,
530    builder: &mut DiffSetBuilder<ChangesetFormat, TableSchema<String>, String, Vec<u8>>,
531) -> Result<usize, ParseError> {
532    let (op_code, indirect, mut pos) = parse_operation_header(data, base_pos)?;
533
534    match op_code {
535        op_codes::INSERT => {
536            let (values, len) = parse_values(&data[pos..], base_pos + pos, schema.column_count)?;
537            pos += len;
538            let values: Vec<Value<String, Vec<u8>>> = values
539                .into_iter()
540                .map(|v| v.unwrap_or(Value::Null))
541                .collect();
542            let pk = schema.extract_pk(&values);
543            builder.add_operation(schema, pk, Operation::Insert { values, indirect });
544        }
545        op_codes::DELETE => {
546            let (values, len) = parse_values(&data[pos..], base_pos + pos, schema.column_count)?;
547            pos += len;
548            let values: Vec<Value<String, Vec<u8>>> = values
549                .into_iter()
550                .map(|v| v.unwrap_or(Value::Null))
551                .collect();
552            let pk = schema.extract_pk(&values);
553            builder.add_operation(
554                schema,
555                pk,
556                Operation::Delete {
557                    data: values,
558                    indirect,
559                },
560            );
561        }
562        op_codes::UPDATE => {
563            let (old_values, old_len) =
564                parse_values(&data[pos..], base_pos + pos, schema.column_count)?;
565            pos += old_len;
566            let (new_values, new_len) =
567                parse_values(&data[pos..], base_pos + pos, schema.column_count)?;
568            pos += new_len;
569            // Extract PK using old values (convert None to Null)
570            let pk_values: Vec<Value<String, Vec<u8>>> = old_values
571                .iter()
572                .map(|v| v.clone().unwrap_or(Value::Null))
573                .collect();
574            let pk = schema.extract_pk(&pk_values);
575            let values: UpdateValues = old_values.into_iter().zip(new_values).collect();
576            builder.add_operation(schema, pk, Operation::Update { values, indirect });
577        }
578        _ => return Err(ParseError::InvalidOpCode(op_code, base_pos)),
579    }
580
581    Ok(pos)
582}
583
584/// Parse a patchset operation.
585fn parse_patchset_operation(
586    data: &[u8],
587    base_pos: usize,
588    schema: &TableSchema<String>,
589    builder: &mut DiffSetBuilder<PatchsetFormat, TableSchema<String>, String, Vec<u8>>,
590) -> Result<usize, ParseError> {
591    let (op_code, indirect, mut pos) = parse_operation_header(data, base_pos)?;
592
593    match op_code {
594        op_codes::INSERT => {
595            let (values, len) = parse_values(&data[pos..], base_pos + pos, schema.column_count)?;
596            pos += len;
597            let values: Vec<Value<String, Vec<u8>>> = values
598                .into_iter()
599                .map(|v| v.unwrap_or(Value::Null))
600                .collect();
601            let pk = schema.extract_pk(&values);
602            builder.add_operation(schema, pk, Operation::Insert { values, indirect });
603        }
604        op_codes::DELETE => {
605            // Patchset DELETE: only PK values in column order
606            let pk_count = schema.pk_flags.iter().filter(|&&b| b > 0).count();
607            let (pk_values, len) = parse_values(&data[pos..], base_pos + pos, pk_count)?;
608            pos += len;
609            // Expand PK values to full row, then extract_pk to get ordinal-sorted PK.
610            // This is needed because the binary format stores PKs in column order,
611            // but the builder stores them sorted by pk_ordinal (matching the serializer).
612            let full_values = expand_pk_values(&schema.pk_flags, pk_values, schema.column_count);
613            // Convert MaybeValue to Value for extract_pk (PK values should always be defined)
614            let full_values_concrete: Vec<Value<String, Vec<u8>>> = full_values
615                .into_iter()
616                .map(|v| v.unwrap_or(Value::Null))
617                .collect();
618            let pk = schema.extract_pk(&full_values_concrete);
619            builder.add_operation(schema, pk, Operation::Delete { data: (), indirect });
620        }
621        op_codes::UPDATE => {
622            // Patchset UPDATE wire layout, matching SQLite's session extension: one record
623            // of exactly `column_count` entries in column order. A primary key column
624            // carries its value, any other column carries its new value or `0x00` when it
625            // did not change. It is NOT a primary key block followed by a non-primary key
626            // block: those only look alike when the primary key is the first column, which
627            // is what hid this for so long.
628            //
629            // The full-width `Vec<((), MaybeValue)>` keeps downstream code (`extract_pk`,
630            // `sql_output`, consolidation, reversal) uniform: primary key slots hold
631            // `Some(value)`, other slots hold `Some(new_value)` or `None` for undefined.
632            let (record, record_len) =
633                parse_values(&data[pos..], base_pos + pos, schema.column_count)?;
634            pos += record_len;
635
636            let mut values: Vec<((), MaybeValue<String, Vec<u8>>)> =
637                alloc::vec![((), None); schema.column_count];
638            for (col_idx, (&pk_flag, entry)) in schema.pk_flags.iter().zip(record).enumerate() {
639                if pk_flag > 0 {
640                    // A primary key column always carries a defined value. A stray
641                    // undefined marker is normalised to Null to stay lenient for
642                    // fuzz-generated input, matching `expand_pk_values` in the DELETE path.
643                    values[col_idx] = ((), Some(entry.unwrap_or(Value::Null)));
644                } else {
645                    values[col_idx] = ((), entry);
646                }
647            }
648
649            let pk = schema.extract_pk(&values);
650            builder.add_operation(schema, pk, Operation::Update { values, indirect });
651        }
652        _ => return Err(ParseError::InvalidOpCode(op_code, base_pos)),
653    }
654
655    Ok(pos)
656}
657
658/// Expand PK-only values to full row with None (undefined) for non-PK columns.
659///
660/// The `pk_flags` are raw bytes where non-zero means the column is part of the PK.
661/// PK values are expected in the order they appear in `pk_flags` (not sorted by ordinal).
662fn expand_pk_values(
663    pk_flags: &[u8],
664    pk_values: Vec<MaybeValue<String, Vec<u8>>>,
665    column_count: usize,
666) -> Vec<MaybeValue<String, Vec<u8>>> {
667    let mut full: Vec<MaybeValue<String, Vec<u8>>> = vec![None; column_count];
668    let mut pk_iter = pk_values.into_iter();
669    for (i, &pk_ordinal) in pk_flags.iter().enumerate() {
670        if pk_ordinal > 0
671            && let Some(v) = pk_iter.next()
672        {
673            full[i] = v;
674        }
675    }
676    full
677}
678
679/// Parse a sequence of values.
680fn parse_values(data: &[u8], base_pos: usize, count: usize) -> Result<ParsedValues, ParseError> {
681    let mut values = Vec::with_capacity(count);
682    let mut pos = 0;
683
684    for _ in 0..count {
685        let (value, value_len) =
686            decode_value(&data[pos..]).ok_or(ParseError::InvalidValue(base_pos + pos))?;
687        values.push(value);
688        pos += value_len;
689    }
690
691    Ok((values, pos))
692}
693
694#[cfg(test)]
695mod tests {
696    use super::*;
697    use crate::SimpleTable;
698    use alloc::vec;
699
700    #[test]
701    fn test_parse_empty() {
702        let result = ParsedDiffSet::parse(&[]);
703        assert!(result.is_ok());
704        assert!(result.unwrap().is_changeset());
705    }
706
707    #[test]
708    fn test_parse_table_header() {
709        // 'T', 2 columns, pk_flags [1, 0], table name "t\0"
710        let data = [b'T', 2, 1, 0, b't', 0];
711        let (schema, format, len) = parse_table_header(&data, 0).unwrap();
712
713        assert_eq!(format, FormatMarker::Changeset);
714        assert_eq!(schema.column_count, 2);
715        assert_eq!(schema.pk_flags, vec![1, 0]); // Raw bytes: 1 = first PK column, 0 = not PK
716        assert_eq!(schema.name, "t");
717        assert_eq!(len, 6);
718    }
719
720    #[test]
721    fn test_parse_insert_changeset() {
722        // Table header + INSERT with integer 1 and text "a"
723        let mut data = vec![b'T', 2, 1, 0, b't', 0];
724        // INSERT opcode, indirect=0
725        data.push(op_codes::INSERT);
726        data.push(0);
727        // Integer 1 (type 1, 8 bytes)
728        data.push(0x01);
729        data.extend(&1i64.to_be_bytes());
730        // Text "a" (type 3, length 1, "a")
731        data.push(0x03);
732        data.push(1);
733        data.push(b'a');
734
735        let parsed = ParsedDiffSet::parse(&data).unwrap();
736        assert!(parsed.is_changeset());
737    }
738
739    #[test]
740    fn test_parse_delete_changeset() {
741        let mut data = vec![b'T', 2, 1, 0, b't', 0];
742        data.push(op_codes::DELETE);
743        data.push(0);
744        // Integer 1
745        data.push(0x01);
746        data.extend(&1i64.to_be_bytes());
747        // Text "a"
748        data.push(0x03);
749        data.push(1);
750        data.push(b'a');
751
752        let parsed = ParsedDiffSet::parse(&data).unwrap();
753        assert!(parsed.is_changeset());
754    }
755
756    #[test]
757    fn test_parse_delete_patchset() {
758        // Patchset DELETE only has PK values
759        let mut data = vec![b'P', 2, 1, 0, b't', 0];
760        data.push(op_codes::DELETE);
761        data.push(0);
762        // Only PK value (integer 1)
763        data.push(0x01);
764        data.extend(&1i64.to_be_bytes());
765
766        let parsed = ParsedDiffSet::parse(&data).unwrap();
767        assert!(parsed.is_patchset());
768    }
769
770    #[test]
771    fn test_parse_update_changeset() {
772        let mut data = vec![b'T', 2, 1, 0, b't', 0];
773        data.push(op_codes::UPDATE);
774        data.push(0);
775        // Old values: integer 1, text "a"
776        data.push(0x01);
777        data.extend(&1i64.to_be_bytes());
778        data.push(0x03);
779        data.push(1);
780        data.push(b'a');
781        // New values: integer 1, text "b"
782        data.push(0x01);
783        data.extend(&1i64.to_be_bytes());
784        data.push(0x03);
785        data.push(1);
786        data.push(b'b');
787
788        let parsed = ParsedDiffSet::parse(&data).unwrap();
789        assert!(parsed.is_changeset());
790    }
791
792    #[test]
793    fn test_is_changeset() {
794        let data = vec![b'T', 1, 1, b't', 0];
795        let parsed = ParsedDiffSet::parse(&data).unwrap();
796        assert!(parsed.is_changeset());
797        assert!(!parsed.is_patchset());
798    }
799
800    #[test]
801    fn test_is_patchset() {
802        let data = vec![b'P', 1, 1, b't', 0];
803        let parsed = ParsedDiffSet::parse(&data).unwrap();
804        assert!(parsed.is_patchset());
805        assert!(!parsed.is_changeset());
806    }
807
808    #[test]
809    fn test_parsed_table_schema_dyn_table() {
810        let schema: TableSchema<String> = TableSchema::new("users".into(), 3, vec![1, 0, 0]);
811        assert_eq!(schema.name(), "users");
812        assert_eq!(schema.number_of_columns(), 3);
813
814        let mut buf = [0u8; 3];
815        schema.write_pk_flags(&mut buf);
816        assert_eq!(buf, [1, 0, 0]);
817    }
818
819    #[test]
820    fn test_parsed_table_schema_extract_pk() {
821        let schema: TableSchema<String> = TableSchema::new("users".into(), 3, vec![1, 0, 2]);
822        let values: Vec<Value<String, Vec<u8>>> = vec![
823            Value::Integer(1),
824            Value::Text("alice".into()),
825            Value::Integer(100),
826        ];
827        let pk = schema.extract_pk(&values);
828        let expected: Vec<Value<String, Vec<u8>>> = vec![Value::Integer(1), Value::Integer(100)];
829        assert_eq!(pk, expected);
830    }
831
832    // ---- Error path tests ----
833
834    #[test]
835    fn test_parse_invalid_table_marker() {
836        let data = [0xFFu8, 1, 1, b't', 0];
837        let err = ParsedDiffSet::parse(&data).unwrap_err();
838        assert!(
839            matches!(err, ParseError::InvalidTableMarker(0xFF, 0)),
840            "got {err:?}"
841        );
842    }
843
844    #[test]
845    fn test_parse_unexpected_eof_in_table_header() {
846        // 'T' marker but no column count
847        let data = *b"T";
848        let err = ParsedDiffSet::parse(&data).unwrap_err();
849        assert!(matches!(err, ParseError::UnexpectedEof(_)), "got {err:?}");
850    }
851
852    #[test]
853    fn test_parse_unexpected_eof_in_pk_flags() {
854        // 'T', column count 3, but only 1 PK flag byte
855        let data = [b'T', 3, 1];
856        let err = ParsedDiffSet::parse(&data).unwrap_err();
857        assert!(matches!(err, ParseError::UnexpectedEof(_)), "got {err:?}");
858    }
859
860    #[test]
861    fn test_parse_unterminated_table_name() {
862        // 'T', 1 column, pk_flags [1], then "abc" with no null terminator
863        let data = [b'T', 1, 1, b'a', b'b', b'c'];
864        let err = ParsedDiffSet::parse(&data).unwrap_err();
865        assert!(
866            matches!(err, ParseError::UnterminatedTableName),
867            "got {err:?}"
868        );
869    }
870
871    #[test]
872    fn test_parse_invalid_utf8_in_table_name() {
873        // 'T', 1 column, pk_flags [1], then 0xFF (invalid UTF-8), then null
874        let data = [b'T', 1, 1, 0xFF, 0];
875        let err = ParsedDiffSet::parse(&data).unwrap_err();
876        assert!(
877            matches!(err, ParseError::InvalidTableName(_)),
878            "got {err:?}"
879        );
880    }
881
882    #[test]
883    fn test_parse_mixed_formats_changeset_then_patchset() {
884        // First table 'T' (changeset), then second table 'P' (patchset)
885        let mut data = vec![b'T', 1, 1, b'a', 0];
886        // Now a 'P' table header without preceding operations
887        data.extend_from_slice(&[b'P', 1, 1, b'b', 0]);
888        let err = ParsedDiffSet::parse(&data).unwrap_err();
889        assert!(
890            matches!(
891                err,
892                ParseError::MixedFormats {
893                    expected: FormatMarker::Changeset,
894                    found: FormatMarker::Patchset,
895                    ..
896                }
897            ),
898            "got {err:?}"
899        );
900    }
901
902    #[test]
903    fn test_parse_mixed_formats_patchset_then_changeset() {
904        let mut data = vec![b'P', 1, 1, b'a', 0];
905        data.extend_from_slice(&[b'T', 1, 1, b'b', 0]);
906        let err = ParsedDiffSet::parse(&data).unwrap_err();
907        assert!(
908            matches!(
909                err,
910                ParseError::MixedFormats {
911                    expected: FormatMarker::Patchset,
912                    found: FormatMarker::Changeset,
913                    ..
914                }
915            ),
916            "got {err:?}"
917        );
918    }
919
920    /// Build the operation header bytes followed by a single integer payload.
921    fn make_insert_with_indirect(indirect_byte: u8) -> Vec<u8> {
922        let mut data = vec![b'T', 1, 1, b't', 0];
923        data.push(op_codes::INSERT);
924        data.push(indirect_byte);
925        // Integer 1
926        data.push(0x01);
927        data.extend(&1i64.to_be_bytes());
928        data
929    }
930
931    fn first_op_indirect_changeset(data: &[u8]) -> bool {
932        let parsed = ParsedDiffSet::parse(data).unwrap();
933        let ParsedDiffSet::Changeset(set) = parsed else {
934            panic!("expected Changeset");
935        };
936        set.tables
937            .iter()
938            .find_map(|(_schema, rows)| rows.first().map(|(_, op)| op.indirect()))
939            .expect("expected at least one op")
940    }
941
942    #[test]
943    fn test_parse_changeset_indirect_flag_set() {
944        let data = make_insert_with_indirect(1);
945        assert!(first_op_indirect_changeset(&data));
946    }
947
948    #[test]
949    fn test_parse_changeset_indirect_flag_clear() {
950        let data = make_insert_with_indirect(0);
951        assert!(!first_op_indirect_changeset(&data));
952    }
953
954    #[test]
955    fn test_parse_indirect_nonzero_treated_as_true() {
956        // Any non-zero byte must parse as indirect = true.
957        let data = make_insert_with_indirect(0x42);
958        assert!(first_op_indirect_changeset(&data));
959    }
960
961    #[test]
962    fn test_parsed_diffset_variant_mismatch_partial_eq() {
963        let changeset = ParsedDiffSet::parse(&[b'T', 1, 1, b't', 0]).unwrap();
964        let patchset = ParsedDiffSet::parse(&[b'P', 1, 1, b't', 0]).unwrap();
965        // Both are empty so PartialEq short-circuits to true. Add a real op
966        // to each so the variant-mismatch arm in the `match` is reached.
967        let mut full_changeset = vec![b'T', 1, 1, b't', 0];
968        full_changeset.push(op_codes::INSERT);
969        full_changeset.push(0);
970        full_changeset.push(0x01);
971        full_changeset.extend(&1i64.to_be_bytes());
972        let cs = ParsedDiffSet::parse(&full_changeset).unwrap();
973
974        let mut full_patchset = vec![b'P', 1, 1, b't', 0];
975        full_patchset.push(op_codes::INSERT);
976        full_patchset.push(0);
977        full_patchset.push(0x01);
978        full_patchset.extend(&1i64.to_be_bytes());
979        let ps = ParsedDiffSet::parse(&full_patchset).unwrap();
980
981        assert_ne!(cs, ps);
982        // Empty/empty still equal regardless of variant.
983        assert_eq!(changeset, patchset);
984    }
985
986    #[test]
987    fn test_parse_unexpected_eof_in_operation_header() {
988        // Valid changeset header followed by a single byte (op_code only,
989        // no indirect byte) — parse_operation_header must return UnexpectedEof.
990        let data = [b'T', 1, 1, b't', 0, op_codes::INSERT];
991        let err = ParsedDiffSet::parse(&data).unwrap_err();
992        assert!(matches!(err, ParseError::UnexpectedEof(_)), "got {err:?}");
993    }
994
995    #[test]
996    fn test_parse_patchset_indirect_flag_set() {
997        // Patchset INSERT carries full row values, same header layout.
998        let mut data = vec![b'P', 1, 1, b't', 0];
999        data.push(op_codes::INSERT);
1000        data.push(1);
1001        data.push(0x01);
1002        data.extend(&1i64.to_be_bytes());
1003
1004        let parsed = ParsedDiffSet::parse(&data).unwrap();
1005        let ParsedDiffSet::Patchset(set) = parsed else {
1006            panic!("expected Patchset");
1007        };
1008        let indirect = set
1009            .tables
1010            .iter()
1011            .find_map(|(_schema, rows)| rows.first().map(|(_, op)| op.indirect()))
1012            .expect("expected at least one op");
1013        assert!(indirect);
1014    }
1015
1016    /// Assert a real SQLite patchset UPDATE byte string parses to a single
1017    /// UPDATE operation, run caller-supplied checks against the destructured
1018    /// state, and confirm the parsed value re-serializes byte-identically.
1019    ///
1020    /// Centralizing the destructures here keeps the individual scenario tests
1021    /// focused on their assertions and folds the defensive `panic!` arms into
1022    /// one place. Each test below feeds bytes captured from a real
1023    /// `Session::patchset_strm` call, so the checker sees the exact wire
1024    /// layout the parser now targets.
1025    fn assert_patchset_update_roundtrip(
1026        data: &[u8],
1027        check: impl FnOnce(
1028            &TableSchema<String>,
1029            &[Value<String, Vec<u8>>],
1030            &[((), MaybeValue<String, Vec<u8>>)],
1031            bool,
1032        ),
1033    ) {
1034        let parsed = ParsedDiffSet::parse(data).expect("SQLite patchset UPDATE must parse");
1035        let ParsedDiffSet::Patchset(set) = parsed else {
1036            panic!("expected Patchset, got {parsed:?}");
1037        };
1038        let (schema, rows) = set.tables.first().expect("expected one table");
1039        assert_eq!(rows.len(), 1, "expected exactly one row");
1040        let (pk, op) = rows.first().expect("row map non-empty");
1041        let Operation::Update { values, indirect } = op else {
1042            panic!("expected Update, got {op:?}");
1043        };
1044        check(schema, pk.as_slice(), values.as_slice(), *indirect);
1045        let serialized: Vec<u8> = set.into();
1046        assert_eq!(serialized, data, "roundtrip must match SQLite output");
1047    }
1048
1049    /// Real SQLite session output for a standalone patchset UPDATE against a
1050    /// pre-existing row on a single-column PK table:
1051    ///
1052    /// ```text
1053    /// CREATE TABLE orders (id INTEGER PRIMARY KEY, amount INTEGER, status TEXT);
1054    /// INSERT INTO orders VALUES (5, 100, 'pending'); -- before session.attach()
1055    /// UPDATE orders SET status = 'shipped' WHERE id = 5; -- after attach, tracked
1056    /// ```
1057    ///
1058    /// Wire layout:
1059    /// - 12 bytes header ('P', 3, [1,0,0], "orders\0")
1060    /// - 2 bytes op header (UPDATE, indirect=0)
1061    /// - 9 bytes old side: INTEGER 5 (only the PK column)
1062    /// - 10 bytes new side: undefined (amount unchanged) + TEXT 'shipped'
1063    ///
1064    /// Total 33 bytes. Historically the parser expected `column_count` values on
1065    /// each side (padded with undefined) and returned `InvalidValue` mid-buffer.
1066    #[test]
1067    fn test_parse_patchset_update_sqlite_wire_layout_single_pk() {
1068        let data: [u8; 33] = [
1069            0x50, 0x03, 0x01, 0x00, 0x00, b'o', b'r', b'd', b'e', b'r', b's', 0x00, 0x17, 0x00,
1070            0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x07, b's', b'h',
1071            b'i', b'p', b'p', b'e', b'd',
1072        ];
1073        assert_patchset_update_roundtrip(&data, |schema, pk, values, indirect| {
1074            assert_eq!(schema.name, "orders");
1075            assert_eq!(schema.column_count, 3);
1076            assert_eq!(schema.pk_flags, vec![1, 0, 0]);
1077            assert_eq!(pk, &[Value::Integer(5)]);
1078            assert!(!indirect);
1079            assert_eq!(values.len(), 3);
1080            assert_eq!(values[0].1, Some(Value::Integer(5))); // PK preserved
1081            assert_eq!(values[1].1, None); // amount unchanged
1082            assert_eq!(values[2].1, Some(Value::Text("shipped".into())));
1083        });
1084    }
1085
1086    /// Real SQLite output for a composite PK, `PRIMARY KEY(a, b)`:
1087    ///
1088    /// ```text
1089    /// CREATE TABLE items (a INTEGER NOT NULL, b INTEGER NOT NULL, val TEXT, PRIMARY KEY(a, b));
1090    /// INSERT INTO items VALUES (1, 2, 'v1'); -- before attach
1091    /// UPDATE items SET val = 'v2' WHERE a = 1 AND b = 2;
1092    /// ```
1093    ///
1094    /// Wire layout: two PK values on the old side (INTEGER 1, INTEGER 2), one
1095    /// non-PK value on the new side (TEXT 'v2'). 35 bytes total.
1096    #[test]
1097    fn test_parse_patchset_update_sqlite_wire_layout_composite_pk() {
1098        let data: [u8; 35] = [
1099            0x50, 0x03, 0x01, 0x02, 0x00, b'i', b't', b'e', b'm', b's', 0x00, 0x17, 0x00, 0x01,
1100            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
1101            0x00, 0x00, 0x02, 0x03, 0x02, b'v', b'2',
1102        ];
1103        assert_patchset_update_roundtrip(&data, |schema, pk, values, _indirect| {
1104            assert_eq!(schema.name, "items");
1105            assert_eq!(schema.pk_flags, vec![1, 2, 0]);
1106            // `extract_pk` returns values sorted by PK ordinal. With PK(a, b) and
1107            // pk_flags [1, 2, 0], ordinal 1 is column `a`, ordinal 2 is column `b`.
1108            assert_eq!(pk, &[Value::Integer(1), Value::Integer(2)]);
1109            assert_eq!(values.len(), 3);
1110            assert_eq!(values[0].1, Some(Value::Integer(1))); // a (PK)
1111            assert_eq!(values[1].1, Some(Value::Integer(2))); // b (PK)
1112            assert_eq!(values[2].1, Some(Value::Text("v2".into())));
1113        });
1114    }
1115
1116    /// Every non-PK column is present on the new side, in column order, either
1117    /// as its new value or as the undefined marker `0x00` when unchanged.
1118    ///
1119    /// ```text
1120    /// UPDATE orders SET amount = 200, status = 'shipped' WHERE id = 5;
1121    /// ```
1122    ///
1123    /// Two non-PK columns changed, so both are defined values (no undefined
1124    /// markers). 41 bytes total.
1125    #[test]
1126    fn test_parse_patchset_update_all_non_pk_changed() {
1127        let data: [u8; 41] = [
1128            0x50, 0x03, 0x01, 0x00, 0x00, b'o', b'r', b'd', b'e', b'r', b's', 0x00, 0x17, 0x00,
1129            0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00,
1130            0x00, 0x00, 0x00, 0xc8, 0x03, 0x07, b's', b'h', b'i', b'p', b'p', b'e', b'd',
1131        ];
1132        assert_patchset_update_roundtrip(&data, |_schema, _pk, values, _indirect| {
1133            assert_eq!(values[0].1, Some(Value::Integer(5)));
1134            assert_eq!(values[1].1, Some(Value::Integer(200)));
1135            assert_eq!(values[2].1, Some(Value::Text("shipped".into())));
1136        });
1137    }
1138
1139    /// Assert a parsed [`TableSchema`] and a [`SimpleTable`] of the same shape
1140    /// agree on every [`SchemaWithPK`] accessor, so the read and build sides
1141    /// are symmetric.
1142    fn assert_schema_pk_parity(
1143        parsed: &TableSchema<String>,
1144        simple: &SimpleTable,
1145        row: &[Value<String, Vec<u8>>],
1146    ) {
1147        assert_eq!(
1148            parsed.number_of_primary_keys(),
1149            simple.number_of_primary_keys(),
1150            "number_of_primary_keys",
1151        );
1152        for col in 0..simple.number_of_columns() {
1153            assert_eq!(
1154                parsed.primary_key_index(col),
1155                simple.primary_key_index(col),
1156                "primary_key_index at col {col}",
1157            );
1158        }
1159        assert_eq!(
1160            parsed.primary_key_columns().collect::<Vec<usize>>(),
1161            simple.primary_key_columns().collect::<Vec<usize>>(),
1162            "primary_key_columns",
1163        );
1164        assert_eq!(
1165            parsed.extract_pk(&row),
1166            simple.extract_pk(&row),
1167            "extract_pk"
1168        );
1169    }
1170
1171    #[test]
1172    fn test_parsed_schema_pk_parity_single_key() {
1173        // Changeset over `kv(id, val)` with single-column key `id` (flags [1, 0]).
1174        let mut data = vec![b'T', 2, 1, 0, b'k', b'v', 0];
1175        data.push(op_codes::INSERT);
1176        data.push(0);
1177        data.push(0x01);
1178        data.extend(&1i64.to_be_bytes());
1179        data.push(0x03);
1180        data.push(1);
1181        data.push(b'x');
1182
1183        let ParsedDiffSet::Changeset(set) = ParsedDiffSet::parse(&data).unwrap() else {
1184            panic!("expected changeset");
1185        };
1186        let (parsed, _rows) = set.tables.first().expect("one table");
1187        assert_eq!(parsed.pk_flags(), &[1, 0]);
1188
1189        let simple = SimpleTable::new("kv", &["id", "val"], &[0]);
1190        let row: Vec<Value<String, Vec<u8>>> = vec![Value::Integer(1), Value::Text("x".into())];
1191        assert_schema_pk_parity(parsed, &simple, &row);
1192        assert_eq!(parsed.primary_key_columns().collect::<Vec<usize>>(), [0]);
1193    }
1194
1195    #[test]
1196    fn test_parsed_schema_pk_parity_composite_reordered_key() {
1197        // Changeset over `abc(a, b, c)` whose key is `(b, a)`, so the key column
1198        // order differs from table order. Flags are [2, 1, 0].
1199        let mut data = vec![b'T', 3, 2, 1, 0, b'a', b'b', b'c', 0];
1200        data.push(op_codes::INSERT);
1201        data.push(0);
1202        data.push(0x01);
1203        data.extend(&10i64.to_be_bytes());
1204        data.push(0x01);
1205        data.extend(&20i64.to_be_bytes());
1206        data.push(0x03);
1207        data.push(1);
1208        data.push(b'z');
1209
1210        let ParsedDiffSet::Changeset(set) = ParsedDiffSet::parse(&data).unwrap() else {
1211            panic!("expected changeset");
1212        };
1213        let (parsed, _rows) = set.tables.first().expect("one table");
1214        assert_eq!(parsed.pk_flags(), &[2, 1, 0]);
1215
1216        let simple = SimpleTable::new("abc", &["a", "b", "c"], &[1, 0]);
1217        let row: Vec<Value<String, Vec<u8>>> = vec![
1218            Value::Integer(10),
1219            Value::Integer(20),
1220            Value::Text("z".into()),
1221        ];
1222        assert_schema_pk_parity(parsed, &simple, &row);
1223        // Key order is (b, a): column 1 first, column 0 second.
1224        assert_eq!(parsed.primary_key_columns().collect::<Vec<usize>>(), [1, 0]);
1225        // `extract_pk` follows key order: b's value, then a's value.
1226        assert_eq!(
1227            parsed.extract_pk(&row),
1228            vec![Value::Integer(20), Value::Integer(10)]
1229        );
1230    }
1231
1232    #[test]
1233    fn dense_ordinals_are_accepted_and_every_other_shape_refused() {
1234        for flags in [
1235            [0, 0, 0].as_slice(),
1236            [1, 0].as_slice(),
1237            [1, 2, 0].as_slice(),
1238            [2, 1, 0].as_slice(),
1239            [3, 2, 0, 1].as_slice(),
1240        ] {
1241            assert!(pk_flags_are_dense_ordinals(flags), "{flags:?} must parse");
1242        }
1243        for flags in [
1244            [2, 0].as_slice(),
1245            [3, 0].as_slice(),
1246            [1, 1, 0].as_slice(),
1247            [255, 64, 0].as_slice(),
1248            [15, 0, 63, 215, 61, 58, 56, 56, 50].as_slice(),
1249        ] {
1250            assert!(!pk_flags_are_dense_ordinals(flags), "{flags:?} must refuse");
1251        }
1252    }
1253
1254    #[test]
1255    fn header_with_dense_flags_parses() {
1256        assert!(ParsedDiffSet::parse(&[b'T', 3, 2, 1, 0, b't', 0]).is_ok());
1257    }
1258
1259    #[test]
1260    fn pk_flags_error_carries_table_name_and_position() {
1261        let data = [b'T', 2, 2, 0, b'm', b'y', b't', b'b', b'l', 0];
1262        let err = ParsedDiffSet::parse(&data).unwrap_err();
1263        let ParseError::InvalidPrimaryKeyFlags {
1264            table_name,
1265            position,
1266        } = err
1267        else {
1268            panic!("expected InvalidPrimaryKeyFlags, got {err:?}");
1269        };
1270        assert_eq!(table_name, "mytbl");
1271        assert_eq!(position, 2);
1272    }
1273
1274    #[test]
1275    #[should_panic(expected = "pk_flags must hold the dense key ordinals")]
1276    fn table_schema_new_refuses_flags_the_parser_would_refuse() {
1277        let _ = TableSchema::new("t", 3, vec![255, 64, 0]);
1278    }
1279}