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::array_slots;
31use vortex_array::arrays::VarBin;
32use vortex_array::arrays::VarBinArray;
33use vortex_array::arrays::varbin::VarBinArraySlotsExt;
34use vortex_array::buffer::BufferHandle;
35use vortex_array::builders::ArrayBuilder;
36use vortex_array::builders::VarBinViewBuilder;
37use vortex_array::dtype::DType;
38use vortex_array::dtype::Nullability;
39use vortex_array::dtype::PType;
40use vortex_array::legacy_session;
41use vortex_array::serde::ArrayChildren;
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.codes_offsets();
176        Ok(Some(
177            FSSTMetadata {
178                uncompressed_lengths_ptype: array.uncompressed_lengths().dtype().as_ptype().into(),
179                codes_offsets_ptype: codes_offsets.dtype().as_ptype().into(),
180            }
181            .encode_to_vec(),
182        ))
183    }
184
185    /// Deserializes an FSST array from its serialized components.
186    ///
187    /// Supports two serialization formats:
188    ///
189    /// ## Legacy format (2 buffers, 2 children)
190    ///
191    /// The original FSST layout stored the compressed codes as a full `VarBinArray` child.
192    /// - **Buffers**: `[symbols, symbol_lengths]`
193    /// - **Children**: `[codes (VarBinArray), uncompressed_lengths (Primitive)]`
194    ///
195    /// The codes VarBinArray child is decomposed: its bytes become the `codes_bytes` buffer,
196    /// and its offsets/validity are extracted into slots.
197    /// See `FSST::deserialize_legacy`.
198    ///
199    /// ## Current format (3 buffers, 2-3 children)
200    ///
201    /// The current layout stores the compressed bytes as a raw buffer alongside the symbol
202    /// table, with offsets and validity as separate children.
203    /// - **Buffers**: `[symbols, symbol_lengths, compressed_codes_bytes]`
204    /// - **Children**: `[uncompressed_lengths, codes_offsets, (optional) codes_validity]`
205    ///
206    /// The `codes_bytes` buffer is stored directly in `FSSTData`. A `VarBinArray` for the
207    /// codes can be reconstructed on demand via [`FSSTArrayExt::codes()`] using the bytes
208    /// from `FSSTData` combined with offsets and validity from the array's slots.
209    fn deserialize(
210        &self,
211        dtype: &DType,
212        len: usize,
213        metadata: &[u8],
214        buffers: &[BufferHandle],
215        children: &dyn ArrayChildren,
216        session: &VortexSession,
217    ) -> VortexResult<ArrayParts<Self>> {
218        let metadata = FSSTMetadata::decode(metadata)?;
219        let symbols = Buffer::<Symbol>::from_byte_buffer(buffers[0].clone().try_to_host_sync()?);
220        let symbol_lengths = Buffer::<u8>::from_byte_buffer(buffers[1].clone().try_to_host_sync()?);
221
222        let mut ctx = session.create_execution_ctx();
223        if buffers.len() == 2 {
224            return Self::deserialize_legacy(
225                self,
226                dtype,
227                len,
228                &metadata,
229                &symbols,
230                &symbol_lengths,
231                children,
232                &mut ctx,
233            );
234        }
235
236        if buffers.len() == 3 {
237            let uncompressed_lengths = children.get(
238                0,
239                &DType::Primitive(
240                    metadata.get_uncompressed_lengths_ptype()?,
241                    Nullability::NonNullable,
242                ),
243                len,
244            )?;
245
246            let codes_bytes = buffers[2].clone();
247            let codes_offsets = children.get(
248                1,
249                &DType::Primitive(
250                    PType::try_from(metadata.codes_offsets_ptype)?,
251                    Nullability::NonNullable,
252                ),
253                // VarBin offsets are len + 1
254                len + 1,
255            )?;
256
257            let codes_validity = if children.len() == 2 {
258                Validity::from(dtype.nullability())
259            } else if children.len() == 3 {
260                let validity = children.get(2, &Validity::DTYPE, len)?;
261                Validity::Array(validity)
262            } else {
263                vortex_bail!("Expected 2 or 3 children, got {}", children.len());
264            };
265
266            FSSTData::validate_parts(
267                &symbols,
268                &symbol_lengths,
269                &codes_bytes,
270                &codes_offsets,
271                dtype.nullability(),
272                &uncompressed_lengths,
273                dtype,
274                len,
275                &mut ctx,
276            )?;
277            let slots = FSSTSlots {
278                uncompressed_lengths,
279                codes_offsets,
280                codes_validity: validity_to_child(&codes_validity, len),
281            }
282            .into_slots();
283            let data = FSSTData::try_new(symbols, symbol_lengths, codes_bytes, len)?;
284            return Ok(ArrayParts::new(self.clone(), dtype.clone(), len, data).with_slots(slots));
285        }
286
287        vortex_bail!(
288            "InvalidArgument: Expected 2 or 3 buffers, got {}",
289            buffers.len()
290        );
291    }
292
293    fn slot_name(_array: ArrayView<'_, Self>, idx: usize) -> String {
294        FSSTSlots::NAMES[idx].to_string()
295    }
296
297    fn execute(array: Array<Self>, ctx: &mut ExecutionCtx) -> VortexResult<ExecutionResult> {
298        canonicalize_fsst(array.as_view(), ctx).map(ExecutionResult::done)
299    }
300
301    fn append_to_builder(
302        array: ArrayView<'_, Self>,
303        builder: &mut dyn ArrayBuilder,
304        ctx: &mut ExecutionCtx,
305    ) -> VortexResult<()> {
306        let Some(builder) = builder.as_any_mut().downcast_mut::<VarBinViewBuilder>() else {
307            return array
308                .array()
309                .clone()
310                .execute::<Canonical>(ctx)?
311                .into_array()
312                .append_to_builder(builder, ctx);
313        };
314
315        // Decompress the whole block of data into a new buffer, and create some views
316        // from it instead. The new buffer lands after any pending in-progress
317        // buffer that push_buffer_and_adjusted_views will flush first.
318        let next_buffer_index = builder.completed_block_count() + u32::from(builder.in_progress());
319        let (buffers, views) = fsst_decode_views(array, next_buffer_index, ctx)?;
320
321        builder.push_buffer_and_adjusted_views(
322            &buffers,
323            &views,
324            array
325                .array()
326                .validity()?
327                .execute_mask(array.array().len(), ctx)?,
328        );
329        Ok(())
330    }
331
332    fn reduce_parent(
333        array: ArrayView<'_, Self>,
334        parent: &ArrayRef,
335        child_idx: usize,
336    ) -> VortexResult<Option<ArrayRef>> {
337        RULES.evaluate(array, parent, child_idx)
338    }
339}
340
341#[array_slots(FSST)]
342pub struct FSSTSlots {
343    /// Lengths of the original values before compression, can be compressed.
344    pub uncompressed_lengths: ArrayRef,
345    /// The offsets array for the FSST-compressed codes.
346    pub codes_offsets: ArrayRef,
347    /// The validity bitmap for the compressed codes.
348    pub codes_validity: Option<ArrayRef>,
349}
350
351/// The inner data for an FSST-compressed array.
352///
353/// Holds the FSST symbol table (`symbols` + `symbol_lengths`) and the raw compressed
354/// codes bytes buffer. The codes offsets and validity live in the outer array's slots
355/// (slots 1 and 2 respectively).
356///
357/// A full [`VarBinArray`] representing the codes can be reconstructed on demand via
358/// [`FSSTArrayExt::codes()`], combining this buffer with the offsets/validity from slots.
359#[derive(Clone)]
360pub struct FSSTData {
361    symbol_table: Arc<FSSTSymbolTable>,
362    /// The raw compressed codes bytes, equivalent to `VarBinData::bytes`.
363    codes_bytes: BufferHandle,
364    /// Cached length (number of elements).
365    len: usize,
366}
367
368impl Display for FSSTData {
369    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
370        write!(
371            f,
372            "len: {}, nsymbols: {}",
373            self.len,
374            self.symbol_table.symbols.len()
375        )
376    }
377}
378
379impl Debug for FSSTData {
380    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
381        f.debug_struct("FSSTArray")
382            .field("symbols", &self.symbol_table.symbols)
383            .field("symbol_lengths", &self.symbol_table.symbol_lengths)
384            .field("codes_bytes_len", &self.codes_bytes.len())
385            .field("len", &self.len)
386            .field("uncompressed_lengths", &"<outer slot>")
387            .field("codes_offsets", &"<outer slot>")
388            .field("codes_validity", &"<outer slot>")
389            .finish()
390    }
391}
392
393pub(crate) struct FSSTSymbolTable {
394    symbols: Buffer<Symbol>,
395    symbol_lengths: Buffer<u8>,
396    /// Memoized compressor used for push-down of compute by compressing the RHS.
397    compressor: OnceLock<Compressor>,
398}
399
400impl FSSTSymbolTable {
401    fn new(symbols: Buffer<Symbol>, symbol_lengths: Buffer<u8>) -> Self {
402        Self {
403            symbols,
404            symbol_lengths,
405            compressor: OnceLock::new(),
406        }
407    }
408
409    fn compressor(&self) -> &Compressor {
410        self.compressor.get_or_init(|| {
411            Compressor::rebuild_from(self.symbols.as_slice(), self.symbol_lengths.as_slice())
412        })
413    }
414}
415
416#[derive(Clone, Debug)]
417pub struct FSST;
418
419impl FSST {
420    /// Build an FSST array from a set of `symbols` and `codes`.
421    ///
422    /// The `codes` VarBinArray is decomposed: its bytes are stored in [`FSSTData`], while
423    /// its offsets and validity become array slots. The codes VarBinArray can be
424    /// reconstructed on demand via [`FSSTArrayExt::codes()`].
425    pub fn try_new(
426        dtype: DType,
427        symbols: Buffer<Symbol>,
428        symbol_lengths: Buffer<u8>,
429        codes: VarBinArray,
430        uncompressed_lengths: ArrayRef,
431        ctx: &mut ExecutionCtx,
432    ) -> VortexResult<FSSTArray> {
433        let len = codes.len();
434        FSSTData::validate_parts_from_codes(
435            &symbols,
436            &symbol_lengths,
437            &codes,
438            &uncompressed_lengths,
439            &dtype,
440            len,
441            ctx,
442        )?;
443        let slots = FSSTData::make_slots(&codes, &uncompressed_lengths);
444        let codes_bytes = codes.bytes_handle().clone();
445        let data = FSSTData::try_new(symbols, symbol_lengths, codes_bytes, len)?;
446        Ok(unsafe {
447            Array::from_parts_unchecked(ArrayParts::new(FSST, dtype, len, data).with_slots(slots))
448        })
449    }
450
451    pub(crate) fn try_new_with_symbol_table(
452        dtype: DType,
453        symbol_table: Arc<FSSTSymbolTable>,
454        codes: VarBinArray,
455        uncompressed_lengths: ArrayRef,
456        ctx: &mut ExecutionCtx,
457    ) -> VortexResult<FSSTArray> {
458        let len = codes.len();
459        FSSTData::validate_parts_from_codes(
460            &symbol_table.symbols,
461            &symbol_table.symbol_lengths,
462            &codes,
463            &uncompressed_lengths,
464            &dtype,
465            len,
466            ctx,
467        )?;
468        let slots = FSSTData::make_slots(&codes, &uncompressed_lengths);
469        let codes_bytes = codes.bytes_handle().clone();
470        let data =
471            unsafe { FSSTData::new_unchecked_with_symbol_table(symbol_table, codes_bytes, len) };
472        Ok(unsafe {
473            Array::from_parts_unchecked(ArrayParts::new(FSST, dtype, len, data).with_slots(slots))
474        })
475    }
476
477    /// Legacy deserialization path (2 buffers): the codes were stored as a full
478    /// `VarBinArray` child. We decompose the VarBinArray into its bytes (stored in
479    /// FSSTData) and offsets/validity (stored in slots).
480    #[allow(clippy::too_many_arguments)]
481    fn deserialize_legacy(
482        &self,
483        dtype: &DType,
484        len: usize,
485        metadata: &FSSTMetadata,
486        symbols: &Buffer<Symbol>,
487        symbol_lengths: &Buffer<u8>,
488        children: &dyn ArrayChildren,
489        ctx: &mut ExecutionCtx,
490    ) -> VortexResult<ArrayParts<Self>> {
491        if children.len() != 2 {
492            vortex_bail!(InvalidArgument: "Expected 2 children, got {}", children.len());
493        }
494        let codes = children.get(0, &DType::Binary(dtype.nullability()), len)?;
495        let codes: VarBinArray = codes
496            .as_opt::<VarBin>()
497            .ok_or_else(|| {
498                vortex_err!(
499                    "Expected VarBinArray for codes, got {}",
500                    codes.encoding_id()
501                )
502            })?
503            .into_owned();
504        let uncompressed_lengths = children.get(
505            1,
506            &DType::Primitive(
507                metadata.get_uncompressed_lengths_ptype()?,
508                Nullability::NonNullable,
509            ),
510            len,
511        )?;
512
513        FSSTData::validate_parts_from_codes(
514            symbols,
515            symbol_lengths,
516            &codes,
517            &uncompressed_lengths,
518            dtype,
519            len,
520            ctx,
521        )?;
522        let slots = FSSTData::make_slots(&codes, &uncompressed_lengths);
523        let codes_bytes = codes.bytes_handle().clone();
524        let data = FSSTData::try_new(symbols.clone(), symbol_lengths.clone(), codes_bytes, len)?;
525        Ok(ArrayParts::new(self.clone(), dtype.clone(), len, data).with_slots(slots))
526    }
527
528    pub(crate) unsafe fn new_unchecked_with_symbol_table(
529        dtype: DType,
530        symbol_table: Arc<FSSTSymbolTable>,
531        codes: VarBinArray,
532        uncompressed_lengths: ArrayRef,
533    ) -> FSSTArray {
534        let len = codes.len();
535        let slots = FSSTData::make_slots(&codes, &uncompressed_lengths);
536        let codes_bytes = codes.bytes_handle().clone();
537        let data =
538            unsafe { FSSTData::new_unchecked_with_symbol_table(symbol_table, codes_bytes, len) };
539        unsafe {
540            Array::from_parts_unchecked(ArrayParts::new(FSST, dtype, len, data).with_slots(slots))
541        }
542    }
543}
544
545impl FSSTData {
546    fn make_slots(codes: &VarBinArray, uncompressed_lengths: &ArrayRef) -> ArraySlots {
547        FSSTSlots {
548            uncompressed_lengths: uncompressed_lengths.clone(),
549            codes_offsets: codes.offsets().clone(),
550            codes_validity: validity_to_child(
551                &codes
552                    .validity()
553                    .vortex_expect("FSST codes validity should be derivable"),
554                codes.len(),
555            ),
556        }
557        .into_slots()
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 fsst_slots = FSSTSlotsView::from_slots(slots);
597        Self::validate_parts(
598            &self.symbol_table.symbols,
599            &self.symbol_table.symbol_lengths,
600            &self.codes_bytes,
601            fsst_slots.codes_offsets,
602            dtype.nullability(),
603            fsst_slots.uncompressed_lengths,
604            dtype,
605            len,
606            ctx,
607        )
608    }
609
610    /// Validate using the decomposed components (codes bytes + offsets + nullability).
611    #[expect(clippy::too_many_arguments)]
612    fn validate_parts(
613        symbols: &Buffer<Symbol>,
614        symbol_lengths: &Buffer<u8>,
615        codes_bytes: &BufferHandle,
616        codes_offsets: &ArrayRef,
617        codes_nullability: Nullability,
618        uncompressed_lengths: &ArrayRef,
619        dtype: &DType,
620        len: usize,
621        ctx: &mut ExecutionCtx,
622    ) -> VortexResult<()> {
623        vortex_ensure!(
624            matches!(dtype, DType::Binary(_) | DType::Utf8(_)),
625            "FSST arrays must be Binary or Utf8, found {dtype}"
626        );
627
628        if symbols.len() > 255 {
629            vortex_bail!(InvalidArgument: "symbols array must have length <= 255");
630        }
631
632        if symbols.len() != symbol_lengths.len() {
633            vortex_bail!(InvalidArgument: "symbols and symbol_lengths arrays must have same length");
634        }
635
636        Self::validate_symbol_lengths(symbol_lengths.as_slice())?;
637
638        // codes_offsets.len() - 1 == number of elements
639        let codes_len = codes_offsets.len().saturating_sub(1);
640        if codes_len != len {
641            vortex_bail!(InvalidArgument: "codes must have same len as outer array");
642        }
643
644        if uncompressed_lengths.len() != len {
645            vortex_bail!(InvalidArgument: "uncompressed_lengths must be same len as codes");
646        }
647
648        if !uncompressed_lengths.dtype().is_int() || uncompressed_lengths.dtype().is_nullable() {
649            vortex_bail!(InvalidArgument: "uncompressed_lengths must have integer type and cannot be nullable, found {}", uncompressed_lengths.dtype());
650        }
651
652        // Offsets must be non-nullable integer.
653        if !codes_offsets.dtype().is_int() || codes_offsets.dtype().is_nullable() {
654            vortex_bail!(InvalidArgument: "codes offsets must be non-nullable integer type, found {}", codes_offsets.dtype());
655        }
656
657        if codes_nullability != dtype.nullability() {
658            vortex_bail!(InvalidArgument: "codes nullability must match outer dtype nullability");
659        }
660
661        // Validate that last offset doesn't exceed bytes length (when host-resident).
662        if codes_bytes.is_on_host() && codes_offsets.is_host() && !codes_offsets.is_empty() {
663            let last_offset: usize = (&codes_offsets
664                .execute_scalar(codes_offsets.len() - 1, ctx)
665                .vortex_expect("offsets must support scalar_at"))
666                .try_into()
667                .vortex_expect("Failed to convert offset to usize");
668            vortex_ensure!(
669                last_offset <= codes_bytes.len(),
670                InvalidArgument: "Last codes offset {} exceeds codes bytes length {}",
671                last_offset,
672                codes_bytes.len()
673            );
674        }
675
676        Ok(())
677    }
678
679    fn validate_symbol_lengths(symbol_lengths: &[u8]) -> VortexResult<()> {
680        let mut expected = 2;
681        for (idx, &len) in symbol_lengths.iter().enumerate() {
682            if len > 8 || len == 0 {
683                vortex_bail!(InvalidArgument: "symbol length at index {idx} must be between 1 and 8, found {len}");
684            }
685
686            if expected == 1 {
687                if len != 1 {
688                    vortex_bail!(InvalidArgument: "symbol length at index {idx} must be 1 after one-byte symbols begin, found {len}");
689                }
690            } else {
691                if len == 1 {
692                    expected = 1;
693                }
694
695                if len < expected {
696                    vortex_bail!(InvalidArgument: "symbol length at index {idx} violates FSST symbol table ordering");
697                }
698                expected = len;
699            }
700        }
701
702        Ok(())
703    }
704
705    /// Validate using a VarBinArray for the codes (convenience for construction paths).
706    fn validate_parts_from_codes(
707        symbols: &Buffer<Symbol>,
708        symbol_lengths: &Buffer<u8>,
709        codes: &VarBinArray,
710        uncompressed_lengths: &ArrayRef,
711        dtype: &DType,
712        len: usize,
713        ctx: &mut ExecutionCtx,
714    ) -> VortexResult<()> {
715        Self::validate_parts(
716            symbols,
717            symbol_lengths,
718            codes.bytes_handle(),
719            codes.offsets(),
720            codes.dtype().nullability(),
721            uncompressed_lengths,
722            dtype,
723            len,
724            ctx,
725        )
726    }
727
728    pub(crate) unsafe fn new_unchecked(
729        symbols: Buffer<Symbol>,
730        symbol_lengths: Buffer<u8>,
731        codes_bytes: BufferHandle,
732        len: usize,
733    ) -> Self {
734        let symbol_table = Arc::new(FSSTSymbolTable::new(symbols, symbol_lengths));
735        unsafe { Self::new_unchecked_with_symbol_table(symbol_table, codes_bytes, len) }
736    }
737
738    pub(crate) unsafe fn new_unchecked_with_symbol_table(
739        symbol_table: Arc<FSSTSymbolTable>,
740        codes_bytes: BufferHandle,
741        len: usize,
742    ) -> Self {
743        Self {
744            symbol_table,
745            codes_bytes,
746            len,
747        }
748    }
749
750    /// Returns the number of elements in the array.
751    pub fn len(&self) -> usize {
752        self.len
753    }
754
755    /// Returns `true` if the array contains no elements.
756    pub fn is_empty(&self) -> bool {
757        self.len == 0
758    }
759
760    /// Access the symbol table array.
761    pub fn symbols(&self) -> &Buffer<Symbol> {
762        &self.symbol_table.symbols
763    }
764
765    /// Access the symbol lengths array.
766    pub fn symbol_lengths(&self) -> &Buffer<u8> {
767        &self.symbol_table.symbol_lengths
768    }
769
770    pub(crate) fn symbol_table(&self) -> Arc<FSSTSymbolTable> {
771        Arc::clone(&self.symbol_table)
772    }
773
774    /// Access the compressed codes bytes buffer handle (may be on host or device).
775    pub fn codes_bytes_handle(&self) -> &BufferHandle {
776        &self.codes_bytes
777    }
778
779    /// Access the compressed codes bytes on the host.
780    pub fn codes_bytes(&self) -> &ByteBuffer {
781        self.codes_bytes.as_host()
782    }
783
784    /// Build a [`Decompressor`] that can be used to decompress values from
785    /// this array.
786    pub fn decompressor(&self) -> Decompressor<'_> {
787        Decompressor::new(self.symbols().as_slice(), self.symbol_lengths().as_slice())
788    }
789
790    /// Retrieves the FSST compressor.
791    pub fn compressor(&self) -> &Compressor {
792        self.symbol_table.compressor()
793    }
794}
795
796pub trait FSSTArrayExt: FSSTArraySlotsExt {
797    fn uncompressed_lengths_dtype(&self) -> &DType {
798        self.uncompressed_lengths().dtype()
799    }
800
801    /// Reconstruct a [`VarBinArray`] for the compressed codes by combining the bytes
802    /// from [`FSSTData`] with the offsets and validity stored in the array's slots.
803    fn codes(&self) -> VarBinArray {
804        let offsets = self.codes_offsets().clone();
805        let validity =
806            child_to_validity(self.codes_validity(), self.as_ref().dtype().nullability());
807        let codes_bytes = self.codes_bytes_handle().clone();
808        // SAFETY: components were validated at construction time.
809        unsafe {
810            VarBinArray::new_unchecked_from_handle(
811                offsets,
812                codes_bytes,
813                DType::Binary(self.as_ref().dtype().nullability()),
814                validity,
815            )
816        }
817    }
818
819    /// Get the DType of the codes array.
820    fn codes_dtype(&self) -> DType {
821        DType::Binary(self.as_ref().dtype().nullability())
822    }
823}
824
825impl<T: TypedArrayRef<FSST>> FSSTArrayExt for T {}
826
827impl ValidityVTable<FSST> for FSST {
828    fn validity(array: ArrayView<'_, FSST>) -> VortexResult<Validity> {
829        Ok(child_to_validity(
830            array.codes_validity(),
831            array.dtype().nullability(),
832        ))
833    }
834}
835
836#[cfg(test)]
837mod test {
838    use fsst::Compressor;
839    use fsst::Symbol;
840    use prost::Message;
841    use vortex_array::ArrayPlugin;
842    use vortex_array::IntoArray;
843    use vortex_array::VortexSessionExecute;
844    use vortex_array::array_session;
845    use vortex_array::arrays::VarBinViewArray;
846    use vortex_array::buffer::BufferHandle;
847    use vortex_array::dtype::DType;
848    use vortex_array::dtype::Nullability;
849    use vortex_array::dtype::PType;
850    use vortex_array::test_harness::check_metadata;
851    use vortex_buffer::Buffer;
852    use vortex_error::VortexResult;
853    use vortex_error::vortex_err;
854
855    use crate::FSST;
856    use crate::array::FSSTArrayExt;
857    use crate::array::FSSTArraySlotsExt;
858    use crate::array::FSSTMetadata;
859    use crate::fsst_compress;
860
861    #[test]
862    fn slice_reuses_initialized_compressor() -> VortexResult<()> {
863        let symbols = Buffer::<Symbol>::copy_from([
864            Symbol::from_slice(b"abc00000"),
865            Symbol::from_slice(b"defghijk"),
866        ]);
867        let symbol_lengths = Buffer::<u8>::copy_from([3, 8]);
868
869        let compressor = Compressor::rebuild_from(symbols.as_slice(), symbol_lengths.as_slice());
870        let mut ctx = array_session().create_execution_ctx();
871        let strings = VarBinViewArray::from_iter_str(["abcabcab", "defghijk", "abcxyz"]);
872        let fsst_array = fsst_compress(&strings.into_array(), &compressor, &mut ctx)?;
873
874        let compressor_ptr = fsst_array.compressor() as *const Compressor;
875        let sliced = fsst_array
876            .slice(1..3)?
877            .try_downcast::<FSST>()
878            .map_err(|_| vortex_err!("slice must return an FSST array"))?;
879        let sliced_compressor_ptr = sliced.compressor() as *const Compressor;
880
881        assert_eq!(compressor_ptr, sliced_compressor_ptr);
882        Ok(())
883    }
884
885    #[cfg_attr(miri, ignore)]
886    #[test]
887    fn test_fsst_metadata() {
888        check_metadata(
889            "fsst.metadata",
890            &FSSTMetadata {
891                uncompressed_lengths_ptype: PType::U64 as i32,
892                codes_offsets_ptype: PType::I32 as i32,
893            }
894            .encode_to_vec(),
895        );
896    }
897
898    /// The original FSST array stored codes as a VarBinArray child and required that the child
899    /// have this encoding. Vortex forbids this kind of introspection, therefore we had to fix
900    /// the array to store the compressed offsets and compressed data buffer separately, and only
901    /// use VarBinArray to delegate behavior.
902    ///
903    /// This test manually constructs an old-style FSST array and ensures that it can still be
904    /// deserialized.
905    #[test]
906    fn test_back_compat() -> VortexResult<()> {
907        let symbols = Buffer::<Symbol>::copy_from([
908            Symbol::from_slice(b"abc00000"),
909            Symbol::from_slice(b"defghijk"),
910        ]);
911        let symbol_lengths = Buffer::<u8>::copy_from([3, 8]);
912
913        let compressor = Compressor::rebuild_from(symbols.as_slice(), symbol_lengths.as_slice());
914        let mut ctx = array_session().create_execution_ctx();
915        let input = VarBinViewArray::from_iter_str(["abcabcab", "defghijk"]);
916        let fsst_array = fsst_compress(&input.into_array(), &compressor, &mut ctx)?;
917
918        let compressed_codes = fsst_array.codes();
919
920        // There were two buffers:
921        // 1. The 8 byte symbols
922        // 2. The symbol lengths as u8.
923        let buffers = [
924            BufferHandle::new_host(symbols.into_byte_buffer()),
925            BufferHandle::new_host(symbol_lengths.into_byte_buffer()),
926        ];
927
928        // There were 2 children:
929        // 1. The compressed codes, stored as a VarBinArray.
930        // 2. The uncompressed lengths, stored as a Primitive array.
931        let children = vec![
932            compressed_codes.into_array(),
933            fsst_array.uncompressed_lengths().clone(),
934        ];
935
936        let fsst = ArrayPlugin::deserialize(
937            &FSST,
938            &DType::Utf8(Nullability::NonNullable),
939            2,
940            &FSSTMetadata {
941                uncompressed_lengths_ptype: fsst_array
942                    .uncompressed_lengths()
943                    .dtype()
944                    .as_ptype()
945                    .into(),
946                // Legacy array did not store this field, use Protobuf default of 0.
947                codes_offsets_ptype: 0,
948            }
949            .encode_to_vec(),
950            &buffers,
951            &children.as_slice(),
952            &array_session(),
953        )?;
954
955        let decompressed =
956            fsst.execute::<VarBinViewArray>(&mut array_session().create_execution_ctx())?;
957        let mask = decompressed
958            .validity()?
959            .execute_mask(decompressed.len(), &mut ctx)?;
960        assert!(mask.value(0));
961        assert_eq!(decompressed.bytes_at(0).as_slice(), b"abcabcab".as_ref());
962        assert!(mask.value(1));
963        assert_eq!(decompressed.bytes_at(1).as_slice(), b"defghijk".as_ref());
964        Ok(())
965    }
966}