Archived
1
0
Fork 0

Compare commits

..

No commits in common. "main" and "core-v1.0.0" have entirely different histories.

10 changed files with 157 additions and 46 deletions

View file

@ -12,7 +12,6 @@ pub type MigraResult<T> = Result<T, Error>;
/// Migra error /// Migra error
#[derive(Debug)] #[derive(Debug)]
#[non_exhaustive]
pub enum Error { pub enum Error {
/// Represents database errors. /// Represents database errors.
Db(DbError), Db(DbError),
@ -55,7 +54,6 @@ impl Error {
/// All kinds of errors with witch this crate works. /// All kinds of errors with witch this crate works.
#[derive(Debug)] #[derive(Debug)]
#[non_exhaustive]
pub enum DbKind { pub enum DbKind {
/// Failed to database connection. /// Failed to database connection.
DatabaseConnection, DatabaseConnection,

View file

@ -21,7 +21,7 @@ pub fn get_all_migrations(dir_path: &Path) -> MigraResult<migration::List> {
Err(e) if e.kind() == io::ErrorKind::NotFound => vec![], Err(e) if e.kind() == io::ErrorKind::NotFound => vec![],
entries => entries? entries => entries?
.filter_map(|res| res.ok().map(|e| e.path())) .filter_map(|res| res.ok().map(|e| e.path()))
.filter(|path| is_migration_dir(path)) .filter(|path| is_migration_dir(&path))
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
}; };

View file

@ -29,6 +29,9 @@ impl Migration {
/// ///
/// Can be presented as a list of all migrations, a list of pending migrations /// Can be presented as a list of all migrations, a list of pending migrations
/// or a list of applied migrations, depending on the implementation. /// or a list of applied migrations, depending on the implementation.
///
///
///
#[derive(Debug, Clone, Default, PartialEq, Eq)] #[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct List { pub struct List {
inner: Vec<Migration>, inner: Vec<Migration>,
@ -98,7 +101,7 @@ impl List {
/// Push migration to list. /// Push migration to list.
pub fn push(&mut self, migration: Migration) { pub fn push(&mut self, migration: Migration) {
self.inner.push(migration); self.inner.push(migration)
} }
/// Push migration name to list. /// Push migration name to list.
@ -120,7 +123,7 @@ impl List {
/// # assert_eq!(list, List::from(vec!["name"])); /// # assert_eq!(list, List::from(vec!["name"]));
/// ``` /// ```
pub fn push_name(&mut self, name: &str) { pub fn push_name(&mut self, name: &str) {
self.inner.push(Migration::new(name)); self.inner.push(Migration::new(name))
} }
/// Check if list contains specific migration. /// Check if list contains specific migration.
@ -198,7 +201,7 @@ mod tests {
assert_eq!(list, List::from(vec![FIRST_MIGRATION])); assert_eq!(list, List::from(vec![FIRST_MIGRATION]));
list.push(Migration::new(SECOND_MIGRATION)); list.push(Migration::new(SECOND_MIGRATION));
assert_eq!(list, List::from(vec![FIRST_MIGRATION, SECOND_MIGRATION])); assert_eq!(list, List::from(vec![FIRST_MIGRATION, SECOND_MIGRATION]))
} }
#[test] #[test]
@ -209,23 +212,23 @@ mod tests {
assert_eq!(list, List::from(vec![FIRST_MIGRATION])); assert_eq!(list, List::from(vec![FIRST_MIGRATION]));
list.push_name(&String::from(SECOND_MIGRATION)); list.push_name(&String::from(SECOND_MIGRATION));
assert_eq!(list, List::from(vec![FIRST_MIGRATION, SECOND_MIGRATION])); assert_eq!(list, List::from(vec![FIRST_MIGRATION, SECOND_MIGRATION]))
} }
#[test] #[test]
fn contains_migration() { fn contains_migration() {
let list = List::from(vec![FIRST_MIGRATION]); let list = List::from(vec![FIRST_MIGRATION]);
assert!(list.contains(&Migration::new(FIRST_MIGRATION))); assert_eq!(list.contains(&Migration::new(FIRST_MIGRATION)), true);
assert!(!list.contains(&Migration::new(SECOND_MIGRATION))); assert_eq!(list.contains(&Migration::new(SECOND_MIGRATION)), false);
} }
#[test] #[test]
fn contains_migration_name() { fn contains_migration_name() {
let list = List::from(vec![FIRST_MIGRATION]); let list = List::from(vec![FIRST_MIGRATION]);
assert!(list.contains_name(FIRST_MIGRATION)); assert_eq!(list.contains_name(FIRST_MIGRATION), true);
assert!(!list.contains_name(SECOND_MIGRATION)); assert_eq!(list.contains_name(SECOND_MIGRATION), false);
} }
#[test] #[test]
@ -234,6 +237,6 @@ mod tests {
let applied_migrations = List::from(vec![FIRST_MIGRATION]); let applied_migrations = List::from(vec![FIRST_MIGRATION]);
let excluded = all_migrations.exclude(&applied_migrations); let excluded = all_migrations.exclude(&applied_migrations);
assert_eq!(excluded, List::from(vec![SECOND_MIGRATION])); assert_eq!(excluded, List::from(vec![SECOND_MIGRATION]))
} }
} }

View file

@ -20,12 +20,22 @@ pub(crate) fn apply_sql(app: &App, cmd_opts: &ApplyCommandOpt) -> migra::StdResu
.map(std::fs::read_to_string) .map(std::fs::read_to_string)
.collect::<Result<Vec<_>, _>>()?; .collect::<Result<Vec<_>, _>>()?;
database::run_in_transaction(&mut client, |client| { database::should_run_in_transaction(
file_contents &mut client,
.iter() cmd_opts.transaction_opts.single_transaction,
.try_for_each(|content| client.apply_sql(content)) |client| {
.map_err(From::from) file_contents
})?; .iter()
.try_for_each(|content| {
database::should_run_in_transaction(
client,
!cmd_opts.transaction_opts.single_transaction,
|client| client.apply_sql(content),
)
})
.map_err(From::from)
},
)?;
Ok(()) Ok(())
} }

