Migra core (#11)

* feat(core): init migra lib
* refac(core): add utils for migration list
* feat(core): add managers
* refac(core): add batch exec trait
* refac(core): smarter managers
* refac(cli): removed adapter, builder
* refac(cli): use migra core for cli
* chore(cli): add dev deps for tests
* chore(cli): improve error handling
* refac(core): make migrations simpler
* refac(cli): change transaction utils
* chore(core): add documentation
This commit is contained in:
Dmitriy Pleshevskiy 2021-06-13 01:39:56 +03:00 committed by GitHub
parent c144086cb1
commit ec02367680
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
69 changed files with 1419 additions and 919 deletions

View File

@ -1,4 +1,5 @@
[workspace]
members = [
"migra-cli"
"migra",
"migra_cli",
]

View File

@ -1,48 +0,0 @@
use crate::app::App;
use crate::database::prelude::*;
use crate::database::transaction::maybe_with_transaction;
use crate::database::{DatabaseConnectionManager, MigrationManager};
use crate::opts::DowngradeCommandOpt;
use crate::StdResult;
use std::cmp;
pub(crate) fn rollback_applied_migrations(app: &App, opts: DowngradeCommandOpt) -> StdResult<()> {
let config = app.config()?;
let mut connection_manager = DatabaseConnectionManager::connect(&config.database)?;
let conn = connection_manager.connection();
let migration_manager = MigrationManager::from(&config);
let applied_migrations = migration_manager.applied_migration_names(conn)?;
let migrations = config.migrations()?;
let rollback_migrations_number = if opts.all_migrations {
applied_migrations.len()
} else {
cmp::min(opts.migrations_number, applied_migrations.len())
};
maybe_with_transaction(
opts.transaction_opts.single_transaction,
conn,
&mut |conn| {
applied_migrations[..rollback_migrations_number]
.iter()
.try_for_each(|migration_name| {
if let Some(migration) = migrations.iter().find(|m| m.name() == migration_name)
{
println!("downgrade {}...", migration.name());
maybe_with_transaction(
!opts.transaction_opts.single_transaction,
conn,
&mut |conn| migration_manager.downgrade(conn, &migration),
)
} else {
Ok(())
}
})
.map_err(From::from)
},
)?;
Ok(())
}

View File

@ -1,65 +0,0 @@
use crate::app::App;
use crate::database::migration::filter_pending_migrations;
use crate::database::prelude::*;
use crate::database::{DatabaseConnectionManager, Migration, MigrationManager};
use crate::error::{Error, StdResult};
const EM_DASH: char = '—';
pub(crate) fn print_migration_lists(app: &App) -> StdResult<()> {
let config = app.config()?;
let applied_migration_names = match config.database.connection_string() {
Ok(ref database_connection_string) => {
let mut connection_manager = DatabaseConnectionManager::connect_with_string(
&config.database,
database_connection_string,
)?;
let conn = connection_manager.connection();
let migration_manager = MigrationManager::from(&config);
let applied_migration_names = migration_manager.applied_migration_names(conn)?;
show_applied_migrations(&applied_migration_names);
applied_migration_names
}
Err(e) if e == Error::MissedEnvVar(String::new()) => {
eprintln!("WARNING: {}", e);
eprintln!("WARNING: No connection to database");
Vec::new()
}
Err(e) => panic!("{}", e),
};
println!();
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);
} else {
pending_migrations.iter().for_each(|m| {
println!("{}", m.name());
});
}
}

View File

@ -1,66 +0,0 @@
use crate::app::App;
use crate::database::migration::*;
use crate::database::transaction::maybe_with_transaction;
use crate::database::DatabaseConnectionManager;
use crate::opts::UpgradeCommandOpt;
use crate::StdResult;
pub(crate) fn upgrade_pending_migrations(app: &App, opts: UpgradeCommandOpt) -> StdResult<()> {
let config = app.config()?;
let mut connection_manager = DatabaseConnectionManager::connect(&config.database)?;
let conn = connection_manager.connection();
let migration_manager = MigrationManager::from(&config);
let applied_migration_names = migration_manager.applied_migration_names(conn)?;
let migrations = config.migrations()?;
let pending_migrations = filter_pending_migrations(migrations, &applied_migration_names);
if pending_migrations.is_empty() {
println!("Up to date");
return Ok(());
}
let migrations: Vec<Migration> = if let Some(migration_name) = opts.migration_name.clone() {
let target_migration = pending_migrations
.into_iter()
.find(|m| m.name() == &migration_name);
match target_migration {
Some(migration) => vec![migration],
None => {
eprintln!(r#"Cannot find migration with "{}" name"#, migration_name);
return Ok(());
}
}
} else {
let upgrade_migrations_number = opts
.migrations_number
.unwrap_or_else(|| pending_migrations.len());
pending_migrations[..upgrade_migrations_number].to_vec()
};
maybe_with_transaction(
opts.transaction_opts.single_transaction,
conn,
&mut |conn| {
migrations
.iter()
.try_for_each(|migration| {
print_migration_info(migration);
maybe_with_transaction(
!opts.transaction_opts.single_transaction,
conn,
&mut |conn| migration_manager.upgrade(conn, migration),
)
})
.map_err(From::from)
},
)?;
Ok(())
}
fn print_migration_info(migration: &Migration) {
println!("upgrade {}...", migration.name());
}

View File

@ -1,17 +0,0 @@
use crate::error::StdResult;
pub trait ToSql {
fn to_sql(&self) -> String;
}
pub type ToSqlParams<'a> = &'a [&'a dyn ToSql];
impl ToSql for &str {
fn to_sql(&self) -> String {
format!("'{}'", self)
}
}
pub trait TryFromSql<QueryResultRow>: Sized {
fn try_from_sql(row: QueryResultRow) -> StdResult<Self>;
}

View File

@ -1,39 +0,0 @@
use super::prelude::*;
pub(crate) fn merge_query_with_params(query: &str, params: ToSqlParams) -> String {
params
.iter()
.enumerate()
.fold(query.to_string(), |acc, (i, p)| {
str::replace(&acc, &format!("${}", i + 1), &p.to_sql())
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn replace_one_param_in_query() {
assert_eq!(
merge_query_with_params("SELECT $1", &[&"foo"]),
"SELECT 'foo'"
);
}
#[test]
fn replace_two_params_in_query() {
assert_eq!(
merge_query_with_params("SELECT $1, $2", &[&"foo", &"bar"]),
"SELECT 'foo', 'bar'"
);
}
#[test]
fn replace_all_bonds_in_query_with_first_param() {
assert_eq!(
merge_query_with_params("SELECT $1, $1", &[&"foo"]),
"SELECT 'foo', 'foo'"
);
}
}

View File

@ -1,20 +0,0 @@
cfg_if! {
if #[cfg(feature = "postgres")] {
mod postgres;
pub use self::postgres::*;
}
}
cfg_if! {
if #[cfg(feature = "mysql")] {
mod mysql;
pub use self::mysql::*;
}
}
cfg_if! {
if #[cfg(feature = "sqlite")] {
mod sqlite;
pub use self::sqlite::*;
}
}

View File

@ -1,53 +0,0 @@
use crate::database::builder::merge_query_with_params;
use crate::database::prelude::*;
use crate::error::StdResult;
use mysql::prelude::*;
use mysql::{Pool, PooledConn};
pub struct MySqlConnection {
conn: PooledConn,
}
impl OpenDatabaseConnection for MySqlConnection {
fn open(connection_string: &str) -> StdResult<Self> {
let pool = Pool::new_manual(1, 1, connection_string)?;
let conn = pool.get_conn()?;
Ok(MySqlConnection { conn })
}
}
impl DatabaseStatements for MySqlConnection {
fn create_migration_table_stmt(&self, migrations_table_name: &str) -> String {
format!(
r#"CREATE TABLE IF NOT EXISTS {} (
id int AUTO_INCREMENT PRIMARY KEY,
name varchar(256) NOT NULL UNIQUE
)"#,
migrations_table_name
)
}
}
impl SupportsTransactionalDdl for MySqlConnection {}
impl DatabaseConnection for MySqlConnection {
fn batch_execute(&mut self, query: &str) -> StdResult<()> {
self.conn.query_drop(query)?;
Ok(())
}
fn execute<'b>(&mut self, query: &str, params: ToSqlParams<'b>) -> StdResult<u64> {
let stmt = merge_query_with_params(query, params);
let res = self.conn.query_first(stmt)?.unwrap_or_default();
Ok(res)
}
fn query<'b>(&mut self, query: &str, params: ToSqlParams<'b>) -> StdResult<Vec<Vec<String>>> {
let stmt = merge_query_with_params(query, params);
let res = self.conn.query_map(stmt, |(column,)| vec![column])?;
Ok(res)
}
}

View File

