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 + Debug + AsRef<str>, B: Clone + Debug + AsRef<[u8]>>
864    DiffSetBuilder<ChangesetFormat, T, S, B>
865{
866    /// Walk operations grouped by table in DML insertion order.
867    ///
868    /// Mirrors [`DiffSet::iter`] but keeps insertion order; the
869    /// session-extension hash ordering only applies at [`build`](Self::build)
870    /// time.
871    pub fn iter(&self) -> impl Iterator<Item = ChangesetOp<'_, T, S, B>> {
872        self.tables.iter().flat_map(|(table, rows)| {
873            rows.iter().map(move |(_pk, op)| match op {
874                Operation::Insert { values, indirect } => ChangesetOp::Insert {
875                    table,
876                    values: values.as_slice(),
877                    indirect: *indirect,
878                },
879                Operation::Update { values, indirect } => ChangesetOp::Update {
880                    table,
881                    values: values.as_slice(),
882                    indirect: *indirect,
883                },
884                Operation::Delete { data, indirect } => ChangesetOp::Delete {
885                    table,
886                    old_values: data.as_slice(),
887                    indirect: *indirect,
888                },
889            })
890        })
891    }
892}
893
894impl<T: SchemaWithPK, S: Clone + Hash + Eq + AsRef<str>, B: Clone + Hash + Eq + AsRef<[u8]>>
895    DiffSetBuilder<PatchsetFormat, T, S, B>
896{
897    /// Build the patchset binary data.
898    ///
899    /// Returns the binary representation compatible with `SQLite`'s session extension.
900    ///
901    /// # Panics
902    ///
903    /// This function does not panic under normal usage. Internal indexing is guaranteed
904    /// to be within bounds.
905    #[must_use]
906    pub fn build(&self) -> Vec<u8> {
907        let mut out = Vec::new();
908
909        for (table, rows) in &self.tables {
910            if rows.is_empty() {
911                continue;
912            }
913
914            write_table_header(&mut out, markers::PATCHSET, table);
915
916            let (pk_flags, pk_col_to_pk_pos) = patchset_pk_mapping(table);
917
918            for idx in session_row_order(rows) {
919                let (pk, op) = rows.get_index(idx).unwrap();
920                encode_patchset_op(&mut out, op, pk, &pk_flags, &pk_col_to_pk_pos);
921            }
922        }
923
924        out
925    }
926}
927
928impl<T: SchemaWithPK, S: Clone + AsRef<str>, B: Clone + AsRef<[u8]>>
929    DiffSetBuilder<PatchsetFormat, T, S, B>
930{
931    /// Walk operations grouped by table in DML insertion order.
932    ///
933    /// Mirrors [`DiffSet::iter`] but keeps insertion order; the
934    /// session-extension hash ordering only applies at [`build`](Self::build)
935    /// time. With the `diesel` feature enabled, each item implements
936    /// [`QueryFragment`](diesel::query_builder::QueryFragment) and executes
937    /// via [`RunQueryDsl`](diesel::RunQueryDsl).
938    pub fn iter(&self) -> impl Iterator<Item = PatchsetOp<'_, T, S, B>> {
939        self.tables.iter().flat_map(|(table, rows)| {
940            rows.iter().map(move |(pk, op)| match op {
941                Operation::Insert { values, indirect } => PatchsetOp::Insert {
942                    table,
943                    values: values.as_slice(),
944                    indirect: *indirect,
945                },
946                Operation::Update { values, indirect } => PatchsetOp::Update {
947                    table,
948                    pk: pk.as_slice(),
949                    entries: values.as_slice(),
950                    indirect: *indirect,
951                },
952                Operation::Delete { indirect, .. } => PatchsetOp::Delete {
953                    table,
954                    pk: pk.as_slice(),
955                    indirect: *indirect,
956                },
957            })
958        })
959    }
960}
961
962// ============================================================================
963// Reverse implementation for DiffSetBuilder
964// ============================================================================
965
966use crate::builders::operation::Reverse;
967
968impl<
969    T: SchemaWithPK,
970    S: Clone + Debug + Hash + Eq + AsRef<str>,
971    B: Clone + Debug + Hash + Eq + AsRef<[u8]>,
972> Reverse for DiffSetBuilder<ChangesetFormat, T, S, B>
973{
974    type Output = DiffSetBuilder<ChangesetFormat, T, S, B>;
975
976    fn reverse(self) -> Self::Output {
977        let mut reversed: DiffSetBuilder<ChangesetFormat, T, S, B> = DiffSetBuilder::new();
978
979        for (table, rows) in self.tables {
980            for (pk, op) in rows {
981                let rev_op = op.reverse();
982
983                reversed.add_operation(&table, pk, rev_op);
984            }
985        }
986
987        reversed
988    }
989}
990
991// ============================================================================
992// BitOr / BitOrAssign for DiffSetBuilder (changeset/patchset concatenation,
993// equivalent to SQLite's `sqlite3changeset_concat()`)
994// ============================================================================
995
996impl<
997    F: Format<S, B>,
998    T: SchemaWithPK,
999    S: Clone + Hash + Eq + AsRef<str>,
1000    B: Clone + Hash + Eq + AsRef<[u8]>,
1001> BitOrAssign for DiffSetBuilder<F, T, S, B>
1002where
1003    Operation<F, S, B>: core::ops::Add<Output = Option<Operation<F, S, B>>>,
1004{
1005    /// Merge another diff set into this one, consolidating operations on the same row.
1006    fn bitor_assign(&mut self, rhs: Self) {
1007        for (table, rows) in rhs.tables {
1008            for (pk, op) in rows {
1009                self.add_operation(&table, pk, op);
1010            }
1011        }
1012    }
1013}
1014
1015impl<
1016    F: Format<S, B>,
1017    T: SchemaWithPK,
1018    S: Clone + Hash + Eq + AsRef<str>,
1019    B: Clone + Hash + Eq + AsRef<[u8]>,
1020> BitOr for DiffSetBuilder<F, T, S, B>
1021where
1022    Operation<F, S, B>: core::ops::Add<Output = Option<Operation<F, S, B>>>,
1023{
1024    type Output = Self;
1025
1026    /// Merge two diff sets, consolidating operations on the same row.
1027    #[inline]
1028    fn bitor(mut self, rhs: Self) -> Self::Output {
1029        self |= rhs;
1030        self
1031    }
1032}
1033
1034// ============================================================================
1035// DiffSet: frozen (parsed) changeset/patchset with sequential row order
1036// ============================================================================
1037
1038/// A frozen changeset or patchset whose rows are emitted in stored order.
1039///
1040/// `DiffSet` is produced by the binary parser (via [`ParsedDiffSet`](crate::parser::ParsedDiffSet))
1041/// or by converting from a [`DiffSetBuilder`] using `Into::into`.  Unlike
1042/// [`DiffSetBuilder`], it stores tables and rows in a plain `Vec`, reflecting
1043/// the fact that no further mutation or PK-based lookup is needed.
1044///
1045/// [`build`](Self::build) serializes rows in the order they are stored. No
1046/// session hash-table simulation is applied. This preserves the original
1047/// row order of parsed binary data across roundtrips.
1048///
1049/// To modify a `DiffSet`, convert it back to a [`DiffSetBuilder`] using
1050/// `Into::into`.
1051#[derive(Debug, Clone)]
1052pub struct DiffSet<F: Format<S, B>, T: SchemaWithPK, S, B> {
1053    /// Tables and their rows, stored in order. Each row is a `(pk, operation)` pair.
1054    pub(crate) tables: TableVec<F, T, S, B>,
1055}
1056
1057/// Custom `PartialEq` that ignores tables with no operations (same semantics
1058/// as `DiffSetBuilder`).
1059impl<F: Format<S, B>, T: SchemaWithPK, S, B> PartialEq for DiffSet<F, T, S, B>
1060where
1061    S: PartialEq + Eq + Hash + AsRef<str>,
1062    B: PartialEq + Eq + Hash + AsRef<[u8]>,
1063    F::Old: PartialEq,
1064    F::DeleteData: PartialEq,
1065{
1066    fn eq(&self, other: &Self) -> bool {
1067        self.tables
1068            .iter()
1069            .filter(|(_, ops)| !ops.is_empty())
1070            .eq(other.tables.iter().filter(|(_, ops)| !ops.is_empty()))
1071    }
1072}
1073
1074impl<F: Format<S, B>, T: SchemaWithPK, S, B> Eq for DiffSet<F, T, S, B>
1075where
1076    S: Eq + Hash + AsRef<str>,
1077    B: Eq + Hash + AsRef<[u8]>,
1078    F::Old: Eq,
1079    F::DeleteData: Eq,
1080{
1081}
1082
1083impl<F: Format<S, B>, T: SchemaWithPK, S: AsRef<str> + Hash + Eq, B: AsRef<[u8]> + Hash + Eq>
1084    Default for DiffSet<F, T, S, B>
1085{
1086    fn default() -> Self {
1087        Self { tables: Vec::new() }
1088    }
1089}
1090
1091impl<F: Format<S, B>, T: SchemaWithPK, S: AsRef<str> + Hash + Eq, B: AsRef<[u8]> + Hash + Eq>
1092    DiffSet<F, T, S, B>
1093{
1094    /// Returns `true` if there are no operations in any table.
1095    #[inline]
1096    #[must_use]
1097    pub fn is_empty(&self) -> bool {
1098        self.tables.iter().all(|(_, rows)| rows.is_empty())
1099    }
1100
1101    /// Returns the total number of operations across all tables.
1102    #[inline]
1103    #[must_use]
1104    pub fn len(&self) -> usize {
1105        self.tables.iter().map(|(_, rows)| rows.len()).sum()
1106    }
1107}
1108
1109// -- Changeset iter (DiffSet) -------------------------------------------------
1110
1111impl<F: Format<S, B>, T: SchemaWithPK, S: AsRef<str> + Hash + Eq, B: AsRef<[u8]> + Hash + Eq>
1112    DiffSet<F, T, S, B>
1113{
1114    /// Returns the schema of each table that holds at least one operation,
1115    /// in stored order.
1116    pub fn tables(&self) -> impl Iterator<Item = &T> {
1117        self.tables
1118            .iter()
1119            .filter(|(_, rows)| !rows.is_empty())
1120            .map(|(t, _)| t)
1121    }
1122}
1123
1124impl<T: SchemaWithPK, S: Clone + Debug + AsRef<str>, B: Clone + Debug + AsRef<[u8]>>
1125    DiffSet<ChangesetFormat, T, S, B>
1126{
1127    /// Iterate over every operation in the changeset.
1128    ///
1129    /// Operations are yielded in stored order, grouped by table. Each
1130    /// [`ChangesetOp`] borrows from this `DiffSet`, so the returned
1131    /// iterator is invalidated when the `DiffSet` is dropped or mutated.
1132    pub fn iter(&self) -> impl Iterator<Item = ChangesetOp<'_, T, S, B>> {
1133        self.tables.iter().flat_map(|(table, rows)| {
1134            rows.iter().map(move |(_pk, op)| match op {
1135                Operation::Insert { values, indirect } => ChangesetOp::Insert {
1136                    table,
1137                    values: values.as_slice(),
1138                    indirect: *indirect,
1139                },
1140                Operation::Update { values, indirect } => ChangesetOp::Update {
1141                    table,
1142                    values: values.as_slice(),
1143                    indirect: *indirect,
1144                },
1145                Operation::Delete { data, indirect } => ChangesetOp::Delete {
1146                    table,
1147                    old_values: data.as_slice(),
1148                    indirect: *indirect,
1149                },
1150            })
1151        })
1152    }
1153}
1154
1155impl<T: SchemaWithPK, S: Clone + AsRef<str>, B: Clone + AsRef<[u8]>>
1156    DiffSet<PatchsetFormat, T, S, B>
1157{
1158    /// Iterate over every operation in the patchset.
1159    ///
1160    /// Operations are yielded in stored order, grouped by table. Each
1161    /// [`PatchsetOp`] borrows from this `DiffSet`. For DELETE and UPDATE
1162    /// ops only the primary-key columns are available (patchset format
1163    /// does not carry full old-row values).
1164    pub fn iter(&self) -> impl Iterator<Item = PatchsetOp<'_, T, S, B>> {
1165        self.tables.iter().flat_map(|(table, rows)| {
1166            rows.iter().map(move |(pk, op)| match op {
1167                Operation::Insert { values, indirect } => PatchsetOp::Insert {
1168                    table,
1169                    values: values.as_slice(),
1170                    indirect: *indirect,
1171                },
1172                Operation::Update { values, indirect } => PatchsetOp::Update {
1173                    table,
1174                    pk: pk.as_slice(),
1175                    entries: values.as_slice(),
1176                    indirect: *indirect,
1177                },
1178                Operation::Delete { indirect, .. } => PatchsetOp::Delete {
1179                    table,
1180                    pk: pk.as_slice(),
1181                    indirect: *indirect,
1182                },
1183            })
1184        })
1185    }
1186}
1187
1188// -- Changeset build (DiffSet) ------------------------------------------------
1189
1190impl<
1191    T: SchemaWithPK,
1192    S: Clone + Debug + Hash + Eq + AsRef<str>,
1193    B: Clone + Debug + Hash + Eq + AsRef<[u8]>,
1194> DiffSet<ChangesetFormat, T, S, B>
1195{
1196    /// Serialize the changeset to binary.
1197    ///
1198    /// Rows are emitted in stored order (no hash simulation).
1199    #[must_use]
1200    pub fn build(&self) -> Vec<u8> {
1201        let mut out = Vec::new();
1202
1203        for (table, rows) in &self.tables {
1204            if rows.is_empty() {
1205                continue;
1206            }
1207
1208            write_table_header(&mut out, markers::CHANGESET, table);
1209
1210            for (_pk, op) in rows {
1211                encode_changeset_op(&mut out, op);
1212            }
1213        }
1214
1215        out
1216    }
1217}
1218
1219// -- Patchset build (DiffSet) -------------------------------------------------
1220
1221impl<T: SchemaWithPK, S: Clone + Hash + Eq + AsRef<str>, B: Clone + Hash + Eq + AsRef<[u8]>>
1222    DiffSet<PatchsetFormat, T, S, B>
1223{
1224    /// Serialize the patchset to binary.
1225    ///
1226    /// Rows are emitted in stored order (no hash simulation).
1227    #[must_use]
1228    pub fn build(&self) -> Vec<u8> {
1229        let mut out = Vec::new();
1230
1231        for (table, rows) in &self.tables {
1232            if rows.is_empty() {
1233                continue;
1234            }
1235
1236            write_table_header(&mut out, markers::PATCHSET, table);
1237
1238            let (pk_flags, pk_col_to_pk_pos) = patchset_pk_mapping(table);
1239
1240            for (pk, op) in rows {
1241                encode_patchset_op(&mut out, op, pk, &pk_flags, &pk_col_to_pk_pos);
1242            }
1243        }
1244
1245        out
1246    }
1247}
1248
1249// -- From<DiffSet> for Vec<u8> ------------------------------------------------
1250
1251impl<
1252    T: SchemaWithPK,
1253    S: Clone + Debug + Hash + Eq + AsRef<str>,
1254    B: Clone + Debug + Hash + Eq + AsRef<[u8]>,
1255> From<&DiffSet<ChangesetFormat, T, S, B>> for Vec<u8>
1256{
1257    #[inline]
1258    fn from(diffset: &DiffSet<ChangesetFormat, T, S, B>) -> Self {
1259        diffset.build()
1260    }
1261}
1262
1263impl<
1264    T: SchemaWithPK,
1265    S: Clone + Debug + Hash + Eq + AsRef<str>,
1266    B: Clone + Debug + Hash + Eq + AsRef<[u8]>,
1267> From<DiffSet<ChangesetFormat, T, S, B>> for Vec<u8>
1268{
1269    #[inline]
1270    fn from(diffset: DiffSet<ChangesetFormat, T, S, B>) -> Self {
1271        diffset.build()
1272    }
1273}
1274
1275impl<T: SchemaWithPK, S: AsRef<str> + Clone + Hash + Eq, B: AsRef<[u8]> + Clone + Hash + Eq>
1276    From<&DiffSet<PatchsetFormat, T, S, B>> for Vec<u8>
1277{
1278    #[inline]
1279    fn from(diffset: &DiffSet<PatchsetFormat, T, S, B>) -> Self {
1280        diffset.build()
1281    }
1282}
1283
1284impl<T: SchemaWithPK, S: AsRef<str> + Clone + Hash + Eq, B: AsRef<[u8]> + Clone + Hash + Eq>
1285    From<DiffSet<PatchsetFormat, T, S, B>> for Vec<u8>
1286{
1287    #[inline]
1288    fn from(diffset: DiffSet<PatchsetFormat, T, S, B>) -> Self {
1289        diffset.build()
1290    }
1291}
1292
1293// -- Reverse for DiffSet<ChangesetFormat> -------------------------------------
1294
1295impl<
1296    T: SchemaWithPK,
1297    S: Clone + Debug + Hash + Eq + AsRef<str>,
1298    B: Clone + Debug + Hash + Eq + AsRef<[u8]>,
1299> Reverse for DiffSet<ChangesetFormat, T, S, B>
1300{
1301    type Output = DiffSet<ChangesetFormat, T, S, B>;
1302
1303    fn reverse(self) -> Self::Output {
1304        DiffSet {
1305            tables: self
1306                .tables
1307                .into_iter()
1308                .map(|(table, rows)| {
1309                    let rev_rows = rows
1310                        .into_iter()
1311                        .map(|(pk, op)| (pk, op.reverse()))
1312                        .collect();
1313                    (table, rev_rows)
1314                })
1315                .collect(),
1316        }
1317    }
1318}
1319
1320// -- From conversions between DiffSetBuilder and DiffSet ----------------------
1321
1322impl<F: Format<S, B>, T: SchemaWithPK, S: Hash + Eq + AsRef<str>, B: Hash + Eq + AsRef<[u8]>>
1323    From<DiffSetBuilder<F, T, S, B>> for DiffSet<F, T, S, B>
1324{
1325    fn from(builder: DiffSetBuilder<F, T, S, B>) -> Self {
1326        Self {
1327            tables: builder
1328                .tables
1329                .into_iter()
1330                .map(|(table, rows)| {
1331                    let ordered_rows: RowVec<F, S, B> = rows.into_iter().collect();
1332                    (table, ordered_rows)
1333                })
1334                .collect(),
1335        }
1336    }
1337}
1338
1339impl<F: Format<S, B>, T: SchemaWithPK, S: Hash + Eq + AsRef<str>, B: Hash + Eq + AsRef<[u8]>>
1340    From<DiffSet<F, T, S, B>> for DiffSetBuilder<F, T, S, B>
1341{
1342    fn from(diffset: DiffSet<F, T, S, B>) -> Self {
1343        let mut builder = Self::new();
1344        for (table, rows) in diffset.tables {
1345            let map: IndexMap<Vec<Value<S, B>>, Operation<F, S, B>> = rows.into_iter().collect();
1346            builder.tables.insert(table, map);
1347        }
1348        builder
1349    }
1350}
1351
1352#[cfg(test)]
1353mod tests {
1354    use super::*;
1355    use crate::builders::operation::Indirect;
1356    use crate::encoding::Value;
1357    use alloc::{string::String, vec};
1358
1359    /// Simple test table implementation
1360    #[derive(Debug, Clone, PartialEq, Eq, Hash)]
1361    struct TestTable {
1362        name: String,
1363        num_columns: usize,
1364        pk_column: usize,
1365    }
1366
1367    impl TestTable {
1368        fn new(name: &str, num_columns: usize, pk_column: usize) -> Self {
1369            Self {
1370                name: name.into(),
1371                num_columns,
1372                pk_column,
1373            }
1374        }
1375    }
1376
1377    impl crate::DynTable for TestTable {
1378        fn name(&self) -> &str {
1379            &self.name
1380        }
1381
1382        fn number_of_columns(&self) -> usize {
1383            self.num_columns
1384        }
1385
1386        fn write_pk_flags(&self, buf: &mut [u8]) {
1387            assert_eq!(buf.len(), self.num_columns);
1388            buf.fill(0);
1389            buf[self.pk_column] = 1;
1390        }
1391    }
1392
1393    impl crate::SchemaWithPK for TestTable {
1394        fn number_of_primary_keys(&self) -> usize {
1395            1
1396        }
1397
1398        fn primary_key_index(&self, col_idx: usize) -> Option<usize> {
1399            if col_idx == self.pk_column {
1400                Some(0)
1401            } else {
1402                None
1403            }
1404        }
1405
1406        fn extract_pk<S: Clone, B: Clone>(
1407            &self,
1408            values: &impl crate::IndexableValues<Text = S, Binary = B>,
1409        ) -> alloc::vec::Vec<Value<S, B>> {
1410            alloc::vec![
1411                values
1412                    .get(self.pk_column)
1413                    .expect("primary key column index out of bounds, values shorter than schema")
1414            ]
1415        }
1416    }
1417
1418    // Type alias for cleaner test code
1419    type ChangesetBuilder = DiffSetBuilder<ChangesetFormat, TestTable, String, Vec<u8>>;
1420
1421    #[test]
1422    fn test_insert_single_row() {
1423        let table = TestTable::new("users", 2, 0);
1424        let insert = Insert::from(table.clone())
1425            .set(0, 1i64)
1426            .unwrap()
1427            .set(1, "alice")
1428            .unwrap();
1429
1430        let builder = ChangesetBuilder::new().insert(insert);
1431
1432        assert_eq!(builder.len(), 1);
1433        assert!(!builder.is_empty());
1434    }
1435
1436    #[test]
1437    fn test_insert_then_delete_cancels_out() {
1438        let table = TestTable::new("users", 2, 0);
1439
1440        let insert = Insert::from(table.clone())
1441            .set(0, 1i64)
1442            .unwrap()
1443            .set(1, "alice")
1444            .unwrap();
1445
1446        let delete = ChangeDelete::from(table.clone())
1447            .set(0, 1i64)
1448            .unwrap()
1449            .set(1, "alice")
1450            .unwrap();
1451
1452        let builder = ChangesetBuilder::new().insert(insert).delete(delete);
1453
1454        assert_eq!(builder.len(), 0);
1455        assert!(builder.is_empty());
1456    }
1457
1458    #[test]
1459    fn test_insert_then_update_becomes_insert() {
1460        let table = TestTable::new("users", 2, 0);
1461
1462        let insert = Insert::from(table.clone())
1463            .set(0, 1i64)
1464            .unwrap()
1465            .set(1, "alice")
1466            .unwrap();
1467
1468        let update = Update::<TestTable, ChangesetFormat, String, Vec<u8>>::from(table.clone())
1469            .set(0, 1i64, 1i64) // PK unchanged
1470            .unwrap()
1471            .set(1, "alice", "bob")
1472            .unwrap();
1473
1474        let builder = ChangesetBuilder::new().insert(insert).update(update);
1475
1476        assert_eq!(builder.len(), 1);
1477        // Should still be an INSERT with "bob" as the name
1478    }
1479
1480    #[test]
1481    fn test_delete_then_insert_same_values_cancels_out() {
1482        let table = TestTable::new("users", 2, 0);
1483
1484        let delete = ChangeDelete::from(table.clone())
1485            .set(0, 1i64)
1486            .unwrap()
1487            .set(1, "alice")
1488            .unwrap();
1489
1490        let insert = Insert::from(table.clone())
1491            .set(0, 1i64)
1492            .unwrap()
1493            .set(1, "alice")
1494            .unwrap();
1495
1496        let builder = ChangesetBuilder::new().delete(delete).insert(insert);
1497
1498        assert_eq!(builder.len(), 0);
1499        assert!(builder.is_empty());
1500    }
1501
1502    #[test]
1503    fn test_delete_then_insert_different_values_becomes_update() {
1504        let table = TestTable::new("users", 2, 0);
1505
1506        let delete = ChangeDelete::from(table.clone())
1507            .set(0, 1i64)
1508            .unwrap()
1509            .set(1, "alice")
1510            .unwrap();
1511
1512        let insert = Insert::from(table.clone())
1513            .set(0, 1i64)
1514            .unwrap()
1515            .set(1, "bob")
1516            .unwrap();
1517
1518        let builder = ChangesetBuilder::new().delete(delete).insert(insert);
1519
1520        assert_eq!(builder.len(), 1);
1521        // Should be an UPDATE from alice to bob
1522    }
1523
1524    #[test]
1525    fn test_multiple_rows() {
1526        let table = TestTable::new("users", 2, 0);
1527
1528        let insert1 = Insert::from(table.clone())
1529            .set(0, 1i64)
1530            .unwrap()
1531            .set(1, "alice")
1532            .unwrap();
1533
1534        let insert2 = Insert::from(table.clone())
1535            .set(0, 2i64)
1536            .unwrap()
1537            .set(1, "bob")
1538            .unwrap();
1539
1540        let builder = ChangesetBuilder::new().insert(insert1).insert(insert2);
1541
1542        assert_eq!(builder.len(), 2);
1543    }
1544
1545    #[test]
1546    fn test_update_then_update_consolidates() {
1547        let table = TestTable::new("users", 2, 0);
1548
1549        let update1 = Update::<TestTable, ChangesetFormat, String, Vec<u8>>::from(table.clone())
1550            .set(0, 1i64, 1i64)
1551            .unwrap()
1552            .set(1, "alice", "bob")
1553            .unwrap();
1554
1555        let update2 = Update::<TestTable, ChangesetFormat, String, Vec<u8>>::from(table.clone())
1556            .set(0, 1i64, 1i64)
1557            .unwrap()
1558            .set(1, "bob", "charlie")
1559            .unwrap();
1560
1561        let builder = ChangesetBuilder::new().update(update1).update(update2);
1562
1563        assert_eq!(builder.len(), 1);
1564        // Should be a single UPDATE from alice to charlie
1565    }
1566
1567    // ========================================================================
1568    // Reverse trait tests
1569    // ========================================================================
1570
1571    #[test]
1572    fn test_reverse_operation_insert_becomes_delete() {
1573        let op: Operation<ChangesetFormat, String, Vec<u8>> = Operation::Insert {
1574            values: vec![Value::Integer(1), Value::Text("alice".into())],
1575            indirect: false,
1576        };
1577        let reversed = op.reverse();
1578        let Operation::Delete { data, .. } = reversed else {
1579            panic!("Expected Delete operation");
1580        };
1581        assert_eq!(
1582            data,
1583            vec![
1584                Value::<String, Vec<u8>>::Integer(1),
1585                Value::Text("alice".into())
1586            ]
1587        );
1588    }
1589
1590    #[test]
1591    fn test_reverse_operation_delete_becomes_insert() {
1592        let op: Operation<ChangesetFormat, String, Vec<u8>> = Operation::Delete {
1593            data: vec![Value::Integer(1), Value::Text("alice".into())],
1594            indirect: false,
1595        };
1596        let reversed = op.reverse();
1597        let Operation::Insert { values, .. } = reversed else {
1598            panic!("Expected Insert operation");
1599        };
1600        assert_eq!(
1601            values,
1602            vec![
1603                Value::<String, Vec<u8>>::Integer(1),
1604                Value::Text("alice".into())
1605            ]
1606        );
1607    }
1608
1609    #[test]
1610    fn test_reverse_operation_update_swaps_old_new() {
1611        let op: Operation<ChangesetFormat, String, Vec<u8>> = Operation::Update {
1612            values: vec![
1613                (Some(Value::Integer(1)), Some(Value::Integer(1))),
1614                (
1615                    Some(Value::Text("alice".into())),
1616                    Some(Value::Text("bob".into())),
1617                ),
1618            ],
1619            indirect: false,
1620        };
1621        let reversed = op.reverse();
1622        let Operation::Update { values, .. } = reversed else {
1623            panic!("Expected Update operation");
1624        };
1625        assert_eq!(
1626            values[0],
1627            (Some(Value::Integer(1)), Some(Value::Integer(1)))
1628        );
1629        assert_eq!(
1630            values[1],
1631            (
1632                Some(Value::Text("bob".into())),
1633                Some(Value::Text("alice".into()))
1634            )
1635        );
1636    }
1637
1638    #[test]
1639    fn test_reverse_builder_insert_becomes_delete() {
1640        let table = TestTable::new("users", 2, 0);
1641        let insert = Insert::from(table.clone())
1642            .set(0, 1i64)
1643            .unwrap()
1644            .set(1, "alice")
1645            .unwrap();
1646
1647        let builder = ChangesetBuilder::new().insert(insert);
1648        let reversed = builder.reverse();
1649
1650        assert_eq!(reversed.len(), 1);
1651        // The reversed builder should have a delete operation
1652        let rows = reversed.tables.get(&table).unwrap();
1653        assert!(matches!(
1654            rows.values().next().unwrap(),
1655            Operation::Delete { .. }
1656        ));
1657    }
1658
1659    #[test]
1660    fn test_reverse_builder_delete_becomes_insert() {
1661        let table = TestTable::new("users", 2, 0);
1662        let delete = ChangeDelete::from(table.clone())
1663            .set(0, 1i64)
1664            .unwrap()
1665            .set(1, "alice")
1666            .unwrap();
1667
1668        let builder = ChangesetBuilder::new().delete(delete);
1669        let reversed = builder.reverse();
1670
1671        assert_eq!(reversed.len(), 1);
1672        // The reversed builder should have an insert operation
1673        let rows = reversed.tables.get(&table).unwrap();
1674        assert!(matches!(
1675            rows.values().next().unwrap(),
1676            Operation::Insert { .. }
1677        ));
1678    }
1679
1680    #[test]
1681    fn test_reverse_builder_update_swaps() {
1682        let table = TestTable::new("users", 2, 0);
1683        let update = Update::<TestTable, ChangesetFormat, String, Vec<u8>>::from(table.clone())
1684            .set(0, 1i64, 1i64)
1685            .unwrap()
1686            .set(1, "alice", "bob")
1687            .unwrap();
1688
1689        let builder = ChangesetBuilder::new().update(update);
1690        let reversed = builder.reverse();
1691
1692        assert_eq!(reversed.len(), 1);
1693        // The reversed builder should have an update operation with swapped values
1694        let rows = reversed.tables.get(&table).unwrap();
1695        let Operation::Update { values, .. } = rows.values().next().unwrap() else {
1696            panic!("Expected Update operation");
1697        };
1698        assert_eq!(
1699            values[1],
1700            (
1701                Some(Value::Text("bob".into())),
1702                Some(Value::Text("alice".into()))
1703            )
1704        );
1705    }
1706
1707    #[test]
1708    fn test_reverse_is_involutory() {
1709        // reverse(reverse(x)) == x
1710        let table = TestTable::new("users", 2, 0);
1711        let insert = Insert::from(table.clone())
1712            .set(0, 1i64)
1713            .unwrap()
1714            .set(1, "alice")
1715            .unwrap();
1716
1717        let original_values = insert.into_values();
1718        let insert2 = Insert::from(table.clone())
1719            .set(0, 1i64)
1720            .unwrap()
1721            .set(1, "alice")
1722            .unwrap();
1723        let builder = ChangesetBuilder::new().insert(insert2);
1724        let double_reversed = builder.reverse().reverse();
1725
1726        assert_eq!(double_reversed.len(), 1);
1727        let rows = double_reversed.tables.get(&table).unwrap();
1728        let Operation::Insert { values, .. } = rows.values().next().unwrap() else {
1729            panic!("Expected Insert operation");
1730        };
1731        assert_eq!(values, &original_values);
1732    }
1733
1734    // ========================================================================
1735    // Build (serialization) tests
1736    // ========================================================================
1737
1738    #[test]
1739    fn test_build_empty_builder() {
1740        let builder = ChangesetBuilder::new();
1741        let bytes = builder.build();
1742        assert!(bytes.is_empty());
1743    }
1744
1745    #[test]
1746    fn test_build_insert_format() {
1747        let table = TestTable::new("t", 2, 0);
1748        let insert = Insert::from(table.clone())
1749            .set(0, 1i64)
1750            .unwrap()
1751            .set(1, "a")
1752            .unwrap();
1753
1754        let builder = ChangesetBuilder::new().insert(insert);
1755        let bytes = builder.build();
1756
1757        // Verify the structure:
1758        // Table header: 'T', col_count(2), pk_flags(1,0), name("t\0")
1759        // Operation: INSERT(0x12), indirect(0), values...
1760        assert!(!bytes.is_empty());
1761
1762        // Check table marker
1763        assert_eq!(bytes[0], b'T');
1764        // Column count
1765        assert_eq!(bytes[1], 2);
1766        // PK flags: first column is PK
1767        assert_eq!(bytes[2], 1);
1768        assert_eq!(bytes[3], 0);
1769        // Table name "t" + null terminator
1770        assert_eq!(bytes[4], b't');
1771        assert_eq!(bytes[5], 0);
1772        // Operation code: INSERT = 0x12
1773        assert_eq!(bytes[6], 0x12);
1774        // Indirect flag
1775        assert_eq!(bytes[7], 0);
1776    }
1777
1778    #[test]
1779    fn test_build_delete_format() {
1780        let table = TestTable::new("t", 2, 0);
1781        let delete = ChangeDelete::from(table.clone())
1782            .set(0, 1i64)
1783            .unwrap()
1784            .set(1, "a")
1785            .unwrap();
1786
1787        let builder = ChangesetBuilder::new().delete(delete);
1788        let bytes = builder.build();
1789
1790        assert!(!bytes.is_empty());
1791        assert_eq!(bytes[0], b'T');
1792        // Operation code: DELETE = 0x09
1793        assert_eq!(bytes[6], 0x09);
1794    }
1795
1796    #[test]
1797    fn test_build_update_format() {
1798        let table = TestTable::new("t", 2, 0);
1799        let update = Update::<TestTable, ChangesetFormat, String, Vec<u8>>::from(table.clone())
1800            .set(0, 1i64, 1i64)
1801            .unwrap()
1802            .set(1, "a", "b")
1803            .unwrap();
1804
1805        let builder = ChangesetBuilder::new().update(update);
1806        let bytes = builder.build();
1807
1808        assert!(!bytes.is_empty());
1809        assert_eq!(bytes[0], b'T');
1810        // Operation code: UPDATE = 0x17
1811        assert_eq!(bytes[6], 0x17);
1812    }
1813
1814    #[test]
1815    fn test_build_multiple_operations() {
1816        let table = TestTable::new("t", 2, 0);
1817
1818        let insert = Insert::from(table.clone())
1819            .set(0, 1i64)
1820            .unwrap()
1821            .set(1, "a")
1822            .unwrap();
1823
1824        let insert2 = Insert::from(table.clone())
1825            .set(0, 2i64)
1826            .unwrap()
1827            .set(1, "b")
1828            .unwrap();
1829
1830        let builder = ChangesetBuilder::new().insert(insert).insert(insert2);
1831        let bytes = builder.build();
1832
1833        assert!(!bytes.is_empty());
1834        // Should have one table header and two insert operations
1835        assert_eq!(bytes[0], b'T');
1836    }
1837
1838    #[test]
1839    fn test_build_cancelled_operations_produce_empty() {
1840        let table = TestTable::new("t", 2, 0);
1841
1842        let insert = Insert::from(table.clone())
1843            .set(0, 1i64)
1844            .unwrap()
1845            .set(1, "a")
1846            .unwrap();
1847
1848        let delete = ChangeDelete::from(table.clone())
1849            .set(0, 1i64)
1850            .unwrap()
1851            .set(1, "a")
1852            .unwrap();
1853
1854        let builder = ChangesetBuilder::new().insert(insert).delete(delete);
1855        let bytes = builder.build();
1856
1857        // INSERT + DELETE with same values cancels out
1858        assert!(bytes.is_empty());
1859    }
1860
1861    // ========================================================================
1862    // BitOr / BitOrAssign tests
1863    // ========================================================================
1864
1865    #[test]
1866    fn test_bitor_changeset_disjoint_rows() {
1867        let table = TestTable::new("users", 2, 0);
1868
1869        let insert1 = Insert::from(table.clone())
1870            .set(0, 1i64)
1871            .unwrap()
1872            .set(1, "alice")
1873            .unwrap();
1874
1875        let insert2 = Insert::from(table.clone())
1876            .set(0, 2i64)
1877            .unwrap()
1878            .set(1, "bob")
1879            .unwrap();
1880
1881        let cs1 = ChangesetBuilder::new().insert(insert1);
1882        let cs2 = ChangesetBuilder::new().insert(insert2);
1883
1884        let merged = cs1 | cs2;
1885        assert_eq!(merged.len(), 2);
1886    }
1887
1888    #[test]
1889    fn test_bitor_changeset_consolidates_same_row() {
1890        let table = TestTable::new("users", 2, 0);
1891
1892        // First changeset: INSERT row 1
1893        let insert = Insert::from(table.clone())
1894            .set(0, 1i64)
1895            .unwrap()
1896            .set(1, "alice")
1897            .unwrap();
1898
1899        // Second changeset: DELETE row 1
1900        let delete = ChangeDelete::from(table.clone())
1901            .set(0, 1i64)
1902            .unwrap()
1903            .set(1, "alice")
1904            .unwrap();
1905
1906        let cs1 = ChangesetBuilder::new().insert(insert);
1907        let cs2 = ChangesetBuilder::new().delete(delete);
1908
1909        // INSERT + DELETE with same values should cancel out
1910        let merged = cs1 | cs2;
1911        assert_eq!(merged.len(), 0);
1912        assert!(merged.is_empty());
1913    }
1914
1915    #[test]
1916    fn test_bitor_assign_changeset() {
1917        let table = TestTable::new("users", 2, 0);
1918
1919        let insert1 = Insert::from(table.clone())
1920            .set(0, 1i64)
1921            .unwrap()
1922            .set(1, "alice")
1923            .unwrap();
1924
1925        let insert2 = Insert::from(table.clone())
1926            .set(0, 2i64)
1927            .unwrap()
1928            .set(1, "bob")
1929            .unwrap();
1930
1931        let mut cs = ChangesetBuilder::new().insert(insert1);
1932        cs |= ChangesetBuilder::new().insert(insert2);
1933
1934        assert_eq!(cs.len(), 2);
1935    }
1936
1937    #[test]
1938    fn test_bitor_patchset_disjoint_rows() {
1939        type PatchsetBuilder = DiffSetBuilder<PatchsetFormat, TestTable, String, Vec<u8>>;
1940
1941        let table = TestTable::new("users", 2, 0);
1942
1943        let insert1 = Insert::from(table.clone())
1944            .set(0, 1i64)
1945            .unwrap()
1946            .set(1, "alice")
1947            .unwrap();
1948
1949        let insert2 = Insert::from(table.clone())
1950            .set(0, 2i64)
1951            .unwrap()
1952            .set(1, "bob")
1953            .unwrap();
1954
1955        let ps1 = PatchsetBuilder::new().insert(insert1);
1956        let ps2 = PatchsetBuilder::new().insert(insert2);
1957
1958        let merged = ps1 | ps2;
1959        assert_eq!(merged.len(), 2);
1960    }
1961
1962    #[test]
1963    fn test_bitor_patchset_consolidates_same_row() {
1964        type PatchsetBuilder = DiffSetBuilder<PatchsetFormat, TestTable, String, Vec<u8>>;
1965
1966        let table = TestTable::new("users", 2, 0);
1967
1968        // First patchset: INSERT row 1
1969        let insert = Insert::from(table.clone())
1970            .set(0, 1i64)
1971            .unwrap()
1972            .set(1, "alice")
1973            .unwrap();
1974
1975        // Second patchset: DELETE row 1
1976        let delete = PatchDelete::new(table.clone(), vec![Value::Integer(1)]);
1977
1978        let ps1 = PatchsetBuilder::new().insert(insert);
1979        let ps2 = PatchsetBuilder::new().delete(delete);
1980
1981        // INSERT + DELETE should cancel out
1982        let merged = ps1 | ps2;
1983        assert_eq!(merged.len(), 0);
1984        assert!(merged.is_empty());
1985    }
1986
1987    #[test]
1988    fn test_bitor_multiple_tables() {
1989        let table1 = TestTable::new("users", 2, 0);
1990        let table2 = TestTable::new("posts", 2, 0);
1991
1992        let insert1 = Insert::from(table1.clone())
1993            .set(0, 1i64)
1994            .unwrap()
1995            .set(1, "alice")
1996            .unwrap();
1997
1998        let insert2 = Insert::from(table2.clone())
1999            .set(0, 100i64)
2000            .unwrap()
2001            .set(1, "first post")
2002            .unwrap();
2003
2004        let cs1 = ChangesetBuilder::new().insert(insert1);
2005        let cs2 = ChangesetBuilder::new().insert(insert2);
2006
2007        let merged = cs1 | cs2;
2008        assert_eq!(merged.len(), 2);
2009    }
2010
2011    #[test]
2012    fn test_bitor_insert_then_update_consolidates() {
2013        let table = TestTable::new("users", 2, 0);
2014
2015        // First changeset: INSERT row 1
2016        let insert = Insert::from(table.clone())
2017            .set(0, 1i64)
2018            .unwrap()
2019            .set(1, "alice")
2020            .unwrap();
2021
2022        // Second changeset: UPDATE row 1
2023        let update = Update::<TestTable, ChangesetFormat, String, Vec<u8>>::from(table.clone())
2024            .set(0, 1i64, 1i64)
2025            .unwrap()
2026            .set(1, "alice", "alicia")
2027            .unwrap();
2028
2029        let cs1 = ChangesetBuilder::new().insert(insert);
2030        let cs2 = ChangesetBuilder::new().update(update);
2031
2032        // INSERT + UPDATE should consolidate to INSERT with final values
2033        let merged = cs1 | cs2;
2034        assert_eq!(merged.len(), 1);
2035    }
2036
2037    #[test]
2038    fn test_session_hash_growth_with_many_rows() {
2039        // The simulated session hash table grows from 256 buckets when entries
2040        // reach 128. Insert 200 rows so the rehash branch in session_row_order
2041        // is exercised. Builds the binary and reparses to confirm consistency.
2042        let table = TestTable::new("many", 2, 0);
2043        let mut cs = ChangesetBuilder::new();
2044        for i in 0..200i64 {
2045            let insert = Insert::from(table.clone())
2046                .set(0, i)
2047                .unwrap()
2048                .set(1, alloc::format!("row-{i}"))
2049                .unwrap();
2050            cs = cs.insert(insert);
2051        }
2052        assert_eq!(cs.len(), 200);
2053
2054        let bytes = cs.build();
2055        assert!(!bytes.is_empty());
2056
2057        // Reparse the binary to make sure 200 ops survived the round-trip.
2058        let reparsed = crate::parser::ParsedDiffSet::try_from(bytes.as_slice()).unwrap();
2059        let reparsed_bytes: Vec<u8> = reparsed.into();
2060        assert_eq!(bytes, reparsed_bytes);
2061    }
2062
2063    // ========================================================================
2064    // Indirect-flag tests
2065    // ========================================================================
2066
2067    /// Header offset of the indirect byte within a single-op single-table
2068    /// changeset. Layout: 'T' marker (1) + col_count (1) + pk_flags (2)
2069    /// + table_name "t\0" (2) + op_code (1) = 7. Patchset is identical.
2070    const INDIRECT_BYTE_OFFSET: usize = 7;
2071
2072    #[test]
2073    fn test_build_insert_indirect_byte_set() {
2074        let table = TestTable::new("t", 2, 0);
2075        let insert = Insert::from(table)
2076            .set(0, 1i64)
2077            .unwrap()
2078            .set(1, "a")
2079            .unwrap()
2080            .indirect(true);
2081
2082        let bytes = ChangesetBuilder::new().insert(insert).build();
2083        assert_eq!(bytes[INDIRECT_BYTE_OFFSET], 1);
2084    }
2085
2086    #[test]
2087    fn test_build_delete_indirect_byte_set() {
2088        let table = TestTable::new("t", 2, 0);
2089        let delete = ChangeDelete::from(table)
2090            .set(0, 1i64)
2091            .unwrap()
2092            .set(1, "a")
2093            .unwrap()
2094            .indirect(true);
2095
2096        let bytes = ChangesetBuilder::new().delete(delete).build();
2097        assert_eq!(bytes[INDIRECT_BYTE_OFFSET], 1);
2098    }
2099
2100    #[test]
2101    fn test_build_update_indirect_byte_set() {
2102        let table = TestTable::new("t", 2, 0);
2103        let update = Update::<TestTable, ChangesetFormat, String, Vec<u8>>::from(table)
2104            .set(0, 1i64, 1i64)
2105            .unwrap()
2106            .set(1, "a", "b")
2107            .unwrap()
2108            .indirect(true);
2109
2110        let bytes = ChangesetBuilder::new().update(update).build();
2111        assert_eq!(bytes[INDIRECT_BYTE_OFFSET], 1);
2112    }
2113
2114    #[test]
2115    fn test_build_patchset_insert_indirect_byte() {
2116        let table = TestTable::new("t", 2, 0);
2117        let insert = Insert::from(table)
2118            .set(0, 1i64)
2119            .unwrap()
2120            .set(1, "a")
2121            .unwrap()
2122            .indirect(true);
2123
2124        let patchset: PatchSet<TestTable, String, Vec<u8>> = PatchSet::new().insert(insert);
2125        let bytes = patchset.build();
2126        assert_eq!(bytes[INDIRECT_BYTE_OFFSET], 1);
2127    }
2128
2129    #[test]
2130    fn test_reverse_preserves_indirect() {
2131        // INSERT -> DELETE: indirect carries
2132        let insert: Operation<ChangesetFormat, String, Vec<u8>> = Operation::Insert {
2133            values: vec![Value::Integer(1)],
2134            indirect: true,
2135        };
2136        let reversed = insert.reverse();
2137        assert!(reversed.indirect());
2138        assert!(matches!(reversed, Operation::Delete { .. }));
2139
2140        // DELETE -> INSERT: indirect carries
2141        let delete: Operation<ChangesetFormat, String, Vec<u8>> = Operation::Delete {
2142            data: vec![Value::Integer(1)],
2143            indirect: true,
2144        };
2145        let reversed = delete.reverse();
2146        assert!(reversed.indirect());
2147        assert!(matches!(reversed, Operation::Insert { .. }));
2148
2149        // UPDATE -> UPDATE swapped: indirect carries
2150        let update: Operation<ChangesetFormat, String, Vec<u8>> = Operation::Update {
2151            values: vec![(Some(Value::Integer(1)), Some(Value::Integer(2)))],
2152            indirect: true,
2153        };
2154        let reversed = update.reverse();
2155        assert!(reversed.indirect());
2156        assert!(matches!(reversed, Operation::Update { .. }));
2157    }
2158
2159    #[test]
2160    fn test_patchdelete_indirect_byte_set() {
2161        let table = TestTable::new("t", 2, 0);
2162        let delete: PatchDelete<TestTable, String, Vec<u8>> =
2163            PatchDelete::new(table, vec![Value::Integer(1)]).indirect(true);
2164        let bytes = PatchSet::new().delete(delete).build();
2165        assert_eq!(bytes[INDIRECT_BYTE_OFFSET], 1);
2166    }
2167
2168    #[test]
2169    fn test_operation_eq_indirect_differs() {
2170        // Two ops with identical payload but different indirect flags must not be equal.
2171        let a: Operation<ChangesetFormat, String, Vec<u8>> = Operation::Insert {
2172            values: vec![Value::Integer(1)],
2173            indirect: false,
2174        };
2175        let b: Operation<ChangesetFormat, String, Vec<u8>> = Operation::Insert {
2176            values: vec![Value::Integer(1)],
2177            indirect: true,
2178        };
2179        assert_ne!(a, b);
2180    }
2181
2182    #[test]
2183    fn test_operation_eq_variant_mismatch() {
2184        // Different variants must compare unequal regardless of payload.
2185        let insert: Operation<ChangesetFormat, String, Vec<u8>> = Operation::Insert {
2186            values: vec![Value::Integer(1)],
2187            indirect: false,
2188        };
2189        let delete: Operation<ChangesetFormat, String, Vec<u8>> = Operation::Delete {
2190            data: vec![Value::Integer(1)],
2191            indirect: false,
2192        };
2193        assert_ne!(insert, delete);
2194    }
2195
2196    #[test]
2197    fn test_bitor_indirect_rhs_wins() {
2198        // Two INSERTs on the same PK with opposite indirect bits. The merged
2199        // op should carry the rhs's bit (last-write-wins).
2200        let table = TestTable::new("t", 2, 0);
2201        let lhs_insert = Insert::from(table.clone())
2202            .set(0, 1i64)
2203            .unwrap()
2204            .set(1, "a")
2205            .unwrap()
2206            .indirect(true);
2207        let rhs_insert = Insert::from(table.clone())
2208            .set(0, 1i64)
2209            .unwrap()
2210            .set(1, "a")
2211            .unwrap()
2212            .indirect(false);
2213
2214        let merged =
2215            ChangesetBuilder::new().insert(lhs_insert) | ChangesetBuilder::new().insert(rhs_insert);
2216        let rows = merged.tables.get(&table).unwrap();
2217        let op = rows.values().next().unwrap();
2218        assert!(!op.indirect(), "rhs (false) should win over lhs (true)");
2219
2220        // Reverse direction: rhs=true wins over lhs=false.
2221        let lhs_insert = Insert::from(table.clone())
2222            .set(0, 2i64)
2223            .unwrap()
2224            .set(1, "b")
2225            .unwrap()
2226            .indirect(false);
2227        let rhs_insert = Insert::from(table.clone())
2228            .set(0, 2i64)
2229            .unwrap()
2230            .set(1, "b")
2231            .unwrap()
2232            .indirect(true);
2233
2234        let merged =
2235            ChangesetBuilder::new().insert(lhs_insert) | ChangesetBuilder::new().insert(rhs_insert);
2236        let rows = merged.tables.get(&table).unwrap();
2237        let op = rows.values().next().unwrap();
2238        assert!(op.indirect(), "rhs (true) should win over lhs (false)");
2239    }
2240
2241    #[test]
2242    fn test_roundtrip_indirect_changeset() {
2243        let table = TestTable::new("t", 2, 0);
2244        let insert = Insert::from(table)
2245            .set(0, 1i64)
2246            .unwrap()
2247            .set(1, "a")
2248            .unwrap()
2249            .indirect(true);
2250
2251        let bytes = ChangesetBuilder::new().insert(insert).build();
2252        let reparsed = crate::parser::ParsedDiffSet::try_from(bytes.as_slice()).unwrap();
2253        let reparsed_bytes: Vec<u8> = reparsed.into();
2254        assert_eq!(bytes, reparsed_bytes);
2255        assert_eq!(reparsed_bytes[INDIRECT_BYTE_OFFSET], 1);
2256    }
2257
2258    #[test]
2259    fn test_roundtrip_indirect_patchset() {
2260        let table = TestTable::new("t", 2, 0);
2261        let insert = Insert::from(table)
2262            .set(0, 1i64)
2263            .unwrap()
2264            .set(1, "a")
2265            .unwrap()
2266            .indirect(true);
2267
2268        let patchset: PatchSet<TestTable, String, Vec<u8>> = PatchSet::new().insert(insert);
2269        let bytes = patchset.build();
2270        let reparsed = crate::parser::ParsedDiffSet::try_from(bytes.as_slice()).unwrap();
2271        let reparsed_bytes: Vec<u8> = reparsed.into();
2272        assert_eq!(bytes, reparsed_bytes);
2273        assert_eq!(reparsed_bytes[INDIRECT_BYTE_OFFSET], 1);
2274    }
2275
2276    #[test]
2277    fn test_indirect_full_pipeline_roundtrip() {
2278        // Serialize -> parse -> reverse -> reverse -> BitOr(empty) -> re-serialize.
2279        // The indirect bit must survive every stage.
2280        let table = TestTable::new("t", 2, 0);
2281        let insert = Insert::from(table)
2282            .set(0, 1i64)
2283            .unwrap()
2284            .set(1, "alice")
2285            .unwrap()
2286            .indirect(true);
2287        let original = ChangesetBuilder::new().insert(insert);
2288        let bytes_a = original.build();
2289        assert_eq!(bytes_a[INDIRECT_BYTE_OFFSET], 1);
2290
2291        // Parse the bytes back into a builder over TableSchema<String>.
2292        let parsed = crate::parser::ParsedDiffSet::try_from(bytes_a.as_slice()).unwrap();
2293        let crate::parser::ParsedDiffSet::Changeset(parsed_set) = parsed else {
2294            panic!("expected Changeset variant");
2295        };
2296
2297        let parsed_builder: DiffSetBuilder<
2298            ChangesetFormat,
2299            crate::parser::TableSchema<String>,
2300            String,
2301            Vec<u8>,
2302        > = parsed_set.into();
2303        let empty: DiffSetBuilder<
2304            ChangesetFormat,
2305            crate::parser::TableSchema<String>,
2306            String,
2307            Vec<u8>,
2308        > = DiffSetBuilder::new();
2309        let doubled = parsed_builder.reverse().reverse();
2310        let merged = doubled | empty;
2311
2312        let bytes_b = merged.build();
2313        assert_eq!(bytes_a, bytes_b);
2314        assert_eq!(bytes_b[INDIRECT_BYTE_OFFSET], 1);
2315    }
2316
2317    // ========================================================================
2318    // Operation merge (Add) arms
2319    // ========================================================================
2320
2321    fn changeset_insert(v: i64) -> Operation<ChangesetFormat, String, Vec<u8>> {
2322        Operation::Insert {
2323            values: vec![Value::Integer(v), Value::Text("a".into())],
2324            indirect: false,
2325        }
2326    }
2327
2328    fn changeset_delete(v: i64) -> Operation<ChangesetFormat, String, Vec<u8>> {
2329        Operation::Delete {
2330            data: vec![Value::Integer(v), Value::Text("a".into())],
2331            indirect: false,
2332        }
2333    }
2334
2335    fn changeset_update(old: i64, new: i64) -> Operation<ChangesetFormat, String, Vec<u8>> {
2336        Operation::Update {
2337            values: vec![
2338                (Some(Value::Integer(old)), Some(Value::Integer(new))),
2339                (Some(Value::Text("a".into())), Some(Value::Text("b".into()))),
2340            ],
2341            indirect: false,
2342        }
2343    }
2344
2345    #[test]
2346    fn test_add_changeset_insert_plus_update() {
2347        let merged = (changeset_insert(1) + changeset_update(1, 2)).unwrap();
2348        let Operation::Insert { values, .. } = merged else {
2349            panic!("expected Insert");
2350        };
2351        assert_eq!(values[0], Value::Integer(2));
2352        assert_eq!(values[1], Value::Text("b".into()));
2353    }
2354
2355    #[test]
2356    fn test_add_changeset_update_plus_insert() {
2357        // UPDATE wins, values are the UPDATE's, indirect is rhs (=false here).
2358        let merged = (changeset_update(1, 2) + changeset_insert(99)).unwrap();
2359        assert!(matches!(merged, Operation::Update { .. }));
2360    }
2361
2362    #[test]
2363    fn test_add_changeset_update_plus_update() {
2364        // First old, last new.
2365        let merged = (changeset_update(1, 2) + changeset_update(2, 3)).unwrap();
2366        let Operation::Update { values, .. } = merged else {
2367            panic!("expected Update");
2368        };
2369        assert_eq!(values[0].0, Some(Value::Integer(1)));
2370        assert_eq!(values[0].1, Some(Value::Integer(3)));
2371    }
2372
2373    #[test]
2374    fn test_add_changeset_update_plus_delete() {
2375        // UPDATE+DELETE collapses to DELETE carrying the UPDATE's old values.
2376        let merged = (changeset_update(1, 2) + changeset_delete(99)).unwrap();
2377        let Operation::Delete { data, .. } = merged else {
2378            panic!("expected Delete");
2379        };
2380        assert_eq!(data[0], Value::Integer(1));
2381        assert_eq!(data[1], Value::Text("a".into()));
2382    }
2383
2384    #[test]
2385    fn test_add_changeset_delete_plus_update_keeps_delete() {
2386        let merged = (changeset_delete(1) + changeset_update(1, 2)).unwrap();
2387        assert!(matches!(merged, Operation::Delete { .. }));
2388    }
2389
2390    #[test]
2391    fn test_add_changeset_delete_plus_delete_keeps_first() {
2392        let merged = (changeset_delete(1) + changeset_delete(2)).unwrap();
2393        let Operation::Delete { data, .. } = merged else {
2394            panic!("expected Delete");
2395        };
2396        assert_eq!(data[0], Value::Integer(1));
2397    }
2398
2399    fn patchset_insert(v: i64) -> Operation<PatchsetFormat, String, Vec<u8>> {
2400        Operation::Insert {
2401            values: vec![Value::Integer(v), Value::Text("a".into())],
2402            indirect: false,
2403        }
2404    }
2405
2406    fn patchset_update(new: i64) -> Operation<PatchsetFormat, String, Vec<u8>> {
2407        Operation::Update {
2408            values: vec![
2409                ((), Some(Value::Integer(new))),
2410                ((), Some(Value::Text("b".into()))),
2411            ],
2412            indirect: false,
2413        }
2414    }
2415
2416    fn patchset_delete() -> Operation<PatchsetFormat, String, Vec<u8>> {
2417        Operation::Delete {
2418            data: (),
2419            indirect: false,
2420        }
2421    }
2422
2423    #[test]
2424    fn test_add_patchset_insert_plus_update() {
2425        let merged = (patchset_insert(1) + patchset_update(2)).unwrap();
2426        let Operation::Insert { values, .. } = merged else {
2427            panic!("expected Insert");
2428        };
2429        assert_eq!(values[0], Value::Integer(2));
2430        assert_eq!(values[1], Value::Text("b".into()));
2431    }
2432
2433    #[test]
2434    fn test_add_patchset_update_plus_insert() {
2435        let merged = (patchset_update(2) + patchset_insert(99)).unwrap();
2436        assert!(matches!(merged, Operation::Update { .. }));
2437    }
2438
2439    #[test]
2440    fn test_add_patchset_update_plus_update() {
2441        let merged = (patchset_update(2) + patchset_update(3)).unwrap();
2442        let Operation::Update { values, .. } = merged else {
2443            panic!("expected Update");
2444        };
2445        assert_eq!(values[0].1, Some(Value::Integer(3)));
2446    }
2447
2448    #[test]
2449    fn test_add_patchset_update_plus_delete() {
2450        let merged = (patchset_update(2) + patchset_delete()).unwrap();
2451        assert!(matches!(merged, Operation::Delete { .. }));
2452    }
2453
2454    #[test]
2455    fn test_add_patchset_delete_plus_insert_promotes_to_update() {
2456        // Patchset can't compare old values, so DELETE + INSERT always becomes UPDATE.
2457        let merged = (patchset_delete() + patchset_insert(1)).unwrap();
2458        assert!(matches!(merged, Operation::Update { .. }));
2459    }
2460
2461    #[test]
2462    fn test_add_patchset_delete_plus_update_keeps_delete() {
2463        let merged = (patchset_delete() + patchset_update(2)).unwrap();
2464        assert!(matches!(merged, Operation::Delete { .. }));
2465    }
2466
2467    #[test]
2468    fn test_add_patchset_delete_plus_delete_keeps_first() {
2469        let merged = (patchset_delete() + patchset_delete()).unwrap();
2470        assert!(matches!(merged, Operation::Delete { .. }));
2471    }
2472
2473    // ========================================================================
2474    // Session-hash coverage: PKs of every Value type
2475    // ========================================================================
2476
2477    #[test]
2478    fn test_session_hash_real_pk() {
2479        let table = TestTable::new("t", 2, 0);
2480        let insert = Insert::from(table)
2481            .set(0, 2.5f64)
2482            .unwrap()
2483            .set(1, "a")
2484            .unwrap();
2485        let bytes = ChangesetBuilder::new().insert(insert).build();
2486        assert!(!bytes.is_empty());
2487    }
2488
2489    #[test]
2490    fn test_session_hash_text_pk() {
2491        let table = TestTable::new("t", 2, 0);
2492        let insert = Insert::from(table)
2493            .set(0, "alice")
2494            .unwrap()
2495            .set(1, 42i64)
2496            .unwrap();
2497        let bytes = ChangesetBuilder::new().insert(insert).build();
2498        assert!(!bytes.is_empty());
2499    }
2500
2501    #[test]
2502    fn test_session_hash_blob_pk() {
2503        let table = TestTable::new("t", 2, 0);
2504        let insert = Insert::from(table)
2505            .set(0, alloc::vec![0xDE_u8, 0xAD, 0xBE, 0xEF])
2506            .unwrap()
2507            .set(1, "a")
2508            .unwrap();
2509        let bytes = ChangesetBuilder::new().insert(insert).build();
2510        assert!(!bytes.is_empty());
2511    }
2512
2513    // ========================================================================
2514    // session_row_order empty-rows short-circuit
2515    // ========================================================================
2516
2517    #[test]
2518    fn test_session_row_order_empty() {
2519        // An empty builder builds to empty bytes (no headers, no ops).
2520        let cs: ChangesetBuilder = ChangesetBuilder::new();
2521        let bytes = cs.build();
2522        assert!(bytes.is_empty());
2523    }
2524
2525    #[test]
2526    fn test_session_row_order_empty_rows_returns_empty_vec() {
2527        // Direct exercise of the empty-rows short-circuit (line 157-158).
2528        let rows: RowMap<ChangesetFormat, String, Vec<u8>> = IndexMap::default();
2529        assert!(session_row_order(&rows).is_empty());
2530    }
2531
2532    #[test]
2533    fn test_diffset_patchset_build_skips_empty_table() {
2534        // A patchset DiffSet with a registered-but-empty table builds to nothing.
2535        let table = TestTable::new("t", 2, 0);
2536        let mut builder: PatchSet<TestTable, String, Vec<u8>> = PatchSet::new();
2537        builder.add_table(&table);
2538        let frozen: DiffSet<PatchsetFormat, TestTable, String, Vec<u8>> = builder.into();
2539        let bytes = frozen.build();
2540        assert!(bytes.is_empty());
2541    }
2542
2543    // ========================================================================
2544    // From<DiffSetBuilder> / From<&DiffSetBuilder> / From<DiffSet> / From<&DiffSet> for Vec<u8>
2545    // ========================================================================
2546
2547    #[test]
2548    fn test_from_changeset_builder_into_vec() {
2549        let table = TestTable::new("t", 2, 0);
2550        let insert = Insert::from(table)
2551            .set(0, 1i64)
2552            .unwrap()
2553            .set(1, "a")
2554            .unwrap();
2555        let builder = ChangesetBuilder::new().insert(insert);
2556        let bytes_owned: Vec<u8> = builder.clone().into();
2557        let bytes_ref: Vec<u8> = (&builder).into();
2558        assert_eq!(bytes_owned, bytes_ref);
2559        let frozen: DiffSet<ChangesetFormat, TestTable, String, Vec<u8>> = builder.into();
2560        let bytes_frozen_owned: Vec<u8> = frozen.clone().into();
2561        let bytes_frozen_ref: Vec<u8> = (&frozen).into();
2562        assert_eq!(bytes_frozen_owned, bytes_frozen_ref);
2563    }
2564
2565    #[test]
2566    fn test_from_patchset_builder_into_vec() {
2567        let table = TestTable::new("t", 2, 0);
2568        let insert = Insert::from(table)
2569            .set(0, 1i64)
2570            .unwrap()
2571            .set(1, "a")
2572            .unwrap();
2573        let builder: PatchSet<TestTable, String, Vec<u8>> = PatchSet::new().insert(insert);
2574        let bytes_owned: Vec<u8> = builder.clone().into();
2575        let bytes_ref: Vec<u8> = (&builder).into();
2576        assert_eq!(bytes_owned, bytes_ref);
2577        let frozen: DiffSet<PatchsetFormat, TestTable, String, Vec<u8>> = builder.into();
2578        let bytes_frozen_owned: Vec<u8> = frozen.clone().into();
2579        let bytes_frozen_ref: Vec<u8> = (&frozen).into();
2580        assert_eq!(bytes_frozen_owned, bytes_frozen_ref);
2581    }
2582
2583    // ========================================================================
2584    // add_operation INSERT+UPDATE pk-change branch
2585    // ========================================================================
2586
2587    #[test]
2588    fn test_add_operation_insert_then_update_changes_pk() {
2589        // Insert id=1, then update id=1 to id=2. Triggers the special-case branch
2590        // in add_operation that re-extracts the PK from the merged INSERT values.
2591        let table = TestTable::new("t", 2, 0);
2592        let insert = Insert::from(table.clone())
2593            .set(0, 1i64)
2594            .unwrap()
2595            .set(1, "alice")
2596            .unwrap();
2597        let update = Update::<TestTable, ChangesetFormat, String, Vec<u8>>::from(table.clone())
2598            .set(0, 1i64, 2i64)
2599            .unwrap()
2600            .set(1, "alice", "bob")
2601            .unwrap();
2602        let builder = ChangesetBuilder::new().insert(insert).update(update);
2603        let rows = builder.tables.get(&table).unwrap();
2604        assert_eq!(rows.len(), 1);
2605        // The row should now be keyed by id=2.
2606        let (pk, op) = rows.iter().next().unwrap();
2607        assert_eq!(pk[0], Value::Integer(2));
2608        let Operation::Insert { values, .. } = op else {
2609            panic!("expected merged INSERT");
2610        };
2611        assert_eq!(values[0], Value::Integer(2));
2612        assert_eq!(values[1], Value::Text("bob".into()));
2613    }
2614
2615    // ========================================================================
2616    // DiffOps for DiffSet<F> wrappers
2617    // ========================================================================
2618
2619    #[test]
2620    fn test_diffset_changeset_diffops_wrappers() {
2621        let table = TestTable::new("t", 2, 0);
2622        let initial = Insert::from(table.clone())
2623            .set(0, 1i64)
2624            .unwrap()
2625            .set(1, "a")
2626            .unwrap();
2627        let frozen: DiffSet<ChangesetFormat, TestTable, String, Vec<u8>> =
2628            ChangesetBuilder::new().insert(initial).into();
2629
2630        let insert2 = Insert::from(table.clone())
2631            .set(0, 2i64)
2632            .unwrap()
2633            .set(1, "b")
2634            .unwrap();
2635        let after_insert = <_ as DiffOps<_, _, _>>::insert(frozen.clone(), insert2);
2636        assert_eq!(after_insert.len(), 2);
2637
2638        let delete = ChangeDelete::from(table.clone())
2639            .set(0, 1i64)
2640            .unwrap()
2641            .set(1, "a")
2642            .unwrap();
2643        let after_delete = <_ as DiffOps<_, _, _>>::delete(frozen.clone(), delete);
2644        assert_eq!(after_delete.len(), 0);
2645
2646        let update = Update::<TestTable, ChangesetFormat, String, Vec<u8>>::from(table)
2647            .set(0, 1i64, 1i64)
2648            .unwrap()
2649            .set(1, "a", "z")
2650            .unwrap();
2651        let after_update = <_ as DiffOps<_, _, _>>::update(frozen, update);
2652        assert_eq!(after_update.len(), 1);
2653    }
2654
2655    #[test]
2656    fn test_diffset_patchset_diffops_wrappers() {
2657        let table = TestTable::new("t", 2, 0);
2658        let initial = Insert::from(table.clone())
2659            .set(0, 1i64)
2660            .unwrap()
2661            .set(1, "a")
2662            .unwrap();
2663        let frozen: DiffSet<PatchsetFormat, TestTable, String, Vec<u8>> =
2664            PatchSet::new().insert(initial).into();
2665
2666        let insert2 = Insert::from(table.clone())
2667            .set(0, 2i64)
2668            .unwrap()
2669            .set(1, "b")
2670            .unwrap();
2671        let after_insert = <_ as DiffOps<_, _, _>>::insert(frozen.clone(), insert2);
2672        assert_eq!(after_insert.len(), 2);
2673
2674        let delete: PatchDelete<TestTable, String, Vec<u8>> =
2675            PatchDelete::new(table.clone(), vec![Value::Integer(1)]);
2676        let after_delete = <_ as DiffOps<_, _, _>>::delete(frozen.clone(), delete);
2677        // Insert(id=1) + PatchDelete(id=1) cancel out.
2678        assert_eq!(after_delete.len(), 0);
2679
2680        let update = Update::<TestTable, PatchsetFormat, String, Vec<u8>>::from(table)
2681            .set(0, 1i64)
2682            .unwrap()
2683            .set(1, "z")
2684            .unwrap();
2685        let after_update = <_ as DiffOps<_, _, _>>::update(frozen, update);
2686        assert_eq!(after_update.len(), 1);
2687    }
2688
2689    #[test]
2690    fn test_diffset_changeset_iter_yields_inserts_and_indirect_flag() {
2691        let table = TestTable::new("t", 2, 0);
2692        let direct = Insert::from(table.clone()).set(0, 1i64).unwrap();
2693        let indirect = Insert::from(table.clone())
2694            .set(0, 2i64)
2695            .unwrap()
2696            .indirect(true);
2697        let frozen: DiffSet<ChangesetFormat, TestTable, String, Vec<u8>> = ChangesetBuilder::new()
2698            .insert(direct)
2699            .insert(indirect)
2700            .into();
2701
2702        let ops: Vec<_> = frozen.iter().collect();
2703        assert_eq!(ops.len(), 2);
2704        for op in &ops {
2705            assert_eq!(crate::DynTable::name(op.table()), "t");
2706        }
2707        assert!(matches!(
2708            ops[0],
2709            ChangesetOp::Insert {
2710                indirect: false,
2711                ..
2712            }
2713        ));
2714        assert!(matches!(ops[1], ChangesetOp::Insert { indirect: true, .. }));
2715    }
2716
2717    #[test]
2718    fn test_diffset_changeset_iter_yields_update_and_delete() {
2719        let table = TestTable::new("t", 2, 0);
2720        let starting = Insert::from(table.clone())
2721            .set(0, 1i64)
2722            .unwrap()
2723            .set(1, "a")
2724            .unwrap();
2725        let update = Update::<TestTable, ChangesetFormat, String, Vec<u8>>::from(table.clone())
2726            .set(0, 2i64, 2i64)
2727            .unwrap()
2728            .set(1, "before", "after")
2729            .unwrap();
2730        let delete = ChangeDelete::from(table.clone())
2731            .set(0, 3i64)
2732            .unwrap()
2733            .set(1, "gone")
2734            .unwrap();
2735        let frozen: DiffSet<ChangesetFormat, TestTable, String, Vec<u8>> = ChangesetBuilder::new()
2736            .insert(starting)
2737            .update(update)
2738            .delete(delete)
2739            .into();
2740
2741        let kinds: Vec<&'static str> = frozen
2742            .iter()
2743            .map(|op| match op {
2744                ChangesetOp::Insert { .. } => "insert",
2745                ChangesetOp::Update { .. } => "update",
2746                ChangesetOp::Delete { .. } => "delete",
2747            })
2748            .collect();
2749        assert_eq!(kinds, ["insert", "update", "delete"]);
2750    }
2751
2752    #[test]
2753    fn test_diffset_patchset_iter_exposes_pk_for_delete_and_update() {
2754        let table = TestTable::new("t", 2, 0);
2755        let starting = Insert::from(table.clone())
2756            .set(0, 1i64)
2757            .unwrap()
2758            .set(1, "a")
2759            .unwrap();
2760        let update = Update::<TestTable, PatchsetFormat, String, Vec<u8>>::from(table.clone())
2761            .set(0, 5i64)
2762            .unwrap()
2763            .set(1, "z")
2764            .unwrap();
2765        let delete: PatchDelete<TestTable, String, Vec<u8>> =
2766            PatchDelete::new(table.clone(), vec![Value::Integer(7)]);
2767
2768        let frozen: DiffSet<PatchsetFormat, TestTable, String, Vec<u8>> = PatchSet::new()
2769            .insert(starting)
2770            .update(update)
2771            .delete(delete)
2772            .into();
2773
2774        let mut saw_insert = false;
2775        let mut saw_update_pk: Option<i64> = None;
2776        let mut saw_delete_pk: Option<i64> = None;
2777        for op in frozen.iter() {
2778            match op {
2779                PatchsetOp::Insert { values, .. } => {
2780                    saw_insert = true;
2781                    assert!(matches!(values[0], Value::Integer(1)));
2782                }
2783                PatchsetOp::Update { pk, .. } => {
2784                    if let Value::Integer(id) = pk[0] {
2785                        saw_update_pk = Some(id);
2786                    }
2787                }
2788                PatchsetOp::Delete { pk, .. } => {
2789                    if let Value::Integer(id) = pk[0] {
2790                        saw_delete_pk = Some(id);
2791                    }
2792                }
2793            }
2794        }
2795        assert!(saw_insert);
2796        assert_eq!(saw_update_pk, Some(5));
2797        assert_eq!(saw_delete_pk, Some(7));
2798    }
2799
2800    #[test]
2801    fn test_diffset_tables_skips_empty() {
2802        let t1 = TestTable::new("t1", 2, 0);
2803        let t2 = TestTable::new("t2", 2, 0);
2804        let insert_t1 = Insert::from(t1.clone()).set(0, 1i64).unwrap();
2805        let insert_then_delete_t2 = Insert::from(t2.clone()).set(0, 9i64).unwrap();
2806        let delete_t2 = ChangeDelete::from(t2.clone()).set(0, 9i64).unwrap();
2807        let frozen: DiffSet<ChangesetFormat, TestTable, String, Vec<u8>> = ChangesetBuilder::new()
2808            .insert(insert_t1)
2809            .insert(insert_then_delete_t2)
2810            .delete(delete_t2)
2811            .into();
2812
2813        let names: Vec<&str> = frozen.tables().map(crate::DynTable::name).collect();
2814        assert_eq!(names, ["t1"]);
2815    }
2816}