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;
6
7/// All possible migration operations.
8#[derive(Debug, Clone, PartialEq)]
9pub enum Operation {
10    /// Create a new table.
11    CreateTable(CreateTableOp),
12    /// Drop an existing table.
13    DropTable(DropTableOp),
14    /// Rename a table.
15    RenameTable(RenameTableOp),
16    /// Add a column to an existing table.
17    AddColumn(AddColumnOp),
18    /// Drop a column from a table.
19    DropColumn(DropColumnOp),
20    /// Alter a column definition.
21    AlterColumn(AlterColumnOp),
22    /// Rename a column.
23    RenameColumn(RenameColumnOp),
24    /// Create an index.
25    CreateIndex(CreateIndexOp),
26    /// Drop an index.
27    DropIndex(DropIndexOp),
28    /// Add a foreign key constraint.
29    AddForeignKey(AddForeignKeyOp),
30    /// Drop a foreign key constraint.
31    DropForeignKey(DropForeignKeyOp),
32    /// Run raw SQL.
33    RunSql(RawSqlOp),
34}
35
36impl Operation {
37    /// Creates a drop table operation.
38    #[must_use]
39    pub fn drop_table(name: impl Into<String>) -> Self {
40        Self::DropTable(DropTableOp {
41            name: name.into(),
42            if_exists: false,
43            cascade: false,
44        })
45    }
46
47    /// Creates a drop table if exists operation.
48    #[must_use]
49    pub fn drop_table_if_exists(name: impl Into<String>) -> Self {
50        Self::DropTable(DropTableOp {
51            name: name.into(),
52            if_exists: true,
53            cascade: false,
54        })
55    }
56
57    /// Creates a rename table operation.
58    #[must_use]
59    pub fn rename_table(old_name: impl Into<String>, new_name: impl Into<String>) -> Self {
60        Self::RenameTable(RenameTableOp {
61            old_name: old_name.into(),
62            new_name: new_name.into(),
63        })
64    }
65
66    /// Creates an add column operation.
67    #[must_use]
68    pub fn add_column(table: impl Into<String>, column: ColumnDefinition) -> Self {
69        Self::AddColumn(AddColumnOp {
70            table: table.into(),
71            column,
72        })
73    }
74
75    /// Creates a drop column operation.
76    #[must_use]
77    pub fn drop_column(table: impl Into<String>, column: impl Into<String>) -> Self {
78        Self::DropColumn(DropColumnOp {
79            table: table.into(),
80            column: column.into(),
81        })
82    }
83
84    /// Creates a rename column operation.
85    #[must_use]
86    pub fn rename_column(
87        table: impl Into<String>,
88        old_name: impl Into<String>,
89        new_name: impl Into<String>,
90    ) -> Self {
91        Self::RenameColumn(RenameColumnOp {
92            table: table.into(),
93            old_name: old_name.into(),
94            new_name: new_name.into(),
95        })
96    }
97
98    /// Creates a raw SQL operation.
99    #[must_use]
100    pub fn run_sql(sql: impl Into<String>) -> Self {
101        Self::RunSql(RawSqlOp {
102            up_sql: sql.into(),
103            down_sql: None,
104        })
105    }
106
107    /// Creates a raw SQL operation with both up and down SQL.
108    #[must_use]
109    pub fn run_sql_reversible(up_sql: impl Into<String>, down_sql: impl Into<String>) -> Self {
110        Self::RunSql(RawSqlOp {
111            up_sql: up_sql.into(),
112            down_sql: Some(down_sql.into()),
113        })
114    }
115
116    /// Attempts to generate the reverse operation.
117    ///
118    /// Returns `None` if the operation is not reversible.
119    #[must_use]
120    pub fn reverse(&self) -> Option<Self> {
121        match self {
122            Self::CreateTable(op) => Some(Self::drop_table(&op.name)),
123            Self::DropTable(_) => None, // Cannot reverse without knowing the schema
124            Self::RenameTable(op) => {
125                Some(Self::rename_table(op.new_name.clone(), op.old_name.clone()))
126            }
127            Self::AddColumn(op) => Some(Self::drop_column(&op.table, &op.column.name)),
128            Self::DropColumn(_) => None, // Cannot reverse without knowing the column definition
129            Self::AlterColumn(_) => None, // Cannot reverse without knowing the old definition
130            Self::RenameColumn(op) => Some(Self::rename_column(
131                &op.table,
132                op.new_name.clone(),
133                op.old_name.clone(),
134            )),
135            Self::CreateIndex(op) => Some(Self::DropIndex(DropIndexOp {
136                name: op.name.clone(),
137                table: Some(op.table.clone()),
138                if_exists: false,
139            })),
140            Self::DropIndex(_) => None, // Cannot reverse without knowing the index definition
141            Self::AddForeignKey(op) => op.name.as_ref().map(|name| {
142                Self::DropForeignKey(DropForeignKeyOp {
143                    table: op.table.clone(),
144                    name: name.clone(),
145                })
146            }),
147            Self::DropForeignKey(_) => None, // Cannot reverse without knowing the FK definition
148            Self::RunSql(op) => op.down_sql.as_ref().map(|down| Self::run_sql(down.clone())),
149        }
150    }
151
152    /// Returns whether this operation is reversible.
153    #[must_use]
154    pub fn is_reversible(&self) -> bool {
155        self.reverse().is_some()
156    }
157}
158
159/// Create table operation.
160#[derive(Debug, Clone, PartialEq)]
161pub struct CreateTableOp {
162    /// Table name.
163    pub name: String,
164    /// Column definitions.
165    pub columns: Vec<ColumnDefinition>,
166    /// Table-level constraints.
167    pub constraints: Vec<TableConstraint>,
168    /// Whether to use IF NOT EXISTS.
169    pub if_not_exists: bool,
170}
171
172impl From<CreateTableOp> for Operation {
173    fn from(op: CreateTableOp) -> Self {
174        Self::CreateTable(op)
175    }
176}
177
178/// Table-level constraint.
179#[derive(Debug, Clone, PartialEq, Eq)]
180pub enum TableConstraint {
181    /// Primary key constraint on multiple columns.
182    PrimaryKey {
183        /// Optional constraint name.
184        name: Option<String>,
185        /// Column names.
186        columns: Vec<String>,
187    },
188    /// Unique constraint on multiple columns.
189    Unique {
190        /// Optional constraint name.
191        name: Option<String>,
192        /// Column names.
193        columns: Vec<String>,
194    },
195    /// Foreign key constraint.
196    ForeignKey {
197        /// Optional constraint name.
198        name: Option<String>,
199        /// Columns in this table.
200        columns: Vec<String>,
201        /// Referenced table.
202        references_table: String,
203        /// Referenced columns.
204        references_columns: Vec<String>,
205        /// ON DELETE action.
206        on_delete: Option<super::column_builder::ForeignKeyAction>,
207        /// ON UPDATE action.
208        on_update: Option<super::column_builder::ForeignKeyAction>,
209    },
210    /// Check constraint.
211    Check {
212        /// Optional constraint name.
213        name: Option<String>,
214        /// Check expression.
215        expression: String,
216    },
217}
218
219/// Drop table operation.
220#[derive(Debug, Clone, PartialEq, Eq)]
221pub struct DropTableOp {
222    /// Table name.
223    pub name: String,
224    /// Whether to use IF EXISTS.
225    pub if_exists: bool,
226    /// Whether to cascade.
227    pub cascade: bool,
228}
229
230impl From<DropTableOp> for Operation {
231    fn from(op: DropTableOp) -> Self {
232        Self::DropTable(op)
233    }
234}
235
236/// Rename table operation.
237#[derive(Debug, Clone, PartialEq, Eq)]
238pub struct RenameTableOp {
239    /// Current table name.
240    pub old_name: String,
241    /// New table name.
242    pub new_name: String,
243}
244
245impl From<RenameTableOp> for Operation {
246    fn from(op: RenameTableOp) -> Self {
247        Self::RenameTable(op)
248    }
249}
250
251/// Add column operation.
252#[derive(Debug, Clone, PartialEq)]
253pub struct AddColumnOp {
254    /// Table name.
255    pub table: String,
256    /// Column definition.
257    pub column: ColumnDefinition,
258}
259
260impl From<AddColumnOp> for Operation {
261    fn from(op: AddColumnOp) -> Self {
262        Self::AddColumn(op)
263    }
264}
265
266/// Drop column operation.
267#[derive(Debug, Clone, PartialEq, Eq)]
268pub struct DropColumnOp {
269    /// Table name.
270    pub table: String,
271    /// Column name.
272    pub column: String,
273}
274
275impl From<DropColumnOp> for Operation {
276    fn from(op: DropColumnOp) -> Self {
277        Self::DropColumn(op)
278    }
279}
280
281/// Column alteration type.
282#[derive(Debug, Clone, PartialEq)]
283pub enum AlterColumnChange {
284    /// Change the data type.
285    SetDataType(crate::ast::DataType),
286    /// Set or remove NOT NULL constraint.
287    SetNullable(bool),
288    /// Set a new default value.
289    SetDefault(super::column_builder::DefaultValue),
290    /// Remove the default value.
291    DropDefault,
292}
293
294/// Alter column operation.
295#[derive(Debug, Clone, PartialEq)]
296pub struct AlterColumnOp {
297    /// Table name.
298    pub table: String,
299    /// Column name.
300    pub column: String,
301    /// The change to apply.
302    pub change: AlterColumnChange,
303}
304
305impl From<AlterColumnOp> for Operation {
306    fn from(op: AlterColumnOp) -> Self {
307        Self::AlterColumn(op)
308    }
309}
310
311/// Rename column operation.
312#[derive(Debug, Clone, PartialEq, Eq)]
313pub struct RenameColumnOp {
314    /// Table name.
315    pub table: String,
316    /// Current column name.
317    pub old_name: String,
318    /// New column name.
319    pub new_name: String,
320}
321
322impl From<RenameColumnOp> for Operation {
323    fn from(op: RenameColumnOp) -> Self {
324        Self::RenameColumn(op)
325    }
326}
327
328/// Index type.
329#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
330pub enum IndexType {
331    /// B-tree index (default).
332    #[default]
333    BTree,
334    /// Hash index.
335    Hash,
336    /// GiST index (PostgreSQL).
337    Gist,
338    /// GIN index (PostgreSQL).
339    Gin,
340}
341
342/// Create index operation.
343#[derive(Debug, Clone, PartialEq, Eq)]
344pub struct CreateIndexOp {
345    /// Index name.
346    pub name: String,
347    /// Table name.
348    pub table: String,
349    /// Columns to index.
350    pub columns: Vec<String>,
351    /// Whether this is a unique index.
352    pub unique: bool,
353    /// Index type.
354    pub index_type: IndexType,
355    /// Whether to use IF NOT EXISTS.
356    pub if_not_exists: bool,
357    /// Partial index condition (WHERE clause).
358    pub condition: Option<String>,
359}
360
361impl From<CreateIndexOp> for Operation {
362    fn from(op: CreateIndexOp) -> Self {
363        Self::CreateIndex(op)
364    }
365}
366
367/// Drop index operation.
368#[derive(Debug, Clone, PartialEq, Eq)]
369pub struct DropIndexOp {
370    /// Index name.
371    pub name: String,
372    /// Table name (required for some dialects).
373    pub table: Option<String>,
374    /// Whether to use IF EXISTS.
375    pub if_exists: bool,
376}
377
378impl From<DropIndexOp> for Operation {
379    fn from(op: DropIndexOp) -> Self {
380        Self::DropIndex(op)
381    }
382}
383
384/// Add foreign key operation.
385#[derive(Debug, Clone, PartialEq, Eq)]
386pub struct AddForeignKeyOp {
387    /// Table name.
388    pub table: String,
389    /// Optional constraint name.
390    pub name: Option<String>,
391    /// Columns in this table.
392    pub columns: Vec<String>,
393    /// Referenced table.
394    pub references_table: String,
395    /// Referenced columns.
396    pub references_columns: Vec<String>,
397    /// ON DELETE action.
398    pub on_delete: Option<super::column_builder::ForeignKeyAction>,
399    /// ON UPDATE action.
400    pub on_update: Option<super::column_builder::ForeignKeyAction>,
401}
402
403impl From<AddForeignKeyOp> for Operation {
404    fn from(op: AddForeignKeyOp) -> Self {
405        Self::AddForeignKey(op)
406    }
407}
408
409/// Drop foreign key operation.
410#[derive(Debug, Clone, PartialEq, Eq)]
411pub struct DropForeignKeyOp {
412    /// Table name.
413    pub table: String,
414    /// Constraint name.
415    pub name: String,
416}
417
418impl From<DropForeignKeyOp> for Operation {
419    fn from(op: DropForeignKeyOp) -> Self {
420        Self::DropForeignKey(op)
421    }
422}
423
424/// Raw SQL operation.
425#[derive(Debug, Clone, PartialEq, Eq)]
426pub struct RawSqlOp {
427    /// SQL to run for the up migration.
428    pub up_sql: String,
429    /// SQL to run for the down migration (if reversible).
430    pub down_sql: Option<String>,
431}
432
433impl From<RawSqlOp> for Operation {
434    fn from(op: RawSqlOp) -> Self {
435        Self::RunSql(op)
436    }
437}
438
439#[cfg(test)]
440mod tests {
441    use super::*;
442    use crate::migrations::column_builder::{bigint, varchar, ForeignKeyAction};
443
444    #[test]
445    fn test_drop_table_operation() {
446        let op = Operation::drop_table("users");
447        match op {
448            Operation::DropTable(drop) => {
449                assert_eq!(drop.name, "users");
450                assert!(!drop.if_exists);
451                assert!(!drop.cascade);
452            }
453            _ => panic!("Expected DropTable operation"),
454        }
455    }
456
457    #[test]
458    fn test_rename_table_operation() {
459        let op = Operation::rename_table("old_name", "new_name");
460        match op {
461            Operation::RenameTable(rename) => {
462                assert_eq!(rename.old_name, "old_name");
463                assert_eq!(rename.new_name, "new_name");
464            }
465            _ => panic!("Expected RenameTable operation"),
466        }
467    }
468
469    #[test]
470    fn test_add_column_operation() {
471        let col = varchar("email", 255).not_null().build();
472        let op = Operation::add_column("users", col);
473        match op {
474            Operation::AddColumn(add) => {
475                assert_eq!(add.table, "users");
476                assert_eq!(add.column.name, "email");
477            }
478            _ => panic!("Expected AddColumn operation"),
479        }
480    }
481
482    #[test]
483    fn test_reverse_operations() {
484        // Create table can be reversed to drop table
485        let create = CreateTableOp {
486            name: "users".to_string(),
487            columns: vec![bigint("id").primary_key().build()],
488            constraints: vec![],
489            if_not_exists: false,
490        };
491        let op = Operation::CreateTable(create);
492        let reversed = op.reverse().expect("Should be reversible");
493        match reversed {
494            Operation::DropTable(drop) => assert_eq!(drop.name, "users"),
495            _ => panic!("Expected DropTable"),
496        }
497
498        // Rename table is reversible
499        let rename = Operation::rename_table("old", "new");
500        let reversed = rename.reverse().expect("Should be reversible");
501        match reversed {
502            Operation::RenameTable(r) => {
503                assert_eq!(r.old_name, "new");
504                assert_eq!(r.new_name, "old");
505            }
506            _ => panic!("Expected RenameTable"),
507        }
508
509        // Add column can be reversed to drop column
510        let add = Operation::add_column("users", varchar("email", 255).build());
511        let reversed = add.reverse().expect("Should be reversible");
512        match reversed {
513            Operation::DropColumn(drop) => {
514                assert_eq!(drop.table, "users");
515                assert_eq!(drop.column, "email");
516            }
517            _ => panic!("Expected DropColumn"),
518        }
519
520        // Drop table is NOT reversible (no schema info)
521        let drop = Operation::drop_table("users");
522        assert!(drop.reverse().is_none());
523    }
524
525    #[test]
526    fn test_raw_sql_reversibility() {
527        // Non-reversible raw SQL
528        let op = Operation::run_sql("INSERT INTO config VALUES ('key', 'value')");
529        assert!(!op.is_reversible());
530
531        // Reversible raw SQL
532        let op = Operation::run_sql_reversible(
533            "INSERT INTO config VALUES ('key', 'value')",
534            "DELETE FROM config WHERE key = 'key'",
535        );
536        assert!(op.is_reversible());
537    }
538
539    #[test]
540    fn test_table_constraint() {
541        let pk = TableConstraint::PrimaryKey {
542            name: Some("pk_users".to_string()),
543            columns: vec!["id".to_string()],
544        };
545        match pk {
546            TableConstraint::PrimaryKey { name, columns } => {
547                assert_eq!(name, Some("pk_users".to_string()));
548                assert_eq!(columns, vec!["id"]);
549            }
550            _ => panic!("Expected PrimaryKey"),
551        }
552
553        let fk = TableConstraint::ForeignKey {
554            name: Some("fk_user_company".to_string()),
555            columns: vec!["company_id".to_string()],
556            references_table: "companies".to_string(),
557            references_columns: vec!["id".to_string()],
558            on_delete: Some(ForeignKeyAction::Cascade),
559            on_update: None,
560        };
561        match fk {
562            TableConstraint::ForeignKey {
563                references_table,
564                on_delete,
565                ..
566            } => {
567                assert_eq!(references_table, "companies");
568                assert_eq!(on_delete, Some(ForeignKeyAction::Cascade));
569            }
570            _ => panic!("Expected ForeignKey"),
571        }
572    }
573}