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 SetUnique(bool),
341 SetAutoincrement(bool),
344}
345
346#[derive(Debug, Clone, PartialEq)]
348pub struct AlterColumnOp {
349 pub table: String,
351 pub column: String,
353 pub change: AlterColumnChange,
355}
356
357impl From<AlterColumnOp> for Operation {
358 fn from(op: AlterColumnOp) -> Self {
359 Self::AlterColumn(op)
360 }
361}
362
363#[derive(Debug, Clone, PartialEq, Eq)]
365pub struct RenameColumnOp {
366 pub table: String,
368 pub old_name: String,
370 pub new_name: String,
372}
373
374impl From<RenameColumnOp> for Operation {
375 fn from(op: RenameColumnOp) -> Self {
376 Self::RenameColumn(op)
377 }
378}
379
380#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
382pub enum IndexType {
383 #[default]
385 BTree,
386 Hash,
388 Gist,
390 Gin,
392}
393
394#[derive(Debug, Clone, PartialEq, Eq)]
396pub struct CreateIndexOp {
397 pub name: String,
399 pub table: String,
401 pub columns: Vec<String>,
403 pub unique: bool,
405 pub index_type: IndexType,
407 pub if_not_exists: bool,
409 pub condition: Option<String>,
411}
412
413impl From<CreateIndexOp> for Operation {
414 fn from(op: CreateIndexOp) -> Self {
415 Self::CreateIndex(op)
416 }
417}
418
419#[derive(Debug, Clone, PartialEq, Eq)]
421pub struct DropIndexOp {
422 pub name: String,
424 pub table: Option<String>,
426 pub if_exists: bool,
428}
429
430impl From<DropIndexOp> for Operation {
431 fn from(op: DropIndexOp) -> Self {
432 Self::DropIndex(op)
433 }
434}
435
436#[derive(Debug, Clone, PartialEq, Eq)]
438pub struct AddForeignKeyOp {
439 pub table: String,
441 pub name: Option<String>,
443 pub columns: Vec<String>,
445 pub references_table: String,
447 pub references_columns: Vec<String>,
449 pub on_delete: Option<super::column_builder::ForeignKeyAction>,
451 pub on_update: Option<super::column_builder::ForeignKeyAction>,
453}
454
455impl From<AddForeignKeyOp> for Operation {
456 fn from(op: AddForeignKeyOp) -> Self {
457 Self::AddForeignKey(op)
458 }
459}
460
461#[derive(Debug, Clone, PartialEq, Eq)]
463pub struct DropForeignKeyOp {
464 pub table: String,
466 pub name: String,
468}
469
470impl From<DropForeignKeyOp> for Operation {
471 fn from(op: DropForeignKeyOp) -> Self {
472 Self::DropForeignKey(op)
473 }
474}
475
476#[derive(Debug, Clone, PartialEq, Eq)]
478pub struct RawSqlOp {
479 pub up_sql: String,
481 pub down_sql: Option<String>,
483}
484
485impl From<RawSqlOp> for Operation {
486 fn from(op: RawSqlOp) -> Self {
487 Self::RunSql(op)
488 }
489}
490
491#[cfg(test)]
492mod tests {
493 use super::*;
494 use crate::migrations::column_builder::{ForeignKeyAction, bigint, varchar};
495
496 #[test]
497 fn test_drop_table_operation() {
498 let op = Operation::drop_table("users");
499 match op {
500 Operation::DropTable(drop) => {
501 assert_eq!(drop.name, "users");
502 assert!(!drop.if_exists);
503 assert!(!drop.cascade);
504 }
505 _ => panic!("Expected DropTable operation"),
506 }
507 }
508
509 #[test]
510 fn test_rename_table_operation() {
511 let op = Operation::rename_table("old_name", "new_name");
512 match op {
513 Operation::RenameTable(rename) => {
514 assert_eq!(rename.old_name, "old_name");
515 assert_eq!(rename.new_name, "new_name");
516 }
517 _ => panic!("Expected RenameTable operation"),
518 }
519 }
520
521 #[test]
522 fn test_add_column_operation() {
523 let col = varchar("email", 255).not_null().build();
524 let op = Operation::add_column("users", col);
525 match op {
526 Operation::AddColumn(add) => {
527 assert_eq!(add.table, "users");
528 assert_eq!(add.column.name, "email");
529 }
530 _ => panic!("Expected AddColumn operation"),
531 }
532 }
533
534 #[test]
535 fn test_reverse_operations() {
536 let create = CreateTableOp {
538 name: "users".to_string(),
539 columns: vec![bigint("id").primary_key().build()],
540 constraints: vec![],
541 if_not_exists: false,
542 };
543 let op = Operation::CreateTable(create);
544 let reversed = op.reverse().expect("Should be reversible");
545 match reversed {
546 Operation::DropTable(drop) => assert_eq!(drop.name, "users"),
547 _ => panic!("Expected DropTable"),
548 }
549
550 let rename = Operation::rename_table("old", "new");
552 let reversed = rename.reverse().expect("Should be reversible");
553 match reversed {
554 Operation::RenameTable(r) => {
555 assert_eq!(r.old_name, "new");
556 assert_eq!(r.new_name, "old");
557 }
558 _ => panic!("Expected RenameTable"),
559 }
560
561 let add = Operation::add_column("users", varchar("email", 255).build());
563 let reversed = add.reverse().expect("Should be reversible");
564 match reversed {
565 Operation::DropColumn(drop) => {
566 assert_eq!(drop.table, "users");
567 assert_eq!(drop.column, "email");
568 }
569 _ => panic!("Expected DropColumn"),
570 }
571
572 let drop = Operation::drop_table("users");
574 assert!(drop.reverse().is_none());
575 }
576
577 #[test]
578 fn test_raw_sql_reversibility() {
579 let op = Operation::run_sql("INSERT INTO config VALUES ('key', 'value')");
581 assert!(!op.is_reversible());
582
583 let op = Operation::run_sql_reversible(
585 "INSERT INTO config VALUES ('key', 'value')",
586 "DELETE FROM config WHERE key = 'key'",
587 );
588 assert!(op.is_reversible());
589 }
590
591 #[test]
592 fn test_table_constraint() {
593 let pk = TableConstraint::PrimaryKey {
594 name: Some("pk_users".to_string()),
595 columns: vec!["id".to_string()],
596 };
597 match pk {
598 TableConstraint::PrimaryKey { name, columns } => {
599 assert_eq!(name, Some("pk_users".to_string()));
600 assert_eq!(columns, vec!["id"]);
601 }
602 _ => panic!("Expected PrimaryKey"),
603 }
604
605 let fk = TableConstraint::ForeignKey {
606 name: Some("fk_user_company".to_string()),
607 columns: vec!["company_id".to_string()],
608 references_table: "companies".to_string(),
609 references_columns: vec!["id".to_string()],
610 on_delete: Some(ForeignKeyAction::Cascade),
611 on_update: None,
612 };
613 match fk {
614 TableConstraint::ForeignKey {
615 references_table,
616 on_delete,
617 ..
618 } => {
619 assert_eq!(references_table, "companies");
620 assert_eq!(on_delete, Some(ForeignKeyAction::Cascade));
621 }
622 _ => panic!("Expected ForeignKey"),
623 }
624 }
625}