Skip to content

Commit a377a9b

Browse files
committed
feat(arrow): export strings and binary arrays with offsets
Problem Vortex exports UTF-8 and binary data as Arrow view arrays by default. Some consumers require Arrow arrays that use 32-bit or 64-bit offsets. These consumers must cast each view array and rebuild its buffers. Implementation Add VarBinBufferBuilder for 32-bit and 64-bit offsets. Write Arrow value, offset, and validity buffers in this builder. Decode FSST, OnPair, Zstd, Dict, RunEnd, and Sparse arrays directly into this builder. Do not create an intermediate VarBinViewArray for these paths. Interface Add an optional Arrow type to the Rust and Python array APIs. Add an optional Arrow schema to the Python file API. Keep Arrow view arrays as the default output. Result Arrow uses the builder buffers without another copy. Callers can select StringArray, BinaryArray, LargeStringArray, or LargeBinaryArray. Signed-off-by: cl <cailue@apache.org>
1 parent 037b1a8 commit a377a9b

23 files changed

Lines changed: 967 additions & 94 deletions

File tree

‎encodings/fsst/src/array.rs‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use std::sync::OnceLock;
1111
use fsst::Compressor;
1212
use fsst::Decompressor;
1313
use fsst::Symbol;
14+
use num_traits::AsPrimitive;
1415
use prost::Message as _;
1516
use vortex_array::Array;
1617
use vortex_array::ArrayEq;
@@ -32,11 +33,13 @@ use vortex_array::arrays::VarBinArray;
3233
use vortex_array::arrays::varbin::VarBinArrayExt;
3334
use vortex_array::buffer::BufferHandle;
3435
use vortex_array::builders::ArrayBuilder;
36+
use vortex_array::builders::VarBinBufferBuilder;
3537
use vortex_array::builders::VarBinViewBuilder;
3638
use vortex_array::dtype::DType;
3739
use vortex_array::dtype::Nullability;
3840
use vortex_array::dtype::PType;
3941
use vortex_array::legacy_session;
42+
use vortex_array::match_each_integer_ptype;
4043
use vortex_array::serde::ArrayChildren;
4144
use vortex_array::smallvec::smallvec;
4245
use vortex_array::validity::Validity;
@@ -56,6 +59,7 @@ use vortex_session::VortexSession;
5659
use vortex_session::registry::CachedId;
5760

5861
use crate::canonical::canonicalize_fsst;
62+
use crate::canonical::fsst_decode_bytes;
5963
use crate::canonical::fsst_decode_views;
6064
use crate::rules::RULES;
6165

@@ -304,6 +308,25 @@ impl VTable for FSST {
304308
builder: &mut dyn ArrayBuilder,
305309
ctx: &mut ExecutionCtx,
306310
) -> VortexResult<()> {
311+
if let Some(builder) = builder.as_any_mut().downcast_mut::<VarBinBufferBuilder>() {
312+
let (bytes, lengths) = fsst_decode_bytes(array, ctx)?;
313+
let validity = array
314+
.array()
315+
.validity()?
316+
.execute_mask(array.array().len(), ctx)?;
317+
match_each_integer_ptype!(lengths.ptype(), |P| {
318+
builder.append_values(
319+
bytes.as_slice(),
320+
lengths
321+
.as_slice::<P>()
322+
.iter()
323+
.map(|length| AsPrimitive::<usize>::as_(*length)),
324+
&validity,
325+
);
326+
});
327+
return Ok(());
328+
}
329+
307330
let Some(builder) = builder.as_any_mut().downcast_mut::<VarBinViewBuilder>() else {
308331
return array
309332
.array()

‎encodings/fsst/src/canonical.rs‎

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use vortex_buffer::Buffer;
1818
use vortex_buffer::ByteBuffer;
1919
use vortex_buffer::ByteBufferMut;
2020
use vortex_error::VortexResult;
21+
use vortex_error::vortex_ensure;
2122

2223
use crate::FSST;
2324
use crate::FSSTArrayExt;
@@ -40,20 +41,11 @@ pub(super) fn canonicalize_fsst(
4041
})
4142
}
4243

