Skip to main content

sqlite_diff_rs/builders/
change.rs

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