database_migration/action/
mod.rs1use 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;