diff --git a/sqlx-cli/src/lib.rs b/sqlx-cli/src/lib.rs index 13786efe..8dbf9573 100644 --- a/sqlx-cli/src/lib.rs +++ b/sqlx-cli/src/lib.rs @@ -27,7 +27,9 @@ pub async fn run(opt: Opt) -> Result<()> { source, description, reversible, - } => migrate::add(&source, &description, reversible).await?, + sequential, + timestamp, + } => migrate::add(&source, &description, reversible, sequential, timestamp).await?, MigrateCommand::Run { source, dry_run, diff --git a/sqlx-cli/src/migrate.rs b/sqlx-cli/src/migrate.rs index 7e5602dd..5e94f26f 100644 --- a/sqlx-cli/src/migrate.rs +++ b/sqlx-cli/src/migrate.rs @@ -37,10 +37,75 @@ 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!("{:04}", version)) + } + + 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, ) -> anyhow::Result<()> { fs::create_dir_all(migration_source).context("Unable to create migrations directory")?; @@ -50,15 +115,16 @@ pub async fn add( .unwrap_or(false); let migrator = Migrator::new(Path::new(migration_source)).await?; - // This checks if all existing migrations are of the same type as the reverisble flag passed + // This checks if all existing migrations are of the same type as the reversible flag passed for migration in migrator.iter() { if migration.migration_type.is_reversible() != reversible { bail!(MigrateError::InvalidMixReversibleAndSimple); } } - let dt = Utc::now(); - let file_prefix = dt.format("%Y%m%d%H%M%S").to_string(); + let ordering = MigrationOrdering::infer(sequential, timestamp, &migrator); + let file_prefix = ordering.file_prefix(); + if reversible { create_file( migration_source, diff --git a/sqlx-cli/src/opt.rs b/sqlx-cli/src/opt.rs index fb901d56..a72abc98 100644 --- a/sqlx-cli/src/opt.rs +++ b/sqlx-cli/src/opt.rs @@ -109,8 +109,22 @@ pub struct MigrateOpt { #[derive(Parser, Debug)] pub enum MigrateCommand { - /// Create a new migration with the given description, - /// and the current time as the version. + /// Create a new migration with the given description. + /// + /// A version number will be automatically assigned to the migration. + /// + /// For convenience, this command will attempt to detect if sequential versioning is in use, + /// and if so, continue the sequence. + /// + /// Sequential versioning is inferred if: + /// + /// * The version numbers of the last two migrations differ by exactly 1, or: + /// + /// * only one migration exists and its version number is either 0 or 1. + /// + /// Otherwise timestamp versioning is assumed. + /// + /// This behavior can overridden by `--sequential` or `--timestamp`, respectively. Add { description: String, @@ -121,6 +135,14 @@ pub enum MigrateCommand { /// 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 timestamp versioning for the new migration. Conflicts with `--timestamp`. + #[clap(short, long, conflicts_with = "timestamp")] + sequential: bool, }, /// Run all pending migrations. diff --git a/sqlx-core/src/migrate/migration_type.rs b/sqlx-core/src/migrate/migration_type.rs index 87feedfa..5fe298ae 100644 --- a/sqlx-core/src/migrate/migration_type.rs +++ b/sqlx-core/src/migrate/migration_type.rs @@ -32,6 +32,14 @@ impl MigrationType { } } + pub fn is_up_migration(&self) -> bool { + match self { + MigrationType::Simple => true, + MigrationType::ReversibleUp => true, + MigrationType::ReversibleDown => false, + } + } + pub fn is_down_migration(&self) -> bool { match self { MigrationType::Simple => false,