View file

@ -32,19 +32,27 @@ pub(crate) fn rollback_applied_migrations(
}) })
.collect::<Result<Vec<_>, _>>()?; .collect::<Result<Vec<_>, _>>()?;
database::run_in_transaction(&mut client, |client| { database::should_run_in_transaction(
migrations_with_content &mut client,
.iter() opts.transaction_opts.single_transaction,
.try_for_each(|(migration_name, content)| { |client| {
if all_migrations.contains_name(migration_name) { migrations_with_content
println!("downgrade {}...", migration_name); .iter()
client.run_downgrade_migration(migration_name, content) .try_for_each(|(migration_name, content)| {
} else { if all_migrations.contains_name(migration_name) {
Ok(()) println!("downgrade {}...", migration_name);
} database::should_run_in_transaction(
}) client,
.map_err(From::from) !opts.transaction_opts.single_transaction,
})?; |client| client.run_downgrade_migration(migration_name, &content),
)
} else {
Ok(())
}
})
.map_err(From::from)
},
)?;
Ok(()) Ok(())
} }

View file

@ -52,15 +52,23 @@ pub(crate) fn upgrade_pending_migrations(
}) })
.collect::<Result<Vec<_>, _>>()?; .collect::<Result<Vec<_>, _>>()?;
database::run_in_transaction(&mut client, |client| { database::should_run_in_transaction(
migrations_with_content &mut client,
.iter() opts.transaction_opts.single_transaction,
.try_for_each(|(migration_name, content)| { |client| {
println!("upgrade {}...", migration_name); migrations_with_content
client.run_upgrade_migration(migration_name, content) .iter()
}) .try_for_each(|(migration_name, content)| {
.map_err(From::from) println!("upgrade {}...", migration_name);
})?; database::should_run_in_transaction(
client,
!opts.transaction_opts.single_transaction,
|client| client.run_upgrade_migration(migration_name, &content),
)
})
.map_err(From::from)
},
)?;
Ok(()) Ok(())
} }

View file

