From 0beea311bef66c706fdccf744010352e8f66c49e Mon Sep 17 00:00:00 2001 From: Matt Palmer Date: Sun, 5 May 2024 07:32:38 +1000 Subject: [PATCH 1/6] Support int8 migration versions via new int8-versions feature This addresses, though does not really *fix* #83, because it doesn't make refinery support timestamped migrations *by default*, but only if you opt-in to the new feature. However, making it an optional feature neatly sidesteps the unanswered questions in the issue, and so makes the implementation easier to complete and land. --- README.md | 10 ++-- refinery/Cargo.toml | 1 + refinery/src/lib.rs | 4 +- .../U20240504090241__initial.rs | 15 ++++++ ...240504090301__add_cars_and_motos_table.sql | 8 ++++ ...0240504090322__add_brand_to_cars_table.sql | 2 + ...20240504090343__add_year_to_motos_table.rs | 13 +++++ refinery/tests/postgres.rs | 48 +++++++++++++++++++ refinery_cli/Cargo.toml | 1 + refinery_cli/src/cli.rs | 4 +- refinery_cli/src/migrate.rs | 4 +- refinery_core/Cargo.toml | 1 + refinery_core/src/drivers/mysql_async.rs | 3 +- refinery_core/src/drivers/tiberius.rs | 3 +- refinery_core/src/lib.rs | 2 +- refinery_core/src/runner.rs | 14 +++--- refinery_core/src/traits/async.rs | 9 +++- refinery_core/src/traits/mod.rs | 9 ++-- refinery_core/src/traits/sync.rs | 6 +++ refinery_core/src/util.rs | 9 +++- refinery_macros/src/lib.rs | 12 ++--- 21 files changed, 147 insertions(+), 31 deletions(-) create mode 100644 refinery/tests/migrations_int8/U20240504090241__initial.rs create mode 100644 refinery/tests/migrations_int8/U20240504090301__add_cars_and_motos_table.sql create mode 100644 refinery/tests/migrations_int8/U20240504090322__add_brand_to_cars_table.sql create mode 100644 refinery/tests/migrations_int8/U20240504090343__add_year_to_motos_table.rs diff --git a/README.md b/README.md index bac63cdd..fbca7919 100644 --- a/README.md +++ b/README.md @@ -53,15 +53,17 @@ fn main() { For more library examples, refer to the [`examples`](https://github.com/rust-db/refinery/tree/main/examples). ### Example: CLI -NOTE: +NOTE: -- Contiguous (adjacent) migration version numbers are restricted to `u32` (unsigned, 32-bit integers). -- Non-contiguous (not adjacent) migration version numbers are restricted to `u32` (unsigned, 32-bit integers). +- By default, migration version numbers are restricted to `i32` (signed, 32-bit integers). +- If you enable the `int8-versions` feature, this restriction is lifted to being able to use `i64`s for your migration version numbers (yay timestamps!). + Bear in mind that this feature must be enabled *before* you start using refinery on a given database. + Migrating an existing database's `refinery_schema_history` table to use `int8` versions will break the checksums on all previously-applied migrations, which is... bad. ```bash export DATABASE_URL="postgres://postgres:secret@localhost:5432/your-db" pushd migrations - # Runs ./src/V1__*.rs or ./src/V1__*.sql + # Runs ./src/V1__*.rs or ./src/V1__*.sql refinery migrate -e DATABASE_URL -p ./src -t 1 popd ``` diff --git a/refinery/Cargo.toml b/refinery/Cargo.toml index fce329ec..03679fd5 100644 --- a/refinery/Cargo.toml +++ b/refinery/Cargo.toml @@ -25,6 +25,7 @@ tiberius-config = ["refinery-core/tiberius", "refinery-core/tiberius-config"] serde = ["refinery-core/serde"] toml = ["refinery-core/toml"] enums = ["refinery-macros/enums"] +int8-versions = ["refinery-core/int8-versions"] [dependencies] refinery-core = { version = "0.8.14", path = "../refinery_core" } diff --git a/refinery/src/lib.rs b/refinery/src/lib.rs index 93cfaac5..945303cc 100644 --- a/refinery/src/lib.rs +++ b/refinery/src/lib.rs @@ -32,7 +32,9 @@ 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}; +pub use refinery_core::{ + error, load_sql_migrations, Error, Migration, Report, Runner, SchemaVersion, Target, +}; #[doc(hidden)] pub use refinery_core::{AsyncMigrate, Migrate}; pub use refinery_macros::embed_migrations; diff --git a/refinery/tests/migrations_int8/U20240504090241__initial.rs b/refinery/tests/migrations_int8/U20240504090241__initial.rs new file mode 100644 index 00000000..3a84e506 --- /dev/null +++ b/refinery/tests/migrations_int8/U20240504090241__initial.rs @@ -0,0 +1,15 @@ +use barrel::{types, Migration}; + +use crate::Sql; + +pub fn migration() -> String { + let mut m = Migration::new(); + + m.create_table("persons", |t| { + t.add_column("id", types::primary()); + t.add_column("name", types::varchar(255)); + t.add_column("city", types::varchar(255)); + }); + + m.make::() +} diff --git a/refinery/tests/migrations_int8/U20240504090301__add_cars_and_motos_table.sql b/refinery/tests/migrations_int8/U20240504090301__add_cars_and_motos_table.sql new file mode 100644 index 00000000..13390796 --- /dev/null +++ b/refinery/tests/migrations_int8/U20240504090301__add_cars_and_motos_table.sql @@ -0,0 +1,8 @@ +CREATE TABLE cars ( + id int, + name varchar(255) +); +CREATE TABLE motos ( + id int, + name varchar(255) +); diff --git a/refinery/tests/migrations_int8/U20240504090322__add_brand_to_cars_table.sql b/refinery/tests/migrations_int8/U20240504090322__add_brand_to_cars_table.sql new file mode 100644 index 00000000..c689a6aa --- /dev/null +++ b/refinery/tests/migrations_int8/U20240504090322__add_brand_to_cars_table.sql @@ -0,0 +1,2 @@ +ALTER TABLE cars +ADD brand varchar(255); diff --git a/refinery/tests/migrations_int8/U20240504090343__add_year_to_motos_table.rs b/refinery/tests/migrations_int8/U20240504090343__add_year_to_motos_table.rs new file mode 100644 index 00000000..57623df9 --- /dev/null +++ b/refinery/tests/migrations_int8/U20240504090343__add_year_to_motos_table.rs @@ -0,0 +1,13 @@ +use barrel::{types, Migration}; + +use crate::Sql; + +pub fn migration() -> String { + let mut m = Migration::new(); + + m.change_table("motos", |t| { + t.add_column("brand", types::varchar(255).nullable(true)); + }); + + m.make::() +} diff --git a/refinery/tests/postgres.rs b/refinery/tests/postgres.rs index 2ee4e756..a22c43cd 100644 --- a/refinery/tests/postgres.rs +++ b/refinery/tests/postgres.rs @@ -31,6 +31,16 @@ mod postgres { embed_migrations!("./tests/migrations_missing"); } + #[cfg(feature = "int8-versions")] + mod int8 { + use refinery::embed_migrations; + embed_migrations!("./tests/migrations_int8"); + } + + fn db_uri() -> String { + std::env::var("DB_URI").unwrap_or("postgres://postgres@localhost:5432/postgres".to_string()) + } + fn get_migrations() -> Vec { embed_migrations!("./tests/migrations"); @@ -189,6 +199,37 @@ mod postgres { }); } + #[test] + #[cfg(feature = "int8-versions")] + fn applies_migration_int8() { + run_test(|| { + let mut client = Client::connect(&db_uri(), NoTls).unwrap(); + let report = int8::migrations::runner().run(&mut client).unwrap(); + + let applied_migrations = report.applied_migrations(); + + assert_eq!(4, applied_migrations.len()); + + assert_eq!(20240504090241, applied_migrations[0].version()); + assert_eq!(20240504090301, applied_migrations[1].version()); + assert_eq!(20240504090322, applied_migrations[2].version()); + assert_eq!(20240504090343, applied_migrations[3].version()); + + client + .execute( + "INSERT INTO persons (name, city) VALUES ($1, $2)", + &[&"John Legend", &"New York"], + ) + .unwrap(); + for row in &client.query("SELECT name, city FROM persons", &[]).unwrap() { + let name: String = row.get(0); + let city: String = row.get(1); + assert_eq!("John Legend", name); + assert_eq!("New York", city); + } + }); + } + #[test] fn applies_migration_grouped_transaction() { run_test(|| { @@ -292,8 +333,15 @@ mod postgres { assert_eq!("initial", migrations[0].name()); assert_eq!("add_cars_table", applied_migrations[1].name()); + #[cfg(not(feature = "int8-versions"))] assert_eq!(2959965718684201605, applied_migrations[0].checksum()); + #[cfg(feature = "int8-versions")] + assert_eq!(13938959368620441626, applied_migrations[0].checksum()); + + #[cfg(not(feature = "int8-versions"))] assert_eq!(8238603820526370208, applied_migrations[1].checksum()); + #[cfg(feature = "int8-versions")] + assert_eq!(5394706226941044339, applied_migrations[1].checksum()); }); } diff --git a/refinery_cli/Cargo.toml b/refinery_cli/Cargo.toml index ac79a14e..045bce81 100644 --- a/refinery_cli/Cargo.toml +++ b/refinery_cli/Cargo.toml @@ -21,6 +21,7 @@ mysql = ["refinery-core/mysql"] sqlite = ["refinery-core/rusqlite"] sqlite-bundled = ["sqlite", "refinery-core/rusqlite-bundled"] mssql = ["refinery-core/tiberius-config", "tokio"] +int8-versions = ["refinery-core/int8-versions"] [dependencies] refinery-core = { version = "0.8.14", path = "../refinery_core", default-features = false, features = ["toml"] } diff --git a/refinery_cli/src/cli.rs b/refinery_cli/src/cli.rs index fd188266..d5d49ae6 100644 --- a/refinery_cli/src/cli.rs +++ b/refinery_cli/src/cli.rs @@ -4,6 +4,8 @@ use std::path::PathBuf; use clap::{Args, Parser}; +use refinery_core::SchemaVersion; + #[derive(Parser)] #[clap(version)] pub enum Cli { @@ -38,7 +40,7 @@ pub struct MigrateArgs { /// Migrate to the specified target version #[clap(short)] - pub target: Option, + pub target: Option, /// Set migration table name #[clap(long, default_value = "refinery_schema_history")] diff --git a/refinery_cli/src/migrate.rs b/refinery_cli/src/migrate.rs index b61fa876..77deae02 100644 --- a/refinery_cli/src/migrate.rs +++ b/refinery_cli/src/migrate.rs @@ -3,7 +3,7 @@ use std::path::Path; use anyhow::Context; use refinery_core::{ config::{Config, ConfigDbType}, - find_migration_files, Migration, MigrationType, Runner, Target, + find_migration_files, Migration, MigrationType, Runner, SchemaVersion, Target, }; use crate::cli::MigrateArgs; @@ -30,7 +30,7 @@ fn run_migrations( divergent: bool, missing: bool, fake: bool, - target: Option, + target: Option, env_var_opt: Option<&str>, path: &Path, table_name: &str, diff --git a/refinery_core/Cargo.toml b/refinery_core/Cargo.toml index 9f009554..339a4830 100644 --- a/refinery_core/Cargo.toml +++ b/refinery_core/Cargo.toml @@ -17,6 +17,7 @@ tokio-postgres = ["dep:tokio-postgres", "tokio", "tokio/rt"] mysql_async = ["dep:mysql_async"] serde = ["dep:serde"] toml = ["serde", "dep:toml"] +int8-versions = [] [dependencies] async-trait = "0.1" diff --git a/refinery_core/src/drivers/mysql_async.rs b/refinery_core/src/drivers/mysql_async.rs index 101ab0ab..c92fcd86 100644 --- a/refinery_core/src/drivers/mysql_async.rs +++ b/refinery_core/src/drivers/mysql_async.rs @@ -1,4 +1,5 @@ use crate::traits::r#async::{AsyncMigrate, AsyncQuery, AsyncTransaction}; +use crate::util::SchemaVersion; use crate::Migration; use async_trait::async_trait; use mysql_async::{ @@ -16,7 +17,7 @@ async fn query_applied_migrations<'a>( let applied = result .into_iter() .map(|row| { - let (version, name, applied_on, checksum): (i32, String, String, String) = + let (version, name, applied_on, checksum): (SchemaVersion, String, String, String) = mysql_async::from_row(row); // Safe to call unwrap, as we stored it in RFC3339 format on the database diff --git a/refinery_core/src/drivers/tiberius.rs b/refinery_core/src/drivers/tiberius.rs index 117c6344..a30e0311 100644 --- a/refinery_core/src/drivers/tiberius.rs +++ b/refinery_core/src/drivers/tiberius.rs @@ -1,4 +1,5 @@ use crate::traits::r#async::{AsyncMigrate, AsyncQuery, AsyncTransaction}; +use crate::util::SchemaVersion; use crate::Migration; use async_trait::async_trait; @@ -19,7 +20,7 @@ async fn query_applied_migrations( // Unfortunately too many unwraps as `Row::get` maps to Option instead of T while let Some(item) = rows.try_next().await? { if let QueryItem::Row(row) = item { - let version = row.get::(0).unwrap(); + let version = row.get::(0).unwrap(); let applied_on: &str = row.get::<&str, usize>(2).unwrap(); // Safe to call unwrap, as we stored it in RFC3339 format on the database let applied_on = OffsetDateTime::parse(applied_on, &Rfc3339).unwrap(); diff --git a/refinery_core/src/lib.rs b/refinery_core/src/lib.rs index b4d85b77..2d20865a 100644 --- a/refinery_core/src/lib.rs +++ b/refinery_core/src/lib.rs @@ -10,7 +10,7 @@ pub use crate::runner::{Migration, 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, MigrationType, + find_migration_files, load_sql_migrations, parse_migration_name, MigrationType, SchemaVersion, }; #[cfg(feature = "rusqlite")] diff --git a/refinery_core/src/runner.rs b/refinery_core/src/runner.rs index 46d4d714..003db875 100644 --- a/refinery_core/src/runner.rs +++ b/refinery_core/src/runner.rs @@ -8,7 +8,7 @@ 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::util::{parse_migration_name, SchemaVersion}; use crate::{AsyncMigrate, Error, Migrate}; use std::fmt::Formatter; @@ -43,9 +43,9 @@ impl fmt::Debug for Type { #[derive(Clone, Copy, Debug)] pub enum Target { Latest, - Version(u32), + Version(SchemaVersion), Fake, - FakeVersion(u32), + FakeVersion(SchemaVersion), } // an Enum set that represents the state of the migration: Applied on the database, @@ -66,7 +66,7 @@ pub struct Migration { state: State, name: String, checksum: u64, - version: i32, + version: SchemaVersion, prefix: Type, sql: Option, applied_on: Option, @@ -105,7 +105,7 @@ impl Migration { // Create a migration from an applied migration on the database pub fn applied( - version: i32, + version: SchemaVersion, name: String, applied_on: OffsetDateTime, checksum: u64, @@ -134,8 +134,8 @@ impl Migration { } /// Get the Migration version - pub fn version(&self) -> u32 { - self.version as u32 + pub fn version(&self) -> SchemaVersion { + self.version } /// Get the Prefix diff --git a/refinery_core/src/traits/async.rs b/refinery_core/src/traits/async.rs index 75621449..83babf43 100644 --- a/refinery_core/src/traits/async.rs +++ b/refinery_core/src/traits/async.rs @@ -122,7 +122,14 @@ where { // Needed cause some database vendors like Mssql have a non sql standard way of checking the migrations table fn assert_migrations_table_query(migration_table_name: &str) -> String { - ASSERT_MIGRATIONS_TABLE_QUERY.replace("%MIGRATION_TABLE_NAME%", migration_table_name) + #[cfg(not(feature = "int8-versions"))] + let version_type = "int4"; + #[cfg(feature = "int8-versions")] + let version_type = "int8"; + + ASSERT_MIGRATIONS_TABLE_QUERY + .replace("%MIGRATION_TABLE_NAME%", migration_table_name) + .replace("%VERSION_TYPE%", version_type) } async fn get_last_applied_migration( diff --git a/refinery_core/src/traits/mod.rs b/refinery_core/src/traits/mod.rs index d3eef6d3..f52b9043 100644 --- a/refinery_core/src/traits/mod.rs +++ b/refinery_core/src/traits/mod.rs @@ -4,6 +4,7 @@ pub mod r#async; pub mod sync; use crate::runner::Type; +use crate::util::SchemaVersion; use crate::{error::Kind, Error, Migration}; // Verifies applied and to be applied migrations returning Error if: @@ -49,10 +50,10 @@ pub(crate) fn verify_migrations( } } - let current: i32 = match applied.last() { + let current: SchemaVersion = match applied.last() { Some(last) => { log::info!("current version: {}", last.version()); - last.version() as i32 + last.version() as SchemaVersion } None => { log::info!("schema history table is empty, going to apply all migrations"); @@ -73,7 +74,7 @@ pub(crate) fn verify_migrations( if to_be_applied.contains(&migration) { return Err(Error::new(Kind::RepeatedVersion(migration), None)); } else if migration.prefix() == &Type::Versioned - && current >= migration.version() as i32 + && current >= migration.version() as SchemaVersion { if abort_missing { return Err(Error::new(Kind::MissingVersion(migration), None)); @@ -105,7 +106,7 @@ pub(crate) fn insert_migration_query(migration: &Migration, migration_table_name pub(crate) const ASSERT_MIGRATIONS_TABLE_QUERY: &str = "CREATE TABLE IF NOT EXISTS %MIGRATION_TABLE_NAME%( - version INT4 PRIMARY KEY, + version %VERSION_TYPE% PRIMARY KEY, name VARCHAR(255), applied_on VARCHAR(255), checksum VARCHAR(255));"; diff --git a/refinery_core/src/traits/sync.rs b/refinery_core/src/traits/sync.rs index 58238c0d..4f776781 100644 --- a/refinery_core/src/traits/sync.rs +++ b/refinery_core/src/traits/sync.rs @@ -90,10 +90,16 @@ where Self: Sized, { fn assert_migrations_table(&mut self, migration_table_name: &str) -> Result { + #[cfg(not(feature = "int8-versions"))] + let version_type = "int4"; + #[cfg(feature = "int8-versions")] + let version_type = "int8"; + // Needed cause some database vendors like Mssql have a non sql standard way of checking the migrations table, // thou on this case it's just to be consistent with the async trait `AsyncMigrate` self.execute(&[ASSERT_MIGRATIONS_TABLE_QUERY .replace("%MIGRATION_TABLE_NAME%", migration_table_name) + .replace("%VERSION_TYPE%", version_type) .as_str()]) .migration_err("error asserting migrations table", None) } diff --git a/refinery_core/src/util.rs b/refinery_core/src/util.rs index 24a2c65d..0f46c890 100644 --- a/refinery_core/src/util.rs +++ b/refinery_core/src/util.rs @@ -7,6 +7,11 @@ use std::path::{Path, PathBuf}; use std::sync::OnceLock; use walkdir::{DirEntry, WalkDir}; +#[cfg(not(feature = "int8-versions"))] +pub type SchemaVersion = i32; +#[cfg(feature = "int8-versions")] +pub type SchemaVersion = i64; + const STEM_RE: &'static str = r"^([U|V])(\d+(?:\.\d+)?)__(\w+)"; /// Matches the stem of a migration file. @@ -44,12 +49,12 @@ impl MigrationType { } /// Parse a migration filename stem into a prefix, version, and name. -pub fn parse_migration_name(name: &str) -> Result<(Type, i32, String), Error> { +pub fn parse_migration_name(name: &str) -> Result<(Type, SchemaVersion, String), Error> { let captures = file_stem_re() .captures(name) .filter(|caps| caps.len() == 4) .ok_or_else(|| Error::new(Kind::InvalidName, None))?; - let version: i32 = captures[2] + let version: SchemaVersion = captures[2] .parse() .map_err(|_| Error::new(Kind::InvalidVersion, None))?; diff --git a/refinery_macros/src/lib.rs b/refinery_macros/src/lib.rs index a185058b..47ff402d 100644 --- a/refinery_macros/src/lib.rs +++ b/refinery_macros/src/lib.rs @@ -19,7 +19,7 @@ pub(crate) fn crate_root() -> PathBuf { fn migration_fn_quoted(_migrations: Vec) -> TokenStream2 { let result = quote! { - use refinery::{Migration, Runner}; + use refinery::{Migration, Runner, SchemaVersion}; pub fn runner() -> Runner { let quoted_migrations: Vec<(&str, String)> = vec![#(#_migrations),*]; let mut migrations: Vec = Vec::new(); @@ -48,7 +48,7 @@ fn migration_enum_quoted(migration_names: &[impl AsRef]) -> TokenStream2 { discriminants.push(quote! { v => panic!("Invalid migration version '{}'", v) }); let result = quote! { - #[repr(i32)] + #[repr(i64)] #[derive(Debug)] pub enum EmbeddedMigration { #(#variants),* @@ -56,7 +56,7 @@ fn migration_enum_quoted(migration_names: &[impl AsRef]) -> TokenStream2 { impl From for EmbeddedMigration { fn from(migration: Migration) -> Self { - match migration.version() as i32 { + match migration.version() as SchemaVersion { #(#discriminants),* } } @@ -143,14 +143,14 @@ mod tests { #[cfg(feature = "enums")] fn test_enum_fn() { let expected = concat! { - "# [repr (i32)] # [derive (Debug)] ", + "# [repr (i64)] # [derive (Debug)] ", "pub enum EmbeddedMigration { ", "Foo (Migration) = 1i32 , ", "BarBaz (Migration) = 3i32 ", "} ", "impl From < Migration > for EmbeddedMigration { ", "fn from (migration : Migration) -> Self { ", - "match migration . version () as i32 { ", + "match migration . version () as SchemaVersion { ", "1i32 => Self :: Foo (migration) , ", "3i32 => Self :: BarBaz (migration) , ", "v => panic ! (\"Invalid migration version '{}'\" , v) ", @@ -164,7 +164,7 @@ mod tests { fn test_quote_fn() { let migs = vec![quote!("V1__first", "valid_sql_file")]; let expected = concat! { - "use refinery :: { Migration , Runner } ; ", + "use refinery :: { Migration , Runner , SchemaVersion } ; ", "pub fn runner () -> Runner { ", "let quoted_migrations : Vec < (& str , String) > = vec ! [\"V1__first\" , \"valid_sql_file\"] ; ", "let mut migrations : Vec < Migration > = Vec :: new () ; ", From 1ba080ad345623b468c2b482994d560ed71861df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Oliveira?= Date: Wed, 14 Aug 2024 10:53:21 +0100 Subject: [PATCH 2/6] cargo clippy --- refinery/tests/postgres.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/refinery/tests/postgres.rs b/refinery/tests/postgres.rs index b2392622..0b7c6688 100644 --- a/refinery/tests/postgres.rs +++ b/refinery/tests/postgres.rs @@ -35,7 +35,6 @@ mod postgres { embed_migrations!("./tests/migrations_int8"); } - fn db_uri() -> String { std::env::var("DB_URI").unwrap_or("postgres://postgres@localhost:5432/postgres".to_string()) } From bfceec370ee3020379f21bb7ec386bd9f1bfc8a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Oliveira?= Date: Wed, 14 Aug 2024 13:03:18 +0100 Subject: [PATCH 3/6] update wording --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 510bcd55..59722693 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,13 @@ If you are using a driver that is not yet supported, namely [`SQLx`](https://git - Migrations, both .sql files and Rust modules must be named in the format `[U|V]{1}__{2}.sql` or `[U|V]{1}__{2}.rs`, where `{1}` represents the migration version and `{2}` the name. - Migrations can be run either by embedding them in your Rust code with `embed_migrations` macro, or via [refinery_cli]. +NOTE: + +- By default, migration version numbers are restricted to `i32` (signed, 32-bit integers). +- If you enable the `int8-versions` feature, this restriction is lifted to being able to use `i64`s for your migration version numbers. + Bear in mind that this feature must be enabled *before* you start using refinery on a given database. + Migrating an existing database's `refinery_schema_history` table to use `int8` versions will break the checksums on all previously-applied migrations. + ### Example: Library ```rust,no_run use rusqlite::Connection; @@ -53,13 +60,6 @@ fn main() { For more library examples, refer to the [`examples`](https://github.com/rust-db/refinery/tree/main/examples). ### Example: CLI -NOTE: - -- By default, migration version numbers are restricted to `i32` (signed, 32-bit integers). -- If you enable the `int8-versions` feature, this restriction is lifted to being able to use `i64`s for your migration version numbers (yay timestamps!). - Bear in mind that this feature must be enabled *before* you start using refinery on a given database. - Migrating an existing database's `refinery_schema_history` table to use `int8` versions will break the checksums on all previously-applied migrations, which is... bad. - ```bash export DATABASE_URL="postgres://postgres:secret@localhost:5432/your-db" pushd migrations From 03165caf28c672945e987a4facc12cec823c7a69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Oliveira?= Date: Wed, 14 Aug 2024 14:43:14 +0100 Subject: [PATCH 4/6] remove repr --- refinery_macros/src/lib.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/refinery_macros/src/lib.rs b/refinery_macros/src/lib.rs index 47ff402d..87a0b70e 100644 --- a/refinery_macros/src/lib.rs +++ b/refinery_macros/src/lib.rs @@ -48,7 +48,6 @@ fn migration_enum_quoted(migration_names: &[impl AsRef]) -> TokenStream2 { discriminants.push(quote! { v => panic!("Invalid migration version '{}'", v) }); let result = quote! { - #[repr(i64)] #[derive(Debug)] pub enum EmbeddedMigration { #(#variants),* @@ -143,7 +142,7 @@ mod tests { #[cfg(feature = "enums")] fn test_enum_fn() { let expected = concat! { - "# [repr (i64)] # [derive (Debug)] ", + "# [derive (Debug)] ", "pub enum EmbeddedMigration { ", "Foo (Migration) = 1i32 , ", "BarBaz (Migration) = 3i32 ", From d4a32eac9d7409a9bcc13f04415a298c5427f6f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Oliveira?= Date: Wed, 14 Aug 2024 17:21:55 +0100 Subject: [PATCH 5/6] fix enum migrations --- .github/workflows/ci.yml | 3 +- examples/Cargo.toml | 3 +- refinery/Cargo.toml | 2 +- refinery/tests/postgres.rs | 2 +- refinery_macros/Cargo.toml | 5 +- refinery_macros/src/lib.rs | 97 +++++++++++++++++++++++++++----------- 6 files changed, 79 insertions(+), 33 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 120dda86..20dae58a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,7 +61,8 @@ jobs: - run: rustup self update - run: cd refinery_core && cargo test --all-features -- --test-threads 1 - run: cd refinery && cargo build --all-features - - run: cd refinery_macros && cargo test --features=enums + - run: cd refinery_macros && cargo test --all-features + - run: cd refinery_macros && cargo test --features enums enum_fn - run: cd refinery_cli && cargo test test-sqlite: diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 2a9d6037..8e77d622 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -10,10 +10,11 @@ edition = "2021" [features] enums = ["refinery/enums"] +int8-versions = ["refinery/int8-versions"] [dependencies] refinery = { path = "../refinery", features = ["rusqlite"] } rusqlite = "0.31" barrel = { version = "0.7", features = ["sqlite3"] } log = "0.4" -env_logger = "0.11" \ No newline at end of file +env_logger = "0.11" diff --git a/refinery/Cargo.toml b/refinery/Cargo.toml index 03679fd5..f358398c 100644 --- a/refinery/Cargo.toml +++ b/refinery/Cargo.toml @@ -25,7 +25,7 @@ tiberius-config = ["refinery-core/tiberius", "refinery-core/tiberius-config"] serde = ["refinery-core/serde"] toml = ["refinery-core/toml"] enums = ["refinery-macros/enums"] -int8-versions = ["refinery-core/int8-versions"] +int8-versions = ["refinery-core/int8-versions", "refinery-macros/int8-versions"] [dependencies] refinery-core = { version = "0.8.14", path = "../refinery_core" } diff --git a/refinery/tests/postgres.rs b/refinery/tests/postgres.rs index 0b7c6688..b091e872 100644 --- a/refinery/tests/postgres.rs +++ b/refinery/tests/postgres.rs @@ -195,7 +195,7 @@ mod postgres { fn applies_migration_int8() { run_test(|| { let mut client = Client::connect(&db_uri(), NoTls).unwrap(); - let report = int8::migrations::runner().run(&mut client).unwrap(); + let report = embedded::migrations::runner().run(&mut client).unwrap(); let applied_migrations = report.applied_migrations(); diff --git a/refinery_macros/Cargo.toml b/refinery_macros/Cargo.toml index 3907f378..2e191ca5 100644 --- a/refinery_macros/Cargo.toml +++ b/refinery_macros/Cargo.toml @@ -9,7 +9,8 @@ repository = "https://github.com/rust-db/refinery" edition = "2018" [features] -enums = [] +enums = ["dep:heck"] +int8-versions = ["refinery-core/int8-versions"] [lib] proc-macro = true @@ -20,7 +21,7 @@ quote = "1" syn = "2" proc-macro2 = "1" regex = "1" -heck = "0.5" +heck = { version = "0.5", optional = true } [dev-dependencies] tempfile = "3" diff --git a/refinery_macros/src/lib.rs b/refinery_macros/src/lib.rs index 87a0b70e..cd790cc1 100644 --- a/refinery_macros/src/lib.rs +++ b/refinery_macros/src/lib.rs @@ -1,6 +1,7 @@ //! Contains Refinery macros that are used to import and embed migration files. #![recursion_limit = "128"] +#[cfg(feature = "enums")] use heck::ToUpperCamelCase; use proc_macro::TokenStream; use proc_macro2::{Span as Span2, TokenStream as TokenStream2}; @@ -32,38 +33,53 @@ fn migration_fn_quoted(_migrations: Vec) -> TokenStream2 { result } +#[cfg(feature = "enums")] fn migration_enum_quoted(migration_names: &[impl AsRef]) -> TokenStream2 { - if cfg!(feature = "enums") { - let mut variants = Vec::new(); - let mut discriminants = Vec::new(); - - for m in migration_names { - let m = m.as_ref(); - let (_, version, name) = refinery_core::parse_migration_name(m) - .unwrap_or_else(|e| panic!("Couldn't parse migration filename '{}': {:?}", m, e)); - let variant = Ident::new(name.to_upper_camel_case().as_str(), Span2::call_site()); - variants.push(quote! { #variant(Migration) = #version }); - discriminants.push(quote! { #version => Self::#variant(migration) }); + use refinery_core::SchemaVersion; + + let mut variants = Vec::new(); + let mut discriminants = Vec::new(); + + for m in migration_names { + let m = m.as_ref(); + let (_, version, name) = refinery_core::parse_migration_name(m) + .unwrap_or_else(|e| panic!("Couldn't parse migration filename '{}': {:?}", m, e)); + let version: SchemaVersion = version; + let variant = Ident::new(name.to_upper_camel_case().as_str(), Span2::call_site()); + variants.push(quote! { #variant(Migration) = #version }); + discriminants.push(quote! { #version => Self::#variant(migration) }); + } + discriminants.push(quote! { v => panic!("Invalid migration version '{}'", v) }); + + #[cfg(feature = "int8-versions")] + let embedded = quote! { + #[repr(i64)] + #[derive(Debug)] + pub enum EmbeddedMigration { + #(#variants),* } - discriminants.push(quote! { v => panic!("Invalid migration version '{}'", v) }); + }; - let result = quote! { - #[derive(Debug)] - pub enum EmbeddedMigration { - #(#variants),* - } + #[cfg(not(feature = "int8-versions"))] + let embedded = quote! { + #[repr(i32)] + #[derive(Debug)] + pub enum EmbeddedMigration { + #(#variants),* + } + }; + + quote! { + + #embedded - impl From for EmbeddedMigration { - fn from(migration: Migration) -> Self { - match migration.version() as SchemaVersion { - #(#discriminants),* - } + impl From for EmbeddedMigration { + fn from(migration: Migration) -> Self { + match migration.version() as SchemaVersion { + #(#discriminants),* } } - }; - result - } else { - quote!() + } } } @@ -123,7 +139,11 @@ pub fn embed_migrations(input: TokenStream) -> TokenStream { } let fnq = migration_fn_quoted(_migrations); + #[cfg(feature = "enums")] let enums = migration_enum_quoted(migration_filenames.as_slice()); + #[cfg(not(feature = "enums"))] + let enums = quote!(); + (quote! { pub mod migrations { #(#migrations_mods)* @@ -138,10 +158,33 @@ pub fn embed_migrations(input: TokenStream) -> TokenStream { mod tests { use super::{migration_fn_quoted, quote}; + #[cfg(all(feature = "enums", feature = "int8-versions"))] + #[test] + fn test_enum_fn_i8() { + let expected = concat! { + "# [repr (i64)] ", + "# [derive (Debug)] ", + "pub enum EmbeddedMigration { ", + "Foo (Migration) = 1i64 , ", + "BarBaz (Migration) = 3i64 ", + "} ", + "impl From < Migration > for EmbeddedMigration { ", + "fn from (migration : Migration) -> Self { ", + "match migration . version () as SchemaVersion { ", + "1i64 => Self :: Foo (migration) , ", + "3i64 => Self :: BarBaz (migration) , ", + "v => panic ! (\"Invalid migration version '{}'\" , v) ", + "} } }" + }; + let enums = super::migration_enum_quoted(&["V1__foo", "U3__barBAZ"]).to_string(); + assert_eq!(expected, enums); + } + + #[cfg(all(feature = "enums", not(feature = "int8-versions")))] #[test] - #[cfg(feature = "enums")] fn test_enum_fn() { let expected = concat! { + "# [repr (i32)] ", "# [derive (Debug)] ", "pub enum EmbeddedMigration { ", "Foo (Migration) = 1i32 , ", From 56b81b1761f0e60f24cccba2a6507b1d93906fad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Oliveira?= Date: Wed, 14 Aug 2024 18:04:04 +0100 Subject: [PATCH 6/6] enable int8-versions test --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 20dae58a..28e63473 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -99,6 +99,7 @@ jobs: toolchain: ${{ matrix.rust }} - run: cargo install --path ./refinery_cli --no-default-features --features=postgresql - run: cd refinery && cargo test --features postgres --test postgres -- --test-threads 1 + - run: cd refinery && cargo test --features postgres,int8-versions --test postgres -- --test-threads 1 test-tokio-postgres: name: Test tokio-postgres