1use std::fs::File;
18use std::io::{BufReader, Read, Seek, SeekFrom, Write};
19use std::path::{Path, PathBuf};
20use std::sync::Arc;
21
22use crate::batch::{Batch, OwnedPhysicalRow, PhysicalRow, RowSchema};
23use crate::physical::ExecResult;
24use tempfile::NamedTempFile;
25
26mod format;
27
28use format::{
29 append_batches, decode_batch, decode_physical_row_record, encode_physical_row_record,
30 encoded_batch_overhead_size, encoded_batch_size, encoded_physical_row_record_size,
31 open_spill_reader, read_bounded_spill_record, spill_error, RECORD_PREFIX_BYTES,
32};
33
34const SPILL_MAGIC: &[u8] = b"UQA-SPILL\x01\n";
35
36#[derive(Clone, Copy)]
38pub(crate) struct EncodedBatchSizer {
39 physical_width: usize,
40 bytes: usize,
41 origin_free_rows: usize,
42 has_lock_origins: bool,
43}
44
45impl EncodedBatchSizer {
46 pub(crate) fn new(schema: &RowSchema) -> ExecResult<Self> {
47 Ok(Self {
48 physical_width: schema.physical_width(),
49 bytes: encoded_batch_overhead_size(schema)?,
50 origin_free_rows: 0,
51 has_lock_origins: false,
52 })
53 }
54
55 pub(crate) fn append(&mut self, row: &PhysicalRow) -> ExecResult<()> {
56 let mut additional = encoded_physical_row_record_size(row, self.physical_width)?;
57 if row.lock_origins().is_empty() {
58 self.origin_free_rows = self.origin_free_rows.checked_add(1).ok_or_else(|| {
59 spill_error("incremental spill batch origin-free row count overflow")
60 })?;
61 if self.has_lock_origins {
62 additional = additional.checked_add(8).ok_or_else(|| {
63 spill_error("incremental spill batch lock-origin size overflow")
64 })?;
65 }
66 } else if !self.has_lock_origins {
67 let preceding_metadata = self.origin_free_rows.checked_mul(8).ok_or_else(|| {
68 spill_error("incremental spill batch lock-origin metadata overflow")
69 })?;
70 additional = additional
71 .checked_add(preceding_metadata)
72 .ok_or_else(|| spill_error("incremental spill batch lock-origin size overflow"))?;
73 self.has_lock_origins = true;
74 }
75 self.bytes = self
76 .bytes
77 .checked_add(additional)
78 .ok_or_else(|| spill_error("incremental spill batch size overflow"))?;
79 Ok(())
80 }
81
82 pub(crate) fn bytes(self) -> usize {
83 self.bytes
84 }
85}
86
87pub struct SpillBuffer {
94 schema: Option<RowSchema>,
95 batches: Vec<Batch>,
96 rows: usize,
97 in_memory_rows: usize,
98 in_memory_bytes: usize,
99 max_in_memory_record_bytes: usize,
100 budget_bytes: usize,
102 spill_directory: Option<PathBuf>,
103 spill_file: Option<NamedTempFile>,
104 spilled_batches: usize,
105 spilled_rows: usize,
106 spilled_bytes: usize,
107 max_spilled_record_bytes: usize,
108}
109
110impl SpillBuffer {
111 pub fn new(budget_bytes: usize) -> Self {
112 Self {
113 schema: None,
114 batches: Vec::new(),
115 rows: 0,
116 in_memory_rows: 0,
117 in_memory_bytes: 0,
118 max_in_memory_record_bytes: 0,
119 budget_bytes,
120 spill_directory: None,
121 spill_file: None,
122 spilled_batches: 0,
123 spilled_rows: 0,
124 spilled_bytes: 0,
125 max_spilled_record_bytes: 0,
126 }
127 }
128
129 pub fn new_in(budget_bytes: usize, directory: impl Into<PathBuf>) -> Self {
134 let mut buffer = Self::new(budget_bytes);
135 buffer.spill_directory = Some(directory.into());
136 buffer
137 }
138
139 pub fn unbounded() -> Self {
140 Self::new(usize::MAX)
141 }
142
143 pub fn push(&mut self, batch: Batch) -> ExecResult<bool> {
149 if let Some(schema) = self.schema.as_ref() {
150 if schema != &batch.schema {
151 return Err(spill_error(format!(
152 "spill buffer schema mismatch: expected {:?}, got {:?}",
153 schema.columns(),
154 batch.schema.columns()
155 )));
156 }
157 } else {
158 self.schema = Some(batch.schema.clone());
159 }
160 let batch_rows = batch.rows.len();
161 let next_rows = self
162 .rows
163 .checked_add(batch_rows)
164 .ok_or_else(|| spill_error("spill buffer row count overflow"))?;
165 let batch_bytes = match Self::encoded_size(&batch) {
166 Ok(bytes) => bytes,
167 Err(error) => {
168 self.retain_batch(batch, usize::MAX);
172 return Err(error);
173 }
174 };
175 let would_exceed = self
176 .in_memory_bytes
177 .checked_add(batch_bytes)
178 .is_none_or(|bytes| bytes > self.budget_bytes);
179
180 let mut spilled = false;
181 if would_exceed && !self.batches.is_empty() {
182 if let Err(error) = self.spill_pending() {
183 self.retain_batch(batch, batch_bytes);
184 return Err(error);
185 }
186 spilled = true;
187 }
188
189 let next_in_memory_rows = self
190 .in_memory_rows
191 .checked_add(batch_rows)
192 .ok_or_else(|| spill_error("spill buffer in-memory row count overflow"))?;
193 let next_in_memory_bytes = self
194 .in_memory_bytes
195 .checked_add(batch_bytes)
196 .ok_or_else(|| spill_error("spill buffer in-memory byte count overflow"))?;
197 self.rows = next_rows;
198 self.in_memory_rows = next_in_memory_rows;
199 self.in_memory_bytes = next_in_memory_bytes;
200 self.max_in_memory_record_bytes = self.max_in_memory_record_bytes.max(batch_bytes);
201 self.batches.push(batch);
202
203 if self.in_memory_bytes > self.budget_bytes {
206 self.spill_pending()?;
207 spilled = true;
208 }
209 Ok(spilled)
210 }
211
212 pub fn encoded_size(batch: &Batch) -> ExecResult<usize> {
215 encoded_batch_size(batch)
216 }
217
218 pub fn rows(&self) -> usize {
220 self.rows
221 }
222
223 pub fn in_memory_rows(&self) -> usize {
225 self.in_memory_rows
226 }
227
228 pub fn in_memory_bytes(&self) -> usize {
230 self.in_memory_bytes
231 }
232
233 pub fn budget_bytes(&self) -> usize {
234 self.budget_bytes
235 }
236
237 pub fn over_budget(&self) -> bool {
238 self.in_memory_bytes > self.budget_bytes
239 }
240
241 pub fn has_spilled(&self) -> bool {
242 self.spill_file.is_some()
243 }
244
245 pub fn spilled_rows(&self) -> usize {
246 self.spilled_rows
247 }
248
249 pub fn spilled_batches(&self) -> usize {
250 self.spilled_batches
251 }
252
253 pub fn spilled_bytes(&self) -> usize {
254 self.spilled_bytes
255 }
256
257 pub fn spill_path(&self) -> Option<&Path> {
262 self.spill_file.as_ref().map(NamedTempFile::path)
263 }
264
265 pub fn spill_if_over_budget(&mut self) -> ExecResult<bool> {
271 if !self.over_budget() || self.batches.is_empty() {
272 return Ok(false);
273 }
274 self.spill_pending()
275 }
276
277 pub fn spill_pending(&mut self) -> ExecResult<bool> {
282 if self.batches.is_empty() {
283 return Ok(false);
284 }
285
286 let next_spilled_batches = self
290 .spilled_batches
291 .checked_add(self.batches.len())
292 .ok_or_else(|| spill_error("spill batch count overflow"))?;
293 let next_spilled_rows = self
294 .spilled_rows
295 .checked_add(self.in_memory_rows)
296 .ok_or_else(|| spill_error("spill row count overflow"))?;
297 let next_spilled_bytes = self
298 .spilled_bytes
299 .checked_add(self.in_memory_bytes)
300 .ok_or_else(|| spill_error("spill byte count overflow"))?;
301 let next_max_spilled_record_bytes = self
302 .max_spilled_record_bytes
303 .max(self.max_in_memory_record_bytes);
304
305 if let Some(file) = self.spill_file.as_mut() {
306 append_batches(file.as_file_mut(), &self.batches)?;
307 } else {
308 let mut file = self.create_spill_file()?;
309 append_batches(file.as_file_mut(), &self.batches)?;
310 self.spill_file = Some(file);
311 }
312
313 self.spilled_batches = next_spilled_batches;
314 self.spilled_rows = next_spilled_rows;
315 self.spilled_bytes = next_spilled_bytes;
316 self.max_spilled_record_bytes = next_max_spilled_record_bytes;
317 self.batches.clear();
318 self.in_memory_rows = 0;
319 self.in_memory_bytes = 0;
320 self.max_in_memory_record_bytes = 0;
321 Ok(true)
322 }
323
324 pub fn reader(&self) -> ExecResult<SpillReader<'_>> {
329 let reader = self
330 .spill_file
331 .as_ref()
332 .map(open_spill_reader)
333 .transpose()?;
334 let disk_finished = reader.is_none();
335 Ok(SpillReader {
336 reader,
337 memory: self.batches.iter(),
338 disk_finished,
339 failed: false,
340 max_record_bytes: self.max_spilled_record_bytes,
341 expected_schema: self.schema.clone(),
342 })
343 }
344
345 pub fn read_rows(&self) -> ExecResult<SpillRows<SpillReader<'_>>> {
347 self.reader().map(SpillRows::new)
348 }
349
350 pub fn drain(&mut self) -> ExecResult<SpillDrain> {
356 let reader = self
357 .spill_file
358 .as_ref()
359 .map(open_spill_reader)
360 .transpose()?;
361 let spill_file = self.spill_file.take();
362 let memory = std::mem::take(&mut self.batches).into_iter();
363 let expected_schema = self.schema.take();
364
365 self.rows = 0;
366 self.in_memory_rows = 0;
367 self.in_memory_bytes = 0;
368 self.max_in_memory_record_bytes = 0;
369 self.spilled_batches = 0;
370 self.spilled_rows = 0;
371 self.spilled_bytes = 0;
372 let max_record_bytes = std::mem::take(&mut self.max_spilled_record_bytes);
373
374 let disk_finished = reader.is_none();
375 Ok(SpillDrain {
376 reader,
377 spill_file,
378 memory,
379 disk_finished,
380 failed: false,
381 max_record_bytes,
382 expected_schema,
383 })
384 }
385
386 pub fn drain_all(&mut self) -> ExecResult<Vec<Batch>> {
388 self.drain()?.collect()
389 }
390
391 pub fn drain_rows(&mut self) -> ExecResult<SpillRows<SpillDrain>> {
393 self.drain().map(SpillRows::new)
394 }
395
396 pub fn clear(&mut self) {
398 self.schema = None;
399 self.batches.clear();
400 self.spill_file = None;
401 self.rows = 0;
402 self.in_memory_rows = 0;
403 self.in_memory_bytes = 0;
404 self.max_in_memory_record_bytes = 0;
405 self.spilled_batches = 0;
406 self.spilled_rows = 0;
407 self.spilled_bytes = 0;
408 self.max_spilled_record_bytes = 0;
409 }
410
411 pub fn into_shared(mut self, schema: impl Into<RowSchema>) -> ExecResult<SharedSpill> {
417 let schema = schema.into();
418 if let Some(actual) = self.schema.as_ref() {
419 if actual != &schema {
420 return Err(spill_error(format!(
421 "shared spill schema mismatch: expected {:?}, got {:?}",
422 schema.columns(),
423 actual.columns()
424 )));
425 }
426 }
427 let rows = self.rows;
428 let storage = if self.spill_file.is_none() {
429 let batches = std::mem::take(&mut self.batches);
430 SharedSpillStorage::Memory(batches)
431 } else {
432 self.spill_pending()?;
433 SharedSpillStorage::Disk(
434 self.spill_file
435 .take()
436 .expect("spill file exists after flushing shared materialization"),
437 )
438 };
439 Ok(SharedSpill {
440 inner: Arc::new(SharedSpillInner {
441 storage,
442 schema,
443 rows,
444 max_record_bytes: self.max_spilled_record_bytes,
445 }),
446 })
447 }
448
449 fn create_spill_file(&self) -> ExecResult<NamedTempFile> {
450 let mut file = match &self.spill_directory {
451 Some(directory) => NamedTempFile::new_in(directory).map_err(|error| {
452 spill_error(format!(
453 "failed to create spill file in {}: {error}",
454 directory.display()
455 ))
456 })?,
457 None => NamedTempFile::new()
458 .map_err(|error| spill_error(format!("failed to create spill file: {error}")))?,
459 };
460 file.as_file_mut()
461 .write_all(SPILL_MAGIC)
462 .map_err(|error| spill_error(format!("failed to initialize spill file: {error}")))?;
463 file.as_file_mut()
464 .flush()
465 .map_err(|error| spill_error(format!("failed to flush spill header: {error}")))?;
466 Ok(file)
467 }
468
469 fn retain_batch(&mut self, batch: Batch, encoded_bytes: usize) {
470 self.rows = self.rows.saturating_add(batch.rows.len());
471 self.in_memory_rows = self.in_memory_rows.saturating_add(batch.rows.len());
472 self.in_memory_bytes = self.in_memory_bytes.saturating_add(encoded_bytes);
473 self.max_in_memory_record_bytes = self.max_in_memory_record_bytes.max(encoded_bytes);
474 self.batches.push(batch);
475 }
476}
477
478enum SharedSpillStorage {
479 Memory(Vec<Batch>),
480 Disk(NamedTempFile),
481}
482
483struct SharedSpillInner {
484 storage: SharedSpillStorage,
485 schema: RowSchema,
486 rows: usize,
487 max_record_bytes: usize,
488}
489
490#[derive(Clone)]
493pub struct SharedSpill {
494 inner: Arc<SharedSpillInner>,
495}
496
497impl SharedSpill {
498 pub fn schema(&self) -> &[String] {
499 self.inner.schema.columns()
500 }
501
502 pub fn row_schema(&self) -> &RowSchema {
503 &self.inner.schema
504 }
505
506 pub fn rows(&self) -> usize {
507 self.inner.rows
508 }
509
510 pub fn has_spilled(&self) -> bool {
512 matches!(self.inner.storage, SharedSpillStorage::Disk(_))
513 }
514
515 pub fn reader(&self) -> ExecResult<SharedSpillReader> {
516 let source = Arc::clone(&self.inner);
517 Self::reader_from_source(source)
518 }
519
520 pub fn into_reader(self) -> ExecResult<SharedSpillReader> {
526 match Arc::try_unwrap(self.inner) {
527 Ok(SharedSpillInner {
528 storage: SharedSpillStorage::Memory(batches),
529 schema,
530 max_record_bytes,
531 ..
532 }) => Ok(SharedSpillReader {
533 reader: SharedSpillReaderSource::OwnedMemory(batches.into_iter()),
534 source: None,
535 failed: false,
536 max_record_bytes,
537 expected_schema: Some(schema),
538 }),
539 Ok(inner) => Self::reader_from_source(Arc::new(inner)),
540 Err(source) => Self::reader_from_source(source),
541 }
542 }
543
544 fn reader_from_source(source: Arc<SharedSpillInner>) -> ExecResult<SharedSpillReader> {
545 let reader = match &source.storage {
546 SharedSpillStorage::Memory(_) => SharedSpillReaderSource::Memory { next_batch: 0 },
547 SharedSpillStorage::Disk(file) => {
548 SharedSpillReaderSource::Disk(open_spill_reader(file)?)
549 }
550 };
551 let max_record_bytes = source.max_record_bytes;
552 let expected_schema = Some(source.schema.clone());
553 Ok(SharedSpillReader {
554 reader,
555 source: Some(source),
556 failed: false,
557 max_record_bytes,
558 expected_schema,
559 })
560 }
561
562 pub fn read_rows(&self) -> ExecResult<SpillRows<SharedSpillReader>> {
565 self.reader().map(SpillRows::new)
566 }
567}
568
569enum SharedSpillReaderSource {
570 Memory { next_batch: usize },
571 OwnedMemory(std::vec::IntoIter<Batch>),
572 Disk(BufReader<File>),
573}
574
575fn validate_decoded_schema(batch: Batch, expected_schema: Option<&RowSchema>) -> ExecResult<Batch> {
576 if expected_schema.is_none_or(|expected| expected == &batch.schema) {
577 return Ok(batch);
578 }
579 let expected = expected_schema.expect("schema presence checked above");
580 Err(spill_error(format!(
581 "spill batch schema mismatch: expected {:?}, got {:?}",
582 expected.columns(),
583 batch.schema.columns()
584 )))
585}
586
587pub struct SharedSpillReader {
590 reader: SharedSpillReaderSource,
591 source: Option<Arc<SharedSpillInner>>,
592 failed: bool,
593 max_record_bytes: usize,
594 expected_schema: Option<RowSchema>,
595}
596
597impl Iterator for SharedSpillReader {
598 type Item = ExecResult<Batch>;
599
600 fn next(&mut self) -> Option<Self::Item> {
601 if self.failed {
602 return None;
603 }
604 match &mut self.reader {
605 SharedSpillReaderSource::Memory { next_batch } => {
606 let source = self
607 .source
608 .as_ref()
609 .expect("shared memory reader retains its source");
610 let SharedSpillStorage::Memory(batches) = &source.storage else {
611 unreachable!("shared materialization reader/storage mismatch")
612 };
613 let batch = batches.get(*next_batch)?.clone();
614 *next_batch += 1;
615 Some(validate_decoded_schema(
616 batch,
617 self.expected_schema.as_ref(),
618 ))
619 }
620 SharedSpillReaderSource::OwnedMemory(batches) => batches
621 .next()
622 .map(|batch| validate_decoded_schema(batch, self.expected_schema.as_ref())),
623 SharedSpillReaderSource::Disk(reader) => {
624 match read_bounded_spill_record(reader, self.max_record_bytes, "shared spill batch")
625 {
626 Ok(None) => None,
627 Ok(Some(record)) => {
628 let decoded = decode_batch(&record).and_then(|batch| {
629 validate_decoded_schema(batch, self.expected_schema.as_ref())
630 });
631 if decoded.is_err() {
632 self.failed = true;
633 }
634 Some(decoded)
635 }
636 Err(error) => {
637 self.failed = true;
638 Some(Err(spill_error(format!(
639 "failed to read shared spill batch: {error}"
640 ))))
641 }
642 }
643 }
644 }
645 }
646}
647
648pub struct SpillDrain {
650 reader: Option<BufReader<File>>,
651 spill_file: Option<NamedTempFile>,
654 memory: std::vec::IntoIter<Batch>,
655 disk_finished: bool,
656 failed: bool,
657 max_record_bytes: usize,
658 expected_schema: Option<RowSchema>,
659}
660
661impl Iterator for SpillDrain {
662 type Item = ExecResult<Batch>;
663
664 fn next(&mut self) -> Option<Self::Item> {
665 if self.failed {
666 return None;
667 }
668
669 if !self.disk_finished {
670 let Some(reader) = self.reader.as_mut() else {
671 self.failed = true;
672 self.disk_finished = true;
673 return Some(Err(spill_error(
674 "spill drain entered disk phase without a reader",
675 )));
676 };
677 match read_bounded_spill_record(reader, self.max_record_bytes, "spill batch") {
678 Ok(None) => {
679 self.disk_finished = true;
680 self.reader = None;
681 self.spill_file = None;
682 }
683 Ok(Some(record)) => {
684 let decoded = decode_batch(&record).and_then(|batch| {
685 validate_decoded_schema(batch, self.expected_schema.as_ref())
686 });
687 if decoded.is_err() {
688 self.failed = true;
689 }
690 return Some(decoded);
691 }
692 Err(error) => {
693 self.failed = true;
694 return Some(Err(spill_error(format!(
695 "failed to read spill batch: {error}"
696 ))));
697 }
698 }
699 }
700
701 self.memory
702 .next()
703 .map(|batch| validate_decoded_schema(batch, self.expected_schema.as_ref()))
704 }
705
706 fn size_hint(&self) -> (usize, Option<usize>) {
707 let lower = if self.disk_finished {
708 self.memory.len()
709 } else {
710 0
711 };
712 (lower, None)
713 }
714}
715
716pub struct SpillReader<'a> {
718 reader: Option<BufReader<File>>,
719 memory: std::slice::Iter<'a, Batch>,
720 disk_finished: bool,
721 failed: bool,
722 max_record_bytes: usize,
723 expected_schema: Option<RowSchema>,
724}
725
726impl Iterator for SpillReader<'_> {
727 type Item = ExecResult<Batch>;
728
729 fn next(&mut self) -> Option<Self::Item> {
730 if self.failed {
731 return None;
732 }
733
734 if !self.disk_finished {
735 let Some(reader) = self.reader.as_mut() else {
736 self.failed = true;
737 self.disk_finished = true;
738 return Some(Err(spill_error(
739 "spill reader entered disk phase without a file reader",
740 )));
741 };
742 match read_bounded_spill_record(reader, self.max_record_bytes, "spill batch") {
743 Ok(None) => {
744 self.disk_finished = true;
745 self.reader = None;
746 }
747 Ok(Some(record)) => {
748 let decoded = decode_batch(&record).and_then(|batch| {
749 validate_decoded_schema(batch, self.expected_schema.as_ref())
750 });
751 if decoded.is_err() {
752 self.failed = true;
753 }
754 return Some(decoded);
755 }
756 Err(error) => {
757 self.failed = true;
758 return Some(Err(spill_error(format!(
759 "failed to read spill batch: {error}"
760 ))));
761 }
762 }
763 }
764
765 self.memory
766 .next()
767 .cloned()
768 .map(|batch| validate_decoded_schema(batch, self.expected_schema.as_ref()))
769 }
770
771 fn size_hint(&self) -> (usize, Option<usize>) {
772 let lower = if self.disk_finished {
773 self.memory.len()
774 } else {
775 0
776 };
777 (lower, None)
778 }
779}
780
781pub struct SpillRows<I> {
783 batches: I,
784 current_schema: Option<RowSchema>,
785 current: std::vec::IntoIter<PhysicalRow>,
786}
787
788pub struct IndexedSpill {
797 schema: RowSchema,
798 data: NamedTempFile,
799 offsets: NamedTempFile,
800 rows: u64,
801 encoded_bytes: u64,
802}
803
804impl IndexedSpill {
805 pub fn new(input_schema: RowSchema) -> ExecResult<Self> {
806 Ok(Self {
807 schema: input_schema,
808 data: NamedTempFile::new().map_err(|error| {
809 spill_error(format!("failed to create indexed spill data: {error}"))
810 })?,
811 offsets: NamedTempFile::new().map_err(|error| {
812 spill_error(format!("failed to create indexed spill offsets: {error}"))
813 })?,
814 rows: 0,
815 encoded_bytes: 0,
816 })
817 }
818
819 pub fn len(&self) -> u64 {
820 self.rows
821 }
822
823 pub fn is_empty(&self) -> bool {
824 self.rows == 0
825 }
826
827 pub fn encoded_bytes(&self) -> u64 {
828 self.encoded_bytes
829 }
830
831 pub fn row_schema(&self) -> &RowSchema {
832 &self.schema
833 }
834
835 pub(crate) fn encoded_row_size(schema: &RowSchema, row: &PhysicalRow) -> ExecResult<usize> {
836 encoded_physical_row_record_size(row, schema.physical_width())?
837 .checked_add(RECORD_PREFIX_BYTES)
838 .ok_or_else(|| spill_error("indexed spill row size overflow"))
839 }
840
841 pub fn push(&mut self, row: &PhysicalRow) -> ExecResult<()> {
844 let payload = encode_physical_row_record(row, self.schema.physical_width())?;
845 let length = u64::try_from(payload.len())
846 .map_err(|_| spill_error("indexed spill row is too large"))?;
847 let next_rows = self
852 .rows
853 .checked_add(1)
854 .ok_or_else(|| spill_error("indexed spill row count overflow"))?;
855 let record_bytes = length
856 .checked_add(8)
857 .ok_or_else(|| spill_error("indexed spill row length overflow"))?;
858 let next_encoded_bytes = self
859 .encoded_bytes
860 .checked_add(record_bytes)
861 .ok_or_else(|| spill_error("indexed spill byte count overflow"))?;
862 let data_length = self
863 .data
864 .as_file_mut()
865 .seek(SeekFrom::End(0))
866 .map_err(|error| spill_error(format!("failed to seek indexed spill data: {error}")))?;
867 let offsets_length =
868 self.offsets
869 .as_file_mut()
870 .seek(SeekFrom::End(0))
871 .map_err(|error| {
872 spill_error(format!("failed to seek indexed spill offsets: {error}"))
873 })?;
874
875 let write_result = (|| -> std::io::Result<()> {
876 self.data.as_file_mut().write_all(&length.to_le_bytes())?;
877 self.data.as_file_mut().write_all(&payload)?;
878 self.data.as_file_mut().flush()?;
879 self.offsets
880 .as_file_mut()
881 .write_all(&data_length.to_le_bytes())?;
882 self.offsets.as_file_mut().flush()
883 })();
884 if let Err(error) = write_result {
885 let data_rollback = self.data.as_file_mut().set_len(data_length);
886 let offsets_rollback = self.offsets.as_file_mut().set_len(offsets_length);
887 let rollback_error = match (data_rollback, offsets_rollback) {
888 (Ok(()), Ok(())) => None,
889 (Err(data), Ok(())) => Some(format!("data rollback failed: {data}")),
890 (Ok(()), Err(offsets)) => Some(format!("offset rollback failed: {offsets}")),
891 (Err(data), Err(offsets)) => Some(format!(
892 "data rollback failed: {data}; offset rollback failed: {offsets}"
893 )),
894 };
895 if let Some(rollback) = rollback_error {
896 return Err(spill_error(format!(
897 "failed to append indexed spill row: {error}; {rollback}"
898 )));
899 }
900 return Err(spill_error(format!(
901 "failed to append indexed spill row: {error}"
902 )));
903 }
904
905 self.rows = next_rows;
906 self.encoded_bytes = next_encoded_bytes;
907 Ok(())
908 }
909
910 pub fn get(&mut self, index: u64) -> ExecResult<PhysicalRow> {
912 if index >= self.rows {
913 return Err(spill_error(format!(
914 "indexed spill row {index} is outside 0..{}",
915 self.rows
916 )));
917 }
918 let expected_offsets_length = self
919 .rows
920 .checked_mul(8)
921 .ok_or_else(|| spill_error("indexed spill offsets length overflow"))?;
922 let actual_offsets_length = self
923 .offsets
924 .as_file()
925 .metadata()
926 .map_err(|error| {
927 spill_error(format!("failed to inspect indexed spill offsets: {error}"))
928 })?
929 .len();
930 if actual_offsets_length != expected_offsets_length {
931 return Err(spill_error(format!(
932 "indexed spill offsets length {actual_offsets_length} does not match expected {expected_offsets_length}"
933 )));
934 }
935 let data_length = self
936 .data
937 .as_file()
938 .metadata()
939 .map_err(|error| spill_error(format!("failed to inspect indexed spill data: {error}")))?
940 .len();
941 let offset_position = index
942 .checked_mul(8)
943 .ok_or_else(|| spill_error("indexed spill offset overflow"))?;
944 let offset = read_indexed_offset(self.offsets.as_file_mut(), offset_position)?;
945 let record_end = if index
946 .checked_add(1)
947 .ok_or_else(|| spill_error("indexed spill row index overflow"))?
948 < self.rows
949 {
950 read_indexed_offset(
951 self.offsets.as_file_mut(),
952 offset_position
953 .checked_add(8)
954 .ok_or_else(|| spill_error("indexed spill next offset overflow"))?,
955 )?
956 } else {
957 data_length
958 };
959 let payload_start = offset
960 .checked_add(8)
961 .ok_or_else(|| spill_error("indexed spill payload offset overflow"))?;
962 if payload_start > record_end || record_end > data_length {
963 return Err(spill_error(format!(
964 "indexed spill record bounds {offset}..{record_end} are outside data length {data_length}"
965 )));
966 }
967 self.data
968 .as_file_mut()
969 .seek(SeekFrom::Start(offset))
970 .map_err(|error| spill_error(format!("failed to seek indexed spill row: {error}")))?;
971 let mut length = [0_u8; 8];
972 self.data
973 .as_file_mut()
974 .read_exact(&mut length)
975 .map_err(|error| {
976 spill_error(format!("failed to read indexed spill length: {error}"))
977 })?;
978 let declared_length = u64::from_le_bytes(length);
979 let available_length = record_end - payload_start;
980 if declared_length != available_length {
981 return Err(spill_error(format!(
982 "indexed spill row length {declared_length} does not match record payload {available_length}"
983 )));
984 }
985 let length = usize::try_from(declared_length)
986 .map_err(|_| spill_error("indexed spill row length is outside address space"))?;
987 let mut payload = Vec::new();
988 payload.try_reserve_exact(length).map_err(|error| {
989 spill_error(format!(
990 "unable to allocate indexed spill row payload of {length} bytes: {error}"
991 ))
992 })?;
993 payload.resize(length, 0);
994 self.data
995 .as_file_mut()
996 .read_exact(&mut payload)
997 .map_err(|error| spill_error(format!("failed to read indexed spill row: {error}")))?;
998 decode_physical_row_record(&payload, self.schema.physical_width())
999 }
1000}
1001
1002fn read_indexed_offset(file: &mut File, position: u64) -> ExecResult<u64> {
1003 file.seek(SeekFrom::Start(position))
1004 .map_err(|error| spill_error(format!("failed to seek indexed spill offset: {error}")))?;
1005 let mut encoded = [0_u8; 8];
1006 file.read_exact(&mut encoded)
1007 .map_err(|error| spill_error(format!("failed to read indexed spill offset: {error}")))?;
1008 Ok(u64::from_le_bytes(encoded))
1009}
1010
1011impl<I> SpillRows<I> {
1012 fn new(batches: I) -> Self {
1013 Self {
1014 batches,
1015 current_schema: None,
1016 current: Vec::new().into_iter(),
1017 }
1018 }
1019}
1020
1021impl<I> Iterator for SpillRows<I>
1022where
1023 I: Iterator<Item = ExecResult<Batch>>,
1024{
1025 type Item = ExecResult<OwnedPhysicalRow>;
1026
1027 fn next(&mut self) -> Option<Self::Item> {
1028 loop {
1029 if let Some(row) = self.current.next() {
1030 let schema = self
1031 .current_schema
1032 .as_ref()
1033 .expect("spill row iterator retains the current batch schema")
1034 .clone();
1035 return Some(Ok(OwnedPhysicalRow::new(schema, row)));
1036 }
1037 match self.batches.next()? {
1038 Ok(batch) => {
1039 self.current_schema = Some(batch.schema);
1040 self.current = batch.rows.into_iter();
1041 }
1042 Err(error) => return Some(Err(error)),
1043 }
1044 }
1045 }
1046}
1047
1048#[cfg(test)]
1049mod tests;