43-
pub(crate) fn fsst_decode_views(
44+
pub(crate) fn fsst_decode_bytes(
4445
fsst_array: ArrayView<'_, FSST>,
45-
start_buf_index: u32,
4646
ctx: &mut ExecutionCtx,
47-
) -> VortexResult<(Vec<ByteBuffer>, Buffer<BinaryView>)> {
48-
// FSSTArray has two child arrays:
49-
// 1. A VarBinArray, which holds the string heap of the compressed codes.
50-
// 2. An uncompressed_lengths primitive array, storing the length of each original
51-
// string element.
52-
// To speed up canonicalization, we can decompress the entire string-heap in a single
53-
// call. We then turn our uncompressed_lengths into an offsets buffer
54-
// necessary for a VarBinViewArray and construct the canonical array.
47+
) -> VortexResult<(ByteBufferMut, PrimitiveArray)> {
5548
let bytes = fsst_array.codes().sliced_bytes();
56-
5749
let uncompressed_lens_array = fsst_array
5850
.uncompressed_lengths()
5951
.clone()
@@ -68,14 +60,24 @@ pub(crate) fn fsst_decode_views(
6860
.sum()
6961
});
7062

71-
// Bulk-decompress the entire array.
7263
let decompressor = fsst_array.decompressor();
7364
let mut uncompressed_bytes = ByteBufferMut::with_capacity(total_size + 7);
7465
let len =
7566
decompressor.decompress_into(bytes.as_slice(), uncompressed_bytes.spare_capacity_mut());
67+
vortex_ensure!(
68+
len == total_size,
69+
"FSST decoded {len} bytes, expected {total_size}"
70+
);
7671
unsafe { uncompressed_bytes.set_len(len) };
72+
Ok((uncompressed_bytes, uncompressed_lens_array))
73+
}
7774

