Skip to main content

tideway_cli/commands/
migrate.rs

1//! Migrate command - run database migrations via the configured backend.
2
3use anyhow::{Context, Result};
4use std::fs;
5use std::path::{Path, PathBuf};
6use std::process::Command;
7
8use crate::cli::{MigrateArgs, MigrateBackend};
9use crate::env::{ensure_env, ensure_project_dir, read_env_map};
10use crate::{is_plan_mode, print_info, print_success, print_warning};
11
12pub fn run(args: MigrateArgs) -> Result<()> {
13    if is_plan_mode() {
14        print_info(&format!("Plan: would run migrations ({})", args.action));
15        return Ok(());
16    }
17    let project_dir = PathBuf::from(&args.path);
18    ensure_project_dir(&project_dir)?;
19
20    if !args.no_env {
21        ensure_env(&project_dir, args.fix_env)?;
22    }
23
24    if args.action == "init" {
25        let backend = resolve_backend(&project_dir, args.backend)?;
26        return match backend {
27            MigrateBackend::SeaOrm => init_sea_orm_migration(&project_dir),
28            MigrateBackend::Auto => Err(anyhow::anyhow!(
29                "Unable to detect migration backend; pass --backend"
30            )),
31        };
32    }
33
34    let backend = resolve_backend(&project_dir, args.backend)?;
35    match backend {
36        MigrateBackend::SeaOrm => run_sea_orm_cli(&project_dir, &args),
37        MigrateBackend::Auto => Err(anyhow::anyhow!(
38            "Unable to detect migration backend; pass --backend"
39        )),
40    }
41}
42
43fn resolve_backend(project_dir: &Path, backend: MigrateBackend) -> Result<MigrateBackend> {
44    match backend {
45        MigrateBackend::Auto => detect_backend(project_dir),
46        MigrateBackend::SeaOrm => Ok(MigrateBackend::SeaOrm),
47    }
48}
49
50fn detect_backend(project_dir: &Path) -> Result<MigrateBackend> {
51    let cargo_path = project_dir.join("Cargo.toml");
52    let contents = fs::read_to_string(&cargo_path)
53        .with_context(|| format!("Failed to read {}", cargo_path.display()))?;
54    let doc = contents
55        .parse::<toml_edit::DocumentMut>()
56        .context("Failed to parse Cargo.toml")?;
57
58    let deps = doc.get("dependencies");
59    let has_sea_orm = deps
60        .and_then(|deps| deps.get("sea-orm"))
61        .is_some();
62    let has_tideway_db = deps
63        .and_then(|deps| deps.get("tideway"))
64        .and_then(|item| item.get("features"))
65        .and_then(|item| item.as_array())
66        .map(|arr| arr.iter().any(|v| v.as_str() == Some("database")))
67        .unwrap_or(false);
68
69    if has_sea_orm || has_tideway_db {
70        Ok(MigrateBackend::SeaOrm)
71    } else {
72        Err(anyhow::anyhow!(
73            "Could not detect migration backend (add sea-orm or pass --backend)"
74        ))
75    }
76}
77
78fn run_sea_orm_cli(project_dir: &Path, args: &MigrateArgs) -> Result<()> {
79    let migrations_dir = project_dir.join("migration");
80    if !migrations_dir.exists() {
81        print_warning("migration/ directory not found; sea-orm-cli may fail");
82    }
83
84    if args.action == "status" || args.action == "up" || args.action == "down" || args.action == "reset" {
85        ensure_database_url(project_dir)?;
86    }
87
88    let mut command = Command::new("sea-orm-cli");
89    command
90        .arg("migrate")
91        .arg(&args.action)
92        .current_dir(project_dir);
93
94    if !args.args.is_empty() {
95        command.args(&args.args);
96    }
97
98    if !args.no_env {
99        if let Some(env_map) = read_env_map(&project_dir.join(".env")) {
100            command.envs(env_map);
101        }
102    }
103
104    print_info(&format!("Running sea-orm-cli migrate {}...", args.action));
105    let status = command
106        .status()
107        .context("Failed to run sea-orm-cli (is it installed?)")?;
108
109    if status.success() {
110        print_success("Migrations completed");
111        Ok(())
112    } else {
113        Err(anyhow::anyhow!(
114            "sea-orm-cli exited with status {}",
115            status
116        ))
117    }
118}
119
120fn ensure_database_url(project_dir: &Path) -> Result<()> {
121    if let Some(env_map) = read_env_map(&project_dir.join(".env")) {
122        if let Some(value) = env_map.get("DATABASE_URL") {
123            validate_database_url(value)?;
124            return Ok(());
125        }
126    }
127
128    if let Ok(value) = std::env::var("DATABASE_URL") {
129        validate_database_url(&value)?;
130        return Ok(());
131    }
132
133    Err(anyhow::anyhow!(
134        "DATABASE_URL is missing (set it in .env or the environment)"
135    ))
136}
137
138fn validate_database_url(value: &str) -> Result<()> {
139    if !value.contains("://") {
140        return Err(anyhow::anyhow!(
141            "DATABASE_URL looks invalid (missing scheme): {}",
142            value
143        ));
144    }
145
146    let lower = value.to_lowercase();
147    let valid = lower.starts_with("postgres://")
148        || lower.starts_with("postgresql://")
149        || lower.starts_with("sqlite:");
150
151    if !valid {
152        return Err(anyhow::anyhow!(
153            "DATABASE_URL scheme looks invalid: {}",
154            value
155        ));
156    }
157
158    Ok(())
159}
160
161fn init_sea_orm_migration(project_dir: &Path) -> Result<()> {
162    let migration_root = project_dir.join("migration");
163    if migration_root.exists() {
164        print_warning("migration/ already exists; skipping init");
165        return Ok(());
166    }
167
168    let mut command = Command::new("sea-orm-cli");
169    command
170        .arg("migrate")
171        .arg("init")
172        .current_dir(project_dir);
173
174    if let Some(env_map) = read_env_map(&project_dir.join(".env")) {
175        command.envs(env_map);
176    }
177
178    print_info("Initializing SeaORM migration crate...");
179    let status = command
180        .status()
181        .context("Failed to run sea-orm-cli (is it installed?)")?;
182
183    if status.success() {
184        print_success("Migration crate initialized");
185        Ok(())
186    } else {
187        Err(anyhow::anyhow!(
188            "sea-orm-cli exited with status {}",
189            status
190        ))
191    }
192}