feat: teach sqlx-cli about migrate.defaults

This commit is contained in:
Austin Bonander 2024-10-05 15:21:32 -07:00
parent e951d8e128
commit 367f2cca98
4 changed files with 186 additions and 124 deletions

View File

@ -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,

View File

@ -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::<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(
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()
};

View File

@ -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<Path> 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<String> {
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}")
}

View File

@ -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 {