use std::marker::PhantomData;
use crate::prelude::*;
use crate::query::Insertable;
use crate::schema::{
self,
build::{generate_from_schema, generate_single_entity_table, GeneratedSchema},
datum::Datum,
entity::{Entity, EntityID},
relation::{RelationDomain, RelationExt, RelationMap, RelationRange},
BuildSeal, DatabaseItem, Schema, SentinelDatabaseItem,
};
use crate::{DBResult, Error, IDMap, Transaction};
mod ip;
use ip::*;
pub trait MigratableDatum<From: Datum>: Datum {
fn migrate_datum(from: &From) -> DBResult<Self>
where
Self: Sized;
}
macro_rules! migrate_datum_impl_clone {
($($ty:path),+) => {
$(
impl MigratableDatum<$ty> for $ty {
fn migrate_datum(from: &$ty) -> DBResult<Self> where Self: Sized {
Ok(from.clone())
}
}
)*
}
}
macro_rules! migrate_datum_impl_copy {
($($ty:path),+) => {
$(
impl MigratableDatum<$ty> for $ty {
fn migrate_datum(from: &$ty) -> DBResult<Self> where Self: Sized {
Ok(*from)
}
}
)*
}
}
migrate_datum_impl_clone!(String, Vec<u8>);
migrate_datum_impl_copy!(bool, isize, usize, f32, f64, u64, i64);
impl<T: MigratableDatum<T>> MigratableDatum<Option<T>> for Option<T> {
fn migrate_datum(from: &Option<T>) -> DBResult<Self>
where
Self: Sized,
{
from.as_ref().map(T::migrate_datum).transpose()
}
}
#[cfg(feature = "time")]
const _: () = {
use crate::DBResult;
migrate_datum_impl_copy!(time::OffsetDateTime);
};
impl<R0: Relation, R1: Relation> MigratableDatum<RelationDomain<R0>> for RelationDomain<R1> {
fn migrate_datum(_from: &RelationDomain<R0>) -> DBResult<Self>
where
Self: Sized,
{
R0::try_coerce::<R1>()?;
Ok(RelationDomain::<R1>::default())
}
}
impl<R0: Relation, R1: Relation> MigratableDatum<RelationRange<R0>> for RelationRange<R1> {
fn migrate_datum(_from: &RelationRange<R0>) -> DBResult<Self>
where
Self: Sized,
{
R0::try_coerce::<R1>()?;
Ok(RelationRange::<R1>::default())
}
}
impl<E0: Entity, E1: Entity> MigratableDatum<RelationMap<E0>> for RelationMap<E1> {
fn migrate_datum(_from: &RelationMap<E0>) -> DBResult<Self>
where
Self: Sized,
{
if E0::entity_name() == E1::entity_name() {
Ok(RelationMap::default())
} else {
panic!(
"Cannot coerce a migration of a map between {} and {}",
std::any::type_name::<E0>(),
std::any::type_name::<E1>()
);
}
}
}
pub trait MigratableEntity<From: Entity>: 'static + Entity {
fn migrate(from: &From) -> DBResult<Option<Self>>
where
Self: Sized;
}
impl<T: 'static + Entity + Clone> MigratableEntity<T> for T {
fn migrate(from: &T) -> DBResult<Option<Self>>
where
Self: Sized,
{
Ok(Some(from.clone()))
}
}
pub struct MigrationContext<'a> {
txn: &'a mut Transaction,
from_gen: GeneratedSchema,
into_gen: GeneratedSchema,
in_progress: Vec<(&'static str, &'static str)>,
}
impl MigrationContext<'_> {
pub fn in_progress<E: Entity>(&mut self) -> DBResult<MigrateMap<E>> {
let Some(_query) = self.into_gen.table_queries().get(E::entity_name()) else {
return Err(Error::LogicError(
"tried to create in-progress table for entity that not in new schema",
));
};
let table_sql = generate_single_entity_table::<IPEntity<E>>();
self.txn.lease().execute_raw_sql(table_sql.as_str())?;
self.in_progress
.push((E::entity_name(), IPEntity::<E>::entity_name()));
Ok(MigrateMap::<E> {
..Default::default()
})
}
pub fn migrate_entity<E0: Entity, E1: MigratableEntity<E0>>(
&mut self,
) -> DBResult<MigrateMap<E1>> {
log::trace!("migrating entity via MigratableEntity");
let mm = self.in_progress::<E1>()?;
for e0 in IDMap::<E0>::build(BuildSeal::new()).get(self.txn)? {
log::trace!("\tprocessing entity with ID {:?}", e0.id());
let Some(e1) = <E1 as MigratableEntity<E0>>::migrate(e0.as_ref())? else {
continue;
};
mm.insert_matching(self.txn, e1, e0)?;
}
Ok(mm)
}
pub fn txn(&mut self) -> &mut Transaction {
self.txn
}
fn finish(self) -> DBResult<()> {
log::trace!("in progress: {:?}", self.in_progress);
for (basename, ipname) in self.in_progress {
self.txn
.lease()
.execute_raw_sql(format!("DROP TABLE {basename};"))?;
self.txn
.lease()
.execute_raw_sql(format!("ALTER TABLE `{ipname}` RENAME TO `{basename}`"))?;
let Some(indices) = self.into_gen.related_indices().get(basename) else {
continue;
};
for iname in indices {
self.txn
.lease()
.execute_raw_sql(&self.into_gen.index_queries()[iname])?;
}
}
log::trace!("looking for new indices!");
log::trace!("from_gen indices: {:?}", self.from_gen.index_queries());
log::trace!("into_gen indices: {:?}", self.into_gen.index_queries());
for (iname, sql) in self.into_gen.index_queries().iter() {
if self.from_gen.index_queries().contains_key(iname) {
continue;
}
self.txn.lease().execute_raw_sql(sql)?;
}
Ok(())
}
}
pub trait MigratableItem<From: DatabaseItem>: 'static + DatabaseItem {
fn run_migration(from: &From, ctx: &mut MigrationContext) -> DBResult<()>
where
Self: Sized;
}
impl<T: 'static + DatabaseItem> MigratableItem<super::SentinelDatabaseItem> for T {
fn run_migration(_: &super::SentinelDatabaseItem, _ctx: &mut MigrationContext) -> DBResult<()>
where
Self: Sized,
{
unreachable!()
}
}
pub struct MigrateMap<OE: Entity>(PhantomData<OE>);
impl<OE: Entity> Default for MigrateMap<OE> {
fn default() -> Self {
Self(PhantomData)
}
}
impl<OE: Entity> Insertable<OE> for MigrateMap<OE> {
fn insert(&self, txn: &mut Transaction, value: OE) -> DBResult<OE::ID> {
let id =
IDMap::<IPEntity<OE>>::insert(&IDMap::build(BuildSeal::new()), txn, IPEntity(value))?;
Ok(<OE::ID>::from_raw(id.0))
}
fn insert_ref(&self, txn: &mut Transaction, value: OE::ERef<'_>) -> DBResult<OE::ID> {
let id = IDMap::<IPEntity<OE>>::insert_ref(
&IDMap::build(BuildSeal::new()),
txn,
IPERef::new(value),
)?;
Ok(<OE::ID>::from_raw(id.0))
}
fn insert_and_return(&self, txn: &mut Transaction, value: OE) -> DBResult<crate::Stored<OE>> {
use crate::IDMap;
let rval = IDMap::<IPEntity<OE>>::insert_and_return(
&IDMap::build(BuildSeal::new()),
txn,
IPEntity(value),
)?;
let id = rval.id();
Ok(crate::Stored::new(
<OE::ID>::from_raw(id.into_raw()),
rval.wrapped().0,
))
}
}
impl<OE: Entity> MigrateMap<OE> {
pub fn insert_matching<ME: Entity>(
&self,
txn: &mut Transaction,
value: OE,
old_value: crate::Stored<ME>,
) -> DBResult<()> {
let id = old_value.id().into_raw();
crate::query::base_queries::insert_exact(txn, &IPEntity(value), IPEntityID::from_raw(id))?;
Ok(())
}
}
impl Schema for super::SentinelDatabaseItem {}
pub trait SchemaList {
type Head: Schema + MigratableItem<<Self::Tail as SchemaList>::Head>;
type Tail: SchemaList;
const EMPTY: bool = false;
}
impl SchemaList for () {
type Head = SentinelDatabaseItem;
type Tail = ();
const EMPTY: bool = true;
}
impl<S0: Schema> SchemaList for (S0,)
where
S0: MigratableItem<SentinelDatabaseItem>,
{
type Head = S0;
type Tail = ();
}
fn migration_helper<A: SchemaList>(txn: &mut Transaction) -> DBResult<()> {
if A::EMPTY {
return Err(Error::IncompatibleSchema);
}
let built = generate_from_schema::<A::Head>();
log::trace!(
"checking if head schema ({}) is present",
A::Head::NAME.unwrap_or("<anonymous>")
);
match built.check(txn) {
Some(true) => {
log::trace!("head schema present, nothing to do");
Ok(())
},
Some(false) => {
migration_helper::<A::Tail>(txn)?;
type MigrateTo<A> = <A as SchemaList>::Head;
type MigrateFrom<A> = <<A as SchemaList>::Tail as SchemaList>::Head;
log::trace!(
"running migration: {} -> {}",
std::any::type_name::<MigrateFrom<A>>(),
std::any::type_name::<MigrateTo<A>>(),
);
schema::check::check_schema::<MigrateFrom<A>>(txn)?;
log::trace!("schema check succeeded");
let mut context = MigrationContext {
txn,
into_gen: built.clone(),
from_gen: generate_from_schema::<MigrateFrom<A>>(),
in_progress: vec![],
};
let prev_schema = MigrateFrom::<A>::build(BuildSeal::new());
<MigrateTo<A> as MigratableItem<MigrateFrom<A>>>::run_migration(
&prev_schema,
&mut context,
)?;
context.finish()?;
built.update_metadata(txn)?;
schema::check::check_schema::<A::Head>(txn)?;
Ok(())
},
None => Err(Error::EmptyDatabase),
}
}
pub fn run_migration<A: SchemaList>(pool: &crate::ConnectionPool) -> DBResult<A::Head> {
let mut lease = pool.acquire()?;
lease.execute_raw_sql("PRAGMA foreign_keys=OFF")?;
drop(lease);
let mut txn = pool.start()?;
migration_helper::<A>(&mut txn)?;
let r = txn.lease().check_foreign_keys()?;
if !r.is_empty() {
return Err(Error::ConsistencyError(format!(
"foreign key constraints not satisfied after migration: {}",
r.join(",")
)));
}
txn.commit()?;
let mut lease = pool.acquire()?;
lease.execute_raw_sql("PRAGMA foreign_keys=ON")?;
drop(lease);
Ok(A::Head::build(BuildSeal::new()))
}