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}