Skip to content

Commit

Permalink
Dynamic migration dicovery, bug fix (#313)
Browse files Browse the repository at this point in the history
* Fixes a bug in `get_last_applied_migration` due to an incorrect SQL
  query that partially uses dynamic table names and partially hardcodes
  the default one.

* Adds a new utility function that enables dynamic migration discovery
  where embedding is not desirable.
  • Loading branch information
alfred-hodler authored Mar 3, 2024
1 parent e49eb1f commit 83c8cba
Show file tree
Hide file tree
Showing 5 changed files with 58 additions and 4 deletions.
2 changes: 1 addition & 1 deletion refinery/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ for more examples refer to the [examples](https://github.com/rust-db/refinery/tr
*/

pub use refinery_core::config;
pub use refinery_core::{error, Error, Migration, Report, Runner, Target};
pub use refinery_core::{error, load_sql_migrations, Error, Migration, Report, Runner, Target};
#[doc(hidden)]
pub use refinery_core::{AsyncMigrate, Migrate};
pub use refinery_macros::embed_migrations;
3 changes: 3 additions & 0 deletions refinery_core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ pub enum Kind {
/// An Error from an underlying database connection Error
#[error("`{0}`, `{1}`")]
Connection(String, #[source] Box<dyn std::error::Error + Sync + Send>),
/// An Error from an invalid migration file (not UTF-8 etc)
#[error("invalid migration file at path {0}, {1}")]
InvalidMigrationFile(PathBuf, std::io::Error),
}

// Helper trait for adding custom messages and applied migrations to Connection error's.
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 @@ -9,7 +9,7 @@ pub use crate::error::Error;
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, MigrationType};
pub use crate::util::{find_migration_files, load_sql_migrations, MigrationType};

#[cfg(feature = "rusqlite")]
pub use rusqlite;
Expand Down
2 changes: 1 addition & 1 deletion refinery_core/src/traits/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ pub(crate) const GET_APPLIED_MIGRATIONS_QUERY: &str = "SELECT version, name, app

pub(crate) const GET_LAST_APPLIED_MIGRATION_QUERY: &str =
"SELECT version, name, applied_on, checksum
FROM %MIGRATION_TABLE_NAME% WHERE version=(SELECT MAX(version) from refinery_schema_history)";
FROM %MIGRATION_TABLE_NAME% WHERE version=(SELECT MAX(version) from %MIGRATION_TABLE_NAME%)";

pub(crate) const DEFAULT_MIGRATION_TABLE_NAME: &str = "refinery_schema_history";

Expand Down
53 changes: 52 additions & 1 deletion refinery_core/src/util.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::error::{Error, Kind};
use crate::Migration;
use regex::Regex;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
Expand Down Expand Up @@ -58,9 +59,41 @@ pub fn find_migration_files(
Ok(file_paths)
}

/// 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<Path>) -> Result<Vec<Migration>, Error> {
let migration_files = find_migration_files(location, MigrationType::Sql)?;

let mut migrations = vec![];

for path in migration_files {
let sql = std::fs::read_to_string(path.as_path()).map_err(|e| {
let path = path.to_owned();
let kind = match e.kind() {
std::io::ErrorKind::NotFound => Kind::InvalidMigrationPath(path, e),
_ => Kind::InvalidMigrationFile(path, e),
};

Error::new(kind, None)
})?;

//safe to call unwrap as find_migration_filenames returns canonical paths
let filename = path
.file_stem()
.and_then(|file| file.to_os_string().into_string().ok())
.unwrap();

let migration = Migration::unapplied(&filename, &sql)?;
migrations.push(migration);
}

migrations.sort();
Ok(migrations)
}

#[cfg(test)]
mod tests {
use super::{find_migration_files, MigrationType};
use super::{find_migration_files, load_sql_migrations, MigrationType};
use std::fs;
use std::path::PathBuf;
use tempfile::TempDir;
Expand Down Expand Up @@ -146,4 +179,22 @@ mod tests {
let mut mods = find_migration_files(migrations_dir, MigrationType::All).unwrap();
assert!(mods.next().is_none());
}

#[test]
fn loads_migrations_from_path() {
let tmp_dir = TempDir::new().unwrap();
let migrations_dir = tmp_dir.path().join("migrations");
fs::create_dir(&migrations_dir).unwrap();
let sql1 = migrations_dir.join("V1__first.sql");
fs::File::create(&sql1).unwrap();
let sql2 = migrations_dir.join("V2__second.sql");
fs::File::create(&sql2).unwrap();
let rs3 = migrations_dir.join("V3__third.rs");
fs::File::create(&rs3).unwrap();

let migrations = load_sql_migrations(migrations_dir).unwrap();
assert_eq!(migrations.len(), 2);
assert_eq!(&migrations[0].to_string(), "V1__first");
assert_eq!(&migrations[1].to_string(), "V2__second");
}
}

0 comments on commit 83c8cba

Please sign in to comment.