Skip to main content

rorm_cli/migrate/
apply.rs

1//! Contains two functions to apply a single migration or a single operation
2
3use rorm_db::executor::{Executor, Nothing};
4use rorm_db::sql::alter_table::{AlterTable, AlterTableOperation};
5use rorm_db::sql::create_table::CreateTable;
6use rorm_db::sql::drop_table::DropTable;
7use rorm_db::sql::insert::Insert;
8use rorm_db::sql::value::Value;
9use rorm_db::sql::DBImpl;
10use rorm_db::transaction::{Transaction, TransactionError};
11use rorm_db::Database;
12use rorm_declaration::migration::{Migration, Operation};
13use thiserror::Error;
14
15/// Applies a single `Migration`, updating the "last migration table".
16///
17/// This function will start a transaction
18/// which is rolled back if any of the migration's operations failed.
19///
20/// This function won't check the databases current state.
21/// It will simply try to apply the migration.
22pub async fn apply_migration(
23    db: &Database,
24    migration: &Migration,
25    last_migration_table_name: &str,
26) -> Result<(), ApplyMigrationError> {
27    let mut tx = db
28        .start_transaction()
29        .await
30        .map_err(|error| ApplyMigrationError {
31            error,
32            location: ApplyMigrationErrorLocation::StartTransaction,
33        })?;
34
35    for (index, operation) in migration.operations.iter().enumerate() {
36        apply_operation(&mut tx, operation)
37            .await
38            .map_err(|error| ApplyMigrationError {
39                error,
40                location: ApplyMigrationErrorLocation::ApplyOperation(index),
41            })?;
42    }
43
44    let (query_string, bind_params) = db
45        .dialect()
46        .insert(
47            last_migration_table_name,
48            &["migration_id"],
49            &[&[Value::I32(migration.id as i32)]],
50            None,
51        )
52        .rollback_transaction()
53        .build();
54
55    tx.execute::<Nothing>(query_string, bind_params)
56        .await
57        .map_err(|error| ApplyMigrationError {
58            error,
59            location: ApplyMigrationErrorLocation::UpdateLastMigration,
60        })?;
61
62    tx.commit().await.map_err(|x| ApplyMigrationError {
63        error: match x {
64            TransactionError::Database(x) => x,
65            TransactionError::Hook(_) => unreachable!("rorm-cli does not use hooks"),
66        },
67        location: ApplyMigrationErrorLocation::CommitTransaction,
68    })?;
69
70    Ok(())
71}
72
73/// Error returned by [`apply_migration`].
74///
75/// It is the raw `error` returned by the database
76/// with an additional `location` indicating where in `apply_migration`
77/// the error occurred.
78#[derive(Debug, Error)]
79#[error("{location}: {error}")]
80pub struct ApplyMigrationError {
81    /// Error returned by the database
82    #[source]
83    pub error: rorm_db::Error,
84
85    /// Location where the `error` occurred
86    pub location: ApplyMigrationErrorLocation,
87}
88
89/// Location where an [`ApplyMigrationError`] occurred.
90#[derive(Debug, Error)]
91pub enum ApplyMigrationErrorLocation {
92    /// The error occurred while starting the transaction
93    #[error("Failed to start transaction")]
94    StartTransaction,
95
96    /// The error occurred while applying an operation
97    #[error("Failed to apply operation {}", .0)]
98    ApplyOperation(usize),
99
100    /// The error occurred while updating the "last migration table"
101    #[error("Failed to update last migration")]
102    UpdateLastMigration,
103
104    /// The error occurred while commiting the transaction
105    #[error("Failed to commit transaction")]
106    CommitTransaction,
107}
108
109/// Applies a single migration `Operation`
110pub async fn apply_operation(
111    tx: &mut Transaction,
112    operation: &Operation,
113) -> Result<(), rorm_db::Error> {
114    let db_impl = tx.dialect();
115
116    match operation {
117        Operation::CreateModel { name, fields } => {
118            let mut create_table = db_impl.create_table(name.as_str());
119
120            for field in fields {
121                create_table = create_table.add_column(db_impl.create_column(
122                    name.as_str(),
123                    field.name.as_str(),
124                    field.db_type,
125                    &field.annotations,
126                ));
127            }
128
129            let statements = create_table.build()?;
130
131            for (query_string, query_bind_params) in statements {
132                tx.execute::<Nothing>(query_string, query_bind_params)
133                    .await?;
134            }
135        }
136        Operation::RenameModel { old, new } => {
137            let statements = db_impl
138                .alter_table(
139                    old.as_str(),
140                    AlterTableOperation::RenameTo {
141                        name: new.to_string(),
142                    },
143                )
144                .build()?;
145
146            for (query_string, query_bind_params) in statements {
147                tx.execute::<Nothing>(query_string, query_bind_params)
148                    .await?;
149            }
150        }
151        Operation::DeleteModel { name } => {
152            let query_string = db_impl.drop_table(name.as_str()).build();
153
154            tx.execute::<Nothing>(query_string, Vec::new()).await?;
155        }
156        Operation::CreateField { model, field } => {
157            let statements = db_impl
158                .alter_table(
159                    model.as_str(),
160                    AlterTableOperation::AddColumn {
161                        operation: db_impl.create_column(
162                            model.as_str(),
163                            field.name.as_str(),
164                            field.db_type,
165                            &field.annotations,
166                        ),
167                    },
168                )
169                .build()?;
170
171            for (query_string, query_bind_params) in statements {
172                tx.execute::<Nothing>(query_string, query_bind_params)
173                    .await?;
174            }
175        }
176        Operation::RenameField {
177            table_name,
178            old,
179            new,
180        } => {
181            let statements = db_impl
182                .alter_table(
183                    table_name.as_str(),
184                    AlterTableOperation::RenameColumnTo {
185                        column_name: old.to_string(),
186                        new_column_name: new.to_string(),
187                    },
188                )
189                .build()?;
190
191            for (query_string, query_bind_params) in statements {
192                tx.execute::<Nothing>(query_string, query_bind_params)
193                    .await?;
194            }
195        }
196        Operation::DeleteField { model, name } => {
197            let statements = db_impl
198                .alter_table(
199                    model.as_str(),
200                    AlterTableOperation::DropColumn { name: name.clone() },
201                )
202                .build()?;
203
204            for (query_string, query_bind_params) in statements {
205                tx.execute::<Nothing>(query_string, query_bind_params)
206                    .await?;
207            }
208        }
209        #[allow(unused_variables)]
210        Operation::RawSQL {
211            mysql,
212            postgres,
213            sqlite,
214            ..
215        } => match db_impl {
216            #[cfg(feature = "sqlite")]
217            DBImpl::SQLite => tx.execute::<Nothing>(sqlite.clone(), Vec::new()).await?,
218            #[cfg(feature = "postgres")]
219            DBImpl::Postgres => tx.execute::<Nothing>(postgres.clone(), Vec::new()).await?,
220        },
221    }
222
223    Ok(())
224}