From 64f456a2b294f9f78e71edd52320f11a790b0aea Mon Sep 17 00:00:00 2001 From: quasi-coherent Date: Mon, 30 Sep 2024 07:06:33 -0400 Subject: [PATCH] Dynamic embedded migrations (#2) --- refinery/src/lib.rs | 2 +- refinery/tests/mysql.rs | 16 +- refinery/tests/mysql_async.rs | 16 +- refinery/tests/postgres.rs | 16 +- refinery/tests/rusqlite.rs | 16 +- refinery/tests/tiberius.rs | 16 +- refinery/tests/tokio_postgres.rs | 16 +- refinery_core/src/config.rs | 1 + refinery_core/src/drivers/config.rs | 14 +- refinery_core/src/drivers/mysql.rs | 5 +- refinery_core/src/drivers/mysql_async.rs | 6 +- refinery_core/src/drivers/postgres.rs | 5 +- refinery_core/src/drivers/rusqlite.rs | 9 +- refinery_core/src/drivers/sqlx_postgres.rs | 4 +- refinery_core/src/drivers/tiberius.rs | 6 +- refinery_core/src/drivers/tokio_postgres.rs | 5 +- .../async.rs => executor/async_exec.rs} | 13 +- .../src/{traits/sync.rs => executor/exec.rs} | 11 +- refinery_core/src/{traits => executor}/mod.rs | 10 +- refinery_core/src/lib.rs | 22 +- refinery_core/src/migration.rs | 296 ++++++++++++++++++ refinery_core/src/runner.rs | 231 +------------- refinery_core/src/util.rs | 30 +- refinery_macros/src/lib.rs | 87 ++++- 24 files changed, 557 insertions(+), 296 deletions(-) rename refinery_core/src/{traits/async.rs => executor/async_exec.rs} (98%) rename refinery_core/src/{traits/sync.rs => executor/exec.rs} (97%) rename refinery_core/src/{traits => executor}/mod.rs (98%) create mode 100644 refinery_core/src/migration.rs diff --git a/refinery/src/lib.rs b/refinery/src/lib.rs index 93cfaac5..fba195e6 100644 --- a/refinery/src/lib.rs +++ b/refinery/src/lib.rs @@ -34,5 +34,5 @@ for more examples refer to the [examples](https://github.com/rust-db/refinery/tr pub use refinery_core::config; pub use refinery_core::{error, load_sql_migrations, Error, Migration, Report, Runner, Target}; #[doc(hidden)] -pub use refinery_core::{AsyncMigrate, Migrate}; +pub use refinery_core::{AsyncExecutor, AsyncMigrate, Executor, Migrate}; pub use refinery_macros::embed_migrations; diff --git a/refinery/tests/mysql.rs b/refinery/tests/mysql.rs index a9c487bd..735c4e37 100644 --- a/refinery/tests/mysql.rs +++ b/refinery/tests/mysql.rs @@ -35,29 +35,37 @@ mod mysql { fn get_migrations() -> Vec { embed_migrations!("./tests/migrations"); - let migration1 = - Migration::unapplied("V1__initial.rs", &migrations::V1__initial::migration()).unwrap(); + let migration1 = Migration::unapplied( + "V1__initial.rs", + None, + &migrations::V1__initial::migration(), + ) + .unwrap(); let migration2 = Migration::unapplied( "V2__add_cars_and_motos_table.sql", + None, include_str!("./migrations/V1-2/V2__add_cars_and_motos_table.sql"), ) .unwrap(); let migration3 = Migration::unapplied( "V3__add_brand_to_cars_table", + None, include_str!("./migrations/V3/V3__add_brand_to_cars_table.sql"), ) .unwrap(); let migration4 = Migration::unapplied( "V4__add_year_to_motos_table.rs", + None, &migrations::V4__add_year_to_motos_table::migration(), ) .unwrap(); let migration5 = Migration::unapplied( "V5__add_year_field_to_cars", + None, "ALTER TABLE cars ADD year INTEGER;", ) .unwrap(); @@ -472,6 +480,7 @@ mod mysql { let migration = Migration::unapplied( "V4__add_year_field_to_cars", + None, "ALTER TABLE cars ADD year INTEGER;", ) .unwrap(); @@ -508,6 +517,7 @@ mod mysql { let migration = Migration::unapplied( "V2__add_year_field_to_cars", + None, "ALTER TABLE cars ADD year INTEGER;", ) .unwrap(); @@ -545,6 +555,7 @@ mod mysql { let migration1 = Migration::unapplied( "V1__initial", + None, concat!( "CREATE TABLE persons (", "id int,", @@ -557,6 +568,7 @@ mod mysql { let migration2 = Migration::unapplied( "V2__add_cars_table", + None, include_str!("./migrations_missing/V2__add_cars_table.sql"), ) .unwrap(); diff --git a/refinery/tests/mysql_async.rs b/refinery/tests/mysql_async.rs index 4b3e9cff..7dd1cf1d 100644 --- a/refinery/tests/mysql_async.rs +++ b/refinery/tests/mysql_async.rs @@ -19,29 +19,37 @@ mod mysql_async { fn get_migrations() -> Vec { embed_migrations!("./tests/migrations"); - let migration1 = - Migration::unapplied("V1__initial.rs", &migrations::V1__initial::migration()).unwrap(); + let migration1 = Migration::unapplied( + "V1__initial.rs", + None, + &migrations::V1__initial::migration(), + ) + .unwrap(); let migration2 = Migration::unapplied( "V2__add_cars_and_motos_table.sql", + None, include_str!("./migrations/V1-2/V2__add_cars_and_motos_table.sql"), ) .unwrap(); let migration3 = Migration::unapplied( "V3__add_brand_to_cars_table", + None, include_str!("./migrations/V3/V3__add_brand_to_cars_table.sql"), ) .unwrap(); let migration4 = Migration::unapplied( "V4__add_year_to_motos_table.rs", + None, &migrations::V4__add_year_to_motos_table::migration(), ) .unwrap(); let migration5 = Migration::unapplied( "V5__add_year_field_to_cars", + None, "ALTER TABLE cars ADD year INTEGER;", ) .unwrap(); @@ -482,6 +490,7 @@ mod mysql_async { let migration = Migration::unapplied( "V4__add_year_field_to_cars", + None, "ALTER TABLE cars ADD year INTEGER;", ) .unwrap(); @@ -527,6 +536,7 @@ mod mysql_async { let migration = Migration::unapplied( "V2__add_year_field_to_cars", + None, "ALTER TABLE cars ADD year INTEGER;", ) .unwrap(); @@ -568,6 +578,7 @@ mod mysql_async { let migration1 = Migration::unapplied( "V1__initial", + None, concat!( "CREATE TABLE persons (", "id int,", @@ -580,6 +591,7 @@ mod mysql_async { let migration2 = Migration::unapplied( "V2__add_cars_table", + None, include_str!("./migrations_missing/V2__add_cars_table.sql"), ) .unwrap(); diff --git a/refinery/tests/postgres.rs b/refinery/tests/postgres.rs index 59a263b8..215470c1 100644 --- a/refinery/tests/postgres.rs +++ b/refinery/tests/postgres.rs @@ -36,29 +36,37 @@ mod postgres { fn get_migrations() -> Vec { embed_migrations!("./tests/migrations"); - let migration1 = - Migration::unapplied("V1__initial.rs", &migrations::V1__initial::migration()).unwrap(); + let migration1 = Migration::unapplied( + "V1__initial.rs", + None, + &migrations::V1__initial::migration(), + ) + .unwrap(); let migration2 = Migration::unapplied( "V2__add_cars_and_motos_table.sql", + None, include_str!("./migrations/V1-2/V2__add_cars_and_motos_table.sql"), ) .unwrap(); let migration3 = Migration::unapplied( "V3__add_brand_to_cars_table", + None, include_str!("./migrations/V3/V3__add_brand_to_cars_table.sql"), ) .unwrap(); let migration4 = Migration::unapplied( "V4__add_year_to_motos_table.rs", + None, &migrations::V4__add_year_to_motos_table::migration(), ) .unwrap(); let migration5 = Migration::unapplied( "V5__add_year_field_to_cars", + None, "ALTER TABLE cars ADD year INTEGER;", ) .unwrap(); @@ -446,6 +454,7 @@ mod postgres { let migration = Migration::unapplied( "V4__add_year_field_to_cars", + None, "ALTER TABLE cars ADD year INTEGER;", ) .unwrap(); @@ -479,6 +488,7 @@ mod postgres { let migration = Migration::unapplied( "V2__add_year_field_to_cars", + None, "ALTER TABLE cars ADD year INTEGER;", ) .unwrap(); @@ -513,6 +523,7 @@ mod postgres { let migration1 = Migration::unapplied( "V1__initial", + None, concat!( "CREATE TABLE persons (", "id int,", @@ -525,6 +536,7 @@ mod postgres { let migration2 = Migration::unapplied( "V2__add_cars_table", + None, include_str!("./migrations_missing/V2__add_cars_table.sql"), ) .unwrap(); diff --git a/refinery/tests/rusqlite.rs b/refinery/tests/rusqlite.rs index 258af424..de51aae5 100644 --- a/refinery/tests/rusqlite.rs +++ b/refinery/tests/rusqlite.rs @@ -50,29 +50,37 @@ mod rusqlite { fn get_migrations() -> Vec { embed_migrations!("./tests/migrations"); - let migration1 = - Migration::unapplied("V1__initial.rs", &migrations::V1__initial::migration()).unwrap(); + let migration1 = Migration::unapplied( + "V1__initial.rs", + None, + &migrations::V1__initial::migration(), + ) + .unwrap(); let migration2 = Migration::unapplied( "V2__add_cars_and_motos_table.sql", + None, include_str!("./migrations/V1-2/V2__add_cars_and_motos_table.sql"), ) .unwrap(); let migration3 = Migration::unapplied( "V3__add_brand_to_cars_table", + None, include_str!("./migrations/V3/V3__add_brand_to_cars_table.sql"), ) .unwrap(); let migration4 = Migration::unapplied( "V4__add_year_to_motos_table.rs", + None, &migrations::V4__add_year_to_motos_table::migration(), ) .unwrap(); let migration5 = Migration::unapplied( "V5__add_year_field_to_cars", + None, "ALTER TABLE cars ADD year INTEGER;", ) .unwrap(); @@ -572,6 +580,7 @@ mod rusqlite { let migration = Migration::unapplied( "V4__add_year_field_to_cars", + None, "ALTER TABLE cars ADD year INTEGER;", ) .unwrap(); @@ -603,6 +612,7 @@ mod rusqlite { let migration = Migration::unapplied( "V2__add_year_field_to_cars", + None, "ALTER TABLE cars ADD year INTEGER;", ) .unwrap(); @@ -635,6 +645,7 @@ mod rusqlite { let migration1 = Migration::unapplied( "V1__initial", + None, concat!( "CREATE TABLE persons (", "id int,", @@ -647,6 +658,7 @@ mod rusqlite { let migration2 = Migration::unapplied( "V2__add_cars_table", + None, include_str!("./migrations_missing/V2__add_cars_table.sql"), ) .unwrap(); diff --git a/refinery/tests/tiberius.rs b/refinery/tests/tiberius.rs index f6346063..b6074409 100644 --- a/refinery/tests/tiberius.rs +++ b/refinery/tests/tiberius.rs @@ -22,29 +22,37 @@ mod tiberius { fn get_migrations() -> Vec { embed_migrations!("./tests/migrations"); - let migration1 = - Migration::unapplied("V1__initial.rs", &migrations::V1__initial::migration()).unwrap(); + let migration1 = Migration::unapplied( + "V1__initial.rs", + None, + &migrations::V1__initial::migration(), + ) + .unwrap(); let migration2 = Migration::unapplied( "V2__add_cars_and_motos_table.sql", + None, include_str!("./migrations/V1-2/V2__add_cars_and_motos_table.sql"), ) .unwrap(); let migration3 = Migration::unapplied( "V3__add_brand_to_cars_table", + None, include_str!("./migrations/V3/V3__add_brand_to_cars_table.sql"), ) .unwrap(); let migration4 = Migration::unapplied( "V4__add_year_to_motos_table.rs", + None, &migrations::V4__add_year_to_motos_table::migration(), ) .unwrap(); let migration5 = Migration::unapplied( "V5__add_year_field_to_cars", + None, "ALTER TABLE cars ADD year INTEGER;", ) .unwrap(); @@ -127,6 +135,7 @@ mod tiberius { let migration = Migration::unapplied( "V4__add_year_field_to_cars", + None, "ALTER TABLE cars ADD year INTEGER;", ) .unwrap(); @@ -178,6 +187,7 @@ mod tiberius { let migration = Migration::unapplied( "V2__add_year_field_to_cars", + None, "ALTER TABLE cars ADD year INTEGER;", ) .unwrap(); @@ -230,6 +240,7 @@ mod tiberius { let migration1 = Migration::unapplied( "V1__initial", + None, concat!( "CREATE TABLE persons (", "id int,", @@ -242,6 +253,7 @@ mod tiberius { let migration2 = Migration::unapplied( "V2__add_cars_table", + None, include_str!("./migrations_missing/V2__add_cars_table.sql"), ) .unwrap(); diff --git a/refinery/tests/tokio_postgres.rs b/refinery/tests/tokio_postgres.rs index 2f7f7fc6..d092b160 100644 --- a/refinery/tests/tokio_postgres.rs +++ b/refinery/tests/tokio_postgres.rs @@ -19,29 +19,37 @@ mod tokio_postgres { fn get_migrations() -> Vec { embed_migrations!("./tests/migrations"); - let migration1 = - Migration::unapplied("V1__initial.rs", &migrations::V1__initial::migration()).unwrap(); + let migration1 = Migration::unapplied( + "V1__initial.rs", + None, + &migrations::V1__initial::migration(), + ) + .unwrap(); let migration2 = Migration::unapplied( "V2__add_cars_and_motos_table.sql", + None, include_str!("./migrations/V1-2/V2__add_cars_and_motos_table.sql"), ) .unwrap(); let migration3 = Migration::unapplied( "V3__add_brand_to_cars_table", + None, include_str!("./migrations/V3/V3__add_brand_to_cars_table.sql"), ) .unwrap(); let migration4 = Migration::unapplied( "V4__add_year_to_motos_table.rs", + None, &migrations::V4__add_year_to_motos_table::migration(), ) .unwrap(); let migration5 = Migration::unapplied( "V5__add_year_field_to_cars", + None, "ALTER TABLE cars ADD year INTEGER;", ) .unwrap(); @@ -620,6 +628,7 @@ mod tokio_postgres { let migration = Migration::unapplied( "V4__add_year_field_to_cars", + None, "ALTER TABLE cars ADD year INTEGER;", ) .unwrap(); @@ -665,6 +674,7 @@ mod tokio_postgres { let migration = Migration::unapplied( "V2__add_year_field_to_cars", + None, "ALTER TABLE cars ADD year INTEGER;", ) .unwrap(); @@ -712,6 +722,7 @@ mod tokio_postgres { let migration1 = Migration::unapplied( "V1__initial", + None, concat!( "CREATE TABLE persons (", "id int,", @@ -724,6 +735,7 @@ mod tokio_postgres { let migration2 = Migration::unapplied( "V2__add_cars_table", + None, include_str!("./migrations_missing/V2__add_cars_table.sql"), ) .unwrap(); diff --git a/refinery_core/src/config.rs b/refinery_core/src/config.rs index 82f4173e..28853dae 100644 --- a/refinery_core/src/config.rs +++ b/refinery_core/src/config.rs @@ -48,6 +48,7 @@ impl Config { None, ) })?; + Config::from_str(&value) } diff --git a/refinery_core/src/drivers/config.rs b/refinery_core/src/drivers/config.rs index 88f595e2..fd7cdd75 100644 --- a/refinery_core/src/drivers/config.rs +++ b/refinery_core/src/drivers/config.rs @@ -1,4 +1,7 @@ #![allow(unused_imports)] +use async_trait::async_trait; +use std::convert::Infallible; + #[cfg(any( feature = "mysql", feature = "postgres", @@ -9,13 +12,12 @@ use crate::config::build_db_url; use crate::config::{Config, ConfigDbType}; use crate::error::WrapMigrationError; -use crate::traits::r#async::{AsyncExecutor, AsyncQuerySchemaHistory}; -use crate::traits::sync::{Executor, QuerySchemaHistory}; -use crate::traits::{GET_APPLIED_MIGRATIONS_QUERY, GET_LAST_APPLIED_MIGRATION_QUERY}; +use crate::executor::{ + async_exec::{AsyncExecutor, AsyncQuerySchemaHistory}, + exec::{Executor, QuerySchemaHistory}, + GET_APPLIED_MIGRATIONS_QUERY, GET_LAST_APPLIED_MIGRATION_QUERY, +}; use crate::{Error, Migration, MigrationContent, Report, Target}; -use async_trait::async_trait; - -use std::convert::Infallible; // we impl all the dependent traits as noop's and then override the methods that call them on Migrate and AsyncMigrate impl Executor for Config { diff --git a/refinery_core/src/drivers/mysql.rs b/refinery_core/src/drivers/mysql.rs index f97ec1d2..ee0f0c6f 100644 --- a/refinery_core/src/drivers/mysql.rs +++ b/refinery_core/src/drivers/mysql.rs @@ -1,5 +1,3 @@ -use crate::traits::sync::{Executor, Migrate, QuerySchemaHistory}; -use crate::{Migration, MigrationContent}; use mysql::{ error::Error as MError, prelude::Queryable, Conn, IsolationLevel, PooledConn, Transaction as MTransaction, TxOpts, @@ -7,6 +5,9 @@ use mysql::{ use time::format_description::well_known::Rfc3339; use time::OffsetDateTime; +use crate::executor::{Executor, QuerySchemaHistory}; +use crate::{Migrate, Migration, MigrationContent}; + fn get_tx_opts() -> TxOpts { TxOpts::default() .set_with_consistent_snapshot(true) diff --git a/refinery_core/src/drivers/mysql_async.rs b/refinery_core/src/drivers/mysql_async.rs index 8b28c320..d8e03d40 100644 --- a/refinery_core/src/drivers/mysql_async.rs +++ b/refinery_core/src/drivers/mysql_async.rs @@ -1,5 +1,3 @@ -use crate::traits::r#async::{AsyncExecutor, AsyncMigrate, AsyncQuerySchemaHistory}; -use crate::{Migration, MigrationContent}; use async_trait::async_trait; use mysql_async::{ prelude::Queryable, Error as MError, IsolationLevel, Pool, Transaction as MTransaction, TxOpts, @@ -7,12 +5,14 @@ use mysql_async::{ use time::format_description::well_known::Rfc3339; use time::OffsetDateTime; +use crate::executor::{AsyncExecutor, AsyncQuerySchemaHistory}; +use crate::{AsyncMigrate, Migration, MigrationContent}; + async fn query_applied_migrations<'a>( mut transaction: MTransaction<'a>, query: &str, ) -> Result<(MTransaction<'a>, Vec), MError> { let result = transaction.query(query).await?; - let applied = result .into_iter() .map(|row| { diff --git a/refinery_core/src/drivers/postgres.rs b/refinery_core/src/drivers/postgres.rs index eb8405e7..37ee1b4f 100644 --- a/refinery_core/src/drivers/postgres.rs +++ b/refinery_core/src/drivers/postgres.rs @@ -1,9 +1,10 @@ -use crate::traits::sync::{Executor, Migrate, QuerySchemaHistory}; -use crate::{Migration, MigrationContent}; use postgres::{Client as PgClient, Error as PgError, Transaction as PgTransaction}; use time::format_description::well_known::Rfc3339; use time::OffsetDateTime; +use crate::executor::{Executor, QuerySchemaHistory}; +use crate::{Migrate, Migration, MigrationContent}; + fn query_applied_migrations( transaction: &mut PgTransaction, query: &str, diff --git a/refinery_core/src/drivers/rusqlite.rs b/refinery_core/src/drivers/rusqlite.rs index 87a57b8a..449740f1 100644 --- a/refinery_core/src/drivers/rusqlite.rs +++ b/refinery_core/src/drivers/rusqlite.rs @@ -1,9 +1,10 @@ -use crate::traits::sync::{Executor, Migrate, QuerySchemaHistory}; -use crate::Migration; use rusqlite::{Connection as RqlConnection, Error as RqlError}; use time::format_description::well_known::Rfc3339; use time::OffsetDateTime; +use crate::executor::{Executor, QuerySchemaHistory}; +use crate::{Migrate, Migration, MigrationContent}; + fn query_applied_migrations( transaction: &RqlConnection, query: &str, @@ -16,7 +17,6 @@ fn query_applied_migrations( let applied_on: String = row.get(2)?; // Safe to call unwrap, as we stored it in RFC3339 format on the database let applied_on = OffsetDateTime::parse(&applied_on, &Rfc3339).unwrap(); - let checksum: String = row.get(3)?; applied.push(Migration::applied( version, @@ -27,6 +27,7 @@ fn query_applied_migrations( .expect("checksum must be a valid u64"), )); } + Ok(applied) } @@ -50,7 +51,7 @@ impl Executor for RqlConnection { fn execute<'a, T>(&mut self, queries: T) -> Result where - T: Iterator, + T: Iterator, { let mut count: usize = 0; for (content, update) in queries { diff --git a/refinery_core/src/drivers/sqlx_postgres.rs b/refinery_core/src/drivers/sqlx_postgres.rs index 95301fb1..557fe480 100644 --- a/refinery_core/src/drivers/sqlx_postgres.rs +++ b/refinery_core/src/drivers/sqlx_postgres.rs @@ -3,8 +3,8 @@ use futures::prelude::*; use sqlx::{Acquire, PgPool, Postgres}; use time::OffsetDateTime; -use crate::traits::r#async::{AsyncExecutor, AsyncMigrate, AsyncQuerySchemaHistory}; -use crate::{Migration, MigrationContent}; +use crate::executor::{AsyncExecutor, AsyncQuerySchemaHistory}; +use crate::{AsyncMigrate, Migration, MigrationContent}; /// A representation of a row in the schema /// history table where migrations are persisted. diff --git a/refinery_core/src/drivers/tiberius.rs b/refinery_core/src/drivers/tiberius.rs index c155e766..d07a3874 100644 --- a/refinery_core/src/drivers/tiberius.rs +++ b/refinery_core/src/drivers/tiberius.rs @@ -1,6 +1,3 @@ -use crate::traits::r#async::{AsyncExecutor, AsyncMigrate, AsyncQuerySchemaHistory}; -use crate::{Migration, MigrationContent}; - use async_trait::async_trait; use futures::{ io::{AsyncRead, AsyncWrite}, @@ -10,6 +7,9 @@ use tiberius::{error::Error, Client, QueryItem}; use time::format_description::well_known::Rfc3339; use time::OffsetDateTime; +use crate::executor::{AsyncExecutor, AsyncQuerySchemaHistory}; +use crate::{AsyncMigrate, Migration, MigrationContent}; + async fn query_applied_migrations( client: &mut Client, query: &str, diff --git a/refinery_core/src/drivers/tokio_postgres.rs b/refinery_core/src/drivers/tokio_postgres.rs index 1141d1b4..0fb1a7de 100644 --- a/refinery_core/src/drivers/tokio_postgres.rs +++ b/refinery_core/src/drivers/tokio_postgres.rs @@ -1,11 +1,12 @@ -use crate::traits::r#async::{AsyncExecutor, AsyncMigrate, AsyncQuerySchemaHistory}; -use crate::{Migration, MigrationContent}; use async_trait::async_trait; use time::format_description::well_known::Rfc3339; use time::OffsetDateTime; use tokio_postgres::error::Error as PgError; use tokio_postgres::{Client, Transaction as PgTransaction}; +use crate::executor::{AsyncExecutor, AsyncQuerySchemaHistory}; +use crate::{AsyncMigrate, Migration, MigrationContent}; + async fn query_applied_migrations( transaction: &PgTransaction<'_>, query: &str, diff --git a/refinery_core/src/traits/async.rs b/refinery_core/src/executor/async_exec.rs similarity index 98% rename from refinery_core/src/traits/async.rs rename to refinery_core/src/executor/async_exec.rs index c0e73103..dd9dd230 100644 --- a/refinery_core/src/traits/async.rs +++ b/refinery_core/src/executor/async_exec.rs @@ -1,13 +1,12 @@ -use crate::error::WrapMigrationError; -use crate::runner::MigrationContent; -use crate::traits::{ +use async_trait::async_trait; +use std::string::ToString; + +use super::{ insert_migration_query, verify_migrations, ASSERT_MIGRATIONS_TABLE_QUERY, GET_APPLIED_MIGRATIONS_QUERY, GET_LAST_APPLIED_MIGRATION_QUERY, }; -use crate::{Error, Migration, Report, Target}; - -use async_trait::async_trait; -use std::string::ToString; +use crate::error::WrapMigrationError; +use crate::{Error, Migration, MigrationContent, Report, Target}; #[async_trait] pub trait AsyncExecutor { diff --git a/refinery_core/src/traits/sync.rs b/refinery_core/src/executor/exec.rs similarity index 97% rename from refinery_core/src/traits/sync.rs rename to refinery_core/src/executor/exec.rs index 1251c0b9..1e2d3da2 100644 --- a/refinery_core/src/traits/sync.rs +++ b/refinery_core/src/executor/exec.rs @@ -1,8 +1,8 @@ -use crate::error::WrapMigrationError; -use crate::traits::{ +use super::{ insert_migration_query, verify_migrations, ASSERT_MIGRATIONS_TABLE_QUERY, GET_APPLIED_MIGRATIONS_QUERY, GET_LAST_APPLIED_MIGRATION_QUERY, }; +use crate::error::WrapMigrationError; use crate::{Error, Migration, MigrationContent, Report, Target}; pub trait Executor { @@ -31,6 +31,13 @@ pub trait QuerySchemaHistory: Executor { fn query_schema_history(&mut self, query: &str) -> Result; } +/// A type that needs the driver to produce the final query. +pub trait FinalizeMigration { + type Driver: Executor; + + fn finalize(&self) -> Result::Error>; +} + pub fn migrate( executor: &mut T, migrations: Vec, diff --git a/refinery_core/src/traits/mod.rs b/refinery_core/src/executor/mod.rs similarity index 98% rename from refinery_core/src/traits/mod.rs rename to refinery_core/src/executor/mod.rs index d3eef6d3..307ada89 100644 --- a/refinery_core/src/traits/mod.rs +++ b/refinery_core/src/executor/mod.rs @@ -1,11 +1,13 @@ use time::format_description::well_known::Rfc3339; -pub mod r#async; -pub mod sync; - -use crate::runner::Type; +use crate::migration::Type; use crate::{error::Kind, Error, Migration}; +pub mod async_exec; +pub mod exec; +pub use async_exec::{AsyncExecutor, AsyncQuerySchemaHistory}; +pub use exec::{Executor, QuerySchemaHistory}; + // Verifies applied and to be applied migrations returning Error if: // - `abort_divergent` is true and there are applied migrations with a different name and checksum but same version as a migration to be applied. // - `abort_missing` is true and there are applied migrations that are missing on the file system diff --git a/refinery_core/src/lib.rs b/refinery_core/src/lib.rs index 8fef079b..5c16667f 100644 --- a/refinery_core/src/lib.rs +++ b/refinery_core/src/lib.rs @@ -1,17 +1,23 @@ pub mod config; mod drivers; pub mod error; +pub mod executor; +mod migration; mod runner; -pub mod traits; mod util; -pub use crate::error::Error; -pub use crate::runner::{Migration, MigrationContent, Report, Runner, Target}; -pub use crate::traits::r#async::AsyncMigrate; -pub use crate::traits::sync::Migrate; -pub use crate::util::{ - find_migration_files, load_sql_migrations, parse_migration_name, parse_no_transaction, - MigrationType, +pub use self::error::Error; +pub use self::executor::{ + async_exec::{AsyncExecutor, AsyncMigrate}, + exec::{Executor, Migrate}, +}; +pub use self::migration::{ + AsyncFinalizeMigration, FinalizeMigration, Migration, MigrationContent, Target, +}; +pub use self::runner::{Report, Runner}; +pub use self::util::{ + find_migration_files, load_sql_migrations, parse_finalize_migration, parse_migration_name, + parse_no_transaction, MigrationType, }; #[cfg(feature = "rusqlite")] diff --git a/refinery_core/src/migration.rs b/refinery_core/src/migration.rs new file mode 100644 index 00000000..aa91a982 --- /dev/null +++ b/refinery_core/src/migration.rs @@ -0,0 +1,296 @@ +use async_trait::async_trait; +use siphasher::sip::SipHasher13; +use std::cmp::Ordering; +use std::fmt; +use std::fmt::Formatter; +use std::hash::{Hash, Hasher}; +use time::OffsetDateTime; + +use crate::executor::{AsyncExecutor, Executor}; +use crate::util::parse_migration_name; +use crate::{error::WrapMigrationError, Error}; + +/// An enum set that represents the type of the Migration +#[derive(Clone, PartialEq)] +pub enum Type { + Versioned, + Unversioned, +} + +impl fmt::Display for Type { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let version_type = match self { + Type::Versioned => "V", + Type::Unversioned => "U", + }; + write!(f, "{}", version_type) + } +} + +impl fmt::Debug for Type { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let version_type = match self { + Type::Versioned => "Versioned", + Type::Unversioned => "Unversioned", + }; + write!(f, "{}", version_type) + } +} + +/// An enum that represents the target version up to which refinery should migrate. +/// It is used by [Runner]. +#[derive(Clone, Copy, Debug)] +pub enum Target { + Latest, + Version(u32), + Fake, + FakeVersion(u32), +} + +// an Enum set that represents the state of the migration: Applied on the database, +// or Unapplied yet to be applied on the database +#[derive(Clone, Debug)] +enum State { + Applied, + Unapplied, +} + +/// The query defining the migration and if the file where it was defined +/// came annotated to indicate that it shouldn't be ran in a transaction. +#[derive(Clone, Debug)] +pub struct MigrationContent { + sql: String, + no_transaction: bool, +} + +impl MigrationContent { + pub fn new(no_transaction: Option, sql: String) -> Self { + Self { + sql, + no_transaction: no_transaction.unwrap_or_default(), + } + } + + pub fn sql(&self) -> &str { + &self.sql + } + + pub fn no_transaction(&self) -> bool { + self.no_transaction + } +} + +/// A type that needs the driver to provide the query to run. +pub trait FinalizeMigration: Sized +where + C: Executor, +{ + /// Create an instance of this type from a connection. + fn initialize(conn: &mut C) -> Result::Error>; + + /// Produce the SQL for the migration. + fn finalize(&self, conn: &mut C) -> Result::Error>; +} + +/// A type that needs the driver to asynchronously provide the query to run. +#[async_trait] +pub trait AsyncFinalizeMigration: Sized +where + C: AsyncExecutor + Send, +{ + /// Create an instance of this type from a connection. + async fn initialize(conn: &mut C) -> Result::Error>; + + /// Produce the SQL for the migration. + async fn finalize(&self, conn: &mut C) -> Result::Error>; +} + +/// Represents a migration that is either waiting to be +/// applied or already has been. +/// This is used by the [`embed_migrations!`] macro to gather +/// migration files and shouldn't be needed by the user. +/// +/// [`embed_migrations!`]: macro.embed_migrations.html +#[derive(Clone, Debug)] +pub struct Migration { + state: State, + name: String, + checksum: u64, + version: i32, + prefix: Type, + content: Option, + applied_on: Option, +} + +impl Migration { + /// Create an unapplied migration, name and version are parsed from the input_name, + /// which must be named in the format (U|V){1}__{2}.rs where {1} represents the migration version and {2} the name. + pub fn unapplied( + input_name: &str, + no_transaction: Option, + sql: &str, + ) -> Result { + let (prefix, version, name) = parse_migration_name(input_name)?; + + // Previously, `std::collections::hash_map::DefaultHasher` was used + // to calculate the checksum and the implementation at that time + // was SipHasher13. However, that implementation is not guaranteed: + // > The internal algorithm is not specified, and so it and its + // > hashes should not be relied upon over releases. + // We now explicitly use SipHasher13 to both remain compatible with + // existing migrations and prevent breaking from possible future + // changes to `DefaultHasher`. + let mut hasher = SipHasher13::new(); + name.hash(&mut hasher); + version.hash(&mut hasher); + sql.hash(&mut hasher); + let checksum = hasher.finish(); + let content = Some(MigrationContent { + no_transaction: no_transaction.unwrap_or_default(), + sql: sql.to_string(), + }); + + Ok(Migration { + state: State::Unapplied, + name, + version, + prefix, + content, + applied_on: None, + checksum, + }) + } + + /// Create an unapplied migration that needs its SQL query to be created first. + pub fn finalize_unapplied( + conn: &mut C, + input_name: &str, + no_transaction: Option, + ) -> Result + where + Fin: FinalizeMigration, + { + let finalizer = Fin::initialize(conn).migration_err( + &format!("unable to create finalizer for {input_name}"), + None, + )?; + let sql = finalizer + .finalize(conn) + .migration_err(&format!("unable to finalize query for {input_name}"), None)?; + Self::unapplied(input_name, no_transaction, &sql) + } + + /// Create an unapplied migration that needs its SQL query to be created first. + pub async fn async_finalize_unapplied( + conn: &mut C, + input_name: &str, + no_transaction: Option, + ) -> Result + where + Fin: AsyncFinalizeMigration, + { + let finalizer = Fin::initialize(conn).await.migration_err( + &format!("unable to create finalizer for {input_name}"), + None, + )?; + let sql = finalizer + .finalize(conn) + .await + .migration_err(&format!("unable to finalize query for {input_name}"), None)?; + Self::unapplied(input_name, no_transaction, &sql) + } + + // Create a migration from an applied migration on the database + pub fn applied( + version: i32, + name: String, + applied_on: OffsetDateTime, + checksum: u64, + ) -> Migration { + Migration { + state: State::Applied, + name, + checksum, + version, + // applied migrations are always versioned + prefix: Type::Versioned, + content: None, + applied_on: Some(applied_on), + } + } + + // convert the Unapplied into an Applied Migration + pub fn set_applied(&mut self) { + self.applied_on = Some(OffsetDateTime::now_utc()); + self.state = State::Applied; + } + + /// Get the content of the migration + pub fn content(&self) -> Option<&MigrationContent> { + self.content.as_ref() + } + + /// Get the SQL of the migration content + pub fn sql(&self) -> Option { + self.content().map(|c| c.sql.clone()) + } + + /// Get the flag for running this migration in a transaction + pub fn no_transaction(&self) -> Option { + self.content().map(|c| c.no_transaction) + } + + /// Get the Migration version + pub fn version(&self) -> u32 { + self.version as u32 + } + + /// Get the Prefix + pub fn prefix(&self) -> &Type { + &self.prefix + } + + /// Get the Migration name + pub fn name(&self) -> &str { + &self.name + } + + /// Get the timestamp from when the Migration was applied. `None` when unapplied. + /// Migrations returned from Runner::get_migrations() will always have `None`. + pub fn applied_on(&self) -> Option<&OffsetDateTime> { + self.applied_on.as_ref() + } + + /// Get the Migration checksum. Checksum is formed from the name version and sql of the Migration + pub fn checksum(&self) -> u64 { + self.checksum + } +} + +impl fmt::Display for Migration { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(fmt, "{}{}__{}", self.prefix, self.version, self.name) + } +} + +impl Eq for Migration {} + +impl PartialEq for Migration { + fn eq(&self, other: &Migration) -> bool { + self.version == other.version + && self.name == other.name + && self.checksum() == other.checksum() + } +} + +impl Ord for Migration { + fn cmp(&self, other: &Migration) -> Ordering { + self.version.cmp(&other.version) + } +} + +impl PartialOrd for Migration { + fn partial_cmp(&self, other: &Migration) -> Option { + Some(self.cmp(other)) + } +} diff --git a/refinery_core/src/runner.rs b/refinery_core/src/runner.rs index 877333d1..4ebf290e 100644 --- a/refinery_core/src/runner.rs +++ b/refinery_core/src/runner.rs @@ -1,230 +1,9 @@ -use siphasher::sip::SipHasher13; -use time::OffsetDateTime; - -use log::error; -use std::cmp::Ordering; use std::collections::VecDeque; -use std::fmt; -use std::hash::{Hash, Hasher}; - -use crate::traits::{sync::migrate as sync_migrate, DEFAULT_MIGRATION_TABLE_NAME}; -use crate::util::parse_migration_name; -use crate::{AsyncMigrate, Error, Migrate}; -use std::fmt::Formatter; - -/// An enum set that represents the type of the Migration -#[derive(Clone, PartialEq)] -pub enum Type { - Versioned, - Unversioned, -} - -impl fmt::Display for Type { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let version_type = match self { - Type::Versioned => "V", - Type::Unversioned => "U", - }; - write!(f, "{}", version_type) - } -} - -impl fmt::Debug for Type { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let version_type = match self { - Type::Versioned => "Versioned", - Type::Unversioned => "Unversioned", - }; - write!(f, "{}", version_type) - } -} - -/// An enum that represents the target version up to which refinery should migrate. -/// It is used by [Runner]. -#[derive(Clone, Copy, Debug)] -pub enum Target { - Latest, - Version(u32), - Fake, - FakeVersion(u32), -} - -// an Enum set that represents the state of the migration: Applied on the database, -// or Unapplied yet to be applied on the database -#[derive(Clone, Debug)] -enum State { - Applied, - Unapplied, -} - -/// The query defining the migration and if the file where it was defined -/// came annotated to indicate that it shouldn't be ran in a transaction. -#[derive(Clone, Debug)] -pub struct MigrationContent { - sql: String, - no_transaction: bool, -} - -impl MigrationContent { - pub fn sql(&self) -> &str { - &self.sql - } - - pub fn no_transaction(&self) -> bool { - self.no_transaction - } -} - -/// Represents a migration that is either waiting to be -/// applied or already has been. -/// This is used by the [`embed_migrations!`] macro to gather -/// migration files and shouldn't be needed by the user. -/// -/// [`embed_migrations!`]: macro.embed_migrations.html -#[derive(Clone, Debug)] -pub struct Migration { - state: State, - name: String, - checksum: u64, - version: i32, - prefix: Type, - content: Option, - applied_on: Option, -} - -impl Migration { - /// Create an unapplied migration, name and version are parsed from the input_name, - /// which must be named in the format (U|V){1}__{2}.rs where {1} represents the migration version and {2} the name. - pub fn unapplied( - input_name: &str, - no_transaction: Option, - sql: &str, - ) -> Result { - let (prefix, version, name) = parse_migration_name(input_name)?; - - // Previously, `std::collections::hash_map::DefaultHasher` was used - // to calculate the checksum and the implementation at that time - // was SipHasher13. However, that implementation is not guaranteed: - // > The internal algorithm is not specified, and so it and its - // > hashes should not be relied upon over releases. - // We now explicitly use SipHasher13 to both remain compatible with - // existing migrations and prevent breaking from possible future - // changes to `DefaultHasher`. - let mut hasher = SipHasher13::new(); - name.hash(&mut hasher); - version.hash(&mut hasher); - sql.hash(&mut hasher); - let checksum = hasher.finish(); - let content = Some(MigrationContent { - no_transaction: no_transaction.unwrap_or_default(), - sql: sql.to_string(), - }); - - Ok(Migration { - state: State::Unapplied, - name, - version, - prefix, - content, - applied_on: None, - checksum, - }) - } - - // Create a migration from an applied migration on the database - pub fn applied( - version: i32, - name: String, - applied_on: OffsetDateTime, - checksum: u64, - ) -> Migration { - Migration { - state: State::Applied, - name, - checksum, - version, - // applied migrations are always versioned - prefix: Type::Versioned, - content: None, - applied_on: Some(applied_on), - } - } - // convert the Unapplied into an Applied Migration - pub fn set_applied(&mut self) { - self.applied_on = Some(OffsetDateTime::now_utc()); - self.state = State::Applied; - } - - /// Get the content of the migration - pub fn content(&self) -> Option<&MigrationContent> { - self.content.as_ref() - } - - /// Get the SQL of the migration content - pub fn sql(&self) -> Option { - self.content().map(|c| c.sql.clone()) - } - - /// Get the flag for running this migration in a transaction - pub fn no_transaction(&self) -> Option { - self.content().map(|c| c.no_transaction) - } - - /// Get the Migration version - pub fn version(&self) -> u32 { - self.version as u32 - } - - /// Get the Prefix - pub fn prefix(&self) -> &Type { - &self.prefix - } - - /// Get the Migration name - pub fn name(&self) -> &str { - &self.name - } - - /// Get the timestamp from when the Migration was applied. `None` when unapplied. - /// Migrations returned from Runner::get_migrations() will always have `None`. - pub fn applied_on(&self) -> Option<&OffsetDateTime> { - self.applied_on.as_ref() - } - - /// Get the Migration checksum. Checksum is formed from the name version and sql of the Migration - pub fn checksum(&self) -> u64 { - self.checksum - } -} - -impl fmt::Display for Migration { - fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(fmt, "{}{}__{}", self.prefix, self.version, self.name) - } -} - -impl Eq for Migration {} - -impl PartialEq for Migration { - fn eq(&self, other: &Migration) -> bool { - self.version == other.version - && self.name == other.name - && self.checksum() == other.checksum() - } -} - -impl Ord for Migration { - fn cmp(&self, other: &Migration) -> Ordering { - self.version.cmp(&other.version) - } -} - -impl PartialOrd for Migration { - fn partial_cmp(&self, other: &Migration) -> Option { - Some(self.cmp(other)) - } -} +use crate::{ + executor::{exec::migrate as sync_migrate, DEFAULT_MIGRATION_TABLE_NAME}, + AsyncMigrate, Error, Migrate, Migration, Target, +}; /// Struct that represents the report of the migration cycle. /// A `Report` instance is returned by the [`Runner::run`] and [`Runner::run_async`] methods @@ -483,7 +262,7 @@ where ) .map(|r| r.applied_migrations.first().cloned()) .map_err(|e| { - error!("migration failed: {e:?}"); + log::error!("migration failed: {e:?}"); self.failed = true; e }) diff --git a/refinery_core/src/util.rs b/refinery_core/src/util.rs index 64622218..ade12bbf 100644 --- a/refinery_core/src/util.rs +++ b/refinery_core/src/util.rs @@ -1,12 +1,13 @@ -use crate::error::{Error, Kind}; -use crate::runner::Type; -use crate::Migration; use regex::Regex; use std::ffi::OsStr; use std::path::{Path, PathBuf}; use std::sync::OnceLock; use walkdir::{DirEntry, WalkDir}; +use crate::error::{Error, Kind}; +use crate::migration::Type; +use crate::Migration; + const STEM_RE: &'static str = r"^([U|V])(\d+(?:\.\d+)?)__(\w+)"; /// Matches the stem of a migration file. @@ -43,6 +44,14 @@ fn query_no_transaction_re_all() -> &'static Regex { RE.get_or_init(|| Regex::new(r"^[-|\/]{2,}[\s]?(refinery:noTransaction)$").unwrap()) } +/// Matches the annotation `refinery:finalizeMigration` at the start of +/// a commented line of a .rs migration file, implying that the migration +/// query should be produced at runtime using the selected connection type. +fn file_finalize_query_re_rs() -> &'static Regex { + static RE: OnceLock = OnceLock::new(); + RE.get_or_init(|| Regex::new(r"[^\/]{2,}[\s]?(refinery:finalizeMigration)$").unwrap()) +} + /// enum containing the migration types used to search for migrations /// either just .sql files or both .sql and .rs pub enum MigrationType { @@ -137,6 +146,21 @@ pub fn parse_no_transaction(file_content: String, migration_type: MigrationType) no_transaction } +/// Determines if the embedded Rust migration has the annotation +/// saying that it has a query that is not complete yet. +pub fn parse_finalize_migration(file_content: String) -> Option { + let mut finalize_migration: Option = None; + let re = file_finalize_query_re_rs(); + for line in file_content.lines() { + if re.is_match(line) { + finalize_migration = Some(true); + break; + } + } + + finalize_migration +} + /// Loads SQL migrations from a path. This enables dynamic migration discovery, as opposed to /// embedding. The resulting collection is ordered by version. pub fn load_sql_migrations(location: impl AsRef) -> Result, Error> { diff --git a/refinery_macros/src/lib.rs b/refinery_macros/src/lib.rs index 95da7bd1..a53363d3 100644 --- a/refinery_macros/src/lib.rs +++ b/refinery_macros/src/lib.rs @@ -6,7 +6,9 @@ use proc_macro::TokenStream; use proc_macro2::{Span as Span2, TokenStream as TokenStream2}; use quote::quote; use quote::ToTokens; -use refinery_core::{find_migration_files, parse_no_transaction, MigrationType}; +use refinery_core::{ + find_migration_files, parse_finalize_migration, parse_no_transaction, MigrationType, +}; use std::path::PathBuf; use std::{env, fs}; use syn::{parse_macro_input, Ident, LitStr}; @@ -19,8 +21,8 @@ pub(crate) fn crate_root() -> PathBuf { fn migration_fn_quoted(_migrations: Vec) -> TokenStream2 { let result = quote! { - use refinery::{Migration, Runner}; pub fn runner() -> Runner { + use refinery::{Migration, Runner}; let quoted_migrations: Vec<(&str, Option, String)> = vec![#(#_migrations),*]; let mut migrations: Vec = Vec::new(); for module in quoted_migrations.into_iter() { @@ -32,6 +34,24 @@ fn migration_fn_quoted(_migrations: Vec) -> TokenStream2 { result } +fn finalize_migration_fns_quoted( + _finalized_migrations: Vec, + _async_finalized_migrations: Vec, +) -> TokenStream2 { + let result = quote! { + pub fn runner_with_finalize(conn: &mut C) -> Runner { + let migrations: Vec = vec![#(#_finalized_migrations),*]; + Runner::new(&migrations) + } + + pub async fn runner_with_async_finalize(conn: &mut C) -> Runner { + let migrations: Vec = vec![#(#_async_finalized_migrations),*]; + Runner::new(&migrations) + } + }; + result +} + fn migration_enum_quoted(migration_names: &[impl AsRef]) -> TokenStream2 { if cfg!(feature = "enums") { let mut variants = Vec::new(); @@ -68,6 +88,42 @@ fn migration_enum_quoted(migration_names: &[impl AsRef]) -> TokenStream2 { } } +/// Return a tuple of tokens that create a `Migration` by using the correct method +/// depending on whether or not the migration needs to be finalized. +/// Returns a tuple of tokens of function calls where the first element synchronously +/// created the `Migration`, and the second element asynchronously did. +fn unapplied_migration_call( + filename: &str, + path: String, + no_transaction: Option, + is_rs_finalize: Option, +) -> (TokenStream2, TokenStream2) { + let no_transaction_token = match no_transaction { + Some(val) => quote!(core::option::Option::Some(#val)), + None => quote!(core::option::Option::None), + }; + match is_rs_finalize { + // this is a sql migration so doesn't need to call finalizing unapplied + None => { + let sql_mig = quote! {Migration::unapplied(#filename, #no_transaction_token, include_str!(#path)).unwrap()}; + let async_sql_mig = quote! {Migration::unapplied(#filename, #no_transaction_token, include_str!(#path)).unwrap()}; + (sql_mig, async_sql_mig) + } + Some(finalize) if finalize => { + let ident = Ident::new(&filename, Span2::call_site()); + let rs_fin_mig = quote! {Migration::finalize_unapplied::<#ident::Finalizer, _>(conn, #filename, #no_transaction_token).unwrap()}; + let async_rs_fin_mig = quote! {Migration::async_finalize_unapplied::<#ident::Finalizer, _>(conn, #filename, #no_transaction_token).await.unwrap()}; + (rs_fin_mig, async_rs_fin_mig) + } + _ => { + let ident = Ident::new(&filename, Span2::call_site()); + let rs_mig = quote! {Migration::unapplied(#filename, #no_transaction_token, &#ident::migration()).unwrap()}; + let async_rs_mig = quote! {Migration::unapplied(#filename, #no_transaction_token, &#ident::migration()).unwrap()}; + (rs_mig, async_rs_mig) + } + } +} + /// Interpret Rust or SQL migrations and inserts a function called runner that when called returns a [`Runner`] instance with the collected migration modules. /// /// When called without arguments `embed_migrations` searches for migration files on a directory called `migrations` at the root level of your crate. @@ -93,6 +149,8 @@ pub fn embed_migrations(input: TokenStream) -> TokenStream { let mut migrations_mods = Vec::new(); let mut _migrations = Vec::new(); + let mut _finalized_migrations = Vec::new(); + let mut _async_finalized_migrations = Vec::new(); let mut migration_filenames = Vec::new(); for migration in migration_files { @@ -103,7 +161,8 @@ pub fn embed_migrations(input: TokenStream) -> TokenStream { .unwrap(); let path = migration.display().to_string(); let content = fs::read_to_string(&path).unwrap(); - let no_transaction = match parse_no_transaction(content, MigrationType::All) { + let no_transaction = parse_no_transaction(content, MigrationType::All); + let no_transaction_token = match no_transaction { Some(val) => quote!(core::option::Option::Some(#val)), None => quote!(core::option::Option::None), }; @@ -112,29 +171,39 @@ pub fn embed_migrations(input: TokenStream) -> TokenStream { if extension == "sql" { _migrations - .push(quote! {(#filename, #no_transaction, include_str!(#path).to_string())}); + .push(quote! {(#filename, #no_transaction_token, include_str!(#path).to_string())}); + let (sql_mig, async_sql_mig) = + unapplied_migration_call(&filename, path, no_transaction, None); + _finalized_migrations.push(sql_mig); + _async_finalized_migrations.push(async_sql_mig); } else if extension == "rs" { - let rs_content = fs::read_to_string(&path) - .unwrap() - .parse::() - .unwrap(); + let rs_raw = fs::read_to_string(&path).unwrap(); + let rs_content = rs_raw.parse::().unwrap(); let ident = Ident::new(&filename, Span2::call_site()); + let is_rs_finalize = parse_finalize_migration(rs_raw.to_string()).or(Some(false)); let mig_mod = quote! {pub mod #ident { #rs_content // also include the file as str so we trigger recompilation if it changes const _RECOMPILE_IF_CHANGED: &str = include_str!(#path); }}; - _migrations.push(quote! {(#filename, #no_transaction, #ident::migration())}); + _migrations.push(quote! {(#filename, #no_transaction_token, #ident::migration())}); + let (rs_mig, async_rs_mig) = + unapplied_migration_call(&filename, path, no_transaction, is_rs_finalize); + _finalized_migrations.push(rs_mig); + _async_finalized_migrations.push(async_rs_mig); migrations_mods.push(mig_mod); } } let fnq = migration_fn_quoted(_migrations); + let fnzq = finalize_migration_fns_quoted(_finalized_migrations, _async_finalized_migrations); let enums = migration_enum_quoted(migration_filenames.as_slice()); (quote! { pub mod migrations { #(#migrations_mods)* + use refinery::{Migration, Runner, Executor, AsyncExecutor}; #fnq + #fnzq #enums } })