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