//! DNS record check storage, including pruning of records no longer configured. use super::{DnsCheckResult, Result, SqlitePool}; use tracing::instrument; #[derive(Debug, Clone, sqlx::FromRow, serde::Serialize)] pub struct DnsCheckRow { pub id: i64, pub target: String, pub name: String, pub record_type: String, pub expected: String, pub actual: String, pub matches: bool, pub checked_at: String, pub error: Option, } #[instrument(skip_all)] pub async fn insert_dns_check(pool: &SqlitePool, result: &DnsCheckResult) -> Result { let expected = serde_json::to_string(&result.expected).unwrap_or_default(); let actual = serde_json::to_string(&result.actual).unwrap_or_default(); let record_type_str = result.record_type.to_string(); let row = sqlx::query( "INSERT INTO dns_checks (target, name, record_type, expected, actual, matches, checked_at, error) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", ) .bind(&result.target) .bind(&result.name) .bind(&record_type_str) .bind(&expected) .bind(&actual) .bind(result.matches) .bind(&result.checked_at) .bind(&result.error) .execute(pool) .await?; Ok(row.last_insert_rowid()) } /// Get the latest DNS check per (name, record_type) for a target. #[instrument(skip_all)] pub async fn get_latest_dns_checks(pool: &SqlitePool, target: &str) -> Result> { Ok(sqlx::query_as::<_, DnsCheckRow>( "SELECT d.id, d.target, d.name, d.record_type, d.expected, d.actual, d.matches, d.checked_at, d.error FROM dns_checks d INNER JOIN (SELECT name, record_type, MAX(id) as max_id FROM dns_checks WHERE target = ? GROUP BY name, record_type) latest ON d.id = latest.max_id ORDER BY d.name, d.record_type", ) .bind(target) .fetch_all(pool) .await?) } /// Delete dns_checks for (name, record_type) pairs no longer in the config. #[instrument(skip_all)] pub async fn prune_stale_dns( pool: &SqlitePool, target: &str, expected_dns: &[(String, String)], ) -> Result { if expected_dns.is_empty() { // No configured DNS records, delete all DNS checks for this target let result = sqlx::query("DELETE FROM dns_checks WHERE target = ?") .bind(target) .execute(pool) .await?; return Ok(result.rows_affected()); } // Build a compound condition: keep rows matching any configured (name, record_type) pair let conditions: Vec = expected_dns .iter() .map(|_| "(name = ? AND record_type = ?)".to_string()) .collect(); let sql = format!( "DELETE FROM dns_checks WHERE target = ? AND NOT ({})", conditions.join(" OR ") ); let mut query = sqlx::query(sqlx::AssertSqlSafe(sql)).bind(target); for (name, record_type) in expected_dns { query = query.bind(name).bind(record_type); } let result = query.execute(pool).await?; Ok(result.rows_affected()) }