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 #[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#[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 AutoCreateTime,
149 AutoUpdateTime,
152 AutoIncrement,
154 Choices(Vec<String>),
156 DefaultValue(DefaultValue),
158 Index(Option<IndexValue>),
160 MaxLength(i32),
167 NotNull,
169 PrimaryKey,
171 Unique,
173 ForeignKey(ForeignKey),
175}
176
177#[derive(Serialize, Deserialize, Debug, Clone, Hash, PartialEq, Eq, Default)]
179#[serde(rename_all = "PascalCase")]
180pub struct ForeignKey {
181 pub table_name: String,
183 pub column_name: String,
185 pub on_delete: ReferentialAction,
187 pub on_update: ReferentialAction,
189}
190
191#[derive(Default, Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
195#[serde(rename_all = "PascalCase")]
196pub enum ReferentialAction {
197 #[default]
199 Restrict,
200 Cascade,
202 SetNull,
204 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#[derive(Serialize, Deserialize, Debug, Clone, Hash, PartialEq, Eq)]
221#[serde(rename_all = "PascalCase")]
222pub struct IndexValue {
223 pub name: String,
226
227 #[serde(default)]
230 #[serde(skip_serializing_if = "Option::is_none")]
231 pub priority: Option<i32>,
232}
233
234#[derive(Serialize, Deserialize, Debug, Clone, Hash, PartialEq, Eq)]
240#[serde(rename_all = "PascalCase")]
241pub struct Index {
242 #[serde(default)]
247 #[serde(skip_serializing_if = "Option::is_none")]
248 pub name: Option<String>,
249
250 pub columns: Vec<String>,
252}
253
254impl Index {
255 pub fn sql_name(&self, table: &str) -> String {
261 match &self.name {
262 Some(name) => format!("{table}_{name}_idx"),
263 None => format!("{table}_{}_idx", self.columns.join("_")),
265 }
266 }
267}
268
269impl Model {
270 pub fn indexes(&self) -> Vec<Index> {
276 struct Column<'a> {
278 name: &'a str,
279 priority: i32,
280 }
281
282 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 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 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 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#[derive(Serialize, Deserialize, Debug, Clone, Hash, PartialEq, Eq)]
467#[serde(untagged)]
468pub enum DefaultValue {
469 String(String),
471 Integer(i64),
473 Float(OrderedFloat<f64>),
475 Boolean(bool),
477}