rust_query/migrate/
migration.rs

1use std::{
2    collections::{HashMap, HashSet},
3    convert::Infallible,
4    marker::PhantomData,
5    ops::Deref,
6};
7
8use sea_query::{Alias, IntoTableRef, TableDropStatement};
9
10use crate::{
11    IntoExpr, Lazy, Table, TableRow, Transaction,
12    alias::{Scope, TmpTable},
13    migrate::new_table_inner,
14    transaction::{TXN, try_insert_private},
15};
16
17pub trait Migration {
18    type FromSchema: 'static;
19    type From: Table<Schema = Self::FromSchema>;
20    type To: Table<MigrateFrom = Self::From>;
21    type Conflict;
22
23    #[doc(hidden)]
24    fn prepare(
25        val: Self,
26        prev: crate::Expr<'static, Self::FromSchema, Self::From>,
27    ) -> <Self::To as Table>::Insert;
28    #[doc(hidden)]
29    fn map_conflict(val: TableRow<Self::From>) -> Self::Conflict;
30}
31
32/// Transaction type for use in migrations.
33pub struct TransactionMigrate<FromSchema> {
34    pub(super) inner: Transaction<FromSchema>,
35    pub(super) scope: Scope,
36    pub(super) rename_map: HashMap<&'static str, TmpTable>,
37    // creating indices is delayed so that they don't need to be renamed
38    pub(super) extra_index: Vec<String>,
39}
40
41impl<FromSchema> Deref for TransactionMigrate<FromSchema> {
42    type Target = Transaction<FromSchema>;
43
44    fn deref(&self) -> &Self::Target {
45        &self.inner
46    }
47}
48
49impl<FromSchema: 'static> TransactionMigrate<FromSchema> {
50    fn new_table_name<T: Table>(&mut self) -> TmpTable {
51        *self.rename_map.entry(T::NAME).or_insert_with(|| {
52            let new_table_name = self.scope.tmp_table();
53            TXN.with_borrow(|txn| {
54                let conn = txn.as_ref().unwrap().get();
55                let table = crate::schema::from_macro::Table::new::<T>();
56                new_table_inner(conn, &table, new_table_name);
57                self.extra_index.extend(table.create_indices(T::NAME));
58            });
59            new_table_name
60        })
61    }
62
63    fn unmigrated<M: Migration<FromSchema = FromSchema>>(
64        &self,
65        new_name: TmpTable,
66    ) -> impl Iterator<Item = TableRow<M::From>> {
67        let data = self.inner.query(|rows| {
68            let old = rows.join_private::<M::From>();
69            rows.into_vec(old)
70        });
71
72        let migrated = Transaction::new().query(|rows| {
73            let new = rows.join_tmp::<M::To>(new_name);
74            rows.into_vec(new)
75        });
76        let migrated: HashSet<_> = migrated.into_iter().map(|x| x.inner.idx).collect();
77
78        data.into_iter()
79            .filter(move |row| !migrated.contains(&row.inner.idx))
80    }
81
82    /// Migrate some rows to the new schema.
83    ///
84    /// This will return an error when there is a conflict.
85    /// The error type depends on the number of unique constraints that the
86    /// migration can violate:
87    /// - 0 => [Infallible]
88    /// - 1.. => `TableRow<T::From>` (row in the old table that could not be migrated)
89    pub fn migrate_optional<'t, M: Migration<FromSchema = FromSchema>>(
90        &'t mut self,
91        mut f: impl FnMut(Lazy<'t, M::From>) -> Option<M>,
92    ) -> Result<(), M::Conflict> {
93        let new_name = self.new_table_name::<M::To>();
94
95        for row in self.unmigrated::<M>(new_name) {
96            if let Some(new) = f(self.lazy(row)) {
97                try_insert_private::<M::To>(
98                    new_name.into_table_ref(),
99                    Some(row.inner.idx),
100                    M::prepare(new, row.into_expr()),
101                )
102                .map_err(|_| M::map_conflict(row))?;
103            };
104        }
105        Ok(())
106    }
107
108    /// Migrate all rows to the new schema.
109    ///
110    /// Conflict errors work the same as in [Self::migrate_optional].
111    ///
112    /// However, this method will return [Migrated] when all rows are migrated.
113    /// This can then be used as proof that there will be no foreign key violations.
114    pub fn migrate<'t, M: Migration<FromSchema = FromSchema>>(
115        &'t mut self,
116        mut f: impl FnMut(Lazy<'t, M::From>) -> M,
117    ) -> Result<Migrated<'static, FromSchema, M::To>, M::Conflict> {
118        self.migrate_optional::<M>(|x| Some(f(x)))?;
119
120        Ok(Migrated {
121            _p: PhantomData,
122            f: Box::new(|_| {}),
123            _local: PhantomData,
124        })
125    }
126
127    /// Helper method for [Self::migrate].
128    ///
129    /// It can only be used when the migration is known to never cause unique constraint conflicts.
130    pub fn migrate_ok<'t, M: Migration<FromSchema = FromSchema, Conflict = Infallible>>(
131        &'t mut self,
132        f: impl FnMut(Lazy<'t, M::From>) -> M,
133    ) -> Migrated<'static, FromSchema, M::To> {
134        let Ok(res) = self.migrate(f);
135        res
136    }
137}
138
139/// [Migrated] provides a proof of migration.
140///
141/// This only needs to be provided for tables that are migrated from a previous table.
142pub struct Migrated<'t, FromSchema, T> {
143    _p: PhantomData<T>,
144    f: Box<dyn 't + FnOnce(&mut SchemaBuilder<'t, FromSchema>)>,
145    _local: PhantomData<*const ()>,
146}
147
148impl<'t, FromSchema: 'static, T: Table> Migrated<'t, FromSchema, T> {
149    /// Don't migrate the remaining rows.
150    ///
151    /// This can cause foreign key constraint violations, which is why an error callback needs to be provided.
152    pub fn map_fk_err(err: impl 't + FnOnce() -> Infallible) -> Self {
153        Self {
154            _p: PhantomData,
155            f: Box::new(|x| x.foreign_key::<T>(err)),
156            _local: PhantomData,
157        }
158    }
159
160    #[doc(hidden)]
161    pub fn apply(self, b: &mut SchemaBuilder<'t, FromSchema>) {
162        (self.f)(b)
163    }
164}
165
166pub struct SchemaBuilder<'t, FromSchema> {
167    pub(super) inner: TransactionMigrate<FromSchema>,
168    pub(super) drop: Vec<TableDropStatement>,
169    pub(super) foreign_key: HashMap<&'static str, Box<dyn 't + FnOnce() -> Infallible>>,
170}
171
172impl<'t, FromSchema: 'static> SchemaBuilder<'t, FromSchema> {
173    pub fn foreign_key<To: Table>(&mut self, err: impl 't + FnOnce() -> Infallible) {
174        self.inner.new_table_name::<To>();
175
176        self.foreign_key.insert(To::NAME, Box::new(err));
177    }
178
179    pub fn create_empty<To: Table>(&mut self) {
180        self.inner.new_table_name::<To>();
181    }
182
183    pub fn drop_table<T: Table>(&mut self) {
184        let name = Alias::new(T::NAME);
185        let step = sea_query::Table::drop().table(name).take();
186        self.drop.push(step);
187    }
188}