From ec02367680aeb0004cd502489365fee7ee374934 Mon Sep 17 00:00:00 2001 From: Dmitriy Pleshevskiy Date: Sun, 13 Jun 2021 01:39:56 +0300 Subject: [PATCH] Migra core (#11) * feat(core): init migra lib * refac(core): add utils for migration list * feat(core): add managers * refac(core): add batch exec trait * refac(core): smarter managers * refac(cli): removed adapter, builder * refac(cli): use migra core for cli * chore(cli): add dev deps for tests * chore(cli): improve error handling * refac(core): make migrations simpler * refac(cli): change transaction utils * chore(core): add documentation --- Cargo.toml | 3 +- migra-cli/src/commands/downgrade.rs | 48 ---- migra-cli/src/commands/list.rs | 65 ----- migra-cli/src/commands/upgrade.rs | 66 ----- migra-cli/src/database/adapter.rs | 17 -- migra-cli/src/database/builder.rs | 39 --- migra-cli/src/database/clients/mod.rs | 20 -- migra-cli/src/database/clients/mysql.rs | 53 ---- migra-cli/src/database/clients/postgres.rs | 64 ----- migra-cli/src/database/clients/sqlite.rs | 60 ----- migra-cli/src/database/connection.rs | 64 ----- migra-cli/src/database/migration.rs | 187 -------------- migra-cli/src/database/mod.rs | 19 -- migra-cli/src/database/transaction.rs | 63 ----- migra-cli/src/main.rs | 27 -- migra/Cargo.toml | 23 ++ migra/README.md | 86 +++++++ migra/src/clients/mod.rs | 39 +++ migra/src/clients/mysql.rs | 94 +++++++ migra/src/clients/postgres.rs | 105 ++++++++ migra/src/clients/sqlite.rs | 103 ++++++++ migra/src/errors.rs | 127 +++++++++ migra/src/fs.rs | 34 +++ migra/src/lib.rs | 97 +++++++ migra/src/managers.rs | 74 ++++++ migra/src/migration.rs | 242 ++++++++++++++++++ {migra-cli => migra_cli}/Cargo.toml | 23 +- {migra-cli => migra_cli}/src/app.rs | 14 +- .../src/commands/apply.rs | 24 +- migra_cli/src/commands/downgrade.rs | 58 +++++ {migra-cli => migra_cli}/src/commands/init.rs | 14 +- migra_cli/src/commands/list.rs | 66 +++++ {migra-cli => migra_cli}/src/commands/make.rs | 3 +- {migra-cli => migra_cli}/src/commands/mod.rs | 0 migra_cli/src/commands/upgrade.rs | 74 ++++++ {migra-cli => migra_cli}/src/config.rs | 97 ++++--- migra_cli/src/database.rs | 70 +++++ {migra-cli => migra_cli}/src/error.rs | 1 - migra_cli/src/main.rs | 32 +++ {migra-cli => migra_cli}/src/opts.rs | 0 {migra-cli => migra_cli}/tests/commands.rs | 43 +--- .../tests/data/Migra_env.toml | 0 .../tests/data/Migra_env_empty.toml | 0 .../tests/data/Migra_mysql.toml | 0 .../tests/data/Migra_postgres.toml | 0 .../tests/data/Migra_postgres_invalid.toml | 0 .../tests/data/Migra_sqlite.toml | 0 .../tests/data/Migra_sqlite_invalid.toml | 0 .../tests/data/Migra_url_empty.toml | 0 .../210218232851_create_articles/down.sql | 0 .../210218232851_create_articles/up.sql | 0 .../210218233414_create_persons/down.sql | 0 .../210218233414_create_persons/up.sql | 0 .../210218232851_create_articles/down.sql | 0 .../210218232851_create_articles/up.sql | 0 .../210218233414_create_persons/down.sql | 0 .../210218233414_create_persons/up.sql | 0 .../210218232851_create_articles/down.sql | 0 .../210218232851_create_articles/up.sql | 0 .../210218233414_create_persons/down.sql | 0 .../210218233414_create_persons/up.sql | 0 .../210218232851_create_articles/down.sql | 0 .../210218232851_create_articles/up.sql | 0 .../210218233414_create_persons/down.sql | 0 .../210218233414_create_persons/up.sql | 0 .../210218232851_create_articles/down.sql | 0 .../210218232851_create_articles/up.sql | 0 .../210218233414_create_persons/down.sql | 0 .../210218233414_create_persons/up.sql | 0 69 files changed, 1419 insertions(+), 919 deletions(-) delete mode 100644 migra-cli/src/commands/downgrade.rs delete mode 100644 migra-cli/src/commands/list.rs delete mode 100644 migra-cli/src/commands/upgrade.rs delete mode 100644 migra-cli/src/database/adapter.rs delete mode 100644 migra-cli/src/database/builder.rs delete mode 100644 migra-cli/src/database/clients/mod.rs delete mode 100644 migra-cli/src/database/clients/mysql.rs delete mode 100644 migra-cli/src/database/clients/postgres.rs delete mode 100644 migra-cli/src/database/clients/sqlite.rs delete mode 100644 migra-cli/src/database/connection.rs delete mode 100644 migra-cli/src/database/migration.rs delete mode 100644 migra-cli/src/database/mod.rs delete mode 100644 migra-cli/src/database/transaction.rs delete mode 100644 migra-cli/src/main.rs create mode 100644 migra/Cargo.toml create mode 100644 migra/README.md create mode 100644 migra/src/clients/mod.rs create mode 100644 migra/src/clients/mysql.rs create mode 100644 migra/src/clients/postgres.rs create mode 100644 migra/src/clients/sqlite.rs create mode 100644 migra/src/errors.rs create mode 100644 migra/src/fs.rs create mode 100644 migra/src/lib.rs create mode 100644 migra/src/managers.rs create mode 100644 migra/src/migration.rs rename {migra-cli => migra_cli}/Cargo.toml (76%) rename {migra-cli => migra_cli}/src/app.rs (80%) rename {migra-cli => migra_cli}/src/commands/apply.rs (55%) create mode 100644 migra_cli/src/commands/downgrade.rs rename {migra-cli => migra_cli}/src/commands/init.rs (72%) create mode 100644 migra_cli/src/commands/list.rs rename {migra-cli => migra_cli}/src/commands/make.rs (92%) rename {migra-cli => migra_cli}/src/commands/mod.rs (100%) create mode 100644 migra_cli/src/commands/upgrade.rs rename {migra-cli => migra_cli}/src/config.rs (79%) create mode 100644 migra_cli/src/database.rs rename {migra-cli => migra_cli}/src/error.rs (92%) create mode 100644 migra_cli/src/main.rs rename {migra-cli => migra_cli}/src/opts.rs (100%) rename {migra-cli => migra_cli}/tests/commands.rs (93%) rename {migra-cli => migra_cli}/tests/data/Migra_env.toml (100%) rename {migra-cli => migra_cli}/tests/data/Migra_env_empty.toml (100%) rename {migra-cli => migra_cli}/tests/data/Migra_mysql.toml (100%) rename {migra-cli => migra_cli}/tests/data/Migra_postgres.toml (100%) rename {migra-cli => migra_cli}/tests/data/Migra_postgres_invalid.toml (100%) rename {migra-cli => migra_cli}/tests/data/Migra_sqlite.toml (100%) rename {migra-cli => migra_cli}/tests/data/Migra_sqlite_invalid.toml (100%) rename {migra-cli => migra_cli}/tests/data/Migra_url_empty.toml (100%) rename {migra-cli => migra_cli}/tests/data/mysql/migrations/210218232851_create_articles/down.sql (100%) rename {migra-cli => migra_cli}/tests/data/mysql/migrations/210218232851_create_articles/up.sql (100%) rename {migra-cli => migra_cli}/tests/data/mysql/migrations/210218233414_create_persons/down.sql (100%) rename {migra-cli => migra_cli}/tests/data/mysql/migrations/210218233414_create_persons/up.sql (100%) rename {migra-cli => migra_cli}/tests/data/postgres/migrations/210218232851_create_articles/down.sql (100%) rename {migra-cli => migra_cli}/tests/data/postgres/migrations/210218232851_create_articles/up.sql (100%) rename {migra-cli => migra_cli}/tests/data/postgres/migrations/210218233414_create_persons/down.sql (100%) rename {migra-cli => migra_cli}/tests/data/postgres/migrations/210218233414_create_persons/up.sql (100%) rename {migra-cli => migra_cli}/tests/data/postgres_invalid/migrations/210218232851_create_articles/down.sql (100%) rename {migra-cli => migra_cli}/tests/data/postgres_invalid/migrations/210218232851_create_articles/up.sql (100%) rename {migra-cli => migra_cli}/tests/data/postgres_invalid/migrations/210218233414_create_persons/down.sql (100%) rename {migra-cli => migra_cli}/tests/data/postgres_invalid/migrations/210218233414_create_persons/up.sql (100%) rename {migra-cli => migra_cli}/tests/data/sqlite/migrations/210218232851_create_articles/down.sql (100%) rename {migra-cli => migra_cli}/tests/data/sqlite/migrations/210218232851_create_articles/up.sql (100%) rename {migra-cli => migra_cli}/tests/data/sqlite/migrations/210218233414_create_persons/down.sql (100%) rename {migra-cli => migra_cli}/tests/data/sqlite/migrations/210218233414_create_persons/up.sql (100%) rename {migra-cli => migra_cli}/tests/data/sqlite_invalid/migrations/210218232851_create_articles/down.sql (100%) rename {migra-cli => migra_cli}/tests/data/sqlite_invalid/migrations/210218232851_create_articles/up.sql (100%) rename {migra-cli => migra_cli}/tests/data/sqlite_invalid/migrations/210218233414_create_persons/down.sql (100%) rename {migra-cli => migra_cli}/tests/data/sqlite_invalid/migrations/210218233414_create_persons/up.sql (100%) diff --git a/Cargo.toml b/Cargo.toml index dfca278..020de8e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,5 @@ [workspace] members = [ - "migra-cli" + "migra", + "migra_cli", ] \ No newline at end of file diff --git a/migra-cli/src/commands/downgrade.rs b/migra-cli/src/commands/downgrade.rs deleted file mode 100644 index 4b9c697..0000000 --- a/migra-cli/src/commands/downgrade.rs +++ /dev/null @@ -1,48 +0,0 @@ -use crate::app::App; -use crate::database::prelude::*; -use crate::database::transaction::maybe_with_transaction; -use crate::database::{DatabaseConnectionManager, MigrationManager}; -use crate::opts::DowngradeCommandOpt; -use crate::StdResult; -use std::cmp; - -pub(crate) fn rollback_applied_migrations(app: &App, opts: DowngradeCommandOpt) -> StdResult<()> { - let config = app.config()?; - let mut connection_manager = DatabaseConnectionManager::connect(&config.database)?; - let conn = connection_manager.connection(); - let migration_manager = MigrationManager::from(&config); - - let applied_migrations = migration_manager.applied_migration_names(conn)?; - let migrations = config.migrations()?; - - let rollback_migrations_number = if opts.all_migrations { - applied_migrations.len() - } else { - cmp::min(opts.migrations_number, applied_migrations.len()) - }; - - maybe_with_transaction( - opts.transaction_opts.single_transaction, - conn, - &mut |conn| { - applied_migrations[..rollback_migrations_number] - .iter() - .try_for_each(|migration_name| { - if let Some(migration) = migrations.iter().find(|m| m.name() == migration_name) - { - println!("downgrade {}...", migration.name()); - maybe_with_transaction( - !opts.transaction_opts.single_transaction, - conn, - &mut |conn| migration_manager.downgrade(conn, &migration), - ) - } else { - Ok(()) - } - }) - .map_err(From::from) - }, - )?; - - Ok(()) -} diff --git a/migra-cli/src/commands/list.rs b/migra-cli/src/commands/list.rs deleted file mode 100644 index 880feb6..0000000 --- a/migra-cli/src/commands/list.rs +++ /dev/null @@ -1,65 +0,0 @@ -use crate::app::App; -use crate::database::migration::filter_pending_migrations; -use crate::database::prelude::*; -use crate::database::{DatabaseConnectionManager, Migration, MigrationManager}; -use crate::error::{Error, StdResult}; - -const EM_DASH: char = '—'; - -pub(crate) fn print_migration_lists(app: &App) -> StdResult<()> { - let config = app.config()?; - let applied_migration_names = match config.database.connection_string() { - Ok(ref database_connection_string) => { - let mut connection_manager = DatabaseConnectionManager::connect_with_string( - &config.database, - database_connection_string, - )?; - let conn = connection_manager.connection(); - - let migration_manager = MigrationManager::from(&config); - let applied_migration_names = migration_manager.applied_migration_names(conn)?; - - show_applied_migrations(&applied_migration_names); - - applied_migration_names - } - Err(e) if e == Error::MissedEnvVar(String::new()) => { - eprintln!("WARNING: {}", e); - eprintln!("WARNING: No connection to database"); - - Vec::new() - } - Err(e) => panic!("{}", e), - }; - - println!(); - - let pending_migrations = - filter_pending_migrations(config.migrations()?, &applied_migration_names); - show_pending_migrations(&pending_migrations); - - Ok(()) -} - -fn show_applied_migrations(applied_migration_names: &[String]) { - println!("Applied migrations:"); - if applied_migration_names.is_empty() { - println!("{}", EM_DASH); - } else { - applied_migration_names - .iter() - .rev() - .for_each(|name| println!("{}", name)); - } -} - -fn show_pending_migrations(pending_migrations: &[Migration]) { - println!("Pending migrations:"); - if pending_migrations.is_empty() { - println!("{}", EM_DASH); - } else { - pending_migrations.iter().for_each(|m| { - println!("{}", m.name()); - }); - } -} diff --git a/migra-cli/src/commands/upgrade.rs b/migra-cli/src/commands/upgrade.rs deleted file mode 100644 index b107c9d..0000000 --- a/migra-cli/src/commands/upgrade.rs +++ /dev/null @@ -1,66 +0,0 @@ -use crate::app::App; -use crate::database::migration::*; -use crate::database::transaction::maybe_with_transaction; -use crate::database::DatabaseConnectionManager; -use crate::opts::UpgradeCommandOpt; -use crate::StdResult; - -pub(crate) fn upgrade_pending_migrations(app: &App, opts: UpgradeCommandOpt) -> StdResult<()> { - let config = app.config()?; - let mut connection_manager = DatabaseConnectionManager::connect(&config.database)?; - let conn = connection_manager.connection(); - - let migration_manager = MigrationManager::from(&config); - - let applied_migration_names = migration_manager.applied_migration_names(conn)?; - let migrations = config.migrations()?; - - let pending_migrations = filter_pending_migrations(migrations, &applied_migration_names); - if pending_migrations.is_empty() { - println!("Up to date"); - return Ok(()); - } - - let migrations: Vec = if let Some(migration_name) = opts.migration_name.clone() { - let target_migration = pending_migrations - .into_iter() - .find(|m| m.name() == &migration_name); - match target_migration { - Some(migration) => vec![migration], - None => { - eprintln!(r#"Cannot find migration with "{}" name"#, migration_name); - return Ok(()); - } - } - } else { - let upgrade_migrations_number = opts - .migrations_number - .unwrap_or_else(|| pending_migrations.len()); - - pending_migrations[..upgrade_migrations_number].to_vec() - }; - - maybe_with_transaction( - opts.transaction_opts.single_transaction, - conn, - &mut |conn| { - migrations - .iter() - .try_for_each(|migration| { - print_migration_info(migration); - maybe_with_transaction( - !opts.transaction_opts.single_transaction, - conn, - &mut |conn| migration_manager.upgrade(conn, migration), - ) - }) - .map_err(From::from) - }, - )?; - - Ok(()) -} - -fn print_migration_info(migration: &Migration) { - println!("upgrade {}...", migration.name()); -} diff --git a/migra-cli/src/database/adapter.rs b/migra-cli/src/database/adapter.rs deleted file mode 100644 index f48e404..0000000 --- a/migra-cli/src/database/adapter.rs +++ /dev/null @@ -1,17 +0,0 @@ -use crate::error::StdResult; - -pub trait ToSql { - fn to_sql(&self) -> String; -} - -pub type ToSqlParams<'a> = &'a [&'a dyn ToSql]; - -impl ToSql for &str { - fn to_sql(&self) -> String { - format!("'{}'", self) - } -} - -pub trait TryFromSql: Sized { - fn try_from_sql(row: QueryResultRow) -> StdResult; -} diff --git a/migra-cli/src/database/builder.rs b/migra-cli/src/database/builder.rs deleted file mode 100644 index ae9a049..0000000 --- a/migra-cli/src/database/builder.rs +++ /dev/null @@ -1,39 +0,0 @@ -use super::prelude::*; - -pub(crate) fn merge_query_with_params(query: &str, params: ToSqlParams) -> String { - params - .iter() - .enumerate() - .fold(query.to_string(), |acc, (i, p)| { - str::replace(&acc, &format!("${}", i + 1), &p.to_sql()) - }) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn replace_one_param_in_query() { - assert_eq!( - merge_query_with_params("SELECT $1", &[&"foo"]), - "SELECT 'foo'" - ); - } - - #[test] - fn replace_two_params_in_query() { - assert_eq!( - merge_query_with_params("SELECT $1, $2", &[&"foo", &"bar"]), - "SELECT 'foo', 'bar'" - ); - } - - #[test] - fn replace_all_bonds_in_query_with_first_param() { - assert_eq!( - merge_query_with_params("SELECT $1, $1", &[&"foo"]), - "SELECT 'foo', 'foo'" - ); - } -} diff --git a/migra-cli/src/database/clients/mod.rs b/migra-cli/src/database/clients/mod.rs deleted file mode 100644 index f2fed90..0000000 --- a/migra-cli/src/database/clients/mod.rs +++ /dev/null @@ -1,20 +0,0 @@ -cfg_if! { - if #[cfg(feature = "postgres")] { - mod postgres; - pub use self::postgres::*; - } -} - -cfg_if! { - if #[cfg(feature = "mysql")] { - mod mysql; - pub use self::mysql::*; - } -} - -cfg_if! { - if #[cfg(feature = "sqlite")] { - mod sqlite; - pub use self::sqlite::*; - } -} diff --git a/migra-cli/src/database/clients/mysql.rs b/migra-cli/src/database/clients/mysql.rs deleted file mode 100644 index 6453230..0000000 --- a/migra-cli/src/database/clients/mysql.rs +++ /dev/null @@ -1,53 +0,0 @@ -use crate::database::builder::merge_query_with_params; -use crate::database::prelude::*; -use crate::error::StdResult; -use mysql::prelude::*; -use mysql::{Pool, PooledConn}; - -pub struct MySqlConnection { - conn: PooledConn, -} - -impl OpenDatabaseConnection for MySqlConnection { - fn open(connection_string: &str) -> StdResult { - let pool = Pool::new_manual(1, 1, connection_string)?; - let conn = pool.get_conn()?; - Ok(MySqlConnection { conn }) - } -} - -impl DatabaseStatements for MySqlConnection { - fn create_migration_table_stmt(&self, migrations_table_name: &str) -> String { - format!( - r#"CREATE TABLE IF NOT EXISTS {} ( - id int AUTO_INCREMENT PRIMARY KEY, - name varchar(256) NOT NULL UNIQUE - )"#, - migrations_table_name - ) - } -} - -impl SupportsTransactionalDdl for MySqlConnection {} - -impl DatabaseConnection for MySqlConnection { - fn batch_execute(&mut self, query: &str) -> StdResult<()> { - self.conn.query_drop(query)?; - Ok(()) - } - - fn execute<'b>(&mut self, query: &str, params: ToSqlParams<'b>) -> StdResult { - let stmt = merge_query_with_params(query, params); - - let res = self.conn.query_first(stmt)?.unwrap_or_default(); - Ok(res) - } - - fn query<'b>(&mut self, query: &str, params: ToSqlParams<'b>) -> StdResult>> { - let stmt = merge_query_with_params(query, params); - - let res = self.conn.query_map(stmt, |(column,)| vec![column])?; - - Ok(res) - } -} diff --git a/migra-cli/src/database/clients/postgres.rs b/migra-cli/src/database/clients/postgres.rs deleted file mode 100644 index fb4a16c..0000000 --- a/migra-cli/src/database/clients/postgres.rs +++ /dev/null @@ -1,64 +0,0 @@ -use crate::database::builder::merge_query_with_params; -use crate::database::prelude::*; -use crate::error::StdResult; -use postgres::{Client, NoTls}; - -pub struct PostgresConnection { - client: Client, -} - -impl OpenDatabaseConnection for PostgresConnection { - fn open(connection_string: &str) -> StdResult { - let client = Client::connect(connection_string, NoTls)?; - Ok(PostgresConnection { client }) - } -} - -impl DatabaseStatements for PostgresConnection { - fn create_migration_table_stmt(&self, migrations_table_name: &str) -> String { - format!( - r#"CREATE TABLE IF NOT EXISTS {} ( - id serial PRIMARY KEY, - name text NOT NULL UNIQUE - )"#, - migrations_table_name - ) - } -} - -impl SupportsTransactionalDdl for PostgresConnection { - #[inline] - fn supports_transactional_ddl(&self) -> bool { - true - } -} - -impl DatabaseConnection for PostgresConnection { - fn batch_execute(&mut self, query: &str) -> StdResult<()> { - self.client.batch_execute(query)?; - Ok(()) - } - - fn execute<'b>(&mut self, query: &str, params: ToSqlParams<'b>) -> StdResult { - let stmt = merge_query_with_params(query, params); - - let res = self.client.execute(stmt.as_str(), &[])?; - Ok(res) - } - - fn query<'b>(&mut self, query: &str, params: ToSqlParams<'b>) -> StdResult>> { - let stmt = merge_query_with_params(query, params); - - let res = self.client.query(stmt.as_str(), &[])?; - - let res = res - .into_iter() - .map(|row| { - let column: String = row.get(0); - vec![column] - }) - .collect::>(); - - Ok(res) - } -} diff --git a/migra-cli/src/database/clients/sqlite.rs b/migra-cli/src/database/clients/sqlite.rs deleted file mode 100644 index 8a0eafb..0000000 --- a/migra-cli/src/database/clients/sqlite.rs +++ /dev/null @@ -1,60 +0,0 @@ -use crate::database::builder::merge_query_with_params; -use crate::database::prelude::*; -use crate::error::StdResult; -use rusqlite::Connection; - -pub struct SqliteConnection { - conn: Connection, -} - -impl OpenDatabaseConnection for SqliteConnection { - fn open(connection_string: &str) -> StdResult { - let conn = Connection::open(connection_string)?; - Ok(SqliteConnection { conn }) - } -} - -impl DatabaseStatements for SqliteConnection { - fn create_migration_table_stmt(&self, migrations_table_name: &str) -> String { - format!( - r#"CREATE TABLE IF NOT EXISTS {} ( - id int AUTO_INCREMENT PRIMARY KEY, - name varchar(256) NOT NULL UNIQUE - )"#, - migrations_table_name - ) - } -} - -impl SupportsTransactionalDdl for SqliteConnection { - #[inline] - fn supports_transactional_ddl(&self) -> bool { - true - } -} - -impl DatabaseConnection for SqliteConnection { - fn batch_execute(&mut self, query: &str) -> StdResult<()> { - self.conn.execute_batch(query)?; - Ok(()) - } - - fn execute<'b>(&mut self, query: &str, params: ToSqlParams<'b>) -> StdResult { - let stmt = merge_query_with_params(query, params); - - let res = self.conn.execute(&stmt, [])?; - Ok(res as u64) - } - - fn query<'b>(&mut self, query: &str, params: ToSqlParams<'b>) -> StdResult>> { - let stmt = merge_query_with_params(query, params); - - let mut stmt = self.conn.prepare(&stmt)?; - - let res = stmt - .query_map([], |row| Ok(vec![row.get(0)?]))? - .collect::>()?; - - Ok(res) - } -} diff --git a/migra-cli/src/database/connection.rs b/migra-cli/src/database/connection.rs deleted file mode 100644 index d1acdb8..0000000 --- a/migra-cli/src/database/connection.rs +++ /dev/null @@ -1,64 +0,0 @@ -use super::adapter::ToSqlParams; -use super::clients::*; -use crate::config::{DatabaseConfig, SupportedDatabaseClient}; -use crate::error::StdResult; - -pub type AnyConnection = Box; - -pub trait OpenDatabaseConnection: Sized { - fn open(connection_string: &str) -> StdResult; -} - -pub trait DatabaseStatements { - fn create_migration_table_stmt(&self, migrations_table_name: &str) -> String; -} - -pub trait SupportsTransactionalDdl { - #[inline] - fn supports_transactional_ddl(&self) -> bool { - false - } -} - -pub trait DatabaseConnection: DatabaseStatements + SupportsTransactionalDdl { - fn batch_execute(&mut self, query: &str) -> StdResult<()>; - - fn execute<'b>(&mut self, query: &str, params: ToSqlParams<'b>) -> StdResult; - - fn query<'b>(&mut self, query: &str, params: ToSqlParams<'b>) -> StdResult>>; -} - -pub(crate) struct DatabaseConnectionManager { - conn: AnyConnection, -} - -impl DatabaseConnectionManager { - pub fn connect_with_string( - config: &DatabaseConfig, - connection_string: &str, - ) -> StdResult { - let conn: AnyConnection = match config.client() { - #[cfg(feature = "postgres")] - SupportedDatabaseClient::Postgres => { - Box::new(PostgresConnection::open(&connection_string)?) - } - #[cfg(feature = "mysql")] - SupportedDatabaseClient::Mysql => Box::new(MySqlConnection::open(&connection_string)?), - #[cfg(feature = "sqlite")] - SupportedDatabaseClient::Sqlite => { - Box::new(SqliteConnection::open(&connection_string)?) - } - }; - - Ok(DatabaseConnectionManager { conn }) - } - - pub fn connect(config: &DatabaseConfig) -> StdResult { - let connection_string = config.connection_string()?; - Self::connect_with_string(config, &connection_string) - } - - pub fn connection(&mut self) -> &mut AnyConnection { - &mut self.conn - } -} diff --git a/migra-cli/src/database/migration.rs b/migra-cli/src/database/migration.rs deleted file mode 100644 index d2ad50d..0000000 --- a/migra-cli/src/database/migration.rs +++ /dev/null @@ -1,187 +0,0 @@ -use super::connection::AnyConnection; -use crate::Config; -use crate::StdResult; -use std::fs; -use std::path::{Path, PathBuf}; - -#[derive(Debug, Clone)] -pub struct Migration { - upgrade_sql_file_path: PathBuf, - downgrade_sql_file_path: PathBuf, - name: String, -} - -impl Migration { - pub(crate) fn new(directory: &Path) -> Option { - if directory.is_dir() { - let name = directory - .file_name() - .and_then(|name| name.to_str()) - .unwrap_or_default(); - let upgrade_sql_file_path = directory.join("up.sql"); - let downgrade_sql_file_path = directory.join("down.sql"); - - if upgrade_sql_file_path.exists() && downgrade_sql_file_path.exists() { - return Some(Migration { - upgrade_sql_file_path, - downgrade_sql_file_path, - name: String::from(name), - }); - } - } - - None - } -} - -impl Migration { - pub fn name(&self) -> &String { - &self.name - } - - fn upgrade_sql_content(&self) -> StdResult { - let content = fs::read_to_string(&self.upgrade_sql_file_path)?; - Ok(content) - } - - fn downgrade_sql_content(&self) -> StdResult { - let content = fs::read_to_string(&self.downgrade_sql_file_path)?; - Ok(content) - } -} - -#[derive(Debug)] -pub struct MigrationManager { - migrations_table_name: String, -} - -impl MigrationManager { - fn new(migrations_table_name: &str) -> Self { - MigrationManager { - migrations_table_name: migrations_table_name.to_owned(), - } - } -} - -impl From<&Config> for MigrationManager { - fn from(config: &Config) -> Self { - MigrationManager::new(&config.migrations.table_name()) - } -} - -pub fn is_migrations_table_not_found(error: D) -> bool { - let error_message = error.to_string(); - - fn is_postgres_error(error_message: &str) -> bool { - error_message.contains("relation") && error_message.ends_with("does not exist") - } - - fn is_mysql_error(error_message: &str) -> bool { - error_message.contains("ERROR 1146 (42S02)") - } - - fn is_sqlite_error(error_message: &str) -> bool { - error_message.starts_with("no such table:") - } - - is_postgres_error(&error_message) - || is_mysql_error(&error_message) - || is_sqlite_error(&error_message) -} - -pub trait ManageMigration { - fn apply_sql(&self, conn: &mut AnyConnection, sql_content: &str) -> StdResult<()>; - - fn create_migrations_table(&self, conn: &mut AnyConnection) -> StdResult<()>; - - fn insert_migration_info(&self, conn: &mut AnyConnection, name: &str) -> StdResult; - - fn delete_migration_info(&self, conn: &mut AnyConnection, name: &str) -> StdResult; - - fn applied_migration_names(&self, conn: &mut AnyConnection) -> StdResult>; - - fn upgrade(&self, conn: &mut AnyConnection, migration: &Migration) -> StdResult<()> { - let content = migration.upgrade_sql_content()?; - - self.create_migrations_table(conn)?; - self.apply_sql(conn, &content)?; - self.insert_migration_info(conn, migration.name())?; - - Ok(()) - } - - fn downgrade(&self, conn: &mut AnyConnection, migration: &Migration) -> StdResult<()> { - let content = migration.downgrade_sql_content()?; - - self.apply_sql(conn, &content)?; - self.delete_migration_info(conn, migration.name())?; - - Ok(()) - } -} - -impl ManageMigration for MigrationManager { - fn apply_sql(&self, conn: &mut AnyConnection, sql_content: &str) -> StdResult<()> { - conn.batch_execute(sql_content) - } - - fn create_migrations_table(&self, conn: &mut AnyConnection) -> StdResult<()> { - let stmt = conn.create_migration_table_stmt(&self.migrations_table_name); - conn.batch_execute(&stmt) - } - - fn insert_migration_info(&self, conn: &mut AnyConnection, name: &str) -> StdResult { - conn.execute( - &format!( - "INSERT INTO {} (name) VALUES ($1)", - &self.migrations_table_name - ), - &[&name], - ) - } - - fn delete_migration_info(&self, conn: &mut AnyConnection, name: &str) -> StdResult { - conn.execute( - &format!( - "DELETE FROM {} WHERE name = $1", - &self.migrations_table_name - ), - &[&name], - ) - } - - fn applied_migration_names(&self, conn: &mut AnyConnection) -> StdResult> { - let res = conn - .query( - &format!( - "SELECT name FROM {} ORDER BY id DESC", - &self.migrations_table_name - ), - &[], - ) - .or_else(|e| { - if is_migrations_table_not_found(&e) { - Ok(Vec::new()) - } else { - Err(e) - } - })?; - - let applied_migration_names: Vec = res - .into_iter() - .filter_map(|row| row.first().cloned()) - .collect(); - - Ok(applied_migration_names) - } -} - -pub fn filter_pending_migrations( - migrations: Vec, - applied_migration_names: &[String], -) -> Vec { - migrations - .into_iter() - .filter(|m| !applied_migration_names.contains(m.name())) - .collect() -} diff --git a/migra-cli/src/database/mod.rs b/migra-cli/src/database/mod.rs deleted file mode 100644 index 2510dab..0000000 --- a/migra-cli/src/database/mod.rs +++ /dev/null @@ -1,19 +0,0 @@ -pub(crate) mod adapter; -pub(crate) mod builder; -pub(crate) mod clients; -pub(crate) mod connection; -pub(crate) mod migration; -pub(crate) mod transaction; - -pub mod prelude { - pub use super::adapter::{ToSql, ToSqlParams, TryFromSql}; - pub use super::connection::{ - AnyConnection, DatabaseConnection, DatabaseStatements, OpenDatabaseConnection, - SupportsTransactionalDdl, - }; - pub use super::migration::ManageMigration; - pub use super::transaction::ManageTransaction; -} - -pub(crate) use connection::DatabaseConnectionManager; -pub(crate) use migration::{Migration, MigrationManager}; diff --git a/migra-cli/src/database/transaction.rs b/migra-cli/src/database/transaction.rs deleted file mode 100644 index a5945c4..0000000 --- a/migra-cli/src/database/transaction.rs +++ /dev/null @@ -1,63 +0,0 @@ -use super::connection::AnyConnection; -use crate::error::StdResult; - -pub trait ManageTransaction { - fn begin_transaction(&self, conn: &mut AnyConnection) -> StdResult<()>; - - fn rollback_transaction(&self, conn: &mut AnyConnection) -> StdResult<()>; - - fn commit_transaction(&self, conn: &mut AnyConnection) -> StdResult<()>; -} - -#[derive(Debug)] -pub struct TransactionManager; - -impl TransactionManager { - pub fn new() -> Self { - TransactionManager - } -} - -impl ManageTransaction for TransactionManager { - fn begin_transaction(&self, conn: &mut AnyConnection) -> StdResult<()> { - conn.batch_execute("BEGIN") - } - - fn rollback_transaction(&self, conn: &mut AnyConnection) -> StdResult<()> { - conn.batch_execute("ROLLBACK") - } - - fn commit_transaction(&self, conn: &mut AnyConnection) -> StdResult<()> { - conn.batch_execute("COMMIT") - } -} - -pub fn with_transaction( - conn: &mut AnyConnection, - trx_fn: &mut TrxFnMut, -) -> StdResult -where - TrxFnMut: FnMut(&mut AnyConnection) -> StdResult, -{ - let transaction_manager = TransactionManager::new(); - transaction_manager - .begin_transaction(conn) - .and_then(|_| trx_fn(conn)) - .and_then(|res| transaction_manager.commit_transaction(conn).and(Ok(res))) - .or_else(|err| transaction_manager.rollback_transaction(conn).and(Err(err))) -} - -pub fn maybe_with_transaction( - is_needed: bool, - conn: &mut AnyConnection, - trx_fn: &mut TrxFnMut, -) -> StdResult -where - TrxFnMut: FnMut(&mut AnyConnection) -> StdResult, -{ - if is_needed && conn.supports_transactional_ddl() { - with_transaction(conn, trx_fn) - } else { - trx_fn(conn) - } -} diff --git a/migra-cli/src/main.rs b/migra-cli/src/main.rs deleted file mode 100644 index f9f77da..0000000 --- a/migra-cli/src/main.rs +++ /dev/null @@ -1,27 +0,0 @@ -#![deny(clippy::all)] -#![forbid(unsafe_code)] - -#[macro_use] -extern crate cfg_if; - -#[cfg(not(any(feature = "postgres", feature = "mysql")))] -compile_error!(r#"Either features "postgres" or "mysql" must be enabled for "migra" crate"#); - -mod app; -mod commands; -mod config; -mod database; -mod error; -mod opts; - -use crate::error::StdResult; -use app::App; -use config::Config; -use opts::{AppOpt, StructOpt}; - -fn main() -> StdResult<()> { - #[cfg(feature = "dotenv")] - dotenv::dotenv().ok(); - - App::new(AppOpt::from_args()).run_command() -} diff --git a/migra/Cargo.toml b/migra/Cargo.toml new file mode 100644 index 0000000..74dc7d2 --- /dev/null +++ b/migra/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "migra" +version = "1.0.0" +authors = ["Dmitriy Pleshevskiy "] +edition = "2018" +description = "Migra is a simple library for managing SQL in your application" +homepage = "https://github.com/pleshevskiy/migra" +repository = "https://github.com/pleshevskiy/migra" +license = "MIT OR Apache-2.0" +keywords = ["migration", "sql", "manager"] +categories = ["accessibility", "database"] +readme = "README.md" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[features] +default = ["postgres"] +sqlite = ["rusqlite"] + +[dependencies] +postgres = { version = "0.19", optional = true } +mysql = { version = "20.1", optional = true } +rusqlite = { version = "0.25", optional = true } diff --git a/migra/README.md b/migra/README.md new file mode 100644 index 0000000..9556f01 --- /dev/null +++ b/migra/README.md @@ -0,0 +1,86 @@ +# Migra + +[![CI](https://github.com/pleshevskiy/migra/actions/workflows/rust.yml/badge.svg?branch=main)](https://github.com/pleshevskiy/migra/actions/workflows/rust.yml) +[![unsafe forbidden](https://img.shields.io/badge/unsafe-forbidden-success.svg)](https://github.com/rust-secure-code/safety-dance/) +[![Crates.io](https://img.shields.io/crates/v/migra)](https://crates.io/crates/migra) +![Crates.io](https://img.shields.io/crates/l/migra) + +Migra is a simple library for managing SQL in your application. + +For example, if you have a task list application, you can update the local user database from version to version. + +This is main crate for [migra-cli](https://crates.io/crates/migra-cli), which allows you to manege SQL for web +servers in any program language without being bound to SQL frameworks. + + +### Installation + +Add `migra = { version = "1.0" }` as a dependency in `Cargo.toml`. + +This crate has not required predefined database clients in features with similar name. +If you want to add them, just install crate with additional features (`postgres`, `mysql`, `sqlite`). + +`Cargo.toml` example: + +```toml +[package] +name = "my-crate" +version = "0.1.0" +authors = ["Me "] + +[dependencies] +migra = { version = "1.0", features = ["postgres"] } +``` + +## Basic usage + +**Note:** This example requires to enable `sqlite` feature. + +```rust +use migra::clients::{OpenDatabaseConnection, SqliteClient}; +use migra::managers::{ManageTransaction, ManageMigrations}; + +fn main() -> migra::Result<()> { + let mut client = SqliteClient::new("./tasks.db")?; + + client.create_migrations_table()?; + + let mut migrations = client.get_applied_migrations()?; + + client + .begin_transaction() + .and_then(|_| { + migrations.should_run_upgrade_migration( + &mut client, + "20210615_initial_migration", + r#"CREATE TABLE IF NOT EXISTS tasks ( + title TEXT NOT NULL + );"#, + )?; + + Ok(()) + }) + .and_then(|res| client.commit_transaction().and(Ok(res))) + .or_else(|err| client.rollback_transaction().and(Err(err))); + + Ok(()) +} +``` + +### Supported databases + +| Database | Feature | +|----------|--------------| +| Postgres | postgres | +| MySQL | mysql | +| Sqlite | sqlite | + + +## License + +Licensed under either of these: + + * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE_APACHE) or + https://www.apache.org/licenses/LICENSE-2.0) + * MIT license ([LICENSE-MIT](LICENSE_MIT) or + https://opensource.org/licenses/MIT) diff --git a/migra/src/clients/mod.rs b/migra/src/clients/mod.rs new file mode 100644 index 0000000..15c75d4 --- /dev/null +++ b/migra/src/clients/mod.rs @@ -0,0 +1,39 @@ +use crate::errors::MigraResult; +use crate::managers::{ManageMigrations, ManageTransaction}; + +/// A trait that helps to open a connection to a specific database client. +pub trait OpenDatabaseConnection +where + Self: Sized, +{ + /// Open database connection with predefined migrations table name. + fn new(connection_string: &str) -> MigraResult { + Self::manual(connection_string, "migrations") + } + + /// Open database connection manually with additional migration table name parameter. + fn manual(connection_string: &str, migrations_table_name: &str) -> MigraResult; +} + +/// All client implementations that have migration and transaction manager implementations +/// are considered clients. +pub trait Client: ManageMigrations + ManageTransaction {} + +/// If you have complex application mechanics that allow users to choose which +/// database they can use, then you will most likely need this helper for that. +pub type AnyClient = Box<(dyn Client + 'static)>; + +#[cfg(feature = "postgres")] +mod postgres; +#[cfg(feature = "postgres")] +pub use self::postgres::Client as PostgresClient; + +#[cfg(feature = "mysql")] +mod mysql; +#[cfg(feature = "mysql")] +pub use self::mysql::Client as MysqlClient; + +#[cfg(feature = "sqlite")] +mod sqlite; +#[cfg(feature = "sqlite")] +pub use self::sqlite::Client as SqliteClient; diff --git a/migra/src/clients/mysql.rs b/migra/src/clients/mysql.rs new file mode 100644 index 0000000..f26f295 --- /dev/null +++ b/migra/src/clients/mysql.rs @@ -0,0 +1,94 @@ +use super::OpenDatabaseConnection; +use crate::errors::{DbKind, Error, MigraResult, StdResult}; +use crate::managers::{BatchExecute, ManageMigrations, ManageTransaction}; +use crate::migration; +use mysql::prelude::*; +use mysql::{Pool, PooledConn}; + +/// Predefined `MySQL` client. +/// +/// **Note:** Requires enabling `mysql` feature. +#[derive(Debug)] +pub struct Client { + conn: PooledConn, + migrations_table_name: String, +} + +impl Client { + /// Provide access to the original database connection. + #[must_use] + pub fn conn(&self) -> &PooledConn { + &self.conn + } +} + +impl OpenDatabaseConnection for Client { + fn manual(connection_string: &str, migrations_table_name: &str) -> MigraResult { + let conn = Pool::new_manual(1, 1, connection_string) + .and_then(|pool| pool.get_conn()) + .map_err(|err| Error::db(err.into(), DbKind::DatabaseConnection))?; + + Ok(Client { + conn, + migrations_table_name: migrations_table_name.to_owned(), + }) + } +} + +impl BatchExecute for Client { + fn batch_execute(&mut self, sql: &str) -> StdResult<()> { + self.conn.query_drop(sql).map_err(From::from) + } +} + +impl ManageTransaction for Client {} + +impl ManageMigrations for Client { + fn create_migrations_table(&mut self) -> MigraResult<()> { + let stmt = format!( + r#"CREATE TABLE IF NOT EXISTS {} ( + id int AUTO_INCREMENT PRIMARY KEY, + name varchar(256) NOT NULL UNIQUE + )"#, + &self.migrations_table_name + ); + + self.batch_execute(&stmt) + .map_err(|err| Error::db(err, DbKind::CreateMigrationsTable)) + } + + fn insert_migration(&mut self, name: &str) -> MigraResult { + let stmt = format!( + "INSERT INTO {} (name) VALUES (?)", + &self.migrations_table_name + ); + + self.conn + .exec_first(&stmt, (name,)) + .map(Option::unwrap_or_default) + .map_err(|err| Error::db(err.into(), DbKind::InsertMigration)) + } + + fn delete_migration(&mut self, name: &str) -> MigraResult { + let stmt = format!("DELETE FROM {} WHERE name = ?", &self.migrations_table_name); + + self.conn + .exec_first(&stmt, (name,)) + .map(Option::unwrap_or_default) + .map_err(|err| Error::db(err.into(), DbKind::DeleteMigration)) + } + + fn get_applied_migrations(&mut self) -> MigraResult { + let stmt = format!( + "SELECT name FROM {} ORDER BY id DESC", + &self.migrations_table_name + ); + + self.conn + .query::(stmt) + .map(From::from) + .map_err(|err| Error::db(err.into(), DbKind::GetAppliedMigrations)) + } +} + +impl super::Client for Client {} diff --git a/migra/src/clients/postgres.rs b/migra/src/clients/postgres.rs new file mode 100644 index 0000000..c60ddf6 --- /dev/null +++ b/migra/src/clients/postgres.rs @@ -0,0 +1,105 @@ +use super::OpenDatabaseConnection; +use crate::errors::{DbKind, Error, MigraResult, StdResult}; +use crate::managers::{BatchExecute, ManageMigrations, ManageTransaction}; +use crate::migration; +use postgres::{Client as PostgresClient, NoTls}; +use std::fmt; + +/// Predefined `Postgres` client. +/// +/// **Note:** Requires enabling `postgres` feature. +pub struct Client { + conn: PostgresClient, + migrations_table_name: String, +} + +impl Client { + /// Provide access to the original database connection. + #[must_use] + pub fn conn(&self) -> &PostgresClient { + &self.conn + } +} + +impl fmt::Debug for Client { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt.debug_struct("Client") + .field("migrations_table_name", &self.migrations_table_name) + .finish() + } +} + +impl OpenDatabaseConnection for Client { + fn manual(connection_string: &str, migrations_table_name: &str) -> MigraResult { + let conn = PostgresClient::connect(connection_string, NoTls) + .map_err(|err| Error::db(err.into(), DbKind::DatabaseConnection))?; + Ok(Client { + conn, + migrations_table_name: migrations_table_name.to_owned(), + }) + } +} + +impl BatchExecute for Client { + fn batch_execute(&mut self, sql: &str) -> StdResult<()> { + self.conn.batch_execute(sql).map_err(From::from) + } +} + +impl ManageTransaction for Client {} + +impl ManageMigrations for Client { + fn create_migrations_table(&mut self) -> MigraResult<()> { + let stmt = format!( + r#"CREATE TABLE IF NOT EXISTS {} ( + id serial PRIMARY KEY, + name text NOT NULL UNIQUE + )"#, + &self.migrations_table_name + ); + + self.batch_execute(&stmt) + .map_err(|err| Error::db(err, DbKind::CreateMigrationsTable)) + } + + fn insert_migration(&mut self, name: &str) -> MigraResult { + let stmt = format!( + "INSERT INTO {} (name) VALUES ($1)", + &self.migrations_table_name + ); + + self.conn + .execute(stmt.as_str(), &[&name]) + .map_err(|err| Error::db(err.into(), DbKind::InsertMigration)) + } + + fn delete_migration(&mut self, name: &str) -> MigraResult { + let stmt = format!( + "DELETE FROM {} WHERE name = $1", + &self.migrations_table_name + ); + + self.conn + .execute(stmt.as_str(), &[&name]) + .map_err(|err| Error::db(err.into(), DbKind::DeleteMigration)) + } + + fn get_applied_migrations(&mut self) -> MigraResult { + let stmt = format!( + "SELECT name FROM {} ORDER BY id DESC", + &self.migrations_table_name + ); + + self.conn + .query(stmt.as_str(), &[]) + .and_then(|res| { + res.into_iter() + .map(|row| row.try_get(0)) + .collect::, _>>() + }) + .map(From::from) + .map_err(|err| Error::db(err.into(), DbKind::GetAppliedMigrations)) + } +} + +impl super::Client for Client {} diff --git a/migra/src/clients/sqlite.rs b/migra/src/clients/sqlite.rs new file mode 100644 index 0000000..8617097 --- /dev/null +++ b/migra/src/clients/sqlite.rs @@ -0,0 +1,103 @@ +use super::OpenDatabaseConnection; +use crate::errors::{DbKind, Error, MigraResult, StdResult}; +use crate::managers::{BatchExecute, ManageMigrations, ManageTransaction}; +use crate::migration; +use rusqlite::Connection; + +/// Predefined `Sqlite` client. +/// +/// **Note:** Requires enabling `sqlite` feature. +#[derive(Debug)] +pub struct Client { + conn: Connection, + migrations_table_name: String, +} + +impl Client { + /// Provide access to the original database connection. + #[must_use] + pub fn conn(&self) -> &Connection { + &self.conn + } +} + +impl OpenDatabaseConnection for Client { + fn manual(connection_string: &str, migrations_table_name: &str) -> MigraResult { + let conn = if connection_string == ":memory:" { + Connection::open_in_memory() + } else { + Connection::open(connection_string) + } + .map_err(|err| Error::db(err.into(), DbKind::DatabaseConnection))?; + + Ok(Client { + conn, + migrations_table_name: migrations_table_name.to_owned(), + }) + } +} + +impl BatchExecute for Client { + fn batch_execute(&mut self, sql: &str) -> StdResult<()> { + self.conn.execute_batch(sql).map_err(From::from) + } +} + +impl ManageTransaction for Client {} + +impl ManageMigrations for Client { + fn create_migrations_table(&mut self) -> MigraResult<()> { + let stmt = format!( + r#"CREATE TABLE IF NOT EXISTS {} ( + id int AUTO_INCREMENT PRIMARY KEY, + name varchar(256) NOT NULL UNIQUE + )"#, + &self.migrations_table_name + ); + + self.batch_execute(&stmt) + .map_err(|err| Error::db(err, DbKind::CreateMigrationsTable)) + } + + fn insert_migration(&mut self, name: &str) -> MigraResult { + let stmt = format!( + "INSERT INTO {} (name) VALUES ($1)", + &self.migrations_table_name + ); + + self.conn + .execute(&stmt, [name]) + .map(|res| res as u64) + .map_err(|err| Error::db(err.into(), DbKind::InsertMigration)) + } + + fn delete_migration(&mut self, name: &str) -> MigraResult { + let stmt = format!( + "DELETE FROM {} WHERE name = $1", + &self.migrations_table_name + ); + + self.conn + .execute(&stmt, [name]) + .map(|res| res as u64) + .map_err(|err| Error::db(err.into(), DbKind::DeleteMigration)) + } + + fn get_applied_migrations(&mut self) -> MigraResult { + let stmt = format!( + "SELECT name FROM {} ORDER BY id DESC", + &self.migrations_table_name + ); + + self.conn + .prepare(&stmt) + .and_then(|mut stmt| { + stmt.query_map([], |row| row.get(0))? + .collect::, _>>() + }) + .map(From::from) + .map_err(|err| Error::db(err.into(), DbKind::GetAppliedMigrations)) + } +} + +impl super::Client for Client {} diff --git a/migra/src/errors.rs b/migra/src/errors.rs new file mode 100644 index 0000000..1a346a2 --- /dev/null +++ b/migra/src/errors.rs @@ -0,0 +1,127 @@ +use std::fmt; +use std::io; + +/// A helper type for any standard error. +pub type StdError = Box; + +/// A helper type for any result with standard error. +pub type StdResult = Result; + +/// A helper type for any result with migra error. +pub type MigraResult = Result; + +/// Migra error +#[derive(Debug)] +pub enum Error { + /// Represents database errors. + Db(DbError), + + /// Represents standard input output errors. + Io(io::Error), +} + +impl fmt::Display for Error { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::Db(ref error) => write!(fmt, "{}", error), + Error::Io(ref error) => write!(fmt, "{}", error), + } + } +} + +impl std::error::Error for Error {} + +impl PartialEq for Error { + fn eq(&self, other: &Self) -> bool { + std::mem::discriminant(self) == std::mem::discriminant(other) + } +} + +impl From for Error { + #[inline] + fn from(err: io::Error) -> Error { + Error::Io(err) + } +} + +impl Error { + /// Creates a database error. + #[must_use] + pub fn db(origin: StdError, kind: DbKind) -> Self { + Error::Db(DbError { kind, origin }) + } +} + +/// All kinds of errors with witch this crate works. +#[derive(Debug)] +pub enum DbKind { + /// Failed to database connection. + DatabaseConnection, + + /// Failed to open transaction. + OpenTransaction, + + /// Failed to commit transaction. + CommitTransaction, + + /// Failed to rollback transaction. + RollbackTransaction, + + /// Failed to create a migrations table. + CreateMigrationsTable, + + /// Failed to apply SQL. + ApplySql, + + /// Failed to insert a migration. + InsertMigration, + + /// Failed to delete a migration. + DeleteMigration, + + /// Failed to get applied migrations. + GetAppliedMigrations, +} + +impl fmt::Display for DbKind { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + DbKind::DatabaseConnection => fmt.write_str("Failed database connection"), + DbKind::OpenTransaction => fmt.write_str("Failed to open a transaction"), + DbKind::CommitTransaction => fmt.write_str("Failed to commit a transaction"), + DbKind::RollbackTransaction => fmt.write_str("Failed to rollback a transaction"), + DbKind::CreateMigrationsTable => fmt.write_str("Failed to create a migrations table"), + DbKind::ApplySql => fmt.write_str("Failed to apply sql"), + DbKind::InsertMigration => fmt.write_str("Failed to insert a migration"), + DbKind::DeleteMigration => fmt.write_str("Failed to delete a migration"), + DbKind::GetAppliedMigrations => fmt.write_str("Failed to get applied migrations"), + } + } +} + +/// Represents database error. +#[derive(Debug)] +pub struct DbError { + kind: DbKind, + origin: StdError, +} + +impl fmt::Display for DbError { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(fmt, "{} - {}", &self.kind, &self.origin) + } +} + +impl DbError { + /// Returns database error kind. + #[must_use] + pub fn kind(&self) -> &DbKind { + &self.kind + } + + /// Returns origin database error. + #[must_use] + pub fn origin(&self) -> &StdError { + &self.origin + } +} diff --git a/migra/src/fs.rs b/migra/src/fs.rs new file mode 100644 index 0000000..157f8fb --- /dev/null +++ b/migra/src/fs.rs @@ -0,0 +1,34 @@ +use crate::errors::MigraResult; +use crate::migration; +use std::io; +use std::path::Path; + +/// Checks if the directory is a migration according to the principles of the crate. +#[must_use] +pub fn is_migration_dir(path: &Path) -> bool { + path.join("up.sql").exists() && path.join("down.sql").exists() +} + +/// Get all migration directories from path and returns as [List]. +/// +/// This utility checks if the directory is a migration. See [`is_migration_dir`] for +/// more information. +/// +/// [List]: migration::List +/// [is_migration_dir]: fs::is_migration_dir +pub fn get_all_migrations(dir_path: &Path) -> MigraResult { + let mut entries = match dir_path.read_dir() { + Err(e) if e.kind() == io::ErrorKind::NotFound => vec![], + entries => entries? + .filter_map(|res| res.ok().map(|e| e.path())) + .filter(|path| is_migration_dir(&path)) + .collect::>(), + }; + + if entries.is_empty() { + return Ok(migration::List::new()); + } + + entries.sort(); + Ok(migration::List::from(entries)) +} diff --git a/migra/src/lib.rs b/migra/src/lib.rs new file mode 100644 index 0000000..e7b28ea --- /dev/null +++ b/migra/src/lib.rs @@ -0,0 +1,97 @@ +//! # Migra +//! +//! Migra is a simple library for managing SQL in your application. +//! +//! For example, if you have a task list application, you can update the local user database from version to version. +//! +//! This is main crate for [migra-cli](https://crates.io/crates/migra-cli), which allows you to manege SQL for web +//! servers in any program language without being bound to SQL frameworks. +//! +//! ## Installation +//! +//! Add `migra = { version = "1.0" }` as a dependency in `Cargo.toml`. +//! +//! This crate has not required predefined database clients in features with similar name. +//! If you want to add them, just install crate with additional features (`postgres`, `mysql`, `sqlite`). +//! +//! `Cargo.toml` example: +//! +//! ```toml +//! [package] +//! name = "my-crate" +//! version = "0.1.0" +//! authors = ["Me "] +//! +//! [dependencies] +//! migra = { version = "1.0", features = ["postgres"] } +//! ``` +//! +//! ## Basic usage +//! +//! **Note:** This example requires to enable `sqlite` feature. +//! +//! ```rust +//! use migra::clients::{OpenDatabaseConnection, SqliteClient}; +//! use migra::managers::{ManageTransaction, ManageMigrations}; +//! +//! fn main() -> migra::Result<()> { +//! let mut client = SqliteClient::new(":memory:")?; +//! +//! client.create_migrations_table()?; +//! +//! let mut migrations = client.get_applied_migrations()?; +//! +//! client +//! .begin_transaction() +//! .and_then(|_| { +//! migrations.should_run_upgrade_migration( +//! &mut client, +//! "20210615_initial_migration", +//! r#"CREATE TABLE IF NOT EXISTS tasks ( +//! title TEXT NOT NULL +//! );"#, +//! )?; +//! +//! Ok(()) +//! }) +//! .and_then(|res| client.commit_transaction().and(Ok(res))) +//! .or_else(|err| client.rollback_transaction().and(Err(err))); +//! +//! Ok(()) +//! } +//! ``` +//! +//! ### Supported databases +//! +//! | Database Client | Feature | +//! |-----------------|--------------| +//! | `Postgres` | postgres | +//! | `MySQL` | mysql | +//! | `Sqlite` | sqlite | +//! +#![deny(missing_debug_implementations)] +#![deny(missing_docs)] +#![deny(clippy::all, clippy::pedantic)] +// TODO: add missing errors doc +#![allow(clippy::missing_errors_doc)] + +/// Includes additional client tools and contains predefined +/// database clients that have been enabled in the features. +pub mod clients; + +/// Includes all types of errors that uses in the crate. +pub mod errors; + +/// Includes utilities that use the file system to work. +pub mod fs; + +/// Includes all the basic traits that will allow you +/// to create your own client. +pub mod managers; + +/// Includes basic structures of migration and migration +/// lists, that are used in managers and fs utils. +pub mod migration; + +pub use errors::{Error, MigraResult as Result, StdResult}; +pub use migration::{List as MigrationList, Migration}; diff --git a/migra/src/managers.rs b/migra/src/managers.rs new file mode 100644 index 0000000..87d80a2 --- /dev/null +++ b/migra/src/managers.rs @@ -0,0 +1,74 @@ +use crate::errors::{DbKind, Error, MigraResult, StdResult}; +use crate::migration; + +/// Used to execute SQL. +/// +/// Is a super trait for managers. +pub trait BatchExecute { + /// Executes sql via original database client + fn batch_execute(&mut self, sql: &str) -> StdResult<()>; +} + +/// Used to manage transaction in the database connection. +pub trait ManageTransaction: BatchExecute { + /// Opens transaction in database connection. + fn begin_transaction(&mut self) -> MigraResult<()> { + self.batch_execute("BEGIN") + .map_err(|err| Error::db(err, DbKind::OpenTransaction)) + } + + /// Cancels (Rollbacks) transaction in database connection. + fn rollback_transaction(&mut self) -> MigraResult<()> { + self.batch_execute("ROLLBACK") + .map_err(|err| Error::db(err, DbKind::RollbackTransaction)) + } + + /// Apply (Commit) transaction in database connection. + fn commit_transaction(&mut self) -> MigraResult<()> { + self.batch_execute("COMMIT") + .map_err(|err| Error::db(err, DbKind::CommitTransaction)) + } +} + +/// Used to manage migrations in the database connection. +pub trait ManageMigrations: BatchExecute { + /// Applies SQL. Similar to [`BatchExecute`], but returns migra [Error]. + /// + /// [BatchExecute]: managers::BatchExecute + fn apply_sql(&mut self, sql: &str) -> MigraResult<()> { + self.batch_execute(sql) + .map_err(|err| Error::db(err, DbKind::ApplySql)) + } + + /// Creates migration table. + fn create_migrations_table(&mut self) -> MigraResult<()>; + + /// Inserts new migration to table. + fn insert_migration(&mut self, name: &str) -> MigraResult; + + /// Deletes migration from table. + fn delete_migration(&mut self, name: &str) -> MigraResult; + + /// Get applied migrations from table. + fn get_applied_migrations(&mut self) -> MigraResult; + + /// Applies SQL to upgrade database schema and inserts new migration to table. + /// + /// **Note:** Must be run in a transaction otherwise if the migration causes any + /// error the data in the database may be inconsistent. + fn run_upgrade_migration(&mut self, name: &str, content: &str) -> MigraResult<()> { + self.apply_sql(content)?; + self.insert_migration(name)?; + Ok(()) + } + + /// Applies SQL to downgrade database schema and deletes migration from table. + /// + /// **Note:** Must be run in a transaction otherwise if the migration causes any + /// error the data in the database may be inconsistent. + fn run_downgrade_migration(&mut self, name: &str, content: &str) -> MigraResult<()> { + self.apply_sql(content)?; + self.delete_migration(name)?; + Ok(()) + } +} diff --git a/migra/src/migration.rs b/migra/src/migration.rs new file mode 100644 index 0000000..460792e --- /dev/null +++ b/migra/src/migration.rs @@ -0,0 +1,242 @@ +use crate::errors::MigraResult; +use crate::managers::ManageMigrations; +use std::iter::FromIterator; + +/// A simple wrap over string. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct Migration { + name: String, +} + +impl Migration { + /// Creates new migration by name. + #[must_use] + pub fn new(name: &str) -> Self { + Migration { + name: name.to_owned(), + } + } + + /// Returns name of migration. + #[must_use] + pub fn name(&self) -> &String { + &self.name + } +} + +/// Wrap over migration vector. Can be implicitly converted to a vector and has +/// a few of additional utilities for handling 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. +/// +/// +/// +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct List { + inner: Vec, +} + +impl> From> for List { + fn from(list: Vec) -> Self { + List { + inner: list + .iter() + .map(AsRef::as_ref) + .map(|path| { + path.file_name() + .and_then(std::ffi::OsStr::to_str) + .expect("Cannot read migration name") + }) + .map(Migration::new) + .collect(), + } + } +} + +impl From> for List { + fn from(list: Vec) -> Self { + List { inner: list } + } +} + +impl FromIterator for List { + fn from_iter>(iter: I) -> Self { + let mut list = List::new(); + + for item in iter { + list.push(item); + } + + list + } +} + +impl<'a> FromIterator<&'a Migration> for List { + fn from_iter>(iter: I) -> Self { + let mut list = List::new(); + + for item in iter { + list.push(item.clone()); + } + + list + } +} + +impl std::ops::Deref for List { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl List { + /// Creates empty migration list. + #[must_use] + pub fn new() -> Self { + List { inner: Vec::new() } + } + + /// Push migration to list. + pub fn push(&mut self, migration: Migration) { + self.inner.push(migration) + } + + /// Push migration name to list. + /// + /// # Example + /// + /// ```rust + /// # use migra::migration::List; + /// # let mut list = List::new(); + /// list.push_name("name"); + /// # assert_eq!(list, List::from(vec!["name"])); + /// ``` + /// + /// Is identical to the following + /// ```rust + /// # use migra::migration::{List, Migration}; + /// # let mut list = List::new(); + /// list.push(Migration::new("name")); + /// # assert_eq!(list, List::from(vec!["name"])); + /// ``` + pub fn push_name(&mut self, name: &str) { + self.inner.push(Migration::new(name)) + } + + /// Check if list contains specific migration. + #[must_use] + pub fn contains(&self, other_migration: &Migration) -> bool { + self.inner + .iter() + .any(|migration| migration == other_migration) + } + + /// Check if list contains migration with specific name. + #[must_use] + pub fn contains_name(&self, name: &str) -> bool { + self.inner.iter().any(|migration| migration.name() == name) + } + + /// Exclude specific list from current list. + #[must_use] + pub fn exclude(&self, list: &List) -> List { + self.inner + .iter() + .filter(|migration| !list.contains_name(migration.name())) + .collect() + } + + /// Runs a upgrade migration with SQL content and adds a new migration to the current list + /// If there is no migration migration with specific name in the list. + pub fn should_run_upgrade_migration( + &mut self, + client: &mut dyn ManageMigrations, + name: &str, + content: &str, + ) -> MigraResult { + let is_missed = !self.contains_name(name); + + if is_missed { + client.run_upgrade_migration(name, content)?; + self.push_name(name); + } + + Ok(is_missed) + } + + /// Runs a downgrade migration with SQL content and removes the last migration from the + /// current list if the last item in the list has the specified name. + pub fn should_run_downgrade_migration( + &mut self, + client: &mut dyn ManageMigrations, + name: &str, + content: &str, + ) -> MigraResult { + let is_latest = self.inner.last() == Some(&Migration::new(name)); + + if is_latest { + client.run_downgrade_migration(name, content)?; + self.inner.pop(); + } + + Ok(is_latest) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const FIRST_MIGRATION: &str = "initial_migration"; + const SECOND_MIGRATION: &str = "new_migration"; + + #[test] + fn push_migration_to_list() { + let mut list = List::new(); + + list.push(Migration::new(FIRST_MIGRATION)); + assert_eq!(list, List::from(vec![FIRST_MIGRATION])); + + list.push(Migration::new(SECOND_MIGRATION)); + assert_eq!(list, List::from(vec![FIRST_MIGRATION, SECOND_MIGRATION])) + } + + #[test] + fn push_name_to_list() { + let mut list = List::new(); + + list.push_name(FIRST_MIGRATION); + assert_eq!(list, List::from(vec![FIRST_MIGRATION])); + + list.push_name(&String::from(SECOND_MIGRATION)); + assert_eq!(list, List::from(vec![FIRST_MIGRATION, SECOND_MIGRATION])) + } + + #[test] + fn contains_migration() { + let list = List::from(vec![FIRST_MIGRATION]); + + assert_eq!(list.contains(&Migration::new(FIRST_MIGRATION)), true); + assert_eq!(list.contains(&Migration::new(SECOND_MIGRATION)), false); + } + + #[test] + fn contains_migration_name() { + let list = List::from(vec![FIRST_MIGRATION]); + + assert_eq!(list.contains_name(FIRST_MIGRATION), true); + assert_eq!(list.contains_name(SECOND_MIGRATION), false); + } + + #[test] + fn create_excluded_migration_list() { + let all_migrations = List::from(vec![FIRST_MIGRATION, SECOND_MIGRATION]); + let applied_migrations = List::from(vec![FIRST_MIGRATION]); + let excluded = all_migrations.exclude(&applied_migrations); + + assert_eq!(excluded, List::from(vec![SECOND_MIGRATION])) + } +} diff --git a/migra-cli/Cargo.toml b/migra_cli/Cargo.toml similarity index 76% rename from migra-cli/Cargo.toml rename to migra_cli/Cargo.toml index d2a0791..ce9302b 100644 --- a/migra-cli/Cargo.toml +++ b/migra_cli/Cargo.toml @@ -13,20 +13,27 @@ readme = "../README.md" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +default = ["postgres"] +postgres = ["migra/postgres"] +sqlite = ["migra/sqlite"] +mysql = ["migra/mysql"] + [dependencies] +migra = { version = "1", path = "../migra" } cfg-if = "1.0" structopt = "0.3" serde = { version = "1.0", features = ["derive"] } toml = "0.5" chrono = "0.4" dotenv = { version = "0.15", optional = true } -postgres = { version = "0.19", optional = true } -mysql = { version = "20.1", optional = true } -rusqlite = { version = "0.25", optional = true } -[features] -default = ["postgres"] -sqlite = ["rusqlite"] +[dev-dependencies] +assert_cmd = "1" +predicates = "1" +client_postgres = { package = "postgres", version = "0.19" } +client_mysql = { package = "mysql", version = "20.1" } +client_rusqlite = { package = "rusqlite", version = "0.25" } [badges] maintenance = { status = "actively-developed" } @@ -38,7 +45,3 @@ path = "src/main.rs" [[test]] name = "integration" path = "tests/commands.rs" - -[dev-dependencies] -assert_cmd = "1" -predicates = "1" diff --git a/migra-cli/src/app.rs b/migra_cli/src/app.rs similarity index 80% rename from migra-cli/src/app.rs rename to migra_cli/src/app.rs index 7610ddc..7baccb5 100644 --- a/migra-cli/src/app.rs +++ b/migra_cli/src/app.rs @@ -1,5 +1,5 @@ use crate::commands; -use crate::error::*; +use crate::error::MigraResult; use crate::opts::Command; use crate::AppOpt; use crate::Config; @@ -24,24 +24,24 @@ impl App { Config::read(self.config_path()) } - pub fn run_command(&self) -> StdResult<()> { - match dbg!(self.app_opt.command.clone()) { + pub fn run_command(&self) -> migra::StdResult<()> { + match self.app_opt.command.clone() { Command::Init => { commands::initialize_migra_manifest(self)?; } - Command::Apply(cmd_opts) => { + Command::Apply(ref cmd_opts) => { commands::apply_sql(self, cmd_opts)?; } - Command::Make(cmd_opts) => { + Command::Make(ref cmd_opts) => { commands::make_migration(self, cmd_opts)?; } Command::List => { commands::print_migration_lists(self)?; } - Command::Upgrade(cmd_opts) => { + Command::Upgrade(ref cmd_opts) => { commands::upgrade_pending_migrations(self, cmd_opts)?; } - Command::Downgrade(cmd_opts) => { + Command::Downgrade(ref cmd_opts) => { commands::rollback_applied_migrations(self, cmd_opts)?; } Command::Completions(cmd_opts) => { diff --git a/migra-cli/src/commands/apply.rs b/migra_cli/src/commands/apply.rs similarity index 55% rename from migra-cli/src/commands/apply.rs rename to migra_cli/src/commands/apply.rs index 0a78448..07d69f1 100644 --- a/migra-cli/src/commands/apply.rs +++ b/migra_cli/src/commands/apply.rs @@ -1,16 +1,10 @@ use crate::app::App; -use crate::database::prelude::*; -use crate::database::transaction::maybe_with_transaction; -use crate::database::{DatabaseConnectionManager, MigrationManager}; +use crate::database; use crate::opts::ApplyCommandOpt; -use crate::StdResult; -pub(crate) fn apply_sql(app: &App, cmd_opts: ApplyCommandOpt) -> StdResult<()> { +pub(crate) fn apply_sql(app: &App, cmd_opts: &ApplyCommandOpt) -> migra::StdResult<()> { let config = app.config()?; - let mut connection_manager = DatabaseConnectionManager::connect(&config.database)?; - let conn = connection_manager.connection(); - - let migration_manager = MigrationManager::from(&config); + let mut client = database::create_client_from_config(&config)?; let file_contents = cmd_opts .file_paths @@ -26,17 +20,17 @@ pub(crate) fn apply_sql(app: &App, cmd_opts: ApplyCommandOpt) -> StdResult<()> { .map(std::fs::read_to_string) .collect::, _>>()?; - maybe_with_transaction( + database::should_run_in_transaction( + &mut client, cmd_opts.transaction_opts.single_transaction, - conn, - &mut |conn| { + |client| { file_contents .iter() .try_for_each(|content| { - maybe_with_transaction( + database::should_run_in_transaction( + client, !cmd_opts.transaction_opts.single_transaction, - conn, - &mut |conn| migration_manager.apply_sql(conn, content), + |client| client.apply_sql(content), ) }) .map_err(From::from) diff --git a/migra_cli/src/commands/downgrade.rs b/migra_cli/src/commands/downgrade.rs new file mode 100644 index 0000000..7c2eeeb --- /dev/null +++ b/migra_cli/src/commands/downgrade.rs @@ -0,0 +1,58 @@ +use crate::app::App; +use crate::database; +use crate::opts::DowngradeCommandOpt; +use std::cmp; + +pub(crate) fn rollback_applied_migrations( + app: &App, + opts: &DowngradeCommandOpt, +) -> migra::StdResult<()> { + let config = app.config()?; + let mut client = database::create_client_from_config(&config)?; + + client.create_migrations_table()?; + + let migrations_dir_path = config.migration_dir_path(); + let applied_migrations = client.get_applied_migrations()?; + let all_migrations = migra::fs::get_all_migrations(&migrations_dir_path)?; + + let rollback_migrations_number = if opts.all_migrations { + applied_migrations.len() + } else { + cmp::min(opts.migrations_number, applied_migrations.len()) + }; + + let migrations = applied_migrations[..rollback_migrations_number].to_vec(); + let migrations_with_content = migrations + .iter() + .map(|migration| { + let migration_name = migration.name(); + let migration_file_path = migrations_dir_path.join(migration_name).join("down.sql"); + std::fs::read_to_string(migration_file_path).map(|content| (migration_name, content)) + }) + .collect::, _>>()?; + + database::should_run_in_transaction( + &mut client, + opts.transaction_opts.single_transaction, + |client| { + migrations_with_content + .iter() + .try_for_each(|(migration_name, content)| { + if all_migrations.contains_name(migration_name) { + println!("downgrade {}...", migration_name); + database::should_run_in_transaction( + client, + !opts.transaction_opts.single_transaction, + |client| client.run_downgrade_migration(migration_name, &content), + ) + } else { + Ok(()) + } + }) + .map_err(From::from) + }, + )?; + + Ok(()) +} diff --git a/migra-cli/src/commands/init.rs b/migra_cli/src/commands/init.rs similarity index 72% rename from migra-cli/src/commands/init.rs rename to migra_cli/src/commands/init.rs index b8e7452..6735414 100644 --- a/migra-cli/src/commands/init.rs +++ b/migra_cli/src/commands/init.rs @@ -1,21 +1,19 @@ use crate::app::App; use crate::config::{Config, MIGRA_TOML_FILENAME}; -use crate::StdResult; use std::path::PathBuf; -pub(crate) fn initialize_migra_manifest(app: &App) -> StdResult<()> { - let config_path = app - .config_path() - .cloned() - .map(|mut config_path| { +pub(crate) fn initialize_migra_manifest(app: &App) -> migra::StdResult<()> { + let config_path = app.config_path().cloned().map_or_else( + || PathBuf::from(MIGRA_TOML_FILENAME), + |mut config_path| { let ext = config_path.extension(); if config_path.is_dir() || ext.is_none() { config_path.push(MIGRA_TOML_FILENAME); } config_path - }) - .unwrap_or_else(|| PathBuf::from(MIGRA_TOML_FILENAME)); + }, + ); if config_path.exists() { println!("{} already exists", config_path.to_str().unwrap()); diff --git a/migra_cli/src/commands/list.rs b/migra_cli/src/commands/list.rs new file mode 100644 index 0000000..a11ae7f --- /dev/null +++ b/migra_cli/src/commands/list.rs @@ -0,0 +1,66 @@ +use crate::app::App; +use crate::database; +use crate::error::Error; +use migra::migration; + +const EM_DASH: char = '—'; + +pub(crate) fn print_migration_lists(app: &App) -> migra::StdResult<()> { + let config = app.config()?; + let applied_migrations = match config.database.connection_string() { + Ok(ref database_connection_string) => { + let mut client = database::create_client( + &config.database.client(), + database_connection_string, + &config.migrations.table_name(), + )?; + let applied_migrations = client.get_applied_migrations().unwrap_or_else(|err| { + dbg!(err); + migration::List::new() + }); + + show_applied_migrations(&applied_migrations); + + applied_migrations + } + Err(e) if e == Error::MissedEnvVar(String::new()) => { + eprintln!("WARNING: {}", e); + eprintln!("WARNING: No connection to database"); + + migration::List::new() + } + Err(e) => panic!("{}", e), + }; + + println!(); + + let all_migrations = migra::fs::get_all_migrations(&config.migration_dir_path())?; + let pending_migrations = all_migrations.exclude(&applied_migrations); + + show_pending_migrations(&pending_migrations); + + Ok(()) +} + +fn show_applied_migrations(applied_migrations: &migration::List) { + println!("Applied migrations:"); + if applied_migrations.is_empty() { + println!("{}", EM_DASH); + } else { + applied_migrations + .iter() + .rev() + .for_each(|migration| println!("{}", migration.name())); + } +} + +fn show_pending_migrations(pending_migrations: &migration::List) { + println!("Pending migrations:"); + if pending_migrations.is_empty() { + println!("{}", EM_DASH); + } else { + pending_migrations.iter().for_each(|migration| { + println!("{}", migration.name()); + }); + } +} diff --git a/migra-cli/src/commands/make.rs b/migra_cli/src/commands/make.rs similarity index 92% rename from migra-cli/src/commands/make.rs rename to migra_cli/src/commands/make.rs index 96f3ca6..91d41e4 100644 --- a/migra-cli/src/commands/make.rs +++ b/migra_cli/src/commands/make.rs @@ -1,10 +1,9 @@ use crate::app::App; use crate::opts::MakeCommandOpt; -use crate::StdResult; use chrono::Local; use std::fs; -pub(crate) fn make_migration(app: &App, opts: MakeCommandOpt) -> StdResult<()> { +pub(crate) fn make_migration(app: &App, opts: &MakeCommandOpt) -> migra::StdResult<()> { let config = app.config()?; let date_format = config.migrations.date_format(); let formatted_current_timestamp = Local::now().format(&date_format); diff --git a/migra-cli/src/commands/mod.rs b/migra_cli/src/commands/mod.rs similarity index 100% rename from migra-cli/src/commands/mod.rs rename to migra_cli/src/commands/mod.rs diff --git a/migra_cli/src/commands/upgrade.rs b/migra_cli/src/commands/upgrade.rs new file mode 100644 index 0000000..73cb6a6 --- /dev/null +++ b/migra_cli/src/commands/upgrade.rs @@ -0,0 +1,74 @@ +use crate::app::App; +use crate::database; +use crate::opts::UpgradeCommandOpt; +use migra::migration; + +pub(crate) fn upgrade_pending_migrations( + app: &App, + opts: &UpgradeCommandOpt, +) -> migra::StdResult<()> { + let config = app.config()?; + let mut client = database::create_client_from_config(&config)?; + + client.create_migrations_table()?; + + let migrations_dir_path = config.migration_dir_path(); + let applied_migration_names = client.get_applied_migrations()?; + let all_migrations = migra::fs::get_all_migrations(&migrations_dir_path)?; + + let pending_migrations = all_migrations.exclude(&applied_migration_names); + if pending_migrations.is_empty() { + println!("Up to date"); + return Ok(()); + } + + let migrations: migration::List = if let Some(migration_name) = opts.migration_name.clone() { + let target_migration = (*pending_migrations) + .clone() + .into_iter() + .find(|m| m.name() == &migration_name); + if let Some(migration) = target_migration { + vec![migration].into() + } else { + eprintln!(r#"Cannot find migration with "{}" name"#, migration_name); + return Ok(()); + } + } else { + let upgrade_migrations_number = opts + .migrations_number + .unwrap_or_else(|| pending_migrations.len()); + + pending_migrations[..upgrade_migrations_number] + .to_vec() + .into() + }; + + let migrations_with_content = migrations + .iter() + .map(|migration| { + let migration_name = migration.name(); + let migration_file_path = migrations_dir_path.join(migration_name).join("up.sql"); + std::fs::read_to_string(migration_file_path).map(|content| (migration_name, content)) + }) + .collect::, _>>()?; + + database::should_run_in_transaction( + &mut client, + opts.transaction_opts.single_transaction, + |client| { + migrations_with_content + .iter() + .try_for_each(|(migration_name, content)| { + 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(()) +} diff --git a/migra-cli/src/config.rs b/migra_cli/src/config.rs similarity index 79% rename from migra-cli/src/config.rs rename to migra_cli/src/config.rs index 7a18243..1b73bed 100644 --- a/migra-cli/src/config.rs +++ b/migra_cli/src/config.rs @@ -1,8 +1,7 @@ -use crate::database::migration::Migration; use crate::error::{Error, MigraResult}; use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; -use std::{env, fs, io}; +use std::{env, fs}; //===========================================================================// // Internal Config Utils / Macros // @@ -46,13 +45,21 @@ cargo install migra-cli --features ${database_name}"#, // Database config // //===========================================================================// +fn is_sqlite_database_file(filename: &str) -> bool { + filename + .rsplit('.') + .next() + .map(|ext| ext.eq_ignore_ascii_case("db")) + == Some(true) +} + fn default_database_connection_env() -> String { String::from("$DATABASE_URL") } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] -pub(crate) enum SupportedDatabaseClient { +pub enum SupportedDatabaseClient { #[cfg(feature = "postgres")] Postgres, #[cfg(feature = "mysql")] @@ -114,7 +121,7 @@ impl DatabaseConfig { please_install_with!(feature "mysql") } } - } else if connection_string.ends_with(".db") { + } else if is_sqlite_database_file(&connection_string) { cfg_if! { if #[cfg(feature = "sqlite")] { Some(SupportedDatabaseClient::Sqlite) @@ -131,11 +138,13 @@ impl DatabaseConfig { } pub fn connection_string(&self) -> MigraResult { - if let Some(connection_env) = self.connection.strip_prefix("$") { - env::var(connection_env).map_err(|_| Error::MissedEnvVar(connection_env.to_string())) - } else { - Ok(self.connection.clone()) - } + self.connection.strip_prefix("$").map_or_else( + || Ok(self.connection.clone()), + |connection_env| { + env::var(connection_env) + .map_err(|_| Error::MissedEnvVar(connection_env.to_string())) + }, + ) } } @@ -174,33 +183,35 @@ impl Default for MigrationsConfig { impl MigrationsConfig { pub fn directory(&self) -> String { - if let Some(directory_env) = self.directory.strip_prefix("$") { - env::var(directory_env).unwrap_or_else(|_| { - println!( - "WARN: Cannot read {} variable and use {} directory by default", - directory_env, + self.directory.strip_prefix("$").map_or_else( + || self.directory.clone(), + |directory_env| { + env::var(directory_env).unwrap_or_else(|_| { + println!( + "WARN: Cannot read {} variable and use {} directory by default", + directory_env, + default_migrations_directory() + ); default_migrations_directory() - ); - default_migrations_directory() - }) - } else { - self.directory.clone() - } + }) + }, + ) } pub fn table_name(&self) -> String { - if let Some(table_name_env) = self.table_name.strip_prefix("$") { - env::var(table_name_env).unwrap_or_else(|_| { - println!( - "WARN: Cannot read {} variable and use {} table_name by default", - table_name_env, + self.table_name.strip_prefix("$").map_or_else( + || self.table_name.clone(), + |table_name_env| { + env::var(table_name_env).unwrap_or_else(|_| { + println!( + "WARN: Cannot read {} variable and use {} table_name by default", + table_name_env, + default_migrations_table_name() + ); default_migrations_table_name() - ); - default_migrations_table_name() - }) - } else { - self.table_name.clone() - } + }) + }, + ) } pub fn date_format(&self) -> String { @@ -217,7 +228,7 @@ impl MigrationsConfig { pub(crate) const MIGRA_TOML_FILENAME: &str = "Migra.toml"; #[derive(Debug, Serialize, Deserialize)] -pub(crate) struct Config { +pub struct Config { #[serde(skip)] manifest_root: PathBuf, @@ -276,26 +287,4 @@ impl Config { pub fn migration_dir_path(&self) -> PathBuf { self.directory_path().join(self.migrations.directory()) } - - pub fn migrations(&self) -> MigraResult> { - let mut entries = match self.migration_dir_path().read_dir() { - Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()), - entries => entries? - .map(|res| res.map(|e| e.path())) - .collect::, _>>()?, - }; - - if entries.is_empty() { - return Ok(vec![]); - } - - entries.sort(); - - let migrations = entries - .iter() - .filter_map(|path| Migration::new(&path)) - .collect::>(); - - Ok(migrations) - } } diff --git a/migra_cli/src/database.rs b/migra_cli/src/database.rs new file mode 100644 index 0000000..6d71e06 --- /dev/null +++ b/migra_cli/src/database.rs @@ -0,0 +1,70 @@ +use crate::config::SupportedDatabaseClient; +use crate::Config; +#[cfg(feature = "mysql")] +use migra::clients::MysqlClient; +#[cfg(feature = "postgres")] +use migra::clients::PostgresClient; +#[cfg(feature = "sqlite")] +use migra::clients::SqliteClient; +use migra::clients::{AnyClient, OpenDatabaseConnection}; + +pub fn create_client( + client_kind: &SupportedDatabaseClient, + connection_string: &str, + migrations_table_name: &str, +) -> migra::Result { + let client: AnyClient = match client_kind { + #[cfg(feature = "postgres")] + SupportedDatabaseClient::Postgres => Box::new(PostgresClient::manual( + connection_string, + migrations_table_name, + )?), + #[cfg(feature = "mysql")] + SupportedDatabaseClient::Mysql => Box::new(MysqlClient::manual( + connection_string, + migrations_table_name, + )?), + #[cfg(feature = "sqlite")] + SupportedDatabaseClient::Sqlite => Box::new(SqliteClient::manual( + connection_string, + migrations_table_name, + )?), + }; + + Ok(client) +} + +pub fn create_client_from_config(config: &Config) -> migra::StdResult { + create_client( + &config.database.client(), + &config.database.connection_string()?, + &config.migrations.table_name(), + ) + .map_err(From::from) +} + +pub fn run_in_transaction(client: &mut AnyClient, trx_fn: TrxFnMut) -> migra::Result<()> +where + TrxFnMut: FnOnce(&mut AnyClient) -> migra::Result<()>, +{ + client + .begin_transaction() + .and_then(|_| trx_fn(client)) + .and_then(|res| client.commit_transaction().and(Ok(res))) + .or_else(|err| client.rollback_transaction().and(Err(err))) +} + +pub fn should_run_in_transaction( + 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) + } +} diff --git a/migra-cli/src/error.rs b/migra_cli/src/error.rs similarity index 92% rename from migra-cli/src/error.rs rename to migra_cli/src/error.rs index 45c53c7..d266314 100644 --- a/migra-cli/src/error.rs +++ b/migra_cli/src/error.rs @@ -4,7 +4,6 @@ use std::io; use std::mem; use std::result; -pub type StdResult = result::Result>; pub type MigraResult = result::Result; #[derive(Debug)] diff --git a/migra_cli/src/main.rs b/migra_cli/src/main.rs new file mode 100644 index 0000000..804d779 --- /dev/null +++ b/migra_cli/src/main.rs @@ -0,0 +1,32 @@ +#![deny(clippy::all, clippy::pedantic)] +#![forbid(unsafe_code)] + +#[macro_use] +extern crate cfg_if; + +#[cfg(not(any(feature = "postgres", feature = "mysql", feature = "sqlite")))] +compile_error!( + r#"Either features "postgres", "mysql" or "sqlite" must be enabled for "migra-cli" crate"# +); + +mod app; +mod commands; +mod config; +mod database; +mod error; +pub use error::Error; + +mod opts; + +use app::App; +use config::Config; +use opts::{AppOpt, StructOpt}; + +fn main() { + #[cfg(feature = "dotenv")] + dotenv::dotenv().ok(); + + if let Err(err) = App::new(AppOpt::from_args()).run_command() { + panic!("Error: {}", err); + } +} diff --git a/migra-cli/src/opts.rs b/migra_cli/src/opts.rs similarity index 100% rename from migra-cli/src/opts.rs rename to migra_cli/src/opts.rs diff --git a/migra-cli/tests/commands.rs b/migra_cli/tests/commands.rs similarity index 93% rename from migra-cli/tests/commands.rs rename to migra_cli/tests/commands.rs index d43dcef..ea5ed75 100644 --- a/migra-cli/tests/commands.rs +++ b/migra_cli/tests/commands.rs @@ -1,5 +1,6 @@ pub use assert_cmd::prelude::*; pub use cfg_if::cfg_if; +use client_mysql::prelude::*; pub use predicates::str::contains; pub use std::process::Command; @@ -444,7 +445,7 @@ mod upgrade { #[cfg(feature = "postgres")] inner("postgres", || { - let mut conn = postgres::Client::connect(POSTGRES_URL, postgres::NoTls)?; + let mut conn = client_postgres::Client::connect(POSTGRES_URL, client_postgres::NoTls)?; let res = conn.query("SELECT p.id, a.id FROM persons AS p, articles AS a", &[])?; assert_eq!( @@ -459,9 +460,7 @@ mod upgrade { #[cfg(feature = "mysql")] inner("mysql", || { - use mysql::prelude::*; - - let pool = mysql::Pool::new(MYSQL_URL)?; + let pool = client_mysql::Pool::new(MYSQL_URL)?; let mut conn = pool.get_conn()?; let res = conn.query_drop("SELECT p.id, a.id FROM persons AS p, articles AS a")?; @@ -474,9 +473,7 @@ mod upgrade { #[cfg(feature = "sqlite")] remove_sqlite_db().and_then(|_| { inner("sqlite", || { - use rusqlite::Connection; - - let conn = Connection::open(SQLITE_URL)?; + let conn = client_rusqlite::Connection::open(SQLITE_URL)?; let res = conn.execute_batch("SELECT p.id, a.id FROM persons AS p, articles AS a")?; assert_eq!(res, ()); @@ -517,7 +514,7 @@ mod upgrade { #[cfg(feature = "postgres")] inner("postgres_invalid", || { - let mut conn = postgres::Client::connect(POSTGRES_URL, postgres::NoTls)?; + 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", &[]); @@ -530,9 +527,7 @@ mod upgrade { #[cfg(feature = "sqlite")] remove_sqlite_db().and_then(|_| { inner("sqlite_invalid", || { - use rusqlite::Connection; - - let conn = Connection::open(SQLITE_URL)?; + 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"); @@ -569,7 +564,7 @@ mod upgrade { #[cfg(feature = "postgres")] inner("postgres_invalid", || { - let mut conn = postgres::Client::connect(POSTGRES_URL, postgres::NoTls)?; + 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", &[]); @@ -582,9 +577,7 @@ mod upgrade { #[cfg(feature = "sqlite")] remove_sqlite_db().and_then(|_| { inner("sqlite_invalid", || { - use rusqlite::Connection; - - let conn = Connection::open(SQLITE_URL)?; + 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"); @@ -636,7 +629,7 @@ mod apply { "migrations/210218233414_create_persons/up", ], || { - let mut conn = postgres::Client::connect(POSTGRES_URL, postgres::NoTls)?; + let mut conn = client_postgres::Client::connect(POSTGRES_URL, client_postgres::NoTls)?; let res = conn.query("SELECT p.id, a.id FROM persons AS p, articles AS a", &[])?; assert_eq!( @@ -657,7 +650,7 @@ mod apply { "migrations/210218232851_create_articles/down", ], || { - let mut conn = postgres::Client::connect(POSTGRES_URL, postgres::NoTls)?; + let mut conn = client_postgres::Client::connect(POSTGRES_URL, client_postgres::NoTls)?; let res = conn.query("SELECT p.id, a.id FROM persons AS p, articles AS a", &[]); assert!(res.is_err()); @@ -677,9 +670,7 @@ mod apply { "migrations/210218233414_create_persons/up", ], || { - use mysql::prelude::*; - - let pool = mysql::Pool::new(MYSQL_URL)?; + let pool = client_mysql::Pool::new(MYSQL_URL)?; let mut conn = pool.get_conn()?; let res = conn.query_drop("SELECT p.id, a.id FROM persons AS p, articles AS a")?; @@ -697,9 +688,7 @@ mod apply { "migrations/210218232851_create_articles/down", ], || { - use mysql::prelude::*; - - let pool = mysql::Pool::new(MYSQL_URL)?; + let pool = client_mysql::Pool::new(MYSQL_URL)?; let mut conn = pool.get_conn()?; let res = conn.query_drop("SELECT p.id, a.id FROM persons AS p, articles AS a"); @@ -721,9 +710,7 @@ mod apply { "migrations/210218233414_create_persons/up", ], || { - use rusqlite::Connection; - - let conn = Connection::open(SQLITE_URL)?; + let conn = client_rusqlite::Connection::open(SQLITE_URL)?; let res = conn.execute_batch("SELECT p.id, a.id FROM persons AS p, articles AS a")?; assert_eq!(res, ()); @@ -739,9 +726,7 @@ mod apply { "migrations/210218232851_create_articles/down", ], || { - use rusqlite::Connection; - - let conn = Connection::open(SQLITE_URL)?; + let conn = client_rusqlite::Connection::open(SQLITE_URL)?; let res = conn.execute_batch("SELECT p.id, a.id FROM persons AS p, articles AS a"); assert!(res.is_err()); diff --git a/migra-cli/tests/data/Migra_env.toml b/migra_cli/tests/data/Migra_env.toml similarity index 100% rename from migra-cli/tests/data/Migra_env.toml rename to migra_cli/tests/data/Migra_env.toml diff --git a/migra-cli/tests/data/Migra_env_empty.toml b/migra_cli/tests/data/Migra_env_empty.toml similarity index 100% rename from migra-cli/tests/data/Migra_env_empty.toml rename to migra_cli/tests/data/Migra_env_empty.toml diff --git a/migra-cli/tests/data/Migra_mysql.toml b/migra_cli/tests/data/Migra_mysql.toml similarity index 100% rename from migra-cli/tests/data/Migra_mysql.toml rename to migra_cli/tests/data/Migra_mysql.toml diff --git a/migra-cli/tests/data/Migra_postgres.toml b/migra_cli/tests/data/Migra_postgres.toml similarity index 100% rename from migra-cli/tests/data/Migra_postgres.toml rename to migra_cli/tests/data/Migra_postgres.toml diff --git a/migra-cli/tests/data/Migra_postgres_invalid.toml b/migra_cli/tests/data/Migra_postgres_invalid.toml similarity index 100% rename from migra-cli/tests/data/Migra_postgres_invalid.toml rename to migra_cli/tests/data/Migra_postgres_invalid.toml diff --git a/migra-cli/tests/data/Migra_sqlite.toml b/migra_cli/tests/data/Migra_sqlite.toml similarity index 100% rename from migra-cli/tests/data/Migra_sqlite.toml rename to migra_cli/tests/data/Migra_sqlite.toml diff --git a/migra-cli/tests/data/Migra_sqlite_invalid.toml b/migra_cli/tests/data/Migra_sqlite_invalid.toml similarity index 100% rename from migra-cli/tests/data/Migra_sqlite_invalid.toml rename to migra_cli/tests/data/Migra_sqlite_invalid.toml diff --git a/migra-cli/tests/data/Migra_url_empty.toml b/migra_cli/tests/data/Migra_url_empty.toml similarity index 100% rename from migra-cli/tests/data/Migra_url_empty.toml rename to migra_cli/tests/data/Migra_url_empty.toml diff --git a/migra-cli/tests/data/mysql/migrations/210218232851_create_articles/down.sql b/migra_cli/tests/data/mysql/migrations/210218232851_create_articles/down.sql similarity index 100% rename from migra-cli/tests/data/mysql/migrations/210218232851_create_articles/down.sql rename to migra_cli/tests/data/mysql/migrations/210218232851_create_articles/down.sql diff --git a/migra-cli/tests/data/mysql/migrations/210218232851_create_articles/up.sql b/migra_cli/tests/data/mysql/migrations/210218232851_create_articles/up.sql similarity index 100% rename from migra-cli/tests/data/mysql/migrations/210218232851_create_articles/up.sql rename to migra_cli/tests/data/mysql/migrations/210218232851_create_articles/up.sql diff --git a/migra-cli/tests/data/mysql/migrations/210218233414_create_persons/down.sql b/migra_cli/tests/data/mysql/migrations/210218233414_create_persons/down.sql similarity index 100% rename from migra-cli/tests/data/mysql/migrations/210218233414_create_persons/down.sql rename to migra_cli/tests/data/mysql/migrations/210218233414_create_persons/down.sql diff --git a/migra-cli/tests/data/mysql/migrations/210218233414_create_persons/up.sql b/migra_cli/tests/data/mysql/migrations/210218233414_create_persons/up.sql similarity index 100% rename from migra-cli/tests/data/mysql/migrations/210218233414_create_persons/up.sql rename to migra_cli/tests/data/mysql/migrations/210218233414_create_persons/up.sql diff --git a/migra-cli/tests/data/postgres/migrations/210218232851_create_articles/down.sql b/migra_cli/tests/data/postgres/migrations/210218232851_create_articles/down.sql similarity index 100% rename from migra-cli/tests/data/postgres/migrations/210218232851_create_articles/down.sql rename to migra_cli/tests/data/postgres/migrations/210218232851_create_articles/down.sql diff --git a/migra-cli/tests/data/postgres/migrations/210218232851_create_articles/up.sql b/migra_cli/tests/data/postgres/migrations/210218232851_create_articles/up.sql similarity index 100% rename from migra-cli/tests/data/postgres/migrations/210218232851_create_articles/up.sql rename to migra_cli/tests/data/postgres/migrations/210218232851_create_articles/up.sql diff --git a/migra-cli/tests/data/postgres/migrations/210218233414_create_persons/down.sql b/migra_cli/tests/data/postgres/migrations/210218233414_create_persons/down.sql similarity index 100% rename from migra-cli/tests/data/postgres/migrations/210218233414_create_persons/down.sql rename to migra_cli/tests/data/postgres/migrations/210218233414_create_persons/down.sql diff --git a/migra-cli/tests/data/postgres/migrations/210218233414_create_persons/up.sql b/migra_cli/tests/data/postgres/migrations/210218233414_create_persons/up.sql similarity index 100% rename from migra-cli/tests/data/postgres/migrations/210218233414_create_persons/up.sql rename to migra_cli/tests/data/postgres/migrations/210218233414_create_persons/up.sql diff --git a/migra-cli/tests/data/postgres_invalid/migrations/210218232851_create_articles/down.sql b/migra_cli/tests/data/postgres_invalid/migrations/210218232851_create_articles/down.sql similarity index 100% rename from migra-cli/tests/data/postgres_invalid/migrations/210218232851_create_articles/down.sql rename to migra_cli/tests/data/postgres_invalid/migrations/210218232851_create_articles/down.sql diff --git a/migra-cli/tests/data/postgres_invalid/migrations/210218232851_create_articles/up.sql b/migra_cli/tests/data/postgres_invalid/migrations/210218232851_create_articles/up.sql similarity index 100% rename from migra-cli/tests/data/postgres_invalid/migrations/210218232851_create_articles/up.sql rename to migra_cli/tests/data/postgres_invalid/migrations/210218232851_create_articles/up.sql diff --git a/migra-cli/tests/data/postgres_invalid/migrations/210218233414_create_persons/down.sql b/migra_cli/tests/data/postgres_invalid/migrations/210218233414_create_persons/down.sql similarity index 100% rename from migra-cli/tests/data/postgres_invalid/migrations/210218233414_create_persons/down.sql rename to migra_cli/tests/data/postgres_invalid/migrations/210218233414_create_persons/down.sql diff --git a/migra-cli/tests/data/postgres_invalid/migrations/210218233414_create_persons/up.sql b/migra_cli/tests/data/postgres_invalid/migrations/210218233414_create_persons/up.sql similarity index 100% rename from migra-cli/tests/data/postgres_invalid/migrations/210218233414_create_persons/up.sql rename to migra_cli/tests/data/postgres_invalid/migrations/210218233414_create_persons/up.sql diff --git a/migra-cli/tests/data/sqlite/migrations/210218232851_create_articles/down.sql b/migra_cli/tests/data/sqlite/migrations/210218232851_create_articles/down.sql similarity index 100% rename from migra-cli/tests/data/sqlite/migrations/210218232851_create_articles/down.sql rename to migra_cli/tests/data/sqlite/migrations/210218232851_create_articles/down.sql diff --git a/migra-cli/tests/data/sqlite/migrations/210218232851_create_articles/up.sql b/migra_cli/tests/data/sqlite/migrations/210218232851_create_articles/up.sql similarity index 100% rename from migra-cli/tests/data/sqlite/migrations/210218232851_create_articles/up.sql rename to migra_cli/tests/data/sqlite/migrations/210218232851_create_articles/up.sql diff --git a/migra-cli/tests/data/sqlite/migrations/210218233414_create_persons/down.sql b/migra_cli/tests/data/sqlite/migrations/210218233414_create_persons/down.sql similarity index 100% rename from migra-cli/tests/data/sqlite/migrations/210218233414_create_persons/down.sql rename to migra_cli/tests/data/sqlite/migrations/210218233414_create_persons/down.sql diff --git a/migra-cli/tests/data/sqlite/migrations/210218233414_create_persons/up.sql b/migra_cli/tests/data/sqlite/migrations/210218233414_create_persons/up.sql similarity index 100% rename from migra-cli/tests/data/sqlite/migrations/210218233414_create_persons/up.sql rename to migra_cli/tests/data/sqlite/migrations/210218233414_create_persons/up.sql diff --git a/migra-cli/tests/data/sqlite_invalid/migrations/210218232851_create_articles/down.sql b/migra_cli/tests/data/sqlite_invalid/migrations/210218232851_create_articles/down.sql similarity index 100% rename from migra-cli/tests/data/sqlite_invalid/migrations/210218232851_create_articles/down.sql rename to migra_cli/tests/data/sqlite_invalid/migrations/210218232851_create_articles/down.sql diff --git a/migra-cli/tests/data/sqlite_invalid/migrations/210218232851_create_articles/up.sql b/migra_cli/tests/data/sqlite_invalid/migrations/210218232851_create_articles/up.sql similarity index 100% rename from migra-cli/tests/data/sqlite_invalid/migrations/210218232851_create_articles/up.sql rename to migra_cli/tests/data/sqlite_invalid/migrations/210218232851_create_articles/up.sql diff --git a/migra-cli/tests/data/sqlite_invalid/migrations/210218233414_create_persons/down.sql b/migra_cli/tests/data/sqlite_invalid/migrations/210218233414_create_persons/down.sql similarity index 100% rename from migra-cli/tests/data/sqlite_invalid/migrations/210218233414_create_persons/down.sql rename to migra_cli/tests/data/sqlite_invalid/migrations/210218233414_create_persons/down.sql diff --git a/migra-cli/tests/data/sqlite_invalid/migrations/210218233414_create_persons/up.sql b/migra_cli/tests/data/sqlite_invalid/migrations/210218233414_create_persons/up.sql similarity index 100% rename from migra-cli/tests/data/sqlite_invalid/migrations/210218233414_create_persons/up.sql rename to migra_cli/tests/data/sqlite_invalid/migrations/210218233414_create_persons/up.sql