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