78-
// Directly create the binary views.
75+
pub(crate) fn fsst_decode_views(
76+
fsst_array: ArrayView<'_, FSST>,
77+
start_buf_index: u32,
78+
ctx: &mut ExecutionCtx,
79+
) -> VortexResult<(Vec<ByteBuffer>, Buffer<BinaryView>)> {
80+
let (uncompressed_bytes, uncompressed_lens_array) = fsst_decode_bytes(fsst_array, ctx)?;
7981
match_each_integer_ptype!(uncompressed_lens_array.ptype(), |P| {
8082
Ok(build_views(
8183
start_buf_index,
@@ -97,15 +99,21 @@ mod tests {
9799
use vortex_array::IntoArray;
98100
use vortex_array::VortexSessionExecute;
99101
use vortex_array::arrays::ChunkedArray;
102+
use vortex_array::arrays::PrimitiveArray;
100103
use vortex_array::arrays::VarBinArray;
101104
use vortex_array::arrays::VarBinViewArray;
105+
use vortex_array::arrays::varbin::VarBinArrayExt;
102106
use vortex_array::builders::ArrayBuilder;
107+
use vortex_array::builders::VarBinBufferBuilder;
103108
use vortex_array::builders::VarBinViewBuilder;
104109
use vortex_array::dtype::DType;
105110
use vortex_array::dtype::Nullability;
106111
use vortex_error::VortexResult;
107112
use vortex_session::VortexSession;
108113

114+
use super::fsst_decode_bytes;
115+
use crate::FSST;
116+
use crate::FSSTArrayExt;
109117
use crate::fsst_compress;
110118
use crate::fsst_train_compressor;
111119

@@ -199,6 +207,20 @@ mod tests {
199207
.collect::<Vec<_>>();
200208
assert_eq!(data, res2)
201209
};
210+
211+
{
212+
let mut builder =
213+
VarBinBufferBuilder::with_capacity(chunked_arr.dtype().clone(), false, data.len());
214+
chunked_arr
215+
.into_array()
216+
.append_to_builder(&mut builder, &mut ctx)?;
217+
let arr = builder.finish_into_varbin();
218+
let mask = arr.validity()?.execute_mask(arr.len(), &mut ctx)?;
219+
let actual = (0..arr.len())
220+
.map(|i| mask.value(i).then(|| arr.bytes_at(i).to_vec()))
221+
.collect::<Vec<_>>();
222+
assert_eq!(data, actual);
223+
}
202224
Ok(())
203225
}
204226

@@ -225,4 +247,22 @@ mod tests {
225247
let _result = builder.finish_into_varbinview();
226248
Ok(())
227249
}
250+
251+
#[test]
252+
fn rejects_incorrect_uncompressed_lengths() -> VortexResult<()> {
253+
let input = VarBinViewArray::from_iter_str(["hello"]).into_array();
254+
let mut ctx = SESSION.create_execution_ctx();
255+
let encoded = fsst_compress(&input, &fsst_train_compressor(&input, &mut ctx)?, &mut ctx)?;
256+
let invalid = FSST::try_new(
257+
encoded.dtype().clone(),
258+
encoded.symbols().clone(),
259+
encoded.symbol_lengths().clone(),
260+
encoded.codes(),
261+
PrimitiveArray::from_iter([4i32]).into_array(),
262+
&mut ctx,
263+
)?;
264+
265+
assert!(fsst_decode_bytes(invalid.as_view(), &mut ctx).is_err());
266+
Ok(())
267+
}
228268
}

‎encodings/onpair/src/array.rs‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use std::hash::Hasher;
88
use std::sync::Arc;
99
use std::sync::OnceLock;
1010

11+
use num_traits::AsPrimitive;
1112
use onpair::CompactDictionaryView;
1213
use prost::Message as _;
1314
use vortex_array::Array;
@@ -25,10 +26,12 @@ use vortex_array::IntoArray;
2526
use vortex_array::array_slots;
2627
use vortex_array::buffer::BufferHandle;
2728
use vortex_array::builders::ArrayBuilder;
29+
use vortex_array::builders::VarBinBufferBuilder;
2830
use vortex_array::builders::VarBinViewBuilder;
2931
use vortex_array::dtype::DType;
3032
use vortex_array::dtype::Nullability;
3133
use vortex_array::dtype::PType;
34+
use vortex_array::match_each_integer_ptype;
3235
use vortex_array::serde::ArrayChildren;
3336
use vortex_array::validity::Validity;
3437
use vortex_array::vtable::VTable;
@@ -46,6 +49,7 @@ use vortex_session::VortexSession;
4649
use vortex_session::registry::CachedId;
4750

4851
use crate::canonical::canonicalize_onpair;
52+
use crate::canonical::onpair_decode_bytes;
4953
use crate::canonical::onpair_decode_views;
5054
use crate::decode::collect_widened;
5155
use crate::rules::RULES;
@@ -544,6 +548,25 @@ impl VTable for OnPair {
544548
builder: &mut dyn ArrayBuilder,
545549
ctx: &mut ExecutionCtx,
546550
) -> VortexResult<()> {
551+
if let Some(builder) = builder.as_any_mut().downcast_mut::<VarBinBufferBuilder>() {
552+
let (bytes, lengths) = onpair_decode_bytes(array, ctx)?;
553+
let validity = array
554+
.array()
555+
.validity()?
556+
.execute_mask(array.array().len(), ctx)?;
557+
match_each_integer_ptype!(lengths.ptype(), |P| {
558+
builder.append_values(
559+
bytes.as_slice(),
560+
lengths
561+
.as_slice::<P>()
562+
.iter()
563+
.map(|length| AsPrimitive::<usize>::as_(*length)),
564+
&validity,
565+
);
566+
});
567+
return Ok(());
568+
}
569+
547570
let Some(builder) = builder.as_any_mut().downcast_mut::<VarBinViewBuilder>() else {
548571
return array
549572
.array()

‎encodings/onpair/src/canonical.rs‎

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,10 @@ pub(super) fn canonicalize_onpair(
4444
})
4545
}
4646

47-
pub(crate) fn onpair_decode_views(
47+
pub(crate) fn onpair_decode_bytes(
4848
array: ArrayView<'_, OnPair>,
49-
start_buf_index: u32,
5049
ctx: &mut ExecutionCtx,
51-
) -> VortexResult<(Vec<ByteBuffer>, Buffer<BinaryView>)> {
50+
) -> VortexResult<(ByteBufferMut, PrimitiveArray)> {
5251
let lengths = array
5352
.uncompressed_lengths()
5453
.clone()
@@ -62,15 +61,6 @@ pub(crate) fn onpair_decode_views(
6261
.sum()
6362
});
6463

65-
// `codes_offsets` holds the per-row code boundaries and may itself be a
66-
// sliced or filtered view of the original. Its first and last entries
67-
// bound the contiguous run of `codes` belonging to the rows present in
68-
// this array: `slice` keeps the full `codes` child and only narrows
69-
// `codes_offsets` (so `code_start > 0` and/or `code_end < codes.len()`),
70-
// while `filter` rebuilds both children so the window is the whole stream.
71-
// OnPair has no `TakeExecute`, so a reordering take is served from the
72-
// canonical `VarBinView` and never reaches this path. We only need those
73-
// two boundaries, so point-look them up rather than decoding every offset.
7464
let codes_offsets = array.codes_offsets();
7565
let code_start = code_boundary_at(codes_offsets, 0, ctx)?;
7666
let code_end = code_boundary_at(codes_offsets, array.len(), ctx)?;
@@ -85,13 +75,8 @@ pub(crate) fn onpair_decode_views(
8575
array.codes().len()
8676
);
8777

88-
// Slice the `codes` child to that window *before* unpacking it, so a sliced
89-
// array materialises only its own codes rather than the whole column's. The
90-
// contiguous decoder walks `codes` in order and never reads the per-row
91-
// boundaries, so an empty boundary slice is sound.
9278
let codes = collect_widened::<u16>(&array.codes().slice(code_start..code_end)?, ctx)?;
9379
let dict = dict_view(array, ctx)?;
94-
9580
let mut out_bytes = ByteBufferMut::with_capacity(total_size);
9681
let written =
9782
match onpair::try_decode_into(codes.as_slice(), dict, out_bytes.spare_capacity_mut()) {
@@ -105,9 +90,16 @@ pub(crate) fn onpair_decode_views(
10590
"OnPair codes decoded to {written} bytes but uncompressed_lengths records {total_size}"
10691
);
10792
}
108-
// SAFETY: `try_decode_into` initialised exactly `written` bytes.
10993
unsafe { out_bytes.set_len(written) };
94+
Ok((out_bytes, lengths))
95+
}
11096

97+
pub(crate) fn onpair_decode_views(
98+
array: ArrayView<'_, OnPair>,
99+
start_buf_index: u32,
100+
ctx: &mut ExecutionCtx,
101+
) -> VortexResult<(Vec<ByteBuffer>, Buffer<BinaryView>)> {
102+
let (out_bytes, lengths) = onpair_decode_bytes(array, ctx)?;
111103
match_each_integer_ptype!(lengths.ptype(), |P| {
112104
Ok(build_views(
113105
start_buf_index,

‎encodings/onpair/src/tests.rs‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use vortex_array::arrays::VarBinViewArray;
1616
use vortex_array::arrays::filter::FilterKernel;
1717
use vortex_array::assert_arrays_eq;
1818
use vortex_array::buffer::BufferHandle;
19+
use vortex_array::builders::VarBinBufferBuilder;
1920
use vortex_array::builtins::ArrayBuiltins;
2021
use vortex_array::dtype::DType;
2122
use vortex_array::dtype::Nullability;
@@ -60,6 +61,19 @@ fn sample_input() -> VarBinArray {
6061
)
6162
}
6263

64+
#[test]
65+
fn direct_offset_builder() -> vortex_error::VortexResult<()> {
66+
let mut ctx = SESSION.create_execution_ctx();
67+
let input = sample_input();
68+
let encoded = compress_onpair(input.as_ref(), &mut ctx)?;
69+
let mut builder = VarBinBufferBuilder::with_capacity(input.dtype().clone(), false, input.len());
70+
encoded
71+
.into_array()
72+
.append_to_builder(&mut builder, &mut ctx)?;
73+
assert_arrays_eq!(builder.finish_into_varbin(), input, &mut ctx);
74+
Ok(())
75+
}
76+
6377
#[test]
6478
fn test_onpair_rejects_100k_token_dictionary() -> vortex_error::VortexResult<()> {
6579
let num_tokens = 100_000usize;

0 commit comments

Comments
 (0)