pub mod section_tracking;
use crate::config::types::TrackingTable;
use anyhow::{Context, Result};
use sqlx::PgPool;
pub use section_tracking::{ensure_section_tracking_table, initialize_sections};
pub fn version_to_db(version: u64) -> Result<i64> {
i64::try_from(version).with_context(|| {
format!(
"Migration version {} is too large for database storage (exceeds i64::MAX). \
This typically indicates a timestamp far in the future or corrupted version data.",
version
)
})
}
pub fn version_from_db(version: i64) -> u64 {
if version < 0 {
tracing::warn!(
"Found negative migration version in database: {}. This indicates corrupted data.",
version
);
0
} else {
version as u64 }
}
pub fn format_tracking_table_name(tracking_table: &TrackingTable) -> Result<String> {
fn is_valid_sql_identifier(name: &str) -> bool {
if name.is_empty() {
return false;
}
let first_char = name.chars().next().unwrap();
if !first_char.is_alphabetic() && first_char != '_' {
return false;
}
name.chars()
.all(|c| c.is_alphanumeric() || c == '_' || c == '$')
}
if !is_valid_sql_identifier(&tracking_table.schema) {
return Err(anyhow::anyhow!(
"Invalid schema name '{}': must contain only letters, numbers, underscores, and dollar signs, starting with letter or underscore",
tracking_table.schema
));
}
if !is_valid_sql_identifier(&tracking_table.name) {
return Err(anyhow::anyhow!(
"Invalid table name '{}': must contain only letters, numbers, underscores, and dollar signs, starting with letter or underscore",
tracking_table.name
));
}
Ok(format!(
r#""{}"."{}""#,
tracking_table.schema, tracking_table.name
))
}
pub async fn ensure_tracking_table_exists(
pool: &PgPool,
tracking_table: &TrackingTable,
) -> Result<()> {
let tracking_table_name = format_tracking_table_name(tracking_table)?;
sqlx::query(&format!(
r#"
CREATE TABLE IF NOT EXISTS {} (
version BIGINT PRIMARY KEY,
description TEXT NOT NULL,
applied_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
checksum TEXT NOT NULL,
applied_by TEXT DEFAULT CURRENT_USER
)
"#,
tracking_table_name
))
.execute(pool)
.await
.with_context(|| format!("Failed to create tracking table {}", tracking_table_name))?;
Ok(())
}
pub async fn record_baseline_as_applied(
pool: &PgPool,
tracking_table: &TrackingTable,
version: u64,
description: &str,
checksum: &str,
) -> Result<()> {
let tracking_table_name = format_tracking_table_name(tracking_table)?;
ensure_tracking_table_exists(pool, tracking_table).await?;
sqlx::query(&format!(
"INSERT INTO {} (version, description, checksum) VALUES ($1, $2, $3)",
tracking_table_name
))
.bind(version_to_db(version)?)
.bind(description)
.bind(checksum)
.execute(pool)
.await
.with_context(|| format!("Failed to record baseline {} in tracking table", version))?;
Ok(())
}
pub fn calculate_checksum(content: &str) -> String {
format!("{:x}", md5::compute(content))
}