use serde::Serialize;
use crate::db::DbClient;
use crate::dialect::DialectKind;
use crate::error::Result;
#[cfg(feature = "mysql")]
pub use crate::engines::mysql::advisor::analyze as analyze_mysql;
#[cfg(feature = "postgres")]
pub use crate::engines::postgres::advisor::analyze;
#[derive(Debug, Clone, Default)]
pub struct AdvisorConfig {
pub run_after_migrate: bool,
pub disabled_rules: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum AdvisorySeverity {
Info,
Suggestion,
Warning,
}
impl std::fmt::Display for AdvisorySeverity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Info => write!(f, "info"),
Self::Suggestion => write!(f, "suggestion"),
Self::Warning => write!(f, "warning"),
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct Advisory {
pub rule_id: String,
pub category: String,
pub severity: AdvisorySeverity,
pub object: String,
pub explanation: String,
pub fix_sql: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct AdvisorReport {
pub schema: String,
pub advisories: Vec<Advisory>,
pub warning_count: usize,
pub suggestion_count: usize,
pub info_count: usize,
}
pub async fn analyze_db(
client: &DbClient,
schema: &str,
config: &AdvisorConfig,
) -> Result<AdvisorReport> {
match client.dialect_kind() {
#[cfg(feature = "postgres")]
DialectKind::Postgres => analyze(client.as_postgres()?, schema, config).await,
#[cfg(not(feature = "postgres"))]
DialectKind::Postgres => Err(crate::error::WaypointError::ConfigError(
"PostgreSQL support is not compiled in".into(),
)),
#[cfg(feature = "mysql")]
DialectKind::Mysql => analyze_mysql(client, schema, config).await,
#[cfg(not(feature = "mysql"))]
DialectKind::Mysql => Err(crate::error::WaypointError::ConfigError(
"MySQL support is not compiled in".into(),
)),
}
}
pub fn generate_fix_sql(report: &AdvisorReport) -> String {
let fixes: Vec<String> = report
.advisories
.iter()
.filter_map(|a| {
a.fix_sql.as_ref().map(|sql| {
format!(
"-- {} [{}]: {}\n{}",
a.rule_id, a.severity, a.explanation, sql
)
})
})
.collect();
fixes.join("\n\n")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_advisor_config_default() {
let config = AdvisorConfig::default();
assert!(!config.run_after_migrate);
assert!(config.disabled_rules.is_empty());
}
#[test]
fn test_generate_fix_sql_empty() {
let report = AdvisorReport {
schema: "public".to_string(),
advisories: vec![],
warning_count: 0,
suggestion_count: 0,
info_count: 0,
};
assert!(generate_fix_sql(&report).is_empty());
}
#[test]
fn test_generate_fix_sql_with_advisories() {
let report = AdvisorReport {
schema: "public".to_string(),
advisories: vec![
Advisory {
rule_id: "A001".to_string(),
category: "Performance".to_string(),
severity: AdvisorySeverity::Warning,
object: "orders.user_id".to_string(),
explanation: "FK without index".to_string(),
fix_sql: Some(
"CREATE INDEX idx_orders_user_id ON \"orders\" (\"user_id\");".to_string(),
),
},
Advisory {
rule_id: "A004".to_string(),
category: "Correctness".to_string(),
severity: AdvisorySeverity::Warning,
object: "logs".to_string(),
explanation: "No primary key".to_string(),
fix_sql: None,
},
],
warning_count: 2,
suggestion_count: 0,
info_count: 0,
};
let sql = generate_fix_sql(&report);
assert!(sql.contains("CREATE INDEX"));
assert!(sql.contains("A001"));
assert!(!sql.contains("A004"));
}
#[test]
fn test_advisory_severity_display() {
assert_eq!(AdvisorySeverity::Info.to_string(), "info");
assert_eq!(AdvisorySeverity::Suggestion.to_string(), "suggestion");
assert_eq!(AdvisorySeverity::Warning.to_string(), "warning");
}
}