From 367f2cca98d376adbd5ead2dbc9b85684e1ce067 Mon Sep 17 00:00:00 2001 From: Austin Bonander Date: Sat, 5 Oct 2024 15:21:32 -0700 Subject: [PATCH] feat: teach `sqlx-cli` about `migrate.defaults` --- sqlx-cli/src/lib.rs | 15 +- sqlx-cli/src/migrate.rs | 110 +++----------- sqlx-cli/src/opt.rs | 182 +++++++++++++++++++++--- sqlx-core/src/migrate/migration_type.rs | 3 +- 4 files changed, 186 insertions(+), 124 deletions(-) diff --git a/sqlx-cli/src/lib.rs b/sqlx-cli/src/lib.rs index d08b949c..5d1269f3 100644 --- a/sqlx-cli/src/lib.rs +++ b/sqlx-cli/src/lib.rs @@ -1,5 +1,5 @@ use std::io; -use std::path::{Path, PathBuf}; +use std::path::{PathBuf}; use std::time::Duration; use anyhow::{Context, Result}; @@ -21,21 +21,14 @@ mod prepare; pub use crate::opt::Opt; -pub use sqlx::_unstable::config; -use crate::config::Config; +pub use sqlx::_unstable::config::{self, Config}; pub async fn run(opt: Opt) -> Result<()> { - let config = config_from_current_dir()?; + let config = config_from_current_dir().await?; match opt.command { Command::Migrate(migrate) => match migrate.command { - MigrateCommand::Add { - source, - description, - reversible, - sequential, - timestamp, - } => migrate::add(&source, &description, reversible, sequential, timestamp).await?, + MigrateCommand::Add(opts)=> migrate::add(config, opts).await?, MigrateCommand::Run { source, dry_run, diff --git a/sqlx-cli/src/migrate.rs b/sqlx-cli/src/migrate.rs index e00f6de6..76ad7dfb 100644 --- a/sqlx-cli/src/migrate.rs +++ b/sqlx-cli/src/migrate.rs @@ -1,6 +1,5 @@ -use crate::opt::ConnectOpts; +use crate::opt::{AddMigrationOpts, ConnectOpts}; use anyhow::{bail, Context}; -use chrono::Utc; use console::style; use sqlx::migrate::{AppliedMigration, Migrate, MigrateError, MigrationType, Migrator}; use sqlx::Connection; @@ -10,6 +9,7 @@ use std::fmt::Write; use std::fs::{self, File}; use std::path::Path; use std::time::Duration; +use crate::config::Config; fn create_file( migration_source: &str, @@ -37,116 +37,46 @@ fn create_file( Ok(()) } -enum MigrationOrdering { - Timestamp(String), - Sequential(String), -} - -impl MigrationOrdering { - fn timestamp() -> MigrationOrdering { - Self::Timestamp(Utc::now().format("%Y%m%d%H%M%S").to_string()) - } - - fn sequential(version: i64) -> MigrationOrdering { - Self::Sequential(format!("{version:04}")) - } - - fn file_prefix(&self) -> &str { - match self { - MigrationOrdering::Timestamp(prefix) => prefix, - MigrationOrdering::Sequential(prefix) => prefix, - } - } - - fn infer(sequential: bool, timestamp: bool, migrator: &Migrator) -> Self { - match (timestamp, sequential) { - (true, true) => panic!("Impossible to specify both timestamp and sequential mode"), - (true, false) => MigrationOrdering::timestamp(), - (false, true) => MigrationOrdering::sequential( - migrator - .iter() - .last() - .map_or(1, |last_migration| last_migration.version + 1), - ), - (false, false) => { - // inferring the naming scheme - let migrations = migrator - .iter() - .filter(|migration| migration.migration_type.is_up_migration()) - .rev() - .take(2) - .collect::>(); - if let [last, pre_last] = &migrations[..] { - // there are at least two migrations, compare the last twothere's only one existing migration - if last.version - pre_last.version == 1 { - // their version numbers differ by 1, infer sequential - MigrationOrdering::sequential(last.version + 1) - } else { - MigrationOrdering::timestamp() - } - } else if let [last] = &migrations[..] { - // there is only one existing migration - if last.version == 0 || last.version == 1 { - // infer sequential if the version number is 0 or 1 - MigrationOrdering::sequential(last.version + 1) - } else { - MigrationOrdering::timestamp() - } - } else { - MigrationOrdering::timestamp() - } - } - } - } -} - pub async fn add( - migration_source: &str, - description: &str, - reversible: bool, - sequential: bool, - timestamp: bool, + config: &Config, + opts: AddMigrationOpts, ) -> anyhow::Result<()> { - fs::create_dir_all(migration_source).context("Unable to create migrations directory")?; + fs::create_dir_all(&opts.source).context("Unable to create migrations directory")?; - let migrator = Migrator::new(Path::new(migration_source)).await?; - // Type of newly created migration will be the same as the first one - // or reversible flag if this is the first migration - let migration_type = MigrationType::infer(&migrator, reversible); + let migrator = Migrator::new(opts.source.as_ref()).await?; - let ordering = MigrationOrdering::infer(sequential, timestamp, &migrator); - let file_prefix = ordering.file_prefix(); + let version_prefix = opts.version_prefix(config, &migrator); - if migration_type.is_reversible() { + if opts.reversible(config, &migrator) { create_file( - migration_source, - file_prefix, - description, + &opts.source, + &version_prefix, + &opts.description, MigrationType::ReversibleUp, )?; create_file( - migration_source, - file_prefix, - description, + &opts.source, + &version_prefix, + &opts.description, MigrationType::ReversibleDown, )?; } else { create_file( - migration_source, - file_prefix, - description, + &opts.source, + &version_prefix, + &opts.description, MigrationType::Simple, )?; } // if the migrations directory is empty - let has_existing_migrations = fs::read_dir(migration_source) + let has_existing_migrations = fs::read_dir(&opts.source) .map(|mut dir| dir.next().is_some()) .unwrap_or(false); if !has_existing_migrations { - let quoted_source = if migration_source != "migrations" { - format!("{migration_source:?}") + let quoted_source = if *opts.source != "migrations" { + format!("{:?}", *opts.source) } else { "".to_string() }; diff --git a/sqlx-cli/src/opt.rs b/sqlx-cli/src/opt.rs index a37fda18..6200f4db 100644 --- a/sqlx-cli/src/opt.rs +++ b/sqlx-cli/src/opt.rs @@ -1,10 +1,14 @@ use std::env; use std::ops::{Deref, Not}; +use std::path::Path; use anyhow::Context; +use chrono::Utc; use clap::{Args, Parser}; #[cfg(feature = "completions")] use clap_complete::Shell; -use sqlx::config::Config; +use crate::config::Config; +use sqlx::migrate::Migrator; +use crate::config::migrate::{DefaultMigrationType, DefaultVersioning}; #[derive(Parser, Debug)] #[clap(version, about, author)] @@ -125,8 +129,55 @@ pub struct MigrateOpt { pub enum MigrateCommand { /// Create a new migration with the given description. /// + /// -------------------------------- + /// + /// Migrations may either be simple, or reversible. + /// + /// Reversible migrations can be reverted with `sqlx migrate revert`, simple migrations cannot. + /// + /// Reversible migrations are created as a pair of two files with the same filename but + /// extensions `.up.sql` and `.down.sql` for the up-migration and down-migration, respectively. + /// + /// The up-migration should contain the commands to be used when applying the migration, + /// while the down-migration should contain the commands to reverse the changes made by the + /// up-migration. + /// + /// When writing down-migrations, care should be taken to ensure that they + /// do not leave the database in an inconsistent state. + /// + /// Simple migrations have just `.sql` for their extension and represent an up-migration only. + /// + /// Note that reverting a migration is **destructive** and will likely result in data loss. + /// Reverting a migration will not restore any data discarded by commands in the up-migration. + /// + /// It is recommended to always back up the database before running migrations. + /// + /// -------------------------------- + /// + /// For convenience, this command attempts to detect if reversible migrations are in-use. + /// + /// If the latest existing migration is reversible, the new migration will also be reversible. + /// + /// Otherwise, a simple migration is created. + /// + /// This behavior can be overridden by `--simple` or `--reversible`, respectively. + /// + /// The default type to use can also be set in `sqlx.toml`. + /// + /// -------------------------------- + /// /// A version number will be automatically assigned to the migration. /// + /// Migrations are applied in ascending order by version number. + /// Version numbers do not need to be strictly consecutive. + /// + /// The migration process will abort if SQLx encounters a migration with a version number + /// less than _any_ previously applied migration. + /// + /// Migrations should only be created with increasing version number. + /// + /// -------------------------------- + /// /// For convenience, this command will attempt to detect if sequential versioning is in use, /// and if so, continue the sequence. /// @@ -136,28 +187,12 @@ pub enum MigrateCommand { /// /// * only one migration exists and its version number is either 0 or 1. /// - /// Otherwise timestamp versioning is assumed. + /// Otherwise, timestamp versioning (`YYYYMMDDHHMMSS`) is assumed. /// - /// This behavior can overridden by `--sequential` or `--timestamp`, respectively. - Add { - description: String, - - #[clap(flatten)] - source: Source, - - /// If true, creates a pair of up and down migration files with same version - /// else creates a single sql file - #[clap(short)] - reversible: bool, - - /// If set, use timestamp versioning for the new migration. Conflicts with `--sequential`. - #[clap(short, long)] - timestamp: bool, - - /// If set, use sequential versioning for the new migration. Conflicts with `--timestamp`. - #[clap(short, long, conflicts_with = "timestamp")] - sequential: bool, - }, + /// This behavior can be overridden by `--timestamp` or `--sequential`, respectively. + /// + /// The default versioning to use can also be set in `sqlx.toml`. + Add(AddMigrationOpts), /// Run all pending migrations. Run { @@ -224,6 +259,34 @@ pub enum MigrateCommand { }, } +#[derive(Args, Debug)] +pub struct AddMigrationOpts { + pub description: String, + + #[clap(flatten)] + pub source: Source, + + /// If set, create an up-migration only. Conflicts with `--reversible`. + #[clap(long, conflicts_with = "reversible")] + simple: bool, + + /// If set, create a pair of up and down migration files with same version. + /// + /// Conflicts with `--simple`. + #[clap(short, long, conflicts_with = "simple")] + reversible: bool, + + /// If set, use timestamp versioning for the new migration. Conflicts with `--sequential`. + /// + /// Timestamp format: `YYYYMMDDHHMMSS` + #[clap(short, long, conflicts_with = "sequential")] + timestamp: bool, + + /// If set, use sequential versioning for the new migration. Conflicts with `--timestamp`. + #[clap(short, long, conflicts_with = "timestamp")] + sequential: bool, +} + /// Argument for the migration scripts source. #[derive(Args, Debug)] pub struct Source { @@ -240,6 +303,12 @@ impl Deref for Source { } } +impl AsRef for Source { + fn as_ref(&self) -> &Path { + Path::new(&self.source) + } +} + /// Argument for the database URL. #[derive(Args, Debug)] pub struct ConnectOpts { @@ -338,3 +407,72 @@ impl Not for IgnoreMissing { !self.ignore_missing } } + +impl AddMigrationOpts { + pub fn reversible(&self, config: &Config, migrator: &Migrator) -> bool { + if self.reversible { return true; } + if self.simple { return false; } + + match config.migrate.defaults.migration_type { + DefaultMigrationType::Inferred => { + migrator + .iter() + .last() + .is_some_and(|m| m.migration_type.is_reversible()) + } + DefaultMigrationType::Simple => { + false + } + DefaultMigrationType::Reversible => { + true + } + } + } + + pub fn version_prefix(&self, config: &Config, migrator: &Migrator) -> String { + let default_versioning = &config.migrate.defaults.migration_versioning; + + if self.timestamp || matches!(default_versioning, DefaultVersioning::Timestamp) { + return next_timestamp(); + } + + if self.sequential || matches!(default_versioning, DefaultVersioning::Sequential) { + return next_sequential(migrator) + .unwrap_or_else(|| fmt_sequential(1)); + } + + next_sequential(migrator).unwrap_or_else(next_timestamp) + } +} + +fn next_timestamp() -> String { + Utc::now().format("%Y%m%d%H%M%S").to_string() +} + +fn next_sequential(migrator: &Migrator) -> Option { + let next_version = migrator + .migrations + .windows(2) + .last() + .and_then(|migrations| { + match migrations { + [previous, latest] => { + // If the latest two versions differ by 1, infer sequential. + (latest.version - previous.version == 1) + .then_some(latest.version + 1) + }, + [latest] => { + // If only one migration exists and its version is 0 or 1, infer sequential + matches!(latest.version, 0 | 1) + .then_some(latest.version + 1) + } + _ => unreachable!(), + } + }); + + next_version.map(fmt_sequential) +} + +fn fmt_sequential(version: i64) -> String { + format!("{version:04}") +} diff --git a/sqlx-core/src/migrate/migration_type.rs b/sqlx-core/src/migrate/migration_type.rs index de2b0193..350ddb3f 100644 --- a/sqlx-core/src/migrate/migration_type.rs +++ b/sqlx-core/src/migrate/migration_type.rs @@ -74,8 +74,9 @@ impl MigrationType { } } + #[deprecated = "unused"] pub fn infer(migrator: &Migrator, reversible: bool) -> MigrationType { - match migrator.iter().next() { + match migrator.iter().last() { Some(first_migration) => first_migration.migration_type, None => { if reversible {