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    fn primary_key_columns(&self) -> Vec<usize> {
183        let mut pairs: Vec<(usize, usize)> = (0..self.number_of_columns())
184            .filter_map(|col| self.primary_key_index(col).map(|pos| (pos, col)))
185            .collect();
186        pairs.sort_by_key(|&(pos, _)| pos);
187        pairs.into_iter().map(|(_, col)| col).collect()
188    }
189}
190
191impl<T: SchemaWithPK> SchemaWithPK for &T {
192    fn number_of_primary_keys(&self) -> usize {
193        T::number_of_primary_keys(self)
194    }
195
196    fn primary_key_index(&self, col_idx: usize) -> Option<usize> {
197        T::primary_key_index(self, col_idx)
198    }
199
200    fn extract_pk<S: Clone, B: Clone>(
201        &self,
202        values: &impl IndexableValues<Text = S, Binary = B>,
203    ) -> alloc::vec::Vec<Value<S, B>> {
204        T::extract_pk(self, values)
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::{DynTable, IndexableValues, SchemaWithPK};
211    use crate::encoding::Value;
212    use crate::schema::SimpleTable;
213    use alloc::string::String;
214    use alloc::vec;
215    use alloc::vec::Vec;
216
217    fn users() -> SimpleTable {
218        SimpleTable::new("users", &["id", "name", "email"], &[0, 2])
219    }
220
221    #[test]
222    fn test_dyntable_ref_forwards() {
223        let t = users();
224        let r: &SimpleTable = &t;
225        assert_eq!(<&SimpleTable as DynTable>::name(&r), t.name());
226        assert_eq!(
227            <&SimpleTable as DynTable>::number_of_columns(&r),
228            t.number_of_columns()
229        );
230        let mut buf_ref = [0u8; 3];
231        let mut buf_direct = [0u8; 3];
232        <&SimpleTable as DynTable>::write_pk_flags(&r, &mut buf_ref);
233        t.write_pk_flags(&mut buf_direct);
234        assert_eq!(buf_ref, buf_direct);
235    }
236
237    #[test]
238    fn test_schema_with_pk_ref_forwards() {
239        let t = users();
240        let r: &SimpleTable = &t;
241        assert_eq!(
242            <&SimpleTable as SchemaWithPK>::number_of_primary_keys(&r),
243            t.number_of_primary_keys()
244        );
245        for idx in 0..t.number_of_columns() {
246            assert_eq!(
247                <&SimpleTable as SchemaWithPK>::primary_key_index(&r, idx),
248                t.primary_key_index(idx)
249            );
250        }
251        assert_eq!(
252            <&SimpleTable as SchemaWithPK>::primary_key_columns(&r),
253            t.primary_key_columns()
254        );
255        let values: Vec<Value<String, Vec<u8>>> = vec![
256            Value::Integer(1),
257            Value::Text("alice".into()),
258            Value::Text("a@x".into()),
259        ];
260        let pk_ref = <&SimpleTable as SchemaWithPK>::extract_pk(&r, &values);
261        let pk_direct = t.extract_pk(&values);
262        assert_eq!(pk_ref, pk_direct);
263    }
264
265    #[test]
266    fn test_indexable_values_vec_option() {
267        // Vec<Option<Value>>: None entries map to Value::Null.
268        let v: Vec<Option<Value<String, Vec<u8>>>> =
269            vec![Some(Value::Integer(7)), None, Some(Value::Text("x".into()))];
270        assert_eq!(
271            <Vec<Option<Value<String, Vec<u8>>>> as IndexableValues>::get(&v, 0),
272            Some(Value::Integer(7))
273        );
274        assert_eq!(
275            <Vec<Option<Value<String, Vec<u8>>>> as IndexableValues>::get(&v, 1),
276            Some(Value::Null)
277        );
278        assert_eq!(
279            <Vec<Option<Value<String, Vec<u8>>>> as IndexableValues>::get(&v, 2),
280            Some(Value::Text("x".into()))
281        );
282        assert_eq!(
283            <Vec<Option<Value<String, Vec<u8>>>> as IndexableValues>::get(&v, 99),
284            None
285        );
286    }
287
288    #[test]
289    fn test_indexable_values_slice_option() {
290        let owned: Vec<Option<Value<String, Vec<u8>>>> = vec![Some(Value::Integer(1)), None];
291        let slice: &[Option<Value<String, Vec<u8>>>] = &owned;
292        assert_eq!(
293            <&[Option<Value<String, Vec<u8>>>] as IndexableValues>::get(&slice, 0),
294            Some(Value::Integer(1))
295        );
296        assert_eq!(
297            <&[Option<Value<String, Vec<u8>>>] as IndexableValues>::get(&slice, 1),
298            Some(Value::Null)
299        );
300        assert_eq!(
301            <&[Option<Value<String, Vec<u8>>>] as IndexableValues>::get(&slice, 5),
302            None
303        );
304    }
305
306    type PairVec = Vec<(u8, Option<Value<String, Vec<u8>>>)>;
307
308    #[test]
309    fn test_indexable_values_vec_pair() {
310        // Vec<(O, Option<Value>)>: the impl reads only the second element.
311        let v: PairVec = vec![(0, Some(Value::Integer(2))), (1, None)];
312        assert_eq!(
313            <PairVec as IndexableValues>::get(&v, 0),
314            Some(Value::Integer(2))
315        );
316        assert_eq!(<PairVec as IndexableValues>::get(&v, 1), Some(Value::Null));
317        assert_eq!(<PairVec as IndexableValues>::get(&v, 2), None);
318    }
319
320    type PairSlice<'a> = &'a [(u8, Option<Value<String, Vec<u8>>>)];
321
322    #[test]
323    fn test_indexable_values_slice_pair() {
324        let owned: PairVec = vec![(0, Some(Value::Text("y".into()))), (1, None)];
325        let slice: PairSlice<'_> = &owned;
326        assert_eq!(
327            <PairSlice<'_> as IndexableValues>::get(&slice, 0),
328            Some(Value::Text("y".into()))
329        );
330        assert_eq!(
331            <PairSlice<'_> as IndexableValues>::get(&slice, 1),
332            Some(Value::Null)
333        );
334        assert_eq!(<PairSlice<'_> as IndexableValues>::get(&slice, 3), None);
335    }
336}