Skip to main content

sqlite_diff_rs/builders/
update_operation.rs

1//! Submodule defining a builder for an update operation.
2
3use alloc::vec;
4use alloc::vec::Vec;
5use core::fmt::Debug;
6
7use crate::{
8    DynTable, SchemaWithPK,
9    builders::{ChangesetFormat, PatchsetFormat, format::Format, operation::Indirect},
10    encoding::{MaybeValue, Value},
11};
12
13#[derive(Debug, Clone)]
14/// Builder for an update operation, parameterized by the format type `F` and value types `S`, `B`.
15pub struct Update<T, F: Format<S, B>, S, B> {
16    /// The table being updated.
17    table: T,
18    /// Values for the updated row, stored as pairs of (old, new) values.
19    /// New values use `MaybeValue<S, B>` (Option<Value<S, B>>) where `None` = undefined/unchanged.
20    pub(super) values: Vec<(F::Old, MaybeValue<S, B>)>,
21    /// SQLite session-extension indirect flag. See [`Indirect`].
22    pub(crate) indirect: bool,
23}
24
25impl<
26    T: DynTable + PartialEq,
27    F: Format<S, B>,
28    S: PartialEq + AsRef<str>,
29    B: PartialEq + AsRef<[u8]>,
30> PartialEq for Update<T, F, S, B>
31where
32    F::Old: PartialEq,
33{
34    fn eq(&self, other: &Self) -> bool {
35        self.table == other.table && self.values == other.values && self.indirect == other.indirect
36    }
37}
38
39impl<T: DynTable + Eq, F: Format<S, B>, S: Eq + AsRef<str>, B: Eq + AsRef<[u8]>> Eq
40    for Update<T, F, S, B>
41where
42    F::Old: Eq,
43{
44}
45
46impl<T: DynTable, F: Format<S, B>, S: AsRef<str>, B: AsRef<[u8]>> From<Update<T, F, S, B>>
47    for Vec<(F::Old, MaybeValue<S, B>)>
48{
49    #[inline]
50    fn from(update: Update<T, F, S, B>) -> Self {
51        update.values
52    }
53}
54
55impl<T, F: Format<S, B>, S, B> AsRef<T> for Update<T, F, S, B> {
56    #[inline]
57    fn as_ref(&self) -> &T {
58        &self.table
59    }
60}
61
62impl<T, F: Format<S, B>, S: Clone, B: Clone> Update<T, F, S, B> {
63    /// Returns a reference to the (old, new) value pairs.
64    #[inline]
65    pub fn values(&self) -> &[(F::Old, MaybeValue<S, B>)] {
66        &self.values
67    }
68
69    #[inline]
70    /// Extract the primary key values from this update's values.
71    pub fn extract_pk(&self) -> Vec<Value<S, B>>
72    where
73        T: SchemaWithPK,
74    {
75        self.table.extract_pk(&self.values)
76    }
77}
78
79impl<T: DynTable, F: Format<S, B>, S: Clone + AsRef<str>, B: Clone + AsRef<[u8]>> From<T>
80    for Update<T, F, S, B>
81where
82    F::Old: Clone,
83{
84    #[inline]
85    fn from(table: T) -> Self {
86        let num_cols = table.number_of_columns();
87        Self {
88            table,
89            values: vec![(F::Old::default(), None); num_cols],
90            indirect: false,
91        }
92    }
93}
94
95impl<T, F: Format<S, B>, S, B> Indirect for Update<T, F, S, B> {
96    #[inline]
97    fn indirect(mut self, indirect: bool) -> Self {
98        self.indirect = indirect;
99        self
100    }
101}
102
103impl<T: DynTable, S: Clone + Debug + AsRef<str>, B: Clone + Debug + AsRef<[u8]>>
104    Update<T, ChangesetFormat, S, B>
105{
106    /// Sets the value for a specific column by index.
107    ///
108    /// # Arguments
109    ///
110    /// * `col_idx` - The index of the column to set.
111    /// * `old` - The old value for the column.
112    /// * `new` - The new value for the column.
113    ///
114    /// # Errors
115    ///
116    /// * `ColumnIndexOutOfBounds` - If the provided column index is out of bounds for the table schema.
117    ///
118    pub fn set(
119        mut self,
120        col_idx: usize,
121        old: impl Into<Value<S, B>>,
122        new: impl Into<Value<S, B>>,
123    ) -> Result<Self, crate::errors::Error> {
124        if col_idx >= self.values.len() {
125            return Err(crate::errors::Error::ColumnIndexOutOfBounds(
126                col_idx,
127                self.values.len(),
128            ));
129        }
130
131        self.values[col_idx] = (Some(old.into()), Some(new.into()));
132        Ok(self)
133    }
134
135    /// Sets only the new value for a column, leaving old as undefined.
136    ///
137    /// This is useful when the old value is not known (e.g., when parsing SQL
138    /// UPDATE statements where only the new value is specified).
139    ///
140    /// # Arguments
141    ///
142    /// * `col_idx` - The index of the column to set.
143    /// * `new` - The new value for the column.
144    ///
145    /// # Errors
146    ///
147    /// * `ColumnIndexOutOfBounds` - If the column index is out of bounds.
148    ///
149    /// # Example
150    ///
151    /// ```
152    /// use sqlite_diff_rs::{Update, ChangesetFormat, TableSchema};
153    ///
154    /// // CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)
155    /// let schema: TableSchema<String> = TableSchema::new("users".into(), 3, vec![1, 0, 0]);
156    ///
157    /// // UPDATE users SET name = 'Bob' WHERE id = 1
158    /// // We know id=1 (PK, unchanged) and name='Bob' (new), but not the old name
159    /// let update = Update::<_, ChangesetFormat, String, Vec<u8>>::from(schema)
160    ///     .set(0, 1i64, 1i64).unwrap()      // PK: old=1, new=1 (unchanged)
161    ///     .set_new(1, "Bob").unwrap();      // name: old=undefined, new="Bob"
162    /// ```
163    pub fn set_new(
164        mut self,
165        col_idx: usize,
166        new: impl Into<Value<S, B>>,
167    ) -> Result<Self, crate::errors::Error> {
168        if col_idx >= self.values.len() {
169            return Err(crate::errors::Error::ColumnIndexOutOfBounds(
170                col_idx,
171                self.values.len(),
172            ));
173        }
174
175        self.values[col_idx] = (None, Some(new.into()));
176        Ok(self)
177    }
178
179    /// Sets a column to NULL for both old and new values.
180    ///
181    /// # Errors
182    ///
183    /// * `ColumnIndexOutOfBounds` - If the column index is out of bounds for the table schema.
184    ///
185    /// # Example
186    ///
187    /// ```
188    /// use sqlite_diff_rs::{Update, ChangesetFormat, TableSchema};
189    ///
190    /// // CREATE TABLE items (id INTEGER PRIMARY KEY, description TEXT)
191    /// let schema: TableSchema<String> = TableSchema::new("items".into(), 2, vec![1, 0]);
192    ///
193    /// // UPDATE items SET description = NULL WHERE id = 1 AND description = NULL
194    /// let update = Update::<_, ChangesetFormat, String, Vec<u8>>::from(schema)
195    ///     .set(0, 1i64, 1i64).unwrap()
196    ///     .set_null(1).unwrap();
197    /// ```
198    #[inline]
199    pub fn set_null(self, col_idx: usize) -> Result<Self, crate::errors::Error>
200    where
201        S: Default,
202        B: Default,
203    {
204        self.set(col_idx, Value::Null, Value::Null)
205    }
206}
207
208impl<T: DynTable, S: AsRef<str>, B: AsRef<[u8]>> Update<T, PatchsetFormat, S, B> {
209    /// Sets the value for a specific column by index.
210    ///
211    /// # Implementation Note
212    ///
213    /// In the patchset format, the old value is not stored for updates,
214    /// so we set it to the default value of `()`. Only the new value is stored.
215    ///
216    /// # Arguments
217    ///
218    /// * `col_idx` - The index of the column to set.
219    /// * `new` - The new value for the column.
220    ///
221    /// # Errors
222    ///
223    /// * `ColumnIndexOutOfBounds` - If the provided column index is out of bounds for the table schema.
224    ///
225    pub fn set(
226        mut self,
227        col_idx: usize,
228        new: impl Into<Value<S, B>>,
229    ) -> Result<Self, crate::errors::Error> {
230        if col_idx >= self.values.len() {
231            return Err(crate::errors::Error::ColumnIndexOutOfBounds(
232                col_idx,
233                self.values.len(),
234            ));
235        }
236
237        self.values[col_idx] = ((), Some(new.into()));
238        Ok(self)
239    }
240
241    /// Sets a column to NULL.
242    ///
243    /// # Errors
244    ///
245    /// * `ColumnIndexOutOfBounds` - If the column index is out of bounds for the table schema.
246    ///
247    /// # Example
248    ///
249    /// ```
250    /// use sqlite_diff_rs::{Update, PatchsetFormat, TableSchema};
251    ///
252    /// // CREATE TABLE items (id INTEGER PRIMARY KEY, description TEXT)
253    /// let schema: TableSchema<String> = TableSchema::new("items".into(), 2, vec![1, 0]);
254    ///
255    /// // UPDATE items SET description = NULL WHERE id = 1
256    /// let update = Update::<_, PatchsetFormat, String, Vec<u8>>::from(schema)
257    ///     .set(0, 1i64).unwrap()
258    ///     .set_null(1).unwrap();
259    /// ```
260    pub fn set_null(self, col_idx: usize) -> Result<Self, crate::errors::Error>
261    where
262        S: Default,
263        B: Default,
264    {
265        self.set(col_idx, Value::Null)
266    }
267}
268
269#[cfg(test)]
270mod tests {
271    use super::Update;
272    use crate::builders::{ChangesetFormat, PatchsetFormat};
273    use crate::errors::Error;
274    use crate::schema::SimpleTable;
275    use alloc::string::String;
276    use alloc::vec::Vec;
277
278    fn users() -> SimpleTable {
279        SimpleTable::new("users", &["id", "name"], &[0])
280    }
281
282    #[test]
283    fn test_changeset_update_set_out_of_bounds() {
284        let err = Update::<_, ChangesetFormat, String, Vec<u8>>::from(users())
285            .set(7, 1i64, 2i64)
286            .unwrap_err();
287        assert!(
288            matches!(err, Error::ColumnIndexOutOfBounds(7, 2)),
289            "got {err:?}"
290        );
291    }
292
293    #[test]
294    fn test_changeset_update_set_new_out_of_bounds() {
295        let err = Update::<_, ChangesetFormat, String, Vec<u8>>::from(users())
296            .set_new(7, "x")
297            .unwrap_err();
298        assert!(
299            matches!(err, Error::ColumnIndexOutOfBounds(7, 2)),
300            "got {err:?}"
301        );
302    }
303
304    #[test]
305    fn test_changeset_update_set_null_out_of_bounds() {
306        let err = Update::<_, ChangesetFormat, String, Vec<u8>>::from(users())
307            .set_null(2)
308            .unwrap_err();
309        assert!(
310            matches!(err, Error::ColumnIndexOutOfBounds(2, 2)),
311            "got {err:?}"
312        );
313    }
314
315    #[test]
316    fn test_patchset_update_set_out_of_bounds() {
317        let err = Update::<_, PatchsetFormat, String, Vec<u8>>::from(users())
318            .set(3, 1i64)
319            .unwrap_err();
320        assert!(
321            matches!(err, Error::ColumnIndexOutOfBounds(3, 2)),
322            "got {err:?}"
323        );
324    }
325
326    #[test]
327    fn test_patchset_update_set_null_out_of_bounds() {
328        let err = Update::<_, PatchsetFormat, String, Vec<u8>>::from(users())
329            .set_null(5)
330            .unwrap_err();
331        assert!(
332            matches!(err, Error::ColumnIndexOutOfBounds(5, 2)),
333            "got {err:?}"
334        );
335    }
336
337    #[test]
338    fn test_update_eq() {
339        let a = Update::<_, ChangesetFormat, String, Vec<u8>>::from(users())
340            .set(0, 1i64, 2i64)
341            .unwrap();
342        let b = Update::<_, ChangesetFormat, String, Vec<u8>>::from(users())
343            .set(0, 1i64, 2i64)
344            .unwrap();
345        assert_eq!(a, b);
346
347        let c = Update::<_, ChangesetFormat, String, Vec<u8>>::from(users())
348            .set(0, 1i64, 3i64)
349            .unwrap();
350        assert_ne!(a, c);
351    }
352}