1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
6pub struct ConfigInput {
7 #[serde(skip_serializing_if = "Option::is_none")]
8 pub databases: Option<DatabasesInput>,
9 #[serde(skip_serializing_if = "Option::is_none")]
10 pub directories: Option<DirectoriesInput>,
11 #[serde(skip_serializing_if = "Option::is_none")]
12 pub objects: Option<ObjectsInput>,
13 #[serde(skip_serializing_if = "Option::is_none")]
14 pub migration: Option<MigrationInput>,
15 #[serde(skip_serializing_if = "Option::is_none")]
16 pub schema: Option<SchemaInput>,
17 #[serde(skip_serializing_if = "Option::is_none")]
18 pub docker: Option<DockerInput>,
19}
20
21#[derive(Debug, Clone, Default)]
28pub struct Config {
29 pub directories: Directories,
30 pub objects: Objects,
31 pub migration: Migration,
32 pub schema: Schema,
33 pub docker: Docker,
34}
35
36#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
38pub struct DatabasesInput {
39 #[serde(skip_serializing_if = "Option::is_none")]
40 pub dev_url: Option<String>,
41 #[serde(skip_serializing_if = "Option::is_none")]
42 pub shadow_url: Option<String>,
43 #[serde(skip_serializing_if = "Option::is_none")]
44 pub target_url: Option<String>,
45 #[serde(skip_serializing_if = "Option::is_none")]
46 pub shadow: Option<ShadowDatabaseInput>,
47}
48
49#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
50pub struct ShadowDatabaseInput {
51 #[serde(skip_serializing_if = "Option::is_none")]
52 pub auto: Option<bool>,
53 #[serde(skip_serializing_if = "Option::is_none")]
54 pub url: Option<String>,
55 #[serde(skip_serializing_if = "Option::is_none")]
58 pub reset: Option<ShadowResetMode>,
59 #[serde(skip_serializing_if = "Option::is_none")]
60 pub docker: Option<ShadowDockerInput>,
61}
62
63#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
65#[serde(rename_all = "snake_case")]
66pub enum ShadowResetMode {
67 #[default]
70 Clean,
71 Branch,
77}
78
79#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
80pub struct ShadowDockerInput {
81 #[serde(skip_serializing_if = "Option::is_none")]
82 pub version: Option<String>,
83 #[serde(skip_serializing_if = "Option::is_none")]
84 pub image: Option<String>,
85 #[serde(skip_serializing_if = "Option::is_none")]
89 pub platform: Option<String>,
90 #[serde(skip_serializing_if = "Option::is_none")]
91 pub environment: Option<HashMap<String, String>>,
92 #[serde(skip_serializing_if = "Option::is_none")]
93 pub container_name: Option<String>,
94 #[serde(skip_serializing_if = "Option::is_none")]
95 pub auto_cleanup: Option<bool>,
96 #[serde(skip_serializing_if = "Option::is_none")]
97 pub volumes: Option<Vec<String>>,
98 #[serde(skip_serializing_if = "Option::is_none")]
99 pub network: Option<String>,
100}
101
102#[derive(Debug, Clone)]
103pub enum ShadowDatabase {
104 Auto, Url { url: String, reset: ShadowResetMode },
106 Docker(ShadowDockerConfig),
107}
108
109impl ShadowDatabase {
110 pub async fn get_connection_string(&self) -> anyhow::Result<String> {
112 match self {
113 ShadowDatabase::Auto => {
114 let default_config = ShadowDockerConfig::default();
116 Self::generate_docker_shadow_url(&default_config).await
117 }
118 ShadowDatabase::Url { url, reset } => match reset {
119 ShadowResetMode::Branch => crate::db::branch::branch_url(url).await,
120 ShadowResetMode::Clean => Ok(url.clone()),
121 },
122 ShadowDatabase::Docker(config) => Self::generate_docker_shadow_url(config).await,
123 }
124 }
125
126 pub async fn connect_fresh(&self) -> anyhow::Result<sqlx::PgPool> {
136 let url = self.get_connection_string().await?;
137 crate::db::connection::connect_with_retry(&url).await
138 }
139
140 async fn generate_docker_shadow_url(config: &ShadowDockerConfig) -> anyhow::Result<String> {
142 use crate::docker::DockerManager;
143
144 let docker_manager = DockerManager::new().await?;
145 let shadow_db = docker_manager.start_shadow_database(config).await?;
146 Ok(shadow_db.into_connection_string())
148 }
149}
150
151#[derive(Debug, Clone)]
152pub struct ShadowDockerConfig {
153 pub version: Option<String>,
154 pub image: String,
155 pub platform: Option<String>,
157 pub environment: HashMap<String, String>,
158 pub container_name: Option<String>,
159 pub auto_cleanup: bool,
160 #[allow(dead_code)] pub volumes: Option<Vec<String>>,
162 #[allow(dead_code)] pub network: Option<String>,
164}
165
166impl ShadowDockerConfig {
167 pub fn resolved_image(&self) -> String {
170 if !self.image.is_empty() && self.image != Self::default_image() {
172 return self.image.clone();
173 }
174
175 if let Some(version) = &self.version {
177 return format!("postgres:{}-alpine", version);
178 }
179
180 self.image.clone()
182 }
183
184 fn default_image() -> String {
186 "postgres:18-alpine".to_string()
187 }
188}
189
190#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
192pub struct DirectoriesInput {
193 #[serde(skip_serializing_if = "Option::is_none")]
194 pub schema_dir: Option<String>,
195 #[serde(skip_serializing_if = "Option::is_none")]
196 pub migrations_dir: Option<String>,
197 #[serde(skip_serializing_if = "Option::is_none")]
198 pub baselines_dir: Option<String>,
199 #[serde(skip_serializing_if = "Option::is_none")]
200 pub roles_file: Option<String>,
201}
202
203#[derive(Debug, Clone)]
204pub struct Directories {
205 pub schema: String,
206 pub migrations: String,
207 pub baselines: String,
208 pub roles: String,
209}
210
211#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
216pub struct ObjectsInput {
217 #[serde(skip_serializing_if = "Option::is_none")]
218 pub include: Option<ObjectIncludeInput>,
219 #[serde(skip_serializing_if = "Option::is_none")]
220 pub exclude: Option<ObjectExcludeInput>,
221}
222
223#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
224pub struct ObjectIncludeInput {
225 #[serde(skip_serializing_if = "Option::is_none")]
226 pub schemas: Option<Vec<String>>,
227 #[serde(skip_serializing_if = "Option::is_none")]
228 pub tables: Option<Vec<String>>,
229}
230
231#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
232pub struct ObjectExcludeInput {
233 #[serde(alias = "exclude_schemas", skip_serializing_if = "Option::is_none")]
236 pub schemas: Option<Vec<String>>,
237 #[serde(alias = "exclude_tables", skip_serializing_if = "Option::is_none")]
238 pub tables: Option<Vec<String>>,
239}
240
241#[derive(Debug, Clone, Default)]
242pub struct Objects {
243 pub include: ObjectInclude,
244 pub exclude: ObjectExclude,
245}
246
247#[derive(Debug, Clone, Default)]
248pub struct ObjectInclude {
249 pub schemas: Vec<String>,
250 pub tables: Vec<String>,
251}
252
253#[derive(Debug, Clone)]
254pub struct ObjectExclude {
255 pub schemas: Vec<String>,
256 pub tables: Vec<String>,
257}
258
259#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
261pub struct MigrationInput {
262 #[serde(skip_serializing_if = "Option::is_none")]
263 pub default_mode: Option<String>,
264 #[serde(skip_serializing_if = "Option::is_none")]
265 pub validate_baseline_consistency: Option<bool>,
266 #[serde(skip_serializing_if = "Option::is_none")]
267 pub create_baselines_by_default: Option<bool>,
268 #[serde(skip_serializing_if = "Option::is_none")]
269 pub tracking_table: Option<TrackingTableInput>,
270 #[serde(skip_serializing_if = "Option::is_none")]
271 pub column_order: Option<ColumnOrderMode>,
272 #[serde(skip_serializing_if = "Option::is_none")]
273 pub filename_prefix: Option<String>,
274}
275
276#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
277pub struct TrackingTableInput {
278 #[serde(skip_serializing_if = "Option::is_none")]
279 pub schema: Option<String>,
280 #[serde(skip_serializing_if = "Option::is_none")]
281 pub name: Option<String>,
282}
283
284#[derive(Debug, Clone)]
285pub struct Migration {
286 pub default_mode: String,
287 pub validate_baseline_consistency: bool,
288 pub create_baselines_by_default: bool,
289 pub tracking_table: TrackingTable,
290 pub column_order: ColumnOrderMode,
291 pub filename_prefix: String,
292}
293
294#[derive(Debug, Clone)]
295pub struct TrackingTable {
296 pub schema: String,
297 pub name: String,
298}
299
300#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
302#[serde(rename_all = "snake_case")]
303pub enum ColumnOrderMode {
304 #[default]
306 Strict,
307 Warn,
309 Relaxed,
311}
312
313#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
315pub struct DockerInput {
316 #[serde(skip_serializing_if = "Option::is_none")]
317 pub auto_cleanup: Option<bool>,
318 #[serde(skip_serializing_if = "Option::is_none")]
319 pub check_system_identifier: Option<bool>,
320}
321
322#[derive(Debug, Clone)]
323pub struct Docker {
324 pub auto_cleanup: bool,
325 pub check_system_identifier: bool,
326}
327
328#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
330pub struct SchemaInput {
331 #[serde(skip_serializing_if = "Option::is_none")]
332 pub augment_dependencies_from_files: Option<bool>,
333 #[serde(skip_serializing_if = "Option::is_none")]
334 pub validate_file_dependencies: Option<bool>,
335 #[serde(skip_serializing_if = "Option::is_none")]
336 pub verbose_file_processing: Option<bool>,
337}
338
339#[derive(Debug, Clone)]
340pub struct Schema {
341 pub augment_dependencies_from_files: bool,
342 pub validate_file_dependencies: bool,
343 pub verbose_file_processing: bool,
344}