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