Archived
1
0
Fork 0

feat(cli): add migrations table

feat(cli): implement upgrade database subcommand
feat(cli): add available migrations to list subcommand
This commit is contained in:
Dmitriy Pleshevskiy 2021-02-06 01:22:00 +03:00
parent 4a9ece8c02
commit b5c2533bc6
7 changed files with 138 additions and 19 deletions

View file

@ -1,4 +1,6 @@
use crate::database;
use crate::path::PathBuilder; use crate::path::PathBuilder;
use postgres::Client;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::{fs, io}; 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<Migration> {
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<dyn std::error::Error + 'static>> {
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 { impl Config {
pub fn directory_path(&self) -> PathBuf { pub fn directory_path(&self) -> PathBuf {
PathBuilder::from(&self.root) PathBuilder::from(&self.root)
@ -101,7 +148,7 @@ impl Config {
.build() .build()
} }
pub fn migration_dirs(&self) -> io::Result<Vec<PathBuf>> { pub fn migrations(&self) -> io::Result<Vec<Migration>> {
let mut entries = self let mut entries = self
.migration_dir_path() .migration_dir_path()
.read_dir()? .read_dir()?
@ -110,15 +157,11 @@ impl Config {
entries.sort(); entries.sort();
let migration_dir_entries = entries let migrations = entries
.into_iter() .iter()
.filter(|entry| { .filter_map(Migration::new)
entry.is_dir()
&& PathBuilder::from(entry).append("up.sql").build().exists()
&& PathBuilder::from(entry).append("down.sql").build().exists()
})
.collect::<Vec<_>>(); .collect::<Vec<_>>();
Ok(migration_dir_entries) Ok(migrations)
} }
} }

View file

@ -7,3 +7,36 @@ pub fn connect(connection_string: &str) -> Result<Client, Error> {
pub fn apply_sql(client: &mut Client, sql_content: &str) -> Result<(), Error> { pub fn apply_sql(client: &mut Client, sql_content: &str) -> Result<(), Error> {
client.batch_execute(sql_content) 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<Vec<String>, 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<u64, Error> {
client.execute("INSERT INTO migrations (name) VALUES ($1)", &[&name])
}

View file

@ -80,20 +80,55 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
Command::List => { Command::List => {
let config = Config::read(opt.config)?; let config = Config::read(opt.config)?;
let migration_dirs = config.migration_dirs()?; let mut client = database::connect(&config.database.connection)?;
if migration_dirs.is_empty() { let applied_migrations = database::applied_migrations(&mut client)?;
println!(
"You haven't migrations in {}", println!("Applied migrations:");
config.directory_path().to_str().unwrap() if applied_migrations.is_empty() {
); println!("")
} else { } else {
migration_dirs.iter().for_each(|dir| { applied_migrations
let file_name = dir.file_name().and_then(|name| name.to_str()).unwrap(); .iter()
println!("{}", file_name); .for_each(|name| println!("{}", name));
}
let pending_migrations = config.migrations()?
.into_iter()
.filter(|m| !applied_migrations.contains(m.name()))
.collect::<Vec<_>>();
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!(); unimplemented!();
} }
} }

View file

@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`

View file

@ -0,0 +1,2 @@
-- Your SQL goes here

View file

@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`

View file

@ -0,0 +1,2 @@
-- Your SQL goes here