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