diff --git a/Cargo.lock b/Cargo.lock
index 7a5db0c251..53e4932cc7 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2761,9 +2761,9 @@ dependencies = [
[[package]]
name = "quote"
-version = "1.0.38"
+version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc"
+checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2",
]
@@ -3486,6 +3486,7 @@ dependencies = [
"sha2",
"smallvec",
"sqlx",
+ "subst",
"thiserror 2.0.11",
"time",
"tokio",
@@ -3630,6 +3631,7 @@ dependencies = [
"quote",
"sqlx-core",
"sqlx-macros-core",
+ "subst",
"syn 2.0.96",
]
@@ -3652,6 +3654,7 @@ dependencies = [
"sqlx-mysql",
"sqlx-postgres",
"sqlx-sqlite",
+ "subst",
"syn 2.0.96",
"tempfile",
"tokio",
@@ -3848,6 +3851,16 @@ dependencies = [
"syn 2.0.96",
]
+[[package]]
+name = "subst"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33e7942675ea19db01ef8cf15a1e6443007208e6c74568bd64162da26d40160d"
+dependencies = [
+ "memchr",
+ "unicode-width 0.1.14",
+]
+
[[package]]
name = "subtle"
version = "2.6.1"
diff --git a/Cargo.toml b/Cargo.toml
index 1aef121199..2c15f55d30 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -155,6 +155,7 @@ uuid = "1.1.2"
# Common utility crates
dotenvy = { version = "0.15.7", default-features = false }
+subst = "0.3.7"
# Runtimes
[workspace.dependencies.async-std]
diff --git a/README.md b/README.md
index cc0ecf2e66..94a6314043 100644
--- a/README.md
+++ b/README.md
@@ -458,6 +458,26 @@ opt-level = 3
1 The `dotenv` crate itself appears abandoned as of [December 2021](https://github.com/dotenv-rs/dotenv/issues/74)
so we now use the `dotenvy` crate instead. The file format is the same.
+## Parameter Substitution for Migrations
+
+You can parameterize migrations using parameters, either from the environment or passed in from the cli or to the Migrator.
+
+For example:
+
+```sql
+-- enable-substitution
+CREATE USER ${USER_FROM_ENV} WITH PASSWORD ${PASSWORD_FROM_ENV}
+-- disable-substituion
+```
+
+We use the [subst](https://crates.io/crates/subst) to support substitution. sqlx supports
+
+- Short format: `$NAME`
+- Long format: `${NAME}`
+- Default values: `${NAME:Bob}`
+- Recursive Substitution in Default Values: `${NAME: Bob ${OTHER_NAME: and Alice}}`
+
+
## Safety
This crate uses `#![forbid(unsafe_code)]` to ensure everything is implemented in 100% Safe Rust.
diff --git a/sqlx-cli/README.md b/sqlx-cli/README.md
index b20461b8fd..eabf68c101 100644
--- a/sqlx-cli/README.md
+++ b/sqlx-cli/README.md
@@ -65,6 +65,18 @@ any scripts that are still pending.
---
+Users can also provide parameters through environment variables or pass them in manually.
+
+```bash
+sqlx migrate run --params-from-env
+```
+
+```bash
+sqlx migrate run --params key:value,key1,value1
+```
+
+---
+
Users can provide the directory for the migration scripts to `sqlx migrate` subcommands with the `--source` flag.
```bash
@@ -105,6 +117,16 @@ Creating migrations/20211001154420_.up.sql
Creating migrations/20211001154420_.down.sql
```
+Users can also provide parameters through environment variables or pass them in manually, just as they did with the run command.
+
+```bash
+sqlx migrate revert --params-from-env
+```
+
+```bash
+sqlx migrate revert --params key:value,key1,value1
+```
+
### Enable building in "offline mode" with `query!()`
There are 2 steps to building with "offline mode":
diff --git a/sqlx-cli/src/database.rs b/sqlx-cli/src/database.rs
index 7a9bc6bf2f..791a6b0164 100644
--- a/sqlx-cli/src/database.rs
+++ b/sqlx-cli/src/database.rs
@@ -57,7 +57,16 @@ pub async fn reset(
pub async fn setup(migration_source: &str, connect_opts: &ConnectOpts) -> anyhow::Result<()> {
create(connect_opts).await?;
- migrate::run(migration_source, connect_opts, false, false, None).await
+ migrate::run(
+ migration_source,
+ connect_opts,
+ false,
+ false,
+ None,
+ false,
+ Vec::with_capacity(0),
+ )
+ .await
}
async fn ask_to_continue_drop(db_url: String) -> bool {
diff --git a/sqlx-cli/src/lib.rs b/sqlx-cli/src/lib.rs
index cb31205b4f..7906796eb6 100644
--- a/sqlx-cli/src/lib.rs
+++ b/sqlx-cli/src/lib.rs
@@ -66,6 +66,8 @@ async fn do_run(opt: Opt) -> Result<()> {
ignore_missing,
connect_opts,
target_version,
+ params_from_env,
+ params,
} => {
migrate::run(
&source,
@@ -73,6 +75,8 @@ async fn do_run(opt: Opt) -> Result<()> {
dry_run,
*ignore_missing,
target_version,
+ params_from_env,
+ params,
)
.await?
}
@@ -82,6 +86,8 @@ async fn do_run(opt: Opt) -> Result<()> {
ignore_missing,
connect_opts,
target_version,
+ params_from_env,
+ params,
} => {
migrate::revert(
&source,
@@ -89,6 +95,8 @@ async fn do_run(opt: Opt) -> Result<()> {
dry_run,
*ignore_missing,
target_version,
+ params_from_env,
+ params,
)
.await?
}
diff --git a/sqlx-cli/src/migrate.rs b/sqlx-cli/src/migrate.rs
index e00f6de651..50d74f1464 100644
--- a/sqlx-cli/src/migrate.rs
+++ b/sqlx-cli/src/migrate.rs
@@ -277,6 +277,8 @@ pub async fn run(
dry_run: bool,
ignore_missing: bool,
target_version: Option,
+ params_from_env: bool,
+ parameters: Vec<(String, String)>,
) -> anyhow::Result<()> {
let migrator = Migrator::new(Path::new(migration_source)).await?;
if let Some(target_version) = target_version {
@@ -313,6 +315,14 @@ pub async fn run(
.map(|m| (m.version, m))
.collect();
+ let env_params: HashMap<_, _> = if params_from_env {
+ std::env::vars().collect()
+ } else {
+ HashMap::with_capacity(0)
+ };
+
+ let params: HashMap<_, _> = parameters.into_iter().collect();
+
for migration in migrator.iter() {
if migration.migration_type.is_down_migration() {
// Skipping down migrations
@@ -331,6 +341,11 @@ pub async fn run(
let elapsed = if dry_run || skip {
Duration::new(0, 0)
+ } else if params_from_env {
+ conn.apply(&migration.process_parameters(&env_params)?)
+ .await?
+ } else if !params.is_empty() {
+ conn.apply(&migration.process_parameters(¶ms)?).await?
} else {
conn.apply(migration).await?
};
@@ -370,6 +385,8 @@ pub async fn revert(
dry_run: bool,
ignore_missing: bool,
target_version: Option,
+ params_from_env: bool,
+ parameters: Vec<(String, String)>,
) -> anyhow::Result<()> {
let migrator = Migrator::new(Path::new(migration_source)).await?;
if let Some(target_version) = target_version {
@@ -407,6 +424,15 @@ pub async fn revert(
.collect();
let mut is_applied = false;
+
+ let env_params: HashMap<_, _> = if params_from_env {
+ std::env::vars().collect()
+ } else {
+ HashMap::with_capacity(0)
+ };
+
+ let params: HashMap<_, _> = parameters.into_iter().collect();
+
for migration in migrator.iter().rev() {
if !migration.migration_type.is_down_migration() {
// Skipping non down migration
@@ -420,6 +446,11 @@ pub async fn revert(
let elapsed = if dry_run || skip {
Duration::new(0, 0)
+ } else if params_from_env {
+ conn.revert(&migration.process_parameters(&env_params)?)
+ .await?
+ } else if !params.is_empty() {
+ conn.revert(&migration.process_parameters(¶ms)?).await?
} else {
conn.revert(migration).await?
};
diff --git a/sqlx-cli/src/opt.rs b/sqlx-cli/src/opt.rs
index 133ba084f2..21c8667bd1 100644
--- a/sqlx-cli/src/opt.rs
+++ b/sqlx-cli/src/opt.rs
@@ -1,3 +1,4 @@
+use std::error::Error;
use std::ops::{Deref, Not};
use clap::{
@@ -190,6 +191,15 @@ pub enum MigrateCommand {
/// pending migrations. If already at the target version, then no-op.
#[clap(long)]
target_version: Option,
+
+ #[clap(long)]
+ /// Template parameters for substitution in migrations from environment variables
+ params_from_env: bool,
+
+ #[clap(long, short, value_parser = parse_key_val::, num_args = 1, value_delimiter=',')]
+ /// Provide template parameters for substitution in migrations, e.g. --params
+ /// key:value,key2:value2
+ params: Vec<(String, String)>,
},
/// Revert the latest migration with a down file.
@@ -212,6 +222,15 @@ pub enum MigrateCommand {
/// at the target version, then no-op.
#[clap(long)]
target_version: Option,
+
+ #[clap(long)]
+ /// Template parameters for substitution in migrations from environment variables
+ params_from_env: bool,
+
+ #[clap(long, short, value_parser = parse_key_val::, num_args = 1, value_delimiter=',')]
+ /// Provide template parameters for substitution in migrations, e.g. --params
+ /// key:value,key2:value2
+ params: Vec<(String, String)>,
},
/// List all available migrations.
@@ -334,3 +353,17 @@ impl Not for IgnoreMissing {
!self.ignore_missing
}
}
+
+/// Parse a single key-value pair
+fn parse_key_val(s: &str) -> Result<(T, U), Box>
+where
+ T: std::str::FromStr,
+ T::Err: Error + Send + Sync + 'static,
+ U: std::str::FromStr,
+ U::Err: Error + Send + Sync + 'static,
+{
+ let pos = s
+ .find('=')
+ .ok_or_else(|| format!("invalid KEY=value: no `=` found in `{s}`"))?;
+ Ok((s[..pos].parse()?, s[pos + 1..].parse()?))
+}
diff --git a/sqlx-core/Cargo.toml b/sqlx-core/Cargo.toml
index f6017a9fee..b1f1fd8dad 100644
--- a/sqlx-core/Cargo.toml
+++ b/sqlx-core/Cargo.toml
@@ -83,6 +83,7 @@ hashlink = "0.10.0"
indexmap = "2.0"
event-listener = "5.2.0"
hashbrown = "0.15.0"
+subst = { workspace = true }
[dev-dependencies]
sqlx = { workspace = true, features = ["postgres", "sqlite", "mysql", "migrate", "macros", "time", "uuid"] }
diff --git a/sqlx-core/src/migrate/error.rs b/sqlx-core/src/migrate/error.rs
index 608d55b18d..829f2bd2ed 100644
--- a/sqlx-core/src/migrate/error.rs
+++ b/sqlx-core/src/migrate/error.rs
@@ -39,4 +39,10 @@ pub enum MigrateError {
"migration {0} is partially applied; fix and remove row from `_sqlx_migrations` table"
)]
Dirty(i64),
+
+ #[error("migration {0} was missing a parameter '{1}' at line {2}, column {3}")]
+ MissingParameter(String, String, usize, usize),
+
+ #[error("Invalid parameter syntax {0}")]
+ InvalidParameterSyntax(String),
}
diff --git a/sqlx-core/src/migrate/migration.rs b/sqlx-core/src/migrate/migration.rs
index 9bd7f569d8..b0173a153f 100644
--- a/sqlx-core/src/migrate/migration.rs
+++ b/sqlx-core/src/migrate/migration.rs
@@ -1,8 +1,12 @@
use std::borrow::Cow;
+use std::collections::HashMap;
use sha2::{Digest, Sha384};
-use super::MigrationType;
+use super::{MigrateError, MigrationType};
+
+const ENABLE_SUBSTITUTION: &str = "-- enable-substitution";
+const DISABLE_SUBSTITUTION: &str = "-- disable-substitution";
#[derive(Debug, Clone)]
pub struct Migration {
@@ -23,7 +27,6 @@ impl Migration {
no_tx: bool,
) -> Self {
let checksum = Cow::Owned(Vec::from(Sha384::digest(sql.as_bytes()).as_slice()));
-
Migration {
version,
description,
@@ -33,6 +36,76 @@ impl Migration {
no_tx,
}
}
+
+ fn name(&self) -> String {
+ let description = self.description.replace(' ', "_");
+ match self.migration_type {
+ MigrationType::Simple => {
+ format!("{}_{}", self.version, description)
+ }
+ MigrationType::ReversibleUp => {
+ format!("{}_{}.{}", self.version, description, "up")
+ }
+ MigrationType::ReversibleDown => {
+ format!("{}_{}.{}", self.version, description, "down")
+ }
+ }
+ }
+
+ pub fn process_parameters(
+ &self,
+ params: &HashMap,
+ ) -> Result {
+ let Migration {
+ version,
+ description,
+ migration_type,
+ sql,
+ checksum,
+ no_tx,
+ } = self;
+
+ let mut new_sql = String::with_capacity(sql.len());
+ let mut substitution_enabled = false;
+
+ for (i, line) in sql.lines().enumerate() {
+ if i != 0 {
+ new_sql.push('\n')
+ }
+ let trimmed_line = line.trim();
+ if trimmed_line == ENABLE_SUBSTITUTION {
+ substitution_enabled = true;
+ new_sql.push_str(line);
+ continue;
+ } else if trimmed_line == DISABLE_SUBSTITUTION {
+ new_sql.push_str(line);
+ substitution_enabled = false;
+ continue;
+ }
+
+ if substitution_enabled {
+ let substituted_line = subst::substitute(line, params).map_err(|e| match e {
+ subst::Error::NoSuchVariable(subst::error::NoSuchVariable {
+ position,
+ name,
+ }) => MigrateError::MissingParameter(self.name(), name, i + 1, position),
+ _ => MigrateError::InvalidParameterSyntax(e.to_string()),
+ })?;
+ new_sql.push_str(&substituted_line);
+ } else {
+ new_sql.push_str(line);
+ }
+ }
+
+ Ok(Migration {
+ version: *version,
+ description: description.clone(),
+ migration_type: *migration_type,
+ sql: Cow::Owned(new_sql),
+ checksum: checksum.clone(),
+ no_tx: *no_tx,
+ })
+ }
}
#[derive(Debug, Clone)]
@@ -40,3 +113,133 @@ pub struct AppliedMigration {
pub version: i64,
pub checksum: Cow<'static, [u8]>,
}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn test_migration_process_parameters_with_substitution() -> Result<(), MigrateError> {
+ const CREATE_USER: &str = r#"
+ -- enable-substitution
+ CREATE USER '${substitution_test_user}';
+ -- disable-substitution
+ CREATE TABLE foo (
+ id BIG SERIAL PRIMARY KEY
+ foo TEXT
+ );
+ -- enable-substitution
+ DROP USER '${substitution_test_user}';
+ -- disable-substitution
+ "#;
+ const EXPECTED_RESULT: &str = r#"
+ -- enable-substitution
+ CREATE USER 'my_user';
+ -- disable-substitution
+ CREATE TABLE foo (
+ id BIG SERIAL PRIMARY KEY
+ foo TEXT
+ );
+ -- enable-substitution
+ DROP USER 'my_user';
+ -- disable-substitution
+ "#;
+
+ let migration = Migration::new(
+ 1,
+ Cow::Owned("test a simple parameter substitution".to_string()),
+ crate::migrate::MigrationType::Simple,
+ Cow::Owned(CREATE_USER.to_string()),
+ true,
+ );
+ let result = migration.process_parameters(&HashMap::from([(
+ String::from("substitution_test_user"),
+ String::from("my_user"),
+ )]))?;
+ assert_eq!(result.sql, EXPECTED_RESULT);
+ Ok(())
+ }
+
+ #[test]
+ fn test_migration_process_parameters_no_substitution() -> Result<(), MigrateError> {
+ const CREATE_TABLE: &str = r#"
+ CREATE TABLE foo (
+ id BIG SERIAL PRIMARY KEY
+ foo TEXT
+ );
+ "#;
+ let migration = Migration::new(
+ 1,
+ std::borrow::Cow::Owned("test a simple parameter substitution".to_string()),
+ crate::migrate::MigrationType::Simple,
+ Cow::Owned(CREATE_TABLE.to_string()),
+ true,
+ );
+ let result = migration.process_parameters(&HashMap::from([(
+ String::from("substitution_test_user"),
+ String::from("my_user"),
+ )]))?;
+ assert_eq!(result.sql, CREATE_TABLE);
+ Ok(())
+ }
+
+ #[test]
+ fn test_migration_process_parameters_missing_key() -> Result<(), MigrateError> {
+ const CREATE_TABLE: &str = r#"
+ -- enable-substitution
+ CREATE TABLE foo (
+ id BIG SERIAL PRIMARY KEY
+ foo TEXT,
+ field ${TEST_MISSING_KEY}
+ );
+ -- disable-substitution
+
+ "#;
+ let migration = Migration::new(
+ 1,
+ Cow::Owned("test a simple parameter substitution".to_string()),
+ crate::migrate::MigrationType::Simple,
+ Cow::Owned(CREATE_TABLE.to_string()),
+ true,
+ );
+ let Err(MigrateError::MissingParameter(..)) =
+ migration.process_parameters(&HashMap::with_capacity(0))
+ else {
+ panic!("Missing env var not caught in process parameters missing env var")
+ };
+ Ok(())
+ }
+
+ #[test]
+ fn test_migration_process_parameters_missing_key_with_default_value() -> Result<(), MigrateError>
+ {
+ const CREATE_TABLE: &str = r#"
+ -- enable-substitution
+ CREATE TABLE foo (
+ id BIG SERIAL PRIMARY KEY
+ foo TEXT,
+ field ${TEST_MISSING_KEY:TEXT}
+ );
+ -- disable-substitution
+ "#;
+ const EXPECTED_CREATE_TABLE: &str = r#"
+ -- enable-substitution
+ CREATE TABLE foo (
+ id BIG SERIAL PRIMARY KEY
+ foo TEXT,
+ field TEXT
+ );
+ -- disable-substitution
+ "#;
+ let migration = Migration::new(
+ 1,
+ Cow::Owned("test a simple parameter substitution".to_string()),
+ crate::migrate::MigrationType::Simple,
+ Cow::Owned(CREATE_TABLE.to_string()),
+ true,
+ );
+ let result = migration.process_parameters(&HashMap::with_capacity(0))?;
+ assert_eq!(result.sql, EXPECTED_CREATE_TABLE);
+ Ok(())
+ }
+}
diff --git a/sqlx-core/src/migrate/migrator.rs b/sqlx-core/src/migrate/migrator.rs
index 3209ba6e45..6100508350 100644
--- a/sqlx-core/src/migrate/migrator.rs
+++ b/sqlx-core/src/migrate/migrator.rs
@@ -23,6 +23,10 @@ pub struct Migrator {
pub locking: bool,
#[doc(hidden)]
pub no_tx: bool,
+ #[doc(hidden)]
+ pub template_params: Option>,
+ #[doc(hidden)]
+ pub template_parameters_from_env: bool,
}
fn validate_applied_migrations(
@@ -51,8 +55,33 @@ impl Migrator {
ignore_missing: false,
no_tx: false,
locking: true,
+ template_params: None,
+ template_parameters_from_env: false,
};
+ /// Set or update template parameters for migration placeholders.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// # use sqlx_core::migrate::Migrator;
+ /// let mut migrator = Migrator::DEFAULT;
+ /// migrator.set_template_parameters(vec![("key", "value"), ("name", "test")]);
+ /// ```
+ pub fn set_template_parameters(&mut self, params: I) -> &Self
+ where
+ I: IntoIterator- ,
+ K: Into,
+ V: Into,
+ {
+ let map: HashMap = params
+ .into_iter()
+ .map(|(k, v)| (k.into(), v.into()))
+ .collect();
+ self.template_params = Some(map);
+ self
+ }
+
/// Creates a new instance with the given source.
///
/// # Examples
@@ -87,6 +116,15 @@ impl Migrator {
self
}
+ /// Specify whether template parameters for migrations should be read from the environment
+ pub fn set_template_parameters_from_env(
+ &mut self,
+ template_paramaters_from_env: bool,
+ ) -> &Self {
+ self.template_parameters_from_env = template_paramaters_from_env;
+ self
+ }
+
/// Specify whether or not to lock the database during migration. Defaults to `true`.
///
/// ### Warning
@@ -165,6 +203,12 @@ impl Migrator {
.map(|m| (m.version, m))
.collect();
+ let env_params = if self.template_parameters_from_env {
+ Some(std::env::vars().collect())
+ } else {
+ None
+ };
+
for migration in self.iter() {
if migration.migration_type.is_down_migration() {
continue;
@@ -177,7 +221,14 @@ impl Migrator {
}
}
None => {
- conn.apply(migration).await?;
+ if self.template_parameters_from_env {
+ conn.apply(&migration.process_parameters(env_params.as_ref().unwrap())?)
+ .await?;
+ } else if let Some(params) = self.template_params.as_ref() {
+ conn.apply(&migration.process_parameters(params)?).await?;
+ } else {
+ conn.apply(migration).await?;
+ }
}
}
}
@@ -237,6 +288,12 @@ impl Migrator {
.map(|m| (m.version, m))
.collect();
+ let env_params = if self.template_parameters_from_env {
+ Some(std::env::vars().collect())
+ } else {
+ None
+ };
+
for migration in self
.iter()
.rev()
@@ -244,7 +301,14 @@ impl Migrator {
.filter(|m| applied_migrations.contains_key(&m.version))
.filter(|m| m.version > target)
{
- conn.revert(migration).await?;
+ if self.template_parameters_from_env {
+ conn.revert(&migration.process_parameters(env_params.as_ref().unwrap())?)
+ .await?;
+ } else if let Some(params) = self.template_params.as_ref() {
+ conn.revert(&migration.process_parameters(params)?).await?;
+ } else {
+ conn.revert(migration).await?;
+ }
}
// unlock the migrator to allow other migrators to run
diff --git a/sqlx-macros-core/Cargo.toml b/sqlx-macros-core/Cargo.toml
index 07d9d78862..41ad2748e0 100644
--- a/sqlx-macros-core/Cargo.toml
+++ b/sqlx-macros-core/Cargo.toml
@@ -64,6 +64,7 @@ proc-macro2 = { version = "1.0.79", default-features = false }
serde = { version = "1.0.132", features = ["derive"] }
serde_json = { version = "1.0.73" }
sha2 = { version = "0.10.0" }
+subst = { workspace = true }
syn = { version = "2.0.52", default-features = false, features = ["full", "derive", "parsing", "printing", "clone-impls"] }
quote = { version = "1.0.26", default-features = false }
url = { version = "2.2.2" }
diff --git a/sqlx-macros-core/src/migrate.rs b/sqlx-macros-core/src/migrate.rs
index c9cf5b8eb1..38cd481cf2 100644
--- a/sqlx-macros-core/src/migrate.rs
+++ b/sqlx-macros-core/src/migrate.rs
@@ -1,6 +1,7 @@
#[cfg(any(sqlx_macros_unstable, procmacro2_semver_exempt))]
extern crate proc_macro;
+use std::collections::HashMap;
use std::path::{Path, PathBuf};
use proc_macro2::TokenStream;
@@ -81,20 +82,26 @@ impl ToTokens for QuoteMigration {
}
}
-pub fn expand_migrator_from_lit_dir(dir: LitStr) -> crate::Result {
- expand_migrator_from_dir(&dir.value(), dir.span())
+pub fn expand_migrator_from_lit_dir(
+ dir: LitStr,
+ parameters: Option>,
+) -> crate::Result {
+ expand_migrator_from_dir(&dir.value(), dir.span(), parameters)
}
pub(crate) fn expand_migrator_from_dir(
dir: &str,
err_span: proc_macro2::Span,
+ parameters: Option>,
) -> crate::Result {
let path = crate::common::resolve_path(dir, err_span)?;
-
- expand_migrator(&path)
+ expand_migrator(&path, parameters)
}
-pub(crate) fn expand_migrator(path: &Path) -> crate::Result {
+pub(crate) fn expand_migrator(
+ path: &Path,
+ parameters: Option>,
+) -> crate::Result {
let path = path.canonicalize().map_err(|e| {
format!(
"error canonicalizing migration directory {}: {e}",
@@ -105,7 +112,14 @@ pub(crate) fn expand_migrator(path: &Path) -> crate::Result {
// Use the same code path to resolve migrations at compile time and runtime.
let migrations = sqlx_core::migrate::resolve_blocking(&path)?
.into_iter()
- .map(|(migration, path)| QuoteMigration { migration, path });
+ .map(|(migration, path)| {
+ if let Some(ref params) = parameters {
+ if let Err(e) = migration.process_parameters(params) {
+ panic!("Error processing parameters: {e}");
+ }
+ }
+ QuoteMigration { migration, path }
+ });
#[cfg(any(sqlx_macros_unstable, procmacro2_semver_exempt))]
{
diff --git a/sqlx-macros-core/src/test_attr.rs b/sqlx-macros-core/src/test_attr.rs
index d7c6eb0486..ee70d17fa5 100644
--- a/sqlx-macros-core/src/test_attr.rs
+++ b/sqlx-macros-core/src/test_attr.rs
@@ -143,7 +143,7 @@ fn expand_advanced(args: AttributeArgs, input: syn::ItemFn) -> crate::Result {
- let migrator = crate::migrate::expand_migrator_from_lit_dir(path)?;
+ let migrator = crate::migrate::expand_migrator_from_lit_dir(path, None)?;
quote! { args.migrator(migrator); }
}
MigrationsOpt::InferredPath if !inputs.is_empty() => {
@@ -151,7 +151,7 @@ fn expand_advanced(args: AttributeArgs, input: syn::ItemFn) -> crate::Result TokenStream {
#[cfg(feature = "migrate")]
#[proc_macro]
pub fn migrate(input: TokenStream) -> TokenStream {
- use syn::LitStr;
+ use std::collections::HashMap;
+ use syn::{parse_macro_input, Expr, ExprArray, ExprLit, ExprPath, ExprTuple, Lit, LitStr};
- let input = syn::parse_macro_input!(input as LitStr);
- match migrate::expand_migrator_from_lit_dir(input) {
+ // Extract directory path, handling both direct literals and grouped literals
+ fn extract_dir(expr: Option) -> LitStr {
+ match expr {
+ Some(Expr::Lit(ExprLit {
+ lit: Lit::Str(literal),
+ ..
+ })) => return literal,
+ Some(Expr::Group(group)) => {
+ if let Expr::Lit(ExprLit {
+ lit: Lit::Str(literal),
+ ..
+ }) = *group.expr
+ {
+ return literal;
+ }
+ }
+ _ => {}
+ }
+ panic!("Expected a string literal for the directory path.");
+ }
+
+ // Extract a `String` value from an expression (either a string literal or a variable)
+ fn extract_value(expr: Expr, location: &str) -> String {
+ match expr {
+ Expr::Lit(ExprLit {
+ lit: Lit::Str(lit_str),
+ ..
+ }) => lit_str.value(),
+ Expr::Path(ExprPath { path, .. }) => path.segments.last().unwrap().ident.to_string(),
+ _ => panic!("Expected a string literal or a variable in {location}"),
+ }
+ }
+
+ // Parse substitutions, expecting an array of tuples (String, Expr)
+ fn parse_substitutions(expr: Option) -> Option> {
+ let Expr::Group(group) = expr? else {
+ return None;
+ };
+ let Expr::Array(ExprArray { elems, .. }) = *group.expr else {
+ panic!("Expected an array of tuples (String, Expr).");
+ };
+
+ let mut map = HashMap::new();
+ for elem in elems {
+ let Expr::Tuple(ExprTuple {
+ elems: tuple_elems, ..
+ }) = elem
+ else {
+ panic!("Expected a tuple (String, Expr). Got {:#?}", elem);
+ };
+
+ let mut tuple_elems = tuple_elems.into_iter();
+
+ let key = extract_value(tuple_elems.next().expect("Missing key in tuple."), "key");
+ let value = extract_value(
+ tuple_elems.next().expect("Missing value in tuple."),
+ "value",
+ );
+ map.insert(key, value);
+ }
+ Some(map)
+ }
+
+ // Parse input and extract directory and optional parameters
+ let exp = parse_macro_input!(input as syn::Expr);
+ let (dir, parameters) = match exp {
+ Expr::Tuple(ExprTuple { elems, .. }) => {
+ let mut elems = elems.into_iter();
+ (extract_dir(elems.next()), elems.next())
+ }
+ Expr::Lit(ExprLit {
+ lit: Lit::Str(lit_str),
+ ..
+ }) => {
+ (lit_str, None)
+ }
+ Expr::Group(group) => {
+ if let Expr::Lit(ExprLit {
+ lit: Lit::Str(lit_str),
+ ..
+ }) = *group.expr
+ {
+ (lit_str, None)
+ } else {
+ panic!("Expected a tuple with directory path and optional parameters, or a string literal for the directory path.");
+ }
+ },
+ _ => panic!(
+ "Expected a tuple with directory path and optional parameters, or a string literal for the directory path."
+ ),
+ };
+
+ // Parse substitutions and pass to migration expander
+ let substitutions = parse_substitutions(parameters);
+ match migrate::expand_migrator_from_lit_dir(dir, substitutions) {
Ok(ts) => ts.into(),
Err(e) => {
if let Some(parse_err) = e.downcast_ref::() {
diff --git a/src/macros/mod.rs b/src/macros/mod.rs
index 7f8ff747f9..aecc60c957 100644
--- a/src/macros/mod.rs
+++ b/src/macros/mod.rs
@@ -809,11 +809,16 @@ macro_rules! query_file_scalar_unchecked (
#[cfg(feature = "migrate")]
#[macro_export]
macro_rules! migrate {
- ($dir:literal) => {{
- $crate::sqlx_macros::migrate!($dir)
- }};
-
- () => {{
+ ($directory:literal, parameters = $parameters:expr) => {
+ $crate::sqlx_macros::migrate!(($directory, $parameters));
+ };
+ (parameters = $parameters:expr) => {
+ $crate::sqlx_macros::migrate!(("./migrations", $parameters));
+ };
+ ($dir:literal) => {
+ $crate::sqlx_macros::migrate!($dir);
+ };
+ () => {
$crate::sqlx_macros::migrate!("./migrations")
- }};
+ };
}
diff --git a/src/macros/test.md b/src/macros/test.md
index 30de8070f6..1a3e1c581d 100644
--- a/src/macros/test.md
+++ b/src/macros/test.md
@@ -133,7 +133,9 @@ use sqlx::{PgPool, Row};
# migrations: Cow::Borrowed(&[]),
# ignore_missing: false,
# locking: true,
-# no_tx: false
+# no_tx: false,
+# template_params: None,
+# template_parameters_from_env: false,
# };
# }
diff --git a/tests/ui-tests.rs b/tests/ui-tests.rs
index 4a5ca240e1..fbdd14aa6f 100644
--- a/tests/ui-tests.rs
+++ b/tests/ui-tests.rs
@@ -44,3 +44,12 @@ fn ui_tests() {
t.compile_fail("tests/ui/*.rs");
}
+
+#[test]
+fn ui_migrate_tests() {
+ if cfg!(feature = "migrate") {
+ let t = trybuild::TestCases::new();
+ t.compile_fail("tests/ui/migrate/invalid_key.rs");
+ t.compile_fail("tests/ui/migrate/missing_parameter.rs");
+ }
+}
diff --git a/tests/ui/migrate/invalid_key.rs b/tests/ui/migrate/invalid_key.rs
new file mode 100644
index 0000000000..3edfaa8aa0
--- /dev/null
+++ b/tests/ui/migrate/invalid_key.rs
@@ -0,0 +1,4 @@
+fn main() {
+ //Fails due to invalid key
+ sqlx::migrate!("foo", parameters = [(123, "foo")]);
+}
diff --git a/tests/ui/migrate/invalid_key.stderr b/tests/ui/migrate/invalid_key.stderr
new file mode 100644
index 0000000000..f92b52d11b
--- /dev/null
+++ b/tests/ui/migrate/invalid_key.stderr
@@ -0,0 +1,8 @@
+error: proc macro panicked
+ --> tests/ui/migrate/invalid_key.rs:3:5
+ |
+3 | sqlx::migrate!("foo", parameters = [(123, "foo")]);
+ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ |
+ = help: message: Expected a string literal or a variable in key
+ = note: this error originates in the macro `sqlx::migrate` (in Nightly builds, run with -Z macro-backtrace for more info)
diff --git a/tests/ui/migrate/migrations/20250423195520_create_users.down.sql b/tests/ui/migrate/migrations/20250423195520_create_users.down.sql
new file mode 100644
index 0000000000..d2f607c5b8
--- /dev/null
+++ b/tests/ui/migrate/migrations/20250423195520_create_users.down.sql
@@ -0,0 +1 @@
+-- Add down migration script here
diff --git a/tests/ui/migrate/migrations/20250423195520_create_users.up.sql b/tests/ui/migrate/migrations/20250423195520_create_users.up.sql
new file mode 100644
index 0000000000..fc82f2cb9c
--- /dev/null
+++ b/tests/ui/migrate/migrations/20250423195520_create_users.up.sql
@@ -0,0 +1,4 @@
+-- Add up migration script here
+-- enable-substitution
+CREATE USER ${my_user} WITH ENCRYPTED PASSWORD '${my_password}' INHERIT;
+-- disable-substitution
diff --git a/tests/ui/migrate/missing_parameter.rs b/tests/ui/migrate/missing_parameter.rs
new file mode 100644
index 0000000000..a440b829ce
--- /dev/null
+++ b/tests/ui/migrate/missing_parameter.rs
@@ -0,0 +1,5 @@
+fn main() {
+ //Fails due to missing migration parameter
+ let _shaggy = "shaggy";
+ sqlx::migrate!("../../../../tests/ui/migrate/migrations", parameters = [("my_user", "scooby"), ("fooby", _shaggy)]);
+}
diff --git a/tests/ui/migrate/missing_parameter.stderr b/tests/ui/migrate/missing_parameter.stderr
new file mode 100644
index 0000000000..0717bc64c4
--- /dev/null
+++ b/tests/ui/migrate/missing_parameter.stderr
@@ -0,0 +1,8 @@
+error: proc macro panicked
+ --> tests/ui/migrate/missing_parameter.rs:4:5
+ |
+4 | sqlx::migrate!("../../../../tests/ui/migrate/migrations", parameters = [("my_user", "scooby"), ("fooby", _shaggy)]);
+ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ |
+ = help: message: Error processing parameters: migration 20250423195520_create_users.up was missing a parameter 'my_password' at line 3, column 50
+ = note: this error originates in the macro `sqlx::migrate` (in Nightly builds, run with -Z macro-backtrace for more info)