Skip to main content

rust_query/migrate/
migration.rs

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