database_migration/action/
mod.rs

1use crate::migration::{
2    ApplicableMigration, Execution, Problem, ProblematicMigration, ScriptContent,
3};
4use chrono::NaiveDateTime;
5use enumset::{EnumSet, EnumSetIter, EnumSetType};
6use indexmap::IndexMap;
7use std::marker::PhantomData;
8use std::ops::{Add, AddAssign};
9
10pub trait ListOutOfOrder {
11    fn list_out_of_order(
12        &self,
13        defined_migrations: &[ScriptContent],
14        executed_migrations: &IndexMap<NaiveDateTime, Execution>,
15    ) -> Vec<ProblematicMigration>;
16}
17
18pub trait ListChangedAfterExecution {
19    fn list_changed_after_execution(
20        &self,
21        defined_migrations: &[ScriptContent],
22        executed_migrations: &IndexMap<NaiveDateTime, Execution>,
23    ) -> Vec<ProblematicMigration>;
24}
25
26#[derive(EnumSetType, Debug)]
27pub enum Check {
28    Checksum,
29    Order,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub struct Checks(EnumSet<Check>);
34
35impl Checks {
36    pub const fn none() -> Self {
37        Self(EnumSet::empty())
38    }
39
40    pub const fn all() -> Self {
41        Self(EnumSet::all())
42    }
43
44    pub fn only(check: Check) -> Self {
45        Self(EnumSet::only(check))
46    }
47
48    pub fn contains(&self, check: Check) -> bool {
49        self.0.contains(check)
50    }
51
52    pub fn iter(&self) -> CheckIter {
53        CheckIter {
54            set_iter: self.0.iter(),
55        }
56    }
57}
58
59impl From<Check> for Checks {
60    fn from(value: Check) -> Self {
61        Self(EnumSet::from(value))
62    }
63}
64
65#[allow(clippy::suspicious_arithmetic_impl)]
66impl Add<Self> for Check {
67    type Output = Checks;
68
69    fn add(self, rhs: Self) -> Self::Output {
70        Checks(self | rhs)
71    }
72}
73
74#[allow(clippy::suspicious_op_assign_impl)]
75impl AddAssign<Check> for Checks {
76    fn add_assign(&mut self, rhs: Check) {
77        self.0 |= rhs;
78    }
79}
80
81#[derive(Clone)]
82pub struct CheckIter {
83    set_iter: EnumSetIter<Check>,
84}
85
86impl Iterator for CheckIter {
87    type Item = Check;
88
89    fn next(&mut self) -> Option<Self::Item> {
90        self.set_iter.next()
91    }
92
93    fn size_hint(&self) -> (usize, Option<usize>) {
94        self.set_iter.size_hint()
95    }
96}
97
98impl IntoIterator for &Checks {
99    type Item = Check;
100    type IntoIter = CheckIter;
101
102    fn into_iter(self) -> Self::IntoIter {
103        self.iter()
104    }
105}
106
107impl IntoIterator for Checks {
108    type Item = Check;
109    type IntoIter = CheckIter;
110
111    fn into_iter(self) -> Self::IntoIter {
112        self.iter()
113    }
114}
115
116#[must_use]
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118pub struct Verify {
119    ignore_checksums: bool,
120    ignore_order: bool,
121}
122
123#[allow(clippy::derivable_impls)]
124impl Default for Verify {
125    fn default() -> Self {
126        Self {
127            ignore_checksums: false,
128            ignore_order: false,
129        }
130    }
131}
132
133impl From<Checks> for Verify {
134    fn from(checks: Checks) -> Self {
135        Self {
136            ignore_checksums: !checks.contains(Check::Checksum),
137            ignore_order: !checks.contains(Check::Order),
138        }
139    }
140}
141
142impl Verify {
143    pub const fn with_ignore_checksums(mut self, ignore_checksums: bool) -> Self {
144        self.ignore_checksums = ignore_checksums;
145        self
146    }
147
148    pub const fn ignore_checksums(&self) -> bool {
149        self.ignore_checksums
150    }
151
152    pub const fn with_ignore_order(mut self, ignore_order: bool) -> Self {
153        self.ignore_order = ignore_order;
154        self
155    }
156
157    pub const fn ignore_order(&self) -> bool {
158        self.ignore_order
159    }
160}
161
162impl ListOutOfOrder for Verify {
163    fn list_out_of_order(
164        &self,
165        defined_migrations: &[ScriptContent],
166        executed_migrations: &IndexMap<NaiveDateTime, Execution>,
167    ) -> Vec<ProblematicMigration> {
168        if self.ignore_order {
169            return Vec::new();
170        }
171        if let Some(&last_applied_key) = executed_migrations.keys().max_by_key(|key| **key) {
172            defined_migrations
173                .iter()
174                .filter_map(|mig| {
175                    if last_applied_key > mig.key && !executed_migrations.contains_key(&mig.key) {
176                        Some(ProblematicMigration {
177                            key: mig.key,
178                            kind: mig.kind,
179                            script_path: mig.path.clone(),
180                            problem: Problem::OutOfOrder { last_applied_key },
181                        })
182                    } else {
183                        None
184                    }
185                })
186                .collect()
187        } else {
188            Vec::new()
189        }
190    }
191}
192
193impl ListChangedAfterExecution for Verify {
194    fn list_changed_after_execution(
195        &self,
196        defined_migrations: &[ScriptContent],
197        executed_migrations: &IndexMap<NaiveDateTime, Execution>,
198    ) -> Vec<ProblematicMigration> {
199        if self.ignore_checksums {
200            return Vec::new();
201        }
202        defined_migrations
203            .iter()
204            .filter_map(|mig| {
205                if mig.kind.is_forward() {
206                    executed_migrations.get(&mig.key).and_then(|exec| {
207                        if exec.checksum != mig.checksum {
208                            Some(ProblematicMigration {
209                                key: mig.key,
210                                kind: mig.kind,
211                                script_path: mig.path.clone(),
212                                problem: Problem::ChecksumMismatch {
213                                    definition_checksum: mig.checksum,
214                                    execution_checksum: exec.checksum,
215                                },
216                            })
217                        } else {
218                            None
219                        }
220                    })
221                } else {
222                    None
223                }
224            })
225            .collect()
226    }
227}
228
229pub trait MigrationsToApply {
230    fn list_migrations_to_apply(
231        &self,
232        defined_migrations: &[ScriptContent],
233        executed_migrations: &IndexMap<NaiveDateTime, Execution>,
234    ) -> IndexMap<NaiveDateTime, ApplicableMigration>;
235}
236
237#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
238pub struct Migrate {
239    _seal: PhantomData<()>,
240}
241
242impl MigrationsToApply for Migrate {
243    fn list_migrations_to_apply(
244        &self,
245        defined_migrations: &[ScriptContent],
246        executed_migrations: &IndexMap<NaiveDateTime, Execution>,
247    ) -> IndexMap<NaiveDateTime, ApplicableMigration> {
248        defined_migrations
249            .iter()
250            .filter(|mig| mig.kind.is_forward() && !executed_migrations.contains_key(&mig.key))
251            .map(to_applicable_migration)
252            .collect()
253    }
254}
255
256#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
257pub struct Revert {
258    _seal: PhantomData<()>,
259}
260
261impl MigrationsToApply for Revert {
262    fn list_migrations_to_apply(
263        &self,
264        defined_migrations: &[ScriptContent],
265        executed_migrations: &IndexMap<NaiveDateTime, Execution>,
266    ) -> IndexMap<NaiveDateTime, ApplicableMigration> {
267        defined_migrations
268            .iter()
269            .filter(|mig| mig.kind.is_backward() && executed_migrations.contains_key(&mig.key))
270            .map(to_applicable_migration)
271            .collect()
272    }
273}
274
275fn to_applicable_migration(mig: &ScriptContent) -> (NaiveDateTime, ApplicableMigration) {
276    (
277        mig.key,
278        ApplicableMigration {
279            key: mig.key,
280            kind: mig.kind,
281            script_content: mig.content.clone(),
282            checksum: mig.checksum,
283        },
284    )
285}
286
287#[cfg(test)]
288mod tests;