Skip to main content

oxide_sql_core/migrations/
operation.rs

1//! Migration operations.
2//!
3//! Defines all possible migration operations like CREATE TABLE, ADD COLUMN, etc.
4
5use super::column_builder::{ColumnDefinition, DefaultValue};
6use crate::schema::{RustTypeMapping, TableSchema};
7
8/// All possible migration operations.
9#[derive(Debug, Clone, PartialEq)]
10pub enum Operation {
11    /// Create a new table.
12    CreateTable(CreateTableOp),
13    /// Drop an existing table.
14    DropTable(DropTableOp),
15    /// Rename a table.
16    RenameTable(RenameTableOp),
17    /// Add a column to an existing table.
18    AddColumn(AddColumnOp),
19    /// Drop a column from a table.
20    DropColumn(DropColumnOp),
21    /// Alter a column definition.
22    AlterColumn(AlterColumnOp),
23    /// Rename a column.
24    RenameColumn(RenameColumnOp),
25    /// Create an index.
26    CreateIndex(CreateIndexOp),
27    /// Drop an index.
28    DropIndex(DropIndexOp),
29    /// Add a foreign key constraint.
30    AddForeignKey(AddForeignKeyOp),
31    /// Drop a foreign key constraint.
32    DropForeignKey(DropForeignKeyOp),
33    /// Run raw SQL.
34    RunSql(RawSqlOp),
35}
36
37impl Operation {
38    /// Creates a drop table operation.
39    #[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    /// Creates a drop table if exists operation.
49    #[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    /// Creates a rename table operation.
59    #[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    /// Creates an add column operation.
68    #[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    /// Creates a drop column operation.
77    #[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    /// Creates a rename column operation.
86    #[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    /// Creates a raw SQL operation.
100    #[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    /// Creates a raw SQL operation with both up and down SQL.
109    #[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    /// Attempts to generate the reverse operation.
118    ///
119    /// Returns `None` if the operation is not reversible.
120    #[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, // Cannot reverse without knowing the schema
125            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, // Cannot reverse without knowing the column definition
130            Self::AlterColumn(_) => None, // Cannot reverse without knowing the old definition
131            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, // Cannot reverse without knowing the index definition
142            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, // Cannot reverse without knowing the FK definition
149            Self::RunSql(op) => op.down_sql.as_ref().map(|down| Self::run_sql(down.clone())),
150        }
151    }
152
153    /// Returns whether this operation is reversible.
154    #[must_use]
155    pub fn is_reversible(&self) -> bool {
156        self.reverse().is_some()
157    }
158}
159
160/// Create table operation.
161#[derive(Debug, Clone, PartialEq)]
162pub struct CreateTableOp {
163    /// Table name.
164    pub name: String,
165    /// Column definitions.
166    pub columns: Vec<ColumnDefinition>,
167    /// Table-level constraints.
168    pub constraints: Vec<TableConstraint>,
169    /// Whether to use IF NOT EXISTS.
170    pub if_not_exists: bool,
171}
172
173impl CreateTableOp {
174    /// Builds a `CreateTableOp` from a `#[derive(Table)]` struct
175    /// using the given dialect for Rust-to-SQL type mapping.
176    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    /// Same as `from_table` but with `IF NOT EXISTS`.
202    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
209/// Strips `Option<T>` wrapper from a Rust type string, returning
210/// the inner type. Nullability is tracked separately via
211/// `ColumnSchema::nullable`.
212pub(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/// Table-level constraint.
226#[derive(Debug, Clone, PartialEq, Eq)]
227pub enum TableConstraint {
228    /// Primary key constraint on multiple columns.
229    PrimaryKey {
230        /// Optional constraint name.
231        name: Option<String>,
232        /// Column names.
233        columns: Vec<String>,
234    },
235    /// Unique constraint on multiple columns.
236    Unique {
237        /// Optional constraint name.
238        name: Option<String>,
239        /// Column names.
240        columns: Vec<String>,
241    },
242    /// Foreign key constraint.
243    ForeignKey {
244        /// Optional constraint name.
245        name: Option<String>,
246        /// Columns in this table.
247        columns: Vec<String>,
248        /// Referenced table.
249        references_table: String,
250        /// Referenced columns.
251        references_columns: Vec<String>,
252        /// ON DELETE action.
253        on_delete: Option<super::column_builder::ForeignKeyAction>,
254        /// ON UPDATE action.
255        on_update: Option<super::column_builder::ForeignKeyAction>,
256    },
257    /// Check constraint.
258    Check {
259        /// Optional constraint name.
260        name: Option<String>,
261        /// Check expression.
262        expression: String,
263    },
264}
265
266/// Drop table operation.
267#[derive(Debug, Clone, PartialEq, Eq)]
268pub struct DropTableOp {
269    /// Table name.
270    pub name: String,
271    /// Whether to use IF EXISTS.
272    pub if_exists: bool,
273    /// Whether to cascade.
274    pub cascade: bool,
275}
276
277impl From<DropTableOp> for Operation {
278    fn from(op: DropTableOp) -> Self {
279        Self::DropTable(op)
280    }
281}
282
283/// Rename table operation.
284#[derive(Debug, Clone, PartialEq, Eq)]
285pub struct RenameTableOp {
286    /// Current table name.
287    pub old_name: String,
288    /// New table name.
289    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/// Add column operation.
299#[derive(Debug, Clone, PartialEq)]
300pub struct AddColumnOp {
301    /// Table name.
302    pub table: String,
303    /// Column definition.
304    pub column: ColumnDefinition,
305}
306
307impl From<AddColumnOp> for Operation {
308    fn from(op: AddColumnOp) -> Self {
309        Self::AddColumn(op)
310    }
311}
312
313/// Drop column operation.
314#[derive(Debug, Clone, PartialEq, Eq)]
315pub struct DropColumnOp {
316    /// Table name.
317    pub table: String,
318    /// Column name.
319    pub column: String,
320}
321
322impl From<DropColumnOp> for Operation {
323    fn from(op: DropColumnOp) -> Self {
324        Self::DropColumn(op)
325    }
326}
327
328/// Column alteration type.
329#[derive(Debug, Clone, PartialEq)]
330pub enum AlterColumnChange {
331    /// Change the data type.
332    SetDataType(crate::ast::DataType),
333    /// Set or remove NOT NULL constraint.
334    SetNullable(bool),
335    /// Set a new default value.
336    SetDefault(super::column_builder::DefaultValue),
337    /// Remove the default value.
338    DropDefault,
339    /// Add or drop a UNIQUE constraint.
340    SetUnique(bool),
341    /// Mark autoincrement change (informational — most DBs cannot
342    /// alter this; prefer using [`DiffWarning`] instead).
343    SetAutoincrement(bool),
344}
345
346/// Alter column operation.
347#[derive(Debug, Clone, PartialEq)]
348pub struct AlterColumnOp {
349    /// Table name.
350    pub table: String,
351    /// Column name.
352    pub column: String,
353    /// The change to apply.
354    pub change: AlterColumnChange,
355}
356
357impl From<AlterColumnOp> for Operation {
358    fn from(op: AlterColumnOp) -> Self {
359        Self::AlterColumn(op)
360    }
361}
362
363/// Rename column operation.
364#[derive(Debug, Clone, PartialEq, Eq)]
365pub struct RenameColumnOp {
366    /// Table name.
367    pub table: String,
368    /// Current column name.
369    pub old_name: String,
370    /// New column name.
371    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/// Index type.
381#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
382pub enum IndexType {
383    /// B-tree index (default).
384    #[default]
385    BTree,
386    /// Hash index.
387    Hash,
388    /// GiST index (PostgreSQL).
389    Gist,
390    /// GIN index (PostgreSQL).
391    Gin,
392}
393
394/// Create index operation.
395#[derive(Debug, Clone, PartialEq, Eq)]
396pub struct CreateIndexOp {
397    /// Index name.
398    pub name: String,
399    /// Table name.
400    pub table: String,
401    /// Columns to index.
402    pub columns: Vec<String>,
403    /// Whether this is a unique index.
404    pub unique: bool,
405    /// Index type.
406    pub index_type: IndexType,
407    /// Whether to use IF NOT EXISTS.
408    pub if_not_exists: bool,
409    /// Partial index condition (WHERE clause).
410    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/// Drop index operation.
420#[derive(Debug, Clone, PartialEq, Eq)]
421pub struct DropIndexOp {
422    /// Index name.
423    pub name: String,
424    /// Table name (required for some dialects).
425    pub table: Option<String>,
426    /// Whether to use IF EXISTS.
427    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/// Add foreign key operation.
437#[derive(Debug, Clone, PartialEq, Eq)]
438pub struct AddForeignKeyOp {
439    /// Table name.
440    pub table: String,
441    /// Optional constraint name.
442    pub name: Option<String>,
443    /// Columns in this table.
444    pub columns: Vec<String>,
445    /// Referenced table.
446    pub references_table: String,
447    /// Referenced columns.
448    pub references_columns: Vec<String>,
449    /// ON DELETE action.
450    pub on_delete: Option<super::column_builder::ForeignKeyAction>,
451    /// ON UPDATE action.
452    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/// Drop foreign key operation.
462#[derive(Debug, Clone, PartialEq, Eq)]
463pub struct DropForeignKeyOp {
464    /// Table name.
465    pub table: String,
466    /// Constraint name.
467    pub name: String,
468}
469
470impl From<DropForeignKeyOp> for Operation {
471    fn from(op: DropForeignKeyOp) -> Self {
472        Self::DropForeignKey(op)
473    }
474}
475
476/// Raw SQL operation.
477#[derive(Debug, Clone, PartialEq, Eq)]
478pub struct RawSqlOp {
479    /// SQL to run for the up migration.
480    pub up_sql: String,
481    /// SQL to run for the down migration (if reversible).
482    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        // Create table can be reversed to drop table
537        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        // Rename table is reversible
551        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        // Add column can be reversed to drop column
562        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        // Drop table is NOT reversible (no schema info)
573        let drop = Operation::drop_table("users");
574        assert!(drop.reverse().is_none());
575    }
576
577    #[test]
578    fn test_raw_sql_reversibility() {
579        // Non-reversible raw SQL
580        let op = Operation::run_sql("INSERT INTO config VALUES ('key', 'value')");
581        assert!(!op.is_reversible());
582
583        // Reversible raw SQL
584        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}