lance_encoding/encodings/physical/
fsst.rs1use lance_core::{Error, Result};
19
20use crate::{
21 buffer::LanceBuffer,
22 compression::{MiniBlockDecompressor, VariablePerValueDecompressor},
23 data::{BlockInfo, DataBlock, VariableWidthBlock},
24 encodings::logical::primitive::{
25 fullzip::{PerValueCompressor, PerValueDataBlock},
26 miniblock::{MiniBlockCompressed, MiniBlockCompressionContext, MiniBlockCompressor},
27 },
28 format::{
29 EncodingUtils,
30 metadata::{self, CompressiveEncoding},
31 },
32};
33
34use super::binary::BinaryMiniBlockEncoder;
35
36pub(crate) fn map_fsst_error(err: std::io::Error) -> Error {
37 match err.kind() {
38 std::io::ErrorKind::InvalidData => Error::corrupt_file_named("fsst", err.to_string()),
39 _ => err.into(),
40 }
41}
42
43struct FsstCompressed {
44 data: VariableWidthBlock,
45 symbol_table: Vec<u8>,
46}
47
48impl FsstCompressed {
49 fn fsst_compress(data: DataBlock) -> Result<Self> {
50 match data {
51 DataBlock::VariableWidth(variable_width) => {
52 match variable_width.bits_per_offset {
53 32 => {
54 let offsets = variable_width.offsets.borrow_to_typed_slice::<i32>();
55 let offsets_slice = offsets.as_ref();
56 let bytes_data = variable_width.data.into_buffer();
57
58 let mut dest_offsets = vec![0_i32; offsets_slice.len() * 2];
60 let mut dest_values = vec![0_u8; bytes_data.len() * 2];
61 let mut symbol_table = vec![0_u8; fsst::fsst::FSST_SYMBOL_TABLE_SIZE];
62
63 fsst::fsst::compress(
65 &mut symbol_table,
66 bytes_data.as_slice(),
67 offsets_slice,
68 &mut dest_values,
69 &mut dest_offsets,
70 )?;
71
72 let compressed = VariableWidthBlock {
74 data: LanceBuffer::reinterpret_vec(dest_values),
75 bits_per_offset: 32,
76 offsets: LanceBuffer::reinterpret_vec(dest_offsets),
77 num_values: variable_width.num_values,
78 block_info: BlockInfo::new(),
79 };
80
81 Ok(Self {
82 data: compressed,
83 symbol_table,
84 })
85 }
86 64 => {
87 let offsets = variable_width.offsets.borrow_to_typed_slice::<i64>();
88 let offsets_slice = offsets.as_ref();
89 let bytes_data = variable_width.data.into_buffer();
90
91 let mut dest_offsets = vec![0_i64; offsets_slice.len() * 2];
93 let mut dest_values = vec![0_u8; bytes_data.len() * 2];
94 let mut symbol_table = vec![0_u8; fsst::fsst::FSST_SYMBOL_TABLE_SIZE];
95
96 fsst::fsst::compress(
98 &mut symbol_table,
99 bytes_data.as_slice(),
100 offsets_slice,
101 &mut dest_values,
102 &mut dest_offsets,
103 )?;
104
105 let compressed = VariableWidthBlock {
107 data: LanceBuffer::reinterpret_vec(dest_values),
108 bits_per_offset: 64,
109 offsets: LanceBuffer::reinterpret_vec(dest_offsets),
110 num_values: variable_width.num_values,
111 block_info: BlockInfo::new(),
112 };
113
114 Ok(Self {
115 data: compressed,
116 symbol_table,
117 })
118 }
119 _ => panic!(
120 "Unsupported offsets type {}",
121 variable_width.bits_per_offset
122 ),
123 }
124 }
125 _ => Err(Error::invalid_input_source(
126 format!(
127 "Cannot compress a data block of type {} with FsstEncoder",
128 data.name()
129 )
130 .into(),
131 )),
132 }
133 }
134}
135
136#[derive(Debug, Default)]
137pub struct FsstMiniBlockEncoder {
138 minichunk_size: Option<i64>,
139}
140
141impl FsstMiniBlockEncoder {
142 pub fn new(minichunk_size: Option<i64>) -> Self {
143 Self { minichunk_size }
144 }
145}
146
147impl MiniBlockCompressor for FsstMiniBlockEncoder {
148 fn compress(
149 &self,
150 context: MiniBlockCompressionContext,
151 data: DataBlock,
152 ) -> Result<(MiniBlockCompressed, CompressiveEncoding)> {
153 let compressed = FsstCompressed::fsst_compress(data)?;
154
155 let data_block = DataBlock::VariableWidth(compressed.data);
156
157 let binary_compressor = Box::new(BinaryMiniBlockEncoder::new(self.minichunk_size))
159 as Box<dyn MiniBlockCompressor>;
160
161 let (binary_miniblock_compressed, binary_array_encoding) =
162 binary_compressor.compress(context, data_block)?;
163
164 Ok((
165 binary_miniblock_compressed,
166 EncodingUtils::fsst(binary_array_encoding, compressed.symbol_table),
167 ))
168 }
169}
170
171#[derive(Debug)]
172pub struct FsstPerValueEncoder {
173 inner: Box<dyn PerValueCompressor>,
174}
175
176impl FsstPerValueEncoder {
177 pub fn new(inner: Box<dyn PerValueCompressor>) -> Self {
178 Self { inner }
179 }
180}
181
182impl PerValueCompressor for FsstPerValueEncoder {
183 fn compress(&self, data: DataBlock) -> Result<(PerValueDataBlock, CompressiveEncoding)> {
184 let compressed = FsstCompressed::fsst_compress(data)?;
185
186 let data_block = DataBlock::VariableWidth(compressed.data);
187
188 let (binary_compressed, binary_array_encoding) = self.inner.compress(data_block)?;
189
190 Ok((
191 binary_compressed,
192 EncodingUtils::fsst(binary_array_encoding, compressed.symbol_table),
193 ))
194 }
195}
196
197#[derive(Debug)]
198pub struct FsstPerValueDecompressor {
199 symbol_table: LanceBuffer,
200 inner_decompressor: Box<dyn VariablePerValueDecompressor>,
201}
202
203impl FsstPerValueDecompressor {
204 pub fn new(
205 symbol_table: LanceBuffer,
206 inner_decompressor: Box<dyn VariablePerValueDecompressor>,
207 ) -> Self {
208 Self {
209 symbol_table,
210 inner_decompressor,
211 }
212 }
213}
214
215impl VariablePerValueDecompressor for FsstPerValueDecompressor {
216 fn decompress(&self, data: VariableWidthBlock) -> Result<DataBlock> {
217 let compressed_variable_data = self
219 .inner_decompressor
220 .decompress(data)?
221 .as_variable_width()
222 .unwrap();
223
224 let bytes = compressed_variable_data.data.borrow_to_typed_slice::<u8>();
226 let bytes = bytes.as_ref();
227
228 match compressed_variable_data.bits_per_offset {
229 32 => {
230 let offsets = compressed_variable_data
231 .offsets
232 .borrow_to_typed_slice::<i32>();
233 let offsets = offsets.as_ref();
234 let num_values = compressed_variable_data.num_values;
235
236 let mut decompress_bytes_buf = vec![0u8; bytes.len() * 8];
239 let mut decompress_offset_buf = vec![0i32; offsets.len()];
240 fsst::fsst::decompress(
241 &self.symbol_table,
242 bytes,
243 offsets,
244 &mut decompress_bytes_buf,
245 &mut decompress_offset_buf,
246 )
247 .map_err(map_fsst_error)?;
248
249 decompress_offset_buf.truncate((num_values + 1) as usize);
251
252 Ok(DataBlock::VariableWidth(VariableWidthBlock {
253 data: LanceBuffer::from(decompress_bytes_buf),
254 offsets: LanceBuffer::reinterpret_vec(decompress_offset_buf),
255 bits_per_offset: 32,
256 num_values,
257 block_info: BlockInfo::new(),
258 }))
259 }
260 64 => {
261 let offsets = compressed_variable_data
262 .offsets
263 .borrow_to_typed_slice::<i64>();
264 let offsets = offsets.as_ref();
265 let num_values = compressed_variable_data.num_values;
266
267 let mut decompress_bytes_buf = vec![0u8; bytes.len() * 8];
270 let mut decompress_offset_buf = vec![0i64; offsets.len()];
271 fsst::fsst::decompress(
272 &self.symbol_table,
273 bytes,
274 offsets,
275 &mut decompress_bytes_buf,
276 &mut decompress_offset_buf,
277 )
278 .map_err(map_fsst_error)?;
279
280 decompress_offset_buf.truncate((num_values + 1) as usize);
282
283 Ok(DataBlock::VariableWidth(VariableWidthBlock {
284 data: LanceBuffer::from(decompress_bytes_buf),
285 offsets: LanceBuffer::reinterpret_vec(decompress_offset_buf),
286 bits_per_offset: 64,
287 num_values,
288 block_info: BlockInfo::new(),
289 }))
290 }
291 _ => panic!(
292 "Unsupported offset type {}",
293 compressed_variable_data.bits_per_offset,
294 ),
295 }
296 }
297}
298
299#[derive(Debug)]
300pub struct FsstMiniBlockDecompressor {
301 symbol_table: LanceBuffer,
302 inner_decompressor: Box<dyn MiniBlockDecompressor>,
303}
304
305impl FsstMiniBlockDecompressor {
306 pub fn new(
307 description: &metadata::Fsst,
308 inner_decompressor: Box<dyn MiniBlockDecompressor>,
309 ) -> Self {
310 Self {
311 symbol_table: LanceBuffer::from_bytes(description.symbol_table.clone(), 1),
312 inner_decompressor,
313 }
314 }
315}
316
317impl MiniBlockDecompressor for FsstMiniBlockDecompressor {
318 fn num_buffers(&self) -> usize {
319 self.inner_decompressor.num_buffers()
320 }
321
322 fn decompress(&self, data: Vec<LanceBuffer>, num_values: u64) -> Result<DataBlock> {
323 let compressed_data_block = self.inner_decompressor.decompress(data, num_values)?;
326 let DataBlock::VariableWidth(compressed_data_block) = compressed_data_block else {
327 panic!("BinaryMiniBlockDecompressor should output VariableWidth DataBlock")
328 };
329
330 let bytes = &compressed_data_block.data;
332 let (decompress_bytes_buf, decompress_offset_buf) =
333 if compressed_data_block.bits_per_offset == 64 {
334 let offsets = compressed_data_block.offsets.borrow_to_typed_slice::<i64>();
335 let offsets = offsets.as_ref();
336
337 let mut decompress_bytes_buf = vec![0u8; bytes.len() * 8];
340 let mut decompress_offset_buf = vec![0i64; offsets.len()];
341 fsst::fsst::decompress(
342 &self.symbol_table,
343 bytes.as_ref(),
344 offsets,
345 &mut decompress_bytes_buf,
346 &mut decompress_offset_buf,
347 )
348 .map_err(map_fsst_error)?;
349
350 decompress_offset_buf.truncate((num_values + 1) as usize);
352
353 (
354 decompress_bytes_buf,
355 LanceBuffer::reinterpret_vec(decompress_offset_buf),
356 )
357 } else {
358 let offsets = compressed_data_block.offsets.borrow_to_typed_slice::<i32>();
359 let offsets = offsets.as_ref();
360
361 let mut decompress_bytes_buf = vec![0u8; bytes.len() * 8];
364 let mut decompress_offset_buf = vec![0i32; offsets.len()];
365 fsst::fsst::decompress(
366 &self.symbol_table,
367 bytes.as_ref(),
368 offsets,
369 &mut decompress_bytes_buf,
370 &mut decompress_offset_buf,
371 )
372 .map_err(map_fsst_error)?;
373
374 decompress_offset_buf.truncate((num_values + 1) as usize);
376
377 (
378 decompress_bytes_buf,
379 LanceBuffer::reinterpret_vec(decompress_offset_buf),
380 )
381 };
382
383 Ok(DataBlock::VariableWidth(VariableWidthBlock {
384 data: LanceBuffer::from(decompress_bytes_buf),
385 offsets: decompress_offset_buf,
386 bits_per_offset: compressed_data_block.bits_per_offset,
387 num_values,
388 block_info: BlockInfo::new(),
389 }))
390 }
391}
392
393#[cfg(test)]
394mod tests {
395 use std::collections::HashMap;
396
397 use arrow_array::StringArray;
398 use fsst::fsst::{FSST_SYMBOL_TABLE_SIZE, compress, decompress};
399 use lance_core::Error;
400 use lance_datagen::{ByteCount, RowCount};
401
402 use super::map_fsst_error;
403 use crate::testing::{TestCases, TestEncoding, check_round_trip_encoding_of_data};
404
405 #[rstest::rstest]
406 #[test_log::test(tokio::test)]
407 async fn test_fsst(
408 #[values(false, true)] explicit: bool,
409 #[values(
410 TestEncoding::StructuralU16,
411 TestEncoding::StructuralU32,
412 TestEncoding::StructuralSparse
413 )]
414 encoding: TestEncoding,
415 ) {
416 let test_cases = TestCases::default()
417 .with_expected_encoding("fsst")
418 .with_encoding(encoding);
419
420 let arr = lance_datagen::gen_batch()
422 .anon_col(lance_datagen::array::rand_utf8(ByteCount::from(100), false))
423 .into_batch_rows(RowCount::from(5000))
424 .unwrap()
425 .column(0)
426 .clone();
427
428 let metadata = if explicit {
429 HashMap::from([("lance-encoding:compression".to_string(), "fsst".to_string())])
430 } else {
431 HashMap::new()
433 };
434 check_round_trip_encoding_of_data(vec![arr], &test_cases, metadata).await;
435 }
436
437 #[test]
438 fn test_corrupt_fsst_symbol_table_is_corrupt_file() {
439 let input = "the rain in spain stays mainly in the plain ".repeat(2048);
440 let array = StringArray::from(vec![input.as_str()]);
441 let mut symbol_table = [0u8; FSST_SYMBOL_TABLE_SIZE];
442 let mut compressed = vec![0u8; array.value_data().len().max(1)];
443 let mut compressed_offsets = vec![0i32; array.value_offsets().len()];
444 compress(
445 symbol_table.as_mut(),
446 array.value_data(),
447 array.value_offsets(),
448 &mut compressed,
449 &mut compressed_offsets,
450 )
451 .unwrap();
452
453 let st_info = u64::from_ne_bytes(symbol_table[..8].try_into().unwrap());
454 assert!(st_info & (1 << 24) != 0, "expected decoder_switch_on input");
455 let n_symbols = (st_info & 255) as usize;
456 assert!(n_symbols > 0);
457 symbol_table[8 + n_symbols * 8] = 9;
458
459 let mut out = vec![0u8; compressed.len() * 8];
460 let mut out_offsets = vec![0i32; compressed_offsets.len()];
461 let err = decompress(
462 &symbol_table,
463 &compressed,
464 &compressed_offsets,
465 &mut out,
466 &mut out_offsets,
467 )
468 .map_err(map_fsst_error)
469 .unwrap_err();
470 assert!(matches!(err, Error::CorruptFile { .. }), "{err}");
471 assert!(err.to_string().contains("symbol length"), "{err}");
472 }
473}