Archived
1
0
Fork 0

refac: add migration manager

This commit is contained in:
Dmitriy Pleshevskiy 2021-02-15 13:06:09 +03:00
parent 1b16aff6e5
commit 109c9ce52f
6 changed files with 243 additions and 106 deletions

View file

@ -1,12 +1,14 @@
use crate::config::Config;
use crate::database::PostgresConnection;
use crate::migration::{DatabaseMigrationManager, MigrationManager};
use crate::opts::ApplyCommandOpt;
use crate::path::PathBuilder;
use crate::StdResult;
use std::convert::TryFrom;
pub(crate) fn apply_sql(config: Config, opts: ApplyCommandOpt) -> StdResult<()> {
let mut connection = PostgresConnection::try_from(&config)?;
let connection = PostgresConnection::try_from(&config)?;
let mut manager = MigrationManager::new(connection);
let file_path = PathBuilder::from(config.directory_path())
.append(opts.file_name)
@ -15,7 +17,7 @@ pub(crate) fn apply_sql(config: Config, opts: ApplyCommandOpt) -> StdResult<()>
let content = std::fs::read_to_string(file_path)?;
match connection.apply_sql(&content) {
match manager.apply_sql(&content) {
Ok(_) => {
println!("File was applied successfully");
}

View file

@ -1,13 +1,14 @@
use crate::config::Config;
use crate::database::PostgresConnection;
use crate::migration::Downgrade;
use crate::migration::{DatabaseMigrationManager, MigrationManager};
use crate::StdResult;
use std::convert::TryFrom;
pub(crate) fn downgrade_applied_migrations(config: Config) -> StdResult<()> {
let mut connection = PostgresConnection::try_from(&config)?;
let connection = PostgresConnection::try_from(&config)?;
let mut manager = MigrationManager::new(connection);
let applied_migrations = connection.applied_migration_names()?;
let applied_migrations = manager.applied_migration_names()?;
let migrations = config.migrations()?;
if let Some(first_applied_migration) = applied_migrations.first() {
@ -16,7 +17,7 @@ pub(crate) fn downgrade_applied_migrations(config: Config) -> StdResult<()> {
.find(|m| m.name() == first_applied_migration)
{
println!("downgrade {}...", migration.name());
migration.downgrade(&mut connection)?;
manager.downgrade(&migration)?;
}
}

View file

@ -1,26 +1,23 @@
use crate::config::Config;
use crate::database::PostgresConnection;
use crate::database::{DatabaseConnection, PostgresConnection};
use crate::error::{ErrorKind, StdResult};
use crate::migration::{
filter_pending_migrations, DatabaseMigrationManager, Migration, MigrationManager,
};
const EM_DASH: char = '—';
pub(crate) fn print_migration_lists(config: Config) -> StdResult<()> {
let applied_migrations = match config.database_connection_string() {
let applied_migration_names = match config.database_connection_string() {
Ok(ref database_connection_string) => {
let mut connection = PostgresConnection::open(database_connection_string)?;
let applied_migrations = connection.applied_migration_names()?;
let connection = PostgresConnection::open(database_connection_string)?;
let mut manager = MigrationManager::new(connection);
println!("Applied migrations:");
if applied_migrations.is_empty() {
println!("{}", EM_DASH);
} else {
applied_migrations
.iter()
.rev()
.for_each(|name| println!("{}", name));
}
let applied_migration_names = manager.applied_migration_names()?;
applied_migrations
show_applied_migrations(&applied_migration_names);
applied_migration_names
}
Err(e) if *e.kind() == ErrorKind::MissedEnvVar(String::new()) => {
println!("{}", e.kind());
@ -33,11 +30,26 @@ pub(crate) fn print_migration_lists(config: Config) -> StdResult<()> {
println!();
let pending_migrations = config
.migrations()?
.into_iter()
.filter(|m| !applied_migrations.contains(m.name()))
.collect::<Vec<_>>();
let pending_migrations =
filter_pending_migrations(config.migrations()?, &applied_migration_names);
show_pending_migrations(&pending_migrations);
Ok(())
}
fn show_applied_migrations(applied_migration_names: &[String]) {
println!("Applied migrations:");
if applied_migration_names.is_empty() {
println!("{}", EM_DASH);
} else {
applied_migration_names
.iter()
.rev()
.for_each(|name| println!("{}", name));
}
}
fn show_pending_migrations(pending_migrations: &[Migration]) {
println!("Pending migrations:");
if pending_migrations.is_empty() {
println!("{}", EM_DASH);
@ -46,6 +58,4 @@ pub(crate) fn print_migration_lists(config: Config) -> StdResult<()> {
println!("{}", m.name());
});
}
Ok(())
}

View file

@ -1,24 +1,21 @@
use crate::database::PostgresConnection;
use crate::migration::{Migration, Upgrade};
use crate::database::{DatabaseConnection, PostgresConnection};
use crate::migration::Migration;
use crate::migration::{filter_pending_migrations, DatabaseMigrationManager, MigrationManager};
use crate::Config;
use crate::StdResult;
use std::convert::TryFrom;
pub(crate) fn upgrade_pending_migrations(config: Config) -> StdResult<()> {
let mut connection = PostgresConnection::try_from(&config)?;
let mut manager = MigrationManager::new(PostgresConnection::try_from(&config)?);
let applied_migration_names = connection.applied_migration_names()?;
let applied_migration_names = manager.applied_migration_names()?;
let migrations = config.migrations()?;
if is_up_to_date_migrations(&migrations, &applied_migration_names) {
println!("Up to date");
} else {
let pending_migrations = filter_pending_migrations(migrations, &applied_migration_names);
for migration in pending_migrations.iter() {
println!("upgrade {}...", migration.name());
migration.upgrade(&mut connection)?;
}
upgrade_all_pending_migrations(manager, &pending_migrations)?;
}
Ok(())
@ -28,12 +25,18 @@ fn is_up_to_date_migrations(migrations: &[Migration], applied_migration_names: &
migrations.is_empty() || migrations.last().map(|m| m.name()) == applied_migration_names.first()
}
fn filter_pending_migrations(
migrations: Vec<Migration>,
applied_migration_names: &[String],
) -> Vec<Migration> {
migrations
.into_iter()
.filter(|m| !applied_migration_names.contains(m.name()))
.collect()
fn upgrade_all_pending_migrations<Conn, ManagerT>(
mut manager: ManagerT,
pending_migrations: &[Migration],
) -> StdResult<()>
where
Conn: DatabaseConnection,
ManagerT: Sized + DatabaseMigrationManager<Conn>,
{
for migration in pending_migrations.iter() {
println!("upgrade {}...", migration.name());
manager.upgrade(migration)?;
}
Ok(())
}

View file

@ -1,8 +1,41 @@
use crate::config::Config;
use crate::StdResult;
use postgres::{Client, Error, NoTls};
use postgres::{Client, NoTls};
use std::convert::TryFrom;
pub trait ToSql {
fn to_sql(&self) -> String;
}
impl ToSql for &str {
fn to_sql(&self) -> String {
format!(r#""{}""#, self)
}
}
pub trait TryFromSql<QueryResultRow>: Sized {
fn try_from_sql(row: QueryResultRow) -> StdResult<Self>;
}
pub trait DatabaseConnection: Sized {
type QueryResultRow;
type QueryResult;
fn open(connection_string: &str) -> StdResult<Self>;
fn batch_execute(&mut self, query: &str) -> StdResult<()>;
fn execute<'b>(&mut self, query: &str, params: &'b [&'b dyn ToSql]) -> StdResult<u64>;
fn query<'b, OutputItem>(
&mut self,
query: &str,
params: &'b [&'b dyn ToSql],
) -> StdResult<Vec<OutputItem>>
where
OutputItem: ?Sized + TryFromSql<Self::QueryResultRow>;
}
pub struct PostgresConnection {
client: Client,
}
@ -15,54 +48,54 @@ impl TryFrom<&Config> for PostgresConnection {
}
}
impl PostgresConnection {
pub fn open(connection_string: &str) -> StdResult<PostgresConnection> {
impl DatabaseConnection for PostgresConnection {
type QueryResultRow = postgres::Row;
type QueryResult = Vec<Self::QueryResultRow>;
fn open(connection_string: &str) -> StdResult<Self> {
let client = Client::connect(connection_string, NoTls)?;
Ok(PostgresConnection { client })
}
fn batch_execute(&mut self, query: &str) -> StdResult<()> {
self.client.batch_execute(query)?;
Ok(())
}
pub fn is_migrations_table_not_found(e: &Error) -> bool {
e.to_string()
.contains(r#"relation "migrations" does not exist"#)
fn execute<'b>(&mut self, query: &str, params: &'b [&'b dyn ToSql]) -> StdResult<u64> {
let stmt = params
.iter()
.enumerate()
.fold(query.to_string(), |acc, (i, p)| {
str::replace(&acc, &format!("${}", i), &p.to_sql())
});
let res = self.client.execute(stmt.as_str(), &[])?;
Ok(res)
}
impl PostgresConnection {
pub fn apply_sql(&mut self, sql_content: &str) -> Result<(), Error> {
self.client.batch_execute(sql_content)
}
fn query<'b, OutputItem>(
&mut self,
query: &str,
params: &'b [&'b dyn ToSql],
) -> StdResult<Vec<OutputItem>>
where
OutputItem: ?Sized + TryFromSql<Self::QueryResultRow>,
{
let stmt = params
.iter()
.enumerate()
.fold(query.to_string(), |acc, (i, p)| {
str::replace(&acc, &format!("${}", i), &p.to_sql())
});
pub fn applied_migration_names(&mut self) -> Result<Vec<String>, Error> {
let res = self
.client
.query("SELECT name FROM migrations ORDER BY id DESC", &[])
.or_else(|e| {
if is_migrations_table_not_found(&e) {
Ok(Vec::new())
} else {
Err(e)
}
})?;
let res: Self::QueryResult = self.client.query(stmt.as_str(), &[])?;
Ok(res.into_iter().map(|row| row.get(0)).collect())
}
let res = res
.into_iter()
.map(OutputItem::try_from_sql)
.collect::<Result<Vec<OutputItem>, _>>()?;
pub fn create_migrations_table(&mut self) -> Result<(), Error> {
self.apply_sql(
r#"CREATE TABLE IF NOT EXISTS migrations (
id serial PRIMARY KEY,
name text NOT NULL UNIQUE
)"#,
)
}
pub fn insert_migration_info(&mut self, name: &str) -> Result<u64, Error> {
self.client
.execute("INSERT INTO migrations (name) VALUES ($1)", &[&name])
}
pub fn delete_migration_info(&mut self, name: &str) -> Result<u64, Error> {
self.client
.execute("DELETE FROM migrations WHERE name = $1", &[&name])
Ok(res)
}
}

View file

@ -1,17 +1,10 @@
use crate::database::PostgresConnection;
use crate::database::TryFromSql;
use crate::database::{DatabaseConnection, PostgresConnection};
use crate::path::PathBuilder;
use crate::StdResult;
use std::fs;
use std::path::PathBuf;
pub trait Upgrade {
fn upgrade(&self, connection: &mut PostgresConnection) -> StdResult<()>;
}
pub trait Downgrade {
fn downgrade(&self, connection: &mut PostgresConnection) -> StdResult<()>;
}
#[derive(Debug)]
pub struct Migration {
upgrade_sql: PathBuf,
@ -46,27 +39,122 @@ impl Migration {
pub fn name(&self) -> &String {
&self.name
}
}
impl Upgrade for Migration {
fn upgrade(&self, connection: &mut PostgresConnection) -> StdResult<()> {
fn upgrade_sql_content(&self) -> StdResult<String> {
let content = fs::read_to_string(&self.upgrade_sql)?;
connection.create_migrations_table()?;
connection.apply_sql(&content)?;
connection.insert_migration_info(self.name())?;
Ok(())
}
Ok(content)
}
impl Downgrade for Migration {
fn downgrade(&self, connection: &mut PostgresConnection) -> StdResult<()> {
fn downgrade_sql_content(&self) -> StdResult<String> {
let content = fs::read_to_string(&self.downgrade_sql)?;
Ok(content)
}
}
connection.apply_sql(&content)?;
connection.delete_migration_info(self.name())?;
pub struct MigrationManager<Conn: DatabaseConnection> {
conn: Conn,
}
impl<Conn: DatabaseConnection> MigrationManager<Conn> {
pub fn new(conn: Conn) -> Self {
MigrationManager { conn }
}
}
pub fn is_migrations_table_not_found<D: std::fmt::Display>(error: D) -> bool {
error
.to_string()
.contains(r#"relation "migrations" does not exist"#)
}
impl TryFromSql<postgres::Row> for String {
fn try_from_sql(row: postgres::Row) -> StdResult<Self> {
let res: String = row.get(0);
Ok(res)
}
}
pub trait DatabaseMigrationManager<Conn: DatabaseConnection> {
const CREATE_MIGRATIONS_STMT: &'static str = r#"
CREATE TABLE IF NOT EXISTS migrations (
id serial PRIMARY KEY,
name text NOT NULL UNIQUE
)
"#;
const INSERT_MIGRATION_STMT: &'static str = "INSERT INTO migrations (name) VALUES ($1)";
const DELETE_MIGRATION_STMT: &'static str = "DELETE FROM migrations WHERE name = $1";
fn apply_sql(&mut self, sql_content: &str) -> StdResult<()>;
fn applied_migration_names(&mut self) -> StdResult<Vec<String>>;
fn create_migrations_table(&mut self) -> StdResult<()>;
fn insert_migration_info(&mut self, name: &str) -> StdResult<u64>;
fn delete_migration_info(&mut self, name: &str) -> StdResult<u64>;
fn upgrade(&mut self, migration: &Migration) -> StdResult<()> {
let content = migration.upgrade_sql_content()?;
self.create_migrations_table()?;
self.apply_sql(&content)?;
self.insert_migration_info(migration.name())?;
Ok(())
}
fn downgrade(&mut self, migration: &Migration) -> StdResult<()> {
let content = migration.downgrade_sql_content()?;
self.apply_sql(&content)?;
self.delete_migration_info(migration.name())?;
Ok(())
}
}
impl DatabaseMigrationManager<PostgresConnection> for MigrationManager<PostgresConnection> {
fn apply_sql(&mut self, sql_content: &str) -> StdResult<()> {
self.conn.batch_execute(sql_content)
}
fn applied_migration_names(&mut self) -> StdResult<Vec<String>> {
let res = self
.conn
.query("SELECT name FROM migrations ORDER BY id DESC", &[])
.or_else(|e| {
if is_migrations_table_not_found(&e) {
Ok(Vec::new())
} else {
Err(e)
}
})?;
Ok(res.into_iter().collect())
}
fn create_migrations_table(&mut self) -> StdResult<()> {
self.conn.batch_execute(Self::CREATE_MIGRATIONS_STMT)
}
fn insert_migration_info(&mut self, name: &str) -> StdResult<u64> {
self.conn.execute(Self::INSERT_MIGRATION_STMT, &[&name])
}
fn delete_migration_info(&mut self, name: &str) -> StdResult<u64> {
self.conn.execute(Self::DELETE_MIGRATION_STMT, &[&name])
}
}
pub fn filter_pending_migrations(
migrations: Vec<Migration>,
applied_migration_names: &[String],
) -> Vec<Migration> {
migrations
.into_iter()
.filter(|m| !applied_migration_names.contains(m.name()))
.collect()
}