1use std::collections::HashMap;
24use std::sync::Arc;
25use std::sync::atomic::AtomicU64;
26
27use arrow_schema::{DataType, Field};
28use futures::{StreamExt, TryStreamExt};
29use parquet::arrow::arrow_reader::{ArrowReaderMetadata, ArrowReaderOptions};
30use parquet::arrow::{PARQUET_FIELD_ID_META_KEY, ParquetRecordBatchStreamBuilder, RowNumber};
31use parquet::encryption::decrypt::FileDecryptionProperties;
32
33use super::{
34 ArrowFileReader, ArrowReader, ParquetReadOptions, add_fallback_field_ids_to_arrow_schema,
35 apply_name_mapping_to_arrow_schema,
36};
37use crate::arrow::caching_delete_file_loader::CachingDeleteFileLoader;
38use crate::arrow::int96::coerce_int96_timestamps;
39use crate::arrow::record_batch_transformer::RecordBatchTransformerBuilder;
40use crate::arrow::scan_metrics::{CountingFileRead, ScanMetrics, ScanResult};
41use crate::encryption::StandardKeyMetadata;
42use crate::error::Result;
43use crate::io::{FileIO, FileMetadata, FileRead};
44use crate::metadata_columns::{
45 RESERVED_COL_NAME_POS, RESERVED_FIELD_ID_FILE, RESERVED_FIELD_ID_POS,
46 RESERVED_FIELD_ID_SPEC_ID, is_metadata_field,
47};
48use crate::scan::{ArrowRecordBatchStream, FileScanTask, FileScanTaskStream};
49use crate::spec::Datum;
50use crate::{Error, ErrorKind};
51
52impl ArrowReader {
53 pub fn read(self, tasks: FileScanTaskStream) -> Result<ScanResult> {
56 let concurrency_limit_data_files = self.concurrency_limit_data_files;
57 let scan_metrics = ScanMetrics::new();
58
59 let task_reader = FileScanTaskReader {
60 batch_size: self.batch_size,
61 file_io: self.file_io,
62 delete_file_loader: self
63 .delete_file_loader
64 .with_scan_metrics(scan_metrics.clone()),
65 row_group_filtering_enabled: self.row_group_filtering_enabled,
66 row_selection_enabled: self.row_selection_enabled,
67 parquet_read_options: self.parquet_read_options,
68 scan_metrics: scan_metrics.clone(),
69 };
70
71 let stream: ArrowRecordBatchStream = if concurrency_limit_data_files == 1 {
73 Box::pin(
74 tasks
75 .and_then(move |task| task_reader.clone().process(task))
76 .map_err(|err| {
77 Error::new(ErrorKind::Unexpected, "file scan task generate failed")
78 .with_source(err)
79 })
80 .try_flatten(),
81 )
82 } else {
83 Box::pin(
84 tasks
85 .map_ok(move |task| task_reader.clone().process(task))
86 .map_err(|err| {
87 Error::new(ErrorKind::Unexpected, "file scan task generate failed")
88 .with_source(err)
89 })
90 .try_buffer_unordered(concurrency_limit_data_files)
91 .try_flatten_unordered(concurrency_limit_data_files),
92 )
93 };
94
95 Ok(ScanResult::new(stream, scan_metrics))
96 }
97}
98
99#[derive(Clone)]
102struct FileScanTaskReader {
103 batch_size: Option<usize>,
104 file_io: FileIO,
105 delete_file_loader: CachingDeleteFileLoader,
106 row_group_filtering_enabled: bool,
107 row_selection_enabled: bool,
108 parquet_read_options: ParquetReadOptions,
109 scan_metrics: ScanMetrics,
110}
111
112impl FileScanTaskReader {
113 async fn process(self, task: FileScanTask) -> Result<ArrowRecordBatchStream> {
114 let should_load_page_index =
115 (self.row_selection_enabled && task.predicate.is_some()) || !task.deletes.is_empty();
116 let mut parquet_read_options = self.parquet_read_options;
117 parquet_read_options.preload_page_index = should_load_page_index;
118
119 let delete_filter_rx = self
120 .delete_file_loader
121 .load_deletes(&task.deletes, Arc::clone(&task.schema));
122
123 let (parquet_file_reader, arrow_metadata) = ArrowReader::open_parquet_file(
125 &task.data_file_path,
126 &self.file_io,
127 task.file_size_in_bytes,
128 parquet_read_options,
129 self.scan_metrics.bytes_read_counter(),
130 task.key_metadata.as_deref(),
131 )
132 .await?;
133
134 let missing_field_ids = arrow_metadata
138 .schema()
139 .fields()
140 .iter()
141 .next()
142 .is_some_and(|f| f.metadata().get(PARQUET_FIELD_ID_META_KEY).is_none());
143
144 let use_position_fallback = missing_field_ids && task.name_mapping.is_none();
149
150 let arrow_metadata = if missing_field_ids {
166 let arrow_schema = if let Some(name_mapping) = &task.name_mapping {
168 apply_name_mapping_to_arrow_schema(
173 Arc::clone(arrow_metadata.schema()),
174 name_mapping,
175 )?
176 } else {
177 add_fallback_field_ids_to_arrow_schema(arrow_metadata.schema())
180 };
181
182 let options = ArrowReaderOptions::new().with_schema(arrow_schema);
183 ArrowReaderMetadata::try_new(Arc::clone(arrow_metadata.metadata()), options).map_err(
184 |e| {
185 Error::new(
186 ErrorKind::Unexpected,
187 "Failed to create ArrowReaderMetadata with field ID schema",
188 )
189 .with_source(e)
190 },
191 )?
192 } else {
193 arrow_metadata
195 };
196
197 let arrow_metadata = if let Some(coerced_schema) =
200 coerce_int96_timestamps(arrow_metadata.schema(), &task.schema)
201 {
202 let options = ArrowReaderOptions::new().with_schema(Arc::clone(&coerced_schema));
203 ArrowReaderMetadata::try_new(Arc::clone(arrow_metadata.metadata()), options).map_err(
204 |e| {
205 Error::new(
206 ErrorKind::Unexpected,
207 format!(
208 "Failed to create ArrowReaderMetadata with INT96-coerced schema: {coerced_schema}"
209 ),
210 )
211 .with_source(e)
212 },
213 )?
214 } else {
215 arrow_metadata
216 };
217
218 let project_pos = task.project_field_ids().contains(&RESERVED_FIELD_ID_POS);
219
220 let arrow_metadata = if project_pos {
221 let row_number_field = Arc::new(
222 Field::new(RESERVED_COL_NAME_POS, DataType::Int64, false)
223 .with_metadata(HashMap::from([(
224 PARQUET_FIELD_ID_META_KEY.to_string(),
225 RESERVED_FIELD_ID_POS.to_string(),
226 )]))
227 .with_extension_type(RowNumber),
228 );
229
230 let options = ArrowReaderOptions::new()
231 .with_schema(Arc::clone(arrow_metadata.schema()))
232 .with_virtual_columns(vec![row_number_field])?;
233
234 ArrowReaderMetadata::try_new(Arc::clone(arrow_metadata.metadata()), options).map_err(
235 |e| {
236 Error::new(
237 ErrorKind::Unexpected,
238 "Failed to create ArrowReaderMetadata with the 'row_number' virtual_column",
239 )
240 .with_source(e)
241 },
242 )?
243 } else {
244 arrow_metadata
245 };
246
247 let mut record_batch_stream_builder =
249 ParquetRecordBatchStreamBuilder::new_with_metadata(parquet_file_reader, arrow_metadata);
250
251 let project_field_ids_without_metadata: Vec<i32> = task
253 .project_field_ids
254 .iter()
255 .filter(|&&id| !is_metadata_field(id))
256 .copied()
257 .collect();
258
259 let projection_mask = ArrowReader::get_arrow_projection_mask(
265 &project_field_ids_without_metadata,
266 &task.schema,
267 record_batch_stream_builder.parquet_schema(),
268 record_batch_stream_builder.schema(),
269 use_position_fallback, )?;
271
272 record_batch_stream_builder =
273 record_batch_stream_builder.with_projection(projection_mask.clone());
274
275 let mut record_batch_transformer_builder =
279 RecordBatchTransformerBuilder::new(task.schema_ref(), task.project_field_ids());
280
281 if task.project_field_ids().contains(&RESERVED_FIELD_ID_FILE) {
283 let file_datum = Datum::string(task.data_file_path.clone());
284 record_batch_transformer_builder =
285 record_batch_transformer_builder.with_constant(RESERVED_FIELD_ID_FILE, file_datum);
286 }
287
288 if task
289 .project_field_ids()
290 .contains(&RESERVED_FIELD_ID_SPEC_ID)
291 {
292 let partition_spec = task
293 .partition_spec
294 .as_ref()
295 .ok_or_else(|| Error::new(ErrorKind::Unexpected, "Partition spec is missing"))?;
296
297 let spec_id_datum = Datum::int(partition_spec.spec_id());
298 record_batch_transformer_builder = record_batch_transformer_builder
299 .with_constant(RESERVED_FIELD_ID_SPEC_ID, spec_id_datum);
300 }
301
302 if let (Some(partition_spec), Some(partition_data)) =
303 (task.partition_spec.clone(), task.partition.clone())
304 {
305 record_batch_transformer_builder =
306 record_batch_transformer_builder.with_partition(partition_spec, partition_data)?;
307 }
308
309 if project_pos {
310 record_batch_transformer_builder =
311 record_batch_transformer_builder.with_virtual_field(RESERVED_FIELD_ID_POS);
312 }
313
314 let mut record_batch_transformer = record_batch_transformer_builder.build();
315
316 if let Some(batch_size) = self.batch_size {
317 record_batch_stream_builder = record_batch_stream_builder.with_batch_size(batch_size);
318 }
319
320 let delete_filter = delete_filter_rx.await.unwrap()?;
321 let delete_predicate = delete_filter.build_equality_delete_predicate(&task).await?;
322
323 let final_predicate = match (&task.predicate, delete_predicate) {
328 (None, None) => None,
329 (Some(predicate), None) => Some(predicate.clone()),
330 (None, Some(ref predicate)) => Some(predicate.clone()),
331 (Some(filter_predicate), Some(delete_predicate)) => {
332 Some(filter_predicate.clone().and(delete_predicate))
333 }
334 };
335
336 let mut selected_row_group_indices = None;
352 let mut row_selection = None;
353
354 if task.start != 0 || task.length != 0 {
357 let byte_range_filtered_row_groups = ArrowReader::filter_row_groups_by_byte_range(
358 record_batch_stream_builder.metadata(),
359 task.start,
360 task.length,
361 )?;
362 selected_row_group_indices = Some(byte_range_filtered_row_groups);
363 }
364
365 if let Some(predicate) = final_predicate {
366 let (iceberg_field_ids, field_id_map) = ArrowReader::build_field_id_set_and_map(
367 record_batch_stream_builder.parquet_schema(),
368 record_batch_stream_builder.schema(),
369 &predicate,
370 use_position_fallback,
371 )?;
372
373 let row_filter = ArrowReader::get_row_filter(
374 &predicate,
375 record_batch_stream_builder.parquet_schema(),
376 &iceberg_field_ids,
377 &field_id_map,
378 )?;
379 record_batch_stream_builder = record_batch_stream_builder.with_row_filter(row_filter);
380
381 if self.row_group_filtering_enabled {
382 let predicate_filtered_row_groups = ArrowReader::get_selected_row_group_indices(
383 &predicate,
384 record_batch_stream_builder.metadata(),
385 &field_id_map,
386 &task.schema,
387 )?;
388
389 selected_row_group_indices = match selected_row_group_indices {
392 Some(byte_range_filtered) => {
393 let intersection: Vec<usize> = byte_range_filtered
395 .into_iter()
396 .filter(|idx| predicate_filtered_row_groups.contains(idx))
397 .collect();
398 Some(intersection)
399 }
400 None => Some(predicate_filtered_row_groups),
401 };
402 }
403
404 if self.row_selection_enabled {
405 row_selection = ArrowReader::get_row_selection_for_filter_predicate(
406 &predicate,
407 record_batch_stream_builder.metadata(),
408 &selected_row_group_indices,
409 &field_id_map,
410 &task.schema,
411 )?;
412 }
413 }
414
415 let positional_delete_indexes = delete_filter.get_delete_vector(&task);
416
417 if let Some(positional_delete_indexes) = positional_delete_indexes {
418 let delete_row_selection = {
419 let positional_delete_indexes = positional_delete_indexes.lock().unwrap();
420
421 ArrowReader::build_deletes_row_selection(
422 record_batch_stream_builder.metadata().row_groups(),
423 &selected_row_group_indices,
424 &positional_delete_indexes,
425 )
426 }?;
427
428 row_selection = match row_selection {
431 None => Some(delete_row_selection),
432 Some(filter_row_selection) => {
433 Some(filter_row_selection.intersection(&delete_row_selection))
434 }
435 };
436 }
437
438 if let Some(row_selection) = row_selection {
439 record_batch_stream_builder =
440 record_batch_stream_builder.with_row_selection(row_selection);
441 }
442
443 if let Some(selected_row_group_indices) = selected_row_group_indices {
444 record_batch_stream_builder =
445 record_batch_stream_builder.with_row_groups(selected_row_group_indices);
446 }
447
448 let record_batch_stream =
451 record_batch_stream_builder
452 .build()?
453 .map(move |batch| match batch {
454 Ok(batch) => {
455 record_batch_transformer.process_record_batch(batch)
457 }
458 Err(err) => Err(err.into()),
459 });
460
461 Ok(Box::pin(record_batch_stream) as ArrowRecordBatchStream)
462 }
463}
464
465impl ArrowReader {
466 pub(crate) async fn open_parquet_file(
469 data_file_path: &str,
470 file_io: &FileIO,
471 file_size_in_bytes: u64,
472 parquet_read_options: ParquetReadOptions,
473 bytes_read: &Arc<AtomicU64>,
474 key_metadata: Option<&[u8]>,
475 ) -> Result<(ArrowFileReader, ArrowReaderMetadata)> {
476 let parquet_file = file_io.new_input(data_file_path)?;
477 let counting_reader =
478 CountingFileRead::new(parquet_file.reader().await?, Arc::clone(bytes_read));
479 Self::build_parquet_reader(
480 Box::new(counting_reader),
481 file_size_in_bytes,
482 parquet_read_options,
483 key_metadata,
484 )
485 .await
486 }
487
488 async fn build_parquet_reader(
489 parquet_reader: Box<dyn FileRead>,
490 file_size_in_bytes: u64,
491 parquet_read_options: ParquetReadOptions,
492 key_metadata: Option<&[u8]>,
493 ) -> Result<(ArrowFileReader, ArrowReaderMetadata)> {
494 let mut reader = ArrowFileReader::new(
495 FileMetadata {
496 size: file_size_in_bytes,
497 },
498 parquet_reader,
499 )
500 .with_parquet_read_options(parquet_read_options);
501
502 let arrow_reader_options = Self::build_arrow_reader_options(key_metadata)?;
503
504 let arrow_metadata = ArrowReaderMetadata::load_async(&mut reader, arrow_reader_options)
505 .await
506 .map_err(|e| {
507 Error::new(ErrorKind::Unexpected, "Failed to load Parquet metadata").with_source(e)
508 })?;
509
510 Ok((reader, arrow_metadata))
511 }
512
513 fn build_arrow_reader_options(key_metadata: Option<&[u8]>) -> Result<ArrowReaderOptions> {
516 match key_metadata {
517 Some(km) => {
518 let standard_key_metadata = StandardKeyMetadata::decode(km)?;
519 let mut builder = FileDecryptionProperties::builder(
520 standard_key_metadata.encryption_key().as_bytes().to_vec(),
521 );
522 if let Some(aad) = standard_key_metadata.aad_prefix() {
523 builder = builder.with_aad_prefix(aad.to_vec());
524 }
525 let decryption_properties = builder.build().map_err(|e| {
526 Error::new(
527 ErrorKind::Unexpected,
528 "Failed to build Parquet file decryption properties",
529 )
530 .with_source(e)
531 })?;
532 Ok(
533 ArrowReaderOptions::new()
534 .with_file_decryption_properties(decryption_properties),
535 )
536 }
537 None => Ok(ArrowReaderOptions::default()),
538 }
539 }
540}
541
542#[cfg(test)]
543mod tests {
544 use std::collections::HashMap;
545 use std::fs::File;
546 use std::sync::Arc;
547
548 use arrow_array::cast::AsArray;
549 use arrow_array::{Array, ArrayRef, Int32Array, RecordBatch};
550 use arrow_schema::{DataType, Field, Schema as ArrowSchema};
551 use futures::TryStreamExt;
552 use parquet::arrow::{ArrowWriter, PARQUET_FIELD_ID_META_KEY};
553 use parquet::basic::Compression;
554 use parquet::file::properties::WriterProperties;
555 use tempfile::TempDir;
556
557 use crate::Runtime;
558 use crate::arrow::ArrowReaderBuilder;
559 use crate::arrow::test_utils::write_encrypted_parquet;
560 use crate::io::FileIO;
561 use crate::scan::{FileScanTask, FileScanTaskStream};
562 use crate::spec::{DataFileFormat, NestedField, PrimitiveType, Schema, SchemaRef, Type};
563
564 const UNIX_EPOCH_JULIAN: i64 = 2_440_588;
567 const MICROS_PER_DAY: i64 = 86_400_000_000;
568 const INT96_TEST_NANOS_WITHIN_DAY: u64 = 43_200_000_000_000;
570 const INT96_TEST_JULIAN_DAY: u32 = 2_953_529;
571
572 fn make_int96_test_value() -> (parquet::data_type::Int96, i64) {
573 let mut val = parquet::data_type::Int96::new();
574 val.set_data(
575 (INT96_TEST_NANOS_WITHIN_DAY & 0xFFFFFFFF) as u32,
576 (INT96_TEST_NANOS_WITHIN_DAY >> 32) as u32,
577 INT96_TEST_JULIAN_DAY,
578 );
579 let expected_micros = (INT96_TEST_JULIAN_DAY as i64 - UNIX_EPOCH_JULIAN) * MICROS_PER_DAY
580 + (INT96_TEST_NANOS_WITHIN_DAY / 1_000) as i64;
581 (val, expected_micros)
582 }
583
584 async fn read_int96_batches(
585 file_path: &str,
586 schema: SchemaRef,
587 project_field_ids: Vec<i32>,
588 ) -> Vec<RecordBatch> {
589 let file_io = FileIO::new_with_fs();
590 let reader = ArrowReaderBuilder::new(file_io, Runtime::current()).build();
591
592 let file_size = std::fs::metadata(file_path).unwrap().len();
593 let task = FileScanTask::builder()
594 .with_file_size_in_bytes(file_size)
595 .with_start(0)
596 .with_length(file_size)
597 .with_data_file_path(file_path.to_string())
598 .with_data_file_format(DataFileFormat::Parquet)
599 .with_schema(schema)
600 .with_project_field_ids(project_field_ids)
601 .with_case_sensitive(false)
602 .build();
603
604 let tasks = Box::pin(futures::stream::iter(vec![Ok(task)])) as FileScanTaskStream;
605 reader
606 .read(tasks)
607 .unwrap()
608 .stream()
609 .try_collect()
610 .await
611 .unwrap()
612 }
613
614 fn write_int96_parquet_file(
616 table_location: &str,
617 filename: &str,
618 with_field_ids: bool,
619 ) -> (String, Vec<i64>) {
620 use parquet::basic::{Repetition, Type as PhysicalType};
621 use parquet::data_type::{Int32Type, Int96, Int96Type};
622 use parquet::file::writer::SerializedFileWriter;
623 use parquet::schema::types::Type as SchemaType;
624
625 let file_path = format!("{table_location}/{filename}");
626
627 let mut ts_builder = SchemaType::primitive_type_builder("ts", PhysicalType::INT96)
628 .with_repetition(Repetition::OPTIONAL);
629 let mut id_builder = SchemaType::primitive_type_builder("id", PhysicalType::INT32)
630 .with_repetition(Repetition::REQUIRED);
631
632 if with_field_ids {
633 ts_builder = ts_builder.with_id(Some(1));
634 id_builder = id_builder.with_id(Some(2));
635 }
636
637 let schema = SchemaType::group_type_builder("schema")
638 .with_fields(vec![
639 Arc::new(ts_builder.build().unwrap()),
640 Arc::new(id_builder.build().unwrap()),
641 ])
642 .build()
643 .unwrap();
644
645 const NOON_NANOS: u64 = INT96_TEST_NANOS_WITHIN_DAY;
647 const JULIAN_3333: u32 = INT96_TEST_JULIAN_DAY;
648 const JULIAN_2100: u32 = 2_488_070;
649
650 let test_data: Vec<(u32, u32, u32, i64)> = vec![
651 (
653 0,
654 0,
655 JULIAN_3333,
656 (JULIAN_3333 as i64 - UNIX_EPOCH_JULIAN) * MICROS_PER_DAY,
657 ),
658 (
660 (NOON_NANOS & 0xFFFFFFFF) as u32,
661 (NOON_NANOS >> 32) as u32,
662 JULIAN_3333,
663 (JULIAN_3333 as i64 - UNIX_EPOCH_JULIAN) * MICROS_PER_DAY
664 + (NOON_NANOS / 1_000) as i64,
665 ),
666 (
668 0,
669 0,
670 JULIAN_2100,
671 (JULIAN_2100 as i64 - UNIX_EPOCH_JULIAN) * MICROS_PER_DAY,
672 ),
673 ];
674
675 let int96_values: Vec<Int96> = test_data
676 .iter()
677 .map(|(lo, hi, day, _)| {
678 let mut v = Int96::new();
679 v.set_data(*lo, *hi, *day);
680 v
681 })
682 .collect();
683
684 let id_values: Vec<i32> = (0..test_data.len() as i32).collect();
685 let expected_micros: Vec<i64> = test_data.iter().map(|(_, _, _, m)| *m).collect();
686
687 let file = File::create(&file_path).unwrap();
688 let mut writer =
689 SerializedFileWriter::new(file, Arc::new(schema), Default::default()).unwrap();
690
691 let mut row_group = writer.next_row_group().unwrap();
692 {
693 let mut col = row_group.next_column().unwrap().unwrap();
695 col.typed::<Int96Type>()
696 .write_batch(&int96_values, Some(&vec![1; test_data.len()]), None)
697 .unwrap();
698 col.close().unwrap();
699 }
700 {
701 let mut col = row_group.next_column().unwrap().unwrap();
702 col.typed::<Int32Type>()
703 .write_batch(&id_values, None, None)
704 .unwrap();
705 col.close().unwrap();
706 }
707 row_group.close().unwrap();
708 writer.close().unwrap();
709
710 (file_path, expected_micros)
711 }
712
713 async fn assert_int96_read_matches(
714 file_path: &str,
715 schema: SchemaRef,
716 project_field_ids: Vec<i32>,
717 expected_micros: &[i64],
718 ) {
719 use arrow_array::TimestampMicrosecondArray;
720
721 let batches = read_int96_batches(file_path, schema, project_field_ids).await;
722
723 assert_eq!(batches.len(), 1);
724 let ts_array = batches[0]
725 .column(0)
726 .as_any()
727 .downcast_ref::<TimestampMicrosecondArray>()
728 .expect("Expected TimestampMicrosecondArray");
729
730 for (i, expected) in expected_micros.iter().enumerate() {
731 assert_eq!(
732 ts_array.value(i),
733 *expected,
734 "Row {i}: got {}, expected {expected}",
735 ts_array.value(i)
736 );
737 }
738 }
739
740 #[tokio::test]
741 async fn test_read_encrypted_parquet() {
742 let encryption_key = b"0123456789abcdef";
743 let aad_prefix = b"aad_prefix";
744
745 let schema = Arc::new(
746 Schema::builder()
747 .with_schema_id(1)
748 .with_fields(vec![
749 NestedField::required(1, "id", Type::Primitive(PrimitiveType::Int)).into(),
750 ])
751 .build()
752 .unwrap(),
753 );
754
755 let arrow_schema = Arc::new(ArrowSchema::new(vec![
756 Field::new("id", DataType::Int32, false).with_metadata(HashMap::from([(
757 PARQUET_FIELD_ID_META_KEY.to_string(),
758 "1".to_string(),
759 )])),
760 ]));
761
762 let tmp_dir = TempDir::new().unwrap();
763 let table_location = tmp_dir.path().to_str().unwrap().to_string();
764 let file_io = FileIO::new_with_fs();
765
766 let id_data = Arc::new(Int32Array::from(vec![10, 20, 30])) as ArrayRef;
767 let batch = RecordBatch::try_new(arrow_schema.clone(), vec![id_data]).unwrap();
768
769 let file_path = format!("{table_location}/encrypted.parquet");
770 write_encrypted_parquet(&file_path, &batch, encryption_key, Some(aad_prefix));
771
772 let key_metadata = crate::encryption::StandardKeyMetadata::try_new(encryption_key)
773 .unwrap()
774 .with_aad_prefix(aad_prefix)
775 .encode()
776 .unwrap();
777
778 let reader = ArrowReaderBuilder::new(file_io, Runtime::current()).build();
779
780 let task = FileScanTask::builder()
781 .with_file_size_in_bytes(std::fs::metadata(&file_path).unwrap().len())
782 .with_start(0)
783 .with_length(0)
784 .with_data_file_path(file_path)
785 .with_data_file_format(DataFileFormat::Parquet)
786 .with_schema(schema)
787 .with_project_field_ids(vec![1])
788 .with_case_sensitive(false)
789 .with_key_metadata(Some(key_metadata))
790 .build();
791
792 let tasks = Box::pin(futures::stream::iter(vec![Ok(task)])) as FileScanTaskStream;
793 let batches: Vec<RecordBatch> = reader
794 .read(tasks)
795 .unwrap()
796 .stream()
797 .try_collect()
798 .await
799 .unwrap();
800
801 assert_eq!(batches.len(), 1);
802 let ids = batches[0]
803 .column(0)
804 .as_any()
805 .downcast_ref::<Int32Array>()
806 .unwrap();
807 assert_eq!(ids.values(), &[10, 20, 30]);
808 }
809
810 #[tokio::test]
811 async fn test_read_encrypted_parquet_without_key_metadata_fails() {
812 let encryption_key = b"0123456789abcdef";
813
814 let schema = Arc::new(
815 Schema::builder()
816 .with_schema_id(1)
817 .with_fields(vec![
818 NestedField::required(1, "id", Type::Primitive(PrimitiveType::Int)).into(),
819 ])
820 .build()
821 .unwrap(),
822 );
823
824 let arrow_schema = Arc::new(ArrowSchema::new(vec![
825 Field::new("id", DataType::Int32, false).with_metadata(HashMap::from([(
826 PARQUET_FIELD_ID_META_KEY.to_string(),
827 "1".to_string(),
828 )])),
829 ]));
830
831 let tmp_dir = TempDir::new().unwrap();
832 let table_location = tmp_dir.path().to_str().unwrap().to_string();
833 let file_io = FileIO::new_with_fs();
834
835 let id_data = Arc::new(Int32Array::from(vec![1, 2, 3])) as ArrayRef;
836 let batch = RecordBatch::try_new(arrow_schema.clone(), vec![id_data]).unwrap();
837
838 let file_path = format!("{table_location}/encrypted_no_key.parquet");
839 write_encrypted_parquet(&file_path, &batch, encryption_key, None);
840
841 let reader = ArrowReaderBuilder::new(file_io, Runtime::current()).build();
842
843 let task = FileScanTask::builder()
844 .with_file_size_in_bytes(std::fs::metadata(&file_path).unwrap().len())
845 .with_start(0)
846 .with_length(0)
847 .with_data_file_path(file_path)
848 .with_data_file_format(DataFileFormat::Parquet)
849 .with_schema(schema)
850 .with_project_field_ids(vec![1])
851 .with_case_sensitive(false)
852 .build();
853
854 let tasks = Box::pin(futures::stream::iter(vec![Ok(task)])) as FileScanTaskStream;
855 let result: Result<Vec<RecordBatch>, _> =
856 reader.read(tasks).unwrap().stream().try_collect().await;
857
858 let err = result.unwrap_err();
859 assert_eq!(err.kind(), crate::ErrorKind::Unexpected);
860 let err_str = format!("{err}");
861 assert!(
862 err_str.contains("encrypted footer"),
863 "Expected error about encrypted footer, got: {err_str}"
864 );
865 assert!(
866 err_str.contains("decryption properties were not provided"),
867 "Expected error about missing decryption properties, got: {err_str}"
868 );
869 }
870
871 #[tokio::test]
872 async fn test_read_encrypted_parquet_with_wrong_key_fails() {
873 let encryption_key = b"0123456789abcdef";
874 let wrong_key = b"fedcba9876543210";
875
876 let schema = Arc::new(
877 Schema::builder()
878 .with_schema_id(1)
879 .with_fields(vec![
880 NestedField::required(1, "id", Type::Primitive(PrimitiveType::Int)).into(),
881 ])
882 .build()
883 .unwrap(),
884 );
885
886 let arrow_schema = Arc::new(ArrowSchema::new(vec![
887 Field::new("id", DataType::Int32, false).with_metadata(HashMap::from([(
888 PARQUET_FIELD_ID_META_KEY.to_string(),
889 "1".to_string(),
890 )])),
891 ]));
892
893 let tmp_dir = TempDir::new().unwrap();
894 let table_location = tmp_dir.path().to_str().unwrap().to_string();
895 let file_io = FileIO::new_with_fs();
896
897 let id_data = Arc::new(Int32Array::from(vec![1, 2, 3])) as ArrayRef;
898 let batch = RecordBatch::try_new(arrow_schema.clone(), vec![id_data]).unwrap();
899
900 let file_path = format!("{table_location}/encrypted_wrong_key.parquet");
901 write_encrypted_parquet(&file_path, &batch, encryption_key, None);
902
903 let wrong_key_metadata = crate::encryption::StandardKeyMetadata::try_new(wrong_key)
904 .unwrap()
905 .encode()
906 .unwrap();
907
908 let reader = ArrowReaderBuilder::new(file_io, Runtime::current()).build();
909
910 let task = FileScanTask::builder()
911 .with_file_size_in_bytes(std::fs::metadata(&file_path).unwrap().len())
912 .with_start(0)
913 .with_length(0)
914 .with_data_file_path(file_path)
915 .with_data_file_format(DataFileFormat::Parquet)
916 .with_schema(schema)
917 .with_project_field_ids(vec![1])
918 .with_case_sensitive(false)
919 .with_key_metadata(Some(wrong_key_metadata))
920 .build();
921
922 let tasks = Box::pin(futures::stream::iter(vec![Ok(task)])) as FileScanTaskStream;
923 let result: Result<Vec<RecordBatch>, _> =
924 reader.read(tasks).unwrap().stream().try_collect().await;
925
926 let err = result.unwrap_err();
927 assert_eq!(err.kind(), crate::ErrorKind::Unexpected);
928 let err_str = format!("{err}");
929 assert!(
930 err_str.contains("unable to decrypt parquet footer"),
931 "Expected error about decryption failure, got: {err_str}"
932 );
933 }
934
935 #[tokio::test]
938 async fn test_read_with_concurrency_one() {
939 use arrow_array::Int32Array;
940
941 let schema = Arc::new(
942 Schema::builder()
943 .with_schema_id(1)
944 .with_fields(vec![
945 NestedField::required(1, "id", Type::Primitive(PrimitiveType::Int)).into(),
946 NestedField::required(2, "file_num", Type::Primitive(PrimitiveType::Int))
947 .into(),
948 ])
949 .build()
950 .unwrap(),
951 );
952
953 let arrow_schema = Arc::new(ArrowSchema::new(vec![
954 Field::new("id", DataType::Int32, false).with_metadata(HashMap::from([(
955 PARQUET_FIELD_ID_META_KEY.to_string(),
956 "1".to_string(),
957 )])),
958 Field::new("file_num", DataType::Int32, false).with_metadata(HashMap::from([(
959 PARQUET_FIELD_ID_META_KEY.to_string(),
960 "2".to_string(),
961 )])),
962 ]));
963
964 let tmp_dir = TempDir::new().unwrap();
965 let table_location = tmp_dir.path().to_str().unwrap().to_string();
966 let file_io = FileIO::new_with_fs();
967
968 let props = WriterProperties::builder()
970 .set_compression(Compression::SNAPPY)
971 .build();
972
973 for file_num in 0..3 {
974 let id_data = Arc::new(Int32Array::from_iter_values(
975 file_num * 10..(file_num + 1) * 10,
976 )) as ArrayRef;
977 let file_num_data = Arc::new(Int32Array::from(vec![file_num; 10])) as ArrayRef;
978
979 let to_write =
980 RecordBatch::try_new(arrow_schema.clone(), vec![id_data, file_num_data]).unwrap();
981
982 let file = File::create(format!("{table_location}/file_{file_num}.parquet")).unwrap();
983 let mut writer =
984 ArrowWriter::try_new(file, to_write.schema(), Some(props.clone())).unwrap();
985 writer.write(&to_write).expect("Writing batch");
986 writer.close().unwrap();
987 }
988
989 let reader = ArrowReaderBuilder::new(file_io, Runtime::current())
991 .with_data_file_concurrency_limit(1)
992 .build();
993
994 let tasks = vec![
996 Ok(FileScanTask::builder()
997 .with_file_size_in_bytes(
998 std::fs::metadata(format!("{table_location}/file_0.parquet"))
999 .unwrap()
1000 .len(),
1001 )
1002 .with_start(0)
1003 .with_length(0)
1004 .with_data_file_path(format!("{table_location}/file_0.parquet"))
1005 .with_data_file_format(DataFileFormat::Parquet)
1006 .with_schema(schema.clone())
1007 .with_project_field_ids(vec![1, 2])
1008 .with_case_sensitive(false)
1009 .build()),
1010 Ok(FileScanTask::builder()
1011 .with_file_size_in_bytes(
1012 std::fs::metadata(format!("{table_location}/file_1.parquet"))
1013 .unwrap()
1014 .len(),
1015 )
1016 .with_start(0)
1017 .with_length(0)
1018 .with_data_file_path(format!("{table_location}/file_1.parquet"))
1019 .with_data_file_format(DataFileFormat::Parquet)
1020 .with_schema(schema.clone())
1021 .with_project_field_ids(vec![1, 2])
1022 .with_case_sensitive(false)
1023 .build()),
1024 Ok(FileScanTask::builder()
1025 .with_file_size_in_bytes(
1026 std::fs::metadata(format!("{table_location}/file_2.parquet"))
1027 .unwrap()
1028 .len(),
1029 )
1030 .with_start(0)
1031 .with_length(0)
1032 .with_data_file_path(format!("{table_location}/file_2.parquet"))
1033 .with_data_file_format(DataFileFormat::Parquet)
1034 .with_schema(schema.clone())
1035 .with_project_field_ids(vec![1, 2])
1036 .with_case_sensitive(false)
1037 .build()),
1038 ];
1039
1040 let tasks_stream = Box::pin(futures::stream::iter(tasks)) as FileScanTaskStream;
1041
1042 let result = reader
1043 .read(tasks_stream)
1044 .unwrap()
1045 .stream()
1046 .try_collect::<Vec<RecordBatch>>()
1047 .await
1048 .unwrap();
1049
1050 let total_rows: usize = result.iter().map(|b| b.num_rows()).sum();
1052 assert_eq!(total_rows, 30, "Should have 30 total rows");
1053
1054 let mut all_ids = Vec::new();
1056 let mut all_file_nums = Vec::new();
1057
1058 for batch in &result {
1059 let id_col = batch
1060 .column(0)
1061 .as_primitive::<arrow_array::types::Int32Type>();
1062 let file_num_col = batch
1063 .column(1)
1064 .as_primitive::<arrow_array::types::Int32Type>();
1065
1066 for i in 0..batch.num_rows() {
1067 all_ids.push(id_col.value(i));
1068 all_file_nums.push(file_num_col.value(i));
1069 }
1070 }
1071
1072 assert_eq!(all_ids.len(), 30);
1073 assert_eq!(all_file_nums.len(), 30);
1074
1075 for i in 0..10 {
1080 assert_eq!(all_file_nums[i], 0, "First 10 rows should be from file_0");
1081 assert_eq!(all_ids[i], i as i32, "IDs should be 0-9");
1082 }
1083 for i in 10..20 {
1084 assert_eq!(all_file_nums[i], 1, "Next 10 rows should be from file_1");
1085 assert_eq!(all_ids[i], i as i32, "IDs should be 10-19");
1086 }
1087 for i in 20..30 {
1088 assert_eq!(all_file_nums[i], 2, "Last 10 rows should be from file_2");
1089 assert_eq!(all_ids[i], i as i32, "IDs should be 20-29");
1090 }
1091 }
1092
1093 #[tokio::test]
1094 async fn test_read_int96_timestamps_with_field_ids() {
1095 let schema = Arc::new(
1096 Schema::builder()
1097 .with_schema_id(1)
1098 .with_fields(vec![
1099 NestedField::optional(1, "ts", Type::Primitive(PrimitiveType::Timestamp))
1100 .into(),
1101 NestedField::required(2, "id", Type::Primitive(PrimitiveType::Int)).into(),
1102 ])
1103 .build()
1104 .unwrap(),
1105 );
1106
1107 let tmp_dir = TempDir::new().unwrap();
1108 let table_location = tmp_dir.path().to_str().unwrap().to_string();
1109 let (file_path, expected_micros) =
1110 write_int96_parquet_file(&table_location, "with_ids.parquet", true);
1111
1112 assert_int96_read_matches(&file_path, schema, vec![1, 2], &expected_micros).await;
1113 }
1114
1115 #[tokio::test]
1116 async fn test_read_int96_timestamps_without_field_ids() {
1117 let schema = Arc::new(
1118 Schema::builder()
1119 .with_schema_id(1)
1120 .with_fields(vec![
1121 NestedField::optional(1, "ts", Type::Primitive(PrimitiveType::Timestamp))
1122 .into(),
1123 NestedField::required(2, "id", Type::Primitive(PrimitiveType::Int)).into(),
1124 ])
1125 .build()
1126 .unwrap(),
1127 );
1128
1129 let tmp_dir = TempDir::new().unwrap();
1130 let table_location = tmp_dir.path().to_str().unwrap().to_string();
1131 let (file_path, expected_micros) =
1132 write_int96_parquet_file(&table_location, "no_ids.parquet", false);
1133
1134 assert_int96_read_matches(&file_path, schema, vec![1, 2], &expected_micros).await;
1135 }
1136
1137 #[tokio::test]
1138 async fn test_read_int96_timestamps_in_struct() {
1139 use arrow_array::{StructArray, TimestampMicrosecondArray};
1140 use parquet::basic::{Repetition, Type as PhysicalType};
1141 use parquet::data_type::Int96Type;
1142 use parquet::file::writer::SerializedFileWriter;
1143 use parquet::schema::types::Type as SchemaType;
1144
1145 let tmp_dir = TempDir::new().unwrap();
1146 let table_location = tmp_dir.path().to_str().unwrap().to_string();
1147 let file_path = format!("{table_location}/struct_int96.parquet");
1148
1149 let ts_type = SchemaType::primitive_type_builder("ts", PhysicalType::INT96)
1150 .with_repetition(Repetition::OPTIONAL)
1151 .with_id(Some(2))
1152 .build()
1153 .unwrap();
1154
1155 let struct_type = SchemaType::group_type_builder("data")
1156 .with_repetition(Repetition::REQUIRED)
1157 .with_id(Some(1))
1158 .with_fields(vec![Arc::new(ts_type)])
1159 .build()
1160 .unwrap();
1161
1162 let parquet_schema = SchemaType::group_type_builder("schema")
1163 .with_fields(vec![Arc::new(struct_type)])
1164 .build()
1165 .unwrap();
1166
1167 let (int96_val, expected_micros) = make_int96_test_value();
1168
1169 let file = File::create(&file_path).unwrap();
1170 let mut writer =
1171 SerializedFileWriter::new(file, Arc::new(parquet_schema), Default::default()).unwrap();
1172
1173 let mut row_group = writer.next_row_group().unwrap();
1176 {
1177 let mut col = row_group.next_column().unwrap().unwrap();
1178 col.typed::<Int96Type>()
1179 .write_batch(&[int96_val], Some(&[1]), None)
1180 .unwrap();
1181 col.close().unwrap();
1182 }
1183 row_group.close().unwrap();
1184 writer.close().unwrap();
1185
1186 let iceberg_schema = Arc::new(
1187 Schema::builder()
1188 .with_schema_id(1)
1189 .with_fields(vec![
1190 NestedField::required(
1191 1,
1192 "data",
1193 Type::Struct(crate::spec::StructType::new(vec![
1194 NestedField::optional(
1195 2,
1196 "ts",
1197 Type::Primitive(PrimitiveType::Timestamp),
1198 )
1199 .into(),
1200 ])),
1201 )
1202 .into(),
1203 ])
1204 .build()
1205 .unwrap(),
1206 );
1207
1208 let batches = read_int96_batches(&file_path, iceberg_schema, vec![1]).await;
1209
1210 assert_eq!(batches.len(), 1);
1211 let struct_array = batches[0]
1212 .column(0)
1213 .as_any()
1214 .downcast_ref::<StructArray>()
1215 .expect("Expected StructArray");
1216 let ts_array = struct_array
1217 .column(0)
1218 .as_any()
1219 .downcast_ref::<TimestampMicrosecondArray>()
1220 .expect("Expected TimestampMicrosecondArray inside struct");
1221
1222 assert_eq!(
1223 ts_array.value(0),
1224 expected_micros,
1225 "INT96 in struct: got {}, expected {expected_micros}",
1226 ts_array.value(0)
1227 );
1228 }
1229
1230 #[tokio::test]
1231 async fn test_read_int96_timestamps_in_list() {
1232 use arrow_array::{ListArray, TimestampMicrosecondArray};
1233 use parquet::basic::{Repetition, Type as PhysicalType};
1234 use parquet::data_type::Int96Type;
1235 use parquet::file::writer::SerializedFileWriter;
1236 use parquet::schema::types::Type as SchemaType;
1237
1238 let tmp_dir = TempDir::new().unwrap();
1239 let table_location = tmp_dir.path().to_str().unwrap().to_string();
1240 let file_path = format!("{table_location}/list_int96.parquet");
1241
1242 let element_type = SchemaType::primitive_type_builder("element", PhysicalType::INT96)
1249 .with_repetition(Repetition::OPTIONAL)
1250 .with_id(Some(2))
1251 .build()
1252 .unwrap();
1253
1254 let list_group = SchemaType::group_type_builder("list")
1255 .with_repetition(Repetition::REPEATED)
1256 .with_fields(vec![Arc::new(element_type)])
1257 .build()
1258 .unwrap();
1259
1260 let list_type = SchemaType::group_type_builder("timestamps")
1261 .with_repetition(Repetition::OPTIONAL)
1262 .with_id(Some(1))
1263 .with_logical_type(Some(parquet::basic::LogicalType::List))
1264 .with_fields(vec![Arc::new(list_group)])
1265 .build()
1266 .unwrap();
1267
1268 let parquet_schema = SchemaType::group_type_builder("schema")
1269 .with_fields(vec![Arc::new(list_type)])
1270 .build()
1271 .unwrap();
1272
1273 let (int96_val, expected_micros) = make_int96_test_value();
1274
1275 let file = File::create(&file_path).unwrap();
1276 let mut writer =
1277 SerializedFileWriter::new(file, Arc::new(parquet_schema), Default::default()).unwrap();
1278
1279 let mut row_group = writer.next_row_group().unwrap();
1283 {
1284 let mut col = row_group.next_column().unwrap().unwrap();
1285 col.typed::<Int96Type>()
1286 .write_batch(&[int96_val], Some(&[3]), Some(&[0]))
1287 .unwrap();
1288 col.close().unwrap();
1289 }
1290 row_group.close().unwrap();
1291 writer.close().unwrap();
1292
1293 let iceberg_schema = Arc::new(
1294 Schema::builder()
1295 .with_schema_id(1)
1296 .with_fields(vec![
1297 NestedField::optional(
1298 1,
1299 "timestamps",
1300 Type::List(crate::spec::ListType {
1301 element_field: NestedField::optional(
1302 2,
1303 "element",
1304 Type::Primitive(PrimitiveType::Timestamp),
1305 )
1306 .into(),
1307 }),
1308 )
1309 .into(),
1310 ])
1311 .build()
1312 .unwrap(),
1313 );
1314
1315 let batches = read_int96_batches(&file_path, iceberg_schema, vec![1]).await;
1316
1317 assert_eq!(batches.len(), 1);
1318 let list_array = batches[0]
1319 .column(0)
1320 .as_any()
1321 .downcast_ref::<ListArray>()
1322 .expect("Expected ListArray");
1323 let ts_array = list_array
1324 .values()
1325 .as_any()
1326 .downcast_ref::<TimestampMicrosecondArray>()
1327 .expect("Expected TimestampMicrosecondArray inside list");
1328
1329 assert_eq!(
1330 ts_array.value(0),
1331 expected_micros,
1332 "INT96 in list: got {}, expected {expected_micros}",
1333 ts_array.value(0)
1334 );
1335 }
1336
1337 #[tokio::test]
1338 async fn test_read_int96_timestamps_in_map() {
1339 use arrow_array::{MapArray, TimestampMicrosecondArray};
1340 use parquet::basic::{Repetition, Type as PhysicalType};
1341 use parquet::data_type::{ByteArrayType, Int96Type};
1342 use parquet::file::writer::SerializedFileWriter;
1343 use parquet::schema::types::Type as SchemaType;
1344
1345 let tmp_dir = TempDir::new().unwrap();
1346 let table_location = tmp_dir.path().to_str().unwrap().to_string();
1347 let file_path = format!("{table_location}/map_int96.parquet");
1348
1349 let key_type = SchemaType::primitive_type_builder("key", PhysicalType::BYTE_ARRAY)
1357 .with_repetition(Repetition::REQUIRED)
1358 .with_logical_type(Some(parquet::basic::LogicalType::String))
1359 .with_id(Some(2))
1360 .build()
1361 .unwrap();
1362
1363 let value_type = SchemaType::primitive_type_builder("value", PhysicalType::INT96)
1364 .with_repetition(Repetition::OPTIONAL)
1365 .with_id(Some(3))
1366 .build()
1367 .unwrap();
1368
1369 let key_value_group = SchemaType::group_type_builder("key_value")
1370 .with_repetition(Repetition::REPEATED)
1371 .with_fields(vec![Arc::new(key_type), Arc::new(value_type)])
1372 .build()
1373 .unwrap();
1374
1375 let map_type = SchemaType::group_type_builder("ts_map")
1376 .with_repetition(Repetition::OPTIONAL)
1377 .with_id(Some(1))
1378 .with_logical_type(Some(parquet::basic::LogicalType::Map))
1379 .with_fields(vec![Arc::new(key_value_group)])
1380 .build()
1381 .unwrap();
1382
1383 let parquet_schema = SchemaType::group_type_builder("schema")
1384 .with_fields(vec![Arc::new(map_type)])
1385 .build()
1386 .unwrap();
1387
1388 let (int96_val, expected_micros) = make_int96_test_value();
1389
1390 let file = File::create(&file_path).unwrap();
1391 let mut writer =
1392 SerializedFileWriter::new(file, Arc::new(parquet_schema), Default::default()).unwrap();
1393
1394 let mut row_group = writer.next_row_group().unwrap();
1399 {
1400 let mut col = row_group.next_column().unwrap().unwrap();
1401 col.typed::<ByteArrayType>()
1402 .write_batch(
1403 &[parquet::data_type::ByteArray::from("event_time")],
1404 Some(&[2]),
1405 Some(&[0]),
1406 )
1407 .unwrap();
1408 col.close().unwrap();
1409 }
1410 {
1411 let mut col = row_group.next_column().unwrap().unwrap();
1412 col.typed::<Int96Type>()
1413 .write_batch(&[int96_val], Some(&[3]), Some(&[0]))
1414 .unwrap();
1415 col.close().unwrap();
1416 }
1417 row_group.close().unwrap();
1418 writer.close().unwrap();
1419
1420 let iceberg_schema = Arc::new(
1421 Schema::builder()
1422 .with_schema_id(1)
1423 .with_fields(vec![
1424 NestedField::optional(
1425 1,
1426 "ts_map",
1427 Type::Map(crate::spec::MapType {
1428 key_field: NestedField::required(
1429 2,
1430 "key",
1431 Type::Primitive(PrimitiveType::String),
1432 )
1433 .into(),
1434 value_field: NestedField::optional(
1435 3,
1436 "value",
1437 Type::Primitive(PrimitiveType::Timestamp),
1438 )
1439 .into(),
1440 }),
1441 )
1442 .into(),
1443 ])
1444 .build()
1445 .unwrap(),
1446 );
1447
1448 let batches = read_int96_batches(&file_path, iceberg_schema, vec![1]).await;
1449
1450 assert_eq!(batches.len(), 1);
1451 let map_array = batches[0]
1452 .column(0)
1453 .as_any()
1454 .downcast_ref::<MapArray>()
1455 .expect("Expected MapArray");
1456 let ts_array = map_array
1457 .values()
1458 .as_any()
1459 .downcast_ref::<TimestampMicrosecondArray>()
1460 .expect("Expected TimestampMicrosecondArray as map values");
1461
1462 assert_eq!(
1463 ts_array.value(0),
1464 expected_micros,
1465 "INT96 in map: got {}, expected {expected_micros}",
1466 ts_array.value(0)
1467 );
1468 }
1469}