Skip to content

Commit

Permalink
fix(macros): tell the compiler about external files/env vars to watch (
Browse files Browse the repository at this point in the history
…#1332)

* fix(macros): tell the compiler about external files/env vars to watch

closes #663
closes #681

* feat(cli): add `migrate` subcommand for generating a build script

suggest embedding migrations on `sqlx migrate add` in a new project
  • Loading branch information
abonander committed Jul 21, 2021
1 parent a8544fd commit e89cb09
Show file tree
Hide file tree
Showing 12 changed files with 259 additions and 31 deletions.
1 change: 1 addition & 0 deletions sqlx-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ pub async fn run(opt: Opt) -> anyhow::Result<()> {
ignore_missing,
} => migrate::revert(&migrate.source, &database_url, dry_run, ignore_missing).await?,
MigrateCommand::Info => migrate::info(&migrate.source, &database_url).await?,
MigrateCommand::BuildScript { force } => migrate::build_script(&migrate.source, force)?,
},

Command::Database(database) => match database.command {
Expand Down
57 changes: 57 additions & 0 deletions sqlx-cli/src/migrate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ pub async fn add(
) -> anyhow::Result<()> {
fs::create_dir_all(migration_source).context("Unable to create migrations directory")?;

// if the migrations directory is empty
let has_existing_migrations = fs::read_dir(migration_source)
.map(|mut dir| dir.next().is_some())
.unwrap_or(false);

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
for migration in migrator.iter() {
Expand Down Expand Up @@ -74,6 +79,31 @@ pub async fn add(
)?;
}

if !has_existing_migrations {
let quoted_source = if migration_source != "migrations" {
format!("{:?}", migration_source)
} else {
"".to_string()
};

print!(
r#"
Congratulations on creating your first migration!
Did you know you can embed your migrations in your application binary?
On startup, after creating your database connection or pool, add:
sqlx::migrate!({}).run(<&your_pool OR &mut your_connection>).await?;
Note that the compiler won't pick up new migrations if no Rust source files have changed.
You can create a Cargo build script to work around this with `sqlx migrate build-script`.
See: https://docs.rs/sqlx/0.5/sqlx/macro.migrate.html
"#,
quoted_source
);
}

Ok(())
}

Expand Down Expand Up @@ -245,3 +275,30 @@ pub async fn revert(

Ok(())
}

pub fn build_script(migration_source: &str, force: bool) -> anyhow::Result<()> {
anyhow::ensure!(
Path::new("Cargo.toml").exists(),
"must be run in a Cargo project root"
);

anyhow::ensure!(
(force || !Path::new("build.rs").exists()),
"build.rs already exists; use --force to overwrite"
);

let contents = format!(
r#"// generated by `sqlx migrate build-script`
fn main() {{
// trigger recompilation when a new migration is added
println!("cargo:rerun-if-changed={}");
}}"#,
migration_source
);

fs::write("build.rs", contents)?;

println!("Created `build.rs`; be sure to check it into version control!");

Ok(())
}
9 changes: 9 additions & 0 deletions sqlx-cli/src/opt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,13 @@ pub enum MigrateCommand {

/// List all available migrations.
Info,

/// Generate a `build.rs` to trigger recompilation when a new migration is added.
///
/// Must be run in a Cargo project root.
BuildScript {
/// Overwrite the build script if it already exists.
#[clap(long)]
force: bool,
},
}
4 changes: 4 additions & 0 deletions sqlx-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
not(any(feature = "postgres", feature = "mysql", feature = "offline")),
allow(dead_code, unused_macros, unused_imports)
)]
#![cfg_attr(
any(sqlx_macros_unstable, procmacro2_semver_exempt),
feature(track_path, proc_macro_tracked_env)
)]
extern crate proc_macro;

use proc_macro::TokenStream;
Expand Down
36 changes: 31 additions & 5 deletions sqlx-macros/src/migrate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ struct QuotedMigration {
version: i64,
description: String,
migration_type: QuotedMigrationType,
sql: String,
path: String,
checksum: Vec<u8>,
}

Expand All @@ -34,7 +34,7 @@ impl ToTokens for QuotedMigration {
version,
description,
migration_type,
sql,
path,
checksum,
} = &self;

