Skip to main content

tern_core/
runner.rs

1//! A migration runner for a context.
2//!
3//! The [`Runner`] type accepts any [`MigrationContext`] and exposes the methods
4//! needed for tasks related to database migrations.
5//!
6//! Each method also exists as a (sub)command of the `App`, available with the
7//! feature flag "cli" enabled.
8use 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
19/// Run operations on a set of migrations for the chosen context.
20pub struct Runner<C: MigrationContext> {
21    context: C,
22}
23
24impl<C> Runner<C>
25where
26    C: MigrationContext,
27{
28    /// Create a new `Runner` with default arguments from a context.
29    pub fn new(context: C) -> Self {
30        Self { context }
31    }
32
33    /// `CREATE IF NOT EXISTS` the history table.
34    pub async fn init_history(&mut self) -> TernResult<()> {
35        self.context.check_history_table().await
36    }
37
38    /// `DROP` the history table.
39    pub async fn drop_history(&mut self) -> TernResult<()> {
40        self.context.drop_history_table().await
41    }
42
43    // Find applied migrations that are not in the source directory.
44    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    // Check that the target migration version (for some operation) is valid.
64    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    /// Apply unapplied migrations up to and including the specified version.
90    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            // Reached the target version, break the loop.
107            if matches!(target_version, Some(end) if ver > end) {
108                break;
109            }
110
111            let result = if dryrun {
112                // Build each query, which possibly includes dynamic ones.
113                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    /// Apply all unapplied migrations.
142    #[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    /// Apply all unapplied migrations.
148    pub async fn run_apply_all(&mut self, dryrun: bool) -> TernResult<Report> {
149        self.run_apply(None, dryrun).await
150    }
151
152    /// List the migrations that have already been applied.
153    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    /// Run a "soft apply" of the migrations up to and including the specified
187    /// version.
188    ///
189    /// This means that the migration will be saved in the history table, but
190    /// will not have its query applied.  This is useful in the case where you
191    /// want to change migration tables, apply a patch to the current one,
192    /// migrate from a different migration tool, etc.
193    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            // Reached the last version, break the loop.
210            if matches!(target_version, Some(end) if ver > end) {
211                break;
212            }
213
214            // Build each query, which possibly includes dynamic ones.
215            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/// A formatted version of a collection of migrations.
242#[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    /// Return the vector of results.
257    pub fn results(&self) -> Vec<MigrationResult> {
258        self.migrations.clone()
259    }
260
261    /// Return an iterator of the migration results.
262    pub fn iter_results(&self) -> impl Iterator<Item = MigrationResult> {
263        self.migrations.clone().into_iter()
264    }
265}
266
267/// A formatted version of a migration that is the return type for `Runner`
268/// actions.
269#[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
392// Migrations that have been applied already but do not exist locally.
393fn 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}