Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support int8 migration versions via new int8-versions feature #330

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand Down
1 change: 1 addition & 0 deletions refinery/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
4 changes: 3 additions & 1 deletion refinery/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
15 changes: 15 additions & 0 deletions refinery/tests/migrations_int8/U20240504090241__initial.rs
Original file line number Diff line number Diff line change
@@ -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::<Sql>()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
CREATE TABLE cars (
id int,
name varchar(255)
);
CREATE TABLE motos (
id int,
name varchar(255)
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE cars
ADD brand varchar(255);
Original file line number Diff line number Diff line change
@@ -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::<Sql>()
}
48 changes: 48 additions & 0 deletions refinery/tests/postgres.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Migration> {
embed_migrations!("./tests/migrations");

Expand Down Expand Up @@ -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(|| {
Expand Down Expand Up @@ -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());
});
}

Expand Down
1 change: 1 addition & 0 deletions refinery_cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
4 changes: 3 additions & 1 deletion refinery_cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ use std::path::PathBuf;

use clap::{Args, Parser};

use refinery_core::SchemaVersion;

#[derive(Parser)]
#[clap(version)]
pub enum Cli {
Expand Down Expand Up @@ -38,7 +40,7 @@ pub struct MigrateArgs {

/// Migrate to the specified target version
#[clap(short)]
pub target: Option<u32>,
pub target: Option<SchemaVersion>,

/// Set migration table name
#[clap(long, default_value = "refinery_schema_history")]
Expand Down
4 changes: 2 additions & 2 deletions refinery_cli/src/migrate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -30,7 +30,7 @@ fn run_migrations(
divergent: bool,
missing: bool,
fake: bool,
target: Option<u32>,
target: Option<SchemaVersion>,
env_var_opt: Option<&str>,
path: &Path,
table_name: &str,
Expand Down
1 change: 1 addition & 0 deletions refinery_core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 2 additions & 1 deletion refinery_core/src/drivers/mysql_async.rs
Original file line number Diff line number Diff line change
@@ -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::{
Expand All @@ -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
Expand Down
3 changes: 2 additions & 1 deletion refinery_core/src/drivers/tiberius.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::traits::r#async::{AsyncMigrate, AsyncQuery, AsyncTransaction};
use crate::util::SchemaVersion;
use crate::Migration;

use async_trait::async_trait;
Expand All @@ -19,7 +20,7 @@ async fn query_applied_migrations<S: AsyncRead + AsyncWrite + Unpin + Send>(
// Unfortunately too many unwraps as `Row::get` maps to Option<T> instead of T
while let Some(item) = rows.try_next().await? {
if let QueryItem::Row(row) = item {
let version = row.get::<i32, usize>(0).unwrap();
let version = row.get::<SchemaVersion, usize>(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();
Expand Down
2 changes: 1 addition & 1 deletion refinery_core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down
14 changes: 7 additions & 7 deletions refinery_core/src/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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,
Expand All @@ -66,7 +66,7 @@ pub struct Migration {
state: State,
name: String,
checksum: u64,
version: i32,
version: SchemaVersion,
prefix: Type,
sql: Option<String>,
applied_on: Option<OffsetDateTime>,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
9 changes: 8 additions & 1 deletion refinery_core/src/traits/async.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
9 changes: 5 additions & 4 deletions refinery_core/src/traits/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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");
Expand All @@ -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));
Expand Down Expand Up @@ -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));";
Expand Down
6 changes: 6 additions & 0 deletions refinery_core/src/traits/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,16 @@ where
Self: Sized,
{
fn assert_migrations_table(&mut self, migration_table_name: &str) -> Result<usize, Error> {
#[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)
}
Expand Down
9 changes: 7 additions & 2 deletions refinery_core/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Comment on lines +10 to +14
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

features in Rust should be additive not mutually exclusive, enabling --all-features should not break compilation, see here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enabling --all-features doesn't, as far as I'm aware, break compilation. CI is all green.

const STEM_RE: &'static str = r"^([U|V])(\d+(?:\.\d+)?)__(\w+)";

/// Matches the stem of a migration file.
Expand Down Expand Up @@ -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))?;

Expand Down
Loading
Loading