1use crate::error::MigrationError;
12use serde::{Deserialize, Serialize};
13use siderite_orm::{
14 ConstraintMeta, DbDefault as OrmDbDefault, FieldMeta, ModelMeta, OnDelete as OrmOnDelete,
15 SqlType as OrmSqlType,
16};
17use std::collections::BTreeMap;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
21#[serde(rename_all = "snake_case")]
22pub enum SqlType {
23 SmallInt,
25 Integer,
27 BigInt,
29 Real,
31 Double,
33 Decimal,
35 Bool,
37 Text,
39 Binary,
41 Date,
43 Time,
45 Timestamp,
47 Duration,
49 Uuid,
51 Json,
53 IpAddr,
55}
56
57impl From<OrmSqlType> for SqlType {
58 fn from(value: OrmSqlType) -> Self {
59 match value {
60 OrmSqlType::SmallInt => Self::SmallInt,
61 OrmSqlType::Integer => Self::Integer,
62 OrmSqlType::BigInt => Self::BigInt,
63 OrmSqlType::Real => Self::Real,
64 OrmSqlType::Double => Self::Double,
65 OrmSqlType::Decimal => Self::Decimal,
66 OrmSqlType::Bool => Self::Bool,
67 OrmSqlType::Text => Self::Text,
68 OrmSqlType::Binary => Self::Binary,
69 OrmSqlType::Date => Self::Date,
70 OrmSqlType::Time => Self::Time,
71 OrmSqlType::Timestamp => Self::Timestamp,
72 OrmSqlType::Duration => Self::Duration,
73 OrmSqlType::Uuid => Self::Uuid,
74 OrmSqlType::Json => Self::Json,
75 OrmSqlType::IpAddr => Self::IpAddr,
76 other => unknown_sql_type(other),
77 }
78 }
79}
80
81impl From<SqlType> for OrmSqlType {
82 fn from(value: SqlType) -> Self {
83 match value {
84 SqlType::SmallInt => Self::SmallInt,
85 SqlType::Integer => Self::Integer,
86 SqlType::BigInt => Self::BigInt,
87 SqlType::Real => Self::Real,
88 SqlType::Double => Self::Double,
89 SqlType::Decimal => Self::Decimal,
90 SqlType::Bool => Self::Bool,
91 SqlType::Text => Self::Text,
92 SqlType::Binary => Self::Binary,
93 SqlType::Date => Self::Date,
94 SqlType::Time => Self::Time,
95 SqlType::Timestamp => Self::Timestamp,
96 SqlType::Duration => Self::Duration,
97 SqlType::Uuid => Self::Uuid,
98 SqlType::Json => Self::Json,
99 SqlType::IpAddr => Self::IpAddr,
100 }
101 }
102}
103
104fn unknown_sql_type(other: OrmSqlType) -> SqlType {
105 let _ = other;
108 SqlType::Text
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
113#[serde(rename_all = "snake_case")]
114pub enum OnDelete {
115 #[default]
117 Cascade,
118 Protect,
120 SetNull,
122 SetDefault,
124 DoNothing,
126}
127
128impl From<OrmOnDelete> for OnDelete {
129 fn from(value: OrmOnDelete) -> Self {
130 match value {
131 OrmOnDelete::Cascade => Self::Cascade,
132 OrmOnDelete::Protect => Self::Protect,
133 OrmOnDelete::SetNull => Self::SetNull,
134 OrmOnDelete::SetDefault => Self::SetDefault,
135 OrmOnDelete::DoNothing => Self::DoNothing,
136 }
137 }
138}
139
140impl From<OnDelete> for OrmOnDelete {
141 fn from(value: OnDelete) -> Self {
142 match value {
143 OnDelete::Cascade => Self::Cascade,
144 OnDelete::Protect => Self::Protect,
145 OnDelete::SetNull => Self::SetNull,
146 OnDelete::SetDefault => Self::SetDefault,
147 OnDelete::DoNothing => Self::DoNothing,
148 }
149 }
150}
151
152#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
154#[serde(rename_all = "snake_case")]
155pub enum DbDefault {
156 Now,
158 Int(i64),
160 Bool(bool),
162 Text(String),
164}
165
166impl From<OrmDbDefault> for DbDefault {
167 fn from(value: OrmDbDefault) -> Self {
168 match value {
169 OrmDbDefault::Now => Self::Now,
170 OrmDbDefault::Int(i) => Self::Int(i),
171 OrmDbDefault::Bool(b) => Self::Bool(b),
172 OrmDbDefault::Text(s) => Self::Text(s.to_owned()),
173 }
174 }
175}
176
177#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
179pub struct ForeignKeyState {
180 pub target_table: String,
182 pub target_column: String,
184 pub on_delete: OnDelete,
186}
187
188#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
190pub struct FieldState {
191 pub name: String,
193 pub column: String,
195 pub sql_type: SqlType,
197 pub nullable: bool,
199 pub primary_key: bool,
201 pub auto: bool,
203 pub unique: bool,
205 pub index: bool,
207 #[serde(default, skip_serializing_if = "Option::is_none")]
209 pub max_length: Option<u32>,
210 #[serde(default, skip_serializing_if = "Option::is_none")]
212 pub max_digits: Option<u32>,
213 #[serde(default, skip_serializing_if = "Option::is_none")]
215 pub decimal_places: Option<u32>,
216 #[serde(default, skip_serializing_if = "Option::is_none")]
218 pub default: Option<DbDefault>,
219 #[serde(default, skip_serializing_if = "Option::is_none")]
221 pub fk: Option<ForeignKeyState>,
222}
223
224impl FieldState {
225 pub fn new(name: impl Into<String>, column: impl Into<String>, sql_type: SqlType) -> Self {
227 Self {
228 name: name.into(),
229 column: column.into(),
230 sql_type,
231 nullable: false,
232 primary_key: false,
233 auto: false,
234 unique: false,
235 index: false,
236 max_length: None,
237 max_digits: None,
238 decimal_places: None,
239 default: None,
240 fk: None,
241 }
242 }
243
244 pub fn from_meta(field: &FieldMeta) -> Self {
246 let fk = field.relation.map(|rel| {
247 let target = (rel.target)();
248 let target_column = target
249 .pk()
250 .map(|pk| pk.column.to_owned())
251 .unwrap_or_else(|| "id".to_owned());
252 ForeignKeyState {
253 target_table: target.table.to_owned(),
254 target_column,
255 on_delete: rel.on_delete.into(),
256 }
257 });
258 Self {
259 name: field.name.to_owned(),
260 column: field.column.to_owned(),
261 sql_type: field.sql_type.into(),
262 nullable: field.nullable,
263 primary_key: field.primary_key,
264 auto: field.auto,
265 unique: field.unique,
266 index: field.index,
267 max_length: field.max_length,
268 max_digits: field.max_digits,
269 decimal_places: field.decimal_places,
270 default: field.default.map(Into::into),
271 fk,
272 }
273 }
274}
275
276#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
278pub struct IndexState {
279 pub name: String,
281 pub columns: Vec<String>,
283 pub unique: bool,
285}
286
287impl IndexState {
288 pub fn from_meta(meta: &siderite_orm::IndexMeta) -> Self {
290 Self {
291 name: meta.name.to_owned(),
292 columns: meta.columns.iter().map(|c| (*c).to_owned()).collect(),
293 unique: meta.unique,
294 }
295 }
296}
297
298#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
300#[serde(tag = "kind", rename_all = "snake_case")]
301pub enum ConstraintState {
302 Unique {
304 name: String,
306 columns: Vec<String>,
308 },
309 Check {
311 name: String,
313 sql: String,
315 },
316}
317
318impl ConstraintState {
319 pub fn from_meta(meta: &ConstraintMeta) -> Self {
321 match *meta {
322 ConstraintMeta::Unique { name, columns } => Self::Unique {
323 name: name.to_owned(),
324 columns: columns.iter().map(|c| (*c).to_owned()).collect(),
325 },
326 ConstraintMeta::Check { name, sql } => Self::Check {
327 name: name.to_owned(),
328 sql: sql.to_owned(),
329 },
330 }
331 }
332
333 pub fn name(&self) -> &str {
335 match self {
336 Self::Unique { name, .. } | Self::Check { name, .. } => name,
337 }
338 }
339}
340
341#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
343pub struct ModelState {
344 pub name: String,
346 pub table: String,
348 pub fields: Vec<FieldState>,
350 #[serde(default, skip_serializing_if = "Vec::is_empty")]
352 pub indexes: Vec<IndexState>,
353 #[serde(default, skip_serializing_if = "Vec::is_empty")]
355 pub constraints: Vec<ConstraintState>,
356}
357
358impl ModelState {
359 pub fn from_meta(meta: &ModelMeta) -> Option<Self> {
361 if !meta.managed {
362 return None;
363 }
364 Some(Self {
365 name: meta.name.to_owned(),
366 table: meta.table.to_owned(),
367 fields: meta.fields.iter().map(FieldState::from_meta).collect(),
368 indexes: meta.indexes.iter().map(IndexState::from_meta).collect(),
369 constraints: meta
370 .constraints
371 .iter()
372 .map(ConstraintState::from_meta)
373 .collect(),
374 })
375 }
376
377 pub fn field(&self, name: &str) -> Option<&FieldState> {
379 self.fields.iter().find(|f| f.name == name)
380 }
381
382 pub fn field_mut(&mut self, name: &str) -> Option<&mut FieldState> {
384 self.fields.iter_mut().find(|f| f.name == name)
385 }
386
387 pub fn column(&self, column: &str) -> Option<&FieldState> {
389 self.fields.iter().find(|f| f.column == column)
390 }
391
392 pub fn pk(&self) -> Option<&FieldState> {
394 self.fields.iter().find(|f| f.primary_key)
395 }
396
397 pub fn index(&self, name: &str) -> Option<&IndexState> {
399 self.indexes.iter().find(|i| i.name == name)
400 }
401
402 pub fn constraint(&self, name: &str) -> Option<&ConstraintState> {
404 self.constraints.iter().find(|c| c.name() == name)
405 }
406}
407
408#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
410pub struct ProjectState {
411 pub models: BTreeMap<String, ModelState>,
413}
414
415impl ProjectState {
416 pub fn new() -> Self {
418 Self::default()
419 }
420
421 pub fn from_metas(metas: &[&ModelMeta]) -> Self {
423 let mut models = BTreeMap::new();
424 for meta in metas {
425 if let Some(state) = ModelState::from_meta(meta) {
426 models.insert(state.name.clone(), state);
427 }
428 }
429 for meta in metas {
430 if !meta.managed {
431 continue;
432 }
433 for m2m in meta.many_to_many {
434 if m2m.through.is_some() {
435 continue;
436 }
437 let join = synthesize_m2m(meta, m2m);
438 models.insert(join.name.clone(), join);
439 }
440 }
441 Self { models }
442 }
443
444 pub fn model(&self, name: &str) -> Option<&ModelState> {
446 self.models.get(name)
447 }
448
449 pub fn model_mut(&mut self, name: &str) -> Option<&mut ModelState> {
451 self.models.get_mut(name)
452 }
453
454 pub fn model_for_table(&self, table: &str) -> Option<&ModelState> {
456 self.models.values().find(|m| m.table == table)
457 }
458
459 pub fn require(&self, name: &str) -> Result<&ModelState, MigrationError> {
461 self.model(name).ok_or_else(|| {
462 MigrationError::state(format!("model `{name}` is not in the project state"))
463 })
464 }
465
466 pub fn require_mut(&mut self, name: &str) -> Result<&mut ModelState, MigrationError> {
468 self.model_mut(name).ok_or_else(|| {
469 MigrationError::state(format!("model `{name}` is not in the project state"))
470 })
471 }
472}
473
474fn synthesize_m2m(source: &ModelMeta, m2m: &siderite_orm::ManyToManyMeta) -> ModelState {
475 let target = (m2m.target)();
476 let source_pk = source.pk();
477 let target_pk = target.pk();
478 let source_type = source_pk
479 .map(|f| SqlType::from(f.sql_type))
480 .unwrap_or(SqlType::BigInt);
481 let target_type = target_pk
482 .map(|f| SqlType::from(f.sql_type))
483 .unwrap_or(SqlType::BigInt);
484 let source_column_pk = source_pk.map(|f| f.column).unwrap_or("id");
485 let target_column_pk = target_pk.map(|f| f.column).unwrap_or("id");
486
487 let source_field = FieldState {
488 index: true,
489 fk: Some(ForeignKeyState {
490 target_table: source.table.to_owned(),
491 target_column: source_column_pk.to_owned(),
492 on_delete: OnDelete::Cascade,
493 }),
494 ..FieldState::new(m2m.source_column, m2m.source_column, source_type)
495 };
496 let target_field = FieldState {
497 index: true,
498 fk: Some(ForeignKeyState {
499 target_table: target.table.to_owned(),
500 target_column: target_column_pk.to_owned(),
501 on_delete: OnDelete::Cascade,
502 }),
503 ..FieldState::new(m2m.target_column, m2m.target_column, target_type)
504 };
505
506 ModelState {
507 name: m2m.through_table.to_owned(),
508 table: m2m.through_table.to_owned(),
509 fields: vec![source_field, target_field],
510 indexes: Vec::new(),
511 constraints: vec![ConstraintState::Unique {
512 name: format!("{}_uniq", m2m.through_table),
513 columns: vec![m2m.source_column.to_owned(), m2m.target_column.to_owned()],
514 }],
515 }
516}
517
518pub fn auto_index_name(table: &str, column: &str) -> String {
520 format!("{table}_{column}_idx")
521}