1use std::cmp::Ordering;
2use std::path::Path;
3
4use anyhow::{anyhow, Context};
5use rorm_db::executor::{Executor, Nothing, Optional};
6use rorm_db::sql::create_table::CreateTable;
7use rorm_db::transaction::{Transaction, TransactionError};
8use rorm_db::Database;
9use rorm_declaration::config::DatabaseConfig;
10use rorm_declaration::imr::{Annotation, DbType};
11use tracing::{error, info};
12
13use crate::migrate::apply::apply_migration;
14use crate::migrate::config::{create_db_config, deserialize_db_conf};
15pub use crate::utils::migrations::get_existing_migrations;
16
17pub mod apply;
18pub mod config;
19
20pub struct MigrateOptions {
22 pub migration_dir: String,
24
25 pub database_config: String,
27
28 pub apply_until: Option<u16>,
30}
31
32pub async fn run_migrate_custom(
34 db_conf: DatabaseConfig,
35 migration_dir: String,
36 apply_until: Option<u16>,
37) -> anyhow::Result<()> {
38 let p = Path::new(migration_dir.as_str());
39 if !p.exists() || p.is_file() {
40 error!(
41 "Couldn't find the migration directory in {} \n\n\
42 You can specify an alternative path with --migration-dir <PATH>",
43 migration_dir.as_str()
44 );
45 return Ok(());
46 }
47
48 let existing_migrations = get_existing_migrations(migration_dir.as_str())
49 .with_context(|| "Couldn't retrieve existing migrations")?;
50
51 if existing_migrations.is_empty() {
52 info!("No migrations found.\nExiting.");
53 return Ok(());
54 }
55
56 let db = Database::connect(rorm_db::DatabaseConfiguration {
57 driver: db_conf.driver,
58 min_connections: 1,
59 max_connections: 1,
60 })
61 .await?;
62
63 let last_migration_table_name = db_conf
64 .last_migration_table_name
65 .as_ref()
66 .map_or("_rorm__last_migration", |x| x.as_str());
67
68 let mut tx = db
69 .start_transaction()
70 .await
71 .with_context(|| "Could not create transaction")?;
72
73 create_last_migration_table(&mut tx, last_migration_table_name)
74 .await
75 .with_context(|| "Couldn't create internal last migration table")?;
76
77 tx.commit()
78 .await
79 .map_err(|x| match x {
80 TransactionError::Database(x) => x,
81 TransactionError::Hook(_) => unreachable!("rorm-cli does not use hooks"),
82 })
83 .with_context(|| "Couldn't create internal last migration table")?;
84
85 let last_migration: Option<i32> = db
86 .execute::<Optional>(
87 format!(
88 "SELECT migration_id FROM {} ORDER BY id DESC LIMIT 1;",
89 &last_migration_table_name
90 ),
91 Vec::new(),
92 )
93 .await
94 .and_then(|option| option.map(|row| row.get(0)).transpose().map_err(Into::into))
95 .with_context(|| {
96 "Couldn't fetch information about successful migrations from migration table"
97 })?;
98
99 match last_migration {
100 None => {
101 for migration in &existing_migrations {
103 apply_migration(&db, migration, last_migration_table_name).await?;
104 info!("Applied migration {:04}_{}", migration.id, migration.name);
105
106 if let Some(apply_until) = apply_until {
107 if migration.id == apply_until {
108 info!(
109 "Applied all migrations until (inclusive) migration {apply_until:04}"
110 );
111 break;
112 }
113 }
114 }
115 }
116 Some(id) => {
117 let id = id as u16;
118 if existing_migrations.iter().any(|x| x.id == id) {
120 let mut apply = false;
121 for (idx, migration) in existing_migrations.iter().enumerate() {
122 if apply {
123 apply_migration(&db, migration, last_migration_table_name).await?;
124 info!("Applied migration {:04}_{}", migration.id, migration.name);
125 continue;
126 }
127
128 if migration.id == id {
129 apply = true;
130
131 if idx == existing_migrations.len() - 1 {
132 info!("All migration have already been applied.");
133 }
134 }
135
136 if let Some(apply_until) = apply_until {
137 match migration.id.cmp(&apply_until) {
138 Ordering::Equal => {
139 if apply {
140 info!(
141 "Applied all migrations until (inclusive) migration {apply_until:04}"
142 );
143 } else {
144 info!(
145 "All migrations until (inclusive) migration {apply_until:04} have already been applied"
146 );
147 }
148 break;
149 }
150 Ordering::Greater => break,
151 Ordering::Less => {}
152 }
153 }
154 }
155 } else {
156 return Err(anyhow!(
159 r#"Last applied migration {id} was not found in current migrations.
160
161Can not proceed any further without damaging data.
162To correct, empty the {last_migration_table_name} table or reset the whole database."#,
163 ));
164 }
165 }
166 }
167
168 db.close().await;
169 Ok(())
170}
171
172pub async fn run_migrate(options: MigrateOptions) -> anyhow::Result<()> {
174 let db_conf_path = Path::new(options.database_config.as_str());
175
176 if !&db_conf_path.exists() {
177 error!(
178 "Couldn't find the database configuration file, created {} and exiting",
179 options.database_config.as_str()
180 );
181 create_db_config(db_conf_path)?;
182 return Ok(());
183 }
184
185 let db_conf = deserialize_db_conf(db_conf_path)?;
186
187 run_migrate_custom(db_conf, options.migration_dir, options.apply_until).await
188}
189
190pub async fn create_last_migration_table(
209 tx: &mut Transaction,
210 table_name: &str,
211) -> Result<(), rorm_db::Error> {
212 let db_impl = tx.dialect();
213 let statements = db_impl
214 .create_table(table_name)
215 .add_column(db_impl.create_column(
216 table_name,
217 "id",
218 DbType::Int64,
219 &[Annotation::PrimaryKey, Annotation::AutoIncrement],
220 ))
221 .add_column(db_impl.create_column(
222 table_name,
223 "updated_at",
224 DbType::DateTime,
225 &[Annotation::AutoUpdateTime],
226 ))
227 .add_column(db_impl.create_column(
228 table_name,
229 "migration_id",
230 DbType::Int32,
231 &[Annotation::NotNull],
232 ))
233 .if_not_exists()
234 .build()?;
235
236 for (query_string, bind_params) in statements {
237 tx.execute::<Nothing>(query_string, bind_params).await?;
238 }
239
240 Ok(())
241}