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}
340
341/// Alter column operation.
342#[derive(Debug, Clone, PartialEq)]
343pub struct AlterColumnOp {
344    /// Table name.
345    pub table: String,
346    /// Column name.
347    pub column: String,
348    /// The change to apply.
349    pub change: AlterColumnChange,
350}
351
352impl From<AlterColumnOp> for Operation {
353    fn from(op: AlterColumnOp) -> Self {
354        Self::AlterColumn(op)
355    }
356}
357
358/// Rename column operation.
359#[derive(Debug, Clone, PartialEq, Eq)]
360pub struct RenameColumnOp {
361    /// Table name.
362    pub table: String,
363    /// Current column name.
364    pub old_name: String,
365    /// New column name.
366    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/// Index type.
376#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
377pub enum IndexType {
378    /// B-tree index (default).
379    #[default]
380    BTree,
381    /// Hash index.
382    Hash,
383    /// GiST index (PostgreSQL).
384    Gist,
385    /// GIN index (PostgreSQL).
386    Gin,
387}
388
389/// Create index operation.
390#[derive(Debug, Clone, PartialEq, Eq)]
391pub struct CreateIndexOp {
392    /// Index name.
393    pub name: String,
394    /// Table name.
395    pub table: String,
396    /// Columns to index.
397    pub columns: Vec<String>,
398    /// Whether this is a unique index.
399    pub unique: bool,
400    /// Index type.
401    pub index_type: IndexType,
402    /// Whether to use IF NOT EXISTS.
403    pub if_not_exists: bool,
404    /// Partial index condition (WHERE clause).
405    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/// Drop index operation.
415#[derive(Debug, Clone, PartialEq, Eq)]
416pub struct DropIndexOp {
417    /// Index name.
418    pub name: String,
419    /// Table name (required for some dialects).
420    pub table: Option<String>,
421    /// Whether to use IF EXISTS.
422    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/// Add foreign key operation.
432#[derive(Debug, Clone, PartialEq, Eq)]
433pub struct AddForeignKeyOp {
434    /// Table name.
435    pub table: String,
436    /// Optional constraint name.
437    pub name: Option<String>,
438    /// Columns in this table.
439    pub columns: Vec<String>,
440    /// Referenced table.
441    pub references_table: String,
442    /// Referenced columns.
443    pub references_columns: Vec<String>,
444    /// ON DELETE action.
445    pub on_delete: Option<super::column_builder::ForeignKeyAction>,
446    /// ON UPDATE action.
447    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/// Drop foreign key operation.
457#[derive(Debug, Clone, PartialEq, Eq)]
458pub struct DropForeignKeyOp {
459    /// Table name.
460    pub table: String,
461    /// Constraint name.
462    pub name: String,
463}
464
465impl From<DropForeignKeyOp> for Operation {
466    fn from(op: DropForeignKeyOp) -> Self {
467        Self::DropForeignKey(op)
468    }
469}
470
471/// Raw SQL operation.
472#[derive(Debug, Clone, PartialEq, Eq)]
473pub struct RawSqlOp {
474    /// SQL to run for the up migration.
475    pub up_sql: String,
476    /// SQL to run for the down migration (if reversible).
477    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        // Create table can be reversed to drop table
532        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        // Rename table is reversible
546        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        // Add column can be reversed to drop column
557        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        // Drop table is NOT reversible (no schema info)
568        let drop = Operation::drop_table("users");
569        assert!(drop.reverse().is_none());
570    }
571
572    #[test]
573    fn test_raw_sql_reversibility() {
574        // Non-reversible raw SQL
575        let op = Operation::run_sql("INSERT INTO config VALUES ('key', 'value')");
576        assert!(!op.is_reversible());
577
578        // Reversible raw SQL
579        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}