Skip to main content

akar_storage/
compression.rs

1//! Column compression algorithms.
2//!
3//! Supports constant, one-value, boolean, integer bitpacking,
4//! string dictionary, float, and list delta compression.
5
6use crate::StringDictionary;
7use akar_common::enums::CompressionType;
8
9/// A compressed column chunk.
10#[derive(Debug, Clone)]
11pub struct CompressedChunk {
12    pub compression: CompressionType,
13    pub data: Vec<u8>,
14    pub num_values: usize,
15}
16
17/// Compress a byte slice using the given algorithm.
18pub fn compress(compression: CompressionType, data: &[u8], num_values: usize) -> CompressedChunk {
19    match compression {
20        CompressionType::Constant => compress_constant(data, num_values),
21        CompressionType::Boolean => compress_boolean(data, num_values),
22        CompressionType::IntegerBitpacking | CompressionType::ListDelta => {
23            // Determine value size from the data length
24            let value_size = data.len().checked_div(num_values).unwrap_or(8);
25            compress_integer_bitpacking(data, num_values, value_size)
26        }
27        CompressionType::Float => {
28            let value_size = data.len().checked_div(num_values).unwrap_or(4);
29            compress_float(data, num_values, value_size)
30        }
31        CompressionType::OneValue | CompressionType::Uncompressed => CompressedChunk {
32            compression,
33            data: data.to_vec(),
34            num_values,
35        },
36        CompressionType::StringDictionary => compress_string_dictionary(data, num_values),
37    }
38}
39
40/// Decompress a chunk back to raw bytes.
41pub fn decompress(chunk: &CompressedChunk, expected_size: usize) -> Vec<u8> {
42    match chunk.compression {
43        CompressionType::Constant => decompress_constant(&chunk.data, expected_size),
44        CompressionType::Boolean => decompress_boolean(&chunk.data, expected_size),
45        CompressionType::IntegerBitpacking => decompress_integer_bitpacking(&chunk.data, expected_size),
46        CompressionType::Float => decompress_float(&chunk.data, expected_size),
47        CompressionType::StringDictionary => decompress_string_dictionary(&chunk.data, expected_size),
48        _ => chunk.data.clone(),
49    }
50}
51
52// =====================================================================
53// String Dictionary — deduplicate strings via ID encoding
54// =====================================================================
55
56fn compress_string_dictionary(data: &[u8], num_values: usize) -> CompressedChunk {
57    let dict = StringDictionary::deserialize(data).unwrap_or_default();
58    let dict_bytes = dict.serialize();
59    CompressedChunk {
60        compression: CompressionType::StringDictionary,
61        data: dict_bytes,
62        num_values,
63    }
64}
65
66fn decompress_string_dictionary(data: &[u8], _expected_size: usize) -> Vec<u8> {
67    let dict = match StringDictionary::deserialize(data) {
68        Ok(d) => d,
69        Err(_) => return Vec::new(),
70    };
71    dict.serialize()
72}
73
74// =====================================================================
75// Integer Bitpacking — encode integers in the minimum bytes needed
76// =====================================================================
77//
78// Each integer is stored as: [num_bytes: u8][value_bytes...]
79// where num_bytes is the minimum number of bytes needed to represent
80// the value (1-8 for i64, 1-4 for i32, etc.).
81//
82// For batch compression of a page, we determine the common bit width
83// across all values and pack them tightly.
84
85/// Compress a single integer value using bitpacking.
86///
87/// Strips leading zero bytes from the LE representation.
88/// Format: [used_bytes: u8][significant_bytes...]
89fn compress_integer_impl(value_bytes: &[u8]) -> Vec<u8> {
90    let n = value_bytes.len();
91    // Find the last non-zero byte from the high end of the LE representation.
92    let significant = (0..n).rev().find(|&i| value_bytes[i] != 0).map_or(0, |i| i + 1);
93    let used = significant.max(1); // at least 1 byte
94    let mut out = Vec::with_capacity(1 + used);
95    out.push(used as u8);
96    out.extend_from_slice(&value_bytes[..used]);
97    out
98}
99
100/// Decompress a single integer value packed by `compress_integer_impl`.
101///
102/// Format: [used_bytes: u8][significant_bytes...]
103/// Returns the value expanded to `original_size` bytes (zero-extended).
104fn decompress_integer_impl(data: &[u8], original_size: usize) -> Vec<u8> {
105    if data.is_empty() {
106        return vec![0u8; original_size];
107    }
108    let used = data[0] as usize;
109    let used = used.min(original_size);
110    let mut out = vec![0u8; original_size];
111    let avail = data.len().saturating_sub(1);
112    let copy = used.min(avail);
113    out[..copy].copy_from_slice(&data[1..1 + copy]);
114    out
115}
116
117/// Integer bitpacking compression for a batch of values.
118/// Format: [value_size: u8][num_values: u32][packed_values...]
119/// Each packed value is [used_bytes: u8][significant_bytes...]
120pub fn compress_integer_bitpacking(data: &[u8], num_values: usize, value_size: usize) -> CompressedChunk {
121    let mut compressed = Vec::with_capacity(data.len());
122    compressed.push(value_size as u8);
123    compressed.extend_from_slice(&(num_values as u32).to_le_bytes());
124
125    let mut offset = 0;
126    for _ in 0..num_values {
127        if offset + value_size > data.len() {
128            break;
129        }
130        let val_bytes = &data[offset..offset + value_size];
131        let packed = compress_integer_impl(val_bytes);
132        compressed.extend_from_slice(&packed);
133        offset += value_size;
134    }
135
136    CompressedChunk {
137        compression: CompressionType::IntegerBitpacking,
138        data: compressed,
139        num_values,
140    }
141}
142
143fn decompress_integer_bitpacking(data: &[u8], expected_size: usize) -> Vec<u8> {
144    if data.len() < 5 {
145        return Vec::new();
146    }
147    let value_size = data[0] as usize;
148    let num_values = u32::from_le_bytes(data[1..5].try_into().unwrap()) as usize;
149    let mut result = Vec::with_capacity(expected_size.max(num_values * value_size));
150    let mut offset = 5;
151
152    for _ in 0..num_values {
153        if offset >= data.len() {
154            break;
155        }
156        let used = data[offset] as usize;
157        let total = 1 + used.min(value_size);
158        if offset + total > data.len() {
159            break;
160        }
161        let val_bytes = &data[offset..offset + total];
162        let expanded = decompress_integer_impl(val_bytes, value_size);
163        result.extend_from_slice(&expanded);
164        offset += total;
165    }
166
167    result
168}
169
170// =====================================================================
171// Float compression (Delta/Offset + Integer Bitpacking)
172// =====================================================================
173
174#[repr(u8)]
175#[derive(Debug, Clone, Copy, PartialEq, Eq)]
176pub enum FloatCompressionStrategy {
177    Raw = 0,
178    Delta = 1,
179    Offset = 2,
180}
181
182/// Float compression: uses integer bitpacking on delta/offset representations.
183/// Format: [value_size: u8][num_values: u32][strategy: u8][payload]
184pub fn compress_float(data: &[u8], num_values: usize, value_size: usize) -> CompressedChunk {
185    if num_values == 0 || (value_size != 4 && value_size != 8) {
186        return compress_float_raw(data, num_values, value_size);
187    }
188
189    let mut ints_u32 = Vec::new();
190    let mut ints_u64 = Vec::new();
191
192    if value_size == 4 {
193        ints_u32.reserve(num_values);
194        let mut offset = 0;
195        for _ in 0..num_values {
196            if offset + 4 > data.len() {
197                break;
198            }
199            ints_u32.push(u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()));
200            offset += 4;
201        }
202    } else {
203        ints_u64.reserve(num_values);
204        let mut offset = 0;
205        for _ in 0..num_values {
206            if offset + 8 > data.len() {
207                break;
208            }
209            ints_u64.push(u64::from_le_bytes(data[offset..offset + 8].try_into().unwrap()));
210            offset += 8;
211        }
212    }
213
214    let actual_values = if value_size == 4 {
215        ints_u32.len()
216    } else {
217        ints_u64.len()
218    };
219    if actual_values == 0 {
220        return compress_float_raw(data, num_values, value_size);
221    }
222
223    let mut offset_data = Vec::with_capacity(actual_values * value_size);
224    let mut delta_data = Vec::with_capacity(actual_values * value_size);
225
226    if value_size == 4 {
227        let min_val = *ints_u32.iter().min().unwrap_or(&0);
228        for &v in &ints_u32 {
229            let diff = v.wrapping_sub(min_val);
230            offset_data.extend_from_slice(&diff.to_le_bytes());
231        }
232
233        let mut prev = 0u32;
234        for (i, &v) in ints_u32.iter().enumerate() {
235            let diff = if i == 0 { v } else { v.wrapping_sub(prev) };
236            delta_data.extend_from_slice(&diff.to_le_bytes());
237            prev = v;
238        }
239    } else {
240        let min_val = *ints_u64.iter().min().unwrap_or(&0);
241        for &v in &ints_u64 {
242            let diff = v.wrapping_sub(min_val);
243            offset_data.extend_from_slice(&diff.to_le_bytes());
244        }
245
246        let mut prev = 0u64;
247        for (i, &v) in ints_u64.iter().enumerate() {
248            let diff = if i == 0 { v } else { v.wrapping_sub(prev) };
249            delta_data.extend_from_slice(&diff.to_le_bytes());
250            prev = v;
251        }
252    }
253
254    let offset_chunk = compress_integer_bitpacking(&offset_data, actual_values, value_size);
255    let delta_chunk = compress_integer_bitpacking(&delta_data, actual_values, value_size);
256
257    let raw_len = actual_values * value_size;
258    let offset_payload_len = value_size + offset_chunk.data.len().saturating_sub(5);
259    let delta_payload_len = delta_chunk.data.len().saturating_sub(5);
260
261    let min_len = raw_len.min(offset_payload_len).min(delta_payload_len);
262
263    let mut compressed = Vec::new();
264    compressed.push(value_size as u8);
265    compressed.extend_from_slice(&(num_values as u32).to_le_bytes());
266
267    if min_len == raw_len {
268        compressed.push(FloatCompressionStrategy::Raw as u8);
269        compressed.extend_from_slice(&data[..raw_len]);
270    } else if min_len == offset_payload_len {
271        compressed.push(FloatCompressionStrategy::Offset as u8);
272        if value_size == 4 {
273            let min_val = *ints_u32.iter().min().unwrap();
274            compressed.extend_from_slice(&min_val.to_le_bytes());
275        } else {
276            let min_val = *ints_u64.iter().min().unwrap();
277            compressed.extend_from_slice(&min_val.to_le_bytes());
278        }
279        if offset_chunk.data.len() >= 5 {
280            compressed.extend_from_slice(&offset_chunk.data[5..]);
281        }
282    } else {
283        compressed.push(FloatCompressionStrategy::Delta as u8);
284        if delta_chunk.data.len() >= 5 {
285            compressed.extend_from_slice(&delta_chunk.data[5..]);
286        }
287    }
288
289    CompressedChunk {
290        compression: CompressionType::Float,
291        data: compressed,
292        num_values,
293    }
294}
295
296fn compress_float_raw(data: &[u8], num_values: usize, value_size: usize) -> CompressedChunk {
297    let mut compressed = Vec::with_capacity(6 + data.len());
298    compressed.push(value_size as u8);
299    compressed.extend_from_slice(&(num_values as u32).to_le_bytes());
300    compressed.push(FloatCompressionStrategy::Raw as u8);
301    let byte_count = num_values * value_size;
302    compressed.extend_from_slice(&data[..byte_count.min(data.len())]);
303
304    CompressedChunk {
305        compression: CompressionType::Float,
306        data: compressed,
307        num_values,
308    }
309}
310
311fn decompress_float(data: &[u8], expected_size: usize) -> Vec<u8> {
312    if data.len() < 6 {
313        return Vec::new();
314    }
315    let value_size = data[0] as usize;
316    let num_values = u32::from_le_bytes(data[1..5].try_into().unwrap()) as usize;
317    let strategy = data[5];
318    let expected = expected_size.max(num_values * value_size);
319
320    if strategy == FloatCompressionStrategy::Raw as u8 {
321        let mut result = vec![0u8; expected];
322        let avail = data.len().saturating_sub(6);
323        let byte_count = num_values * value_size;
324        let copy = byte_count.min(avail);
325        result[..copy].copy_from_slice(&data[6..6 + copy]);
326        return result;
327    }
328
329    let mut int_pack_data = Vec::new();
330    int_pack_data.push(value_size as u8);
331    int_pack_data.extend_from_slice(&(num_values as u32).to_le_bytes());
332
333    if strategy == FloatCompressionStrategy::Offset as u8 {
334        let mut offset_idx = 6;
335        if offset_idx + value_size > data.len() {
336            return Vec::new();
337        }
338        let min_val_bytes = &data[offset_idx..offset_idx + value_size];
339        offset_idx += value_size;
340        int_pack_data.extend_from_slice(&data[offset_idx..]);
341
342        let unpacked = decompress_integer_bitpacking(&int_pack_data, num_values * value_size);
343        let mut result = vec![0u8; expected];
344        if value_size == 4 {
345            let min_val = u32::from_le_bytes(min_val_bytes.try_into().unwrap());
346            for i in 0..num_values {
347                if i * 4 + 4 > unpacked.len() {
348                    break;
349                }
350                let diff = u32::from_le_bytes(unpacked[i * 4..i * 4 + 4].try_into().unwrap());
351                let val = diff.wrapping_add(min_val);
352                result[i * 4..i * 4 + 4].copy_from_slice(&val.to_le_bytes());
353            }
354        } else {
355            let min_val = u64::from_le_bytes(min_val_bytes.try_into().unwrap());
356            for i in 0..num_values {
357                if i * 8 + 8 > unpacked.len() {
358                    break;
359                }
360                let diff = u64::from_le_bytes(unpacked[i * 8..i * 8 + 8].try_into().unwrap());
361                let val = diff.wrapping_add(min_val);
362                result[i * 8..i * 8 + 8].copy_from_slice(&val.to_le_bytes());
363            }
364        }
365        return result;
366    }
367
368    if strategy == FloatCompressionStrategy::Delta as u8 {
369        int_pack_data.extend_from_slice(&data[6..]);
370        let unpacked = decompress_integer_bitpacking(&int_pack_data, num_values * value_size);
371
372        let mut result = vec![0u8; expected];
373        if value_size == 4 {
374            let mut prev = 0u32;
375            for i in 0..num_values {
376                if i * 4 + 4 > unpacked.len() {
377                    break;
378                }
379                let diff = u32::from_le_bytes(unpacked[i * 4..i * 4 + 4].try_into().unwrap());
380                let val = if i == 0 { diff } else { prev.wrapping_add(diff) };
381                result[i * 4..i * 4 + 4].copy_from_slice(&val.to_le_bytes());
382                prev = val;
383            }
384        } else {
385            let mut prev = 0u64;
386            for i in 0..num_values {
387                if i * 8 + 8 > unpacked.len() {
388                    break;
389                }
390                let diff = u64::from_le_bytes(unpacked[i * 8..i * 8 + 8].try_into().unwrap());
391                let val = if i == 0 { diff } else { prev.wrapping_add(diff) };
392                result[i * 8..i * 8 + 8].copy_from_slice(&val.to_le_bytes());
393                prev = val;
394            }
395        }
396        return result;
397    }
398
399    Vec::new()
400}
401
402// =====================================================================
403// Individual value compression (for Column integration)
404// =====================================================================
405
406/// Compress a single serialized value.
407///
408/// The serialized format is: [tag: u8][payload...]
409/// For IntegerBitpacking/Float, only the payload (after the tag) is compressed.
410/// The tag byte is always preserved as the first byte of the output.
411///
412/// Returns: [tag: u8][compressed_payload...]
413/// For pass-through types, the payload is unchanged.
414pub fn compress_serialized_value(compression: CompressionType, raw: &[u8], value_size: usize) -> Vec<u8> {
415    if raw.is_empty() {
416        return Vec::new();
417    }
418    let tag = raw[0];
419    let payload = &raw[1..];
420
421    match compression {
422        CompressionType::IntegerBitpacking if value_size > 0 && value_size <= 8 => {
423            // Compress only the payload, keep the tag
424            let packed = compress_integer_impl(payload);
425            let mut out = Vec::with_capacity(1 + packed.len());
426            out.push(tag);
427            out.extend_from_slice(&packed);
428            out
429        }
430        CompressionType::Float if value_size > 0 && value_size <= 8 => {
431            // Float pass-through: keep tag + raw payload
432            let mut out = Vec::with_capacity(1 + 1 + payload.len());
433            out.push(tag);
434            out.push(payload.len() as u8);
435            out.extend_from_slice(payload);
436            out
437        }
438        _ => {
439            // Pass-through: keep tag + payload as-is
440            let mut out = Vec::with_capacity(raw.len());
441            out.extend_from_slice(raw);
442            out
443        }
444    }
445}
446
447/// Decompress a single serialized value.
448///
449/// Input format: [tag: u8][compressed_payload...]
450/// Output format: [tag: u8][decompressed_payload...]
451pub fn decompress_serialized_value(compression: CompressionType, compressed: &[u8], value_size: usize) -> Vec<u8> {
452    if compressed.is_empty() {
453        return Vec::new();
454    }
455    let tag = compressed[0];
456    let stored_payload = &compressed[1..];
457
458    match compression {
459        CompressionType::IntegerBitpacking if value_size > 0 && value_size <= 8 => {
460            let expanded = decompress_integer_impl(stored_payload, value_size);
461            let mut out = Vec::with_capacity(1 + expanded.len());
462            out.push(tag);
463            out.extend_from_slice(&expanded);
464            out
465        }
466        CompressionType::Float if value_size > 0 && value_size <= 8 => {
467            // Float stored as [tag][len_byte][payload...]
468            if stored_payload.is_empty() {
469                return compressed.to_vec();
470            }
471            let len = stored_payload[0] as usize;
472            let len = len.min(stored_payload.len().saturating_sub(1));
473            let mut out = Vec::with_capacity(1 + len);
474            out.push(tag);
475            out.extend_from_slice(&stored_payload[1..1 + len]);
476            out
477        }
478        _ => {
479            // Pass-through: return as-is
480            compressed.to_vec()
481        }
482    }
483}
484
485/// Determine the byte size of a serialized primitive value based on its
486/// physical type. Used to know how many bytes to expect for compression.
487pub fn serialized_value_size(physical_type: akar_common::types::PhysicalTypeID) -> usize {
488    use akar_common::types::PhysicalTypeID;
489    match physical_type {
490        PhysicalTypeID::Int64 | PhysicalTypeID::UInt64 | PhysicalTypeID::Double => 8,
491        PhysicalTypeID::Int32 | PhysicalTypeID::UInt32 | PhysicalTypeID::Float => 4,
492        PhysicalTypeID::Int16 | PhysicalTypeID::UInt16 => 2,
493        PhysicalTypeID::Int8 | PhysicalTypeID::UInt8 | PhysicalTypeID::Bool => 1,
494        PhysicalTypeID::Interval => 16,
495        _ => 0, // variable-length types (string, list, struct, etc.)
496    }
497}
498
499/// Constant compression: for columns where all values are the same.
500/// Format: [num_vals: u32][value_bytes...]
501fn compress_constant(data: &[u8], num_values: usize) -> CompressedChunk {
502    let val_size = if data.is_empty() {
503        0
504    } else {
505        data.len() / num_values.max(1)
506    };
507    let mut compressed = Vec::with_capacity(4 + val_size);
508    compressed.extend_from_slice(&(num_values as u32).to_le_bytes());
509    if val_size > 0 {
510        compressed.extend_from_slice(&data[..val_size]);
511    }
512    CompressedChunk {
513        compression: CompressionType::Constant,
514        data: compressed,
515        num_values,
516    }
517}
518
519fn decompress_constant(data: &[u8], expected_size: usize) -> Vec<u8> {
520    if data.len() < 4 {
521        return Vec::new();
522    }
523    let mut arr = [0u8; 4];
524    arr.copy_from_slice(&data[..4]);
525    let num_vals = u32::from_le_bytes(arr) as usize;
526    let val_bytes = &data[4..];
527    let mut result = Vec::with_capacity(expected_size);
528    for _ in 0..num_vals {
529        result.extend_from_slice(val_bytes);
530    }
531    result
532}
533
534/// Boolean compression: pack 8 booleans per byte.
535fn compress_boolean(data: &[u8], num_values: usize) -> CompressedChunk {
536    let packed_len = num_values.div_ceil(8);
537    let mut packed = vec![0u8; packed_len];
538    for i in 0..num_values.min(data.len()) {
539        if data[i] != 0 {
540            packed[i / 8] |= 1 << (i % 8);
541        }
542    }
543    CompressedChunk {
544        compression: CompressionType::Boolean,
545        data: packed,
546        num_values,
547    }
548}
549
550fn decompress_boolean(data: &[u8], num_values: usize) -> Vec<u8> {
551    let mut result = vec![0u8; num_values];
552    for i in 0..num_values {
553        result[i] = if data[i / 8] & (1 << (i % 8)) != 0 { 1 } else { 0 };
554    }
555    result
556}
557
558#[cfg(test)]
559mod tests {
560    use super::*;
561
562    #[test]
563    fn test_constant_roundtrip() {
564        let original = vec![42u8; 100];
565        let chunk = compress(CompressionType::Constant, &original, 100);
566        assert!(chunk.data.len() < original.len());
567        let dec = decompress(&chunk, 100);
568        assert_eq!(dec.len(), 100);
569        assert_eq!(dec[0], 42);
570        assert_eq!(dec[99], 42);
571    }
572
573    #[test]
574    fn test_boolean_roundtrip() {
575        let mut original = vec![0u8; 16];
576        original[0] = 1;
577        original[7] = 1;
578        original[15] = 1;
579        let chunk = compress(CompressionType::Boolean, &original, 16);
580        assert!(chunk.data.len() < original.len());
581        let dec = decompress(&chunk, 16);
582        assert_eq!(dec[0], 1);
583        assert_eq!(dec[7], 1);
584        assert_eq!(dec[15], 1);
585        assert_eq!(dec[1], 0);
586        assert_eq!(dec[8], 0);
587    }
588
589    #[test]
590    fn test_uncompressed_roundtrip() {
591        let original = vec![1u8, 2, 3, 4, 5];
592        let chunk = compress(CompressionType::Uncompressed, &original, 5);
593        assert_eq!(chunk.data, original);
594        let dec = decompress(&chunk, 5);
595        assert_eq!(dec, original);
596    }
597
598    // --- Integer bitpacking ---
599
600    #[test]
601    fn test_compress_integer_small() {
602        // i64 value 42 = 0x2A in LE → 1 significant byte
603        let val = 42i64.to_le_bytes();
604        let packed = super::compress_integer_impl(&val);
605        assert_eq!(packed[0], 1); // 1 byte used
606        assert_eq!(packed[1], 42);
607    }
608
609    #[test]
610    fn test_compress_integer_large() {
611        // i64 value 0x12345678 = needs 4 bytes in LE
612        let val = 0x12345678i64.to_le_bytes();
613        let packed = super::compress_integer_impl(&val);
614        assert_eq!(packed[0], 4); // 4 bytes used
615        // First 4 bytes should match
616        assert_eq!(&packed[1..5], &val[..4]);
617    }
618
619    #[test]
620    fn test_compress_integer_negative() {
621        // i64 -1 = 0xFF..FF in LE → all 8 bytes significant
622        let val = (-1i64).to_le_bytes();
623        let packed = super::compress_integer_impl(&val);
624        assert_eq!(packed[0], 8); // all 8 bytes
625    }
626
627    #[test]
628    fn test_integer_roundtrip_batch() {
629        let values: Vec<i64> = vec![0, 1, 42, 127, 255, 1000, 65535, 100000, -1, -128];
630        let value_size = 8;
631        let mut data = Vec::with_capacity(values.len() * value_size);
632        for v in &values {
633            data.extend_from_slice(&v.to_le_bytes());
634        }
635        let chunk = compress(CompressionType::IntegerBitpacking, &data, values.len());
636        let dec = decompress(&chunk, values.len() * value_size);
637        assert_eq!(dec.len(), values.len() * value_size);
638        for (i, v) in values.iter().enumerate() {
639            let val = i64::from_le_bytes(dec[i * value_size..(i + 1) * value_size].try_into().unwrap());
640            assert_eq!(val, *v, "mismatch at index {}", i);
641        }
642    }
643
644    #[test]
645    fn test_single_value_compress_decompress() {
646        // Simulate a serialized value with tag byte (Int64 = 0x02)
647        let mut raw = vec![0x02u8]; // tag byte
648        raw.extend_from_slice(&42i64.to_le_bytes()); // payload
649        assert_eq!(raw.len(), 9);
650
651        let compressed = super::compress_serialized_value(
652            CompressionType::IntegerBitpacking,
653            &raw,
654            8, // value_size for i64
655        );
656        // Tag byte should be preserved
657        assert_eq!(compressed[0], 0x02);
658        // Compressed payload should be smaller than 8 bytes for value 42
659        assert!(compressed.len() < raw.len(), "compression should reduce size");
660
661        let decompressed = super::decompress_serialized_value(CompressionType::IntegerBitpacking, &compressed, 8);
662        assert_eq!(decompressed, raw, "full roundtrip should match original");
663
664        // Verify value roundtrips correctly
665        let restored = i64::from_le_bytes(decompressed[1..9].try_into().unwrap());
666        assert_eq!(restored, 42);
667    }
668
669    // --- Float compression ---
670
671    #[test]
672    fn test_float_batch_roundtrip_raw() {
673        // High variance, will likely use Raw
674        let values: Vec<f64> = vec![1.0, 3.15, -2.5, 0.0, 1e10, f64::MAX, f64::MIN];
675        let value_size = 8;
676        let mut data = Vec::with_capacity(values.len() * value_size);
677        for v in &values {
678            data.extend_from_slice(&v.to_le_bytes());
679        }
680        let chunk = compress(CompressionType::Float, &data, values.len());
681        assert_eq!(chunk.data[5], FloatCompressionStrategy::Raw as u8);
682        let dec = decompress(&chunk, values.len() * value_size);
683        for (i, v) in values.iter().enumerate() {
684            let val = f64::from_le_bytes(dec[i * value_size..(i + 1) * value_size].try_into().unwrap());
685            assert_eq!(val.to_bits(), v.to_bits(), "mismatch at index {}", i);
686        }
687    }
688
689    #[test]
690    fn test_float_batch_roundtrip_delta() {
691        // Linearly increasing, suitable for Delta compression
692        let mut values: Vec<f32> = Vec::new();
693        for i in 0..100 {
694            values.push(i as f32 * 1.5);
695        }
696        let value_size = 4;
697        let mut data = Vec::with_capacity(values.len() * value_size);
698        for v in &values {
699            data.extend_from_slice(&v.to_le_bytes());
700        }
701        let chunk = compress(CompressionType::Float, &data, values.len());
702        assert_eq!(chunk.data[5], FloatCompressionStrategy::Delta as u8);
703        assert!(chunk.data.len() < data.len() + 6);
704        let dec = decompress(&chunk, values.len() * value_size);
705        for (i, v) in values.iter().enumerate() {
706            let val = f32::from_le_bytes(dec[i * value_size..(i + 1) * value_size].try_into().unwrap());
707            assert_eq!(val.to_bits(), v.to_bits(), "mismatch at index {}", i);
708        }
709    }
710
711    #[test]
712    fn test_float_batch_roundtrip_offset() {
713        // Near values, suitable for Offset compression
714        let values: Vec<f64> = vec![1000.1, 1000.15, 1000.0, 1000.05, 1000.2];
715        let value_size = 8;
716        let mut data = Vec::with_capacity(values.len() * value_size);
717        for v in &values {
718            data.extend_from_slice(&v.to_le_bytes());
719        }
720        let chunk = compress(CompressionType::Float, &data, values.len());
721        // Could be offset or delta depending on sizes, both should compress well
722        assert!(
723            chunk.data[5] == FloatCompressionStrategy::Offset as u8
724                || chunk.data[5] == FloatCompressionStrategy::Delta as u8
725        );
726        let dec = decompress(&chunk, values.len() * value_size);
727        for (i, v) in values.iter().enumerate() {
728            let val = f64::from_le_bytes(dec[i * value_size..(i + 1) * value_size].try_into().unwrap());
729            assert_eq!(val.to_bits(), v.to_bits(), "mismatch at index {}", i);
730        }
731    }
732
733    // --- Pass-through behavior ---
734
735    #[test]
736    fn test_pass_through_roundtrip() {
737        // String data (tag + payload) with IntegerBitpacking as pass-through
738        // since value_size=0 for variable-length types
739        let mut raw = vec![0x0D]; // TAG_STRING
740        raw.extend_from_slice(b"hello world");
741        let compressed = super::compress_serialized_value(
742            CompressionType::IntegerBitpacking,
743            &raw,
744            0, // value_size=0 for variable-length
745        );
746        // Should be pass-through (unchanged)
747        assert_eq!(compressed, raw);
748        let decompressed = super::decompress_serialized_value(CompressionType::IntegerBitpacking, &compressed, 0);
749        assert_eq!(decompressed, raw);
750    }
751
752    #[test]
753    fn test_compression_metadata() {
754        // Verify Constant compression shrinks data
755        let data = vec![42u8; 100];
756        let chunk = compress(CompressionType::Constant, &data, 100);
757        assert_eq!(chunk.compression, CompressionType::Constant);
758        assert_eq!(chunk.num_values, 100);
759        assert!(chunk.data.len() < data.len());
760    }
761}