feat: teach sqlx-cli about migrate.migrations-dir

This commit is contained in:
Austin Bonander
2025-01-15 10:31:03 -08:00
parent 367f2cca98
commit 1ff6a8a950
9 changed files with 163 additions and 91 deletions

67
Cargo.lock generated
View File

@@ -1990,12 +1990,12 @@ dependencies = [
[[package]]
name = "indexmap"
version = "2.2.5"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4"
checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f"
dependencies = [
"equivalent",
"hashbrown 0.14.5",
"hashbrown 0.15.2",
]
[[package]]
@@ -2774,7 +2774,7 @@ version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284"
dependencies = [
"toml_edit",
"toml_edit 0.21.1",
]
[[package]]
@@ -3306,6 +3306,15 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_spanned"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
dependencies = [
"serde",
]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
@@ -3559,7 +3568,7 @@ dependencies = [
"futures-util",
"hashbrown 0.15.2",
"hashlink",
"indexmap 2.2.5",
"indexmap 2.7.0",
"ipnetwork",
"log",
"mac_address",
@@ -3581,6 +3590,7 @@ dependencies = [
"time",
"tokio",
"tokio-stream",
"toml",
"tracing",
"url",
"uuid",
@@ -4187,10 +4197,25 @@ dependencies = [
]
[[package]]
name = "toml_datetime"
version = "0.6.6"
name = "toml"
version = "0.8.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf"
checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit 0.22.22",
]
[[package]]
name = "toml_datetime"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
@@ -4198,9 +4223,22 @@ version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1"
dependencies = [
"indexmap 2.2.5",
"indexmap 2.7.0",
"toml_datetime",
"winnow",
"winnow 0.5.40",
]
[[package]]
name = "toml_edit"
version = "0.22.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5"
dependencies = [
"indexmap 2.7.0",
"serde",
"serde_spanned",
"toml_datetime",
"winnow 0.6.22",
]
[[package]]
@@ -4791,6 +4829,15 @@ dependencies = [
"memchr",
]
[[package]]
name = "winnow"
version = "0.6.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39281189af81c07ec09db316b302a3e67bf9bd7cbf6c820b50e35fee9c2fa980"
dependencies = [
"memchr",
]
[[package]]
name = "write16"
version = "1.0.0"

View File

@@ -1,5 +1,5 @@
use crate::migrate;
use crate::opt::ConnectOpts;
use crate::{migrate, Config};
use crate::opt::{ConnectOpts, MigrationSourceOpt};
use console::style;
use promptly::{prompt, ReadlineError};
use sqlx::any::Any;
@@ -44,18 +44,19 @@ pub async fn drop(connect_opts: &ConnectOpts, confirm: bool, force: bool) -> any
}
pub async fn reset(
migration_source: &str,
config: &Config,
migration_source: &MigrationSourceOpt,
connect_opts: &ConnectOpts,
confirm: bool,
force: bool,
) -> anyhow::Result<()> {
drop(connect_opts, confirm, force).await?;
setup(migration_source, connect_opts).await
setup(config, migration_source, connect_opts).await
}
pub async fn setup(migration_source: &str, connect_opts: &ConnectOpts) -> anyhow::Result<()> {
pub async fn setup(config: &Config, migration_source: &MigrationSourceOpt, connect_opts: &ConnectOpts) -> anyhow::Result<()> {
create(connect_opts).await?;
migrate::run(migration_source, connect_opts, false, false, None).await
migrate::run(config, migration_source, connect_opts, false, false, None).await
}
fn ask_to_continue_drop(db_url: &str) -> bool {

View File

@@ -39,6 +39,7 @@ pub async fn run(opt: Opt) -> Result<()> {
connect_opts.populate_db_url(config)?;
migrate::run(
config,
&source,
&connect_opts,
dry_run,
@@ -57,6 +58,7 @@ pub async fn run(opt: Opt) -> Result<()> {
connect_opts.populate_db_url(config)?;
migrate::revert(
config,
&source,
&connect_opts,
dry_run,
@@ -71,9 +73,9 @@ pub async fn run(opt: Opt) -> Result<()> {
} => {
connect_opts.populate_db_url(config)?;
migrate::info(&source, &connect_opts).await?
migrate::info(config, &source, &connect_opts).await?
},
MigrateCommand::BuildScript { source, force } => migrate::build_script(&source, force)?,
MigrateCommand::BuildScript { source, force } => migrate::build_script(config, &source, force)?,
},
Command::Database(database) => match database.command {
@@ -96,14 +98,14 @@ pub async fn run(opt: Opt) -> Result<()> {
force,
} => {
connect_opts.populate_db_url(config)?;
database::reset(&source, &connect_opts, !confirmation.yes, force).await?
database::reset(config, &source, &connect_opts, !confirmation.yes, force).await?
},
DatabaseCommand::Setup {
source,
mut connect_opts,
} => {
connect_opts.populate_db_url(config)?;
database::setup(&source, &connect_opts).await?
database::setup(config, &source, &connect_opts).await?
},
},

View File

@@ -1,7 +1,7 @@
use crate::opt::{AddMigrationOpts, ConnectOpts};
use crate::opt::{AddMigrationOpts, ConnectOpts, MigrationSourceOpt};
use anyhow::{bail, Context};
use console::style;
use sqlx::migrate::{AppliedMigration, Migrate, MigrateError, MigrationType, Migrator};
use sqlx::migrate::{AppliedMigration, Migrate, MigrateError, MigrationType, Migrator, ResolveWith};
use sqlx::Connection;
use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
@@ -11,58 +11,34 @@ use std::path::Path;
use std::time::Duration;
use crate::config::Config;
fn create_file(
migration_source: &str,
file_prefix: &str,
description: &str,
migration_type: MigrationType,
) -> anyhow::Result<()> {
use std::path::PathBuf;
let mut file_name = file_prefix.to_string();
file_name.push('_');
file_name.push_str(&description.replace(' ', "_"));
file_name.push_str(migration_type.suffix());
let mut path = PathBuf::new();
path.push(migration_source);
path.push(&file_name);
println!("Creating {}", style(path.display()).cyan());
let mut file = File::create(&path).context("Failed to create migration file")?;
std::io::Write::write_all(&mut file, migration_type.file_content().as_bytes())?;
Ok(())
}
pub async fn add(
config: &Config,
opts: AddMigrationOpts,
) -> anyhow::Result<()> {
fs::create_dir_all(&opts.source).context("Unable to create migrations directory")?;
let source = opts.source.resolve(config);
fs::create_dir_all(source).context("Unable to create migrations directory")?;
let migrator = Migrator::new(opts.source.as_ref()).await?;
let migrator = Migrator::new(Path::new(source)).await?;
let version_prefix = opts.version_prefix(config, &migrator);
if opts.reversible(config, &migrator) {
create_file(
&opts.source,
source,
&version_prefix,
&opts.description,
MigrationType::ReversibleUp,
)?;
create_file(
&opts.source,
source,
&version_prefix,
&opts.description,
MigrationType::ReversibleDown,
)?;
} else {
create_file(
&opts.source,
source,
&version_prefix,
&opts.description,
MigrationType::Simple,
@@ -70,13 +46,13 @@ pub async fn add(
}
// if the migrations directory is empty
let has_existing_migrations = fs::read_dir(&opts.source)
let has_existing_migrations = fs::read_dir(source)
.map(|mut dir| dir.next().is_some())
.unwrap_or(false);
if !has_existing_migrations {
let quoted_source = if *opts.source != "migrations" {
format!("{:?}", *opts.source)
let quoted_source = if opts.source.source.is_some() {
format!("{source:?}")
} else {
"".to_string()
};
@@ -114,6 +90,32 @@ See: https://docs.rs/sqlx/{version}/sqlx/macro.migrate.html
Ok(())
}
fn create_file(
migration_source: &str,
file_prefix: &str,
description: &str,
migration_type: MigrationType,
) -> anyhow::Result<()> {
use std::path::PathBuf;
let mut file_name = file_prefix.to_string();
file_name.push('_');
file_name.push_str(&description.replace(' ', "_"));
file_name.push_str(migration_type.suffix());
let mut path = PathBuf::new();
path.push(migration_source);
path.push(&file_name);
println!("Creating {}", style(path.display()).cyan());
let mut file = File::create(&path).context("Failed to create migration file")?;
std::io::Write::write_all(&mut file, migration_type.file_content().as_bytes())?;
Ok(())
}
fn short_checksum(checksum: &[u8]) -> String {
let mut s = String::with_capacity(checksum.len() * 2);
for b in checksum {
@@ -122,8 +124,10 @@ fn short_checksum(checksum: &[u8]) -> String {
s
}
pub async fn info(migration_source: &str, connect_opts: &ConnectOpts) -> anyhow::Result<()> {
let migrator = Migrator::new(Path::new(migration_source)).await?;
pub async fn info(config: &Config, migration_source: &MigrationSourceOpt, connect_opts: &ConnectOpts) -> anyhow::Result<()> {
let source = migration_source.resolve(config);
let migrator = Migrator::new(ResolveWith(Path::new(source), config.migrate.to_resolve_config())).await?;
let mut conn = crate::connect(connect_opts).await?;
conn.ensure_migrations_table().await?;
@@ -202,13 +206,16 @@ fn validate_applied_migrations(
}
pub async fn run(
migration_source: &str,
config: &Config,
migration_source: &MigrationSourceOpt,
connect_opts: &ConnectOpts,
dry_run: bool,
ignore_missing: bool,
target_version: Option<i64>,
) -> anyhow::Result<()> {
let migrator = Migrator::new(Path::new(migration_source)).await?;
let source = migration_source.resolve(config);
let migrator = Migrator::new(Path::new(source)).await?;
if let Some(target_version) = target_version {
if !migrator.version_exists(target_version) {
bail!(MigrateError::VersionNotPresent(target_version));
@@ -295,13 +302,15 @@ pub async fn run(
}
pub async fn revert(
migration_source: &str,
config: &Config,
migration_source: &MigrationSourceOpt,
connect_opts: &ConnectOpts,
dry_run: bool,
ignore_missing: bool,
target_version: Option<i64>,
) -> anyhow::Result<()> {
let migrator = Migrator::new(Path::new(migration_source)).await?;
let source = migration_source.resolve(config);
let migrator = Migrator::new(Path::new(source)).await?;
if let Some(target_version) = target_version {
if target_version != 0 && !migrator.version_exists(target_version) {
bail!(MigrateError::VersionNotPresent(target_version));
@@ -388,7 +397,9 @@ pub async fn revert(
Ok(())
}
pub fn build_script(migration_source: &str, force: bool) -> anyhow::Result<()> {
pub fn build_script(config: &Config, migration_source: &MigrationSourceOpt, force: bool) -> anyhow::Result<()> {
let source = migration_source.resolve(config);
anyhow::ensure!(
Path::new("Cargo.toml").exists(),
"must be run in a Cargo project root"
@@ -403,7 +414,7 @@ pub fn build_script(migration_source: &str, force: bool) -> anyhow::Result<()> {
r#"// generated by `sqlx migrate build-script`
fn main() {{
// trigger recompilation when a new migration is added
println!("cargo:rerun-if-changed={migration_source}");
println!("cargo:rerun-if-changed={source}");
}}
"#,
);

View File

@@ -1,6 +1,5 @@
use std::env;
use std::ops::{Deref, Not};
use std::path::Path;
use anyhow::Context;
use chrono::Utc;
use clap::{Args, Parser};
@@ -98,7 +97,7 @@ pub enum DatabaseCommand {
confirmation: Confirmation,
#[clap(flatten)]
source: Source,
source: MigrationSourceOpt,
#[clap(flatten)]
connect_opts: ConnectOpts,
@@ -111,7 +110,7 @@ pub enum DatabaseCommand {
/// Creates the database specified in your DATABASE_URL and runs any pending migrations.
Setup {
#[clap(flatten)]
source: Source,
source: MigrationSourceOpt,
#[clap(flatten)]
connect_opts: ConnectOpts,
@@ -197,7 +196,7 @@ pub enum MigrateCommand {
/// Run all pending migrations.
Run {
#[clap(flatten)]
source: Source,
source: MigrationSourceOpt,
/// List all the migrations to be run without applying
#[clap(long)]
@@ -218,7 +217,7 @@ pub enum MigrateCommand {
/// Revert the latest migration with a down file.
Revert {
#[clap(flatten)]
source: Source,
source: MigrationSourceOpt,
/// List the migration to be reverted without applying
#[clap(long)]
@@ -240,7 +239,7 @@ pub enum MigrateCommand {
/// List all available migrations.
Info {
#[clap(flatten)]
source: Source,
source: MigrationSourceOpt,
#[clap(flatten)]
connect_opts: ConnectOpts,
@@ -251,7 +250,7 @@ pub enum MigrateCommand {
/// Must be run in a Cargo project root.
BuildScript {
#[clap(flatten)]
source: Source,
source: MigrationSourceOpt,
/// Overwrite the build script if it already exists.
#[clap(long)]
@@ -264,7 +263,7 @@ pub struct AddMigrationOpts {
pub description: String,
#[clap(flatten)]
pub source: Source,
pub source: MigrationSourceOpt,
/// If set, create an up-migration only. Conflicts with `--reversible`.
#[clap(long, conflicts_with = "reversible")]
@@ -289,23 +288,21 @@ pub struct AddMigrationOpts {
/// Argument for the migration scripts source.
#[derive(Args, Debug)]
pub struct Source {
pub struct MigrationSourceOpt {
/// Path to folder containing migrations.
#[clap(long, default_value = "migrations")]
source: String,
///
/// Defaults to `migrations/` if not specified, but a different default may be set by `sqlx.toml`.
#[clap(long)]
pub source: Option<String>,
}
impl Deref for Source {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.source
}
}
impl AsRef<Path> for Source {
fn as_ref(&self) -> &Path {
Path::new(&self.source)
impl MigrationSourceOpt {
pub fn resolve<'a>(&'a self, config: &'a Config) -> &'a str {
if let Some(source) = &self.source {
return source;
}
config.migrate.migrations_dir()
}
}

View File

@@ -85,7 +85,7 @@ pub struct Config {
/// To make your migrations amenable to reformatting, you may wish to tell SQLx to ignore
/// _all_ whitespace characters in migrations.
///
/// ##### Warning: Beware Syntatically Significant Whitespace!
/// ##### Warning: Beware Syntactically Significant Whitespace!
/// If your migrations use string literals or quoted identifiers which contain whitespace,
/// this configuration will cause the migration machinery to ignore some changes to these.
/// This may result in a mismatch between the development and production versions of
@@ -179,3 +179,16 @@ pub enum DefaultVersioning {
/// Use sequential integers for migration versions.
Sequential,
}
#[cfg(feature = "migrate")]
impl Config {
pub fn migrations_dir(&self) -> &str {
self.migrations_dir.as_deref().unwrap_or("migrations")
}
pub fn to_resolve_config(&self) -> crate::migrate::ResolveConfig {
let mut config = crate::migrate::ResolveConfig::new();
config.ignore_chars(self.ignored_chars.iter().copied());
config
}
}

View File

@@ -111,8 +111,7 @@ pub fn expand_with_path(config: &Config, path: &Path) -> crate::Result<TokenStre
)
})?;
let mut resolve_config = ResolveConfig::new();
resolve_config.ignore_chars(&config.migrate.ignored_chars);
let resolve_config = config.migrate.to_resolve_config();
// Use the same code path to resolve migrations at compile time and runtime.
let migrations = sqlx_core::migrate::resolve_blocking_with_config(&path, &resolve_config)?

View File

@@ -68,7 +68,7 @@ pub fn derive_from_row(input: TokenStream) -> TokenStream {
pub fn migrate(input: TokenStream) -> TokenStream {
use syn::LitStr;
let input = syn::parse_macro_input!(input as LitStr);
let input = syn::parse_macro_input!(input as Option<LitStr>);
match migrate::expand(input) {
Ok(ts) => ts.into(),
Err(e) => {

View File

@@ -209,7 +209,8 @@ impl PgConnection {
should_fetch: bool,
) -> Result<ColumnOrigin, Error> {
if let Some(origin) =
self.cache_table_to_column_names
self.inner
.cache_table_to_column_names
.get(&relation_id)
.and_then(|table_columns| {
let column_name = table_columns.columns.get(&attribute_no).cloned()?;
@@ -245,6 +246,7 @@ impl PgConnection {
};
let table_columns = self
.inner
.cache_table_to_column_names
.entry(relation_id)
.or_insert_with(|| TableColumns {