Skip to main content

cqlite_cli/commands/
info.rs

1// SSTable Info Command Implementation - Issue #26
2// Comprehensive SSTable analysis and metadata display
3
4// Allow deprecated BulletproofReader usage (Issue #190 - experimental reader)
5// This will be removed once BulletproofReader is fully replaced with SSTableReader
6#![allow(deprecated)]
7
8use anyhow::{Context, Result};
9use cqlite_core::{
10    schema::{parse_cql_schema, TableSchema},
11    storage::sstable::{
12        bulletproof_reader::BulletproofReader, directory::SSTableDirectory, reader::SSTableReader,
13        statistics_reader::find_statistics_file,
14    },
15    Config,
16};
17use csv;
18use indicatif::{ProgressBar, ProgressStyle};
19use serde_json;
20use std::collections::HashMap;
21use std::path::{Path, PathBuf};
22use std::sync::Arc;
23use tracing::{info, warn};
24
25use crate::cli::{
26    create_version_error, detect_sstable_version, validate_cassandra_version, InfoOutputFormat,
27};
28
29/// Main info command implementation
30pub async fn execute_info_command(
31    sstable_path: &Path,
32    detailed: bool,
33    format: InfoOutputFormat,
34    validate: bool,
35    schema_path: Option<&Path>,
36    auto_detect: bool,
37    cassandra_version: Option<String>,
38) -> Result<()> {
39    println!("šŸ” CQLite SSTable Info Analysis");
40    println!("================================");
41
42    // Create progress bar for analysis
43    let pb = create_progress_bar("Analyzing SSTable");
44    pb.set_message("Detecting format...");
45
46    // Version detection and validation
47    let detected_version = if auto_detect {
48        match detect_sstable_version(&sstable_path.to_path_buf()) {
49            Ok(version) => {
50                info!("Auto-detected SSTable version: {}", version);
51                pb.set_message("Version detected");
52                Some(version)
53            }
54            Err(e) => {
55                warn!("Failed to auto-detect version: {}", e);
56                pb.set_message("Version detection failed");
57                None
58            }
59        }
60    } else {
61        None
62    };
63
64    // Validate provided Cassandra version if specified
65    let validated_version = if let Some(ref version) = cassandra_version {
66        match validate_cassandra_version(version) {
67            Ok(v) => {
68                info!("Using Cassandra version: {:?}", v);
69                pb.set_message("Cassandra version validated");
70                Some(v.to_string())
71            }
72            Err(e) => {
73                pb.finish_with_message("āŒ Version validation failed");
74                return Err(anyhow::anyhow!(create_version_error(
75                    &format!("Invalid Cassandra version: {e}"),
76                    detected_version.as_deref(),
77                    Some(version)
78                )));
79            }
80        }
81    } else {
82        None
83    };
84
85    pb.set_message("Analyzing SSTable structure...");
86
87    // Check if path is directory or file
88    let info_result = if sstable_path.is_dir() {
89        analyze_sstable_directory(
90            sstable_path,
91            detected_version.clone(),
92            validated_version.clone(),
93            detailed,
94            validate,
95            schema_path,
96            &pb,
97        )
98        .await
99    } else {
100        analyze_sstable_file(
101            sstable_path,
102            detected_version.clone(),
103            validated_version.clone(),
104            detailed,
105            validate,
106            schema_path,
107            &pb,
108        )
109        .await
110    };
111
112    match info_result {
113        Ok(metadata) => {
114            pb.finish_with_message("āœ… Analysis complete");
115            display_info_output(metadata, format).await
116        }
117        Err(e) => {
118            pb.finish_with_message("āŒ Analysis failed");
119            Err(e)
120        }
121    }
122}
123
124/// Analyze SSTable directory structure
125async fn analyze_sstable_directory(
126    directory_path: &Path,
127    detected_version: Option<String>,
128    validated_version: Option<String>,
129    _detailed: bool,
130    validate: bool,
131    schema_path: Option<&Path>,
132    pb: &ProgressBar,
133) -> Result<SSTableInfoMetadata> {
134    pb.set_message("Validating directory structure...");
135
136    // Validate directory structure
137    SSTableDirectory::validate_directory_path(directory_path)
138        .with_context(|| format!("Invalid SSTable directory: {}", directory_path.display()))?;
139
140    pb.set_message("Scanning directory contents...");
141
142    // Scan the directory
143    let directory = SSTableDirectory::scan(directory_path).with_context(|| {
144        format!(
145            "Failed to scan SSTable directory: {}",
146            directory_path.display()
147        )
148    })?;
149
150    pb.set_message("Analyzing generations...");
151
152    // Analyze each generation
153    let mut total_size = 0u64;
154    let mut total_components = 0;
155    let mut generation_info = Vec::new();
156
157    for generation in &directory.generations {
158        let mut gen_size = 0u64;
159        let mut component_details = Vec::new();
160
161        for (component, path) in &generation.components {
162            if let Ok(metadata) = std::fs::metadata(path) {
163                gen_size += metadata.len();
164                component_details.push(ComponentInfo {
165                    component_type: format!("{component:?}"),
166                    file_path: path.clone(),
167                    file_size: metadata.len(),
168                });
169            }
170            total_components += 1;
171        }
172
173        total_size += gen_size;
174        generation_info.push(GenerationInfo {
175            generation: generation.generation,
176            format: generation.format.clone(),
177            size: gen_size,
178            components: component_details,
179        });
180    }
181
182    // Load schema information if provided
183    let schema_info = if let Some(schema_path) = schema_path {
184        pb.set_message("Loading schema information...");
185        load_schema_info(schema_path).await.ok()
186    } else {
187        None
188    };
189
190    // Perform validation if requested
191    let validation_result = if validate {
192        pb.set_message("Validating directory integrity...");
193        Some(perform_directory_validation(&directory).await)
194    } else {
195        None
196    };
197
198    // Try to analyze with bulletproof reader for enhanced info
199    let bulletproof_info = if let Some(data_path) = find_data_file_in_directory(directory_path) {
200        pb.set_message("Analyzing with bulletproof reader...");
201        analyze_with_bulletproof_reader(&data_path).await.ok()
202    } else {
203        None
204    };
205
206    Ok(SSTableInfoMetadata {
207        file_path: directory_path.to_path_buf(),
208        file_type: SSTableFileType::Directory,
209        file_size: total_size,
210        detected_version: detected_version.clone(),
211        validated_version,
212        format_features: get_format_features(detected_version.as_deref()),
213        schema_info,
214        validation_result,
215        directory_info: Some(DirectoryInfo {
216            table_name: directory.table_name.clone(),
217            generations: directory.generations.len(),
218            is_valid: directory.is_valid(),
219            total_components,
220            generation_info,
221        }),
222        bulletproof_info,
223        statistics_file: None, // Directories don't have a single statistics file
224        // Default stats for directories (would need aggregation)
225        entry_count: 0,
226        table_count: 0,
227        block_count: 0,
228        index_size: 0,
229        bloom_filter_size: 0,
230        compression_ratio: 0.0,
231        cache_hit_rate: 0.0,
232    })
233}
234
235/// Analyze single SSTable file
236async fn analyze_sstable_file(
237    file_path: &Path,
238    detected_version: Option<String>,
239    validated_version: Option<String>,
240    _detailed: bool,
241    validate: bool,
242    schema_path: Option<&Path>,
243    pb: &ProgressBar,
244) -> Result<SSTableInfoMetadata> {
245    pb.set_message("Opening SSTable file...");
246
247    // Get file size
248    let file_size = std::fs::metadata(file_path)
249        .with_context(|| format!("Failed to get file metadata: {}", file_path.display()))?
250        .len();
251
252    // Try bulletproof reader first for enhanced analysis
253    let bulletproof_info = {
254        pb.set_message("Analyzing with bulletproof reader...");
255        analyze_with_bulletproof_reader(file_path).await.ok()
256    };
257
258    // Open with standard reader for statistics
259    pb.set_message("Loading SSTable reader...");
260    let config = Config::default();
261    let platform = Arc::new(cqlite_core::platform::Platform::new(&config).await?);
262    let reader = SSTableReader::open(file_path, &config, platform.clone())
263        .await
264        .with_context(|| format!("Failed to open SSTable: {}", file_path.display()))?;
265
266    pb.set_message("Reading SSTable statistics...");
267    let reader_stats = reader.stats().await?;
268
269    // Load schema information if provided
270    let schema_info = if let Some(schema_path) = schema_path {
271        pb.set_message("Loading schema information...");
272        load_schema_info(schema_path).await.ok()
273    } else {
274        None
275    };
276
277    // Check for Statistics.db file
278    let statistics_file = {
279        pb.set_message("Looking for Statistics.db...");
280        find_statistics_file(file_path)
281            .await
282            .map(|path| (path.to_path_buf(), "Statistics.db file found".to_string()))
283    };
284
285    // Perform validation if requested
286    let validation_result = if validate {
287        pb.set_message("Validating file integrity...");
288        perform_file_validation(file_path, &reader).await.ok()
289    } else {
290        None
291    };
292
293    Ok(SSTableInfoMetadata {
294        file_path: file_path.to_path_buf(),
295        file_type: SSTableFileType::SingleFile,
296        file_size,
297        detected_version: detected_version.clone(),
298        validated_version,
299        format_features: get_format_features(detected_version.as_deref()),
300        schema_info,
301        validation_result,
302        directory_info: None,
303        bulletproof_info,
304        statistics_file,
305        // Extract individual fields from reader_stats
306        entry_count: reader_stats.entry_count,
307        table_count: reader_stats.table_count as u32,
308        block_count: reader_stats.block_count as u32,
309        index_size: reader_stats.index_size,
310        bloom_filter_size: reader_stats.bloom_filter_size,
311        compression_ratio: reader_stats.compression_ratio,
312        cache_hit_rate: reader_stats.cache_hit_rate,
313    })
314}
315
316/// Analyze SSTable with bulletproof reader for enhanced information
317async fn analyze_with_bulletproof_reader(file_path: &Path) -> Result<BulletproofInfo> {
318    let mut reader = BulletproofReader::open(file_path).with_context(|| {
319        format!(
320            "Failed to open with bulletproof reader: {}",
321            file_path.display()
322        )
323    })?;
324
325    // Get all information first to avoid borrowing conflicts
326    let info = reader.info();
327    let format_info = format!("{:?}", info.format);
328    let generation = info.generation_numeric().unwrap_or(0) as u32;
329    let detected_size = info.size.parse().unwrap_or(0);
330
331    let compression_info = reader.compression_info().map(|c| CompressionInfo {
332        algorithm: c.algorithm.clone(),
333        chunk_length: c.chunk_length,
334    });
335
336    let integrity_check = reader.verify_integrity().await.ok();
337
338    // Try to parse some data for analysis
339    let data_sample = match reader.parse_sstable_data() {
340        Ok(entries) => Some(DataSampleInfo {
341            total_entries: entries.len(),
342            sample_entries: entries
343                .into_iter()
344                .take(5)
345                .map(|e| format!("{e:?}"))
346                .collect(),
347        }),
348        Err(_) => None,
349    };
350
351    Ok(BulletproofInfo {
352        format: format_info,
353        generation,
354        detected_size,
355        compression_info,
356        integrity_check,
357        data_sample,
358    })
359}
360
361/// Load schema information from schema file
362pub async fn load_schema_info(schema_path: &Path) -> Result<SchemaInfo> {
363    let schema = load_schema_file(schema_path)?;
364
365    let keyspace = schema.keyspace.clone();
366    let table = schema.table.clone();
367    let partition_keys = schema
368        .partition_keys
369        .iter()
370        .map(|k| k.name.clone())
371        .collect();
372    let clustering_keys = schema
373        .clustering_keys
374        .iter()
375        .map(|k| k.name.clone())
376        .collect();
377    let columns = schema
378        .columns
379        .iter()
380        .map(|c| ColumnInfo {
381            name: c.name.clone(),
382            data_type: c.data_type.clone(),
383            kind: determine_column_kind(&schema, &c.name),
384        })
385        .collect();
386
387    Ok(SchemaInfo {
388        keyspace,
389        table,
390        columns,
391        partition_keys,
392        clustering_keys,
393    })
394}
395
396/// Load schema from file (simplified version)
397fn load_schema_file(schema_path: &Path) -> Result<TableSchema> {
398    let schema_content = std::fs::read_to_string(schema_path)
399        .with_context(|| format!("Failed to read schema file: {}", schema_path.display()))?;
400
401    let extension = schema_path
402        .extension()
403        .and_then(|s| s.to_str())
404        .unwrap_or("")
405        .to_lowercase();
406
407    match extension.as_str() {
408        "json" => parse_json_schema(&schema_content),
409        "cql" | "sql" | "" => {
410            parse_cql_schema(&schema_content).with_context(|| "Failed to parse CQL schema")
411        }
412        _ => Err(anyhow::anyhow!(
413            "Unsupported schema file extension: .{}",
414            extension
415        )),
416    }
417}
418
419/// Parse JSON schema (simplified)
420fn parse_json_schema(schema_content: &str) -> Result<TableSchema> {
421    // This is a simplified implementation - would need full JSON schema parsing
422    let json: serde_json::Value = serde_json::from_str(schema_content)?;
423
424    let keyspace = json["keyspace"]
425        .as_str()
426        .ok_or_else(|| anyhow::anyhow!("Missing keyspace in schema"))?;
427    let table = json["table"]
428        .as_str()
429        .ok_or_else(|| anyhow::anyhow!("Missing table in schema"))?;
430
431    // Create a basic schema structure
432    Ok(TableSchema {
433        keyspace: keyspace.to_string(),
434        table: table.to_string(),
435        columns: Vec::new(), // Would need to parse columns from JSON
436        partition_keys: Vec::new(),
437        clustering_keys: Vec::new(),
438        comments: HashMap::new(),
439    })
440}
441
442/// Determine column kind based on schema
443fn determine_column_kind(schema: &TableSchema, column_name: &str) -> String {
444    if schema.partition_keys.iter().any(|k| k.name == column_name) {
445        "PartitionKey".to_string()
446    } else if schema.clustering_keys.iter().any(|k| k.name == column_name) {
447        "ClusteringKey".to_string()
448    } else {
449        "Regular".to_string()
450    }
451}
452
453/// Perform file validation
454pub async fn perform_file_validation(
455    _file_path: &Path,
456    _reader: &SSTableReader,
457) -> Result<ValidationResult> {
458    // TODO: Implement actual file validation
459    Ok(ValidationResult {
460        is_valid: true,
461        errors: vec![],
462        warnings: vec!["File validation not yet fully implemented".to_string()],
463        total_blocks_checked: 0,
464        corrupted_blocks: 0,
465    })
466}
467
468/// Perform directory validation
469pub async fn perform_directory_validation(directory: &SSTableDirectory) -> ValidationResult {
470    match directory.validate_all_generations() {
471        Ok(report) => ValidationResult {
472            is_valid: report.is_valid(),
473            errors: report.validation_errors.clone(),
474            warnings: report.toc_inconsistencies.clone(),
475            total_blocks_checked: 0,
476            corrupted_blocks: 0,
477        },
478        Err(e) => ValidationResult {
479            is_valid: false,
480            errors: vec![format!("Directory validation failed: {}", e)],
481            warnings: vec![],
482            total_blocks_checked: 0,
483            corrupted_blocks: 0,
484        },
485    }
486}
487
488/// Find the Data.db file in a directory
489fn find_data_file_in_directory(dir_path: &Path) -> Option<PathBuf> {
490    let patterns = ["*-Data.db", "*-big-Data.db", "nb-*-big-Data.db"];
491
492    for _pattern in &patterns {
493        if let Ok(entries) = std::fs::read_dir(dir_path) {
494            for entry in entries.flatten() {
495                let file_name = entry.file_name();
496                let file_name_str = file_name.to_string_lossy();
497
498                // Simple pattern matching (could be improved)
499                if file_name_str.ends_with("-Data.db") {
500                    return Some(entry.path());
501                }
502            }
503        }
504    }
505
506    None
507}
508
509/// Get format features based on detected version
510pub fn get_format_features(version: Option<&str>) -> Vec<String> {
511    match version {
512        Some("3.11") => vec![
513            "Legacy SSTable format (la)".to_string(),
514            "Basic compression support".to_string(),
515            "Simple bloom filters".to_string(),
516            "Traditional indexing".to_string(),
517        ],
518        Some("4.0") => vec![
519            "Modern compact format (mc)".to_string(),
520            "Improved compression algorithms".to_string(),
521            "Enhanced bloom filters".to_string(),
522            "Streaming support".to_string(),
523            "Better statistics".to_string(),
524        ],
525        Some("5.0") => vec![
526            "New big format (nb)".to_string(),
527            "Big Trie Index (bti) support".to_string(),
528            "Advanced compression".to_string(),
529            "Optimized I/O patterns".to_string(),
530            "Enhanced metadata".to_string(),
531            "Native ZSTD compression".to_string(),
532        ],
533        _ => vec!["Unknown format features".to_string()],
534    }
535}
536
537/// Create a progress bar for analysis
538fn create_progress_bar(message: &str) -> ProgressBar {
539    let pb = ProgressBar::new_spinner();
540    pb.set_style(
541        ProgressStyle::default_spinner()
542            .template("{spinner:.green} [{elapsed_precise}] {msg}")
543            .unwrap(),
544    );
545    pb.set_message(message.to_string());
546    pb
547}
548
549/// Display info output in specified format
550pub async fn display_info_output(
551    metadata: SSTableInfoMetadata,
552    format: InfoOutputFormat,
553) -> Result<()> {
554    match format {
555        InfoOutputFormat::Text => display_text_format(&metadata).await,
556        InfoOutputFormat::Json => display_json_format(&metadata).await,
557        InfoOutputFormat::Csv => display_csv_format(&metadata).await,
558    }
559}
560
561/// Display metadata in human-readable text format
562async fn display_text_format(metadata: &SSTableInfoMetadata) -> Result<()> {
563    println!();
564    println!("šŸ“Š SSTable Analysis Results");
565    println!("===========================");
566
567    // Basic information
568    println!("šŸ“‚ Path: {}", metadata.file_path.display());
569    println!(
570        "šŸ“„ Type: {}",
571        match metadata.file_type {
572            SSTableFileType::SingleFile => "Single SSTable File",
573            SSTableFileType::Directory => "SSTable Directory",
574        }
575    );
576    println!(
577        "šŸ“ Size: {} bytes ({:.2} MB)",
578        metadata.file_size,
579        metadata.file_size as f64 / 1_048_576.0
580    );
581
582    // Version information
583    if let Some(ref version) = metadata.detected_version {
584        println!("šŸ” Detected Version: {version}");
585    }
586    if let Some(ref version) = metadata.validated_version {
587        println!("āœ… Cassandra Version: {version}");
588    }
589
590    // Format features
591    println!("\nšŸš€ Format Features:");
592    for feature in &metadata.format_features {
593        println!("  • {feature}");
594    }
595
596    // Bulletproof reader information
597    if let Some(ref bp_info) = metadata.bulletproof_info {
598        println!("\nšŸ›”ļø Bulletproof Reader Analysis:");
599        println!("  Format: {}", bp_info.format);
600        println!("  Generation: {}", bp_info.generation);
601        println!("  Detected Size: {} bytes", bp_info.detected_size);
602
603        if let Some(ref compression) = bp_info.compression_info {
604            println!(
605                "  Compression: {} (chunk size: {})",
606                compression.algorithm, compression.chunk_length
607            );
608        }
609
610        if let Some(integrity) = bp_info.integrity_check {
611            println!(
612                "  Integrity: {}",
613                if integrity {
614                    "āœ… Valid"
615                } else {
616                    "āŒ Issues detected"
617                }
618            );
619        }
620
621        if let Some(ref sample) = bp_info.data_sample {
622            println!("  Data Entries: {} total", sample.total_entries);
623            if !sample.sample_entries.is_empty() {
624                println!(
625                    "  Sample Keys: {}",
626                    sample
627                        .sample_entries
628                        .iter()
629                        .take(3)
630                        .cloned()
631                        .collect::<Vec<_>>()
632                        .join(", ")
633                );
634            }
635        }
636    }
637
638    // Schema information
639    if let Some(ref schema) = metadata.schema_info {
640        println!("\nšŸ“‹ Schema Information:");
641        println!("  Keyspace: {}", schema.keyspace);
642        println!("  Table: {}", schema.table);
643        println!("  Columns: {}", schema.columns.len());
644        println!("  Partition Keys: {}", schema.partition_keys.join(", "));
645        if !schema.clustering_keys.is_empty() {
646            println!("  Clustering Keys: {}", schema.clustering_keys.join(", "));
647        }
648
649        // Column details
650        if !schema.columns.is_empty() {
651            println!("  Column Details:");
652            for col in &schema.columns {
653                println!("    {} ({}) - {}", col.name, col.data_type, col.kind);
654            }
655        }
656    }
657
658    // Directory-specific information
659    if let Some(ref dir_info) = metadata.directory_info {
660        println!("\nšŸ“ Directory Information:");
661        println!("  Table Name: {}", dir_info.table_name);
662        println!("  Generations: {}", dir_info.generations);
663        println!("  Total Components: {}", dir_info.total_components);
664        println!(
665            "  Valid: {}",
666            if dir_info.is_valid {
667                "āœ… Yes"
668            } else {
669                "āŒ No"
670            }
671        );
672
673        // Generation details
674        if !dir_info.generation_info.is_empty() {
675            println!("\n  šŸ“Š Generation Details:");
676            for generation in &dir_info.generation_info {
677                println!(
678                    "    Generation {}: {} format, {} bytes, {} components",
679                    generation.generation,
680                    generation.format,
681                    generation.size,
682                    generation.components.len()
683                );
684
685                for component in &generation.components {
686                    println!(
687                        "      {} - {} bytes",
688                        component.component_type, component.file_size
689                    );
690                }
691            }
692        }
693    }
694
695    // Reader statistics
696    if metadata.entry_count > 0 || metadata.table_count > 0 {
697        println!("\nšŸ“ˆ SSTable Statistics:");
698        println!("  Entry Count: {}", metadata.entry_count);
699        println!("  Table Count: {}", metadata.table_count);
700        println!("  Block Count: {}", metadata.block_count);
701        println!("  Index Size: {} bytes", metadata.index_size);
702        println!("  Bloom Filter Size: {} bytes", metadata.bloom_filter_size);
703        println!(
704            "  Compression Ratio: {:.2}%",
705            metadata.compression_ratio * 100.0
706        );
707        println!("  Cache Hit Rate: {:.2}%", metadata.cache_hit_rate * 100.0);
708    }
709
710    // Statistics file information
711    if let Some((ref path, ref status)) = metadata.statistics_file {
712        println!("\nšŸ“Š Statistics File:");
713        println!("  Path: {}", path.display());
714        println!("  Status: {status}");
715    }
716
717    // Validation results
718    if let Some(ref validation) = metadata.validation_result {
719        println!("\nšŸ” Validation Results:");
720        println!(
721            "  Status: {}",
722            if validation.is_valid {
723                "āœ… Valid"
724            } else {
725                "āŒ Invalid"
726            }
727        );
728
729        if validation.total_blocks_checked > 0 {
730            println!("  Blocks Checked: {}", validation.total_blocks_checked);
731            println!("  Corrupted Blocks: {}", validation.corrupted_blocks);
732        }
733
734        if !validation.errors.is_empty() {
735            println!("  Errors:");
736            for error in &validation.errors {
737                println!("    āŒ {error}");
738            }
739        }
740
741        if !validation.warnings.is_empty() {
742            println!("  Warnings:");
743            for warning in &validation.warnings {
744                println!("    āš ļø {warning}");
745            }
746        }
747    }
748
749    println!("\nāœ… Analysis Complete");
750    Ok(())
751}
752
753/// Display metadata in JSON format
754async fn display_json_format(metadata: &SSTableInfoMetadata) -> Result<()> {
755    let json = serde_json::to_string_pretty(metadata)?;
756    println!("{json}");
757    Ok(())
758}
759
760/// Display metadata in CSV format
761async fn display_csv_format(metadata: &SSTableInfoMetadata) -> Result<()> {
762    let mut wtr = csv::Writer::from_writer(std::io::stdout());
763
764    // Write headers
765    wtr.write_record([
766        "path",
767        "type",
768        "size_bytes",
769        "detected_version",
770        "cassandra_version",
771        "generations",
772        "components",
773        "entry_count",
774        "table_count",
775        "block_count",
776        "compression_ratio",
777        "cache_hit_rate",
778        "is_valid",
779        "errors",
780        "warnings",
781    ])?;
782
783    // Prepare data
784    let file_type = match metadata.file_type {
785        SSTableFileType::SingleFile => "file",
786        SSTableFileType::Directory => "directory",
787    };
788
789    let generations = metadata
790        .directory_info
791        .as_ref()
792        .map(|d| d.generations.to_string())
793        .unwrap_or_else(|| "1".to_string());
794
795    let components = metadata
796        .directory_info
797        .as_ref()
798        .map(|d| d.total_components.to_string())
799        .unwrap_or_else(|| "1".to_string());
800
801    let entry_count = metadata.entry_count.to_string();
802    let table_count = metadata.table_count.to_string();
803    let block_count = metadata.block_count.to_string();
804    let compression_ratio = format!("{:.2}", metadata.compression_ratio * 100.0);
805    let cache_hit_rate = format!("{:.2}", metadata.cache_hit_rate * 100.0);
806
807    let is_valid = metadata
808        .validation_result
809        .as_ref()
810        .map(|v| v.is_valid.to_string())
811        .unwrap_or_else(|| "unknown".to_string());
812
813    let errors = metadata
814        .validation_result
815        .as_ref()
816        .map(|v| v.errors.join("; "))
817        .unwrap_or_default();
818
819    let warnings = metadata
820        .validation_result
821        .as_ref()
822        .map(|v| v.warnings.join("; "))
823        .unwrap_or_default();
824
825    // Write data row
826    wtr.write_record([
827        &metadata.file_path.display().to_string(),
828        file_type,
829        &metadata.file_size.to_string(),
830        metadata.detected_version.as_deref().unwrap_or("unknown"),
831        metadata.validated_version.as_deref().unwrap_or("unknown"),
832        &generations,
833        &components,
834        &entry_count,
835        &table_count,
836        &block_count,
837        &compression_ratio,
838        &cache_hit_rate,
839        &is_valid,
840        &errors,
841        &warnings,
842    ])?;
843
844    wtr.flush()?;
845    Ok(())
846}
847
848// Data structures for enhanced info output
849
850/// Complete SSTable information metadata
851#[derive(Debug, Clone, serde::Serialize)]
852pub struct SSTableInfoMetadata {
853    pub file_path: PathBuf,
854    pub file_type: SSTableFileType,
855    pub file_size: u64,
856    pub detected_version: Option<String>,
857    pub validated_version: Option<String>,
858    pub format_features: Vec<String>,
859    pub schema_info: Option<SchemaInfo>,
860    pub validation_result: Option<ValidationResult>,
861    pub directory_info: Option<DirectoryInfo>,
862    pub bulletproof_info: Option<BulletproofInfo>,
863    pub statistics_file: Option<(PathBuf, String)>,
864    // Note: SSTableReaderStats doesn't implement Serialize, so we'll store the data separately
865    pub entry_count: u64,
866    pub table_count: u32,
867    pub block_count: u32,
868    pub index_size: u64,
869    pub bloom_filter_size: u64,
870    pub compression_ratio: f64,
871    pub cache_hit_rate: f64,
872}
873
874/// File type enumeration
875#[derive(Debug, Clone, serde::Serialize)]
876pub enum SSTableFileType {
877    SingleFile,
878    Directory,
879}
880
881/// Schema information
882#[derive(Debug, Clone, serde::Serialize)]
883pub struct SchemaInfo {
884    pub keyspace: String,
885    pub table: String,
886    pub columns: Vec<ColumnInfo>,
887    pub partition_keys: Vec<String>,
888    pub clustering_keys: Vec<String>,
889}
890
891/// Column information
892#[derive(Debug, Clone, serde::Serialize)]
893pub struct ColumnInfo {
894    pub name: String,
895    pub data_type: String,
896    pub kind: String,
897}
898
899/// Directory-specific information
900#[derive(Debug, Clone, serde::Serialize)]
901pub struct DirectoryInfo {
902    pub table_name: String,
903    pub generations: usize,
904    pub is_valid: bool,
905    pub total_components: usize,
906    pub generation_info: Vec<GenerationInfo>,
907}
908
909/// Generation information
910#[derive(Debug, Clone, serde::Serialize)]
911pub struct GenerationInfo {
912    pub generation: u32,
913    pub format: String,
914    pub size: u64,
915    pub components: Vec<ComponentInfo>,
916}
917
918/// Component information
919#[derive(Debug, Clone, serde::Serialize)]
920pub struct ComponentInfo {
921    pub component_type: String,
922    pub file_path: PathBuf,
923    pub file_size: u64,
924}
925
926/// Bulletproof reader information
927#[derive(Debug, Clone, serde::Serialize)]
928pub struct BulletproofInfo {
929    pub format: String,
930    pub generation: u32,
931    pub detected_size: u64,
932    pub compression_info: Option<CompressionInfo>,
933    pub integrity_check: Option<bool>,
934    pub data_sample: Option<DataSampleInfo>,
935}
936
937/// Compression information
938#[derive(Debug, Clone, serde::Serialize)]
939pub struct CompressionInfo {
940    pub algorithm: String,
941    pub chunk_length: u32,
942}
943
944/// Data sample information
945#[derive(Debug, Clone, serde::Serialize)]
946pub struct DataSampleInfo {
947    pub total_entries: usize,
948    pub sample_entries: Vec<String>, // Convert to serializable format
949}
950
951/// Validation result
952#[derive(Debug, Clone, serde::Serialize)]
953pub struct ValidationResult {
954    pub is_valid: bool,
955    pub errors: Vec<String>,
956    pub warnings: Vec<String>,
957    pub total_blocks_checked: usize,
958    pub corrupted_blocks: usize,
959}