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