kore_fileformat 1.3.3

KORE — Killer Optimized Record Exchange: standalone Rust crate (zero deps)
Documentation
// Dictionary Encoding for String Columns
// Compresses strings by mapping to integer indices
//
// EXAMPLE:
// Input: ["customer_1", "customer_2", "customer_1", "customer_3", ...]
// Dictionary: {0: "customer_1", 1: "customer_2", 2: "customer_3"}
// Encoded: [0, 1, 0, 2, 0, 1, 2, 0, 1, ...]
// Benefit: 80-95% compression on high-cardinality string columns

use std::collections::HashMap;
use std::error::Error;
use std::fmt;

#[derive(Debug)]
pub enum DictError {
    ExceedsDictSize(usize),
    InvalidIndex(usize),
    EmptyInput,
}

impl fmt::Display for DictError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            DictError::ExceedsDictSize(size) => write!(f, "Dictionary size exceeds capacity: {}", size),
            DictError::InvalidIndex(idx) => write!(f, "Invalid dictionary index: {}", idx),
            DictError::EmptyInput => write!(f, "Empty input provided"),
        }
    }
}

impl Error for DictError {}

/// Dictionary encoder for string columns
/// Supports up to 65,536 unique strings (u16 indices)
pub struct DictionaryEncoder {
    dictionary: HashMap<String, u16>,
    reverse_dict: Vec<String>,
    encoded_values: Vec<u16>,
    statistics: EncodingStatistics,
}

#[derive(Debug, Clone, Default)]
pub struct EncodingStatistics {
    pub unique_values: usize,
    pub total_values: usize,
    pub cardinality_percent: f64,
    pub original_bytes: usize,
    pub encoded_bytes: usize,
}

impl DictionaryEncoder {
    /// Create new dictionary encoder from string values
    pub fn encode(values: &[String]) -> Result<Self, DictError> {
        if values.is_empty() {
            return Err(DictError::EmptyInput);
        }
        
        let mut dictionary = HashMap::new();
        let mut reverse_dict = Vec::new();
        let mut encoded_values = Vec::new();
        
        let original_bytes: usize = values.iter().map(|s| s.len()).sum();
        
        for value in values {
            if !dictionary.contains_key(value) {
                let idx = dictionary.len() as u16;
                
                // Limit dictionary to 65,536 entries (u16 max)
                if idx >= u16::MAX {
                    return Err(DictError::ExceedsDictSize(dictionary.len()));
                }
                
                dictionary.insert(value.clone(), idx);
                reverse_dict.push(value.clone());
            }
            
            encoded_values.push(dictionary[value]);
        }
        
        let unique_values = dictionary.len();
        let cardinality_percent = (unique_values as f64 / values.len() as f64) * 100.0;
        
        // Estimate encoded size: indices + dictionary
        let encoded_bytes = encoded_values.len() * 2 + // u16 per value
                          dictionary.values().map(|_| 1).sum::<usize>() * 2 + // u16 indices
                          reverse_dict.iter().map(|s| s.len()).sum::<usize>(); // dict strings
        
        let statistics = EncodingStatistics {
            unique_values,
            total_values: values.len(),
            cardinality_percent,
            original_bytes,
            encoded_bytes,
        };
        
        Ok(Self {
            dictionary,
            reverse_dict,
            encoded_values,
            statistics,
        })
    }
    
    /// Get encoded indices
    pub fn indices(&self) -> &[u16] {
        &self.encoded_values
    }
    
    /// Get dictionary (mapping of string -> index)
    pub fn dictionary(&self) -> &HashMap<String, u16> {
        &self.dictionary
    }
    
    /// Get reverse dictionary (mapping of index -> string)
    pub fn reverse_dictionary(&self) -> &[String] {
        &self.reverse_dict
    }
    
    /// Get encoding statistics
    pub fn statistics(&self) -> &EncodingStatistics {
        &self.statistics
    }
    
    /// Estimate compression savings
    pub fn compression_ratio(&self) -> f64 {
        self.statistics.encoded_bytes as f64 / self.statistics.original_bytes as f64
    }
    
    /// Serialize to bytes for storage
    pub fn serialize(&self) -> Vec<u8> {
        let mut result = Vec::new();
        
        // Header: num_entries (u16)
        result.extend_from_slice(&(self.reverse_dict.len() as u16).to_le_bytes());
        
        // Dictionary strings
        for entry in &self.reverse_dict {
            let bytes = entry.as_bytes();
            result.extend_from_slice(&(bytes.len() as u16).to_le_bytes());
            result.extend_from_slice(bytes);
        }
        
        // Number of encoded values
        result.extend_from_slice(&(self.encoded_values.len() as u32).to_le_bytes());
        
        // Encoded indices
        for idx in &self.encoded_values {
            result.extend_from_slice(&idx.to_le_bytes());
        }
        
        result
    }
}

