Skip to main content

sqlite_diff_rs/schema/
dyn_table.rs

1//! Dynamic (runtime) table schema traits.
2use core::{fmt::Debug, hash::Hash};
3
4use alloc::vec::Vec;
5
6use crate::encoding::Value;
7
8/// A table schema known at runtime (object-safe).
9///
10/// While extremely generic, this trait does not provide much type safety.
11pub trait DynTable: Debug + Eq + Clone + PartialEq {
12    /// The table name.
13    fn name(&self) -> &str;
14
15    /// The number of columns in the table.
16    fn number_of_columns(&self) -> usize;
17
18    /// Write primary key flags to the buffer.
19    ///
20    /// The buffer must have length equal to `number_of_columns()`.
21    /// Each byte represents the 1-based ordinal position of the column
22    /// in the composite primary key, or 0 if the column is not part of
23    /// the primary key.
24    ///
25    /// For example, for a table with columns (A, B, C) where (B, A) is the PK
26    /// (B is the first PK column, A is the second), the buffer should be:
27    /// `[2, 1, 0]` - A is 2nd in PK order, B is 1st in PK order, C is not PK.
28    ///
29    /// # Panics
30    ///
31    /// Panics if `buf.len() != self.number_of_columns()`.
32    fn write_pk_flags(&self, buf: &mut [u8]);
33}
34
35impl<T: DynTable> DynTable for &T {
36    #[inline]
37    fn name(&self) -> &str {
38        T::name(self)
39    }
40
41    #[inline]
42    fn number_of_columns(&self) -> usize {
43        T::number_of_columns(self)
44    }
45
46    #[inline]
47    fn write_pk_flags(&self, buf: &mut [u8]) {
48        T::write_pk_flags(self, buf);
49    }
50}
51
52/// Collection of indexable values.
53pub trait IndexableValues {
54    /// The string variant.
55    type Text: Clone;
56    /// The binary variant.
57    type Binary: Clone;
58
59    /// Get the value at the specified column index.
60    ///
61    /// # Arguments
62    ///
63    /// * `col_idx` - The index of the column to retrieve.
64    ///
65    /// # Returns
66    ///
67    /// The value at the specified column index, or `None` if the index is out of bounds.
68    fn get(&self, col_idx: usize) -> Option<Value<Self::Text, Self::Binary>>;
69}
70
71impl<S: Clone, B: Clone> IndexableValues for Vec<Value<S, B>> {
72    type Text = S;
73    type Binary = B;
74
75    #[inline]
76    fn get(&self, col_idx: usize) -> Option<Value<Self::Text, Self::Binary>> {
77        <&[Value<S, B>]>::get(&self.as_slice(), col_idx)
78    }
79}
80
81impl<S: Clone, B: Clone> IndexableValues for &[Value<S, B>] {
82    type Text = S;
83    type Binary = B;
84
85    #[inline]
86    fn get(&self, col_idx: usize) -> Option<Value<Self::Text, Self::Binary>> {
87        <[Value<S, B>]>::get(self, col_idx).cloned()
88    }
89}
90
91impl<S: Clone, B: Clone> IndexableValues for Vec<Option<Value<S, B>>> {
92    type Text = S;
93    type Binary = B;
94
95    #[inline]
96    fn get(&self, col_idx: usize) -> Option<Value<Self::Text, Self::Binary>> {
97        <&[Option<Value<S, B>>]>::get(&self.as_slice(), col_idx)
98    }
99}
100
101impl<S: Clone, B: Clone> IndexableValues for &[Option<Value<S, B>>] {
102    type Text = S;
103    type Binary = B;
104
105    #[inline]
106    fn get(&self, col_idx: usize) -> Option<Value<Self::Text, Self::Binary>> {
107        <[Option<Value<S, B>>]>::get(self, col_idx).map(|v| {
108            if let Some(value) = v {
109                value.clone()
110            } else {
111                Value::Null
112            }
113        })
114    }
115}
116
117impl<O, S: Clone, B: Clone> IndexableValues for Vec<(O, Option<Value<S, B>>)> {
118    type Text = S;
119    type Binary = B;
120
121    #[inline]
122    fn get(&self, col_idx: usize) -> Option<Value<Self::Text, Self::Binary>> {
123        <&[(O, Option<Value<S, B>>)]>::get(&self.as_slice(), col_idx)
124    }
125}
126
127impl<O, S: Clone, B: Clone> IndexableValues for &[(O, Option<Value<S, B>>)] {
128    type Text = S;
129    type Binary = B;
130
131    #[inline]
132    fn get(&self, col_idx: usize) -> Option<Value<Self::Text, Self::Binary>> {
133        <[(O, Option<Value<S, B>>)]>::get(self, col_idx).map(|(_old, new)| {
134            if let Some(value) = new {
135                value.clone()
136            } else {
137                Value::Null
138            }
139        })
140    }
141}
142
143/// Extension trait for schemas with typed primary key extraction.
144///
145/// This trait is NOT object-safe due to the associated type.
146/// Use [`DynTable`] with the `extract_pk` method for dynamic dispatch.
147///
148/// # Type Parameter
149///
150/// The `PrimaryKeyValue` type varies by schema:
151/// - For `TableSchema` implementors: derived from `<PrimaryKey as NestedColumns>::NestedValues`,
152///   e.g., `(i64,)` or `(i64, String)`
153/// - For `Box<dyn DynTable>`: `Vec<Value>` (runtime, unknown structure)
154pub trait SchemaWithPK: DynTable + Clone + Hash {
155    /// Returns the number of primary key columns in the schema.
156    fn number_of_primary_keys(&self) -> usize;
157
158    /// Returns the primary key index of the primary key by the column index.
159    fn primary_key_index(&self, col_idx: usize) -> Option<usize>;
160
161    /// Extract primary key values from a full row.
162    ///
163    /// The values slice must have length equal to `number_of_columns()`.
164    /// Returns the PK values in column order, typed appropriately.
165    ///
166    /// # Panics
167    ///
168    /// Panics if the values collection is shorter than the schema's column count.
169    fn extract_pk<S: Clone, B: Clone>(
170        &self,
171        values: &impl IndexableValues<Text = S, Binary = B>,
172    ) -> alloc::vec::Vec<Value<S, B>>;
173
174    /// Returns the column indices of the primary key, ordered by their
175    /// position within the composite key (key order).
176    ///
177    /// This is the forward companion to [`primary_key_index`](Self::primary_key_index):
178    /// that maps a column index to its key position, while this lists the key
179    /// column indices in key order. The ordering matches
180    /// [`extract_pk`](Self::extract_pk), so the two agree on which cell is which
181    /// key component.
182    ///
183    /// The walk selects the column holding key position `0`, then `1`, up to the
184    /// key width, so it allocates nothing and costs `O(k*n)` for a key of width
185    /// `k` over `n` columns.
186    fn primary_key_columns(&self) -> impl Iterator<Item = usize> {
187        (0..self.number_of_primary_keys()).filter_map(move |position| {
188            (0..self.number_of_columns()).find(|&col| self.primary_key_index(col) == Some(position))
189        })
190    }
191}
192
193impl<T: SchemaWithPK> SchemaWithPK for &T {
194    fn number_of_primary_keys(&self) -> usize {
195        T::number_of_primary_keys(self)
196    }
197
198    fn primary_key_index(&self, col_idx: usize) -> Option<usize> {
199        T::primary_key_index(self, col_idx)
200    }
201
202    fn extract_pk<S: Clone, B: Clone>(
203        &self,
204        values: &impl IndexableValues<Text = S, Binary = B>,
205    ) -> alloc::vec::Vec<Value<S, B>> {
206        T::extract_pk(self, values)
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::{DynTable, IndexableValues, SchemaWithPK};
213    use crate::encoding::Value;
214    use crate::schema::SimpleTable;
215    use alloc::string::String;
216    use alloc::vec;
217    use alloc::vec::Vec;
218
219    fn users() -> SimpleTable {
220        SimpleTable::new("users", &["id", "name", "email"], &[0, 2])
221    }
222
223    #[test]
224    fn test_dyntable_ref_forwards() {
225        let t = users();
226        let r: &SimpleTable = &t;
227        assert_eq!(<&SimpleTable as DynTable>::name(&r), t.name());
228        assert_eq!(
229            <&SimpleTable as DynTable>::number_of_columns(&r),
230            t.number_of_columns()
231        );
232        let mut buf_ref = [0u8; 3];
233        let mut buf_direct = [0u8; 3];
234        <&SimpleTable as DynTable>::write_pk_flags(&r, &mut buf_ref);
235        t.write_pk_flags(&mut buf_direct);
236        assert_eq!(buf_ref, buf_direct);
237    }
238
239    #[test]
240    fn test_schema_with_pk_ref_forwards() {
241        let t = users();
242        let r: &SimpleTable = &t;
243        assert_eq!(
244            <&SimpleTable as SchemaWithPK>::number_of_primary_keys(&r),
245            t.number_of_primary_keys()
246        );
247        for idx in 0..t.number_of_columns() {
248            assert_eq!(
249                <&SimpleTable as SchemaWithPK>::primary_key_index(&r, idx),
250                t.primary_key_index(idx)
251            );
252        }
253        assert_eq!(
254            <&SimpleTable as SchemaWithPK>::primary_key_columns(&r).collect::<Vec<usize>>(),
255            t.primary_key_columns().collect::<Vec<usize>>()
256        );
257        let values: Vec<Value<String, Vec<u8>>> = vec![
258            Value::Integer(1),
259            Value::Text("alice".into()),
260            Value::Text("a@x".into()),
261        ];
262        let pk_ref = <&SimpleTable as SchemaWithPK>::extract_pk(&r, &values);
263        let pk_direct = t.extract_pk(&values);
264        assert_eq!(pk_ref, pk_direct);
265    }
266
267    #[test]
268    fn test_indexable_values_vec_option() {
269        // Vec<Option<Value>>: None entries map to Value::Null.
270        let v: Vec<Option<Value<String, Vec<u8>>>> =
271            vec![Some(Value::Integer(7)), None, Some(Value::Text("x".into()))];
272        assert_eq!(
273            <Vec<Option<Value<String, Vec<u8>>>> as IndexableValues>::get(&v, 0),
274            Some(Value::Integer(7))
275        );
276        assert_eq!(
277            <Vec<Option<Value<String, Vec<u8>>>> as IndexableValues>::get(&v, 1),
278            Some(Value::Null)
279        );
280        assert_eq!(
281            <Vec<Option<Value<String, Vec<u8>>>> as IndexableValues>::get(&v, 2),
282            Some(Value::Text("x".into()))
283        );
284        assert_eq!(
285            <Vec<Option<Value<String, Vec<u8>>>> as IndexableValues>::get(&v, 99),
286            None
287        );
288    }
289
290    #[test]
291    fn test_indexable_values_slice_option() {
292        let owned: Vec<Option<Value<String, Vec<u8>>>> = vec![Some(Value::Integer(1)), None];
293        let slice: &[Option<Value<String, Vec<u8>>>] = &owned;
294        assert_eq!(
295            <&[Option<Value<String, Vec<u8>>>] as IndexableValues>::get(&slice, 0),
296            Some(Value::Integer(1))
297        );
298        assert_eq!(
299            <&[Option<Value<String, Vec<u8>>>] as IndexableValues>::get(&slice, 1),
300            Some(Value::Null)
301        );
302        assert_eq!(
303            <&[Option<Value<String, Vec<u8>>>] as IndexableValues>::get(&slice, 5),
304            None
305        );
306    }
307
308    type PairVec = Vec<(u8, Option<Value<String, Vec<u8>>>)>;
309
310    #[test]
311    fn test_indexable_values_vec_pair() {
312        // Vec<(O, Option<Value>)>: the impl reads only the second element.
313        let v: PairVec = vec![(0, Some(Value::Integer(2))), (1, None)];
314        assert_eq!(
315            <PairVec as IndexableValues>::get(&v, 0),
316            Some(Value::Integer(2))
317        );
318        assert_eq!(<PairVec as IndexableValues>::get(&v, 1), Some(Value::Null));
319        assert_eq!(<PairVec as IndexableValues>::get(&v, 2), None);
320    }
321
322    type PairSlice<'a> = &'a [(u8, Option<Value<String, Vec<u8>>>)];
323
324    #[test]
325    fn test_indexable_values_slice_pair() {
326        let owned: PairVec = vec![(0, Some(Value::Text("y".into()))), (1, None)];
327        let slice: PairSlice<'_> = &owned;
328        assert_eq!(
329            <PairSlice<'_> as IndexableValues>::get(&slice, 0),
330            Some(Value::Text("y".into()))
331        );
332        assert_eq!(
333            <PairSlice<'_> as IndexableValues>::get(&slice, 1),
334            Some(Value::Null)
335        );
336        assert_eq!(<PairSlice<'_> as IndexableValues>::get(&slice, 3), None);
337    }
338}