1use crate::error::{DatabaseError as _, Error, TernResult};
9use crate::migration::{
10 AppliedMigration, Migration, MigrationContext, MigrationId,
11};
12
13use chrono::{DateTime, Utc};
14use display_json::{DebugAsJson, DisplayAsJsonPretty};
15use serde::Serialize;
16use std::collections::HashSet;
17use std::fmt::Write;
18
19pub struct Runner<C: MigrationContext> {
21 context: C,
22}
23
24impl<C> Runner<C>
25where
26 C: MigrationContext,
27{
28 pub fn new(context: C) -> Self {
30 Self { context }
31 }
32
33 pub async fn init_history(&mut self) -> TernResult<()> {
35 self.context.check_history_table().await
36 }
37
38 pub async fn drop_history(&mut self) -> TernResult<()> {
40 self.context.drop_history_table().await
41 }
42
43 async fn validate_source(&mut self) -> TernResult<()> {
45 self.context.check_history_table().await?;
46 let applied: HashSet<MigrationId> = self
47 .context
48 .previously_applied()
49 .await?
50 .into_iter()
51 .map(MigrationId::from)
52 .collect();
53 let source: HashSet<MigrationId> = self
54 .context
55 .migration_set(None)
56 .migration_ids()
57 .into_iter()
58 .collect();
59
60 check_migrations_in_sync(applied, source)
61 }
62
63 fn validate_target(
65 &self,
66 last_applied: Option<i64>,
67 target_version: Option<i64>,
68 ) -> TernResult<()> {
69 let Some(source) = self.context.migration_set(None).max() else {
70 return Ok(());
71 };
72 if let Some(target) = target_version {
73 match last_applied {
74 Some(applied) if target < applied => {
75 Err(Error::Invalid(format!(
76 "target version V{target} earlier than latest applied version V{applied}",
77 )))?
78 },
79 _ if target > source => Err(Error::Invalid(format!(
80 "target version V{target} does not exist, latest version found was V{source}",
81 )))?,
82 _ => Ok(()),
83 }
84 } else {
85 Ok(())
86 }
87 }
88
89 pub async fn run_apply(
91 &mut self,
92 target_version: Option<i64>,
93 dryrun: bool,
94 ) -> TernResult<Report> {
95 self.validate_source().await?;
96 let last_applied = self.context.latest_version().await?;
97 self.validate_target(last_applied, target_version)?;
98
99 let unapplied = self.context.migration_set(last_applied);
100
101 let mut results = Vec::new();
102 for migration in &unapplied.migrations {
103 let id = migration.migration_id();
104 let ver = migration.version();
105
106 if matches!(target_version, Some(end) if ver > end) {
108 break;
109 }
110
111 let result = if dryrun {
112 let query = migration
114 .build(&mut self.context)
115 .await
116 .with_report(&results)?;
117
118 MigrationResult::from_unapplied(migration.as_ref(), query.sql())
119 } else {
120 log::trace!("applying migration {id}");
121
122 self.context
123 .apply(migration.as_ref())
124 .await
125 .tern_migration_result(migration.as_ref())
126 .with_report(&results)
127 .map(|v| {
128 MigrationResult::from_applied(
129 &v,
130 Some(migration.no_tx()),
131 )
132 })?
133 };
134
135 results.push(result);
136 }
137
138 Ok(Report::new(results))
139 }
140
141 #[deprecated(since = "3.1.0", note = "use `run_apply_all`")]
143 pub async fn apply_all(&mut self) -> TernResult<Report> {
144 self.run_apply(None, false).await
145 }
146
147 pub async fn run_apply_all(&mut self, dryrun: bool) -> TernResult<Report> {
149 self.run_apply(None, dryrun).await
150 }
151
152 pub async fn list_applied(&mut self) -> TernResult<Report> {
154 self.validate_source().await?;
155
156 let applied = self
157 .context
158 .previously_applied()
159 .await?
160 .iter()
161 .map(|m| MigrationResult::from_applied(m, None))
162 .collect::<Vec<_>>();
163 let report = Report::new(applied);
164
165 Ok(report)
166 }
167
168 #[deprecated(
169 since = "3.1.0",
170 note = "no valid use case for `start_version`"
171 )]
172 pub async fn soft_apply(
173 &mut self,
174 start_version: Option<i64>,
175 target_version: Option<i64>,
176 ) -> TernResult<Report> {
177 if start_version.is_some() {
178 return Err(Error::Invalid(
179 "no valid `start_version` other than the first unapplied, use `run_soft_apply`"
180 .into(),
181 ));
182 }
183 self.run_soft_apply(target_version, false).await
184 }
185
186 pub async fn run_soft_apply(
194 &mut self,
195 target_version: Option<i64>,
196 dryrun: bool,
197 ) -> TernResult<Report> {
198 self.validate_source().await?;
199 let last_applied = self.context.latest_version().await?;
200 self.validate_target(last_applied, target_version)?;
201
202 let unapplied = self.context.migration_set(last_applied);
203
204 let mut results = Vec::new();
205 for migration in &unapplied.migrations {
206 let id = migration.migration_id();
207 let ver = migration.version();
208
209 if matches!(target_version, Some(end) if ver > end) {
211 break;
212 }
213
214 let query = migration
216 .build(&mut self.context)
217 .await
218 .with_report(&results)?;
219 let mut content = String::from("-- SOFT APPLIED:\n\n");
220 writeln!(content, "{query}")?;
221
222 let applied = migration.to_applied(0, Utc::now(), &content);
223 let result = MigrationResult::from_soft_applied(&applied, dryrun);
224
225 if !dryrun {
226 log::trace!("soft applying migration {id}");
227 self.context
228 .insert_applied(&applied)
229 .await
230 .with_report(&results)?;
231 }
232
233 results.push(result);
234 }
235 let report = Report::new(results);
236
237 Ok(report)
238 }
239}
240
241#[derive(Clone, Serialize, DebugAsJson, DisplayAsJsonPretty, Default)]
243pub struct Report {
244 migrations: Vec<MigrationResult>,
245}
246
247impl Report {
248 pub fn new(migrations: Vec<MigrationResult>) -> Self {
249 Self { migrations }
250 }
251
252 pub fn count(&self) -> usize {
253 self.migrations.len()
254 }
255
256 pub fn results(&self) -> Vec<MigrationResult> {
258 self.migrations.clone()
259 }
260
261 pub fn iter_results(&self) -> impl Iterator<Item = MigrationResult> {
263 self.migrations.clone().into_iter()
264 }
265}
266
267#[derive(Clone, Serialize, DebugAsJson, DisplayAsJsonPretty)]
270#[allow(dead_code)]
271pub struct MigrationResult {
272 dryrun: bool,
273 version: i64,
274 state: MigrationState,
275 applied_at: Option<DateTime<Utc>>,
276 description: String,
277 content: String,
278 transactional: Transactional,
279 duration_ms: RunDuration,
280}
281
282impl MigrationResult {
283 pub(crate) fn from_applied(
284 applied: &AppliedMigration,
285 no_tx: Option<bool>,
286 ) -> Self {
287 Self {
288 dryrun: false,
289 version: applied.version,
290 state: MigrationState::Applied,
291 applied_at: Some(applied.applied_at),
292 description: applied.description.clone(),
293 content: applied.content.clone(),
294 transactional: no_tx.map(Transactional::from_boolean).unwrap_or(
295 Transactional::Other("Previously applied".to_string()),
296 ),
297 duration_ms: RunDuration::Duration(applied.duration_ms),
298 }
299 }
300
301 pub(crate) fn from_soft_applied(
302 applied: &AppliedMigration,
303 dryrun: bool,
304 ) -> Self {
305 Self {
306 dryrun,
307 version: applied.version,
308 state: MigrationState::SoftApplied,
309 applied_at: Some(applied.applied_at),
310 description: applied.description.clone(),
311 content: applied.content.clone(),
312 transactional: Transactional::Other("Soft applied".to_string()),
313 duration_ms: RunDuration::Duration(applied.duration_ms),
314 }
315 }
316
317 pub(crate) fn from_unapplied<M>(migration: &M, content: &str) -> Self
318 where
319 M: Migration + ?Sized,
320 {
321 Self {
322 dryrun: true,
323 version: migration.version(),
324 state: MigrationState::Unapplied,
325 applied_at: None,
326 description: migration.migration_id().description(),
327 content: content.into(),
328 transactional: Transactional::from_boolean(migration.no_tx()),
329 duration_ms: RunDuration::Unapplied,
330 }
331 }
332}
333
334#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Serialize)]
335enum MigrationState {
336 Applied,
337 SoftApplied,
338 Unapplied,
339}
340
341#[derive(Debug, Clone, Serialize)]
342enum Transactional {
343 NoTransaction,
344 InTransaction,
345 Other(String),
346}
347
348impl Transactional {
349 fn from_boolean(v: bool) -> Self {
350 if v {
351 return Self::NoTransaction;
352 };
353 Self::InTransaction
354 }
355}
356
357#[derive(Debug, Clone, Copy, Serialize)]
358enum RunDuration {
359 Duration(i64),
360 Unapplied,
361}
362
363impl std::fmt::Display for Transactional {
364 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
365 match self {
366 Self::NoTransaction => write!(f, "No Transaction"),
367 Self::InTransaction => write!(f, "In Transaction"),
368 Self::Other(s) => write!(f, "{s}"),
369 }
370 }
371}
372
373impl std::fmt::Display for MigrationState {
374 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
375 match self {
376 Self::Applied => write!(f, "Applied"),
377 Self::SoftApplied => write!(f, "Soft Applied"),
378 Self::Unapplied => write!(f, "Not Applied"),
379 }
380 }
381}
382
383impl std::fmt::Display for RunDuration {
384 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
385 match self {
386 Self::Duration(ms) => write!(f, "{}ms", ms),
387 Self::Unapplied => write!(f, "Not Applied"),
388 }
389 }
390}
391
392fn check_migrations_in_sync(
394 applied: HashSet<MigrationId>,
395 source: HashSet<MigrationId>,
396) -> TernResult<()> {
397 let source_not_found: Vec<&MigrationId> =
398 applied.difference(&source).collect();
399
400 if !source_not_found.is_empty() {
401 return Err(Error::OutOfSync {
402 at_issue: source_not_found.into_iter().cloned().collect(),
403 msg: "version/name applied but missing in source".into(),
404 });
405 }
406
407 Ok(())
408}
409
410#[cfg(test)]
411mod tests {
412 use super::Error;
413 use super::MigrationId;
414
415 use std::collections::HashSet;
416
417 #[test]
418 fn missing_source() {
419 let source: HashSet<MigrationId> = vec![
420 MigrationId::new(1, "first".into()),
421 MigrationId::new(2, "second".into()),
422 MigrationId::new(3, "fourth".into()),
423 ]
424 .into_iter()
425 .collect();
426 let applied: HashSet<MigrationId> = vec![
427 MigrationId::new(1, "first".into()),
428 MigrationId::new(2, "second".into()),
429 MigrationId::new(3, "third".into()),
430 ]
431 .into_iter()
432 .collect();
433 let missing = vec![MigrationId::new(3, "third".into())];
434 let result = super::check_migrations_in_sync(applied, source);
435 assert!(result.is_err());
436 let err = result.unwrap_err();
437 assert!(
438 matches!(err, Error::OutOfSync { at_issue, .. } if *at_issue == missing)
439 );
440 }
441
442 #[test]
443 fn fewer_in_source() {
444 let source: HashSet<MigrationId> = vec![
445 MigrationId::new(1, "first".into()),
446 MigrationId::new(2, "second".into()),
447 MigrationId::new(3, "third".into()),
448 ]
449 .into_iter()
450 .collect();
451 let applied: HashSet<MigrationId> = vec![
452 MigrationId::new(1, "first".into()),
453 MigrationId::new(2, "second".into()),
454 MigrationId::new(3, "third".into()),
455 MigrationId::new(4, "fourth".into()),
456 ]
457 .into_iter()
458 .collect();
459 let missing = vec![MigrationId::new(4, "fourth".into())];
460 let result = super::check_migrations_in_sync(applied, source);
461 assert!(result.is_err());
462 let err = result.unwrap_err();
463 assert!(
464 matches!(err, Error::OutOfSync { at_issue, .. } if *at_issue == missing)
465 );
466 }
467
468 #[test]
469 fn mismatched_source() {
470 let source: HashSet<MigrationId> = vec![
471 MigrationId::new(1, "first".into()),
472 MigrationId::new(2, "second".into()),
473 MigrationId::new(3, "third".into()),
474 MigrationId::new(4, "fifth".into()),
475 MigrationId::new(5, "sixth".into()),
476 MigrationId::new(6, "seventh".into()),
477 MigrationId::new(7, "eighth".into()),
478 ]
479 .into_iter()
480 .collect();
481 let applied: HashSet<MigrationId> = vec![
482 MigrationId::new(1, "first".into()),
483 MigrationId::new(2, "second".into()),
484 MigrationId::new(3, "third".into()),
485 MigrationId::new(4, "fourth".into()),
486 MigrationId::new(5, "fifth".into()),
487 ]
488 .into_iter()
489 .collect();
490 let divergence = vec![
491 MigrationId::new(4, "fourth".into()),
492 MigrationId::new(5, "fifth".into()),
493 ];
494 let result = super::check_migrations_in_sync(applied, source);
495 assert!(result.is_err());
496 let err = result.unwrap_err();
497 let Error::OutOfSync { mut at_issue, .. } = err else {
498 panic!("expected Error::OutOfSync");
499 };
500 at_issue.sort_by_key(|migration| migration.version());
501 assert_eq!(divergence, at_issue);
502 }
503}