1use std::sync::Arc;
7use tokio::sync::RwLock;
8
9use crate::error::Result;
10use crate::schema::registry::SchemaRegistry;
11
12#[derive(Debug, Clone)]
14pub struct CoverageInfo {
15 pub total_tables: usize,
17 pub tables_with_schema: usize,
19 pub tables_missing_schema: Vec<String>,
21 pub schemas_without_data: Vec<String>,
23}
24
25impl CoverageInfo {
26 pub fn coverage_percentage(&self) -> f64 {
28 if self.total_tables == 0 {
29 return 0.0;
30 }
31 (self.tables_with_schema as f64 / self.total_tables as f64) * 100.0
32 }
33
34 pub fn has_critical_issues(&self) -> bool {
36 if self.total_tables > 0 {
38 let missing_pct =
39 (self.tables_missing_schema.len() as f64 / self.total_tables as f64) * 100.0;
40 missing_pct > 50.0
41 } else {
42 false
43 }
44 }
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum CoverageBadge {
50 Green,
52 Yellow,
54 Red,
56 Unknown,
58}
59
60impl CoverageBadge {
61 pub fn description(&self) -> &'static str {
63 match self {
64 CoverageBadge::Green => "Excellent coverage (≥95%)",
65 CoverageBadge::Yellow => "Acceptable coverage (50-95%)",
66 CoverageBadge::Red => "Poor coverage (<50% or critical issues)",
67 CoverageBadge::Unknown => "Unknown coverage (no schema data)",
68 }
69 }
70
71 pub fn color(&self) -> &'static str {
73 match self {
74 CoverageBadge::Green => "green",
75 CoverageBadge::Yellow => "yellow",
76 CoverageBadge::Red => "red",
77 CoverageBadge::Unknown => "gray",
78 }
79 }
80}
81
82pub struct CoverageCalculator {
84 schema_registry: Arc<RwLock<SchemaRegistry>>,
85}
86
87impl CoverageCalculator {
88 pub fn new(schema_registry: Arc<RwLock<SchemaRegistry>>) -> Self {
90 Self { schema_registry }
91 }
92
93 pub async fn calculate(&self, discovered_tables: &[String]) -> Result<CoverageInfo> {
103 let registry = self.schema_registry.read().await;
104
105 let registered_schemas = registry.list_schemas(None).await?;
107
108 let registered_names: Vec<String> = registered_schemas
110 .iter()
111 .map(|s| format!("{}.{}", s.keyspace, s.table))
112 .collect();
113
114 let mut tables_with_schema = Vec::new();
116 let mut tables_missing_schema = Vec::new();
117
118 for table in discovered_tables {
119 let normalized = normalize_table_name(table);
121
122 if registered_names.iter().any(|s| {
123 let schema_normalized = normalize_table_name(s);
124 schema_normalized == normalized
125 }) {
126 tables_with_schema.push(table.clone());
127 } else {
128 tables_missing_schema.push(table.clone());
129 }
130 }
131
132 let mut schemas_without_data = Vec::new();
134 for schema_name in ®istered_names {
135 let schema_normalized = normalize_table_name(schema_name);
136 if !discovered_tables.iter().any(|t| {
137 let table_normalized = normalize_table_name(t);
138 table_normalized == schema_normalized
139 }) {
140 schemas_without_data.push(schema_name.clone());
141 }
142 }
143
144 Ok(CoverageInfo {
145 total_tables: discovered_tables.len(),
146 tables_with_schema: tables_with_schema.len(),
147 tables_missing_schema,
148 schemas_without_data,
149 })
150 }
151
152 pub fn compute_badge(&self, coverage: &CoverageInfo) -> CoverageBadge {
159 if coverage.total_tables == 0 {
160 return CoverageBadge::Unknown;
161 }
162
163 if coverage.has_critical_issues() {
165 return CoverageBadge::Red;
166 }
167
168 let coverage_pct = coverage.coverage_percentage();
169
170 if coverage_pct >= 95.0 {
171 CoverageBadge::Green
172 } else if coverage_pct >= 50.0 {
173 CoverageBadge::Yellow
174 } else {
175 CoverageBadge::Red
176 }
177 }
178}
179
180fn normalize_table_name(name: &str) -> String {
182 name.to_lowercase()
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188 use crate::schema::registry::{SchemaRegistry, SchemaRegistryConfig};
189 use crate::schema::TableSchema;
190 use crate::{Config, Platform};
191 use std::sync::Arc;
192 use tokio::sync::RwLock;
193
194 async fn create_test_registry() -> Arc<RwLock<SchemaRegistry>> {
195 let config = Config::default();
196 let platform = Arc::new(Platform::new(&config).await.unwrap());
197 let registry_config = SchemaRegistryConfig::default();
198
199 Arc::new(RwLock::new(
200 SchemaRegistry::new(registry_config, platform, config)
201 .await
202 .unwrap(),
203 ))
204 }
205
206 async fn register_test_schema(
207 registry: Arc<RwLock<SchemaRegistry>>,
208 keyspace: &str,
209 table: &str,
210 ) {
211 use crate::schema::registry::SchemaSource;
212
213 let schema = TableSchema::new_for_testing(keyspace, table);
214
215 let reg = registry.write().await;
216 reg.register_schema(schema, SchemaSource::Manual)
217 .await
218 .unwrap();
219 }
220
221 #[tokio::test]
222 async fn test_coverage_info_percentage() {
223 let info = CoverageInfo {
224 total_tables: 10,
225 tables_with_schema: 8,
226 tables_missing_schema: vec!["ks.t1".to_string(), "ks.t2".to_string()],
227 schemas_without_data: vec![],
228 };
229
230 assert_eq!(info.coverage_percentage(), 80.0);
231 }
232
233 #[tokio::test]
234 async fn test_coverage_info_zero_tables() {
235 let info = CoverageInfo {
236 total_tables: 0,
237 tables_with_schema: 0,
238 tables_missing_schema: vec![],
239 schemas_without_data: vec![],
240 };
241
242 assert_eq!(info.coverage_percentage(), 0.0);
243 }
244
245 #[tokio::test]
246 async fn test_coverage_info_critical_issues() {
247 let info = CoverageInfo {
249 total_tables: 10,
250 tables_with_schema: 4,
251 tables_missing_schema: vec![
252 "ks.t1".to_string(),
253 "ks.t2".to_string(),
254 "ks.t3".to_string(),
255 "ks.t4".to_string(),
256 "ks.t5".to_string(),
257 "ks.t6".to_string(),
258 ],
259 schemas_without_data: vec![],
260 };
261
262 assert!(info.has_critical_issues());
263 }
264
265 #[tokio::test]
266 async fn test_coverage_badge_green() {
267 let registry = create_test_registry().await;
268 let calculator = CoverageCalculator::new(registry);
269
270 let coverage = CoverageInfo {
271 total_tables: 100,
272 tables_with_schema: 96,
273 tables_missing_schema: vec![],
274 schemas_without_data: vec![],
275 };
276
277 let badge = calculator.compute_badge(&coverage);
278 assert_eq!(badge, CoverageBadge::Green);
279 assert_eq!(badge.description(), "Excellent coverage (≥95%)");
280 assert_eq!(badge.color(), "green");
281 }
282
283 #[tokio::test]
284 async fn test_coverage_badge_yellow() {
285 let registry = create_test_registry().await;
286 let calculator = CoverageCalculator::new(registry);
287
288 let coverage = CoverageInfo {
289 total_tables: 100,
290 tables_with_schema: 75,
291 tables_missing_schema: vec![],
292 schemas_without_data: vec![],
293 };
294
295 let badge = calculator.compute_badge(&coverage);
296 assert_eq!(badge, CoverageBadge::Yellow);
297 assert_eq!(badge.description(), "Acceptable coverage (50-95%)");
298 assert_eq!(badge.color(), "yellow");
299 }
300
301 #[tokio::test]
302 async fn test_coverage_badge_red() {
303 let registry = create_test_registry().await;
304 let calculator = CoverageCalculator::new(registry);
305
306 let coverage = CoverageInfo {
307 total_tables: 100,
308 tables_with_schema: 30,
309 tables_missing_schema: vec![],
310 schemas_without_data: vec![],
311 };
312
313 let badge = calculator.compute_badge(&coverage);
314 assert_eq!(badge, CoverageBadge::Red);
315 assert_eq!(
316 badge.description(),
317 "Poor coverage (<50% or critical issues)"
318 );
319 assert_eq!(badge.color(), "red");
320 }
321
322 #[tokio::test]
323 async fn test_coverage_badge_unknown() {
324 let registry = create_test_registry().await;
325 let calculator = CoverageCalculator::new(registry);
326
327 let coverage = CoverageInfo {
328 total_tables: 0,
329 tables_with_schema: 0,
330 tables_missing_schema: vec![],
331 schemas_without_data: vec![],
332 };
333
334 let badge = calculator.compute_badge(&coverage);
335 assert_eq!(badge, CoverageBadge::Unknown);
336 assert_eq!(badge.description(), "Unknown coverage (no schema data)");
337 assert_eq!(badge.color(), "gray");
338 }
339
340 #[tokio::test]
341 async fn test_coverage_calculator() {
342 let registry = create_test_registry().await;
343
344 register_test_schema(registry.clone(), "ks1", "table1").await;
346 register_test_schema(registry.clone(), "ks1", "table2").await;
347 register_test_schema(registry.clone(), "ks2", "table3").await;
348
349 let calculator = CoverageCalculator::new(registry);
350
351 let discovered = vec![
353 "ks1.table1".to_string(),
354 "ks1.table2".to_string(),
355 "ks2.table4".to_string(), ];
357
358 let coverage = calculator.calculate(&discovered).await.unwrap();
359
360 assert_eq!(coverage.total_tables, 3);
361 assert_eq!(coverage.tables_with_schema, 2);
362 assert_eq!(coverage.tables_missing_schema.len(), 1);
363 assert!(coverage
364 .tables_missing_schema
365 .contains(&"ks2.table4".to_string()));
366 assert_eq!(coverage.schemas_without_data.len(), 1);
367 assert!(coverage
368 .schemas_without_data
369 .contains(&"ks2.table3".to_string()));
370 }
371
372 #[tokio::test]
373 async fn test_coverage_calculator_empty_discovered() {
374 let registry = create_test_registry().await;
375 register_test_schema(registry.clone(), "ks1", "table1").await;
376
377 let calculator = CoverageCalculator::new(registry);
378 let discovered: Vec<String> = vec![];
379
380 let coverage = calculator.calculate(&discovered).await.unwrap();
381
382 assert_eq!(coverage.total_tables, 0);
383 assert_eq!(coverage.tables_with_schema, 0);
384 assert_eq!(coverage.tables_missing_schema.len(), 0);
385 assert_eq!(coverage.schemas_without_data.len(), 1);
386 }
387
388 #[tokio::test]
389 async fn test_coverage_calculator_case_insensitive() {
390 let registry = create_test_registry().await;
391 register_test_schema(registry.clone(), "MyKS", "MyTable").await;
392
393 let calculator = CoverageCalculator::new(registry);
394 let discovered = vec!["myks.mytable".to_string()]; let coverage = calculator.calculate(&discovered).await.unwrap();
397
398 assert_eq!(coverage.total_tables, 1);
399 assert_eq!(coverage.tables_with_schema, 1);
400 assert_eq!(coverage.tables_missing_schema.len(), 0);
401 }
402
403 #[tokio::test]
404 async fn test_normalize_table_name() {
405 assert_eq!(normalize_table_name("MyKS.MyTable"), "myks.mytable");
406 assert_eq!(normalize_table_name("ks.table"), "ks.table");
407 assert_eq!(normalize_table_name("KS.TABLE"), "ks.table");
408 }
409}