1use std::{
2 fs::File,
3 path::{Path, PathBuf},
4 sync::Arc,
5};
6
7use arrow::{
8 array::{Array, ArrayRef, Float32Array, Float64Array},
9 datatypes::{DataType, Field, Schema as ArrowSchema, SchemaRef},
10 record_batch::RecordBatch,
11};
12use laddu_physics::vectors::RealVec4;
13use parquet::{
14 arrow::{ArrowWriter, ProjectionMask, arrow_reader::ParquetRecordBatchReaderBuilder},
15 file::properties::WriterProperties,
16 schema::types::SchemaDescriptor,
17};
18
19use crate::{
20 LadduDataError, LadduDataResult, Name,
21 data::EventBatch,
22 io::{
23 DataFragment, EventSink, EventSource, FragmentedSource, OutputMode, OutputPath, ReadPlan,
24 SliceBatchIter, SourceCapabilities, WritePlan, fragmented_batches,
25 },
26 schema::{
27 ColumnInfo, ColumnType, Precision, Schema, SchemaColumnNames, SchemaInferenceOptions,
28 SchemaWriteOptions, WriteWeightColumn,
29 },
30};
31
32#[derive(Clone, Debug)]
34pub struct ParquetSource {
35 files: Arc<[Arc<PathBuf>]>,
36 schema: Arc<Schema>,
37 options: ParquetReadOptions,
38}
39
40#[derive(Clone, Debug)]
42pub struct ParquetReadOptions {
43 pub infer_schema: bool,
45 pub validate_all_files: bool,
47 pub null_handling: NullHandling,
49 pub sort_glob: bool,
51 pub schema_inference: SchemaInferenceOptions,
53}
54
55impl Default for ParquetReadOptions {
56 fn default() -> Self {
57 Self {
58 infer_schema: true,
59 validate_all_files: true,
60 null_handling: NullHandling::Error,
61 sort_glob: true,
62 schema_inference: SchemaInferenceOptions::default(),
63 }
64 }
65}
66
67#[derive(Clone, Copy, Debug)]
69pub enum NullHandling {
70 Error,
72 NaN,
74}
75
76#[derive(Clone, Debug)]
78pub struct ParquetFragmentKey {
79 pub file: Arc<PathBuf>,
81 pub row_group: usize,
83}
84
85impl ParquetSource {
86 pub fn open(pattern: impl AsRef<str>) -> LadduDataResult<Self> {
93 Self::builder(pattern).build()
94 }
95
96 pub fn builder(pattern: impl AsRef<str>) -> ParquetSourceBuilder {
98 ParquetSourceBuilder {
99 pattern: pattern.as_ref().to_owned(),
100 schema: None,
101 options: ParquetReadOptions::default(),
102 }
103 }
104
105 pub fn files(&self) -> &[Arc<PathBuf>] {
107 &self.files
108 }
109}
110
111pub struct ParquetSourceBuilder {
113 pattern: String,
114 schema: Option<Arc<Schema>>,
115 options: ParquetReadOptions,
116}
117
118impl ParquetSourceBuilder {
119 pub fn schema(mut self, schema: Arc<Schema>) -> Self {
121 self.schema = Some(schema);
122 self.options.infer_schema = false;
123 self
124 }
125
126 pub fn infer_schema(mut self, value: bool) -> Self {
128 self.options.infer_schema = value;
129 self
130 }
131
132 pub fn require_weight(mut self, value: bool) -> Self {
134 self.options.schema_inference.require_weight = value;
135 self
136 }
137
138 pub fn validate_all_files(mut self, value: bool) -> Self {
140 self.options.validate_all_files = value;
141 self
142 }
143
144 pub fn nulls_as_nan(mut self) -> Self {
146 self.options.null_handling = NullHandling::NaN;
147 self
148 }
149
150 pub fn error_on_nulls(mut self) -> Self {
152 self.options.null_handling = NullHandling::Error;
153 self
154 }
155
156 pub fn sort_glob(mut self, value: bool) -> Self {
158 self.options.sort_glob = value;
159 self
160 }
161
162 pub fn schema_inference(mut self, options: SchemaInferenceOptions) -> Self {
164 self.options.schema_inference = options;
165 self
166 }
167
168 pub fn build(self) -> LadduDataResult<ParquetSource> {
175 let mut files: Vec<PathBuf> = glob::glob(&self.pattern)
176 .map_err(|e| LadduDataError::Source(e.to_string()))?
177 .collect::<std::result::Result<_, _>>()
178 .map_err(|e| LadduDataError::Source(e.to_string()))?;
179
180 if self.options.sort_glob {
181 files.sort();
182 }
183
184 if files.is_empty() {
185 return Err(LadduDataError::Source(
186 "no parquet files matched glob".into(),
187 ));
188 }
189
190 let files: Arc<[Arc<PathBuf>]> = files.into_iter().map(Arc::new).collect();
191
192 let schema = match self.schema {
193 Some(schema) => schema,
194 None if self.options.infer_schema => Arc::new({
195 let path: &Path = files[0].as_ref();
196 let options: &ParquetReadOptions = &self.options;
197 let arrow_schema = parquet_arrow_schema(path)?;
198 Schema::infer_from_columns(arrow_columns(&arrow_schema), &options.schema_inference)
199 }?),
200 None => return Err(LadduDataError::InvalidArgument("schema required")),
201 };
202
203 if self.options.validate_all_files {
204 for file in files.iter() {
205 {
206 let path: &Path = file.as_ref();
207 let schema: &Schema = &schema;
208 let options: &ParquetReadOptions = &self.options;
209 let arrow_schema = parquet_arrow_schema(path)?;
210 schema.validate_required_columns(
211 arrow_columns(&arrow_schema),
212 &options.schema_inference,
213 )
214 }?;
215 }
216 }
217
218 Ok(ParquetSource {
219 files,
220 schema,
221 options: self.options,
222 })
223 }
224}
225
226impl EventSource for ParquetSource {
227 fn schema(&self) -> LadduDataResult<Arc<Schema>> {
228 Ok(Arc::clone(&self.schema))
229 }
230
231 fn capabilities(&self) -> SourceCapabilities {
232 SourceCapabilities {
233 exact_len: true,
234 exact_weighted_total: false,
235 random_access: false,
236 deterministic_partitioning: true,
237 predicate_pushdown: false,
238 projection_pushdown: true,
239 streaming: true,
240 }
241 }
242
243 fn num_events(&self) -> LadduDataResult<Option<u64>> {
244 Ok(Some(self.fragments()?.iter().map(|f| f.rows).sum()))
245 }
246
247 fn batches(
248 &self,
249 plan: ReadPlan,
250 ) -> LadduDataResult<Box<dyn Iterator<Item = LadduDataResult<EventBatch>> + Send>> {
251 fragmented_batches(Arc::new(self.clone()), plan)
252 }
253}
254
255impl FragmentedSource for ParquetSource {
256 type Key = ParquetFragmentKey;
257
258 fn fragments(&self) -> LadduDataResult<Vec<DataFragment<Self::Key>>> {
259 let mut fragments = Vec::new();
260 let mut global_start = 0_u64;
261
262 for path in self.files.iter() {
263 let file =
264 File::open(path.as_ref()).map_err(|e| LadduDataError::Source(e.to_string()))?;
265
266 let builder = ParquetRecordBatchReaderBuilder::try_new(file)
267 .map_err(|e| LadduDataError::Source(e.to_string()))?;
268
269 let metadata = builder.metadata();
270
271 for row_group in 0..metadata.num_row_groups() {
272 let rows = metadata.row_group(row_group).num_rows() as u64;
273
274 fragments.push(DataFragment {
275 key: ParquetFragmentKey {
276 file: Arc::clone(path),
277 row_group,
278 },
279 global_start,
280 rows,
281 });
282
283 global_start += rows;
284 }
285 }
286
287 Ok(fragments)
288 }
289
290 fn read_fragment_range(
291 &self,
292 key: &Self::Key,
293 local_start: usize,
294 local_len: usize,
295 chunk_size: Option<usize>,
296 ) -> LadduDataResult<Box<dyn Iterator<Item = LadduDataResult<EventBatch>> + Send>> {
297 open_parquet_fragment_reader(
298 Arc::clone(&self.schema),
299 self.options.clone(),
300 key.clone(),
301 local_start,
302 local_len,
303 chunk_size,
304 )
305 }
306}
307
308fn open_parquet_fragment_reader(
309 schema: Arc<Schema>,
310 options: ParquetReadOptions,
311 key: ParquetFragmentKey,
312 local_start: usize,
313 local_len: usize,
314 chunk_size: Option<usize>,
315) -> LadduDataResult<Box<dyn Iterator<Item = LadduDataResult<EventBatch>> + Send>> {
316 let file = File::open(key.file.as_ref()).map_err(|e| LadduDataError::Source(e.to_string()))?;
317
318 let mut builder = ParquetRecordBatchReaderBuilder::try_new(file)
319 .map_err(|e| LadduDataError::Source(e.to_string()))?
320 .with_row_groups(vec![key.row_group]);
321
322 let projection = parquet_projection_for_schema(
323 builder.parquet_schema(),
324 builder.schema().as_ref(),
325 &schema,
326 &options.schema_inference.column_names,
327 )?;
328 builder = builder.with_projection(projection);
329
330 if matches!(chunk_size, Some(0)) {
331 return Err(LadduDataError::InvalidArgument(
332 "chunk_size must be nonzero",
333 ));
334 }
335 let end = local_start
336 .checked_add(local_len)
337 .ok_or(LadduDataError::InvalidArgument(
338 "slice range overflows usize",
339 ))?;
340 let batch_size = chunk_size.unwrap_or(end.max(1));
341 builder = builder.with_batch_size(batch_size);
342
343 let reader = builder
344 .build()
345 .map_err(|e| LadduDataError::Source(e.to_string()))?;
346
347 let batches = reader.map(move |rb| {
348 let rb = rb.map_err(|e| LadduDataError::Source(e.to_string()))?;
349 record_batch_to_event_batch(rb, Arc::clone(&schema), &options)
350 });
351
352 Ok(Box::new(SliceBatchIter::new(
353 batches,
354 local_start,
355 local_len,
356 )?))
357}
358
359fn parquet_projection_for_schema(
360 parquet_schema: &SchemaDescriptor,
361 arrow_schema: &ArrowSchema,
362 schema: &Schema,
363 column_names: &SchemaColumnNames,
364) -> LadduDataResult<ProjectionMask> {
365 let mut indices = Vec::new();
366
367 for name in schema.physical_columns(column_names) {
368 let index = arrow_schema
369 .index_of(name.as_ref())
370 .map_err(|_| LadduDataError::MissingColumn(name))?;
371
372 indices.push(index);
373 }
374
375 indices.sort_unstable();
376 indices.dedup();
377
378 Ok(ProjectionMask::roots(parquet_schema, indices))
379}
380
381fn parquet_arrow_schema(path: &Path) -> LadduDataResult<SchemaRef> {
382 let file = File::open(path).map_err(|e| LadduDataError::Source(e.to_string()))?;
383
384 let builder = ParquetRecordBatchReaderBuilder::try_new(file)
385 .map_err(|e| LadduDataError::Source(e.to_string()))?;
386
387 Ok(builder.schema().clone())
388}
389
390fn arrow_columns(schema: &ArrowSchema) -> impl Iterator<Item = ColumnInfo<'_>> {
391 schema.fields().iter().map(|field| ColumnInfo {
392 name: field.name().as_str(),
393 dtype: arrow_column_type(field.data_type()),
394 })
395}
396
397fn arrow_column_type(data_type: &DataType) -> ColumnType {
398 match data_type {
399 DataType::Float64 => ColumnType::F64,
400 DataType::Float32 => ColumnType::F32,
401 _ => ColumnType::Other,
402 }
403}
404
405fn record_batch_to_event_batch(
406 rb: RecordBatch,
407 schema: Arc<Schema>,
408 options: &ParquetReadOptions,
409) -> LadduDataResult<EventBatch> {
410 let arrow_schema = rb.schema();
411
412 let mut p4s = Vec::with_capacity(schema.n_p4s());
413 let mut scalars = Vec::with_capacity(schema.n_scalars());
414
415 for name in schema.p4s() {
416 let [e_name, px_name, py_name, pz_name] = options
417 .schema_inference
418 .column_names
419 .p4_suffixes
420 .physical_p4_names(name);
421
422 let e = read_f64_column(&rb, &arrow_schema, &e_name, options)?;
423 let px = read_f64_column(&rb, &arrow_schema, &px_name, options)?;
424 let py = read_f64_column(&rb, &arrow_schema, &py_name, options)?;
425 let pz = read_f64_column(&rb, &arrow_schema, &pz_name, options)?;
426
427 let col: Arc<[RealVec4]> = (0..rb.num_rows())
428 .map(|i| RealVec4 {
429 e: e[i],
430 px: px[i],
431 py: py[i],
432 pz: pz[i],
433 })
434 .collect();
435
436 p4s.push(col);
437 }
438
439 for name in schema.scalars() {
440 scalars.push(read_f64_column(&rb, &arrow_schema, name, options)?.into());
441 }
442
443 let weights = if schema.has_weight() {
444 Some(
445 read_f64_column(
446 &rb,
447 &arrow_schema,
448 &options.schema_inference.column_names.weight_column,
449 options,
450 )?
451 .into(),
452 )
453 } else {
454 None
455 };
456
457 EventBatch::new(schema, p4s, scalars, weights)
458}
459
460fn read_f64_column(
461 rb: &RecordBatch,
462 arrow_schema: &SchemaRef,
463 name: &str,
464 options: &ParquetReadOptions,
465) -> LadduDataResult<Vec<f64>> {
466 let index = arrow_schema
467 .index_of(name)
468 .map_err(|_| LadduDataError::MissingColumn(Name::from(name)))?;
469
470 let array = rb.column(index);
471
472 match array.data_type() {
473 DataType::Float64 => {
474 let array = array
475 .as_any()
476 .downcast_ref::<Float64Array>()
477 .ok_or_else(|| LadduDataError::Source(format!("failed to read {name} as f64")))?;
478
479 collect_f64(array, name, options.null_handling)
480 }
481
482 DataType::Float32 => {
483 let array = array
484 .as_any()
485 .downcast_ref::<Float32Array>()
486 .ok_or_else(|| LadduDataError::Source(format!("failed to read {name} as f32")))?;
487
488 collect_f32(array, name, options.null_handling)
489 }
490
491 other => Err(LadduDataError::Source(format!(
492 "column {name} has unsupported type {other:?}"
493 ))),
494 }
495}
496
497fn collect_f64(array: &Float64Array, name: &str, nulls: NullHandling) -> LadduDataResult<Vec<f64>> {
498 let mut out = Vec::with_capacity(array.len());
499
500 for i in 0..array.len() {
501 if array.is_null(i) {
502 match nulls {
503 NullHandling::Error => {
504 return Err(LadduDataError::Source(format!("null in column {name}")));
505 }
506 NullHandling::NaN => out.push(f64::NAN),
507 }
508 } else {
509 out.push(array.value(i));
510 }
511 }
512
513 Ok(out)
514}
515
516fn collect_f32(array: &Float32Array, name: &str, nulls: NullHandling) -> LadduDataResult<Vec<f64>> {
517 let mut out = Vec::with_capacity(array.len());
518
519 for i in 0..array.len() {
520 if array.is_null(i) {
521 match nulls {
522 NullHandling::Error => {
523 return Err(LadduDataError::Source(format!("null in column {name}")));
524 }
525 NullHandling::NaN => out.push(f64::NAN),
526 }
527 } else {
528 out.push(array.value(i) as f64);
529 }
530 }
531
532 Ok(out)
533}
534
535pub struct ParquetSink {
537 output: OutputPath,
538 writer: Option<ArrowWriter<File>>,
539 arrow_schema: Option<SchemaRef>,
540 event_schema: Option<Arc<Schema>>,
541 options: ParquetWriteOptions,
542 resolved_path: Option<PathBuf>,
543}
544
545#[derive(Clone, Debug, Default)]
547pub struct ParquetWriteOptions {
548 pub writer_properties: Option<WriterProperties>,
550 pub schema_write: SchemaWriteOptions,
552}
553
554impl ParquetSink {
555 pub fn create(path: impl Into<PathBuf>) -> Self {
557 Self::builder(path).build()
558 }
559
560 pub fn builder(path: impl Into<PathBuf>) -> ParquetSinkBuilder {
562 ParquetSinkBuilder {
563 output: OutputPath::new(path),
564 options: ParquetWriteOptions::default(),
565 }
566 }
567
568 pub fn resolved_path(&self) -> Option<&Path> {
570 self.resolved_path.as_deref()
571 }
572}
573
574pub struct ParquetSinkBuilder {
576 output: OutputPath,
577 options: ParquetWriteOptions,
578}
579
580impl ParquetSinkBuilder {
581 pub fn output_mode(mut self, mode: OutputMode) -> Self {
583 self.output = self.output.with_mode(mode);
584 self
585 }
586
587 pub fn single_file(self) -> Self {
589 self.output_mode(OutputMode::SingleFile)
590 }
591
592 pub fn per_rank_files(self) -> Self {
594 self.output_mode(OutputMode::PerRankFiles)
595 }
596
597 pub fn auto_output(self) -> Self {
599 self.output_mode(OutputMode::Auto)
600 }
601
602 pub fn schema_write(mut self, options: SchemaWriteOptions) -> Self {
604 self.options.schema_write = options;
605 self
606 }
607
608 pub fn column_names(mut self, column_names: SchemaColumnNames) -> Self {
610 self.options.schema_write.column_names = column_names;
611 self
612 }
613
614 pub fn precision(mut self, precision: Precision) -> Self {
616 self.options.schema_write.precision = precision;
617 self
618 }
619
620 pub fn writer_properties(mut self, props: WriterProperties) -> Self {
622 self.options.writer_properties = Some(props);
623 self
624 }
625
626 pub fn write_weight_column(mut self, value: WriteWeightColumn) -> Self {
628 self.options.schema_write.write_weight_column = value;
629 self
630 }
631
632 pub fn build(self) -> ParquetSink {
634 ParquetSink {
635 output: self.output,
636 writer: None,
637 arrow_schema: None,
638 event_schema: None,
639 options: self.options,
640 resolved_path: None,
641 }
642 }
643}
644
645impl EventSink for ParquetSink {
646 fn begin(&mut self, schema: Arc<Schema>, plan: WritePlan) -> LadduDataResult<()> {
647 let path = self.output.resolve(plan, "parquet")?;
648 OutputPath::create_parent_dirs(&path)?;
649
650 let arrow_schema = Arc::new(arrow_schema_from_event_schema(
651 &schema,
652 self.options.schema_write.write_weight_column,
653 &self.options.schema_write,
654 ));
655
656 let file = File::create(&path).map_err(|e| LadduDataError::Sink(e.to_string()))?;
657
658 let writer = ArrowWriter::try_new(
659 file,
660 Arc::clone(&arrow_schema),
661 self.options.writer_properties.clone(),
662 )
663 .map_err(|e| LadduDataError::Sink(e.to_string()))?;
664
665 self.arrow_schema = Some(arrow_schema);
666 self.event_schema = Some(schema);
667 self.writer = Some(writer);
668 self.resolved_path = Some(path);
669
670 Ok(())
671 }
672
673 fn write_batch(&mut self, batch: &EventBatch) -> LadduDataResult<()> {
674 let arrow_schema = self
675 .arrow_schema
676 .as_ref()
677 .ok_or_else(|| LadduDataError::Sink("parquet sink not initialized".into()))?;
678
679 let event_schema = self
680 .event_schema
681 .as_ref()
682 .ok_or_else(|| LadduDataError::Sink("parquet sink not initialized".into()))?;
683 if event_schema.as_ref() != batch.schema().as_ref() {
684 return Err(LadduDataError::Sink(
685 "batch schema does not match parquet sink schema".into(),
686 ));
687 }
688
689 let rb = event_batch_to_record_batch(
690 batch,
691 Arc::clone(arrow_schema),
692 self.options.schema_write.write_weight_column,
693 self.options.schema_write.precision,
694 )?;
695
696 self.writer
697 .as_mut()
698 .ok_or_else(|| LadduDataError::Sink("parquet sink not initialized".into()))?
699 .write(&rb)
700 .map_err(|e| LadduDataError::Sink(e.to_string()))
701 }
702
703 fn finish(&mut self) -> LadduDataResult<()> {
704 if let Some(writer) = self.writer.take() {
705 writer
706 .close()
707 .map_err(|e| LadduDataError::Sink(e.to_string()))?;
708 }
709
710 Ok(())
711 }
712}
713
714fn arrow_schema_from_event_schema(
715 schema: &Schema,
716 write_weight: WriteWeightColumn,
717 options: &SchemaWriteOptions,
718) -> ArrowSchema {
719 let should_write_weight =
720 matches!(write_weight, WriteWeightColumn::Always) || schema.has_weight();
721
722 let mut fields = Vec::with_capacity(
723 4 * schema.n_p4s() + schema.n_scalars() + usize::from(should_write_weight),
724 );
725
726 let data_type = match options.precision {
727 Precision::F64 => DataType::Float64,
728 Precision::F32 => DataType::Float32,
729 };
730
731 for name in schema.p4s() {
732 let [e, px, py, pz] = options.column_names.p4_suffixes.physical_p4_names(name);
733 fields.push(Field::new(e, data_type.clone(), false));
734 fields.push(Field::new(px, data_type.clone(), false));
735 fields.push(Field::new(py, data_type.clone(), false));
736 fields.push(Field::new(pz, data_type.clone(), false));
737 }
738
739 for name in schema.scalars() {
740 fields.push(Field::new(name.as_ref(), data_type.clone(), false));
741 }
742
743 if should_write_weight {
744 fields.push(Field::new(
745 options.column_names.weight_column.as_ref(),
746 data_type,
747 false,
748 ));
749 }
750
751 ArrowSchema::new(fields)
752}
753
754fn array_from_iter<I: IntoIterator<Item = f64>>(iter: I, precision: Precision) -> ArrayRef {
755 match precision {
756 Precision::F64 => Arc::new(Float64Array::from_iter_values(iter)),
757 Precision::F32 => Arc::new(Float32Array::from_iter_values(
758 iter.into_iter().map(|f| f as f32),
759 )),
760 }
761}
762
763fn event_batch_to_record_batch(
764 batch: &EventBatch,
765 arrow_schema: SchemaRef,
766 write_weight: WriteWeightColumn,
767 precision: Precision,
768) -> LadduDataResult<RecordBatch> {
769 let mut columns: Vec<ArrayRef> = Vec::with_capacity(arrow_schema.fields().len());
770
771 for col in 0..batch.schema().n_p4s() {
772 let p = batch.vec4_column(col);
773
774 columns.push(array_from_iter(p.iter().map(|x| x.e), precision));
775 columns.push(array_from_iter(p.iter().map(|x| x.px), precision));
776 columns.push(array_from_iter(p.iter().map(|x| x.py), precision));
777 columns.push(array_from_iter(p.iter().map(|x| x.pz), precision));
778 }
779
780 for col in 0..batch.schema().n_scalars() {
781 columns.push(array_from_iter(
782 batch.scalar_column(col).iter().copied(),
783 precision,
784 ));
785 }
786
787 let should_write_weight =
788 matches!(write_weight, WriteWeightColumn::Always) || batch.schema().has_weight();
789
790 if should_write_weight {
791 columns.push(array_from_iter(
792 (0..batch.len()).map(|i| batch.weights_at(i)),
793 precision,
794 ));
795 }
796
797 RecordBatch::try_new(arrow_schema, columns).map_err(|e| LadduDataError::Sink(e.to_string()))
798}
799
800#[cfg(test)]
801mod tests {
802 use arrow::array::{Float32Array, Float64Array};
803
804 use super::*;
805 use crate::data::{Dataset, EventBatchBuilder};
806
807 fn temp_path(ext: &str) -> PathBuf {
808 let nanos = std::time::SystemTime::now()
809 .duration_since(std::time::UNIX_EPOCH)
810 .unwrap()
811 .as_nanos();
812
813 std::env::temp_dir().join(format!(
814 "laddu-parquet-test-{}-{nanos}.{ext}",
815 std::process::id()
816 ))
817 }
818
819 fn v(x: f64) -> RealVec4 {
820 RealVec4 {
821 e: x + 0.3,
822 px: x,
823 py: x + 0.1,
824 pz: x + 0.2,
825 }
826 }
827
828 fn schema() -> Arc<Schema> {
829 Arc::new(Schema::new(["p"], ["mass"], true).unwrap())
830 }
831
832 fn batch() -> EventBatch {
833 let schema = schema();
834 let mut builder = EventBatchBuilder::new(schema);
835
836 for i in 0..4 {
837 builder
838 .push_weighted([v(i as f64)], [100.0 + i as f64], 10.0 + i as f64)
839 .unwrap();
840 }
841
842 builder.finish().unwrap()
843 }
844
845 #[test]
846 fn parquet_sink_and_source_roundtrip_with_f32_write_and_schema_inference() {
847 let path = temp_path("parquet");
848 let batch = batch();
849
850 let mut sink = ParquetSink::builder(path.clone())
851 .precision(Precision::F32)
852 .build();
853
854 sink.begin(Arc::clone(batch.schema()), WritePlan::default())
855 .unwrap();
856 sink.write_batch(&batch).unwrap();
857 sink.finish().unwrap();
858
859 let source = ParquetSource::builder(path.to_str().unwrap())
860 .infer_schema(true)
861 .validate_all_files(true)
862 .build()
863 .unwrap();
864
865 let inferred = source.schema().unwrap();
866 assert_eq!(
867 inferred
868 .p4s()
869 .iter()
870 .map(|n| n.to_string())
871 .collect::<Vec<_>>(),
872 vec!["p"]
873 );
874 assert_eq!(
875 inferred
876 .scalars()
877 .iter()
878 .map(|n| n.to_string())
879 .collect::<Vec<_>>(),
880 vec!["mass"]
881 );
882 assert!(inferred.has_weight());
883
884 let read_batches: Vec<EventBatch> = source
885 .batches(ReadPlan {
886 chunk_size: Some(2),
887 #[cfg(feature = "mpi")]
888 distribution: Default::default(),
889 })
890 .unwrap()
891 .map(Result::unwrap)
892 .collect();
893
894 assert_eq!(
895 read_batches.iter().map(EventBatch::len).collect::<Vec<_>>(),
896 vec![2, 2]
897 );
898
899 let read = EventBatch::concat(&read_batches).unwrap();
900
901 assert_eq!(read.scalar_column(0), &[100.0, 101.0, 102.0, 103.0]);
902 assert_eq!(read.weights_column().unwrap(), &[10.0, 11.0, 12.0, 13.0]);
903 assert!((read.p4_at(0, 2).e - 2.3).abs() < 1.0e-6);
904
905 let _ = std::fs::remove_file(path);
906 }
907
908 #[test]
909 fn parquet_source_require_weight_fails_when_written_without_weight_column() {
910 let path = temp_path("parquet");
911 let schema = Arc::new(Schema::new(["p"], ["mass"], false).unwrap());
912
913 let mut builder = EventBatchBuilder::new(Arc::clone(&schema));
914 builder.push([v(1.0)], [5.0]).unwrap();
915 let batch = builder.finish().unwrap();
916
917 let mut sink = ParquetSink::builder(path.clone())
918 .write_weight_column(WriteWeightColumn::OnlyIfPresent)
919 .build();
920
921 sink.begin(schema, WritePlan::default()).unwrap();
922 sink.write_batch(&batch).unwrap();
923 sink.finish().unwrap();
924
925 let err = ParquetSource::builder(path.to_str().unwrap())
926 .require_weight(true)
927 .build()
928 .unwrap_err();
929
930 assert!(matches!(err, LadduDataError::MissingColumn(name) if name.as_ref() == "weight"));
931
932 let _ = std::fs::remove_file(path);
933 }
934
935 #[test]
936 fn record_batch_to_event_batch_handles_nulls_as_error_or_nan_and_reads_f32_as_f64() {
937 let arrow_schema = Arc::new(ArrowSchema::new(vec![
938 Field::new("p_e", DataType::Float64, true),
939 Field::new("p_px", DataType::Float32, true),
940 Field::new("p_py", DataType::Float64, true),
941 Field::new("p_pz", DataType::Float32, true),
942 Field::new("mass", DataType::Float32, true),
943 Field::new("weight", DataType::Float64, true),
944 ]));
945
946 let rb = RecordBatch::try_new(
947 Arc::clone(&arrow_schema),
948 vec![
949 Arc::new(Float64Array::from(vec![Some(1.0), None])) as ArrayRef,
950 Arc::new(Float32Array::from(vec![Some(0.1), Some(0.2)])) as ArrayRef,
951 Arc::new(Float64Array::from(vec![Some(0.3), Some(0.4)])) as ArrayRef,
952 Arc::new(Float32Array::from(vec![Some(0.5), Some(0.6)])) as ArrayRef,
953 Arc::new(Float32Array::from(vec![Some(2.0), Some(3.0)])) as ArrayRef,
954 Arc::new(Float64Array::from(vec![Some(4.0), Some(5.0)])) as ArrayRef,
955 ],
956 )
957 .unwrap();
958
959 let schema = schema();
960
961 let error_options = ParquetReadOptions {
962 null_handling: NullHandling::Error,
963 ..ParquetReadOptions::default()
964 };
965
966 let err = record_batch_to_event_batch(rb.clone(), Arc::clone(&schema), &error_options)
967 .unwrap_err();
968
969 assert!(matches!(err, LadduDataError::Source(msg) if msg.contains("null in column p_e")));
970
971 let nan_options = ParquetReadOptions {
972 null_handling: NullHandling::NaN,
973 ..ParquetReadOptions::default()
974 };
975
976 let batch = record_batch_to_event_batch(rb, schema, &nan_options).unwrap();
977
978 assert!(batch.p4_at(0, 1).e.is_nan());
979 assert!((batch.p4_at(0, 1).px - 0.2).abs() < 1.0e-6);
980 assert_eq!(batch.scalar_column(0), &[2.0, 3.0]);
981 assert_eq!(batch.weights_column().unwrap(), &[4.0, 5.0]);
982 }
983
984 #[test]
985 fn parquet_sink_rejects_batches_with_different_schema() {
986 let path = temp_path("parquet");
987 let batch = batch();
988
989 let mut sink = ParquetSink::builder(path.clone()).build();
990 sink.begin(Arc::clone(batch.schema()), WritePlan::default())
991 .unwrap();
992
993 let other_schema = Arc::new(Schema::new(["q"], ["mass"], true).unwrap());
994 let mut builder = EventBatchBuilder::new(other_schema);
995 builder.push_weighted([v(1.0)], [1.0], 1.0).unwrap();
996 let other = builder.finish().unwrap();
997
998 let err = sink.write_batch(&other).unwrap_err();
999 assert!(matches!(err, LadduDataError::Sink(msg) if msg.contains("schema")));
1000
1001 sink.finish().unwrap();
1002 let _ = std::fs::remove_file(path);
1003 }
1004
1005 #[test]
1006 fn dataset_write_to_parquet_applies_dataset_transformations_before_writing() {
1007 let path = temp_path("parquet");
1008
1009 let dataset = Dataset::from_batch(batch()).filter(|ev| ev.scalar(0) >= 102.0);
1010
1011 let mut sink = ParquetSink::builder(path.clone()).build();
1012 dataset.write_to(&mut sink).unwrap();
1013
1014 let source = ParquetSource::open(path.to_str().unwrap()).unwrap();
1015 let read = EventBatch::concat(
1016 &source
1017 .batches(ReadPlan::default())
1018 .unwrap()
1019 .map(Result::unwrap)
1020 .collect::<Vec<_>>(),
1021 )
1022 .unwrap();
1023
1024 assert_eq!(read.scalar_column(0), &[102.0, 103.0]);
1025 assert_eq!(read.weights_column().unwrap(), &[12.0, 13.0]);
1026
1027 let _ = std::fs::remove_file(path);
1028 }
1029}