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