1use std::sync::Arc;
11
12use fsst::Compressor;
13use fsst::Symbol;
14use num_traits::AsPrimitive;
15use vortex_array::ArrayRef;
16use vortex_array::ArrayView;
17use vortex_array::ExecutionCtx;
18use vortex_array::IntoArray;
19use vortex_array::arrays::PrimitiveArray;
20use vortex_array::arrays::VarBin;
21use vortex_array::arrays::VarBinView;
22use vortex_array::arrays::varbin::VarBinArraySlotsExt;
23use vortex_array::arrays::varbin::builder::VarBinBuilder;
24use vortex_array::arrays::varbinview::BinaryView;
25use vortex_array::buffer::BufferHandle;
26use vortex_array::dtype::DType;
27use vortex_array::dtype::IntegerPType;
28use vortex_array::dtype::OffsetBuilderPType;
29use vortex_array::match_each_integer_ptype;
30use vortex_buffer::BufferMut;
31use vortex_error::VortexExpect;
32use vortex_error::VortexResult;
33use vortex_error::vortex_bail;
34use vortex_mask::AllOr;
35use vortex_mask::Mask;
36
37use crate::FSST;
38use crate::FSSTArray;
39use crate::array::FSSTSymbolTable;
40use crate::array::padded_symbol_table;
41
42const FSST_PER_BYTE_OVERHEAD: usize = 2;
44
45const DEFAULT_BUFFER_LEN: usize = 1024 * 1024;
47
48pub fn fsst_compress(
52 array: &ArrayRef,
53 compressor: &Compressor,
54 ctx: &mut ExecutionCtx,
55) -> VortexResult<FSSTArray> {
56 if let Some(view) = array.as_opt::<VarBinView>() {
57 compress_varbinview(view, compressor, ctx)
58 } else if let Some(varbin) = array.as_opt::<VarBin>() {
59 compress_varbin_array(varbin, compressor, ctx)
60 } else {
61 vortex_bail!(
62 "fsst_compress requires VarBinView or VarBin encoding, got {}",
63 array.encoding_id()
64 )
65 }
66}
67
68pub fn fsst_train_compressor(array: &ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult<Compressor> {
72 if let Some(view) = array.as_opt::<VarBinView>() {
73 train_varbinview(view, ctx)
74 } else if let Some(varbin) = array.as_opt::<VarBin>() {
75 train_varbin_array(varbin, ctx)
76 } else {
77 vortex_bail!(
78 "fsst_train_compressor requires VarBinView or VarBin encoding, got {}",
79 array.encoding_id()
80 )
81 }
82}
83
84fn compress_varbinview(
85 strings: ArrayView<VarBinView>,
86 compressor: &Compressor,
87 ctx: &mut ExecutionCtx,
88) -> VortexResult<FSSTArray> {
89 let mask = strings.validity()?.execute_mask(strings.len(), ctx)?;
90 let views = strings.views();
91
92 let total_input_bytes = match mask.bit_buffer() {
93 AllOr::All => views.iter().map(|v| v.len() as usize).sum(),
94 AllOr::None => 0,
95 AllOr::Some(bits) => views
96 .iter()
97 .zip(bits.iter())
98 .filter(|&(_, b)| b)
99 .map(|(v, _)| v.len() as usize)
100 .sum(),
101 };
102
103 if fsst_output_fits_in_i32_offsets(total_input_bytes) {
104 compress_views::<i32>(strings, &mask, compressor, ctx)
105 } else {
106 compress_views::<i64>(strings, &mask, compressor, ctx)
107 }
108}
109
110fn compress_varbin_array(
111 strings: ArrayView<VarBin>,
112 compressor: &Compressor,
113 ctx: &mut ExecutionCtx,
114) -> VortexResult<FSSTArray> {
115 let mask = strings.validity()?.execute_mask(strings.len(), ctx)?;
116 let offsets = strings.offsets().clone().execute::<PrimitiveArray>(ctx)?;
117 let total_input_bytes = match_each_integer_ptype!(offsets.ptype(), |O| {
118 let off = offsets.as_slice::<O>();
119 let first: usize = off[0].as_();
120 let last: usize = off[off.len() - 1].as_();
121 last - first
122 });
123
124 if fsst_output_fits_in_i32_offsets(total_input_bytes) {
125 compress_varbin::<i32>(strings, &offsets, &mask, compressor, ctx)
126 } else {
127 compress_varbin::<i64>(strings, &offsets, &mask, compressor, ctx)
128 }
129}
130
131fn train_varbinview(
132 strings: ArrayView<VarBinView>,
133 ctx: &mut ExecutionCtx,
134) -> VortexResult<Compressor> {
135 let mask = strings.validity()?.execute_mask(strings.len(), ctx)?;
136 let views = strings.views();
137 let buffers = strings.data_buffers();
138 let mut lines: Vec<&[u8]> = Vec::with_capacity(mask.true_count());
139
140 match mask.bit_buffer() {
141 AllOr::All => {
142 for view in views {
143 lines.push(view_bytes(view, buffers));
144 }
145 }
146 AllOr::None => {}
147 AllOr::Some(bits) => {
148 for (view, valid) in views.iter().zip(bits.iter()) {
149 if valid {
150 lines.push(view_bytes(view, buffers));
151 }
152 }
153 }
154 }
155
156 Ok(Compressor::train(&lines))
157}
158
159fn train_varbin_array(
160 strings: ArrayView<VarBin>,
161 ctx: &mut ExecutionCtx,
162) -> VortexResult<Compressor> {
163 let mask = strings.validity()?.execute_mask(strings.len(), ctx)?;
164 let offsets = strings.offsets().clone().execute::<PrimitiveArray>(ctx)?;
165 let bytes = strings.bytes().as_slice();
166 let mut lines: Vec<&[u8]> = Vec::with_capacity(mask.true_count());
167
168 match_each_integer_ptype!(offsets.ptype(), |I| {
169 let off = offsets.as_slice::<I>();
170 for_each_varbin_row(off, bytes, &mask, |row| {
171 if let Some(s) = row {
172 lines.push(s);
173 }
174 });
175 });
176
177 Ok(Compressor::train(&lines))
178}
179
180#[inline]
181fn fsst_output_fits_in_i32_offsets(total_input_bytes: usize) -> bool {
182 let worst = total_input_bytes.saturating_mul(FSST_PER_BYTE_OVERHEAD);
183 worst <= i32::MAX as usize
184}
185
186#[inline]
187fn view_bytes<'a>(view: &'a BinaryView, buffers: &'a Arc<[BufferHandle]>) -> &'a [u8] {
188 if view.is_inlined() {
189 view.as_inlined().value()
190 } else {
191 let r = view.as_view();
192 &buffers[r.buffer_index as usize].as_host()[r.as_range()]
193 }
194}
195
196fn compress_views<O>(
197 strings: ArrayView<VarBinView>,
198 mask: &Mask,
199 compressor: &Compressor,
200 ctx: &mut ExecutionCtx,
201) -> VortexResult<FSSTArray>
202where
203 O: OffsetBuilderPType + 'static,
204{
205 let mut sink = FsstSink::<O>::with_capacity(
206 DType::Binary(strings.dtype().nullability()),
207 strings.len(),
208 compressor,
209 );
210 let views = strings.views();
211 let buffers = strings.data_buffers();
212 match mask.bit_buffer() {
213 AllOr::All => {
214 for view in views {
215 sink.emit(Some(view_bytes(view, buffers)));
216 }
217 }
218 AllOr::None => {
219 for _ in 0..mask.len() {
220 sink.emit(None);
221 }
222 }
223 AllOr::Some(bits) => {
224 for (view, valid) in views.iter().zip(bits.iter()) {
225 sink.emit(valid.then(|| view_bytes(view, buffers)));
226 }
227 }
228 }
229 sink.finish(strings.dtype().clone(), ctx)
230}
231
232fn compress_varbin<O>(
233 strings: ArrayView<VarBin>,
234 offsets: &PrimitiveArray,
235 mask: &Mask,
236 compressor: &Compressor,
237 ctx: &mut ExecutionCtx,
238) -> VortexResult<FSSTArray>
239where
240 O: OffsetBuilderPType + 'static,
241{
242 let mut sink = FsstSink::<O>::with_capacity(
243 DType::Binary(strings.dtype().nullability()),
244 strings.len(),
245 compressor,
246 );
247 let bytes = strings.bytes().as_slice();
248 match_each_integer_ptype!(offsets.ptype(), |I| {
249 let off = offsets.as_slice::<I>();
250 for_each_varbin_row(off, bytes, mask, |row| sink.emit(row));
251 });
252 sink.finish(strings.dtype().clone(), ctx)
253}
254
255#[inline]
258fn for_each_varbin_row<'a, I, F>(off: &[I], bytes: &'a [u8], mask: &Mask, mut f: F)
259where
260 I: IntegerPType + 'static,
261 F: FnMut(Option<&'a [u8]>),
262{
263 match mask.bit_buffer() {
264 AllOr::All => {
265 for w in off.windows(2) {
266 f(Some(&bytes[w[0].as_()..w[1].as_()]));
267 }
268 }
269 AllOr::None => {
270 for _ in 0..mask.len() {
271 f(None);
272 }
273 }
274 AllOr::Some(bits) => {
275 for (w, valid) in off.windows(2).zip(bits.iter()) {
276 f(valid.then(|| &bytes[w[0].as_()..w[1].as_()]));
277 }
278 }
279 }
280}
281
282struct FsstSink<'c, O: OffsetBuilderPType + 'static> {
284 buffer: Vec<u8>,
285 builder: VarBinBuilder<O>,
286 uncompressed_lengths: BufferMut<i32>,
287 compressor: &'c Compressor,
288}
289
290impl<'c, O: OffsetBuilderPType + 'static> FsstSink<'c, O> {
291 fn with_capacity(dtype: DType, len: usize, compressor: &'c Compressor) -> Self {
292 Self {
293 buffer: Vec::with_capacity(DEFAULT_BUFFER_LEN),
294 builder: VarBinBuilder::<O>::with_capacity(dtype, len),
295 uncompressed_lengths: BufferMut::with_capacity(len),
296 compressor,
297 }
298 }
299
300 #[inline]
301 fn emit(&mut self, row: Option<&[u8]>) {
302 let Some(s) = row else {
303 self.builder.push_null();
304 self.uncompressed_lengths.push(0);
305 return;
306 };
307
308 self.uncompressed_lengths.push(
310 i32::try_from(s.len()).vortex_expect("per-row uncompressed length must fit in i32"),
311 );
312
313 self.buffer.clear();
315 self.buffer.reserve(FSST_PER_BYTE_OVERHEAD * s.len());
316
317 let written = unsafe {
319 self.compressor
320 .compress_into(s, self.buffer.spare_capacity_mut())
321 };
322 unsafe { self.buffer.set_len(written) };
324
325 self.builder.append_value(&self.buffer);
326 }
327
328 fn finish(mut self, dtype: DType, ctx: &mut ExecutionCtx) -> VortexResult<FSSTArray> {
329 let codes = self.builder.finish_into_varbin();
330 FSST::try_new_with_symbol_table(
333 dtype,
334 Arc::new(FSSTSymbolTable::new_padded(
335 padded_symbol_table(self.compressor.symbol_table(), Symbol::ZERO),
336 padded_symbol_table(self.compressor.symbol_lengths(), 0),
337 self.compressor.n_symbols(),
338 )?),
339 codes,
340 self.uncompressed_lengths.into_array(),
341 ctx,
342 )
343 }
344}
345
346#[cfg(test)]
347mod tests {
348 use vortex_array::VortexSessionExecute;
349 use vortex_array::array_session;
350 use vortex_array::arrays::VarBinViewArray;
351 use vortex_array::arrays::varbin::VarBinArraySlotsExt;
352 use vortex_array::dtype::PType;
353 use vortex_error::VortexResult;
354
355 use super::fsst_compress;
356 use super::fsst_output_fits_in_i32_offsets;
357 use super::fsst_train_compressor;
358 use crate::array::FSSTArrayExt;
359
360 #[test]
363 fn offset_width_boundary() {
364 let m = i32::MAX as usize;
365 assert!(fsst_output_fits_in_i32_offsets(m / 2 - 7));
366 assert!(fsst_output_fits_in_i32_offsets(m / 2));
367 assert!(fsst_output_fits_in_i32_offsets(0));
368 assert!(!fsst_output_fits_in_i32_offsets(usize::MAX));
369 }
370
371 #[test]
374 fn codes_offsets_dtype_small_input_is_i32() -> VortexResult<()> {
375 let array = VarBinViewArray::from_iter_str(["hello", "world", "fsst encoded"]);
376 let mut ctx = array_session().create_execution_ctx();
377 let compressor = fsst_train_compressor(array.as_array(), &mut ctx)?;
378 let fsst = fsst_compress(array.as_array(), &compressor, &mut ctx)?;
379 assert_eq!(fsst.codes().offsets().dtype().as_ptype(), PType::I32);
380 Ok(())
381 }
382}