waypoint_core/commands/
simulate.rs1use serde::Serialize;
5use tokio_postgres::Client;
6
7use crate::config::WaypointConfig;
8use crate::db::quote_ident;
9use crate::error::{Result, WaypointError};
10use crate::history;
11use crate::migration::scan_migrations;
12use crate::placeholder::{build_placeholders, replace_placeholders};
13use crate::schema;
14
15#[derive(Debug, Clone, Serialize)]
17pub struct SimulationReport {
18 pub passed: bool,
20 pub migrations_simulated: usize,
22 pub temp_schema: String,
24 pub errors: Vec<SimulationError>,
26}
27
28#[derive(Debug, Clone, Serialize)]
30pub struct SimulationError {
31 pub script: String,
33 pub error: String,
35}
36
37pub async fn execute(client: &Client, config: &WaypointConfig) -> Result<SimulationReport> {
39 let schema_name = &config.migrations.schema;
40 let table = &config.migrations.table;
41
42 history::create_history_table(client, schema_name, table).await?;
44
45 let temp_schema = format!(
47 "waypoint_sim_{}",
48 std::time::SystemTime::now()
49 .duration_since(std::time::UNIX_EPOCH)
50 .unwrap_or_default()
51 .as_millis()
52 );
53
54 let result = run_simulation(client, config, &temp_schema).await;
55
56 let drop_sql = format!(
58 "DROP SCHEMA IF EXISTS {} CASCADE",
59 quote_ident(&temp_schema)
60 );
61 if let Err(e) = client.batch_execute(&drop_sql).await {
62 log::warn!(
63 "First attempt to drop simulation schema {} failed, retrying: {}",
64 temp_schema,
65 e
66 );
67 if let Err(e2) = client.batch_execute(&drop_sql).await {
68 log::error!(
69 "Failed to drop simulation schema {} after retry: {}",
70 temp_schema,
71 e2
72 );
73 }
74 }
75
76 result
77}
78
79async fn run_simulation(
80 client: &Client,
81 config: &WaypointConfig,
82 temp_schema: &str,
83) -> Result<SimulationReport> {
84 let schema_name = &config.migrations.schema;
85 let table = &config.migrations.table;
86
87 let create_sql = format!("CREATE SCHEMA {}", quote_ident(temp_schema));
89 client
90 .batch_execute(&create_sql)
91 .await
92 .map_err(|e| WaypointError::SimulationFailed {
93 reason: format!("Failed to create simulation schema: {}", e),
94 })?;
95
96 let snapshot = schema::introspect(client, schema_name).await?;
98 let ddl = schema::to_ddl(&snapshot);
99
100 if !ddl.is_empty() {
101 let set_path = format!("SET search_path TO {}", quote_ident(temp_schema));
103 client
104 .batch_execute(&set_path)
105 .await
106 .map_err(|e| WaypointError::SimulationFailed {
107 reason: format!("Failed to set search_path: {}", e),
108 })?;
109
110 if let Err(e) = client.batch_execute(&ddl).await {
112 log::debug!("Partial schema replication in simulation: {}", e);
113 }
114 }
115
116 let set_path = format!("SET search_path TO {}", quote_ident(temp_schema));
118 client
119 .batch_execute(&set_path)
120 .await
121 .map_err(|e| WaypointError::SimulationFailed {
122 reason: format!("Failed to set search_path: {}", e),
123 })?;
124
125 let resolved = scan_migrations(&config.migrations.locations)?;
127 let applied = history::get_applied_migrations(client, schema_name, table).await?;
128 let effective = history::effective_applied_versions(&applied);
129
130 let db_user = crate::db::get_current_user(client)
131 .await
132 .unwrap_or_else(|_| "unknown".to_string());
133 let db_name = crate::db::get_current_database(client)
134 .await
135 .unwrap_or_else(|_| "unknown".to_string());
136
137 let mut errors = Vec::new();
138 let mut simulated = 0;
139
140 for migration in &resolved {
141 if migration.is_undo() {
142 continue;
143 }
144 if let Some(version) = migration.version() {
145 if effective.contains(&version.raw) {
146 continue; }
148 }
149
150 let placeholders = build_placeholders(
151 &config.placeholders,
152 temp_schema,
153 &db_user,
154 &db_name,
155 &migration.script,
156 );
157 let sql = match replace_placeholders(&migration.sql, &placeholders) {
158 Ok(s) => s,
159 Err(e) => {
160 errors.push(SimulationError {
161 script: migration.script.clone(),
162 error: e.to_string(),
163 });
164 continue;
165 }
166 };
167
168 match client.batch_execute(&sql).await {
169 Ok(_) => {
170 simulated += 1;
171 }
172 Err(e) => {
173 errors.push(SimulationError {
174 script: migration.script.clone(),
175 error: crate::error::format_db_error(&e),
176 });
177 }
178 }
179 }
180
181 let restore_path = format!("SET search_path TO {}", quote_ident(schema_name));
183 if let Err(e) = client.batch_execute(&restore_path).await {
184 log::warn!("Failed to restore search_path: {}", e);
185 }
186
187 Ok(SimulationReport {
188 passed: errors.is_empty(),
189 migrations_simulated: simulated,
190 temp_schema: temp_schema.to_string(),
191 errors,
192 })
193}