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