diff --git a/Cargo.toml b/Cargo.toml index ea59d56..020de8e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,5 @@ [workspace] members = [ "migra", - "migra_clients", - # "migra_cli" + "migra_cli", ] \ No newline at end of file diff --git a/migra/Cargo.toml b/migra/Cargo.toml index ccb49a8..2346e80 100644 --- a/migra/Cargo.toml +++ b/migra/Cargo.toml @@ -6,4 +6,11 @@ edition = "2018" # 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/src/clients/mod.rs b/migra/src/clients/mod.rs new file mode 100644 index 0000000..7a08f94 --- /dev/null +++ b/migra/src/clients/mod.rs @@ -0,0 +1,37 @@ +// #![deny(missing_debug_implementations)] +// #![deny(clippy::all, clippy::pedantic)] +// #![allow(clippy::module_name_repetitions)] +// #![allow(clippy::missing_errors_doc)] + +use crate::error::MigraResult; +use crate::managers::{ManageMigrations, ManageTransaction}; + +pub trait OpenDatabaseConnection +where + Self: Sized, +{ + fn new(connection_string: &str) -> MigraResult { + Self::manual(connection_string, "migrations") + } + + fn manual(connection_string: &str, migrations_table_name: &str) -> MigraResult; +} + +pub trait Client: ManageMigrations + ManageTransaction {} + +pub type AnyClient = Box; + +#[cfg(feature = "postgres")] +pub mod postgres; +#[cfg(feature = "postgres")] +pub use self::postgres::Client as PostgresClient; + +#[cfg(feature = "mysql")] +pub mod mysql; +#[cfg(feature = "mysql")] +pub use self::mysql::Client as MysqlClient; + +#[cfg(feature = "sqlite")] +pub mod sqlite; +#[cfg(feature = "sqlite")] +pub use self::sqlite::Client as SqliteClient; diff --git a/migra_clients/src/mysql.rs b/migra/src/clients/mysql.rs similarity index 58% rename from migra_clients/src/mysql.rs rename to migra/src/clients/mysql.rs index d3749fd..f2164ab 100644 --- a/migra_clients/src/mysql.rs +++ b/migra/src/clients/mysql.rs @@ -1,38 +1,39 @@ -use crate::OpenDatabaseConnection; -use migra::managers::{BatchExecute, ManageMigrations, ManageTransaction}; -use migra::migration; +use super::OpenDatabaseConnection; +use crate::error::{Error, MigraResult, StdResult}; +use crate::managers::{BatchExecute, ManageMigrations, ManageTransaction}; +use crate::migration; use mysql::prelude::*; use mysql::{Pool, PooledConn}; #[derive(Debug)] -pub struct MySqlClient { +pub struct Client { conn: PooledConn, migrations_table_name: String, } -impl OpenDatabaseConnection for MySqlClient { - fn manual(connection_string: &str, migrations_table_name: &str) -> migra::Result { +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(|_| migra::Error::FailedDatabaseConnection)?; + .map_err(|_| Error::FailedDatabaseConnection)?; - Ok(MySqlClient { + Ok(Client { conn, migrations_table_name: migrations_table_name.to_owned(), }) } } -impl BatchExecute for MySqlClient { - fn batch_execute(&mut self, sql: &str) -> migra::StdResult<()> { +impl BatchExecute for Client { + fn batch_execute(&mut self, sql: &str) -> StdResult<()> { self.conn.query_drop(sql).map_err(From::from) } } -impl ManageTransaction for MySqlClient {} +impl ManageTransaction for Client {} -impl ManageMigrations for MySqlClient { - fn create_migrations_table(&mut self) -> migra::Result<()> { +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, @@ -42,10 +43,10 @@ impl ManageMigrations for MySqlClient { ); self.batch_execute(&stmt) - .map_err(|_| migra::Error::FailedCreateMigrationsTable) + .map_err(|_| Error::FailedCreateMigrationsTable) } - fn insert_migration(&mut self, name: &str) -> migra::Result { + fn insert_migration(&mut self, name: &str) -> MigraResult { let stmt = format!( "INSERT INTO {} (name) VALUES ($1)", &self.migrations_table_name @@ -54,10 +55,10 @@ impl ManageMigrations for MySqlClient { self.conn .exec_first(&stmt, (name,)) .map(Option::unwrap_or_default) - .map_err(|_| migra::Error::FailedInsertMigration) + .map_err(|_| Error::FailedInsertMigration) } - fn delete_migration(&mut self, name: &str) -> migra::Result { + fn delete_migration(&mut self, name: &str) -> MigraResult { let stmt = format!( "DELETE FROM {} WHERE name = $1", &self.migrations_table_name @@ -66,15 +67,17 @@ impl ManageMigrations for MySqlClient { self.conn .exec_first(&stmt, (name,)) .map(Option::unwrap_or_default) - .map_err(|_| migra::Error::FailedDeleteMigration) + .map_err(|_| Error::FailedDeleteMigration) } - fn applied_migrations(&mut self) -> migra::Result { + fn applied_migrations(&mut self) -> MigraResult { let stmt = format!("SELECT name FROM {}", &self.migrations_table_name); self.conn .query::(stmt) .map(From::from) - .map_err(|_| migra::Error::FailedGetAppliedMigrations) + .map_err(|_| Error::FailedGetAppliedMigrations) } } + +impl super::Client for Client {} diff --git a/migra_clients/src/postgres.rs b/migra/src/clients/postgres.rs similarity index 54% rename from migra_clients/src/postgres.rs rename to migra/src/clients/postgres.rs index f355c7f..96b16ab 100644 --- a/migra_clients/src/postgres.rs +++ b/migra/src/clients/postgres.rs @@ -1,43 +1,44 @@ -use crate::OpenDatabaseConnection; -use migra::managers::{BatchExecute, ManageMigrations, ManageTransaction}; -use migra::migration; -use postgres::{Client, NoTls}; +use super::OpenDatabaseConnection; +use crate::error::{Error, MigraResult, StdResult}; +use crate::managers::{BatchExecute, ManageMigrations, ManageTransaction}; +use crate::migration; +use postgres::{Client as PostgresClient, NoTls}; use std::fmt; -pub struct PostgresClient { - client: Client, +pub struct Client { + client: PostgresClient, migrations_table_name: String, } -impl fmt::Debug for PostgresClient { +impl fmt::Debug for Client { fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt.debug_struct("PostgresClient") + fmt.debug_struct("Client") .field("migrations_table_name", &self.migrations_table_name) .finish() } } -impl OpenDatabaseConnection for PostgresClient { - fn manual(connection_string: &str, migrations_table_name: &str) -> migra::Result { - let client = Client::connect(connection_string, NoTls) - .map_err(|_| migra::Error::FailedDatabaseConnection)?; - Ok(PostgresClient { +impl OpenDatabaseConnection for Client { + fn manual(connection_string: &str, migrations_table_name: &str) -> MigraResult { + let client = PostgresClient::connect(connection_string, NoTls) + .map_err(|_| Error::FailedDatabaseConnection)?; + Ok(Client { client, migrations_table_name: migrations_table_name.to_owned(), }) } } -impl BatchExecute for PostgresClient { - fn batch_execute(&mut self, sql: &str) -> migra::StdResult<()> { +impl BatchExecute for Client { + fn batch_execute(&mut self, sql: &str) -> StdResult<()> { self.client.batch_execute(sql).map_err(From::from) } } -impl ManageTransaction for PostgresClient {} +impl ManageTransaction for Client {} -impl ManageMigrations for PostgresClient { - fn create_migrations_table(&mut self) -> migra::Result<()> { +impl ManageMigrations for Client { + fn create_migrations_table(&mut self) -> MigraResult<()> { let stmt = format!( r#"CREATE TABLE IF NOT EXISTS {} ( id serial PRIMARY KEY, @@ -47,10 +48,10 @@ impl ManageMigrations for PostgresClient { ); self.batch_execute(&stmt) - .map_err(|_| migra::Error::FailedCreateMigrationsTable) + .map_err(|_| Error::FailedCreateMigrationsTable) } - fn insert_migration(&mut self, name: &str) -> migra::Result { + fn insert_migration(&mut self, name: &str) -> MigraResult { let stmt = format!( "INSERT INTO {} (name) VALUES ($1)", &self.migrations_table_name @@ -58,10 +59,10 @@ impl ManageMigrations for PostgresClient { self.client .execute(stmt.as_str(), &[&name]) - .map_err(|_| migra::Error::FailedInsertMigration) + .map_err(|_| Error::FailedInsertMigration) } - fn delete_migration(&mut self, name: &str) -> migra::Result { + fn delete_migration(&mut self, name: &str) -> MigraResult { let stmt = format!( "DELETE FROM {} WHERE name = $1", &self.migrations_table_name @@ -69,10 +70,10 @@ impl ManageMigrations for PostgresClient { self.client .execute(stmt.as_str(), &[&name]) - .map_err(|_| migra::Error::FailedDeleteMigration) + .map_err(|_| Error::FailedDeleteMigration) } - fn applied_migrations(&mut self) -> migra::Result { + fn applied_migrations(&mut self) -> MigraResult { let stmt = format!("SELECT name FROM {}", &self.migrations_table_name); self.client @@ -83,6 +84,8 @@ impl ManageMigrations for PostgresClient { .collect::, _>>() }) .map(From::from) - .map_err(|_| migra::Error::FailedGetAppliedMigrations) + .map_err(|_| Error::FailedGetAppliedMigrations) } } + +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..a7a9969 --- /dev/null +++ b/migra/src/clients/sqlite.rs @@ -0,0 +1,84 @@ +use super::OpenDatabaseConnection; +use crate::error::{Error, MigraResult, StdResult}; +use crate::managers::{BatchExecute, ManageMigrations, ManageTransaction}; +use crate::migration; +use rusqlite::Connection; + +#[derive(Debug)] +pub struct Client { + conn: Connection, + migrations_table_name: String, +} + +impl OpenDatabaseConnection for Client { + fn manual(connection_string: &str, migrations_table_name: &str) -> MigraResult { + let conn = + Connection::open(connection_string).map_err(|_| Error::FailedDatabaseConnection)?; + 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(|_| Error::FailedCreateMigrationsTable) + } + + 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(|_| Error::FailedInsertMigration) + } + + 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(|_| Error::FailedDeleteMigration) + } + + fn applied_migrations(&mut self) -> MigraResult { + let stmt = format!("SELECT name FROM {}", &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(|_| Error::FailedGetAppliedMigrations) + } +} + +impl super::Client for Client {} diff --git a/migra/src/fs.rs b/migra/src/fs.rs index 82d4e56..790974f 100644 --- a/migra/src/fs.rs +++ b/migra/src/fs.rs @@ -33,3 +33,15 @@ pub fn get_all_migrations(dir_path: &Path) -> MigraResult { Ok(migration::List::from(file_names)) } + +#[must_use] +pub fn filter_pending_migrations( + all_migrations: &migration::List, + applied_migrations: &migration::List, +) -> migration::List { + all_migrations + .clone() + .iter() + .filter(|m| !applied_migrations.contains(m)) + .collect() +} diff --git a/migra/src/lib.rs b/migra/src/lib.rs index 86cc5dc..c3a65d9 100644 --- a/migra/src/lib.rs +++ b/migra/src/lib.rs @@ -2,6 +2,8 @@ #![deny(clippy::all, clippy::pedantic)] #![allow(clippy::missing_errors_doc)] +pub mod clients; + pub mod fs; pub mod managers; pub mod migration; @@ -10,31 +12,3 @@ mod error; pub use error::{Error, MigraResult as Result, StdResult}; pub use migration::Migration; - -/* - -# list - -fs::get_all_migrations() -db::get_applied_migrations() -utils::filter_pending_migrations(all_migrations, applied_migrations) -show_migrations(applied_migrations) -show_migrations(pending_migrations) - - -# upgrade - -fs::get_all_migrations() -db::get_applied_migrations() -utils::filter_pending_migrations(all_migrations, applied_migrations) - -db::upgrade_migration() - - - -# downgrade - - - - -*/ diff --git a/migra/src/migration.rs b/migra/src/migration.rs index ed1dc2e..d35fb80 100644 --- a/migra/src/migration.rs +++ b/migra/src/migration.rs @@ -52,6 +52,36 @@ impl> From> for List { } } +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; @@ -112,30 +142,6 @@ impl 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 - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/migra_cli/Cargo.toml b/migra_cli/Cargo.toml index b0223b3..932f81e 100644 --- a/migra_cli/Cargo.toml +++ b/migra_cli/Cargo.toml @@ -15,13 +15,12 @@ readme = "../README.md" [features] default = ["postgres"] -postgres = ["migra-clients/postgres"] -sqlite = ["migra-clients/sqlite"] -mysql = ["migra-clients/mysql"] +postgres = ["migra/postgres"] +sqlite = ["migra/sqlite"] +mysql = ["migra/mysql"] [dependencies] migra = { version = "0", path = "../migra" } -migra-clients = { version = "0", path = "../migra_clients" } cfg-if = "1.0" structopt = "0.3" serde = { version = "1.0", features = ["derive"] } diff --git a/migra_cli/src/app.rs b/migra_cli/src/app.rs index 7610ddc..8386e8a 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, StdResult}; use crate::opts::Command; use crate::AppOpt; use crate::Config; @@ -29,19 +29,19 @@ impl App { 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/client.rs b/migra_cli/src/client.rs new file mode 100644 index 0000000..fa87919 --- /dev/null +++ b/migra_cli/src/client.rs @@ -0,0 +1,53 @@ +use crate::config::SupportedDatabaseClient; +#[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_kind: &SupportedDatabaseClient, + connection_string: &str, +) -> migra::Result { + let client: AnyClient = match client_kind { + #[cfg(feature = "postgres")] + SupportedDatabaseClient::Postgres => Box::new(PostgresClient::new(&connection_string)?), + #[cfg(feature = "mysql")] + SupportedDatabaseClient::Mysql => Box::new(MysqlClient::new(&connection_string)?), + #[cfg(feature = "sqlite")] + SupportedDatabaseClient::Sqlite => Box::new(SqliteClient::new(&connection_string)?), + }; + + Ok(client) +} + +pub fn with_transaction( + client: &mut AnyClient, + trx_fn: &mut TrxFnMut, +) -> migra::Result +where + TrxFnMut: FnMut(&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 maybe_with_transaction( + is_needed: bool, + client: &mut AnyClient, + trx_fn: &mut TrxFnMut, +) -> migra::Result +where + TrxFnMut: FnMut(&mut AnyClient) -> migra::Result, +{ + if is_needed { + with_transaction(client, trx_fn) + } else { + trx_fn(client) + } +} diff --git a/migra_cli/src/commands/apply.rs b/migra_cli/src/commands/apply.rs index 0a78448..6364974 100644 --- a/migra_cli/src/commands/apply.rs +++ b/migra_cli/src/commands/apply.rs @@ -1,16 +1,14 @@ use crate::app::App; -use crate::database::prelude::*; -use crate::database::transaction::maybe_with_transaction; -use crate::database::{DatabaseConnectionManager, MigrationManager}; +use crate::client::maybe_with_transaction; 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) -> 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 = crate::client::create( + &config.database.client(), + &config.database.connection_string()?, + )?; let file_contents = cmd_opts .file_paths @@ -28,15 +26,15 @@ pub(crate) fn apply_sql(app: &App, cmd_opts: ApplyCommandOpt) -> StdResult<()> { maybe_with_transaction( cmd_opts.transaction_opts.single_transaction, - conn, - &mut |conn| { + &mut client, + &mut |mut client| { file_contents .iter() .try_for_each(|content| { maybe_with_transaction( !cmd_opts.transaction_opts.single_transaction, - conn, - &mut |conn| migration_manager.apply_sql(conn, content), + &mut client, + &mut |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 index 4b9c697..0eff571 100644 --- a/migra_cli/src/commands/downgrade.rs +++ b/migra_cli/src/commands/downgrade.rs @@ -1,19 +1,19 @@ use crate::app::App; -use crate::database::prelude::*; -use crate::database::transaction::maybe_with_transaction; -use crate::database::{DatabaseConnectionManager, MigrationManager}; +use crate::client; +use crate::client::maybe_with_transaction; use crate::opts::DowngradeCommandOpt; use crate::StdResult; use std::cmp; -pub(crate) fn rollback_applied_migrations(app: &App, opts: DowngradeCommandOpt) -> StdResult<()> { +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 mut client = client::create( + &config.database.client(), + &config.database.connection_string()?, + )?; - let applied_migrations = migration_manager.applied_migration_names(conn)?; - let migrations = config.migrations()?; + let applied_migrations = client.applied_migrations()?; + let all_migrations = migra::fs::get_all_migrations(&config.migration_dir_path())?; let rollback_migrations_number = if opts.all_migrations { applied_migrations.len() @@ -23,18 +23,17 @@ pub(crate) fn rollback_applied_migrations(app: &App, opts: DowngradeCommandOpt) maybe_with_transaction( opts.transaction_opts.single_transaction, - conn, - &mut |conn| { + &mut client, + &mut |mut client| { 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()); + .try_for_each(|applied_migration| { + if all_migrations.contains(applied_migration) { + println!("downgrade {}...", applied_migration.name()); maybe_with_transaction( !opts.transaction_opts.single_transaction, - conn, - &mut |conn| migration_manager.downgrade(conn, &migration), + &mut client, + &mut |client| client.apply_downgrade_migration(&applied_migration), ) } else { Ok(()) diff --git a/migra_cli/src/commands/init.rs b/migra_cli/src/commands/init.rs index b8e7452..cd2c4da 100644 --- a/migra_cli/src/commands/init.rs +++ b/migra_cli/src/commands/init.rs @@ -4,18 +4,17 @@ 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| { + 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 index 880feb6..9d9f80a 100644 --- a/migra_cli/src/commands/list.rs +++ b/migra_cli/src/commands/list.rs @@ -1,65 +1,60 @@ use crate::app::App; -use crate::database::migration::filter_pending_migrations; -use crate::database::prelude::*; -use crate::database::{DatabaseConnectionManager, Migration, MigrationManager}; +use crate::client; use crate::error::{Error, StdResult}; +use migra::migration; 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() { + let applied_migrations = 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 mut client = client::create(&config.database.client(), database_connection_string)?; + let applied_migrations = client.applied_migrations()?; - let migration_manager = MigrationManager::from(&config); - let applied_migration_names = migration_manager.applied_migration_names(conn)?; + show_applied_migrations(&applied_migrations); - show_applied_migrations(&applied_migration_names); - - applied_migration_names + applied_migrations } Err(e) if e == Error::MissedEnvVar(String::new()) => { eprintln!("WARNING: {}", e); eprintln!("WARNING: No connection to database"); - Vec::new() + migration::List::new() } Err(e) => panic!("{}", e), }; println!(); + let all_migrations = migra::fs::get_all_migrations(&config.migration_dir_path())?; let pending_migrations = - filter_pending_migrations(config.migrations()?, &applied_migration_names); + migra::fs::filter_pending_migrations(&all_migrations, &applied_migrations); + show_pending_migrations(&pending_migrations); Ok(()) } -fn show_applied_migrations(applied_migration_names: &[String]) { +fn show_applied_migrations(applied_migrations: &migration::List) { println!("Applied migrations:"); - if applied_migration_names.is_empty() { + if applied_migrations.is_empty() { println!("{}", EM_DASH); } else { - applied_migration_names + applied_migrations .iter() .rev() - .for_each(|name| println!("{}", name)); + .for_each(|migration| println!("{}", migration.name())); } } -fn show_pending_migrations(pending_migrations: &[Migration]) { +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(|m| { - println!("{}", m.name()); + 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 index 96f3ca6..ec12675 100644 --- a/migra_cli/src/commands/make.rs +++ b/migra_cli/src/commands/make.rs @@ -4,7 +4,7 @@ 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) -> 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/upgrade.rs b/migra_cli/src/commands/upgrade.rs index b107c9d..65bdfef 100644 --- a/migra_cli/src/commands/upgrade.rs +++ b/migra_cli/src/commands/upgrade.rs @@ -1,57 +1,60 @@ use crate::app::App; -use crate::database::migration::*; -use crate::database::transaction::maybe_with_transaction; -use crate::database::DatabaseConnectionManager; +use crate::client; +use crate::client::maybe_with_transaction; use crate::opts::UpgradeCommandOpt; use crate::StdResult; +use migra::migration; -pub(crate) fn upgrade_pending_migrations(app: &App, opts: UpgradeCommandOpt) -> 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 mut client = client::create( + &config.database.client(), + &config.database.connection_string()?, + )?; - let migration_manager = MigrationManager::from(&config); + let applied_migration_names = client.applied_migrations()?; + let all_migrations = migra::fs::get_all_migrations(&config.migration_dir_path())?; - let applied_migration_names = migration_manager.applied_migration_names(conn)?; - let migrations = config.migrations()?; - - let pending_migrations = filter_pending_migrations(migrations, &applied_migration_names); + let pending_migrations = + migra::fs::filter_pending_migrations(&all_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 + 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); - match target_migration { - Some(migration) => vec![migration], - None => { - eprintln!(r#"Cannot find migration with "{}" name"#, migration_name); - return Ok(()); - } + if let Some(migration) = target_migration.clone() { + 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() + pending_migrations[..upgrade_migrations_number] + .to_vec() + .into() }; maybe_with_transaction( opts.transaction_opts.single_transaction, - conn, - &mut |conn| { + &mut client, + &mut |mut client| { 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), + &mut client, + &mut |client| client.apply_upgrade_migration(migration), ) }) .map_err(From::from) @@ -61,6 +64,6 @@ pub(crate) fn upgrade_pending_migrations(app: &App, opts: UpgradeCommandOpt) -> Ok(()) } -fn print_migration_info(migration: &Migration) { +fn print_migration_info(migration: &migra::Migration) { println!("upgrade {}...", migration.name()); } diff --git a/migra_cli/src/config.rs b/migra_cli/src/config.rs index 7a18243..feb1e1a 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 { @@ -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/connection.rs b/migra_cli/src/database/connection.rs index d1acdb8..8b13789 100644 --- a/migra_cli/src/database/connection.rs +++ b/migra_cli/src/database/connection.rs @@ -1,64 +1 @@ -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 index d2ad50d..1498b99 100644 --- a/migra_cli/src/database/migration.rs +++ b/migra_cli/src/database/migration.rs @@ -175,13 +175,3 @@ impl ManageMigration for MigrationManager { 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 index 2510dab..4f719c4 100644 --- a/migra_cli/src/database/mod.rs +++ b/migra_cli/src/database/mod.rs @@ -1,19 +1,3 @@ -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}; +// pub(crate) mod migration; +// pub(crate) mod transaction; diff --git a/migra_cli/src/database/transaction.rs b/migra_cli/src/database/transaction.rs index a5945c4..3a6e507 100644 --- a/migra_cli/src/database/transaction.rs +++ b/migra_cli/src/database/transaction.rs @@ -31,33 +31,3 @@ impl ManageTransaction for TransactionManager { 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 index 026d663..263a108 100644 --- a/migra_cli/src/main.rs +++ b/migra_cli/src/main.rs @@ -1,4 +1,4 @@ -#![deny(clippy::all)] +#![deny(clippy::all, clippy::pedantic)] #![forbid(unsafe_code)] #[macro_use] @@ -8,9 +8,9 @@ extern crate cfg_if; compile_error!(r#"Either features "postgres" or "mysql" must be enabled for "migra" crate"#); mod app; +mod client; mod commands; mod config; -mod database; mod error; pub use error::Error; diff --git a/migra_clients/Cargo.toml b/migra_clients/Cargo.toml deleted file mode 100644 index 28a736d..0000000 --- a/migra_clients/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "migra-clients" -version = "0.1.0" -authors = ["Dmitriy Pleshevskiy "] -edition = "2018" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[features] -default = ["postgres"] -sqlite = ["rusqlite"] - -[dependencies] -migra = { version = "0", path = "../migra" } -postgres = { version = "0.19", optional = true } -mysql = { version = "20.1", optional = true } -rusqlite = { version = "0.25", optional = true } diff --git a/migra_clients/src/lib.rs b/migra_clients/src/lib.rs deleted file mode 100644 index 48e0051..0000000 --- a/migra_clients/src/lib.rs +++ /dev/null @@ -1,27 +0,0 @@ -#![deny(missing_debug_implementations)] -#![deny(clippy::all, clippy::pedantic)] -#![allow(clippy::module_name_repetitions)] -#![allow(clippy::missing_errors_doc)] - -trait OpenDatabaseConnection: Sized { - fn new(connection_string: &str) -> migra::Result { - Self::manual(connection_string, "migrations") - } - - fn manual(connection_string: &str, migrations_table_name: &str) -> migra::Result; -} - -#[cfg(feature = "postgres")] -mod postgres; -#[cfg(feature = "postgres")] -pub use self::postgres::*; - -#[cfg(feature = "mysql")] -mod mysql; -#[cfg(feature = "mysql")] -pub use self::mysql::*; - -#[cfg(feature = "sqlite")] -mod sqlite; -#[cfg(feature = "sqlite")] -pub use self::sqlite::*; diff --git a/migra_clients/src/sqlite.rs b/migra_clients/src/sqlite.rs deleted file mode 100644 index ea4abe7..0000000 --- a/migra_clients/src/sqlite.rs +++ /dev/null @@ -1,107 +0,0 @@ -use crate::OpenDatabaseConnection; -use migra::managers::{BatchExecute, ManageMigrations, ManageTransaction}; -use migra::migration; -use rusqlite::Connection; - -#[derive(Debug)] -pub struct SqliteClient { - conn: Connection, - migrations_table_name: String, -} - -impl OpenDatabaseConnection for SqliteClient { - fn manual(connection_string: &str, migrations_table_name: &str) -> migra::Result { - let conn = Connection::open(connection_string) - .map_err(|_| migra::Error::FailedDatabaseConnection)?; - Ok(SqliteClient { - conn, - migrations_table_name: migrations_table_name.to_owned(), - }) - } -} - -impl BatchExecute for SqliteClient { - fn batch_execute(&mut self, sql: &str) -> migra::StdResult<()> { - self.conn.execute_batch(sql).map_err(From::from) - } -} - -impl ManageTransaction for SqliteClient {} - -impl ManageMigrations for SqliteClient { - fn create_migrations_table(&mut self) -> migra::Result<()> { - 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(|_| migra::Error::FailedCreateMigrationsTable) - } - - fn insert_migration(&mut self, name: &str) -> migra::Result { - let stmt = format!( - "INSERT INTO {} (name) VALUES ($1)", - &self.migrations_table_name - ); - - self.conn - .execute(&stmt, [name]) - .map(|res| res as u64) - .map_err(|_| migra::Error::FailedInsertMigration) - } - - fn delete_migration(&mut self, name: &str) -> migra::Result { - let stmt = format!( - "DELETE FROM {} WHERE name = $1", - &self.migrations_table_name - ); - - self.conn - .execute(&stmt, [name]) - .map(|res| res as u64) - .map_err(|_| migra::Error::FailedDeleteMigration) - } - - fn applied_migrations(&mut self) -> migra::Result { - let stmt = format!("SELECT name FROM {}", &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(|_| migra::Error::FailedGetAppliedMigrations) - } -} - -// 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) -// } -// }