Skip to content

Commit 5515820

Browse files
committed
improve projection eval
1 parent 63222ce commit 5515820

2 files changed

Lines changed: 218 additions & 37 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vortex-layout/src/layouts/list/reader.rs

Lines changed: 217 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,22 @@ use std::sync::Arc;
77

88
use futures::FutureExt;
99
use futures::try_join;
10-
use vortex_array::Array;
1110
use vortex_array::ArrayRef;
1211
use vortex_array::IntoArray;
1312
use vortex_array::MaskFuture;
14-
use vortex_array::arrays::List;
13+
use vortex_array::VortexSessionExecute;
14+
use vortex_array::arrays::ConstantArray;
15+
use vortex_array::arrays::ListArray;
16+
use vortex_array::builtins::ArrayBuiltins;
1517
use vortex_array::dtype::DType;
1618
use vortex_array::dtype::FieldMask;
1719
use vortex_array::dtype::Nullability;
1820
use vortex_array::expr::Expression;
21+
use vortex_array::expr::is_root;
1922
use vortex_array::expr::root;
23+
use vortex_array::scalar_fn::fns::operators::Operator;
2024
use vortex_array::validity::Validity;
25+
use vortex_error::VortexExpect;
2126
use vortex_error::VortexResult;
2227
use vortex_mask::Mask;
2328
use vortex_session::VortexSession;
@@ -30,14 +35,10 @@ use crate::layouts::list::ListLayout;
3035
use crate::segments::SegmentSource;
3136

3237
/// Reader for [`ListLayout`].
33-
///
34-
/// Reads the underlying `elements`, `offsets`, and (optional) `validity` children in parallel
35-
/// and reassembles them into a [`ListArray`](vortex_array::arrays::ListArray) before applying
36-
/// the requested projection.
3738
pub struct ListReader {
3839
layout: ListLayout,
3940
name: Arc<str>,
40-
_session: VortexSession,
41+
session: VortexSession,
4142
elements: LayoutReaderRef,
4243
offsets: LayoutReaderRef,
4344
validity: Option<LayoutReaderRef>,
@@ -74,12 +75,54 @@ impl ListReader {
7475
Ok(Self {
7576
layout,
7677
name,
77-
_session: session,
78+
session,
7879
elements,
7980
offsets,
8081
validity,
8182
})
8283
}
84+
85+
fn fetch_offsets(&self, row_range: &Range<u64>) -> VortexResult<ArrayFuture> {
86+
// The offsets child has an extra entry, so reading `row_range` maps to offsets in
87+
// `[row_range.start..row_range.end + 1)`.
88+
let offsets_range = row_range.start..(row_range.end + 1);
89+
let offsets_count = usize::try_from(offsets_range.end - offsets_range.start)?;
90+
91+
self.offsets.projection_evaluation(
92+
&offsets_range,
93+
&root(),
94+
MaskFuture::new_true(offsets_count),
95+
)
96+
}
97+
}
98+
99+
/// Read `offsets[0]` and `offsets[-1]` and return the elements-buffer range they describe.
100+
fn calculate_elements_range(
101+
offsets: &ArrayRef,
102+
session: &VortexSession,
103+
) -> VortexResult<Range<u64>> {
104+
if offsets.is_empty() {
105+
return Ok(0..0);
106+
}
107+
let mut exec_ctx = session.create_execution_ctx();
108+
let start = offsets
109+
.execute_scalar(0, &mut exec_ctx)?
110+
.as_primitive()
111+
.as_::<u64>()
112+
.vortex_expect("offset value fits in u64");
113+
let end = offsets
114+
.execute_scalar(offsets.len() - 1, &mut exec_ctx)?
115+
.as_primitive()
116+
.as_::<u64>()
117+
.vortex_expect("offset value fits in u64");
118+
Ok(start..end)
119+
}
120+
121+
/// Subtract `first` from every offset so the resulting offsets index into a sliced
122+
/// `elements[first..]` buffer starting at zero.
123+
fn rebase_offsets(offsets: ArrayRef, first: u64) -> VortexResult<ArrayRef> {
124+
let constant = ConstantArray::new(first, offsets.len()).into_array();
125+
offsets.binary(constant, Operator::Sub)
83126
}
84127