@ -1,64 +0,0 @@
use crate::database::builder::merge_query_with_params;
use crate::database::prelude::*;
use crate::error::StdResult;
use postgres::{Client, NoTls};
pub struct PostgresConnection {
client: Client,
}
impl OpenDatabaseConnection for PostgresConnection {
fn open(connection_string: &str) -> StdResult<Self> {
let client = Client::connect(connection_string, NoTls)?;
Ok(PostgresConnection { client })
}
}
impl DatabaseStatements for PostgresConnection {
fn create_migration_table_stmt(&self, migrations_table_name: &str) -> String {
format!(
r#"CREATE TABLE IF NOT EXISTS {} (
id serial PRIMARY KEY,
name text NOT NULL UNIQUE
)"#,
migrations_table_name
)
}
}
impl SupportsTransactionalDdl for PostgresConnection {
#[inline]
fn supports_transactional_ddl(&self) -> bool {
true
}
}
impl DatabaseConnection for PostgresConnection {
fn batch_execute(&mut self, query: &str) -> StdResult<()> {
self.client.batch_execute(query)?;
Ok(())
}
fn execute<'b>(&mut self, query: &str, params: ToSqlParams<'b>) -> StdResult<u64> {
let stmt = merge_query_with_params(query, params);
let res = self.client.execute(stmt.as_str(), &[])?;
Ok(res)
}
fn query<'b>(&mut self, query: &str, params: ToSqlParams<'b>) -> StdResult<Vec<Vec<String>>> {
let stmt = merge_query_with_params(query, params);
let res = self.client.query(stmt.as_str(), &[])?;
let res = res
.into_iter()
.map(|row| {
let column: String = row.get(0);
vec![column]
})
.collect::<Vec<_>>();
Ok(res)
}
}

View File

@ -1,60 +0,0 @@
use crate::database::builder::merge_query_with_params;
use crate::database::prelude::*;
use crate::error::StdResult;
use rusqlite::Connection;
pub struct SqliteConnection {
conn: Connection,
}
impl OpenDatabaseConnection for SqliteConnection {
fn open(connection_string: &str) -> StdResult<Self> {
let conn = Connection::open(connection_string)?;
Ok(SqliteConnection { conn })
}
}
impl DatabaseStatements for SqliteConnection {
fn create_migration_table_stmt(&self, migrations_table_name: &str) -> String {
format!(
r#"CREATE TABLE IF NOT EXISTS {} (
id int AUTO_INCREMENT PRIMARY KEY,
name varchar(256) NOT NULL UNIQUE
)"#,
migrations_table_name
)
}
}
impl SupportsTransactionalDdl for SqliteConnection {
#[inline]
fn supports_transactional_ddl(&self) -> bool {
true
}
}
impl DatabaseConnection for SqliteConnection {
fn batch_execute(&mut self, query: &str) -> StdResult<()> {
self.conn.execute_batch(query)?;
Ok(())
}
fn execute<'b>(&mut self, query: &str, params: ToSqlParams<'b>) -> StdResult<u64> {
let stmt = merge_query_with_params(query, params);
let res = self.conn.execute(&stmt, [])?;
Ok(res as u64)
}
fn query<'b>(&mut self, query: &str, params: ToSqlParams<'b>) -> StdResult<Vec<Vec<String>>> {
let stmt = merge_query_with_params(query, params);
let mut stmt = self.conn.prepare(&stmt)?;
let res = stmt
.query_map([], |row| Ok(vec![row.get(0)?]))?
.collect::<Result<_, _>>()?;
Ok(res)
}
}

View File

@ -1,64 +0,0 @@
use super::adapter::ToSqlParams;
use super::clients::*;
use crate::config::{DatabaseConfig, SupportedDatabaseClient};
use crate::error::StdResult;
pub type AnyConnection = Box<dyn DatabaseConnection>;
pub trait OpenDatabaseConnection: Sized {
fn open(connection_string: &str) -> StdResult<Self>;
}
pub trait DatabaseStatements {
fn create_migration_table_stmt(&self, migrations_table_name: &str) -> String;
}
pub trait SupportsTransactionalDdl {
#[inline]
fn supports_transactional_ddl(&self) -> bool {
false
}
}
pub trait DatabaseConnection: DatabaseStatements + SupportsTransactionalDdl {
fn batch_execute(&mut self, query: &str) -> StdResult<()>;
fn execute<'b>(&mut self, query: &str, params: ToSqlParams<'b>) -> StdResult<u64>;
fn query<'b>(&mut self, query: &str, params: ToSqlParams<'b>) -> StdResult<Vec<Vec<String>>>;
}
pub(crate) struct DatabaseConnectionManager {
conn: AnyConnection,
}
impl DatabaseConnectionManager {
pub fn connect_with_string(
config: &DatabaseConfig,
connection_string: &str,
) -> StdResult<Self> {
let conn: AnyConnection = match config.client() {
#[cfg(feature = "postgres")]
SupportedDatabaseClient::Postgres => {
Box::new(PostgresConnection::open(&connection_string)?)
}
#[cfg(feature = "mysql")]
SupportedDatabaseClient::Mysql => Box::new(MySqlConnection::open(&connection_string)?),
#[cfg(feature = "sqlite")]
SupportedDatabaseClient::Sqlite => {
Box::new(SqliteConnection::open(&connection_string)?)
}
};
Ok(DatabaseConnectionManager { conn })
}
pub fn connect(config: &DatabaseConfig) -> StdResult<Self> {
let connection_string = config.connection_string()?;
Self::connect_with_string(config, &connection_string)
}
pub fn connection(&mut self) -> &mut AnyConnection {
&mut self.conn
}
}

View File

@ -1,187 +0,0 @@
use super::connection::AnyConnection;
use crate::Config;
use crate::StdResult;
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct Migration {
upgrade_sql_file_path: PathBuf,
downgrade_sql_file_path: PathBuf,
name: String,
}
impl Migration {
pub(crate) fn new(directory: &Path) -> Option<Migration> {
if directory.is_dir() {
let name = directory
.file_name()
.and_then(|name| name.to_str())
.unwrap_or_default();
let upgrade_sql_file_path = directory.join("up.sql");
let downgrade_sql_file_path = directory.join("down.sql");
if upgrade_sql_file_path.exists() && downgrade_sql_file_path.exists() {
return Some(Migration {
upgrade_sql_file_path,
downgrade_sql_file_path,
name: String::from(name),
});
}
}
None
}
}
impl Migration {
pub fn name(&self) -> &String {
&self.name
}
fn upgrade_sql_content(&self) -> StdResult<String> {
let content = fs::read_to_string(&self.upgrade_sql_file_path)?;
Ok(content)
}
fn downgrade_sql_content(&self) -> StdResult<String> {
let content = fs::read_to_string(&self.downgrade_sql_file_path)?;
Ok(content)
}
}
#[derive(Debug)]
pub struct MigrationManager {
migrations_table_name: String,
}
impl MigrationManager {
fn new(migrations_table_name: &str) -> Self {
MigrationManager {
migrations_table_name: migrations_table_name.to_owned(),
}
}
}
impl From<&Config> for MigrationManager {
fn from(config: &Config) -> Self {
MigrationManager::new(&config.migrations.table_name())
}
}
pub fn is_migrations_table_not_found<D: std::fmt::Display>(error: D) -> bool {
let error_message = error.to_string();
fn is_postgres_error(error_message: &str) -> bool {
error_message.contains("relation") && error_message.ends_with("does not exist")
}
fn is_mysql_error(error_message: &str) -> bool {
error_message.contains("ERROR 1146 (42S02)")
}
fn is_sqlite_error(error_message: &str) -> bool {
error_message.starts_with("no such table:")
}
is_postgres_error(&error_message)
|| is_mysql_error(&error_message)
|| is_sqlite_error(&error_message)
}
pub trait ManageMigration {
fn apply_sql(&self, conn: &mut AnyConnection, sql_content: &str) -> StdResult<()>;
fn create_migrations_table(&self, conn: &mut AnyConnection) -> StdResult<()>;
fn insert_migration_info(&self, conn: &mut AnyConnection, name: &str) -> StdResult<u64>;
fn delete_migration_info(&self, conn: &mut AnyConnection, name: &str) -> StdResult<u64>;
fn applied_migration_names(&self, conn: &mut AnyConnection) -> StdResult<Vec<String>>;
fn upgrade(&self, conn: &mut AnyConnection, migration: &Migration) -> StdResult<()> {
let content = migration.upgrade_sql_content()?;
self.create_migrations_table(conn)?;
self.apply_sql(conn, &content)?;
self.insert_migration_info(conn, migration.name())?;
Ok(())
}
fn downgrade(&self, conn: &mut AnyConnection, migration: &Migration) -> StdResult<()> {
let content = migration.downgrade_sql_content()?;
self.apply_sql(conn, &content)?;
self.delete_migration_info(conn, migration.name())?;
Ok(())
}
}
impl ManageMigration for MigrationManager {
fn apply_sql(&self, conn: &mut AnyConnection, sql_content: &str) -> StdResult<()> {
conn.batch_execute(sql_content)
}
fn create_migrations_table(&self, conn: &mut AnyConnection) -> StdResult<()> {
let stmt = conn.create_migration_table_stmt(&self.migrations_table_name);
conn.batch_execute(&stmt)
}
fn insert_migration_info(&self, conn: &mut AnyConnection, name: &str) -> StdResult<u64> {
conn.execute(
&format!(
"INSERT INTO {} (name) VALUES ($1)",
&self.migrations_table_name
),
&[&name],
)
}
fn delete_migration_info(&self, conn: &mut AnyConnection, name: &str) -> StdResult<u64> {
conn.execute(
&format!(
"DELETE FROM {} WHERE name = $1",
&self.migrations_table_name
),
&[&name],
)
}
fn applied_migration_names(&self, conn: &mut AnyConnection) -> StdResult<Vec<String>> {
let res = conn
.query(
&format!(
"SELECT name FROM {} ORDER BY id DESC",
&self.migrations_table_name
),
&[],
)
.or_else(|e| {
if is_migrations_table_not_found(&e) {
Ok(Vec::new())
} else {
Err(e)
}
})?;
let applied_migration_names: Vec<String> = res
.into_iter()
.filter_map(|row| row.first().cloned())
.collect();
Ok(applied_migration_names)
}
}
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()
}

