Skip to main content

spawn_db/commands/migration/
adopt.rs

1use crate::commands::migration::get_pending_and_confirm;
2use crate::commands::{Command, Outcome, TelemetryDescribe, TelemetryInfo};
3use crate::config::Config;
4use crate::engine::MigrationError;
5use anyhow::{anyhow, Result};
6use dialoguer::Editor;
7
8pub struct AdoptMigration {
9    pub migration: Option<String>,
10    pub yes: bool,
11    pub description: Option<String>,
12}
13
14impl TelemetryDescribe for AdoptMigration {
15    fn telemetry(&self) -> TelemetryInfo {
16        TelemetryInfo::new("migration adopt")
17    }
18}
19
20/// Prompt the user for a description using their preferred editor.
21/// Returns an error if the description is empty or the editor is aborted.
22fn prompt_description() -> Result<String> {
23    let description = Editor::new()
24        .require_save(true)
25        .edit("# Why is this migration being adopted?\n# Lines starting with # will be ignored.\n")?
26        .map(|s| {
27            s.lines()
28                .filter(|line| !line.starts_with('#'))
29                .collect::<Vec<_>>()
30                .join("\n")
31                .trim()
32                .to_string()
33        })
34        .unwrap_or_default();
35
36    if description.is_empty() {
37        return Err(anyhow!("A description is required when adopting migrations. Use --description or provide one in the editor."));
38    }
39
40    Ok(description)
41}
42
43impl Command for AdoptMigration {
44    async fn execute(&self, config: &Config) -> Result<Outcome> {
45        let migrations = match &self.migration {
46            Some(migration) => vec![migration.clone()],
47            None => match get_pending_and_confirm(config, "adopt", self.yes).await? {
48                Some(pending) => pending,
49                None => return Ok(Outcome::AdoptedMigration),
50            },
51        };
52
53        let description = match &self.description {
54            Some(desc) => {
55                if desc.trim().is_empty() {
56                    return Err(anyhow!(
57                        "A description is required when adopting migrations."
58                    ));
59                }
60                desc.clone()
61            }
62            None => prompt_description()?,
63        };
64
65        let engine = config.new_engine().await?;
66
67        let total = migrations.len();
68        for (i, migration) in migrations.iter().enumerate() {
69            let counter = if total > 1 {
70                format!(
71                    "[{:>width$}/{}] ",
72                    i + 1,
73                    total,
74                    width = total.to_string().len()
75                )
76            } else {
77                String::new()
78            };
79            match engine
80                .migration_adopt(migration, super::DEFAULT_NAMESPACE, &description)
81                .await
82            {
83                Ok(msg) => {
84                    println!("{}{}", counter, msg);
85                }
86                Err(MigrationError::AlreadyApplied { info, .. }) => {
87                    println!(
88                        "{}Migration '{}' already applied (status: {}, activity: {})",
89                        counter, migration, info.last_status, info.last_activity
90                    );
91                }
92                Err(e) => {
93                    return Err(
94                        anyhow!(e).context(format!("Failed adopting migration '{}'", migration))
95                    );
96                }
97            }
98        }
99
100        Ok(Outcome::AdoptedMigration)
101    }
102}