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 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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])
|
||||||
|
}
|
||||||
|
|
|
@ -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!();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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