Skip to main content

rorm_declaration/
imr.rs

1//! The Internal Model Representation used by our migration cli tool
2use std::fmt::Display;
3use std::fmt::Formatter;
4use std::hash::Hash;
5use std::hash::Hasher;
6
7use ordered_float::OrderedFloat;
8use serde::Deserialize;
9use serde::Serialize;
10
11/// A collection of all models used in the resulting application
12#[derive(Serialize, Deserialize, Debug, Clone, Hash)]
13#[serde(rename_all = "PascalCase")]
14pub struct InternalModelFormat {
15    /// List of all models
16    pub models: Vec<Model>,
17}
18
19/// A single model i.e. database table
20#[derive(Serialize, Deserialize, Debug, Clone)]
21#[serde(rename_all = "PascalCase")]
22pub struct Model {
23    /// Name of the table
24    pub name: String,
25
26    /// List of columns of the table
27    pub fields: Vec<Field>,
28
29    /// Optional source reference to enhance error messages
30    #[serde(default)]
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub source_defined_at: Option<Source>,
33}
34
35impl PartialEq for Model {
36    fn eq(&self, other: &Self) -> bool {
37        self.name == other.name && self.fields == other.fields
38    }
39}
40
41impl Hash for Model {
42    fn hash<H: Hasher>(&self, state: &mut H) {
43        self.fields.hash(state);
44        self.name.hash(state);
45    }
46
47    fn hash_slice<H: Hasher>(data: &[Self], state: &mut H)
48    where
49        Self: Sized,
50    {
51        data.iter().for_each(|x| x.hash(state));
52    }
53}
54
55/// Model's fields i.e. the table's columns
56#[derive(Serialize, Deserialize, Debug, Clone)]
57#[serde(rename_all = "PascalCase")]
58pub struct Field {
59    /// Name of the column
60    pub name: String,
61
62    /// Type of the column
63    #[serde(rename = "Type")]
64    pub db_type: DbType,
65
66    /// List of annotations, constraints, etc.
67    pub annotations: Vec<Annotation>,
68
69    /// Optional source reference to enhance error messages
70    #[serde(default)]
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub source_defined_at: Option<Source>,
73}
74
75impl PartialEq for Field {
76    fn eq(&self, other: &Self) -> bool {
77        self.name == other.name
78            && self.db_type == other.db_type
79            && self.annotations == other.annotations
80    }
81}
82
83impl Hash for Field {
84    fn hash<H: Hasher>(&self, state: &mut H) {
85        self.name.hash(state);
86        self.annotations.hash(state);
87        self.db_type.hash(state);
88    }
89
90    fn hash_slice<H: Hasher>(data: &[Self], state: &mut H)
91    where
92        Self: Sized,
93    {
94        data.iter().for_each(|x| x.hash(state));
95    }
96}
97
98/// Location in the source code a [Model] or [Field] originates from
99/// Used for better error messages in the migration tool
100#[derive(Serialize, Deserialize, Debug, Clone, Hash)]
101#[serde(rename_all = "PascalCase")]
102pub struct Source {
103    /// Filename of the source code of the [Model] or [Field]
104    pub file: String,
105    /// Line of the [Model] or [Field]
106    pub line: usize,
107    /// Column of the [Model] or [Field]
108    pub column: usize,
109}
110
111/// All column types supported by the migration tool
112#[allow(missing_docs)]
113#[derive(Serialize, Deserialize, Debug, Copy, Clone, Hash, PartialEq, Eq)]
114#[serde(rename_all = "lowercase")]
115pub enum DbType {
116    VarChar,
117    Binary,
118    Int8,
119    Int16,
120    Int32,
121    Int64,
122    #[serde(rename = "float_number")]
123    Float,
124    #[serde(rename = "double_number")]
125    Double,
126    Boolean,
127    Date,
128    DateTime,
129    Timestamp,
130    Time,
131    Choices,
132    Uuid,
133    MacAddress,
134    IpNetwork,
135    BitVec,
136}
137
138/// The subset of annotations which need to be communicated with the migration tool
139#[non_exhaustive]
140#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
141#[serde(tag = "Type", content = "Value")]
142#[serde(rename_all = "snake_case")]
143pub enum Annotation {
144    /// Only for [DbType::Timestamp], [DbType::DateTime], [DbType::Time] and [DbType::Date].
145    /// Will set the current time of the database when a row is created.
146    AutoCreateTime,
147    /// Only for [DbType::Timestamp], [DbType::DateTime], [DbType::Time] and [DbType::Date].
148    /// Will set the current time of the database when a row is updated.
149    AutoUpdateTime,
150    /// AUTO_INCREMENT constraint
151    AutoIncrement,
152    /// A list of choices to set
153    Choices(Vec<String>),
154    /// DEFAULT constraint
155    DefaultValue(DefaultValue),
156    /// Create an index. The optional [IndexValue] can be used, to build more complex indexes.
157    Index(Option<IndexValue>),
158    /// Only for VARCHAR, VARBINARY. Specifies the maximum length of the column's content.
159    MaxLength(i32),
160    /// NOT NULL constraint
161    NotNull,
162    /// The annotated column will be used as primary key
163    PrimaryKey,
164    /// UNIQUE constraint
165    Unique,
166    /// Foreign Key constraint
167    ForeignKey(ForeignKey),
168}
169
170/// Represents a foreign key
171#[derive(Serialize, Deserialize, Debug, Clone, Hash, PartialEq, Eq, Default)]
172#[serde(rename_all = "PascalCase")]
173pub struct ForeignKey {
174    /// Name of the table that should be referenced
175    pub table_name: String,
176    /// Name of the column that should be referenced
177    pub column_name: String,
178    /// Action to be used in case of on delete
179    pub on_delete: ReferentialAction,
180    /// Action to be used in case of an update
181    pub on_update: ReferentialAction,
182}
183
184/**
185Action that gets trigger on update and on delete.
186*/
187#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
188#[serde(rename_all = "PascalCase")]
189pub enum ReferentialAction {
190    /// Stop operation if any keys still depend on the parent table
191    Restrict,
192    /// The action is cascaded
193    Cascade,
194    /// The field is set to null
195    SetNull,
196    /// The field is set to its default
197    SetDefault,
198}
199
200impl Default for ReferentialAction {
201    fn default() -> Self {
202        Self::Restrict
203    }
204}
205
206impl Display for ReferentialAction {
207    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
208        match self {
209            ReferentialAction::Restrict => write!(f, "RESTRICT"),
210            ReferentialAction::Cascade => write!(f, "CASCADE"),
211            ReferentialAction::SetNull => write!(f, "SET NULL"),
212            ReferentialAction::SetDefault => write!(f, "SET DEFAULT"),
213        }
214    }
215}
216
217/// Represents a complex index
218#[derive(Serialize, Deserialize, Debug, Clone, Hash, PartialEq, Eq)]
219#[serde(rename_all = "PascalCase")]
220pub struct IndexValue {
221    /// Name of the index. Can be used multiple times in a [Model] to create an
222    /// index with multiple columns.
223    pub name: String,
224
225    /// The order to put the columns in while generating an index.
226    /// Only useful if multiple columns with the same name are present.
227    #[serde(default)]
228    #[serde(skip_serializing_if = "Option::is_none")]
229    pub priority: Option<i32>,
230}
231
232/// An index of a [Model] i.e. the table's index over one or more of its columns
233///
234/// Indexes are not declared on a [Model] directly.
235/// They are spread over its [Field]s using [Annotation::Index]
236/// and gathered by [Model::indexes].
237#[derive(Serialize, Deserialize, Debug, Clone, Hash, PartialEq, Eq)]
238#[serde(rename_all = "PascalCase")]
239pub struct Index {
240    /// The name the index was declared under i.e. [IndexValue::name]
241    ///
242    /// It is `None` for an index which was declared without an [IndexValue].
243    /// Such an index always spans exactly one column.
244    #[serde(default)]
245    #[serde(skip_serializing_if = "Option::is_none")]
246    pub name: Option<String>,
247
248    /// The columns the index spans, in the order they should be indexed in
249    pub columns: Vec<String>,
250}
251
252impl Index {
253    /// The identifier to create this index under in the database
254    ///
255    /// Unlike columns, indexes don't live in their table's namespace.
256    /// Their names have to be unique across the whole database,
257    /// which is why they are prefixed with their `table`'s name.
258    pub fn sql_name(&self, table: &str) -> String {
259        match &self.name {
260            Some(name) => format!("{table}_{name}_idx"),
261            // An index without a name always spans exactly one column
262            None => format!("{table}_{}_idx", self.columns.join("_")),
263        }
264    }
265}
266
267impl Model {
268    /// Gathers the [Index]es declared by this model's [Field]s
269    ///
270    /// Fields sharing an [IndexValue::name] are combined into a single index
271    /// spanning all of them. Their order inside the index is their order of
272    /// declaration, which can be overwritten using [IndexValue::priority].
273    pub fn indexes(&self) -> Vec<Index> {
274        /// A column of an index paired with the priority to sort it by
275        struct Column<'a> {
276            name: &'a str,
277            priority: i32,
278        }
279
280        // Both vectors are kept in sync and ordered by the indexes' first occurrence,
281        // to produce the same output for the same model every time.
282        let mut names: Vec<Option<&str>> = Vec::new();
283        let mut columns: Vec<Vec<Column>> = Vec::new();
284
285        for field in &self.fields {
286            for annotation in &field.annotations {
287                let Annotation::Index(value) = annotation else {
288                    continue;
289                };
290
291                let name = value.as_ref().map(|value| value.name.as_str());
292                let priority = value.as_ref().and_then(|value| value.priority).unwrap_or(0);
293
294                // Fields sharing a name contribute to the same index
295                let index = match name.and_then(|name| names.iter().position(|x| *x == Some(name)))
296                {
297                    Some(index) => index,
298                    None => {
299                        names.push(name);
300                        columns.push(Vec::new());
301                        names.len() - 1
302                    }
303                };
304
305                columns[index].push(Column {
306                    name: &field.name,
307                    priority,
308                });
309            }
310        }
311
312        names
313            .into_iter()
314            .zip(columns)
315            .map(|(name, mut columns)| {
316                // `sort_by_key` is stable, so columns of equal priority
317                // keep their order of declaration
318                columns.sort_by_key(|column| column.priority);
319                Index {
320                    name: name.map(str::to_string),
321                    columns: columns
322                        .into_iter()
323                        .map(|column| column.name.to_string())
324                        .collect(),
325                }
326            })
327            .collect()
328    }
329}
330
331#[cfg(test)]
332mod test_indexes {
333    use crate::imr::{Annotation, DbType, Field, Index, IndexValue, Model};
334
335    /// Builds a model whose fields are named after and annotated with `indexes`
336    fn model(indexes: Vec<(&str, Option<IndexValue>)>) -> Model {
337        Model {
338            name: "user".to_string(),
339            fields: indexes
340                .into_iter()
341                .map(|(name, index)| Field {
342                    name: name.to_string(),
343                    db_type: DbType::VarChar,
344                    annotations: vec![Annotation::Index(index)],
345                    source_defined_at: None,
346                })
347                .collect(),
348            source_defined_at: None,
349        }
350    }
351
352    fn named(name: &str, priority: Option<i32>) -> Option<IndexValue> {
353        Some(IndexValue {
354            name: name.to_string(),
355            priority,
356        })
357    }
358
359    #[test]
360    fn every_unnamed_index_spans_a_single_column() {
361        assert_eq!(
362            model(vec![("a", None), ("b", None)]).indexes(),
363            vec![
364                Index {
365                    name: None,
366                    columns: vec!["a".to_string()]
367                },
368                Index {
369                    name: None,
370                    columns: vec!["b".to_string()]
371                },
372            ]
373        );
374    }
375
376    #[test]
377    fn fields_sharing_a_name_are_combined() {
378        assert_eq!(
379            model(vec![
380                ("a", named("ab", None)),
381                ("c", None),
382                ("b", named("ab", None)),
383            ])
384            .indexes(),
385            vec![
386                Index {
387                    name: Some("ab".to_string()),
388                    columns: vec!["a".to_string(), "b".to_string()]
389                },
390                Index {
391                    name: None,
392                    columns: vec!["c".to_string()]
393                },
394            ]
395        );
396    }
397
398    #[test]
399    fn priority_overwrites_the_order_of_declaration() {
400        assert_eq!(
401            model(vec![
402                ("a", named("ab", Some(2))),
403                ("b", named("ab", Some(1))),
404            ])
405            .indexes(),
406            vec![Index {
407                name: Some("ab".to_string()),
408                columns: vec!["b".to_string(), "a".to_string()]
409            }]
410        );
411    }
412
413    #[test]
414    fn columns_of_equal_priority_keep_their_order() {
415        assert_eq!(
416            model(vec![
417                ("a", named("abc", Some(1))),
418                ("b", named("abc", Some(1))),
419                ("c", named("abc", Some(0))),
420            ])
421            .indexes(),
422            vec![Index {
423                name: Some("abc".to_string()),
424                columns: vec!["c".to_string(), "a".to_string(), "b".to_string()]
425            }]
426        );
427    }
428
429    #[test]
430    fn a_model_without_index_annotations_has_no_indexes() {
431        assert_eq!(
432            Model {
433                name: "user".to_string(),
434                fields: vec![Field {
435                    name: "id".to_string(),
436                    db_type: DbType::Int64,
437                    annotations: vec![Annotation::PrimaryKey],
438                    source_defined_at: None,
439                }],
440                source_defined_at: None,
441            }
442            .indexes(),
443            vec![]
444        );
445    }
446
447    #[test]
448    fn sql_names_are_prefixed_with_their_table() {
449        let unnamed = Index {
450            name: None,
451            columns: vec!["login".to_string()],
452        };
453        assert_eq!(unnamed.sql_name("user"), "user_login_idx");
454
455        let named = Index {
456            name: Some("full_name".to_string()),
457            columns: vec!["last_name".to_string(), "first_name".to_string()],
458        };
459        assert_eq!(named.sql_name("user"), "user_full_name_idx");
460    }
461}
462
463/// A column's default value which is any non object / array json value
464#[derive(Serialize, Deserialize, Debug, Clone, Hash, PartialEq, Eq)]
465#[serde(untagged)]
466pub enum DefaultValue {
467    /// Use hexadecimal to represent binary data
468    String(String),
469    /// i64 is used as it can represent any integer defined in DbType
470    Integer(i64),
471    /// Ordered float is used as f64 does not Eq and Order which are needed for Hash
472    Float(OrderedFloat<f64>),
473    /// Just a bool. Nothing interesting here.
474    Boolean(bool),
475}