1use std::ops::Range;
8use std::sync::atomic::AtomicU64;
9use std::sync::{Arc, Mutex};
10
11use arrow::compute::concat_batches;
12use arrow::datatypes::UInt64Type;
13use arrow::{array::AsArray, compute::sort_to_indices};
14use arrow_array::{RecordBatch, UInt32Array, UInt64Array};
15use arrow_schema::{DataType, Field, Schema};
16use futures::{future::try_join_all, prelude::*};
17use lance_arrow::stream::rechunk_stream_by_size;
18use lance_arrow::{RecordBatchExt, SchemaExt};
19use lance_core::{
20 Error, Result,
21 cache::LanceCache,
22 utils::tokio::{get_num_compute_intensive_cpus, spawn_cpu},
23};
24use lance_encoding::decoder::{DecoderPlugins, FilterExpression};
25use lance_encoding::version::LanceFileVersion;
26use lance_file::reader::{FileReader, FileReaderOptions};
27use lance_file::writer::{FileWriter, FileWriterOptions};
28use lance_io::{
29 ReadBatchParams,
30 object_store::ObjectStore,
31 scheduler::{ScanScheduler, SchedulerConfig},
32 stream::{RecordBatchStream, RecordBatchStreamAdapter},
33 utils::CachedFileSize,
34};
35use object_store::path::Path;
36
37use crate::vector::{LOSS_METADATA_KEY, PART_ID_COLUMN};
38
39#[async_trait::async_trait]
40pub trait ShuffleReader: Send + Sync {
42 async fn read_partition(
46 &self,
47 partition_id: usize,
48 ) -> Result<Option<Box<dyn RecordBatchStream + Unpin + 'static>>>;
49
50 fn partition_size(&self, partition_id: usize) -> Result<usize>;
52
53 fn total_loss(&self) -> Option<f64>;
58}
59
60#[async_trait::async_trait]
61pub trait Shuffler: Send + Sync {
64 async fn shuffle(
67 &self,
68 data: Box<dyn RecordBatchStream + Unpin + 'static>,
69 ) -> Result<Box<dyn ShuffleReader>>;
70}
71
72pub struct IvfShuffler {
73 object_store: Arc<ObjectStore>,
74 output_dir: Path,
75 num_partitions: usize,
76 format_version: LanceFileVersion,
77
78 progress: Arc<dyn crate::progress::IndexBuildProgress>,
79}
80
81impl IvfShuffler {
82 pub fn new(output_dir: Path, num_partitions: usize) -> Self {
83 Self {
84 object_store: Arc::new(ObjectStore::local()),
85 output_dir,
86 num_partitions,
87 format_version: LanceFileVersion::V2_0,
88 progress: crate::progress::noop_progress(),
89 }
90 }
91
92 pub fn with_format_version(mut self, format_version: LanceFileVersion) -> Self {
93 self.format_version = format_version;
94 self
95 }
96
97 pub fn with_progress(mut self, progress: Arc<dyn crate::progress::IndexBuildProgress>) -> Self {
98 self.progress = progress;
99 self
100 }
101}
102
103#[async_trait::async_trait]
104impl Shuffler for IvfShuffler {
105 async fn shuffle(
106 &self,
107 data: Box<dyn RecordBatchStream + Unpin + 'static>,
108 ) -> Result<Box<dyn ShuffleReader>> {
109 let num_partitions = self.num_partitions;
110 let mut partition_sizes = vec![0; num_partitions];
111 let schema = data.schema().without_column(PART_ID_COLUMN);
112 let mut writers = stream::iter(0..num_partitions)
113 .map(|partition_id| {
114 let part_path = self.output_dir.child(format!("ivf_{}.lance", partition_id));
115 let spill_path = self.output_dir.child(format!("ivf_{}.spill", partition_id));
116 let object_store = self.object_store.clone();
117 let schema = schema.clone();
118 let format_version = self.format_version;
119 async move {
120 let writer = object_store.create(&part_path).await?;
121 let file_writer = FileWriter::try_new(
122 writer,
123 lance_core::datatypes::Schema::try_from(&schema)?,
124 FileWriterOptions {
125 format_version: Some(format_version),
126 ..Default::default()
127 },
128 )?
129 .with_page_metadata_spill(object_store.clone(), spill_path);
130 Result::Ok(file_writer)
131 }
132 })
133 .buffered(self.object_store.io_parallelism())
134 .try_collect::<Vec<_>>()
135 .await?;
136 let mut parallel_sort_stream = data
137 .map(|batch| {
138 spawn_cpu(move || {
139 let batch = batch?;
140
141 let loss = batch
142 .metadata()
143 .get(LOSS_METADATA_KEY)
144 .map(|s| s.parse::<f64>().unwrap_or_default())
145 .unwrap_or_default();
146
147 let part_ids: &UInt32Array = batch[PART_ID_COLUMN].as_primitive();
148
149 let indices = sort_to_indices(&part_ids, None, None)?;
150 let batch = batch.take(&indices)?;
151
152 let part_ids: &UInt32Array = batch[PART_ID_COLUMN].as_primitive();
153 let batch = batch.drop_column(PART_ID_COLUMN)?;
154
155 let mut partition_buffers = vec![Vec::new(); num_partitions];
156
157 let mut start = 0;
158 while start < batch.num_rows() {
159 let part_id: u32 = part_ids.value(start);
160 let mut end = start + 1;
161 while end < batch.num_rows() && part_ids.value(end) == part_id {
162 end += 1;
163 }
164
165 let part_batches = &mut partition_buffers[part_id as usize];
166 part_batches.push(batch.slice(start, end - start));
167 start = end;
168 }
169
170 Ok::<(Vec<Vec<RecordBatch>>, f64), Error>((partition_buffers, loss))
171 })
172 })
173 .buffered(get_num_compute_intensive_cpus());
174
175 let mut total_loss = 0.0;
176 let mut num_rows = 0u64;
177 while let Some(shuffled) = parallel_sort_stream.next().await {
178 let (shuffled, loss) = shuffled?;
179 total_loss += loss;
180
181 let mut futs = Vec::new();
182 for (part_id, (writer, batches)) in writers.iter_mut().zip(shuffled.iter()).enumerate()
183 {
184 if !batches.is_empty() {
185 let rows = batches.iter().map(|b| b.num_rows()).sum::<usize>();
186 partition_sizes[part_id] += rows;
187 num_rows += rows as u64;
188 futs.push(writer.write_batches(batches.iter()));
189 }
190 }
191 try_join_all(futs).await?;
192
193 self.progress.stage_progress("shuffle", num_rows).await?;
194 }
195
196 for writer in writers.iter_mut() {
198 writer.finish().await?;
199 }
200
201 Ok(Box::new(IvfShufflerReader::new(
202 self.object_store.clone(),
203 self.output_dir.clone(),
204 partition_sizes,
205 total_loss,
206 )))
207 }
208}
209
210pub struct IvfShufflerReader {
211 scheduler: Arc<ScanScheduler>,
212 output_dir: Path,
213 partition_sizes: Vec<usize>,
214 loss: f64,
215}
216
217impl IvfShufflerReader {
218 pub fn new(
219 object_store: Arc<ObjectStore>,
220 output_dir: Path,
221 partition_sizes: Vec<usize>,
222 loss: f64,
223 ) -> Self {
224 let scheduler_config = SchedulerConfig::max_bandwidth(&object_store);
225 let scheduler = ScanScheduler::new(object_store, scheduler_config);
226 Self {
227 scheduler,
228 output_dir,
229 partition_sizes,
230 loss,
231 }
232 }
233}
234
235#[async_trait::async_trait]
236impl ShuffleReader for IvfShufflerReader {
237 async fn read_partition(
238 &self,
239 partition_id: usize,
240 ) -> Result<Option<Box<dyn RecordBatchStream + Unpin + 'static>>> {
241 if partition_id >= self.partition_sizes.len() {
242 return Ok(None);
243 }
244
245 let partition_path = self.output_dir.child(format!("ivf_{}.lance", partition_id));
246
247 let reader = FileReader::try_open(
248 self.scheduler
249 .open_file(&partition_path, &CachedFileSize::unknown())
250 .await?,
251 None,
252 Arc::<DecoderPlugins>::default(),
253 &LanceCache::no_cache(),
254 FileReaderOptions::default(),
255 )
256 .await?;
257 let schema: Schema = reader.schema().as_ref().into();
258 Ok(Some(Box::new(RecordBatchStreamAdapter::new(
259 Arc::new(schema),
260 reader.read_stream(
261 lance_io::ReadBatchParams::RangeFull,
262 u32::MAX,
263 16,
264 FilterExpression::no_filter(),
265 )?,
266 ))))
267 }
268
269 fn partition_size(&self, partition_id: usize) -> Result<usize> {
270 Ok(self.partition_sizes.get(partition_id).copied().unwrap_or(0))
271 }
272
273 fn total_loss(&self) -> Option<f64> {
274 Some(self.loss)
275 }
276}
277
278pub struct EmptyReader;
279
280#[async_trait::async_trait]
281impl ShuffleReader for EmptyReader {
282 async fn read_partition(
283 &self,
284 _partition_id: usize,
285 ) -> Result<Option<Box<dyn RecordBatchStream + Unpin + 'static>>> {
286 Ok(None)
287 }
288
289 fn partition_size(&self, _partition_id: usize) -> Result<usize> {
290 Ok(0)
291 }
292
293 fn total_loss(&self) -> Option<f64> {
294 None
295 }
296}
297
298pub fn create_ivf_shuffler(
306 output_dir: Path,
307 num_partitions: usize,
308 format_version: LanceFileVersion,
309 progress: Option<Arc<dyn crate::progress::IndexBuildProgress>>,
310) -> Box<dyn Shuffler> {
311 let use_legacy = std::env::var("LANCE_LEGACY_SHUFFLER")
312 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
313 .unwrap_or(false);
314 if use_legacy {
315 let mut shuffler =
316 IvfShuffler::new(output_dir, num_partitions).with_format_version(format_version);
317 if let Some(progress) = progress {
318 shuffler = shuffler.with_progress(progress);
319 }
320 Box::new(shuffler)
321 } else {
322 let mut shuffler = TwoFileShuffler::new(output_dir, num_partitions);
323 if let Some(progress) = progress {
324 shuffler = shuffler.with_progress(progress);
325 }
326 Box::new(shuffler)
327 }
328}
329
330const DEFAULT_SHUFFLE_BATCH_BYTES: usize = 128 * 1024 * 1024;
331
332fn shuffle_batch_bytes() -> usize {
339 let batch_size = std::env::var("LANCE_SHUFFLE_BATCH_BYTES")
340 .ok()
341 .and_then(|s| s.parse().ok())
342 .unwrap_or(DEFAULT_SHUFFLE_BATCH_BYTES);
343 if batch_size == 0 {
344 log::warn!(
345 "LANCE_SHUFFLE_BATCH_BYTES is 0, using default of {}",
346 DEFAULT_SHUFFLE_BATCH_BYTES
347 );
348 DEFAULT_SHUFFLE_BATCH_BYTES
349 } else {
350 batch_size
351 }
352}
353
354pub struct TwoFileShuffler {
367 object_store: Arc<ObjectStore>,
368 output_dir: Path,
369 num_partitions: usize,
370 batch_size_bytes: usize,
371
372 progress: Arc<dyn crate::progress::IndexBuildProgress>,
373}
374
375impl TwoFileShuffler {
376 pub fn new(output_dir: Path, num_partitions: usize) -> Self {
377 Self {
378 object_store: Arc::new(ObjectStore::local()),
379 output_dir,
380 num_partitions,
381 batch_size_bytes: shuffle_batch_bytes(),
382 progress: crate::progress::noop_progress(),
383 }
384 }
385
386 pub fn with_progress(mut self, progress: Arc<dyn crate::progress::IndexBuildProgress>) -> Self {
387 self.progress = progress;
388 self
389 }
390
391 #[cfg(test)]
392 fn with_batch_size_bytes(mut self, batch_size_bytes: usize) -> Self {
393 self.batch_size_bytes = batch_size_bytes;
394 self
395 }
396}
397
398#[async_trait::async_trait]
399impl Shuffler for TwoFileShuffler {
400 async fn shuffle(
401 &self,
402 data: Box<dyn RecordBatchStream + Unpin + 'static>,
403 ) -> Result<Box<dyn ShuffleReader>> {
404 let num_partitions = self.num_partitions;
405 let full_schema = Arc::new(data.schema().as_ref().clone());
406 let schema = data.schema().without_column(PART_ID_COLUMN);
408 let offsets_schema = Arc::new(Schema::new(vec![Field::new(
409 "offset",
410 DataType::UInt64,
411 false,
412 )]));
413 let batch_size_bytes = self.batch_size_bytes;
414
415 let total_loss = Arc::new(Mutex::new(0.0f64));
417 let loss_ref = total_loss.clone();
418 let loss_stream = data.map(move |result| {
419 result.inspect(|batch| {
420 let loss = batch
421 .metadata()
422 .get(LOSS_METADATA_KEY)
423 .and_then(|s| s.parse::<f64>().ok())
424 .unwrap_or(0.0);
425 *loss_ref.lock().unwrap() += loss;
426 })
427 });
428
429 let rechunked = rechunk_stream_by_size(
431 loss_stream,
432 full_schema,
433 batch_size_bytes,
434 batch_size_bytes * 2,
435 );
436
437 let data_path = self.output_dir.child("shuffle_data.lance");
439 let spill_path = self.output_dir.child("shuffle_data.spill");
440 let writer = self.object_store.create(&data_path).await?;
441 let mut file_writer = FileWriter::try_new(
442 writer,
443 lance_core::datatypes::Schema::try_from(&schema)?,
444 Default::default(),
445 )?
446 .with_page_metadata_spill(self.object_store.clone(), spill_path);
447
448 let offsets_path = self.output_dir.child("shuffle_offsets.lance");
450 let spill_path = self.output_dir.child("shuffle_offsets.spill");
451 let writer = self.object_store.create(&offsets_path).await?;
452 let mut offsets_writer = FileWriter::try_new(
453 writer,
454 lance_core::datatypes::Schema::try_from(offsets_schema.as_ref())?,
455 Default::default(),
456 )?
457 .with_page_metadata_spill(self.object_store.clone(), spill_path);
458
459 let num_batches = Arc::new(AtomicU64::new(0));
460 let num_batches_ref = num_batches.clone();
461 let mut partition_counts: Vec<u64> = vec![0; num_partitions];
462 let mut global_row_count: u64 = 0;
463 let mut rows_processed: u64 = 0;
464
465 let mut rechunked = std::pin::pin!(rechunked);
466 while let Some(batch) = rechunked.next().await {
467 num_batches_ref.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
468 let batch = batch?;
469 let np = num_partitions;
470 let num_rows = batch.num_rows() as u64;
471
472 let (sorted_batch, batch_offsets) = spawn_cpu(move || {
474 let part_ids: &UInt32Array = batch[PART_ID_COLUMN].as_primitive();
475 let indices = sort_to_indices(part_ids, None, None)?;
476 let batch = batch.take(&indices)?;
477
478 let part_ids: &UInt32Array = batch[PART_ID_COLUMN].as_primitive();
479 let batch = batch.drop_column(PART_ID_COLUMN)?;
480
481 let mut partition_counts = vec![0u64; np];
483 for i in 0..part_ids.len() {
484 let pid = part_ids.value(i) as usize;
485 if pid < np {
486 partition_counts[pid] += 1;
487 } else {
488 log::warn!("Partition ID {} is out of range [0, {})", pid, np);
489 }
490 }
491
492 let mut batch_offsets = Vec::with_capacity(np);
494 let mut running = 0u64;
495 for count in &partition_counts {
496 running += count;
497 batch_offsets.push(running);
498 }
499
500 Ok::<(RecordBatch, Vec<u64>), Error>((batch, batch_offsets))
501 })
502 .await?;
503
504 file_writer.write_batch(&sorted_batch).await?;
506
507 let mut adjusted_offsets = Vec::with_capacity(batch_offsets.len());
509 let mut last_offset = 0;
510 for (idx, offset) in batch_offsets.iter().enumerate() {
511 adjusted_offsets.push(global_row_count + offset);
512 partition_counts[idx] += offset - last_offset;
513 last_offset = *offset;
514 }
515 global_row_count += sorted_batch.num_rows() as u64;
516
517 let offsets_batch = RecordBatch::try_new(
519 offsets_schema.clone(),
520 vec![Arc::new(UInt64Array::from(adjusted_offsets))],
521 )?;
522 offsets_writer.write_batch(&offsets_batch).await?;
523
524 rows_processed += num_rows;
525 self.progress
526 .stage_progress("shuffle", rows_processed)
527 .await?;
528 }
529
530 file_writer.finish().await?;
532 offsets_writer.finish().await?;
533
534 let num_batches = num_batches.load(std::sync::atomic::Ordering::Relaxed);
535
536 let total_loss_val = *total_loss.lock().unwrap();
537
538 TwoFileShuffleReader::try_new(
539 self.object_store.clone(),
540 self.output_dir.clone(),
541 num_partitions,
542 num_batches,
543 partition_counts,
544 total_loss_val,
545 )
546 .await
547 }
548}
549
550pub struct TwoFileShuffleReader {
551 _scheduler: Arc<ScanScheduler>,
552 file_reader: FileReader,
553 offsets_reader: FileReader,
554 num_partitions: usize,
555 num_batches: u64,
556 partition_counts: Vec<u64>,
557 total_loss: f64,
558}
559
560impl TwoFileShuffleReader {
561 async fn try_new(
562 object_store: Arc<ObjectStore>,
563 output_dir: Path,
564 num_partitions: usize,
565 num_batches: u64,
566 partition_counts: Vec<u64>,
567 total_loss: f64,
568 ) -> Result<Box<dyn ShuffleReader>> {
569 if num_batches == 0 {
570 return Ok(Box::new(EmptyReader));
571 }
572
573 let scheduler_config = SchedulerConfig::max_bandwidth(&object_store);
574 let scheduler = ScanScheduler::new(object_store, scheduler_config);
575
576 let data_path = output_dir.child("shuffle_data.lance");
577 let file_reader = FileReader::try_open(
578 scheduler
579 .open_file(&data_path, &CachedFileSize::unknown())
580 .await?,
581 None,
582 Arc::<DecoderPlugins>::default(),
583 &LanceCache::no_cache(),
584 FileReaderOptions::default(),
585 )
586 .await?;
587
588 let offsets_path = output_dir.child("shuffle_offsets.lance");
589 let offsets_reader = FileReader::try_open(
590 scheduler
591 .open_file(&offsets_path, &CachedFileSize::unknown())
592 .await?,
593 None,
594 Arc::<DecoderPlugins>::default(),
595 &LanceCache::no_cache(),
596 FileReaderOptions::default(),
597 )
598 .await?;
599
600 Ok(Box::new(Self {
601 _scheduler: scheduler,
602 file_reader,
603 offsets_reader,
604 num_partitions,
605 num_batches,
606 partition_counts,
607 total_loss,
608 }))
609 }
610
611 async fn partition_ranges(&self, partition_id: usize) -> Result<Vec<Range<u64>>> {
612 let mut positions = Vec::with_capacity(self.num_batches as usize * 2);
613 for batch_idx in 0..self.num_batches {
614 let end_pos = u32::try_from(batch_idx as usize * self.num_partitions + partition_id)
615 .map_err(|_| Error::invalid_input("There are more than 2^32 partition offsets in the spill file. Need to support 64-bit take"))?;
616 if end_pos != 0 {
617 positions.push(end_pos - 1);
618 }
619 positions.push(end_pos);
620 }
621 let positions = UInt32Array::from(positions);
622 let num_positions = positions.len() as u32;
623 let offsets_stream = self.offsets_reader.read_stream(
624 ReadBatchParams::Indices(positions),
625 num_positions,
626 1,
627 FilterExpression::no_filter(),
628 )?;
629 let schema = offsets_stream.schema().clone();
630 let offsets = offsets_stream.try_collect::<Vec<_>>().await?;
631 let offsets = if offsets.is_empty() {
632 unreachable!()
634 } else if offsets.len() == 1 {
635 offsets.into_iter().next().unwrap()
636 } else {
637 concat_batches(&schema, &offsets)?
638 };
639
640 let offsets = offsets.column(0).as_primitive::<UInt64Type>();
641 let mut offsets_iter = offsets.values().iter().copied();
642
643 let mut ranges = Vec::with_capacity(self.num_batches as usize);
644 for batch_idx in 0..self.num_batches {
645 if batch_idx == 0 && partition_id == 0 {
646 ranges.push(0..offsets_iter.next().unwrap());
648 } else {
649 ranges.push(offsets_iter.next().unwrap()..offsets_iter.next().unwrap());
650 }
651 }
652 Ok(ranges)
653 }
654}
655
656#[async_trait::async_trait]
657impl ShuffleReader for TwoFileShuffleReader {
658 async fn read_partition(
659 &self,
660 partition_id: usize,
661 ) -> Result<Option<Box<dyn RecordBatchStream + Unpin + 'static>>> {
662 if partition_id >= self.num_partitions {
663 return Ok(None);
664 }
665 if self.partition_counts[partition_id] == 0 {
666 return Ok(None);
667 }
668
669 let ranges = self.partition_ranges(partition_id).await?;
670 if ranges.is_empty() {
671 return Ok(None);
672 }
673
674 let schema: Schema = self.file_reader.schema().as_ref().into();
675 Ok(Some(Box::new(RecordBatchStreamAdapter::new(
676 Arc::new(schema),
677 self.file_reader.read_stream(
678 ReadBatchParams::Ranges(ranges.into()),
679 u32::MAX,
680 16,
681 FilterExpression::no_filter(),
682 )?,
683 ))))
684 }
685
686 fn partition_size(&self, partition_id: usize) -> Result<usize> {
687 Ok(self
688 .partition_counts
689 .get(partition_id)
690 .copied()
691 .unwrap_or(0) as usize)
692 }
693
694 fn total_loss(&self) -> Option<f64> {
695 Some(self.total_loss)
696 }
697}
698
699#[cfg(test)]
700mod tests {
701 use super::*;
702
703 use arrow_array::{Int32Array, RecordBatch, UInt32Array};
704 use arrow_schema::{DataType, Field, Schema as ArrowSchema};
705 use futures::stream;
706 use lance_arrow::RecordBatchExt;
707 use lance_core::utils::tempfile::TempStrDir;
708 use lance_io::stream::RecordBatchStreamAdapter;
709
710 use crate::vector::{LOSS_METADATA_KEY, PART_ID_COLUMN};
711
712 fn make_batch(part_ids: &[u32], values: &[i32], loss: Option<f64>) -> RecordBatch {
714 let schema = Arc::new(ArrowSchema::new(vec![
715 Field::new(PART_ID_COLUMN, DataType::UInt32, false),
716 Field::new("val", DataType::Int32, false),
717 ]));
718 let batch = RecordBatch::try_new(
719 schema,
720 vec![
721 Arc::new(UInt32Array::from(part_ids.to_vec())),
722 Arc::new(Int32Array::from(values.to_vec())),
723 ],
724 )
725 .unwrap();
726 if let Some(loss_val) = loss {
727 batch
728 .add_metadata(LOSS_METADATA_KEY.to_owned(), loss_val.to_string())
729 .unwrap()
730 } else {
731 batch
732 }
733 }
734
735 fn batches_to_stream(
736 batches: Vec<RecordBatch>,
737 ) -> Box<dyn RecordBatchStream + Unpin + 'static> {
738 let schema = batches[0].schema();
739 let stream = stream::iter(batches.into_iter().map(Ok));
740 Box::new(RecordBatchStreamAdapter::new(schema, stream))
741 }
742
743 async fn collect_partition(
745 reader: &dyn ShuffleReader,
746 partition_id: usize,
747 ) -> Option<RecordBatch> {
748 let stream = reader.read_partition(partition_id).await.unwrap()?;
749 let batches: Vec<RecordBatch> = stream.try_collect().await.unwrap();
750 if batches.is_empty() {
751 return None;
752 }
753 Some(arrow::compute::concat_batches(&batches[0].schema(), &batches).unwrap())
754 }
755
756 #[tokio::test]
757 async fn test_two_file_shuffler_round_trip() {
758 let dir = TempStrDir::default();
759 let output_dir = Path::from(dir.as_ref());
760 let num_partitions = 3;
761
762 let batch = make_batch(&[0, 1, 2, 0, 1], &[10, 20, 30, 40, 50], None);
766
767 let shuffler = TwoFileShuffler::new(output_dir, num_partitions);
768 let stream = batches_to_stream(vec![batch]);
769 let reader = shuffler.shuffle(stream).await.unwrap();
770
771 assert_eq!(reader.partition_size(0).unwrap(), 2);
773 assert_eq!(reader.partition_size(1).unwrap(), 2);
774 assert_eq!(reader.partition_size(2).unwrap(), 1);
775
776 let p0 = collect_partition(reader.as_ref(), 0).await.unwrap();
778 let vals: &Int32Array = p0.column_by_name("val").unwrap().as_primitive();
779 let mut v: Vec<i32> = vals.iter().map(|x| x.unwrap()).collect();
780 v.sort();
781 assert_eq!(v, vec![10, 40]);
782
783 let p1 = collect_partition(reader.as_ref(), 1).await.unwrap();
785 let vals: &Int32Array = p1.column_by_name("val").unwrap().as_primitive();
786 let mut v: Vec<i32> = vals.iter().map(|x| x.unwrap()).collect();
787 v.sort();
788 assert_eq!(v, vec![20, 50]);
789
790 let p2 = collect_partition(reader.as_ref(), 2).await.unwrap();
792 let vals: &Int32Array = p2.column_by_name("val").unwrap().as_primitive();
793 let v: Vec<i32> = vals.iter().map(|x| x.unwrap()).collect();
794 assert_eq!(v, vec![30]);
795
796 assert!(reader.read_partition(3).await.unwrap().is_none());
798 }
799
800 #[tokio::test]
801 async fn test_two_file_shuffler_empty_partitions() {
802 let dir = TempStrDir::default();
803 let output_dir = Path::from(dir.as_ref());
804 let num_partitions = 5;
805
806 let batch = make_batch(&[0, 3, 0, 3], &[10, 20, 30, 40], None);
808
809 let shuffler = TwoFileShuffler::new(output_dir, num_partitions);
810 let stream = batches_to_stream(vec![batch]);
811 let reader = shuffler.shuffle(stream).await.unwrap();
812
813 assert_eq!(reader.partition_size(0).unwrap(), 2);
814 assert_eq!(reader.partition_size(1).unwrap(), 0);
815 assert_eq!(reader.partition_size(2).unwrap(), 0);
816 assert_eq!(reader.partition_size(3).unwrap(), 2);
817 assert_eq!(reader.partition_size(4).unwrap(), 0);
818
819 assert!(reader.read_partition(1).await.unwrap().is_none());
820 assert!(reader.read_partition(2).await.unwrap().is_none());
821 assert!(reader.read_partition(4).await.unwrap().is_none());
822
823 let p0 = collect_partition(reader.as_ref(), 0).await.unwrap();
824 assert_eq!(p0.num_rows(), 2);
825 let p3 = collect_partition(reader.as_ref(), 3).await.unwrap();
826 assert_eq!(p3.num_rows(), 2);
827 }
828
829 #[tokio::test]
830 async fn test_two_file_shuffler_loss_tracking() {
831 let dir = TempStrDir::default();
832 let output_dir = Path::from(dir.as_ref());
833 let num_partitions = 2;
834
835 let batch1 = make_batch(&[0, 1], &[10, 20], Some(1.5));
836 let batch2 = make_batch(&[0, 1], &[30, 40], Some(2.5));
837 let batch3 = make_batch(&[0], &[50], Some(0.25));
838
839 let shuffler = TwoFileShuffler::new(output_dir, num_partitions);
840 let stream = batches_to_stream(vec![batch1, batch2, batch3]);
841 let reader = shuffler.shuffle(stream).await.unwrap();
842
843 let loss = reader.total_loss().unwrap();
844 assert!((loss - 4.25).abs() < 1e-10, "expected 4.25, got {}", loss);
845 }
846
847 #[tokio::test]
848 async fn test_two_file_shuffler_single_batch() {
849 let dir = TempStrDir::default();
850 let output_dir = Path::from(dir.as_ref());
851 let num_partitions = 2;
852
853 let batch = make_batch(&[1, 0], &[100, 200], Some(3.0));
854
855 let shuffler = TwoFileShuffler::new(output_dir, num_partitions);
856 let stream = batches_to_stream(vec![batch]);
857 let reader = shuffler.shuffle(stream).await.unwrap();
858
859 assert_eq!(reader.partition_size(0).unwrap(), 1);
860 assert_eq!(reader.partition_size(1).unwrap(), 1);
861
862 let p0 = collect_partition(reader.as_ref(), 0).await.unwrap();
863 let vals: &Int32Array = p0.column_by_name("val").unwrap().as_primitive();
864 assert_eq!(vals.value(0), 200);
865
866 let p1 = collect_partition(reader.as_ref(), 1).await.unwrap();
867 let vals: &Int32Array = p1.column_by_name("val").unwrap().as_primitive();
868 assert_eq!(vals.value(0), 100);
869
870 assert!((reader.total_loss().unwrap() - 3.0).abs() < 1e-10);
871 }
872
873 #[tokio::test]
874 async fn test_two_file_shuffler_multiple_batches() {
875 let dir = TempStrDir::default();
876 let output_dir = Path::from(dir.as_ref());
877 let num_partitions = 3;
878
879 let batch1 = make_batch(&[0, 1, 2], &[10, 20, 30], Some(1.0));
883 let batch2 = make_batch(&[2, 0, 1], &[40, 50, 60], Some(2.0));
884 let batch3 = make_batch(&[1, 2, 0], &[70, 80, 90], Some(3.0));
885
886 let shuffler = TwoFileShuffler::new(output_dir, num_partitions)
887 .with_batch_size_bytes(16);
889 let stream = batches_to_stream(vec![batch1, batch2, batch3]);
890 let reader = shuffler.shuffle(stream).await.unwrap();
891
892 assert_eq!(reader.partition_size(0).unwrap(), 3);
894 let p0 = collect_partition(reader.as_ref(), 0).await.unwrap();
895 let vals: &Int32Array = p0.column_by_name("val").unwrap().as_primitive();
896 let mut v: Vec<i32> = vals.iter().map(|x| x.unwrap()).collect();
897 v.sort();
898 assert_eq!(v, vec![10, 50, 90]);
899
900 assert_eq!(reader.partition_size(1).unwrap(), 3);
902 let p1 = collect_partition(reader.as_ref(), 1).await.unwrap();
903 let vals: &Int32Array = p1.column_by_name("val").unwrap().as_primitive();
904 let mut v: Vec<i32> = vals.iter().map(|x| x.unwrap()).collect();
905 v.sort();
906 assert_eq!(v, vec![20, 60, 70]);
907
908 assert_eq!(reader.partition_size(2).unwrap(), 3);
910 let p2 = collect_partition(reader.as_ref(), 2).await.unwrap();
911 let vals: &Int32Array = p2.column_by_name("val").unwrap().as_primitive();
912 let mut v: Vec<i32> = vals.iter().map(|x| x.unwrap()).collect();
913 v.sort();
914 assert_eq!(v, vec![30, 40, 80]);
915
916 assert!((reader.total_loss().unwrap() - 6.0).abs() < 1e-10);
917 }
918}