/// Dictionary decoder for string columns
pub struct DictionaryDecoder {
    reverse_dict: Vec<String>,
    encoded_values: Vec<u16>,
}

impl DictionaryDecoder {
    /// Deserialize from bytes
    pub fn deserialize(data: &[u8]) -> Result<Self, DictError> {
        let mut pos = 0;
        
        // Read number of dictionary entries
        if data.len() < 2 {
            return Err(DictError::InvalidIndex(0));
        }
        let num_entries = u16::from_le_bytes([data[pos], data[pos + 1]]) as usize;
        pos += 2;
        
        // Read dictionary strings
        let mut reverse_dict = Vec::new();
        for _ in 0..num_entries {
            if pos + 2 > data.len() {
                return Err(DictError::InvalidIndex(pos));
            }
            let str_len = u16::from_le_bytes([data[pos], data[pos + 1]]) as usize;
            pos += 2;
            
            if pos + str_len > data.len() {
                return Err(DictError::InvalidIndex(pos));
            }
            let s = String::from_utf8_lossy(&data[pos..pos + str_len]).to_string();
            reverse_dict.push(s);
            pos += str_len;
        }
        
        // Read number of encoded values
        if pos + 4 > data.len() {
            return Err(DictError::InvalidIndex(pos));
        }
        let num_values = u32::from_le_bytes([
            data[pos],
            data[pos + 1],
            data[pos + 2],
            data[pos + 3],
        ]) as usize;
        pos += 4;
        
        // Read encoded indices
        let mut encoded_values = Vec::new();
        for _ in 0..num_values {
            if pos + 2 > data.len() {
                return Err(DictError::InvalidIndex(pos));
            }
            let idx = u16::from_le_bytes([data[pos], data[pos + 1]]);
            encoded_values.push(idx);
            pos += 2;
        }
        
        Ok(Self {
            reverse_dict,
            encoded_values,
        })
    }
    
    /// Decode all values back to original strings
    pub fn decode(&self) -> Result<Vec<String>, DictError> {
        let mut result = Vec::new();
        
        for &idx in &self.encoded_values {
            let idx_usize = idx as usize;
            if idx_usize >= self.reverse_dict.len() {
                return Err(DictError::InvalidIndex(idx_usize));
            }
            result.push(self.reverse_dict[idx_usize].clone());
        }
        
        Ok(result)
    }
    
    /// Decode single value by index
    pub fn decode_value(&self, index: u16) -> Result<String, DictError> {
        let idx_usize = index as usize;
        if idx_usize >= self.reverse_dict.len() {
            return Err(DictError::InvalidIndex(idx_usize));
        }
        Ok(self.reverse_dict[idx_usize].clone())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_dictionary_encoding() {
        let values = vec![
            "customer_1".to_string(),
            "customer_2".to_string(),
            "customer_1".to_string(),
            "customer_3".to_string(),
            "customer_1".to_string(),
        ];
        
        let encoder = DictionaryEncoder::encode(&values).unwrap();
        
        assert_eq!(encoder.statistics().unique_values, 3);
        assert_eq!(encoder.statistics().total_values, 5);
        assert_eq!(encoder.indices(), &[0, 1, 0, 2, 0]);
    }
    
    #[test]
    fn test_dictionary_roundtrip() {
        let values = vec![
            "apple".to_string(),
            "banana".to_string(),
            "apple".to_string(),
            "cherry".to_string(),
        ];
        
        let encoder = DictionaryEncoder::encode(&values).unwrap();
        let serialized = encoder.serialize();
        
        let decoder = DictionaryDecoder::deserialize(&serialized).unwrap();
        let decoded = decoder.decode().unwrap();
        
        assert_eq!(decoded, values);
    }
    
    #[test]
    fn test_compression_ratio() {
        let values = vec!["same".to_string(); 1000]; // 1000 identical strings
        let encoder = DictionaryEncoder::encode(&values).unwrap();
        
        let ratio = encoder.compression_ratio();
        // 1000 identical strings: 1000 u16 indices (2000 bytes) + small dict overhead
        // Original: 4 * 1000 = 4000 bytes, so ratio ~= 0.5 (50% compression)
        assert!(ratio < 0.6, "Should achieve significant compression on identical data");
    }
    
    #[test]
    fn test_empty_input() {
        let values: Vec<String> = vec![];
        let result = DictionaryEncoder::encode(&values);
        
        assert!(matches!(result, Err(DictError::EmptyInput)));
    }
}