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 {
43 self.to_waypoint_config_inheriting(&WaypointConfig::default())
44 }
45
46 pub fn to_waypoint_config_inheriting(&self, parent: &WaypointConfig) -> WaypointConfig {
54 WaypointConfig {
55 database: self.database.clone(),
56 migrations: self.migrations.clone(),
57 hooks: self.hooks.clone(),
58 placeholders: self.placeholders.clone(),
59 lint: parent.lint.clone(),
61 snapshots: parent.snapshots.clone(),
62 preflight: parent.preflight.clone(),
63 guards: parent.guards.clone(),
64 reversals: parent.reversals.clone(),
65 safety: parent.safety.clone(),
66 advisor: parent.advisor.clone(),
67 simulation: parent.simulation.clone(),
68 multi_database: None,
70 }
71 }
72}
73
74pub struct MultiWaypoint {
76 pub databases: Vec<NamedDatabaseConfig>,
78}
79
80#[derive(Debug, Serialize)]
82pub struct DatabaseResult {
83 pub name: String,
85 pub success: bool,
87 pub message: String,
89}
90
91#[derive(Debug, Serialize)]
93pub struct MultiResult {
94 pub results: Vec<DatabaseResult>,
96 pub all_succeeded: bool,
98}
99
100impl MultiWaypoint {
101 pub fn execution_order(databases: &[NamedDatabaseConfig]) -> Result<Vec<String>> {
106 let all_names: HashSet<&str> = databases.iter().map(|d| d.name.as_str()).collect();
107
108 let mut in_degree: HashMap<&str, usize> = HashMap::new();
109 let mut reverse_edges: HashMap<&str, Vec<&str>> = HashMap::new();
110
111 for db in databases {
112 in_degree.entry(db.name.as_str()).or_insert(0);
113 for dep in &db.depends_on {
114 if !all_names.contains(dep.as_str()) {
115 return Err(WaypointError::DatabaseNotFound {
116 name: dep.clone(),
117 available: all_names.iter().copied().collect::<Vec<_>>().join(", "),
118 });
119 }
120 *in_degree.entry(db.name.as_str()).or_insert(0) += 1;
121 reverse_edges
122 .entry(dep.as_str())
123 .or_default()
124 .push(db.name.as_str());
125 }
126 }
127
128 let mut queue: VecDeque<&str> = VecDeque::new();
129 for (&name, °) in &in_degree {
130 if deg == 0 {
131 queue.push_back(name);
132 }
133 }
134
135 let mut sorted = Vec::new();
136 while let Some(name) = queue.pop_front() {
137 sorted.push(name.to_string());
138 if let Some(dependents) = reverse_edges.get(name) {
139 for &dep in dependents {
140 let deg = in_degree
141 .get_mut(dep)
142 .expect("dependency not found in in_degree map");
143 *deg -= 1;
144 if *deg == 0 {
145 queue.push_back(dep);
146 }
147 }
148 }
149 }
150
151 if sorted.len() != databases.len() {
152 let in_cycle: Vec<&str> = in_degree
153 .iter()
154 .filter(|(_, deg)| **deg > 0)
155 .map(|(&name, _)| name)
156 .collect();
157 return Err(WaypointError::MultiDbDependencyCycle {
158 path: in_cycle.join(" -> "),
159 });
160 }
161
162 Ok(sorted)
163 }
164
165 pub async fn connect(
173 databases: &[NamedDatabaseConfig],
174 filter: Option<&str>,
175 ) -> Result<HashMap<String, DbClient>> {
176 Self::connect_inheriting(databases, filter, &WaypointConfig::default()).await
177 }
178
179 pub async fn connect_inheriting(
181 databases: &[NamedDatabaseConfig],
182 filter: Option<&str>,
183 parent: &WaypointConfig,
184 ) -> Result<HashMap<String, DbClient>> {
185 let mut clients = HashMap::new();
186
187 for db in databases {
188 if let Some(name_filter) = filter
189 && db.name != name_filter
190 {
191 continue;
192 }
193
194 let config = db.to_waypoint_config_inheriting(parent);
195 let conn_string = config.connection_string()?;
196 let client = crate::db::connect_for_url(&conn_string, &config).await?;
197 clients.insert(db.name.clone(), client);
198 }
199
200 if let Some(name_filter) = filter
201 && !clients.contains_key(name_filter)
202 {
203 let available = databases
204 .iter()
205 .map(|d| d.name.clone())
206 .collect::<Vec<_>>()
207 .join(", ");
208 return Err(WaypointError::DatabaseNotFound {
209 name: name_filter.to_string(),
210 available,
211 });
212 }
213
214 Ok(clients)
215 }
216
217 pub async fn migrate(
219 databases: &[NamedDatabaseConfig],
220 clients: &HashMap<String, DbClient>,
221 order: &[String],
222 target_version: Option<&str>,
223 fail_fast: bool,
224 ) -> Result<MultiResult> {
225 Self::migrate_with_options(databases, clients, order, target_version, fail_fast, false)
226 .await
227 }
228
229 pub async fn migrate_with_options(
236 databases: &[NamedDatabaseConfig],
237 clients: &HashMap<String, DbClient>,
238 order: &[String],
239 target_version: Option<&str>,
240 fail_fast: bool,
241 force: bool,
242 ) -> Result<MultiResult> {
243 Self::migrate_inheriting(
244 databases,
245 clients,
246 order,
247 target_version,
248 fail_fast,
249 force,
250 &WaypointConfig::default(),
251 )
252 .await
253 }
254
255 #[allow(clippy::too_many_arguments)]
258 pub async fn migrate_inheriting(
259 databases: &[NamedDatabaseConfig],
260 clients: &HashMap<String, DbClient>,
261 order: &[String],
262 target_version: Option<&str>,
263 fail_fast: bool,
264 force: bool,
265 parent: &WaypointConfig,
266 ) -> Result<MultiResult> {
267 let mut results = Vec::new();
268
269 for name in order {
270 let db = databases.iter().find(|d| &d.name == name);
271 let client = clients.get(name);
272
273 match (db, client) {
274 (Some(db), Some(client)) => {
275 let config = db.to_waypoint_config_inheriting(parent);
276 let outcome = dispatch_migrate(client, &config, target_version, force).await;
277 match outcome {
278 Ok(report) => {
279 results.push(DatabaseResult {
280 name: name.clone(),
281 success: true,
282 message: format!(
283 "Applied {} migration(s) ({}ms)",
284 report.migrations_applied, report.total_time_ms
285 ),
286 });
287 }
288 Err(e) => {
289 results.push(DatabaseResult {
290 name: name.clone(),
291 success: false,
292 message: format!("{}", e),
293 });
294 if fail_fast {
295 break;
296 }
297 }
298 }
299 }
300 _ => {
301 results.push(DatabaseResult {
302 name: name.clone(),
303 success: false,
304 message: "Database not connected".to_string(),
305 });
306 if fail_fast {
307 break;
308 }
309 }
310 }
311 }
312
313 let all_succeeded = results.iter().all(|r| r.success);
314 Ok(MultiResult {
315 results,
316 all_succeeded,
317 })
318 }
319
320 pub async fn info(
322 databases: &[NamedDatabaseConfig],
323 clients: &HashMap<String, DbClient>,
324 order: &[String],
325 ) -> Result<HashMap<String, Vec<crate::commands::info::MigrationInfo>>> {
326 Self::info_inheriting(databases, clients, order, &WaypointConfig::default()).await
327 }
328
329 pub async fn info_inheriting(
331 databases: &[NamedDatabaseConfig],
332 clients: &HashMap<String, DbClient>,
333 order: &[String],
334 parent: &WaypointConfig,
335 ) -> Result<HashMap<String, Vec<crate::commands::info::MigrationInfo>>> {
336 let mut all_info = HashMap::new();
337
338 for name in order {
339 let db = databases.iter().find(|d| &d.name == name);
340 let client = clients.get(name);
341
342 if let (Some(db), Some(client)) = (db, client) {
343 let config = db.to_waypoint_config_inheriting(parent);
344 let info = crate::commands::info::execute_db(client, &config).await?;
345 all_info.insert(name.clone(), info);
346 }
347 }
348
349 Ok(all_info)
350 }
351}
352
353async fn dispatch_migrate(
355 client: &DbClient,
356 config: &WaypointConfig,
357 target_version: Option<&str>,
358 force: bool,
359) -> Result<crate::commands::migrate::MigrateReport> {
360 match client.dialect_kind() {
361 #[cfg(feature = "postgres")]
362 DialectKind::Postgres => {
363 crate::commands::migrate::execute_with_options(
364 client.as_postgres()?,
365 config,
366 target_version,
367 force,
368 )
369 .await
370 }
371 #[cfg(not(feature = "postgres"))]
372 DialectKind::Postgres => Err(WaypointError::ConfigError(
373 "PostgreSQL support is not compiled in (enable the `postgres` feature)".into(),
374 )),
375 #[cfg(feature = "mysql")]
376 DialectKind::Mysql => {
377 crate::commands::migrate::execute_mysql_with_options(
378 client,
379 config,
380 target_version,
381 force,
382 )
383 .await
384 }
385 #[cfg(not(feature = "mysql"))]
386 DialectKind::Mysql => Err(WaypointError::ConfigError(
387 "MySQL support is not compiled in (enable the `mysql` feature)".into(),
388 )),
389 }
390}