Skip to main content

cqlite_core/parser/
statistics.rs

1//! Statistics.db parser for Cassandra 5+ SSTable format
2//!
3//! This module provides comprehensive parsing of Statistics.db files which contain
4//! detailed metadata about SSTable contents including row counts, min/max timestamps,
5//! column statistics, and other metadata for efficient query planning.
6
7// Issue #1623: this legacy Statistics.db parser is superseded on the prod path
8// by enhanced_statistics_parser; its self-tests use CQLite's ZigZag encoder, so
9// keep the signed length helper here (no real-data impact).
10use super::vint::{parse_vint, parse_vint_length_signed};
11use crate::error::{Error, Result};
12use nom::{
13    bytes::complete::take,
14    multi::count,
15    number::complete::{be_f64, be_i64, be_u32, be_u64, be_u8},
16    IResult,
17};
18use serde::{Deserialize, Serialize};
19use std::collections::HashMap;
20
21/// Statistics.db file header with version and metadata
22/// Updated to support both legacy and enhanced formats
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct StatisticsHeader {
25    /// Format version/type identifier
26    pub version: u32,
27    /// Statistics type/kind identifier (for enhanced format) or table_id (legacy)
28    pub statistics_kind: u32,
29    /// Data length or offset
30    pub data_length: u32,
31    /// Additional metadata field
32    pub metadata1: u32,
33    /// Additional metadata field
34    pub metadata2: u32,
35    /// Additional metadata field
36    pub metadata3: u32,
37    /// CRC32 checksum of the statistics data
38    pub checksum: u32,
39    /// Table UUID for validation (optional for enhanced format)
40    pub table_id: Option<[u8; 16]>,
41}
42
43/// Comprehensive SSTable statistics extracted from Statistics.db
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct SSTableStatistics {
46    /// Header information
47    pub header: StatisticsHeader,
48    /// Row count statistics
49    pub row_stats: RowStatistics,
50    /// Timestamp range information
51    pub timestamp_stats: TimestampStatistics,
52    /// Column-level statistics
53    pub column_stats: Vec<ColumnStatistics>,
54    /// Table-level aggregated statistics
55    pub table_stats: TableStatistics,
56    /// Partition size distribution
57    pub partition_stats: PartitionStatistics,
58    /// Compression statistics
59    pub compression_stats: CompressionStatistics,
60    /// Additional metadata
61    pub metadata: HashMap<String, String>,
62    /// SerializationHeader columns (Issue #163)
63    ///
64    /// Column definitions parsed from SerializationHeader embedded in nb-format
65    /// Statistics.db files. Used for schema extraction in V5CompressedLegacy format.
66    /// Empty if SerializationHeader not found in Statistics.db.
67    #[serde(default)]
68    pub serialization_header_columns: Vec<super::header::ColumnInfo>,
69    /// Partition key definitions extracted from SerializationHeader (Issue #195)
70    #[serde(default)]
71    pub serialization_header_partition_keys: Vec<super::header::ColumnInfo>,
72    /// Clustering key definitions extracted from SerializationHeader (Issue #195)
73    #[serde(default)]
74    pub serialization_header_clustering_keys: Vec<super::header::ColumnInfo>,
75    /// Estimated tombstone-drop-times histogram as `(point, count)` pairs,
76    /// decoded best-effort from the STATS component (Issue #1073). Empty when
77    /// the SSTable carries no tombstones or the histogram could not be decoded.
78    #[serde(default)]
79    pub tombstone_drop_times: Vec<(i64, u64)>,
80}
81
82/// Row count and distribution statistics
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct RowStatistics {
85    /// Total number of rows in the SSTable
86    pub total_rows: u64,
87    /// Number of live (non-tombstone) rows
88    pub live_rows: u64,
89    /// Number of tombstone markers
90    pub tombstone_count: u64,
91    /// Estimated number of partitions
92    pub partition_count: u64,
93    /// Average rows per partition
94    pub avg_rows_per_partition: f64,
95    /// Row size distribution histogram
96    pub row_size_histogram: Vec<RowSizeBucket>,
97}
98
99/// Timestamp range and TTL statistics
100#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct TimestampStatistics {
102    /// Minimum timestamp in the SSTable (microseconds since epoch)
103    pub min_timestamp: i64,
104    /// Maximum timestamp in the SSTable (microseconds since epoch)
105    pub max_timestamp: i64,
106    /// Minimum deletion time (for tombstones)
107    pub min_deletion_time: i64,
108    /// Maximum deletion time (for tombstones)
109    pub max_deletion_time: i64,
110    /// Minimum TTL value
111    pub min_ttl: Option<i64>,
112    /// Maximum TTL value
113    pub max_ttl: Option<i64>,
114    /// Number of rows with TTL
115    pub rows_with_ttl: u64,
116}
117
118/// Per-column statistics for query optimization
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct ColumnStatistics {
121    /// Column name
122    pub name: String,
123    /// Column type (CQL type)
124    pub column_type: String,
125    /// Number of non-null values
126    pub value_count: u64,
127    /// Number of null values
128    pub null_count: u64,
129    /// Minimum value (serialized as bytes)
130    pub min_value: Option<Vec<u8>>,
131    /// Maximum value (serialized as bytes)
132    pub max_value: Option<Vec<u8>>,
133    /// Average serialized size in bytes
134    pub avg_size: f64,
135    /// Estimated cardinality (distinct values)
136    pub cardinality: u64,
137    /// Value frequency histogram for common values
138    pub value_histogram: Vec<ValueFrequency>,
139    /// Whether this column has an index
140    pub has_index: bool,
141}
142
143/// Table-level aggregated statistics
144#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct TableStatistics {
146    /// Total disk space used by the SSTable
147    pub disk_size: u64,
148    /// Uncompressed size
149    pub uncompressed_size: u64,
150    /// Compressed size
151    pub compressed_size: u64,
152    /// Compression ratio
153    pub compression_ratio: f64,
154    /// Number of blocks in the SSTable
155    pub block_count: u64,
156    /// Average block size
157    pub avg_block_size: f64,
158    /// Index size in bytes
159    pub index_size: u64,
160    /// Bloom filter size in bytes
161    pub bloom_filter_size: u64,
162    /// Number of levels in LSM tree
163    pub level_count: u32,
164}
165
166/// Partition size distribution for efficient range queries
167#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct PartitionStatistics {
169    /// Average partition size in bytes
170    pub avg_partition_size: f64,
171    /// Minimum partition size
172    pub min_partition_size: u64,
173    /// Maximum partition size
174    pub max_partition_size: u64,
175    /// Partition size distribution
176    pub size_histogram: Vec<PartitionSizeBucket>,
177    /// Percentage of large partitions (>1MB)
178    pub large_partition_percentage: f64,
179}
180
181/// Compression algorithm performance statistics
182#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct CompressionStatistics {
184    /// Compression algorithm used
185    pub algorithm: String,
186    /// Original size before compression
187    pub original_size: u64,
188    /// Compressed size
189    pub compressed_size: u64,
190    /// Compression ratio (compressed/original)
191    pub ratio: f64,
192    /// Compression speed in MB/s
193    pub compression_speed: f64,
194    /// Decompression speed in MB/s
195    pub decompression_speed: f64,
196    /// Number of compressed blocks
197    pub compressed_blocks: u64,
198}
199
200/// Row size distribution bucket
201#[derive(Debug, Clone, Serialize, Deserialize)]
202pub struct RowSizeBucket {
203    /// Size range start (inclusive)
204    pub size_start: u64,
205    /// Size range end (exclusive)
206    pub size_end: u64,
207    /// Number of rows in this bucket
208    pub count: u64,
209    /// Percentage of total rows
210    pub percentage: f64,
211}
212
213/// Value frequency information for column statistics
214#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct ValueFrequency {
216    /// Serialized value (truncated for large values)
217    pub value: Vec<u8>,
218    /// Number of occurrences
219    pub frequency: u64,
220    /// Percentage of total non-null values
221    pub percentage: f64,
222}
223
224/// Partition size distribution bucket
225#[derive(Debug, Clone, Serialize, Deserialize)]
226pub struct PartitionSizeBucket {
227    /// Size range start (inclusive)
228    pub size_start: u64,
229    /// Size range end (exclusive)
230    pub size_end: u64,
231    /// Number of partitions in this bucket
232    pub count: u64,
233    /// Cumulative percentage
234    pub cumulative_percentage: f64,
235}
236
237/// Parse the complete Statistics.db file
238pub fn parse_statistics_file(input: &[u8]) -> IResult<&[u8], SSTableStatistics> {
239    let (input, header) = parse_statistics_header(input)?;
240    let (input, row_stats) = parse_row_statistics(input)?;
241    let (input, timestamp_stats) = parse_timestamp_statistics(input)?;
242    let (input, column_stats) = parse_column_statistics(input, header.data_length)?;
243    let (input, table_stats) = parse_table_statistics(input)?;
244    let (input, partition_stats) = parse_partition_statistics(input)?;
245    let (input, compression_stats) = parse_compression_statistics(input)?;
246    let (input, metadata) = parse_metadata_section(input)?;
247
248    Ok((
249        input,
250        SSTableStatistics {
251            header,
252            row_stats,
253            timestamp_stats,
254            column_stats,
255            table_stats,
256            partition_stats,
257            compression_stats,
258            metadata,
259            serialization_header_columns: vec![], // Not available in legacy format
260            serialization_header_partition_keys: vec![],
261            serialization_header_clustering_keys: vec![],
262            tombstone_drop_times: vec![],
263        },
264    ))
265}
266
267/// Parse the Statistics.db file header with authoritative format detection
268///
269/// Statistics.db format is definitively identified by the version field:
270/// - **Version 4**: 'nb' (new big) format - Cassandra 5.0+ enhanced statistics
271///     - Structure: version(4) + statistics_kind(4) + reserved(4) + data_length(4) +
272///       metadata1(4) + metadata2(4) + metadata3(4) + checksum(4) = 32 bytes
273///     - Authoritative marker: version == 4
274///     - Used by: Cassandra 5.0+ with 'nb' SSTable format
275///
276/// - **Versions 1-3**: Legacy format - pre-Cassandra 5.0 statistics
277///     - Structure: version(4) + table_id(16) + section_count(4) + file_size(8) + checksum(4) = 36 bytes
278///     - Authoritative marker: version in range 1..=3
279///     - Used by: Cassandra 3.x and 4.x
280///
281/// Any other version number is unsupported and results in a parse error.
282pub fn parse_statistics_header(input: &[u8]) -> IResult<&[u8], StatisticsHeader> {
283    let (remaining, version) = be_u32(input)?;
284
285    match version {
286        // nb-format: Cassandra 5.0+ enhanced statistics (version 4)
287        // This is the authoritative format identifier - no heuristics needed
288        4 => parse_nb_format_header(remaining, version),
289
290        // Legacy format: Cassandra 3.x/4.x statistics (versions 1-3)
291        // Definitively identified by version range
292        1..=3 => parse_legacy_format_header(remaining, version),
293
294        // Unknown/unsupported version - fail explicitly
295        // This ensures we never silently misparse corrupt or future formats
296        _ => Err(nom::Err::Error(nom::error::Error::new(
297            input,
298            nom::error::ErrorKind::Verify,
299        ))),
300    }
301}
302
303/// Parse nb-format (version 4) Statistics.db header
304///
305/// Format structure (Cassandra 5.0+):
306/// ```text
307/// [0..4]   version: u32          = 4 (nb-format identifier)
308/// [4..8]   statistics_kind: u32  (statistics type/kind identifier)
309/// [8..12]  reserved: u32         (reserved field, typically 0)
310/// [12..16] data_length: u32      (length of statistics data section)
311/// [16..20] metadata1: u32        (metadata field 1)
312/// [20..24] metadata2: u32        (metadata field 2)
313/// [24..28] metadata3: u32        (metadata field 3)
314/// [28..32] checksum: u32         (CRC32 checksum)
315/// ```
316fn parse_nb_format_header(input: &[u8], version: u32) -> IResult<&[u8], StatisticsHeader> {
317    let (input, statistics_kind) = be_u32(input)?;
318    let (input, _reserved) = be_u32(input)?;
319    let (input, data_length) = be_u32(input)?;
320    let (input, metadata1) = be_u32(input)?;
321    let (input, metadata2) = be_u32(input)?;
322    let (input, metadata3) = be_u32(input)?;
323    let (input, checksum) = be_u32(input)?;
324
325    Ok((
326        input,
327        StatisticsHeader {
328            version,
329            statistics_kind,
330            data_length,
331            metadata1,
332            metadata2,
333            metadata3,
334            checksum,
335            table_id: None, // nb-format does not include table_id in header
336        },
337    ))
338}
339
340/// Parse legacy format (versions 1-3) Statistics.db header
341///
342/// Format structure (Cassandra 3.x/4.x):
343/// ```text
344/// [0..4]   version: u32          = 1, 2, or 3 (legacy format identifier)
345/// [4..20]  table_id: [u8; 16]    (UUID of the table)
346/// [20..24] section_count: u32    (number of statistics sections)
347/// [24..32] file_size: u64        (total file size)
348/// [32..36] checksum: u32         (CRC32 checksum)
349/// ```
350fn parse_legacy_format_header(input: &[u8], version: u32) -> IResult<&[u8], StatisticsHeader> {
351    let (input, table_id_raw) = take(16u8)(input)?;
352    let mut table_id_array = [0u8; 16];
353    table_id_array.copy_from_slice(table_id_raw);
354
355    let (input, section_count) = be_u32(input)?;
356    let (input, file_size) = be_u64(input)?;
357    let (input, checksum) = be_u32(input)?;
358
359    Ok((
360        input,
361        StatisticsHeader {
362            version,
363            statistics_kind: 0, // Not used in legacy format
364            data_length: section_count,
365            metadata1: (file_size >> 32) as u32,
366            metadata2: file_size as u32,
367            metadata3: 0,
368            checksum,
369            table_id: Some(table_id_array),
370        },
371    ))
372}
373
374/// Parse row count and distribution statistics
375pub fn parse_row_statistics(input: &[u8]) -> IResult<&[u8], RowStatistics> {
376    let (input, total_rows) = parse_vint_as_u64(input)?;
377    let (input, live_rows) = parse_vint_as_u64(input)?;
378    let (input, tombstone_count) = parse_vint_as_u64(input)?;
379    let (input, partition_count) = parse_vint_as_u64(input)?;
380    let (input, avg_rows_per_partition) = be_f64(input)?;
381    let (input, histogram_count) = be_u32(input)?;
382    let (input, row_size_histogram) =
383        count(parse_row_size_bucket, histogram_count as usize)(input)?;
384
385    Ok((
386        input,
387        RowStatistics {
388            total_rows,
389            live_rows,
390            tombstone_count,
391            partition_count,
392            avg_rows_per_partition,
393            row_size_histogram,
394        },
395    ))
396}
397
398/// Parse timestamp range statistics
399pub fn parse_timestamp_statistics(input: &[u8]) -> IResult<&[u8], TimestampStatistics> {
400    let (input, min_timestamp) = be_i64(input)?;
401    let (input, max_timestamp) = be_i64(input)?;
402    let (input, min_deletion_time) = be_i64(input)?;
403    let (input, max_deletion_time) = be_i64(input)?;
404    let (input, has_ttl) = be_u8(input)?;
405    let (input, min_ttl, max_ttl, rows_with_ttl) = if has_ttl != 0 {
406        let (input, min_ttl) = be_i64(input)?;
407        let (input, max_ttl) = be_i64(input)?;
408        let (input, rows_with_ttl) = parse_vint_as_u64(input)?;
409        (input, Some(min_ttl), Some(max_ttl), rows_with_ttl)
410    } else {
411        (input, None, None, 0)
412    };
413
414    Ok((
415        input,
416        TimestampStatistics {
417            min_timestamp,
418            max_timestamp,
419            min_deletion_time,
420            max_deletion_time,
421            min_ttl,
422            max_ttl,
423            rows_with_ttl,
424        },
425    ))
426}
427
428/// Parse column-level statistics
429pub fn parse_column_statistics(
430    input: &[u8],
431    column_count: u32,
432) -> IResult<&[u8], Vec<ColumnStatistics>> {
433    count(parse_single_column_statistics, column_count as usize)(input)
434}
435
436/// Parse statistics for a single column
437pub fn parse_single_column_statistics(input: &[u8]) -> IResult<&[u8], ColumnStatistics> {
438    let (input, name_len) = parse_vint_length_signed(input)?;
439    let (input, name_bytes) = take(name_len)(input)?;
440    let name = String::from_utf8_lossy(name_bytes).to_string();
441
442    let (input, type_len) = parse_vint_length_signed(input)?;
443    let (input, type_bytes) = take(type_len)(input)?;
444    let column_type = String::from_utf8_lossy(type_bytes).to_string();
445
446    let (input, value_count) = parse_vint_as_u64(input)?;
447    let (input, null_count) = parse_vint_as_u64(input)?;
448
449    let (input, has_min_max) = be_u8(input)?;
450    let (input, min_value, max_value) = if has_min_max != 0 {
451        let (input, min_len) = parse_vint_length_signed(input)?;
452        let (input, min_bytes) = take(min_len)(input)?;
453        let (input, max_len) = parse_vint_length_signed(input)?;
454        let (input, max_bytes) = take(max_len)(input)?;
455        (input, Some(min_bytes.to_vec()), Some(max_bytes.to_vec()))
456    } else {
457        (input, None, None)
458    };
459
460    let (input, avg_size) = be_f64(input)?;
461    let (input, cardinality) = parse_vint_as_u64(input)?;
462
463    let (input, histogram_count) = be_u32(input)?;
464    let (input, value_histogram) = count(parse_value_frequency, histogram_count as usize)(input)?;
465
466    let (input, has_index) = be_u8(input)?;
467
468    Ok((
469        input,
470        ColumnStatistics {
471            name,
472            column_type,
473            value_count,
474            null_count,
475            min_value,
476            max_value,
477            avg_size,
478            cardinality,
479            value_histogram,
480            has_index: has_index != 0,
481        },
482    ))
483}
484
485/// Parse table-level statistics
486pub fn parse_table_statistics(input: &[u8]) -> IResult<&[u8], TableStatistics> {
487    let (input, disk_size) = be_u64(input)?;
488    let (input, uncompressed_size) = be_u64(input)?;
489    let (input, compression_ratio) = be_f64(input)?;
490    let (input, block_count) = parse_vint_as_u64(input)?;
491    let (input, avg_block_size) = be_f64(input)?;
492    let (input, index_size) = be_u64(input)?;
493    let (input, bloom_filter_size) = be_u64(input)?;
494    let (input, level_count) = be_u32(input)?;
495
496    Ok((
497        input,
498        TableStatistics {
499            disk_size,
500            uncompressed_size,
501            compressed_size: disk_size, // For now, assume disk_size is compressed_size
502            compression_ratio,
503            block_count,
504            avg_block_size,
505            index_size,
506            bloom_filter_size,
507            level_count,
508        },
509    ))
510}
511
512/// Parse partition size distribution statistics
513pub fn parse_partition_statistics(input: &[u8]) -> IResult<&[u8], PartitionStatistics> {
514    let (input, avg_partition_size) = be_f64(input)?;
515    let (input, min_partition_size) = be_u64(input)?;
516    let (input, max_partition_size) = be_u64(input)?;
517    let (input, large_partition_percentage) = be_f64(input)?;
518
519    let (input, histogram_count) = be_u32(input)?;
520    let (input, size_histogram) =
521        count(parse_partition_size_bucket, histogram_count as usize)(input)?;
522
523    Ok((
524        input,
525        PartitionStatistics {
526            avg_partition_size,
527            min_partition_size,
528            max_partition_size,
529            size_histogram,
530            large_partition_percentage,
531        },
532    ))
533}
534
535/// Parse compression performance statistics
536pub fn parse_compression_statistics(input: &[u8]) -> IResult<&[u8], CompressionStatistics> {
537    let (input, algorithm_len) = parse_vint_length_signed(input)?;
538    let (input, algorithm_bytes) = take(algorithm_len)(input)?;
539    let algorithm = String::from_utf8_lossy(algorithm_bytes).to_string();
540
541    let (input, original_size) = be_u64(input)?;
542    let (input, compressed_size) = be_u64(input)?;
543    let (input, ratio) = be_f64(input)?;
544    let (input, compression_speed) = be_f64(input)?;
545    let (input, decompression_speed) = be_f64(input)?;
546    let (input, compressed_blocks) = parse_vint_as_u64(input)?;
547
548    Ok((
549        input,
550        CompressionStatistics {
551            algorithm,
552            original_size,
553            compressed_size,
554            ratio,
555            compression_speed,
556            decompression_speed,
557            compressed_blocks,
558        },
559    ))
560}
561
562/// Parse additional metadata section
563pub fn parse_metadata_section(input: &[u8]) -> IResult<&[u8], HashMap<String, String>> {
564    let (input, metadata_count) = be_u32(input)?;
565    let mut metadata = HashMap::new();
566
567    let mut remaining = input;
568    for _ in 0..metadata_count {
569        let (next, key_len) = parse_vint_length_signed(remaining)?;
570        let (next, key_bytes) = take(key_len)(next)?;
571        let key = String::from_utf8_lossy(key_bytes).to_string();
572
573        let (next, value_len) = parse_vint_length_signed(next)?;
574        let (next, value_bytes) = take(value_len)(next)?;
575        let value = String::from_utf8_lossy(value_bytes).to_string();
576
577        metadata.insert(key, value);
578        remaining = next;
579    }
580
581    Ok((remaining, metadata))
582}
583
584/// Parse a row size histogram bucket
585pub fn parse_row_size_bucket(input: &[u8]) -> IResult<&[u8], RowSizeBucket> {
586    let (input, size_start) = parse_vint_as_u64(input)?;
587    let (input, size_end) = parse_vint_as_u64(input)?;
588    let (input, count) = parse_vint_as_u64(input)?;
589    let (input, percentage) = be_f64(input)?;
590
591    Ok((
592        input,
593        RowSizeBucket {
594            size_start,
595            size_end,
596            count,
597            percentage,
598        },
599    ))
600}
601
602/// Parse a partition size histogram bucket
603pub fn parse_partition_size_bucket(input: &[u8]) -> IResult<&[u8], PartitionSizeBucket> {
604    let (input, size_start) = parse_vint_as_u64(input)?;
605    let (input, size_end) = parse_vint_as_u64(input)?;
606    let (input, count) = parse_vint_as_u64(input)?;
607    let (input, cumulative_percentage) = be_f64(input)?;
608
609    Ok((
610        input,
611        PartitionSizeBucket {
612            size_start,
613            size_end,
614            count,
615            cumulative_percentage,
616        },
617    ))
618}
619
620/// Parse a value frequency entry
621pub fn parse_value_frequency(input: &[u8]) -> IResult<&[u8], ValueFrequency> {
622    let (input, value_len) = parse_vint_length_signed(input)?;
623    let (input, value_bytes) = take(value_len)(input)?;
624    let (input, frequency) = parse_vint_as_u64(input)?;
625    let (input, percentage) = be_f64(input)?;
626
627    Ok((
628        input,
629        ValueFrequency {
630            value: value_bytes.to_vec(),
631            frequency,
632            percentage,
633        },
634    ))
635}
636
637/// Helper function to parse VInt as u64
638fn parse_vint_as_u64(input: &[u8]) -> IResult<&[u8], u64> {
639    let (input, value) = parse_vint(input)?;
640    Ok((input, value as u64))
641}
642
643/// Whether `avg_rows_per_partition` holds a REAL derived value rather than the
644/// documented #1325 unavailable sentinel `0.0`.
645///
646/// The average is `total_rows / partition_count`; the parser leaves it `0.0`
647/// whenever EITHER `total_rows` is not authoritatively reachable from STATS OR
648/// `partition_count == 0`. So the value is only real when BOTH counts are
649/// positive. Single-sourced here and reused by every consumer (recommendations
650/// in this module and the report renderer in `statistics_reader`) to keep the
651/// availability condition from drifting. No heuristic (#28); see #1352.
652pub(crate) fn avg_rows_available(stats: &SSTableStatistics) -> bool {
653    stats.row_stats.total_rows > 0 && stats.row_stats.partition_count > 0
654}
655
656/// Statistics analyzer for enhanced reporting
657pub struct StatisticsAnalyzer;
658
659impl StatisticsAnalyzer {
660    /// Analyze statistics and generate human-readable summary
661    pub fn analyze(stats: &SSTableStatistics) -> StatisticsSummary {
662        let data_efficiency = Self::calculate_data_efficiency(stats);
663        let query_performance_hints = Self::generate_query_hints(stats);
664        let storage_recommendations = Self::generate_storage_recommendations(stats);
665        let health_score = Self::calculate_health_score(stats);
666
667        StatisticsSummary {
668            total_rows: stats.row_stats.total_rows,
669            // `None` when not authoritatively available; see `live_data_percentage`.
670            live_data_percentage: Self::live_data_percentage(stats),
671            compression_efficiency: stats.compression_stats.ratio * 100.0,
672            timestamp_range_days: Self::calculate_timestamp_range_days(stats),
673            largest_partition_mb: stats.partition_stats.max_partition_size as f64 / 1_048_576.0,
674            data_efficiency,
675            query_performance_hints,
676            storage_recommendations,
677            health_score,
678        }
679    }
680
681    /// Live-data percentage, or `None` when it is not authoritatively available.
682    ///
683    /// `live_rows == 0` is the documented #1325 sentinel meaning "not
684    /// authoritatively derivable from STATS" (STATS has no per-SSTable live-row
685    /// count), so we return `None` rather than a misleading concrete `0.00%`.
686    /// A genuinely fully-tombstoned SSTable also reads as unavailable here —
687    /// honest, since we cannot distinguish it from "unknown" until the
688    /// `RowStatistics` representation is redesigned (#1352). No heuristic (#28).
689    fn live_data_percentage(stats: &SSTableStatistics) -> Option<f64> {
690        if stats.row_stats.live_rows == 0 || stats.row_stats.total_rows == 0 {
691            return None;
692        }
693        Some((stats.row_stats.live_rows as f64 / stats.row_stats.total_rows as f64) * 100.0)
694    }
695
696    /// Overall data efficiency, or `None` when it is not authoritatively
697    /// derivable.
698    ///
699    /// Data efficiency blends the live-row ratio with compression and partition
700    /// efficiency, so it can only be computed when the live ratio is available.
701    /// `live_rows == 0` is the documented #1325 sentinel meaning "not
702    /// authoritatively available from STATS" (STATS has no per-SSTable live-row
703    /// count), and `total_rows == 0` would make the ratio `NaN`; in either case
704    /// we return `None` rather than a misleading concrete number. This reuses
705    /// the exact availability check of `live_data_percentage`. No heuristic
706    /// (#28); representation redesign is tracked by #1352.
707    fn calculate_data_efficiency(stats: &SSTableStatistics) -> Option<f64> {
708        if stats.row_stats.live_rows == 0 || stats.row_stats.total_rows == 0 {
709            return None;
710        }
711        let live_ratio = stats.row_stats.live_rows as f64 / stats.row_stats.total_rows as f64;
712        let compression_ratio = stats.compression_stats.ratio;
713        let partition_efficiency = 1.0 - (stats.partition_stats.large_partition_percentage / 100.0);
714
715        Some((live_ratio + compression_ratio + partition_efficiency) / 3.0 * 100.0)
716    }
717
718    fn generate_query_hints(stats: &SSTableStatistics) -> Vec<String> {
719        let mut hints = Vec::new();
720
721        if stats.partition_stats.large_partition_percentage > 10.0 {
722            hints.push("Consider reviewing partition key design - high percentage of large partitions detected".to_string());
723        }
724
725        // The "high tombstone ratio" hint compares tombstones against the live
726        // row count. When `live_rows == 0` that is the documented #1325 sentinel
727        // for "not authoritatively available from STATS" (not a measured zero),
728        // so `live_rows / 4 == 0` would make ANY tombstone trip the hint. Skip
729        // the hint entirely when the live count is unavailable rather than emit
730        // a misleading recommendation. No heuristic (#28); see #1352.
731        if stats.row_stats.live_rows > 0
732            && stats.row_stats.tombstone_count > stats.row_stats.live_rows / 4
733        {
734            hints.push("High tombstone ratio - consider running compaction".to_string());
735        }
736
737        if stats.table_stats.compression_ratio < 0.5 {
738            hints.push("Low compression ratio - data may not be well-suited for current compression algorithm".to_string());
739        }
740
741        hints
742    }
743
744    fn generate_storage_recommendations(stats: &SSTableStatistics) -> Vec<String> {
745        let mut recommendations = Vec::new();
746
747        if stats.table_stats.disk_size > 1_073_741_824 {
748            recommendations
749                .push("Large SSTable detected - consider more frequent compaction".to_string());
750        }
751
752        // `avg_rows_per_partition == 0.0` is the documented #1325 unavailable
753        // sentinel: the parser leaves it 0.0 whenever `total_rows` is not
754        // authoritatively reachable from STATS OR `partition_count == 0` (the
755        // average is `total_rows / partition_count`). Only emit the granularity
756        // recommendation when the average is a REAL derived value — i.e. BOTH
757        // `total_rows > 0` AND `partition_count > 0` (so the average was actually
758        // computed). Otherwise the sentinel `0.0 < 10.0` would always trip this
759        // hint (e.g. on nb SSTables whose gated walk can't reach `totalRows`, or
760        // when `partition_count == 0`), a misleading recommendation from a
761        // non-value. No heuristic (#28); see #1352.
762        if avg_rows_available(stats) && stats.row_stats.avg_rows_per_partition < 10.0 {
763            recommendations.push(
764                "Low average rows per partition - partition key may be too granular".to_string(),
765            );
766        }
767
768        recommendations
769    }
770
771    fn calculate_health_score(stats: &SSTableStatistics) -> f64 {
772        let mut score = 100.0;
773
774        // Deduct for high tombstone ratio. This derives from `total_rows` (an
775        // authoritative STATS count), NOT the #1325 `live_rows` unavailable
776        // sentinel, so it stays a concrete score. Guard `total_rows == 0` so the
777        // ratio does not become `NaN` (which would poison the whole score).
778        if stats.row_stats.total_rows > 0 {
779            let tombstone_ratio =
780                stats.row_stats.tombstone_count as f64 / stats.row_stats.total_rows as f64;
781            score -= tombstone_ratio * 30.0;
782        }
783
784        // Deduct for poor compression
785        if stats.compression_stats.ratio < 0.5 {
786            score -= 20.0;
787        }
788
789        // Deduct for large partitions
790        score -= stats.partition_stats.large_partition_percentage;
791
792        score.max(0.0)
793    }
794
795    fn calculate_timestamp_range_days(stats: &SSTableStatistics) -> f64 {
796        // Fail-closed (#1729): the enhanced parser marks an unavailable
797        // authoritative maxTimestamp with the `i64::MIN` sentinel. When the max
798        // is unavailable — or is somehow below the min — we cannot compute a
799        // real range, so report 0.0 rather than an underflowing/garbage span.
800        let max = stats.timestamp_stats.max_timestamp;
801        let min = stats.timestamp_stats.min_timestamp;
802        if max == i64::MIN || max < min {
803            return 0.0;
804        }
805        // `max >= min` here, so the difference is non-negative and cannot
806        // overflow i64 for realistic timestamps; use a checked subtraction to
807        // stay defensive against pathological inputs.
808        let range_micros = max.saturating_sub(min);
809        range_micros as f64 / (1_000_000.0 * 60.0 * 60.0 * 24.0)
810    }
811}
812
813/// Human-readable statistics summary
814#[derive(Debug, Clone, Serialize, Deserialize)]
815pub struct StatisticsSummary {
816    pub total_rows: u64,
817    /// Percentage of live (non-tombstoned) data, or `None` when not
818    /// authoritatively available (the #1325 `live_rows == 0` sentinel; see
819    /// `StatisticsAnalyzer::live_data_percentage` and #1352).
820    pub live_data_percentage: Option<f64>,
821    pub compression_efficiency: f64,
822    pub timestamp_range_days: f64,
823    pub largest_partition_mb: f64,
824    /// Blended data-efficiency score, or `None` when the live-row ratio is not
825    /// authoritatively available (the #1325 `live_rows == 0` sentinel, or
826    /// `total_rows == 0`); see `StatisticsAnalyzer::calculate_data_efficiency`
827    /// and #1352.
828    pub data_efficiency: Option<f64>,
829    pub query_performance_hints: Vec<String>,
830    pub storage_recommendations: Vec<String>,
831    pub health_score: f64,
832}
833
834/// Serialize Statistics structure to bytes (for testing and validation)
835pub fn serialize_statistics(_stats: &SSTableStatistics) -> Result<Vec<u8>> {
836    // This would implement the reverse of parsing for complete round-trip testing
837    // For now, return an error indicating this is not implemented
838    Err(Error::corruption(
839        "Statistics serialization not yet implemented",
840    ))
841}
842
843#[cfg(test)]
844mod tests {
845    use super::*;
846
847    #[test]
848    fn test_statistics_header_parsing() {
849        let test_data = vec![
850            0x00, 0x00, 0x00, 0x01, // version = 1
851            // table_id (16 bytes)
852            0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
853            0x0F, 0x10, 0x00, 0x00, 0x00, 0x05, // section_count = 5
854            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, // file_size = 4096
855            0x12, 0x34, 0x56, 0x78, // checksum
856        ];
857
858        let result = parse_statistics_header(&test_data);
859        assert!(result.is_ok());
860
861        let (_, header) = result.unwrap();
862        assert_eq!(header.version, 1);
863        // assert_eq!(header.section_count, 5); // Field not available
864        // assert_eq!(header.file_size, 4096); // Field not available
865        assert_eq!(header.checksum, 0x12345678);
866    }
867
868    #[test]
869    fn test_nb_format_authoritative_detection() {
870        // nb-format (version 4) - should parse as nb-format
871        let nb_data = vec![
872            0x00, 0x00, 0x00, 0x04, // version = 4 (authoritative nb-format marker)
873            0x26, 0x29, 0x1b, 0x05, // statistics_kind
874            0x00, 0x00, 0x00, 0x00, // reserved
875            0x00, 0x00, 0x00, 0x2c, // data_length = 44
876            0x00, 0x00, 0x00, 0x01, // metadata1 = 1
877            0x00, 0x00, 0x00, 0x65, // metadata2 = 101
878            0x00, 0x00, 0x00, 0x02, // metadata3 = 2
879            0x00, 0x00, 0x14, 0xd4, // checksum = 5332
880        ];
881
882        let result = parse_statistics_header(&nb_data);
883        assert!(result.is_ok());
884
885        let (_, header) = result.unwrap();
886        assert_eq!(header.version, 4);
887        assert_eq!(header.statistics_kind, 0x26291b05);
888        assert_eq!(header.data_length, 44);
889        assert!(header.table_id.is_none()); // nb-format has no table_id
890    }
891
892    #[test]
893    fn test_legacy_format_authoritative_detection() {
894        // Legacy format (version 2) - should parse as legacy
895        let legacy_data = vec![
896            0x00, 0x00, 0x00, 0x02, // version = 2 (authoritative legacy marker)
897            // table_id (16 bytes)
898            0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE,
899            0xFF, 0x00, 0x00, 0x00, 0x00, 0x0A, // section_count = 10
900            0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, // file_size = 65536
901            0xAB, 0xCD, 0xEF, 0x12, // checksum
902        ];
903
904        let result = parse_statistics_header(&legacy_data);
905        assert!(result.is_ok());
906
907        let (_, header) = result.unwrap();
908        assert_eq!(header.version, 2);
909        assert_eq!(header.statistics_kind, 0); // legacy format doesn't use this
910        assert!(header.table_id.is_some()); // legacy format has table_id
911    }
912
913    #[test]
914    fn test_unsupported_version_rejection() {
915        // Version 0 - should be rejected
916        let invalid_v0 = vec![
917            0x00, 0x00, 0x00, 0x00, // version = 0 (invalid)
918            0x00, 0x00, 0x00, 0x00, // ...rest doesn't matter
919        ];
920        assert!(parse_statistics_header(&invalid_v0).is_err());
921
922        // Version 5 - should be rejected (future/unknown version)
923        let invalid_v5 = vec![
924            0x00, 0x00, 0x00, 0x05, // version = 5 (unsupported)
925            0x00, 0x00, 0x00, 0x00, // ...rest doesn't matter
926        ];
927        assert!(parse_statistics_header(&invalid_v5).is_err());
928
929        // Version 255 - should be rejected
930        let invalid_v255 = vec![
931            0x00, 0x00, 0x00, 0xFF, // version = 255 (unsupported)
932            0x00, 0x00, 0x00, 0x00, // ...rest doesn't matter
933        ];
934        assert!(parse_statistics_header(&invalid_v255).is_err());
935    }
936
937    #[test]
938    fn test_no_heuristics_version_4_with_short_input() {
939        // Previous implementation used heuristic: version == 4 && input.len() >= 28
940        // New implementation uses ONLY version number - no length check heuristic
941        // This test ensures we don't fall back to legacy parsing with short input
942
943        let short_nb_data = vec![
944            0x00, 0x00, 0x00, 0x04, // version = 4 (authoritative nb-format)
945            0x26, 0x29, 0x1b, 0x05, // statistics_kind
946            0x00, 0x00, 0x00, 0x00, // reserved
947            0x00, 0x00, 0x00,
948            0x2c, // data_length = 44
949                  // Missing remaining fields - should fail parsing, not switch formats
950        ];
951
952        let result = parse_statistics_header(&short_nb_data);
953        // Should fail because version 4 DEFINITIVELY means nb-format
954        // and nb-format requires 32 bytes. This is NOT a heuristic,
955        // it's the authoritative format specification.
956        assert!(result.is_err());
957    }
958
959    #[test]
960    fn test_statistics_analyzer() {
961        let stats = create_test_statistics();
962        let summary = StatisticsAnalyzer::analyze(&stats);
963
964        assert!(summary.total_rows > 0);
965        assert!(summary.health_score >= 0.0 && summary.health_score <= 100.0);
966        let live_pct = summary
967            .live_data_percentage
968            .expect("fixture has authoritative live_rows > 0");
969        assert!((0.0..=100.0).contains(&live_pct));
970    }
971
972    /// #1325 roborev finding: when `live_rows` is the documented "not
973    /// authoritatively available from STATS" sentinel (0) but `total_rows` is a
974    /// real authoritative count, the analyzer MUST report live-data% as
975    /// unavailable (`None`) rather than a misleading concrete `0.00%`. Other
976    /// authoritative fields (`total_rows`) stay intact. See #1352.
977    #[test]
978    fn test_live_data_percentage_unavailable_when_live_rows_sentinel() {
979        let mut stats = create_test_statistics();
980        stats.row_stats.total_rows = 1000; // real authoritative count
981        stats.row_stats.live_rows = 0; // documented "unavailable" sentinel
982
983        let summary = StatisticsAnalyzer::analyze(&stats);
984
985        assert_eq!(
986            summary.live_data_percentage, None,
987            "live_rows==0 sentinel with total_rows>0 must report None, not 0.00%"
988        );
989        // Data efficiency is derived from the live ratio, so it must be
990        // unavailable under the same sentinel (not an artificially low number).
991        assert_eq!(
992            summary.data_efficiency, None,
993            "data_efficiency must be None when live_rows is the unavailable sentinel"
994        );
995        // Authoritative counts are unaffected.
996        assert_eq!(summary.total_rows, 1000);
997        // Health score derives from total_rows (authoritative), not live_rows,
998        // so it stays a concrete, finite value.
999        assert!(
1000            summary.health_score.is_finite(),
1001            "health_score must stay finite when live_rows is the sentinel"
1002        );
1003    }
1004
1005    /// #1325 sweep: the "high tombstone ratio" query hint compares tombstones
1006    /// against `live_rows`. When `live_rows == 0` (unavailable sentinel), the
1007    /// old `live_rows / 4 == 0` comparison would trip the hint for ANY
1008    /// tombstone. It must be suppressed instead. No heuristic (#28); see #1352.
1009    #[test]
1010    fn test_query_hint_suppressed_when_live_rows_sentinel() {
1011        let mut stats = create_test_statistics();
1012        stats.row_stats.total_rows = 1000;
1013        stats.row_stats.live_rows = 0; // unavailable sentinel
1014        stats.row_stats.tombstone_count = 500; // would trip vs. live_rows/4 == 0
1015
1016        let summary = StatisticsAnalyzer::analyze(&stats);
1017
1018        assert!(
1019            !summary
1020                .query_performance_hints
1021                .iter()
1022                .any(|h| h.contains("tombstone")),
1023            "tombstone hint must be suppressed when live_rows is the unavailable sentinel"
1024        );
1025    }
1026
1027    /// #1325 sweep: when `live_rows` is a real count, the tombstone hint and
1028    /// data-efficiency stay concrete (non-vacuous positive path).
1029    #[test]
1030    fn test_derived_metrics_available_when_live_rows_present() {
1031        let mut stats = create_test_statistics();
1032        stats.row_stats.total_rows = 1000;
1033        stats.row_stats.live_rows = 100; // real count; live_rows/4 == 25
1034        stats.row_stats.tombstone_count = 500; // 500 > 25 -> hint fires
1035
1036        let summary = StatisticsAnalyzer::analyze(&stats);
1037
1038        assert!(
1039            summary.live_data_percentage.is_some(),
1040            "live_data_percentage must be Some when live_rows > 0"
1041        );
1042        let eff = summary
1043            .data_efficiency
1044            .expect("data_efficiency must be Some when live_rows > 0");
1045        assert!(eff.is_finite());
1046        assert!(
1047            summary
1048                .query_performance_hints
1049                .iter()
1050                .any(|h| h.contains("tombstone")),
1051            "tombstone hint must fire when tombstones exceed live_rows/4"
1052        );
1053    }
1054
1055    /// #1325 sweep: health score must not become `NaN` when `total_rows == 0`
1056    /// (guard the div-by-zero in the tombstone-ratio deduction).
1057    #[test]
1058    fn test_health_score_finite_when_total_rows_zero() {
1059        let mut stats = create_test_statistics();
1060        stats.row_stats.total_rows = 0;
1061        stats.row_stats.live_rows = 0;
1062        stats.row_stats.tombstone_count = 0;
1063
1064        let summary = StatisticsAnalyzer::analyze(&stats);
1065
1066        assert!(
1067            summary.health_score.is_finite(),
1068            "health_score must be finite (not NaN) when total_rows == 0"
1069        );
1070        assert!((0.0..=100.0).contains(&summary.health_score));
1071    }
1072
1073    /// #1325 roborev finding: the "partition key may be too granular" storage
1074    /// recommendation compares `avg_rows_per_partition < 10.0`. When
1075    /// `total_rows == 0` the parser leaves `avg_rows_per_partition == 0.0` as the
1076    /// documented unavailable sentinel, so `0.0 < 10.0` would ALWAYS trip the
1077    /// recommendation on nb SSTables whose gated walk cannot reach `totalRows`.
1078    /// It must be suppressed. No heuristic (#28); see #1352.
1079    #[test]
1080    fn test_granularity_recommendation_suppressed_when_avg_rows_sentinel() {
1081        let mut stats = create_test_statistics();
1082        // Unavailable sentinel: no authoritative total_rows -> avg left 0.0.
1083        stats.row_stats.total_rows = 0;
1084        stats.row_stats.avg_rows_per_partition = 0.0;
1085        // Keep disk_size small so the "large SSTable" recommendation does not fire.
1086        stats.table_stats.disk_size = 1024;
1087
1088        let summary = StatisticsAnalyzer::analyze(&stats);
1089
1090        assert!(
1091            !summary
1092                .storage_recommendations
1093                .iter()
1094                .any(|r| r.contains("too granular")),
1095            "granularity recommendation must be suppressed when avg_rows_per_partition \
1096             is the unavailable sentinel (total_rows == 0)"
1097        );
1098    }
1099
1100    /// #1325 sweep (non-vacuous positive path): with a REAL low average
1101    /// (total_rows > 0), the granularity recommendation still fires.
1102    #[test]
1103    fn test_granularity_recommendation_fires_when_avg_rows_real_and_low() {
1104        let mut stats = create_test_statistics();
1105        stats.row_stats.total_rows = 8; // real authoritative count
1106        stats.row_stats.partition_count = 4;
1107        stats.row_stats.avg_rows_per_partition = 2.0; // real, genuinely low (< 10)
1108        stats.table_stats.disk_size = 1024;
1109
1110        let summary = StatisticsAnalyzer::analyze(&stats);
1111
1112        assert!(
1113            summary
1114                .storage_recommendations
1115                .iter()
1116                .any(|r| r.contains("too granular")),
1117            "granularity recommendation must fire for a REAL low avg_rows_per_partition"
1118        );
1119    }
1120
1121    /// #1325 sweep (non-vacuous): with a real HIGH average the recommendation
1122    /// does NOT fire — proving the guard did not just always-suppress.
1123    #[test]
1124    fn test_granularity_recommendation_absent_when_avg_rows_real_and_high() {
1125        let mut stats = create_test_statistics();
1126        stats.row_stats.total_rows = 1000; // real; default avg is 20.0 (>= 10)
1127        stats.table_stats.disk_size = 1024;
1128
1129        let summary = StatisticsAnalyzer::analyze(&stats);
1130
1131        assert!(
1132            !summary
1133                .storage_recommendations
1134                .iter()
1135                .any(|r| r.contains("too granular")),
1136            "granularity recommendation must NOT fire for a REAL high avg_rows_per_partition"
1137        );
1138    }
1139
1140    /// #1374 roborev finding: `avg_rows_per_partition` is `total_rows /
1141    /// partition_count`, so the `0.0` unavailable sentinel also occurs when
1142    /// `total_rows > 0` but `partition_count == 0`. The availability guard must
1143    /// require BOTH counts positive; a stats object with real `total_rows` but
1144    /// zero partitions must still SUPPRESS the "too granular" recommendation.
1145    #[test]
1146    fn test_granularity_recommendation_suppressed_when_partition_count_zero() {
1147        let mut stats = create_test_statistics();
1148        stats.row_stats.total_rows = 8; // real authoritative count
1149        stats.row_stats.partition_count = 0; // but no partitions -> avg is sentinel
1150        stats.row_stats.avg_rows_per_partition = 0.0;
1151        stats.table_stats.disk_size = 1024;
1152
1153        // The shared availability helper must report unavailable.
1154        assert!(
1155            !avg_rows_available(&stats),
1156            "avg_rows must be unavailable when partition_count == 0 despite total_rows > 0"
1157        );
1158
1159        let summary = StatisticsAnalyzer::analyze(&stats);
1160
1161        assert!(
1162            !summary
1163                .storage_recommendations
1164                .iter()
1165                .any(|r| r.contains("too granular")),
1166            "granularity recommendation must be suppressed when partition_count == 0 \
1167             (avg_rows_per_partition is the unavailable sentinel)"
1168        );
1169    }
1170
1171    #[test]
1172    fn test_parse_row_statistics() {
1173        use super::super::vint::encode_vint;
1174
1175        let mut data = Vec::new();
1176        // total_rows: VInt
1177        data.extend_from_slice(&encode_vint(1000));
1178        // live_rows: VInt
1179        data.extend_from_slice(&encode_vint(900));
1180        // tombstone_count: VInt
1181        data.extend_from_slice(&encode_vint(100));
1182        // partition_count: VInt
1183        data.extend_from_slice(&encode_vint(50));
1184        // avg_rows_per_partition: f64
1185        data.extend_from_slice(&20.0f64.to_be_bytes());
1186        // histogram_count: u32
1187        data.extend_from_slice(&0u32.to_be_bytes());
1188
1189        let result = parse_row_statistics(&data);
1190        assert!(result.is_ok());
1191
1192        let (remaining, row_stats) = result.unwrap();
1193        assert!(remaining.is_empty());
1194        assert_eq!(row_stats.total_rows, 1000);
1195        assert_eq!(row_stats.live_rows, 900);
1196        assert_eq!(row_stats.tombstone_count, 100);
1197        assert_eq!(row_stats.partition_count, 50);
1198        assert_eq!(row_stats.avg_rows_per_partition, 20.0);
1199        assert_eq!(row_stats.row_size_histogram.len(), 0);
1200    }
1201
1202    #[test]
1203    fn test_parse_row_statistics_with_histogram() {
1204        use super::super::vint::encode_vint;
1205
1206        let mut data = Vec::new();
1207        data.extend_from_slice(&encode_vint(1000));
1208        data.extend_from_slice(&encode_vint(900));
1209        data.extend_from_slice(&encode_vint(100));
1210        data.extend_from_slice(&encode_vint(50));
1211        data.extend_from_slice(&20.0f64.to_be_bytes());
1212        // histogram_count: 2 buckets
1213        data.extend_from_slice(&2u32.to_be_bytes());
1214        // Bucket 1
1215        data.extend_from_slice(&encode_vint(0)); // size_start
1216        data.extend_from_slice(&encode_vint(1024)); // size_end
1217        data.extend_from_slice(&encode_vint(500)); // count
1218        data.extend_from_slice(&50.0f64.to_be_bytes()); // percentage
1219                                                        // Bucket 2
1220        data.extend_from_slice(&encode_vint(1024)); // size_start
1221        data.extend_from_slice(&encode_vint(10240)); // size_end
1222        data.extend_from_slice(&encode_vint(500)); // count
1223        data.extend_from_slice(&50.0f64.to_be_bytes()); // percentage
1224
1225        let result = parse_row_statistics(&data);
1226        assert!(result.is_ok());
1227
1228        let (_, row_stats) = result.unwrap();
1229        assert_eq!(row_stats.row_size_histogram.len(), 2);
1230        assert_eq!(row_stats.row_size_histogram[0].size_start, 0);
1231        assert_eq!(row_stats.row_size_histogram[0].size_end, 1024);
1232        assert_eq!(row_stats.row_size_histogram[0].count, 500);
1233        assert_eq!(row_stats.row_size_histogram[0].percentage, 50.0);
1234    }
1235
1236    #[test]
1237    fn test_parse_timestamp_statistics_no_ttl() {
1238        let mut data = Vec::new();
1239        data.extend_from_slice(&1000000i64.to_be_bytes()); // min_timestamp
1240        data.extend_from_slice(&2000000i64.to_be_bytes()); // max_timestamp
1241        data.extend_from_slice(&0i64.to_be_bytes()); // min_deletion_time
1242        data.extend_from_slice(&0i64.to_be_bytes()); // max_deletion_time
1243        data.push(0); // has_ttl = false
1244
1245        let result = parse_timestamp_statistics(&data);
1246        assert!(result.is_ok());
1247
1248        let (remaining, ts_stats) = result.unwrap();
1249        assert!(remaining.is_empty());
1250        assert_eq!(ts_stats.min_timestamp, 1000000);
1251        assert_eq!(ts_stats.max_timestamp, 2000000);
1252        assert_eq!(ts_stats.min_deletion_time, 0);
1253        assert_eq!(ts_stats.max_deletion_time, 0);
1254        assert!(ts_stats.min_ttl.is_none());
1255        assert!(ts_stats.max_ttl.is_none());
1256        assert_eq!(ts_stats.rows_with_ttl, 0);
1257    }
1258
1259    #[test]
1260    fn test_parse_timestamp_statistics_with_ttl() {
1261        use super::super::vint::encode_vint;
1262
1263        let mut data = Vec::new();
1264        data.extend_from_slice(&1000000i64.to_be_bytes());
1265        data.extend_from_slice(&2000000i64.to_be_bytes());
1266        data.extend_from_slice(&0i64.to_be_bytes());
1267        data.extend_from_slice(&0i64.to_be_bytes());
1268        data.push(1); // has_ttl = true
1269        data.extend_from_slice(&3600i64.to_be_bytes()); // min_ttl
1270        data.extend_from_slice(&86400i64.to_be_bytes()); // max_ttl
1271        data.extend_from_slice(&encode_vint(250)); // rows_with_ttl
1272
1273        let result = parse_timestamp_statistics(&data);
1274        assert!(result.is_ok());
1275
1276        let (_, ts_stats) = result.unwrap();
1277        assert_eq!(ts_stats.min_ttl, Some(3600));
1278        assert_eq!(ts_stats.max_ttl, Some(86400));
1279        assert_eq!(ts_stats.rows_with_ttl, 250);
1280    }
1281
1282    #[test]
1283    fn test_parse_column_statistics_empty() {
1284        let data = Vec::new();
1285        let result = parse_column_statistics(&data, 0);
1286        assert!(result.is_ok());
1287
1288        let (remaining, col_stats) = result.unwrap();
1289        assert!(remaining.is_empty());
1290        assert_eq!(col_stats.len(), 0);
1291    }
1292
1293    #[test]
1294    fn test_parse_column_statistics_single_column() {
1295        use super::super::vint::encode_vint;
1296
1297        let mut data = Vec::new();
1298        // Column name
1299        let name = b"user_id";
1300        data.extend_from_slice(&encode_vint(name.len() as i64));
1301        data.extend_from_slice(name);
1302        // Column type
1303        let col_type = b"int";
1304        data.extend_from_slice(&encode_vint(col_type.len() as i64));
1305        data.extend_from_slice(col_type);
1306        // value_count
1307        data.extend_from_slice(&encode_vint(1000));
1308        // null_count
1309        data.extend_from_slice(&encode_vint(0));
1310        // has_min_max = false
1311        data.push(0);
1312        // avg_size
1313        data.extend_from_slice(&4.0f64.to_be_bytes());
1314        // cardinality
1315        data.extend_from_slice(&encode_vint(500));
1316        // histogram_count
1317        data.extend_from_slice(&0u32.to_be_bytes());
1318        // has_index
1319        data.push(1);
1320
1321        let result = parse_column_statistics(&data, 1);
1322        assert!(result.is_ok());
1323
1324        let (_, col_stats) = result.unwrap();
1325        assert_eq!(col_stats.len(), 1);
1326        assert_eq!(col_stats[0].name, "user_id");
1327        assert_eq!(col_stats[0].column_type, "int");
1328        assert_eq!(col_stats[0].value_count, 1000);
1329        assert_eq!(col_stats[0].null_count, 0);
1330        assert!(col_stats[0].min_value.is_none());
1331        assert!(col_stats[0].max_value.is_none());
1332        assert_eq!(col_stats[0].avg_size, 4.0);
1333        assert_eq!(col_stats[0].cardinality, 500);
1334        assert_eq!(col_stats[0].value_histogram.len(), 0);
1335        assert!(col_stats[0].has_index);
1336    }
1337
1338    #[test]
1339    fn test_parse_column_statistics_with_min_max() {
1340        use super::super::vint::encode_vint;
1341
1342        let mut data = Vec::new();
1343        let name = b"score";
1344        data.extend_from_slice(&encode_vint(name.len() as i64));
1345        data.extend_from_slice(name);
1346        let col_type = b"int";
1347        data.extend_from_slice(&encode_vint(col_type.len() as i64));
1348        data.extend_from_slice(col_type);
1349        data.extend_from_slice(&encode_vint(500));
1350        data.extend_from_slice(&encode_vint(10));
1351        // has_min_max = true
1352        data.push(1);
1353        // min_value
1354        let min_val = vec![0x00, 0x00, 0x00, 0x01];
1355        data.extend_from_slice(&encode_vint(min_val.len() as i64));
1356        data.extend_from_slice(&min_val);
1357        // max_value
1358        let max_val = vec![0x00, 0x00, 0x03, 0xE8];
1359        data.extend_from_slice(&encode_vint(max_val.len() as i64));
1360        data.extend_from_slice(&max_val);
1361        data.extend_from_slice(&4.0f64.to_be_bytes());
1362        data.extend_from_slice(&encode_vint(400));
1363        data.extend_from_slice(&0u32.to_be_bytes());
1364        data.push(0);
1365
1366        let result = parse_column_statistics(&data, 1);
1367        assert!(result.is_ok());
1368
1369        let (_, col_stats) = result.unwrap();
1370        assert_eq!(col_stats.len(), 1);
1371        assert!(col_stats[0].min_value.is_some());
1372        assert!(col_stats[0].max_value.is_some());
1373        assert_eq!(col_stats[0].min_value.as_ref().unwrap(), &min_val);
1374        assert_eq!(col_stats[0].max_value.as_ref().unwrap(), &max_val);
1375    }
1376
1377    #[test]
1378    fn test_parse_table_statistics() {
1379        use super::super::vint::encode_vint;
1380
1381        let mut data = Vec::new();
1382        data.extend_from_slice(&(1024 * 1024u64).to_be_bytes()); // disk_size
1383        data.extend_from_slice(&(2048 * 1024u64).to_be_bytes()); // uncompressed_size
1384        data.extend_from_slice(&0.5f64.to_be_bytes()); // compression_ratio
1385        data.extend_from_slice(&encode_vint(100)); // block_count
1386        data.extend_from_slice(&1024.0f64.to_be_bytes()); // avg_block_size
1387        data.extend_from_slice(&1024u64.to_be_bytes()); // index_size
1388        data.extend_from_slice(&512u64.to_be_bytes()); // bloom_filter_size
1389        data.extend_from_slice(&1u32.to_be_bytes()); // level_count
1390
1391        let result = parse_table_statistics(&data);
1392        assert!(result.is_ok());
1393
1394        let (remaining, table_stats) = result.unwrap();
1395        assert!(remaining.is_empty());
1396        assert_eq!(table_stats.disk_size, 1024 * 1024);
1397        assert_eq!(table_stats.uncompressed_size, 2048 * 1024);
1398        assert_eq!(table_stats.compression_ratio, 0.5);
1399        assert_eq!(table_stats.block_count, 100);
1400        assert_eq!(table_stats.avg_block_size, 1024.0);
1401        assert_eq!(table_stats.index_size, 1024);
1402        assert_eq!(table_stats.bloom_filter_size, 512);
1403        assert_eq!(table_stats.level_count, 1);
1404    }
1405
1406    #[test]
1407    fn test_parse_partition_statistics() {
1408        let mut data = Vec::new();
1409        data.extend_from_slice(&20480.0f64.to_be_bytes()); // avg_partition_size
1410        data.extend_from_slice(&1024u64.to_be_bytes()); // min_partition_size
1411        data.extend_from_slice(&1048576u64.to_be_bytes()); // max_partition_size
1412        data.extend_from_slice(&5.0f64.to_be_bytes()); // large_partition_percentage
1413        data.extend_from_slice(&0u32.to_be_bytes()); // histogram_count
1414
1415        let result = parse_partition_statistics(&data);
1416        assert!(result.is_ok());
1417
1418        let (remaining, part_stats) = result.unwrap();
1419        assert!(remaining.is_empty());
1420        assert_eq!(part_stats.avg_partition_size, 20480.0);
1421        assert_eq!(part_stats.min_partition_size, 1024);
1422        assert_eq!(part_stats.max_partition_size, 1048576);
1423        assert_eq!(part_stats.large_partition_percentage, 5.0);
1424        assert_eq!(part_stats.size_histogram.len(), 0);
1425    }
1426
1427    #[test]
1428    fn test_parse_partition_statistics_with_histogram() {
1429        use super::super::vint::encode_vint;
1430
1431        let mut data = Vec::new();
1432        data.extend_from_slice(&20480.0f64.to_be_bytes());
1433        data.extend_from_slice(&1024u64.to_be_bytes());
1434        data.extend_from_slice(&1048576u64.to_be_bytes());
1435        data.extend_from_slice(&5.0f64.to_be_bytes());
1436        // histogram_count: 2 buckets
1437        data.extend_from_slice(&2u32.to_be_bytes());
1438        // Bucket 1
1439        data.extend_from_slice(&encode_vint(0));
1440        data.extend_from_slice(&encode_vint(10240));
1441        data.extend_from_slice(&encode_vint(30));
1442        data.extend_from_slice(&60.0f64.to_be_bytes()); // cumulative_percentage
1443                                                        // Bucket 2
1444        data.extend_from_slice(&encode_vint(10240));
1445        data.extend_from_slice(&encode_vint(1048576));
1446        data.extend_from_slice(&encode_vint(20));
1447        data.extend_from_slice(&100.0f64.to_be_bytes());
1448
1449        let result = parse_partition_statistics(&data);
1450        assert!(result.is_ok());
1451
1452        let (_, part_stats) = result.unwrap();
1453        assert_eq!(part_stats.size_histogram.len(), 2);
1454        assert_eq!(part_stats.size_histogram[0].size_start, 0);
1455        assert_eq!(part_stats.size_histogram[0].cumulative_percentage, 60.0);
1456        assert_eq!(part_stats.size_histogram[1].cumulative_percentage, 100.0);
1457    }
1458
1459    #[test]
1460    fn test_parse_compression_statistics() {
1461        use super::super::vint::encode_vint;
1462
1463        let mut data = Vec::new();
1464        // algorithm
1465        let algo = b"LZ4";
1466        data.extend_from_slice(&encode_vint(algo.len() as i64));
1467        data.extend_from_slice(algo);
1468        // original_size
1469        data.extend_from_slice(&(2048 * 1024u64).to_be_bytes());
1470        // compressed_size
1471        data.extend_from_slice(&(1024 * 1024u64).to_be_bytes());
1472        // ratio
1473        data.extend_from_slice(&0.5f64.to_be_bytes());
1474        // compression_speed
1475        data.extend_from_slice(&100.0f64.to_be_bytes());
1476        // decompression_speed
1477        data.extend_from_slice(&200.0f64.to_be_bytes());
1478        // compressed_blocks
1479        data.extend_from_slice(&encode_vint(100));
1480
1481        let result = parse_compression_statistics(&data);
1482        assert!(result.is_ok());
1483
1484        let (remaining, comp_stats) = result.unwrap();
1485        assert!(remaining.is_empty());
1486        assert_eq!(comp_stats.algorithm, "LZ4");
1487        assert_eq!(comp_stats.original_size, 2048 * 1024);
1488        assert_eq!(comp_stats.compressed_size, 1024 * 1024);
1489        assert_eq!(comp_stats.ratio, 0.5);
1490        assert_eq!(comp_stats.compression_speed, 100.0);
1491        assert_eq!(comp_stats.decompression_speed, 200.0);
1492        assert_eq!(comp_stats.compressed_blocks, 100);
1493    }
1494
1495    #[test]
1496    fn test_parse_compression_statistics_different_algorithms() {
1497        use super::super::vint::encode_vint;
1498
1499        for algorithm in &["LZ4", "Snappy", "Deflate", "Zstd"] {
1500            let mut data = Vec::new();
1501            data.extend_from_slice(&encode_vint(algorithm.len() as i64));
1502            data.extend_from_slice(algorithm.as_bytes());
1503            data.extend_from_slice(&(1000000u64).to_be_bytes());
1504            data.extend_from_slice(&(500000u64).to_be_bytes());
1505            data.extend_from_slice(&0.5f64.to_be_bytes());
1506            data.extend_from_slice(&100.0f64.to_be_bytes());
1507            data.extend_from_slice(&200.0f64.to_be_bytes());
1508            data.extend_from_slice(&encode_vint(50));
1509
1510            let result = parse_compression_statistics(&data);
1511            assert!(result.is_ok());
1512            let (_, comp_stats) = result.unwrap();
1513            assert_eq!(comp_stats.algorithm, *algorithm);
1514        }
1515    }
1516
1517    #[test]
1518    fn test_parse_metadata_section_empty() {
1519        let mut data = Vec::new();
1520        data.extend_from_slice(&0u32.to_be_bytes()); // metadata_count = 0
1521
1522        let result = parse_metadata_section(&data);
1523        assert!(result.is_ok());
1524
1525        let (remaining, metadata) = result.unwrap();
1526        assert!(remaining.is_empty());
1527        assert_eq!(metadata.len(), 0);
1528    }
1529
1530    #[test]
1531    fn test_parse_metadata_section_with_entries() {
1532        use super::super::vint::encode_vint;
1533
1534        let mut data = Vec::new();
1535        data.extend_from_slice(&2u32.to_be_bytes()); // metadata_count = 2
1536
1537        // Entry 1: "compaction_strategy" -> "LeveledCompactionStrategy"
1538        let key1 = b"compaction_strategy";
1539        data.extend_from_slice(&encode_vint(key1.len() as i64));
1540        data.extend_from_slice(key1);
1541        let val1 = b"LeveledCompactionStrategy";
1542        data.extend_from_slice(&encode_vint(val1.len() as i64));
1543        data.extend_from_slice(val1);
1544
1545        // Entry 2: "sstable_format" -> "nb"
1546        let key2 = b"sstable_format";
1547        data.extend_from_slice(&encode_vint(key2.len() as i64));
1548        data.extend_from_slice(key2);
1549        let val2 = b"nb";
1550        data.extend_from_slice(&encode_vint(val2.len() as i64));
1551        data.extend_from_slice(val2);
1552
1553        let result = parse_metadata_section(&data);
1554        assert!(result.is_ok());
1555
1556        let (remaining, metadata) = result.unwrap();
1557        assert!(remaining.is_empty());
1558        assert_eq!(metadata.len(), 2);
1559        assert_eq!(
1560            metadata.get("compaction_strategy"),
1561            Some(&"LeveledCompactionStrategy".to_string())
1562        );
1563        assert_eq!(metadata.get("sstable_format"), Some(&"nb".to_string()));
1564    }
1565
1566    #[test]
1567    fn test_parse_statistics_file() {
1568        use super::super::vint::encode_vint;
1569
1570        let mut data = Vec::new();
1571
1572        // Header (legacy format)
1573        data.extend_from_slice(&1u32.to_be_bytes()); // version
1574        data.extend_from_slice(&[1u8; 16]); // table_id
1575        data.extend_from_slice(&0u32.to_be_bytes()); // section_count
1576        data.extend_from_slice(&4096u64.to_be_bytes()); // file_size
1577        data.extend_from_slice(&0x12345678u32.to_be_bytes()); // checksum
1578
1579        // Row statistics
1580        data.extend_from_slice(&encode_vint(1000));
1581        data.extend_from_slice(&encode_vint(900));
1582        data.extend_from_slice(&encode_vint(100));
1583        data.extend_from_slice(&encode_vint(50));
1584        data.extend_from_slice(&20.0f64.to_be_bytes());
1585        data.extend_from_slice(&0u32.to_be_bytes()); // histogram_count
1586
1587        // Timestamp statistics
1588        data.extend_from_slice(&1000000i64.to_be_bytes());
1589        data.extend_from_slice(&2000000i64.to_be_bytes());
1590        data.extend_from_slice(&0i64.to_be_bytes());
1591        data.extend_from_slice(&0i64.to_be_bytes());
1592        data.push(0); // has_ttl = false
1593
1594        // Column statistics (0 columns)
1595        // (no data needed since column_count from header.data_length is 0)
1596
1597        // Table statistics
1598        data.extend_from_slice(&(1024 * 1024u64).to_be_bytes());
1599        data.extend_from_slice(&(2048 * 1024u64).to_be_bytes());
1600        data.extend_from_slice(&0.5f64.to_be_bytes());
1601        data.extend_from_slice(&encode_vint(100));
1602        data.extend_from_slice(&1024.0f64.to_be_bytes());
1603        data.extend_from_slice(&1024u64.to_be_bytes());
1604        data.extend_from_slice(&512u64.to_be_bytes());
1605        data.extend_from_slice(&1u32.to_be_bytes());
1606
1607        // Partition statistics
1608        data.extend_from_slice(&20480.0f64.to_be_bytes());
1609        data.extend_from_slice(&1024u64.to_be_bytes());
1610        data.extend_from_slice(&1048576u64.to_be_bytes());
1611        data.extend_from_slice(&5.0f64.to_be_bytes());
1612        data.extend_from_slice(&0u32.to_be_bytes()); // histogram_count
1613
1614        // Compression statistics
1615        let algo = b"LZ4";
1616        data.extend_from_slice(&encode_vint(algo.len() as i64));
1617        data.extend_from_slice(algo);
1618        data.extend_from_slice(&(2048 * 1024u64).to_be_bytes());
1619        data.extend_from_slice(&(1024 * 1024u64).to_be_bytes());
1620        data.extend_from_slice(&0.5f64.to_be_bytes());
1621        data.extend_from_slice(&100.0f64.to_be_bytes());
1622        data.extend_from_slice(&200.0f64.to_be_bytes());
1623        data.extend_from_slice(&encode_vint(100));
1624
1625        // Metadata section
1626        data.extend_from_slice(&0u32.to_be_bytes()); // metadata_count
1627
1628        let result = parse_statistics_file(&data);
1629        assert!(result.is_ok());
1630
1631        let (remaining, stats) = result.unwrap();
1632        assert!(remaining.is_empty());
1633        assert_eq!(stats.header.version, 1);
1634        assert_eq!(stats.row_stats.total_rows, 1000);
1635        assert_eq!(stats.timestamp_stats.min_timestamp, 1000000);
1636        assert_eq!(stats.table_stats.disk_size, 1024 * 1024);
1637        assert_eq!(stats.partition_stats.avg_partition_size, 20480.0);
1638        assert_eq!(stats.compression_stats.algorithm, "LZ4");
1639        assert_eq!(stats.metadata.len(), 0);
1640    }
1641
1642    #[test]
1643    fn test_parse_statistics_file_with_extra_data() {
1644        use super::super::vint::encode_vint;
1645
1646        let mut data = Vec::new();
1647
1648        // Header
1649        data.extend_from_slice(&1u32.to_be_bytes());
1650        data.extend_from_slice(&[1u8; 16]);
1651        data.extend_from_slice(&0u32.to_be_bytes());
1652        data.extend_from_slice(&4096u64.to_be_bytes());
1653        data.extend_from_slice(&0x12345678u32.to_be_bytes());
1654
1655        // Minimal required sections
1656        data.extend_from_slice(&encode_vint(100));
1657        data.extend_from_slice(&encode_vint(90));
1658        data.extend_from_slice(&encode_vint(10));
1659        data.extend_from_slice(&encode_vint(10));
1660        data.extend_from_slice(&10.0f64.to_be_bytes());
1661        data.extend_from_slice(&0u32.to_be_bytes());
1662
1663        data.extend_from_slice(&0i64.to_be_bytes());
1664        data.extend_from_slice(&1000000i64.to_be_bytes());
1665        data.extend_from_slice(&0i64.to_be_bytes());
1666        data.extend_from_slice(&0i64.to_be_bytes());
1667        data.push(0);
1668
1669        data.extend_from_slice(&1024u64.to_be_bytes());
1670        data.extend_from_slice(&2048u64.to_be_bytes());
1671        data.extend_from_slice(&0.5f64.to_be_bytes());
1672        data.extend_from_slice(&encode_vint(10));
1673        data.extend_from_slice(&100.0f64.to_be_bytes());
1674        data.extend_from_slice(&100u64.to_be_bytes());
1675        data.extend_from_slice(&50u64.to_be_bytes());
1676        data.extend_from_slice(&1u32.to_be_bytes());
1677
1678        data.extend_from_slice(&1000.0f64.to_be_bytes());
1679        data.extend_from_slice(&100u64.to_be_bytes());
1680        data.extend_from_slice(&10000u64.to_be_bytes());
1681        data.extend_from_slice(&1.0f64.to_be_bytes());
1682        data.extend_from_slice(&0u32.to_be_bytes());
1683
1684        let algo = b"Snappy";
1685        data.extend_from_slice(&encode_vint(algo.len() as i64));
1686        data.extend_from_slice(algo);
1687        data.extend_from_slice(&2048u64.to_be_bytes());
1688        data.extend_from_slice(&1024u64.to_be_bytes());
1689        data.extend_from_slice(&0.5f64.to_be_bytes());
1690        data.extend_from_slice(&100.0f64.to_be_bytes());
1691        data.extend_from_slice(&200.0f64.to_be_bytes());
1692        data.extend_from_slice(&encode_vint(10));
1693
1694        data.extend_from_slice(&0u32.to_be_bytes());
1695
1696        // Extra trailing data
1697        data.extend_from_slice(b"extra_data");
1698
1699        let result = parse_statistics_file(&data);
1700        assert!(result.is_ok());
1701
1702        let (remaining, stats) = result.unwrap();
1703        // Should have consumed all except the extra trailing data
1704        assert_eq!(remaining, b"extra_data");
1705        assert_eq!(stats.compression_stats.algorithm, "Snappy");
1706    }
1707
1708    fn create_test_statistics() -> SSTableStatistics {
1709        SSTableStatistics {
1710            header: StatisticsHeader {
1711                version: 1,
1712                statistics_kind: 3,
1713                data_length: 1024,
1714                metadata1: 0,
1715                metadata2: 0,
1716                metadata3: 0,
1717                checksum: 0x12345678,
1718                table_id: Some([1; 16]),
1719            },
1720            row_stats: RowStatistics {
1721                total_rows: 1000,
1722                live_rows: 900,
1723                tombstone_count: 100,
1724                partition_count: 50,
1725                avg_rows_per_partition: 20.0,
1726                row_size_histogram: vec![],
1727            },
1728            timestamp_stats: TimestampStatistics {
1729                min_timestamp: 1000000,
1730                max_timestamp: 2000000,
1731                min_deletion_time: 0,
1732                max_deletion_time: 0,
1733                min_ttl: None,
1734                max_ttl: None,
1735                rows_with_ttl: 0,
1736            },
1737            column_stats: vec![],
1738            table_stats: TableStatistics {
1739                disk_size: 1024 * 1024,
1740                uncompressed_size: 2048 * 1024,
1741                compressed_size: 1024 * 1024,
1742                compression_ratio: 0.5,
1743                block_count: 100,
1744                avg_block_size: 1024.0,
1745                index_size: 1024,
1746                bloom_filter_size: 512,
1747                level_count: 1,
1748            },
1749            partition_stats: PartitionStatistics {
1750                avg_partition_size: 20480.0,
1751                min_partition_size: 1024,
1752                max_partition_size: 1048576,
1753                size_histogram: vec![],
1754                large_partition_percentage: 5.0,
1755            },
1756            compression_stats: CompressionStatistics {
1757                algorithm: "LZ4".to_string(),
1758                original_size: 2048 * 1024,
1759                compressed_size: 1024 * 1024,
1760                ratio: 0.5,
1761                compression_speed: 100.0,
1762                decompression_speed: 200.0,
1763                compressed_blocks: 100,
1764            },
1765            metadata: HashMap::new(),
1766            serialization_header_columns: vec![],
1767            serialization_header_partition_keys: vec![],
1768            serialization_header_clustering_keys: vec![],
1769            tombstone_drop_times: vec![],
1770        }
1771    }
1772}