From 1ff6a8a950565c668f415ec8f8806b907f62c7f9 Mon Sep 17 00:00:00 2001 From: Austin Bonander Date: Wed, 15 Jan 2025 10:31:03 -0800 Subject: [PATCH] feat: teach `sqlx-cli` about `migrate.migrations-dir` --- Cargo.lock | 67 +++++++++++++--- sqlx-cli/src/database.rs | 13 ++-- sqlx-cli/src/lib.rs | 10 ++- sqlx-cli/src/migrate.rs | 99 +++++++++++++----------- sqlx-cli/src/opt.rs | 41 +++++----- sqlx-core/src/config/migrate.rs | 15 +++- sqlx-macros-core/src/migrate.rs | 3 +- sqlx-macros/src/lib.rs | 2 +- sqlx-postgres/src/connection/describe.rs | 4 +- 9 files changed, 163 insertions(+), 91 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 68f3c84cd..0e0b156bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/sqlx-cli/src/database.rs b/sqlx-cli/src/database.rs index 53834c111..1fd8bcc53 100644 --- a/sqlx-cli/src/database.rs +++ b/sqlx-cli/src/database.rs @@ -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 { diff --git a/sqlx-cli/src/lib.rs b/sqlx-cli/src/lib.rs index 5d1269f34..63257f541 100644 --- a/sqlx-cli/src/lib.rs +++ b/sqlx-cli/src/lib.rs @@ -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? }, }, diff --git a/sqlx-cli/src/migrate.rs b/sqlx-cli/src/migrate.rs index 76ad7dfb9..aabee2928 100644 --- a/sqlx-cli/src/migrate.rs +++ b/sqlx-cli/src/migrate.rs @@ -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, ) -> 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, ) -> 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}"); }} "#, ); diff --git a/sqlx-cli/src/opt.rs b/sqlx-cli/src/opt.rs index 6200f4dbb..0b72af659 100644 --- a/sqlx-cli/src/opt.rs +++ b/sqlx-cli/src/opt.rs @@ -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, } -impl Deref for Source { - type Target = String; - - fn deref(&self) -> &Self::Target { - &self.source - } -} - -impl AsRef 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() } } diff --git a/sqlx-core/src/config/migrate.rs b/sqlx-core/src/config/migrate.rs index 64529f9f0..666ed5bf9 100644 --- a/sqlx-core/src/config/migrate.rs +++ b/sqlx-core/src/config/migrate.rs @@ -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 + } +} \ No newline at end of file diff --git a/sqlx-macros-core/src/migrate.rs b/sqlx-macros-core/src/migrate.rs index 976cb181b..0ae2eaebd 100644 --- a/sqlx-macros-core/src/migrate.rs +++ b/sqlx-macros-core/src/migrate.rs @@ -111,8 +111,7 @@ pub fn expand_with_path(config: &Config, path: &Path) -> crate::Result 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); match migrate::expand(input) { Ok(ts) => ts.into(), Err(e) => { diff --git a/sqlx-postgres/src/connection/describe.rs b/sqlx-postgres/src/connection/describe.rs index 4decdde5d..5b6a2aa09 100644 --- a/sqlx-postgres/src/connection/describe.rs +++ b/sqlx-postgres/src/connection/describe.rs @@ -209,7 +209,8 @@ impl PgConnection { should_fetch: bool, ) -> Result { 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 {