feat(cli): add migrations table
feat(cli): implement upgrade database subcommand feat(cli): add available migrations to list subcommand
This commit is contained in:
parent
4a9ece8c02
commit
b5c2533bc6
7 changed files with 138 additions and 19 deletions
|
@ -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<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 {
|
||||
pub fn directory_path(&self) -> PathBuf {
|
||||
PathBuilder::from(&self.root)
|
||||
|
@ -101,7 +148,7 @@ impl Config {
|
|||
.build()
|
||||
}
|
||||
|
||||
pub fn migration_dirs(&self) -> io::Result<Vec<PathBuf>> {
|
||||
pub fn migrations(&self) -> io::Result<Vec<Migration>> {
|
||||
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::<Vec<_>>();
|
||||
|
||||
Ok(migration_dir_entries)
|
||||
Ok(migrations)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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> {
|
||||
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])
|
||||
}
|
||||
|
|
|
@ -80,20 +80,55 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
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::<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!();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
-- This file should undo anything in `up.sql`
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
-- Your SQL goes here
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
-- This file should undo anything in `up.sql`
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
-- Your SQL goes here
|
||||
|
Reference in a new issue