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