Skip to main content

oxirs_tdb/compression/
prefix.rs

1use crate::error::{Result, TdbError};
2use oxicode::Decode;
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6/// Prefix compression for dictionary strings
7///
8/// Stores common prefixes separately to reduce storage overhead
9/// for IRIs sharing the same namespace.
10#[derive(Debug, Clone)]
11pub struct PrefixCompressor {
12    /// Prefix dictionary: prefix_id -> prefix string
13    prefixes: HashMap<u32, String>,
14    /// Reverse mapping: prefix -> prefix_id
15    prefix_to_id: HashMap<String, u32>,
16    /// Next prefix ID
17    next_prefix_id: u32,
18    /// Minimum prefix length to compress
19    min_prefix_len: usize,
20}
21
22/// Compressed string representation
23#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
24pub struct CompressedString {
25    /// Prefix ID (0 = no prefix)
26    pub prefix_id: u32,
27    /// Suffix string
28    pub suffix: String,
29}
30
31impl PrefixCompressor {
32    /// Create a new prefix compressor
33    pub fn new(min_prefix_len: usize) -> Self {
34        Self {
35            prefixes: HashMap::new(),
36            prefix_to_id: HashMap::new(),
37            next_prefix_id: 1,
38            min_prefix_len,
39        }
40    }
41
42    /// Compress a string by extracting common prefix
43    pub fn compress(&mut self, s: &str) -> Result<CompressedString> {
44        // Try to find longest matching prefix
45        let mut best_prefix_id = 0;
46        let mut best_prefix_len = 0;
47
48        for (prefix, &prefix_id) in &self.prefix_to_id {
49            if s.starts_with(prefix) && prefix.len() > best_prefix_len {
50                best_prefix_id = prefix_id;
51                best_prefix_len = prefix.len();
52            }
53        }
54
55        if best_prefix_len > 0 {
56            // Use existing prefix
57            Ok(CompressedString {
58                prefix_id: best_prefix_id,
59                suffix: s[best_prefix_len..].to_string(),
60            })
61        } else if s.len() >= self.min_prefix_len {
62            // Try to create new prefix from common namespace patterns
63            if let Some(prefix_end) = self.find_prefix_boundary(s) {
64                let prefix = &s[..prefix_end];
65                let prefix_id = self.add_prefix(prefix);
66
67                Ok(CompressedString {
68                    prefix_id,
69                    suffix: s[prefix_end..].to_string(),
70                })
71            } else {
72                // No prefix found
73                Ok(CompressedString {
74                    prefix_id: 0,
75                    suffix: s.to_string(),
76                })
77            }
78        } else {
79            // String too short to compress
80            Ok(CompressedString {
81                prefix_id: 0,
82                suffix: s.to_string(),
83            })
84        }
85    }
86
87    /// Decompress a compressed string
88    pub fn decompress(&self, compressed: &CompressedString) -> Result<String> {
89        if compressed.prefix_id == 0 {
90            Ok(compressed.suffix.clone())
91        } else {
92            let prefix = self.prefixes.get(&compressed.prefix_id).ok_or_else(|| {
93                TdbError::Other(format!("Prefix {} not found", compressed.prefix_id))
94            })?;
95
96            Ok(format!("{}{}", prefix, compressed.suffix))
97        }
98    }
99
100    /// Add a new prefix
101    fn add_prefix(&mut self, prefix: &str) -> u32 {
102        if let Some(&id) = self.prefix_to_id.get(prefix) {
103            return id;
104        }
105
106        let id = self.next_prefix_id;
107        self.next_prefix_id += 1;
108
109        self.prefixes.insert(id, prefix.to_string());
110        self.prefix_to_id.insert(prefix.to_string(), id);
111
112        id
113    }
114
115    /// Find prefix boundary (e.g., after namespace separator)
116    fn find_prefix_boundary(&self, s: &str) -> Option<usize> {
117        // For IRIs, try to split at '#' or last '/'
118        if let Some(pos) = s.rfind('#') {
119            return Some(pos + 1);
120        }
121
122        if let Some(pos) = s.rfind('/') {
123            if pos + 1 < s.len() {
124                return Some(pos + 1);
125            }
126        }
127
128        None
129    }
130
131    /// Get compression statistics
132    pub fn stats(&self) -> CompressionStats {
133        CompressionStats {
134            num_prefixes: self.prefixes.len(),
135            total_prefix_bytes: self.prefixes.values().map(|p| p.len()).sum(),
136        }
137    }
138
139    /// Get number of prefixes
140    pub fn num_prefixes(&self) -> usize {
141        self.prefixes.len()
142    }
143
144    /// Get a prefix by ID
145    pub fn get_prefix(&self, prefix_id: u32) -> Option<&String> {
146        self.prefixes.get(&prefix_id)
147    }
148}
149
150impl Default for PrefixCompressor {
151    fn default() -> Self {
152        Self::new(10) // Default minimum prefix length
153    }
154}
155
156/// Compression statistics
157#[derive(Debug, Clone, Copy)]
158pub struct CompressionStats {
159    /// Number of unique prefixes
160    pub num_prefixes: usize,
161    /// Total bytes used by prefixes
162    pub total_prefix_bytes: usize,
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    #[test]
170    fn test_prefix_compression_basic() {
171        let mut compressor = PrefixCompressor::new(10);
172
173        let iri1 = "http://example.org/Person";
174        let iri2 = "http://example.org/name";
175
176        let c1 = compressor.compress(iri1).unwrap();
177        let c2 = compressor.compress(iri2).unwrap();
178
179        // Both should share the same prefix
180        assert_eq!(c1.prefix_id, c2.prefix_id);
181        assert!(c1.prefix_id > 0);
182
183        assert_eq!(c1.suffix, "Person");
184        assert_eq!(c2.suffix, "name");
185
186        let d1 = compressor.decompress(&c1).unwrap();
187        let d2 = compressor.decompress(&c2).unwrap();
188
189        assert_eq!(d1, iri1);
190        assert_eq!(d2, iri2);
191    }
192
193    #[test]
194    fn test_prefix_compression_no_prefix() {
195        let mut compressor = PrefixCompressor::new(10);
196
197        let short_str = "abc";
198        let c = compressor.compress(short_str).unwrap();
199
200        assert_eq!(c.prefix_id, 0);
201        assert_eq!(c.suffix, "abc");
202
203        let d = compressor.decompress(&c).unwrap();
204        assert_eq!(d, short_str);
205    }
206
207    #[test]
208    fn test_prefix_compression_hash_iri() {
209        let mut compressor = PrefixCompressor::new(10);
210
211        let iri = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";
212        let c = compressor.compress(iri).unwrap();
213
214        assert!(c.prefix_id > 0);
215        assert_eq!(c.suffix, "type");
216
217        let d = compressor.decompress(&c).unwrap();
218        assert_eq!(d, iri);
219    }
220
221    #[test]
222    fn test_prefix_compression_multiple_prefixes() {
223        let mut compressor = PrefixCompressor::new(10);
224
225        let iri1 = "http://example.org/Person";
226        let iri2 = "http://xmlns.com/foaf/0.1/name";
227
228        let c1 = compressor.compress(iri1).unwrap();
229        let c2 = compressor.compress(iri2).unwrap();
230
231        // Different prefixes
232        assert_ne!(c1.prefix_id, c2.prefix_id);
233        assert!(c1.prefix_id > 0);
234        assert!(c2.prefix_id > 0);
235
236        assert_eq!(compressor.num_prefixes(), 2);
237    }
238
239    #[test]
240    fn test_prefix_compression_stats() {
241        let mut compressor = PrefixCompressor::new(10);
242
243        compressor.compress("http://example.org/Person").unwrap();
244        compressor.compress("http://example.org/name").unwrap();
245
246        let stats = compressor.stats();
247        assert_eq!(stats.num_prefixes, 1);
248        assert!(stats.total_prefix_bytes > 0);
249    }
250
251    #[test]
252    fn test_prefix_reuse() {
253        let mut compressor = PrefixCompressor::new(10);
254
255        let iri1 = "http://example.org/Person";
256        let iri2 = "http://example.org/name";
257        let iri3 = "http://example.org/age";
258
259        let c1 = compressor.compress(iri1).unwrap();
260        let c2 = compressor.compress(iri2).unwrap();
261        let c3 = compressor.compress(iri3).unwrap();
262
263        assert_eq!(c1.prefix_id, c2.prefix_id);
264        assert_eq!(c2.prefix_id, c3.prefix_id);
265        assert_eq!(compressor.num_prefixes(), 1);
266    }
267
268    #[test]
269    fn test_prefix_get() {
270        let mut compressor = PrefixCompressor::new(10);
271
272        let iri = "http://example.org/Person";
273        let c = compressor.compress(iri).unwrap();
274
275        let prefix = compressor.get_prefix(c.prefix_id).unwrap();
276        assert_eq!(prefix, "http://example.org/");
277    }
278
279    #[test]
280    fn test_compression_serialization() {
281        let compressed = CompressedString {
282            prefix_id: 1,
283            suffix: "Person".to_string(),
284        };
285
286        let serialized =
287            oxicode::serde::encode_to_vec(&compressed, oxicode::config::standard()).unwrap();
288        let deserialized: CompressedString =
289            oxicode::serde::decode_from_slice(&serialized, oxicode::config::standard())
290                .unwrap()
291                .0;
292
293        assert_eq!(compressed, deserialized);
294    }
295}