Skip to main content

vitrail_pg/
cli.rs

1use std::error::Error;
2use std::marker::PhantomData;
3use std::path::PathBuf;
4
5use clap::{Args, Parser, Subcommand};
6use vitrail_pg_core::{PostgresMigrator, SchemaAccess};
7
8#[derive(Debug)]
9pub struct VitrailCli<S> {
10    _schema: PhantomData<S>,
11}
12
13impl<S> Default for VitrailCli<S> {
14    fn default() -> Self {
15        Self {
16            _schema: PhantomData,
17        }
18    }
19}
20
21impl<S> VitrailCli<S>
22where
23    S: SchemaAccess,
24{
25    pub async fn run(self) -> Result<(), Box<dyn Error>> {
26        Cli::parse().run::<S>().await
27    }
28}
29
30pub async fn run_cli<S>() -> Result<(), Box<dyn Error>>
31where
32    S: SchemaAccess,
33{
34    VitrailCli::<S>::default().run().await
35}
36
37#[derive(Debug, Parser)]
38#[command(name = "vitrail")]
39#[command(about = "Vitrail CLI for schema-aware PostgreSQL migrations")]
40struct Cli {
41    #[command(subcommand)]
42    command: Command,
43}
44
45impl Cli {
46    async fn run<S>(self) -> Result<(), Box<dyn Error>>
47    where
48        S: SchemaAccess,
49    {
50        self.command.run::<S>().await
51    }
52}
53
54#[derive(Debug, Subcommand)]
55enum Command {
56    #[command(subcommand)]
57    Migrate(MigrateCommand),
58}
59
60impl Command {
61    async fn run<S>(self) -> Result<(), Box<dyn Error>>
62    where
63        S: SchemaAccess,
64    {
65        match self {
66            Self::Migrate(command) => command.run::<S>().await,
67        }
68    }
69}
70
71#[derive(Debug, Subcommand)]
72enum MigrateCommand {
73    #[command(about = "Create a new migration from the current schema")]
74    Dev(MigrateDevArgs),
75    #[command(about = "Apply all migrations from disk to the database")]
76    Deploy(MigrationConnectionArgs),
77    #[command(about = "Show migration status")]
78    Status(MigrationConnectionArgs),
79}
80
81impl MigrateCommand {
82    async fn run<S>(self) -> Result<(), Box<dyn Error>>
83    where
84        S: SchemaAccess,
85    {
86        match self {
87            Self::Dev(args) => args.run::<S>().await,
88            Self::Deploy(args) => args.run_deploy().await,
89            Self::Status(args) => args.run_status().await,
90        }
91    }
92}
93
94#[derive(Debug, Args)]
95struct MigrateDevArgs {
96    #[command(flatten)]
97    connection: MigrationConnectionArgs,
98    #[arg(long, short, help = "Human-readable name for the new migration")]
99    name: String,
100}
101
102impl MigrateDevArgs {
103    async fn run<S>(self) -> Result<(), Box<dyn Error>>
104    where
105        S: SchemaAccess,
106    {
107        let migrator = self.connection.migrator();
108
109        match migrator.generate_migration::<S>(&self.name).await? {
110            Some(generated) => {
111                println!(
112                    "Created migration `{}` at `{}`",
113                    generated.migration().name(),
114                    generated
115                        .migration()
116                        .sql_path()
117                        .expect("generated migration should always have a filesystem path")
118                        .display(),
119                );
120                print!("{}", generated.sql());
121            }
122            None => {
123                println!("Schema is already up to date. No migration was created.");
124            }
125        }
126
127        Ok(())
128    }
129}
130
131#[derive(Debug, Args)]
132struct MigrationConnectionArgs {
133    #[arg(long, env = "VITRAIL_DATABASE_URL", help = "PostgreSQL database URL")]
134    database_url: String,
135    #[arg(
136        long,
137        default_value = "migrations",
138        help = "Path to the migrations directory"
139    )]
140    migrations_path: PathBuf,
141}
142
143impl MigrationConnectionArgs {
144    fn migrator(&self) -> PostgresMigrator {
145        PostgresMigrator::new(self.database_url.clone(), self.migrations_path.clone())
146    }
147
148    async fn run_deploy(self) -> Result<(), Box<dyn Error>> {
149        let migrator = self.migrator();
150        let report = migrator.apply_all().await?;
151
152        if report.applied().is_empty() {
153            println!("No pending migrations.");
154        } else {
155            println!("Applied {} migration(s):", report.applied().len());
156            for migration in report.applied() {
157                println!("- {}", migration.name());
158            }
159        }
160
161        if !report.skipped().is_empty() {
162            println!(
163                "Skipped {} already applied migration(s).",
164                report.skipped().len()
165            );
166        }
167
168        Ok(())
169    }
170
171    async fn run_status(self) -> Result<(), Box<dyn Error>> {
172        let migrator = self.migrator();
173        let applied = migrator.applied_migrations().await?;
174        let migration_directory = migrator
175            .migration_directory()
176            .expect("CLI migrator should always use a migration directory");
177        let disk = migration_directory.read_all()?;
178
179        println!(
180            "Migration directory: {}",
181            migration_directory.path().display()
182        );
183        println!("Migrations on disk: {}", disk.len());
184        println!("Migrations applied: {}", applied.len());
185
186        if !disk.is_empty() {
187            println!("On disk:");
188            for migration in &disk {
189                println!("- {}", migration.name());
190            }
191        }
192
193        if !applied.is_empty() {
194            println!("Applied:");
195            for migration in &applied {
196                println!("- {}", migration.name());
197            }
198        }
199
200        Ok(())
201    }
202}