View File

@ -1,19 +0,0 @@
pub(crate) mod adapter;
pub(crate) mod builder;
pub(crate) mod clients;
pub(crate) mod connection;
pub(crate) mod migration;
pub(crate) mod transaction;
pub mod prelude {
pub use super::adapter::{ToSql, ToSqlParams, TryFromSql};
pub use super::connection::{
AnyConnection, DatabaseConnection, DatabaseStatements, OpenDatabaseConnection,
SupportsTransactionalDdl,
};
pub use super::migration::ManageMigration;
pub use super::transaction::ManageTransaction;
}
pub(crate) use connection::DatabaseConnectionManager;
pub(crate) use migration::{Migration, MigrationManager};

View File

@ -1,63 +0,0 @@
use super::connection::AnyConnection;
use crate::error::StdResult;
pub trait ManageTransaction {
fn begin_transaction(&self, conn: &mut AnyConnection) -> StdResult<()>;
fn rollback_transaction(&self, conn: &mut AnyConnection) -> StdResult<()>;
fn commit_transaction(&self, conn: &mut AnyConnection) -> StdResult<()>;
}
#[derive(Debug)]
pub struct TransactionManager;
impl TransactionManager {
pub fn new() -> Self {
TransactionManager
}
}
impl ManageTransaction for TransactionManager {
fn begin_transaction(&self, conn: &mut AnyConnection) -> StdResult<()> {
conn.batch_execute("BEGIN")
}
fn rollback_transaction(&self, conn: &mut AnyConnection) -> StdResult<()> {
conn.batch_execute("ROLLBACK")
}
fn commit_transaction(&self, conn: &mut AnyConnection) -> StdResult<()> {
conn.batch_execute("COMMIT")
}
}
pub fn with_transaction<TrxFnMut, Res>(
conn: &mut AnyConnection,
trx_fn: &mut TrxFnMut,
) -> StdResult<Res>
where
TrxFnMut: FnMut(&mut AnyConnection) -> StdResult<Res>,
{
let transaction_manager = TransactionManager::new();
transaction_manager
.begin_transaction(conn)
.and_then(|_| trx_fn(conn))
.and_then(|res| transaction_manager.commit_transaction(conn).and(Ok(res)))
.or_else(|err| transaction_manager.rollback_transaction(conn).and(Err(err)))
}
pub fn maybe_with_transaction<TrxFnMut, Res>(
is_needed: bool,
conn: &mut AnyConnection,
trx_fn: &mut TrxFnMut,
) -> StdResult<Res>
where
TrxFnMut: FnMut(&mut AnyConnection) -> StdResult<Res>,
{
if is_needed && conn.supports_transactional_ddl() {
with_transaction(conn, trx_fn)
} else {
trx_fn(conn)
}
}

View File

