tideway_cli/commands/
migrate.rs1use 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.and_then(|deps| deps.get("sea-orm")).is_some();
60 let has_tideway_db = deps
61 .and_then(|deps| deps.get("tideway"))
62 .and_then(|item| item.get("features"))
63 .and_then(|item| item.as_array())
64 .map(|arr| arr.iter().any(|v| v.as_str() == Some("database")))
65 .unwrap_or(false);
66
67 if has_sea_orm || has_tideway_db {
68 Ok(MigrateBackend::SeaOrm)
69 } else {
70 Err(anyhow::anyhow!(
71 "Could not detect migration backend (add sea-orm or pass --backend)"
72 ))
73 }
74}
75
76fn run_sea_orm_cli(project_dir: &Path, args: &MigrateArgs) -> Result<()> {
77 let migrations_dir = project_dir.join("migration");
78 if !migrations_dir.exists() {
79 print_warning("migration/ directory not found; sea-orm-cli may fail");
80 }
81
82 if args.action == "status"
83 || args.action == "up"
84 || args.action == "down"
85 || args.action == "reset"
86 {
87 ensure_database_url(project_dir)?;
88 }
89
90 let mut command = Command::new("sea-orm-cli");
91 command
92 .arg("migrate")
93 .arg(&args.action)
94 .current_dir(project_dir);
95
96 if !args.args.is_empty() {
97 command.args(&args.args);
98 }
99
100 if !args.no_env {
101 if let Some(env_map) = read_env_map(&project_dir.join(".env")) {
102 command.envs(env_map);
103 }
104 }
105
106 print_info(&format!("Running sea-orm-cli migrate {}...", args.action));
107 let status = command
108 .status()
109 .context("Failed to run sea-orm-cli (is it installed?)")?;
110
111 if status.success() {
112 print_success("Migrations completed");
113 Ok(())
114 } else {
115 Err(anyhow::anyhow!("sea-orm-cli exited with status {}", status))
116 }
117}
118
119fn ensure_database_url(project_dir: &Path) -> Result<()> {
120 if let Some(env_map) = read_env_map(&project_dir.join(".env")) {
121 if let Some(value) = env_map.get("DATABASE_URL") {
122 validate_database_url(value)?;
123 return Ok(());
124 }
125 }
126
127 if let Ok(value) = std::env::var("DATABASE_URL") {
128 validate_database_url(&value)?;
129 return Ok(());
130 }
131
132 Err(anyhow::anyhow!(
133 "DATABASE_URL is missing (set it in .env or the environment)"
134 ))
135}
136
137fn validate_database_url(value: &str) -> Result<()> {
138 if !value.contains("://") {
139 return Err(anyhow::anyhow!(
140 "DATABASE_URL looks invalid (missing scheme): {}",
141 value
142 ));
143 }
144
145 let lower = value.to_lowercase();
146 let valid = lower.starts_with("postgres://")
147 || lower.starts_with("postgresql://")
148 || lower.starts_with("sqlite:");
149
150 if !valid {
151 return Err(anyhow::anyhow!(
152 "DATABASE_URL scheme looks invalid: {}",
153 value
154 ));
155 }
156
157 Ok(())
158}
159
160fn init_sea_orm_migration(project_dir: &Path) -> Result<()> {
161 let migration_root = project_dir.join("migration");
162 if migration_root.exists() {
163 print_warning("migration/ already exists; skipping init");
164 return Ok(());
165 }
166
167 let mut command = Command::new("sea-orm-cli");
168 command.arg("migrate").arg("init").current_dir(project_dir);
169
170 if let Some(env_map) = read_env_map(&project_dir.join(".env")) {
171 command.envs(env_map);
172 }
173
174 print_info("Initializing SeaORM migration crate...");
175 let status = command
176 .status()
177 .context("Failed to run sea-orm-cli (is it installed?)")?;
178
179 if status.success() {
180 print_success("Migration crate initialized");
181 Ok(())
182 } else {
183 Err(anyhow::anyhow!("sea-orm-cli exited with status {}", status))
184 }
185}