1use 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#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct StatisticsHeader {
25 pub version: u32,
27 pub statistics_kind: u32,
29 pub data_length: u32,
31 pub metadata1: u32,
33 pub metadata2: u32,
35 pub metadata3: u32,
37 pub checksum: u32,
39 pub table_id: Option<[u8; 16]>,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct SSTableStatistics {
46 pub header: StatisticsHeader,
48 pub row_stats: RowStatistics,
50 pub timestamp_stats: TimestampStatistics,
52 pub column_stats: Vec<ColumnStatistics>,
54 pub table_stats: TableStatistics,
56 pub partition_stats: PartitionStatistics,
58 pub compression_stats: CompressionStatistics,
60 pub metadata: HashMap<String, String>,
62 #[serde(default)]
68 pub serialization_header_columns: Vec<super::header::ColumnInfo>,
69 #[serde(default)]
71 pub serialization_header_partition_keys: Vec<super::header::ColumnInfo>,
72 #[serde(default)]
74 pub serialization_header_clustering_keys: Vec<super::header::ColumnInfo>,
75 #[serde(default)]
79 pub tombstone_drop_times: Vec<(i64, u64)>,
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct RowStatistics {
85 pub total_rows: u64,
87 pub live_rows: u64,
89 pub tombstone_count: u64,
91 pub partition_count: u64,
93 pub avg_rows_per_partition: f64,
95 pub row_size_histogram: Vec<RowSizeBucket>,
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct TimestampStatistics {
102 pub min_timestamp: i64,
104 pub max_timestamp: i64,
106 pub min_deletion_time: i64,
108 pub max_deletion_time: i64,
110 pub min_ttl: Option<i64>,
112 pub max_ttl: Option<i64>,
114 pub rows_with_ttl: u64,
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct ColumnStatistics {
121 pub name: String,
123 pub column_type: String,
125 pub value_count: u64,
127 pub null_count: u64,
129 pub min_value: Option<Vec<u8>>,
131 pub max_value: Option<Vec<u8>>,
133 pub avg_size: f64,
135 pub cardinality: u64,
137 pub value_histogram: Vec<ValueFrequency>,
139 pub has_index: bool,
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct TableStatistics {
146 pub disk_size: u64,
148 pub uncompressed_size: u64,
150 pub compressed_size: u64,
152 pub compression_ratio: f64,
154 pub block_count: u64,
156 pub avg_block_size: f64,
158 pub index_size: u64,
160 pub bloom_filter_size: u64,
162 pub level_count: u32,
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct PartitionStatistics {
169 pub avg_partition_size: f64,
171 pub min_partition_size: u64,
173 pub max_partition_size: u64,
175 pub size_histogram: Vec<PartitionSizeBucket>,
177 pub large_partition_percentage: f64,
179}
180
181#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct CompressionStatistics {
184 pub algorithm: String,
186 pub original_size: u64,
188 pub compressed_size: u64,
190 pub ratio: f64,
192 pub compression_speed: f64,
194 pub decompression_speed: f64,
196 pub compressed_blocks: u64,
198}
199
200#[derive(Debug, Clone, Serialize, Deserialize)]
202pub struct RowSizeBucket {
203 pub size_start: u64,
205 pub size_end: u64,
207 pub count: u64,
209 pub percentage: f64,
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct ValueFrequency {
216 pub value: Vec<u8>,
218 pub frequency: u64,
220 pub percentage: f64,
222}
223
224#[derive(Debug, Clone, Serialize, Deserialize)]
226pub struct PartitionSizeBucket {
227 pub size_start: u64,
229 pub size_end: u64,
231 pub count: u64,
233 pub cumulative_percentage: f64,
235}
236
237pub 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![], serialization_header_partition_keys: vec![],
261 serialization_header_clustering_keys: vec![],
262 tombstone_drop_times: vec![],
263 },
264 ))
265}
266
267pub fn parse_statistics_header(input: &[u8]) -> IResult<&[u8], StatisticsHeader> {
283 let (remaining, version) = be_u32(input)?;
284
285 match version {
286 4 => parse_nb_format_header(remaining, version),
289
290 1..=3 => parse_legacy_format_header(remaining, version),
293
294 _ => Err(nom::Err::Error(nom::error::Error::new(
297 input,
298 nom::error::ErrorKind::Verify,
299 ))),
300 }
301}
302
303fn 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, },
337 ))
338}
339
340fn 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, 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
374pub 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
398pub 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
428pub 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
436pub 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
485pub 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, compression_ratio,
503 block_count,
504 avg_block_size,
505 index_size,
506 bloom_filter_size,
507 level_count,
508 },
509 ))
510}
511
512pub 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
535pub 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
562pub 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
584pub 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
602pub 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
620pub 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
637fn parse_vint_as_u64(input: &[u8]) -> IResult<&[u8], u64> {
639 let (input, value) = parse_vint(input)?;
640 Ok((input, value as u64))
641}
642
643pub(crate) fn avg_rows_available(stats: &SSTableStatistics) -> bool {
653 stats.row_stats.total_rows > 0 && stats.row_stats.partition_count > 0
654}
655
656pub struct StatisticsAnalyzer;
658
659impl StatisticsAnalyzer {
660 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 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 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 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 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 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 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 if stats.compression_stats.ratio < 0.5 {
786 score -= 20.0;
787 }
788
789 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 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
815pub struct StatisticsSummary {
816 pub total_rows: u64,
817 pub live_data_percentage: Option<f64>,
821 pub compression_efficiency: f64,
822 pub timestamp_range_days: f64,
823 pub largest_partition_mb: f64,
824 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
834pub fn serialize_statistics(_stats: &SSTableStatistics) -> Result<Vec<u8>> {
836 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, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
853 0x0F, 0x10, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x12, 0x34, 0x56, 0x78, ];
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.checksum, 0x12345678);
866 }
867
868 #[test]
869 fn test_nb_format_authoritative_detection() {
870 let nb_data = vec![
872 0x00, 0x00, 0x00, 0x04, 0x26, 0x29, 0x1b, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x14, 0xd4, ];
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()); }
891
892 #[test]
893 fn test_legacy_format_authoritative_detection() {
894 let legacy_data = vec![
896 0x00, 0x00, 0x00, 0x02, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE,
899 0xFF, 0x00, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0xAB, 0xCD, 0xEF, 0x12, ];
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); assert!(header.table_id.is_some()); }
912
913 #[test]
914 fn test_unsupported_version_rejection() {
915 let invalid_v0 = vec![
917 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ];
920 assert!(parse_statistics_header(&invalid_v0).is_err());
921
922 let invalid_v5 = vec![
924 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, ];
927 assert!(parse_statistics_header(&invalid_v5).is_err());
928
929 let invalid_v255 = vec![
931 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, ];
934 assert!(parse_statistics_header(&invalid_v255).is_err());
935 }
936
937 #[test]
938 fn test_no_heuristics_version_4_with_short_input() {
939 let short_nb_data = vec![
944 0x00, 0x00, 0x00, 0x04, 0x26, 0x29, 0x1b, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
948 0x2c, ];
951
952 let result = parse_statistics_header(&short_nb_data);
953 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 #[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; stats.row_stats.live_rows = 0; 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 assert_eq!(
992 summary.data_efficiency, None,
993 "data_efficiency must be None when live_rows is the unavailable sentinel"
994 );
995 assert_eq!(summary.total_rows, 1000);
997 assert!(
1000 summary.health_score.is_finite(),
1001 "health_score must stay finite when live_rows is the sentinel"
1002 );
1003 }
1004
1005 #[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; stats.row_stats.tombstone_count = 500; 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 #[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; stats.row_stats.tombstone_count = 500; 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 #[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 #[test]
1080 fn test_granularity_recommendation_suppressed_when_avg_rows_sentinel() {
1081 let mut stats = create_test_statistics();
1082 stats.row_stats.total_rows = 0;
1084 stats.row_stats.avg_rows_per_partition = 0.0;
1085 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 #[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; stats.row_stats.partition_count = 4;
1107 stats.row_stats.avg_rows_per_partition = 2.0; 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 #[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; 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 #[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; stats.row_stats.partition_count = 0; stats.row_stats.avg_rows_per_partition = 0.0;
1151 stats.table_stats.disk_size = 1024;
1152
1153 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 data.extend_from_slice(&encode_vint(1000));
1178 data.extend_from_slice(&encode_vint(900));
1180 data.extend_from_slice(&encode_vint(100));
1182 data.extend_from_slice(&encode_vint(50));
1184 data.extend_from_slice(&20.0f64.to_be_bytes());
1186 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 data.extend_from_slice(&2u32.to_be_bytes());
1214 data.extend_from_slice(&encode_vint(0)); data.extend_from_slice(&encode_vint(1024)); data.extend_from_slice(&encode_vint(500)); data.extend_from_slice(&50.0f64.to_be_bytes()); data.extend_from_slice(&encode_vint(1024)); data.extend_from_slice(&encode_vint(10240)); data.extend_from_slice(&encode_vint(500)); data.extend_from_slice(&50.0f64.to_be_bytes()); 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()); data.extend_from_slice(&2000000i64.to_be_bytes()); data.extend_from_slice(&0i64.to_be_bytes()); data.extend_from_slice(&0i64.to_be_bytes()); data.push(0); 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); data.extend_from_slice(&3600i64.to_be_bytes()); data.extend_from_slice(&86400i64.to_be_bytes()); data.extend_from_slice(&encode_vint(250)); 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 let name = b"user_id";
1300 data.extend_from_slice(&encode_vint(name.len() as i64));
1301 data.extend_from_slice(name);
1302 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 data.extend_from_slice(&encode_vint(1000));
1308 data.extend_from_slice(&encode_vint(0));
1310 data.push(0);
1312 data.extend_from_slice(&4.0f64.to_be_bytes());
1314 data.extend_from_slice(&encode_vint(500));
1316 data.extend_from_slice(&0u32.to_be_bytes());
1318 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 data.push(1);
1353 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 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()); data.extend_from_slice(&(2048 * 1024u64).to_be_bytes()); data.extend_from_slice(&0.5f64.to_be_bytes()); data.extend_from_slice(&encode_vint(100)); data.extend_from_slice(&1024.0f64.to_be_bytes()); data.extend_from_slice(&1024u64.to_be_bytes()); data.extend_from_slice(&512u64.to_be_bytes()); data.extend_from_slice(&1u32.to_be_bytes()); 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()); data.extend_from_slice(&1024u64.to_be_bytes()); data.extend_from_slice(&1048576u64.to_be_bytes()); data.extend_from_slice(&5.0f64.to_be_bytes()); data.extend_from_slice(&0u32.to_be_bytes()); 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 data.extend_from_slice(&2u32.to_be_bytes());
1438 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()); 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 let algo = b"LZ4";
1466 data.extend_from_slice(&encode_vint(algo.len() as i64));
1467 data.extend_from_slice(algo);
1468 data.extend_from_slice(&(2048 * 1024u64).to_be_bytes());
1470 data.extend_from_slice(&(1024 * 1024u64).to_be_bytes());
1472 data.extend_from_slice(&0.5f64.to_be_bytes());
1474 data.extend_from_slice(&100.0f64.to_be_bytes());
1476 data.extend_from_slice(&200.0f64.to_be_bytes());
1478 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()); 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()); 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 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 data.extend_from_slice(&1u32.to_be_bytes()); data.extend_from_slice(&[1u8; 16]); data.extend_from_slice(&0u32.to_be_bytes()); data.extend_from_slice(&4096u64.to_be_bytes()); data.extend_from_slice(&0x12345678u32.to_be_bytes()); 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()); 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); 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 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()); 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 data.extend_from_slice(&0u32.to_be_bytes()); 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 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 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 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 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}