Skip to main content

sqlite_diff_rs/builders/
change.rs

1//! `DiffSet` builder for constructing changeset/patchset binary data.
2//!
3//! [`DiffSetBuilder`] builds `SQLite` session-extension changesets and
4//! patchsets. It tracks row state and consolidates operations according to
5//! `SQLite`'s changegroup semantics.
6//!
7//! # Terminology
8//!
9//! A *changeset* carries full row data for all operations and is invertible.
10//! A *patchset* carries only the PK for deletes and only the changed columns
11//! for updates. *`DiffSet`* is the generic term for either.
12//!
13//! # Consolidation Rules
14//!
15//! When multiple operations affect the same row (by primary key), they are
16//! consolidated according to `SQLite`'s `sqlite3changegroup_add()` semantics:
17//!
18//! | Existing | New | Result |
19//! |----------|--------|--------|
20//! | INSERT | INSERT | Ignore new |
21//! | INSERT | UPDATE | INSERT with updated values |
22//! | INSERT | DELETE | Remove both (no-op) |
23//! | UPDATE | INSERT | Ignore new |
24//! | UPDATE | UPDATE | Single UPDATE original to final |
25//! | UPDATE | DELETE | DELETE of original |
26//! | DELETE | INSERT | UPDATE if different, no-op if same |
27//! | DELETE | UPDATE | Ignore new |
28//! | DELETE | DELETE | Ignore new |
29//!
30//! # Merging Changesets / Patchsets
31//!
32//! Two changesets or patchsets can be merged using the `|` (BitOr) operator,
33//! which is equivalent to SQLite's `sqlite3changeset_concat()`:
34//!
35//! ```ignore
36//! let combined = changeset_a | changeset_b;
37//! // or in-place:
38//! changeset_a |= changeset_b;
39//! ```
40//!
41//! Operations affecting the same row are consolidated using the rules above.
42
43use indexmap::IndexMap as IndexMapRaw;
44
45use alloc::vec;
46use alloc::vec::Vec;
47use core::fmt::Debug;
48use core::hash::Hash;
49use core::ops::{BitOr, BitOrAssign};
50
51use crate::{
52    SchemaWithPK,
53    builders::{
54        ChangeDelete, ChangesetFormat, ChangesetOp, Insert, Operation, PatchDelete, PatchsetFormat,
55        PatchsetOp, Update, format::Format,
56    },
57    encoding::{MaybeValue, Value, encode_defined_value, encode_value, markers, op_codes},
58};
59
60/// `IndexMap` alias using hashbrown's default hasher for `no_std` compatibility.
61type IndexMap<K, V> = IndexMapRaw<K, V, hashbrown::DefaultHashBuilder>;
62
63/// Type alias for the row map in a table.
64type RowMap<F, S, B> = IndexMap<Vec<Value<S, B>>, Operation<F, S, B>>;
65
66/// Type alias for the table map.
67type TableMap<F, T, S, B> = IndexMap<T, RowMap<F, S, B>>;
68
69/// Type alias for a vector of rows in a table.
70type RowVec<F, S, B> = Vec<(Vec<Value<S, B>>, Operation<F, S, B>)>;
71
72/// Type alias for a vector of tables with their rows.
73type TableVec<F, T, S, B> = Vec<(T, RowVec<F, S, B>)>;
74
75// ============================================================================
76// SQLite session extension hash simulation
77// ============================================================================
78
79/// The core hash-combine step used throughout `SQLite`'s session extension.
80///
81/// Matches the C macro: `#define HASH_APPEND(hash, add) ((hash) << 3) ^ (hash) ^ (unsigned int)(add)`
82const fn hash_append(h: u32, add: u32) -> u32 {
83    (h << 3) ^ h ^ add
84}
85
86/// Hash a 64-bit integer using `SQLite`'s `sessionHashAppendI64`.
87///
88/// Hashes the lower 32 bits first, then the upper 32 bits.
89#[allow(clippy::cast_sign_loss)]
90fn session_hash_append_i64(h: u32, i: i64) -> u32 {
91    let lo = (i as u64 & 0xFFFF_FFFF) as u32;
92    let hi = ((i as u64 >> 32) & 0xFFFF_FFFF) as u32;
93    let h = hash_append(h, lo);
94    hash_append(h, hi)
95}
96
97/// Hash a blob using `SQLite`'s `sessionHashAppendBlob`.
98///
99/// Applies `HASH_APPEND` to each byte.
100fn session_hash_append_blob(mut h: u32, data: &[u8]) -> u32 {
101    for &byte in data {
102        h = hash_append(h, u32::from(byte));
103    }
104    h
105}
106
107/// Hash a primary key using `SQLite`'s `sessionPreupdateHash` algorithm.
108///
109/// For each PK value: `h = HASH_APPEND(h, type_code)`, then hash the value.
110/// Type codes match `SQLite`: INTEGER=1, FLOAT=2, TEXT=3, BLOB=4.
111fn session_hash_pk<S: AsRef<str>, B: AsRef<[u8]>>(pk: &[Value<S, B>]) -> u32 {
112    let mut h: u32 = 0;
113    for value in pk {
114        match value {
115            Value::Integer(i) => {
116                h = hash_append(h, 1); // SQLITE_INTEGER
117                h = session_hash_append_i64(h, *i);
118            }
119            Value::Real(f) => {
120                h = hash_append(h, 2); // SQLITE_FLOAT
121                // SQLite does memcpy(&iVal, &rVal, 8) then hashes as i64
122                let i = i64::from_ne_bytes(f.to_ne_bytes());
123                h = session_hash_append_i64(h, i);
124            }
125            Value::Text(s) => {
126                h = hash_append(h, 3); // SQLITE_TEXT
127                h = session_hash_append_blob(h, s.as_ref().as_bytes());
128            }
129            Value::Blob(b) => {
130                h = hash_append(h, 4); // SQLITE_BLOB
131                h = session_hash_append_blob(h, b.as_ref());
132            }
133            Value::Null => {
134                // NULL PKs: SQLite skips hashing for these.
135                // In practice, PKs should never be NULL.
136            }
137        }
138    }
139    h
140}
141
142/// Simulate `SQLite`'s session extension hash table to determine row output order.
143///
144/// `SQLite`'s session extension tracks changes in a hash table where:
145/// - New entries are prepended to their bucket (most recent at list head)
146/// - The table starts at 256 buckets and doubles when entries >= buckets/2
147/// - Changeset iteration walks buckets 0..n-1, following each linked list
148///
149/// This function returns indices into `rows` in the order that `SQLite`'s
150/// changeset/patchset output would contain them.
151fn session_row_order<S: AsRef<str>, B: AsRef<[u8]>, V>(
152    rows: &IndexMap<Vec<Value<S, B>>, V>,
153) -> Vec<usize> {
154    let n = rows.len();
155    if n == 0 {
156        return Vec::new();
157    }
158
159    let pks: Vec<&Vec<Value<S, B>>> = rows.keys().collect();
160
161    // Simulate the hash table. We store each bucket as a Vec of entry indices
162    // in the REVERSE of SQLite's linked-list order (we push; SQLite prepends).
163    // We reverse each bucket during final iteration to recover SQLite's order.
164    let mut n_change: usize = 0;
165    let mut buckets: Vec<Vec<usize>> = Vec::new();
166
167    for idx in 0..n {
168        // Growth check (before each insert), matching SQLite's sessionGrowHash.
169        // SQLite: grows when nChange==0 or nEntry >= nChange/2.
170        // Here idx == current nEntry (entries 0..idx-1 already inserted).
171        if n_change == 0 || idx >= n_change / 2 {
172            let new_size = if n_change == 0 { 256 } else { n_change * 2 };
173            let mut new_buckets: Vec<Vec<usize>> = vec![Vec::new(); new_size];
174
175            // Rehash existing entries. In SQLite, old buckets are walked
176            // 0..old_nChange-1, and within each bucket entries are walked
177            // from head to tail (reverse of our Vec order), prepending to
178            // new buckets. We simulate by walking our Vecs in reverse
179            // (= SQLite's head-to-tail) and pushing (= SQLite's prepend
180            // into our reversed representation).
181            for old_bucket in &buckets {
182                for &entry_idx in old_bucket.iter().rev() {
183                    let h = session_hash_pk(pks[entry_idx]) as usize % new_size;
184                    new_buckets[h].push(entry_idx);
185                }
186            }
187
188            buckets = new_buckets;
189            n_change = new_size;
190        }
191
192        // Insert entry (push = prepend in our reversed representation)
193        let h = session_hash_pk(pks[idx]) as usize % n_change;
194        buckets[h].push(idx);
195    }
196
197    // Walk buckets in order. Reverse each bucket to recover SQLite's
198    // linked-list iteration order (head to tail).
199    let mut order = Vec::with_capacity(n);
200    for bucket in &buckets {
201        for &idx in bucket.iter().rev() {
202            order.push(idx);
203        }
204    }
205
206    order
207}
208
209// ============================================================================
210// Shared encoding helpers
211// ============================================================================
212
213/// Write a table header to the output buffer.
214///
215/// Format:
216/// - Table marker byte (`'T'` for changeset, `'P'` for patchset)
217/// - Column count (1 byte)
218/// - PK flags (1 byte per column: non-zero = PK ordinal, 0 = not PK)
219/// - Table name (null-terminated UTF-8)
220fn write_table_header<T: SchemaWithPK>(out: &mut Vec<u8>, marker: u8, table: &T) {
221    out.push(marker);
222
223    let num_cols = table.number_of_columns();
224    out.push(u8::try_from(num_cols).unwrap());
225
226    let pk_start = out.len();
227    out.resize(pk_start + num_cols, 0);
228    table.write_pk_flags(&mut out[pk_start..]);
229
230    out.extend(table.name().as_bytes());
231    out.push(0);
232}
233
234/// Build the column-index to PK-vector-position mapping used by patchset serialization.
235///
236/// Returns `(pk_flags, pk_col_to_pk_pos)` where `pk_col_to_pk_pos[col_idx]`
237/// gives the index into the PK vector for PK columns, or `None` for non-PK columns.
238fn patchset_pk_mapping<T: SchemaWithPK>(table: &T) -> (Vec<u8>, Vec<Option<usize>>) {
239    let num_cols = table.number_of_columns();
240    let mut pk_flags = alloc::vec![0u8; num_cols];
241    table.write_pk_flags(&mut pk_flags);
242
243    let mut pk_col_to_pk_pos: Vec<Option<usize>> = alloc::vec![None; num_cols];
244    let mut pk_cols: Vec<(usize, u8)> = pk_flags
245        .iter()
246        .enumerate()
247        .filter_map(|(i, &ord)| if ord > 0 { Some((i, ord)) } else { None })
248        .collect();
249    pk_cols.sort_by_key(|(_, ord)| *ord);
250    for (pos, (col_idx, _)) in pk_cols.into_iter().enumerate() {
251        pk_col_to_pk_pos[col_idx] = Some(pos);
252    }
253
254    (pk_flags, pk_col_to_pk_pos)
255}
256
257/// Encode patchset DELETE old values: PK columns get their values, non-PK columns are skipped.
258fn encode_patchset_delete_values<S: AsRef<str>, B: AsRef<[u8]>>(
259    out: &mut Vec<u8>,
260    pk_flags: &[u8],
261    pk_col_to_pk_pos: &[Option<usize>],
262    pk: &[Value<S, B>],
263) {
264    for (col_idx, &pk_flag) in pk_flags.iter().enumerate() {
265        if pk_flag > 0 {
266            if let Some(pk_pos) = pk_col_to_pk_pos[col_idx] {
267                encode_value(out, Some(&pk[pk_pos]));
268            } else {
269                encode_value::<S, B>(out, None);
270            }
271        }
272    }
273}
274
275/// Encode patchset UPDATE old values: PK columns only, in column order.
276///
277/// Matches SQLite's session-extension wire layout: the old side of a patchset
278/// UPDATE contains exactly `pk_count` values (no undefined padding for non-PK
279/// columns). The trailing new side (written by the caller) is symmetric and
280/// carries only the `column_count - pk_count` non-PK entries.
281fn encode_patchset_update_old_values<S: AsRef<str>, B: AsRef<[u8]>>(
282    out: &mut Vec<u8>,
283    pk_flags: &[u8],
284    pk_col_to_pk_pos: &[Option<usize>],
285    pk: &[Value<S, B>],
286) {
287    for (col_idx, &pk_flag) in pk_flags.iter().enumerate() {
288        if pk_flag > 0
289            && let Some(pk_pos) = pk_col_to_pk_pos[col_idx]
290        {
291            encode_defined_value(out, &pk[pk_pos]);
292        }
293    }
294}
295
296/// Encode a single changeset operation (op_code, indirect byte, then row payload).
297fn encode_changeset_op<S: AsRef<str> + Clone + Debug, B: AsRef<[u8]> + Clone + Debug>(
298    out: &mut Vec<u8>,
299    op: &Operation<ChangesetFormat, S, B>,
300) {
301    match op {
302        Operation::Insert { values, indirect } => {
303            out.push(op_codes::INSERT);
304            out.push(u8::from(*indirect));
305            for value in values {
306                encode_value(out, Some(value));
307            }
308        }
309        Operation::Delete {
310            data: values,
311            indirect,
312        } => {
313            out.push(op_codes::DELETE);
314            out.push(u8::from(*indirect));
315            for value in values {
316                encode_value(out, Some(value));
317            }
318        }
319        Operation::Update { values, indirect } => {
320            out.push(op_codes::UPDATE);
321            out.push(u8::from(*indirect));
322            for (old, _new) in values {
323                encode_value(out, old.as_ref());
324            }
325            for (_old, new) in values {
326                encode_value(out, new.as_ref());
327            }
328        }
329    }
330}
331
332/// Encode a single patchset operation. The PK and per-table PK mapping are
333/// supplied because DELETE/UPDATE rows in patchset format derive their old-value
334/// section from the row's PK rather than from data carried on the operation.
335fn encode_patchset_op<S: AsRef<str>, B: AsRef<[u8]>>(
336    out: &mut Vec<u8>,
337    op: &Operation<PatchsetFormat, S, B>,
338    pk: &[Value<S, B>],
339    pk_flags: &[u8],
340    pk_col_to_pk_pos: &[Option<usize>],
341) {
342    match op {
343        Operation::Insert { values, indirect } => {
344            out.push(op_codes::INSERT);
345            out.push(u8::from(*indirect));
346            for value in values {
347                encode_value(out, Some(value));
348            }
349        }
350        Operation::Delete { data: (), indirect } => {
351            out.push(op_codes::DELETE);
352            out.push(u8::from(*indirect));
353            encode_patchset_delete_values(out, pk_flags, pk_col_to_pk_pos, pk);
354        }
355        Operation::Update { values, indirect } => {
356            out.push(op_codes::UPDATE);
357            out.push(u8::from(*indirect));
358            encode_patchset_update_old_values(out, pk_flags, pk_col_to_pk_pos, pk);
359            // New side: non-PK columns in column order. Each entry is either the
360            // new column value or `0x00` (undefined) if that non-PK column did not
361            // change. PK columns are skipped entirely, matching SQLite's session
362            // extension patchset output.
363            for (col_idx, &pk_flag) in pk_flags.iter().enumerate() {
364                if pk_flag == 0 {
365                    encode_value(out, values[col_idx].1.as_ref());
366                }
367            }
368        }
369    }
370}
371
372// ============================================================================
373// DiffSetBuilder: mutable builder (DML insertion order, hash-simulated build)
374// ============================================================================
375
376/// Builder for constructing changeset or patchset binary data.
377///
378/// `DiffSetBuilder` tracks rows in DML insertion order. When [`build`](Self::build)
379/// is called, it simulates `SQLite`'s session-extension hash table to produce
380/// byte-identical output.
381///
382/// For parsed (frozen) data that should be emitted in its original order,
383/// see [`DiffSet`].
384///
385/// Generic over the format `F` (Changeset or Patchset), table schema `T`, and value types `S`, `B`.
386#[derive(Debug, Clone)]
387pub struct DiffSetBuilder<F: Format<S, B>, T: SchemaWithPK, S, B> {
388    pub(crate) tables: TableMap<F, T, S, B>,
389}
390
391/// Custom `PartialEq` that ignores tables with empty operations.
392///
393/// Tables with no operations are not serialized (skipped in `build()`), so after
394/// roundtrip they won't exist. This makes empty tables semantically equivalent
395/// to non-existent tables for comparison purposes.
396///
397/// Verified: `SQLite`'s session extension does NOT include empty table entries in
398/// changesets/patchsets when all operations cancel out. Our builder keeps them
399/// in memory to preserve table ordering, but they are correctly excluded here
400/// and in `build()`.
401impl<F: Format<S, B>, T: SchemaWithPK, S, B> PartialEq for DiffSetBuilder<F, T, S, B>
402where
403    S: PartialEq + Eq + Hash + AsRef<str>,
404    B: PartialEq + Eq + Hash + AsRef<[u8]>,
405    F::Old: PartialEq,
406    F::DeleteData: PartialEq,
407{
408    fn eq(&self, other: &Self) -> bool {
409        // Filter out tables with empty operations, then compare element by element.
410        // IndexMap preserves insertion order, so this also checks table ordering.
411        self.tables
412            .iter()
413            .filter(|(_, ops)| !ops.is_empty())
414            .eq(other.tables.iter().filter(|(_, ops)| !ops.is_empty()))
415    }
416}
417
418impl<F: Format<S, B>, T: SchemaWithPK, S, B> Eq for DiffSetBuilder<F, T, S, B>
419where
420    S: Eq + Hash + AsRef<str>,
421    B: Eq + Hash + AsRef<[u8]>,
422    F::Old: Eq,
423    F::DeleteData: Eq,
424{
425}
426
427/// Type alias for building changesets.
428pub type ChangeSet<T, S, B> = DiffSetBuilder<ChangesetFormat, T, S, B>;
429/// Type alias for building patchsets.
430pub type PatchSet<T, S, B> = DiffSetBuilder<PatchsetFormat, T, S, B>;
431
432impl<F: Format<S, B>, T: SchemaWithPK, S: AsRef<str> + Hash + Eq, B: AsRef<[u8]> + Hash + Eq>
433    Default for DiffSetBuilder<F, T, S, B>
434{
435    fn default() -> Self {
436        Self::new()
437    }
438}
439
440impl<
441    T: SchemaWithPK,
442    S: Clone + Debug + Hash + Eq + AsRef<str>,
443    B: Clone + Debug + Hash + Eq + AsRef<[u8]>,
444> From<&DiffSetBuilder<ChangesetFormat, T, S, B>> for Vec<u8>
445{
446    #[inline]
447    fn from(builder: &DiffSetBuilder<ChangesetFormat, T, S, B>) -> Self {
448        builder.build()
449    }
450}
451
452impl<
453    T: SchemaWithPK,
454    S: Clone + Debug + Hash + Eq + AsRef<str>,
455    B: Clone + Debug + Hash + Eq + AsRef<[u8]>,
456> From<DiffSetBuilder<ChangesetFormat, T, S, B>> for Vec<u8>
457{
458    #[inline]
459    fn from(builder: DiffSetBuilder<ChangesetFormat, T, S, B>) -> Self {
460        builder.build()
461    }
462}
463
464impl<T: SchemaWithPK, S: AsRef<str> + Clone + Hash + Eq, B: AsRef<[u8]> + Clone + Hash + Eq>
465    From<&DiffSetBuilder<PatchsetFormat, T, S, B>> for Vec<u8>
466{
467    #[inline]
468    fn from(builder: &DiffSetBuilder<PatchsetFormat, T, S, B>) -> Self {
469        builder.build()
470    }
471}
472
473impl<T: SchemaWithPK, S: AsRef<str> + Clone + Hash + Eq, B: AsRef<[u8]> + Clone + Hash + Eq>
474    From<DiffSetBuilder<PatchsetFormat, T, S, B>> for Vec<u8>
475{
476    #[inline]
477    fn from(builder: DiffSetBuilder<PatchsetFormat, T, S, B>) -> Self {
478        builder.build()
479    }
480}
481
482impl<F: Format<S, B>, T: SchemaWithPK, S, B> DiffSetBuilder<F, T, S, B> {
483    /// Returns the table corresponding to the given name, if it exists in the builder.
484    pub(super) fn table<'builder>(&'builder self, name: &str) -> Option<&'builder T> {
485        self.tables.keys().find(|t| t.name() == name)
486    }
487}
488
489impl<F: Format<S, B>, T: SchemaWithPK, S: AsRef<str> + Hash + Eq, B: AsRef<[u8]> + Hash + Eq>
490    DiffSetBuilder<F, T, S, B>
491{
492    /// Create a new builder.
493    #[inline]
494    #[must_use]
495    pub fn new() -> Self {
496        Self {
497            tables: IndexMap::default(),
498        }
499    }
500
501    /// Ensure a table exists in the builder, returning its row map.
502    ///
503    /// If the table doesn't exist yet, it's inserted at the end of the
504    /// `IndexMap`, preserving first-touch ordering.
505    #[inline]
506    fn ensure_table(&mut self, table: &T) -> &mut RowMap<F, S, B> {
507        self.tables.entry(table.clone()).or_default()
508    }
509
510    /// Register a table schema without adding any operations.
511    ///
512    /// This is useful when you need the table present (e.g. before calling
513    /// [`DiffSetBuilder::digest_sql`]) but don't have operations yet.
514    /// If the table is already registered, this is a no-op.
515    pub fn add_table(&mut self, table: &T) -> &mut Self {
516        self.ensure_table(table);
517        self
518    }
519
520    /// Returns true if the builder has no operations.
521    #[inline]
522    #[must_use]
523    pub fn is_empty(&self) -> bool {
524        self.tables.values().all(IndexMap::is_empty)
525    }
526
527    /// Returns the number of operations across all tables.
528    #[inline]
529    #[must_use]
530    pub fn len(&self) -> usize {
531        self.tables.values().map(IndexMap::len).sum()
532    }
533
534    /// Add any operation, consolidating with existing operations on the same row.
535    ///
536    /// The table schema is passed separately, operations are schema-less.
537    pub(crate) fn add_operation(
538        &mut self,
539        table: &T,
540        pk: Vec<Value<S, B>>,
541        new_op: Operation<F, S, B>,
542    ) -> &mut Self
543    where
544        S: Clone,
545        B: Clone,
546        Operation<F, S, B>: core::ops::Add<Output = Option<Operation<F, S, B>>>,
547    {
548        let rows = self.ensure_table(table);
549
550        match rows.shift_remove_full(&pk) {
551            None => {
552                rows.insert(pk, new_op);
553            }
554            Some((original_index, _removed_key, existing)) => {
555                // Special case: INSERT + UPDATE may change the PK
556                match (&existing, &new_op) {
557                    (Operation::Insert { .. }, Operation::Update { .. }) => {
558                        // Apply update to insert values, then re-extract PK
559                        if let Some(combined) = existing + new_op
560                            && let Operation::Insert { values, .. } = &combined
561                        {
562                            let new_pk = table.extract_pk(values);
563                            // The new PK may collide with a different existing row
564                            rows.shift_remove(&new_pk);
565                            let index = original_index.min(rows.len());
566                            rows.shift_insert(index, new_pk, combined);
567                        }
568                    }
569                    _ => {
570                        // Standard consolidation
571                        if let Some(combined) = existing + new_op {
572                            // Re-insert at original position to preserve row ordering
573                            rows.shift_insert(original_index, pk, combined);
574                        }
575                    }
576                }
577            }
578        }
579
580        self
581    }
582}
583
584// Unified digest entry point for wire events (0.2.0+).
585
586impl<F, T, S, B> DiffSetBuilder<F, T, S, B>
587where
588    F: Format<S, B>,
589    T: crate::schema::NamedColumns,
590    S: Clone + Debug + Hash + Eq + AsRef<str> + Default,
591    B: Clone + Debug + Hash + Eq + AsRef<[u8]> + Default,
592{
593    /// Digest one CDC wire event, resolving each affected table via
594    /// `schema` and decoding column payloads via `adapter`.
595    ///
596    /// # Errors
597    ///
598    /// Whatever `E::Error` reports (per-source `ConversionError`).
599    pub fn digest<E, Sch, A>(self, event: &E, schema: &Sch, adapter: &A) -> Result<Self, E::Error>
600    where
601        E: crate::wire::Digestable<F, T, S, B>,
602        Sch: crate::wire::WireSchema<E::Src, Table = T>,
603        A: crate::wire::WireAdapter<E::Src, S, B>,
604        T: crate::wire::WireColumnTypes<E::Src>,
605    {
606        event.digest_into(self, schema, adapter)
607    }
608}
609
610// ============================================================================
611// DiffOps trait: unified insert / delete / update for DiffSetBuilder & DiffSet
612// ============================================================================
613
614/// Trait for adding DML operations (INSERT, DELETE, UPDATE) to a diff set.
615///
616/// Implemented for both [`DiffSetBuilder`] and [`DiffSet`], allowing
617/// operations to be added to either type. Methods consume `self` and
618/// return a [`DiffSetBuilder`].
619pub trait DiffOps<T: SchemaWithPK, S, B>: Sized {
620    /// The format (changeset or patchset) of the diff set.
621    type Format: Format<S, B>;
622
623    /// The argument type for the [`delete`](Self::delete) operation.
624    ///
625    /// * Changeset: [`ChangeDelete<T, S, B>`]
626    /// * Patchset: [`PatchDelete<T, S, B>`]
627    type DeleteArg;
628
629    /// Add an INSERT operation.
630    fn insert(self, insert: Insert<T, S, B>) -> DiffSetBuilder<Self::Format, T, S, B>;
631
632    /// Add a DELETE operation.
633    fn delete(self, delete: Self::DeleteArg) -> DiffSetBuilder<Self::Format, T, S, B>;
634
635    /// Add an UPDATE operation.
636    fn update(self, update: Update<T, Self::Format, S, B>)
637    -> DiffSetBuilder<Self::Format, T, S, B>;
638}
639
640// -- DiffOps for DiffSetBuilder<ChangesetFormat> ------------------------------
641
642impl<
643    T: SchemaWithPK,
644    S: Clone + Debug + Hash + Eq + AsRef<str>,
645    B: Clone + Debug + Hash + Eq + AsRef<[u8]>,
646> DiffOps<T, S, B> for DiffSetBuilder<ChangesetFormat, T, S, B>
647{
648    type Format = ChangesetFormat;
649    type DeleteArg = ChangeDelete<T, S, B>;
650
651    fn insert(mut self, insert: Insert<T, S, B>) -> Self {
652        let pk = insert.extract_pk();
653        let table = insert.as_ref().clone();
654        let indirect = insert.indirect;
655        self.add_operation(
656            &table,
657            pk,
658            Operation::Insert {
659                values: insert.into_values(),
660                indirect,
661            },
662        );
663        self
664    }
665
666    fn delete(mut self, delete: ChangeDelete<T, S, B>) -> Self {
667        let pk = delete.as_ref().extract_pk(&delete.values);
668        let table = delete.as_ref().clone();
669        let indirect = delete.indirect;
670        self.add_operation(
671            &table,
672            pk,
673            Operation::Delete {
674                data: delete.into_values(),
675                indirect,
676            },
677        );
678        self
679    }
680
681    fn update(mut self, update: Update<T, ChangesetFormat, S, B>) -> Self {
682        let old_values: Vec<_> = update
683            .values()
684            .iter()
685            .map(|(old, _): &(_, _)| old.clone().unwrap_or(Value::Null))
686            .collect();
687        let pk = update.as_ref().extract_pk(&old_values);
688        let table = update.as_ref().clone();
689        let indirect = update.indirect;
690        let values: Vec<(MaybeValue<S, B>, MaybeValue<S, B>)> = update.into();
691        self.add_operation(&table, pk, Operation::Update { values, indirect });
692        self
693    }
694}
695
696// -- DiffOps for DiffSetBuilder<PatchsetFormat> -------------------------------
697
698impl<T: SchemaWithPK, S: Clone + Hash + Eq + AsRef<str>, B: Clone + Hash + Eq + AsRef<[u8]>>
699    DiffOps<T, S, B> for DiffSetBuilder<PatchsetFormat, T, S, B>
700{
701    type Format = PatchsetFormat;
702    type DeleteArg = PatchDelete<T, S, B>;
703
704    fn insert(mut self, insert: Insert<T, S, B>) -> Self {
705        let pk = insert.extract_pk();
706        let table = insert.as_ref().clone();
707        let indirect = insert.indirect;
708        self.add_operation(
709            &table,
710            pk,
711            Operation::Insert {
712                values: insert.into_values(),
713                indirect,
714            },
715        );
716        self
717    }
718
719    /// Delete by primary key.
720    ///
721    /// # Example
722    ///
723    /// ```
724    /// use sqlite_diff_rs::{DiffOps, PatchDelete, PatchSet, SchemaWithPK, TableSchema};
725    ///
726    /// // CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)
727    /// let schema: TableSchema<String> = TableSchema::new("users".into(), 2, vec![1, 0]);
728    ///
729    /// // Delete row where id = 1
730    /// let patchset = PatchSet::<_, String, Vec<u8>>::new()
731    ///     .delete(PatchDelete::new(schema, vec![1i64.into()]));
732    /// ```
733    fn delete(mut self, delete: PatchDelete<T, S, B>) -> Self {
734        let indirect = delete.indirect;
735        self.add_operation(
736            &delete.table,
737            delete.pk,
738            Operation::Delete { data: (), indirect },
739        );
740        self
741    }
742
743    /// Update by primary key.
744    ///
745    /// # Example
746    ///
747    /// ```
748    /// use sqlite_diff_rs::{DiffOps, PatchSet, PatchsetFormat, Update, TableSchema};
749    ///
750    /// // CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)
751    /// let schema: TableSchema<String> = TableSchema::new("users".into(), 2, vec![1, 0]);
752    ///
753    /// // UPDATE users SET name = 'Bob' WHERE id = 1
754    /// let update = Update::<_, PatchsetFormat, String, Vec<u8>>::from(schema)
755    ///     .set(0, 1i64).unwrap()  // PK value
756    ///     .set(1, "Bob").unwrap();
757    ///
758    /// let patchset = PatchSet::<_, String, Vec<u8>>::new()
759    ///     .update(update);
760    /// ```
761    fn update(mut self, update: Update<T, PatchsetFormat, S, B>) -> Self {
762        let pk = update.extract_pk();
763        let table = update.as_ref().clone();
764        let indirect = update.indirect;
765        let values: Vec<((), MaybeValue<S, B>)> = update.into();
766        self.add_operation(&table, pk, Operation::Update { values, indirect });
767        self
768    }
769}
770
771// -- DiffOps for DiffSet<ChangesetFormat> -------------------------------------
772
773impl<
774    T: SchemaWithPK,
775    S: Clone + Debug + Hash + Eq + AsRef<str>,
776    B: Clone + Debug + Hash + Eq + AsRef<[u8]>,
777> DiffOps<T, S, B> for DiffSet<ChangesetFormat, T, S, B>
778{
779    type Format = ChangesetFormat;
780    type DeleteArg = ChangeDelete<T, S, B>;
781
782    fn insert(self, insert: Insert<T, S, B>) -> DiffSetBuilder<ChangesetFormat, T, S, B> {
783        let builder: DiffSetBuilder<ChangesetFormat, T, S, B> = self.into();
784        builder.insert(insert)
785    }
786
787    fn delete(self, delete: ChangeDelete<T, S, B>) -> DiffSetBuilder<ChangesetFormat, T, S, B> {
788        let builder: DiffSetBuilder<ChangesetFormat, T, S, B> = self.into();
789        builder.delete(delete)
790    }
791
792    fn update(
793        self,
794        update: Update<T, ChangesetFormat, S, B>,
795    ) -> DiffSetBuilder<ChangesetFormat, T, S, B> {
796        let builder: DiffSetBuilder<ChangesetFormat, T, S, B> = self.into();
797        builder.update(update)
798    }
799}
800
801// -- DiffOps for DiffSet<PatchsetFormat> --------------------------------------
802
803impl<T: SchemaWithPK, S: Clone + Hash + Eq + AsRef<str>, B: Clone + Hash + Eq + AsRef<[u8]>>
804    DiffOps<T, S, B> for DiffSet<PatchsetFormat, T, S, B>
805{
806    type Format = PatchsetFormat;
807    type DeleteArg = PatchDelete<T, S, B>;
808
809    fn insert(self, insert: Insert<T, S, B>) -> DiffSetBuilder<PatchsetFormat, T, S, B> {
810        let builder: DiffSetBuilder<PatchsetFormat, T, S, B> = self.into();
811        builder.insert(insert)
812    }
813
814    fn delete(self, delete: PatchDelete<T, S, B>) -> DiffSetBuilder<PatchsetFormat, T, S, B> {
815        let builder: DiffSetBuilder<PatchsetFormat, T, S, B> = self.into();
816        builder.delete(delete)
817    }
818
819    fn update(
820        self,
821        update: Update<T, PatchsetFormat, S, B>,
822    ) -> DiffSetBuilder<PatchsetFormat, T, S, B> {
823        let builder: DiffSetBuilder<PatchsetFormat, T, S, B> = self.into();
824        builder.update(update)
825    }
826}
827
828impl<T: crate::schema::NamedColumns, S: Clone + Hash + Eq + AsRef<str> + for<'a> From<&'a str>>
829    DiffSetBuilder<PatchsetFormat, T, S, Vec<u8>>
830{
831    /// Digest a SQL string containing INSERT, UPDATE, and DELETE statements
832    /// into this patchset builder.
833    ///
834    /// The SQL statements are parsed and their effects are directly applied
835    /// to the builder, consolidating operations by primary key as usual.
836    ///
837    /// # Errors
838    ///
839    /// Returns a [`crate::builders::sql::ParseError`] if the SQL cannot be parsed.
840    pub fn digest_sql<'input>(
841        &mut self,
842        input: &'input str,
843    ) -> Result<&mut Self, crate::builders::sql::ParseError<'input>> {
844        let mut parser = crate::builders::sql::Parser::new(input, self);
845        parser.digest_all()?;
846        Ok(self)
847    }
848}
849
850// ============================================================================
851// Unified build implementation
852// ============================================================================
853
854impl<
855    T: SchemaWithPK,
856    S: Clone + Debug + Hash + Eq + AsRef<str>,
857    B: Clone + Debug + Hash + Eq + AsRef<[u8]>,
858> DiffSetBuilder<ChangesetFormat, T, S, B>
859{
860    /// Build the changeset binary data.
861    ///
862    /// Returns the binary representation compatible with `SQLite`'s session extension.
863    ///
864    /// # Panics
865    ///
866    /// This function does not panic under normal usage. Internal indexing is guaranteed
867    /// to be within bounds.
868    #[must_use]
869    pub fn build(&self) -> Vec<u8> {
870        let mut out = Vec::new();
871
872        for (table, rows) in &self.tables {
873            if rows.is_empty() {
874                continue;
875            }
876
877            write_table_header(&mut out, markers::CHANGESET, table);
878
879            for idx in session_row_order(rows) {
880                let (_pk, op) = rows.get_index(idx).unwrap();
881                encode_changeset_op(&mut out, op);
882            }
883        }
884
885        out
886    }
887}
888
889impl<T: SchemaWithPK, S: Clone + Debug + AsRef<str>, B: Clone + Debug + AsRef<[u8]>>
890    DiffSetBuilder<ChangesetFormat, T, S, B>
891{
892    /// Walk operations grouped by table in DML insertion order.
893    ///
894    /// Mirrors [`DiffSet::iter`] but keeps insertion order; the
895    /// session-extension hash ordering only applies at [`build`](Self::build)
896    /// time.
897    pub fn iter(&self) -> impl Iterator<Item = ChangesetOp<'_, T, S, B>> {
898        self.tables.iter().flat_map(|(table, rows)| {
899            rows.iter().map(move |(_pk, op)| match op {
900                Operation::Insert { values, indirect } => ChangesetOp::Insert {
901                    table,
902                    values: values.as_slice(),
903                    indirect: *indirect,
904                },
905                Operation::Update { values, indirect } => ChangesetOp::Update {
906                    table,
907                    values: values.as_slice(),
908                    indirect: *indirect,
909                },
910                Operation::Delete { data, indirect } => ChangesetOp::Delete {
911                    table,
912                    old_values: data.as_slice(),
913                    indirect: *indirect,
914                },
915            })
916        })
917    }
918}
919
920impl<T: SchemaWithPK, S: Clone + Hash + Eq + AsRef<str>, B: Clone + Hash + Eq + AsRef<[u8]>>
921    DiffSetBuilder<PatchsetFormat, T, S, B>
922{
923    /// Build the patchset binary data.
924    ///
925    /// Returns the binary representation compatible with `SQLite`'s session extension.
926    ///
927    /// # Panics
928    ///
929    /// This function does not panic under normal usage. Internal indexing is guaranteed
930    /// to be within bounds.
931    #[must_use]
932    pub fn build(&self) -> Vec<u8> {
933        let mut out = Vec::new();
934
935        for (table, rows) in &self.tables {
936            if rows.is_empty() {
937                continue;
938            }
939
940            write_table_header(&mut out, markers::PATCHSET, table);
941
942            let (pk_flags, pk_col_to_pk_pos) = patchset_pk_mapping(table);
943
944            for idx in session_row_order(rows) {
945                let (pk, op) = rows.get_index(idx).unwrap();
946                encode_patchset_op(&mut out, op, pk, &pk_flags, &pk_col_to_pk_pos);
947            }
948        }
949
950        out
951    }
952}
953
954impl<T: SchemaWithPK, S: Clone + AsRef<str>, B: Clone + AsRef<[u8]>>
955    DiffSetBuilder<PatchsetFormat, T, S, B>
956{
957    /// Walk operations grouped by table in DML insertion order.
958    ///
959    /// Mirrors [`DiffSet::iter`] but keeps insertion order; the
960    /// session-extension hash ordering only applies at [`build`](Self::build)
961    /// time. With the `diesel` feature enabled, each item implements
962    /// [`QueryFragment`](diesel::query_builder::QueryFragment) and executes
963    /// via [`RunQueryDsl`](diesel::RunQueryDsl).
964    pub fn iter(&self) -> impl Iterator<Item = PatchsetOp<'_, T, S, B>> {
965        self.tables.iter().flat_map(|(table, rows)| {
966            rows.iter().map(move |(pk, op)| match op {
967                Operation::Insert { values, indirect } => PatchsetOp::Insert {
968                    table,
969                    values: values.as_slice(),
970                    indirect: *indirect,
971                },
972                Operation::Update { values, indirect } => PatchsetOp::Update {
973                    table,
974                    pk: pk.as_slice(),
975                    entries: values.as_slice(),
976                    indirect: *indirect,
977                },
978                Operation::Delete { indirect, .. } => PatchsetOp::Delete {
979                    table,
980                    pk: pk.as_slice(),
981                    indirect: *indirect,
982                },
983            })
984        })
985    }
986}
987
988// ============================================================================
989// Reverse implementation for DiffSetBuilder
990// ============================================================================
991
992use crate::builders::operation::Reverse;
993
994impl<
995    T: SchemaWithPK,
996    S: Clone + Debug + Hash + Eq + AsRef<str>,
997    B: Clone + Debug + Hash + Eq + AsRef<[u8]>,
998> Reverse for DiffSetBuilder<ChangesetFormat, T, S, B>
999{
1000    type Output = DiffSetBuilder<ChangesetFormat, T, S, B>;
1001
1002    fn reverse(self) -> Self::Output {
1003        let mut reversed: DiffSetBuilder<ChangesetFormat, T, S, B> = DiffSetBuilder::new();
1004
1005        for (table, rows) in self.tables {
1006            for (pk, op) in rows {
1007                let rev_op = op.reverse();
1008
1009                reversed.add_operation(&table, pk, rev_op);
1010            }
1011        }
1012
1013        reversed
1014    }
1015}
1016
1017// ============================================================================
1018// BitOr / BitOrAssign for DiffSetBuilder (changeset/patchset concatenation,
1019// equivalent to SQLite's `sqlite3changeset_concat()`)
1020// ============================================================================
1021
1022impl<
1023    F: Format<S, B>,
1024    T: SchemaWithPK,
1025    S: Clone + Hash + Eq + AsRef<str>,
1026    B: Clone + Hash + Eq + AsRef<[u8]>,
1027> BitOrAssign for DiffSetBuilder<F, T, S, B>
1028where
1029    Operation<F, S, B>: core::ops::Add<Output = Option<Operation<F, S, B>>>,
1030{
1031    /// Merge another diff set into this one, consolidating operations on the same row.
1032    fn bitor_assign(&mut self, rhs: Self) {
1033        for (table, rows) in rhs.tables {
1034            for (pk, op) in rows {
1035                self.add_operation(&table, pk, op);
1036            }
1037        }
1038    }
1039}
1040
1041impl<
1042    F: Format<S, B>,
1043    T: SchemaWithPK,
1044    S: Clone + Hash + Eq + AsRef<str>,
1045    B: Clone + Hash + Eq + AsRef<[u8]>,
1046> BitOr for DiffSetBuilder<F, T, S, B>
1047where
1048    Operation<F, S, B>: core::ops::Add<Output = Option<Operation<F, S, B>>>,
1049{
1050    type Output = Self;
1051
1052    /// Merge two diff sets, consolidating operations on the same row.
1053    #[inline]
1054    fn bitor(mut self, rhs: Self) -> Self::Output {
1055        self |= rhs;
1056        self
1057    }
1058}
1059
1060// ============================================================================
1061// DiffSet: frozen (parsed) changeset/patchset with sequential row order
1062// ============================================================================
1063
1064/// A frozen changeset or patchset whose rows are emitted in stored order.
1065///
1066/// `DiffSet` is produced by the binary parser (via [`ParsedDiffSet`](crate::parser::ParsedDiffSet))
1067/// or by converting from a [`DiffSetBuilder`] using `Into::into`.  Unlike
1068/// [`DiffSetBuilder`], it stores tables and rows in a plain `Vec`, reflecting
1069/// the fact that no further mutation or PK-based lookup is needed.
1070///
1071/// [`build`](Self::build) serializes rows in the order they are stored. No
1072/// session hash-table simulation is applied. This preserves the original
1073/// row order of parsed binary data across roundtrips.
1074///
1075/// To modify a `DiffSet`, convert it back to a [`DiffSetBuilder`] using
1076/// `Into::into`.
1077#[derive(Debug, Clone)]
1078pub struct DiffSet<F: Format<S, B>, T: SchemaWithPK, S, B> {
1079    /// Tables and their rows, stored in order. Each row is a `(pk, operation)` pair.
1080    pub(crate) tables: TableVec<F, T, S, B>,
1081}
1082
1083/// Custom `PartialEq` that ignores tables with no operations (same semantics
1084/// as `DiffSetBuilder`).
1085impl<F: Format<S, B>, T: SchemaWithPK, S, B> PartialEq for DiffSet<F, T, S, B>
1086where
1087    S: PartialEq + Eq + Hash + AsRef<str>,
1088    B: PartialEq + Eq + Hash + AsRef<[u8]>,
1089    F::Old: PartialEq,
1090    F::DeleteData: PartialEq,
1091{
1092    fn eq(&self, other: &Self) -> bool {
1093        self.tables
1094            .iter()
1095            .filter(|(_, ops)| !ops.is_empty())
1096            .eq(other.tables.iter().filter(|(_, ops)| !ops.is_empty()))
1097    }
1098}
1099
1100impl<F: Format<S, B>, T: SchemaWithPK, S, B> Eq for DiffSet<F, T, S, B>
1101where
1102    S: Eq + Hash + AsRef<str>,
1103    B: Eq + Hash + AsRef<[u8]>,
1104    F::Old: Eq,
1105    F::DeleteData: Eq,
1106{
1107}
1108
1109impl<F: Format<S, B>, T: SchemaWithPK, S: AsRef<str> + Hash + Eq, B: AsRef<[u8]> + Hash + Eq>
1110    Default for DiffSet<F, T, S, B>
1111{
1112    fn default() -> Self {
1113        Self { tables: Vec::new() }
1114    }
1115}
1116
1117impl<F: Format<S, B>, T: SchemaWithPK, S: AsRef<str> + Hash + Eq, B: AsRef<[u8]> + Hash + Eq>
1118    DiffSet<F, T, S, B>
1119{
1120    /// Returns `true` if there are no operations in any table.
1121    #[inline]
1122    #[must_use]
1123    pub fn is_empty(&self) -> bool {
1124        self.tables.iter().all(|(_, rows)| rows.is_empty())
1125    }
1126
1127    /// Returns the total number of operations across all tables.
1128    #[inline]
1129    #[must_use]
1130    pub fn len(&self) -> usize {
1131        self.tables.iter().map(|(_, rows)| rows.len()).sum()
1132    }
1133}
1134
1135// -- Changeset iter (DiffSet) -------------------------------------------------
1136
1137impl<F: Format<S, B>, T: SchemaWithPK, S: AsRef<str> + Hash + Eq, B: AsRef<[u8]> + Hash + Eq>
1138    DiffSet<F, T, S, B>
1139{
1140    /// Returns the schema of each table that holds at least one operation,
1141    /// in stored order.
1142    pub fn tables(&self) -> impl Iterator<Item = &T> {
1143        self.tables
1144            .iter()
1145            .filter(|(_, rows)| !rows.is_empty())
1146            .map(|(t, _)| t)
1147    }
1148}
1149
1150impl<T: SchemaWithPK, S: Clone + Debug + AsRef<str>, B: Clone + Debug + AsRef<[u8]>>
1151    DiffSet<ChangesetFormat, T, S, B>
1152{
1153    /// Iterate over every operation in the changeset.
1154    ///
1155    /// Operations are yielded in stored order, grouped by table. Each
1156    /// [`ChangesetOp`] borrows from this `DiffSet`, so the returned
1157    /// iterator is invalidated when the `DiffSet` is dropped or mutated.
1158    pub fn iter(&self) -> impl Iterator<Item = ChangesetOp<'_, T, S, B>> {
1159        self.tables.iter().flat_map(|(table, rows)| {
1160            rows.iter().map(move |(_pk, op)| match op {
1161                Operation::Insert { values, indirect } => ChangesetOp::Insert {
1162                    table,
1163                    values: values.as_slice(),
1164                    indirect: *indirect,
1165                },
1166                Operation::Update { values, indirect } => ChangesetOp::Update {
1167                    table,
1168                    values: values.as_slice(),
1169                    indirect: *indirect,
1170                },
1171                Operation::Delete { data, indirect } => ChangesetOp::Delete {
1172                    table,
1173                    old_values: data.as_slice(),
1174                    indirect: *indirect,
1175                },
1176            })
1177        })
1178    }
1179}
1180
1181impl<T: SchemaWithPK, S: Clone + AsRef<str>, B: Clone + AsRef<[u8]>>
1182    DiffSet<PatchsetFormat, T, S, B>
1183{
1184    /// Iterate over every operation in the patchset.
1185    ///
1186    /// Operations are yielded in stored order, grouped by table. Each
1187    /// [`PatchsetOp`] borrows from this `DiffSet`. For DELETE and UPDATE
1188    /// ops only the primary-key columns are available (patchset format
1189    /// does not carry full old-row values).
1190    pub fn iter(&self) -> impl Iterator<Item = PatchsetOp<'_, T, S, B>> {
1191        self.tables.iter().flat_map(|(table, rows)| {
1192            rows.iter().map(move |(pk, op)| match op {
1193                Operation::Insert { values, indirect } => PatchsetOp::Insert {
1194                    table,
1195                    values: values.as_slice(),
1196                    indirect: *indirect,
1197                },
1198                Operation::Update { values, indirect } => PatchsetOp::Update {
1199                    table,
1200                    pk: pk.as_slice(),
1201                    entries: values.as_slice(),
1202                    indirect: *indirect,
1203                },
1204                Operation::Delete { indirect, .. } => PatchsetOp::Delete {
1205                    table,
1206                    pk: pk.as_slice(),
1207                    indirect: *indirect,
1208                },
1209            })
1210        })
1211    }
1212}
1213
1214// -- Changeset build (DiffSet) ------------------------------------------------
1215
1216impl<
1217    T: SchemaWithPK,
1218    S: Clone + Debug + Hash + Eq + AsRef<str>,
1219    B: Clone + Debug + Hash + Eq + AsRef<[u8]>,
1220> DiffSet<ChangesetFormat, T, S, B>
1221{
1222    /// Serialize the changeset to binary.
1223    ///
1224    /// Rows are emitted in stored order (no hash simulation).
1225    #[must_use]
1226    pub fn build(&self) -> Vec<u8> {
1227        let mut out = Vec::new();
1228
1229        for (table, rows) in &self.tables {
1230            if rows.is_empty() {
1231                continue;
1232            }
1233
1234            write_table_header(&mut out, markers::CHANGESET, table);
1235
1236            for (_pk, op) in rows {
1237                encode_changeset_op(&mut out, op);
1238            }
1239        }
1240
1241        out
1242    }
1243}
1244
1245// -- Patchset build (DiffSet) -------------------------------------------------
1246
1247impl<T: SchemaWithPK, S: Clone + Hash + Eq + AsRef<str>, B: Clone + Hash + Eq + AsRef<[u8]>>
1248    DiffSet<PatchsetFormat, T, S, B>
1249{
1250    /// Serialize the patchset to binary.
1251    ///
1252    /// Rows are emitted in stored order (no hash simulation).
1253    #[must_use]
1254    pub fn build(&self) -> Vec<u8> {
1255        let mut out = Vec::new();
1256
1257        for (table, rows) in &self.tables {
1258            if rows.is_empty() {
1259                continue;
1260            }
1261
1262            write_table_header(&mut out, markers::PATCHSET, table);
1263
1264            let (pk_flags, pk_col_to_pk_pos) = patchset_pk_mapping(table);
1265
1266            for (pk, op) in rows {
1267                encode_patchset_op(&mut out, op, pk, &pk_flags, &pk_col_to_pk_pos);
1268            }
1269        }
1270
1271        out
1272    }
1273}
1274
1275// -- From<DiffSet> for Vec<u8> ------------------------------------------------
1276
1277impl<
1278    T: SchemaWithPK,
1279    S: Clone + Debug + Hash + Eq + AsRef<str>,
1280    B: Clone + Debug + Hash + Eq + AsRef<[u8]>,
1281> From<&DiffSet<ChangesetFormat, T, S, B>> for Vec<u8>
1282{
1283    #[inline]
1284    fn from(diffset: &DiffSet<ChangesetFormat, T, S, B>) -> Self {
1285        diffset.build()
1286    }
1287}
1288
1289impl<
1290    T: SchemaWithPK,
1291    S: Clone + Debug + Hash + Eq + AsRef<str>,
1292    B: Clone + Debug + Hash + Eq + AsRef<[u8]>,
1293> From<DiffSet<ChangesetFormat, T, S, B>> for Vec<u8>
1294{
1295    #[inline]
1296    fn from(diffset: DiffSet<ChangesetFormat, T, S, B>) -> Self {
1297        diffset.build()
1298    }
1299}
1300
1301impl<T: SchemaWithPK, S: AsRef<str> + Clone + Hash + Eq, B: AsRef<[u8]> + Clone + Hash + Eq>
1302    From<&DiffSet<PatchsetFormat, T, S, B>> for Vec<u8>
1303{
1304    #[inline]
1305    fn from(diffset: &DiffSet<PatchsetFormat, T, S, B>) -> Self {
1306        diffset.build()
1307    }
1308}
1309
1310impl<T: SchemaWithPK, S: AsRef<str> + Clone + Hash + Eq, B: AsRef<[u8]> + Clone + Hash + Eq>
1311    From<DiffSet<PatchsetFormat, T, S, B>> for Vec<u8>
1312{
1313    #[inline]
1314    fn from(diffset: DiffSet<PatchsetFormat, T, S, B>) -> Self {
1315        diffset.build()
1316    }
1317}
1318
1319// -- Reverse for DiffSet<ChangesetFormat> -------------------------------------
1320
1321impl<
1322    T: SchemaWithPK,
1323    S: Clone + Debug + Hash + Eq + AsRef<str>,
1324    B: Clone + Debug + Hash + Eq + AsRef<[u8]>,
1325> Reverse for DiffSet<ChangesetFormat, T, S, B>
1326{
1327    type Output = DiffSet<ChangesetFormat, T, S, B>;
1328
1329    fn reverse(self) -> Self::Output {
1330        DiffSet {
1331            tables: self
1332                .tables
1333                .into_iter()
1334                .map(|(table, rows)| {
1335                    let rev_rows = rows
1336                        .into_iter()
1337                        .map(|(pk, op)| (pk, op.reverse()))
1338                        .collect();
1339                    (table, rev_rows)
1340                })
1341                .collect(),
1342        }
1343    }
1344}
1345
1346// -- From conversions between DiffSetBuilder and DiffSet ----------------------
1347
1348impl<F: Format<S, B>, T: SchemaWithPK, S: Hash + Eq + AsRef<str>, B: Hash + Eq + AsRef<[u8]>>
1349    From<DiffSetBuilder<F, T, S, B>> for DiffSet<F, T, S, B>
1350{
1351    fn from(builder: DiffSetBuilder<F, T, S, B>) -> Self {
1352        Self {
1353            tables: builder
1354                .tables
1355                .into_iter()
1356                .map(|(table, rows)| {
1357                    let ordered_rows: RowVec<F, S, B> = rows.into_iter().collect();
1358                    (table, ordered_rows)
1359                })
1360                .collect(),
1361        }
1362    }
1363}
1364
1365impl<F: Format<S, B>, T: SchemaWithPK, S: Hash + Eq + AsRef<str>, B: Hash + Eq + AsRef<[u8]>>
1366    From<DiffSet<F, T, S, B>> for DiffSetBuilder<F, T, S, B>
1367{
1368    fn from(diffset: DiffSet<F, T, S, B>) -> Self {
1369        let mut builder = Self::new();
1370        for (table, rows) in diffset.tables {
1371            let map: IndexMap<Vec<Value<S, B>>, Operation<F, S, B>> = rows.into_iter().collect();
1372            builder.tables.insert(table, map);
1373        }
1374        builder
1375    }
1376}
1377
1378#[cfg(test)]
1379mod tests {
1380    use super::*;
1381    use crate::builders::operation::Indirect;
1382    use crate::encoding::Value;
1383    use alloc::{string::String, vec};
1384
1385    /// Simple test table implementation
1386    #[derive(Debug, Clone, PartialEq, Eq, Hash)]
1387    struct TestTable {
1388        name: String,
1389        num_columns: usize,
1390        pk_column: usize,
1391    }
1392
1393    impl TestTable {
1394        fn new(name: &str, num_columns: usize, pk_column: usize) -> Self {
1395            Self {
1396                name: name.into(),
1397                num_columns,
1398                pk_column,
1399            }
1400        }
1401    }
1402
1403    impl crate::DynTable for TestTable {
1404        fn name(&self) -> &str {
1405            &self.name
1406        }
1407
1408        fn number_of_columns(&self) -> usize {
1409            self.num_columns
1410        }
1411
1412        fn write_pk_flags(&self, buf: &mut [u8]) {
1413            assert_eq!(buf.len(), self.num_columns);
1414            buf.fill(0);
1415            buf[self.pk_column] = 1;
1416        }
1417    }
1418
1419    impl crate::SchemaWithPK for TestTable {
1420        fn number_of_primary_keys(&self) -> usize {
1421            1
1422        }
1423
1424        fn primary_key_index(&self, col_idx: usize) -> Option<usize> {
1425            if col_idx == self.pk_column {
1426                Some(0)
1427            } else {
1428                None
1429            }
1430        }
1431
1432        fn extract_pk<S: Clone, B: Clone>(
1433            &self,
1434            values: &impl crate::IndexableValues<Text = S, Binary = B>,
1435        ) -> alloc::vec::Vec<Value<S, B>> {
1436            alloc::vec![
1437                values
1438                    .get(self.pk_column)
1439                    .expect("primary key column index out of bounds, values shorter than schema")
1440            ]
1441        }
1442    }
1443
1444    // Type alias for cleaner test code
1445    type ChangesetBuilder = DiffSetBuilder<ChangesetFormat, TestTable, String, Vec<u8>>;
1446
1447    #[test]
1448    fn test_insert_single_row() {
1449        let table = TestTable::new("users", 2, 0);
1450        let insert = Insert::from(table.clone())
1451            .set(0, 1i64)
1452            .unwrap()
1453            .set(1, "alice")
1454            .unwrap();
1455
1456        let builder = ChangesetBuilder::new().insert(insert);
1457
1458        assert_eq!(builder.len(), 1);
1459        assert!(!builder.is_empty());
1460    }
1461
1462    #[test]
1463    fn test_insert_then_delete_cancels_out() {
1464        let table = TestTable::new("users", 2, 0);
1465
1466        let insert = Insert::from(table.clone())
1467            .set(0, 1i64)
1468            .unwrap()
1469            .set(1, "alice")
1470            .unwrap();
1471
1472        let delete = ChangeDelete::from(table.clone())
1473            .set(0, 1i64)
1474            .unwrap()
1475            .set(1, "alice")
1476            .unwrap();
1477
1478        let builder = ChangesetBuilder::new().insert(insert).delete(delete);
1479
1480        assert_eq!(builder.len(), 0);
1481        assert!(builder.is_empty());
1482    }
1483
1484    #[test]
1485    fn test_insert_then_update_becomes_insert() {
1486        let table = TestTable::new("users", 2, 0);
1487
1488        let insert = Insert::from(table.clone())
1489            .set(0, 1i64)
1490            .unwrap()
1491            .set(1, "alice")
1492            .unwrap();
1493
1494        let update = Update::<TestTable, ChangesetFormat, String, Vec<u8>>::from(table.clone())
1495            .set(0, 1i64, 1i64) // PK unchanged
1496            .unwrap()
1497            .set(1, "alice", "bob")
1498            .unwrap();
1499
1500        let builder = ChangesetBuilder::new().insert(insert).update(update);
1501
1502        assert_eq!(builder.len(), 1);
1503        // Should still be an INSERT with "bob" as the name
1504    }
1505
1506    #[test]
1507    fn test_delete_then_insert_same_values_cancels_out() {
1508        let table = TestTable::new("users", 2, 0);
1509
1510        let delete = ChangeDelete::from(table.clone())
1511            .set(0, 1i64)
1512            .unwrap()
1513            .set(1, "alice")
1514            .unwrap();
1515
1516        let insert = Insert::from(table.clone())
1517            .set(0, 1i64)
1518            .unwrap()
1519            .set(1, "alice")
1520            .unwrap();
1521
1522        let builder = ChangesetBuilder::new().delete(delete).insert(insert);
1523
1524        assert_eq!(builder.len(), 0);
1525        assert!(builder.is_empty());
1526    }
1527
1528    #[test]
1529    fn test_delete_then_insert_different_values_becomes_update() {
1530        let table = TestTable::new("users", 2, 0);
1531
1532        let delete = ChangeDelete::from(table.clone())
1533            .set(0, 1i64)
1534            .unwrap()
1535            .set(1, "alice")
1536            .unwrap();
1537
1538        let insert = Insert::from(table.clone())
1539            .set(0, 1i64)
1540            .unwrap()
1541            .set(1, "bob")
1542            .unwrap();
1543
1544        let builder = ChangesetBuilder::new().delete(delete).insert(insert);
1545
1546        assert_eq!(builder.len(), 1);
1547        // Should be an UPDATE from alice to bob
1548    }
1549
1550    #[test]
1551    fn test_multiple_rows() {
1552        let table = TestTable::new("users", 2, 0);
1553
1554        let insert1 = Insert::from(table.clone())
1555            .set(0, 1i64)
1556            .unwrap()
1557            .set(1, "alice")
1558            .unwrap();
1559
1560        let insert2 = Insert::from(table.clone())
1561            .set(0, 2i64)
1562            .unwrap()
1563            .set(1, "bob")
1564            .unwrap();
1565
1566        let builder = ChangesetBuilder::new().insert(insert1).insert(insert2);
1567
1568        assert_eq!(builder.len(), 2);
1569    }
1570
1571    #[test]
1572    fn test_update_then_update_consolidates() {
1573        let table = TestTable::new("users", 2, 0);
1574
1575        let update1 = Update::<TestTable, ChangesetFormat, String, Vec<u8>>::from(table.clone())
1576            .set(0, 1i64, 1i64)
1577            .unwrap()
1578            .set(1, "alice", "bob")
1579            .unwrap();
1580
1581        let update2 = Update::<TestTable, ChangesetFormat, String, Vec<u8>>::from(table.clone())
1582            .set(0, 1i64, 1i64)
1583            .unwrap()
1584            .set(1, "bob", "charlie")
1585            .unwrap();
1586
1587        let builder = ChangesetBuilder::new().update(update1).update(update2);
1588
1589        assert_eq!(builder.len(), 1);
1590        // Should be a single UPDATE from alice to charlie
1591    }
1592
1593    // ========================================================================
1594    // Reverse trait tests
1595    // ========================================================================
1596
1597    #[test]
1598    fn test_reverse_operation_insert_becomes_delete() {
1599        let op: Operation<ChangesetFormat, String, Vec<u8>> = Operation::Insert {
1600            values: vec![Value::Integer(1), Value::Text("alice".into())],
1601            indirect: false,
1602        };
1603        let reversed = op.reverse();
1604        let Operation::Delete { data, .. } = reversed else {
1605            panic!("Expected Delete operation");
1606        };
1607        assert_eq!(
1608            data,
1609            vec![
1610                Value::<String, Vec<u8>>::Integer(1),
1611                Value::Text("alice".into())
1612            ]
1613        );
1614    }
1615
1616    #[test]
1617    fn test_reverse_operation_delete_becomes_insert() {
1618        let op: Operation<ChangesetFormat, String, Vec<u8>> = Operation::Delete {
1619            data: vec![Value::Integer(1), Value::Text("alice".into())],
1620            indirect: false,
1621        };
1622        let reversed = op.reverse();
1623        let Operation::Insert { values, .. } = reversed else {
1624            panic!("Expected Insert operation");
1625        };
1626        assert_eq!(
1627            values,
1628            vec![
1629                Value::<String, Vec<u8>>::Integer(1),
1630                Value::Text("alice".into())
1631            ]
1632        );
1633    }
1634
1635    #[test]
1636    fn test_reverse_operation_update_swaps_old_new() {
1637        let op: Operation<ChangesetFormat, String, Vec<u8>> = Operation::Update {
1638            values: vec![
1639                (Some(Value::Integer(1)), Some(Value::Integer(1))),
1640                (
1641                    Some(Value::Text("alice".into())),
1642                    Some(Value::Text("bob".into())),
1643                ),
1644            ],
1645            indirect: false,
1646        };
1647        let reversed = op.reverse();
1648        let Operation::Update { values, .. } = reversed else {
1649            panic!("Expected Update operation");
1650        };
1651        assert_eq!(
1652            values[0],
1653            (Some(Value::Integer(1)), Some(Value::Integer(1)))
1654        );
1655        assert_eq!(
1656            values[1],
1657            (
1658                Some(Value::Text("bob".into())),
1659                Some(Value::Text("alice".into()))
1660            )
1661        );
1662    }
1663
1664    #[test]
1665    fn test_reverse_builder_insert_becomes_delete() {
1666        let table = TestTable::new("users", 2, 0);
1667        let insert = Insert::from(table.clone())
1668            .set(0, 1i64)
1669            .unwrap()
1670            .set(1, "alice")
1671            .unwrap();
1672
1673        let builder = ChangesetBuilder::new().insert(insert);
1674        let reversed = builder.reverse();
1675
1676        assert_eq!(reversed.len(), 1);
1677        // The reversed builder should have a delete operation
1678        let rows = reversed.tables.get(&table).unwrap();
1679        assert!(matches!(
1680            rows.values().next().unwrap(),
1681            Operation::Delete { .. }
1682        ));
1683    }
1684
1685    #[test]
1686    fn test_reverse_builder_delete_becomes_insert() {
1687        let table = TestTable::new("users", 2, 0);
1688        let delete = ChangeDelete::from(table.clone())
1689            .set(0, 1i64)
1690            .unwrap()
1691            .set(1, "alice")
1692            .unwrap();
1693
1694        let builder = ChangesetBuilder::new().delete(delete);
1695        let reversed = builder.reverse();
1696
1697        assert_eq!(reversed.len(), 1);
1698        // The reversed builder should have an insert operation
1699        let rows = reversed.tables.get(&table).unwrap();
1700        assert!(matches!(
1701            rows.values().next().unwrap(),
1702            Operation::Insert { .. }
1703        ));
1704    }
1705
1706    #[test]
1707    fn test_reverse_builder_update_swaps() {
1708        let table = TestTable::new("users", 2, 0);
1709        let update = Update::<TestTable, ChangesetFormat, String, Vec<u8>>::from(table.clone())
1710            .set(0, 1i64, 1i64)
1711            .unwrap()
1712            .set(1, "alice", "bob")
1713            .unwrap();
1714
1715        let builder = ChangesetBuilder::new().update(update);
1716        let reversed = builder.reverse();
1717
1718        assert_eq!(reversed.len(), 1);
1719        // The reversed builder should have an update operation with swapped values
1720        let rows = reversed.tables.get(&table).unwrap();
1721        let Operation::Update { values, .. } = rows.values().next().unwrap() else {
1722            panic!("Expected Update operation");
1723        };
1724        assert_eq!(
1725            values[1],
1726            (
1727                Some(Value::Text("bob".into())),
1728                Some(Value::Text("alice".into()))
1729            )
1730        );
1731    }
1732
1733    #[test]
1734    fn test_reverse_is_involutory() {
1735        // reverse(reverse(x)) == x
1736        let table = TestTable::new("users", 2, 0);
1737        let insert = Insert::from(table.clone())
1738            .set(0, 1i64)
1739            .unwrap()
1740            .set(1, "alice")
1741            .unwrap();
1742
1743        let original_values = insert.into_values();
1744        let insert2 = Insert::from(table.clone())
1745            .set(0, 1i64)
1746            .unwrap()
1747            .set(1, "alice")
1748            .unwrap();
1749        let builder = ChangesetBuilder::new().insert(insert2);
1750        let double_reversed = builder.reverse().reverse();
1751
1752        assert_eq!(double_reversed.len(), 1);
1753        let rows = double_reversed.tables.get(&table).unwrap();
1754        let Operation::Insert { values, .. } = rows.values().next().unwrap() else {
1755            panic!("Expected Insert operation");
1756        };
1757        assert_eq!(values, &original_values);
1758    }
1759
1760    // ========================================================================
1761    // Build (serialization) tests
1762    // ========================================================================
1763
1764    #[test]
1765    fn test_build_empty_builder() {
1766        let builder = ChangesetBuilder::new();
1767        let bytes = builder.build();
1768        assert!(bytes.is_empty());
1769    }
1770
1771    #[test]
1772    fn test_build_insert_format() {
1773        let table = TestTable::new("t", 2, 0);
1774        let insert = Insert::from(table.clone())
1775            .set(0, 1i64)
1776            .unwrap()
1777            .set(1, "a")
1778            .unwrap();
1779
1780        let builder = ChangesetBuilder::new().insert(insert);
1781        let bytes = builder.build();
1782
1783        // Verify the structure:
1784        // Table header: 'T', col_count(2), pk_flags(1,0), name("t\0")
1785        // Operation: INSERT(0x12), indirect(0), values...
1786        assert!(!bytes.is_empty());
1787
1788        // Check table marker
1789        assert_eq!(bytes[0], b'T');
1790        // Column count
1791        assert_eq!(bytes[1], 2);
1792        // PK flags: first column is PK
1793        assert_eq!(bytes[2], 1);
1794        assert_eq!(bytes[3], 0);
1795        // Table name "t" + null terminator
1796        assert_eq!(bytes[4], b't');
1797        assert_eq!(bytes[5], 0);
1798        // Operation code: INSERT = 0x12
1799        assert_eq!(bytes[6], 0x12);
1800        // Indirect flag
1801        assert_eq!(bytes[7], 0);
1802    }
1803
1804    #[test]
1805    fn test_build_delete_format() {
1806        let table = TestTable::new("t", 2, 0);
1807        let delete = ChangeDelete::from(table.clone())
1808            .set(0, 1i64)
1809            .unwrap()
1810            .set(1, "a")
1811            .unwrap();
1812
1813        let builder = ChangesetBuilder::new().delete(delete);
1814        let bytes = builder.build();
1815
1816        assert!(!bytes.is_empty());
1817        assert_eq!(bytes[0], b'T');
1818        // Operation code: DELETE = 0x09
1819        assert_eq!(bytes[6], 0x09);
1820    }
1821
1822    #[test]
1823    fn test_build_update_format() {
1824        let table = TestTable::new("t", 2, 0);
1825        let update = Update::<TestTable, ChangesetFormat, String, Vec<u8>>::from(table.clone())
1826            .set(0, 1i64, 1i64)
1827            .unwrap()
1828            .set(1, "a", "b")
1829            .unwrap();
1830
1831        let builder = ChangesetBuilder::new().update(update);
1832        let bytes = builder.build();
1833
1834        assert!(!bytes.is_empty());
1835        assert_eq!(bytes[0], b'T');
1836        // Operation code: UPDATE = 0x17
1837        assert_eq!(bytes[6], 0x17);
1838    }
1839
1840    #[test]
1841    fn test_build_multiple_operations() {
1842        let table = TestTable::new("t", 2, 0);
1843
1844        let insert = Insert::from(table.clone())
1845            .set(0, 1i64)
1846            .unwrap()
1847            .set(1, "a")
1848            .unwrap();
1849
1850        let insert2 = Insert::from(table.clone())
1851            .set(0, 2i64)
1852            .unwrap()
1853            .set(1, "b")
1854            .unwrap();
1855
1856        let builder = ChangesetBuilder::new().insert(insert).insert(insert2);
1857        let bytes = builder.build();
1858
1859        assert!(!bytes.is_empty());
1860        // Should have one table header and two insert operations
1861        assert_eq!(bytes[0], b'T');
1862    }
1863
1864    #[test]
1865    fn test_build_cancelled_operations_produce_empty() {
1866        let table = TestTable::new("t", 2, 0);
1867
1868        let insert = Insert::from(table.clone())
1869            .set(0, 1i64)
1870            .unwrap()
1871            .set(1, "a")
1872            .unwrap();
1873
1874        let delete = ChangeDelete::from(table.clone())
1875            .set(0, 1i64)
1876            .unwrap()
1877            .set(1, "a")
1878            .unwrap();
1879
1880        let builder = ChangesetBuilder::new().insert(insert).delete(delete);
1881        let bytes = builder.build();
1882
1883        // INSERT + DELETE with same values cancels out
1884        assert!(bytes.is_empty());
1885    }
1886
1887    // ========================================================================
1888    // BitOr / BitOrAssign tests
1889    // ========================================================================
1890
1891    #[test]
1892    fn test_bitor_changeset_disjoint_rows() {
1893        let table = TestTable::new("users", 2, 0);
1894
1895        let insert1 = Insert::from(table.clone())
1896            .set(0, 1i64)
1897            .unwrap()
1898            .set(1, "alice")
1899            .unwrap();
1900
1901        let insert2 = Insert::from(table.clone())
1902            .set(0, 2i64)
1903            .unwrap()
1904            .set(1, "bob")
1905            .unwrap();
1906
1907        let cs1 = ChangesetBuilder::new().insert(insert1);
1908        let cs2 = ChangesetBuilder::new().insert(insert2);
1909
1910        let merged = cs1 | cs2;
1911        assert_eq!(merged.len(), 2);
1912    }
1913
1914    #[test]
1915    fn test_bitor_changeset_consolidates_same_row() {
1916        let table = TestTable::new("users", 2, 0);
1917
1918        // First changeset: INSERT row 1
1919        let insert = Insert::from(table.clone())
1920            .set(0, 1i64)
1921            .unwrap()
1922            .set(1, "alice")
1923            .unwrap();
1924
1925        // Second changeset: DELETE row 1
1926        let delete = ChangeDelete::from(table.clone())
1927            .set(0, 1i64)
1928            .unwrap()
1929            .set(1, "alice")
1930            .unwrap();
1931
1932        let cs1 = ChangesetBuilder::new().insert(insert);
1933        let cs2 = ChangesetBuilder::new().delete(delete);
1934
1935        // INSERT + DELETE with same values should cancel out
1936        let merged = cs1 | cs2;
1937        assert_eq!(merged.len(), 0);
1938        assert!(merged.is_empty());
1939    }
1940
1941    #[test]
1942    fn test_bitor_assign_changeset() {
1943        let table = TestTable::new("users", 2, 0);
1944
1945        let insert1 = Insert::from(table.clone())
1946            .set(0, 1i64)
1947            .unwrap()
1948            .set(1, "alice")
1949            .unwrap();
1950
1951        let insert2 = Insert::from(table.clone())
1952            .set(0, 2i64)
1953            .unwrap()
1954            .set(1, "bob")
1955            .unwrap();
1956
1957        let mut cs = ChangesetBuilder::new().insert(insert1);
1958        cs |= ChangesetBuilder::new().insert(insert2);
1959
1960        assert_eq!(cs.len(), 2);
1961    }
1962
1963    #[test]
1964    fn test_bitor_patchset_disjoint_rows() {
1965        type PatchsetBuilder = DiffSetBuilder<PatchsetFormat, TestTable, String, Vec<u8>>;
1966
1967        let table = TestTable::new("users", 2, 0);
1968
1969        let insert1 = Insert::from(table.clone())
1970            .set(0, 1i64)
1971            .unwrap()
1972            .set(1, "alice")
1973            .unwrap();
1974
1975        let insert2 = Insert::from(table.clone())
1976            .set(0, 2i64)
1977            .unwrap()
1978            .set(1, "bob")
1979            .unwrap();
1980
1981        let ps1 = PatchsetBuilder::new().insert(insert1);
1982        let ps2 = PatchsetBuilder::new().insert(insert2);
1983
1984        let merged = ps1 | ps2;
1985        assert_eq!(merged.len(), 2);
1986    }
1987
1988    #[test]
1989    fn test_bitor_patchset_consolidates_same_row() {
1990        type PatchsetBuilder = DiffSetBuilder<PatchsetFormat, TestTable, String, Vec<u8>>;
1991
1992        let table = TestTable::new("users", 2, 0);
1993
1994        // First patchset: INSERT row 1
1995        let insert = Insert::from(table.clone())
1996            .set(0, 1i64)
1997            .unwrap()
1998            .set(1, "alice")
1999            .unwrap();
2000
2001        // Second patchset: DELETE row 1
2002        let delete = PatchDelete::new(table.clone(), vec![Value::Integer(1)]);
2003
2004        let ps1 = PatchsetBuilder::new().insert(insert);
2005        let ps2 = PatchsetBuilder::new().delete(delete);
2006
2007        // INSERT + DELETE should cancel out
2008        let merged = ps1 | ps2;
2009        assert_eq!(merged.len(), 0);
2010        assert!(merged.is_empty());
2011    }
2012
2013    #[test]
2014    fn test_bitor_multiple_tables() {
2015        let table1 = TestTable::new("users", 2, 0);
2016        let table2 = TestTable::new("posts", 2, 0);
2017
2018        let insert1 = Insert::from(table1.clone())
2019            .set(0, 1i64)
2020            .unwrap()
2021            .set(1, "alice")
2022            .unwrap();
2023
2024        let insert2 = Insert::from(table2.clone())
2025            .set(0, 100i64)
2026            .unwrap()
2027            .set(1, "first post")
2028            .unwrap();
2029
2030        let cs1 = ChangesetBuilder::new().insert(insert1);
2031        let cs2 = ChangesetBuilder::new().insert(insert2);
2032
2033        let merged = cs1 | cs2;
2034        assert_eq!(merged.len(), 2);
2035    }
2036
2037    #[test]
2038    fn test_bitor_insert_then_update_consolidates() {
2039        let table = TestTable::new("users", 2, 0);
2040
2041        // First changeset: INSERT row 1
2042        let insert = Insert::from(table.clone())
2043            .set(0, 1i64)
2044            .unwrap()
2045            .set(1, "alice")
2046            .unwrap();
2047
2048        // Second changeset: UPDATE row 1
2049        let update = Update::<TestTable, ChangesetFormat, String, Vec<u8>>::from(table.clone())
2050            .set(0, 1i64, 1i64)
2051            .unwrap()
2052            .set(1, "alice", "alicia")
2053            .unwrap();
2054
2055        let cs1 = ChangesetBuilder::new().insert(insert);
2056        let cs2 = ChangesetBuilder::new().update(update);
2057
2058        // INSERT + UPDATE should consolidate to INSERT with final values
2059        let merged = cs1 | cs2;
2060        assert_eq!(merged.len(), 1);
2061    }
2062
2063    #[test]
2064    fn test_session_hash_growth_with_many_rows() {
2065        // The simulated session hash table grows from 256 buckets when entries
2066        // reach 128. Insert 200 rows so the rehash branch in session_row_order
2067        // is exercised. Builds the binary and reparses to confirm consistency.
2068        let table = TestTable::new("many", 2, 0);
2069        let mut cs = ChangesetBuilder::new();
2070        for i in 0..200i64 {
2071            let insert = Insert::from(table.clone())
2072                .set(0, i)
2073                .unwrap()
2074                .set(1, alloc::format!("row-{i}"))
2075                .unwrap();
2076            cs = cs.insert(insert);
2077        }
2078        assert_eq!(cs.len(), 200);
2079
2080        let bytes = cs.build();
2081        assert!(!bytes.is_empty());
2082
2083        // Reparse the binary to make sure 200 ops survived the round-trip.
2084        let reparsed = crate::parser::ParsedDiffSet::try_from(bytes.as_slice()).unwrap();
2085        let reparsed_bytes: Vec<u8> = reparsed.into();
2086        assert_eq!(bytes, reparsed_bytes);
2087    }
2088
2089    // ========================================================================
2090    // Indirect-flag tests
2091    // ========================================================================
2092
2093    /// Header offset of the indirect byte within a single-op single-table
2094    /// changeset. Layout: 'T' marker (1) + col_count (1) + pk_flags (2)
2095    /// + table_name "t\0" (2) + op_code (1) = 7. Patchset is identical.
2096    const INDIRECT_BYTE_OFFSET: usize = 7;
2097
2098    #[test]
2099    fn test_build_insert_indirect_byte_set() {
2100        let table = TestTable::new("t", 2, 0);
2101        let insert = Insert::from(table)
2102            .set(0, 1i64)
2103            .unwrap()
2104            .set(1, "a")
2105            .unwrap()
2106            .indirect(true);
2107
2108        let bytes = ChangesetBuilder::new().insert(insert).build();
2109        assert_eq!(bytes[INDIRECT_BYTE_OFFSET], 1);
2110    }
2111
2112    #[test]
2113    fn test_build_delete_indirect_byte_set() {
2114        let table = TestTable::new("t", 2, 0);
2115        let delete = ChangeDelete::from(table)
2116            .set(0, 1i64)
2117            .unwrap()
2118            .set(1, "a")
2119            .unwrap()
2120            .indirect(true);
2121
2122        let bytes = ChangesetBuilder::new().delete(delete).build();
2123        assert_eq!(bytes[INDIRECT_BYTE_OFFSET], 1);
2124    }
2125
2126    #[test]
2127    fn test_build_update_indirect_byte_set() {
2128        let table = TestTable::new("t", 2, 0);
2129        let update = Update::<TestTable, ChangesetFormat, String, Vec<u8>>::from(table)
2130            .set(0, 1i64, 1i64)
2131            .unwrap()
2132            .set(1, "a", "b")
2133            .unwrap()
2134            .indirect(true);
2135
2136        let bytes = ChangesetBuilder::new().update(update).build();
2137        assert_eq!(bytes[INDIRECT_BYTE_OFFSET], 1);
2138    }
2139
2140    #[test]
2141    fn test_build_patchset_insert_indirect_byte() {
2142        let table = TestTable::new("t", 2, 0);
2143        let insert = Insert::from(table)
2144            .set(0, 1i64)
2145            .unwrap()
2146            .set(1, "a")
2147            .unwrap()
2148            .indirect(true);
2149
2150        let patchset: PatchSet<TestTable, String, Vec<u8>> = PatchSet::new().insert(insert);
2151        let bytes = patchset.build();
2152        assert_eq!(bytes[INDIRECT_BYTE_OFFSET], 1);
2153    }
2154
2155    #[test]
2156    fn test_reverse_preserves_indirect() {
2157        // INSERT -> DELETE: indirect carries
2158        let insert: Operation<ChangesetFormat, String, Vec<u8>> = Operation::Insert {
2159            values: vec![Value::Integer(1)],
2160            indirect: true,
2161        };
2162        let reversed = insert.reverse();
2163        assert!(reversed.indirect());
2164        assert!(matches!(reversed, Operation::Delete { .. }));
2165
2166        // DELETE -> INSERT: indirect carries
2167        let delete: Operation<ChangesetFormat, String, Vec<u8>> = Operation::Delete {
2168            data: vec![Value::Integer(1)],
2169            indirect: true,
2170        };
2171        let reversed = delete.reverse();
2172        assert!(reversed.indirect());
2173        assert!(matches!(reversed, Operation::Insert { .. }));
2174
2175        // UPDATE -> UPDATE swapped: indirect carries
2176        let update: Operation<ChangesetFormat, String, Vec<u8>> = Operation::Update {
2177            values: vec![(Some(Value::Integer(1)), Some(Value::Integer(2)))],
2178            indirect: true,
2179        };
2180        let reversed = update.reverse();
2181        assert!(reversed.indirect());
2182        assert!(matches!(reversed, Operation::Update { .. }));
2183    }
2184
2185    #[test]
2186    fn test_patchdelete_indirect_byte_set() {
2187        let table = TestTable::new("t", 2, 0);
2188        let delete: PatchDelete<TestTable, String, Vec<u8>> =
2189            PatchDelete::new(table, vec![Value::Integer(1)]).indirect(true);
2190        let bytes = PatchSet::new().delete(delete).build();
2191        assert_eq!(bytes[INDIRECT_BYTE_OFFSET], 1);
2192    }
2193
2194    #[test]
2195    fn test_operation_eq_indirect_differs() {
2196        // Two ops with identical payload but different indirect flags must not be equal.
2197        let a: Operation<ChangesetFormat, String, Vec<u8>> = Operation::Insert {
2198            values: vec![Value::Integer(1)],
2199            indirect: false,
2200        };
2201        let b: Operation<ChangesetFormat, String, Vec<u8>> = Operation::Insert {
2202            values: vec![Value::Integer(1)],
2203            indirect: true,
2204        };
2205        assert_ne!(a, b);
2206    }
2207
2208    #[test]
2209    fn test_operation_eq_variant_mismatch() {
2210        // Different variants must compare unequal regardless of payload.
2211        let insert: Operation<ChangesetFormat, String, Vec<u8>> = Operation::Insert {
2212            values: vec![Value::Integer(1)],
2213            indirect: false,
2214        };
2215        let delete: Operation<ChangesetFormat, String, Vec<u8>> = Operation::Delete {
2216            data: vec![Value::Integer(1)],
2217            indirect: false,
2218        };
2219        assert_ne!(insert, delete);
2220    }
2221
2222    #[test]
2223    fn test_bitor_indirect_rhs_wins() {
2224        // Two INSERTs on the same PK with opposite indirect bits. The merged
2225        // op should carry the rhs's bit (last-write-wins).
2226        let table = TestTable::new("t", 2, 0);
2227        let lhs_insert = Insert::from(table.clone())
2228            .set(0, 1i64)
2229            .unwrap()
2230            .set(1, "a")
2231            .unwrap()
2232            .indirect(true);
2233        let rhs_insert = Insert::from(table.clone())
2234            .set(0, 1i64)
2235            .unwrap()
2236            .set(1, "a")
2237            .unwrap()
2238            .indirect(false);
2239
2240        let merged =
2241            ChangesetBuilder::new().insert(lhs_insert) | ChangesetBuilder::new().insert(rhs_insert);
2242        let rows = merged.tables.get(&table).unwrap();
2243        let op = rows.values().next().unwrap();
2244        assert!(!op.indirect(), "rhs (false) should win over lhs (true)");
2245
2246        // Reverse direction: rhs=true wins over lhs=false.
2247        let lhs_insert = Insert::from(table.clone())
2248            .set(0, 2i64)
2249            .unwrap()
2250            .set(1, "b")
2251            .unwrap()
2252            .indirect(false);
2253        let rhs_insert = Insert::from(table.clone())
2254            .set(0, 2i64)
2255            .unwrap()
2256            .set(1, "b")
2257            .unwrap()
2258            .indirect(true);
2259
2260        let merged =
2261            ChangesetBuilder::new().insert(lhs_insert) | ChangesetBuilder::new().insert(rhs_insert);
2262        let rows = merged.tables.get(&table).unwrap();
2263        let op = rows.values().next().unwrap();
2264        assert!(op.indirect(), "rhs (true) should win over lhs (false)");
2265    }
2266
2267    #[test]
2268    fn test_roundtrip_indirect_changeset() {
2269        let table = TestTable::new("t", 2, 0);
2270        let insert = Insert::from(table)
2271            .set(0, 1i64)
2272            .unwrap()
2273            .set(1, "a")
2274            .unwrap()
2275            .indirect(true);
2276
2277        let bytes = ChangesetBuilder::new().insert(insert).build();
2278        let reparsed = crate::parser::ParsedDiffSet::try_from(bytes.as_slice()).unwrap();
2279        let reparsed_bytes: Vec<u8> = reparsed.into();
2280        assert_eq!(bytes, reparsed_bytes);
2281        assert_eq!(reparsed_bytes[INDIRECT_BYTE_OFFSET], 1);
2282    }
2283
2284    #[test]
2285    fn test_roundtrip_indirect_patchset() {
2286        let table = TestTable::new("t", 2, 0);
2287        let insert = Insert::from(table)
2288            .set(0, 1i64)
2289            .unwrap()
2290            .set(1, "a")
2291            .unwrap()
2292            .indirect(true);
2293
2294        let patchset: PatchSet<TestTable, String, Vec<u8>> = PatchSet::new().insert(insert);
2295        let bytes = patchset.build();
2296        let reparsed = crate::parser::ParsedDiffSet::try_from(bytes.as_slice()).unwrap();
2297        let reparsed_bytes: Vec<u8> = reparsed.into();
2298        assert_eq!(bytes, reparsed_bytes);
2299        assert_eq!(reparsed_bytes[INDIRECT_BYTE_OFFSET], 1);
2300    }
2301
2302    #[test]
2303    fn test_indirect_full_pipeline_roundtrip() {
2304        // Serialize -> parse -> reverse -> reverse -> BitOr(empty) -> re-serialize.
2305        // The indirect bit must survive every stage.
2306        let table = TestTable::new("t", 2, 0);
2307        let insert = Insert::from(table)
2308            .set(0, 1i64)
2309            .unwrap()
2310            .set(1, "alice")
2311            .unwrap()
2312            .indirect(true);
2313        let original = ChangesetBuilder::new().insert(insert);
2314        let bytes_a = original.build();
2315        assert_eq!(bytes_a[INDIRECT_BYTE_OFFSET], 1);
2316
2317        // Parse the bytes back into a builder over TableSchema<String>.
2318        let parsed = crate::parser::ParsedDiffSet::try_from(bytes_a.as_slice()).unwrap();
2319        let crate::parser::ParsedDiffSet::Changeset(parsed_set) = parsed else {
2320            panic!("expected Changeset variant");
2321        };
2322
2323        let parsed_builder: DiffSetBuilder<
2324            ChangesetFormat,
2325            crate::parser::TableSchema<String>,
2326            String,
2327            Vec<u8>,
2328        > = parsed_set.into();
2329        let empty: DiffSetBuilder<
2330            ChangesetFormat,
2331            crate::parser::TableSchema<String>,
2332            String,
2333            Vec<u8>,
2334        > = DiffSetBuilder::new();
2335        let doubled = parsed_builder.reverse().reverse();
2336        let merged = doubled | empty;
2337
2338        let bytes_b = merged.build();
2339        assert_eq!(bytes_a, bytes_b);
2340        assert_eq!(bytes_b[INDIRECT_BYTE_OFFSET], 1);
2341    }
2342
2343    // ========================================================================
2344    // Operation merge (Add) arms
2345    // ========================================================================
2346
2347    fn changeset_insert(v: i64) -> Operation<ChangesetFormat, String, Vec<u8>> {
2348        Operation::Insert {
2349            values: vec![Value::Integer(v), Value::Text("a".into())],
2350            indirect: false,
2351        }
2352    }
2353
2354    fn changeset_delete(v: i64) -> Operation<ChangesetFormat, String, Vec<u8>> {
2355        Operation::Delete {
2356            data: vec![Value::Integer(v), Value::Text("a".into())],
2357            indirect: false,
2358        }
2359    }
2360
2361    fn changeset_update(old: i64, new: i64) -> Operation<ChangesetFormat, String, Vec<u8>> {
2362        Operation::Update {
2363            values: vec![
2364                (Some(Value::Integer(old)), Some(Value::Integer(new))),
2365                (Some(Value::Text("a".into())), Some(Value::Text("b".into()))),
2366            ],
2367            indirect: false,
2368        }
2369    }
2370
2371    #[test]
2372    fn test_add_changeset_insert_plus_update() {
2373        let merged = (changeset_insert(1) + changeset_update(1, 2)).unwrap();
2374        let Operation::Insert { values, .. } = merged else {
2375            panic!("expected Insert");
2376        };
2377        assert_eq!(values[0], Value::Integer(2));
2378        assert_eq!(values[1], Value::Text("b".into()));
2379    }
2380
2381    #[test]
2382    fn test_add_changeset_update_plus_insert() {
2383        // UPDATE wins, values are the UPDATE's, indirect is rhs (=false here).
2384        let merged = (changeset_update(1, 2) + changeset_insert(99)).unwrap();
2385        assert!(matches!(merged, Operation::Update { .. }));
2386    }
2387
2388    #[test]
2389    fn test_add_changeset_update_plus_update() {
2390        // First old, last new.
2391        let merged = (changeset_update(1, 2) + changeset_update(2, 3)).unwrap();
2392        let Operation::Update { values, .. } = merged else {
2393            panic!("expected Update");
2394        };
2395        assert_eq!(values[0].0, Some(Value::Integer(1)));
2396        assert_eq!(values[0].1, Some(Value::Integer(3)));
2397    }
2398
2399    #[test]
2400    fn test_add_changeset_update_plus_delete() {
2401        // UPDATE+DELETE collapses to DELETE carrying the UPDATE's old values.
2402        let merged = (changeset_update(1, 2) + changeset_delete(99)).unwrap();
2403        let Operation::Delete { data, .. } = merged else {
2404            panic!("expected Delete");
2405        };
2406        assert_eq!(data[0], Value::Integer(1));
2407        assert_eq!(data[1], Value::Text("a".into()));
2408    }
2409
2410    #[test]
2411    fn test_add_changeset_delete_plus_update_keeps_delete() {
2412        let merged = (changeset_delete(1) + changeset_update(1, 2)).unwrap();
2413        assert!(matches!(merged, Operation::Delete { .. }));
2414    }
2415
2416    #[test]
2417    fn test_add_changeset_delete_plus_delete_keeps_first() {
2418        let merged = (changeset_delete(1) + changeset_delete(2)).unwrap();
2419        let Operation::Delete { data, .. } = merged else {
2420            panic!("expected Delete");
2421        };
2422        assert_eq!(data[0], Value::Integer(1));
2423    }
2424
2425    fn patchset_insert(v: i64) -> Operation<PatchsetFormat, String, Vec<u8>> {
2426        Operation::Insert {
2427            values: vec![Value::Integer(v), Value::Text("a".into())],
2428            indirect: false,
2429        }
2430    }
2431
2432    fn patchset_update(new: i64) -> Operation<PatchsetFormat, String, Vec<u8>> {
2433        Operation::Update {
2434            values: vec![
2435                ((), Some(Value::Integer(new))),
2436                ((), Some(Value::Text("b".into()))),
2437            ],
2438            indirect: false,
2439        }
2440    }
2441
2442    fn patchset_delete() -> Operation<PatchsetFormat, String, Vec<u8>> {
2443        Operation::Delete {
2444            data: (),
2445            indirect: false,
2446        }
2447    }
2448
2449    #[test]
2450    fn test_add_patchset_insert_plus_update() {
2451        let merged = (patchset_insert(1) + patchset_update(2)).unwrap();
2452        let Operation::Insert { values, .. } = merged else {
2453            panic!("expected Insert");
2454        };
2455        assert_eq!(values[0], Value::Integer(2));
2456        assert_eq!(values[1], Value::Text("b".into()));
2457    }
2458
2459    #[test]
2460    fn test_add_patchset_update_plus_insert() {
2461        let merged = (patchset_update(2) + patchset_insert(99)).unwrap();
2462        assert!(matches!(merged, Operation::Update { .. }));
2463    }
2464
2465    #[test]
2466    fn test_add_patchset_update_plus_update() {
2467        let merged = (patchset_update(2) + patchset_update(3)).unwrap();
2468        let Operation::Update { values, .. } = merged else {
2469            panic!("expected Update");
2470        };
2471        assert_eq!(values[0].1, Some(Value::Integer(3)));
2472    }
2473
2474    #[test]
2475    fn test_add_patchset_update_plus_delete() {
2476        let merged = (patchset_update(2) + patchset_delete()).unwrap();
2477        assert!(matches!(merged, Operation::Delete { .. }));
2478    }
2479
2480    #[test]
2481    fn test_add_patchset_delete_plus_insert_promotes_to_update() {
2482        // Patchset can't compare old values, so DELETE + INSERT always becomes UPDATE.
2483        let merged = (patchset_delete() + patchset_insert(1)).unwrap();
2484        assert!(matches!(merged, Operation::Update { .. }));
2485    }
2486
2487    #[test]
2488    fn test_add_patchset_delete_plus_update_keeps_delete() {
2489        let merged = (patchset_delete() + patchset_update(2)).unwrap();
2490        assert!(matches!(merged, Operation::Delete { .. }));
2491    }
2492
2493    #[test]
2494    fn test_add_patchset_delete_plus_delete_keeps_first() {
2495        let merged = (patchset_delete() + patchset_delete()).unwrap();
2496        assert!(matches!(merged, Operation::Delete { .. }));
2497    }
2498
2499    // ========================================================================
2500    // Session-hash coverage: PKs of every Value type
2501    // ========================================================================
2502
2503    #[test]
2504    fn test_session_hash_real_pk() {
2505        let table = TestTable::new("t", 2, 0);
2506        let insert = Insert::from(table)
2507            .set(0, 2.5f64)
2508            .unwrap()
2509            .set(1, "a")
2510            .unwrap();
2511        let bytes = ChangesetBuilder::new().insert(insert).build();
2512        assert!(!bytes.is_empty());
2513    }
2514
2515    #[test]
2516    fn test_session_hash_text_pk() {
2517        let table = TestTable::new("t", 2, 0);
2518        let insert = Insert::from(table)
2519            .set(0, "alice")
2520            .unwrap()
2521            .set(1, 42i64)
2522            .unwrap();
2523        let bytes = ChangesetBuilder::new().insert(insert).build();
2524        assert!(!bytes.is_empty());
2525    }
2526
2527    #[test]
2528    fn test_session_hash_blob_pk() {
2529        let table = TestTable::new("t", 2, 0);
2530        let insert = Insert::from(table)
2531            .set(0, alloc::vec![0xDE_u8, 0xAD, 0xBE, 0xEF])
2532            .unwrap()
2533            .set(1, "a")
2534            .unwrap();
2535        let bytes = ChangesetBuilder::new().insert(insert).build();
2536        assert!(!bytes.is_empty());
2537    }
2538
2539    // ========================================================================
2540    // session_row_order empty-rows short-circuit
2541    // ========================================================================
2542
2543    #[test]
2544    fn test_session_row_order_empty() {
2545        // An empty builder builds to empty bytes (no headers, no ops).
2546        let cs: ChangesetBuilder = ChangesetBuilder::new();
2547        let bytes = cs.build();
2548        assert!(bytes.is_empty());
2549    }
2550
2551    #[test]
2552    fn test_session_row_order_empty_rows_returns_empty_vec() {
2553        // Direct exercise of the empty-rows short-circuit (line 157-158).
2554        let rows: RowMap<ChangesetFormat, String, Vec<u8>> = IndexMap::default();
2555        assert!(session_row_order(&rows).is_empty());
2556    }
2557
2558    #[test]
2559    fn test_diffset_patchset_build_skips_empty_table() {
2560        // A patchset DiffSet with a registered-but-empty table builds to nothing.
2561        let table = TestTable::new("t", 2, 0);
2562        let mut builder: PatchSet<TestTable, String, Vec<u8>> = PatchSet::new();
2563        builder.add_table(&table);
2564        let frozen: DiffSet<PatchsetFormat, TestTable, String, Vec<u8>> = builder.into();
2565        let bytes = frozen.build();
2566        assert!(bytes.is_empty());
2567    }
2568
2569    // ========================================================================
2570    // From<DiffSetBuilder> / From<&DiffSetBuilder> / From<DiffSet> / From<&DiffSet> for Vec<u8>
2571    // ========================================================================
2572
2573    #[test]
2574    fn test_from_changeset_builder_into_vec() {
2575        let table = TestTable::new("t", 2, 0);
2576        let insert = Insert::from(table)
2577            .set(0, 1i64)
2578            .unwrap()
2579            .set(1, "a")
2580            .unwrap();
2581        let builder = ChangesetBuilder::new().insert(insert);
2582        let bytes_owned: Vec<u8> = builder.clone().into();
2583        let bytes_ref: Vec<u8> = (&builder).into();
2584        assert_eq!(bytes_owned, bytes_ref);
2585        let frozen: DiffSet<ChangesetFormat, TestTable, String, Vec<u8>> = builder.into();
2586        let bytes_frozen_owned: Vec<u8> = frozen.clone().into();
2587        let bytes_frozen_ref: Vec<u8> = (&frozen).into();
2588        assert_eq!(bytes_frozen_owned, bytes_frozen_ref);
2589    }
2590
2591    #[test]
2592    fn test_from_patchset_builder_into_vec() {
2593        let table = TestTable::new("t", 2, 0);
2594        let insert = Insert::from(table)
2595            .set(0, 1i64)
2596            .unwrap()
2597            .set(1, "a")
2598            .unwrap();
2599        let builder: PatchSet<TestTable, String, Vec<u8>> = PatchSet::new().insert(insert);
2600        let bytes_owned: Vec<u8> = builder.clone().into();
2601        let bytes_ref: Vec<u8> = (&builder).into();
2602        assert_eq!(bytes_owned, bytes_ref);
2603        let frozen: DiffSet<PatchsetFormat, TestTable, String, Vec<u8>> = builder.into();
2604        let bytes_frozen_owned: Vec<u8> = frozen.clone().into();
2605        let bytes_frozen_ref: Vec<u8> = (&frozen).into();
2606        assert_eq!(bytes_frozen_owned, bytes_frozen_ref);
2607    }
2608
2609    // ========================================================================
2610    // add_operation INSERT+UPDATE pk-change branch
2611    // ========================================================================
2612
2613    #[test]
2614    fn test_add_operation_insert_then_update_changes_pk() {
2615        // Insert id=1, then update id=1 to id=2. Triggers the special-case branch
2616        // in add_operation that re-extracts the PK from the merged INSERT values.
2617        let table = TestTable::new("t", 2, 0);
2618        let insert = Insert::from(table.clone())
2619            .set(0, 1i64)
2620            .unwrap()
2621            .set(1, "alice")
2622            .unwrap();
2623        let update = Update::<TestTable, ChangesetFormat, String, Vec<u8>>::from(table.clone())
2624            .set(0, 1i64, 2i64)
2625            .unwrap()
2626            .set(1, "alice", "bob")
2627            .unwrap();
2628        let builder = ChangesetBuilder::new().insert(insert).update(update);
2629        let rows = builder.tables.get(&table).unwrap();
2630        assert_eq!(rows.len(), 1);
2631        // The row should now be keyed by id=2.
2632        let (pk, op) = rows.iter().next().unwrap();
2633        assert_eq!(pk[0], Value::Integer(2));
2634        let Operation::Insert { values, .. } = op else {
2635            panic!("expected merged INSERT");
2636        };
2637        assert_eq!(values[0], Value::Integer(2));
2638        assert_eq!(values[1], Value::Text("bob".into()));
2639    }
2640
2641    // ========================================================================
2642    // DiffOps for DiffSet<F> wrappers
2643    // ========================================================================
2644
2645    #[test]
2646    fn test_diffset_changeset_diffops_wrappers() {
2647        let table = TestTable::new("t", 2, 0);
2648        let initial = Insert::from(table.clone())
2649            .set(0, 1i64)
2650            .unwrap()
2651            .set(1, "a")
2652            .unwrap();
2653        let frozen: DiffSet<ChangesetFormat, TestTable, String, Vec<u8>> =
2654            ChangesetBuilder::new().insert(initial).into();
2655
2656        let insert2 = Insert::from(table.clone())
2657            .set(0, 2i64)
2658            .unwrap()
2659            .set(1, "b")
2660            .unwrap();
2661        let after_insert = <_ as DiffOps<_, _, _>>::insert(frozen.clone(), insert2);
2662        assert_eq!(after_insert.len(), 2);
2663
2664        let delete = ChangeDelete::from(table.clone())
2665            .set(0, 1i64)
2666            .unwrap()
2667            .set(1, "a")
2668            .unwrap();
2669        let after_delete = <_ as DiffOps<_, _, _>>::delete(frozen.clone(), delete);
2670        assert_eq!(after_delete.len(), 0);
2671
2672        let update = Update::<TestTable, ChangesetFormat, String, Vec<u8>>::from(table)
2673            .set(0, 1i64, 1i64)
2674            .unwrap()
2675            .set(1, "a", "z")
2676            .unwrap();
2677        let after_update = <_ as DiffOps<_, _, _>>::update(frozen, update);
2678        assert_eq!(after_update.len(), 1);
2679    }
2680
2681    #[test]
2682    fn test_diffset_patchset_diffops_wrappers() {
2683        let table = TestTable::new("t", 2, 0);
2684        let initial = Insert::from(table.clone())
2685            .set(0, 1i64)
2686            .unwrap()
2687            .set(1, "a")
2688            .unwrap();
2689        let frozen: DiffSet<PatchsetFormat, TestTable, String, Vec<u8>> =
2690            PatchSet::new().insert(initial).into();
2691
2692        let insert2 = Insert::from(table.clone())
2693            .set(0, 2i64)
2694            .unwrap()
2695            .set(1, "b")
2696            .unwrap();
2697        let after_insert = <_ as DiffOps<_, _, _>>::insert(frozen.clone(), insert2);
2698        assert_eq!(after_insert.len(), 2);
2699
2700        let delete: PatchDelete<TestTable, String, Vec<u8>> =
2701            PatchDelete::new(table.clone(), vec![Value::Integer(1)]);
2702        let after_delete = <_ as DiffOps<_, _, _>>::delete(frozen.clone(), delete);
2703        // Insert(id=1) + PatchDelete(id=1) cancel out.
2704        assert_eq!(after_delete.len(), 0);
2705
2706        let update = Update::<TestTable, PatchsetFormat, String, Vec<u8>>::from(table)
2707            .set(0, 1i64)
2708            .unwrap()
2709            .set(1, "z")
2710            .unwrap();
2711        let after_update = <_ as DiffOps<_, _, _>>::update(frozen, update);
2712        assert_eq!(after_update.len(), 1);
2713    }
2714
2715    #[test]
2716    fn test_diffset_changeset_iter_yields_inserts_and_indirect_flag() {
2717        let table = TestTable::new("t", 2, 0);
2718        let direct = Insert::from(table.clone()).set(0, 1i64).unwrap();
2719        let indirect = Insert::from(table.clone())
2720            .set(0, 2i64)
2721            .unwrap()
2722            .indirect(true);
2723        let frozen: DiffSet<ChangesetFormat, TestTable, String, Vec<u8>> = ChangesetBuilder::new()
2724            .insert(direct)
2725            .insert(indirect)
2726            .into();
2727
2728        let ops: Vec<_> = frozen.iter().collect();
2729        assert_eq!(ops.len(), 2);
2730        for op in &ops {
2731            assert_eq!(crate::DynTable::name(op.table()), "t");
2732        }
2733        assert!(matches!(
2734            ops[0],
2735            ChangesetOp::Insert {
2736                indirect: false,
2737                ..
2738            }
2739        ));
2740        assert!(matches!(ops[1], ChangesetOp::Insert { indirect: true, .. }));
2741    }
2742
2743    #[test]
2744    fn test_diffset_changeset_iter_yields_update_and_delete() {
2745        let table = TestTable::new("t", 2, 0);
2746        let starting = Insert::from(table.clone())
2747            .set(0, 1i64)
2748            .unwrap()
2749            .set(1, "a")
2750            .unwrap();
2751        let update = Update::<TestTable, ChangesetFormat, String, Vec<u8>>::from(table.clone())
2752            .set(0, 2i64, 2i64)
2753            .unwrap()
2754            .set(1, "before", "after")
2755            .unwrap();
2756        let delete = ChangeDelete::from(table.clone())
2757            .set(0, 3i64)
2758            .unwrap()
2759            .set(1, "gone")
2760            .unwrap();
2761        let frozen: DiffSet<ChangesetFormat, TestTable, String, Vec<u8>> = ChangesetBuilder::new()
2762            .insert(starting)
2763            .update(update)
2764            .delete(delete)
2765            .into();
2766
2767        let kinds: Vec<&'static str> = frozen
2768            .iter()
2769            .map(|op| match op {
2770                ChangesetOp::Insert { .. } => "insert",
2771                ChangesetOp::Update { .. } => "update",
2772                ChangesetOp::Delete { .. } => "delete",
2773            })
2774            .collect();
2775        assert_eq!(kinds, ["insert", "update", "delete"]);
2776    }
2777
2778    #[test]
2779    fn test_diffset_patchset_iter_exposes_pk_for_delete_and_update() {
2780        let table = TestTable::new("t", 2, 0);
2781        let starting = Insert::from(table.clone())
2782            .set(0, 1i64)
2783            .unwrap()
2784            .set(1, "a")
2785            .unwrap();
2786        let update = Update::<TestTable, PatchsetFormat, String, Vec<u8>>::from(table.clone())
2787            .set(0, 5i64)
2788            .unwrap()
2789            .set(1, "z")
2790            .unwrap();
2791        let delete: PatchDelete<TestTable, String, Vec<u8>> =
2792            PatchDelete::new(table.clone(), vec![Value::Integer(7)]);
2793
2794        let frozen: DiffSet<PatchsetFormat, TestTable, String, Vec<u8>> = PatchSet::new()
2795            .insert(starting)
2796            .update(update)
2797            .delete(delete)
2798            .into();
2799
2800        let mut saw_insert = false;
2801        let mut saw_update_pk: Option<i64> = None;
2802        let mut saw_delete_pk: Option<i64> = None;
2803        for op in frozen.iter() {
2804            match op {
2805                PatchsetOp::Insert { values, .. } => {
2806                    saw_insert = true;
2807                    assert!(matches!(values[0], Value::Integer(1)));
2808                }
2809                PatchsetOp::Update { pk, .. } => {
2810                    if let Value::Integer(id) = pk[0] {
2811                        saw_update_pk = Some(id);
2812                    }
2813                }
2814                PatchsetOp::Delete { pk, .. } => {
2815                    if let Value::Integer(id) = pk[0] {
2816                        saw_delete_pk = Some(id);
2817                    }
2818                }
2819            }
2820        }
2821        assert!(saw_insert);
2822        assert_eq!(saw_update_pk, Some(5));
2823        assert_eq!(saw_delete_pk, Some(7));
2824    }
2825
2826    #[test]
2827    fn test_diffset_tables_skips_empty() {
2828        let t1 = TestTable::new("t1", 2, 0);
2829        let t2 = TestTable::new("t2", 2, 0);
2830        let insert_t1 = Insert::from(t1.clone()).set(0, 1i64).unwrap();
2831        let insert_then_delete_t2 = Insert::from(t2.clone()).set(0, 9i64).unwrap();
2832        let delete_t2 = ChangeDelete::from(t2.clone()).set(0, 9i64).unwrap();
2833        let frozen: DiffSet<ChangesetFormat, TestTable, String, Vec<u8>> = ChangesetBuilder::new()
2834            .insert(insert_t1)
2835            .insert(insert_then_delete_t2)
2836            .delete(delete_t2)
2837            .into();
2838
2839        let names: Vec<&str> = frozen.tables().map(crate::DynTable::name).collect();
2840        assert_eq!(names, ["t1"]);
2841    }
2842}