This repository has been archived by the owner on Sep 12, 2022. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
e868350
commit b172535
Showing
9 changed files
with
368 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,16 @@ | ||
# Generated by Cargo | ||
# will have compiled files and executables | ||
/target/ | ||
/example/target/ | ||
|
||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries | ||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html | ||
Cargo.lock | ||
|
||
# These are backup files generated by rustfmt | ||
**/*.rs.bk | ||
|
||
# SQLite Databases | ||
test.db | ||
test.db-shm | ||
test.db-wal |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
[package] | ||
name = "sea-migrations" | ||
version = "0.0.1" | ||
authors = [ "Oscar Beaumont <oscar@otbeaumont.me>"] | ||
edition = "2021" | ||
description = "Effortless database migrations for SeaORM!" | ||
repository = "https://github.com/oscartbeaumont/sea-migrations" | ||
license = "MIT" | ||
|
||
[dependencies] | ||
sea-orm = { version = "0.3.2", features = [ "mock" ], default-features = false } | ||
|
||
[dev-dependencies] | ||
sea-orm = { version = "0.3.2", features = [ "sqlx-sqlite", "runtime-tokio-native-tls", "macros" ], default-features = false } | ||
tokio = { version = "1.13.0", features = [ "macros", "rt-multi-thread" ] } | ||
|
||
[workspace] | ||
members = [ | ||
"example", | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,60 @@ | ||
# sea-migrations | ||
Effortless database migrations for SeaORM! | ||
<h1 align="center">Sea Migrations</h1> | ||
<div align="center"> | ||
<strong> | ||
Effortless database migrations for <a href="https://www.sea-ql.org/SeaORM/">SeaORM</a>! | ||
</strong> | ||
</div> | ||
|
||
<br /> | ||
|
||
<div align="center"> | ||
<!-- Crates version --> | ||
<a href="https://crates.io/crates/sea-migrations"> | ||
<img src="https://img.shields.io/crates/v/sea-migrations.svg?style=flat-square" | ||
alt="Crates.io version" /> | ||
</a> | ||
<!-- Downloads --> | ||
<a href="https://crates.io/crates/sea-migrations"> | ||
<img src="https://img.shields.io/crates/d/sea-migrations.svg?style=flat-square" | ||
alt="Download" /> | ||
</a> | ||
<!-- docs.rs docs --> | ||
<a href="https://docs.rs/sea-migrations"> | ||
<img src="https://img.shields.io/badge/docs-latest-blue.svg?style=flat-square" | ||
alt="docs.rs docs" /> | ||
</a> | ||
</div> | ||
<br/> | ||
|
||
This crate aims to provide a simple solution to doing database migrations with [SeaORM](https://www.sea-ql.org/SeaORM/). | ||
|
||
Features: | ||
- Automatically create database tables from your SeaORM entities | ||
- Write your migration code in Rust | ||
- Supports all SeaORM database backends | ||
|
||
## Beta Warning | ||
|
||
This project is in beta and could have major changes to API or behavior in future updates. Below are some issues the project currently has: | ||
|
||
Internal issues: | ||
- Doesn't have unit tests | ||
- Uses unsafe code to access private variables from SeaORM | ||
- If migrations are not all run sequentially (user upgrades by 2 or more versions of the application at once) the migration version will get out of date with the migration code. | ||
|
||
Missing features: | ||
- Creating join tables | ||
- Automatically doing basic migrations such as adding a column based on the SeaORM Entity | ||
|
||
## Install | ||
|
||
Add `sea-migrations` to your dependencies: | ||
|
||
```toml | ||
[dependencies] | ||
# ... | ||
sea-migrations= "0.0.1" | ||
``` | ||
## Usage | ||
|
||
Check out [this example application](https://github.com/oscartbeaumont/sea-migrations/tree/main/example). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
[package] | ||
name = "sea-migrations-example" | ||
version = "0.1.0" | ||
edition = "2021" | ||
publish = false | ||
|
||
[dependencies] | ||
sea-orm = { version = "^0", features = [ "sqlx-sqlite", "runtime-tokio-native-tls", "macros", "mock" ], default-features = false } | ||
sea-migrations = { path = "../" } | ||
tokio = { version = "1.13.0", features = [ "macros" ] } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
use sea_migrations::run_migrations; | ||
use sea_orm::Database; | ||
|
||
mod models; | ||
|
||
#[tokio::main] | ||
async fn main() -> Result<(), sea_orm::DbErr> { | ||
let db = Database::connect("sqlite://./test.db?mode=rwc").await?; | ||
|
||
let migrations_result = run_migrations(&db, models::do_migrations).await; | ||
if let Err(e) = migrations_result { | ||
eprintln!("{}", e); | ||
} else { | ||
println!("Migrations successful"); | ||
} | ||
|
||
Ok(()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
use sea_orm::entity::prelude::*; | ||
|
||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] | ||
#[sea_orm(table_name = "customer")] | ||
pub struct Model { | ||
#[sea_orm(primary_key)] | ||
pub id: i32, | ||
pub name: String, | ||
#[sea_orm(column_type = "Text", nullable)] | ||
pub notes: Option<String>, | ||
} | ||
|
||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] | ||
pub enum Relation {} | ||
|
||
impl ActiveModelBehavior for ActiveModel {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
use sea_migrations::{create_entity_table, MigrationStatus}; | ||
use sea_orm::{ | ||
sea_query::{Alias, ColumnDef, Table}, | ||
ConnectionTrait, DbConn, DbErr, | ||
}; | ||
|
||
pub mod customer; | ||
|
||
/// do_migrations is the callback for sea-migrations to run the migrations | ||
pub async fn do_migrations( | ||
db: &DbConn, | ||
current_migration_version: Option<u32>, | ||
) -> Result<MigrationStatus, DbErr> { | ||
match current_migration_version { | ||
None => { | ||
println!("Migrating empty DB -> version 1!"); | ||
create_entity_table(db, customer::Entity).await?; | ||
Ok(MigrationStatus::Complete) | ||
} | ||
Some(1) => { | ||
println!("Migrating from version 1 -> 2!"); | ||
let stmt = Table::alter() | ||
.table(customer::Entity) | ||
.add_column( | ||
ColumnDef::new(Alias::new("new_column")) | ||
.integer() | ||
.not_null() | ||
.default(100), | ||
) | ||
.to_owned(); | ||
db.execute(db.get_database_backend().build(&stmt)).await?; | ||
|
||
Ok(MigrationStatus::Complete) | ||
} | ||
Some(2) => Ok(MigrationStatus::NotRequired), | ||
_ => Err(DbErr::Custom(":(".into())), | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
#![deny(missing_docs)] | ||
|
||
//! Effortless database migrations for [SeaORM](https://www.sea-ql.org/SeaORM/)! | ||
//! | ||
//! Checkout an example using this package [here](https://github.com/oscartbeaumont/sea-migrations/tree/main/example). | ||
use std::future::Future; | ||
|
||
use sea_orm::{ | ||
sea_query::{ColumnDef, Table}, | ||
ColumnTrait, ColumnType, ConnectionTrait, DbConn, DbErr, EntityTrait, ExecResult, Iterable, | ||
PrimaryKeyToColumn, PrimaryKeyTrait, | ||
}; | ||
|
||
mod migrations_table; | ||
|
||
/// MigrationStatus is used to represent the status of a migration. | ||
#[derive(PartialEq)] | ||
pub enum MigrationStatus { | ||
/// NotRequired is returned when no database migrations are required. If this is returned from the database migrations callback it is assumed by sea-migrations that the database is already up to date. | ||
NotRequired, | ||
/// Complete is returned when a migration has been run successfully. This will cause a new migration event to be added and the migration version to be incremented. | ||
Complete, | ||
} | ||
|
||
/// run_migrations will run the database migrations. It takes in callback function which will be called to do the migrations. | ||
/// | ||
/// ```rust | ||
/// use sea_migrations::{run_migrations, create_entity_table, MigrationStatus}; | ||
/// use sea_orm::{ Database, DbErr, ConnectionTrait, DbConn}; | ||
/// use sea_orm::sea_query::{Alias, ColumnDef, Table}; | ||
/// | ||
/// pub async fn migrations_callback(db: &DbConn, current_migration_version: Option<u32>) -> Result<MigrationStatus, DbErr> { | ||
/// match current_migration_version { | ||
/// None => Ok(MigrationStatus::NotRequired), // Tells sea-migrations that no further migrations are required. | ||
/// _ => Err(DbErr::Custom("Invalid migrations version number!".into())), | ||
/// } | ||
/// } | ||
/// | ||
/// #[tokio::main] | ||
/// async fn main() -> Result<(), sea_orm::DbErr> { | ||
/// let db = Database::connect("sqlite::memory:").await?; | ||
/// let migrations_result = run_migrations(&db, migrations_callback).await?; | ||
/// Ok(()) | ||
/// } | ||
/// | ||
/// ``` | ||
pub async fn run_migrations<'a, T, F>(db: &'a DbConn, handler: F) -> Result<MigrationStatus, DbErr> | ||
where | ||
T: Future<Output = Result<MigrationStatus, DbErr>>, | ||
F: Fn(&'a DbConn, Option<u32>) -> T, | ||
{ | ||
migrations_table::init(db).await?; | ||
let current_migrations_version = migrations_table::get_latest(db).await?; | ||
let result = handler(db, current_migrations_version).await; | ||
if result == Ok(MigrationStatus::Complete) { | ||
migrations_table::insert_migration(db).await?; | ||
} | ||
|
||
result | ||
} | ||
|
||
// CustomColumnDef is a copy of the struct defined at https://github.com/SeaQL/sea-query/blob/master/src/table/column.rs#L5 with all fields set to public. | ||
// It exists so that the unsafe transmutate operation can be applied to access private fields on the struct. | ||
// This is a TEMPORARY solution and I will ask if these values can be directly exposed by sea_query in the future. This solution relies on internal implementation details of sea_query and unsafe code which is not good! | ||
struct CustomColumnDef { | ||
pub col_type: ColumnType, | ||
pub null: bool, | ||
pub unique: bool, | ||
pub indexed: bool, | ||
} | ||
|
||
/// create_entity_table will automatically create a database table if it does not exist for a sea_query Entity. | ||
/// | ||
/// ```rust | ||
/// use sea_orm::{Database, DbErr, ConnectionTrait, DbConn}; | ||
/// use sea_orm::entity::prelude::*; | ||
/// use sea_migrations::create_entity_table; | ||
/// | ||
/// #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] | ||
/// #[sea_orm(table_name = "cake")] | ||
/// pub struct Model { | ||
/// #[sea_orm(primary_key)] | ||
/// pub id: i32, | ||
/// pub name: String, | ||
/// } | ||
/// #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] | ||
/// pub enum Relation {} | ||
/// | ||
/// impl ActiveModelBehavior for ActiveModel {} | ||
/// | ||
/// #[tokio::main] | ||
/// async fn main() -> Result<(), sea_orm::DbErr> { | ||
/// let db = Database::connect("sqlite::memory:").await?; | ||
/// | ||
/// create_entity_table(&db, crate::Entity).await?; // Replace "crate" with the name of the module containing your SeaORM Model. | ||
/// | ||
/// Ok(()) | ||
/// } | ||
/// | ||
/// ``` | ||
pub async fn create_entity_table<T: 'static>(db: &DbConn, entity: T) -> Result<ExecResult, DbErr> | ||
where | ||
T: EntityTrait, | ||
{ | ||
let mut stmt = Table::create(); | ||
stmt.table(entity).if_not_exists(); | ||
|
||
for column in T::Column::iter() { | ||
let column_def_prelude: CustomColumnDef = unsafe { std::mem::transmute(column.def()) }; // Note: This is used to access private fields and hence relies on internal implementation details of sea_query and unsafe code which is not good! | ||
let column_def = | ||
&mut ColumnDef::new_with_type(column, column_def_prelude.col_type.clone().into()); | ||
if column_def_prelude.null { | ||
column_def.not_null(); | ||
} | ||
if column_def_prelude.unique { | ||
column_def.unique_key(); | ||
} | ||
if column_def_prelude.indexed { | ||
panic!("Indexed columns are not yet able to be migrated!"); | ||
} | ||
|
||
if let Some(_) = T::PrimaryKey::from_column(column) { | ||
column_def.primary_key(); | ||
|
||
if T::PrimaryKey::auto_increment() && column_def_prelude.col_type == ColumnType::Integer | ||
{ | ||
column_def.auto_increment(); | ||
} | ||
} | ||
|
||
stmt.col(column_def); | ||
} | ||
|
||
db.execute(db.get_database_backend().build(&stmt)).await | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
use sea_orm::{ | ||
sea_query::{Alias, ColumnDef, Expr, Query, Table}, | ||
ConnectionTrait, DbConn, DbErr, Value, | ||
}; | ||
|
||
// MIGRATIONS_TABLE_NAME is the name of the table created in the Database to keep track of the current state of the migrations. | ||
const MIGRATIONS_TABLE_NAME: &str = "_sea_migrations"; | ||
|
||
// MIGRATIONS_TABLE_VERSION_COLUMN is the name of the column used to store the version of the migrations within the table used to track to current state of migrations. | ||
const MIGRATIONS_TABLE_VERSION_COLUMN: &str = "version"; | ||
|
||
/// init will create the migrations table in the database if it does not exist. | ||
pub async fn init(db: &DbConn) -> Result<(), DbErr> { | ||
let stmt = Table::create() | ||
.table(Alias::new(MIGRATIONS_TABLE_NAME)) | ||
.if_not_exists() | ||
.col( | ||
ColumnDef::new(Alias::new(MIGRATIONS_TABLE_VERSION_COLUMN)) | ||
.integer() | ||
.not_null() | ||
.auto_increment() | ||
.primary_key(), | ||
) | ||
.to_owned(); | ||
|
||
db.execute(db.get_database_backend().build(&stmt)).await?; | ||
Ok(()) | ||
} | ||
|
||
/// get_latest will return the version of the latest migration (or None if no migrations have previous been run). | ||
pub async fn get_latest(db: &DbConn) -> Result<Option<u32>, DbErr> { | ||
let stmt = Query::select() | ||
.expr(Expr::cust(&format!( | ||
"MAX (`{}`) AS `{0}`", | ||
MIGRATIONS_TABLE_VERSION_COLUMN | ||
))) | ||
.from(Alias::new(MIGRATIONS_TABLE_NAME)) | ||
.to_owned(); | ||
|
||
let result = db.query_one(db.get_database_backend().build(&stmt)).await?; | ||
if let Some(result) = result { | ||
let latest_migration_version = result.try_get("", MIGRATIONS_TABLE_VERSION_COLUMN); | ||
if let Ok(latest_migration_version) = latest_migration_version { | ||
Ok(Some(latest_migration_version)) | ||
} else { | ||
Ok(None) | ||
} | ||
} else { | ||
Ok(None) | ||
} | ||
} | ||
|
||
/// insert_migration will create a new migration event in the database. | ||
pub async fn insert_migration(db: &DbConn) -> Result<u32, DbErr> { | ||
let stmt = Query::insert() | ||
.into_table(Alias::new(MIGRATIONS_TABLE_NAME)) | ||
.columns(vec![Alias::new(MIGRATIONS_TABLE_VERSION_COLUMN)]) | ||
.values_panic(vec![Value::BigInt(None)]) | ||
.to_owned(); | ||
|
||
let result = db.execute(db.get_database_backend().build(&stmt)).await?; | ||
Ok(result.last_insert_id() as u32) | ||
} |