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