Skip to main content

sea_orm_migration/
cli.rs

1use std::future::Future;
2
3use clap::Parser;
4use dotenvy::dotenv;
5use std::{error::Error, fmt::Display, process::exit};
6use tracing_subscriber::{EnvFilter, prelude::*};
7
8use sea_orm::{ConnectOptions, Database, DbConn, DbErr};
9use sea_orm_cli::{MigrateSubcommands, run_migrate_generate, run_migrate_init};
10
11use super::MigratorTraitSelf;
12
13const MIGRATION_DIR: &str = "./";
14
15pub async fn run_cli<M>(migrator: M)
16where
17    M: MigratorTraitSelf,
18{
19    run_cli_with_connection(migrator, Database::connect).await;
20}
21
22/// Same as [`run_cli`] where you provide the function to create the [`DbConn`].
23///
24/// This allows configuring the database connection as you see fit.
25/// E.g. you can change settings in [`ConnectOptions`] or you can load sqlite
26/// extensions.
27pub async fn run_cli_with_connection<M, F, Fut>(migrator: M, make_connection: F)
28where
29    M: MigratorTraitSelf,
30    F: FnOnce(ConnectOptions) -> Fut,
31    Fut: Future<Output = Result<DbConn, DbErr>>,
32{
33    dotenv().ok();
34    let cli = Cli::parse();
35
36    run_cli_with_connection_inner(migrator, cli, async move |cli| {
37        let url = cli
38            .database_url
39            .clone()
40            .expect("Environment variable 'DATABASE_URL' not set");
41        let schema = cli
42            .database_schema
43            .clone()
44            .unwrap_or_else(|| "public".to_owned());
45
46        let connect_options = ConnectOptions::new(url)
47            .set_schema_search_path(schema)
48            .to_owned();
49
50        make_connection(connect_options).await
51    })
52    .await
53    .unwrap_or_else(handle_error);
54}
55
56/// Same as [`run_cli`] where you provide a fully customized way to create the
57/// [`DbConn`].
58///
59/// This allows loading database configuration from sources other than CLI
60/// arguments or environment variables.
61pub async fn run_cli_with_custom_connection(
62    migrator: impl MigratorTraitSelf,
63    make_connection: impl AsyncFnOnce() -> Result<DbConn, DbErr>,
64) {
65    dotenv().ok();
66    let cli = Cli::parse();
67
68    run_cli_with_connection_inner(migrator, cli, async |_| make_connection().await)
69        .await
70        .unwrap_or_else(handle_error);
71}
72
73pub async fn run_migrate<M>(
74    migrator: M,
75    db: &DbConn,
76    command: Option<MigrateSubcommands>,
77    verbose: bool,
78) -> Result<(), Box<dyn Error>>
79where
80    M: MigratorTraitSelf,
81{
82    setup_tracing(verbose);
83    run_migrate_inner(migrator, db, command).await
84}
85
86async fn run_cli_with_connection_inner(
87    migrator: impl MigratorTraitSelf,
88    cli: Cli,
89    make_connection: impl AsyncFnOnce(&Cli) -> Result<DbConn, DbErr>,
90) -> Result<(), Box<dyn Error>> {
91    setup_tracing(cli.verbose);
92
93    if run_non_db_command(cli.command.as_ref())? {
94        return Ok(());
95    }
96
97    let db = make_connection(&cli)
98        .await
99        .expect("Fail to acquire database connection");
100    let command = cli.command;
101    run_migrate_inner(migrator, &db, command).await?;
102
103    Ok(())
104}
105
106async fn run_migrate_inner<M>(
107    migrator: M,
108    db: &DbConn,
109    command: Option<MigrateSubcommands>,
110) -> Result<(), Box<dyn Error>>
111where
112    M: MigratorTraitSelf,
113{
114    if run_non_db_command(command.as_ref())? {
115        return Ok(());
116    }
117
118    match command {
119        Some(MigrateSubcommands::Fresh) => migrator.fresh(db).await?,
120        Some(MigrateSubcommands::Refresh) => migrator.refresh(db).await?,
121        Some(MigrateSubcommands::Reset) => migrator.reset(db).await?,
122        Some(MigrateSubcommands::Status) => migrator.status(db).await?,
123        Some(MigrateSubcommands::Up { num }) => migrator.up(db, num).await?,
124        Some(MigrateSubcommands::Down { num }) => migrator.down(db, Some(num)).await?,
125        _ => migrator.up(db, None).await?,
126    };
127
128    Ok(())
129}
130
131fn run_non_db_command(command: Option<&MigrateSubcommands>) -> Result<bool, Box<dyn Error>> {
132    match command {
133        Some(MigrateSubcommands::Init) => {
134            run_migrate_init(MIGRATION_DIR)?;
135            Ok(true)
136        }
137        Some(MigrateSubcommands::Generate {
138            migration_name,
139            universal_time: _,
140            local_time,
141        }) => {
142            run_migrate_generate(MIGRATION_DIR, migration_name, !*local_time)?;
143            Ok(true)
144        }
145        _ => Ok(false),
146    }
147}
148
149fn setup_tracing(verbose: bool) {
150    let filter = match verbose {
151        true => "debug",
152        false => "sea_orm_migration=info",
153    };
154
155    let filter_layer = EnvFilter::try_new(filter).unwrap();
156
157    if verbose {
158        let fmt_layer = tracing_subscriber::fmt::layer();
159        tracing_subscriber::registry()
160            .with(filter_layer)
161            .with(fmt_layer)
162            .init()
163    } else {
164        let fmt_layer = tracing_subscriber::fmt::layer()
165            .with_target(false)
166            .with_level(false)
167            .without_time();
168        tracing_subscriber::registry()
169            .with(filter_layer)
170            .with(fmt_layer)
171            .init()
172    };
173}
174
175#[derive(Parser)]
176#[command(version)]
177pub struct Cli {
178    #[arg(short = 'v', long, global = true, help = "Show debug messages")]
179    verbose: bool,
180
181    #[arg(
182        global = true,
183        short = 's',
184        long,
185        env = "DATABASE_SCHEMA",
186        long_help = "Database schema\n \
187                    - For MySQL and SQLite, this argument is ignored.\n \
188                    - For PostgreSQL, this argument is optional with default value 'public'.\n"
189    )]
190    database_schema: Option<String>,
191
192    #[arg(
193        global = true,
194        short = 'u',
195        long,
196        env = "DATABASE_URL",
197        help = "Database URL"
198    )]
199    database_url: Option<String>,
200
201    #[command(subcommand)]
202    command: Option<MigrateSubcommands>,
203}
204
205fn handle_error<E>(error: E)
206where
207    E: Display,
208{
209    eprintln!("{error}");
210    exit(1);
211}