Restructure SeaORM migration

Bump codegen's sea-query version [cli]

Update CLI subcommand method

Move migration utility into sea-orm-migration
This commit is contained in:
Billy Chan 2022-04-20 14:06:34 +08:00 committed by Chris Tsang
parent 86e7e808b3
commit 498c0154ca
65 changed files with 973 additions and 72 deletions

View File

@ -44,7 +44,7 @@ once_cell = "1.8"
[dev-dependencies]
smol = { version = "^1.2" }
smol-potat = { version = "^1.1" }
async-std = { version = "^1.9", features = ["attributes", "tokio1"] }
async-std = { version = "^1", features = ["attributes", "tokio1"] }
tokio = { version = "^1.6", features = ["full"] }
actix-rt = { version = "2.2.0" }
maplit = { version = "^1" }

View File

@ -12,7 +12,7 @@ path = "src/lib.rs"
serde = { version = "1", features = ["derive"] }
[dependencies.sea-orm]
# path = "../../../" # remove this line in your own project
path = "../../../" # remove this line in your own project
version = "^0.7.0"
features = [
"macros",

View File

@ -9,5 +9,8 @@ name = "migration"
path = "src/lib.rs"
[dependencies]
sea-schema = { version = "^0.7.0", default-features = false, features = [ "migration", "debug-print" ] }
entity = { path = "../entity" }
[dependencies.sea-orm-migration]
path = "../../../sea-orm-migration" # remove this line in your own project
version = "^0.7.0"

View File

@ -1,4 +1,4 @@
pub use sea_schema::migration::prelude::*;
pub use sea_orm_migration::prelude::*;
mod m20220120_000001_create_post_table;

View File

@ -1,5 +1,5 @@
use entity::post::*;
use sea_schema::migration::prelude::*;
use sea_orm_migration::prelude::*;
pub struct Migration;

View File

@ -1,7 +1,6 @@
use migration::Migrator;
use sea_schema::migration::*;
use sea_orm_migration::prelude::*;
#[async_std::main]
async fn main() {
cli::run_cli(Migrator).await;
cli::run_cli(migration::Migrator).await;
}

View File

@ -12,7 +12,7 @@ path = "src/lib.rs"
serde = { version = "1", features = ["derive"] }
[dependencies.sea-orm]
# path = "../../../" # remove this line in your own project
path = "../../../" # remove this line in your own project
version = "^0.7.0"
features = [
"macros",

View File

@ -9,5 +9,8 @@ name = "migration"
path = "src/lib.rs"
[dependencies]
sea-schema = { version = "^0.7.0", default-features = false, features = [ "migration", "debug-print" ] }
entity = { path = "../entity" }
[dependencies.sea-orm-migration]
path = "../../../sea-orm-migration" # remove this line in your own project
version = "^0.7.0"

View File

@ -1,4 +1,4 @@
pub use sea_schema::migration::prelude::*;
pub use sea_orm_migration::prelude::*;
mod m20220120_000001_create_post_table;

View File

@ -1,5 +1,5 @@
use entity::post::*;
use sea_schema::migration::prelude::*;
use sea_orm_migration::prelude::*;
pub struct Migration;

View File

@ -1,7 +1,6 @@
use migration::Migrator;
use sea_schema::migration::*;
use sea_orm_migration::prelude::*;
#[async_std::main]
async fn main() {
cli::run_cli(Migrator).await;
cli::run_cli(migration::Migrator).await;
}

View File

@ -1,6 +1,6 @@
use actix_files::Files as Fs;
use actix_web::{
error, get, middleware, post, web, App, Error, HttpRequest, HttpResponse, HttpServer, Result
error, get, middleware, post, web, App, Error, HttpRequest, HttpResponse, HttpServer, Result,
};
use entity::post;

View File

@ -12,7 +12,7 @@ path = "src/lib.rs"
serde = { version = "1", features = ["derive"] }
[dependencies.sea-orm]
# path = "../../../" # remove this line in your own project
path = "../../../" # remove this line in your own project
version = "^0.7.0"
features = [
"macros",

View File

@ -9,5 +9,8 @@ name = "migration"
path = "src/lib.rs"
[dependencies]
sea-schema = { version = "^0.7.0", default-features = false, features = [ "migration", "debug-print" ] }
entity = { path = "../entity" }
[dependencies.sea-orm-migration]
path = "../../../sea-orm-migration" # remove this line in your own project
version = "^0.7.0"

View File

@ -1,4 +1,4 @@
pub use sea_schema::migration::prelude::*;
pub use sea_orm_migration::prelude::*;
mod m20220120_000001_create_post_table;

View File

@ -1,5 +1,5 @@
use entity::post::*;
use sea_schema::migration::prelude::*;
use sea_orm_migration::prelude::*;
pub struct Migration;

View File

@ -1,7 +1,6 @@
use migration::Migrator;
use sea_schema::migration::*;
use sea_orm_migration::prelude::*;
#[async_std::main]
async fn main() {
cli::run_cli(Migrator).await;
cli::run_cli(migration::Migrator).await;
}

View File

@ -15,6 +15,7 @@ serde = { version = "1", features = ["derive"] }
version = "^3.0.38"
[dependencies.sea-orm]
path = "../../../" # remove this line in your own project
version = "^0.7.0"
features = [
"macros",

View File

@ -9,6 +9,9 @@ name = "migration"
path = "src/lib.rs"
[dependencies]
sea-schema = { version = "^0.7.0", default-features = false, features = [ "migration", "debug-print" ] }
dotenv = "0.15.0"
entity = { path = "../entity" }
[dependencies.sea-orm-migration]
path = "../../../sea-orm-migration" # remove this line in your own project
version = "^0.7.0"

View File

@ -1,4 +1,4 @@
pub use sea_schema::migration::*;
pub use sea_orm_migration::prelude::*;
mod m20220101_000001_create_table;

View File

@ -1,11 +1,8 @@
use sea_orm_migration::prelude::*;
use entity::{
note,
sea_orm::{DbBackend, EntityTrait, Schema},
};
use sea_schema::migration::{
sea_query::*,
*,
};
pub struct Migration;

View File

@ -1,5 +1,4 @@
use migration::Migrator;
use sea_schema::migration::*;
use sea_orm_migration::prelude::*;
use std::path::PathBuf;
#[cfg(debug_assertions)]
@ -22,5 +21,5 @@ async fn main() {
}
};
cli::run_cli(Migrator).await;
cli::run_cli(migration::Migrator).await;
}

View File

@ -12,7 +12,7 @@ path = "src/lib.rs"
serde = { version = "1", features = ["derive"] }
[dependencies.sea-orm]
# path = "../../../" # remove this line in your own project
path = "../../../" # remove this line in your own project
version = "^0.7.0"
features = [
"macros",

View File

@ -9,5 +9,8 @@ name = "migration"
path = "src/lib.rs"
[dependencies]
sea-schema = { version = "^0.7.0", default-features = false, features = [ "migration", "debug-print" ] }
entity = { path = "../entity" }
[dependencies.sea-orm-migration]
path = "../../../sea-orm-migration" # remove this line in your own project
version = "^0.7.0"

View File

@ -1,4 +1,4 @@
pub use sea_schema::migration::prelude::*;
pub use sea_orm_migration::prelude::*;
mod m20220120_000001_create_post_table;

View File

@ -1,5 +1,5 @@
use entity::post::*;
use sea_schema::migration::prelude::*;
use sea_orm_migration::prelude::*;
pub struct Migration;

View File

@ -1,7 +1,6 @@
use migration::Migrator;
use sea_schema::migration::*;
use sea_orm_migration::prelude::*;
#[async_std::main]
async fn main() {
cli::run_cli(Migrator).await;
cli::run_cli(migration::Migrator).await;
}

View File

@ -12,7 +12,7 @@ path = "src/lib.rs"
serde = { version = "1", features = ["derive"] }
[dependencies.sea-orm]
# path = "../../../" # remove this line in your own project
path = "../../../" # remove this line in your own project
version = "^0.7.0"
features = [
"macros",

View File

@ -9,5 +9,8 @@ name = "migration"
path = "src/lib.rs"
[dependencies]
sea-schema = { version = "^0.7.0", default-features = false, features = [ "migration", "debug-print" ] }
entity = { path = "../entity" }
[dependencies.sea-orm-migration]
path = "../../../sea-orm-migration" # remove this line in your own project
version = "^0.7.0"

View File

@ -1,4 +1,4 @@
pub use sea_schema::migration::prelude::*;
pub use sea_orm_migration::prelude::*;
mod m20220120_000001_create_post_table;

View File

@ -1,5 +1,5 @@
use entity::post::*;
use sea_schema::migration::prelude::*;
use sea_orm_migration::prelude::*;
pub struct Migration;

View File

@ -1,7 +1,6 @@
use migration::Migrator;
use sea_schema::migration::*;
use sea_orm_migration::prelude::*;
#[async_std::main]
async fn main() {
cli::run_cli(Migrator).await;
cli::run_cli(migration::Migrator).await;
}

View File

@ -14,7 +14,7 @@ rocket = { version = "0.5.0-rc.1", features = [
] }
[dependencies.sea-orm]
# path = "../../../" # remove this line in your own project
path = "../../../" # remove this line in your own project
version = "^0.7.0"
features = [
"macros",

View File

@ -9,6 +9,9 @@ name = "migration"
path = "src/lib.rs"
[dependencies]
sea-schema = { version = "^0.7.0", default-features = false, features = [ "migration", "debug-print" ] }
entity = { path = "../entity" }
[dependencies.sea-orm-migration]
path = "../../../sea-orm-migration" # remove this line in your own project
version = "^0.7.0"
rocket = { version = "0.5.0-rc.1" }

View File

@ -1,4 +1,4 @@
pub use sea_schema::migration::prelude::*;
pub use sea_orm_migration::prelude::*;
mod m20220120_000001_create_post_table;

View File

@ -1,5 +1,5 @@
use entity::post::*;
use sea_schema::migration::prelude::*;
use sea_orm_migration::prelude::*;
pub struct Migration;

View File

@ -1,5 +1,4 @@
use migration::Migrator;
use sea_schema::migration::prelude::*;
use sea_orm_migration::prelude::*;
#[async_std::main]
async fn main() {
@ -14,5 +13,5 @@ async fn main() {
std::env::set_var(key, database_url);
}
cli::run_cli(Migrator).await;
cli::run_cli(migration::Migrator).await;
}

View File

@ -14,7 +14,6 @@ tonic = "0.7"
tokio = { version = "1.17", features = ["macros", "rt-multi-thread", "full"] }
entity = { path = "entity" }
migration = { path = "migration" }
sea-orm = { version = "0.7.1", features = [ "sqlx-postgres", "runtime-tokio-rustls", "macros" ] }
prost = "0.10.0"
serde = "1.0"

View File

@ -12,7 +12,7 @@ path = "src/lib.rs"
serde = { version = "1", features = ["derive"] }
[dependencies.sea-orm]
# path = "../../../" # remove this line in your own project
path = "../../../" # remove this line in your own project
version = "^0.7.0"
features = [
"macros",

View File

@ -9,5 +9,8 @@ name = "migration"
path = "src/lib.rs"
[dependencies]
sea-schema = { version = "^0.7.0", default-features = false, features = [ "migration", "debug-print" ] }
entity = { path = "../entity" }
[dependencies.sea-orm-migration]
path = "../../../sea-orm-migration" # remove this line in your own project
version = "^0.7.0"

View File

@ -1,4 +1,4 @@
pub use sea_schema::migration::prelude::*;
pub use sea_orm_migration::prelude::*;
mod m20220120_000001_create_post_table;

View File

@ -1,5 +1,5 @@
use entity::post::*;
use sea_schema::migration::prelude::*;
use sea_orm_migration::prelude::*;
pub struct Migration;

View File

@ -1,7 +1,6 @@
use migration::Migrator;
use sea_schema::migration::*;
use sea_orm_migration::prelude::*;
#[async_std::main]
async fn main() {
cli::run_cli(Migrator).await;
cli::run_cli(migration::Migrator).await;
}

View File

@ -8,7 +8,7 @@ use sea_orm_tonic_example::post::{
use entity::{
post::{self, Entity as PostEntity},
sea_orm::{entity::*, query::*, DatabaseConnection},
sea_orm::{self, entity::*, query::*, DatabaseConnection},
};
use migration::{Migrator, MigratorTrait};

View File

@ -32,7 +32,7 @@ clap = { version = "^2.33.3" }
dotenv = { version = "^0.15" }
async-std = { version = "^1.9", features = [ "attributes", "tokio1" ] }
sea-orm-codegen = { version = "^0.7.0", path = "../sea-orm-codegen" }
sea-schema = { version = "^0.7.0", default-features = false, features = [
sea-schema = { git = "https://github.com/SeaQL/sea-schema", branch = "dump-sea-orm-dep", default-features = false, features = [
"debug-print",
"sqlx-mysql",
"sqlx-sqlite",

View File

@ -1,4 +1,5 @@
use clap::{App, AppSettings, Arg, SubCommand};
use sea_schema::get_cli_subcommands;
pub fn build_cli() -> App<'static, 'static> {
let entity_subcommand = SubCommand::with_name("generate")
@ -95,7 +96,7 @@ pub fn build_cli() -> App<'static, 'static> {
.arg(arg_migration_dir.clone()),
)
.arg(arg_migration_dir.clone());
for subcommand in sea_schema::migration::get_subcommands() {
for subcommand in get_cli_subcommands!() {
migrate_subcommands =
migrate_subcommands.subcommand(subcommand.arg(arg_migration_dir.clone()));
}

View File

@ -9,4 +9,8 @@ name = "migration"
path = "src/lib.rs"
[dependencies]
sea-schema = { version = "^0.7.0", default-features = false, features = [ "migration", "debug-print" ] }
entity = { path = "../entity" }
[dependencies.sea-orm-migration]
path = "../../../sea-orm-migration" # remove this line in your own project
version = "^0.7.0"

View File

@ -1,4 +1,4 @@
pub use sea_schema::migration::prelude::*;
pub use sea_orm_migration::prelude::*;
mod m20220101_000001_create_table;

View File

@ -1,4 +1,4 @@
use sea_schema::migration::prelude::*;
use sea_orm_migration::prelude::*;
pub struct Migration;

View File

@ -1,7 +1,6 @@
use migration::Migrator;
use sea_schema::migration::prelude::*;
use sea_orm_migration::prelude::*;
#[async_std::main]
async fn main() {
cli::run_cli(Migrator).await;
cli::run_cli(migration::Migrator).await;
}

View File

@ -15,7 +15,7 @@ name = "sea_orm_codegen"
path = "src/lib.rs"
[dependencies]
sea-query = { version = "0.22.0" }
sea-query = { version = "0.24.0" }
syn = { version = "^1", default-features = false, features = [
"derive",
"parsing",

View File

@ -0,0 +1,28 @@
[workspace]
# A separate workspace
[package]
name = "sea-orm-migration"
version = "0.7.2"
authors = [ "Billy Chan <ccw.billy.123@gmail.com>" ]
edition = "2021"
description = "Migration utility for SeaORM"
license = "MIT OR Apache-2.0"
documentation = "https://docs.rs/sea-orm"
repository = "https://github.com/SeaQL/sea-orm"
categories = [ "database" ]
keywords = ["async", "orm", "mysql", "postgres", "sqlite"]
[lib]
name = "sea_orm_migration"
path = "src/lib.rs"
[dependencies]
sea-orm = { path = "../", default-features = false, features = ["macros"] }
sea-schema = { git = "https://github.com/SeaQL/sea-schema", branch = "dump-sea-orm-dep", features = ["migration"], default-features = false }
async-std = { version = "^1", features = ["attributes", "tokio1"] }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tracing = { version = "0.1", features = ["log"] }
async-trait = { version = "^0.1" }
dotenv = { version = "^0.15" }
clap = { version = "^2.33" }

View File

@ -0,0 +1,77 @@
# SeaORM CLI
Install and Usage:
```sh
> cargo install sea-orm-cli
> sea-orm-cli help
```
Or:
```sh
> cargo install --bin sea
> sea help
```
Getting Help:
```sh
cargo run -- -h
```
## Running Entity Generator:
```sh
# MySQL (`--database-schema` option is ignored)
cargo run -- generate entity -u mysql://sea:sea@localhost/bakery -o out
# SQLite (`--database-schema` option is ignored)
cargo run -- generate entity -u sqlite://bakery.db -o out
# PostgreSQL
cargo run -- generate entity -u postgres://sea:sea@localhost/bakery -s public -o out
```
## Running Migration:
- Initialize migration directory
```sh
cargo run -- migrate init
```
- Apply all pending migrations
```sh
cargo run -- migrate
```
```sh
cargo run -- migrate up
```
- Apply first 10 pending migrations
```sh
cargo run -- migrate up -n 10
```
- Rollback last applied migrations
```sh
cargo run -- migrate down
```
- Rollback last 10 applied migrations
```sh
cargo run -- migrate down -n 10
```
- Drop all tables from the database, then reapply all migrations
```sh
cargo run -- migrate fresh
```
- Rollback all applied migrations, then reapply all migrations
```sh
cargo run -- migrate refresh
```
- Rollback all applied migrations
```sh
cargo run -- migrate reset
```
- Check the status of all migrations
```sh
cargo run -- migrate status
```

View File

@ -0,0 +1,103 @@
//! Run migrator CLI
use crate::MigratorTrait;
use clap::{App, AppSettings, Arg};
use dotenv::dotenv;
use sea_orm::{Database, DbConn};
use sea_schema::get_cli_subcommands;
use std::{fmt::Display, process::exit};
use tracing_subscriber::{prelude::*, EnvFilter};
#[allow(dead_code)]
/// Migrator CLI application
pub async fn run_cli<M>(migrator: M)
where
M: MigratorTrait,
{
dotenv().ok();
let url = std::env::var("DATABASE_URL").expect("Environment variable 'DATABASE_URL' not set");
let db = &Database::connect(&url).await.unwrap();
let app = build_cli();
get_matches(migrator, db, app).await;
}
async fn get_matches<M>(_: M, db: &DbConn, app: App<'static, 'static>)
where
M: MigratorTrait,
{
let matches = app.get_matches();
let mut verbose = false;
let filter = match matches.subcommand() {
(_, None) => "sea_orm::migration=info",
(_, Some(args)) => match args.is_present("VERBOSE") {
true => {
verbose = true;
"debug"
}
false => "sea_orm::migration=info",
},
};
let filter_layer = EnvFilter::try_new(filter).unwrap();
if verbose {
let fmt_layer = tracing_subscriber::fmt::layer();
tracing_subscriber::registry()
.with(filter_layer)
.with(fmt_layer)
.init()
} else {
let fmt_layer = tracing_subscriber::fmt::layer()
.with_target(false)
.with_level(false)
.without_time();
tracing_subscriber::registry()
.with(filter_layer)
.with(fmt_layer)
.init()
};
match matches.subcommand() {
("fresh", _) => M::fresh(db).await,
("refresh", _) => M::refresh(db).await,
("reset", _) => M::reset(db).await,
("status", _) => M::status(db).await,
("up", None) => M::up(db, None).await,
("down", None) => M::down(db, Some(1)).await,
("up", Some(args)) => {
let str = args.value_of("NUM_MIGRATION").unwrap_or_default();
let steps = str.parse().ok();
M::up(db, steps).await
}
("down", Some(args)) => {
let str = args.value_of("NUM_MIGRATION").unwrap();
let steps = str.parse().ok().unwrap_or(1);
M::down(db, Some(steps)).await
}
_ => M::up(db, None).await,
}
.unwrap_or_else(handle_error);
}
fn build_cli() -> App<'static, 'static> {
let mut app = App::new("sea-schema-migration")
.version(env!("CARGO_PKG_VERSION"))
.setting(AppSettings::VersionlessSubcommands)
.arg(
Arg::with_name("VERBOSE")
.long("verbose")
.short("v")
.help("Show debug messages")
.takes_value(false)
.global(true),
);
for subcommand in get_cli_subcommands!() {
app = app.subcommand(subcommand);
}
app
}
fn handle_error<E>(error: E)
where
E: Display,
{
eprintln!("{}", error);
exit(1);
}

View File

@ -0,0 +1,48 @@
//! Manage migration connection
use crate::{into_migration_db_backend, into_migration_err, into_orm_stmt, QueryResult};
use sea_orm::ConnectionTrait;
use sea_schema::migration::{self, MigrationErr, QueryResultTrait};
#[derive(Debug)]
pub(crate) struct DatabaseConnection<'c> {
pub(crate) conn: &'c sea_orm::DatabaseConnection,
}
#[async_trait::async_trait]
impl migration::ConnectionTrait for DatabaseConnection<'_> {
fn get_database_backend(&self) -> migration::DbBackend {
into_migration_db_backend(ConnectionTrait::get_database_backend(self.conn))
}
async fn execute(&self, stmt: migration::Statement) -> Result<(), MigrationErr> {
ConnectionTrait::execute(self.conn, into_orm_stmt(stmt))
.await
.map(|_| ())
.map_err(|e| MigrationErr(e.to_string()))
}
async fn query_one(
&self,
stmt: migration::Statement,
) -> Result<Option<Box<dyn QueryResultTrait>>, MigrationErr> {
ConnectionTrait::query_one(self.conn, into_orm_stmt(stmt))
.await
.map(|res| res.map(|res| Box::new(QueryResult { res }) as Box<dyn QueryResultTrait>))
.map_err(into_migration_err)
}
async fn query_all(
&self,
stmt: migration::Statement,
) -> Result<Vec<Box<dyn QueryResultTrait>>, MigrationErr> {
ConnectionTrait::query_all(self.conn, into_orm_stmt(stmt))
.await
.map(|rows| {
rows.into_iter()
.map(|res| Box::new(QueryResult { res }) as Box<dyn QueryResultTrait>)
.collect()
})
.map_err(into_migration_err)
}
}

View File

@ -0,0 +1,19 @@
//! Convert database backend
use sea_schema::migration;
pub(crate) fn into_orm_db_backend(db_backend: migration::DbBackend) -> sea_orm::DbBackend {
match db_backend {
migration::DbBackend::MySql => sea_orm::DbBackend::MySql,
migration::DbBackend::Postgres => sea_orm::DbBackend::Postgres,
migration::DbBackend::Sqlite => sea_orm::DbBackend::Sqlite,
}
}
pub(crate) fn into_migration_db_backend(db_backend: sea_orm::DbBackend) -> migration::DbBackend {
match db_backend {
sea_orm::DbBackend::MySql => migration::DbBackend::MySql,
sea_orm::DbBackend::Postgres => migration::DbBackend::Postgres,
sea_orm::DbBackend::Sqlite => migration::DbBackend::Sqlite,
}
}

View File

@ -0,0 +1,11 @@
//! Convert migration error
use sea_schema::migration;
pub(crate) fn into_orm_db_err(err: migration::MigrationErr) -> sea_orm::DbErr {
sea_orm::DbErr::Migration(err.to_string())
}
pub(crate) fn into_migration_err(err: sea_orm::DbErr) -> migration::MigrationErr {
migration::MigrationErr(err.to_string())
}

View File

@ -0,0 +1,36 @@
pub mod cli;
pub mod connection;
pub mod database;
pub mod error;
pub mod manager;
pub mod migrator;
pub mod prelude;
pub mod query;
pub mod seaql_migrations;
pub mod statement;
pub use manager::*;
pub use migrator::*;
use connection::*;
use database::*;
use error::*;
use query::*;
use statement::*;
use sea_orm::DbErr;
/// Define the name of a migration
pub trait MigrationName {
/// Get migration name
fn name(&self) -> &str;
}
/// Define the actions of a migration
#[async_trait::async_trait]
pub trait MigrationTrait: MigrationName + Send + Sync {
/// Define actions to perform when applying the migration
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr>;
/// Define actions to perform when rolling back the migration
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr>;
}

View File

@ -0,0 +1,163 @@
//! Schema manager
use crate::{into_orm_db_err, DatabaseConnection};
use sea_orm::sea_query::{
extension::postgres::{TypeAlterStatement, TypeCreateStatement, TypeDropStatement},
ForeignKeyCreateStatement, ForeignKeyDropStatement, IndexCreateStatement, IndexDropStatement,
TableAlterStatement, TableCreateStatement, TableDropStatement, TableRenameStatement,
TableTruncateStatement,
};
use sea_orm::{ConnectionTrait, DbBackend, DbConn, DbErr, StatementBuilder};
use sea_schema::migration;
/// Helper struct for writing migration scripts in migration file
#[derive(Debug)]
pub struct SchemaManager<'c> {
conn: DatabaseConnection<'c>,
}
impl<'c> SchemaManager<'c> {
/// Initialize [`SchemaManager`]
pub fn new(conn: &'c DbConn) -> Self {
Self {
conn: DatabaseConnection { conn },
}
}
/// Execute any statement that implemented [`StatementBuilder`]
pub async fn exec_stmt<S>(&self, stmt: S) -> Result<(), DbErr>
where
S: StatementBuilder,
{
let builder = self.conn.conn.get_database_backend();
self.conn
.conn
.execute(builder.build(&stmt))
.await
.map(|_| ())
}
/// Get database backend
pub fn get_database_backend(&self) -> DbBackend {
self.conn.conn.get_database_backend()
}
/// Borrow database connection
pub fn get_connection(&self) -> &'c DbConn {
self.conn.conn
}
}
/// Schema Creation
impl<'c> SchemaManager<'c> {
/// Create table
pub async fn create_table(&self, stmt: TableCreateStatement) -> Result<(), DbErr> {
migration::SchemaManager::create_table(stmt, &self.conn)
.await
.map_err(into_orm_db_err)
}
/// Create index
pub async fn create_index(&self, stmt: IndexCreateStatement) -> Result<(), DbErr> {
migration::SchemaManager::create_index(stmt, &self.conn)
.await
.map_err(into_orm_db_err)
}
/// Create foreign key
pub async fn create_foreign_key(&self, stmt: ForeignKeyCreateStatement) -> Result<(), DbErr> {
migration::SchemaManager::create_foreign_key(stmt, &self.conn)
.await
.map_err(into_orm_db_err)
}
/// Create type
pub async fn create_type(&self, stmt: TypeCreateStatement) -> Result<(), DbErr> {
migration::SchemaManager::create_type(stmt, &self.conn)
.await
.map_err(into_orm_db_err)
}
}
/// Schema Mutation
impl<'c> SchemaManager<'c> {
/// Alter table
pub async fn alter_table(&self, stmt: TableAlterStatement) -> Result<(), DbErr> {
migration::SchemaManager::alter_table(stmt, &self.conn)
.await
.map_err(into_orm_db_err)
}
/// Drop table
pub async fn drop_table(&self, stmt: TableDropStatement) -> Result<(), DbErr> {
migration::SchemaManager::drop_table(stmt, &self.conn)
.await
.map_err(into_orm_db_err)
}
/// Rename table
pub async fn rename_table(&self, stmt: TableRenameStatement) -> Result<(), DbErr> {
migration::SchemaManager::rename_table(stmt, &self.conn)
.await
.map_err(into_orm_db_err)
}
/// Truncate table
pub async fn truncate_table(&self, stmt: TableTruncateStatement) -> Result<(), DbErr> {
migration::SchemaManager::truncate_table(stmt, &self.conn)
.await
.map_err(into_orm_db_err)
}
/// Drop index
pub async fn drop_index(&self, stmt: IndexDropStatement) -> Result<(), DbErr> {
migration::SchemaManager::drop_index(stmt, &self.conn)
.await
.map_err(into_orm_db_err)
}
/// Drop foreign key
pub async fn drop_foreign_key(&self, stmt: ForeignKeyDropStatement) -> Result<(), DbErr> {
migration::SchemaManager::drop_foreign_key(stmt, &self.conn)
.await
.map_err(into_orm_db_err)
}
/// Alter type
pub async fn alter_type(&self, stmt: TypeAlterStatement) -> Result<(), DbErr> {
migration::SchemaManager::alter_type(stmt, &self.conn)
.await
.map_err(into_orm_db_err)
}
/// Drop type
pub async fn drop_type(&self, stmt: TypeDropStatement) -> Result<(), DbErr> {
migration::SchemaManager::drop_type(stmt, &self.conn)
.await
.map_err(into_orm_db_err)
}
}
/// Schema Inspection
impl<'c> SchemaManager<'c> {
/// Check if a table exists in the database
pub async fn has_table<T>(&self, table: T) -> Result<bool, DbErr>
where
T: AsRef<str>,
{
migration::SchemaManager::has_table(table, &self.conn)
.await
.map_err(into_orm_db_err)
}
/// Check if a column exists in a specific database table
pub async fn has_column<T, C>(&self, table: T, column: C) -> Result<bool, DbErr>
where
T: AsRef<str>,
C: AsRef<str>,
{
migration::SchemaManager::has_column(table, column, &self.conn)
.await
.map_err(into_orm_db_err)
}
}

View File

@ -0,0 +1,338 @@
//! Migration executor
use crate::{seaql_migrations, MigrationTrait, SchemaManager};
use sea_orm::sea_query::{
Alias, Expr, ForeignKey, IntoTableRef, Query, SelectStatement, SimpleExpr, Table,
};
use sea_orm::{
ActiveModelTrait, ActiveValue, ColumnTrait, Condition, ConnectionTrait, DbBackend, DbConn,
DbErr, EntityTrait, QueryFilter, QueryOrder, Schema, Statement,
};
use std::fmt::Display;
use std::time::SystemTime;
use tracing::info;
#[derive(Debug, PartialEq)]
/// Status of migration
pub enum MigrationStatus {
/// Not yet applied
Pending,
/// Applied
Applied,
}
/// Wrapper of [`MigrationTrait`] with migration status
#[allow(missing_debug_implementations)]
pub struct Migration {
migration: Box<dyn MigrationTrait>,
status: MigrationStatus,
}
impl Display for MigrationStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let status = match self {
MigrationStatus::Pending => "Pending",
MigrationStatus::Applied => "Applied",
};
write!(f, "{}", status)
}
}
/// Performing migrations on a database
#[async_trait::async_trait]
pub trait MigratorTrait: Send {
/// Vector of migrations in time sequence
fn migrations() -> Vec<Box<dyn MigrationTrait>>;
/// Get list of migrations wrapped in `Migration` struct
fn get_migration_files() -> Vec<Migration> {
Self::migrations()
.into_iter()
.map(|migration| Migration {
migration,
status: MigrationStatus::Pending,
})
.collect()
}
/// Get list of applied migrations from database
async fn get_migration_models(db: &DbConn) -> Result<Vec<seaql_migrations::Model>, DbErr> {
Self::install(db).await?;
seaql_migrations::Entity::find()
.order_by_asc(seaql_migrations::Column::Version)
.all(db)
.await
}
/// Get list of migrations with status
async fn get_migration_with_status(db: &DbConn) -> Result<Vec<Migration>, DbErr> {
Self::install(db).await?;
let mut migration_files = Self::get_migration_files();
let migration_models = Self::get_migration_models(db).await?;
for (i, migration_model) in migration_models.into_iter().enumerate() {
if let Some(migration_file) = migration_files.get_mut(i) {
if migration_file.migration.name() == migration_model.version.as_str() {
migration_file.status = MigrationStatus::Applied;
} else {
return Err(DbErr::Custom(format!("Migration mismatch: applied migration != migration file, '{0}' != '{1}'\nMigration '{0}' has been applied but its corresponding migration file is missing.", migration_file.migration.name(), migration_model.version)));
}
} else {
return Err(DbErr::Custom(format!("Migration file of version '{}' is missing, this migration has been applied but its file is missing", migration_model.version)));
}
}
Ok(migration_files)
}
/// Get list of pending migrations
async fn get_pending_migrations(db: &DbConn) -> Result<Vec<Migration>, DbErr> {
Self::install(db).await?;
Ok(Self::get_migration_with_status(db)
.await?
.into_iter()
.filter(|file| file.status == MigrationStatus::Pending)
.collect())
}
/// Get list of applied migrations
async fn get_applied_migrations(db: &DbConn) -> Result<Vec<Migration>, DbErr> {
Self::install(db).await?;
Ok(Self::get_migration_with_status(db)
.await?
.into_iter()
.filter(|file| file.status == MigrationStatus::Applied)
.collect())
}
/// Create migration table `seaql_migrations` in the database
async fn install(db: &DbConn) -> Result<(), DbErr> {
let builder = db.get_database_backend();
let schema = Schema::new(builder);
let mut stmt = schema.create_table_from_entity(seaql_migrations::Entity);
stmt.if_not_exists();
db.execute(builder.build(&stmt)).await.map(|_| ())
}
/// Drop all tables from the database, then reapply all migrations
async fn fresh(db: &DbConn) -> Result<(), DbErr> {
Self::install(db).await?;
let db_backend = db.get_database_backend();
// Temporarily disable the foreign key check
if db_backend == DbBackend::Sqlite {
info!("Disabling foreign key check");
db.execute(Statement::from_string(
db_backend,
"PRAGMA foreign_keys = OFF".to_owned(),
))
.await?;
info!("Foreign key check disabled");
}
// Drop all foreign keys
if db_backend == DbBackend::MySql {
info!("Dropping all foreign keys");
let mut stmt = Query::select();
stmt.columns([Alias::new("TABLE_NAME"), Alias::new("CONSTRAINT_NAME")])
.from((
Alias::new("information_schema"),
Alias::new("table_constraints"),
))
.cond_where(
Condition::all()
.add(
Expr::expr(get_current_schema(db)).equals(
Alias::new("table_constraints"),
Alias::new("table_schema"),
),
)
.add(Expr::expr(Expr::value("FOREIGN KEY")).equals(
Alias::new("table_constraints"),
Alias::new("constraint_type"),
)),
);
let rows = db.query_all(db_backend.build(&stmt)).await?;
for row in rows.into_iter() {
let constraint_name: String = row.try_get("", "CONSTRAINT_NAME")?;
let table_name: String = row.try_get("", "TABLE_NAME")?;
info!(
"Dropping foreign key '{}' from table '{}'",
constraint_name, table_name
);
let mut stmt = ForeignKey::drop();
stmt.table(Alias::new(table_name.as_str()))
.name(constraint_name.as_str());
db.execute(db_backend.build(&stmt)).await?;
info!("Foreign key '{}' has been dropped", constraint_name);
}
info!("All foreign keys dropped");
}
// Drop all tables
let stmt = query_tables(db);
let rows = db.query_all(db_backend.build(&stmt)).await?;
for row in rows.into_iter() {
let table_name: String = row.try_get("", "table_name")?;
info!("Dropping table '{}'", table_name);
let mut stmt = Table::drop();
stmt.table(Alias::new(table_name.as_str()))
.if_exists()
.cascade();
db.execute(db_backend.build(&stmt)).await?;
info!("Table '{}' has been dropped", table_name);
}
// Restore the foreign key check
if db_backend == DbBackend::Sqlite {
info!("Restoring foreign key check");
db.execute(Statement::from_string(
db_backend,
"PRAGMA foreign_keys = ON".to_owned(),
))
.await?;
info!("Foreign key check restored");
}
// Reapply all migrations
Self::up(db, None).await
}
/// Rollback all applied migrations, then reapply all migrations
async fn refresh(db: &DbConn) -> Result<(), DbErr> {
Self::down(db, None).await?;
Self::up(db, None).await
}
/// Rollback all applied migrations
async fn reset(db: &DbConn) -> Result<(), DbErr> {
Self::down(db, None).await
}
/// Check the status of all migrations
async fn status(db: &DbConn) -> Result<(), DbErr> {
Self::install(db).await?;
info!("Checking migration status");
for Migration { migration, status } in Self::get_migration_with_status(db).await? {
info!("Migration '{}'... {}", migration.name(), status);
}
Ok(())
}
/// Apply pending migrations
async fn up(db: &DbConn, mut steps: Option<u32>) -> Result<(), DbErr> {
Self::install(db).await?;
let manager = SchemaManager::new(db);
if let Some(steps) = steps {
info!("Applying {} pending migrations", steps);
} else {
info!("Applying all pending migrations");
}
let migrations = Self::get_pending_migrations(db).await?.into_iter();
if migrations.len() == 0 {
info!("No pending migrations");
}
for Migration { migration, .. } in migrations {
if let Some(steps) = steps.as_mut() {
if steps == &0 {
break;
}
*steps -= 1;
}
info!("Applying migration '{}'", migration.name());
migration.up(&manager).await?;
info!("Migration '{}' has been applied", migration.name());
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.expect("SystemTime before UNIX EPOCH!");
seaql_migrations::ActiveModel {
version: ActiveValue::Set(migration.name().to_owned()),
applied_at: ActiveValue::Set(now.as_secs() as i64),
}
.insert(db)
.await?;
}
Ok(())
}
/// Rollback applied migrations
async fn down(db: &DbConn, mut steps: Option<u32>) -> Result<(), DbErr> {
Self::install(db).await?;
let manager = SchemaManager::new(db);
if let Some(steps) = steps {
info!("Rolling back {} applied migrations", steps);
} else {
info!("Rolling back all applied migrations");
}
let migrations = Self::get_applied_migrations(db).await?.into_iter().rev();
if migrations.len() == 0 {
info!("No applied migrations");
}
for Migration { migration, .. } in migrations {
if let Some(steps) = steps.as_mut() {
if steps == &0 {
break;
}
*steps -= 1;
}
info!("Rolling back migration '{}'", migration.name());
migration.down(&manager).await?;
info!("Migration '{}' has been rollbacked", migration.name());
seaql_migrations::Entity::delete_many()
.filter(seaql_migrations::Column::Version.eq(migration.name()))
.exec(db)
.await?;
}
Ok(())
}
}
pub(crate) fn query_tables(db: &DbConn) -> SelectStatement {
let mut stmt = Query::select();
let (expr, tbl_ref, condition) = match db.get_database_backend() {
DbBackend::MySql => (
Expr::col(Alias::new("table_name")),
(Alias::new("information_schema"), Alias::new("tables")).into_table_ref(),
Condition::all().add(
Expr::expr(get_current_schema(db))
.equals(Alias::new("tables"), Alias::new("table_schema")),
),
),
DbBackend::Postgres => (
Expr::col(Alias::new("table_name")),
(Alias::new("information_schema"), Alias::new("tables")).into_table_ref(),
Condition::all()
.add(
Expr::expr(get_current_schema(db))
.equals(Alias::new("tables"), Alias::new("table_schema")),
)
.add(Expr::col(Alias::new("table_type")).eq("BASE TABLE")),
),
DbBackend::Sqlite => (
Expr::col(Alias::new("name")),
Alias::new("sqlite_master").into_table_ref(),
Condition::all()
.add(Expr::col(Alias::new("type")).eq("table"))
.add(Expr::col(Alias::new("name")).ne("sqlite_sequence")),
),
};
stmt.expr_as(expr, Alias::new("table_name"))
.from(tbl_ref)
.cond_where(condition);
stmt
}
pub(crate) fn get_current_schema(db: &DbConn) -> SimpleExpr {
match db.get_database_backend() {
DbBackend::MySql => Expr::cust("DATABASE()"),
DbBackend::Postgres => Expr::cust("CURRENT_SCHEMA()"),
DbBackend::Sqlite => unimplemented!(),
}
}

View File

@ -0,0 +1,8 @@
//! Import migration utility
pub use crate::*;
pub use async_std;
pub use async_trait;
pub use sea_orm::sea_query;
pub use sea_orm::sea_query::*;
pub use sea_orm::DbErr;

View File

@ -0,0 +1,20 @@
//! Get query result from db
use crate::into_migration_err;
use sea_schema::migration::{self, MigrationErr};
pub(crate) struct QueryResult {
pub(crate) res: sea_orm::QueryResult,
}
impl migration::QueryResultTrait for QueryResult {
fn try_get_string(&self, col: &str) -> Result<String, MigrationErr> {
self.res
.try_get::<String>("", col)
.map_err(into_migration_err)
}
fn try_get_i64(&self, col: &str) -> Result<i64, MigrationErr> {
self.res.try_get::<i64>("", col).map_err(into_migration_err)
}
}

View File

@ -0,0 +1,18 @@
#![allow(missing_docs)]
//! Migration entity
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "seaql_migrations")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub version: String,
pub applied_at: i64,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,12 @@
//! Convert SQL statement
use crate::into_orm_db_backend;
use sea_schema::migration;
pub(crate) fn into_orm_stmt(stmt: migration::Statement) -> sea_orm::Statement {
sea_orm::Statement {
sql: stmt.sql,
values: stmt.values,
db_backend: into_orm_db_backend(stmt.db_backend),
}
}

View File

@ -15,6 +15,8 @@ pub enum DbErr {
Type(String),
/// Error occurred while parsing json value as target type
Json(String),
/// A migration error
Migration(String),
}
impl std::error::Error for DbErr {}
@ -29,6 +31,7 @@ impl std::fmt::Display for DbErr {
Self::Custom(s) => write!(f, "Custom Error: {}", s),
Self::Type(s) => write!(f, "Type Error: {}", s),
Self::Json(s) => write!(f, "Json Error: {}", s),
Self::Migration(s) => write!(f, "Migration Error: {}", s),
}
}
}