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
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}