1use serde::Serialize;
12
13use crate::db::DbClient;
14use crate::dialect::DialectKind;
15use crate::error::Result;
16
17#[cfg(feature = "mysql")]
20pub use crate::engines::mysql::advisor::analyze as analyze_mysql;
21#[cfg(feature = "postgres")]
22pub use crate::engines::postgres::advisor::analyze;
23
24#[derive(Debug, Clone, Default)]
28pub struct AdvisorConfig {
29 pub run_after_migrate: bool,
31 pub disabled_rules: Vec<String>,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
37#[serde(rename_all = "lowercase")]
38pub enum AdvisorySeverity {
39 Info,
40 Suggestion,
41 Warning,
42}
43
44impl std::fmt::Display for AdvisorySeverity {
45 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 match self {
47 Self::Info => write!(f, "info"),
48 Self::Suggestion => write!(f, "suggestion"),
49 Self::Warning => write!(f, "warning"),
50 }
51 }
52}
53
54#[derive(Debug, Clone, Serialize)]
56pub struct Advisory {
57 pub rule_id: String,
59 pub category: String,
61 pub severity: AdvisorySeverity,
63 pub object: String,
65 pub explanation: String,
67 pub fix_sql: Option<String>,
69}
70
71#[derive(Debug, Clone, Serialize)]
73pub struct AdvisorReport {
74 pub schema: String,
76 pub advisories: Vec<Advisory>,
78 pub warning_count: usize,
80 pub suggestion_count: usize,
82 pub info_count: usize,
84}
85
86pub async fn analyze_db(
88 client: &DbClient,
89 schema: &str,
90 config: &AdvisorConfig,
91) -> Result<AdvisorReport> {
92 match client.dialect_kind() {
93 #[cfg(feature = "postgres")]
94 DialectKind::Postgres => analyze(client.as_postgres()?, schema, config).await,
95 #[cfg(not(feature = "postgres"))]
96 DialectKind::Postgres => Err(crate::error::WaypointError::ConfigError(
97 "PostgreSQL support is not compiled in".into(),
98 )),
99 #[cfg(feature = "mysql")]
100 DialectKind::Mysql => analyze_mysql(client, schema, config).await,
101 #[cfg(not(feature = "mysql"))]
102 DialectKind::Mysql => Err(crate::error::WaypointError::ConfigError(
103 "MySQL support is not compiled in".into(),
104 )),
105 }
106}
107
108pub fn generate_fix_sql(report: &AdvisorReport) -> String {
110 let fixes: Vec<String> = report
111 .advisories
112 .iter()
113 .filter_map(|a| {
114 a.fix_sql.as_ref().map(|sql| {
115 format!(
116 "-- {} [{}]: {}\n{}",
117 a.rule_id, a.severity, a.explanation, sql
118 )
119 })
120 })
121 .collect();
122 fixes.join("\n\n")
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 #[test]
130 fn test_advisor_config_default() {
131 let config = AdvisorConfig::default();
132 assert!(!config.run_after_migrate);
133 assert!(config.disabled_rules.is_empty());
134 }
135
136 #[test]
137 fn test_generate_fix_sql_empty() {
138 let report = AdvisorReport {
139 schema: "public".to_string(),
140 advisories: vec![],
141 warning_count: 0,
142 suggestion_count: 0,
143 info_count: 0,
144 };
145 assert!(generate_fix_sql(&report).is_empty());
146 }
147
148 #[test]
149 fn test_generate_fix_sql_with_advisories() {
150 let report = AdvisorReport {
151 schema: "public".to_string(),
152 advisories: vec![
153 Advisory {
154 rule_id: "A001".to_string(),
155 category: "Performance".to_string(),
156 severity: AdvisorySeverity::Warning,
157 object: "orders.user_id".to_string(),
158 explanation: "FK without index".to_string(),
159 fix_sql: Some(
160 "CREATE INDEX idx_orders_user_id ON \"orders\" (\"user_id\");".to_string(),
161 ),
162 },
163 Advisory {
164 rule_id: "A004".to_string(),
165 category: "Correctness".to_string(),
166 severity: AdvisorySeverity::Warning,
167 object: "logs".to_string(),
168 explanation: "No primary key".to_string(),
169 fix_sql: None,
170 },
171 ],
172 warning_count: 2,
173 suggestion_count: 0,
174 info_count: 0,
175 };
176 let sql = generate_fix_sql(&report);
177 assert!(sql.contains("CREATE INDEX"));
178 assert!(sql.contains("A001"));
179 assert!(!sql.contains("A004"));
180 }
181
182 #[test]
183 fn test_advisory_severity_display() {
184 assert_eq!(AdvisorySeverity::Info.to_string(), "info");
185 assert_eq!(AdvisorySeverity::Suggestion.to_string(), "suggestion");
186 assert_eq!(AdvisorySeverity::Warning.to_string(), "warning");
187 }
188}