Skip to main content

vortex_fsst/
array.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::fmt::Debug;
5use std::fmt::Display;
6use std::fmt::Formatter;
7use std::hash::Hasher;
8use std::sync::Arc;
9use std::sync::OnceLock;
10
11use fsst::Compressor;
12use fsst::Decompressor;
13use fsst::Symbol;
14use prost::Message as _;
15use vortex_array::Array;
16use vortex_array::ArrayEq;
17use vortex_array::ArrayHash;
18use vortex_array::ArrayId;
19use vortex_array::ArrayParts;
20use vortex_array::ArrayRef;
21use vortex_array::ArraySlots;
22use vortex_array::ArrayView;
23use vortex_array::Canonical;
24use vortex_array::EqMode;
25use vortex_array::ExecutionCtx;
26use vortex_array::ExecutionResult;
27use vortex_array::IntoArray;
28use vortex_array::TypedArrayRef;
29use vortex_array::VortexSessionExecute;
30use vortex_array::arrays::VarBin;
31use vortex_array::arrays::VarBinArray;
32use vortex_array::arrays::varbin::VarBinArrayExt;
33use vortex_array::buffer::BufferHandle;
34use vortex_array::builders::ArrayBuilder;
35use vortex_array::builders::VarBinViewBuilder;
36use vortex_array::dtype::DType;
37use vortex_array::dtype::Nullability;
38use vortex_array::dtype::PType;
39use vortex_array::legacy_session;
40use vortex_array::serde::ArrayChildren;
41use vortex_array::smallvec::smallvec;
42use vortex_array::validity::Validity;
43use vortex_array::vtable::VTable;
44use vortex_array::vtable::ValidityVTable;
45use vortex_array::vtable::child_to_validity;
46use vortex_array::vtable::validity_to_child;
47use vortex_buffer::Buffer;
48use vortex_buffer::ByteBuffer;
49use vortex_error::VortexExpect;
50use vortex_error::VortexResult;
51use vortex_error::vortex_bail;
52use vortex_error::vortex_ensure;
53use vortex_error::vortex_err;
54use vortex_error::vortex_panic;
55use vortex_session::VortexSession;
56use vortex_session::registry::CachedId;
57
58use crate::canonical::canonicalize_fsst;
59use crate::canonical::fsst_decode_views;
60use crate::rules::RULES;
61
62/// A [`FSST`]-encoded Vortex array.
63pub type FSSTArray = Array<FSST>;
64
65#[derive(Clone, prost::Message)]
66pub struct FSSTMetadata {
67    #[prost(enumeration = "PType", tag = "1")]
68    uncompressed_lengths_ptype: i32,
69
70    #[prost(enumeration = "PType", tag = "2")]
71    codes_offsets_ptype: i32,
72}
73
74impl FSSTMetadata {
75    pub fn get_uncompressed_lengths_ptype(&self) -> VortexResult<PType> {
76        PType::try_from(self.uncompressed_lengths_ptype)
77            .map_err(|_| vortex_err!("Invalid PType {}", self.uncompressed_lengths_ptype))
78    }
79}
80
81impl ArrayHash for FSSTData {
82    fn array_hash<H: Hasher>(&self, state: &mut H, precision: EqMode) {
83        self.symbol_table.symbols.array_hash(state, precision);
84        self.symbol_table
85            .symbol_lengths
86            .array_hash(state, precision);
87        self.codes_bytes.as_host().array_hash(state, precision);
88    }
89}
90
91impl ArrayEq for FSSTData {
92    fn array_eq(&self, other: &Self, precision: EqMode) -> bool {
93        self.symbol_table
94            .symbols
95            .array_eq(&other.symbol_table.symbols, precision)
96            && self
97                .symbol_table
98                .symbol_lengths
99                .array_eq(&other.symbol_table.symbol_lengths, precision)
100            && self
101                .codes_bytes
102                .as_host()
103                .array_eq(other.codes_bytes.as_host(), precision)
104    }
105}
106
107impl VTable for FSST {
108    type TypedArrayData = FSSTData;
109    type OperationsVTable = Self;
110    type ValidityVTable = Self;
111
112    fn id(&self) -> ArrayId {
113        static ID: CachedId = CachedId::new("vortex.fsst");
114        *ID
115    }
116
117    #[allow(clippy::disallowed_methods)]
118    fn validate(
119        &self,
120        data: &Self::TypedArrayData,
121        dtype: &DType,
122        len: usize,
123        slots: &[Option<ArrayRef>],
124    ) -> VortexResult<()> {
125        // TODO(ctx): trait fixes - VTable::validate has a fixed signature.
126        let mut ctx = legacy_session().create_execution_ctx();
127        data.validate(dtype, len, slots, &mut ctx)
128    }
129
130    fn nbuffers(_array: ArrayView<'_, Self>) -> usize {
131        3
132    }
133
134    fn buffer(array: ArrayView<'_, Self>, idx: usize) -> BufferHandle {
135        match idx {
136            0 => BufferHandle::new_host(array.symbols().clone().into_byte_buffer()),
137            1 => BufferHandle::new_host(array.symbol_lengths().clone().into_byte_buffer()),
138            2 => array.codes_bytes_handle().clone(),
139            _ => vortex_panic!("FSSTArray buffer index {idx} out of bounds"),
140        }
141    }
142
143    fn buffer_name(_array: ArrayView<'_, Self>, idx: usize) -> Option<String> {
144        match idx {
145            0 => Some("symbols".to_string()),
146            1 => Some("symbol_lengths".to_string()),
147            2 => Some("compressed_codes".to_string()),
148            _ => vortex_panic!("FSSTArray buffer_name index {idx} out of bounds"),
149        }
150    }
151
152    fn with_buffers(
153        &self,
154        array: ArrayView<'_, Self>,
155        buffers: &[BufferHandle],
156    ) -> VortexResult<ArrayParts<Self>> {
157        vortex_ensure!(
158            buffers.len() == 3,
159            "Expected 3 buffers, got {}",
160            buffers.len()
161        );
162        let symbols = Buffer::<Symbol>::from_byte_buffer(buffers[0].clone().try_to_host_sync()?);
163        let symbol_lengths = Buffer::<u8>::from_byte_buffer(buffers[1].clone().try_to_host_sync()?);
164        let data = FSSTData::try_new(symbols, symbol_lengths, buffers[2].clone(), array.len())?;
165        Ok(
166            ArrayParts::new(self.clone(), array.dtype().clone(), array.len(), data)
167                .with_slots(array.slots().iter().cloned().collect()),
168        )
169    }
170
171    fn serialize(
172        array: ArrayView<'_, Self>,
173        _session: &VortexSession,
174    ) -> VortexResult<Option<Vec<u8>>> {
175        let codes_offsets = array.as_ref().slots()[CODES_OFFSETS_SLOT]
176            .as_ref()
177            .vortex_expect("FSSTArray codes_offsets slot");
178        Ok(Some(
179            FSSTMetadata {
180                uncompressed_lengths_ptype: array.uncompressed_lengths().dtype().as_ptype().into(),
181                codes_offsets_ptype: codes_offsets.dtype().as_ptype().into(),
182            }
183            .encode_to_vec(),
184        ))
185    }
186
187    /// Deserializes an FSST array from its serialized components.
188    ///
189    /// Supports two serialization formats:
190    ///
191    /// ## Legacy format (2 buffers, 2 children)
192    ///
193    /// The original FSST layout stored the compressed codes as a full `VarBinArray` child.
194    /// - **Buffers**: `[symbols, symbol_lengths]`
195    /// - **Children**: `[codes (VarBinArray), uncompressed_lengths (Primitive)]`
196    ///
197    /// The codes VarBinArray child is decomposed: its bytes become the `codes_bytes` buffer,
198    /// and its offsets/validity are extracted into slots.
199    /// See `FSST::deserialize_legacy`.
200    ///
201    /// ## Current format (3 buffers, 2-3 children)
202    ///
203    /// The current layout stores the compressed bytes as a raw buffer alongside the symbol
204    /// table, with offsets and validity as separate children.
205    /// - **Buffers**: `[symbols, symbol_lengths, compressed_codes_bytes]`
206    /// - **Children**: `[uncompressed_lengths, codes_offsets, (optional) codes_validity]`
207    ///
208    /// The `codes_bytes` buffer is stored directly in `FSSTData`. A `VarBinArray` for the
209    /// codes can be reconstructed on demand via [`FSSTArrayExt::codes()`] using the bytes
210    /// from `FSSTData` combined with offsets and validity from the array's slots.
211    fn deserialize(
212        &self,
213        dtype: &DType,
214        len: usize,
215        metadata: &[u8],
216        buffers: &[BufferHandle],
217        children: &dyn ArrayChildren,
218        session: &VortexSession,
219    ) -> VortexResult<ArrayParts<Self>> {
220        let metadata = FSSTMetadata::decode(metadata)?;
221        let symbols = Buffer::<Symbol>::from_byte_buffer(buffers[0].clone().try_to_host_sync()?);
222        let symbol_lengths = Buffer::<u8>::from_byte_buffer(buffers[1].clone().try_to_host_sync()?);
223
224        let mut ctx = session.create_execution_ctx();
225        if buffers.len() == 2 {
226            return Self::deserialize_legacy(
227                self,
228                dtype,
229                len,
230                &metadata,
231                &symbols,
232                &symbol_lengths,
233                children,
234                &mut ctx,
235            );
236        }
237
238        if buffers.len() == 3 {
239            let uncompressed_lengths = children.get(
240                0,
241                &DType::Primitive(
242                    metadata.get_uncompressed_lengths_ptype()?,
243                    Nullability::NonNullable,
244                ),
245                len,
246            )?;
247
248            let codes_bytes = buffers[2].clone();
249            let codes_offsets = children.get(
250                1,
251                &DType::Primitive(
252                    PType::try_from(metadata.codes_offsets_ptype)?,
253                    Nullability::NonNullable,
254                ),
255                // VarBin offsets are len + 1
256                len + 1,
257            )?;
258
259            let codes_validity = if children.len() == 2 {
260                Validity::from(dtype.nullability())
261            } else if children.len() == 3 {
262                let validity = children.get(2, &Validity::DTYPE, len)?;
263                Validity::Array(validity)
264            } else {
265                vortex_bail!("Expected 2 or 3 children, got {}", children.len());
266            };
267
268            FSSTData::validate_parts(
269                &symbols,
270                &symbol_lengths,
271                &codes_bytes,
272                &codes_offsets,
273                dtype.nullability(),
274                &uncompressed_lengths,
275                dtype,
276                len,
277                &mut ctx,
278            )?;
279            let slots = smallvec![
280                Some(uncompressed_lengths),
281                Some(codes_offsets),
282                validity_to_child(&codes_validity, len),
283            ];
284            let data = FSSTData::try_new(symbols, symbol_lengths, codes_bytes, len)?;
285            return Ok(ArrayParts::new(self.clone(), dtype.clone(), len, data).with_slots(slots));
286        }
287
288        vortex_bail!(
289            "InvalidArgument: Expected 2 or 3 buffers, got {}",
290            buffers.len()
291        );
292    }
293
294    fn slot_name(_array: ArrayView<'_, Self>, idx: usize) -> String {
295        SLOT_NAMES[idx].to_string()
296    }
297
298    fn execute(array: Array<Self>, ctx: &mut ExecutionCtx) -> VortexResult<ExecutionResult> {
299        canonicalize_fsst(array.as_view(), ctx).map(ExecutionResult::done)
300    }
301
302    fn append_to_builder(
303        array: ArrayView<'_, Self>,
304        builder: &mut dyn ArrayBuilder,
305        ctx: &mut ExecutionCtx,
306    ) -> VortexResult<()> {
307        let Some(builder) = builder.as_any_mut().downcast_mut::<VarBinViewBuilder>() else {
308            return array
309                .array()
310                .clone()
311                .execute::<Canonical>(ctx)?
312                .into_array()
313                .append_to_builder(builder, ctx);
314        };
315
316        // Decompress the whole block of data into a new buffer, and create some views
317        // from it instead. The new buffer lands after any pending in-progress
318        // buffer that push_buffer_and_adjusted_views will flush first.
319        let next_buffer_index = builder.completed_block_count() + u32::from(builder.in_progress());
320        let (buffers, views) = fsst_decode_views(array, next_buffer_index, ctx)?;
321
322        builder.push_buffer_and_adjusted_views(
323            &buffers,
324            &views,
325            array
326                .array()
327                .validity()?
328                .execute_mask(array.array().len(), ctx)?,
329        );
330        Ok(())
331    }
332
333    fn reduce_parent(
334        array: ArrayView<'_, Self>,
335        parent: &ArrayRef,
336        child_idx: usize,
337    ) -> VortexResult<Option<ArrayRef>> {
338        RULES.evaluate(array, parent, child_idx)
339    }
340}
341
342/// Lengths of the original values before compression, can be compressed.
343pub(crate) const UNCOMPRESSED_LENGTHS_SLOT: usize = 0;
344/// The offsets array for the FSST-compressed codes.
345pub(crate) const CODES_OFFSETS_SLOT: usize = 1;
346/// The validity bitmap for the compressed codes.
347pub(crate) const CODES_VALIDITY_SLOT: usize = 2;
348pub(crate) const NUM_SLOTS: usize = 3;
349pub(crate) const SLOT_NAMES: [&str; NUM_SLOTS] =
350    ["uncompressed_lengths", "codes_offsets", "codes_validity"];
351
352/// The inner data for an FSST-compressed array.
353///
354/// Holds the FSST symbol table (`symbols` + `symbol_lengths`) and the raw compressed
355/// codes bytes buffer. The codes offsets and validity live in the outer array's slots
356/// (slots 1 and 2 respectively).
357///
358/// A full [`VarBinArray`] representing the codes can be reconstructed on demand via
359/// [`FSSTArrayExt::codes()`], combining this buffer with the offsets/validity from slots.
360#[derive(Clone)]
361pub struct FSSTData {
362    symbol_table: Arc<FSSTSymbolTable>,
363    /// The raw compressed codes bytes, equivalent to `VarBinData::bytes`.
364    codes_bytes: BufferHandle,
365    /// Cached length (number of elements).
366    len: usize,
367}
368
369impl Display for FSSTData {
370    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
371        write!(
372            f,
373            "len: {}, nsymbols: {}",
374            self.len,
375            self.symbol_table.symbols.len()
376        )
377    }
378}
379
380impl Debug for FSSTData {
381    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
382        f.debug_struct("FSSTArray")
383            .field("symbols", &self.symbol_table.symbols)
384            .field("symbol_lengths", &self.symbol_table.symbol_lengths)
385            .field("codes_bytes_len", &self.codes_bytes.len())
386            .field("len", &self.len)
387            .field("uncompressed_lengths", &"<outer slot>")
388            .field("codes_offsets", &"<outer slot>")
389            .field("codes_validity", &"<outer slot>")
390            .finish()
391    }
392}
393
394pub(crate) struct FSSTSymbolTable {
395    symbols: Buffer<Symbol>,
396    symbol_lengths: Buffer<u8>,
397    /// Memoized compressor used for push-down of compute by compressing the RHS.
398    compressor: OnceLock<Compressor>,
399}
400
401impl FSSTSymbolTable {
402    fn new(symbols: Buffer<Symbol>, symbol_lengths: Buffer<u8>) -> Self {
403        Self {
404            symbols,
405            symbol_lengths,
406            compressor: OnceLock::new(),
407        }
408    }
409
410    fn compressor(&self) -> &Compressor {
411        self.compressor.get_or_init(|| {
412            Compressor::rebuild_from(self.symbols.as_slice(), self.symbol_lengths.as_slice())
413        })
414    }
415}
416
417#[derive(Clone, Debug)]
418pub struct FSST;
419
420impl FSST {
421    /// Build an FSST array from a set of `symbols` and `codes`.
422    ///
423    /// The `codes` VarBinArray is decomposed: its bytes are stored in [`FSSTData`], while
424    /// its offsets and validity become array slots. The codes VarBinArray can be
425    /// reconstructed on demand via [`FSSTArrayExt::codes()`].
426    pub fn try_new(
427        dtype: DType,
428        symbols: Buffer<Symbol>,
429        symbol_lengths: Buffer<u8>,
430        codes: VarBinArray,
431        uncompressed_lengths: ArrayRef,
432        ctx: &mut ExecutionCtx,
433    ) -> VortexResult<FSSTArray> {
434        let len = codes.len();
435        FSSTData::validate_parts_from_codes(
436            &symbols,
437            &symbol_lengths,
438            &codes,
439            &uncompressed_lengths,
440            &dtype,
441            len,
442            ctx,
443        )?;
444        let slots = FSSTData::make_slots(&codes, &uncompressed_lengths);
445        let codes_bytes = codes.bytes_handle().clone();
446        let data = FSSTData::try_new(symbols, symbol_lengths, codes_bytes, len)?;
447        Ok(unsafe {
448            Array::from_parts_unchecked(ArrayParts::new(FSST, dtype, len, data).with_slots(slots))
449        })
450    }
451
452    pub(crate) fn try_new_with_symbol_table(
453        dtype: DType,
454        symbol_table: Arc<FSSTSymbolTable>,
455        codes: VarBinArray,
456        uncompressed_lengths: ArrayRef,
457        ctx: &mut ExecutionCtx,
458    ) -> VortexResult<FSSTArray> {
459        let len = codes.len();
460        FSSTData::validate_parts_from_codes(
461            &symbol_table.symbols,
462            &symbol_table.symbol_lengths,
463            &codes,
464            &uncompressed_lengths,
465            &dtype,
466            len,
467            ctx,
468        )?;
469        let slots = FSSTData::make_slots(&codes, &uncompressed_lengths);
470        let codes_bytes = codes.bytes_handle().clone();
471        let data =
472            unsafe { FSSTData::new_unchecked_with_symbol_table(symbol_table, codes_bytes, len) };
473        Ok(unsafe {
474            Array::from_parts_unchecked(ArrayParts::new(FSST, dtype, len, data).with_slots(slots))
475        })
476    }
477
478    /// Legacy deserialization path (2 buffers): the codes were stored as a full
479    /// `VarBinArray` child. We decompose the VarBinArray into its bytes (stored in
480    /// FSSTData) and offsets/validity (stored in slots).
481    #[allow(clippy::too_many_arguments)]
482    fn deserialize_legacy(
483        &self,
484        dtype: &DType,
485        len: usize,
486        metadata: &FSSTMetadata,
487        symbols: &Buffer<Symbol>,
488        symbol_lengths: &Buffer<u8>,
489        children: &dyn ArrayChildren,
490        ctx: &mut ExecutionCtx,
491    ) -> VortexResult<ArrayParts<Self>> {
492        if children.len() != 2 {
493            vortex_bail!(InvalidArgument: "Expected 2 children, got {}", children.len());
494        }
495        let codes = children.get(0, &DType::Binary(dtype.nullability()), len)?;
496        let codes: VarBinArray = codes
497            .as_opt::<VarBin>()
498            .ok_or_else(|| {
499                vortex_err!(
500                    "Expected VarBinArray for codes, got {}",
501                    codes.encoding_id()
502                )
503            })?
504            .into_owned();
505        let uncompressed_lengths = children.get(
506            1,
507            &DType::Primitive(
508                metadata.get_uncompressed_lengths_ptype()?,
509                Nullability::NonNullable,
510            ),
511            len,
512        )?;
513
514        FSSTData::validate_parts_from_codes(
515            symbols,
516            symbol_lengths,
517            &codes,
518            &uncompressed_lengths,
519            dtype,
520            len,
521            ctx,
522        )?;
523        let slots = FSSTData::make_slots(&codes, &uncompressed_lengths);
524        let codes_bytes = codes.bytes_handle().clone();
525        let data = FSSTData::try_new(symbols.clone(), symbol_lengths.clone(), codes_bytes, len)?;
526        Ok(ArrayParts::new(self.clone(), dtype.clone(), len, data).with_slots(slots))
527    }
528
529    pub(crate) unsafe fn new_unchecked_with_symbol_table(
530        dtype: DType,
531        symbol_table: Arc<FSSTSymbolTable>,
532        codes: VarBinArray,
533        uncompressed_lengths: ArrayRef,
534    ) -> FSSTArray {
535        let len = codes.len();
536        let slots = FSSTData::make_slots(&codes, &uncompressed_lengths);
537        let codes_bytes = codes.bytes_handle().clone();
538        let data =
539            unsafe { FSSTData::new_unchecked_with_symbol_table(symbol_table, codes_bytes, len) };
540        unsafe {
541            Array::from_parts_unchecked(ArrayParts::new(FSST, dtype, len, data).with_slots(slots))
542        }
543    }
544}
545
546impl FSSTData {
547    fn make_slots(codes: &VarBinArray, uncompressed_lengths: &ArrayRef) -> ArraySlots {
548        smallvec![
549            Some(uncompressed_lengths.clone()),
550            Some(codes.offsets().clone()),
551            validity_to_child(
552                &codes
553                    .validity()
554                    .vortex_expect("FSST codes validity should be derivable"),
555                codes.len(),
556            ),
557        ]
558    }
559
560    /// Build FSST data from a set of `symbols`, `symbol_lengths`, and compressed codes bytes.
561    ///
562    /// Symbols are 8-bytes and can represent short strings, each of which is assigned
563    /// a code.
564    ///
565    /// The `codes_bytes` buffer contains the concatenated compressed bytecodes for all elements.
566    /// Each element's compressed bytecodes are a sequence of 8-bit codes, where each code
567    /// corresponds either to a symbol or to the "escape code" (which tells the decoder to
568    /// emit the following byte without doing a table lookup).
569    ///
570    /// The offsets and validity for the codes are stored in the array's slots, not here.
571    /// Use [`FSSTArrayExt::codes()`] to reconstruct a full `VarBinArray`.
572    pub fn try_new(
573        symbols: Buffer<Symbol>,
574        symbol_lengths: Buffer<u8>,
575        codes_bytes: BufferHandle,
576        len: usize,
577    ) -> VortexResult<Self> {
578        // SAFETY: all components validated above
579        unsafe {
580            Ok(Self::new_unchecked(
581                symbols,
582                symbol_lengths,
583                codes_bytes,
584                len,
585            ))
586        }
587    }
588
589    pub fn validate(
590        &self,
591        dtype: &DType,
592        len: usize,
593        slots: &[Option<ArrayRef>],
594        ctx: &mut ExecutionCtx,
595    ) -> VortexResult<()> {
596        let codes_offsets = slots[CODES_OFFSETS_SLOT]
597            .as_ref()
598            .vortex_expect("FSSTArray codes_offsets slot");
599        Self::validate_parts(
600            &self.symbol_table.symbols,
601            &self.symbol_table.symbol_lengths,
602            &self.codes_bytes,
603            codes_offsets,
604            dtype.nullability(),
605            uncompressed_lengths_from_slots(slots),
606            dtype,
607            len,
608            ctx,
609        )
610    }
611
612    /// Validate using the decomposed components (codes bytes + offsets + nullability).
613    #[expect(clippy::too_many_arguments)]
614    fn validate_parts(
615        symbols: &Buffer<Symbol>,
616        symbol_lengths: &Buffer<u8>,
617        codes_bytes: &BufferHandle,
618        codes_offsets: &ArrayRef,
619        codes_nullability: Nullability,
620        uncompressed_lengths: &ArrayRef,
621        dtype: &DType,
622        len: usize,
623        ctx: &mut ExecutionCtx,
624    ) -> VortexResult<()> {
625        vortex_ensure!(
626            matches!(dtype, DType::Binary(_) | DType::Utf8(_)),
627            "FSST arrays must be Binary or Utf8, found {dtype}"
628        );
629
630        if symbols.len() > 255 {
631            vortex_bail!(InvalidArgument: "symbols array must have length <= 255");
632        }
633
634        if symbols.len() != symbol_lengths.len() {
635            vortex_bail!(InvalidArgument: "symbols and symbol_lengths arrays must have same length");
636        }
637
638        Self::validate_symbol_lengths(symbol_lengths.as_slice())?;
639
640        // codes_offsets.len() - 1 == number of elements
641        let codes_len = codes_offsets.len().saturating_sub(1);
642        if codes_len != len {
643            vortex_bail!(InvalidArgument: "codes must have same len as outer array");
644        }
645
646        if uncompressed_lengths.len() != len {
647            vortex_bail!(InvalidArgument: "uncompressed_lengths must be same len as codes");
648        }
649
650        if !uncompressed_lengths.dtype().is_int() || uncompressed_lengths.dtype().is_nullable() {
651            vortex_bail!(InvalidArgument: "uncompressed_lengths must have integer type and cannot be nullable, found {}", uncompressed_lengths.dtype());
652        }
653
654        // Offsets must be non-nullable integer.
655        if !codes_offsets.dtype().is_int() || codes_offsets.dtype().is_nullable() {
656            vortex_bail!(InvalidArgument: "codes offsets must be non-nullable integer type, found {}", codes_offsets.dtype());
657        }
658
659        if codes_nullability != dtype.nullability() {
660            vortex_bail!(InvalidArgument: "codes nullability must match outer dtype nullability");
661        }
662
663        // Validate that last offset doesn't exceed bytes length (when host-resident).
664        if codes_bytes.is_on_host() && codes_offsets.is_host() && !codes_offsets.is_empty() {
665            let last_offset: usize = (&codes_offsets
666                .execute_scalar(codes_offsets.len() - 1, ctx)
667                .vortex_expect("offsets must support scalar_at"))
668                .try_into()
669                .vortex_expect("Failed to convert offset to usize");
670            vortex_ensure!(
671                last_offset <= codes_bytes.len(),
672                InvalidArgument: "Last codes offset {} exceeds codes bytes length {}",
673                last_offset,
674                codes_bytes.len()
675            );
676        }
677
678        Ok(())
679    }
680
681    fn validate_symbol_lengths(symbol_lengths: &[u8]) -> VortexResult<()> {
682        let mut expected = 2;
683        for (idx, &len) in symbol_lengths.iter().enumerate() {
684            if len > 8 || len == 0 {
685                vortex_bail!(InvalidArgument: "symbol length at index {idx} must be between 1 and 8, found {len}");
686            }
687
688            if expected == 1 {
689                if len != 1 {
690                    vortex_bail!(InvalidArgument: "symbol length at index {idx} must be 1 after one-byte symbols begin, found {len}");
691                }
692            } else {
693                if len == 1 {
694                    expected = 1;
695                }
696
697                if len < expected {
698                    vortex_bail!(InvalidArgument: "symbol length at index {idx} violates FSST symbol table ordering");
699                }
700                expected = len;
701            }
702        }
703
704        Ok(())
705    }
706
707    /// Validate using a VarBinArray for the codes (convenience for construction paths).
708    fn validate_parts_from_codes(
709        symbols: &Buffer<Symbol>,
710        symbol_lengths: &Buffer<u8>,
711        codes: &VarBinArray,
712        uncompressed_lengths: &ArrayRef,
713        dtype: &DType,
714        len: usize,
715        ctx: &mut ExecutionCtx,
716    ) -> VortexResult<()> {
717        Self::validate_parts(
718            symbols,
719            symbol_lengths,
720            codes.bytes_handle(),
721            codes.offsets(),
722            codes.dtype().nullability(),
723            uncompressed_lengths,
724            dtype,
725            len,
726            ctx,
727        )
728    }
729
730    pub(crate) unsafe fn new_unchecked(
731        symbols: Buffer<Symbol>,
732        symbol_lengths: Buffer<u8>,
733        codes_bytes: BufferHandle,
734        len: usize,
735    ) -> Self {
736        let symbol_table = Arc::new(FSSTSymbolTable::new(symbols, symbol_lengths));
737        unsafe { Self::new_unchecked_with_symbol_table(symbol_table, codes_bytes, len) }
738    }
739
740    pub(crate) unsafe fn new_unchecked_with_symbol_table(
741        symbol_table: Arc<FSSTSymbolTable>,
742        codes_bytes: BufferHandle,
743        len: usize,
744    ) -> Self {
745        Self {
746            symbol_table,
747            codes_bytes,
748            len,
749        }
750    }
751
752    /// Returns the number of elements in the array.
753    pub fn len(&self) -> usize {
754        self.len
755    }
756
757    /// Returns `true` if the array contains no elements.
758    pub fn is_empty(&self) -> bool {
759        self.len == 0
760    }
761
762    /// Access the symbol table array.
763    pub fn symbols(&self) -> &Buffer<Symbol> {
764        &self.symbol_table.symbols
765    }
766
767    /// Access the symbol lengths array.
768    pub fn symbol_lengths(&self) -> &Buffer<u8> {
769        &self.symbol_table.symbol_lengths
770    }
771
772    pub(crate) fn symbol_table(&self) -> Arc<FSSTSymbolTable> {
773        Arc::clone(&self.symbol_table)
774    }
775
776    /// Access the compressed codes bytes buffer handle (may be on host or device).
777    pub fn codes_bytes_handle(&self) -> &BufferHandle {
778        &self.codes_bytes
779    }
780
781    /// Access the compressed codes bytes on the host.
782    pub fn codes_bytes(&self) -> &ByteBuffer {
783        self.codes_bytes.as_host()
784    }
785
786    /// Build a [`Decompressor`] that can be used to decompress values from
787    /// this array.
788    pub fn decompressor(&self) -> Decompressor<'_> {
789        Decompressor::new(self.symbols().as_slice(), self.symbol_lengths().as_slice())
790    }
791
792    /// Retrieves the FSST compressor.
793    pub fn compressor(&self) -> &Compressor {
794        self.symbol_table.compressor()
795    }
796}
797
798fn uncompressed_lengths_from_slots(slots: &[Option<ArrayRef>]) -> &ArrayRef {
799    slots[UNCOMPRESSED_LENGTHS_SLOT]
800        .as_ref()
801        .vortex_expect("FSSTArray uncompressed_lengths slot")
802}
803
804pub trait FSSTArrayExt: TypedArrayRef<FSST> {
805    fn uncompressed_lengths(&self) -> &ArrayRef {
806        uncompressed_lengths_from_slots(self.as_ref().slots())
807    }
808
809    fn uncompressed_lengths_dtype(&self) -> &DType {
810        self.uncompressed_lengths().dtype()
811    }
812
813    /// Reconstruct a [`VarBinArray`] for the compressed codes by combining the bytes
814    /// from [`FSSTData`] with the offsets and validity stored in the array's slots.
815    fn codes(&self) -> VarBinArray {
816        let offsets = self.as_ref().slots()[CODES_OFFSETS_SLOT]
817            .as_ref()
818            .vortex_expect("FSSTArray codes_offsets slot")
819            .clone();
820        let validity = child_to_validity(
821            self.as_ref().slots()[CODES_VALIDITY_SLOT].as_ref(),
822            self.as_ref().dtype().nullability(),
823        );
824        let codes_bytes = self.codes_bytes_handle().clone();
825        // SAFETY: components were validated at construction time.
826        unsafe {
827            VarBinArray::new_unchecked_from_handle(
828                offsets,
829                codes_bytes,
830                DType::Binary(self.as_ref().dtype().nullability()),
831                validity,
832            )
833        }
834    }
835
836    /// Get the DType of the codes array.
837    fn codes_dtype(&self) -> DType {
838        DType::Binary(self.as_ref().dtype().nullability())
839    }
840}
841
842impl<T: TypedArrayRef<FSST>> FSSTArrayExt for T {}
843
844impl ValidityVTable<FSST> for FSST {
845    fn validity(array: ArrayView<'_, FSST>) -> VortexResult<Validity> {
846        Ok(child_to_validity(
847            array.slots()[CODES_VALIDITY_SLOT].as_ref(),
848            array.dtype().nullability(),
849        ))
850    }
851}
852
853#[cfg(test)]
854mod test {
855    use fsst::Compressor;
856    use fsst::Symbol;
857    use prost::Message;
858    use vortex_array::ArrayPlugin;
859    use vortex_array::IntoArray;
860    use vortex_array::VortexSessionExecute;
861    use vortex_array::array_session;
862    use vortex_array::arrays::VarBinViewArray;
863    use vortex_array::buffer::BufferHandle;
864    use vortex_array::dtype::DType;
865    use vortex_array::dtype::Nullability;
866    use vortex_array::dtype::PType;
867    use vortex_array::test_harness::check_metadata;
868    use vortex_buffer::Buffer;
869    use vortex_error::VortexResult;
870    use vortex_error::vortex_err;
871
872    use crate::FSST;
873    use crate::array::FSSTArrayExt;
874    use crate::array::FSSTMetadata;
875    use crate::fsst_compress;
876
877    #[test]
878    fn slice_reuses_initialized_compressor() -> VortexResult<()> {
879        let symbols = Buffer::<Symbol>::copy_from([
880            Symbol::from_slice(b"abc00000"),
881            Symbol::from_slice(b"defghijk"),
882        ]);
883        let symbol_lengths = Buffer::<u8>::copy_from([3, 8]);
884
885        let compressor = Compressor::rebuild_from(symbols.as_slice(), symbol_lengths.as_slice());
886        let mut ctx = array_session().create_execution_ctx();
887        let strings = VarBinViewArray::from_iter_str(["abcabcab", "defghijk", "abcxyz"]);
888        let fsst_array = fsst_compress(&strings.into_array(), &compressor, &mut ctx)?;
889
890        let compressor_ptr = fsst_array.compressor() as *const Compressor;
891        let sliced = fsst_array
892            .slice(1..3)?
893            .try_downcast::<FSST>()
894            .map_err(|_| vortex_err!("slice must return an FSST array"))?;
895        let sliced_compressor_ptr = sliced.compressor() as *const Compressor;
896
897        assert_eq!(compressor_ptr, sliced_compressor_ptr);
898        Ok(())
899    }
900
901    #[cfg_attr(miri, ignore)]
902    #[test]
903    fn test_fsst_metadata() {
904        check_metadata(
905            "fsst.metadata",
906            &FSSTMetadata {
907                uncompressed_lengths_ptype: PType::U64 as i32,
908                codes_offsets_ptype: PType::I32 as i32,
909            }
910            .encode_to_vec(),
911        );
912    }
913
914    /// The original FSST array stored codes as a VarBinArray child and required that the child
915    /// have this encoding. Vortex forbids this kind of introspection, therefore we had to fix
916    /// the array to store the compressed offsets and compressed data buffer separately, and only
917    /// use VarBinArray to delegate behavior.
918    ///
919    /// This test manually constructs an old-style FSST array and ensures that it can still be
920    /// deserialized.
921    #[test]
922    fn test_back_compat() -> VortexResult<()> {
923        let symbols = Buffer::<Symbol>::copy_from([
924            Symbol::from_slice(b"abc00000"),
925            Symbol::from_slice(b"defghijk"),
926        ]);
927        let symbol_lengths = Buffer::<u8>::copy_from([3, 8]);
928
929        let compressor = Compressor::rebuild_from(symbols.as_slice(), symbol_lengths.as_slice());
930        let mut ctx = array_session().create_execution_ctx();
931        let input = VarBinViewArray::from_iter_str(["abcabcab", "defghijk"]);
932        let fsst_array = fsst_compress(&input.into_array(), &compressor, &mut ctx)?;
933
934        let compressed_codes = fsst_array.codes();
935
936        // There were two buffers:
937        // 1. The 8 byte symbols
938        // 2. The symbol lengths as u8.
939        let buffers = [
940            BufferHandle::new_host(symbols.into_byte_buffer()),
941            BufferHandle::new_host(symbol_lengths.into_byte_buffer()),
942        ];
943
944        // There were 2 children:
945        // 1. The compressed codes, stored as a VarBinArray.
946        // 2. The uncompressed lengths, stored as a Primitive array.
947        let children = vec![
948            compressed_codes.into_array(),
949            fsst_array.uncompressed_lengths().clone(),
950        ];
951
952        let fsst = ArrayPlugin::deserialize(
953            &FSST,
954            &DType::Utf8(Nullability::NonNullable),
955            2,
956            &FSSTMetadata {
957                uncompressed_lengths_ptype: fsst_array
958                    .uncompressed_lengths()
959                    .dtype()
960                    .as_ptype()
961                    .into(),
962                // Legacy array did not store this field, use Protobuf default of 0.
963                codes_offsets_ptype: 0,
964            }
965            .encode_to_vec(),
966            &buffers,
967            &children.as_slice(),
968            &array_session(),
969        )?;
970
971        let decompressed =
972            fsst.execute::<VarBinViewArray>(&mut array_session().create_execution_ctx())?;
973        let mask = decompressed
974            .validity()?
975            .execute_mask(decompressed.len(), &mut ctx)?;
976        assert!(mask.value(0));
977        assert_eq!(decompressed.bytes_at(0).as_slice(), b"abcabcab".as_ref());
978        assert!(mask.value(1));
979        assert_eq!(decompressed.bytes_at(1).as_slice(), b"defghijk".as_ref());
980        Ok(())
981    }
982}