Expand All @@ -43,7 +43,8 @@ impl ToTokens for QuotedMigration {
version: #version,
description: ::std::borrow::Cow::Borrowed(#description),
migration_type: #migration_type,
sql: ::std::borrow::Cow::Borrowed(#sql),
// this tells the compiler to watch this path for changes
sql: ::std::borrow::Cow::Borrowed(include_str!(#path)),
checksum: ::std::borrow::Cow::Borrowed(&[
#(#checksum),*
]),
Expand All @@ -59,7 +60,7 @@ pub(crate) fn expand_migrator_from_dir(dir: LitStr) -> crate::Result<TokenStream
let path = crate::common::resolve_path(&dir.value(), dir.span())?;
let mut migrations = Vec::new();

for entry in fs::read_dir(path)? {
for entry in fs::read_dir(&path)? {
let entry = entry?;
if !fs::metadata(entry.path())?.is_file() {
// not a file; ignore
Expand Down Expand Up @@ -89,18 +90,43 @@ pub(crate) fn expand_migrator_from_dir(dir: LitStr) -> crate::Result<TokenStream

let checksum = Vec::from(Sha384::digest(sql.as_bytes()).as_slice());

// canonicalize the path so we can pass it to `include_str!()`
let path = entry.path().canonicalize()?;
let path = path
.to_str()
.ok_or_else(|| {
format!(
"migration path cannot be represented as a string: {:?}",
path
)
})?
.to_owned();

migrations.push(QuotedMigration {
version,
description,
migration_type: QuotedMigrationType(migration_type),
sql,
path,
checksum,
})
}

// ensure that we are sorted by `VERSION ASC`
migrations.sort_by_key(|m| m.version);

#[cfg(any(sqlx_macros_unstable, procmacro2_semver_exempt))]
{
let path = path.canonicalize()?;
let path = path.to_str().ok_or_else(|| {
format!(
"migration directory path cannot be represented as a string: {:?}",
path
)
})?;

proc_macro::tracked_path::path(path);
}

Ok(quote! {
::sqlx::migrate::Migrator {
migrations: ::std::borrow::Cow::Borrowed(&[
Expand Down
20 changes: 17 additions & 3 deletions sqlx-macros/src/query/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,16 +61,30 @@ pub mod offline {
/// Find and deserialize the data table for this query from a shared `sqlx-data.json`
/// file. The expected structure is a JSON map keyed by the SHA-256 hash of queries in hex.
pub fn from_data_file(path: impl AsRef<Path>, query: &str) -> crate::Result<Self> {
serde_json::Deserializer::from_reader(BufReader::new(
let this = serde_json::Deserializer::from_reader(BufReader::new(
File::open(path.as_ref()).map_err(|e| {
format!("failed to open path {}: {}", path.as_ref().display(), e)
})?,
))
.deserialize_map(DataFileVisitor {
query,
hash: hash_string(query),
})
.map_err(Into::into)
})?;

#[cfg(procmacr2_semver_exempt)]
{
let path = path.as_ref().canonicalize()?;
let path = path.to_str().ok_or_else(|| {
format!(
"sqlx-data.json path cannot be represented as a string: {:?}",
path
)
})?;

proc_macro::tracked_path::path(path);
}

Ok(this)
}
}

Expand Down
30 changes: 28 additions & 2 deletions sqlx-macros/src/query/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use syn::{ExprArray, Type};

/// Macro input shared by `query!()` and `query_file!()`
pub struct QueryMacroInput {
pub(super) src: String,
pub(super) sql: String,

#[cfg_attr(not(feature = "offline"), allow(dead_code))]
pub(super) src_span: Span,
Expand All @@ -18,6 +18,8 @@ pub struct QueryMacroInput {
pub(super) arg_exprs: Vec<Expr>,

pub(super) checked: bool,

pub(super) file_path: Option<String>,
}

enum QuerySrc {
Expand Down Expand Up @@ -94,12 +96,15 @@ impl Parse for QueryMacroInput {

let arg_exprs = args.unwrap_or_default();

let file_path = src.file_path(src_span)?;

Ok(QueryMacroInput {
src: src.resolve(src_span)?,
sql: src.resolve(src_span)?,
src_span,
record_type,
arg_exprs,
checked,
file_path,
})
}
}
Expand All @@ -112,6 +117,27 @@ impl QuerySrc {
QuerySrc::File(file) => read_file_src(&file, source_span),
}
}

fn file_path(&self, source_span: Span) -> syn::Result<Option<String>> {
if let QuerySrc::File(ref file) = *self {
let path = std::path::Path::new(file)
.canonicalize()
.map_err(|e| syn::Error::new(source_span, e))?;

Ok(Some(
path.to_str()
.ok_or_else(|| {
syn::Error::new(
source_span,
"query file path cannot be represented as a string",
)
})?
.to_string(),
))
} else {
Ok(None)
}
}
}

fn read_file_src(source: &str, source_span: Span) -> syn::Result<String> {
Expand Down
Loading

0 comments on commit e89cb09

Please sign in to comment.