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::{
31 PARQUET_FIELD_ID_META_KEY, ParquetRecordBatchStreamBuilder, ProjectionMask, RowNumber,
32};
33use parquet::encryption::decrypt::FileDecryptionProperties;
34
35use super::{
36 ArrowFileReader, ArrowReader, ParquetReadOptions, add_fallback_field_ids_to_arrow_schema,
37 apply_name_mapping_to_arrow_schema, build_field_id_map,
38};
39use crate::arrow::build_partition_constant;
40use crate::arrow::caching_delete_file_loader::CachingDeleteFileLoader;
41use crate::arrow::int96::coerce_int96_timestamps;
42use crate::arrow::record_batch_transformer::RecordBatchTransformerBuilder;
43use crate::arrow::scan_metrics::{CountingFileRead, ScanMetrics, ScanResult};
44use crate::encryption::StandardKeyMetadata;
45use crate::error::Result;
46use crate::io::{FileIO, FileMetadata, FileRead};
47use crate::metadata_columns::{
48 RESERVED_COL_NAME_LAST_UPDATED_SEQUENCE_NUMBER, RESERVED_COL_NAME_POS, RESERVED_FIELD_ID_FILE,
49 RESERVED_FIELD_ID_LAST_UPDATED_SEQUENCE_NUMBER, RESERVED_FIELD_ID_PARTITION,
50 RESERVED_FIELD_ID_POS, RESERVED_FIELD_ID_SPEC_ID, is_metadata_field,
51};
52use crate::scan::{ArrowRecordBatchStream, FileScanTask, FileScanTaskStream};
53use crate::spec::{Datum, PartitionSpec, Struct};
54use crate::{Error, ErrorKind};
55
56impl ArrowReader {
57 pub fn read(self, tasks: FileScanTaskStream) -> Result<ScanResult> {
60 let concurrency_limit_data_files = self.concurrency_limit_data_files;
61 let scan_metrics = ScanMetrics::new();
62
63 let task_reader = FileScanTaskReader {
64 batch_size: self.batch_size,
65 file_io: self.file_io,
66 delete_file_loader: self
67 .delete_file_loader
68 .with_scan_metrics(scan_metrics.clone()),
69 row_group_filtering_enabled: self.row_group_filtering_enabled,
70 row_selection_enabled: self.row_selection_enabled,
71 parquet_read_options: self.parquet_read_options,
72 scan_metrics: scan_metrics.clone(),
73 };
74
75 let stream: ArrowRecordBatchStream = if concurrency_limit_data_files == 1 {
77 Box::pin(
78 tasks
79 .and_then(move |task| task_reader.clone().process(task))
80 .map_err(|err| {
81 Error::new(ErrorKind::Unexpected, "file scan task generate failed")
82 .with_source(err)
83 })
84 .try_flatten(),
85 )
86 } else {
87 Box::pin(
88 tasks
89 .map_ok(move |task| task_reader.clone().process(task))
90 .map_err(|err| {
91 Error::new(ErrorKind::Unexpected, "file scan task generate failed")
92 .with_source(err)
93 })
94 .try_buffer_unordered(concurrency_limit_data_files)
95 .try_flatten_unordered(concurrency_limit_data_files),
96 )
97 };
98
99 Ok(ScanResult::new(stream, scan_metrics))
100 }
101}
102
103#[derive(Clone)]
106struct FileScanTaskReader {
107 batch_size: Option<usize>,
108 file_io: FileIO,
109 delete_file_loader: CachingDeleteFileLoader,
110 row_group_filtering_enabled: bool,
111 row_selection_enabled: bool,
112 parquet_read_options: ParquetReadOptions,
113 scan_metrics: ScanMetrics,
114}
115
116impl FileScanTaskReader {
117 async fn process(self, task: FileScanTask) -> Result<ArrowRecordBatchStream> {
118 let should_load_page_index =
119 (self.row_selection_enabled && task.predicate.is_some()) || !task.deletes.is_empty();
120 let mut parquet_read_options = self.parquet_read_options;
121 parquet_read_options.preload_page_index = should_load_page_index;
122
123 let delete_filter_rx = self
124 .delete_file_loader
125 .load_deletes(&task.deletes, Arc::clone(&task.schema));
126
127 let (parquet_file_reader, arrow_metadata) = ArrowReader::open_parquet_file(
129 &task.data_file_path,
130 &self.file_io,
131 task.file_size_in_bytes,
132 parquet_read_options,
133 self.scan_metrics.bytes_read_counter(),
134 task.key_metadata.as_deref(),
135 )
136 .await?;
137
138 let missing_field_ids = arrow_metadata
142 .schema()
143 .fields()
144 .iter()
145 .next()
146 .is_some_and(|f| f.metadata().get(PARQUET_FIELD_ID_META_KEY).is_none());
147
148 let use_position_fallback = missing_field_ids && task.name_mapping.is_none();
153
154 let arrow_metadata = if missing_field_ids {
170 let arrow_schema = if let Some(name_mapping) = &task.name_mapping {
172 apply_name_mapping_to_arrow_schema(
177 Arc::clone(arrow_metadata.schema()),
178 name_mapping,
179 )?
180 } else {
181 add_fallback_field_ids_to_arrow_schema(arrow_metadata.schema())
184 };
185
186 let options = ArrowReaderOptions::new().with_schema(arrow_schema);
187 ArrowReaderMetadata::try_new(Arc::clone(arrow_metadata.metadata()), options).map_err(
188 |e| {
189 Error::new(
190 ErrorKind::Unexpected,
191 "Failed to create ArrowReaderMetadata with field ID schema",
192 )
193 .with_source(e)
194 },
195 )?
196 } else {
197 arrow_metadata
199 };
200
201 let arrow_metadata = if let Some(coerced_schema) =
204 coerce_int96_timestamps(arrow_metadata.schema(), &task.schema)
205 {
206 let options = ArrowReaderOptions::new().with_schema(Arc::clone(&coerced_schema));
207 ArrowReaderMetadata::try_new(Arc::clone(arrow_metadata.metadata()), options).map_err(
208 |e| {
209 Error::new(
210 ErrorKind::Unexpected,
211 format!(
212 "Failed to create ArrowReaderMetadata with INT96-coerced schema: {coerced_schema}"
213 ),
214 )
215 .with_source(e)
216 },
217 )?
218 } else {
219 arrow_metadata
220 };
221
222 let project_pos = task.project_field_ids().contains(&RESERVED_FIELD_ID_POS);
223
224 let arrow_metadata = if project_pos {
225 let row_number_field = Arc::new(
226 Field::new(RESERVED_COL_NAME_POS, DataType::Int64, false)
227 .with_metadata(HashMap::from([(
228 PARQUET_FIELD_ID_META_KEY.to_string(),
229 RESERVED_FIELD_ID_POS.to_string(),
230 )]))
231 .with_extension_type(RowNumber),
232 );
233
234 let options = ArrowReaderOptions::new()
235 .with_schema(Arc::clone(arrow_metadata.schema()))
236 .with_virtual_columns(vec![row_number_field])?;
237
238 ArrowReaderMetadata::try_new(Arc::clone(arrow_metadata.metadata()), options).map_err(
239 |e| {
240 Error::new(
241 ErrorKind::Unexpected,
242 "Failed to create ArrowReaderMetadata with the 'row_number' virtual_column",
243 )
244 .with_source(e)
245 },
246 )?
247 } else {
248 arrow_metadata
249 };
250
251 let mut record_batch_stream_builder =
253 ParquetRecordBatchStreamBuilder::new_with_metadata(parquet_file_reader, arrow_metadata);
254
255 let project_last_updated_seq = task
259 .project_field_ids()
260 .contains(&RESERVED_FIELD_ID_LAST_UPDATED_SEQUENCE_NUMBER);
261
262 let phys_last_updated_seq_leaf = if project_last_updated_seq {
267 build_field_id_map(record_batch_stream_builder.parquet_schema())?.and_then(|m| {
268 m.get(&RESERVED_FIELD_ID_LAST_UPDATED_SEQUENCE_NUMBER)
269 .copied()
270 })
271 } else {
272 None
273 };
274
275 let last_updated_seq_present_by_name_only = project_last_updated_seq
278 && phys_last_updated_seq_leaf.is_none()
279 && record_batch_stream_builder
280 .schema()
281 .fields()
282 .iter()
283 .any(|f| f.name() == RESERVED_COL_NAME_LAST_UPDATED_SEQUENCE_NUMBER);
284
285 let coalesce_last_updated_seq_leaf = phys_last_updated_seq_leaf
290 .filter(|_| task.first_row_id.is_some() && task.data_sequence_number.is_some());
291
292 let project_field_ids_without_metadata: Vec<i32> = task
294 .project_field_ids
295 .iter()
296 .filter(|&&id| !is_metadata_field(id))
297 .copied()
298 .collect();
299
300 let mut projection_mask = ArrowReader::get_arrow_projection_mask(
306 &project_field_ids_without_metadata,
307 &task.schema,
308 record_batch_stream_builder.parquet_schema(),
309 record_batch_stream_builder.schema(),
310 use_position_fallback, )?;
312
313 if project_field_ids_without_metadata.is_empty()
325 && (project_pos || coalesce_last_updated_seq_leaf.is_some())
326 {
327 projection_mask =
328 ProjectionMask::none(record_batch_stream_builder.parquet_schema().num_columns());
329 }
330
331 if let Some(leaf) = coalesce_last_updated_seq_leaf {
336 let phys_mask =
337 ProjectionMask::leaves(record_batch_stream_builder.parquet_schema(), vec![leaf]);
338 projection_mask.union(&phys_mask);
339 }
340
341 record_batch_stream_builder =
342 record_batch_stream_builder.with_projection(projection_mask.clone());
343
344 let mut record_batch_transformer_builder =
348 RecordBatchTransformerBuilder::new(task.schema_ref(), task.project_field_ids());
349
350 if task.project_field_ids().contains(&RESERVED_FIELD_ID_FILE) {
352 let file_datum = Datum::string(task.data_file_path.clone());
353 record_batch_transformer_builder =
354 record_batch_transformer_builder.with_constant(RESERVED_FIELD_ID_FILE, file_datum);
355 }
356
357 if task
358 .project_field_ids()
359 .contains(&RESERVED_FIELD_ID_SPEC_ID)
360 {
361 let partition_spec = task
362 .partition_spec
363 .as_ref()
364 .ok_or_else(|| Error::new(ErrorKind::Unexpected, "Partition spec is missing"))?;
365
366 let spec_id_datum = Datum::int(partition_spec.spec_id());
367 record_batch_transformer_builder = record_batch_transformer_builder
368 .with_constant(RESERVED_FIELD_ID_SPEC_ID, spec_id_datum);
369 }
370
371 if project_last_updated_seq {
372 record_batch_transformer_builder = match (task.first_row_id, task.data_sequence_number)
377 {
378 (Some(_), Some(seq)) => {
379 let datum = Datum::long(seq);
380 if coalesce_last_updated_seq_leaf.is_some() {
381 record_batch_transformer_builder.with_coalesced_last_updated_seq_column(
384 RESERVED_FIELD_ID_LAST_UPDATED_SEQUENCE_NUMBER,
385 datum,
386 )
387 } else if last_updated_seq_present_by_name_only {
388 return Err(Error::new(
396 ErrorKind::FeatureUnsupported,
397 "Reading a physically-stored _last_updated_sequence_number column \
398 without an embedded field id is not supported",
399 ));
400 } else {
401 record_batch_transformer_builder
403 .with_constant(RESERVED_FIELD_ID_LAST_UPDATED_SEQUENCE_NUMBER, datum)
404 }
405 }
406 (None, _) => record_batch_transformer_builder
408 .with_null_metadata_column(RESERVED_FIELD_ID_LAST_UPDATED_SEQUENCE_NUMBER)?,
409 (Some(_), None) => {
413 return Err(Error::new(
414 ErrorKind::DataInvalid,
415 format!(
416 "Data file {} has a first_row_id but no data sequence number",
417 task.data_file_path
418 ),
419 ));
420 }
421 };
422 }
423
424 if let (Some(partition_spec), Some(partition_data)) =
425 (task.partition_spec.clone(), task.partition.clone())
426 {
427 record_batch_transformer_builder =
428 record_batch_transformer_builder.with_partition(partition_spec, partition_data)?;
429 }
430
431 if project_pos {
432 record_batch_transformer_builder =
433 record_batch_transformer_builder.with_virtual_field(RESERVED_FIELD_ID_POS);
434 }
435
436 if task
439 .project_field_ids()
440 .contains(&RESERVED_FIELD_ID_PARTITION)
441 && let Some(unified_type) = &task.unified_partition_type
442 {
443 let (spec, partition_data) = match (&task.partition_spec, &task.partition) {
444 (Some(spec), Some(data)) => (spec.clone(), data.clone()),
445 _ if unified_type.fields().is_empty() => {
450 (Arc::new(PartitionSpec::unpartition_spec()), Struct::empty())
451 }
452 _ => {
453 return Err(Error::new(
454 ErrorKind::Unexpected,
455 "cannot build _partition column: unified partition type has fields \
456 but the scan task is missing its partition spec or data",
457 ));
458 }
459 };
460 let constant = build_partition_constant(unified_type, &spec, &partition_data)?;
461 record_batch_transformer_builder =
462 record_batch_transformer_builder.with_partition_constant(constant);
463 }
464
465 let mut record_batch_transformer = record_batch_transformer_builder.build();
466
467 if let Some(batch_size) = self.batch_size {
468 record_batch_stream_builder = record_batch_stream_builder.with_batch_size(batch_size);
469 }
470
471 let delete_filter = delete_filter_rx.await.unwrap()?;
472 let delete_predicate = delete_filter.build_equality_delete_predicate(&task).await?;
473
474 let final_predicate = match (&task.predicate, delete_predicate) {
479 (None, None) => None,
480 (Some(predicate), None) => Some(predicate.clone()),
481 (None, Some(ref predicate)) => Some(predicate.clone()),
482 (Some(filter_predicate), Some(delete_predicate)) => {
483 Some(filter_predicate.clone().and(delete_predicate))
484 }
485 };
486
487 let mut selected_row_group_indices = None;
503 let mut row_selection = None;
504
505 if task.start != 0 || task.length != 0 {
508 let byte_range_filtered_row_groups = ArrowReader::filter_row_groups_by_byte_range(
509 record_batch_stream_builder.metadata(),
510 task.start,
511 task.length,
512 )?;
513 selected_row_group_indices = Some(byte_range_filtered_row_groups);
514 }
515
516 if let Some(predicate) = final_predicate {
517 let (iceberg_field_ids, field_id_map) = ArrowReader::build_field_id_set_and_map(
518 record_batch_stream_builder.parquet_schema(),
519 record_batch_stream_builder.schema(),
520 &predicate,
521 use_position_fallback,
522 )?;
523
524 let row_filter = ArrowReader::get_row_filter(
525 &predicate,
526 record_batch_stream_builder.parquet_schema(),
527 &iceberg_field_ids,
528 &field_id_map,
529 )?;
530 record_batch_stream_builder = record_batch_stream_builder.with_row_filter(row_filter);
531
532 if self.row_group_filtering_enabled {
533 let predicate_filtered_row_groups = ArrowReader::get_selected_row_group_indices(
534 &predicate,
535 record_batch_stream_builder.metadata(),
536 &field_id_map,
537 &task.schema,
538 )?;
539
540 selected_row_group_indices = match selected_row_group_indices {
543 Some(byte_range_filtered) => {
544 let intersection: Vec<usize> = byte_range_filtered
546 .into_iter()
547 .filter(|idx| predicate_filtered_row_groups.contains(idx))
548 .collect();
549 Some(intersection)
550 }
551 None => Some(predicate_filtered_row_groups),
552 };
553 }
554
555 if self.row_selection_enabled {
556 row_selection = ArrowReader::get_row_selection_for_filter_predicate(
557 &predicate,
558 record_batch_stream_builder.metadata(),
559 &selected_row_group_indices,
560 &field_id_map,
561 &task.schema,
562 )?;
563 }
564 }
565
566 let positional_delete_indexes = delete_filter.get_delete_vector(&task);
567
568 if let Some(positional_delete_indexes) = positional_delete_indexes {
569 let delete_row_selection = {
570 let positional_delete_indexes = positional_delete_indexes.lock().unwrap();
571
572 ArrowReader::build_deletes_row_selection(
573 record_batch_stream_builder.metadata().row_groups(),
574 &selected_row_group_indices,
575 &positional_delete_indexes,
576 )
577 }?;
578
579 row_selection = match row_selection {
582 None => Some(delete_row_selection),
583 Some(filter_row_selection) => {
584 Some(filter_row_selection.intersection(&delete_row_selection))
585 }
586 };
587 }
588
589 if let Some(row_selection) = row_selection {
590 record_batch_stream_builder =
591 record_batch_stream_builder.with_row_selection(row_selection);
592 }
593
594 if let Some(selected_row_group_indices) = selected_row_group_indices {
595 record_batch_stream_builder =
596 record_batch_stream_builder.with_row_groups(selected_row_group_indices);
597 }
598
599 let record_batch_stream =
602 record_batch_stream_builder
603 .build()?
604 .map(move |batch| match batch {
605 Ok(batch) => {
606 record_batch_transformer.process_record_batch(batch)
608 }
609 Err(err) => Err(err.into()),
610 });
611
612 Ok(Box::pin(record_batch_stream) as ArrowRecordBatchStream)
613 }
614}
615
616impl ArrowReader {
617 pub(crate) async fn open_parquet_file(
620 data_file_path: &str,
621 file_io: &FileIO,
622 file_size_in_bytes: u64,
623 parquet_read_options: ParquetReadOptions,
624 bytes_read: &Arc<AtomicU64>,
625 key_metadata: Option<&[u8]>,
626 ) -> Result<(ArrowFileReader, ArrowReaderMetadata)> {
627 let parquet_file = file_io.new_input(data_file_path)?;
628 let counting_reader =
629 CountingFileRead::new(parquet_file.reader().await?, Arc::clone(bytes_read));
630 Self::build_parquet_reader(
631 Box::new(counting_reader),
632 file_size_in_bytes,
633 parquet_read_options,
634 key_metadata,
635 )
636 .await
637 }
638
639 async fn build_parquet_reader(
640 parquet_reader: Box<dyn FileRead>,
641 file_size_in_bytes: u64,
642 parquet_read_options: ParquetReadOptions,
643 key_metadata: Option<&[u8]>,
644 ) -> Result<(ArrowFileReader, ArrowReaderMetadata)> {
645 let mut reader = ArrowFileReader::new(
646 FileMetadata {
647 size: file_size_in_bytes,
648 },
649 parquet_reader,
650 )
651 .with_parquet_read_options(parquet_read_options);
652
653 let arrow_reader_options = Self::build_arrow_reader_options(key_metadata)?;
654
655 let arrow_metadata = ArrowReaderMetadata::load_async(&mut reader, arrow_reader_options)
656 .await
657 .map_err(|e| {
658 Error::new(ErrorKind::Unexpected, "Failed to load Parquet metadata").with_source(e)
659 })?;
660
661 Ok((reader, arrow_metadata))
662 }
663
664 fn build_arrow_reader_options(key_metadata: Option<&[u8]>) -> Result<ArrowReaderOptions> {
667 match key_metadata {
668 Some(km) => {
669 let standard_key_metadata = StandardKeyMetadata::decode(km)?;
670 let mut builder = FileDecryptionProperties::builder(
671 standard_key_metadata.encryption_key().as_bytes().to_vec(),
672 );
673 if let Some(aad) = standard_key_metadata.aad_prefix() {
674 builder = builder.with_aad_prefix(aad.to_vec());
675 }
676 let decryption_properties = builder.build().map_err(|e| {
677 Error::new(
678 ErrorKind::Unexpected,
679 "Failed to build Parquet file decryption properties",
680 )
681 .with_source(e)
682 })?;
683 Ok(
684 ArrowReaderOptions::new()
685 .with_file_decryption_properties(decryption_properties),
686 )
687 }
688 None => Ok(ArrowReaderOptions::default()),
689 }
690 }
691}
692
693#[cfg(test)]
694mod tests {
695 use std::collections::HashMap;
696 use std::fs::File;
697 use std::sync::Arc;
698
699 use arrow_array::cast::AsArray;
700 use arrow_array::{Array, ArrayRef, Int32Array, Int64Array, RecordBatch, StringArray};
701 use arrow_cast::cast;
702 use arrow_schema::{DataType, Field, Schema as ArrowSchema};
703 use futures::TryStreamExt;
704 use parquet::arrow::{ArrowWriter, PARQUET_FIELD_ID_META_KEY};
705 use parquet::basic::Compression;
706 use parquet::file::properties::WriterProperties;
707 use tempfile::TempDir;
708
709 use crate::Runtime;
710 use crate::arrow::ArrowReaderBuilder;
711 use crate::arrow::test_utils::write_encrypted_parquet;
712 use crate::io::FileIO;
713 use crate::metadata_columns::{
714 RESERVED_COL_NAME_POS, RESERVED_FIELD_ID_FILE, RESERVED_FIELD_ID_POS,
715 };
716 use crate::scan::{FileScanTask, FileScanTaskStream};
717 use crate::spec::{DataFileFormat, NestedField, PrimitiveType, Schema, SchemaRef, Type};
718
719 const UNIX_EPOCH_JULIAN: i64 = 2_440_588;
722 const MICROS_PER_DAY: i64 = 86_400_000_000;
723 const INT96_TEST_NANOS_WITHIN_DAY: u64 = 43_200_000_000_000;
725 const INT96_TEST_JULIAN_DAY: u32 = 2_953_529;
726
727 fn make_int96_test_value() -> (parquet::data_type::Int96, i64) {
728 let mut val = parquet::data_type::Int96::new();
729 val.set_data(
730 (INT96_TEST_NANOS_WITHIN_DAY & 0xFFFFFFFF) as u32,
731 (INT96_TEST_NANOS_WITHIN_DAY >> 32) as u32,
732 INT96_TEST_JULIAN_DAY,
733 );
734 let expected_micros = (INT96_TEST_JULIAN_DAY as i64 - UNIX_EPOCH_JULIAN) * MICROS_PER_DAY
735 + (INT96_TEST_NANOS_WITHIN_DAY / 1_000) as i64;
736 (val, expected_micros)
737 }
738
739 async fn read_int96_batches(
740 file_path: &str,
741 schema: SchemaRef,
742 project_field_ids: Vec<i32>,
743 ) -> Vec<RecordBatch> {
744 let file_io = FileIO::new_with_fs();
745 let reader = ArrowReaderBuilder::new(file_io, Runtime::current()).build();
746
747 let file_size = std::fs::metadata(file_path).unwrap().len();
748 let task = FileScanTask::builder()
749 .with_file_size_in_bytes(file_size)
750 .with_start(0)
751 .with_length(file_size)
752 .with_data_file_path(file_path.to_string())
753 .with_data_file_format(DataFileFormat::Parquet)
754 .with_schema(schema)
755 .with_project_field_ids(project_field_ids)
756 .with_case_sensitive(false)
757 .build();
758
759 let tasks = Box::pin(futures::stream::iter(vec![Ok(task)])) as FileScanTaskStream;
760 reader
761 .read(tasks)
762 .unwrap()
763 .stream()
764 .try_collect()
765 .await
766 .unwrap()
767 }
768
769 fn write_int96_parquet_file(
771 table_location: &str,
772 filename: &str,
773 with_field_ids: bool,
774 ) -> (String, Vec<i64>) {
775 use parquet::basic::{Repetition, Type as PhysicalType};
776 use parquet::data_type::{Int32Type, Int96, Int96Type};
777 use parquet::file::writer::SerializedFileWriter;
778 use parquet::schema::types::Type as SchemaType;
779
780 let file_path = format!("{table_location}/{filename}");
781
782 let mut ts_builder = SchemaType::primitive_type_builder("ts", PhysicalType::INT96)
783 .with_repetition(Repetition::OPTIONAL);
784 let mut id_builder = SchemaType::primitive_type_builder("id", PhysicalType::INT32)
785 .with_repetition(Repetition::REQUIRED);
786
787 if with_field_ids {
788 ts_builder = ts_builder.with_id(Some(1));
789 id_builder = id_builder.with_id(Some(2));
790 }
791
792 let schema = SchemaType::group_type_builder("schema")
793 .with_fields(vec![
794 Arc::new(ts_builder.build().unwrap()),
795 Arc::new(id_builder.build().unwrap()),
796 ])
797 .build()
798 .unwrap();
799
800 const NOON_NANOS: u64 = INT96_TEST_NANOS_WITHIN_DAY;
802 const JULIAN_3333: u32 = INT96_TEST_JULIAN_DAY;
803 const JULIAN_2100: u32 = 2_488_070;
804
805 let test_data: Vec<(u32, u32, u32, i64)> = vec![
806 (
808 0,
809 0,
810 JULIAN_3333,
811 (JULIAN_3333 as i64 - UNIX_EPOCH_JULIAN) * MICROS_PER_DAY,
812 ),
813 (
815 (NOON_NANOS & 0xFFFFFFFF) as u32,
816 (NOON_NANOS >> 32) as u32,
817 JULIAN_3333,
818 (JULIAN_3333 as i64 - UNIX_EPOCH_JULIAN) * MICROS_PER_DAY
819 + (NOON_NANOS / 1_000) as i64,
820 ),
821 (
823 0,
824 0,
825 JULIAN_2100,
826 (JULIAN_2100 as i64 - UNIX_EPOCH_JULIAN) * MICROS_PER_DAY,
827 ),
828 ];
829
830 let int96_values: Vec<Int96> = test_data
831 .iter()
832 .map(|(lo, hi, day, _)| {
833 let mut v = Int96::new();
834 v.set_data(*lo, *hi, *day);
835 v
836 })
837 .collect();
838
839 let id_values: Vec<i32> = (0..test_data.len() as i32).collect();
840 let expected_micros: Vec<i64> = test_data.iter().map(|(_, _, _, m)| *m).collect();
841
842 let file = File::create(&file_path).unwrap();
843 let mut writer =
844 SerializedFileWriter::new(file, Arc::new(schema), Default::default()).unwrap();
845
846 let mut row_group = writer.next_row_group().unwrap();
847 {
848 let mut col = row_group.next_column().unwrap().unwrap();
850 col.typed::<Int96Type>()
851 .write_batch(&int96_values, Some(&vec![1; test_data.len()]), None)
852 .unwrap();
853 col.close().unwrap();
854 }
855 {
856 let mut col = row_group.next_column().unwrap().unwrap();
857 col.typed::<Int32Type>()
858 .write_batch(&id_values, None, None)
859 .unwrap();
860 col.close().unwrap();
861 }
862 row_group.close().unwrap();
863 writer.close().unwrap();
864
865 (file_path, expected_micros)
866 }
867
868 async fn assert_int96_read_matches(
869 file_path: &str,
870 schema: SchemaRef,
871 project_field_ids: Vec<i32>,
872 expected_micros: &[i64],
873 ) {
874 use arrow_array::TimestampMicrosecondArray;
875
876 let batches = read_int96_batches(file_path, schema, project_field_ids).await;
877
878 assert_eq!(batches.len(), 1);
879 let ts_array = batches[0]
880 .column(0)
881 .as_any()
882 .downcast_ref::<TimestampMicrosecondArray>()
883 .expect("Expected TimestampMicrosecondArray");
884
885 for (i, expected) in expected_micros.iter().enumerate() {
886 assert_eq!(
887 ts_array.value(i),
888 *expected,
889 "Row {i}: got {}, expected {expected}",
890 ts_array.value(i)
891 );
892 }
893 }
894
895 async fn assert_encrypted_parquet_roundtrip(encryption_key: &[u8]) {
899 let aad_prefix = b"aad_prefix";
900
901 let schema = Arc::new(
902 Schema::builder()
903 .with_schema_id(1)
904 .with_fields(vec![
905 NestedField::required(1, "id", Type::Primitive(PrimitiveType::Int)).into(),
906 ])
907 .build()
908 .unwrap(),
909 );
910
911 let arrow_schema = Arc::new(ArrowSchema::new(vec![
912 Field::new("id", DataType::Int32, false).with_metadata(HashMap::from([(
913 PARQUET_FIELD_ID_META_KEY.to_string(),
914 "1".to_string(),
915 )])),
916 ]));
917
918 let tmp_dir = TempDir::new().unwrap();
919 let table_location = tmp_dir.path().to_str().unwrap().to_string();
920 let file_io = FileIO::new_with_fs();
921
922 let id_data = Arc::new(Int32Array::from(vec![10, 20, 30])) as ArrayRef;
923 let batch = RecordBatch::try_new(arrow_schema.clone(), vec![id_data]).unwrap();
924
925 let file_path = format!("{table_location}/encrypted.parquet");
926 write_encrypted_parquet(&file_path, &batch, encryption_key, Some(aad_prefix));
927
928 let key_metadata = crate::encryption::StandardKeyMetadata::try_new(encryption_key)
929 .unwrap()
930 .with_aad_prefix(aad_prefix)
931 .encode()
932 .unwrap();
933
934 let reader = ArrowReaderBuilder::new(file_io, Runtime::current()).build();
935
936 let task = FileScanTask::builder()
937 .with_file_size_in_bytes(std::fs::metadata(&file_path).unwrap().len())
938 .with_start(0)
939 .with_length(0)
940 .with_data_file_path(file_path)
941 .with_data_file_format(DataFileFormat::Parquet)
942 .with_schema(schema)
943 .with_project_field_ids(vec![1])
944 .with_case_sensitive(false)
945 .with_key_metadata(Some(key_metadata))
946 .build();
947
948 let tasks = Box::pin(futures::stream::iter(vec![Ok(task)])) as FileScanTaskStream;
949 let batches: Vec<RecordBatch> = reader
950 .read(tasks)
951 .unwrap()
952 .stream()
953 .try_collect()
954 .await
955 .unwrap();
956
957 assert_eq!(batches.len(), 1);
958 let ids = batches[0]
959 .column(0)
960 .as_any()
961 .downcast_ref::<Int32Array>()
962 .unwrap();
963 assert_eq!(ids.values(), &[10, 20, 30]);
964 }
965
966 #[tokio::test]
967 async fn test_read_encrypted_parquet_aes_128() {
968 assert_encrypted_parquet_roundtrip(b"0123456789abcdef").await;
969 }
970
971 #[tokio::test]
972 async fn test_read_encrypted_parquet_aes_256() {
973 assert_encrypted_parquet_roundtrip(b"0123456789abcdef0123456789abcdef").await;
974 }
975
976 #[tokio::test]
977 async fn test_read_encrypted_parquet_without_key_metadata_fails() {
978 let encryption_key = b"0123456789abcdef";
979
980 let schema = Arc::new(
981 Schema::builder()
982 .with_schema_id(1)
983 .with_fields(vec![
984 NestedField::required(1, "id", Type::Primitive(PrimitiveType::Int)).into(),
985 ])
986 .build()
987 .unwrap(),
988 );
989
990 let arrow_schema = Arc::new(ArrowSchema::new(vec![
991 Field::new("id", DataType::Int32, false).with_metadata(HashMap::from([(
992 PARQUET_FIELD_ID_META_KEY.to_string(),
993 "1".to_string(),
994 )])),
995 ]));
996
997 let tmp_dir = TempDir::new().unwrap();
998 let table_location = tmp_dir.path().to_str().unwrap().to_string();
999 let file_io = FileIO::new_with_fs();
1000
1001 let id_data = Arc::new(Int32Array::from(vec![1, 2, 3])) as ArrayRef;
1002 let batch = RecordBatch::try_new(arrow_schema.clone(), vec![id_data]).unwrap();
1003
1004 let file_path = format!("{table_location}/encrypted_no_key.parquet");
1005 write_encrypted_parquet(&file_path, &batch, encryption_key, None);
1006
1007 let reader = ArrowReaderBuilder::new(file_io, Runtime::current()).build();
1008
1009 let task = FileScanTask::builder()
1010 .with_file_size_in_bytes(std::fs::metadata(&file_path).unwrap().len())
1011 .with_start(0)
1012 .with_length(0)
1013 .with_data_file_path(file_path)
1014 .with_data_file_format(DataFileFormat::Parquet)
1015 .with_schema(schema)
1016 .with_project_field_ids(vec![1])
1017 .with_case_sensitive(false)
1018 .build();
1019
1020 let tasks = Box::pin(futures::stream::iter(vec![Ok(task)])) as FileScanTaskStream;
1021 let result: Result<Vec<RecordBatch>, _> =
1022 reader.read(tasks).unwrap().stream().try_collect().await;
1023
1024 let err = result.unwrap_err();
1025 assert_eq!(err.kind(), crate::ErrorKind::Unexpected);
1026 let err_str = format!("{err}");
1027 assert!(
1028 err_str.contains("encrypted footer"),
1029 "Expected error about encrypted footer, got: {err_str}"
1030 );
1031 assert!(
1032 err_str.contains("decryption properties were not provided"),
1033 "Expected error about missing decryption properties, got: {err_str}"
1034 );
1035 }
1036
1037 fn write_plain_parquet(
1040 dir: &str,
1041 name: &str,
1042 extra_fields: Vec<Field>,
1043 extra_columns: Vec<ArrayRef>,
1044 ) -> String {
1045 let mut fields =
1046 vec![
1047 Field::new("id", DataType::Int32, false).with_metadata(HashMap::from([(
1048 PARQUET_FIELD_ID_META_KEY.to_string(),
1049 "1".to_string(),
1050 )])),
1051 ];
1052 fields.extend(extra_fields);
1053 let arrow_schema = Arc::new(ArrowSchema::new(fields));
1054
1055 let mut columns: Vec<ArrayRef> = vec![Arc::new(Int32Array::from(vec![1, 2, 3]))];
1056 columns.extend(extra_columns);
1057 let batch = RecordBatch::try_new(arrow_schema.clone(), columns).unwrap();
1058
1059 let file_path = format!("{dir}/{name}");
1060 let file = File::create(&file_path).unwrap();
1061 let props = WriterProperties::builder()
1062 .set_compression(Compression::SNAPPY)
1063 .build();
1064 let mut writer = ArrowWriter::try_new(file, arrow_schema, Some(props)).unwrap();
1065 writer.write(&batch).unwrap();
1066 writer.close().unwrap();
1067 file_path
1068 }
1069
1070 fn last_updated_seq_task(
1071 file_path: String,
1072 first_row_id: Option<i64>,
1073 data_sequence_number: Option<i64>,
1074 ) -> FileScanTask {
1075 use crate::metadata_columns::RESERVED_FIELD_ID_LAST_UPDATED_SEQUENCE_NUMBER;
1076
1077 let schema = Arc::new(
1078 Schema::builder()
1079 .with_schema_id(1)
1080 .with_fields(vec![
1081 NestedField::required(1, "id", Type::Primitive(PrimitiveType::Int)).into(),
1082 ])
1083 .build()
1084 .unwrap(),
1085 );
1086
1087 FileScanTask::builder()
1088 .with_file_size_in_bytes(std::fs::metadata(&file_path).unwrap().len())
1089 .with_start(0)
1090 .with_length(0)
1091 .with_data_file_path(file_path)
1092 .with_data_file_format(DataFileFormat::Parquet)
1093 .with_schema(schema)
1094 .with_project_field_ids(vec![1, RESERVED_FIELD_ID_LAST_UPDATED_SEQUENCE_NUMBER])
1095 .with_first_row_id(first_row_id)
1096 .with_data_sequence_number(data_sequence_number)
1097 .with_case_sensitive(false)
1098 .build()
1099 }
1100
1101 fn assert_last_updated_seq_column(batches: &[RecordBatch], expected: &[Option<i64>]) {
1104 use arrow_array::cast::AsArray;
1105 use arrow_cast::cast;
1106 use arrow_schema::DataType;
1107
1108 use crate::metadata_columns::RESERVED_COL_NAME_LAST_UPDATED_SEQUENCE_NUMBER;
1109
1110 let mut actual = Vec::new();
1111 for batch in batches {
1112 let col = batch
1113 .column_by_name(RESERVED_COL_NAME_LAST_UPDATED_SEQUENCE_NUMBER)
1114 .expect("_last_updated_sequence_number column should be present");
1115 let logical = cast(col, &DataType::Int64).unwrap();
1116 let values = logical.as_primitive::<arrow_array::types::Int64Type>();
1117 for i in 0..values.len() {
1118 actual.push((!values.is_null(i)).then(|| values.value(i)));
1119 }
1120 }
1121 assert_eq!(actual, expected);
1122 }
1123
1124 #[tokio::test]
1125 async fn test_last_updated_sequence_number_null_when_no_first_row_id() {
1126 let tmp_dir = TempDir::new().unwrap();
1127 let dir = tmp_dir.path().to_str().unwrap();
1128 let file_path = write_plain_parquet(dir, "no_first_row_id.parquet", vec![], vec![]);
1129
1130 let task = last_updated_seq_task(file_path, None, Some(9));
1134
1135 let reader = ArrowReaderBuilder::new(FileIO::new_with_fs(), Runtime::current()).build();
1136 let tasks = Box::pin(futures::stream::iter(vec![Ok(task)])) as FileScanTaskStream;
1137 let batches: Vec<RecordBatch> = reader
1138 .read(tasks)
1139 .unwrap()
1140 .stream()
1141 .try_collect()
1142 .await
1143 .unwrap();
1144
1145 assert_last_updated_seq_column(&batches, &[None, None, None]);
1146 }
1147
1148 #[tokio::test]
1149 async fn test_last_updated_sequence_number_error_when_no_data_seq() {
1150 let tmp_dir = TempDir::new().unwrap();
1151 let dir = tmp_dir.path().to_str().unwrap();
1152 let file_path = write_plain_parquet(dir, "no_data_seq.parquet", vec![], vec![]);
1153
1154 let task = last_updated_seq_task(file_path, Some(42), None);
1158
1159 let reader = ArrowReaderBuilder::new(FileIO::new_with_fs(), Runtime::current()).build();
1160 let tasks = Box::pin(futures::stream::iter(vec![Ok(task)])) as FileScanTaskStream;
1161 let result: Result<Vec<RecordBatch>, _> =
1162 reader.read(tasks).unwrap().stream().try_collect().await;
1163
1164 let err = result.unwrap_err();
1165 assert_eq!(err.kind(), crate::ErrorKind::DataInvalid);
1166 assert!(
1167 format!("{err}").contains("no data sequence number"),
1168 "unexpected error: {err}"
1169 );
1170 }
1171
1172 #[tokio::test]
1173 async fn test_last_updated_sequence_number_derived_from_data_seq() {
1174 let tmp_dir = TempDir::new().unwrap();
1175 let dir = tmp_dir.path().to_str().unwrap();
1176 let file_path = write_plain_parquet(dir, "with_first_row_id.parquet", vec![], vec![]);
1177
1178 let task = last_updated_seq_task(file_path, Some(42), Some(7));
1181
1182 let reader = ArrowReaderBuilder::new(FileIO::new_with_fs(), Runtime::current()).build();
1183 let tasks = Box::pin(futures::stream::iter(vec![Ok(task)])) as FileScanTaskStream;
1184 let batches: Vec<RecordBatch> = reader
1185 .read(tasks)
1186 .unwrap()
1187 .stream()
1188 .try_collect()
1189 .await
1190 .unwrap();
1191
1192 assert_last_updated_seq_column(&batches, &[Some(7), Some(7), Some(7)]);
1193 }
1194
1195 #[tokio::test]
1196 async fn test_last_updated_sequence_number_mixed_files_share_schema() {
1197 use arrow_select::concat::concat_batches;
1198
1199 let tmp_dir = TempDir::new().unwrap();
1200 let dir = tmp_dir.path().to_str().unwrap();
1201
1202 let constant = last_updated_seq_task(
1208 write_plain_parquet(dir, "constant.parquet", vec![], vec![]),
1209 Some(42),
1210 Some(7),
1211 );
1212 let nulled = last_updated_seq_task(
1213 write_plain_parquet(dir, "nulled.parquet", vec![], vec![]),
1214 None,
1215 Some(7),
1216 );
1217 let coalesced = last_updated_seq_task(
1218 write_plain_parquet(
1219 dir,
1220 "coalesced.parquet",
1221 vec![physical_last_updated_seq_field()],
1222 vec![Arc::new(Int64Array::from(vec![Some(5), None, Some(8)])) as ArrayRef],
1223 ),
1224 Some(50),
1225 Some(7),
1226 );
1227
1228 let reader = ArrowReaderBuilder::new(FileIO::new_with_fs(), Runtime::current()).build();
1229 let tasks = Box::pin(futures::stream::iter(vec![
1230 Ok(constant),
1231 Ok(nulled),
1232 Ok(coalesced),
1233 ])) as FileScanTaskStream;
1234 let batches: Vec<RecordBatch> = reader
1235 .read(tasks)
1236 .unwrap()
1237 .stream()
1238 .try_collect()
1239 .await
1240 .unwrap();
1241
1242 assert_eq!(batches.len(), 3);
1243 let schema = batches[0].schema();
1245 concat_batches(&schema, &batches)
1246 .expect("constant, null and coalesce files must share one column type");
1247 }
1248
1249 fn physical_last_updated_seq_field() -> Field {
1251 use crate::metadata_columns::{
1252 RESERVED_COL_NAME_LAST_UPDATED_SEQUENCE_NUMBER,
1253 RESERVED_FIELD_ID_LAST_UPDATED_SEQUENCE_NUMBER,
1254 };
1255 Field::new(
1256 RESERVED_COL_NAME_LAST_UPDATED_SEQUENCE_NUMBER,
1257 DataType::Int64,
1258 true,
1259 )
1260 .with_metadata(HashMap::from([(
1261 PARQUET_FIELD_ID_META_KEY.to_string(),
1262 RESERVED_FIELD_ID_LAST_UPDATED_SEQUENCE_NUMBER.to_string(),
1263 )]))
1264 }
1265
1266 #[tokio::test]
1267 async fn test_last_updated_sequence_number_physical_column_coalesced() {
1268 let tmp_dir = TempDir::new().unwrap();
1269 let dir = tmp_dir.path().to_str().unwrap();
1270 let seq_col = Arc::new(Int64Array::from(vec![Some(5), None, Some(8)])) as ArrayRef;
1274 let file_path = write_plain_parquet(
1275 dir,
1276 "with_seq.parquet",
1277 vec![physical_last_updated_seq_field()],
1278 vec![seq_col],
1279 );
1280
1281 let task = last_updated_seq_task(file_path, Some(100), Some(9));
1282
1283 let reader = ArrowReaderBuilder::new(FileIO::new_with_fs(), Runtime::current()).build();
1284 let tasks = Box::pin(futures::stream::iter(vec![Ok(task)])) as FileScanTaskStream;
1285 let batches: Vec<RecordBatch> = reader
1286 .read(tasks)
1287 .unwrap()
1288 .stream()
1289 .try_collect()
1290 .await
1291 .unwrap();
1292
1293 assert_last_updated_seq_column(&batches, &[Some(5), Some(9), Some(8)]);
1295 }
1296
1297 #[tokio::test]
1298 async fn test_last_updated_sequence_number_coalesced_with_pos_column() {
1299 use crate::metadata_columns::{
1300 RESERVED_COL_NAME_POS, RESERVED_FIELD_ID_LAST_UPDATED_SEQUENCE_NUMBER,
1301 RESERVED_FIELD_ID_POS,
1302 };
1303
1304 let tmp_dir = TempDir::new().unwrap();
1305 let dir = tmp_dir.path().to_str().unwrap();
1306 let seq_col = Arc::new(Int64Array::from(vec![Some(5), None, Some(8)])) as ArrayRef;
1307 let file_path = write_plain_parquet(
1308 dir,
1309 "with_seq_and_pos.parquet",
1310 vec![physical_last_updated_seq_field()],
1311 vec![seq_col],
1312 );
1313
1314 let schema = Arc::new(
1319 Schema::builder()
1320 .with_schema_id(1)
1321 .with_fields(vec![
1322 NestedField::required(1, "id", Type::Primitive(PrimitiveType::Int)).into(),
1323 ])
1324 .build()
1325 .unwrap(),
1326 );
1327 let task = FileScanTask::builder()
1328 .with_file_size_in_bytes(std::fs::metadata(&file_path).unwrap().len())
1329 .with_start(0)
1330 .with_length(0)
1331 .with_data_file_path(file_path)
1332 .with_data_file_format(DataFileFormat::Parquet)
1333 .with_schema(schema)
1334 .with_project_field_ids(vec![
1335 1,
1336 RESERVED_FIELD_ID_POS,
1337 RESERVED_FIELD_ID_LAST_UPDATED_SEQUENCE_NUMBER,
1338 ])
1339 .with_first_row_id(Some(100))
1340 .with_data_sequence_number(Some(9))
1341 .with_case_sensitive(false)
1342 .build();
1343
1344 let reader = ArrowReaderBuilder::new(FileIO::new_with_fs(), Runtime::current()).build();
1345 let tasks = Box::pin(futures::stream::iter(vec![Ok(task)])) as FileScanTaskStream;
1346 let batches: Vec<RecordBatch> = reader
1347 .read(tasks)
1348 .unwrap()
1349 .stream()
1350 .try_collect()
1351 .await
1352 .unwrap();
1353
1354 assert_last_updated_seq_column(&batches, &[Some(5), Some(9), Some(8)]);
1356 let pos_col = batches[0]
1358 .column_by_name(RESERVED_COL_NAME_POS)
1359 .expect("_pos column should be present")
1360 .as_primitive::<arrow_array::types::Int64Type>();
1361 assert_eq!(pos_col.values(), &[0, 1, 2]);
1362 }
1363
1364 #[tokio::test]
1365 async fn test_last_updated_sequence_number_physical_column_nulled_without_first_row_id() {
1366 let tmp_dir = TempDir::new().unwrap();
1367 let dir = tmp_dir.path().to_str().unwrap();
1368 let seq_col = Arc::new(Int64Array::from(vec![Some(5), None, Some(8)])) as ArrayRef;
1369 let file_path = write_plain_parquet(
1370 dir,
1371 "with_seq_no_first_row_id.parquet",
1372 vec![physical_last_updated_seq_field()],
1373 vec![seq_col],
1374 );
1375
1376 let task = last_updated_seq_task(file_path, None, Some(9));
1379
1380 let reader = ArrowReaderBuilder::new(FileIO::new_with_fs(), Runtime::current()).build();
1381 let tasks = Box::pin(futures::stream::iter(vec![Ok(task)])) as FileScanTaskStream;
1382 let batches: Vec<RecordBatch> = reader
1383 .read(tasks)
1384 .unwrap()
1385 .stream()
1386 .try_collect()
1387 .await
1388 .unwrap();
1389
1390 assert_last_updated_seq_column(&batches, &[None, None, None]);
1391 }
1392
1393 #[tokio::test]
1394 async fn test_last_updated_sequence_number_present_by_name_without_id_unsupported() {
1395 let tmp_dir = TempDir::new().unwrap();
1396 let dir = tmp_dir.path().to_str().unwrap();
1397 let seq_field = Field::new(
1401 crate::metadata_columns::RESERVED_COL_NAME_LAST_UPDATED_SEQUENCE_NUMBER,
1402 DataType::Int64,
1403 true,
1404 );
1405 let seq_col = Arc::new(Int64Array::from(vec![Some(5), None, Some(8)])) as ArrayRef;
1406 let file_path =
1407 write_plain_parquet(dir, "with_seq_by_name.parquet", vec![seq_field], vec![
1408 seq_col,
1409 ]);
1410
1411 let task = last_updated_seq_task(file_path, Some(100), Some(9));
1412
1413 let reader = ArrowReaderBuilder::new(FileIO::new_with_fs(), Runtime::current()).build();
1414 let tasks = Box::pin(futures::stream::iter(vec![Ok(task)])) as FileScanTaskStream;
1415 let result: Result<Vec<RecordBatch>, _> =
1416 reader.read(tasks).unwrap().stream().try_collect().await;
1417
1418 let err = result.unwrap_err();
1419 assert_eq!(err.kind(), crate::ErrorKind::FeatureUnsupported);
1420 assert!(
1421 format!("{err}").contains("without an embedded field id"),
1422 "unexpected error: {err}"
1423 );
1424 }
1425
1426 #[tokio::test]
1427 async fn test_last_updated_sequence_number_physical_column_first_row_id_without_data_seq() {
1428 let tmp_dir = TempDir::new().unwrap();
1429 let dir = tmp_dir.path().to_str().unwrap();
1430 let seq_col = Arc::new(Int64Array::from(vec![Some(5), None, Some(8)])) as ArrayRef;
1431 let file_path = write_plain_parquet(
1432 dir,
1433 "with_seq_no_data_seq.parquet",
1434 vec![physical_last_updated_seq_field()],
1435 vec![seq_col],
1436 );
1437
1438 let task = last_updated_seq_task(file_path, Some(100), None);
1442
1443 let reader = ArrowReaderBuilder::new(FileIO::new_with_fs(), Runtime::current()).build();
1444 let tasks = Box::pin(futures::stream::iter(vec![Ok(task)])) as FileScanTaskStream;
1445 let result: Result<Vec<RecordBatch>, _> =
1446 reader.read(tasks).unwrap().stream().try_collect().await;
1447
1448 let err = result.unwrap_err();
1449 assert_eq!(err.kind(), crate::ErrorKind::DataInvalid);
1450 assert!(
1451 format!("{err}").contains("no data sequence number"),
1452 "unexpected error: {err}"
1453 );
1454 }
1455
1456 #[tokio::test]
1457 async fn test_read_encrypted_parquet_with_wrong_key_fails() {
1458 let encryption_key = b"0123456789abcdef";
1459 let wrong_key = b"fedcba9876543210";
1460
1461 let schema = Arc::new(
1462 Schema::builder()
1463 .with_schema_id(1)
1464 .with_fields(vec![
1465 NestedField::required(1, "id", Type::Primitive(PrimitiveType::Int)).into(),
1466 ])
1467 .build()
1468 .unwrap(),
1469 );
1470
1471 let arrow_schema = Arc::new(ArrowSchema::new(vec![
1472 Field::new("id", DataType::Int32, false).with_metadata(HashMap::from([(
1473 PARQUET_FIELD_ID_META_KEY.to_string(),
1474 "1".to_string(),
1475 )])),
1476 ]));
1477
1478 let tmp_dir = TempDir::new().unwrap();
1479 let table_location = tmp_dir.path().to_str().unwrap().to_string();
1480 let file_io = FileIO::new_with_fs();
1481
1482 let id_data = Arc::new(Int32Array::from(vec![1, 2, 3])) as ArrayRef;
1483 let batch = RecordBatch::try_new(arrow_schema.clone(), vec![id_data]).unwrap();
1484
1485 let file_path = format!("{table_location}/encrypted_wrong_key.parquet");
1486 write_encrypted_parquet(&file_path, &batch, encryption_key, None);
1487
1488 let wrong_key_metadata = crate::encryption::StandardKeyMetadata::try_new(wrong_key)
1489 .unwrap()
1490 .encode()
1491 .unwrap();
1492
1493 let reader = ArrowReaderBuilder::new(file_io, Runtime::current()).build();
1494
1495 let task = FileScanTask::builder()
1496 .with_file_size_in_bytes(std::fs::metadata(&file_path).unwrap().len())
1497 .with_start(0)
1498 .with_length(0)
1499 .with_data_file_path(file_path)
1500 .with_data_file_format(DataFileFormat::Parquet)
1501 .with_schema(schema)
1502 .with_project_field_ids(vec![1])
1503 .with_case_sensitive(false)
1504 .with_key_metadata(Some(wrong_key_metadata))
1505 .build();
1506
1507 let tasks = Box::pin(futures::stream::iter(vec![Ok(task)])) as FileScanTaskStream;
1508 let result: Result<Vec<RecordBatch>, _> =
1509 reader.read(tasks).unwrap().stream().try_collect().await;
1510
1511 let err = result.unwrap_err();
1512 assert_eq!(err.kind(), crate::ErrorKind::Unexpected);
1513 let err_str = format!("{err}");
1514 assert!(
1515 err_str.contains("unable to decrypt parquet footer"),
1516 "Expected error about decryption failure, got: {err_str}"
1517 );
1518 }
1519
1520 #[tokio::test]
1523 async fn test_read_with_concurrency_one() {
1524 use arrow_array::Int32Array;
1525
1526 let schema = Arc::new(
1527 Schema::builder()
1528 .with_schema_id(1)
1529 .with_fields(vec![
1530 NestedField::required(1, "id", Type::Primitive(PrimitiveType::Int)).into(),
1531 NestedField::required(2, "file_num", Type::Primitive(PrimitiveType::Int))
1532 .into(),
1533 ])
1534 .build()
1535 .unwrap(),
1536 );
1537
1538 let arrow_schema = Arc::new(ArrowSchema::new(vec![
1539 Field::new("id", DataType::Int32, false).with_metadata(HashMap::from([(
1540 PARQUET_FIELD_ID_META_KEY.to_string(),
1541 "1".to_string(),
1542 )])),
1543 Field::new("file_num", DataType::Int32, false).with_metadata(HashMap::from([(
1544 PARQUET_FIELD_ID_META_KEY.to_string(),
1545 "2".to_string(),
1546 )])),
1547 ]));
1548
1549 let tmp_dir = TempDir::new().unwrap();
1550 let table_location = tmp_dir.path().to_str().unwrap().to_string();
1551 let file_io = FileIO::new_with_fs();
1552
1553 let props = WriterProperties::builder()
1555 .set_compression(Compression::SNAPPY)
1556 .build();
1557
1558 for file_num in 0..3 {
1559 let id_data = Arc::new(Int32Array::from_iter_values(
1560 file_num * 10..(file_num + 1) * 10,
1561 )) as ArrayRef;
1562 let file_num_data = Arc::new(Int32Array::from(vec![file_num; 10])) as ArrayRef;
1563
1564 let to_write =
1565 RecordBatch::try_new(arrow_schema.clone(), vec![id_data, file_num_data]).unwrap();
1566
1567 let file = File::create(format!("{table_location}/file_{file_num}.parquet")).unwrap();
1568 let mut writer =
1569 ArrowWriter::try_new(file, to_write.schema(), Some(props.clone())).unwrap();
1570 writer.write(&to_write).expect("Writing batch");
1571 writer.close().unwrap();
1572 }
1573
1574 let reader = ArrowReaderBuilder::new(file_io, Runtime::current())
1576 .with_data_file_concurrency_limit(1)
1577 .build();
1578
1579 let tasks = vec![
1581 Ok(FileScanTask::builder()
1582 .with_file_size_in_bytes(
1583 std::fs::metadata(format!("{table_location}/file_0.parquet"))
1584 .unwrap()
1585 .len(),
1586 )
1587 .with_start(0)
1588 .with_length(0)
1589 .with_data_file_path(format!("{table_location}/file_0.parquet"))
1590 .with_data_file_format(DataFileFormat::Parquet)
1591 .with_schema(schema.clone())
1592 .with_project_field_ids(vec![1, 2])
1593 .with_case_sensitive(false)
1594 .build()),
1595 Ok(FileScanTask::builder()
1596 .with_file_size_in_bytes(
1597 std::fs::metadata(format!("{table_location}/file_1.parquet"))
1598 .unwrap()
1599 .len(),
1600 )
1601 .with_start(0)
1602 .with_length(0)
1603 .with_data_file_path(format!("{table_location}/file_1.parquet"))
1604 .with_data_file_format(DataFileFormat::Parquet)
1605 .with_schema(schema.clone())
1606 .with_project_field_ids(vec![1, 2])
1607 .with_case_sensitive(false)
1608 .build()),
1609 Ok(FileScanTask::builder()
1610 .with_file_size_in_bytes(
1611 std::fs::metadata(format!("{table_location}/file_2.parquet"))
1612 .unwrap()
1613 .len(),
1614 )
1615 .with_start(0)
1616 .with_length(0)
1617 .with_data_file_path(format!("{table_location}/file_2.parquet"))
1618 .with_data_file_format(DataFileFormat::Parquet)
1619 .with_schema(schema.clone())
1620 .with_project_field_ids(vec![1, 2])
1621 .with_case_sensitive(false)
1622 .build()),
1623 ];
1624
1625 let tasks_stream = Box::pin(futures::stream::iter(tasks)) as FileScanTaskStream;
1626
1627 let result = reader
1628 .read(tasks_stream)
1629 .unwrap()
1630 .stream()
1631 .try_collect::<Vec<RecordBatch>>()
1632 .await
1633 .unwrap();
1634
1635 let total_rows: usize = result.iter().map(|b| b.num_rows()).sum();
1637 assert_eq!(total_rows, 30, "Should have 30 total rows");
1638
1639 let mut all_ids = Vec::new();
1641 let mut all_file_nums = Vec::new();
1642
1643 for batch in &result {
1644 let id_col = batch
1645 .column(0)
1646 .as_primitive::<arrow_array::types::Int32Type>();
1647 let file_num_col = batch
1648 .column(1)
1649 .as_primitive::<arrow_array::types::Int32Type>();
1650
1651 for i in 0..batch.num_rows() {
1652 all_ids.push(id_col.value(i));
1653 all_file_nums.push(file_num_col.value(i));
1654 }
1655 }
1656
1657 assert_eq!(all_ids.len(), 30);
1658 assert_eq!(all_file_nums.len(), 30);
1659
1660 for i in 0..10 {
1665 assert_eq!(all_file_nums[i], 0, "First 10 rows should be from file_0");
1666 assert_eq!(all_ids[i], i as i32, "IDs should be 0-9");
1667 }
1668 for i in 10..20 {
1669 assert_eq!(all_file_nums[i], 1, "Next 10 rows should be from file_1");
1670 assert_eq!(all_ids[i], i as i32, "IDs should be 10-19");
1671 }
1672 for i in 20..30 {
1673 assert_eq!(all_file_nums[i], 2, "Last 10 rows should be from file_2");
1674 assert_eq!(all_ids[i], i as i32, "IDs should be 20-29");
1675 }
1676 }
1677
1678 #[tokio::test]
1679 async fn test_read_int96_timestamps_with_field_ids() {
1680 let schema = Arc::new(
1681 Schema::builder()
1682 .with_schema_id(1)
1683 .with_fields(vec![
1684 NestedField::optional(1, "ts", Type::Primitive(PrimitiveType::Timestamp))
1685 .into(),
1686 NestedField::required(2, "id", Type::Primitive(PrimitiveType::Int)).into(),
1687 ])
1688 .build()
1689 .unwrap(),
1690 );
1691
1692 let tmp_dir = TempDir::new().unwrap();
1693 let table_location = tmp_dir.path().to_str().unwrap().to_string();
1694 let (file_path, expected_micros) =
1695 write_int96_parquet_file(&table_location, "with_ids.parquet", true);
1696
1697 assert_int96_read_matches(&file_path, schema, vec![1, 2], &expected_micros).await;
1698 }
1699
1700 #[tokio::test]
1701 async fn test_read_int96_timestamps_without_field_ids() {
1702 let schema = Arc::new(
1703 Schema::builder()
1704 .with_schema_id(1)
1705 .with_fields(vec![
1706 NestedField::optional(1, "ts", Type::Primitive(PrimitiveType::Timestamp))
1707 .into(),
1708 NestedField::required(2, "id", Type::Primitive(PrimitiveType::Int)).into(),
1709 ])
1710 .build()
1711 .unwrap(),
1712 );
1713
1714 let tmp_dir = TempDir::new().unwrap();
1715 let table_location = tmp_dir.path().to_str().unwrap().to_string();
1716 let (file_path, expected_micros) =
1717 write_int96_parquet_file(&table_location, "no_ids.parquet", false);
1718
1719 assert_int96_read_matches(&file_path, schema, vec![1, 2], &expected_micros).await;
1720 }
1721
1722 #[tokio::test]
1723 async fn test_read_int96_timestamps_in_struct() {
1724 use arrow_array::{StructArray, TimestampMicrosecondArray};
1725 use parquet::basic::{Repetition, Type as PhysicalType};
1726 use parquet::data_type::Int96Type;
1727 use parquet::file::writer::SerializedFileWriter;
1728 use parquet::schema::types::Type as SchemaType;
1729
1730 let tmp_dir = TempDir::new().unwrap();
1731 let table_location = tmp_dir.path().to_str().unwrap().to_string();
1732 let file_path = format!("{table_location}/struct_int96.parquet");
1733
1734 let ts_type = SchemaType::primitive_type_builder("ts", PhysicalType::INT96)
1735 .with_repetition(Repetition::OPTIONAL)
1736 .with_id(Some(2))
1737 .build()
1738 .unwrap();
1739
1740 let struct_type = SchemaType::group_type_builder("data")
1741 .with_repetition(Repetition::REQUIRED)
1742 .with_id(Some(1))
1743 .with_fields(vec![Arc::new(ts_type)])
1744 .build()
1745 .unwrap();
1746
1747 let parquet_schema = SchemaType::group_type_builder("schema")
1748 .with_fields(vec![Arc::new(struct_type)])
1749 .build()
1750 .unwrap();
1751
1752 let (int96_val, expected_micros) = make_int96_test_value();
1753
1754 let file = File::create(&file_path).unwrap();
1755 let mut writer =
1756 SerializedFileWriter::new(file, Arc::new(parquet_schema), Default::default()).unwrap();
1757
1758 let mut row_group = writer.next_row_group().unwrap();
1761 {
1762 let mut col = row_group.next_column().unwrap().unwrap();
1763 col.typed::<Int96Type>()
1764 .write_batch(&[int96_val], Some(&[1]), None)
1765 .unwrap();
1766 col.close().unwrap();
1767 }
1768 row_group.close().unwrap();
1769 writer.close().unwrap();
1770
1771 let iceberg_schema = Arc::new(
1772 Schema::builder()
1773 .with_schema_id(1)
1774 .with_fields(vec![
1775 NestedField::required(
1776 1,
1777 "data",
1778 Type::Struct(crate::spec::StructType::new(vec![
1779 NestedField::optional(
1780 2,
1781 "ts",
1782 Type::Primitive(PrimitiveType::Timestamp),
1783 )
1784 .into(),
1785 ])),
1786 )
1787 .into(),
1788 ])
1789 .build()
1790 .unwrap(),
1791 );
1792
1793 let batches = read_int96_batches(&file_path, iceberg_schema, vec![1]).await;
1794
1795 assert_eq!(batches.len(), 1);
1796 let struct_array = batches[0]
1797 .column(0)
1798 .as_any()
1799 .downcast_ref::<StructArray>()
1800 .expect("Expected StructArray");
1801 let ts_array = struct_array
1802 .column(0)
1803 .as_any()
1804 .downcast_ref::<TimestampMicrosecondArray>()
1805 .expect("Expected TimestampMicrosecondArray inside struct");
1806
1807 assert_eq!(
1808 ts_array.value(0),
1809 expected_micros,
1810 "INT96 in struct: got {}, expected {expected_micros}",
1811 ts_array.value(0)
1812 );
1813 }
1814
1815 #[tokio::test]
1816 async fn test_read_int96_timestamps_in_list() {
1817 use arrow_array::{ListArray, TimestampMicrosecondArray};
1818 use parquet::basic::{Repetition, Type as PhysicalType};
1819 use parquet::data_type::Int96Type;
1820 use parquet::file::writer::SerializedFileWriter;
1821 use parquet::schema::types::Type as SchemaType;
1822
1823 let tmp_dir = TempDir::new().unwrap();
1824 let table_location = tmp_dir.path().to_str().unwrap().to_string();
1825 let file_path = format!("{table_location}/list_int96.parquet");
1826
1827 let element_type = SchemaType::primitive_type_builder("element", PhysicalType::INT96)
1834 .with_repetition(Repetition::OPTIONAL)
1835 .with_id(Some(2))
1836 .build()
1837 .unwrap();
1838
1839 let list_group = SchemaType::group_type_builder("list")
1840 .with_repetition(Repetition::REPEATED)
1841 .with_fields(vec![Arc::new(element_type)])
1842 .build()
1843 .unwrap();
1844
1845 let list_type = SchemaType::group_type_builder("timestamps")
1846 .with_repetition(Repetition::OPTIONAL)
1847 .with_id(Some(1))
1848 .with_logical_type(Some(parquet::basic::LogicalType::List))
1849 .with_fields(vec![Arc::new(list_group)])
1850 .build()
1851 .unwrap();
1852
1853 let parquet_schema = SchemaType::group_type_builder("schema")
1854 .with_fields(vec![Arc::new(list_type)])
1855 .build()
1856 .unwrap();
1857
1858 let (int96_val, expected_micros) = make_int96_test_value();
1859
1860 let file = File::create(&file_path).unwrap();
1861 let mut writer =
1862 SerializedFileWriter::new(file, Arc::new(parquet_schema), Default::default()).unwrap();
1863
1864 let mut row_group = writer.next_row_group().unwrap();
1868 {
1869 let mut col = row_group.next_column().unwrap().unwrap();
1870 col.typed::<Int96Type>()
1871 .write_batch(&[int96_val], Some(&[3]), Some(&[0]))
1872 .unwrap();
1873 col.close().unwrap();
1874 }
1875 row_group.close().unwrap();
1876 writer.close().unwrap();
1877
1878 let iceberg_schema = Arc::new(
1879 Schema::builder()
1880 .with_schema_id(1)
1881 .with_fields(vec![
1882 NestedField::optional(
1883 1,
1884 "timestamps",
1885 Type::List(crate::spec::ListType {
1886 element_field: NestedField::optional(
1887 2,
1888 "element",
1889 Type::Primitive(PrimitiveType::Timestamp),
1890 )
1891 .into(),
1892 }),
1893 )
1894 .into(),
1895 ])
1896 .build()
1897 .unwrap(),
1898 );
1899
1900 let batches = read_int96_batches(&file_path, iceberg_schema, vec![1]).await;
1901
1902 assert_eq!(batches.len(), 1);
1903 let list_array = batches[0]
1904 .column(0)
1905 .as_any()
1906 .downcast_ref::<ListArray>()
1907 .expect("Expected ListArray");
1908 let ts_array = list_array
1909 .values()
1910 .as_any()
1911 .downcast_ref::<TimestampMicrosecondArray>()
1912 .expect("Expected TimestampMicrosecondArray inside list");
1913
1914 assert_eq!(
1915 ts_array.value(0),
1916 expected_micros,
1917 "INT96 in list: got {}, expected {expected_micros}",
1918 ts_array.value(0)
1919 );
1920 }
1921
1922 #[tokio::test]
1923 async fn test_read_int96_timestamps_in_map() {
1924 use arrow_array::{MapArray, TimestampMicrosecondArray};
1925 use parquet::basic::{Repetition, Type as PhysicalType};
1926 use parquet::data_type::{ByteArrayType, Int96Type};
1927 use parquet::file::writer::SerializedFileWriter;
1928 use parquet::schema::types::Type as SchemaType;
1929
1930 let tmp_dir = TempDir::new().unwrap();
1931 let table_location = tmp_dir.path().to_str().unwrap().to_string();
1932 let file_path = format!("{table_location}/map_int96.parquet");
1933
1934 let key_type = SchemaType::primitive_type_builder("key", PhysicalType::BYTE_ARRAY)
1942 .with_repetition(Repetition::REQUIRED)
1943 .with_logical_type(Some(parquet::basic::LogicalType::String))
1944 .with_id(Some(2))
1945 .build()
1946 .unwrap();
1947
1948 let value_type = SchemaType::primitive_type_builder("value", PhysicalType::INT96)
1949 .with_repetition(Repetition::OPTIONAL)
1950 .with_id(Some(3))
1951 .build()
1952 .unwrap();
1953
1954 let key_value_group = SchemaType::group_type_builder("key_value")
1955 .with_repetition(Repetition::REPEATED)
1956 .with_fields(vec![Arc::new(key_type), Arc::new(value_type)])
1957 .build()
1958 .unwrap();
1959
1960 let map_type = SchemaType::group_type_builder("ts_map")
1961 .with_repetition(Repetition::OPTIONAL)
1962 .with_id(Some(1))
1963 .with_logical_type(Some(parquet::basic::LogicalType::Map))
1964 .with_fields(vec![Arc::new(key_value_group)])
1965 .build()
1966 .unwrap();
1967
1968 let parquet_schema = SchemaType::group_type_builder("schema")
1969 .with_fields(vec![Arc::new(map_type)])
1970 .build()
1971 .unwrap();
1972
1973 let (int96_val, expected_micros) = make_int96_test_value();
1974
1975 let file = File::create(&file_path).unwrap();
1976 let mut writer =
1977 SerializedFileWriter::new(file, Arc::new(parquet_schema), Default::default()).unwrap();
1978
1979 let mut row_group = writer.next_row_group().unwrap();
1984 {
1985 let mut col = row_group.next_column().unwrap().unwrap();
1986 col.typed::<ByteArrayType>()
1987 .write_batch(
1988 &[parquet::data_type::ByteArray::from("event_time")],
1989 Some(&[2]),
1990 Some(&[0]),
1991 )
1992 .unwrap();
1993 col.close().unwrap();
1994 }
1995 {
1996 let mut col = row_group.next_column().unwrap().unwrap();
1997 col.typed::<Int96Type>()
1998 .write_batch(&[int96_val], Some(&[3]), Some(&[0]))
1999 .unwrap();
2000 col.close().unwrap();
2001 }
2002 row_group.close().unwrap();
2003 writer.close().unwrap();
2004
2005 let iceberg_schema = Arc::new(
2006 Schema::builder()
2007 .with_schema_id(1)
2008 .with_fields(vec![
2009 NestedField::optional(
2010 1,
2011 "ts_map",
2012 Type::Map(crate::spec::MapType {
2013 key_field: NestedField::required(
2014 2,
2015 "key",
2016 Type::Primitive(PrimitiveType::String),
2017 )
2018 .into(),
2019 value_field: NestedField::optional(
2020 3,
2021 "value",
2022 Type::Primitive(PrimitiveType::Timestamp),
2023 )
2024 .into(),
2025 }),
2026 )
2027 .into(),
2028 ])
2029 .build()
2030 .unwrap(),
2031 );
2032
2033 let batches = read_int96_batches(&file_path, iceberg_schema, vec![1]).await;
2034
2035 assert_eq!(batches.len(), 1);
2036 let map_array = batches[0]
2037 .column(0)
2038 .as_any()
2039 .downcast_ref::<MapArray>()
2040 .expect("Expected MapArray");
2041 let ts_array = map_array
2042 .values()
2043 .as_any()
2044 .downcast_ref::<TimestampMicrosecondArray>()
2045 .expect("Expected TimestampMicrosecondArray as map values");
2046
2047 assert_eq!(
2048 ts_array.value(0),
2049 expected_micros,
2050 "INT96 in map: got {}, expected {expected_micros}",
2051 ts_array.value(0)
2052 );
2053 }
2054
2055 fn write_parquet_with_wide_column(
2061 dir: &str,
2062 name: &str,
2063 extra_fields: Vec<Field>,
2064 extra_columns: Vec<ArrayRef>,
2065 ) -> String {
2066 let wide_field =
2067 Field::new("wide", DataType::Utf8, false).with_metadata(HashMap::from([(
2068 PARQUET_FIELD_ID_META_KEY.to_string(),
2069 "2".to_string(),
2070 )]));
2071 let wide_values: Vec<String> = (0..3)
2074 .map(|i| {
2075 (0..2048)
2076 .map(|j| ((i * 2048 + j) % 251) as u8 as char)
2077 .collect()
2078 })
2079 .collect();
2080
2081 let mut fields = vec![wide_field];
2082 fields.extend(extra_fields);
2083 let mut columns: Vec<ArrayRef> = vec![Arc::new(StringArray::from(wide_values))];
2084 columns.extend(extra_columns);
2085 write_plain_parquet(dir, name, fields, columns)
2086 }
2087
2088 fn id_and_wide_schema() -> SchemaRef {
2091 Arc::new(
2092 Schema::builder()
2093 .with_schema_id(1)
2094 .with_fields(vec![
2095 NestedField::required(1, "id", Type::Primitive(PrimitiveType::Int)).into(),
2096 NestedField::required(2, "wide", Type::Primitive(PrimitiveType::String)).into(),
2097 ])
2098 .build()
2099 .unwrap(),
2100 )
2101 }
2102
2103 fn metadata_projection_task(
2105 file_path: String,
2106 schema: SchemaRef,
2107 project_field_ids: Vec<i32>,
2108 ) -> FileScanTask {
2109 FileScanTask::builder()
2110 .with_file_size_in_bytes(std::fs::metadata(&file_path).unwrap().len())
2111 .with_start(0)
2112 .with_length(0)
2113 .with_data_file_path(file_path)
2114 .with_data_file_format(DataFileFormat::Parquet)
2115 .with_schema(schema)
2116 .with_project_field_ids(project_field_ids)
2117 .with_case_sensitive(false)
2118 .build()
2119 }
2120
2121 async fn scan_task(task: FileScanTask) -> (Vec<RecordBatch>, u64) {
2123 let reader = ArrowReaderBuilder::new(FileIO::new_with_fs(), Runtime::current()).build();
2124 let tasks = Box::pin(futures::stream::iter(vec![Ok(task)])) as FileScanTaskStream;
2125 let scan = reader.read(tasks).unwrap();
2126 let metrics = scan.metrics().clone();
2127 let batches = scan.stream().try_collect().await.unwrap();
2128 (batches, metrics.bytes_read())
2129 }
2130
2131 #[tokio::test]
2132 async fn test_pos_only_projection_reads_no_data_columns() {
2133 let tmp_dir = TempDir::new().unwrap();
2134 let dir = tmp_dir.path().to_str().unwrap();
2135
2136 let pos_only = metadata_projection_task(
2137 write_parquet_with_wide_column(dir, "pos_only.parquet", vec![], vec![]),
2138 id_and_wide_schema(),
2139 vec![RESERVED_FIELD_ID_POS],
2140 );
2141 let (batches, pos_only_bytes) = scan_task(pos_only).await;
2142
2143 assert_eq!(batches[0].num_columns(), 1);
2145 let pos_col = batches[0]
2146 .column_by_name(RESERVED_COL_NAME_POS)
2147 .expect("_pos column should be present")
2148 .as_primitive::<arrow_array::types::Int64Type>();
2149 assert_eq!(pos_col.values(), &[0, 1, 2]);
2150
2151 let with_data = metadata_projection_task(
2154 write_parquet_with_wide_column(dir, "pos_only_ref.parquet", vec![], vec![]),
2155 id_and_wide_schema(),
2156 vec![2, RESERVED_FIELD_ID_POS],
2157 );
2158 let (_, with_data_bytes) = scan_task(with_data).await;
2159
2160 assert!(
2161 pos_only_bytes < with_data_bytes,
2162 "_pos-only scan should read fewer bytes than a scan of the wide column: \
2163 {pos_only_bytes} vs {with_data_bytes}"
2164 );
2165 }
2166
2167 #[tokio::test]
2168 async fn test_pos_only_projection_keeps_absolute_pos_under_predicate() {
2169 use crate::expr::{Bind, Reference};
2170 use crate::spec::Datum;
2171
2172 let tmp_dir = TempDir::new().unwrap();
2173 let dir = tmp_dir.path().to_str().unwrap();
2174 let file_path = write_plain_parquet(dir, "pos_only_predicate.parquet", vec![], vec![]);
2176
2177 let schema = Arc::new(
2178 Schema::builder()
2179 .with_schema_id(1)
2180 .with_fields(vec![
2181 NestedField::required(1, "id", Type::Primitive(PrimitiveType::Int)).into(),
2182 ])
2183 .build()
2184 .unwrap(),
2185 );
2186 let bound = Reference::new("id")
2187 .not_equal_to(Datum::int(2))
2188 .bind(Arc::clone(&schema), false)
2189 .unwrap();
2190 let task = FileScanTask::builder()
2191 .with_file_size_in_bytes(std::fs::metadata(&file_path).unwrap().len())
2192 .with_start(0)
2193 .with_length(0)
2194 .with_data_file_path(file_path)
2195 .with_data_file_format(DataFileFormat::Parquet)
2196 .with_schema(schema)
2197 .with_project_field_ids(vec![RESERVED_FIELD_ID_POS])
2198 .with_predicate(Some(bound))
2199 .with_case_sensitive(false)
2200 .build();
2201
2202 let reader = ArrowReaderBuilder::new(FileIO::new_with_fs(), Runtime::current())
2206 .with_row_selection_enabled(true)
2207 .build();
2208 let tasks = Box::pin(futures::stream::iter(vec![Ok(task)])) as FileScanTaskStream;
2209 let batches: Vec<RecordBatch> = reader
2210 .read(tasks)
2211 .unwrap()
2212 .stream()
2213 .try_collect()
2214 .await
2215 .unwrap();
2216
2217 let pos: Vec<i64> = batches
2218 .iter()
2219 .flat_map(|b| {
2220 b.column_by_name(RESERVED_COL_NAME_POS)
2221 .expect("_pos column should be present")
2222 .as_primitive::<arrow_array::types::Int64Type>()
2223 .values()
2224 .to_vec()
2225 })
2226 .collect();
2227 assert_eq!(pos, vec![0, 2]);
2228 }
2229
2230 #[tokio::test]
2231 async fn test_pos_and_file_projection() {
2232 use crate::metadata_columns::RESERVED_COL_NAME_FILE;
2233
2234 let tmp_dir = TempDir::new().unwrap();
2235 let dir = tmp_dir.path().to_str().unwrap();
2236 let file_path = write_parquet_with_wide_column(dir, "pos_and_file.parquet", vec![], vec![]);
2239 let task = metadata_projection_task(file_path.clone(), id_and_wide_schema(), vec![
2240 RESERVED_FIELD_ID_POS,
2241 RESERVED_FIELD_ID_FILE,
2242 ]);
2243 let (batches, _) = scan_task(task).await;
2244
2245 assert_eq!(batches[0].num_columns(), 2);
2247 let pos_col = batches[0]
2248 .column_by_name(RESERVED_COL_NAME_POS)
2249 .expect("_pos column should be present")
2250 .as_primitive::<arrow_array::types::Int64Type>();
2251 assert_eq!(pos_col.values(), &[0, 1, 2]);
2252 let file_col = batches[0]
2253 .column_by_name(RESERVED_COL_NAME_FILE)
2254 .expect("_file column should be present");
2255 let file_col = cast(file_col, &DataType::Utf8).unwrap();
2256 let file_col = file_col.as_any().downcast_ref::<StringArray>().unwrap();
2257 assert_eq!(file_col.value(0), file_path);
2258 }
2259
2260 #[tokio::test]
2261 async fn test_pos_and_physical_seq_projection_reads_only_the_leaf() {
2262 use crate::metadata_columns::{
2263 RESERVED_COL_NAME_LAST_UPDATED_SEQUENCE_NUMBER,
2264 RESERVED_FIELD_ID_LAST_UPDATED_SEQUENCE_NUMBER,
2265 };
2266
2267 let tmp_dir = TempDir::new().unwrap();
2271 let dir = tmp_dir.path().to_str().unwrap();
2272
2273 let write = |name: &str| {
2275 write_parquet_with_wide_column(
2276 dir,
2277 name,
2278 vec![physical_last_updated_seq_field()],
2279 vec![Arc::new(Int64Array::from(vec![Some(5), None, Some(8)])) as ArrayRef],
2280 )
2281 };
2282
2283 let seq_task = |path: String, ids: Vec<i32>| {
2284 FileScanTask::builder()
2285 .with_file_size_in_bytes(std::fs::metadata(&path).unwrap().len())
2286 .with_start(0)
2287 .with_length(0)
2288 .with_data_file_path(path)
2289 .with_data_file_format(DataFileFormat::Parquet)
2290 .with_schema(id_and_wide_schema())
2291 .with_project_field_ids(ids)
2292 .with_first_row_id(Some(100))
2293 .with_data_sequence_number(Some(9))
2294 .with_case_sensitive(false)
2295 .build()
2296 };
2297
2298 let meta_only = seq_task(write("pos_seq.parquet"), vec![
2299 RESERVED_FIELD_ID_POS,
2300 RESERVED_FIELD_ID_LAST_UPDATED_SEQUENCE_NUMBER,
2301 ]);
2302 let (batches, meta_only_bytes) = scan_task(meta_only).await;
2303
2304 let pos_col = batches[0]
2306 .column_by_name(RESERVED_COL_NAME_POS)
2307 .expect("_pos column should be present")
2308 .as_primitive::<arrow_array::types::Int64Type>();
2309 assert_eq!(pos_col.values(), &[0, 1, 2]);
2310 let seq_col = batches[0]
2311 .column_by_name(RESERVED_COL_NAME_LAST_UPDATED_SEQUENCE_NUMBER)
2312 .expect("_last_updated_sequence_number column should be present");
2313 let seq_col = cast(seq_col, &DataType::Int64).unwrap();
2314 let seq_col = seq_col.as_any().downcast_ref::<Int64Array>().unwrap();
2315 assert_eq!(seq_col.value(0), 5);
2317 assert_eq!(seq_col.value(1), 9);
2318 assert_eq!(seq_col.value(2), 8);
2319 assert!(batches[0].column_by_name("wide").is_none());
2320
2321 let with_data = seq_task(write("pos_seq_ref.parquet"), vec![
2324 2,
2325 RESERVED_FIELD_ID_POS,
2326 RESERVED_FIELD_ID_LAST_UPDATED_SEQUENCE_NUMBER,
2327 ]);
2328 let (_, with_data_bytes) = scan_task(with_data).await;
2329
2330 assert!(
2331 meta_only_bytes < with_data_bytes,
2332 "_pos + physical sequence scan should read fewer bytes than one that also \
2333 reads the wide column: {meta_only_bytes} vs {with_data_bytes}"
2334 );
2335 }
2336
2337 #[tokio::test]
2338 async fn test_seq_only_projection_reads_only_the_leaf() {
2339 use crate::metadata_columns::{
2340 RESERVED_COL_NAME_LAST_UPDATED_SEQUENCE_NUMBER,
2341 RESERVED_FIELD_ID_LAST_UPDATED_SEQUENCE_NUMBER,
2342 };
2343
2344 let tmp_dir = TempDir::new().unwrap();
2348 let dir = tmp_dir.path().to_str().unwrap();
2349 let write = |name: &str| {
2350 write_parquet_with_wide_column(
2351 dir,
2352 name,
2353 vec![physical_last_updated_seq_field()],
2354 vec![Arc::new(Int64Array::from(vec![Some(5), None, Some(8)])) as ArrayRef],
2355 )
2356 };
2357 let seq_task = |path: String, ids: Vec<i32>| {
2358 FileScanTask::builder()
2359 .with_file_size_in_bytes(std::fs::metadata(&path).unwrap().len())
2360 .with_start(0)
2361 .with_length(0)
2362 .with_data_file_path(path)
2363 .with_data_file_format(DataFileFormat::Parquet)
2364 .with_schema(id_and_wide_schema())
2365 .with_project_field_ids(ids)
2366 .with_first_row_id(Some(100))
2367 .with_data_sequence_number(Some(9))
2368 .with_case_sensitive(false)
2369 .build()
2370 };
2371
2372 let meta_only = seq_task(write("seq_only.parquet"), vec![
2373 RESERVED_FIELD_ID_LAST_UPDATED_SEQUENCE_NUMBER,
2374 ]);
2375 let (batches, meta_only_bytes) = scan_task(meta_only).await;
2376
2377 let seq_col = batches[0]
2378 .column_by_name(RESERVED_COL_NAME_LAST_UPDATED_SEQUENCE_NUMBER)
2379 .expect("_last_updated_sequence_number column should be present");
2380 let seq_col = cast(seq_col, &DataType::Int64).unwrap();
2381 let seq_col = seq_col.as_any().downcast_ref::<Int64Array>().unwrap();
2382 assert_eq!(seq_col.value(0), 5);
2383 assert_eq!(seq_col.value(1), 9);
2384 assert_eq!(seq_col.value(2), 8);
2385 let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
2386 assert_eq!(total_rows, 3);
2387 assert!(batches[0].column_by_name("wide").is_none());
2388
2389 let with_data = seq_task(write("seq_only_ref.parquet"), vec![
2390 2,
2391 RESERVED_FIELD_ID_LAST_UPDATED_SEQUENCE_NUMBER,
2392 ]);
2393 let (_, with_data_bytes) = scan_task(with_data).await;
2394
2395 assert!(
2396 meta_only_bytes < with_data_bytes,
2397 "seq-only scan should read fewer bytes than one that also reads the wide \
2398 column: {meta_only_bytes} vs {with_data_bytes}"
2399 );
2400 }
2401
2402 #[tokio::test]
2403 async fn test_seq_only_projection_null_first_row_id_preserves_row_count() {
2404 use crate::metadata_columns::{
2405 RESERVED_COL_NAME_LAST_UPDATED_SEQUENCE_NUMBER,
2406 RESERVED_FIELD_ID_LAST_UPDATED_SEQUENCE_NUMBER,
2407 };
2408
2409 let tmp_dir = TempDir::new().unwrap();
2414 let dir = tmp_dir.path().to_str().unwrap();
2415 let file_path = write_parquet_with_wide_column(
2416 dir,
2417 "seq_only_null_first.parquet",
2418 vec![physical_last_updated_seq_field()],
2419 vec![Arc::new(Int64Array::from(vec![Some(5), Some(6), Some(7)])) as ArrayRef],
2420 );
2421 let task = FileScanTask::builder()
2422 .with_file_size_in_bytes(std::fs::metadata(&file_path).unwrap().len())
2423 .with_start(0)
2424 .with_length(0)
2425 .with_data_file_path(file_path)
2426 .with_data_file_format(DataFileFormat::Parquet)
2427 .with_schema(id_and_wide_schema())
2428 .with_project_field_ids(vec![RESERVED_FIELD_ID_LAST_UPDATED_SEQUENCE_NUMBER])
2429 .with_first_row_id(None)
2430 .with_data_sequence_number(Some(9))
2431 .with_case_sensitive(false)
2432 .build();
2433 let (batches, _) = scan_task(task).await;
2434
2435 let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
2436 assert_eq!(total_rows, 3);
2437 let seq_col = batches[0]
2438 .column_by_name(RESERVED_COL_NAME_LAST_UPDATED_SEQUENCE_NUMBER)
2439 .expect("_last_updated_sequence_number column should be present");
2440 let seq_col = cast(seq_col, &DataType::Int64).unwrap();
2441 let seq_col = seq_col.as_any().downcast_ref::<Int64Array>().unwrap();
2442 assert!((0..3).all(|i| seq_col.is_null(i)));
2443 }
2444
2445 #[tokio::test]
2446 async fn test_file_only_projection_preserves_row_count() {
2447 use crate::metadata_columns::RESERVED_COL_NAME_FILE;
2448
2449 let tmp_dir = TempDir::new().unwrap();
2450 let dir = tmp_dir.path().to_str().unwrap();
2451 let file_path = write_plain_parquet(dir, "file_only.parquet", vec![], vec![]);
2452 let schema = Arc::new(
2453 Schema::builder()
2454 .with_schema_id(1)
2455 .with_fields(vec![
2456 NestedField::required(1, "id", Type::Primitive(PrimitiveType::Int)).into(),
2457 ])
2458 .build()
2459 .unwrap(),
2460 );
2461 let task =
2462 metadata_projection_task(file_path.clone(), schema, vec![RESERVED_FIELD_ID_FILE]);
2463 let (batches, _) = scan_task(task).await;
2464
2465 let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
2468 assert_eq!(total_rows, 3);
2469 let file_col = batches[0]
2470 .column_by_name(RESERVED_COL_NAME_FILE)
2471 .expect("_file column should be present");
2472 let file_col = cast(file_col, &DataType::Utf8).unwrap();
2473 let file_col = file_col.as_any().downcast_ref::<StringArray>().unwrap();
2474 assert_eq!(file_col.value(0), file_path);
2475 }
2476
2477 #[tokio::test]
2478 async fn test_empty_projection_preserves_row_count() {
2479 let tmp_dir = TempDir::new().unwrap();
2480 let dir = tmp_dir.path().to_str().unwrap();
2481 let file_path = write_plain_parquet(dir, "empty_projection.parquet", vec![], vec![]);
2482 let schema = Arc::new(
2483 Schema::builder()
2484 .with_schema_id(1)
2485 .with_fields(vec![
2486 NestedField::required(1, "id", Type::Primitive(PrimitiveType::Int)).into(),
2487 ])
2488 .build()
2489 .unwrap(),
2490 );
2491 let task = metadata_projection_task(file_path, schema, vec![]);
2492 let (batches, _) = scan_task(task).await;
2493
2494 let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
2496 assert_eq!(total_rows, 3);
2497 }
2498}