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