use std::{collections::HashMap, convert::Infallible, marker::PhantomData, ops::Deref};
use crate::{
Lazy, Table, TableRow, Transaction, aggregate,
lower::{self, JoinableTableWithId, list_writer::Alias},
transaction::try_insert_private,
};
pub trait Migrateable: Table<MigrateFrom: Table<Schema = Self::FromSchema>> {
type Migration;
type FromSchema;
type MigrateConflict;
#[doc(hidden)]
fn prepare(val: Self::Migration, prev: Lazy<'_, Self::MigrateFrom>) -> Self;
#[doc(hidden)]
fn map_conflict(val: TableRow<Self::MigrateFrom>) -> Self::MigrateConflict;
}
pub struct TransactionMigrate<FromSchema: 'static> {
pub(super) inner: Transaction<FromSchema>,
pub(super) scope: lower::Scope,
pub(super) rename_map: HashMap<&'static str, lower::TmpTable>,
pub(super) extra_index: Vec<String>,
}
impl<FromSchema> Deref for TransactionMigrate<FromSchema> {
type Target = Transaction<FromSchema>;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl<FromSchema: 'static> TransactionMigrate<FromSchema> {
fn new_table_name<T: Table>(&mut self) -> lower::TmpTable {
*self.rename_map.entry(T::NAME).or_insert_with(|| {
let new_table_name = self.scope.tmp_table();
let table = crate::schema::from_macro::Table::new::<T>().into_db();
self.inner
.execute(&table.create(lower::JoinableTable::Tmp(new_table_name)));
self.extra_index.extend(table.delayed_indices(T::NAME));
new_table_name
})
}
fn unmigrated<T: Migrateable<FromSchema = FromSchema>>(
&self,
new_name: lower::TmpTable,
) -> impl Iterator<Item = TableRow<T::MigrateFrom>> {
self.inner.query(|rows| {
let old = rows.join_private::<T::MigrateFrom>();
rows.filter(aggregate(|rows| {
let new = rows.join(crate::private::Joinable::new(JoinableTableWithId {
name: lower::JoinableTable::Tmp(new_name),
main_column: <T as Table>::ID,
}));
rows.filter(old.eq(&new));
rows.exists().not()
}));
rows.into_iter(old)
})
}
pub fn migrate_optional<'t, T: Migrateable<FromSchema = FromSchema>>(
&'t mut self,
mut f: impl FnMut(Lazy<'t, T::MigrateFrom>) -> Option<T::Migration>,
) -> Result<MigratedOptional<T>, T::MigrateConflict> {
let new_name = self.new_table_name::<T>();
for row in self.unmigrated::<T>(new_name) {
if let Some(new) = f(self.lazy(row)) {
let val = T::prepare(new, self.lazy(row));
try_insert_private::<T>(
lower::JoinableTable::Tmp(new_name),
Some(row.inner.idx),
val,
)
.map_err(|_| T::map_conflict(row))?;
};
}
Ok(MigratedOptional { inner: PhantomData })
}
pub fn migrate<'t, T: Migrateable<FromSchema = FromSchema>>(
&'t mut self,
mut f: impl FnMut(Lazy<'t, T::MigrateFrom>) -> T::Migration,
) -> Result<Migrated<'static, T>, T::MigrateConflict> {
self.migrate_optional(|x| Some(f(x)))
.map(|x| x.map_fk_err(|| unreachable!("all rows are migrated")))
}
pub fn migrate_ok<'t, T: Migrateable<FromSchema = FromSchema, MigrateConflict = Infallible>>(
&'t mut self,
f: impl FnMut(Lazy<'t, T::MigrateFrom>) -> T::Migration,
) -> Migrated<'static, T> {
let Ok(res) = self.migrate(f);
res
}
}
pub struct Migrated<'t, T: Migrateable> {
_p: PhantomData<T>,
f: FkErrHandler<'t>,
_local: PhantomData<*const ()>,
}
impl<'t, To: Migrateable> Migrated<'t, To> {
#[doc(hidden)]
pub fn apply(self, b: &mut SchemaBuilder<'t, To::FromSchema>) {
b.foreign_key::<To>(self.f);
}
}
pub struct SchemaBuilder<'t, FromSchema: 'static> {
pub(super) inner: TransactionMigrate<FromSchema>,
pub(super) drop: Vec<String>,
pub(super) foreign_key: HashMap<&'static str, FkErrHandler<'t>>,
}
impl<'t, FromSchema: 'static> SchemaBuilder<'t, FromSchema> {
pub fn foreign_key<To: Table>(&mut self, err: FkErrHandler<'t>) {
self.inner.new_table_name::<To>();
self.foreign_key.insert(To::NAME, err);
}
pub fn create_empty<To: Table>(&mut self) {
self.inner.new_table_name::<To>();
}
pub fn drop_table<T: Table>(&mut self) {
self.drop.push(format!("DROP TABLE {}", Alias(T::NAME)));
}
}
pub struct MigratedOptional<T: Migrateable> {
inner: PhantomData<Migrated<'static, T>>,
}
impl<T: Migrateable> MigratedOptional<T> {
pub fn map_fk_err<'t>(self, f: impl 't + FnOnce() -> Infallible) -> Migrated<'t, T> {
Migrated {
_p: PhantomData,
f: FkErrHandler(Box::new(f)),
_local: PhantomData,
}
}
}
impl<T: Migrateable<Referer = Infallible>> MigratedOptional<T> {
pub fn no_reference(self) -> Migrated<'static, T> {
self.map_fk_err(|| unreachable!("no references exist to this table"))
}
}
pub(crate) struct FkErrHandler<'t>(pub Box<dyn 't + FnOnce() -> Infallible>);