Skip to main content

sqlite_diff_rs/builders/
view.rs

1//! Read-only views of operations inside a [`DiffSet`].
2//!
3//! [`DiffSet::iter`] (changeset format) and [`DiffSet::iter`] (patchset
4//! format) yield one of these per stored operation. The fields borrow
5//! from the underlying [`DiffSet`], so consumers can render or inspect
6//! the contents without copying any row data.
7//!
8//! Patchsets do not carry full old-row values, so [`PatchsetOp::Delete`]
9//! and [`PatchsetOp::Update`] expose only the primary-key columns. The
10//! [`PatchsetOp::Update`] `entries` slice is the raw `(F::Old, new)`
11//! storage (with `F::Old = ()` for patchsets); call `.iter().map(|(_, v)| v)`
12//! to drop the unit and walk just the new values.
13//!
14//! [`DiffSet`]: super::DiffSet
15//! [`DiffSet::iter`]: super::DiffSet::iter
16
17use alloc::vec::Vec;
18
19use crate::encoding::Value;
20use crate::schema::SchemaWithPK;
21
22/// `(old, new)` values for one changeset UPDATE column, where `None` is the
23/// undefined wire marker.
24///
25/// A changeset UPDATE carries the old image of the key columns and of the
26/// changed columns, and the new image of the changed columns only, so the four
27/// pair shapes are distinct. [`ChangesetUpdatePairExt::is_changed`] reads them.
28pub type ChangesetUpdatePair<S, B> = (Option<Value<S, B>>, Option<Value<S, B>>);
29
30/// Reads changeset UPDATE pair wire semantics.
31pub trait ChangesetUpdatePairExt {
32    /// Whether the wire says this column's value changed.
33    ///
34    /// Undefined on both sides is a column outside the diff. Old-only is the
35    /// row identity, which SQLite writes for every key column of every UPDATE,
36    /// so it did not change. Both sides present changed exactly when the values
37    /// differ, which is how an unchanged key column encoded by this crate
38    /// reads. New without old carries no old image to compare, so it counts as
39    /// changed.
40    #[must_use]
41    fn is_changed(&self) -> bool;
42}
43
44impl<S, B> ChangesetUpdatePairExt for ChangesetUpdatePair<S, B>
45where
46    Value<S, B>: PartialEq,
47{
48    #[inline]
49    fn is_changed(&self) -> bool {
50        match self {
51            (_, None) => false,
52            (Some(old), Some(new)) => old != new,
53            (None, Some(_)) => true,
54        }
55    }
56}
57
58/// Entry stored per column in a patchset UPDATE: a unit (the format
59/// does not carry the old value) plus the new value (or `None` for
60/// unchanged columns).
61pub type PatchsetUpdateEntry<S, B> = ((), Option<Value<S, B>>);
62
63/// View over one operation in a changeset.
64#[derive(Debug)]
65pub enum ChangesetOp<'a, T, S, B> {
66    /// `INSERT`. Carries every column's value in column order.
67    Insert {
68        /// Table this row belongs to.
69        table: &'a T,
70        /// Full row values, one per column.
71        values: &'a [Value<S, B>],
72        /// SQLite session-extension indirect flag.
73        indirect: bool,
74    },
75    /// `UPDATE`. Carries `(old, new)` pairs per column. `None` in either
76    /// slot means "undefined" (the column was not part of the diff).
77    Update {
78        /// Table this row belongs to.
79        table: &'a T,
80        /// `(old, new)` pairs, one per column.
81        values: &'a [ChangesetUpdatePair<S, B>],
82        /// SQLite session-extension indirect flag.
83        indirect: bool,
84    },
85    /// `DELETE`. Carries the full old-row values in column order.
86    Delete {
87        /// Table this row belongs to.
88        table: &'a T,
89        /// Full old-row values, one per column.
90        old_values: &'a [Value<S, B>],
91        /// SQLite session-extension indirect flag.
92        indirect: bool,
93    },
94}
95
96impl<'a, T, S, B> ChangesetOp<'a, T, S, B> {
97    /// Returns the schema of the table this operation applies to.
98    #[must_use]
99    pub fn table(&self) -> &'a T {
100        match self {
101            Self::Insert { table, .. }
102            | Self::Update { table, .. }
103            | Self::Delete { table, .. } => table,
104        }
105    }
106
107    /// Returns the SQLite session-extension indirect flag.
108    #[must_use]
109    pub fn indirect(&self) -> bool {
110        match self {
111            Self::Insert { indirect, .. }
112            | Self::Update { indirect, .. }
113            | Self::Delete { indirect, .. } => *indirect,
114        }
115    }
116}
117
118impl<T, S, B> ChangesetOp<'_, T, S, B>
119where
120    Value<S, B>: PartialEq,
121{
122    /// Returns the indices of the columns whose pairs are
123    /// [`ChangesetUpdatePairExt::is_changed`], and nothing for a non-UPDATE.
124    pub fn changed_column_indices(&self) -> impl Iterator<Item = usize> {
125        let values: &[ChangesetUpdatePair<S, B>] = match self {
126            Self::Update { values, .. } => values,
127            Self::Insert { .. } | Self::Delete { .. } => &[],
128        };
129        values
130            .iter()
131            .enumerate()
132            .filter_map(|(index, pair)| pair.is_changed().then_some(index))
133    }
134}
135
136impl<T: SchemaWithPK, S: Clone, B: Clone> ChangesetOp<'_, T, S, B> {
137    /// Returns the primary-key cells of this operation, in key order.
138    ///
139    /// `Insert` and `Delete` read the key from the full row via
140    /// [`SchemaWithPK::extract_pk`]. For `Update` each key cell is taken
141    /// old-first: a changeset UPDATE carries the key in the old slot as the
142    /// row identity, while the new slot is absent for a key column that did
143    /// not change, so reading the new slot (as `extract_pk` would over the
144    /// pair storage) can yield `None`.
145    #[must_use]
146    pub fn primary_key(&self) -> Vec<Value<S, B>> {
147        match *self {
148            Self::Insert { table, values, .. } => table.extract_pk(&values),
149            Self::Delete {
150                table, old_values, ..
151            } => table.extract_pk(&old_values),
152            Self::Update { table, values, .. } => table
153                .primary_key_columns()
154                .map(|col_idx| {
155                    let (old, new) = &values[col_idx];
156                    old.clone().or_else(|| new.clone()).unwrap_or(Value::Null)
157                })
158                .collect(),
159        }
160    }
161}
162
163/// View over one operation in a patchset.
164#[derive(Debug)]
165pub enum PatchsetOp<'a, T, S, B> {
166    /// `INSERT`. Carries every column's value in column order.
167    Insert {
168        /// Table this row belongs to.
169        table: &'a T,
170        /// Full row values, one per column.
171        values: &'a [Value<S, B>],
172        /// SQLite session-extension indirect flag.
173        indirect: bool,
174    },
175    /// `UPDATE`. Carries primary-key values plus a `(unit, new)` entry
176    /// per non-PK column. The unit reflects the patchset format's
177    /// missing old-value storage; consumers can map `|(_, v)| v` to walk
178    /// just the new values.
179    Update {
180        /// Table this row belongs to.
181        table: &'a T,
182        /// Primary-key column values for the row being updated.
183        pk: &'a [Value<S, B>],
184        /// `(unit, new)` entries, one per column.
185        entries: &'a [PatchsetUpdateEntry<S, B>],
186        /// SQLite session-extension indirect flag.
187        indirect: bool,
188    },
189    /// `DELETE`. Carries only the primary-key columns; the rest of the
190    /// old row is not stored in patchset format.
191    Delete {
192        /// Table this row belongs to.
193        table: &'a T,
194        /// Primary-key column values for the row being deleted.
195        pk: &'a [Value<S, B>],
196        /// SQLite session-extension indirect flag.
197        indirect: bool,
198    },
199}
200
201impl<'a, T, S, B> PatchsetOp<'a, T, S, B> {
202    /// Returns the schema of the table this operation applies to.
203    #[must_use]
204    pub fn table(&self) -> &'a T {
205        match self {
206            Self::Insert { table, .. }
207            | Self::Update { table, .. }
208            | Self::Delete { table, .. } => table,
209        }
210    }
211
212    /// Returns the SQLite session-extension indirect flag.
213    #[must_use]
214    pub fn indirect(&self) -> bool {
215        match self {
216            Self::Insert { indirect, .. }
217            | Self::Update { indirect, .. }
218            | Self::Delete { indirect, .. } => *indirect,
219        }
220    }
221
222    /// For an `Update` op, returns the new values per column (with the
223    /// unit dropped). Returns `None` for `Insert` and `Delete`.
224    #[must_use]
225    pub fn update_new_values(&self) -> Option<Vec<&'a Option<Value<S, B>>>> {
226        match self {
227            Self::Update { entries, .. } => Some(entries.iter().map(|((), v)| v).collect()),
228            _ => None,
229        }
230    }
231}
232
233impl<T: SchemaWithPK, S: Clone, B: Clone> PatchsetOp<'_, T, S, B> {
234    /// Returns the primary-key cells of this operation, in key order.
235    ///
236    /// `Insert` reads the key from the full row via
237    /// [`SchemaWithPK::extract_pk`]. `Update` and `Delete` already store only
238    /// the key columns in key order, so their cells are returned directly.
239    #[must_use]
240    pub fn primary_key(&self) -> Vec<Value<S, B>> {
241        match *self {
242            Self::Insert { table, values, .. } => table.extract_pk(&values),
243            Self::Update { pk, .. } | Self::Delete { pk, .. } => pk.to_vec(),
244        }
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251    use crate::{ChangeDelete, ChangeSet, DiffOps, Insert, ParsedDiffSet, PatchSet, SimpleTable};
252    use alloc::string::String;
253    use alloc::vec;
254
255    type Pair = ChangesetUpdatePair<String, Vec<u8>>;
256    type Entry = PatchsetUpdateEntry<String, Vec<u8>>;
257    type Val = Value<String, Vec<u8>>;
258
259    #[test]
260    fn changeset_primary_key_single_key() {
261        let schema = SimpleTable::new("kv", &["id", "val"], &[0]);
262
263        let insert_values: Vec<Val> = vec![Value::Integer(1), Value::Text("a".into())];
264        let insert = ChangesetOp::Insert {
265            table: &schema,
266            values: &insert_values,
267            indirect: false,
268        };
269        assert_eq!(insert.primary_key(), vec![Value::Integer(1)]);
270
271        // UPDATE touching only the non-key column: the key sits in the old slot
272        // with an undefined (None) new slot, so old-first must recover it.
273        let update_values: Vec<Pair> = vec![
274            (Some(Value::Integer(2)), None),
275            (
276                Some(Value::Text("before".into())),
277                Some(Value::Text("after".into())),
278            ),
279        ];
280        let update = ChangesetOp::Update {
281            table: &schema,
282            values: &update_values,
283            indirect: false,
284        };
285        assert_eq!(update.primary_key(), vec![Value::Integer(2)]);
286
287        let delete_values: Vec<Val> = vec![Value::Integer(3), Value::Text("gone".into())];
288        let delete = ChangesetOp::Delete {
289            table: &schema,
290            old_values: &delete_values,
291            indirect: false,
292        };
293        assert_eq!(delete.primary_key(), vec![Value::Integer(3)]);
294    }
295
296    #[test]
297    fn changeset_primary_key_composite_reordered_key() {
298        // Columns (a, b, c), key (b, a): flags [2, 1, 0], key order is b then a.
299        let schema = SimpleTable::new("abc", &["a", "b", "c"], &[1, 0]);
300        let expected: Vec<Val> = vec![Value::Integer(20), Value::Integer(10)];
301
302        let insert_values: Vec<Val> = vec![
303            Value::Integer(10),
304            Value::Integer(20),
305            Value::Text("z".into()),
306        ];
307        let insert = ChangesetOp::Insert {
308            table: &schema,
309            values: &insert_values,
310            indirect: false,
311        };
312        assert_eq!(insert.primary_key(), expected);
313
314        // Only non-key column c changes, so both key columns are undefined in
315        // the new slot and must be recovered from the old slot.
316        let update_values: Vec<Pair> = vec![
317            (Some(Value::Integer(10)), None),
318            (Some(Value::Integer(20)), None),
319            (
320                Some(Value::Text("z".into())),
321                Some(Value::Text("z2".into())),
322            ),
323        ];
324        let update = ChangesetOp::Update {
325            table: &schema,
326            values: &update_values,
327            indirect: false,
328        };
329        assert_eq!(update.primary_key(), expected);
330
331        let delete_values: Vec<Val> = vec![
332            Value::Integer(10),
333            Value::Integer(20),
334            Value::Text("z".into()),
335        ];
336        let delete = ChangesetOp::Delete {
337            table: &schema,
338            old_values: &delete_values,
339            indirect: false,
340        };
341        assert_eq!(delete.primary_key(), expected);
342    }
343
344    #[test]
345    fn patchset_primary_key_variants() {
346        let kv = SimpleTable::new("kv", &["id", "val"], &[0]);
347
348        let insert_values: Vec<Val> = vec![Value::Integer(1), Value::Text("a".into())];
349        let insert = PatchsetOp::Insert {
350            table: &kv,
351            values: &insert_values,
352            indirect: false,
353        };
354        assert_eq!(insert.primary_key(), vec![Value::Integer(1)]);
355
356        let update_pk: Vec<Val> = vec![Value::Integer(9)];
357        let entries: Vec<Entry> = vec![((), None), ((), Some(Value::Text("z".into())))];
358        let update = PatchsetOp::Update {
359            table: &kv,
360            pk: &update_pk,
361            entries: &entries,
362            indirect: false,
363        };
364        assert_eq!(update.primary_key(), vec![Value::Integer(9)]);
365
366        let delete_pk: Vec<Val> = vec![Value::Integer(7)];
367        let delete = PatchsetOp::Delete {
368            table: &kv,
369            pk: &delete_pk,
370            indirect: false,
371        };
372        assert_eq!(delete.primary_key(), vec![Value::Integer(7)]);
373
374        // Composite key: INSERT recovers key order (b, a) from the full row.
375        let abc = SimpleTable::new("abc", &["a", "b", "c"], &[1, 0]);
376        let abc_values: Vec<Val> = vec![
377            Value::Integer(10),
378            Value::Integer(20),
379            Value::Text("z".into()),
380        ];
381        let abc_insert = PatchsetOp::Insert {
382            table: &abc,
383            values: &abc_values,
384            indirect: false,
385        };
386        assert_eq!(
387            abc_insert.primary_key(),
388            vec![Value::Integer(20), Value::Integer(10)]
389        );
390    }
391
392    #[test]
393    fn changeset_primary_key_through_iter_composite() {
394        // Build a real changeset and read the key back through iter(), so the
395        // extract_pk key ordering is exercised end to end.
396        let schema = SimpleTable::new("abc", &["a", "b", "c"], &[1, 0]);
397        let cs: ChangeSet<SimpleTable, String, Vec<u8>> = ChangeSet::new().insert(
398            Insert::from(schema)
399                .set(0, 10i64)
400                .unwrap()
401                .set(1, 20i64)
402                .unwrap()
403                .set(2, "z")
404                .unwrap(),
405        );
406        let ops: Vec<_> = cs.iter().collect();
407        assert_eq!(ops.len(), 1);
408        assert_eq!(
409            ops[0].primary_key(),
410            vec![Value::Integer(20), Value::Integer(10)]
411        );
412    }
413
414    #[test]
415    fn patchset_primary_key_through_iter_composite_delete() {
416        // A digested DELETE stores its key in key order; primary_key() must
417        // return it in that same order through iter().
418        let schema = SimpleTable::new("abc", &["a", "b", "c"], &[1, 0]);
419        let mut ps: PatchSet<SimpleTable, String, Vec<u8>> = PatchSet::new();
420        ps.add_table(&schema);
421        ps.digest_sql("DELETE FROM abc WHERE a = 10 AND b = 20")
422            .unwrap();
423        let ops: Vec<_> = ps.iter().collect();
424        assert_eq!(ops.len(), 1);
425        assert_eq!(
426            ops[0].primary_key(),
427            vec![Value::Integer(20), Value::Integer(10)]
428        );
429    }
430
431    #[test]
432    fn primary_key_on_parsed_changeset_ops() {
433        // The whole point: ops from a parsed diff are over TableSchema<String>,
434        // not SimpleTable. primary_key() must be callable there.
435        let schema = SimpleTable::new("kv", &["id", "val"], &[0]);
436        let bytes = ChangeSet::<SimpleTable, String, Vec<u8>>::new()
437            .insert(
438                Insert::from(schema.clone())
439                    .set(0, 1i64)
440                    .unwrap()
441                    .set(1, "a")
442                    .unwrap(),
443            )
444            .delete(
445                ChangeDelete::from(schema)
446                    .set(0, 2i64)
447                    .unwrap()
448                    .set(1, "b")
449                    .unwrap(),
450            )
451            .build();
452        let ParsedDiffSet::Changeset(set) = ParsedDiffSet::parse(&bytes).unwrap() else {
453            panic!("expected changeset");
454        };
455        let keys: Vec<Vec<Val>> = set.iter().map(|op| op.primary_key()).collect();
456        assert!(keys.contains(&vec![Value::Integer(1)]));
457        assert!(keys.contains(&vec![Value::Integer(2)]));
458    }
459
460    #[test]
461    fn primary_key_on_parsed_patchset_ops() {
462        let schema = SimpleTable::new("kv", &["id", "val"], &[0]);
463        let mut ps: PatchSet<SimpleTable, String, Vec<u8>> = PatchSet::new();
464        ps.add_table(&schema);
465        ps.digest_sql("INSERT INTO kv (id, val) VALUES (1, 'a')")
466            .unwrap();
467        ps.digest_sql("DELETE FROM kv WHERE id = 2").unwrap();
468        let bytes = ps.build();
469        let ParsedDiffSet::Patchset(set) = ParsedDiffSet::parse(&bytes).unwrap() else {
470            panic!("expected patchset");
471        };
472        let keys: Vec<Vec<Val>> = set.iter().map(|op| op.primary_key()).collect();
473        assert!(keys.contains(&vec![Value::Integer(1)]));
474        assert!(keys.contains(&vec![Value::Integer(2)]));
475    }
476}