@ -50,7 +50,7 @@ fn is_sqlite_database_file(filename: &str) -> bool {
.rsplit('.') .rsplit('.')
.next() .next()
.map(|ext| ext.eq_ignore_ascii_case("db")) .map(|ext| ext.eq_ignore_ascii_case("db"))
.unwrap_or_default() == Some(true)
} }
fn default_database_connection_env() -> String { fn default_database_connection_env() -> String {
@ -138,7 +138,7 @@ impl DatabaseConfig {
} }
pub fn connection_string(&self) -> MigraResult<String> { pub fn connection_string(&self) -> MigraResult<String> {
self.connection.strip_prefix('$').map_or_else( self.connection.strip_prefix("$").map_or_else(
|| Ok(self.connection.clone()), || Ok(self.connection.clone()),
|connection_env| { |connection_env| {
env::var(connection_env) env::var(connection_env)
@ -183,7 +183,7 @@ impl Default for MigrationsConfig {
impl MigrationsConfig { impl MigrationsConfig {
pub fn directory(&self) -> String { pub fn directory(&self) -> String {
self.directory.strip_prefix('$').map_or_else( self.directory.strip_prefix("$").map_or_else(
|| self.directory.clone(), || self.directory.clone(),
|directory_env| { |directory_env| {
env::var(directory_env).unwrap_or_else(|_| { env::var(directory_env).unwrap_or_else(|_| {
@ -199,7 +199,7 @@ impl MigrationsConfig {
} }
pub fn table_name(&self) -> String { pub fn table_name(&self) -> String {
self.table_name.strip_prefix('$').map_or_else( self.table_name.strip_prefix("$").map_or_else(
|| self.table_name.clone(), || self.table_name.clone(),
|table_name_env| { |table_name_env| {
env::var(table_name_env).unwrap_or_else(|_| { env::var(table_name_env).unwrap_or_else(|_| {

View file

@ -53,3 +53,18 @@ where
.and_then(|res| client.commit_transaction().and(Ok(res))) .and_then(|res| client.commit_transaction().and(Ok(res)))
.or_else(|err| client.rollback_transaction().and(Err(err))) .or_else(|err| client.rollback_transaction().and(Err(err)))
} }
pub fn should_run_in_transaction<TrxFnMut>(
client: &mut AnyClient,
is_needed: bool,
trx_fn: TrxFnMut,
) -> migra::Result<()>
where
TrxFnMut: FnOnce(&mut AnyClient) -> migra::Result<()>,
{
if is_needed {
run_in_transaction(client, trx_fn)
} else {
trx_fn(client)
}
}

View file

@ -32,10 +32,19 @@ pub(crate) enum Command {
Completions(CompletionsShell), Completions(CompletionsShell),
} }
#[derive(Debug, StructOpt, Clone)]
pub(crate) struct TransactionOpts {
#[structopt(long = "single-transaction")]
pub single_transaction: bool,
}
#[derive(Debug, StructOpt, Clone)] #[derive(Debug, StructOpt, Clone)]
pub(crate) struct ApplyCommandOpt { pub(crate) struct ApplyCommandOpt {
#[structopt(parse(from_os_str), required = true)] #[structopt(parse(from_os_str), required = true)]
pub file_paths: Vec<PathBuf>, pub file_paths: Vec<PathBuf>,
#[structopt(flatten)]
pub transaction_opts: TransactionOpts,
} }
#[derive(Debug, StructOpt, Clone)] #[derive(Debug, StructOpt, Clone)]
@ -55,6 +64,9 @@ pub(crate) struct UpgradeCommandOpt {
/// How many existing migrations do we have to update. /// How many existing migrations do we have to update.
#[structopt(long = "number", short = "n")] #[structopt(long = "number", short = "n")]
pub migrations_number: Option<usize>, pub migrations_number: Option<usize>,
#[structopt(flatten)]
pub transaction_opts: TransactionOpts,
} }
#[derive(Debug, StructOpt, Clone)] #[derive(Debug, StructOpt, Clone)]
@ -66,6 +78,9 @@ pub(crate) struct DowngradeCommandOpt {
/// Rolls back all applied migrations. Ignores --number option. /// Rolls back all applied migrations. Ignores --number option.
#[structopt(long = "all")] #[structopt(long = "all")]
pub all_migrations: bool, pub all_migrations: bool,
#[structopt(flatten)]
pub transaction_opts: TransactionOpts,
} }
#[derive(Debug, StructOpt, Clone)] #[derive(Debug, StructOpt, Clone)]

View file

@ -485,6 +485,62 @@ mod upgrade {
Ok(()) Ok(())
} }
#[test]
fn partial_applied_invalid_migrations() -> TestResult {
fn inner<ValidateFn>(database_name: &'static str, validate: ValidateFn) -> TestResult
where
ValidateFn: Fn() -> TestResult,
{
let manifest_path = database_manifest_path(database_name);
Command::cargo_bin("migra")?
.arg("-c")
.arg(&manifest_path)
.arg("up")
.assert()
.failure();
validate()?;
Command::cargo_bin("migra")?
.arg("-c")
.arg(&manifest_path)
.arg("down")
.assert()
.success();
Ok(())
}
#[cfg(feature = "postgres")]
inner("postgres_invalid", || {
let mut conn = client_postgres::Client::connect(POSTGRES_URL, client_postgres::NoTls)?;
let articles_res = conn.query("SELECT a.id FROM articles AS a", &[]);
let persons_res = conn.query("SELECT p.id FROM persons AS p", &[]);
assert!(articles_res.is_ok());
assert!(persons_res.is_err());
Ok(())
})?;
#[cfg(feature = "sqlite")]
remove_sqlite_db().and_then(|_| {
inner("sqlite_invalid", || {
let conn = client_rusqlite::Connection::open(SQLITE_URL)?;
let articles_res = conn.execute_batch("SELECT a.id FROM articles AS a");
let persons_res = conn.execute_batch("SELECT p.id FROM persons AS p");
assert!(articles_res.is_ok());
assert!(persons_res.is_err());
Ok(())
})
})?;
Ok(())
}
#[test] #[test]
fn cannot_applied_invalid_migrations_in_single_transaction() -> TestResult { fn cannot_applied_invalid_migrations_in_single_transaction() -> TestResult {
fn inner<ValidateFn>(database_name: &'static str, validate: ValidateFn) -> TestResult fn inner<ValidateFn>(database_name: &'static str, validate: ValidateFn) -> TestResult
@ -532,8 +588,6 @@ mod upgrade {
}) })
})?; })?;
// mysql doesn't support DDL in transaction 🤷
Ok(()) Ok(())
} }
} }