1use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13pub struct TableSchema {
14 pub name: String,
16 pub columns: Vec<ColumnSchema>,
18 pub indexes: Vec<IndexSchema>,
20 pub foreign_keys: Vec<ForeignKeySchema>,
22 pub primary_key: Vec<String>,
24 pub migration_id: Option<String>,
26}
27
28impl TableSchema {
29 pub fn new(name: impl Into<String>) -> Self {
31 Self {
32 name: name.into(),
33 columns: Vec::new(),
34 indexes: Vec::new(),
35 foreign_keys: Vec::new(),
36 primary_key: Vec::new(),
37 migration_id: None,
38 }
39 }
40
41 pub fn add_column(&mut self, column: ColumnSchema) {
43 self.columns.push(column);
44 }
45
46 pub fn find_column(&self, name: &str) -> Option<&ColumnSchema> {
48 self.columns.iter().find(|c| c.name == name)
49 }
50
51 pub fn find_column_mut(&mut self, name: &str) -> Option<&mut ColumnSchema> {
53 self.columns.iter_mut().find(|c| c.name == name)
54 }
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59pub struct ColumnSchema {
60 pub name: String,
62 pub column_type: ColumnType,
64 pub nullable: bool,
66 pub default: Option<String>,
68 pub unique: bool,
70 pub primary_key: bool,
72 pub auto_increment: bool,
74 pub indexed: bool,
76 pub index_name: Option<String>,
78 pub max_length: Option<u32>,
80 pub min_length: Option<u32>,
82 pub range: Option<RangeConstraint>,
84 pub soft_delete: bool,
86 pub renamed_from: Option<String>,
88 pub dropped: bool,
90}
91
92impl ColumnSchema {
93 pub fn new(name: impl Into<String>, column_type: ColumnType) -> Self {
95 Self {
96 name: name.into(),
97 column_type,
98 nullable: false,
99 default: None,
100 unique: false,
101 primary_key: false,
102 auto_increment: false,
103 indexed: false,
104 index_name: None,
105 max_length: None,
106 min_length: None,
107 range: None,
108 soft_delete: false,
109 renamed_from: None,
110 dropped: false,
111 }
112 }
113
114 pub fn nullable(mut self, nullable: bool) -> Self {
116 self.nullable = nullable;
117 self
118 }
119
120 pub fn default(mut self, default: impl Into<String>) -> Self {
122 self.default = Some(default.into());
123 self
124 }
125
126 pub fn primary_key(mut self, auto_increment: bool) -> Self {
128 self.primary_key = true;
129 self.auto_increment = auto_increment;
130 self
131 }
132
133 pub fn indexed(mut self) -> Self {
135 self.indexed = true;
136 self
137 }
138
139 pub fn unique(mut self) -> Self {
141 self.unique = true;
142 self
143 }
144
145 pub fn max_length(mut self, len: u32) -> Self {
147 self.max_length = Some(len);
148 self
149 }
150}
151
152#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
154pub enum ColumnType {
155 Boolean,
157 SmallInteger,
159 Integer,
161 BigInteger,
163 Float,
165 Double,
167 Decimal { precision: u32, scale: u32 },
169 String(Option<u32>),
171 Text,
173 Binary,
175 Date,
177 Time,
179 DateTime,
181 TimestampTz,
183 Uuid,
185 Json,
187 JsonB,
189}
190
191impl ColumnType {
192 pub fn from_rust_type(type_str: &str) -> Self {
194 let type_str = type_str.trim();
195
196 if type_str.starts_with("Option<") && type_str.ends_with('>') {
198 let inner = &type_str[7..type_str.len() - 1];
199 return Self::from_rust_type(inner);
200 }
201
202 let type_str = if let Some(last) = type_str.rsplit("::").next() { last } else { type_str };
204
205 match type_str {
206 "bool" => Self::Boolean,
207 "i16" => Self::SmallInteger,
208 "i32" => Self::Integer,
209 "i64" => Self::BigInteger,
210 "f32" => Self::Float,
211 "f64" => Self::Double,
212 "String" => Self::String(None),
213 "Vec<u8>" => Self::Binary,
214 "Uuid" => Self::Uuid,
215 "NaiveDate" | "Date" => Self::Date,
216 "NaiveTime" | "Time" => Self::Time,
217 "NaiveDateTime" | "DateTime" => Self::DateTime,
218 "DateTimeWithTimeZone" | "DateTime<FixedOffset>" => Self::TimestampTz,
219 "Value" => Self::Json,
220 _ => Self::String(None), }
222 }
223
224 pub fn is_option_type(type_str: &str) -> bool {
226 type_str.trim().starts_with("Option<")
227 }
228}
229
230#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
232pub struct RangeConstraint {
233 pub min: Option<i64>,
234 pub max: Option<i64>,
235}
236
237#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
239pub struct IndexSchema {
240 pub name: String,
242 pub columns: Vec<String>,
244 pub unique: bool,
246}
247
248impl IndexSchema {
249 pub fn new(name: impl Into<String>, columns: Vec<String>) -> Self {
251 Self {
252 name: name.into(),
253 columns,
254 unique: false,
255 }
256 }
257
258 pub fn unique(mut self) -> Self {
260 self.unique = true;
261 self
262 }
263}
264
265#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
267pub struct ForeignKeySchema {
268 pub name: Option<String>,
270 pub column: String,
272 pub references_table: String,
274 pub references_column: String,
276 pub on_delete: OnDeleteAction,
278 pub on_update: OnUpdateAction,
280}
281
282impl ForeignKeySchema {
283 pub fn new(
285 column: impl Into<String>,
286 references_table: impl Into<String>,
287 references_column: impl Into<String>,
288 ) -> Self {
289 Self {
290 name: None,
291 column: column.into(),
292 references_table: references_table.into(),
293 references_column: references_column.into(),
294 on_delete: OnDeleteAction::NoAction,
295 on_update: OnUpdateAction::NoAction,
296 }
297 }
298
299 pub fn on_delete(mut self, action: OnDeleteAction) -> Self {
301 self.on_delete = action;
302 self
303 }
304}
305
306#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
308pub enum OnDeleteAction {
309 #[default]
310 NoAction,
311 Restrict,
312 Cascade,
313 SetNull,
314 SetDefault,
315}
316
317impl OnDeleteAction {
318 #[allow(clippy::should_implement_trait)]
320 pub fn from_str(s: &str) -> Self {
321 match s.to_lowercase().as_str() {
322 "cascade" => Self::Cascade,
323 "restrict" => Self::Restrict,
324 "setnull" | "set_null" | "set null" => Self::SetNull,
325 "setdefault" | "set_default" | "set default" => Self::SetDefault,
326 _ => Self::NoAction,
327 }
328 }
329}
330
331#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
333pub enum OnUpdateAction {
334 #[default]
335 NoAction,
336 Restrict,
337 Cascade,
338 SetNull,
339 SetDefault,
340}
341
342#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
344pub struct MigrationMeta {
345 pub id: String,
347 pub after: Option<String>,
349 pub tables: Vec<TableSchema>,
351 pub data_migration: Option<String>,
353}
354
355impl MigrationMeta {
356 pub fn new(id: impl Into<String>) -> Self {
358 Self {
359 id: id.into(),
360 after: None,
361 tables: Vec::new(),
362 data_migration: None,
363 }
364 }
365
366 pub fn after(mut self, after: impl Into<String>) -> Self {
368 self.after = Some(after.into());
369 self
370 }
371
372 pub fn add_table(&mut self, table: TableSchema) {
374 self.tables.push(table);
375 }
376}
377
378#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
380pub struct TableDelta {
381 pub table: String,
383 pub extends: String,
385 pub add_columns: Vec<ColumnSchema>,
387 pub drop_columns: Vec<String>,
389 pub rename_columns: Vec<(String, String)>,
391 pub alter_columns: Vec<ColumnSchema>,
393}
394
395#[cfg(test)]
396mod tests {
397 use super::*;
398
399 #[test]
400 fn test_column_type_from_rust_type_primitives() {
401 assert_eq!(ColumnType::from_rust_type("bool"), ColumnType::Boolean);
402 assert_eq!(ColumnType::from_rust_type("i16"), ColumnType::SmallInteger);
403 assert_eq!(ColumnType::from_rust_type("i32"), ColumnType::Integer);
404 assert_eq!(ColumnType::from_rust_type("i64"), ColumnType::BigInteger);
405 assert_eq!(ColumnType::from_rust_type("f32"), ColumnType::Float);
406 assert_eq!(ColumnType::from_rust_type("f64"), ColumnType::Double);
407 assert_eq!(ColumnType::from_rust_type("String"), ColumnType::String(None));
408 }
409
410 #[test]
411 fn test_column_type_from_rust_type_datetime() {
412 assert_eq!(ColumnType::from_rust_type("DateTimeWithTimeZone"), ColumnType::TimestampTz);
414 assert_eq!(ColumnType::from_rust_type("NaiveDateTime"), ColumnType::DateTime);
415 assert_eq!(ColumnType::from_rust_type("NaiveDate"), ColumnType::Date);
416 assert_eq!(ColumnType::from_rust_type("NaiveTime"), ColumnType::Time);
417
418 assert_eq!(
420 ColumnType::from_rust_type("ormada::prelude::DateTimeWithTimeZone"),
421 ColumnType::TimestampTz
422 );
423 assert_eq!(ColumnType::from_rust_type("chrono::NaiveDateTime"), ColumnType::DateTime);
424 assert_eq!(ColumnType::from_rust_type("chrono::NaiveDate"), ColumnType::Date);
425 assert_eq!(ColumnType::from_rust_type("chrono::NaiveTime"), ColumnType::Time);
426 }
427
428 #[test]
429 fn test_column_type_from_rust_type_special() {
430 assert_eq!(ColumnType::from_rust_type("Uuid"), ColumnType::Uuid);
431 assert_eq!(ColumnType::from_rust_type("uuid::Uuid"), ColumnType::Uuid);
432 assert_eq!(ColumnType::from_rust_type("Vec<u8>"), ColumnType::Binary);
433 assert_eq!(ColumnType::from_rust_type("Value"), ColumnType::Json);
434 assert_eq!(ColumnType::from_rust_type("serde_json::Value"), ColumnType::Json);
435 }
436
437 #[test]
438 fn test_column_type_from_rust_type_option() {
439 assert_eq!(ColumnType::from_rust_type("Option<i32>"), ColumnType::Integer);
441 assert_eq!(ColumnType::from_rust_type("Option<String>"), ColumnType::String(None));
442 assert_eq!(
443 ColumnType::from_rust_type("Option<DateTimeWithTimeZone>"),
444 ColumnType::TimestampTz
445 );
446 assert_eq!(
447 ColumnType::from_rust_type("Option<ormada::prelude::DateTimeWithTimeZone>"),
448 ColumnType::TimestampTz
449 );
450 }
451
452 #[test]
453 fn test_column_type_from_rust_type_unknown_fallback() {
454 assert_eq!(ColumnType::from_rust_type("CustomType"), ColumnType::String(None));
456 assert_eq!(ColumnType::from_rust_type("my_module::MyType"), ColumnType::String(None));
457 }
458
459 #[test]
460 fn test_is_option_type() {
461 assert!(ColumnType::is_option_type("Option<i32>"));
462 assert!(ColumnType::is_option_type("Option<String>"));
463 assert!(ColumnType::is_option_type("Option<DateTimeWithTimeZone>"));
464 assert!(!ColumnType::is_option_type("i32"));
465 assert!(!ColumnType::is_option_type("String"));
466 assert!(!ColumnType::is_option_type("DateTimeWithTimeZone"));
467 }
468
469 #[test]
470 fn test_table_schema_builder() {
471 let mut table = TableSchema::new("books");
472 table.add_column(ColumnSchema::new("id", ColumnType::Integer).primary_key(true));
473 table.add_column(ColumnSchema::new("title", ColumnType::String(Some(200))).max_length(200));
474 table.primary_key = vec!["id".to_string()];
475
476 assert_eq!(table.name, "books");
477 assert_eq!(table.columns.len(), 2);
478 assert!(table.find_column("id").is_some());
479 assert!(table.find_column("title").is_some());
480 assert!(table.find_column("nonexistent").is_none());
481 }
482
483 #[test]
484 fn test_on_delete_from_str() {
485 assert_eq!(OnDeleteAction::from_str("Cascade"), OnDeleteAction::Cascade);
486 assert_eq!(OnDeleteAction::from_str("CASCADE"), OnDeleteAction::Cascade);
487 assert_eq!(OnDeleteAction::from_str("SetNull"), OnDeleteAction::SetNull);
488 assert_eq!(OnDeleteAction::from_str("set_null"), OnDeleteAction::SetNull);
489 assert_eq!(OnDeleteAction::from_str("unknown"), OnDeleteAction::NoAction);
490 }
491}