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 ProtobufUtils21,
30 pb21::{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 ProtobufUtils21::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 ProtobufUtils21::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: &pb21::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 decompress(&self, data: Vec<LanceBuffer>, num_values: u64) -> Result<DataBlock> {
319 let compressed_data_block = self.inner_decompressor.decompress(data, num_values)?;
322 let DataBlock::VariableWidth(compressed_data_block) = compressed_data_block else {
323 panic!("BinaryMiniBlockDecompressor should output VariableWidth DataBlock")
324 };
325
326 let bytes = &compressed_data_block.data;
328 let (decompress_bytes_buf, decompress_offset_buf) =
329 if compressed_data_block.bits_per_offset == 64 {
330 let offsets = compressed_data_block.offsets.borrow_to_typed_slice::<i64>();
331 let offsets = offsets.as_ref();
332
333 let mut decompress_bytes_buf = vec![0u8; bytes.len() * 8];
336 let mut decompress_offset_buf = vec![0i64; offsets.len()];
337 fsst::fsst::decompress(
338 &self.symbol_table,
339 bytes.as_ref(),
340 offsets,
341 &mut decompress_bytes_buf,
342 &mut decompress_offset_buf,
343 )
344 .map_err(map_fsst_error)?;
345
346 decompress_offset_buf.truncate((num_values + 1) as usize);
348
349 (
350 decompress_bytes_buf,
351 LanceBuffer::reinterpret_vec(decompress_offset_buf),
352 )
353 } else {
354 let offsets = compressed_data_block.offsets.borrow_to_typed_slice::<i32>();
355 let offsets = offsets.as_ref();
356
357 let mut decompress_bytes_buf = vec![0u8; bytes.len() * 8];
360 let mut decompress_offset_buf = vec![0i32; offsets.len()];
361 fsst::fsst::decompress(
362 &self.symbol_table,
363 bytes.as_ref(),
364 offsets,
365 &mut decompress_bytes_buf,
366 &mut decompress_offset_buf,
367 )
368 .map_err(map_fsst_error)?;
369
370 decompress_offset_buf.truncate((num_values + 1) as usize);
372
373 (
374 decompress_bytes_buf,
375 LanceBuffer::reinterpret_vec(decompress_offset_buf),
376 )
377 };
378
379 Ok(DataBlock::VariableWidth(VariableWidthBlock {
380 data: LanceBuffer::from(decompress_bytes_buf),
381 offsets: decompress_offset_buf,
382 bits_per_offset: compressed_data_block.bits_per_offset,
383 num_values,
384 block_info: BlockInfo::new(),
385 }))
386 }
387}
388
389#[cfg(test)]
390mod tests {
391 use std::collections::HashMap;
392
393 use arrow_array::StringArray;
394 use fsst::fsst::{FSST_SYMBOL_TABLE_SIZE, compress, decompress};
395 use lance_core::Error;
396 use lance_datagen::{ByteCount, RowCount};
397
398 use super::map_fsst_error;
399 use crate::testing::{TestCases, check_round_trip_encoding_of_data};
400
401 #[test_log::test(tokio::test)]
402 async fn test_fsst() {
403 let test_cases = TestCases::default()
404 .with_expected_encoding("fsst")
405 .with_structural_encodings();
406
407 let arr = lance_datagen::gen_batch()
409 .anon_col(lance_datagen::array::rand_utf8(ByteCount::from(100), false))
410 .into_batch_rows(RowCount::from(5000))
411 .unwrap()
412 .column(0)
413 .clone();
414
415 let metadata_explicit =
418 HashMap::from([("lance-encoding:compression".to_string(), "fsst".to_string())]);
419 check_round_trip_encoding_of_data(vec![arr.clone()], &test_cases, metadata_explicit).await;
420
421 check_round_trip_encoding_of_data(vec![arr], &test_cases, HashMap::new()).await;
424 }
425
426 #[test]
427 fn test_corrupt_fsst_symbol_table_is_corrupt_file() {
428 let input = "the rain in spain stays mainly in the plain ".repeat(2048);
429 let array = StringArray::from(vec![input.as_str()]);
430 let mut symbol_table = [0u8; FSST_SYMBOL_TABLE_SIZE];
431 let mut compressed = vec![0u8; array.value_data().len().max(1)];
432 let mut compressed_offsets = vec![0i32; array.value_offsets().len()];
433 compress(
434 symbol_table.as_mut(),
435 array.value_data(),
436 array.value_offsets(),
437 &mut compressed,
438 &mut compressed_offsets,
439 )
440 .unwrap();
441
442 let st_info = u64::from_ne_bytes(symbol_table[..8].try_into().unwrap());
443 assert!(st_info & (1 << 24) != 0, "expected decoder_switch_on input");
444 let n_symbols = (st_info & 255) as usize;
445 assert!(n_symbols > 0);
446 symbol_table[8 + n_symbols * 8] = 9;
447
448 let mut out = vec![0u8; compressed.len() * 8];
449 let mut out_offsets = vec![0i32; compressed_offsets.len()];
450 let err = decompress(
451 &symbol_table,
452 &compressed,
453 &compressed_offsets,
454 &mut out,
455 &mut out_offsets,
456 )
457 .map_err(map_fsst_error)
458 .unwrap_err();
459 assert!(matches!(err, Error::CorruptFile { .. }), "{err}");
460 assert!(err.to_string().contains("symbol length"), "{err}");
461 }
462}