1use std::collections::{HashMap, HashSet, VecDeque};
9
10use serde::Serialize;
11
12use crate::config::{DatabaseConfig, HooksConfig, MigrationSettings, WaypointConfig};
13use crate::db::DbClient;
14use crate::dialect::DialectKind;
15use crate::error::{Result, WaypointError};
16
17#[derive(Debug, Clone)]
19pub struct NamedDatabaseConfig {
20 pub name: String,
22 pub database: DatabaseConfig,
24 pub migrations: MigrationSettings,
26 pub hooks: HooksConfig,
28 pub placeholders: HashMap<String, String>,
30 pub depends_on: Vec<String>,
32}
33
34impl NamedDatabaseConfig {
35 pub fn to_waypoint_config(&self) -> WaypointConfig {
37 WaypointConfig {
38 database: self.database.clone(),
39 migrations: self.migrations.clone(),
40 hooks: self.hooks.clone(),
41 placeholders: self.placeholders.clone(),
42 ..WaypointConfig::default()
43 }
44 }
45}
46
47pub struct MultiWaypoint {
49 pub databases: Vec<NamedDatabaseConfig>,
51}
52
53#[derive(Debug, Serialize)]
55pub struct DatabaseResult {
56 pub name: String,
58 pub success: bool,
60 pub message: String,
62}
63
64#[derive(Debug, Serialize)]
66pub struct MultiResult {
67 pub results: Vec<DatabaseResult>,
69 pub all_succeeded: bool,
71}
72
73impl MultiWaypoint {
74 pub fn execution_order(databases: &[NamedDatabaseConfig]) -> Result<Vec<String>> {
79 let all_names: HashSet<&str> = databases.iter().map(|d| d.name.as_str()).collect();
80
81 let mut in_degree: HashMap<&str, usize> = HashMap::new();
82 let mut reverse_edges: HashMap<&str, Vec<&str>> = HashMap::new();
83
84 for db in databases {
85 in_degree.entry(db.name.as_str()).or_insert(0);
86 for dep in &db.depends_on {
87 if !all_names.contains(dep.as_str()) {
88 return Err(WaypointError::DatabaseNotFound {
89 name: dep.clone(),
90 available: all_names.iter().copied().collect::<Vec<_>>().join(", "),
91 });
92 }
93 *in_degree.entry(db.name.as_str()).or_insert(0) += 1;
94 reverse_edges
95 .entry(dep.as_str())
96 .or_default()
97 .push(db.name.as_str());
98 }
99 }
100
101 let mut queue: VecDeque<&str> = VecDeque::new();
102 for (&name, °) in &in_degree {
103 if deg == 0 {
104 queue.push_back(name);
105 }
106 }
107
108 let mut sorted = Vec::new();
109 while let Some(name) = queue.pop_front() {
110 sorted.push(name.to_string());
111 if let Some(dependents) = reverse_edges.get(name) {
112 for &dep in dependents {
113 let deg = in_degree
114 .get_mut(dep)
115 .expect("dependency not found in in_degree map");
116 *deg -= 1;
117 if *deg == 0 {
118 queue.push_back(dep);
119 }
120 }
121 }
122 }
123
124 if sorted.len() != databases.len() {
125 let in_cycle: Vec<&str> = in_degree
126 .iter()
127 .filter(|(_, deg)| **deg > 0)
128 .map(|(&name, _)| name)
129 .collect();
130 return Err(WaypointError::MultiDbDependencyCycle {
131 path: in_cycle.join(" -> "),
132 });
133 }
134
135 Ok(sorted)
136 }
137
138 pub async fn connect(
142 databases: &[NamedDatabaseConfig],
143 filter: Option<&str>,
144 ) -> Result<HashMap<String, DbClient>> {
145 let mut clients = HashMap::new();
146
147 for db in databases {
148 if let Some(name_filter) = filter {
149 if db.name != name_filter {
150 continue;
151 }
152 }
153
154 let config = db.to_waypoint_config();
155 let conn_string = config.connection_string()?;
156 let client = connect_one(&conn_string, &config).await?;
157 clients.insert(db.name.clone(), client);
158 }
159
160 if let Some(name_filter) = filter {
161 if !clients.contains_key(name_filter) {
162 let available = databases
163 .iter()
164 .map(|d| d.name.clone())
165 .collect::<Vec<_>>()
166 .join(", ");
167 return Err(WaypointError::DatabaseNotFound {
168 name: name_filter.to_string(),
169 available,
170 });
171 }
172 }
173
174 Ok(clients)
175 }
176
177 pub async fn migrate(
179 databases: &[NamedDatabaseConfig],
180 clients: &HashMap<String, DbClient>,
181 order: &[String],
182 target_version: Option<&str>,
183 fail_fast: bool,
184 ) -> Result<MultiResult> {
185 Self::migrate_with_options(databases, clients, order, target_version, fail_fast, false)
186 .await
187 }
188
189 pub async fn migrate_with_options(
192 databases: &[NamedDatabaseConfig],
193 clients: &HashMap<String, DbClient>,
194 order: &[String],
195 target_version: Option<&str>,
196 fail_fast: bool,
197 force: bool,
198 ) -> Result<MultiResult> {
199 let mut results = Vec::new();
200
201 for name in order {
202 let db = databases.iter().find(|d| &d.name == name);
203 let client = clients.get(name);
204
205 match (db, client) {
206 (Some(db), Some(client)) => {
207 let config = db.to_waypoint_config();
208 let outcome = dispatch_migrate(client, &config, target_version, force).await;
209 match outcome {
210 Ok(report) => {
211 results.push(DatabaseResult {
212 name: name.clone(),
213 success: true,
214 message: format!(
215 "Applied {} migration(s) ({}ms)",
216 report.migrations_applied, report.total_time_ms
217 ),
218 });
219 }
220 Err(e) => {
221 results.push(DatabaseResult {
222 name: name.clone(),
223 success: false,
224 message: format!("{}", e),
225 });
226 if fail_fast {
227 break;
228 }
229 }
230 }
231 }
232 _ => {
233 results.push(DatabaseResult {
234 name: name.clone(),
235 success: false,
236 message: "Database not connected".to_string(),
237 });
238 if fail_fast {
239 break;
240 }
241 }
242 }
243 }
244
245 let all_succeeded = results.iter().all(|r| r.success);
246 Ok(MultiResult {
247 results,
248 all_succeeded,
249 })
250 }
251
252 pub async fn info(
254 databases: &[NamedDatabaseConfig],
255 clients: &HashMap<String, DbClient>,
256 order: &[String],
257 ) -> Result<HashMap<String, Vec<crate::commands::info::MigrationInfo>>> {
258 let mut all_info = HashMap::new();
259
260 for name in order {
261 let db = databases.iter().find(|d| &d.name == name);
262 let client = clients.get(name);
263
264 if let (Some(db), Some(client)) = (db, client) {
265 let config = db.to_waypoint_config();
266 let info = crate::commands::info::execute_db(client, &config).await?;
267 all_info.insert(name.clone(), info);
268 }
269 }
270
271 Ok(all_info)
272 }
273}
274
275async fn connect_one(
277 conn_string: &str,
278 #[cfg_attr(not(feature = "postgres"), allow(unused_variables))] config: &WaypointConfig,
279) -> Result<DbClient> {
280 let kind = DialectKind::from_url(conn_string).unwrap_or(DialectKind::Postgres);
281 match kind {
282 #[cfg(feature = "postgres")]
283 DialectKind::Postgres => {
284 let client = crate::db::connect_with_full_config(
285 conn_string,
286 &config.database.ssl_mode,
287 config.database.connect_retries,
288 config.database.connect_timeout_secs,
289 config.database.statement_timeout_secs,
290 config.database.keepalive_secs,
291 )
292 .await?;
293 Ok(DbClient::with_postgres(client))
294 }
295 #[cfg(not(feature = "postgres"))]
296 DialectKind::Postgres => Err(WaypointError::ConfigError(
297 "PostgreSQL support is not compiled in (enable the `postgres` feature)".into(),
298 )),
299 #[cfg(feature = "mysql")]
300 DialectKind::Mysql => {
301 let pool = mysql_async::Pool::from_url(conn_string)
302 .map_err(|e| WaypointError::ConfigError(format!("Invalid MySQL URL: {}", e)))?;
303 Ok(DbClient::with_mysql(pool))
304 }
305 #[cfg(not(feature = "mysql"))]
306 DialectKind::Mysql => Err(WaypointError::ConfigError(
307 "MySQL support is not compiled in (enable the `mysql` feature)".into(),
308 )),
309 }
310}
311
312async fn dispatch_migrate(
314 client: &DbClient,
315 config: &WaypointConfig,
316 target_version: Option<&str>,
317 force: bool,
318) -> Result<crate::commands::migrate::MigrateReport> {
319 match client.dialect_kind() {
320 #[cfg(feature = "postgres")]
321 DialectKind::Postgres => {
322 crate::commands::migrate::execute_with_options(
323 client.as_postgres()?,
324 config,
325 target_version,
326 force,
327 )
328 .await
329 }
330 #[cfg(not(feature = "postgres"))]
331 DialectKind::Postgres => Err(WaypointError::ConfigError(
332 "PostgreSQL support is not compiled in (enable the `postgres` feature)".into(),
333 )),
334 #[cfg(feature = "mysql")]
335 DialectKind::Mysql => {
336 crate::commands::migrate::execute_mysql_with_options(
337 client,
338 config,
339 target_version,
340 force,
341 )
342 .await
343 }
344 #[cfg(not(feature = "mysql"))]
345 DialectKind::Mysql => Err(WaypointError::ConfigError(
346 "MySQL support is not compiled in (enable the `mysql` feature)".into(),
347 )),
348 }
349}