Support naming migrations sequentially (#2602)

* Support naming migrations sequentially and inferring naming scheme

* Document new options and how naming is inferred

* Only account for up migrations when inferring ordering
This commit is contained in:
Max Vorobev 2023-07-25 02:00:26 +03:00 committed by GitHub
parent c70cfaf035
commit f2bb464bcd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 104 additions and 6 deletions

View file

@ -27,7 +27,9 @@ pub async fn run(opt: Opt) -> Result<()> {
source, source,
description, description,
reversible, reversible,
} => migrate::add(&source, &description, reversible).await?, sequential,
timestamp,
} => migrate::add(&source, &description, reversible, sequential, timestamp).await?,
MigrateCommand::Run { MigrateCommand::Run {
source, source,
dry_run, dry_run,

View file

@ -37,10 +37,75 @@ fn create_file(
Ok(()) 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::<Vec<_>>();
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( pub async fn add(
migration_source: &str, migration_source: &str,
description: &str, description: &str,
reversible: bool, reversible: bool,
sequential: bool,
timestamp: bool,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
fs::create_dir_all(migration_source).context("Unable to create migrations directory")?; fs::create_dir_all(migration_source).context("Unable to create migrations directory")?;
@ -50,15 +115,16 @@ pub async fn add(
.unwrap_or(false); .unwrap_or(false);
let migrator = Migrator::new(Path::new(migration_source)).await?; 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() { for migration in migrator.iter() {
if migration.migration_type.is_reversible() != reversible { if migration.migration_type.is_reversible() != reversible {
bail!(MigrateError::InvalidMixReversibleAndSimple); bail!(MigrateError::InvalidMixReversibleAndSimple);
} }
} }
let dt = Utc::now(); let ordering = MigrationOrdering::infer(sequential, timestamp, &migrator);
let file_prefix = dt.format("%Y%m%d%H%M%S").to_string(); let file_prefix = ordering.file_prefix();
if reversible { if reversible {
create_file( create_file(
migration_source, migration_source,

View file

@ -109,8 +109,22 @@ pub struct MigrateOpt {
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
pub enum MigrateCommand { pub enum MigrateCommand {
/// Create a new migration with the given description, /// Create a new migration with the given description.
/// and the current time as the version. ///
/// 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 { Add {
description: String, description: String,
@ -121,6 +135,14 @@ pub enum MigrateCommand {
/// else creates a single sql file /// else creates a single sql file
#[clap(short)] #[clap(short)]
reversible: bool, 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. /// Run all pending migrations.

View file

@ -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 { pub fn is_down_migration(&self) -> bool {
match self { match self {
MigrationType::Simple => false, MigrationType::Simple => false,