Skip to main content

oxirs_tdb/compression/
adaptive.rs

1//! Adaptive compression implementation that selects the best algorithm
2
3use crate::compression::{
4    bitmap::{BitmapRoaringEncoder, BitmapWAHEncoder},
5    column_store::ColumnStoreCompressor,
6    delta::DeltaEncoder,
7    dictionary::AdaptiveDictionary,
8    frame_of_reference::FrameOfReferenceEncoder,
9    run_length::RunLengthEncoder,
10    AdvancedCompressionType, CompressedData, CompressionAlgorithm,
11};
12use anyhow::{anyhow, Result};
13use std::time::Instant;
14
15/// Adaptive compressor that selects the best algorithm for given data
16pub struct AdaptiveCompressor {
17    /// Sample size for analysis
18    sample_size: usize,
19    /// Compression threshold
20    threshold: f64,
21}
22
23impl AdaptiveCompressor {
24    /// Create new adaptive compressor
25    pub fn new(sample_size: usize, threshold: f64) -> Self {
26        Self {
27            sample_size,
28            threshold,
29        }
30    }
31
32    /// Select best compression algorithm for data
33    pub fn select_best_algorithm(&self, data: &[u8]) -> AdvancedCompressionType {
34        if data.len() < 10 {
35            return AdvancedCompressionType::RunLength; // Default for small data
36        }
37
38        let sample_data = if data.len() > self.sample_size {
39            &data[..self.sample_size]
40        } else {
41            data
42        };
43
44        // Analyze data characteristics
45        let stats = self.analyze_data(sample_data);
46
47        // Check if compression is beneficial based on threshold
48        if stats.repetition_ratio < self.threshold && stats.sparsity < self.threshold {
49            // Data doesn't meet compression threshold, use simple algorithm
50            return AdvancedCompressionType::RunLength;
51        }
52
53        // Decision tree based on data characteristics
54        if stats.sparsity > 0.9 {
55            // Very sparse data (lots of zeros) - use bitmap compression
56            if stats.bit_runs > stats.byte_runs {
57                AdvancedCompressionType::BitmapRoaring
58            } else {
59                AdvancedCompressionType::BitmapWAH
60            }
61        } else if stats.repetition_ratio > 0.5 {
62            // High repetition - use run-length encoding
63            AdvancedCompressionType::RunLength
64        } else if stats.is_sorted && stats.delta_efficiency > 0.7 {
65            // Sorted numeric data - use delta or FOR encoding
66            if stats.range_ratio < 0.1 {
67                AdvancedCompressionType::FrameOfReference
68            } else {
69                AdvancedCompressionType::Delta
70            }
71        } else if stats.dictionary_efficiency > 0.6 {
72            // Text-like data with repeated patterns
73            AdvancedCompressionType::AdaptiveDictionary
74        } else if data.len() % 8 == 0 && stats.structured_score > 0.5 {
75            // Structured data that might benefit from column store
76            AdvancedCompressionType::ColumnStore
77        } else {
78            // Default fallback
79            AdvancedCompressionType::RunLength
80        }
81    }
82
83    /// Analyze data characteristics
84    fn analyze_data(&self, data: &[u8]) -> DataStats {
85        let mut stats = DataStats::default();
86
87        if data.is_empty() {
88            return stats;
89        }
90
91        // Basic statistics
92        stats.length = data.len();
93        stats.unique_bytes = data.iter().collect::<std::collections::HashSet<_>>().len();
94
95        // Sparsity analysis (count zeros)
96        let zero_count = data.iter().filter(|&&b| b == 0).count();
97        stats.sparsity = zero_count as f64 / data.len() as f64;
98
99        // Repetition analysis
100        let mut repetitions = 0;
101        let mut current_byte = data[0];
102        let mut run_length = 1;
103
104        for &byte in &data[1..] {
105            if byte == current_byte {
106                run_length += 1;
107            } else {
108                if run_length > 1 {
109                    repetitions += run_length;
110                }
111                stats.byte_runs += 1;
112                current_byte = byte;
113                run_length = 1;
114            }
115        }
116
117        if run_length > 1 {
118            repetitions += run_length;
119        }
120        stats.byte_runs += 1;
121
122        stats.repetition_ratio = repetitions as f64 / data.len() as f64;
123
124        // Bit-level runs (for bitmap analysis)
125        let mut bit_runs = 0;
126        let mut current_bit = data[0] & 1;
127
128        for &byte in data {
129            for i in 0..8 {
130                let bit = (byte >> i) & 1;
131                if bit != current_bit {
132                    bit_runs += 1;
133                    current_bit = bit;
134                }
135            }
136        }
137        stats.bit_runs = bit_runs;
138
139        // Sortedness check (for numeric data)
140        if data.len() >= 8 {
141            let u64_values: Vec<u64> = data
142                .chunks_exact(8)
143                .map(|chunk| {
144                    u64::from_le_bytes([
145                        chunk[0], chunk[1], chunk[2], chunk[3], chunk[4], chunk[5], chunk[6],
146                        chunk[7],
147                    ])
148                })
149                .collect();
150
151            if u64_values.len() > 1 {
152                let mut sorted_count = 0;
153                for i in 1..u64_values.len() {
154                    if u64_values[i] >= u64_values[i - 1] {
155                        sorted_count += 1;
156                    }
157                }
158                stats.is_sorted = sorted_count as f64 / (u64_values.len() - 1) as f64 > 0.8;
159
160                // Range analysis for FOR encoding
161                if let (Some(&min_val), Some(&max_val)) =
162                    (u64_values.iter().min(), u64_values.iter().max())
163                {
164                    stats.range_ratio = if max_val > 0 {
165                        (max_val - min_val) as f64 / max_val as f64
166                    } else {
167                        0.0
168                    };
169                }
170
171                // Delta efficiency
172                let mut small_deltas = 0;
173                for i in 1..u64_values.len() {
174                    if u64_values[i].abs_diff(u64_values[i - 1]) < 65536 {
175                        small_deltas += 1;
176                    }
177                }
178                stats.delta_efficiency = small_deltas as f64 / (u64_values.len() - 1) as f64;
179            }
180        }
181
182        // Dictionary efficiency (text-like patterns)
183        let mut word_count = 0;
184        let mut word_bytes = 0;
185        let mut in_word = false;
186
187        for &byte in data {
188            if byte.is_ascii_alphanumeric() || byte == b'_' || byte == b'-' {
189                if !in_word {
190                    word_count += 1;
191                    in_word = true;
192                }
193                word_bytes += 1;
194            } else {
195                in_word = false;
196            }
197        }
198
199        if word_count > 0 {
200            stats.dictionary_efficiency = word_bytes as f64 / data.len() as f64;
201        }
202
203        // Structured data score (regularity in byte patterns)
204        if data.len() >= 16 {
205            let mut pattern_score = 0.0;
206            let chunk_size = 8;
207            let chunks: Vec<_> = data.chunks_exact(chunk_size).collect();
208
209            if chunks.len() > 1 {
210                let mut similar_chunks = 0;
211                for i in 1..chunks.len() {
212                    let mut differences = 0;
213                    // Need index j to compare elements at the same position in different chunks
214                    #[allow(clippy::needless_range_loop)]
215                    for j in 0..chunk_size {
216                        if chunks[i][j] != chunks[0][j] {
217                            differences += 1;
218                        }
219                    }
220                    if differences <= chunk_size / 2 {
221                        similar_chunks += 1;
222                    }
223                }
224                pattern_score = similar_chunks as f64 / (chunks.len() - 1) as f64;
225            }
226            stats.structured_score = pattern_score;
227        }
228
229        stats
230    }
231
232    /// Compress data using selected algorithm
233    pub fn compress_adaptive(&self, data: &[u8]) -> Result<CompressedData> {
234        let algorithm = self.select_best_algorithm(data);
235
236        let start = Instant::now();
237        let result = match algorithm {
238            AdvancedCompressionType::RunLength => {
239                let encoder = RunLengthEncoder;
240                encoder.compress(data)?
241            }
242            AdvancedCompressionType::Delta => {
243                let encoder = DeltaEncoder;
244                encoder.compress(data)?
245            }
246            AdvancedCompressionType::FrameOfReference => {
247                let encoder = FrameOfReferenceEncoder::default();
248                encoder.compress(data)?
249            }
250            AdvancedCompressionType::AdaptiveDictionary => {
251                let encoder = AdaptiveDictionary::new();
252                encoder.compress(data)?
253            }
254            AdvancedCompressionType::BitmapWAH => {
255                let encoder = BitmapWAHEncoder;
256                encoder.compress(data)?
257            }
258            AdvancedCompressionType::BitmapRoaring => {
259                let encoder = BitmapRoaringEncoder;
260                encoder.compress(data)?
261            }
262            AdvancedCompressionType::ColumnStore => {
263                let encoder = ColumnStoreCompressor::new();
264                encoder.compress(data)?
265            }
266            AdvancedCompressionType::Lz4 => {
267                use crate::compression::Lz4Compressor;
268                let encoder = Lz4Compressor::balanced();
269                encoder.compress(data)?
270            }
271            AdvancedCompressionType::Zstd => {
272                use crate::compression::ZstdCompressor;
273                let encoder = ZstdCompressor::balanced();
274                encoder.compress(data)?
275            }
276            AdvancedCompressionType::Snappy => {
277                use crate::compression::SnappyCompressor;
278                let encoder = SnappyCompressor::new();
279                encoder.compress(data)?
280            }
281            AdvancedCompressionType::Brotli => {
282                use crate::compression::BrotliCompressor;
283                let encoder = BrotliCompressor::balanced();
284                encoder.compress(data)?
285            }
286            AdvancedCompressionType::Adaptive => {
287                return Err(anyhow!("Recursive adaptive compression not allowed"));
288            }
289        };
290        let selection_time = start.elapsed();
291
292        // Override metadata to indicate this was adaptively selected
293        let mut metadata = result.metadata;
294        metadata.algorithm = AdvancedCompressionType::Adaptive;
295        metadata.compression_time_us += selection_time.as_micros() as u64;
296        metadata
297            .metadata
298            .insert("selected_algorithm".to_string(), algorithm.to_string());
299
300        Ok(CompressedData {
301            data: result.data,
302            metadata,
303        })
304    }
305
306    /// Decompress adaptively compressed data
307    pub fn decompress_adaptive(&self, compressed: &CompressedData) -> Result<Vec<u8>> {
308        if compressed.metadata.algorithm != AdvancedCompressionType::Adaptive {
309            return Err(anyhow!(
310                "Invalid compression algorithm: expected Adaptive, got {}",
311                compressed.metadata.algorithm
312            ));
313        }
314
315        // Get the actual algorithm that was used
316        let selected_algorithm = compressed
317            .metadata
318            .metadata
319            .get("selected_algorithm")
320            .ok_or_else(|| {
321                anyhow!("Missing selected algorithm in adaptive compression metadata")
322            })?;
323
324        let algorithm = match selected_algorithm.as_str() {
325            "RunLength" => AdvancedCompressionType::RunLength,
326            "Delta" => AdvancedCompressionType::Delta,
327            "FrameOfReference" => AdvancedCompressionType::FrameOfReference,
328            "AdaptiveDictionary" => AdvancedCompressionType::AdaptiveDictionary,
329            "BitmapWAH" => AdvancedCompressionType::BitmapWAH,
330            "BitmapRoaring" => AdvancedCompressionType::BitmapRoaring,
331            "ColumnStore" => AdvancedCompressionType::ColumnStore,
332            "LZ4" => AdvancedCompressionType::Lz4,
333            "Zstd" => AdvancedCompressionType::Zstd,
334            "Snappy" => AdvancedCompressionType::Snappy,
335            "Brotli" => AdvancedCompressionType::Brotli,
336            _ => {
337                return Err(anyhow!(
338                    "Unknown selected algorithm: {}",
339                    selected_algorithm
340                ))
341            }
342        };
343
344        // Create temporary compressed data with the original algorithm
345        let mut temp_compressed = compressed.clone();
346        temp_compressed.metadata.algorithm = algorithm;
347
348        // Decompress using the original algorithm
349        match algorithm {
350            AdvancedCompressionType::RunLength => {
351                let encoder = RunLengthEncoder;
352                encoder.decompress(&temp_compressed)
353            }
354            AdvancedCompressionType::Delta => {
355                let encoder = DeltaEncoder;
356                encoder.decompress(&temp_compressed)
357            }
358            AdvancedCompressionType::FrameOfReference => {
359                let encoder = FrameOfReferenceEncoder::default();
360                encoder.decompress(&temp_compressed)
361            }
362            AdvancedCompressionType::AdaptiveDictionary => {
363                let encoder = AdaptiveDictionary::new();
364                encoder.decompress(&temp_compressed)
365            }
366            AdvancedCompressionType::BitmapWAH => {
367                let encoder = BitmapWAHEncoder;
368                encoder.decompress(&temp_compressed)
369            }
370            AdvancedCompressionType::BitmapRoaring => {
371                let encoder = BitmapRoaringEncoder;
372                encoder.decompress(&temp_compressed)
373            }
374            AdvancedCompressionType::ColumnStore => {
375                let encoder = ColumnStoreCompressor::new();
376                encoder.decompress(&temp_compressed)
377            }
378            AdvancedCompressionType::Lz4 => {
379                use crate::compression::Lz4Compressor;
380                let encoder = Lz4Compressor::balanced();
381                encoder.decompress(&temp_compressed)
382            }
383            AdvancedCompressionType::Zstd => {
384                use crate::compression::ZstdCompressor;
385                let encoder = ZstdCompressor::balanced();
386                encoder.decompress(&temp_compressed)
387            }
388            AdvancedCompressionType::Snappy => {
389                use crate::compression::SnappyCompressor;
390                let encoder = SnappyCompressor::new();
391                encoder.decompress(&temp_compressed)
392            }
393            AdvancedCompressionType::Brotli => {
394                use crate::compression::BrotliCompressor;
395                let encoder = BrotliCompressor::balanced();
396                encoder.decompress(&temp_compressed)
397            }
398            AdvancedCompressionType::Adaptive => {
399                Err(anyhow!("Recursive adaptive compression not allowed"))
400            }
401        }
402    }
403}
404
405impl Default for AdaptiveCompressor {
406    fn default() -> Self {
407        Self::new(1024, 0.8) // Default sample size and threshold
408    }
409}
410
411impl CompressionAlgorithm for AdaptiveCompressor {
412    fn compress(&self, data: &[u8]) -> Result<CompressedData> {
413        self.compress_adaptive(data)
414    }
415
416    fn decompress(&self, compressed: &CompressedData) -> Result<Vec<u8>> {
417        self.decompress_adaptive(compressed)
418    }
419
420    fn algorithm_type(&self) -> AdvancedCompressionType {
421        AdvancedCompressionType::Adaptive
422    }
423}
424
425/// Data statistics for algorithm selection
426#[derive(Debug, Default)]
427struct DataStats {
428    length: usize,
429    unique_bytes: usize,
430    sparsity: f64,
431    repetition_ratio: f64,
432    byte_runs: usize,
433    bit_runs: usize,
434    is_sorted: bool,
435    range_ratio: f64,
436    delta_efficiency: f64,
437    dictionary_efficiency: f64,
438    structured_score: f64,
439}
440
441#[cfg(test)]
442mod tests {
443    use super::*;
444
445    #[test]
446    fn test_adaptive_compressor_basic() {
447        let compressor = AdaptiveCompressor::default();
448
449        // Test with repetitive data (should select RunLength)
450        let repetitive_data = vec![1u8; 100];
451        let algorithm = compressor.select_best_algorithm(&repetitive_data);
452        assert_eq!(algorithm, AdvancedCompressionType::RunLength);
453
454        let compressed = compressor.compress(&repetitive_data).unwrap();
455        assert_eq!(
456            compressed.metadata.algorithm,
457            AdvancedCompressionType::Adaptive
458        );
459        assert!(compressed
460            .metadata
461            .metadata
462            .contains_key("selected_algorithm"));
463
464        let decompressed = compressor.decompress(&compressed).unwrap();
465        assert_eq!(repetitive_data, decompressed);
466    }
467
468    #[test]
469    fn test_sparse_data_selection() {
470        let compressor = AdaptiveCompressor::default();
471
472        // Create sparse data (mostly zeros)
473        let mut sparse_data = vec![0u8; 1000];
474        sparse_data[10] = 1;
475        sparse_data[100] = 1;
476        sparse_data[500] = 1;
477
478        let algorithm = compressor.select_best_algorithm(&sparse_data);
479        // Should select a bitmap algorithm for sparse data
480        assert!(matches!(
481            algorithm,
482            AdvancedCompressionType::BitmapWAH | AdvancedCompressionType::BitmapRoaring
483        ));
484    }
485
486    #[test]
487    fn test_empty_data() {
488        let compressor = AdaptiveCompressor::default();
489        let data = vec![];
490
491        let compressed = compressor.compress(&data).unwrap();
492        let decompressed = compressor.decompress(&compressed).unwrap();
493        assert_eq!(data, decompressed);
494    }
495
496    #[test]
497    fn test_small_data() {
498        let compressor = AdaptiveCompressor::default();
499        let data = vec![1, 2, 3];
500
501        let algorithm = compressor.select_best_algorithm(&data);
502        assert_eq!(algorithm, AdvancedCompressionType::RunLength); // Default for small data
503    }
504
505    #[test]
506    fn test_data_analysis() {
507        let compressor = AdaptiveCompressor::default();
508
509        // Test with sorted numeric data
510        let sorted_data: Vec<u8> = (0..64u64).flat_map(|i| i.to_le_bytes()).collect();
511
512        let stats = compressor.analyze_data(&sorted_data);
513        assert!(stats.is_sorted);
514        assert!(stats.delta_efficiency > 0.5);
515    }
516}
517
518// ============================================================================
519// AdaptiveCodec - v0.3.0 lightweight adaptive codec with CodecType selection
520// ============================================================================
521
522use crate::error::{Result as TdbResult, TdbError};
523
524/// Codec selection result for [`AdaptiveCodec`].
525///
526/// Each variant represents a distinct compression strategy chosen adaptively
527/// based on data characteristics (size, entropy, structure).
528#[derive(Debug, Clone, Copy, PartialEq, Eq)]
529pub enum CodecType {
530    /// Zstd without a pre-trained dictionary (best for data > threshold)
531    ZstdBasic,
532    /// Zstd with a pre-trained Zstd dictionary (best for small repetitive blocks)
533    ZstdDict,
534    /// Delta encoding for sorted or near-sorted numerical streams
535    Delta,
536    /// No compression (data is incompressible or below size threshold)
537    None,
538}
539
540impl std::fmt::Display for CodecType {
541    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
542        match self {
543            CodecType::ZstdBasic => write!(f, "ZstdBasic"),
544            CodecType::ZstdDict => write!(f, "ZstdDict"),
545            CodecType::Delta => write!(f, "Delta"),
546            CodecType::None => write!(f, "None"),
547        }
548    }
549}
550
551/// Magic byte prepended to adaptive-compressed output to identify the [`CodecType`].
552const CODEC_MAGIC_ZSTD_BASIC: u8 = 0x01;
553const CODEC_MAGIC_ZSTD_DICT: u8 = 0x02;
554const CODEC_MAGIC_DELTA: u8 = 0x03;
555const CODEC_MAGIC_NONE: u8 = 0x00;
556
557/// A lightweight adaptive codec that selects the best compression strategy based
558/// on the data being compressed.
559///
560/// `AdaptiveCodec` serves as the primary entry point for v0.3.0 compression.
561/// It examines data characteristics (size, byte entropy, byte-delta statistics)
562/// and dispatches to the most appropriate codec:
563///
564/// | Condition | Selected Codec |
565/// |-----------|---------------|
566/// | `data.len() < threshold_bytes` | `CodecType::None` |
567/// | Has Zstd dictionary **and** data appears text-like | `CodecType::ZstdDict` |
568/// | Data is sorted/near-sorted u8 stream | `CodecType::Delta` |
569/// | Otherwise | `CodecType::ZstdBasic` |
570///
571/// The compressed output is self-describing: a single-byte header encodes the
572/// codec type so that [`decompress`](AdaptiveCodec::decompress) can always recover
573/// the original data without out-of-band metadata.
574///
575/// # Example
576///
577/// ```rust,no_run
578/// use oxirs_tdb::compression::adaptive::{AdaptiveCodec, CodecType};
579///
580/// let codec = AdaptiveCodec { threshold_bytes: 64, zstd_dict: None, zstd_level: 3 };
581/// let data = b"<http://example.org/s> <p> <o> .".repeat(10);
582/// let (compressed, chosen) = codec.compress(&data).unwrap();
583/// let decompressed = codec.decompress(&compressed, chosen).unwrap();
584/// assert_eq!(decompressed, data);
585/// ```
586pub struct AdaptiveCodec {
587    /// Minimum data size (bytes) required before compression is attempted.
588    /// Data smaller than this threshold is stored as-is (`CodecType::None`).
589    pub threshold_bytes: usize,
590    /// Optional pre-trained Zstd dictionary to use for `CodecType::ZstdDict`.
591    pub zstd_dict: Option<Vec<u8>>,
592    /// Zstd compression level used when dispatching to Zstd codecs (1-22).
593    pub zstd_level: i32,
594}
595
596impl AdaptiveCodec {
597    /// Create a new `AdaptiveCodec` with the given threshold and no dictionary.
598    ///
599    /// # Arguments
600    ///
601    /// * `threshold_bytes` - Minimum data size to apply compression
602    pub fn new(threshold_bytes: usize) -> Self {
603        Self {
604            threshold_bytes,
605            zstd_dict: None,
606            zstd_level: 3,
607        }
608    }
609
610    /// Create a new `AdaptiveCodec` with a pre-trained Zstd dictionary.
611    pub fn with_dict(threshold_bytes: usize, dict: Vec<u8>, zstd_level: i32) -> Self {
612        Self {
613            threshold_bytes,
614            zstd_dict: Some(dict),
615            zstd_level: zstd_level.clamp(1, 22),
616        }
617    }
618
619    /// Select the most appropriate codec for the given data.
620    ///
621    /// The decision is based on:
622    /// 1. Data size vs `threshold_bytes`
623    /// 2. Availability of a Zstd dictionary
624    /// 3. Byte entropy / sortedness heuristics
625    pub fn select_codec(&self, data: &[u8]) -> CodecType {
626        if data.len() < self.threshold_bytes {
627            return CodecType::None;
628        }
629
630        // If we have a dictionary and the data looks text-like (high ASCII printable ratio)
631        // prefer dictionary compression for smaller blocks
632        if self.zstd_dict.is_some() {
633            let printable_ratio = Self::compute_printable_ratio(data);
634            // Text/URI data benefits most from dictionary compression
635            if printable_ratio > 0.7 {
636                return CodecType::ZstdDict;
637            }
638        }
639
640        // Check if data is sorted/near-sorted: delta encoding excels here
641        if Self::is_mostly_sorted(data) {
642            return CodecType::Delta;
643        }
644
645        // Fall back to basic Zstd
646        CodecType::ZstdBasic
647    }
648
649    /// Compress `data` using adaptively selected codec.
650    ///
651    /// Returns `(compressed_bytes, chosen_codec)`. The compressed bytes include a
652    /// single-byte codec marker as the first byte so [`decompress`](AdaptiveCodec::decompress)
653    /// can determine the algorithm without external metadata.
654    pub fn compress(&self, data: &[u8]) -> TdbResult<(Vec<u8>, CodecType)> {
655        let codec = self.select_codec(data);
656        let compressed = match codec {
657            CodecType::None => {
658                let mut out = Vec::with_capacity(1 + data.len());
659                out.push(CODEC_MAGIC_NONE);
660                out.extend_from_slice(data);
661                out
662            }
663            CodecType::ZstdBasic => {
664                let payload = Self::zstd_compress_raw(data, self.zstd_level, None)?;
665                let mut out = Vec::with_capacity(1 + payload.len());
666                out.push(CODEC_MAGIC_ZSTD_BASIC);
667                out.extend_from_slice(&payload);
668                out
669            }
670            CodecType::ZstdDict => {
671                let dict = self.zstd_dict.as_deref().ok_or_else(|| {
672                    TdbError::Other("ZstdDict selected but no dictionary loaded".to_string())
673                })?;
674                let payload = Self::zstd_compress_raw(data, self.zstd_level, Some(dict))?;
675                let mut out = Vec::with_capacity(1 + payload.len());
676                out.push(CODEC_MAGIC_ZSTD_DICT);
677                out.extend_from_slice(&payload);
678                out
679            }
680            CodecType::Delta => {
681                let payload = Self::delta_compress(data)?;
682                let mut out = Vec::with_capacity(1 + payload.len());
683                out.push(CODEC_MAGIC_DELTA);
684                out.extend_from_slice(&payload);
685                out
686            }
687        };
688        Ok((compressed, codec))
689    }
690
691    /// Decompress data that was compressed by [`AdaptiveCodec::compress`].
692    ///
693    /// The `codec` argument must match the codec used during compression (the same value
694    /// returned from `compress`). Alternatively, use [`decompress_auto`](AdaptiveCodec::decompress_auto)
695    /// to auto-detect from the embedded header byte.
696    pub fn decompress(&self, data: &[u8], codec: CodecType) -> TdbResult<Vec<u8>> {
697        if data.is_empty() {
698            return Ok(Vec::new());
699        }
700
701        // data.len() >= 1 guaranteed by is_empty check above
702        let header = data[0];
703        let payload = &data[1..];
704
705        // Verify header matches expected codec
706        let expected_header = match codec {
707            CodecType::None => CODEC_MAGIC_NONE,
708            CodecType::ZstdBasic => CODEC_MAGIC_ZSTD_BASIC,
709            CodecType::ZstdDict => CODEC_MAGIC_ZSTD_DICT,
710            CodecType::Delta => CODEC_MAGIC_DELTA,
711        };
712        if header != expected_header {
713            return Err(TdbError::Other(format!(
714                "Codec mismatch: header byte 0x{:02x} does not match expected codec {}",
715                header, codec
716            )));
717        }
718
719        self.decompress_payload(payload, codec)
720    }
721
722    /// Decompress data, auto-detecting the codec from the embedded header byte.
723    ///
724    /// This is more convenient than [`decompress`](AdaptiveCodec::decompress) when the
725    /// original `CodecType` is not stored separately.
726    pub fn decompress_auto(&self, data: &[u8]) -> TdbResult<Vec<u8>> {
727        if data.is_empty() {
728            return Ok(Vec::new());
729        }
730        // data.len() >= 1 guaranteed by is_empty check above
731        let header = data[0];
732        let payload = &data[1..];
733
734        let codec = match header {
735            CODEC_MAGIC_NONE => CodecType::None,
736            CODEC_MAGIC_ZSTD_BASIC => CodecType::ZstdBasic,
737            CODEC_MAGIC_ZSTD_DICT => CodecType::ZstdDict,
738            CODEC_MAGIC_DELTA => CodecType::Delta,
739            _ => {
740                return Err(TdbError::Other(format!(
741                    "Unknown codec header byte: 0x{:02x}",
742                    header
743                )))
744            }
745        };
746
747        self.decompress_payload(payload, codec)
748    }
749
750    // -- Private helpers --
751
752    fn decompress_payload(&self, payload: &[u8], codec: CodecType) -> TdbResult<Vec<u8>> {
753        match codec {
754            CodecType::None => Ok(payload.to_vec()),
755            CodecType::ZstdBasic => Self::zstd_decompress_raw(payload, None),
756            CodecType::ZstdDict => {
757                let dict = self.zstd_dict.as_deref().ok_or_else(|| {
758                    TdbError::Other(
759                        "ZstdDict decompression requested but no dictionary loaded".to_string(),
760                    )
761                })?;
762                Self::zstd_decompress_raw(payload, Some(dict))
763            }
764            CodecType::Delta => Self::delta_decompress(payload),
765        }
766    }
767
768    fn zstd_compress_raw(data: &[u8], level: i32, dict: Option<&[u8]>) -> TdbResult<Vec<u8>> {
769        use std::io::Write;
770        let compressed = match dict {
771            None => {
772                let mut enc = oxiarc_zstd::ZstdStreamEncoder::new(Vec::new(), level);
773                enc.write_all(data)
774                    .map_err(|e| TdbError::Other(format!("zstd write: {e}")))?;
775                enc.finish()
776                    .map_err(|e| TdbError::Other(format!("zstd finish: {e}")))?
777            }
778            Some(d) => {
779                let mut enc = oxiarc_zstd::ZstdEncoder::new();
780                enc.set_level(level);
781                enc.set_dictionary(d);
782                enc.compress(data)
783                    .map_err(|e| TdbError::Other(format!("zstd dict compress: {e}")))?
784            }
785        };
786        Ok(compressed)
787    }
788
789    fn zstd_decompress_raw(data: &[u8], dict: Option<&[u8]>) -> TdbResult<Vec<u8>> {
790        use std::io::Read;
791        match dict {
792            None => {
793                let mut dec = oxiarc_zstd::ZstdStreamDecoder::new(data);
794                let mut out = Vec::new();
795                dec.read_to_end(&mut out)
796                    .map_err(|e| TdbError::Other(format!("zstd decompress: {e}")))?;
797                Ok(out)
798            }
799            Some(d) => {
800                // Dictionary decompression - must pass the dictionary used during compression
801                let out = oxiarc_zstd::decompress_with_dict(data, d)
802                    .map_err(|e| TdbError::Other(format!("zstd dict decompress: {e}")))?;
803                Ok(out)
804            }
805        }
806    }
807
808    /// Simple byte delta encoding: store first byte, then signed i16 deltas.
809    fn delta_compress(data: &[u8]) -> TdbResult<Vec<u8>> {
810        if data.is_empty() {
811            return Ok(Vec::new());
812        }
813        let mut out = Vec::with_capacity(1 + (data.len() - 1) * 2);
814        out.push(data[0]);
815        for i in 1..data.len() {
816            let delta = data[i] as i16 - data[i - 1] as i16;
817            out.extend_from_slice(&delta.to_le_bytes());
818        }
819        Ok(out)
820    }
821
822    fn delta_decompress(encoded: &[u8]) -> TdbResult<Vec<u8>> {
823        if encoded.is_empty() {
824            return Ok(Vec::new());
825        }
826        let mut out = Vec::new();
827        out.push(encoded[0]);
828        let mut current = encoded[0] as i16;
829        for chunk in encoded[1..].chunks_exact(2) {
830            let delta = i16::from_le_bytes([chunk[0], chunk[1]]);
831            current = (current + delta).clamp(0, 255);
832            out.push(current as u8);
833        }
834        Ok(out)
835    }
836
837    /// Fraction of bytes in `data` that are printable ASCII (0x20-0x7E).
838    fn compute_printable_ratio(data: &[u8]) -> f64 {
839        if data.is_empty() {
840            return 0.0;
841        }
842        let printable = data.iter().filter(|&&b| (0x20..=0x7e).contains(&b)).count();
843        printable as f64 / data.len() as f64
844    }
845
846    /// Heuristic: returns `true` if >= 80% of consecutive byte pairs are non-decreasing.
847    fn is_mostly_sorted(data: &[u8]) -> bool {
848        if data.len() < 2 {
849            return false;
850        }
851        let non_decreasing = data.windows(2).filter(|w| w[1] >= w[0]).count();
852        non_decreasing as f64 / (data.len() - 1) as f64 >= 0.80
853    }
854}
855
856impl Default for AdaptiveCodec {
857    fn default() -> Self {
858        Self::new(64)
859    }
860}
861
862#[cfg(test)]
863mod adaptive_codec_tests {
864    use super::*;
865
866    #[test]
867    fn test_adaptive_codec_none_below_threshold() {
868        let codec = AdaptiveCodec::new(1000);
869        let data = b"small data".to_vec();
870        let selected = codec.select_codec(&data);
871        assert_eq!(
872            selected,
873            CodecType::None,
874            "small data below threshold should select None"
875        );
876    }
877
878    #[test]
879    fn test_adaptive_codec_zstd_basic_roundtrip() {
880        let codec = AdaptiveCodec::new(10);
881        // Not sorted, not text-like: should pick ZstdBasic
882        let data: Vec<u8> = (0..200u8).rev().collect(); // descending bytes
883        let (compressed, chosen) = codec.compress(&data).expect("compress");
884        // Accept either ZstdBasic or Delta (both are valid adaptive choices)
885        assert!(
886            chosen == CodecType::ZstdBasic || chosen == CodecType::Delta,
887            "expected ZstdBasic or Delta, got {:?}",
888            chosen
889        );
890        let decompressed = codec.decompress(&compressed, chosen).expect("decompress");
891        assert_eq!(decompressed, data, "ZstdBasic roundtrip must be identical");
892    }
893
894    #[test]
895    fn test_adaptive_codec_zstd_dict_roundtrip() {
896        use crate::compression::zstd_codec::ZstdCodec;
897
898        // Train a dictionary
899        let samples: Vec<Vec<u8>> = (0..300)
900            .map(|i| {
901                format!(
902                    "<http://example.org/r/{}> <http://schema.org/name> \"Name {}\" .",
903                    i, i
904                )
905                .into_bytes()
906            })
907            .collect();
908        let dict = ZstdCodec::train_dict(&samples, 4096).expect("train dict");
909
910        let codec = AdaptiveCodec::with_dict(10, dict, 3);
911        // Text data with a dictionary should prefer ZstdDict
912        let data = b"<http://example.org/r/42> <http://schema.org/name> \"Name 42\" .".repeat(5);
913        let (compressed, chosen) = codec.compress(&data).expect("compress with dict");
914        assert_eq!(
915            chosen,
916            CodecType::ZstdDict,
917            "text-like data with dict should select ZstdDict"
918        );
919
920        let decompressed = codec
921            .decompress(&compressed, chosen)
922            .expect("decompress with dict");
923        assert_eq!(decompressed, data, "ZstdDict roundtrip must be identical");
924    }
925
926    #[test]
927    fn test_adaptive_codec_delta_roundtrip() {
928        let codec = AdaptiveCodec::new(10);
929        // Sorted bytes: delta encoding should be selected
930        let data: Vec<u8> = (0..200u8).collect();
931        let (compressed, chosen) = codec.compress(&data).expect("compress sorted");
932        assert_eq!(
933            chosen,
934            CodecType::Delta,
935            "sorted data should select Delta codec"
936        );
937
938        let decompressed = codec
939            .decompress(&compressed, chosen)
940            .expect("decompress delta");
941        assert_eq!(decompressed, data, "Delta roundtrip must be identical");
942    }
943
944    #[test]
945    fn test_adaptive_codec_none_roundtrip() {
946        let codec = AdaptiveCodec::new(1000);
947        let data = b"small".to_vec();
948        let (compressed, chosen) = codec.compress(&data).expect("compress none");
949        assert_eq!(chosen, CodecType::None);
950
951        let decompressed = codec
952            .decompress(&compressed, chosen)
953            .expect("decompress none");
954        assert_eq!(decompressed, data, "None codec roundtrip must be identical");
955    }
956
957    #[test]
958    fn test_adaptive_codec_decompress_auto() {
959        let codec = AdaptiveCodec::new(10);
960        let data = b"Auto-detect decompression test data for the adaptive codec.".repeat(10);
961        let (compressed, _chosen) = codec.compress(&data).expect("compress");
962        let decompressed = codec.decompress_auto(&compressed).expect("decompress auto");
963        assert_eq!(
964            decompressed, data,
965            "decompress_auto roundtrip must be identical"
966        );
967    }
968
969    #[test]
970    fn test_adaptive_codec_empty_data() {
971        let codec = AdaptiveCodec::new(64);
972        let (compressed, chosen) = codec.compress(&[]).expect("compress empty");
973        // Empty data is below threshold: CodecType::None
974        assert_eq!(chosen, CodecType::None);
975        let decompressed = codec
976            .decompress(&compressed, chosen)
977            .expect("decompress empty");
978        assert_eq!(decompressed, b"");
979    }
980
981    #[test]
982    fn test_adaptive_codec_codec_type_display() {
983        assert_eq!(CodecType::ZstdBasic.to_string(), "ZstdBasic");
984        assert_eq!(CodecType::ZstdDict.to_string(), "ZstdDict");
985        assert_eq!(CodecType::Delta.to_string(), "Delta");
986        assert_eq!(CodecType::None.to_string(), "None");
987    }
988
989    #[test]
990    fn test_adaptive_codec_wrong_codec_type_error() {
991        let codec = AdaptiveCodec::new(10);
992        let data: Vec<u8> = (0..200u8).collect(); // sorted -> Delta
993        let (compressed, _chosen) = codec.compress(&data).expect("compress");
994        // Deliberately pass wrong codec type
995        let result = codec.decompress(&compressed, CodecType::ZstdBasic);
996        assert!(result.is_err(), "wrong CodecType should return error");
997    }
998
999    #[test]
1000    fn test_adaptive_codec_large_rdf_data() {
1001        let codec = AdaptiveCodec::new(64);
1002        let rdf_triple = b"<http://dbpedia.org/resource/Thing> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://dbpedia.org/ontology/Thing> .\n";
1003        let mut data = Vec::new();
1004        for _ in 0..500 {
1005            data.extend_from_slice(rdf_triple);
1006        }
1007
1008        let (compressed, chosen) = codec.compress(&data).expect("compress large RDF");
1009        println!(
1010            "Large RDF: {} -> {} bytes ({:?})",
1011            data.len(),
1012            compressed.len(),
1013            chosen
1014        );
1015
1016        let decompressed = codec
1017            .decompress(&compressed, chosen)
1018            .expect("decompress large RDF");
1019        assert_eq!(decompressed, data, "large RDF roundtrip must be identical");
1020        // Should achieve compression
1021        assert!(
1022            compressed.len() < data.len(),
1023            "large repetitive RDF should compress, ratio: {:.2}",
1024            compressed.len() as f64 / data.len() as f64
1025        );
1026    }
1027}