From e5e9665bd9f84dc9a1e016a8ce1cdf120c9bd7c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Th=C3=A9riault?= Date: Thu, 23 Jul 2020 14:22:50 -0400 Subject: [PATCH] Add migrate! macro for embedded migrations --- Cargo.toml | 2 +- sqlx-cli/src/migrate.rs | 4 +- sqlx-core/src/migrate/migrator.rs | 2 +- sqlx-macros/Cargo.toml | 3 +- sqlx-macros/src/lib.rs | 26 +++++++++ sqlx-macros/src/migrate.rs | 91 +++++++++++++++++++++++++++++++ src/lib.rs | 4 ++ 7 files changed, 127 insertions(+), 5 deletions(-) create mode 100644 sqlx-macros/src/migrate.rs diff --git a/Cargo.toml b/Cargo.toml index e5706e39..fd37a64d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,7 +39,7 @@ rustdoc-args = ["--cfg", "docsrs"] [features] default = [ "macros", "runtime-async-std", "migrate" ] macros = [ "sqlx-macros" ] -migrate = [ "sqlx-core/migrate" ] +migrate = [ "sqlx-macros/migrate", "sqlx-core/migrate" ] # [deprecated] TLS is not possible to disable due to it being conditional on multiple features # Hopefully Cargo can handle this in the future diff --git a/sqlx-cli/src/migrate.rs b/sqlx-cli/src/migrate.rs index 45dead33..08dccf82 100644 --- a/sqlx-cli/src/migrate.rs +++ b/sqlx-cli/src/migrate.rs @@ -50,7 +50,7 @@ pub async fn info(uri: &str) -> anyhow::Result<()> { } else { style("pending").yellow() }, - migration.description(), + migration.description, ); } @@ -77,7 +77,7 @@ pub async fn run(uri: &str) -> anyhow::Result<()> { "{}/{} {} {}", style(migration.version()).cyan(), style("migrate").green(), - migration.description(), + migration.description, style(format!("({:?})", elapsed)).dim() ); } else { diff --git a/sqlx-core/src/migrate/migrator.rs b/sqlx-core/src/migrate/migrator.rs index 33491e4e..916a8df0 100644 --- a/sqlx-core/src/migrate/migrator.rs +++ b/sqlx-core/src/migrate/migrator.rs @@ -71,7 +71,7 @@ impl Migrator { } for migration in self.iter() { - if migration.version() > version { + if migration.version > version { conn.apply(migration).await?; } else { conn.validate(migration).await?; diff --git a/sqlx-macros/Cargo.toml b/sqlx-macros/Cargo.toml index 0566655d..28c7fecb 100644 --- a/sqlx-macros/Cargo.toml +++ b/sqlx-macros/Cargo.toml @@ -16,7 +16,8 @@ authors = [ proc-macro = true [features] -default = [ "runtime-async-std" ] +default = [ "runtime-async-std", "migrate" ] +migrate = [ "sha2" ] # runtimes runtime-async-std = [ "sqlx-core/runtime-async-std", "sqlx-rt/runtime-async-std" ] diff --git a/sqlx-macros/src/lib.rs b/sqlx-macros/src/lib.rs index e7d848d3..d1d64cec 100644 --- a/sqlx-macros/src/lib.rs +++ b/sqlx-macros/src/lib.rs @@ -16,6 +16,9 @@ mod database; mod derives; mod query; +#[cfg(feature = "migrate")] +mod migrate; + fn macro_result(tokens: proc_macro2::TokenStream) -> TokenStream { quote!( macro_rules! macro_result { @@ -79,6 +82,29 @@ pub fn derive_from_row(input: TokenStream) -> TokenStream { } } +#[cfg(feature = "migrate")] +#[proc_macro] +pub fn migrate(input: TokenStream) -> TokenStream { + use syn::LitStr; + + let input = syn::parse_macro_input!(input as Option); + let dir = input + .as_ref() + .map_or("migrations".to_owned(), LitStr::value); + + match migrate::expand_migrator_from_dir(dir) { + Ok(ts) => ts.into(), + Err(e) => { + if let Some(parse_err) = e.downcast_ref::() { + macro_result(parse_err.to_compile_error()) + } else { + let msg = e.to_string(); + macro_result(quote!(compile_error!(#msg))) + } + } + } +} + #[doc(hidden)] #[proc_macro_attribute] pub fn test(_attr: TokenStream, input: TokenStream) -> TokenStream { diff --git a/sqlx-macros/src/migrate.rs b/sqlx-macros/src/migrate.rs new file mode 100644 index 00000000..1b1ac224 --- /dev/null +++ b/sqlx-macros/src/migrate.rs @@ -0,0 +1,91 @@ +use proc_macro2::TokenStream; +use quote::{quote, ToTokens, TokenStreamExt}; +use sha2::{Digest, Sha384}; +use std::fs; +use std::fs::DirEntry; +use std::path::Path; + +struct QuotedMigration { + version: i64, + description: String, + sql: String, + checksum: Vec, +} + +impl ToTokens for QuotedMigration { + fn to_tokens(&self, tokens: &mut TokenStream) { + let QuotedMigration { + version, + description, + sql, + checksum, + } = &self; + + let ts = quote! { + sqlx::migrate::Migration { + version: #version, + description: std::borrow::Cow::Borrowed(#description), + sql: std::borrow::Cow::Borrowed(#sql), + checksum: std::borow::Cow::Borrowed(&[ + #(#checksum),* + ]), + } + }; + + tokens.append_all(ts.into_iter()); + } +} + +// mostly copied from sqlx-core/src/migrate/source.rs +pub(crate) fn expand_migrator_from_dir>( + dir: P, +) -> crate::Result { + let mut s = fs::read_dir(dir.as_ref().canonicalize()?)?; + let mut migrations = Vec::new(); + + while let Some(entry) = s.next() { + let entry = entry?; + if !entry.metadata()?.is_file() { + // not a file; ignore + continue; + } + + let file_name = entry.file_name(); + let file_name = file_name.to_string_lossy(); + + let parts = file_name.splitn(2, '_').collect::>(); + + if parts.len() != 2 || !parts[1].ends_with(".sql") { + // not of the format: _.sql; ignore + continue; + } + + let version: i64 = parts[0].parse()?; + + // remove the `.sql` and replace `_` with ` ` + let description = parts[1] + .trim_end_matches(".sql") + .replace('_', " ") + .to_owned(); + + let sql = fs::read_to_string(&entry.path())?; + + let checksum = Vec::from(Sha384::digest(sql.as_bytes()).as_slice()); + + migrations.push(QuotedMigration { + version, + description, + sql, + checksum, + }) + } + + // ensure that we are sorted by `VERSION ASC` + migrations.sort_by_key(|m| m.version); + + Ok(quote! { + sqlx::migrate::Migrator::from_static(&[ + #(#migrations),* + ]) + }) +} diff --git a/src/lib.rs b/src/lib.rs index 125342ba..53b01bd9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -61,6 +61,10 @@ pub extern crate sqlx_macros; #[doc(hidden)] pub use sqlx_macros::{FromRow, Type}; +// embedded migrations +#[cfg(all(feature = "migrate", features = "macros"))] +pub use sqlx_macros::migrate; + #[cfg(feature = "macros")] mod macros;