Skip to content

Commit b6ba850

Browse files
authored
RLEExporter for duckdb (#8539)
Add RLE exporter for fastlanes.rle to Duckdb. Usable only when the underlying column is non-nullable, falls back to canonicalization otherwise. Resolves: #8247 Signed-off-by: Mikhail Kot <mikhail@spiraldb.com>
1 parent 682c7b8 commit b6ba850

5 files changed

Lines changed: 342 additions & 1 deletion

File tree

encodings/fastlanes/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ mod delta;
4444
mod r#for;
4545
mod rle;
4646

47-
pub(crate) const FL_CHUNK_SIZE: usize = 1024;
47+
pub const FL_CHUNK_SIZE: usize = 1024;
4848

4949
use bitpacking::compute::is_constant::BitPackedIsConstantKernel;
5050
use r#for::compute::is_constant::FoRIsConstantKernel;

vortex-duckdb/src/e2e_test/vortex_scan_test.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ use vortex::array::validity::Validity;
3434
use vortex::buffer::buffer;
3535
use vortex::dtype::Nullability;
3636
use vortex::dtype::PType;
37+
use vortex::encodings::fastlanes::RLEData;
3738
use vortex::file::WriteOptionsSessionExt;
3839
use vortex::io::runtime::BlockingRuntime;
3940
use vortex::scalar::PValue;
@@ -956,6 +957,20 @@ fn test_vortex_encodings_roundtrip() {
956957
not(duckdb_release),
957958
ignore = "spatial extension requires a release DuckDB build"
958959
)]
960+
#[test]
961+
fn test_fastlanes_rle_roundtrip() {
962+
let expected: Vec<i32> = (0i32..2048).map(|i| i / 256).collect();
963+
let file = RUNTIME.block_on(async {
964+
let mut ctx = SESSION.create_execution_ctx();
965+
let primitive = PrimitiveArray::from_iter(expected.clone());
966+
let rle = RLEData::encode(primitive.as_view(), &mut ctx).unwrap();
967+
write_single_column_vortex_file("rle_col", rle.into_array()).await
968+
});
969+
970+
let values: Vec<i32> = scan_vortex_file::<i32, _>(file, "SELECT rle_col FROM ?", 0).unwrap();
971+
assert_eq!(values, expected);
972+
}
973+
959974
#[test]
960975
fn test_geometry() {
961976
let file = RUNTIME.block_on(async {

vortex-duckdb/src/exporter/mod.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ mod geo;
1515
mod list;
1616
mod list_view;
1717
mod primitive;
18+
mod rle;
1819
mod run_end;
1920
mod sequence;
2021
mod struct_;
@@ -34,6 +35,7 @@ use vortex::array::arrays::List;
3435
use vortex::array::arrays::StructArray;
3536
use vortex::array::arrays::struct_::StructArrayExt;
3637
use vortex::buffer::BitChunks;
38+
use vortex::encodings::fastlanes::RLE;
3739
use vortex::encodings::runend::RunEnd;
3840
use vortex::encodings::sequence::Sequence;
3941
use vortex::error::VortexExpect;
@@ -244,6 +246,11 @@ fn new_array_exporter_with_flatten(
244246
Err(array) => array,
245247
};
246248

249+
let array = match array.try_downcast::<RLE>() {
250+
Ok(array) => return rle::new_exporter_with_flatten(array, cache, ctx, flatten),
251+
Err(array) => array,
252+
};
253+
247254
let array = match array.try_downcast::<Dict>() {
248255
Ok(array) => return dict::new_exporter_with_flatten(&array, cache, ctx, flatten),
249256
Err(array) => array,

vortex-duckdb/src/exporter/rle.rs

Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
use std::marker::PhantomData;
5+
6+
use num_traits::AsPrimitive;
7+
use vortex::array::ExecutionCtx;
8+
use vortex::array::IntoArray;
9+
use vortex::array::arrays::PrimitiveArray;
10+
use vortex::array::match_each_unsigned_integer_ptype;
11+
use vortex::array::validity::Validity;
12+
use vortex::dtype::IntegerPType;
13+
use vortex::encodings::fastlanes::FL_CHUNK_SIZE;
14+
use vortex::encodings::fastlanes::RLEArray;
15+
use vortex::encodings::fastlanes::RLEArrayExt;
16+
use vortex::error::VortexResult;
17+
18+
use crate::duckdb::ReusableDict;
19+
use crate::duckdb::SelectionVector;
20+
use crate::duckdb::VectorRef;
21+
use crate::exporter::ColumnExporter;
22+
use crate::exporter::all_invalid;
23+
use crate::exporter::cache::ConversionCache;
24+
use crate::exporter::cached_values_dict;
25+
use crate::exporter::canonical;
26+
27+
struct RLEExporter<I: IntegerPType, O: IntegerPType> {
28+
values: ReusableDict,
29+
indices: PrimitiveArray,
30+
values_idx_offsets: PrimitiveArray,
31+
/// Offset relative to the first chunk
32+
offset: usize,
33+
indices_type: PhantomData<I>,
34+
values_idx_offsets_type: PhantomData<O>,
35+
}
36+
37+
pub(crate) fn new_exporter_with_flatten(
38+
array: RLEArray,
39+
cache: &ConversionCache,
40+
ctx: &mut ExecutionCtx,
41+
flatten: bool,
42+
) -> VortexResult<Box<dyn ColumnExporter>> {
43+
if flatten || array.is_empty() {
44+
return canonical::new_exporter(array.into_array(), cache, ctx);
45+
}
46+
// DuckDB dictionary can't carry validity on codes.
47+
// Don't execute the validity mask, if there's a chance of NULL,
48+
// canonicalize
49+
match array.indices().validity()? {
50+
Validity::AllInvalid => return Ok(all_invalid::new_exporter()),
51+
Validity::Array(_) => return canonical::new_exporter(array.into_array(), cache, ctx),
52+
_ => {}
53+
}
54+
55+
let indices = array.indices().clone().execute::<PrimitiveArray>(ctx)?;
56+
let values = array.values().clone();
57+
let values_idx_offsets = array
58+
.values_idx_offsets()
59+
.clone()
60+
.execute::<PrimitiveArray>(ctx)?;
61+
62+
let values = cached_values_dict(values, cache, ctx)?;
63+
match_each_unsigned_integer_ptype!(indices.ptype(), |I| {
64+
match_each_unsigned_integer_ptype!(values_idx_offsets.ptype(), |O| {
65+
Ok(Box::new(RLEExporter {
66+
values,
67+
indices,
68+
values_idx_offsets,
69+
offset: array.offset(),
70+
indices_type: PhantomData::<I>,
71+
values_idx_offsets_type: PhantomData::<O>,
72+
}))
73+
})
74+
})
75+
}
76+
77+
impl<I, O> ColumnExporter for RLEExporter<I, O>
78+
where
79+
I: IntegerPType + AsPrimitive<u32>,
80+
O: IntegerPType + AsPrimitive<u32>,
81+
{
82+
fn export(
83+
&self,
84+
offset: usize,
85+
len: usize,
86+
vector: &mut VectorRef,
87+
_ctx: &mut ExecutionCtx,
88+
) -> VortexResult<()> {
89+
let mut selection_vec = SelectionVector::with_capacity(len);
90+
let mut selection = unsafe { selection_vec.as_slice_mut(len) };
91+
92+
let indices = self.indices.as_slice::<I>();
93+
let values_idx_offsets = self.values_idx_offsets.as_slice::<O>();
94+
95+
let mut pos = self.offset + offset;
96+
let end = pos + len;
97+
98+
let first_idx_offset = values_idx_offsets[0];
99+
while pos < end {
100+
let chunk_idx = pos / FL_CHUNK_SIZE;
101+
let base: u32 = (values_idx_offsets[chunk_idx] - first_idx_offset).as_();
102+
let take = ((chunk_idx + 1) * FL_CHUNK_SIZE).min(end) - pos;
103+
104+
for (dst, idx) in selection[..take].iter_mut().zip(&indices[pos..pos + take]) {
105+
let idx: u32 = idx.as_();
106+
*dst = base + idx;
107+
}
108+
109+
selection = &mut selection[take..];
110+
pos += take;
111+
}
112+
113+
vector.reuse_dictionary(&self.values, &selection_vec);
114+
Ok(())
115+
}
116+
}
117+
118+
#[cfg(test)]
119+
mod tests {
120+
use vortex::array::ArrayRef;
121+
use vortex::array::IntoArray;
122+
use vortex::array::VortexSessionExecute;
123+
use vortex::array::arrays::PrimitiveArray;
124+
use vortex::encodings::fastlanes::RLEArray;
125+
use vortex::encodings::fastlanes::RLEData;
126+
use vortex::error::VortexResult;
127+
128+
use crate::SESSION;
129+
use crate::cpp::duckdb_type::DUCKDB_TYPE_INTEGER;
130+
use crate::duckdb::DataChunk;
131+
use crate::duckdb::LogicalType;
132+
use crate::exporter::ConversionCache;
133+
use crate::exporter::new_array_exporter;
134+
135+
fn encode_rle(values: Vec<i32>) -> VortexResult<RLEArray> {
136+
let mut ctx = SESSION.create_execution_ctx();
137+
let primitive = PrimitiveArray::from_iter(values);
138+
RLEData::encode(primitive.as_view(), &mut ctx)
139+
}
140+
141+
fn export_flat(array: ArrayRef, len: usize) -> VortexResult<Vec<i32>> {
142+
let mut ctx = SESSION.create_execution_ctx();
143+
let mut chunk = DataChunk::new([LogicalType::new(DUCKDB_TYPE_INTEGER)]);
144+
new_array_exporter(array, &ConversionCache::default(), &mut ctx)?.export(
145+
0,
146+
len,
147+
chunk.get_vector_mut(0),
148+
&mut ctx,
149+
)?;
150+
chunk.set_len(len);
151+
let vector = chunk.get_vector(0);
152+
vector.flatten(len as u64);
153+
Ok(vector.as_slice_with_len::<i32>(len).to_vec())
154+
}
155+
156+
#[test]
157+
fn test_roundtrip_two_chunks() -> VortexResult<()> {
158+
let expected: Vec<i32> = (0i32..2048).map(|i| i / 100).collect();
159+
let rle = encode_rle(expected.clone())?;
160+
let exported = export_flat(rle.into_array(), 2048)?;
161+
assert_eq!(exported, expected);
162+
Ok(())
163+
}
164+
165+
#[test]
166+
fn test_roundtrip_boundary() -> VortexResult<()> {
167+
let source: Vec<i32> = (0i32..2048).map(|i| i / 100).collect();
168+
let rle = encode_rle(source.clone())?;
169+
let sliced = rle.into_array().slice(500..1700)?;
170+
let exported = export_flat(sliced, 1200)?;
171+
assert_eq!(exported, source[500..1700]);
172+
Ok(())
173+
}
174+
175+
#[test]
176+
fn test_roundtrip_slice() -> VortexResult<()> {
177+
let source: Vec<i32> = (0i32..3072).map(|i| i / 100).collect();
178+
let rle = encode_rle(source.clone())?;
179+
let sliced = rle.into_array().slice(1200..2000)?;
180+
let exported = export_flat(sliced, 800)?;
181+
assert_eq!(exported, source[1200..2000]);
182+
Ok(())
183+
}
184+
185+
fn chunk_string(array: ArrayRef, offset: usize, len: usize) -> VortexResult<String> {
186+
let mut ctx = SESSION.create_execution_ctx();
187+
let mut chunk = DataChunk::new([LogicalType::new(DUCKDB_TYPE_INTEGER)]);
188+
new_array_exporter(array, &ConversionCache::default(), &mut ctx)?.export(
189+
offset,
190+
len,
191+
chunk.get_vector_mut(0),
192+
&mut ctx,
193+
)?;
194+
chunk.set_len(len);
195+
String::try_from(&*chunk)
196+
}
197+
198+
fn two_chunk_rle() -> VortexResult<RLEArray> {
199+
let mut ctx = SESSION.create_execution_ctx();
200+
let source: Vec<i32> = std::iter::repeat_n(10i32, 1024)
201+
.chain(std::iter::repeat_n(20, 1024))
202+
.collect();
203+
RLEData::encode(PrimitiveArray::from_iter(source).as_view(), &mut ctx)
204+
}
205+
206+
#[test]
207+
fn test_one_chunk() -> VortexResult<()> {
208+
let rle = two_chunk_rle()?;
209+
let chunk_str = chunk_string(rle.into_array(), 0, 5)?;
210+
assert_eq!(
211+
chunk_str,
212+
r#"Chunk - [1 Columns]
213+
- DICTIONARY INTEGER: 5 = [ 10, 10, 10, 10, 10]
214+
"#
215+
);
216+
Ok(())
217+
}
218+
219+
#[test]
220+
fn test_one_chunk_nulls() -> VortexResult<()> {
221+
let mut ctx = SESSION.create_execution_ctx();
222+
let source = vec![Some(0u32), Some(1), None, Some(3), None];
223+
let rle = RLEData::encode(PrimitiveArray::from_option_iter(source).as_view(), &mut ctx)?;
224+
let chunk_str = chunk_string(rle.into_array(), 0, 5)?;
225+
assert_eq!(
226+
chunk_str,
227+
r#"Chunk - [1 Columns]
228+
- FLAT INTEGER: 5 = [ 0, 1, NULL, 3, NULL]
229+
"#
230+
);
231+
Ok(())
232+
}
233+
234+
#[test]
235+
fn test_chunk_boundary() -> VortexResult<()> {
236+
let rle = two_chunk_rle()?;
237+
let chunk_str = chunk_string(rle.into_array(), 1020, 10)?;
238+
assert_eq!(
239+
chunk_str,
240+
r#"Chunk - [1 Columns]
241+
- DICTIONARY INTEGER: 10 = [ 10, 10, 10, 10, 20, 20, 20, 20, 20, 20]
242+
"#
243+
);
244+
Ok(())
245+
}
246+
247+
#[test]
248+
fn test_chunk_slice() -> VortexResult<()> {
249+
let rle = two_chunk_rle()?;
250+
let sliced = rle.into_array().slice(1500..1510)?;
251+
let chunk_str = chunk_string(sliced, 0, 10)?;
252+
assert_eq!(
253+
chunk_str,
254+
r#"Chunk - [1 Columns]
255+
- FLAT INTEGER: 10 = [ 20, 20, 20, 20, 20, 20, 20, 20, 20, 20]
256+
"#
257+
);
258+
Ok(())
259+
}
260+
261+
#[test]
262+
fn test_roundtrip_with_nulls() -> VortexResult<()> {
263+
let source: Vec<Option<i32>> = (0i32..1024)
264+
.map(|i| if i % 7 == 0 { None } else { Some(i / 50) })
265+
.collect();
266+
let mut ctx = SESSION.create_execution_ctx();
267+
let primitive = PrimitiveArray::from_option_iter(source.clone());
268+
let rle = RLEData::encode(primitive.as_view(), &mut ctx)?;
269+
270+
let mut chunk = DataChunk::new([LogicalType::new(DUCKDB_TYPE_INTEGER)]);
271+
new_array_exporter(rle.into_array(), &ConversionCache::default(), &mut ctx)?.export(
272+
0,
273+
1024,
274+
chunk.get_vector_mut(0),
275+
&mut ctx,
276+
)?;
277+
chunk.set_len(1024);
278+
279+
let vector = chunk.get_vector(0);
280+
vector.flatten(1024);
281+
let slice = vector.as_slice_with_len::<i32>(1024);
282+
for (i, expected) in source.iter().enumerate() {
283+
if let Some(v) = expected {
284+
assert!(!vector.row_is_null(i as u64), "row {i} is null");
285+
assert_eq!(slice[i], *v);
286+
} else {
287+
assert!(vector.row_is_null(i as u64), "row {i} not null");
288+
}
289+
}
290+
Ok(())
291+
}
292+
}

vortex-duckdb/src/exporter/run_end.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,10 +135,37 @@ mod tests {
135135
use vortex::error::VortexResult;
136136

137137
use crate::SESSION;
138+
use crate::cpp::duckdb_type::DUCKDB_TYPE_INTEGER;
138139
use crate::duckdb::DataChunk;
139140
use crate::duckdb::LogicalType;
140141
use crate::exporter::ArrayExporter;
141142
use crate::exporter::ConversionCache;
143+
use crate::exporter::new_array_exporter;
144+
145+
#[test]
146+
fn test_one_chunk_null() -> VortexResult<()> {
147+
let mut ctx = SESSION.create_execution_ctx();
148+
let source = vec![Some(0u32), Some(1), None, Some(3), None];
149+
let array = PrimitiveArray::from_option_iter(source);
150+
let array = RunEnd::encode(array.into_array(), &mut ctx)?;
151+
152+
let mut chunk = DataChunk::new([LogicalType::new(DUCKDB_TYPE_INTEGER)]);
153+
new_array_exporter(array.into_array(), &ConversionCache::default(), &mut ctx)?.export(
154+
0,
155+
5,
156+
chunk.get_vector_mut(0),
157+
&mut ctx,
158+
)?;
159+
chunk.set_len(5);
160+
let chunk_str = String::try_from(&*chunk)?;
161+
assert_eq!(
162+
chunk_str,
163+
r#"Chunk - [1 Columns]
164+
- DICTIONARY INTEGER: 5 = [ 0, 1, NULL, 3, NULL]
165+
"#
166+
);
167+
Ok(())
168+
}
142169

143170
#[test]
144171
fn run_end_with_chunked_values_exports_across_value_chunks() -> VortexResult<()> {

0 commit comments

Comments
 (0)