From b5c2533bc66f3e2f1eefd65e031ebd2441a2c72e Mon Sep 17 00:00:00 2001 From: Dmitriy Pleshevskiy Date: Sat, 6 Feb 2021 01:22:00 +0300 Subject: [PATCH] feat(cli): add migrations table feat(cli): implement upgrade database subcommand feat(cli): add available migrations to list subcommand --- migra-cli/src/config.rs | 61 ++++++++++++++++--- migra-cli/src/database.rs | 33 ++++++++++ migra-cli/src/main.rs | 55 ++++++++++++++--- .../210206002058_hello_world/down.sql | 2 + .../210206002058_hello_world/up.sql | 2 + .../210206002359_second_migration/down.sql | 2 + .../210206002359_second_migration/up.sql | 2 + 7 files changed, 138 insertions(+), 19 deletions(-) create mode 100644 sample/database/migrations/210206002058_hello_world/down.sql create mode 100644 sample/database/migrations/210206002058_hello_world/up.sql create mode 100644 sample/database/migrations/210206002359_second_migration/down.sql create mode 100644 sample/database/migrations/210206002359_second_migration/up.sql diff --git a/migra-cli/src/config.rs b/migra-cli/src/config.rs index 1a36713..0e070c7 100644 --- a/migra-cli/src/config.rs +++ b/migra-cli/src/config.rs @@ -1,4 +1,6 @@ +use crate::database; use crate::path::PathBuilder; +use postgres::Client; use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; use std::{fs, io}; @@ -88,6 +90,51 @@ impl Config { } } +pub struct Migration { + upgrade_sql: PathBuf, + downgrade_sql: PathBuf, + name: String, +} + +impl Migration { + fn new(directory: &PathBuf) -> Option { + if directory.is_dir() { + let name = directory + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or_default(); + let upgrade_sql = PathBuilder::from(directory).append("up.sql").build(); + let downgrade_sql = PathBuilder::from(directory).append("down.sql").build(); + + if upgrade_sql.exists() && downgrade_sql.exists() { + return Some(Migration { + upgrade_sql, + downgrade_sql, + name: String::from(name), + }); + } + } + + None + } + + pub fn name(&self) -> &String { + &self.name + } + + pub fn upgrade(&self, client: &mut Client) -> Result<(), Box> { + let content = fs::read_to_string(&self.upgrade_sql)?; + + database::create_migration_table(client)?; + + database::apply_sql(client, &content)?; + + database::insert_migration_info(client, self.name())?; + + Ok(()) + } +} + impl Config { pub fn directory_path(&self) -> PathBuf { PathBuilder::from(&self.root) @@ -101,7 +148,7 @@ impl Config { .build() } - pub fn migration_dirs(&self) -> io::Result> { + pub fn migrations(&self) -> io::Result> { let mut entries = self .migration_dir_path() .read_dir()? @@ -110,15 +157,11 @@ impl Config { entries.sort(); - let migration_dir_entries = entries - .into_iter() - .filter(|entry| { - entry.is_dir() - && PathBuilder::from(entry).append("up.sql").build().exists() - && PathBuilder::from(entry).append("down.sql").build().exists() - }) + let migrations = entries + .iter() + .filter_map(Migration::new) .collect::>(); - Ok(migration_dir_entries) + Ok(migrations) } } diff --git a/migra-cli/src/database.rs b/migra-cli/src/database.rs index 62e072b..1df4f1d 100644 --- a/migra-cli/src/database.rs +++ b/migra-cli/src/database.rs @@ -7,3 +7,36 @@ pub fn connect(connection_string: &str) -> Result { pub fn apply_sql(client: &mut Client, sql_content: &str) -> Result<(), Error> { client.batch_execute(sql_content) } + +pub fn is_migration_table_not_found(e: &Error) -> bool { + e.to_string() + .contains(r#"relation "migrations" does not exist"#) +} + +pub fn applied_migrations(client: &mut Client) -> Result, Error> { + let res = client + .query("SELECT name FROM migrations ORDER BY id DESC", &[]) + .or_else(|e| { + if is_migration_table_not_found(&e) { + Ok(Vec::new()) + } else { + Err(e) + } + })?; + + Ok(res.into_iter().map(|row| row.get(0)).collect()) +} + +pub fn create_migration_table(client: &mut Client) -> Result<(), Error> { + apply_sql( + client, + r#"CREATE TABLE IF NOT EXISTS migrations ( + id serial PRIMARY KEY, + name text NOT NULL UNIQUE + )"#, + ) +} + +pub fn insert_migration_info(client: &mut Client, name: &str) -> Result { + client.execute("INSERT INTO migrations (name) VALUES ($1)", &[&name]) +} diff --git a/migra-cli/src/main.rs b/migra-cli/src/main.rs index 317dd65..375b93e 100644 --- a/migra-cli/src/main.rs +++ b/migra-cli/src/main.rs @@ -80,20 +80,55 @@ fn main() -> Result<(), Box> { Command::List => { let config = Config::read(opt.config)?; - let migration_dirs = config.migration_dirs()?; - if migration_dirs.is_empty() { - println!( - "You haven't migrations in {}", - config.directory_path().to_str().unwrap() - ); + let mut client = database::connect(&config.database.connection)?; + let applied_migrations = database::applied_migrations(&mut client)?; + + println!("Applied migrations:"); + if applied_migrations.is_empty() { + println!("–") } else { - migration_dirs.iter().for_each(|dir| { - let file_name = dir.file_name().and_then(|name| name.to_str()).unwrap(); - println!("{}", file_name); + applied_migrations + .iter() + .for_each(|name| println!("{}", name)); + } + + let pending_migrations = config.migrations()? + .into_iter() + .filter(|m| !applied_migrations.contains(m.name())) + .collect::>(); + println!("Pending migrations:"); + if pending_migrations.is_empty() { + println!("–"); + } else { + pending_migrations.iter().for_each(|m| { + println!("{}", m.name()); }); } } - Command::Upgrade | Command::Downgrade => { + Command::Upgrade => { + let config = Config::read(opt.config)?; + + let mut client = database::connect(&config.database.connection)?; + + let applied_migrations = database::applied_migrations(&mut client)?; + + let migrations = config.migrations()?; + + if migrations.is_empty() + || migrations.last().map(|m| m.name()) == applied_migrations.first() + { + println!("Up to date"); + } else { + for m in migrations + .iter() + .filter(|m| !applied_migrations.contains(m.name())) + { + println!("{}", m.name()); + m.upgrade(&mut client)?; + } + } + } + Command::Downgrade => { unimplemented!(); } } diff --git a/sample/database/migrations/210206002058_hello_world/down.sql b/sample/database/migrations/210206002058_hello_world/down.sql new file mode 100644 index 0000000..17e9f2a --- /dev/null +++ b/sample/database/migrations/210206002058_hello_world/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` + diff --git a/sample/database/migrations/210206002058_hello_world/up.sql b/sample/database/migrations/210206002058_hello_world/up.sql new file mode 100644 index 0000000..495311a --- /dev/null +++ b/sample/database/migrations/210206002058_hello_world/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here + diff --git a/sample/database/migrations/210206002359_second_migration/down.sql b/sample/database/migrations/210206002359_second_migration/down.sql new file mode 100644 index 0000000..17e9f2a --- /dev/null +++ b/sample/database/migrations/210206002359_second_migration/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` + diff --git a/sample/database/migrations/210206002359_second_migration/up.sql b/sample/database/migrations/210206002359_second_migration/up.sql new file mode 100644 index 0000000..495311a --- /dev/null +++ b/sample/database/migrations/210206002359_second_migration/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +