1use clap::{Parser, Subcommand};
39
40#[derive(Parser, Debug)]
42#[command(name = "migratio-runner")]
43#[command(about = "Database migration runner")]
44pub struct CliArgs {
45 #[command(subcommand)]
46 pub command: Commands,
47}
48
49#[derive(Subcommand, Debug)]
51pub enum Commands {
52 Status,
54 Upgrade {
56 #[arg(long)]
58 to: Option<u32>,
59 },
60 Downgrade {
62 #[arg(long)]
64 to: u32,
65 },
66 History,
68 Preview,
70 List,
72}
73
74#[cfg(feature = "sqlite")]
75pub use sqlite::run_sqlite;
76
77#[cfg(feature = "sqlite")]
78mod sqlite {
79 use super::{CliArgs, Commands};
80 use migratio::sqlite::SqliteMigrator;
81 use rusqlite::Connection;
82
83 pub fn run_sqlite(
97 migrator: SqliteMigrator,
98 database_url: &str,
99 args: CliArgs,
100 ) -> Result<(), Box<dyn std::error::Error>> {
101 if !std::path::Path::new(database_url).exists() {
104 return Err(format!(
105 "Database file not found: {}\n\nTo create a new database, first create the file manually or use your application's initialization logic.",
106 database_url
107 ).into());
108 }
109
110 let mut conn = Connection::open(database_url)?;
111
112 match args.command {
113 Commands::Status => {
114 let version = migrator.get_current_version(&mut conn)?;
115 let pending = migrator.preview_upgrade(&mut conn)?;
116 println!("Current version: {}", version);
117 println!("Pending migrations: {}", pending.len());
118 for m in pending {
119 println!(" - {} (v{})", m.name(), m.version());
120 }
121 }
122 Commands::Upgrade { to } => {
123 let report = match to {
124 Some(target) => migrator.upgrade_to(&mut conn, target)?,
125 None => migrator.upgrade(&mut conn)?,
126 };
127 if report.migrations_run.is_empty() {
128 println!("No migrations to run.");
129 } else {
130 println!("Migrations run: {:?}", report.migrations_run);
131 }
132 }
133 Commands::Downgrade { to } => {
134 let report = migrator.downgrade(&mut conn, to)?;
135 if report.migrations_run.is_empty() {
136 println!("No migrations to roll back.");
137 } else {
138 println!("Migrations rolled back: {:?}", report.migrations_run);
139 }
140 }
141 Commands::History => {
142 let history = migrator.get_migration_history(&mut conn)?;
143 if history.is_empty() {
144 println!("No migrations have been applied yet.");
145 } else {
146 println!("Migration history:");
147 for entry in history {
148 println!(
149 " v{}: {} [{}] (applied {})",
150 entry.version, entry.name, entry.migration_type, entry.applied_at
151 );
152 }
153 }
154 }
155 Commands::Preview => {
156 let pending = migrator.preview_upgrade(&mut conn)?;
157 if pending.is_empty() {
158 println!("No pending migrations.");
159 } else {
160 println!("Pending migrations:");
161 for m in pending {
162 println!(" - {} (v{})", m.name(), m.version());
163 if let Some(desc) = m.description() {
164 println!(" {}", desc);
165 }
166 }
167 }
168 }
169 Commands::List => {
170 unreachable!("List command should be handled before run_sqlite");
173 }
174 }
175
176 Ok(())
177 }
178}
179
180#[cfg(feature = "mysql")]
181pub use mysql_support::run_mysql;
182
183#[cfg(feature = "postgres")]
184pub use postgres_support::run_postgres;
185
186#[cfg(feature = "mysql")]
187mod mysql_support {
188 use super::{CliArgs, Commands};
189 use migratio::mysql::MysqlMigrator;
190 use mysql::{Conn, Opts};
191
192 pub fn run_mysql(
206 migrator: MysqlMigrator,
207 database_url: &str,
208 args: CliArgs,
209 ) -> Result<(), Box<dyn std::error::Error>> {
210 let opts = Opts::from_url(database_url)?;
211 let mut conn = Conn::new(opts)?;
212
213 match args.command {
214 Commands::Status => {
215 let version = migrator.get_current_version(&mut conn)?;
216 let pending = migrator.preview_upgrade(&mut conn)?;
217 println!("Current version: {}", version);
218 println!("Pending migrations: {}", pending.len());
219 for m in pending {
220 println!(" - {} (v{})", m.name(), m.version());
221 }
222 }
223 Commands::Upgrade { to } => {
224 let report = match to {
225 Some(target) => migrator.upgrade_to(&mut conn, target)?,
226 None => migrator.upgrade(&mut conn)?,
227 };
228 if report.migrations_run.is_empty() {
229 println!("No migrations to run.");
230 } else {
231 println!("Migrations run: {:?}", report.migrations_run);
232 }
233 }
234 Commands::Downgrade { to } => {
235 let report = migrator.downgrade(&mut conn, to)?;
236 if report.migrations_run.is_empty() {
237 println!("No migrations to roll back.");
238 } else {
239 println!("Migrations rolled back: {:?}", report.migrations_run);
240 }
241 }
242 Commands::History => {
243 let history = migrator.get_migration_history(&mut conn)?;
244 if history.is_empty() {
245 println!("No migrations have been applied yet.");
246 } else {
247 println!("Migration history:");
248 for entry in history {
249 println!(
250 " v{}: {} [{}] (applied {})",
251 entry.version, entry.name, entry.migration_type, entry.applied_at
252 );
253 }
254 }
255 }
256 Commands::Preview => {
257 let pending = migrator.preview_upgrade(&mut conn)?;
258 if pending.is_empty() {
259 println!("No pending migrations.");
260 } else {
261 println!("Pending migrations:");
262 for m in pending {
263 println!(" - {} (v{})", m.name(), m.version());
264 if let Some(desc) = m.description() {
265 println!(" {}", desc);
266 }
267 }
268 }
269 }
270 Commands::List => {
271 unreachable!("List command should be handled before run_mysql");
274 }
275 }
276
277 Ok(())
278 }
279}
280
281#[cfg(feature = "postgres")]
282mod postgres_support {
283 use super::{CliArgs, Commands};
284 use migratio::postgres::PostgresMigrator;
285 use postgres::{Client, NoTls};
286
287 pub fn run_postgres(
301 migrator: PostgresMigrator,
302 database_url: &str,
303 args: CliArgs,
304 ) -> Result<(), Box<dyn std::error::Error>> {
305 let mut client = Client::connect(database_url, NoTls)?;
306
307 match args.command {
308 Commands::Status => {
309 let version = migrator.get_current_version(&mut client)?;
310 let pending = migrator.preview_upgrade(&mut client)?;
311 println!("Current version: {}", version);
312 println!("Pending migrations: {}", pending.len());
313 for m in pending {
314 println!(" - {} (v{})", m.name(), m.version());
315 }
316 }
317 Commands::Upgrade { to } => {
318 let report = match to {
319 Some(target) => migrator.upgrade_to(&mut client, target)?,
320 None => migrator.upgrade(&mut client)?,
321 };
322 if report.migrations_run.is_empty() {
323 println!("No migrations to run.");
324 } else {
325 println!("Migrations run: {:?}", report.migrations_run);
326 }
327 }
328 Commands::Downgrade { to } => {
329 let report = migrator.downgrade(&mut client, to)?;
330 if report.migrations_run.is_empty() {
331 println!("No migrations to roll back.");
332 } else {
333 println!("Migrations rolled back: {:?}", report.migrations_run);
334 }
335 }
336 Commands::History => {
337 let history = migrator.get_migration_history(&mut client)?;
338 if history.is_empty() {
339 println!("No migrations have been applied yet.");
340 } else {
341 println!("Migration history:");
342 for entry in history {
343 println!(
344 " v{}: {} [{}] (applied {})",
345 entry.version, entry.name, entry.migration_type, entry.applied_at
346 );
347 }
348 }
349 }
350 Commands::Preview => {
351 let pending = migrator.preview_upgrade(&mut client)?;
352 if pending.is_empty() {
353 println!("No pending migrations.");
354 } else {
355 println!("Pending migrations:");
356 for m in pending {
357 println!(" - {} (v{})", m.name(), m.version());
358 if let Some(desc) = m.description() {
359 println!(" {}", desc);
360 }
361 }
362 }
363 }
364 Commands::List => {
365 unreachable!("List command should be handled before run_postgres");
368 }
369 }
370
371 Ok(())
372 }
373}