Skip to content

Commit 0beea31

Browse files
committed
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.
1 parent ff1c5c0 commit 0beea31

21 files changed

+147
-31
lines changed

README.md

+6-4
Original file line numberDiff line numberDiff line change
@@ -53,15 +53,17 @@ fn main() {
5353
For more library examples, refer to the [`examples`](https://github.com/rust-db/refinery/tree/main/examples).
5454
### Example: CLI
5555

56-
NOTE:
56+
NOTE:
5757

58-
- Contiguous (adjacent) migration version numbers are restricted to `u32` (unsigned, 32-bit integers).
59-
- Non-contiguous (not adjacent) migration version numbers are restricted to `u32` (unsigned, 32-bit integers).
58+
- By default, migration version numbers are restricted to `i32` (signed, 32-bit integers).
59+
- 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!).
60+
Bear in mind that this feature must be enabled *before* you start using refinery on a given database.
61+
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.
6062

6163
```bash
6264
export DATABASE_URL="postgres://postgres:secret@localhost:5432/your-db"
6365
pushd migrations
64-
# Runs ./src/V1__*.rs or ./src/V1__*.sql
66+
# Runs ./src/V1__*.rs or ./src/V1__*.sql
6567
refinery migrate -e DATABASE_URL -p ./src -t 1
6668
popd
6769
```

refinery/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ tiberius-config = ["refinery-core/tiberius", "refinery-core/tiberius-config"]
2525
serde = ["refinery-core/serde"]
2626
toml = ["refinery-core/toml"]
2727
enums = ["refinery-macros/enums"]
28+
int8-versions = ["refinery-core/int8-versions"]
2829

2930
[dependencies]
3031
refinery-core = { version = "0.8.14", path = "../refinery_core" }

refinery/src/lib.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ for more examples refer to the [examples](https://github.com/rust-db/refinery/tr
3232
*/
3333

3434
pub use refinery_core::config;
35-
pub use refinery_core::{error, load_sql_migrations, Error, Migration, Report, Runner, Target};
35+
pub use refinery_core::{
36+
error, load_sql_migrations, Error, Migration, Report, Runner, SchemaVersion, Target,
37+
};
3638
#[doc(hidden)]
3739
pub use refinery_core::{AsyncMigrate, Migrate};
3840
pub use refinery_macros::embed_migrations;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
use barrel::{types, Migration};
2+
3+
use crate::Sql;
4+
5+
pub fn migration() -> String {
6+
let mut m = Migration::new();
7+
8+
m.create_table("persons", |t| {
9+
t.add_column("id", types::primary());
10+
t.add_column("name", types::varchar(255));
11+
t.add_column("city", types::varchar(255));
12+
});
13+
14+
m.make::<Sql>()
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
CREATE TABLE cars (
2+
id int,
3+
name varchar(255)
4+
);
5+
CREATE TABLE motos (
6+
id int,
7+
name varchar(255)
8+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
ALTER TABLE cars
2+
ADD brand varchar(255);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
use barrel::{types, Migration};
2+
3+
use crate::Sql;
4+
5+
pub fn migration() -> String {
6+
let mut m = Migration::new();
7+
8+
m.change_table("motos", |t| {
9+
t.add_column("brand", types::varchar(255).nullable(true));
10+
});
11+
12+
m.make::<Sql>()
13+
}

refinery/tests/postgres.rs

+48
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,16 @@ mod postgres {
3131
embed_migrations!("./tests/migrations_missing");
3232
}
3333

34+
#[cfg(feature = "int8-versions")]
35+
mod int8 {
36+
use refinery::embed_migrations;
37+
embed_migrations!("./tests/migrations_int8");
38+
}
39+
40+
fn db_uri() -> String {
41+
std::env::var("DB_URI").unwrap_or("postgres://postgres@localhost:5432/postgres".to_string())
42+
}
43+
3444
fn get_migrations() -> Vec<Migration> {
3545
embed_migrations!("./tests/migrations");
3646

@@ -189,6 +199,37 @@ mod postgres {
189199
});
190200
}
191201

202+
#[test]
203+
#[cfg(feature = "int8-versions")]
204+
fn applies_migration_int8() {
205+
run_test(|| {
206+
let mut client = Client::connect(&db_uri(), NoTls).unwrap();
207+
let report = int8::migrations::runner().run(&mut client).unwrap();
208+
209+
let applied_migrations = report.applied_migrations();
210+
211+
assert_eq!(4, applied_migrations.len());
212+
213+
assert_eq!(20240504090241, applied_migrations[0].version());
214+
assert_eq!(20240504090301, applied_migrations[1].version());
215+
assert_eq!(20240504090322, applied_migrations[2].version());
216+
assert_eq!(20240504090343, applied_migrations[3].version());
217+
218+
client
219+
.execute(
220+
"INSERT INTO persons (name, city) VALUES ($1, $2)",
221+
&[&"John Legend", &"New York"],
222+
)
223+
.unwrap();
224+
for row in &client.query("SELECT name, city FROM persons", &[]).unwrap() {
225+
let name: String = row.get(0);
226+
let city: String = row.get(1);
227+
assert_eq!("John Legend", name);
228+
assert_eq!("New York", city);
229+
}
230+
});
231+
}
232+
192233
#[test]
193234
fn applies_migration_grouped_transaction() {
194235
run_test(|| {
@@ -292,8 +333,15 @@ mod postgres {
292333
assert_eq!("initial", migrations[0].name());
293334
assert_eq!("add_cars_table", applied_migrations[1].name());
294335

336+
#[cfg(not(feature = "int8-versions"))]
295337
assert_eq!(2959965718684201605, applied_migrations[0].checksum());
338+
#[cfg(feature = "int8-versions")]
339+
assert_eq!(13938959368620441626, applied_migrations[0].checksum());
340+
341+
#[cfg(not(feature = "int8-versions"))]
296342
assert_eq!(8238603820526370208, applied_migrations[1].checksum());
343+
#[cfg(feature = "int8-versions")]
344+
assert_eq!(5394706226941044339, applied_migrations[1].checksum());
297345
});
298346
}
299347

refinery_cli/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ mysql = ["refinery-core/mysql"]
2121
sqlite = ["refinery-core/rusqlite"]
2222
sqlite-bundled = ["sqlite", "refinery-core/rusqlite-bundled"]
2323
mssql = ["refinery-core/tiberius-config", "tokio"]
24+
int8-versions = ["refinery-core/int8-versions"]
2425

2526
[dependencies]
2627
refinery-core = { version = "0.8.14", path = "../refinery_core", default-features = false, features = ["toml"] }

refinery_cli/src/cli.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ use std::path::PathBuf;
44

55
use clap::{Args, Parser};
66

7+
use refinery_core::SchemaVersion;
8+
79
#[derive(Parser)]
810
#[clap(version)]
911
pub enum Cli {
@@ -38,7 +40,7 @@ pub struct MigrateArgs {
3840

3941
/// Migrate to the specified target version
4042
#[clap(short)]
41-
pub target: Option<u32>,
43+
pub target: Option<SchemaVersion>,
4244

4345
/// Set migration table name
4446
#[clap(long, default_value = "refinery_schema_history")]

refinery_cli/src/migrate.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::path::Path;
33
use anyhow::Context;
44
use refinery_core::{
55
config::{Config, ConfigDbType},
6-
find_migration_files, Migration, MigrationType, Runner, Target,
6+
find_migration_files, Migration, MigrationType, Runner, SchemaVersion, Target,
77
};
88

99
use crate::cli::MigrateArgs;
@@ -30,7 +30,7 @@ fn run_migrations(
3030
divergent: bool,
3131
missing: bool,
3232
fake: bool,
33-
target: Option<u32>,
33+
target: Option<SchemaVersion>,
3434
env_var_opt: Option<&str>,
3535
path: &Path,
3636
table_name: &str,

refinery_core/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ tokio-postgres = ["dep:tokio-postgres", "tokio", "tokio/rt"]
1717
mysql_async = ["dep:mysql_async"]
1818
serde = ["dep:serde"]
1919
toml = ["serde", "dep:toml"]
20+
int8-versions = []
2021

2122
[dependencies]
2223
async-trait = "0.1"

refinery_core/src/drivers/mysql_async.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::traits::r#async::{AsyncMigrate, AsyncQuery, AsyncTransaction};
2+
use crate::util::SchemaVersion;
23
use crate::Migration;
34
use async_trait::async_trait;
45
use mysql_async::{
@@ -16,7 +17,7 @@ async fn query_applied_migrations<'a>(
1617
let applied = result
1718
.into_iter()
1819
.map(|row| {
19-
let (version, name, applied_on, checksum): (i32, String, String, String) =
20+
let (version, name, applied_on, checksum): (SchemaVersion, String, String, String) =
2021
mysql_async::from_row(row);
2122

2223
// Safe to call unwrap, as we stored it in RFC3339 format on the database

refinery_core/src/drivers/tiberius.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::traits::r#async::{AsyncMigrate, AsyncQuery, AsyncTransaction};
2+
use crate::util::SchemaVersion;
23
use crate::Migration;
34

45
use async_trait::async_trait;
@@ -19,7 +20,7 @@ async fn query_applied_migrations<S: AsyncRead + AsyncWrite + Unpin + Send>(
1920
// Unfortunately too many unwraps as `Row::get` maps to Option<T> instead of T
2021
while let Some(item) = rows.try_next().await? {
2122
if let QueryItem::Row(row) = item {
22-
let version = row.get::<i32, usize>(0).unwrap();
23+
let version = row.get::<SchemaVersion, usize>(0).unwrap();
2324
let applied_on: &str = row.get::<&str, usize>(2).unwrap();
2425
// Safe to call unwrap, as we stored it in RFC3339 format on the database
2526
let applied_on = OffsetDateTime::parse(applied_on, &Rfc3339).unwrap();

refinery_core/src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ pub use crate::runner::{Migration, Report, Runner, Target};
1010
pub use crate::traits::r#async::AsyncMigrate;
1111
pub use crate::traits::sync::Migrate;
1212
pub use crate::util::{
13-
find_migration_files, load_sql_migrations, parse_migration_name, MigrationType,
13+
find_migration_files, load_sql_migrations, parse_migration_name, MigrationType, SchemaVersion,
1414
};
1515

1616
#[cfg(feature = "rusqlite")]

refinery_core/src/runner.rs

+7-7
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use std::fmt;
88
use std::hash::{Hash, Hasher};
99

1010
use crate::traits::{sync::migrate as sync_migrate, DEFAULT_MIGRATION_TABLE_NAME};
11-
use crate::util::parse_migration_name;
11+
use crate::util::{parse_migration_name, SchemaVersion};
1212
use crate::{AsyncMigrate, Error, Migrate};
1313
use std::fmt::Formatter;
1414

@@ -43,9 +43,9 @@ impl fmt::Debug for Type {
4343
#[derive(Clone, Copy, Debug)]
4444
pub enum Target {
4545
Latest,
46-
Version(u32),
46+
Version(SchemaVersion),
4747
Fake,
48-
FakeVersion(u32),
48+
FakeVersion(SchemaVersion),
4949
}
5050

5151
// an Enum set that represents the state of the migration: Applied on the database,
@@ -66,7 +66,7 @@ pub struct Migration {
6666
state: State,
6767
name: String,
6868
checksum: u64,
69-
version: i32,
69+
version: SchemaVersion,
7070
prefix: Type,
7171
sql: Option<String>,
7272
applied_on: Option<OffsetDateTime>,
@@ -105,7 +105,7 @@ impl Migration {
105105

106106
// Create a migration from an applied migration on the database
107107
pub fn applied(
108-
version: i32,
108+
version: SchemaVersion,
109109
name: String,
110110
applied_on: OffsetDateTime,
111111
checksum: u64,
@@ -134,8 +134,8 @@ impl Migration {
134134
}
135135

136136
/// Get the Migration version
137-
pub fn version(&self) -> u32 {
138-
self.version as u32
137+
pub fn version(&self) -> SchemaVersion {
138+
self.version
139139
}
140140

141141
/// Get the Prefix

refinery_core/src/traits/async.rs

+8-1
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,14 @@ where
122122
{
123123
// Needed cause some database vendors like Mssql have a non sql standard way of checking the migrations table
124124
fn assert_migrations_table_query(migration_table_name: &str) -> String {
125-
ASSERT_MIGRATIONS_TABLE_QUERY.replace("%MIGRATION_TABLE_NAME%", migration_table_name)
125+
#[cfg(not(feature = "int8-versions"))]
126+
let version_type = "int4";
127+
#[cfg(feature = "int8-versions")]
128+
let version_type = "int8";
129+
130+
ASSERT_MIGRATIONS_TABLE_QUERY
131+
.replace("%MIGRATION_TABLE_NAME%", migration_table_name)
132+
.replace("%VERSION_TYPE%", version_type)
126133
}
127134

128135
async fn get_last_applied_migration(

refinery_core/src/traits/mod.rs

+5-4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ pub mod r#async;
44
pub mod sync;
55

66
use crate::runner::Type;
7+
use crate::util::SchemaVersion;
78
use crate::{error::Kind, Error, Migration};
89

910
// Verifies applied and to be applied migrations returning Error if:
@@ -49,10 +50,10 @@ pub(crate) fn verify_migrations(
4950
}
5051
}
5152

52-
let current: i32 = match applied.last() {
53+
let current: SchemaVersion = match applied.last() {
5354
Some(last) => {
5455
log::info!("current version: {}", last.version());
55-
last.version() as i32
56+
last.version() as SchemaVersion
5657
}
5758
None => {
5859
log::info!("schema history table is empty, going to apply all migrations");
@@ -73,7 +74,7 @@ pub(crate) fn verify_migrations(
7374
if to_be_applied.contains(&migration) {
7475
return Err(Error::new(Kind::RepeatedVersion(migration), None));
7576
} else if migration.prefix() == &Type::Versioned
76-
&& current >= migration.version() as i32
77+
&& current >= migration.version() as SchemaVersion
7778
{
7879
if abort_missing {
7980
return Err(Error::new(Kind::MissingVersion(migration), None));
@@ -105,7 +106,7 @@ pub(crate) fn insert_migration_query(migration: &Migration, migration_table_name
105106

106107
pub(crate) const ASSERT_MIGRATIONS_TABLE_QUERY: &str =
107108
"CREATE TABLE IF NOT EXISTS %MIGRATION_TABLE_NAME%(
108-
version INT4 PRIMARY KEY,
109+
version %VERSION_TYPE% PRIMARY KEY,
109110
name VARCHAR(255),
110111
applied_on VARCHAR(255),
111112
checksum VARCHAR(255));";

refinery_core/src/traits/sync.rs

+6
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,16 @@ where
9090
Self: Sized,
9191
{
9292
fn assert_migrations_table(&mut self, migration_table_name: &str) -> Result<usize, Error> {
93+
#[cfg(not(feature = "int8-versions"))]
94+
let version_type = "int4";
95+
#[cfg(feature = "int8-versions")]
96+
let version_type = "int8";
97+
9398
// Needed cause some database vendors like Mssql have a non sql standard way of checking the migrations table,
9499
// thou on this case it's just to be consistent with the async trait `AsyncMigrate`
95100
self.execute(&[ASSERT_MIGRATIONS_TABLE_QUERY
96101
.replace("%MIGRATION_TABLE_NAME%", migration_table_name)
102+
.replace("%VERSION_TYPE%", version_type)
97103
.as_str()])
98104
.migration_err("error asserting migrations table", None)
99105
}

refinery_core/src/util.rs

+7-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ use std::path::{Path, PathBuf};
77
use std::sync::OnceLock;
88
use walkdir::{DirEntry, WalkDir};
99

10+
#[cfg(not(feature = "int8-versions"))]
11+
pub type SchemaVersion = i32;
12+
#[cfg(feature = "int8-versions")]
13+
pub type SchemaVersion = i64;
14+
1015
const STEM_RE: &'static str = r"^([U|V])(\d+(?:\.\d+)?)__(\w+)";
1116

1217
/// Matches the stem of a migration file.
@@ -44,12 +49,12 @@ impl MigrationType {
4449
}
4550

4651
/// Parse a migration filename stem into a prefix, version, and name.
47-
pub fn parse_migration_name(name: &str) -> Result<(Type, i32, String), Error> {
52+
pub fn parse_migration_name(name: &str) -> Result<(Type, SchemaVersion, String), Error> {
4853
let captures = file_stem_re()
4954
.captures(name)
5055
.filter(|caps| caps.len() == 4)
5156
.ok_or_else(|| Error::new(Kind::InvalidName, None))?;
52-
let version: i32 = captures[2]
57+
let version: SchemaVersion = captures[2]
5358
.parse()
5459
.map_err(|_| Error::new(Kind::InvalidVersion, None))?;
5560

0 commit comments

Comments
 (0)