1use super::column_builder::{ColumnDefinition, DefaultValue};
6use crate::schema::{RustTypeMapping, TableSchema};
7
8#[derive(Debug, Clone, PartialEq)]
10pub enum Operation {
11 CreateTable(CreateTableOp),
13 DropTable(DropTableOp),
15 RenameTable(RenameTableOp),
17 AddColumn(AddColumnOp),
19 DropColumn(DropColumnOp),
21 AlterColumn(AlterColumnOp),
23 RenameColumn(RenameColumnOp),
25 CreateIndex(CreateIndexOp),
27 DropIndex(DropIndexOp),
29 AddForeignKey(AddForeignKeyOp),
31 DropForeignKey(DropForeignKeyOp),
33 RunSql(RawSqlOp),
35}
36
37impl Operation {
38 #[must_use]
40 pub fn drop_table(name: impl Into<String>) -> Self {
41 Self::DropTable(DropTableOp {
42 name: name.into(),
43 if_exists: false,
44 cascade: false,
45 })
46 }
47
48 #[must_use]
50 pub fn drop_table_if_exists(name: impl Into<String>) -> Self {
51 Self::DropTable(DropTableOp {
52 name: name.into(),
53 if_exists: true,
54 cascade: false,
55 })
56 }
57
58 #[must_use]
60 pub fn rename_table(old_name: impl Into<String>, new_name: impl Into<String>) -> Self {
61 Self::RenameTable(RenameTableOp {
62 old_name: old_name.into(),
63 new_name: new_name.into(),
64 })
65 }
66
67 #[must_use]
69 pub fn add_column(table: impl Into<String>, column: ColumnDefinition) -> Self {
70 Self::AddColumn(AddColumnOp {
71 table: table.into(),
72 column,
73 })
74 }
75
76 #[must_use]
78 pub fn drop_column(table: impl Into<String>, column: impl Into<String>) -> Self {
79 Self::DropColumn(DropColumnOp {
80 table: table.into(),
81 column: column.into(),
82 })
83 }
84
85 #[must_use]
87 pub fn rename_column(
88 table: impl Into<String>,
89 old_name: impl Into<String>,
90 new_name: impl Into<String>,
91 ) -> Self {
92 Self::RenameColumn(RenameColumnOp {
93 table: table.into(),
94 old_name: old_name.into(),
95 new_name: new_name.into(),
96 })
97 }
98
99 #[must_use]
101 pub fn run_sql(sql: impl Into<String>) -> Self {
102 Self::RunSql(RawSqlOp {
103 up_sql: sql.into(),
104 down_sql: None,
105 })
106 }
107
108 #[must_use]
110 pub fn run_sql_reversible(up_sql: impl Into<String>, down_sql: impl Into<String>) -> Self {
111 Self::RunSql(RawSqlOp {
112 up_sql: up_sql.into(),
113 down_sql: Some(down_sql.into()),
114 })
115 }
116
117 #[must_use]
121 pub fn reverse(&self) -> Option<Self> {
122 match self {
123 Self::CreateTable(op) => Some(Self::drop_table(&op.name)),
124 Self::DropTable(_) => None, Self::RenameTable(op) => {
126 Some(Self::rename_table(op.new_name.clone(), op.old_name.clone()))
127 }
128 Self::AddColumn(op) => Some(Self::drop_column(&op.table, &op.column.name)),
129 Self::DropColumn(_) => None, Self::AlterColumn(_) => None, Self::RenameColumn(op) => Some(Self::rename_column(
132 &op.table,
133 op.new_name.clone(),
134 op.old_name.clone(),
135 )),
136 Self::CreateIndex(op) => Some(Self::DropIndex(DropIndexOp {
137 name: op.name.clone(),
138 table: Some(op.table.clone()),
139 if_exists: false,
140 })),
141 Self::DropIndex(_) => None, Self::AddForeignKey(op) => op.name.as_ref().map(|name| {
143 Self::DropForeignKey(DropForeignKeyOp {
144 table: op.table.clone(),
145 name: name.clone(),
146 })
147 }),
148 Self::DropForeignKey(_) => None, Self::RunSql(op) => op.down_sql.as_ref().map(|down| Self::run_sql(down.clone())),
150 }
151 }
152
153 #[must_use]
155 pub fn is_reversible(&self) -> bool {
156 self.reverse().is_some()
157 }
158}
159
160#[derive(Debug, Clone, PartialEq)]
162pub struct CreateTableOp {
163 pub name: String,
165 pub columns: Vec<ColumnDefinition>,
167 pub constraints: Vec<TableConstraint>,
169 pub if_not_exists: bool,
171}
172
173impl CreateTableOp {
174 pub fn from_table<T: TableSchema>(dialect: &impl RustTypeMapping) -> Self {
177 let columns = T::SCHEMA
178 .iter()
179 .map(|col| {
180 let inner = strip_option(col.rust_type);
181 let data_type = dialect.map_type(inner);
182 let mut def = ColumnDefinition::new(col.name, data_type);
183 def.nullable = col.nullable;
184 def.primary_key = col.primary_key;
185 def.unique = col.unique;
186 def.autoincrement = col.autoincrement;
187 if let Some(expr) = col.default_expr {
188 def.default = Some(DefaultValue::Expression(expr.to_string()));
189 }
190 def
191 })
192 .collect();
193 Self {
194 name: T::NAME.to_string(),
195 columns,
196 constraints: vec![],
197 if_not_exists: false,
198 }
199 }
200
201 pub fn from_table_if_not_exists<T: TableSchema>(dialect: &impl RustTypeMapping) -> Self {
203 let mut op = Self::from_table::<T>(dialect);
204 op.if_not_exists = true;
205 op
206 }
207}
208
209pub(super) fn strip_option(rust_type: &str) -> &str {
213 rust_type
214 .strip_prefix("Option<")
215 .and_then(|s| s.strip_suffix('>'))
216 .unwrap_or(rust_type)
217}
218
219impl From<CreateTableOp> for Operation {
220 fn from(op: CreateTableOp) -> Self {
221 Self::CreateTable(op)
222 }
223}
224
225#[derive(Debug, Clone, PartialEq, Eq)]
227pub enum TableConstraint {
228 PrimaryKey {
230 name: Option<String>,
232 columns: Vec<String>,
234 },
235 Unique {
237 name: Option<String>,
239 columns: Vec<String>,
241 },
242 ForeignKey {
244 name: Option<String>,
246 columns: Vec<String>,
248 references_table: String,
250 references_columns: Vec<String>,
252 on_delete: Option<super::column_builder::ForeignKeyAction>,
254 on_update: Option<super::column_builder::ForeignKeyAction>,
256 },
257 Check {
259 name: Option<String>,
261 expression: String,
263 },
264}
265
266#[derive(Debug, Clone, PartialEq, Eq)]
268pub struct DropTableOp {
269 pub name: String,
271 pub if_exists: bool,
273 pub cascade: bool,
275}
276
277impl From<DropTableOp> for Operation {
278 fn from(op: DropTableOp) -> Self {
279 Self::DropTable(op)
280 }
281}
282
283#[derive(Debug, Clone, PartialEq, Eq)]
285pub struct RenameTableOp {
286 pub old_name: String,
288 pub new_name: String,
290}
291
292impl From<RenameTableOp> for Operation {
293 fn from(op: RenameTableOp) -> Self {
294 Self::RenameTable(op)
295 }
296}
297
298#[derive(Debug, Clone, PartialEq)]
300pub struct AddColumnOp {
301 pub table: String,
303 pub column: ColumnDefinition,
305}
306
307impl From<AddColumnOp> for Operation {
308 fn from(op: AddColumnOp) -> Self {
309 Self::AddColumn(op)
310 }
311}
312
313#[derive(Debug, Clone, PartialEq, Eq)]
315pub struct DropColumnOp {
316 pub table: String,
318 pub column: String,
320}
321
322impl From<DropColumnOp> for Operation {
323 fn from(op: DropColumnOp) -> Self {
324 Self::DropColumn(op)
325 }
326}
327
328#[derive(Debug, Clone, PartialEq)]
330pub enum AlterColumnChange {
331 SetDataType(crate::ast::DataType),
333 SetNullable(bool),
335 SetDefault(super::column_builder::DefaultValue),
337 DropDefault,
339}
340
341#[derive(Debug, Clone, PartialEq)]
343pub struct AlterColumnOp {
344 pub table: String,
346 pub column: String,
348 pub change: AlterColumnChange,
350}
351
352impl From<AlterColumnOp> for Operation {
353 fn from(op: AlterColumnOp) -> Self {
354 Self::AlterColumn(op)
355 }
356}
357
358#[derive(Debug, Clone, PartialEq, Eq)]
360pub struct RenameColumnOp {
361 pub table: String,
363 pub old_name: String,
365 pub new_name: String,
367}
368
369impl From<RenameColumnOp> for Operation {
370 fn from(op: RenameColumnOp) -> Self {
371 Self::RenameColumn(op)
372 }
373}
374
375#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
377pub enum IndexType {
378 #[default]
380 BTree,
381 Hash,
383 Gist,
385 Gin,
387}
388
389#[derive(Debug, Clone, PartialEq, Eq)]
391pub struct CreateIndexOp {
392 pub name: String,
394 pub table: String,
396 pub columns: Vec<String>,
398 pub unique: bool,
400 pub index_type: IndexType,
402 pub if_not_exists: bool,
404 pub condition: Option<String>,
406}
407
408impl From<CreateIndexOp> for Operation {
409 fn from(op: CreateIndexOp) -> Self {
410 Self::CreateIndex(op)
411 }
412}
413
414#[derive(Debug, Clone, PartialEq, Eq)]
416pub struct DropIndexOp {
417 pub name: String,
419 pub table: Option<String>,
421 pub if_exists: bool,
423}
424
425impl From<DropIndexOp> for Operation {
426 fn from(op: DropIndexOp) -> Self {
427 Self::DropIndex(op)
428 }
429}
430
431#[derive(Debug, Clone, PartialEq, Eq)]
433pub struct AddForeignKeyOp {
434 pub table: String,
436 pub name: Option<String>,
438 pub columns: Vec<String>,
440 pub references_table: String,
442 pub references_columns: Vec<String>,
444 pub on_delete: Option<super::column_builder::ForeignKeyAction>,
446 pub on_update: Option<super::column_builder::ForeignKeyAction>,
448}
449
450impl From<AddForeignKeyOp> for Operation {
451 fn from(op: AddForeignKeyOp) -> Self {
452 Self::AddForeignKey(op)
453 }
454}
455
456#[derive(Debug, Clone, PartialEq, Eq)]
458pub struct DropForeignKeyOp {
459 pub table: String,
461 pub name: String,
463}
464
465impl From<DropForeignKeyOp> for Operation {
466 fn from(op: DropForeignKeyOp) -> Self {
467 Self::DropForeignKey(op)
468 }
469}
470
471#[derive(Debug, Clone, PartialEq, Eq)]
473pub struct RawSqlOp {
474 pub up_sql: String,
476 pub down_sql: Option<String>,
478}
479
480impl From<RawSqlOp> for Operation {
481 fn from(op: RawSqlOp) -> Self {
482 Self::RunSql(op)
483 }
484}
485
486#[cfg(test)]
487mod tests {
488 use super::*;
489 use crate::migrations::column_builder::{ForeignKeyAction, bigint, varchar};
490
491 #[test]
492 fn test_drop_table_operation() {
493 let op = Operation::drop_table("users");
494 match op {
495 Operation::DropTable(drop) => {
496 assert_eq!(drop.name, "users");
497 assert!(!drop.if_exists);
498 assert!(!drop.cascade);
499 }
500 _ => panic!("Expected DropTable operation"),
501 }
502 }
503
504 #[test]
505 fn test_rename_table_operation() {
506 let op = Operation::rename_table("old_name", "new_name");
507 match op {
508 Operation::RenameTable(rename) => {
509 assert_eq!(rename.old_name, "old_name");
510 assert_eq!(rename.new_name, "new_name");
511 }
512 _ => panic!("Expected RenameTable operation"),
513 }
514 }
515
516 #[test]
517 fn test_add_column_operation() {
518 let col = varchar("email", 255).not_null().build();
519 let op = Operation::add_column("users", col);
520 match op {
521 Operation::AddColumn(add) => {
522 assert_eq!(add.table, "users");
523 assert_eq!(add.column.name, "email");
524 }
525 _ => panic!("Expected AddColumn operation"),
526 }
527 }
528
529 #[test]
530 fn test_reverse_operations() {
531 let create = CreateTableOp {
533 name: "users".to_string(),
534 columns: vec![bigint("id").primary_key().build()],
535 constraints: vec![],
536 if_not_exists: false,
537 };
538 let op = Operation::CreateTable(create);
539 let reversed = op.reverse().expect("Should be reversible");
540 match reversed {
541 Operation::DropTable(drop) => assert_eq!(drop.name, "users"),
542 _ => panic!("Expected DropTable"),
543 }
544
545 let rename = Operation::rename_table("old", "new");
547 let reversed = rename.reverse().expect("Should be reversible");
548 match reversed {
549 Operation::RenameTable(r) => {
550 assert_eq!(r.old_name, "new");
551 assert_eq!(r.new_name, "old");
552 }
553 _ => panic!("Expected RenameTable"),
554 }
555
556 let add = Operation::add_column("users", varchar("email", 255).build());
558 let reversed = add.reverse().expect("Should be reversible");
559 match reversed {
560 Operation::DropColumn(drop) => {
561 assert_eq!(drop.table, "users");
562 assert_eq!(drop.column, "email");
563 }
564 _ => panic!("Expected DropColumn"),
565 }
566
567 let drop = Operation::drop_table("users");
569 assert!(drop.reverse().is_none());
570 }
571
572 #[test]
573 fn test_raw_sql_reversibility() {
574 let op = Operation::run_sql("INSERT INTO config VALUES ('key', 'value')");
576 assert!(!op.is_reversible());
577
578 let op = Operation::run_sql_reversible(
580 "INSERT INTO config VALUES ('key', 'value')",
581 "DELETE FROM config WHERE key = 'key'",
582 );
583 assert!(op.is_reversible());
584 }
585
586 #[test]
587 fn test_table_constraint() {
588 let pk = TableConstraint::PrimaryKey {
589 name: Some("pk_users".to_string()),
590 columns: vec!["id".to_string()],
591 };
592 match pk {
593 TableConstraint::PrimaryKey { name, columns } => {
594 assert_eq!(name, Some("pk_users".to_string()));
595 assert_eq!(columns, vec!["id"]);
596 }
597 _ => panic!("Expected PrimaryKey"),
598 }
599
600 let fk = TableConstraint::ForeignKey {
601 name: Some("fk_user_company".to_string()),
602 columns: vec!["company_id".to_string()],
603 references_table: "companies".to_string(),
604 references_columns: vec!["id".to_string()],
605 on_delete: Some(ForeignKeyAction::Cascade),
606 on_update: None,
607 };
608 match fk {
609 TableConstraint::ForeignKey {
610 references_table,
611 on_delete,
612 ..
613 } => {
614 assert_eq!(references_table, "companies");
615 assert_eq!(on_delete, Some(ForeignKeyAction::Cascade));
616 }
617 _ => panic!("Expected ForeignKey"),
618 }
619 }
620}