Skip to content

Commit b952147

Browse files
committed
natural layouts
Signed-off-by: Adam Gutglick <adam@spiraldb.com>
1 parent bd3fbab commit b952147

5 files changed

Lines changed: 167 additions & 68 deletions

File tree

‎vortex-datafusion/src/persistent/opener.rs‎

Lines changed: 109 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ use futures::FutureExt;
3737
use futures::StreamExt;
3838
use futures::TryStreamExt;
3939
use futures::stream;
40-
use itertools::Itertools;
4140
use object_store::path::Path;
4241
use tracing::Instrument;
4342
use vortex::array::VortexSessionExecute;
@@ -100,8 +99,8 @@ pub(crate) struct VortexOpener {
10099
/// To save on the overhead of reparsing FlatBuffers and rebuilding the layout tree, we cache
101100
/// a file reader the first time we read a file.
102101
pub layout_readers: Arc<DashMap<Path, Weak<dyn LayoutReader>>>,
103-
/// Shared full-file natural split ranges keyed by file path.
104-
pub natural_split_ranges: Arc<DashMap<Path, Arc<[Range<u64>]>>>,
102+
/// Shared full-file natural splits keyed by file path.
103+
pub natural_splits: Arc<DashMap<Path, Arc<NaturalSplits>>>,
105104
/// Whether the query has output ordering specified
106105
pub has_output_ordering: bool,
107106

@@ -143,7 +142,7 @@ impl FileOpener for VortexOpener {
143142
let batch_size = self.batch_size;
144143
let limit = self.limit;
145144
let layout_readers = Arc::clone(&self.layout_readers);
146-
let natural_split_ranges = Arc::clone(&self.natural_split_ranges);
145+
let natural_splits = Arc::clone(&self.natural_splits);
147146
let has_output_ordering = self.has_output_ordering;
148147
let scan_concurrency = self.scan_concurrency;
149148

@@ -373,21 +372,22 @@ impl FileOpener for VortexOpener {
373372
if byte_range.start != 0 || byte_range.end != file.object_meta.size {
374373
// Full-file scans already cover every natural split. Only translate the
375374
// byte range back into row boundaries when DataFusion has trimmed the file.
376-
let natural_split_ranges = natural_split_ranges_for_file(
377-
natural_split_ranges.as_ref(),
375+
let natural_splits = natural_splits_for_file(
376+
natural_splits.as_ref(),
378377
&file.object_meta.location,
379378
&layout_reader,
379+
file.object_meta.size,
380380
)?;
381381

382-
let Some(row_range) = split_aligned_row_range(
383-
byte_range,
384-
file.object_meta.size,
385-
natural_split_ranges.as_ref(),
386-
) else {
382+
let Some(row_range) =
383+
split_aligned_row_range(byte_range, natural_splits.as_ref())
384+
else {
387385
return Ok(stream::empty().boxed());
388386
};
389387

390-
scan_builder = scan_builder.with_row_range(row_range);
388+
scan_builder = scan_builder
389+
.with_row_range(row_range)
390+
.with_natural_splits(Arc::clone(&natural_splits.row_boundaries));
391391
}
392392
}
393393

@@ -499,74 +499,108 @@ impl FileOpener for VortexOpener {
499499
}
500500
}
501501

502-
fn natural_split_ranges_for_file(
503-
natural_split_ranges: &DashMap<Path, Arc<[Range<u64>]>>,
502+
#[derive(Debug)]
503+
pub(crate) struct NaturalSplits {
504+
row_boundaries: Arc<[u64]>,
505+
assignment_bytes: Box<[u64]>,
506+
}
507+
508+
impl NaturalSplits {
509+
fn new(row_boundaries: Arc<[u64]>, total_size: u64) -> Self {
510+
let row_count = row_boundaries.last().copied().unwrap_or_default();
511+
let assignment_bytes = if row_count == 0 {
512+
Box::default()
513+
} else {
514+
row_boundaries
515+
.windows(2)
516+
.enumerate()
517+
.map(|(idx, boundaries)| {
518+
split_assignment_byte(
519+
idx,
520+
&(boundaries[0]..boundaries[1]),
521+
row_count,
522+
total_size,
523+
)
524+
})
525+
.collect()
526+
};
527+
528+
debug_assert!(assignment_bytes.is_sorted());
529+
debug_assert_eq!(
530+
assignment_bytes.len() + usize::from(!row_boundaries.is_empty()),
531+
row_boundaries.len()
532+
);
533+
534+
Self {
535+
row_boundaries,
536+
assignment_bytes,
537+
}
538+
}
539+
}
540+
541+
fn natural_splits_for_file(
542+
natural_splits: &DashMap<Path, Arc<NaturalSplits>>,
504543
path: &Path,
505544
layout_reader: &Arc<dyn LayoutReader>,
506-
) -> DFResult<Arc<[Range<u64>]>> {
507-
if let Some(split_ranges) = natural_split_ranges.get(path) {
508-
return Ok(Arc::clone(split_ranges.value()));
545+
total_size: u64,
546+
) -> DFResult<Arc<NaturalSplits>> {
547+
if let Some(splits) = natural_splits.get(path) {
548+
return Ok(Arc::clone(splits.value()));
509549
}
510550

511551
// Compute while holding the entry so concurrent partitions opening the same file wait
512552
// for the winner instead of all walking the layout tree; the redundant walks contend on
513553
// the lazily-initialized layout children and dominate the cost of the computation itself.
514-
match natural_split_ranges.entry(path.clone()) {
554+
match natural_splits.entry(path.clone()) {
515555
Entry::Occupied(entry) => Ok(Arc::clone(entry.get())),
516556
Entry::Vacant(entry) => {
517-
let split_ranges = compute_natural_split_ranges(layout_reader.as_ref())?;
518-
entry.insert(Arc::clone(&split_ranges));
519-
Ok(split_ranges)
557+
let splits = compute_natural_splits(layout_reader.as_ref(), total_size)?;
558+
entry.insert(Arc::clone(&splits));
559+
Ok(splits)
520560
}
521561
}
522562
}
523563

524-
fn compute_natural_split_ranges(layout_reader: &dyn LayoutReader) -> DFResult<Arc<[Range<u64>]>> {
564+
fn compute_natural_splits(
565+
layout_reader: &dyn LayoutReader,
566+
total_size: u64,
567+
) -> DFResult<Arc<NaturalSplits>> {
525568
let row_count = layout_reader.row_count();
526569
let row_range = 0..row_count;
527-
let split_points: Vec<_> = SplitBy::Layout
570+
let row_boundaries = SplitBy::Layout
528571
.splits(layout_reader, &row_range, &[FieldMask::All])
529-
.map_err(|e| exec_datafusion_err!("Failed to compute Vortex natural splits: {e}"))?
530-
.into_iter()
531-
.tuple_windows()
532-
.map(|(s, e)| s..e)
533-
.collect::<Vec<_>>();
572+
.map_err(|e| exec_datafusion_err!("Failed to compute Vortex natural splits: {e}"))?;
534573

535-
Ok(split_points.into())
574+
Ok(Arc::new(NaturalSplits::new(
575+
row_boundaries.into(),
576+
total_size,
577+
)))
536578
}
537579

538580
/// Translate a DataFusion byte range to the contiguous natural split ranges it owns.
539581
/// Most splits are assigned by midpoint, but the leading split stays with the range that owns
540582
/// byte 0 so a tiny first byte range still claims the first rows.
541583
fn split_aligned_row_range(
542584
byte_range: Range<u64>,
543-
total_size: u64,
544-
split_ranges: &[Range<u64>],
585+
natural_splits: &NaturalSplits,
545586
) -> Option<Range<u64>> {
546587
if byte_range.start >= byte_range.end {
547588
return None;
548589
}
549590

550-
let row_count = split_ranges.last().map(|split| split.end)?;
551-
if row_count == 0 {
591+
let first_split = natural_splits
592+
.assignment_bytes
593+
.partition_point(|&assignment_byte| assignment_byte < byte_range.start);
594+
let after_last_split = natural_splits
595+
.assignment_bytes
596+
.partition_point(|&assignment_byte| assignment_byte < byte_range.end);
597+
if first_split == after_last_split {
552598
return None;
553599
}
554600

555-
let mut owned_splits = split_ranges
556-
.iter()
557-
.enumerate()
558-
.filter_map(|(idx, split_range)| {
559-
let assignment_byte = split_assignment_byte(idx, split_range, row_count, total_size);
560-
byte_range.contains(&assignment_byte).then_some(split_range)
561-
});
562-
563-
let first_split = owned_splits.next()?;
564-
let mut row_range = first_split.start..first_split.end;
565-
for split_range in owned_splits {
566-
row_range.end = split_range.end;
567-
}
568-
569-
Some(row_range)
601+
Some(
602+
natural_splits.row_boundaries[first_split]..natural_splits.row_boundaries[after_last_split],
603+
)
570604
}
571605

572606
fn split_assignment_byte(
@@ -701,6 +735,15 @@ mod tests {
701735
}
702736
}
703737

738+
fn natural_splits(total_size: u64, split_ranges: &[Range<u64>]) -> NaturalSplits {
739+
let mut row_boundaries = Vec::with_capacity(split_ranges.len() + 1);
740+
if let Some(first) = split_ranges.first() {
741+
row_boundaries.push(first.start);
742+
row_boundaries.extend(split_ranges.iter().map(|range| range.end));
743+
}
744+
NaturalSplits::new(row_boundaries.into(), total_size)
745+
}
746+
704747
#[rstest]
705748
#[case(0..3, 10, vec![0..2, 2..5, 5..10], Some(0..2))]
706749
#[case(3..7, 10, vec![0..2, 2..5, 5..10], Some(2..5))]
@@ -714,7 +757,7 @@ mod tests {
714757
#[case] expected: Option<Range<u64>>,
715758
) {
716759
assert_eq!(
717-
split_aligned_row_range(byte_range, total_size, &split_ranges),
760+
split_aligned_row_range(byte_range, &natural_splits(total_size, &split_ranges)),
718761
expected
719762
);
720763
}
@@ -723,10 +766,11 @@ mod tests {
723766
fn test_split_aligned_ranges_cover_splits_exactly_once() {
724767
let split_ranges = vec![0..1, 1..4, 4..10, 10..13];
725768
let byte_ranges = [0..4, 4..8, 8..12, 12..16];
769+
let natural_splits = natural_splits(16, &split_ranges);
726770

727771
let assigned = byte_ranges
728772
.into_iter()
729-
.filter_map(|byte_range| split_aligned_row_range(byte_range, 16, &split_ranges))
773+
.filter_map(|byte_range| split_aligned_row_range(byte_range, &natural_splits))
730774
.collect::<Vec<_>>();
731775

732776
assert_eq!(assigned, vec![0..4, 4..10, 10..13]);
@@ -757,6 +801,15 @@ mod tests {
757801
}
758802
}
759803

804+
#[test]
805+
fn test_split_aligned_row_range_keeps_colliding_assignments_together() {
806+
let natural_splits = natural_splits(2, &[0..1, 1..2, 2..3, 3..4]);
807+
808+
assert_eq!(natural_splits.assignment_bytes.as_ref(), [0, 0, 1, 1]);
809+
assert_eq!(split_aligned_row_range(0..1, &natural_splits), Some(0..2));
810+
assert_eq!(split_aligned_row_range(1..2, &natural_splits), Some(2..4));
811+
}
812+
760813
async fn write_arrow_to_vortex(
761814
object_store: Arc<dyn ObjectStore>,
762815
path: &str,
@@ -794,7 +847,7 @@ mod tests {
794847
metrics_registry: Arc::new(DefaultMetricsRegistry::default()),
795848
df_metrics: ExecutionPlanMetricsSet::new(),
796849
layout_readers: Default::default(),
797-
natural_split_ranges: Default::default(),
850+
natural_splits: Default::default(),
798851
has_output_ordering: false,
799852
expression_convertor: Arc::new(DefaultExpressionConvertor::default()),
800853
file_metadata_cache: None,
@@ -1200,7 +1253,7 @@ mod tests {
12001253
metrics_registry: Arc::new(DefaultMetricsRegistry::default()),
12011254
df_metrics: ExecutionPlanMetricsSet::new(),
12021255
layout_readers: Default::default(),
1203-
natural_split_ranges: Default::default(),
1256+
natural_splits: Default::default(),
12041257
has_output_ordering: false,
12051258
expression_convertor: Arc::new(DefaultExpressionConvertor::default()),
12061259
file_metadata_cache: None,
@@ -1288,7 +1341,7 @@ mod tests {
12881341
metrics_registry: Arc::new(DefaultMetricsRegistry::default()),
12891342
df_metrics: ExecutionPlanMetricsSet::new(),
12901343
layout_readers: Default::default(),
1291-
natural_split_ranges: Default::default(),
1344+
natural_splits: Default::default(),
12921345
has_output_ordering: false,
12931346
expression_convertor: Arc::new(DefaultExpressionConvertor::default()),
12941347
file_metadata_cache: None,
@@ -1446,7 +1499,7 @@ mod tests {
14461499
metrics_registry: Arc::new(DefaultMetricsRegistry::default()),
14471500
df_metrics: ExecutionPlanMetricsSet::new(),
14481501
layout_readers: Default::default(),
1449-
natural_split_ranges: Default::default(),
1502+
natural_splits: Default::default(),
14501503
has_output_ordering: false,
14511504
expression_convertor: Arc::new(DefaultExpressionConvertor::default()),
14521505
file_metadata_cache: None,
@@ -1507,7 +1560,7 @@ mod tests {
15071560
metrics_registry: Arc::new(DefaultMetricsRegistry::default()),
15081561
df_metrics: ExecutionPlanMetricsSet::new(),
15091562
layout_readers: Default::default(),
1510-
natural_split_ranges: Default::default(),
1563+
natural_splits: Default::default(),
15111564
has_output_ordering: false,
15121565
expression_convertor: Arc::new(DefaultExpressionConvertor::default()),
15131566
file_metadata_cache: None,
@@ -1717,7 +1770,7 @@ mod tests {
17171770
metrics_registry: Arc::new(DefaultMetricsRegistry::default()),
17181771
df_metrics: ExecutionPlanMetricsSet::new(),
17191772
layout_readers: Default::default(),
1720-
natural_split_ranges: Default::default(),
1773+
natural_splits: Default::default(),
17211774
has_output_ordering: false,
17221775
expression_convertor: Arc::new(DefaultExpressionConvertor::default()),
17231776
file_metadata_cache: None,

‎vortex-datafusion/src/persistent/source.rs‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// SPDX-FileCopyrightText: Copyright the Vortex contributors
33

44
use std::fmt::Formatter;
5-
use std::ops::Range;
65
use std::sync::Arc;
76
use std::sync::Weak;
87

@@ -37,6 +36,7 @@ use vortex::metrics::MetricsRegistry;
3736
use vortex::session::VortexSession;
3837
use vortex_utils::aliases::dash_map::DashMap;
3938

39+
use super::opener::NaturalSplits;
4040
use super::opener::VortexOpener;
4141
use crate::VortexTableOptions;
4242
use crate::convert::exprs::DefaultExpressionConvertor;
@@ -198,8 +198,8 @@ pub struct VortexSource {
198198
///
199199
/// Sharing the readers allows us to only read every layout once from the file, even across partitions.
200200
layout_readers: Arc<DashMap<Path, Weak<dyn LayoutReader>>>,
201-
/// Shared full-file natural split ranges keyed by path.
202-
natural_split_ranges: Arc<DashMap<Path, Arc<[Range<u64>]>>>,
201+
/// Shared full-file natural splits keyed by path.
202+
natural_splits: Arc<DashMap<Path, Arc<NaturalSplits>>>,
203203
expression_convertor: Arc<dyn ExpressionConvertor>,
204204
pub(crate) vortex_reader_factory: Option<Arc<dyn VortexReaderFactory>>,
205205
pub(crate) ordered: bool,
@@ -233,7 +233,7 @@ impl VortexSource {
233233
batch_size: None,
234234
df_metrics: Default::default(),
235235
layout_readers: Arc::new(DashMap::default()),
236-
natural_split_ranges: Arc::new(DashMap::default()),
236+
natural_splits: Arc::new(DashMap::default()),
237237
expression_convertor,
238238
vortex_reader_factory: None,
239239
vx_metrics_registry: Arc::new(DefaultMetricsRegistry::default()),
@@ -365,7 +365,7 @@ impl VortexSource {
365365
metrics_registry: Arc::clone(&self.vx_metrics_registry),
366366
df_metrics: self.df_metrics.clone(),
367367
layout_readers: Arc::clone(&self.layout_readers),
368-
natural_split_ranges: Arc::clone(&self.natural_split_ranges),
368+
natural_splits: Arc::clone(&self.natural_splits),
369369
has_output_ordering: !base_config.output_ordering.is_empty() || self.ordered,
370370
expression_convertor: Arc::clone(&self.expression_convertor),
371371
file_metadata_cache: self.file_metadata_cache.clone(),

‎vortex-layout/src/scan/repeated_scan.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,7 @@ impl RepeatedScan {
394394
if range.is_empty() {
395395
return Vec::new();
396396
}
397-
let lo = vec.partition_point(|&x| x < range.start);
397+
let lo = vec.partition_point(|&x| x <= range.start);
398398
let hi = vec.partition_point(|&x| x < range.end);
399399
Either::Right(
400400
iter::once(range.start)

0 commit comments

Comments
 (0)