85128
impl LayoutReader for ListReader {
@@ -105,10 +148,12 @@ impl LayoutReader for ListReader {
105148
split_range: &SplitRange,
106149
splits: &mut BTreeSet<u64>,
107150
) -> VortexResult<()> {
108-
// Offsets has one more row than the list itself but shares the list's chunking
109-
// structure, so it's the appropriate child to drive scan splits.
110151
self.offsets
111-
.register_splits(field_mask, split_range, splits)
152+
.register_splits(field_mask, split_range, splits)?;
153+
if let Some(validity) = &self.validity {
154+
validity.register_splits(field_mask, split_range, splits)?;
155+
}
156+
Ok(())
112157
}
113158

114159
fn pruning_evaluation(
@@ -135,47 +180,55 @@ impl LayoutReader for ListReader {
135180
expr: &Expression,
136181
mask: MaskFuture,
137182
) -> VortexResult<ArrayFuture> {
138-
let elements_len = self.layout.elements().row_count();
139-
let elements_range = 0..elements_len;
140-
let elements_mask = MaskFuture::new_true(usize::try_from(elements_len)?);
141-
142-
let row_count: usize = usize::try_from(row_range.end - row_range.start)?;
143-
// The offsets child has n+1 entries; reading row_range maps to offsets in
144-
// [row_range.start..row_range.end + 1).
145-
let offsets_range = row_range.start..row_range.end + 1;
146-
let offsets_count = usize::try_from(offsets_range.end - offsets_range.start)?;
147-
148-
let elements_fut =
149-
self.elements
150-
.projection_evaluation(&elements_range, &root(), elements_mask)?;
151-
let offsets_fut = self.offsets.projection_evaluation(
152-
&offsets_range,
153-
&root(),
154-
MaskFuture::new_true(offsets_count),
155-
)?;
183+
let offsets_fut = self.fetch_offsets(row_range)?;
184+
// Validity shares the list's row space, so the caller's mask is the right shape to
185+
// push down. Children get to fold it into their reads however they like.
156186
let validity_fut = self
157187
.validity
158188
.as_ref()
159-
.map(|v| v.projection_evaluation(row_range, &root(), MaskFuture::new_true(row_count)))
189+
.map(|v| v.projection_evaluation(row_range, &root(), mask.clone()))
160190
.transpose()?;
161191

192+
let elements_reader = self.elements.clone();
193+
let session = self.session.clone();
162194
let nullability = self.layout.dtype().nullability();
163195
let expr = expr.clone();
164196

165197
Ok(async move {
166-
let (elements, offsets) = try_join!(elements_fut, offsets_fut)?;
167-
let validity = match validity_fut {
168-
Some(fut) => Validity::Array(fut.await?),
198+
// Fetch offsets and validity in parallel. Elements waits until we know
199+
// exactly which slice of the elements buffer it actually needs.
200+
let (offsets, validity_array) = try_join!(offsets_fut, async move {
201+
match validity_fut {
202+
Some(fut) => fut.await.map(Some),
203+
None => Ok(None),
204+
}
205+
},)?;
206+
207+
// Bound the elements read using offsets[0] and offsets[-1]
208+
let elements_range = calculate_elements_range(&offsets, &session)?;
209+
210+
// Rebase the offsets so they start at zero
211+
let rebased_offsets = rebase_offsets(offsets, elements_range.start)?;
212+
213+
// Fetch only the elements we actually need.
214+
let elements_len = elements_range.end - elements_range.start;
215+
let elements = elements_reader
216+
.projection_evaluation(
217+
&elements_range,
218+
&root(),
219+
MaskFuture::new_true(usize::try_from(elements_len)?),
220+
)?
221+
.await?;
222+
223+
let validity = match validity_array {
224+
Some(arr) => Validity::Array(arr),
169225
None => match nullability {
170226
Nullability::Nullable => Validity::AllValid,
171227
Nullability::NonNullable => Validity::NonNullable,
172228
},
173229
};
174230

175-
// SAFETY: the layout was validated at write time, so offsets are monotonic,
176-
// non-nullable, integer, of length n+1.
177-
let array: ArrayRef =
178-
unsafe { Array::<List>::new_unchecked(elements, offsets, validity) }.into_array();
231+
let array = ListArray::try_new(elements, rebased_offsets, validity)?.into_array();
179232

180233
let mask = mask.await?;
181234
let array = if !mask.all_true() {
@@ -189,3 +242,130 @@ impl LayoutReader for ListReader {
189242
.boxed())
190243
}
191244
}
245+
246+
#[cfg(test)]
247+
mod tests {
248+
use std::collections::BTreeSet;
249+
use std::ops::Range;
250+
251+
use vortex_array::ArrayContext;
252+
use vortex_array::arrays::BoolArray;
253+
use vortex_array::arrays::ChunkedArray;
254+
use vortex_array::arrays::ListArray;
255+
use vortex_array::dtype::FieldMask;
256+
use vortex_buffer::buffer;
257+
258+
use super::*;
259+
use crate::LayoutRef;
260+
use crate::LayoutStrategy;
261+
use crate::layouts::chunked::writer::ChunkedLayoutStrategy;
262+
use crate::layouts::flat::writer::FlatLayoutStrategy;
263+
use crate::layouts::list::writer::ListLayoutStrategy;
264+
use crate::scan::split_by::SplitBy;
265+
use crate::segments::SegmentSource;
266+
use crate::segments::TestSegments;
267+
use crate::sequence::SequenceId;
268+
use crate::sequence::SequentialArrayStreamExt;
269+
use crate::test::SESSION;
270+
271+
fn flat_list_strategy() -> ListLayoutStrategy {
272+
let flat: Arc<dyn LayoutStrategy> = Arc::new(FlatLayoutStrategy::default());
273+
ListLayoutStrategy::new(Arc::clone(&flat), Arc::clone(&flat), Arc::clone(&flat))
274+
}
275+
276+
async fn write_layout<S: LayoutStrategy>(
277+
strategy: &S,
278+
array: ArrayRef,
279+
) -> VortexResult<(Arc<dyn SegmentSource>, LayoutRef)> {
280+
let segments = Arc::new(TestSegments::default());
281+
let segments_ref: Arc<dyn SegmentSource> = Arc::<TestSegments>::clone(&segments);
282+
let (ptr, eof) = SequenceId::root().split();
283+
let stream = array.to_array_stream().sequenced(ptr);
284+
let layout = strategy
285+
.write_stream(ArrayContext::empty(), segments, stream, eof, &SESSION)
286+
.await?;
287+
Ok((segments_ref, layout))
288+
}
289+
290+
fn collect_splits(reader: &dyn LayoutReader, row_range: Range<u64>) -> BTreeSet<u64> {
291+
SplitBy::Layout
292+
.splits(reader, &row_range, &[FieldMask::All])
293+
.unwrap()
294+
}
295+
296+
#[tokio::test]
297+
async fn splits_non_nullable_flat() -> VortexResult<()> {
298+
// 5 lists in one flat ListLayout. Offsets is flat (single segment), no validity.
299+
// Only the row_range endpoints should appear in the split set.
300+
let list = ListArray::try_new(
301+
buffer![1i32, 2, 3, 4, 5, 6, 7].into_array(),
302+
buffer![0u32, 2, 3, 3, 6, 7].into_array(),
303+
Validity::NonNullable,
304+
)?
305+
.into_array();
306+
307+
let (segments, layout) = write_layout(&flat_list_strategy(), list).await?;
308+
let reader = layout.new_reader("".into(), segments, &SESSION)?;
309+
310+
assert_eq!(
311+
collect_splits(reader.as_ref(), 0..5),
312+
BTreeSet::from([0, 5])
313+
);
314+
assert_eq!(
315+
collect_splits(reader.as_ref(), 1..4),
316+
BTreeSet::from([1, 4])
317+
);
318+
Ok(())
319+
}
320+
321+
#[tokio::test]
322+
async fn splits_nullable_flat_dedups_validity_and_offsets() -> VortexResult<()> {
323+
// Nullable list with flat validity. Offsets and validity both end at list-row 3;
324+
// BTreeSet deduplicates, so the split set still has only the endpoints.
325+
let list = ListArray::try_new(
326+
buffer![10i32, 20, 30, 40, 50].into_array(),
327+
buffer![0u32, 2, 3, 5].into_array(),
328+
Validity::Array(BoolArray::from_iter([true, false, true]).into_array()),
329+
)?
330+
.into_array();
331+
332+
let (segments, layout) = write_layout(&flat_list_strategy(), list).await?;
333+
let reader = layout.new_reader("".into(), segments, &SESSION)?;
334+
335+
assert_eq!(
336+
collect_splits(reader.as_ref(), 0..3),
337+
BTreeSet::from([0, 3])
338+
);
339+
Ok(())
340+
}
341+
342+
#[tokio::test]
343+
async fn splits_chunked_wrap_exposes_chunk_boundaries() -> VortexResult<()> {
344+
// Two list chunks under a ChunkedLayoutStrategy. The chunk boundary at list-row 2
345+
// should appear as a split.
346+
let chunk0 = ListArray::try_new(
347+
buffer![1i32, 2, 3].into_array(),
348+
buffer![0u32, 2, 3].into_array(),
349+
Validity::NonNullable,
350+
)?
351+
.into_array();
352+
let chunk1 = ListArray::try_new(
353+
buffer![4i32, 5, 6, 7].into_array(),
354+
buffer![0u32, 1, 4].into_array(),
355+
Validity::NonNullable,
356+
)?
357+
.into_array();
358+
let dtype = chunk0.dtype().clone();
359+
let chunked = ChunkedArray::try_new(vec![chunk0, chunk1], dtype)?.into_array();
360+
361+
let strategy = ChunkedLayoutStrategy::new(flat_list_strategy());
362+
let (segments, layout) = write_layout(&strategy, chunked).await?;
363+
let reader = layout.new_reader("".into(), segments, &SESSION)?;
364+
365+
assert_eq!(
366+
collect_splits(reader.as_ref(), 0..4),
367+
BTreeSet::from([0, 2, 4])
368+
);
369+
Ok(())
370+
}
371+
}

0 commit comments

Comments
 (0)