use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
pub struct ConfigInput {
#[serde(skip_serializing_if = "Option::is_none")]
pub databases: Option<DatabasesInput>,
#[serde(skip_serializing_if = "Option::is_none")]
pub directories: Option<DirectoriesInput>,
#[serde(skip_serializing_if = "Option::is_none")]
pub objects: Option<ObjectsInput>,
#[serde(skip_serializing_if = "Option::is_none")]
pub migration: Option<MigrationInput>,
#[serde(skip_serializing_if = "Option::is_none")]
pub schema: Option<SchemaInput>,
#[serde(skip_serializing_if = "Option::is_none")]
pub docker: Option<DockerInput>,
}
#[derive(Debug, Clone, Default)]
pub struct Config {
pub directories: Directories,
pub objects: Objects,
pub migration: Migration,
pub schema: Schema,
pub docker: Docker,
}
#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
pub struct DatabasesInput {
#[serde(skip_serializing_if = "Option::is_none")]
pub dev_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub shadow_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub target_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub shadow: Option<ShadowDatabaseInput>,
}
#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
pub struct ShadowDatabaseInput {
#[serde(skip_serializing_if = "Option::is_none")]
pub auto: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reset: Option<ShadowResetMode>,
#[serde(skip_serializing_if = "Option::is_none")]
pub docker: Option<ShadowDockerInput>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ShadowResetMode {
#[default]
Clean,
Branch,
}
#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
pub struct ShadowDockerInput {
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub image: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub platform: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub environment: Option<HashMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub container_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub auto_cleanup: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub volumes: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub network: Option<String>,
}
#[derive(Debug, Clone)]
pub enum ShadowDatabase {
Auto, Url { url: String, reset: ShadowResetMode },
Docker(ShadowDockerConfig),
}
impl ShadowDatabase {
pub async fn get_connection_string(&self) -> anyhow::Result<String> {
match self {
ShadowDatabase::Auto => {
let default_config = ShadowDockerConfig::default();
Self::generate_docker_shadow_url(&default_config).await
}
ShadowDatabase::Url { url, reset } => match reset {
ShadowResetMode::Branch => crate::db::branch::branch_url(url).await,
ShadowResetMode::Clean => Ok(url.clone()),
},
ShadowDatabase::Docker(config) => Self::generate_docker_shadow_url(config).await,
}
}
pub async fn connect_fresh(&self) -> anyhow::Result<sqlx::PgPool> {
let url = self.get_connection_string().await?;
crate::db::connection::connect_with_retry(&url).await
}
async fn generate_docker_shadow_url(config: &ShadowDockerConfig) -> anyhow::Result<String> {
use crate::docker::DockerManager;
let docker_manager = DockerManager::new().await?;
let shadow_db = docker_manager.start_shadow_database(config).await?;
Ok(shadow_db.into_connection_string())
}
}
#[derive(Debug, Clone)]
pub struct ShadowDockerConfig {
pub version: Option<String>,
pub image: String,
pub platform: Option<String>,
pub environment: HashMap<String, String>,
pub container_name: Option<String>,
pub auto_cleanup: bool,
#[allow(dead_code)] pub volumes: Option<Vec<String>>,
#[allow(dead_code)] pub network: Option<String>,
}
impl ShadowDockerConfig {
pub fn resolved_image(&self) -> String {
if !self.image.is_empty() && self.image != Self::default_image() {
return self.image.clone();
}
if let Some(version) = &self.version {
return format!("postgres:{}-alpine", version);
}
self.image.clone()
}
fn default_image() -> String {
"postgres:18-alpine".to_string()
}
}
#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
pub struct DirectoriesInput {
#[serde(skip_serializing_if = "Option::is_none")]
pub schema_dir: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub migrations_dir: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub baselines_dir: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub roles_file: Option<String>,
}
#[derive(Debug, Clone)]
pub struct Directories {
pub schema: String,
pub migrations: String,
pub baselines: String,
pub roles: String,
}
#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
pub struct ObjectsInput {
#[serde(skip_serializing_if = "Option::is_none")]
pub include: Option<ObjectIncludeInput>,
#[serde(skip_serializing_if = "Option::is_none")]
pub exclude: Option<ObjectExcludeInput>,
}
#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
pub struct ObjectIncludeInput {
#[serde(skip_serializing_if = "Option::is_none")]
pub schemas: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tables: Option<Vec<String>>,
}
#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
pub struct ObjectExcludeInput {
#[serde(alias = "exclude_schemas", skip_serializing_if = "Option::is_none")]
pub schemas: Option<Vec<String>>,
#[serde(alias = "exclude_tables", skip_serializing_if = "Option::is_none")]
pub tables: Option<Vec<String>>,
}
#[derive(Debug, Clone, Default)]
pub struct Objects {
pub include: ObjectInclude,
pub exclude: ObjectExclude,
}
#[derive(Debug, Clone, Default)]
pub struct ObjectInclude {
pub schemas: Vec<String>,
pub tables: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct ObjectExclude {
pub schemas: Vec<String>,
pub tables: Vec<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
pub struct MigrationInput {
#[serde(skip_serializing_if = "Option::is_none")]
pub default_mode: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub validate_baseline_consistency: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub create_baselines_by_default: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tracking_table: Option<TrackingTableInput>,
#[serde(skip_serializing_if = "Option::is_none")]
pub column_order: Option<ColumnOrderMode>,
#[serde(skip_serializing_if = "Option::is_none")]
pub filename_prefix: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
pub struct TrackingTableInput {
#[serde(skip_serializing_if = "Option::is_none")]
pub schema: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
#[derive(Debug, Clone)]
pub struct Migration {
pub default_mode: String,
pub validate_baseline_consistency: bool,
pub create_baselines_by_default: bool,
pub tracking_table: TrackingTable,
pub column_order: ColumnOrderMode,
pub filename_prefix: String,
}
#[derive(Debug, Clone)]
pub struct TrackingTable {
pub schema: String,
pub name: String,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ColumnOrderMode {
#[default]
Strict,
Warn,
Relaxed,
}
#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
pub struct DockerInput {
#[serde(skip_serializing_if = "Option::is_none")]
pub auto_cleanup: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub check_system_identifier: Option<bool>,
}
#[derive(Debug, Clone)]
pub struct Docker {
pub auto_cleanup: bool,
pub check_system_identifier: bool,
}
#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
pub struct SchemaInput {
#[serde(skip_serializing_if = "Option::is_none")]
pub augment_dependencies_from_files: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub validate_file_dependencies: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub verbose_file_processing: Option<bool>,
}
#[derive(Debug, Clone)]
pub struct Schema {
pub augment_dependencies_from_files: bool,
pub validate_file_dependencies: bool,
pub verbose_file_processing: bool,
}