rust_query/
hash.rs

1//! This can be used to define the layout of a table
2//! The layout is hashable and the hashes are independent
3//! of the column ordering and some other stuff.
4
5use std::{
6    collections::{BTreeMap, BTreeSet},
7    marker::PhantomData,
8    mem,
9};
10
11use sea_query::{Alias, IndexCreateStatement, SqliteQueryBuilder, TableCreateStatement};
12
13use crate::value::{EqTyp, MyTyp};
14
15#[derive(Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
16pub enum ColumnType {
17    Integer = 0,
18    Float = 1,
19    String = 2,
20    Blob = 3,
21}
22
23impl ColumnType {
24    pub fn sea_type(&self) -> sea_query::ColumnType {
25        use sea_query::ColumnType as T;
26        match self {
27            ColumnType::Integer => T::Integer,
28            ColumnType::Float => T::custom("REAL"),
29            ColumnType::String => T::Text,
30            ColumnType::Blob => T::Blob,
31        }
32    }
33}
34
35#[derive(Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
36pub struct Column {
37    pub typ: ColumnType,
38    pub nullable: bool,
39    pub fk: Option<(String, String)>,
40}
41
42#[derive(Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
43pub struct Index {
44    // column order matters for performance
45    pub columns: Vec<String>,
46    pub unique: bool,
47}
48
49impl Index {
50    fn normalize(&mut self) -> bool {
51        // column order doesn't matter for correctness
52        self.columns.sort();
53        // non-unique indexes don't matter for correctness
54        self.unique
55    }
56}
57
58#[derive(Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Default)]
59pub struct Table {
60    pub columns: BTreeMap<String, Column>,
61    pub indices: BTreeSet<Index>,
62}
63
64impl Table {
65    pub(crate) fn new<T: crate::Table>() -> Self {
66        let mut f = crate::hash::TypBuilder::default();
67        T::typs(&mut f);
68        f.ast
69    }
70
71    fn normalize(&mut self) {
72        self.indices = mem::take(&mut self.indices)
73            .into_iter()
74            .filter_map(|mut idx| idx.normalize().then_some(idx))
75            .collect();
76    }
77}
78
79impl Table {
80    pub fn create(&self) -> TableCreateStatement {
81        use sea_query::*;
82        let mut create = Table::create();
83        for (name, col) in &self.columns {
84            let name = Alias::new(name);
85            let mut def = ColumnDef::new_with_type(name.clone(), col.typ.sea_type());
86            if col.nullable {
87                def.null();
88            } else {
89                def.not_null();
90            }
91            create.col(&mut def);
92            if let Some((table, fk)) = &col.fk {
93                create.foreign_key(
94                    ForeignKey::create()
95                        .to(Alias::new(table), Alias::new(fk))
96                        .from_col(name),
97                );
98            }
99        }
100        create
101    }
102
103    pub fn create_indices(&self, table_name: &str) -> impl Iterator<Item = String> {
104        let index_table_ref = Alias::new(table_name);
105        self.indices
106            .iter()
107            .enumerate()
108            .map(move |(index_num, index)| {
109                index
110                    .create()
111                    .table(index_table_ref.clone())
112                    .name(format!("{table_name}_index_{index_num}"))
113                    .to_string(SqliteQueryBuilder)
114            })
115    }
116}
117
118impl Index {
119    pub fn create(&self) -> IndexCreateStatement {
120        let mut index = sea_query::Index::create();
121        if self.unique {
122            index.unique();
123        }
124        // Preserve the original order of columns in the unique constraint.
125        // This lets users optimize queries by using index prefixes.
126        for col in &self.columns {
127            index.col(Alias::new(col));
128        }
129        index
130    }
131}
132
133#[derive(Debug, Hash, Default, PartialEq, Eq)]
134pub struct Schema {
135    pub tables: BTreeMap<String, Table>,
136}
137
138impl Schema {
139    pub(crate) fn new<S: crate::migrate::Schema>() -> Self {
140        let mut b = crate::migrate::TableTypBuilder::default();
141        S::typs(&mut b);
142        b.ast
143    }
144
145    pub(crate) fn normalize(mut self) -> Self {
146        self.tables.values_mut().for_each(Table::normalize);
147        self
148    }
149}
150
151#[cfg(feature = "dev")]
152pub mod dev {
153    use std::{
154        hash::{Hash, Hasher},
155        io::{Read, Write},
156    };
157
158    use k12::{
159        KangarooTwelve, KangarooTwelveCore,
160        digest::{ExtendableOutput, core_api::CoreWrapper},
161    };
162
163    pub struct KangarooHasher {
164        inner: CoreWrapper<KangarooTwelveCore<'static>>,
165    }
166
167    impl Default for KangarooHasher {
168        fn default() -> Self {
169            let core = KangarooTwelveCore::new(&[]);
170            let hasher = KangarooTwelve::from_core(core);
171            Self { inner: hasher }
172        }
173    }
174
175    impl Hasher for KangarooHasher {
176        fn finish(&self) -> u64 {
177            let mut xof = self.inner.clone().finalize_xof();
178            let mut buf = [0; 8];
179            xof.read_exact(&mut buf).unwrap();
180            u64::from_le_bytes(buf)
181        }
182
183        fn write(&mut self, bytes: &[u8]) {
184            self.inner.write_all(bytes).unwrap();
185        }
186    }
187
188    /// Calculate the hash of a shema.
189    ///
190    /// This is useful in a test to make sure that old schema versions are not accidentally modified.
191    pub fn hash_schema<S: crate::migrate::Schema>() -> String {
192        let mut hasher = KangarooHasher::default();
193        super::Schema::new::<S>().normalize().hash(&mut hasher);
194        format!("{:x}", hasher.finish())
195    }
196}
197
198pub struct TypBuilder<S> {
199    pub(crate) ast: Table,
200    _p: PhantomData<S>,
201}
202
203impl<S> Default for TypBuilder<S> {
204    fn default() -> Self {
205        Self {
206            ast: Default::default(),
207            _p: Default::default(),
208        }
209    }
210}
211
212impl<S> TypBuilder<S> {
213    pub fn col<T: SchemaType<S>>(&mut self, name: &'static str) {
214        let mut item = Column {
215            typ: T::TYP,
216            nullable: T::NULLABLE,
217            fk: None,
218        };
219        if let Some((table, fk)) = T::FK {
220            item.fk = Some((table.to_owned(), fk.to_owned()))
221        }
222        let old = self.ast.columns.insert(name.to_owned(), item);
223        debug_assert!(old.is_none());
224    }
225
226    pub fn index(&mut self, cols: &[&'static str], unique: bool) {
227        let mut index = Index {
228            columns: Vec::default(),
229            unique,
230        };
231        for &col in cols {
232            index.columns.push(col.to_owned());
233        }
234        self.ast.indices.insert(index);
235    }
236
237    pub fn check_unique_compatible<T: EqTyp>(&mut self) {}
238}
239
240struct Null;
241struct NotNull;
242
243// TODO: maybe remove this trait?
244// currently this prevents storing booleans and nested `Option`.
245#[diagnostic::on_unimplemented(
246    message = "Can not use `{Self}` as a column type in schema `{S}`",
247    note = "Table names can be used as schema column types as long as they are not #[no_reference]"
248)]
249trait SchemaType<S>: MyTyp {
250    type N;
251}
252
253impl<S> SchemaType<S> for String {
254    type N = NotNull;
255}
256impl<S> SchemaType<S> for Vec<u8> {
257    type N = NotNull;
258}
259impl<S> SchemaType<S> for i64 {
260    type N = NotNull;
261}
262impl<S> SchemaType<S> for f64 {
263    type N = NotNull;
264}
265impl<S, T: SchemaType<S, N = NotNull>> SchemaType<S> for Option<T> {
266    type N = Null;
267}
268// only tables with `Referer = ()` are valid columns
269#[diagnostic::do_not_recommend]
270impl<T: crate::Table<Referer = ()>> SchemaType<T::Schema> for T {
271    type N = NotNull;
272}