use crate::error::{DatabaseError as _, TernResult};
use chrono::{DateTime, Utc};
use futures_core::{Future, future::BoxFuture};
use std::time::Instant;
pub use crate::query::Query;
pub trait MigrationContext
where
Self: MigrationSource<Ctx = Self> + Send + Sync + 'static,
{
const HISTORY_TABLE: &str;
type Exec: Executor;
fn executor(&mut self) -> &mut Self::Exec;
fn apply<'migration, 'conn: 'migration, M>(
&'conn mut self,
migration: &'migration M,
) -> BoxFuture<'migration, TernResult<AppliedMigration>>
where
M: Migration<Ctx = Self> + Send + Sync + ?Sized,
{
Box::pin(async move {
let start = Instant::now();
let query = M::build(migration, self).await?;
let executor = self.executor();
if migration.no_tx() {
executor
.apply_no_tx(&query)
.await
.void_tern_migration_result(migration)?;
} else {
executor
.apply_tx(&query)
.await
.void_tern_migration_result(migration)?;
}
let applied_at = Utc::now();
let duration_ms = start.elapsed().as_millis() as i64;
let applied =
migration.to_applied(duration_ms, applied_at, query.sql());
executor
.insert_applied_migration(Self::HISTORY_TABLE, &applied)
.await?;
Ok(applied)
})
}
fn latest_version(&mut self) -> BoxFuture<'_, TernResult<Option<i64>>> {
Box::pin(async move {
let latest = self
.executor()
.get_all_applied(Self::HISTORY_TABLE)
.await?
.into_iter()
.fold(None, |acc, m| match acc {
None => Some(m.version),
Some(v) if m.version > v => Some(m.version),
_ => acc,
});
Ok(latest)
})
}
fn previously_applied(
&mut self,
) -> BoxFuture<'_, TernResult<Vec<AppliedMigration>>> {
Box::pin(self.executor().get_all_applied(Self::HISTORY_TABLE))
}
fn check_history_table(&mut self) -> BoxFuture<'_, TernResult<()>> {
Box::pin(
self.executor().create_history_if_not_exists(Self::HISTORY_TABLE),
)
}
fn drop_history_table(&mut self) -> BoxFuture<'_, TernResult<()>> {
Box::pin(self.executor().drop_history(Self::HISTORY_TABLE))
}
fn insert_applied<'migration, 'conn: 'migration>(
&'conn mut self,
applied: &'migration AppliedMigration,
) -> BoxFuture<'migration, TernResult<()>> {
Box::pin(
self.executor()
.insert_applied_migration(Self::HISTORY_TABLE, applied),
)
}
fn upsert_applied<'migration, 'conn: 'migration>(
&'conn mut self,
applied: &'migration AppliedMigration,
) -> BoxFuture<'migration, TernResult<()>> {
Box::pin(
self.executor()
.upsert_applied_migration(Self::HISTORY_TABLE, applied),
)
}
}
pub trait Executor
where
Self: Send + Sync + 'static,
{
type Queries: QueryRepository;
fn apply_tx(
&mut self,
query: &Query,
) -> impl Future<Output = TernResult<()>> + Send;
fn apply_no_tx(
&mut self,
query: &Query,
) -> impl Future<Output = TernResult<()>> + Send;
fn create_history_if_not_exists(
&mut self,
history_table: &str,
) -> impl Future<Output = TernResult<()>> + Send;
fn drop_history(
&mut self,
history_table: &str,
) -> impl Future<Output = TernResult<()>> + Send;
fn get_all_applied(
&mut self,
history_table: &str,
) -> impl Future<Output = TernResult<Vec<AppliedMigration>>> + Send;
fn insert_applied_migration(
&mut self,
history_table: &str,
applied: &AppliedMigration,
) -> impl Future<Output = TernResult<()>> + Send;
fn upsert_applied_migration(
&mut self,
history_table: &str,
applied: &AppliedMigration,
) -> impl Future<Output = TernResult<()>> + Send;
}
pub trait QueryRepository {
fn create_history_if_not_exists_query(history_table: &str) -> Query;
fn drop_history_query(history_table: &str) -> Query;
fn insert_into_history_query(
history_table: &str,
applied: &AppliedMigration,
) -> Query;
fn select_star_from_history_query(history_table: &str) -> Query;
fn upsert_history_query(
history_table: &str,
applied: &AppliedMigration,
) -> Query;
}
pub trait Migration
where
Self: Send + Sync,
{
type Ctx: MigrationContext;
fn migration_id(&self) -> MigrationId;
fn content(&self) -> String;
fn no_tx(&self) -> bool;
fn build<'a>(
&'a self,
ctx: &'a mut Self::Ctx,
) -> BoxFuture<'a, TernResult<Query>>;
fn version(&self) -> i64 {
self.migration_id().version()
}
fn to_applied(
&self,
duration_ms: i64,
applied_at: DateTime<Utc>,
content: &str,
) -> AppliedMigration {
AppliedMigration::new(
self.migration_id(),
content,
duration_ms,
applied_at,
)
}
}
pub trait MigrationSource {
type Ctx: MigrationContext;
fn migration_set(
&self,
last_applied: Option<i64>,
) -> MigrationSet<Self::Ctx>;
}
pub struct MigrationSet<Ctx: ?Sized> {
pub migrations: Vec<Box<dyn Migration<Ctx = Ctx>>>,
}
impl<Ctx> MigrationSet<Ctx>
where
Ctx: MigrationContext,
{
pub fn new<T>(vs: T) -> MigrationSet<Ctx>
where
T: Into<Vec<Box<dyn Migration<Ctx = Ctx>>>>,
{
let mut migrations = vs.into();
migrations.sort_by_key(|m| m.version());
MigrationSet { migrations }
}
pub fn len(&self) -> usize {
self.migrations.len()
}
pub fn versions(&self) -> Vec<i64> {
self.migrations.iter().map(|m| m.version()).collect::<Vec<_>>()
}
pub fn migration_ids(&self) -> Vec<MigrationId> {
self.migrations.iter().map(|m| m.migration_id()).collect::<Vec<_>>()
}
pub fn max(&self) -> Option<i64> {
self.versions().iter().max().copied()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
pub trait QueryBuilder {
type Ctx: MigrationContext;
fn build(
&self,
ctx: &mut Self::Ctx,
) -> impl Future<Output = TernResult<Query>> + Send;
}
#[derive(Debug, Clone, Hash, PartialOrd, Ord, PartialEq, Eq)]
pub struct MigrationId {
version: i64,
description: String,
}
impl MigrationId {
pub fn new(version: i64, description: String) -> Self {
Self { version, description }
}
pub fn version(&self) -> i64 {
self.version
}
pub fn description(&self) -> String {
self.description.clone()
}
}
impl std::fmt::Display for MigrationId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "V{}__{}", self.version, self.description)
}
}
impl From<AppliedMigration> for MigrationId {
fn from(value: AppliedMigration) -> Self {
Self { version: value.version, description: value.description }
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))]
pub struct AppliedMigration {
pub version: i64,
pub description: String,
pub content: String,
pub duration_ms: i64,
pub applied_at: DateTime<Utc>,
}
impl AppliedMigration {
pub fn new(
id: MigrationId,
content: &str,
duration_ms: i64,
applied_at: DateTime<Utc>,
) -> Self {
Self {
version: id.version,
description: id.description,
content: content.into(),
duration_ms,
applied_at,
}
}
}