diff --git a/.github/workflows/sqlx.yml b/.github/workflows/sqlx.yml index de783b67..b5fa4417 100644 --- a/.github/workflows/sqlx.yml +++ b/.github/workflows/sqlx.yml @@ -48,14 +48,14 @@ jobs: args: > --manifest-path sqlx-core/Cargo.toml --no-default-features - --features offline,all-databases,all-types,runtime-${{ matrix.runtime }} + --features offline,all-databases,all-types,migrate,runtime-${{ matrix.runtime }} - uses: actions-rs/cargo@v1 with: command: check args: > --no-default-features - --features offline,all-databases,all-types,runtime-${{ matrix.runtime }},macros + --features offline,all-databases,all-types,migrate,runtime-${{ matrix.runtime }},macros test: name: Unit Test @@ -97,7 +97,7 @@ jobs: command: test args: > --no-default-features - --features any,macros,sqlite,all-types,runtime-${{ matrix.runtime }} + --features any,macros,migrate,sqlite,all-types,runtime-${{ matrix.runtime }} -- --test-threads=1 env: @@ -143,7 +143,7 @@ jobs: command: test args: > --no-default-features - --features any,postgres,macros,all-types,runtime-${{ matrix.runtime }} + --features any,postgres,macros,migrate,all-types,runtime-${{ matrix.runtime }} env: DATABASE_URL: postgres://postgres:password@localhost:5432/sqlx?sslmode=verify-ca&sslrootcert=.%2Ftests%2Fcerts%2Fca.crt @@ -178,7 +178,7 @@ jobs: command: test args: > --no-default-features - --features any,mysql,macros,all-types,runtime-${{ matrix.runtime }} + --features any,mysql,macros,migrate,all-types,runtime-${{ matrix.runtime }} env: DATABASE_URL: mysql://root:password@localhost:3306/sqlx @@ -213,7 +213,7 @@ jobs: command: test args: > --no-default-features - --features any,mysql,macros,all-types,runtime-${{ matrix.runtime }} + --features any,mysql,macros,migrate,all-types,runtime-${{ matrix.runtime }} env: DATABASE_URL: mysql://root:password@localhost:3306/sqlx @@ -248,6 +248,6 @@ jobs: command: test args: > --no-default-features - --features any,mssql,macros,all-types,runtime-${{ matrix.runtime }} + --features any,mssql,macros,migrate,all-types,runtime-${{ matrix.runtime }} env: DATABASE_URL: mssql://sa:Password123!@localhost/sqlx diff --git a/Cargo.toml b/Cargo.toml index 390f6910..cbc50aff 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 @@ -108,6 +108,15 @@ name = "any-pool" path = "tests/any/pool.rs" required-features = [ "any" ] +# +# Migrations +# + +[[test]] +name = "migrate-macro" +path = "tests/migrate/macro.rs" +required-features = [ "macros", "migrate" ] + # # SQLite # diff --git a/sqlx-cli/src/migrate.rs b/sqlx-cli/src/migrate.rs index 45dead33..8fe7d656 100644 --- a/sqlx-cli/src/migrate.rs +++ b/sqlx-cli/src/migrate.rs @@ -44,13 +44,13 @@ pub async fn info(uri: &str) -> anyhow::Result<()> { for migration in migrator.iter() { println!( "{}/{} {}", - style(migration.version()).cyan(), - if version >= migration.version() { + style(migration.version).cyan(), + if version >= migration.version { style("installed").green() } else { style("pending").yellow() }, - migration.description(), + migration.description, ); } @@ -70,14 +70,14 @@ pub async fn run(uri: &str) -> anyhow::Result<()> { } for migration in migrator.iter() { - if migration.version() > version { + if migration.version > version { let elapsed = conn.apply(migration).await?; println!( "{}/{} {} {}", - style(migration.version()).cyan(), + style(migration.version).cyan(), style("migrate").green(), - migration.description(), + migration.description, style(format!("({:?})", elapsed)).dim() ); } else { diff --git a/sqlx-core/src/migrate/migration.rs b/sqlx-core/src/migrate/migration.rs index 52396a11..0aec8ffb 100644 --- a/sqlx-core/src/migrate/migration.rs +++ b/sqlx-core/src/migrate/migration.rs @@ -2,18 +2,8 @@ use std::borrow::Cow; #[derive(Debug, Clone)] pub struct Migration { - pub(crate) version: i64, - pub(crate) description: Cow<'static, str>, - pub(crate) sql: Cow<'static, str>, - pub(crate) checksum: Cow<'static, [u8]>, -} - -impl Migration { - pub fn version(&self) -> i64 { - self.version - } - - pub fn description(&self) -> &str { - &*self.description - } + pub version: i64, + pub description: Cow<'static, str>, + pub sql: Cow<'static, str>, + pub checksum: Cow<'static, [u8]>, } diff --git a/sqlx-core/src/migrate/migrator.rs b/sqlx-core/src/migrate/migrator.rs index ba602a38..36b38423 100644 --- a/sqlx-core/src/migrate/migrator.rs +++ b/sqlx-core/src/migrate/migrator.rs @@ -1,11 +1,12 @@ use crate::acquire::Acquire; use crate::migrate::{Migrate, MigrateError, Migration, MigrationSource}; +use std::borrow::Cow; use std::ops::Deref; use std::slice; #[derive(Debug)] pub struct Migrator { - migrations: Vec, + pub migrations: Cow<'static, [Migration]>, } impl Migrator { @@ -31,7 +32,7 @@ impl Migrator { S: MigrationSource<'s>, { Ok(Self { - migrations: source.resolve().await.map_err(MigrateError::Source)?, + migrations: Cow::Owned(source.resolve().await.map_err(MigrateError::Source)?), }) } @@ -63,7 +64,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/common.rs b/sqlx-macros/src/common.rs new file mode 100644 index 00000000..1e4dc374 --- /dev/null +++ b/sqlx-macros/src/common.rs @@ -0,0 +1,37 @@ +use proc_macro2::Span; +use std::env; +use std::path::{Path, PathBuf}; + +pub(crate) fn resolve_path(path: &str, err_span: Span) -> syn::Result { + let path = Path::new(path); + + if path.is_absolute() { + return Err(syn::Error::new( + err_span, + "absolute paths will only work on the current machine", + )); + } + + // requires `proc_macro::SourceFile::path()` to be stable + // https://github.com/rust-lang/rust/issues/54725 + if path.is_relative() + && !path + .parent() + .map_or(false, |parent| !parent.as_os_str().is_empty()) + { + return Err(syn::Error::new( + err_span, + "paths relative to the current file's directory are not currently supported", + )); + } + + let base_dir = env::var("CARGO_MANIFEST_DIR").map_err(|_| { + syn::Error::new( + err_span, + "CARGO_MANIFEST_DIR is not set; please use Cargo to build", + ) + })?; + let base_dir_path = Path::new(&base_dir); + + Ok(base_dir_path.join(path)) +} diff --git a/sqlx-macros/src/lib.rs b/sqlx-macros/src/lib.rs index c6ac3b5b..36809ba6 100644 --- a/sqlx-macros/src/lib.rs +++ b/sqlx-macros/src/lib.rs @@ -15,6 +15,10 @@ type Result = std::result::Result; mod database; mod derives; mod query; +mod common; + +#[cfg(feature = "migrate")] +mod migrate; #[proc_macro] pub fn expand_query(input: TokenStream) -> TokenStream { @@ -70,6 +74,25 @@ 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 LitStr); + match migrate::expand_migrator_from_dir(input) { + 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..29e05308 --- /dev/null +++ b/sqlx-macros/src/migrate.rs @@ -0,0 +1,96 @@ +use proc_macro2::TokenStream; +use quote::{quote, ToTokens, TokenStreamExt}; +use sha2::{Digest, Sha384}; +use std::fs; +use syn::LitStr; + +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::borrow::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: LitStr) -> crate::Result { + let path = crate::common::resolve_path(&dir.value(), dir.span())?; + let mut s = fs::read_dir(path)?; + + 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! { + macro_rules! macro_result { + () => { + sqlx::migrate::Migrator { + migrations: std::borrow::Cow::Borrowed(&[ + #(#migrations),* + ]) + } + } + } + }) +} diff --git a/sqlx-macros/src/query/input.rs b/sqlx-macros/src/query/input.rs index b7a70699..ddcd6d4a 100644 --- a/sqlx-macros/src/query/input.rs +++ b/sqlx-macros/src/query/input.rs @@ -1,4 +1,3 @@ -use std::env; use std::fs; use proc_macro2::{Ident, Span}; @@ -101,40 +100,7 @@ impl QuerySrc { } fn read_file_src(source: &str, source_span: Span) -> syn::Result { - use std::path::Path; - - let path = Path::new(source); - - if path.is_absolute() { - return Err(syn::Error::new( - source_span, - "absolute paths will only work on the current machine", - )); - } - - // requires `proc_macro::SourceFile::path()` to be stable - // https://github.com/rust-lang/rust/issues/54725 - if path.is_relative() - && !path - .parent() - .map_or(false, |parent| !parent.as_os_str().is_empty()) - { - return Err(syn::Error::new( - source_span, - "paths relative to the current file's directory are not currently supported", - )); - } - - let base_dir = env::var("CARGO_MANIFEST_DIR").map_err(|_| { - syn::Error::new( - source_span, - "CARGO_MANIFEST_DIR is not set; please use Cargo to build", - ) - })?; - - let base_dir_path = Path::new(&base_dir); - - let file_path = base_dir_path.join(path); + let file_path = crate::common::resolve_path(source, source_span)?; fs::read_to_string(&file_path).map_err(|e| { syn::Error::new( diff --git a/src/macros.rs b/src/macros.rs index 563f4fa6..4393d9ce 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -579,3 +579,40 @@ macro_rules! query_file_as_unchecked ( $crate::sqlx_macros::expand_query!(record = $out_struct, source_file = $path, args = [$($args)*], checked = false) }) ); + +/// Embeds migrations into the binary by expanding to a static instance of [Migrator][crate::migrate::Migrator]. +/// +/// ```rust,ignore +/// sqlx::migrate!("db/migrations") +/// .run(&pool) +/// .await?; +/// ``` +/// +/// ```rust,ignore +/// use sqlx::migrate::Migrator; +/// +/// static MIGRATOR: Migrator = sqlx::migrate!(); // defaults to "migrations" +/// ``` +/// +/// The directory must be relative to the project root (the directory containing `Cargo.toml`), +/// unlike `include_str!()` which uses compiler internals to get the path of the file where it +/// was invoked. +#[cfg(feature = "migrate")] +#[macro_export] +macro_rules! migrate { + ($dir:literal) => {{ + #[macro_use] + mod _macro_result { + $crate::sqlx_macros::migrate!($dir); + } + macro_result!() + }}; + + () => {{ + #[macro_use] + mod _macro_result { + $crate::sqlx_macros::migrate!("migrations"); + } + macro_result!() + }}; +} diff --git a/tests/migrate/macro.rs b/tests/migrate/macro.rs new file mode 100644 index 00000000..9a3c1615 --- /dev/null +++ b/tests/migrate/macro.rs @@ -0,0 +1,18 @@ +use sqlx::migrate::Migrator; +use std::path::Path; + +static EMBEDDED: Migrator = sqlx::migrate!("tests/migrate/migrations"); + +#[sqlx_macros::test] +async fn same_output() -> anyhow::Result<()> { + let runtime = Migrator::new(Path::new("tests/migrate/migrations")).await?; + + for (e, r) in EMBEDDED.iter().zip(runtime.iter()) { + assert_eq!(e.version, r.version); + assert_eq!(e.description, r.description); + assert_eq!(e.sql, r.sql); + assert_eq!(e.checksum, r.checksum); + } + + Ok(()) +} diff --git a/tests/migrate/migrations/20200723212833_tweet.sql b/tests/migrate/migrations/20200723212833_tweet.sql new file mode 100644 index 00000000..45c09606 --- /dev/null +++ b/tests/migrate/migrations/20200723212833_tweet.sql @@ -0,0 +1,6 @@ +CREATE TABLE tweet ( + id BIGINT NOT NULL PRIMARY KEY, + text TEXT NOT NULL, + is_sent BOOLEAN NOT NULL DEFAULT TRUE, + owner_id BIGINT +); diff --git a/tests/migrate/migrations/20200723212841_accounts.sql b/tests/migrate/migrations/20200723212841_accounts.sql new file mode 100644 index 00000000..f2c0a739 --- /dev/null +++ b/tests/migrate/migrations/20200723212841_accounts.sql @@ -0,0 +1,5 @@ +CREATE TABLE accounts ( + id INTEGER NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + is_active BOOLEAN +);