@ -1,27 +0,0 @@
#![deny(clippy::all)]
#![forbid(unsafe_code)]
#[macro_use]
extern crate cfg_if;
#[cfg(not(any(feature = "postgres", feature = "mysql")))]
compile_error!(r#"Either features "postgres" or "mysql" must be enabled for "migra" crate"#);
mod app;
mod commands;
mod config;
mod database;
mod error;
mod opts;
use crate::error::StdResult;
use app::App;
use config::Config;
use opts::{AppOpt, StructOpt};
fn main() -> StdResult<()> {
#[cfg(feature = "dotenv")]
dotenv::dotenv().ok();
App::new(AppOpt::from_args()).run_command()
}

23
migra/Cargo.toml Normal file
View File

@ -0,0 +1,23 @@
[package]
name = "migra"
version = "1.0.0"
authors = ["Dmitriy Pleshevskiy <dmitriy@ideascup.me>"]
edition = "2018"
description = "Migra is a simple library for managing SQL in your application"
homepage = "https://github.com/pleshevskiy/migra"
repository = "https://github.com/pleshevskiy/migra"
license = "MIT OR Apache-2.0"
keywords = ["migration", "sql", "manager"]
categories = ["accessibility", "database"]
readme = "README.md"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
default = ["postgres"]
sqlite = ["rusqlite"]
[dependencies]
postgres = { version = "0.19", optional = true }
mysql = { version = "20.1", optional = true }
rusqlite = { version = "0.25", optional = true }

86
migra/README.md Normal file
View File

@ -0,0 +1,86 @@
# Migra
[![CI](https://github.com/pleshevskiy/migra/actions/workflows/rust.yml/badge.svg?branch=main)](https://github.com/pleshevskiy/migra/actions/workflows/rust.yml)
[![unsafe forbidden](https://img.shields.io/badge/unsafe-forbidden-success.svg)](https://github.com/rust-secure-code/safety-dance/)
[![Crates.io](https://img.shields.io/crates/v/migra)](https://crates.io/crates/migra)
![Crates.io](https://img.shields.io/crates/l/migra)
Migra is a simple library for managing SQL in your application.
For example, if you have a task list application, you can update the local user database from version to version.
This is main crate for [migra-cli](https://crates.io/crates/migra-cli), which allows you to manege SQL for web
servers in any program language without being bound to SQL frameworks.
### Installation
Add `migra = { version = "1.0" }` as a dependency in `Cargo.toml`.
This crate has not required predefined database clients in features with similar name.
If you want to add them, just install crate with additional features (`postgres`, `mysql`, `sqlite`).
`Cargo.toml` example:
```toml
[package]
name = "my-crate"
version = "0.1.0"
authors = ["Me <user@rust-lang.org>"]
[dependencies]
migra = { version = "1.0", features = ["postgres"] }
```
## Basic usage
**Note:** This example requires to enable `sqlite` feature.
```rust
use migra::clients::{OpenDatabaseConnection, SqliteClient};
use migra::managers::{ManageTransaction, ManageMigrations};
fn main() -> migra::Result<()> {
let mut client = SqliteClient::new("./tasks.db")?;
client.create_migrations_table()?;
let mut migrations = client.get_applied_migrations()?;
client
.begin_transaction()
.and_then(|_| {
migrations.should_run_upgrade_migration(
&mut client,
"20210615_initial_migration",
r#"CREATE TABLE IF NOT EXISTS tasks (
title TEXT NOT NULL
);"#,
)?;
Ok(())
})
.and_then(|res| client.commit_transaction().and(Ok(res)))
.or_else(|err| client.rollback_transaction().and(Err(err)));
Ok(())
}
```
### Supported databases
| Database | Feature |
|----------|--------------|
| Postgres | postgres |
| MySQL | mysql |
| Sqlite | sqlite |
## License
Licensed under either of these:
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE_APACHE) or
https://www.apache.org/licenses/LICENSE-2.0)
* MIT license ([LICENSE-MIT](LICENSE_MIT) or
https://opensource.org/licenses/MIT)

39
migra/src/clients/mod.rs Normal file
View File

@ -0,0 +1,39 @@
use crate::errors::MigraResult;
use crate::managers::{ManageMigrations, ManageTransaction};
/// A trait that helps to open a connection to a specific database client.
pub trait OpenDatabaseConnection
where
Self: Sized,
{
/// Open database connection with predefined migrations table name.
fn new(connection_string: &str) -> MigraResult<Self> {
Self::manual(connection_string, "migrations")
}
/// Open database connection manually with additional migration table name parameter.
fn manual(connection_string: &str, migrations_table_name: &str) -> MigraResult<Self>;
}
/// All client implementations that have migration and transaction manager implementations
/// are considered clients.
pub trait Client: ManageMigrations + ManageTransaction {}
/// If you have complex application mechanics that allow users to choose which
/// database they can use, then you will most likely need this helper for that.
pub type AnyClient = Box<(dyn Client + 'static)>;
#[cfg(feature = "postgres")]
mod postgres;
#[cfg(feature = "postgres")]
pub use self::postgres::Client as PostgresClient;
#[cfg(feature = "mysql")]
mod mysql;
#[cfg(feature = "mysql")]
pub use self::mysql::Client as MysqlClient;
#[cfg(feature = "sqlite")]
mod sqlite;
#[cfg(feature = "sqlite")]
pub use self::sqlite::Client as SqliteClient;

View File

@ -0,0 +1,94 @@
use super::OpenDatabaseConnection;
use crate::errors::{DbKind, Error, MigraResult, StdResult};
use crate::managers::{BatchExecute, ManageMigrations, ManageTransaction};
use crate::migration;
use mysql::prelude::*;
use mysql::{Pool, PooledConn};
/// Predefined `MySQL` client.
///
/// **Note:** Requires enabling `mysql` feature.
#[derive(Debug)]
pub struct Client {
conn: PooledConn,
migrations_table_name: String,
}
impl Client {
/// Provide access to the original database connection.
#[must_use]
pub fn conn(&self) -> &PooledConn {
&self.conn
}
}
impl OpenDatabaseConnection for Client {
fn manual(connection_string: &str, migrations_table_name: &str) -> MigraResult<Self> {
let conn = Pool::new_manual(1, 1, connection_string)
.and_then(|pool| pool.get_conn())
.map_err(|err| Error::db(err.into(), DbKind::DatabaseConnection))?;
Ok(Client {
conn,
migrations_table_name: migrations_table_name.to_owned(),
})
}
}
impl BatchExecute for Client {
fn batch_execute(&mut self, sql: &str) -> StdResult<()> {
self.conn.query_drop(sql).map_err(From::from)
}
}
impl ManageTransaction for Client {}
impl ManageMigrations for Client {
fn create_migrations_table(&mut self) -> MigraResult<()> {
let stmt = format!(
r#"CREATE TABLE IF NOT EXISTS {} (
id int AUTO_INCREMENT PRIMARY KEY,
name varchar(256) NOT NULL UNIQUE
)"#,
&self.migrations_table_name
);
self.batch_execute(&stmt)
.map_err(|err| Error::db(err, DbKind::CreateMigrationsTable))
}
fn insert_migration(&mut self, name: &str) -> MigraResult<u64> {
let stmt = format!(
"INSERT INTO {} (name) VALUES (?)",
&self.migrations_table_name
);
self.conn
.exec_first(&stmt, (name,))
.map(Option::unwrap_or_default)
.map_err(|err| Error::db(err.into(), DbKind::InsertMigration))
}
fn delete_migration(&mut self, name: &str) -> MigraResult<u64> {
let stmt = format!("DELETE FROM {} WHERE name = ?", &self.migrations_table_name);
self.conn
.exec_first(&stmt, (name,))
.map(Option::unwrap_or_default)
.map_err(|err| Error::db(err.into(), DbKind::DeleteMigration))
}
fn get_applied_migrations(&mut self) -> MigraResult<migration::List> {
let stmt = format!(
"SELECT name FROM {} ORDER BY id DESC",
&self.migrations_table_name
);
self.conn
.query::<String, _>(stmt)
.map(From::from)
.map_err(|err| Error::db(err.into(), DbKind::GetAppliedMigrations))
}
}
impl super::Client for Client {}

View File

@ -0,0 +1,105 @@
use super::OpenDatabaseConnection;
use crate::errors::{DbKind, Error, MigraResult, StdResult};
use crate::managers::{BatchExecute, ManageMigrations, ManageTransaction};
use crate::migration;
use postgres::{Client as PostgresClient, NoTls};
use std::fmt;
/// Predefined `Postgres` client.
///
/// **Note:** Requires enabling `postgres` feature.
pub struct Client {
conn: PostgresClient,
migrations_table_name: String,
}
impl Client {
/// Provide access to the original database connection.
#[must_use]
pub fn conn(&self) -> &PostgresClient {
&self.conn
}
}
impl fmt::Debug for Client {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt.debug_struct("Client")
.field("migrations_table_name", &self.migrations_table_name)
.finish()
}
}
impl OpenDatabaseConnection for Client {
fn manual(connection_string: &str, migrations_table_name: &str) -> MigraResult<Self> {
let conn = PostgresClient::connect(connection_string, NoTls)
.map_err(|err| Error::db(err.into(), DbKind::DatabaseConnection))?;
Ok(Client {
conn,
migrations_table_name: migrations_table_name.to_owned(),
})
}
}
impl BatchExecute for Client {
fn batch_execute(&mut self, sql: &str) -> StdResult<()> {
self.conn.batch_execute(sql).map_err(From::from)
}
}
impl ManageTransaction for Client {}
impl ManageMigrations for Client {
fn create_migrations_table(&mut self) -> MigraResult<()> {
let stmt = format!(
r#"CREATE TABLE IF NOT EXISTS {} (
id serial PRIMARY KEY,
name text NOT NULL UNIQUE
)"#,
&self.migrations_table_name
);
self.batch_execute(&stmt)
.map_err(|err| Error::db(err, DbKind::CreateMigrationsTable))
}
fn insert_migration(&mut self, name: &str) -> MigraResult<u64> {
let stmt = format!(
"INSERT INTO {} (name) VALUES ($1)",
&self.migrations_table_name
);
self.conn
.execute(stmt.as_str(), &[&name])
.map_err(|err| Error::db(err.into(), DbKind::InsertMigration))
}
fn delete_migration(&mut self, name: &str) -> MigraResult<u64> {
let stmt = format!(
"DELETE FROM {} WHERE name = $1",
&self.migrations_table_name
);
self.conn
.execute(stmt.as_str(), &[&name])
.map_err(|err| Error::db(err.into(), DbKind::DeleteMigration))
}
fn get_applied_migrations(&mut self) -> MigraResult<migration::List> {
let stmt = format!(
"SELECT name FROM {} ORDER BY id DESC",
&self.migrations_table_name
);
self.conn
.query(stmt.as_str(), &[])
.and_then(|res| {
res.into_iter()
.map(|row| row.try_get(0))
.collect::<Result<Vec<String>, _>>()
})
.map(From::from)
.map_err(|err| Error::db(err.into(), DbKind::GetAppliedMigrations))
}
}
impl super::Client for Client {}

103
migra/src/clients/sqlite.rs Normal file
View File

@ -0,0 +1,103 @@
use super::OpenDatabaseConnection;
use crate::errors::{DbKind, Error, MigraResult, StdResult};
use crate::managers::{BatchExecute, ManageMigrations, ManageTransaction};
use crate::migration;
use rusqlite::Connection;
/// Predefined `Sqlite` client.
///
/// **Note:** Requires enabling `sqlite` feature.
#[derive(Debug)]
pub struct Client {
conn: Connection,
migrations_table_name: String,
}
impl Client {
/// Provide access to the original database connection.
#[must_use]
pub fn conn(&self) -> &Connection {
&self.conn
}
}
impl OpenDatabaseConnection for Client {
fn manual(connection_string: &str, migrations_table_name: &str) -> MigraResult<Self> {
let conn = if connection_string == ":memory:" {
Connection::open_in_memory()
} else {
Connection::open(connection_string)
}
.map_err(|err| Error::db(err.into(), DbKind::DatabaseConnection))?;
Ok(Client {
conn,
migrations_table_name: migrations_table_name.to_owned(),
})
}
}
impl BatchExecute for Client {
fn batch_execute(&mut self, sql: &str) -> StdResult<()> {
self.conn.execute_batch(sql).map_err(From::from)
}
}
impl ManageTransaction for Client {}
impl ManageMigrations for Client {
fn create_migrations_table(&mut self) -> MigraResult<()> {
let stmt = format!(
r#"CREATE TABLE IF NOT EXISTS {} (
id int AUTO_INCREMENT PRIMARY KEY,
name varchar(256) NOT NULL UNIQUE
)"#,
&self.migrations_table_name
);
self.batch_execute(&stmt)
.map_err(|err| Error::db(err, DbKind::CreateMigrationsTable))
}
fn insert_migration(&mut self, name: &str) -> MigraResult<u64> {
let stmt = format!(
"INSERT INTO {} (name) VALUES ($1)",
&self.migrations_table_name
);
self.conn
.execute(&stmt, [name])
.map(|res| res as u64)
.map_err(|err| Error::db(err.into(), DbKind::InsertMigration))
}
fn delete_migration(&mut self, name: &str) -> MigraResult<u64> {
let stmt = format!(
"DELETE FROM {} WHERE name = $1",
&self.migrations_table_name
);
self.conn
.execute(&stmt, [name])
.map(|res| res as u64)
.map_err(|err| Error::db(err.into(), DbKind::DeleteMigration))
}
fn get_applied_migrations(&mut self) -> MigraResult<migration::List> {
let stmt = format!(
"SELECT name FROM {} ORDER BY id DESC",
&self.migrations_table_name
);
self.conn
.prepare(&stmt)
.and_then(|mut stmt| {
stmt.query_map([], |row| row.get(0))?
.collect::<Result<Vec<String>, _>>()
})
.map(From::from)
.map_err(|err| Error::db(err.into(), DbKind::GetAppliedMigrations))
}
}
impl super::Client for Client {}

127
migra/src/errors.rs Normal file
View File

@ -0,0 +1,127 @@
use std::fmt;
use std::io;
/// A helper type for any standard error.
pub type StdError = Box<dyn std::error::Error + 'static + Sync + Send>;
/// A helper type for any result with standard error.
pub type StdResult<T> = Result<T, StdError>;
/// A helper type for any result with migra error.
pub type MigraResult<T> = Result<T, Error>;
/// Migra error
#[derive(Debug)]
pub enum Error {
/// Represents database errors.
Db(DbError),
/// Represents standard input output errors.
Io(io::Error),
}
impl fmt::Display for Error {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::Db(ref error) => write!(fmt, "{}", error),
Error::Io(ref error) => write!(fmt, "{}", error),
}
}
}
impl std::error::Error for Error {}
impl PartialEq for Error {
fn eq(&self, other: &Self) -> bool {
std::mem::discriminant(self) == std::mem::discriminant(other)
}
}
impl From<io::Error> for Error {
#[inline]
fn from(err: io::Error) -> Error {
Error::Io(err)
}
}
impl Error {
/// Creates a database error.
#[must_use]
pub fn db(origin: StdError, kind: DbKind) -> Self {
Error::Db(DbError { kind, origin })
}
}
/// All kinds of errors with witch this crate works.
#[derive(Debug)]
pub enum DbKind {
/// Failed to database connection.
DatabaseConnection,
/// Failed to open transaction.
OpenTransaction,
/// Failed to commit transaction.
CommitTransaction,
/// Failed to rollback transaction.
RollbackTransaction,
/// Failed to create a migrations table.
CreateMigrationsTable,
/// Failed to apply SQL.
ApplySql,
/// Failed to insert a migration.
InsertMigration,
/// Failed to delete a migration.
DeleteMigration,
/// Failed to get applied migrations.
GetAppliedMigrations,
}
impl fmt::Display for DbKind {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DbKind::DatabaseConnection => fmt.write_str("Failed database connection"),
DbKind::OpenTransaction => fmt.write_str("Failed to open a transaction"),
DbKind::CommitTransaction => fmt.write_str("Failed to commit a transaction"),
DbKind::RollbackTransaction => fmt.write_str("Failed to rollback a transaction"),
DbKind::CreateMigrationsTable => fmt.write_str("Failed to create a migrations table"),
DbKind::ApplySql => fmt.write_str("Failed to apply sql"),
DbKind::InsertMigration => fmt.write_str("Failed to insert a migration"),
DbKind::DeleteMigration => fmt.write_str("Failed to delete a migration"),
DbKind::GetAppliedMigrations => fmt.write_str("Failed to get applied migrations"),
}
}
}
/// Represents database error.
#[derive(Debug)]
pub struct DbError {
kind: DbKind,
origin: StdError,
}
impl fmt::Display for DbError {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(fmt, "{} - {}", &self.kind, &self.origin)
}
}
impl DbError {
/// Returns database error kind.
#[must_use]
pub fn kind(&self) -> &DbKind {
&self.kind
}
/// Returns origin database error.
#[must_use]
pub fn origin(&self) -> &StdError {
&self.origin
}
}

34
migra/src/fs.rs Normal file
View File

@ -0,0 +1,34 @@
use crate::errors::MigraResult;
use crate::migration;
use std::io;
use std::path::Path;
/// Checks if the directory is a migration according to the principles of the crate.
#[must_use]
pub fn is_migration_dir(path: &Path) -> bool {
path.join("up.sql").exists() && path.join("down.sql").exists()
}
/// Get all migration directories from path and returns as [List].
///
/// This utility checks if the directory is a migration. See [`is_migration_dir`] for
/// more information.
///
/// [List]: migration::List
/// [is_migration_dir]: fs::is_migration_dir
pub fn get_all_migrations(dir_path: &Path) -> MigraResult<migration::List> {
let mut entries = match dir_path.read_dir() {
Err(e) if e.kind() == io::ErrorKind::NotFound => vec![],
entries => entries?
.filter_map(|res| res.ok().map(|e| e.path()))
.filter(|path| is_migration_dir(&path))
.collect::<Vec<_>>(),
};
if entries.is_empty() {
return Ok(migration::List::new());
}
entries.sort();
Ok(migration::List::from(entries))
}

97
migra/src/lib.rs Normal file
View File

@ -0,0 +1,97 @@
//! # Migra
//!
//! Migra is a simple library for managing SQL in your application.
//!
//! For example, if you have a task list application, you can update the local user database from version to version.
//!
//! This is main crate for [migra-cli](https://crates.io/crates/migra-cli), which allows you to manege SQL for web
//! servers in any program language without being bound to SQL frameworks.
//!
//! ## Installation
//!
//! Add `migra = { version = "1.0" }` as a dependency in `Cargo.toml`.
//!
//! This crate has not required predefined database clients in features with similar name.
//! If you want to add them, just install crate with additional features (`postgres`, `mysql`, `sqlite`).
//!
//! `Cargo.toml` example:
//!
//! ```toml
//! [package]
//! name = "my-crate"
//! version = "0.1.0"
//! authors = ["Me <user@rust-lang.org>"]
//!
//! [dependencies]
//! migra = { version = "1.0", features = ["postgres"] }
//! ```
//!
//! ## Basic usage
//!
//! **Note:** This example requires to enable `sqlite` feature.
//!
//! ```rust
//! use migra::clients::{OpenDatabaseConnection, SqliteClient};
//! use migra::managers::{ManageTransaction, ManageMigrations};
//!
//! fn main() -> migra::Result<()> {
//! let mut client = SqliteClient::new(":memory:")?;
//!
//! client.create_migrations_table()?;
//!
//! let mut migrations = client.get_applied_migrations()?;
//!
//! client
//! .begin_transaction()
//! .and_then(|_| {
//! migrations.should_run_upgrade_migration(
//! &mut client,
//! "20210615_initial_migration",
//! r#"CREATE TABLE IF NOT EXISTS tasks (
//! title TEXT NOT NULL
//! );"#,
//! )?;
//!
//! Ok(())
//! })
//! .and_then(|res| client.commit_transaction().and(Ok(res)))
//! .or_else(|err| client.rollback_transaction().and(Err(err)));
//!
//! Ok(())
//! }
//! ```
//!
//! ### Supported databases
//!
//! | Database Client | Feature |
//! |-----------------|--------------|
//! | `Postgres` | postgres |
//! | `MySQL` | mysql |
//! | `Sqlite` | sqlite |
//!
#![deny(missing_debug_implementations)]
#![deny(missing_docs)]
#![deny(clippy::all, clippy::pedantic)]
// TODO: add missing errors doc
#![allow(clippy::missing_errors_doc)]
/// Includes additional client tools and contains predefined
/// database clients that have been enabled in the features.
pub mod clients;
/// Includes all types of errors that uses in the crate.
pub mod errors;
/// Includes utilities that use the file system to work.
pub mod fs;
/// Includes all the basic traits that will allow you
/// to create your own client.
pub mod managers;
/// Includes basic structures of migration and migration
/// lists, that are used in managers and fs utils.
pub mod migration;
pub use errors::{Error, MigraResult as Result, StdResult};
pub use migration::{List as MigrationList, Migration};

74
migra/src/managers.rs Normal file
View File

@ -0,0 +1,74 @@
use crate::errors::{DbKind, Error, MigraResult, StdResult};
use crate::migration;
/// Used to execute SQL.
///
/// Is a super trait for managers.
pub trait BatchExecute {
/// Executes sql via original database client
fn batch_execute(&mut self, sql: &str) -> StdResult<()>;
}
/// Used to manage transaction in the database connection.
pub trait ManageTransaction: BatchExecute {
/// Opens transaction in database connection.
fn begin_transaction(&mut self) -> MigraResult<()> {
self.batch_execute("BEGIN")
.map_err(|err| Error::db(err, DbKind::OpenTransaction))
}
/// Cancels (Rollbacks) transaction in database connection.
fn rollback_transaction(&mut self) -> MigraResult<()> {
self.batch_execute("ROLLBACK")
.map_err(|err| Error::db(err, DbKind::RollbackTransaction))
}
/// Apply (Commit) transaction in database connection.
fn commit_transaction(&mut self) -> MigraResult<()> {
self.batch_execute("COMMIT")
.map_err(|err| Error::db(err, DbKind::CommitTransaction))
}
}
/// Used to manage migrations in the database connection.
pub trait ManageMigrations: BatchExecute {
/// Applies SQL. Similar to [`BatchExecute`], but returns migra [Error].
///
/// [BatchExecute]: managers::BatchExecute
fn apply_sql(&mut self, sql: &str) -> MigraResult<()> {
self.batch_execute(sql)
.map_err(|err| Error::db(err, DbKind::ApplySql))
}
/// Creates migration table.
fn create_migrations_table(&mut self) -> MigraResult<()>;
/// Inserts new migration to table.
fn insert_migration(&mut self, name: &str) -> MigraResult<u64>;
/// Deletes migration from table.
fn delete_migration(&mut self, name: &str) -> MigraResult<u64>;
/// Get applied migrations from table.
fn get_applied_migrations(&mut self) -> MigraResult<migration::List>;
/// Applies SQL to upgrade database schema and inserts new migration to table.
///
/// **Note:** Must be run in a transaction otherwise if the migration causes any
/// error the data in the database may be inconsistent.
fn run_upgrade_migration(&mut self, name: &str, content: &str) -> MigraResult<()> {
self.apply_sql(content)?;
self.insert_migration(name)?;
Ok(())
}
/// Applies SQL to downgrade database schema and deletes migration from table.
///
/// **Note:** Must be run in a transaction otherwise if the migration causes any
/// error the data in the database may be inconsistent.
fn run_downgrade_migration(&mut self, name: &str, content: &str) -> MigraResult<()> {
self.apply_sql(content)?;
self.delete_migration(name)?;
Ok(())
}
}

242
migra/src/migration.rs Normal file
View File

@ -0,0 +1,242 @@
use crate::errors::MigraResult;
use crate::managers::ManageMigrations;
use std::iter::FromIterator;
/// A simple wrap over string.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Migration {
name: String,
}
impl Migration {
/// Creates new migration by name.
#[must_use]
pub fn new(name: &str) -> Self {
Migration {
name: name.to_owned(),
}
}
/// Returns name of migration.
#[must_use]
pub fn name(&self) -> &String {
&self.name
}
}
/// Wrap over migration vector. Can be implicitly converted to a vector and has
/// a few of additional utilities for handling migrations.
///
/// Can be presented as a list of all migrations, a list of pending migrations
/// or a list of applied migrations, depending on the implementation.
///
///
///
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct List {
inner: Vec<Migration>,
}
impl<T: AsRef<std::path::Path>> From<Vec<T>> for List {
fn from(list: Vec<T>) -> Self {
List {
inner: list
.iter()
.map(AsRef::as_ref)
.map(|path| {
path.file_name()
.and_then(std::ffi::OsStr::to_str)
.expect("Cannot read migration name")
})
.map(Migration::new)
.collect(),
}
}
}
impl From<Vec<Migration>> for List {
fn from(list: Vec<Migration>) -> Self {
List { inner: list }
}
}
impl FromIterator<Migration> for List {
fn from_iter<I: IntoIterator<Item = Migration>>(iter: I) -> Self {
let mut list = List::new();
for item in iter {
list.push(item);
}
list
}
}
impl<'a> FromIterator<&'a Migration> for List {
fn from_iter<I: IntoIterator<Item = &'a Migration>>(iter: I) -> Self {
let mut list = List::new();
for item in iter {
list.push(item.clone());
}
list
}
}
impl std::ops::Deref for List {
type Target = Vec<Migration>;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl List {
/// Creates empty migration list.
#[must_use]
pub fn new() -> Self {
List { inner: Vec::new() }
}
/// Push migration to list.
pub fn push(&mut self, migration: Migration) {
self.inner.push(migration)
}
/// Push migration name to list.
///
/// # Example
///
/// ```rust
/// # use migra::migration::List;
/// # let mut list = List::new();
/// list.push_name("name");
/// # assert_eq!(list, List::from(vec!["name"]));
/// ```
///
/// Is identical to the following
/// ```rust
/// # use migra::migration::{List, Migration};
/// # let mut list = List::new();
/// list.push(Migration::new("name"));
/// # assert_eq!(list, List::from(vec!["name"]));
/// ```
pub fn push_name(&mut self, name: &str) {
self.inner.push(Migration::new(name))
}
/// Check if list contains specific migration.
#[must_use]
pub fn contains(&self, other_migration: &Migration) -> bool {
self.inner
.iter()
.any(|migration| migration == other_migration)
}
/// Check if list contains migration with specific name.
#[must_use]
pub fn contains_name(&self, name: &str) -> bool {
self.inner.iter().any(|migration| migration.name() == name)
}
/// Exclude specific list from current list.
#[must_use]
pub fn exclude(&self, list: &List) -> List {
self.inner
.iter()
.filter(|migration| !list.contains_name(migration.name()))
.collect()
}
/// Runs a upgrade migration with SQL content and adds a new migration to the current list
/// If there is no migration migration with specific name in the list.
pub fn should_run_upgrade_migration(
&mut self,
client: &mut dyn ManageMigrations,
name: &str,
content: &str,
) -> MigraResult<bool> {
let is_missed = !self.contains_name(name);
if is_missed {
client.run_upgrade_migration(name, content)?;
self.push_name(name);
}
Ok(is_missed)
}
/// Runs a downgrade migration with SQL content and removes the last migration from the
/// current list if the last item in the list has the specified name.
pub fn should_run_downgrade_migration(
&mut self,
client: &mut dyn ManageMigrations,
name: &str,
content: &str,
) -> MigraResult<bool> {
let is_latest = self.inner.last() == Some(&Migration::new(name));
if is_latest {
client.run_downgrade_migration(name, content)?;
self.inner.pop();
}
Ok(is_latest)
}
}
#[cfg(test)]
mod tests {
use super::*;
const FIRST_MIGRATION: &str = "initial_migration";
const SECOND_MIGRATION: &str = "new_migration";
#[test]
fn push_migration_to_list() {
let mut list = List::new();
list.push(Migration::new(FIRST_MIGRATION));
assert_eq!(list, List::from(vec![FIRST_MIGRATION]));
list.push(Migration::new(SECOND_MIGRATION));
assert_eq!(list, List::from(vec![FIRST_MIGRATION, SECOND_MIGRATION]))
}
#[test]
fn push_name_to_list() {
let mut list = List::new();
list.push_name(FIRST_MIGRATION);
assert_eq!(list, List::from(vec![FIRST_MIGRATION]));
list.push_name(&String::from(SECOND_MIGRATION));
assert_eq!(list, List::from(vec![FIRST_MIGRATION, SECOND_MIGRATION]))
}
#[test]
fn contains_migration() {
let list = List::from(vec![FIRST_MIGRATION]);
assert_eq!(list.contains(&Migration::new(FIRST_MIGRATION)), true);
assert_eq!(list.contains(&Migration::new(SECOND_MIGRATION)), false);
}
#[test]
fn contains_migration_name() {
let list = List::from(vec![FIRST_MIGRATION]);
assert_eq!(list.contains_name(FIRST_MIGRATION), true);
assert_eq!(list.contains_name(SECOND_MIGRATION), false);
}
#[test]
fn create_excluded_migration_list() {
let all_migrations = List::from(vec![FIRST_MIGRATION, SECOND_MIGRATION]);
let applied_migrations = List::from(vec![FIRST_MIGRATION]);
let excluded = all_migrations.exclude(&applied_migrations);
assert_eq!(excluded, List::from(vec![SECOND_MIGRATION]))
}
}

View File

@ -13,20 +13,27 @@ readme = "../README.md"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
default = ["postgres"]
postgres = ["migra/postgres"]
sqlite = ["migra/sqlite"]
mysql = ["migra/mysql"]
[dependencies]
migra = { version = "1", path = "../migra" }
cfg-if = "1.0"
structopt = "0.3"
serde = { version = "1.0", features = ["derive"] }
toml = "0.5"
chrono = "0.4"
dotenv = { version = "0.15", optional = true }
postgres = { version = "0.19", optional = true }
mysql = { version = "20.1", optional = true }
rusqlite = { version = "0.25", optional = true }
[features]
default = ["postgres"]
sqlite = ["rusqlite"]
[dev-dependencies]
assert_cmd = "1"
predicates = "1"
client_postgres = { package = "postgres", version = "0.19" }
client_mysql = { package = "mysql", version = "20.1" }
client_rusqlite = { package = "rusqlite", version = "0.25" }
[badges]
maintenance = { status = "actively-developed" }
@ -38,7 +45,3 @@ path = "src/main.rs"
[[test]]
name = "integration"
path = "tests/commands.rs"
[dev-dependencies]
assert_cmd = "1"
predicates = "1"

View File

@ -1,5 +1,5 @@
use crate::commands;
use crate::error::*;
use crate::error::MigraResult;
use crate::opts::Command;
use crate::AppOpt;
use crate::Config;
@ -24,24 +24,24 @@ impl App {
Config::read(self.config_path())
}
pub fn run_command(&self) -> StdResult<()> {
match dbg!(self.app_opt.command.clone()) {
pub fn run_command(&self) -> migra::StdResult<()> {
match self.app_opt.command.clone() {
Command::Init => {
commands::initialize_migra_manifest(self)?;
}
Command::Apply(cmd_opts) => {
Command::Apply(ref cmd_opts) => {
commands::apply_sql(self, cmd_opts)?;
}
Command::Make(cmd_opts) => {
Command::Make(ref cmd_opts) => {
commands::make_migration(self, cmd_opts)?;
}
Command::List => {
commands::print_migration_lists(self)?;
}
Command::Upgrade(cmd_opts) => {
Command::Upgrade(ref cmd_opts) => {
commands::upgrade_pending_migrations(self, cmd_opts)?;
}
Command::Downgrade(cmd_opts) => {
Command::Downgrade(ref cmd_opts) => {
commands::rollback_applied_migrations(self, cmd_opts)?;
}
Command::Completions(cmd_opts) => {

View File

@ -1,16 +1,10 @@
use crate::app::App;
use crate::database::prelude::*;
use crate::database::transaction::maybe_with_transaction;
use crate::database::{DatabaseConnectionManager, MigrationManager};
use crate::database;
use crate::opts::ApplyCommandOpt;
use crate::StdResult;
pub(crate) fn apply_sql(app: &App, cmd_opts: ApplyCommandOpt) -> StdResult<()> {
pub(crate) fn apply_sql(app: &App, cmd_opts: &ApplyCommandOpt) -> migra::StdResult<()> {
let config = app.config()?;
let mut connection_manager = DatabaseConnectionManager::connect(&config.database)?;
let conn = connection_manager.connection();
let migration_manager = MigrationManager::from(&config);
let mut client = database::create_client_from_config(&config)?;
let file_contents = cmd_opts
.file_paths
@ -26,17 +20,17 @@ pub(crate) fn apply_sql(app: &App, cmd_opts: ApplyCommandOpt) -> StdResult<()> {
.map(std::fs::read_to_string)
.collect::<Result<Vec<_>, _>>()?;
maybe_with_transaction(
database::should_run_in_transaction(
&mut client,
cmd_opts.transaction_opts.single_transaction,
conn,
&mut |conn| {
|client| {
file_contents
.iter()
.try_for_each(|content| {
maybe_with_transaction(
database::should_run_in_transaction(
client,
!cmd_opts.transaction_opts.single_transaction,
conn,
&mut |conn| migration_manager.apply_sql(conn, content),
|client| client.apply_sql(content),
)
})
.map_err(From::from)

View File

@ -0,0 +1,58 @@
use crate::app::App;
use crate::database;
use crate::opts::DowngradeCommandOpt;
use std::cmp;
pub(crate) fn rollback_applied_migrations(
app: &App,
opts: &DowngradeCommandOpt,
) -> migra::StdResult<()> {
let config = app.config()?;
let mut client = database::create_client_from_config(&config)?;
client.create_migrations_table()?;
let migrations_dir_path = config.migration_dir_path();
let applied_migrations = client.get_applied_migrations()?;
let all_migrations = migra::fs::get_all_migrations(&migrations_dir_path)?;
let rollback_migrations_number = if opts.all_migrations {
applied_migrations.len()
} else {
cmp::min(opts.migrations_number, applied_migrations.len())
};
let migrations = applied_migrations[..rollback_migrations_number].to_vec();
let migrations_with_content = migrations
.iter()
.map(|migration| {
let migration_name = migration.name();
let migration_file_path = migrations_dir_path.join(migration_name).join("down.sql");
std::fs::read_to_string(migration_file_path).map(|content| (migration_name, content))
})
.collect::<Result<Vec<_>, _>>()?;
database::should_run_in_transaction(
&mut client,
opts.transaction_opts.single_transaction,
|client| {
migrations_with_content
.iter()
.try_for_each(|(migration_name, content)| {
if all_migrations.contains_name(migration_name) {
println!("downgrade {}...", migration_name);
database::should_run_in_transaction(
client,
!opts.transaction_opts.single_transaction,
|client| client.run_downgrade_migration(migration_name, &content),
)
} else {
Ok(())
}
})
.map_err(From::from)
},
)?;
Ok(())
}

View File

@ -1,21 +1,19 @@
use crate::app::App;
use crate::config::{Config, MIGRA_TOML_FILENAME};
use crate::StdResult;
use std::path::PathBuf;
pub(crate) fn initialize_migra_manifest(app: &App) -> StdResult<()> {
let config_path = app
.config_path()
.cloned()
.map(|mut config_path| {
pub(crate) fn initialize_migra_manifest(app: &App) -> migra::StdResult<()> {
let config_path = app.config_path().cloned().map_or_else(
|| PathBuf::from(MIGRA_TOML_FILENAME),
|mut config_path| {
let ext = config_path.extension();
if config_path.is_dir() || ext.is_none() {
config_path.push(MIGRA_TOML_FILENAME);
}
config_path
})
.unwrap_or_else(|| PathBuf::from(MIGRA_TOML_FILENAME));
},
);
if config_path.exists() {
println!("{} already exists", config_path.to_str().unwrap());

View File

@ -0,0 +1,66 @@
use crate::app::App;
use crate::database;
use crate::error::Error;
use migra::migration;
const EM_DASH: char = '—';
pub(crate) fn print_migration_lists(app: &App) -> migra::StdResult<()> {
let config = app.config()?;
let applied_migrations = match config.database.connection_string() {
Ok(ref database_connection_string) => {
let mut client = database::create_client(
&config.database.client(),
database_connection_string,
&config.migrations.table_name(),
)?;
let applied_migrations = client.get_applied_migrations().unwrap_or_else(|err| {
dbg!(err);
migration::List::new()
});
show_applied_migrations(&applied_migrations);
applied_migrations
}
Err(e) if e == Error::MissedEnvVar(String::new()) => {
eprintln!("WARNING: {}", e);
eprintln!("WARNING: No connection to database");
migration::List::new()
}
Err(e) => panic!("{}", e),
};
println!();
let all_migrations = migra::fs::get_all_migrations(&config.migration_dir_path())?;
let pending_migrations = all_migrations.exclude(&applied_migrations);
show_pending_migrations(&pending_migrations);
Ok(())
}
fn show_applied_migrations(applied_migrations: &migration::List) {
println!("Applied migrations:");
if applied_migrations.is_empty() {
println!("{}", EM_DASH);
} else {
applied_migrations
.iter()
.rev()
.for_each(|migration| println!("{}", migration.name()));
}
}
fn show_pending_migrations(pending_migrations: &migration::List) {
println!("Pending migrations:");
if pending_migrations.is_empty() {
println!("{}", EM_DASH);
} else {
pending_migrations.iter().for_each(|migration| {
println!("{}", migration.name());
});
}
}

View File

@ -1,10 +1,9 @@
use crate::app::App;
use crate::opts::MakeCommandOpt;
use crate::StdResult;
use chrono::Local;
use std::fs;
pub(crate) fn make_migration(app: &App, opts: MakeCommandOpt) -> StdResult<()> {
pub(crate) fn make_migration(app: &App, opts: &MakeCommandOpt) -> migra::StdResult<()> {
let config = app.config()?;
let date_format = config.migrations.date_format();
let formatted_current_timestamp = Local::now().format(&date_format);

View File

@ -0,0 +1,74 @@
use crate::app::App;
use crate::database;
use crate::opts::UpgradeCommandOpt;
use migra::migration;
pub(crate) fn upgrade_pending_migrations(
app: &App,
opts: &UpgradeCommandOpt,
) -> migra::StdResult<()> {
let config = app.config()?;
let mut client = database::create_client_from_config(&config)?;
client.create_migrations_table()?;
let migrations_dir_path = config.migration_dir_path();
let applied_migration_names = client.get_applied_migrations()?;
let all_migrations = migra::fs::get_all_migrations(&migrations_dir_path)?;
let pending_migrations = all_migrations.exclude(&applied_migration_names);
if pending_migrations.is_empty() {
println!("Up to date");
return Ok(());
}
let migrations: migration::List = if let Some(migration_name) = opts.migration_name.clone() {
let target_migration = (*pending_migrations)
.clone()
.into_iter()
.find(|m| m.name() == &migration_name);
if let Some(migration) = target_migration {
vec![migration].into()
} else {
eprintln!(r#"Cannot find migration with "{}" name"#, migration_name);
return Ok(());
}
} else {
let upgrade_migrations_number = opts
.migrations_number
.unwrap_or_else(|| pending_migrations.len());
pending_migrations[..upgrade_migrations_number]
.to_vec()
.into()
};
let migrations_with_content = migrations
.iter()
.map(|migration| {
let migration_name = migration.name();
let migration_file_path = migrations_dir_path.join(migration_name).join("up.sql");
std::fs::read_to_string(migration_file_path).map(|content| (migration_name, content))
})
.collect::<Result<Vec<_>, _>>()?;
database::should_run_in_transaction(
&mut client,
opts.transaction_opts.single_transaction,
|client| {
migrations_with_content
.iter()
.try_for_each(|(migration_name, content)| {
println!("upgrade {}...", migration_name);
database::should_run_in_transaction(
client,
!opts.transaction_opts.single_transaction,
|client| client.run_upgrade_migration(migration_name, &content),
)
})
.map_err(From::from)
},
)?;
Ok(())
}

View File

@ -1,8 +1,7 @@
use crate::database::migration::Migration;
use crate::error::{Error, MigraResult};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use std::{env, fs, io};
use std::{env, fs};
//===========================================================================//
// Internal Config Utils / Macros //
@ -46,13 +45,21 @@ cargo install migra-cli --features ${database_name}"#,
// Database config //
//===========================================================================//
fn is_sqlite_database_file(filename: &str) -> bool {
filename
.rsplit('.')
.next()
.map(|ext| ext.eq_ignore_ascii_case("db"))
== Some(true)
}
fn default_database_connection_env() -> String {
String::from("$DATABASE_URL")
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub(crate) enum SupportedDatabaseClient {
pub enum SupportedDatabaseClient {
#[cfg(feature = "postgres")]
Postgres,
#[cfg(feature = "mysql")]
@ -114,7 +121,7 @@ impl DatabaseConfig {
please_install_with!(feature "mysql")
}
}
} else if connection_string.ends_with(".db") {
} else if is_sqlite_database_file(&connection_string) {
cfg_if! {
if #[cfg(feature = "sqlite")] {
Some(SupportedDatabaseClient::Sqlite)
@ -131,11 +138,13 @@ impl DatabaseConfig {
}
pub fn connection_string(&self) -> MigraResult<String> {
if let Some(connection_env) = self.connection.strip_prefix("$") {
env::var(connection_env).map_err(|_| Error::MissedEnvVar(connection_env.to_string()))
} else {
Ok(self.connection.clone())
}
self.connection.strip_prefix("$").map_or_else(
|| Ok(self.connection.clone()),
|connection_env| {
env::var(connection_env)
.map_err(|_| Error::MissedEnvVar(connection_env.to_string()))
},
)
}
}
@ -174,33 +183,35 @@ impl Default for MigrationsConfig {
impl MigrationsConfig {
pub fn directory(&self) -> String {
if let Some(directory_env) = self.directory.strip_prefix("$") {
env::var(directory_env).unwrap_or_else(|_| {
println!(
"WARN: Cannot read {} variable and use {} directory by default",
directory_env,
self.directory.strip_prefix("$").map_or_else(
|| self.directory.clone(),
|directory_env| {
env::var(directory_env).unwrap_or_else(|_| {
println!(
"WARN: Cannot read {} variable and use {} directory by default",
directory_env,
default_migrations_directory()
);
default_migrations_directory()
);
default_migrations_directory()
})
} else {
self.directory.clone()
}
})
},
)
}
pub fn table_name(&self) -> String {
if let Some(table_name_env) = self.table_name.strip_prefix("$") {
env::var(table_name_env).unwrap_or_else(|_| {
println!(
"WARN: Cannot read {} variable and use {} table_name by default",
table_name_env,
self.table_name.strip_prefix("$").map_or_else(
|| self.table_name.clone(),
|table_name_env| {
env::var(table_name_env).unwrap_or_else(|_| {
println!(
"WARN: Cannot read {} variable and use {} table_name by default",
table_name_env,
default_migrations_table_name()
);
default_migrations_table_name()
);
default_migrations_table_name()
})
} else {
self.table_name.clone()
}
})
},
)
}
pub fn date_format(&self) -> String {
@ -217,7 +228,7 @@ impl MigrationsConfig {
pub(crate) const MIGRA_TOML_FILENAME: &str = "Migra.toml";
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct Config {
pub struct Config {
#[serde(skip)]
manifest_root: PathBuf,
@ -276,26 +287,4 @@ impl Config {
pub fn migration_dir_path(&self) -> PathBuf {
self.directory_path().join(self.migrations.directory())
}
pub fn migrations(&self) -> MigraResult<Vec<Migration>> {
let mut entries = match self.migration_dir_path().read_dir() {
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
entries => entries?
.map(|res| res.map(|e| e.path()))
.collect::<Result<Vec<_>, _>>()?,
};
if entries.is_empty() {
return Ok(vec![]);
}
entries.sort();
let migrations = entries
.iter()
.filter_map(|path| Migration::new(&path))
.collect::<Vec<_>>();
Ok(migrations)
}
}

70
migra_cli/src/database.rs Normal file
View File

@ -0,0 +1,70 @@
use crate::config::SupportedDatabaseClient;
use crate::Config;
#[cfg(feature = "mysql")]
use migra::clients::MysqlClient;
#[cfg(feature = "postgres")]
use migra::clients::PostgresClient;
#[cfg(feature = "sqlite")]
use migra::clients::SqliteClient;
use migra::clients::{AnyClient, OpenDatabaseConnection};
pub fn create_client(
client_kind: &SupportedDatabaseClient,
connection_string: &str,
migrations_table_name: &str,
) -> migra::Result<AnyClient> {
let client: AnyClient = match client_kind {
#[cfg(feature = "postgres")]
SupportedDatabaseClient::Postgres => Box::new(PostgresClient::manual(
connection_string,
migrations_table_name,
)?),
#[cfg(feature = "mysql")]
SupportedDatabaseClient::Mysql => Box::new(MysqlClient::manual(
connection_string,
migrations_table_name,
)?),
#[cfg(feature = "sqlite")]
SupportedDatabaseClient::Sqlite => Box::new(SqliteClient::manual(
connection_string,
migrations_table_name,
)?),
};
Ok(client)
}
pub fn create_client_from_config(config: &Config) -> migra::StdResult<AnyClient> {
create_client(
&config.database.client(),
&config.database.connection_string()?,
&config.migrations.table_name(),
)
.map_err(From::from)
}
pub fn run_in_transaction<TrxFnMut>(client: &mut AnyClient, trx_fn: TrxFnMut) -> migra::Result<()>
where
TrxFnMut: FnOnce(&mut AnyClient) -> migra::Result<()>,
{
client
.begin_transaction()
.and_then(|_| trx_fn(client))
.and_then(|res| client.commit_transaction().and(Ok(res)))
.or_else(|err| client.rollback_transaction().and(Err(err)))
}
pub fn should_run_in_transaction<TrxFnMut>(
client: &mut AnyClient,
is_needed: bool,
trx_fn: TrxFnMut,
) -> migra::Result<()>
where
TrxFnMut: FnOnce(&mut AnyClient) -> migra::Result<()>,
{
if is_needed {
run_in_transaction(client, trx_fn)
} else {
trx_fn(client)
}
}

View File

@ -4,7 +4,6 @@ use std::io;
use std::mem;
use std::result;
pub type StdResult<T> = result::Result<T, Box<dyn std::error::Error>>;
pub type MigraResult<T> = result::Result<T, Error>;
#[derive(Debug)]

32
migra_cli/src/main.rs Normal file
View File

@ -0,0 +1,32 @@
#![deny(clippy::all, clippy::pedantic)]
#![forbid(unsafe_code)]
#[macro_use]
extern crate cfg_if;
#[cfg(not(any(feature = "postgres", feature = "mysql", feature = "sqlite")))]
compile_error!(
r#"Either features "postgres", "mysql" or "sqlite" must be enabled for "migra-cli" crate"#
);
mod app;
mod commands;
mod config;
mod database;
mod error;
pub use error::Error;
mod opts;
use app::App;
use config::Config;
use opts::{AppOpt, StructOpt};
fn main() {
#[cfg(feature = "dotenv")]
dotenv::dotenv().ok();
if let Err(err) = App::new(AppOpt::from_args()).run_command() {
panic!("Error: {}", err);
}
}

View File

@ -1,5 +1,6 @@
pub use assert_cmd::prelude::*;
pub use cfg_if::cfg_if;
use client_mysql::prelude::*;
pub use predicates::str::contains;
pub use std::process::Command;
@ -444,7 +445,7 @@ mod upgrade {
#[cfg(feature = "postgres")]
inner("postgres", || {
let mut conn = postgres::Client::connect(POSTGRES_URL, postgres::NoTls)?;
let mut conn = client_postgres::Client::connect(POSTGRES_URL, client_postgres::NoTls)?;
let res = conn.query("SELECT p.id, a.id FROM persons AS p, articles AS a", &[])?;
assert_eq!(
@ -459,9 +460,7 @@ mod upgrade {
#[cfg(feature = "mysql")]
inner("mysql", || {
use mysql::prelude::*;
let pool = mysql::Pool::new(MYSQL_URL)?;
let pool = client_mysql::Pool::new(MYSQL_URL)?;
let mut conn = pool.get_conn()?;
let res = conn.query_drop("SELECT p.id, a.id FROM persons AS p, articles AS a")?;
@ -474,9 +473,7 @@ mod upgrade {
#[cfg(feature = "sqlite")]
remove_sqlite_db().and_then(|_| {
inner("sqlite", || {
use rusqlite::Connection;
let conn = Connection::open(SQLITE_URL)?;
let conn = client_rusqlite::Connection::open(SQLITE_URL)?;
let res =
conn.execute_batch("SELECT p.id, a.id FROM persons AS p, articles AS a")?;
assert_eq!(res, ());
@ -517,7 +514,7 @@ mod upgrade {
#[cfg(feature = "postgres")]
inner("postgres_invalid", || {
let mut conn = postgres::Client::connect(POSTGRES_URL, postgres::NoTls)?;
let mut conn = client_postgres::Client::connect(POSTGRES_URL, client_postgres::NoTls)?;
let articles_res = conn.query("SELECT a.id FROM articles AS a", &[]);
let persons_res = conn.query("SELECT p.id FROM persons AS p", &[]);
@ -530,9 +527,7 @@ mod upgrade {
#[cfg(feature = "sqlite")]
remove_sqlite_db().and_then(|_| {
inner("sqlite_invalid", || {
use rusqlite::Connection;
let conn = Connection::open(SQLITE_URL)?;
let conn = client_rusqlite::Connection::open(SQLITE_URL)?;
let articles_res = conn.execute_batch("SELECT a.id FROM articles AS a");
let persons_res = conn.execute_batch("SELECT p.id FROM persons AS p");
@ -569,7 +564,7 @@ mod upgrade {
#[cfg(feature = "postgres")]
inner("postgres_invalid", || {
let mut conn = postgres::Client::connect(POSTGRES_URL, postgres::NoTls)?;
let mut conn = client_postgres::Client::connect(POSTGRES_URL, client_postgres::NoTls)?;
let articles_res = conn.query("SELECT a.id FROM articles AS a", &[]);
let persons_res = conn.query("SELECT p.id FROM persons AS p", &[]);
@ -582,9 +577,7 @@ mod upgrade {
#[cfg(feature = "sqlite")]
remove_sqlite_db().and_then(|_| {
inner("sqlite_invalid", || {
use rusqlite::Connection;
let conn = Connection::open(SQLITE_URL)?;
let conn = client_rusqlite::Connection::open(SQLITE_URL)?;
let articles_res = conn.execute_batch("SELECT a.id FROM articles AS a");
let persons_res = conn.execute_batch("SELECT p.id FROM persons AS p");
@ -636,7 +629,7 @@ mod apply {
"migrations/210218233414_create_persons/up",
],
|| {
let mut conn = postgres::Client::connect(POSTGRES_URL, postgres::NoTls)?;
let mut conn = client_postgres::Client::connect(POSTGRES_URL, client_postgres::NoTls)?;
let res = conn.query("SELECT p.id, a.id FROM persons AS p, articles AS a", &[])?;
assert_eq!(
@ -657,7 +650,7 @@ mod apply {
"migrations/210218232851_create_articles/down",
],
|| {
let mut conn = postgres::Client::connect(POSTGRES_URL, postgres::NoTls)?;
let mut conn = client_postgres::Client::connect(POSTGRES_URL, client_postgres::NoTls)?;
let res = conn.query("SELECT p.id, a.id FROM persons AS p, articles AS a", &[]);
assert!(res.is_err());
@ -677,9 +670,7 @@ mod apply {
"migrations/210218233414_create_persons/up",
],
|| {
use mysql::prelude::*;
let pool = mysql::Pool::new(MYSQL_URL)?;
let pool = client_mysql::Pool::new(MYSQL_URL)?;
let mut conn = pool.get_conn()?;
let res = conn.query_drop("SELECT p.id, a.id FROM persons AS p, articles AS a")?;
@ -697,9 +688,7 @@ mod apply {
"migrations/210218232851_create_articles/down",
],
|| {
use mysql::prelude::*;
let pool = mysql::Pool::new(MYSQL_URL)?;
let pool = client_mysql::Pool::new(MYSQL_URL)?;
let mut conn = pool.get_conn()?;
let res = conn.query_drop("SELECT p.id, a.id FROM persons AS p, articles AS a");
@ -721,9 +710,7 @@ mod apply {
"migrations/210218233414_create_persons/up",
],
|| {
use rusqlite::Connection;
let conn = Connection::open(SQLITE_URL)?;
let conn = client_rusqlite::Connection::open(SQLITE_URL)?;
let res =
conn.execute_batch("SELECT p.id, a.id FROM persons AS p, articles AS a")?;
assert_eq!(res, ());
@ -739,9 +726,7 @@ mod apply {
"migrations/210218232851_create_articles/down",
],
|| {
use rusqlite::Connection;
let conn = Connection::open(SQLITE_URL)?;
let conn = client_rusqlite::Connection::open(SQLITE_URL)?;
let res =
conn.execute_batch("SELECT p.id, a.id FROM persons AS p, articles AS a");
assert!(res.is_err());