Merge remote-tracking branch 'raftario/embedded-migrations'

This commit is contained in:
Ryan Leckey 2020-07-26 18:03:59 -07:00
commit f298eb3cf1
14 changed files with 256 additions and 67 deletions

View File

@ -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

View File

@ -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
#

View File

@ -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 {

View File

@ -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]>,
}

View File

@ -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<Migration>,
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?;

View File

@ -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" ]

37
sqlx-macros/src/common.rs Normal file
View File

@ -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<PathBuf> {
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))
}

View File

@ -15,6 +15,10 @@ type Result<T> = std::result::Result<T, Error>;
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::<syn::Error>() {
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 {

View File

@ -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<u8>,
}
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<proc_macro2::TokenStream> {
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::<Vec<_>>();
if parts.len() != 2 || !parts[1].ends_with(".sql") {
// not of the format: <VERSION>_<DESCRIPTION>.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),*
])
}
}
}
})
}

View File

@ -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<String> {
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(

View File

@ -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!()
}};
}

18
tests/migrate/macro.rs Normal file
View File

@ -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(())
}

View File

@ -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
);

View File

@ -0,0 +1,5 @@
CREATE TABLE accounts (
id INTEGER NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
is_active BOOLEAN
);