Skip to main content

sqlite_diff_rs/builders/
operation.rs

1//! Schema-less operation enum for internal use in `DiffSetBuilder`.
2//!
3//! Operations store only row data (values), not the table schema `T`.
4//! The schema lives as the key of the outer `IndexMap` in `DiffSetBuilder`.
5//! All consolidation logic (Operation + Operation) is defined here.
6
7use alloc::vec::Vec;
8use core::fmt::Debug;
9
10use crate::{
11    builders::{ChangesetFormat, PatchsetFormat, format::Format},
12    encoding::{MaybeValue, Value},
13};
14
15/// A schema-less database operation, parameterized by format `F` and value
16/// types `S`, `B`.
17///
18/// Each variant carries the SQLite session-extension `indirect` flag, which
19/// distinguishes direct application writes (`false`) from trigger-induced
20/// or cascading changes (`true`). The table schema `T` is NOT stored here;
21/// it lives as the key in `DiffSetBuilder`'s
22/// `IndexMap<T, IndexMap<Vec<Value<S, B>>, Operation<F, S, B>>>`.
23#[derive(Debug, Clone)]
24pub(crate) enum Operation<F: Format<S, B>, S, B> {
25    /// A row was inserted. Stores all column values.
26    Insert {
27        /// Full row values, one per column.
28        values: Vec<Value<S, B>>,
29        /// SQLite session-extension indirect flag.
30        indirect: bool,
31    },
32    /// A row was deleted. Stores format-specific delete data:
33    /// - Changeset: `Vec<Value<S, B>>` (full old-row values)
34    /// - Patchset: `()` (PK is stored as the `IndexMap` key)
35    Delete {
36        /// Format-specific delete payload.
37        data: F::DeleteData,
38        /// SQLite session-extension indirect flag.
39        indirect: bool,
40    },
41    /// A row was updated. Stores `(old, new)` pairs per column.
42    /// - Changeset: `(MaybeValue<S, B>, MaybeValue<S, B>)` per column (None = undefined)
43    /// - Patchset: `((), MaybeValue<S, B>)` per column
44    Update {
45        /// `(old, new)` pairs, one per column.
46        values: Vec<(F::Old, MaybeValue<S, B>)>,
47        /// SQLite session-extension indirect flag.
48        indirect: bool,
49    },
50}
51
52impl<F: Format<S, B>, S, B> Operation<F, S, B> {
53    /// Returns the indirect flag of this operation.
54    #[inline]
55    pub(crate) fn indirect(&self) -> bool {
56        match self {
57            Self::Insert { indirect, .. }
58            | Self::Delete { indirect, .. }
59            | Self::Update { indirect, .. } => *indirect,
60        }
61    }
62}
63
64impl<F: Format<S, B>, S: PartialEq + AsRef<str>, B: PartialEq + AsRef<[u8]>> PartialEq
65    for Operation<F, S, B>
66where
67    F::DeleteData: PartialEq,
68    F::Old: PartialEq,
69{
70    fn eq(&self, other: &Self) -> bool {
71        if self.indirect() != other.indirect() {
72            return false;
73        }
74        match (self, other) {
75            (Self::Insert { values: a, .. }, Self::Insert { values: b, .. }) => a == b,
76            (Self::Delete { data: a, .. }, Self::Delete { data: b, .. }) => a == b,
77            (Self::Update { values: a, .. }, Self::Update { values: b, .. }) => a == b,
78            _ => false,
79        }
80    }
81}
82
83impl<F: Format<S, B>, S: Eq + AsRef<str>, B: Eq + AsRef<[u8]>> Eq for Operation<F, S, B>
84where
85    F::DeleteData: Eq,
86    F::Old: Eq,
87{
88}
89
90/// Builders that carry the SQLite session-extension indirect-change flag.
91///
92/// Implemented for [`Insert`](crate::Insert), [`Update`](crate::Update),
93/// [`ChangeDelete`](crate::ChangeDelete), and [`PatchDelete`](crate::PatchDelete).
94///
95/// The flag distinguishes direct application writes (`false`) from changes
96/// produced by triggers or foreign-key cascades (`true`). SQLite's session
97/// extension writes it as a single byte after each operation's op-code so
98/// consumers can filter on it when applying changesets. Defaults to `false`
99/// on construction.
100///
101/// See the [SQLite session-extension docs](https://www.sqlite.org/session/sqlite3session_indirect.html).
102pub trait Indirect: Sized {
103    /// Mark this operation as indirect (trigger-induced or cascading).
104    #[must_use]
105    fn indirect(self, indirect: bool) -> Self;
106}
107
108/// Trait for reversing operations.
109///
110/// Reversing a database operation is useful for creating inverse changesets
111/// (undo), resolving conflicts in distributed systems, and testing
112/// bidirectional synchronization.
113pub trait Reverse {
114    /// The reverse of this operation.
115    type Output;
116
117    /// Returns the reverse of this operation.
118    fn reverse(self) -> Self::Output;
119}
120
121impl<S: Clone + Debug + AsRef<str>, B: Clone + Debug + AsRef<[u8]>> Reverse
122    for Operation<ChangesetFormat, S, B>
123{
124    type Output = Self;
125
126    fn reverse(self) -> Self::Output {
127        match self {
128            // INSERT reversed becomes DELETE (same values)
129            Self::Insert { values, indirect } => Self::Delete {
130                data: values,
131                indirect,
132            },
133            // DELETE reversed becomes INSERT (same values)
134            Self::Delete { data, indirect } => Self::Insert {
135                values: data,
136                indirect,
137            },
138            // UPDATE reversed becomes UPDATE with old/new swapped
139            Self::Update { values, indirect } => Self::Update {
140                values: values.into_iter().map(|(old, new)| (new, old)).collect(),
141                indirect,
142            },
143        }
144    }
145}
146
147// ============================================================================
148// Operation + Operation for Changeset
149// ============================================================================
150
151impl<S: Clone + Debug + PartialEq + AsRef<str>, B: Clone + Debug + PartialEq + AsRef<[u8]>>
152    core::ops::Add for Operation<ChangesetFormat, S, B>
153{
154    type Output = Option<Self>;
155
156    fn add(self, rhs: Self) -> Self::Output {
157        // "Last-write-wins": the merged op carries the rhs operand's
158        // indirect flag.
159        let indirect = rhs.indirect();
160        match (self, rhs) {
161            // INSERT + INSERT: keep first values
162            (Self::Insert { values: lhs, .. }, Self::Insert { .. }) => Some(Self::Insert {
163                values: lhs,
164                indirect,
165            }),
166
167            // INSERT + UPDATE: apply update to insert values
168            (
169                Self::Insert { mut values, .. },
170                Self::Update {
171                    values: updates, ..
172                },
173            ) => {
174                for (idx, (_old, new)) in updates.into_iter().enumerate() {
175                    if let Some(new_val) = new {
176                        values[idx] = new_val;
177                    }
178                }
179                Some(Self::Insert { values, indirect })
180            }
181
182            // INSERT + DELETE: cancel out
183            (Self::Insert { .. }, Self::Delete { .. }) => None,
184
185            // UPDATE + INSERT: keep update
186            (Self::Update { values: lhs, .. }, Self::Insert { .. }) => Some(Self::Update {
187                values: lhs,
188                indirect,
189            }),
190
191            // UPDATE + UPDATE: keep original old, use final new
192            (Self::Update { values: lhs, .. }, Self::Update { values: rhs, .. }) => {
193                let merged = lhs
194                    .into_iter()
195                    .zip(rhs)
196                    .map(|((old, _mid), (_mid2, new))| (old, new))
197                    .collect();
198                Some(Self::Update {
199                    values: merged,
200                    indirect,
201                })
202            }
203
204            // UPDATE + DELETE: delete with original old values
205            (Self::Update { values: upd, .. }, Self::Delete { .. }) => {
206                // Collect old values, converting MaybeValue to Value (None becomes Null)
207                let old_values = upd
208                    .into_iter()
209                    .map(|(old, _new)| old.unwrap_or(Value::Null))
210                    .collect();
211                Some(Self::Delete {
212                    data: old_values,
213                    indirect,
214                })
215            }
216
217            // DELETE + INSERT: update if different, cancel if same
218            (Self::Delete { data: del, .. }, Self::Insert { values: ins, .. }) => {
219                if del == ins {
220                    None // Same values, cancel out
221                } else {
222                    let update_values = del
223                        .into_iter()
224                        .zip(ins)
225                        .map(|(old, new)| (Some(old), Some(new)))
226                        .collect();
227                    Some(Self::Update {
228                        values: update_values,
229                        indirect,
230                    })
231                }
232            }
233
234            // DELETE + UPDATE or DELETE + DELETE: keep the delete with rhs's indirect
235            (Self::Delete { data: lhs, .. }, Self::Update { .. } | Self::Delete { .. }) => {
236                Some(Self::Delete {
237                    data: lhs,
238                    indirect,
239                })
240            }
241        }
242    }
243}
244
245// ============================================================================
246// Operation + Operation for Patchset
247// ============================================================================
248
249impl<S: Clone + PartialEq + AsRef<str>, B: Clone + PartialEq + AsRef<[u8]>> core::ops::Add
250    for Operation<PatchsetFormat, S, B>
251{
252    type Output = Option<Self>;
253
254    fn add(self, rhs: Self) -> Self::Output {
255        let indirect = rhs.indirect();
256        match (self, rhs) {
257            // INSERT + INSERT: keep first
258            (Self::Insert { values: lhs, .. }, Self::Insert { .. }) => Some(Self::Insert {
259                values: lhs,
260                indirect,
261            }),
262
263            // INSERT + UPDATE: apply update to insert values
264            (
265                Self::Insert { mut values, .. },
266                Self::Update {
267                    values: updates, ..
268                },
269            ) => {
270                for (idx, ((), new)) in updates.into_iter().enumerate() {
271                    if let Some(new_val) = new {
272                        values[idx] = new_val;
273                    }
274                }
275                Some(Self::Insert { values, indirect })
276            }
277
278            // INSERT + DELETE: cancel out
279            (Self::Insert { .. }, Self::Delete { data: (), .. }) => None,
280
281            // UPDATE + INSERT: keep update
282            (Self::Update { values: lhs, .. }, Self::Insert { .. }) => Some(Self::Update {
283                values: lhs,
284                indirect,
285            }),
286
287            // UPDATE + UPDATE: keep original old (unit), use final new
288            (Self::Update { values: lhs, .. }, Self::Update { values: rhs, .. }) => {
289                let merged = lhs
290                    .into_iter()
291                    .zip(rhs)
292                    .map(|((old, _mid), (_mid2, new))| (old, new))
293                    .collect();
294                Some(Self::Update {
295                    values: merged,
296                    indirect,
297                })
298            }
299
300            // UPDATE + DELETE: keep delete (patchset doesn't need old values)
301            (Self::Update { .. }, Self::Delete { data, .. }) => {
302                Some(Self::Delete { data, indirect })
303            }
304
305            // DELETE + INSERT: always becomes update (patchset can't compare old values)
306            (Self::Delete { data: (), .. }, Self::Insert { values: ins, .. }) => {
307                let update_values = ins.into_iter().map(|new| ((), Some(new))).collect();
308                Some(Self::Update {
309                    values: update_values,
310                    indirect,
311                })
312            }
313
314            // DELETE + UPDATE or DELETE + DELETE: keep the delete with rhs's indirect
315            (Self::Delete { data: lhs, .. }, Self::Update { .. } | Self::Delete { .. }) => {
316                Some(Self::Delete {
317                    data: lhs,
318                    indirect,
319                })
320            }
321        }
322    }
323}