mirror of
https://github.com/launchbadge/sqlx.git
synced 2025-12-29 21:00:54 +00:00
Merge remote-tracking branch 'raftario/embedded-migrations'
This commit is contained in:
commit
f298eb3cf1
14
.github/workflows/sqlx.yml
vendored
14
.github/workflows/sqlx.yml
vendored
@ -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
|
||||
|
||||
11
Cargo.toml
11
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
|
||||
#
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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]>,
|
||||
}
|
||||
|
||||
@ -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?;
|
||||
|
||||
@ -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
37
sqlx-macros/src/common.rs
Normal 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))
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
96
sqlx-macros/src/migrate.rs
Normal file
96
sqlx-macros/src/migrate.rs
Normal 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),*
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -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(
|
||||
|
||||
@ -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
18
tests/migrate/macro.rs
Normal 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(())
|
||||
}
|
||||
6
tests/migrate/migrations/20200723212833_tweet.sql
Normal file
6
tests/migrate/migrations/20200723212833_tweet.sql
Normal 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
|
||||
);
|
||||
5
tests/migrate/migrations/20200723212841_accounts.sql
Normal file
5
tests/migrate/migrations/20200723212841_accounts.sql
Normal file
@ -0,0 +1,5 @@
|
||||
CREATE TABLE accounts (
|
||||
id INTEGER NOT NULL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
is_active BOOLEAN
|
||||
);
|
||||
Loading…
x
Reference in New Issue
Block a user