Skip to main content

sqlite_diff_rs/builders/
delete_operation.rs

1//! Submodule defining a builder for a delete operation.
2
3use alloc::vec;
4use alloc::vec::Vec;
5use core::fmt::Debug;
6
7use crate::{DynTable, builders::operation::Indirect, encoding::Value};
8
9#[derive(Debug, Clone)]
10/// Represents a delete operation in changeset format.
11///
12/// Stores the full old-row values for all columns.
13pub struct ChangeDelete<T: DynTable, S: AsRef<str>, B: AsRef<[u8]>> {
14    pub(crate) table: T,
15    /// Old values for the deleted row.
16    pub(crate) values: Vec<Value<S, B>>,
17    /// SQLite session-extension indirect flag. See [`Indirect`].
18    pub(crate) indirect: bool,
19}
20
21impl<T: DynTable + PartialEq, S: PartialEq + AsRef<str>, B: PartialEq + AsRef<[u8]>> PartialEq
22    for ChangeDelete<T, S, B>
23{
24    fn eq(&self, other: &Self) -> bool {
25        self.table == other.table && self.values == other.values && self.indirect == other.indirect
26    }
27}
28
29impl<T: DynTable + Eq, S: Eq + AsRef<str>, B: Eq + AsRef<[u8]>> Eq for ChangeDelete<T, S, B> {}
30
31impl<T: DynTable, S: AsRef<str>, B: AsRef<[u8]>> AsRef<T> for ChangeDelete<T, S, B> {
32    #[inline]
33    fn as_ref(&self) -> &T {
34        &self.table
35    }
36}
37
38impl<T: DynTable, S: Default + Clone + AsRef<str>, B: Default + Clone + AsRef<[u8]>> From<T>
39    for ChangeDelete<T, S, B>
40{
41    #[inline]
42    fn from(table: T) -> Self {
43        let num_cols = table.number_of_columns();
44        Self {
45            table,
46            values: vec![Value::Null; num_cols],
47            indirect: false,
48        }
49    }
50}
51
52impl<T: DynTable, S: AsRef<str>, B: AsRef<[u8]>> ChangeDelete<T, S, B> {
53    /// Sets the value for a specific column by index.
54    ///
55    /// # Arguments
56    ///
57    /// * `col_idx` - The index of the column to set.
58    /// * `value` - The value to set for the column.
59    ///
60    /// # Errors
61    ///
62    /// * `ColumnIndexOutOfBounds` - If the provided column index is out of bounds for the table schema.
63    ///
64    pub fn set(
65        mut self,
66        col_idx: usize,
67        value: impl Into<Value<S, B>>,
68    ) -> Result<Self, crate::errors::Error> {
69        if col_idx >= self.values.len() {
70            return Err(crate::errors::Error::ColumnIndexOutOfBounds(
71                col_idx,
72                self.values.len(),
73            ));
74        }
75        self.values[col_idx] = value.into();
76        Ok(self)
77    }
78
79    /// Sets a column to NULL.
80    ///
81    /// This is a convenience method equivalent to `.set(col_idx, Value::Null)`.
82    ///
83    /// # Errors
84    ///
85    /// * `ColumnIndexOutOfBounds` - If the provided column index is out of bounds for the table schema.
86    ///
87    /// # Example
88    ///
89    /// ```
90    /// use sqlite_diff_rs::{ChangeDelete, TableSchema};
91    ///
92    /// // CREATE TABLE items (id INTEGER PRIMARY KEY, description TEXT)
93    /// let schema: TableSchema<String> = TableSchema::new("items".into(), 2, vec![1, 0]);
94    ///
95    /// // DELETE FROM items WHERE id = 1 AND description IS NULL
96    /// let delete = ChangeDelete::<_, String, Vec<u8>>::from(schema)
97    ///     .set(0, 1i64).unwrap()
98    ///     .set_null(1).unwrap();
99    /// ```
100    #[inline]
101    pub fn set_null(self, col_idx: usize) -> Result<Self, crate::errors::Error>
102    where
103        S: Default,
104        B: Default,
105    {
106        self.set(col_idx, Value::Null)
107    }
108
109    /// Consumes self and returns the values.
110    #[inline]
111    pub(crate) fn into_values(self) -> Vec<Value<S, B>> {
112        self.values
113    }
114}
115
116impl<T: DynTable, S: AsRef<str>, B: AsRef<[u8]>> Indirect for ChangeDelete<T, S, B> {
117    #[inline]
118    fn indirect(mut self, indirect: bool) -> Self {
119        self.indirect = indirect;
120        self
121    }
122}
123
124/// Represents a delete operation in patchset format.
125///
126/// Only stores the table schema and primary key values, as patchsets
127/// don't include full row data for deletions.
128#[derive(Debug, Clone, PartialEq, Eq)]
129pub struct PatchDelete<T: DynTable, S: AsRef<str>, B: AsRef<[u8]>> {
130    pub(crate) table: T,
131    /// Primary key values for the deleted row.
132    pub(crate) pk: Vec<Value<S, B>>,
133    /// SQLite session-extension indirect flag. See [`Indirect`].
134    pub(crate) indirect: bool,
135}
136
137impl<T: DynTable, S: AsRef<str>, B: AsRef<[u8]>> AsRef<T> for PatchDelete<T, S, B> {
138    #[inline]
139    fn as_ref(&self) -> &T {
140        &self.table
141    }
142}
143
144impl<T: DynTable, S: AsRef<str>, B: AsRef<[u8]>> PatchDelete<T, S, B> {
145    /// Create a new patchset delete for the given table and primary key values.
146    ///
147    /// The `pk` values should be the primary key column values, in the order
148    /// they appear in the table schema. This is the same format returned by
149    /// [`crate::SchemaWithPK::extract_pk`].
150    #[inline]
151    pub fn new(table: T, pk: Vec<Value<S, B>>) -> Self {
152        Self {
153            table,
154            pk,
155            indirect: false,
156        }
157    }
158}
159
160impl<T: DynTable, S: AsRef<str>, B: AsRef<[u8]>> Indirect for PatchDelete<T, S, B> {
161    #[inline]
162    fn indirect(mut self, indirect: bool) -> Self {
163        self.indirect = indirect;
164        self
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::{ChangeDelete, PatchDelete};
171    use crate::DynTable;
172    use crate::encoding::Value;
173    use crate::errors::Error;
174    use crate::schema::SimpleTable;
175    use alloc::string::String;
176    use alloc::vec;
177    use alloc::vec::Vec;
178
179    fn users() -> SimpleTable {
180        SimpleTable::new("users", &["id", "name"], &[0])
181    }
182
183    #[test]
184    fn test_change_delete_set_out_of_bounds() {
185        let err = ChangeDelete::<_, String, Vec<u8>>::from(users())
186            .set(5, 1i64)
187            .unwrap_err();
188        assert!(
189            matches!(err, Error::ColumnIndexOutOfBounds(5, 2)),
190            "got {err:?}"
191        );
192    }
193
194    #[test]
195    fn test_change_delete_set_null_out_of_bounds() {
196        let err = ChangeDelete::<_, String, Vec<u8>>::from(users())
197            .set_null(2)
198            .unwrap_err();
199        assert!(
200            matches!(err, Error::ColumnIndexOutOfBounds(2, 2)),
201            "got {err:?}"
202        );
203    }
204
205    #[test]
206    fn test_patch_delete_as_ref_returns_table() {
207        let table = users();
208        let delete: PatchDelete<_, String, Vec<u8>> =
209            PatchDelete::new(table.clone(), vec![Value::Integer(1)]);
210        let t: &SimpleTable = delete.as_ref();
211        assert_eq!(t.name(), "users");
212    }
213
214    #[test]
215    fn test_change_delete_eq() {
216        let a = ChangeDelete::<_, String, Vec<u8>>::from(users())
217            .set(0, 1i64)
218            .unwrap();
219        let b = ChangeDelete::<_, String, Vec<u8>>::from(users())
220            .set(0, 1i64)
221            .unwrap();
222        assert_eq!(a, b);
223
224        let c = ChangeDelete::<_, String, Vec<u8>>::from(users())
225            .set(0, 2i64)
226            .unwrap();
227        assert_ne!(a, c);
228    }
229}