1use 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#[derive(Serialize, Deserialize, Debug, Clone, Hash)]
13#[serde(rename_all = "PascalCase")]
14pub struct InternalModelFormat {
15 pub models: Vec<Model>,
17}
18
19#[derive(Serialize, Deserialize, Debug, Clone)]
21#[serde(rename_all = "PascalCase")]
22pub struct Model {
23 pub name: String,
25
26 pub fields: Vec<Field>,
28
29 #[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#[derive(Serialize, Deserialize, Debug, Clone)]
57#[serde(rename_all = "PascalCase")]
58pub struct Field {
59 pub name: String,
61
62 #[serde(rename = "Type")]
64 pub db_type: DbType,
65
66 pub annotations: Vec<Annotation>,
68
69 #[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#[derive(Serialize, Deserialize, Debug, Clone, Hash)]
101#[serde(rename_all = "PascalCase")]
102pub struct Source {
103 pub file: String,
105 pub line: usize,
107 pub column: usize,
109}
110
111#[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#[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 AutoCreateTime,
147 AutoUpdateTime,
150 AutoIncrement,
152 Choices(Vec<String>),
154 DefaultValue(DefaultValue),
156 Index(Option<IndexValue>),
158 MaxLength(i32),
160 NotNull,
162 PrimaryKey,
164 Unique,
166 ForeignKey(ForeignKey),
168}
169
170#[derive(Serialize, Deserialize, Debug, Clone, Hash, PartialEq, Eq, Default)]
172#[serde(rename_all = "PascalCase")]
173pub struct ForeignKey {
174 pub table_name: String,
176 pub column_name: String,
178 pub on_delete: ReferentialAction,
180 pub on_update: ReferentialAction,
182}
183
184#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
188#[serde(rename_all = "PascalCase")]
189pub enum ReferentialAction {
190 Restrict,
192 Cascade,
194 SetNull,
196 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#[derive(Serialize, Deserialize, Debug, Clone, Hash, PartialEq, Eq)]
219#[serde(rename_all = "PascalCase")]
220pub struct IndexValue {
221 pub name: String,
224
225 #[serde(default)]
228 #[serde(skip_serializing_if = "Option::is_none")]
229 pub priority: Option<i32>,
230}
231
232#[derive(Serialize, Deserialize, Debug, Clone, Hash, PartialEq, Eq)]
238#[serde(rename_all = "PascalCase")]
239pub struct Index {
240 #[serde(default)]
245 #[serde(skip_serializing_if = "Option::is_none")]
246 pub name: Option<String>,
247
248 pub columns: Vec<String>,
250}
251
252impl Index {
253 pub fn sql_name(&self, table: &str) -> String {
259 match &self.name {
260 Some(name) => format!("{table}_{name}_idx"),
261 None => format!("{table}_{}_idx", self.columns.join("_")),
263 }
264 }
265}
266
267impl Model {
268 pub fn indexes(&self) -> Vec<Index> {
274 struct Column<'a> {
276 name: &'a str,
277 priority: i32,
278 }
279
280 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 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 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 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#[derive(Serialize, Deserialize, Debug, Clone, Hash, PartialEq, Eq)]
465#[serde(untagged)]
466pub enum DefaultValue {
467 String(String),
469 Integer(i64),
471 Float(OrderedFloat<f64>),
473 Boolean(bool),
475}