diff --git a/.changelog-builder-config.json b/.changelog-builder-config.json deleted file mode 100644 index 60b9aaa..0000000 --- a/.changelog-builder-config.json +++ /dev/null @@ -1,149 +0,0 @@ -{ - "categories": [ - { - "title": "## 🚀 Autodeployment", - "labels": [ - "area: autodeployment" - ] - }, - { - "title": "## 🛠 Back-end", - "labels": [ - "area: back-end" - ] - }, - { - "title": "## ⚡ CI/CD", - "labels": [ - "area: CI/CD" - ] - }, - { - "title": "## 🎨 CSS Styling", - "labels": [ - "area: css styling" - ] - }, - { - "title": "## 🚀 Deployment", - "labels": [ - "area: deployment" - ] - }, - { - "title": "## 🕵️‍♂️ Design", - "labels": [ - "area: design" - ] - }, - { - "title": "## 🐳 Docker", - "labels": [ - "area: docker" - ] - }, - { - "title": "## 🔎 Elasticsearch", - "labels": [ - "area: elasticsearch" - ] - }, - { - "title": "## 🎭 Front-end", - "labels": [ - "area: front-end" - ] - }, - { - "title": "## ⚙ Microservices", - "labels": [ - "area: microservices" - ] - }, - { - "title": "## 🏎 Performance", - "labels": [ - "area: performance" - ] - }, - { - "title": "## 📲 Progressive Web Apps", - "labels": [ - "area: PWA" - ] - }, - { - "title": "## 🧰 Tools", - "labels": [ - "area: tools" - ] - }, - { - "title": "## 🖇 Dependencies", - "labels": [ - "dependencies" - ] - }, - { - "title": "## 👨‍💻 Developer Experience", - "labels": [ - "developer experience" - ] - }, - { - "title": "## 🐛 Bug Fixes", - "labels": [ - "type: bug" - ] - }, - { - "title": "## 📃 Documentation Updates", - "labels": [ - "type: documentation (docs)" - ] - }, - { - "title": "## ✨ Enhancements", - "labels": [ - "type: enhancement" - ] - }, - { - "title": "## 💡 Nice to Have", - "labels": [ - "type: nice to have" - ] - }, - { - "title": "## 🔐 Security Enhancements", - "labels": [ - "type: security" - ] - }, - { - "title": "## 🛡 Tests", - "labels": [ - "type: test" - ] - } - ], - "ignore_labels": [ - "ignore" - ], - "sort": "ASC", - "template": "${{CATEGORIZED_COUNT}} changes since ${{FROM_TAG}}\n\n${{CHANGELOG}}\n\n## Other Updates\n\n${{UNCATEGORIZED}}\n", - "pr_template": "- ${{TITLE}} (#${{NUMBER}})", - "empty_template": "No Changes", - "label_extractor": [ - { - "pattern": "(.) (.+)", - "target": "$1" - } - ], - "max_tags_to_fetch": 200, - "max_pull_requests": 200, - "max_back_track_time_days": 90, - "tag_resolver": { - "method": "semver" - } -} diff --git a/.gitignore b/.gitignore index 920c3b1..47f9914 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ **/*.rs.bk .env .env.midas +.migration-state diff --git a/Cargo.lock b/Cargo.lock index 395b088..4c421e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1364,7 +1364,7 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "midas" -version = "0.6.8" +version = "0.7.0" dependencies = [ "anyhow", "clap", @@ -1374,11 +1374,15 @@ dependencies = [ "indoc", "log", "mysql", + "nom", "openssl", "postgres", "prettytable-rs", + "rand", "regex", "rusqlite", + "serde", + "serde_yaml", "tempfile", "thiserror 2.0.3", "tracing", @@ -2171,6 +2175,19 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "sha1" version = "0.10.6" @@ -2678,6 +2695,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "url" version = "2.5.3" diff --git a/Cargo.toml b/Cargo.toml index 416d716..bf6f4f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "midas" -version = "0.6.8" -authors = ["Edward Fitz Abucay gh:@ffimnsr"] +version = "0.7.0" +authors = ["Edward Fitz Abucay "] edition = "2021" readme = "README.md" description = "Do painless migration 🦀" @@ -44,6 +44,10 @@ thiserror = "2.0.3" anyhow = "1.0.93" indicatif = "0.17.9" indicatif-log-bridge = "0.2.3" +nom = "7.1.3" +serde = { version = "1.0.215", features = ["derive"] } +serde_yaml = "0.9.34" +rand = "0.8.5" [dev-dependencies] tempfile = "3.12" diff --git a/midas-rs.code-workspace b/midas-rs.code-workspace index 876a149..8108218 100644 --- a/midas-rs.code-workspace +++ b/midas-rs.code-workspace @@ -4,5 +4,23 @@ "path": "." } ], - "settings": {} -} \ No newline at end of file + "settings": { + "search.exclude": { + "**/node_modules": true, + "**/bower_components": true, + "**/*.code-search": true, + "packages": true + }, + "files.exclude": { + "**/.git": true, + "**/.svn": true, + "**/.hg": true, + "**/CVS": true, + "**/.DS_Store": true, + "**/Thumbs.db": true, + "**/.trigger": true, + "target": true, + "packages": true + } + } +} diff --git a/migrations/1567785996234_init_startup.sql b/migrations/1567785996234_init_startup.sql index 0e3326d..a18f8db 100644 --- a/migrations/1567785996234_init_startup.sql +++ b/migrations/1567785996234_init_startup.sql @@ -8,7 +8,13 @@ create extension if not exists pgcrypto; -- https://docs.postgrest.org/en/v12/explanations/db_authz.html#functions alter default privileges in schema public revoke execute on functions from PUBLIC; +do $$ +begin create role sesame_su_startup_data nologin; +exception when duplicate_object then raise notice '%, skipping', sqlerrm using errcode = sqlstate; +end +$$; + grant all privileges on database startup to sesame_su_startup_data; grant all privileges on schema public to sesame_su_startup_data; grant all privileges on all tables in schema public to sesame_su_startup_data; @@ -19,7 +25,13 @@ alter default privileges in schema public grant all on sequences to sesame_su_st alter default privileges in schema public grant all on functions to sesame_su_startup_data; alter default privileges in schema public grant all on types to sesame_su_startup_data; +do $$ +begin create role sesame_read_startup_data nologin; +exception when duplicate_object then raise notice '%, skipping', sqlerrm using errcode = sqlstate; +end +$$; + grant usage on schema public to sesame_read_startup_data; grant select on all tables in schema public to sesame_read_startup_data; grant usage, select on all sequences in schema public to sesame_read_startup_data; @@ -28,7 +40,13 @@ alter default privileges in schema public grant select on tables to sesame_read_ alter default privileges in schema public grant usage, select on sequences to sesame_read_startup_data; alter default privileges in schema public grant execute on functions to sesame_read_startup_data; +do $$ +begin create role sesame_write_startup_data nologin; +exception when duplicate_object then raise notice '%, skipping', sqlerrm using errcode = sqlstate; +end +$$; + grant usage on schema public to sesame_write_startup_data; grant insert, update, delete on all tables in schema public to sesame_write_startup_data; grant usage, update on all sequences in schema public to sesame_write_startup_data; @@ -37,11 +55,23 @@ alter default privileges in schema public grant insert, update, delete on tables alter default privileges in schema public grant usage, update on sequences to sesame_write_startup_data; alter default privileges in schema public grant execute on functions to sesame_write_startup_data; +do $$ +begin create role webuser nologin; +exception when duplicate_object then raise notice '%, skipping', sqlerrm using errcode = sqlstate; +end +$$; + grant sesame_read_startup_data to webuser; grant sesame_write_startup_data to webuser; +do $$ +begin create role anon nologin; +exception when duplicate_object then raise notice '%, skipping', sqlerrm using errcode = sqlstate; +end +$$; + grant sesame_read_startup_data to anon; create or replace function get_user_uid() diff --git a/migrations/1732119785927_alter_type_salary_detail.sql b/migrations/1732119785927_alter_type_salary_detail.sql index ddfc382..6ce3c1c 100644 --- a/migrations/1732119785927_alter_type_salary_detail.sql +++ b/migrations/1732119785927_alter_type_salary_detail.sql @@ -16,6 +16,7 @@ alter type new_salary_detail rename to salary_detail; alter table jobs alter column salary set default row('5', '10', 'USD', 'hourly')::salary_detail; update jobs set salary = row('5', '10', 'USD', 'hourly')::salary_detail where salary is null; + -- !DOWN -- No down migration for this migration diff --git a/src/commander.rs b/src/commander.rs index 0ba428e..0785183 100644 --- a/src/commander.rs +++ b/src/commander.rs @@ -2,12 +2,16 @@ use std::fs::{self, File}; use std::io::Write; use std::iter::Iterator; use std::path::Path; +use std::thread; +use std::time::Duration; +use anyhow::{Context, Result as AnyhowResult}; +use indicatif::{ProgressBar, ProgressStyle}; use indoc::formatdoc; use prettytable::format::consts; use prettytable::{color, row, Attr, Cell, Row, Table}; +use rand::Rng; use url::Url; -use anyhow::Result as AnyhowResult; use crate::lookup::{self, MigrationFiles, VecStr}; use crate::sequel::{Driver as SequelDriver, VecSerial}; @@ -28,16 +32,22 @@ pub struct Migrator { migrations: MigrationFiles, } +fn ensure_migration_state_dir_exists() -> AnyhowResult<()> { + let migration_dir = Path::new(".migrations-state"); + if !migration_dir.exists() { + fs::create_dir_all(migration_dir) + .context("Failed to create migrations directory")?; + } + + Ok(()) +} + impl Migrator { pub fn new(executor: Box, migrations: MigrationFiles) -> Self { Self { executor, migrations } } - pub fn create( - &mut self, - path: &Path, - slug: &str, - ) -> AnyhowResult<()> { + pub fn create(&mut self, path: &Path, slug: &str) -> AnyhowResult<()> { let fixed_slug = slug.to_ascii_lowercase().replace(' ', "_"); lookup::create_migration_file(path, &fixed_slug)?; Ok(()) @@ -53,7 +63,7 @@ impl Migrator { return Ok(()); } - println!("Migration list:\n"); + println!("\n"); let mut table = Table::new(); table.set_titles(row!["Migration No.", "Status"]); table.set_format(*consts::FORMAT_NO_LINESEP_WITH_TITLE); @@ -68,17 +78,29 @@ impl Migrator { let migration_no = format!("{it:013}"); table.add_row(Row::new(vec![ Cell::new(&migration_no).with_style(Attr::Bold), - Cell::new(if temp_color == color::GREEN { "Active" } else { "Inactive" }) - .with_style(Attr::ForegroundColor(temp_color)), + Cell::new(if temp_color == color::GREEN { + "Active" + } else { + "Inactive" + }) + .with_style(Attr::ForegroundColor(temp_color)), ])); }); table.printstd(); + println!(); + + let available_migrations_count = available_migrations.len(); + let completed_migrations_count = completed_migrations.len(); + println!("Completed migrations: {completed_migrations_count}"); + println!("Total migrations: {available_migrations_count}"); Ok(()) } pub fn up(&mut self) -> AnyhowResult<()> { + ensure_migration_state_dir_exists()?; + let completed_migrations = self.executor.get_completed_migrations()?; let available_migrations = self.migrations.keys().copied().collect::(); @@ -88,33 +110,55 @@ impl Migrator { return Ok(()); } - let filtered = available_migrations + let filtered: Vec<_> = available_migrations .iter() .filter(|s| !completed_migrations.contains(s)) - .map(std::borrow::ToOwned::to_owned) - .collect::(); + .copied() + .collect(); if filtered.is_empty() { println!("Migrations are all up-to-date."); return Ok(()); } + let pb = ProgressBar::new(filtered.len() as u64); + pb.set_style(ProgressStyle::with_template( + "{spinner:.green} [{prefix:.bold.dim}] {wide_msg:.cyan/blue} ", + )?); + let mut rng = rand::thread_rng(); for it in &filtered { - println!("[{it:013}] Applying migration in the database."); - let migration = self.migrations.get(it).unwrap(); - let content_up = migration.content_up.as_ref().unwrap(); + thread::sleep(Duration::from_millis(rng.gen_range(40..300))); + pb.set_prefix(format!("{it:013}")); + + let migration = + self.migrations.get(it).context("Migration file not found")?; + let filename_parts: Vec<&str> = + migration.filename.splitn(2, '_').collect(); + let migration_name = filename_parts + .get(1) + .and_then(|s| s.strip_suffix(".sql")) + .context("Migration name not found")?; + + pb.set_message(format!("Applying migration: {migration_name}")); + + let content_up = migration + .content_up + .as_ref() + .context("Migration content not found")?; let content_up = get_content_string!(content_up); - log::trace!("Running the following up query: {:?}", content_up); - self.executor.migrate(&content_up)?; - self.executor.add_completed_migration(it.to_owned())?; + self.executor.add_completed_migration(*it)?; + pb.inc(1); } + pb.finish(); Ok(()) } pub fn down(&mut self) -> AnyhowResult<()> { + ensure_migration_state_dir_exists()?; + let completed_migrations = self.executor.get_completed_migrations()?; if completed_migrations.is_empty() { println!( @@ -123,43 +167,61 @@ impl Migrator { return Ok(()); } + let pb = ProgressBar::new(completed_migrations.len() as u64); + pb.set_style(ProgressStyle::with_template( + "{spinner:.green} [{prefix:.bold.dim}] {wide_msg:.cyan/blue} ", + )?); + let mut rng = rand::thread_rng(); for it in completed_migrations.iter().rev() { - println!("[{it:013}] Undo migration from database."); - let migration = self.migrations.get(it).unwrap(); - let content_down = migration.content_down.as_ref().unwrap(); + thread::sleep(Duration::from_millis(rng.gen_range(40..300))); + pb.set_prefix(format!("{it:013}")); + let migration = + self.migrations.get(it).context("Migration file not found")?; + let filename_parts: Vec<&str> = + migration.filename.splitn(2, '_').collect(); + let migration_name = filename_parts + .get(1) + .and_then(|s| s.strip_suffix(".sql")) + .context("Migration name not found")?; + + pb.set_message(format!("Undoing migration: {migration_name}")); + + let content_down = migration + .content_down + .as_ref() + .context("Migration content not found")?; let content_down = get_content_string!(content_down); - log::trace!("Running the following down query: {:?}", content_down); - self.executor.migrate(&content_down)?; - - if std::env::var("MIGRATIONS_SKIP_LAST").is_ok() { - if !completed_migrations.first().eq(&Some(it)) { - self.executor.delete_completed_migration(it.to_owned())?; - } - } else { + if std::env::var("MIGRATIONS_SKIP_LAST").is_err() + || !completed_migrations.first().eq(&Some(it)) + { self.executor.delete_completed_migration(it.to_owned())?; } + pb.inc(1); } + pb.finish(); Ok(()) } pub fn redo(&mut self) -> AnyhowResult<()> { - let mut current = self.executor.get_last_completed_migration()?; - - let current_state = current; - if current_state == -1 { - current = 0; - } + let current = self.executor.get_last_completed_migration()?; + let current = if current == -1 { 0 } else { current }; - let migration = self.migrations.get(¤t).unwrap(); + let migration = self + .migrations + .get(¤t) + .context("Migration file not found")?; - if current_state != -1 { + if current != 0 { println!( "[{current:013}] Clearing recent migration from database." ); - let content_down = migration.content_down.as_ref().unwrap(); + let content_down = migration + .content_down + .as_ref() + .context("Migration content not found")?; let content_down = get_content_string!(content_down); self.executor.migrate(&content_down)?; @@ -169,12 +231,14 @@ impl Migrator { log::trace!("Running the method `redo` {:?}", migration); println!("[{current:013}] Applying recent migration in the database."); - let content_up = migration.content_up.as_ref().unwrap(); + let content_up = migration + .content_up + .as_ref() + .context("Migration content not found")?; let content_up = get_content_string!(content_up); self.executor.migrate(&content_up)?; self.executor.add_completed_migration(current)?; - Ok(()) } @@ -189,16 +253,20 @@ impl Migrator { } println!("[{current:013}] Reverting actions from last migration."); - let migration = self.migrations.get(¤t).unwrap(); - let content_down = migration.content_down.as_ref().unwrap(); + let migration = self + .migrations + .get(¤t) + .context("Migration file not found")?; + let content_down = migration + .content_down + .as_ref() + .context("Migration content not found")?; let content_down = get_content_string!(content_down); self.executor.migrate(&content_down)?; - if std::env::var("MIGRATIONS_SKIP_LAST").is_ok() { - if migrations_count > 1 { - self.executor.delete_last_completed_migration()?; - } - } else { + if migrations_count > 1 + || std::env::var("MIGRATIONS_SKIP_LAST").is_err() + { self.executor.delete_last_completed_migration()?; } Ok(()) @@ -208,7 +276,7 @@ impl Migrator { &self, source_path: &Path, source: &str, - dsn: &str, + db_url: &str, ) -> AnyhowResult<()> { let filename = ".env.midas"; let filepath = std::env::current_dir()?.join(filename); @@ -218,7 +286,7 @@ impl Migrator { let contents = formatdoc! {" DATABASE_URL={} MIGRATIONS_ROOT={} - ", dsn, source}; + ", db_url, source}; f.write_all(contents.as_bytes())?; f.sync_all()?; @@ -227,11 +295,8 @@ impl Migrator { Ok(()) } - pub fn drop( - &mut self, - raw_db_url: &str, - ) -> AnyhowResult<()> { - let db_url = Url::parse(raw_db_url).ok(); + pub fn drop(&mut self, db_url: &str) -> AnyhowResult<()> { + let db_url = Url::parse(db_url).ok(); if let Some(db_url) = db_url { let db_name = db_url.path().trim_start_matches('/'); self.executor.drop_database(db_name)?; diff --git a/src/lib.rs b/src/lib.rs index 5b1bad9..b448500 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,6 @@ -pub mod sequel; -pub mod lookup; pub mod commander; pub mod error; +pub mod lookup; +pub mod sequel; pub use error::GenericError; diff --git a/src/midas/bin/cli.rs b/src/midas/bin/cli.rs index 61c30a3..9055980 100644 --- a/src/midas/bin/cli.rs +++ b/src/midas/bin/cli.rs @@ -2,7 +2,6 @@ use clap::{Arg, Command}; use std::env; use std::path::Path; use std::time::Instant; -use url::Url; const PKG_VERSION: &str = env!("CARGO_PKG_VERSION"); const PKG_DESCRIPTION: &str = env!("CARGO_PKG_DESCRIPTION"); @@ -112,36 +111,46 @@ pub(crate) fn midas_entry( internal_matches .subcommand_matches(command_name) - .with_context(|| format!("cargo-{command_name} not invoked via cargo command"))? + .with_context(|| { + format!("cargo-{command_name} not invoked via cargo command") + })? .clone() } else { cli_app.get_matches() }; - let raw_env_db_url = env::var("DATABASE_URL").ok(); - let raw_db_url = matches + // Read the database connection url from the environment variables + // From the following possible sources: + // 1. DATABASE_URL + // 2. DB_URL + // 3. DSN + let env_db_url_1 = env::var("DATABASE_URL").ok(); + let env_db_url_2 = env::var("DB_URL").ok(); + let env_db_url_3 = env::var("DSN").ok(); + let db_url = matches .get_one::("database") - .or(raw_env_db_url.as_ref()) - .expect("msg: No database connection url was provided"); + .or(env_db_url_1.as_ref()) + .or(env_db_url_2.as_ref()) + .or(env_db_url_3.as_ref()) + .context("No database connection url was provided")?; - log::debug!("Using DSN: {}", raw_db_url); + log::debug!("Using DSN: {}", db_url); let default_source_path = Some("migrations".to_string()); let env_source_path = env::var("MIGRATIONS_ROOT").ok(); let source = matches .get_one::("source") .or(env_source_path.as_ref()) .or(default_source_path.as_ref()) - .expect("msg: No migration source path was provided"); + .context("No migration source path was provided")?; let source_path = Path::new(&source); let migrations = lookup::build_migration_list(source_path)?; let start = Instant::now(); - let executor = get_executor(raw_db_url); - let mut migrator = executor - .map(|executor| Migrator::new(executor, migrations)) - .context("Unable to initialize migrator")?; + let executor = get_executor(db_url); + let mut migrator = + executor.map(|executor| Migrator::new(executor, migrations))?; match matches.subcommand_name() { Some("create") => { @@ -149,7 +158,9 @@ pub(crate) fn midas_entry( .subcommand_matches("create") .context("No subcommand name argument was detected")? .get_one::("name") - .context("Name argument was either malformed or undecipherable")?; + .context( + "Name argument was either malformed or undecipherable", + )?; migrator.create(source_path, slug)?; } @@ -168,8 +179,8 @@ pub(crate) fn midas_entry( migrator.revert()?; } } - Some("init") => migrator.init(source_path, source, raw_db_url)?, - Some("drop") => migrator.drop(raw_db_url)?, + Some("init") => migrator.init(source_path, source, db_url)?, + Some("drop") => migrator.drop(db_url)?, None => println!("No subcommand provided"), _ => println!("Invalid subcommand provided"), } @@ -191,30 +202,31 @@ pub(crate) fn midas_entry( Ok(()) } -fn get_executor(raw_db_url: &str) -> Option> { - let db_url = Url::parse(raw_db_url).ok(); - if let Some(db_url) = db_url { - log::debug!("Connecting to database scheme: {}", db_url.scheme()); - - let driver: Box = match db_url.scheme() { - "file" => Box::new( - Sqlite::new(raw_db_url) - .expect("Failed to create Sqlite driver"), - ), - "mysql" => Box::new( - Mysql::new(raw_db_url).expect("Failed to create Mysql driver"), - ), - "postgres" => Box::new( - Postgres::new(raw_db_url) - .expect("Failed to create Postgres driver"), - ), - _ => return None, - }; - - Some(driver) - } else { - Some(Box::new( - Sqlite::new(raw_db_url).expect("Failed to create Sqlite driver"), - )) - } +fn get_executor(db_url: &str) -> AnyhowResult> { + use anyhow::{anyhow, Context}; + use url::Url; + + let url = Url::parse(db_url).context("Failed to parse database URL")?; + log::debug!("Connecting to database scheme: {}", url.scheme()); + + let driver: Box = match url.scheme() { + "file" => Box::new( + Sqlite::new(db_url).context("Failed to create Sqlite driver")?, + ), + "mysql" => Box::new( + Mysql::new(db_url).context("Failed to create Mysql driver")?, + ), + "postgres" => Box::new( + Postgres::new(db_url) + .context("Failed to create Postgres driver")?, + ), + _ => { + return Err(anyhow!( + "Unsupported database scheme: {}", + url.scheme() + )) + } + }; + + Ok(driver) } diff --git a/src/sequel/error.rs b/src/sequel/error.rs index b6c2641..9f2fead 100644 --- a/src/sequel/error.rs +++ b/src/sequel/error.rs @@ -1,7 +1,6 @@ pub use ::mysql::Error as MysqlError; pub use ::postgres::Error as PostgresError; pub use ::rusqlite::Error as SqliteError; -use std::error::Error as StdError; use std::error; use std::fmt; @@ -54,14 +53,9 @@ impl error::Error for Error { impl From for Error { fn from(value: PostgresError) -> Self { let message = value.as_db_error().map(|e| { - format!( - "{} [{}:{}]", - e.message(), - e.code().code(), - e.severity(), - ) + format!("{} [{}:{}]", e.message(), e.code().code(), e.severity(),) }); - Error::new(Kind::Postgres, message.map(|msg| Box::::from(msg))) + Error::new(Kind::Postgres, message.map(GenericError::from)) } } @@ -84,10 +78,7 @@ impl Error { self.0.cause } - fn new( - kind: Kind, - cause: Option, - ) -> Error { + fn new(kind: Kind, cause: Option) -> Error { Error(Box::new(ErrorInner { kind, cause })) } } diff --git a/src/sequel/mod.rs b/src/sequel/mod.rs index acee12e..9a30cb2 100644 --- a/src/sequel/mod.rs +++ b/src/sequel/mod.rs @@ -1,5 +1,4 @@ - -use error::Error; +use anyhow::Result as AnyhowResult; pub mod error; pub mod mysql; @@ -9,21 +8,21 @@ pub mod sqlite; pub type VecSerial = Vec; pub trait Driver { - fn ensure_migration_schema_exists(&mut self) -> Result<(), Error>; - fn ensure_migration_table_exists(&mut self) -> Result<(), Error>; - fn drop_migration_table(&mut self) -> Result<(), Error>; - fn drop_database(&mut self, db_name: &str) -> Result<(), Error>; - fn count_migrations(&mut self) -> Result; - fn get_completed_migrations(&mut self) -> Result; - fn get_last_completed_migration(&mut self) -> Result; + fn ensure_midas_schema(&mut self) -> AnyhowResult<()>; + fn drop_migration_table(&mut self) -> AnyhowResult<()>; + fn drop_database(&mut self, db_name: &str) -> AnyhowResult<()>; + fn count_migrations(&mut self) -> AnyhowResult; + fn get_completed_migrations(&mut self) -> AnyhowResult; + fn get_last_completed_migration(&mut self) -> AnyhowResult; fn add_completed_migration( &mut self, migration_number: i64, - ) -> Result<(), Error>; + ) -> AnyhowResult<()>; fn delete_completed_migration( &mut self, migration_number: i64, - ) -> Result<(), Error>; - fn delete_last_completed_migration(&mut self) -> Result<(), Error>; - fn migrate(&mut self, query: &str) -> Result<(), Error>; + ) -> AnyhowResult<()>; + fn delete_last_completed_migration(&mut self) -> AnyhowResult<()>; + fn migrate(&mut self, query: &str) -> AnyhowResult<()>; + fn db_name(&self) -> &str; } diff --git a/src/sequel/mysql.rs b/src/sequel/mysql.rs index 3388dda..7a544d2 100644 --- a/src/sequel/mysql.rs +++ b/src/sequel/mysql.rs @@ -1,30 +1,25 @@ - use indoc::{formatdoc, indoc}; use log::trace; use mysql::{params, prelude::Queryable, Pool, PooledConn}; -use super::{Driver as SequelDriver, Error, VecSerial}; +use super::{AnyhowResult, Driver as SequelDriver, VecSerial}; pub struct Mysql { conn: PooledConn, } impl Mysql { - pub fn new(database_url: &str) -> Result { + pub fn new(database_url: &str) -> AnyhowResult { let pool = Pool::new(database_url)?; let conn = pool.get_conn()?; let mut db = Mysql { conn }; - db.ensure_migration_schema_exists()?; + db.ensure_midas_schema()?; Ok(db) } } impl SequelDriver for Mysql { - fn ensure_migration_schema_exists(&mut self) -> Result<(), Error> { - Ok(()) - } - - fn ensure_migration_table_exists(&mut self) -> Result<(), Error> { + fn ensure_midas_schema(&mut self) -> AnyhowResult<()> { let payload = indoc! {" CREATE TABLE IF NOT EXISTS __schema_migrations ( id INT NOT NULL AUTO_INCREMENT, @@ -36,13 +31,13 @@ impl SequelDriver for Mysql { Ok(()) } - fn drop_migration_table(&mut self) -> Result<(), Error> { + fn drop_migration_table(&mut self) -> AnyhowResult<()> { let payload = "DROP TABLE __schema_migrations"; self.conn.query_drop(payload)?; Ok(()) } - fn drop_database(&mut self, db_name: &str) -> Result<(), Error> { + fn drop_database(&mut self, db_name: &str) -> AnyhowResult<()> { let payload = formatdoc! {" DROP DATABASE IF EXISTS `{db_name}`; CREATE DATABASE `{db_name}`; @@ -51,7 +46,7 @@ impl SequelDriver for Mysql { Ok(()) } - fn count_migrations(&mut self) -> Result { + fn count_migrations(&mut self) -> AnyhowResult { trace!("Retrieving migrations count"); let payload = "SELECT COUNT(*) as count FROM __schema_migrations"; let row: Option = self.conn.query_first(payload)?; @@ -59,7 +54,7 @@ impl SequelDriver for Mysql { Ok(result) } - fn get_completed_migrations(&mut self) -> Result { + fn get_completed_migrations(&mut self) -> AnyhowResult { trace!("Retrieving all completed migrations"); let payload = "SELECT migration FROM __schema_migrations ORDER BY id ASC"; @@ -67,7 +62,7 @@ impl SequelDriver for Mysql { Ok(result) } - fn get_last_completed_migration(&mut self) -> Result { + fn get_last_completed_migration(&mut self) -> AnyhowResult { trace!("Checking and retrieving the last migration stored on migrations table"); let payload = "SELECT migration FROM __schema_migrations ORDER BY id DESC LIMIT 1"; let row: Option = self.conn.query_first(payload)?; @@ -78,7 +73,7 @@ impl SequelDriver for Mysql { fn add_completed_migration( &mut self, migration_number: i64, - ) -> Result<(), Error> { + ) -> AnyhowResult<()> { trace!("Adding migration to migrations table"); let payload = "INSERT INTO __schema_migrations (migration) VALUES (:migration_number)"; @@ -92,7 +87,7 @@ impl SequelDriver for Mysql { fn delete_completed_migration( &mut self, migration_number: i64, - ) -> Result<(), Error> { + ) -> AnyhowResult<()> { trace!("Removing a migration in the migrations table"); let payload = "DELETE FROM __schema_migrations WHERE migration = :migration_number"; @@ -103,15 +98,19 @@ impl SequelDriver for Mysql { Ok(()) } - fn delete_last_completed_migration(&mut self) -> Result<(), Error> { + fn delete_last_completed_migration(&mut self) -> AnyhowResult<()> { let payload = "DELETE FROM __schema_migrations WHERE id=(SELECT MAX(id) FROM __schema_migrations);"; self.conn.query_drop(payload)?; Ok(()) } - fn migrate(&mut self, query: &str) -> Result<(), Error> { + fn migrate(&mut self, query: &str) -> AnyhowResult<()> { self.conn.query_drop(query)?; Ok(()) } + + fn db_name(&self) -> &str { + "" + } } diff --git a/src/sequel/postgres.rs b/src/sequel/postgres.rs index 13b764d..82d6581 100644 --- a/src/sequel/postgres.rs +++ b/src/sequel/postgres.rs @@ -1,33 +1,37 @@ - +use anyhow::Context; use log::trace; use postgres::{Client, NoTls}; +use url::Url; -use super::{Driver as SequelDriver, Error, VecSerial}; +use super::{AnyhowResult, Driver as SequelDriver, VecSerial}; pub struct Postgres { client: Client, + database_name: String, } impl Postgres { - pub fn new(database_url: &str) -> Result { - let client = Client::connect(database_url, NoTls)?; - let mut db = Postgres { client }; - db.ensure_migration_schema_exists()?; - db.ensure_migration_table_exists()?; + pub fn new(database_url: &str) -> AnyhowResult { + let url = Url::parse(database_url)?; + let database_name = url + .path_segments() + .and_then(|s| s.last()) + .context("Database name not found")?; + + let client = Client::connect(url.as_str(), NoTls)?; + + let mut db = Postgres { client, database_name: database_name.into() }; + db.ensure_midas_schema()?; Ok(db) } } impl SequelDriver for Postgres { - fn ensure_migration_schema_exists(&mut self) -> Result<(), Error> { - self.client.execute("create schema if not exists public", &[])?; - self.client.execute("grant all on schema public to public", &[])?; - Ok(()) - } - - fn ensure_migration_table_exists(&mut self) -> Result<(), Error> { + fn ensure_midas_schema(&mut self) -> AnyhowResult<()> { + self.client.execute("create schema if not exists midas", &[])?; + self.client.execute("grant all on schema midas to public", &[])?; let payload = r#" - create table if not exists public.__schema_migrations ( + create table if not exists midas.__schema_migrations ( id bigint generated by default as identity primary key, migration bigint ) @@ -36,13 +40,13 @@ impl SequelDriver for Postgres { Ok(()) } - fn drop_migration_table(&mut self) -> Result<(), Error> { - let payload = "drop table public.__schema_migrations"; + fn drop_migration_table(&mut self) -> AnyhowResult<()> { + let payload = "drop table midas.__schema_migrations"; self.client.execute(payload, &[])?; Ok(()) } - fn drop_database(&mut self, db_name: &str) -> Result<(), Error> { + fn drop_database(&mut self, db_name: &str) -> AnyhowResult<()> { let payload = format! {"drop database if exists {db_name}"}; self.client.execute(&payload, &[])?; @@ -51,29 +55,28 @@ impl SequelDriver for Postgres { Ok(()) } - fn count_migrations(&mut self) -> Result { + fn count_migrations(&mut self) -> AnyhowResult { trace!("Retrieving migrations count"); let payload = - "select count(*) as count from public.__schema_migrations"; + "select count(*) as count from midas.__schema_migrations"; let row = self.client.query_one(payload, &[])?; let result = row.get::<_, i64>(0); Ok(result) } - fn get_completed_migrations(&mut self) -> Result { + fn get_completed_migrations(&mut self) -> AnyhowResult { trace!("Retrieving all completed migrations"); let payload = - "select migration from public.__schema_migrations order by id asc"; + "select migration from midas.__schema_migrations order by id asc"; let it = self.client.query(payload, &[])?; - let result = - it.iter().map(|r| r.get("migration")).collect::<_>(); + let result = it.iter().map(|r| r.get("migration")).collect::<_>(); Ok(result) } - fn get_last_completed_migration(&mut self) -> Result { + fn get_last_completed_migration(&mut self) -> AnyhowResult { trace!("Checking and retrieving the last migration stored on migrations table"); let payload = - "select migration from public.__schema_migrations order by id desc limit 1"; + "select migration from midas.__schema_migrations order by id desc limit 1"; let result = self.client.query(payload, &[])?; if result.is_empty() { @@ -86,10 +89,10 @@ impl SequelDriver for Postgres { fn add_completed_migration( &mut self, migration_number: i64, - ) -> Result<(), Error> { + ) -> AnyhowResult<()> { trace!("Adding migration to migrations table"); let payload = - "insert into public.__schema_migrations (migration) values ($1)"; + "insert into midas.__schema_migrations (migration) values ($1)"; self.client.execute(payload, &[&migration_number])?; Ok(()) } @@ -97,23 +100,27 @@ impl SequelDriver for Postgres { fn delete_completed_migration( &mut self, migration_number: i64, - ) -> Result<(), Error> { + ) -> AnyhowResult<()> { trace!("Removing a migration in the migrations table"); let payload = - "delete from public.__schema_migrations where migration = $1"; + "delete from midas.__schema_migrations where migration = $1"; self.client.execute(payload, &[&migration_number])?; Ok(()) } - fn delete_last_completed_migration(&mut self) -> Result<(), Error> { + fn delete_last_completed_migration(&mut self) -> AnyhowResult<()> { let payload = - "delete from public.__schema_migrations where id=(select max(id) from __schema_migrations);"; + "delete from midas.__schema_migrations where id=(select max(id) from __schema_migrations);"; self.client.execute(payload, &[])?; Ok(()) } - fn migrate(&mut self, query: &str) -> Result<(), Error> { + fn migrate(&mut self, query: &str) -> AnyhowResult<()> { self.client.simple_query(query)?; Ok(()) } + + fn db_name(&self) -> &str { + &self.database_name + } } diff --git a/src/sequel/sqlite.rs b/src/sequel/sqlite.rs index 99279bf..45539e0 100644 --- a/src/sequel/sqlite.rs +++ b/src/sequel/sqlite.rs @@ -1,29 +1,24 @@ - use indoc::indoc; use log::trace; -use rusqlite::{Connection, Result}; +use rusqlite::Connection; -use super::{Driver as SequelDriver, Error, VecSerial}; +use super::{AnyhowResult, Driver as SequelDriver, VecSerial}; pub struct Sqlite { conn: Connection, } impl Sqlite { - pub fn new(file_url: &str) -> Result { + pub fn new(file_url: &str) -> AnyhowResult { let conn = Connection::open(file_url)?; - let mut db = Sqlite { conn }; - db.ensure_migration_table_exists()?; + let mut db: Sqlite = Sqlite { conn }; + db.ensure_midas_schema()?; Ok(db) } } impl SequelDriver for Sqlite { - fn ensure_migration_schema_exists(&mut self) -> Result<(), Error> { - Ok(()) - } - - fn ensure_migration_table_exists(&mut self) -> Result<(), Error> { + fn ensure_midas_schema(&mut self) -> AnyhowResult<()> { let payload = indoc! {" CREATE TABLE IF NOT EXISTS __schema_migrations ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -34,18 +29,18 @@ impl SequelDriver for Sqlite { Ok(()) } - fn drop_migration_table(&mut self) -> Result<(), Error> { + fn drop_migration_table(&mut self) -> AnyhowResult<()> { let payload = "DROP TABLE __schema_migrations"; self.conn.execute(payload, ())?; Ok(()) } - fn drop_database(&mut self, _: &str) -> Result<(), Error> { + fn drop_database(&mut self, _: &str) -> AnyhowResult<()> { // Cannot drop database in SQLite Ok(()) } - fn count_migrations(&mut self) -> Result { + fn count_migrations(&mut self) -> AnyhowResult { trace!("Retrieving migrations count"); let payload = "SELECT COUNT(*) as count FROM __schema_migrations"; let mut stmt = self.conn.prepare(payload)?; @@ -53,7 +48,7 @@ impl SequelDriver for Sqlite { Ok(result) } - fn get_completed_migrations(&mut self) -> Result { + fn get_completed_migrations(&mut self) -> AnyhowResult { trace!("Retrieving all completed migrations"); let payload = "SELECT migration FROM __schema_migrations ORDER BY id ASC"; @@ -63,7 +58,7 @@ impl SequelDriver for Sqlite { Ok(result) } - fn get_last_completed_migration(&mut self) -> Result { + fn get_last_completed_migration(&mut self) -> AnyhowResult { trace!("Checking and retrieving the last migration stored on migrations table"); let payload = "SELECT migration FROM __schema_migrations ORDER BY id DESC LIMIT 1"; let mut stmt = self.conn.prepare(payload)?; @@ -74,7 +69,7 @@ impl SequelDriver for Sqlite { fn add_completed_migration( &mut self, migration_number: i64, - ) -> Result<(), Error> { + ) -> AnyhowResult<()> { trace!("Adding migration to migrations table"); let payload = "INSERT INTO __schema_migrations (migration) VALUES ($1)"; @@ -85,22 +80,26 @@ impl SequelDriver for Sqlite { fn delete_completed_migration( &mut self, migration_number: i64, - ) -> Result<(), Error> { + ) -> AnyhowResult<()> { trace!("Removing a migration in the migrations table"); let payload = "DELETE FROM __schema_migrations WHERE migration = $1"; self.conn.execute(payload, [&migration_number])?; Ok(()) } - fn delete_last_completed_migration(&mut self) -> Result<(), Error> { + fn delete_last_completed_migration(&mut self) -> AnyhowResult<()> { let payload = "DELETE FROM __schema_migrations WHERE id=(SELECT MAX(id) FROM __schema_migrations);"; self.conn.execute(payload, ())?; Ok(()) } - fn migrate(&mut self, query: &str) -> Result<(), Error> { + fn migrate(&mut self, query: &str) -> AnyhowResult<()> { self.conn.execute(query, ())?; Ok(()) } + + fn db_name(&self) -> &str { + "sqlite" + } }