use std::sync::Arc;
use std::sync::atomic::AtomicU64;
use futures::{StreamExt, TryStreamExt};
use parquet::arrow::arrow_reader::{ArrowReaderMetadata, ArrowReaderOptions};
use parquet::arrow::{PARQUET_FIELD_ID_META_KEY, ParquetRecordBatchStreamBuilder};
use super::{
ArrowFileReader, ArrowReader, ParquetReadOptions, add_fallback_field_ids_to_arrow_schema,
apply_name_mapping_to_arrow_schema,
};
use crate::arrow::caching_delete_file_loader::CachingDeleteFileLoader;
use crate::arrow::int96::coerce_int96_timestamps;
use crate::arrow::record_batch_transformer::RecordBatchTransformerBuilder;
use crate::arrow::scan_metrics::{CountingFileRead, ScanMetrics, ScanResult};
use crate::error::Result;
use crate::io::{FileIO, FileMetadata, FileRead};
use crate::metadata_columns::{RESERVED_FIELD_ID_FILE, is_metadata_field};
use crate::scan::{ArrowRecordBatchStream, FileScanTask, FileScanTaskStream};
use crate::spec::Datum;
use crate::{Error, ErrorKind};
impl ArrowReader {
pub fn read(self, tasks: FileScanTaskStream) -> Result<ScanResult> {
let concurrency_limit_data_files = self.concurrency_limit_data_files;
let scan_metrics = ScanMetrics::new();
let task_reader = FileScanTaskReader {
batch_size: self.batch_size,
file_io: self.file_io,
delete_file_loader: self
.delete_file_loader
.with_scan_metrics(scan_metrics.clone()),
row_group_filtering_enabled: self.row_group_filtering_enabled,
row_selection_enabled: self.row_selection_enabled,
parquet_read_options: self.parquet_read_options,
scan_metrics: scan_metrics.clone(),
};
let stream: ArrowRecordBatchStream = if concurrency_limit_data_files == 1 {
Box::pin(
tasks
.and_then(move |task| task_reader.clone().process(task))
.map_err(|err| {
Error::new(ErrorKind::Unexpected, "file scan task generate failed")
.with_source(err)
})
.try_flatten(),
)
} else {
Box::pin(
tasks
.map_ok(move |task| task_reader.clone().process(task))
.map_err(|err| {
Error::new(ErrorKind::Unexpected, "file scan task generate failed")
.with_source(err)
})
.try_buffer_unordered(concurrency_limit_data_files)
.try_flatten_unordered(concurrency_limit_data_files),
)
};
Ok(ScanResult::new(stream, scan_metrics))
}
}
#[derive(Clone)]
struct FileScanTaskReader {
batch_size: Option<usize>,
file_io: FileIO,
delete_file_loader: CachingDeleteFileLoader,
row_group_filtering_enabled: bool,
row_selection_enabled: bool,
parquet_read_options: ParquetReadOptions,
scan_metrics: ScanMetrics,
}
impl FileScanTaskReader {
async fn process(self, task: FileScanTask) -> Result<ArrowRecordBatchStream> {
let should_load_page_index =
(self.row_selection_enabled && task.predicate.is_some()) || !task.deletes.is_empty();
let mut parquet_read_options = self.parquet_read_options;
parquet_read_options.preload_page_index = should_load_page_index;
let delete_filter_rx = self
.delete_file_loader
.load_deletes(&task.deletes, Arc::clone(&task.schema));
let (parquet_file_reader, arrow_metadata) = ArrowReader::open_parquet_file(
&task.data_file_path,
&self.file_io,
task.file_size_in_bytes,
parquet_read_options,
self.scan_metrics.bytes_read_counter(),
)
.await?;
let missing_field_ids = arrow_metadata
.schema()
.fields()
.iter()
.next()
.is_some_and(|f| f.metadata().get(PARQUET_FIELD_ID_META_KEY).is_none());
let use_position_fallback = missing_field_ids && task.name_mapping.is_none();
let arrow_metadata = if missing_field_ids {
let arrow_schema = if let Some(name_mapping) = &task.name_mapping {
apply_name_mapping_to_arrow_schema(
Arc::clone(arrow_metadata.schema()),
name_mapping,
)?
} else {
add_fallback_field_ids_to_arrow_schema(arrow_metadata.schema())
};
let options = ArrowReaderOptions::new().with_schema(arrow_schema);
ArrowReaderMetadata::try_new(Arc::clone(arrow_metadata.metadata()), options).map_err(
|e| {
Error::new(
ErrorKind::Unexpected,
"Failed to create ArrowReaderMetadata with field ID schema",
)
.with_source(e)
},
)?
} else {
arrow_metadata
};
let arrow_metadata = if let Some(coerced_schema) =
coerce_int96_timestamps(arrow_metadata.schema(), &task.schema)
{
let options = ArrowReaderOptions::new().with_schema(Arc::clone(&coerced_schema));
ArrowReaderMetadata::try_new(Arc::clone(arrow_metadata.metadata()), options).map_err(
|e| {
Error::new(
ErrorKind::Unexpected,
format!(
"Failed to create ArrowReaderMetadata with INT96-coerced schema: {coerced_schema}"
),
)
.with_source(e)
},
)?
} else {
arrow_metadata
};
let mut record_batch_stream_builder =
ParquetRecordBatchStreamBuilder::new_with_metadata(parquet_file_reader, arrow_metadata);
let project_field_ids_without_metadata: Vec<i32> = task
.project_field_ids
.iter()
.filter(|&&id| !is_metadata_field(id))
.copied()
.collect();
let projection_mask = ArrowReader::get_arrow_projection_mask(
&project_field_ids_without_metadata,
&task.schema,
record_batch_stream_builder.parquet_schema(),
record_batch_stream_builder.schema(),
use_position_fallback, )?;
record_batch_stream_builder =
record_batch_stream_builder.with_projection(projection_mask.clone());
let mut record_batch_transformer_builder =
RecordBatchTransformerBuilder::new(task.schema_ref(), task.project_field_ids());
if task.project_field_ids().contains(&RESERVED_FIELD_ID_FILE) {
let file_datum = Datum::string(task.data_file_path.clone());
record_batch_transformer_builder =
record_batch_transformer_builder.with_constant(RESERVED_FIELD_ID_FILE, file_datum);
}
if let (Some(partition_spec), Some(partition_data)) =
(task.partition_spec.clone(), task.partition.clone())
{
record_batch_transformer_builder =
record_batch_transformer_builder.with_partition(partition_spec, partition_data)?;
}
let mut record_batch_transformer = record_batch_transformer_builder.build();
if let Some(batch_size) = self.batch_size {
record_batch_stream_builder = record_batch_stream_builder.with_batch_size(batch_size);
}
let delete_filter = delete_filter_rx.await.unwrap()?;
let delete_predicate = delete_filter.build_equality_delete_predicate(&task).await?;
let final_predicate = match (&task.predicate, delete_predicate) {
(None, None) => None,
(Some(predicate), None) => Some(predicate.clone()),
(None, Some(ref predicate)) => Some(predicate.clone()),
(Some(filter_predicate), Some(delete_predicate)) => {
Some(filter_predicate.clone().and(delete_predicate))
}
};
let mut selected_row_group_indices = None;
let mut row_selection = None;
if task.start != 0 || task.length != 0 {
let byte_range_filtered_row_groups = ArrowReader::filter_row_groups_by_byte_range(
record_batch_stream_builder.metadata(),
task.start,
task.length,
)?;
selected_row_group_indices = Some(byte_range_filtered_row_groups);
}
if let Some(predicate) = final_predicate {
let (iceberg_field_ids, field_id_map) = ArrowReader::build_field_id_set_and_map(
record_batch_stream_builder.parquet_schema(),
record_batch_stream_builder.schema(),
&predicate,
use_position_fallback,
)?;
let row_filter = ArrowReader::get_row_filter(
&predicate,
record_batch_stream_builder.parquet_schema(),
&iceberg_field_ids,
&field_id_map,
)?;
record_batch_stream_builder = record_batch_stream_builder.with_row_filter(row_filter);
if self.row_group_filtering_enabled {
let predicate_filtered_row_groups = ArrowReader::get_selected_row_group_indices(
&predicate,
record_batch_stream_builder.metadata(),
&field_id_map,
&task.schema,
)?;
selected_row_group_indices = match selected_row_group_indices {
Some(byte_range_filtered) => {
let intersection: Vec<usize> = byte_range_filtered
.into_iter()
.filter(|idx| predicate_filtered_row_groups.contains(idx))
.collect();
Some(intersection)
}
None => Some(predicate_filtered_row_groups),
};
}
if self.row_selection_enabled {
row_selection = ArrowReader::get_row_selection_for_filter_predicate(
&predicate,
record_batch_stream_builder.metadata(),
&selected_row_group_indices,
&field_id_map,
&task.schema,
)?;
}
}
let positional_delete_indexes = delete_filter.get_delete_vector(&task);
if let Some(positional_delete_indexes) = positional_delete_indexes {
let delete_row_selection = {
let positional_delete_indexes = positional_delete_indexes.lock().unwrap();
ArrowReader::build_deletes_row_selection(
record_batch_stream_builder.metadata().row_groups(),
&selected_row_group_indices,
&positional_delete_indexes,
)
}?;
row_selection = match row_selection {
None => Some(delete_row_selection),
Some(filter_row_selection) => {
Some(filter_row_selection.intersection(&delete_row_selection))
}
};
}
if let Some(row_selection) = row_selection {
record_batch_stream_builder =
record_batch_stream_builder.with_row_selection(row_selection);
}
if let Some(selected_row_group_indices) = selected_row_group_indices {
record_batch_stream_builder =
record_batch_stream_builder.with_row_groups(selected_row_group_indices);
}
let record_batch_stream =
record_batch_stream_builder
.build()?
.map(move |batch| match batch {
Ok(batch) => {
record_batch_transformer.process_record_batch(batch)
}
Err(err) => Err(err.into()),
});
Ok(Box::pin(record_batch_stream) as ArrowRecordBatchStream)
}
}
impl ArrowReader {
pub(crate) async fn open_parquet_file(
data_file_path: &str,
file_io: &FileIO,
file_size_in_bytes: u64,
parquet_read_options: ParquetReadOptions,
bytes_read: &Arc<AtomicU64>,
) -> Result<(ArrowFileReader, ArrowReaderMetadata)> {
let parquet_file = file_io.new_input(data_file_path)?;
let counting_reader =
CountingFileRead::new(parquet_file.reader().await?, Arc::clone(bytes_read));
Self::build_parquet_reader(
Box::new(counting_reader),
file_size_in_bytes,
parquet_read_options,
)
.await
}
async fn build_parquet_reader(
parquet_reader: Box<dyn FileRead>,
file_size_in_bytes: u64,
parquet_read_options: ParquetReadOptions,
) -> Result<(ArrowFileReader, ArrowReaderMetadata)> {
let mut reader = ArrowFileReader::new(
FileMetadata {
size: file_size_in_bytes,
},
parquet_reader,
)
.with_parquet_read_options(parquet_read_options);
let arrow_metadata = ArrowReaderMetadata::load_async(&mut reader, Default::default())
.await
.map_err(|e| {
Error::new(ErrorKind::Unexpected, "Failed to load Parquet metadata").with_source(e)
})?;
Ok((reader, arrow_metadata))
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use std::fs::File;
use std::sync::Arc;
use arrow_array::cast::AsArray;
use arrow_array::{Array, ArrayRef, RecordBatch};
use arrow_schema::{DataType, Field, Schema as ArrowSchema};
use futures::TryStreamExt;
use parquet::arrow::{ArrowWriter, PARQUET_FIELD_ID_META_KEY};
use parquet::basic::Compression;
use parquet::file::properties::WriterProperties;
use tempfile::TempDir;
use crate::Runtime;
use crate::arrow::ArrowReaderBuilder;
use crate::io::FileIO;
use crate::scan::{FileScanTask, FileScanTaskStream};
use crate::spec::{DataFileFormat, NestedField, PrimitiveType, Schema, SchemaRef, Type};
const UNIX_EPOCH_JULIAN: i64 = 2_440_588;
const MICROS_PER_DAY: i64 = 86_400_000_000;
const INT96_TEST_NANOS_WITHIN_DAY: u64 = 43_200_000_000_000;
const INT96_TEST_JULIAN_DAY: u32 = 2_953_529;
fn make_int96_test_value() -> (parquet::data_type::Int96, i64) {
let mut val = parquet::data_type::Int96::new();
val.set_data(
(INT96_TEST_NANOS_WITHIN_DAY & 0xFFFFFFFF) as u32,
(INT96_TEST_NANOS_WITHIN_DAY >> 32) as u32,
INT96_TEST_JULIAN_DAY,
);
let expected_micros = (INT96_TEST_JULIAN_DAY as i64 - UNIX_EPOCH_JULIAN) * MICROS_PER_DAY
+ (INT96_TEST_NANOS_WITHIN_DAY / 1_000) as i64;
(val, expected_micros)
}
async fn read_int96_batches(
file_path: &str,
schema: SchemaRef,
project_field_ids: Vec<i32>,
) -> Vec<RecordBatch> {
let file_io = FileIO::new_with_fs();
let reader = ArrowReaderBuilder::new(file_io, Runtime::current()).build();
let file_size = std::fs::metadata(file_path).unwrap().len();
let task = FileScanTask::builder()
.with_file_size_in_bytes(file_size)
.with_start(0)
.with_length(file_size)
.with_data_file_path(file_path.to_string())
.with_data_file_format(DataFileFormat::Parquet)
.with_schema(schema)
.with_project_field_ids(project_field_ids)
.with_case_sensitive(false)
.build();
let tasks = Box::pin(futures::stream::iter(vec![Ok(task)])) as FileScanTaskStream;
reader
.read(tasks)
.unwrap()
.stream()
.try_collect()
.await
.unwrap()
}
fn write_int96_parquet_file(
table_location: &str,
filename: &str,
with_field_ids: bool,
) -> (String, Vec<i64>) {
use parquet::basic::{Repetition, Type as PhysicalType};
use parquet::data_type::{Int32Type, Int96, Int96Type};
use parquet::file::writer::SerializedFileWriter;
use parquet::schema::types::Type as SchemaType;
let file_path = format!("{table_location}/{filename}");
let mut ts_builder = SchemaType::primitive_type_builder("ts", PhysicalType::INT96)
.with_repetition(Repetition::OPTIONAL);
let mut id_builder = SchemaType::primitive_type_builder("id", PhysicalType::INT32)
.with_repetition(Repetition::REQUIRED);
if with_field_ids {
ts_builder = ts_builder.with_id(Some(1));
id_builder = id_builder.with_id(Some(2));
}
let schema = SchemaType::group_type_builder("schema")
.with_fields(vec![
Arc::new(ts_builder.build().unwrap()),
Arc::new(id_builder.build().unwrap()),
])
.build()
.unwrap();
const NOON_NANOS: u64 = INT96_TEST_NANOS_WITHIN_DAY;
const JULIAN_3333: u32 = INT96_TEST_JULIAN_DAY;
const JULIAN_2100: u32 = 2_488_070;
let test_data: Vec<(u32, u32, u32, i64)> = vec![
(
0,
0,
JULIAN_3333,
(JULIAN_3333 as i64 - UNIX_EPOCH_JULIAN) * MICROS_PER_DAY,
),
(
(NOON_NANOS & 0xFFFFFFFF) as u32,
(NOON_NANOS >> 32) as u32,
JULIAN_3333,
(JULIAN_3333 as i64 - UNIX_EPOCH_JULIAN) * MICROS_PER_DAY
+ (NOON_NANOS / 1_000) as i64,
),
(
0,
0,
JULIAN_2100,
(JULIAN_2100 as i64 - UNIX_EPOCH_JULIAN) * MICROS_PER_DAY,
),
];
let int96_values: Vec<Int96> = test_data
.iter()
.map(|(lo, hi, day, _)| {
let mut v = Int96::new();
v.set_data(*lo, *hi, *day);
v
})
.collect();
let id_values: Vec<i32> = (0..test_data.len() as i32).collect();
let expected_micros: Vec<i64> = test_data.iter().map(|(_, _, _, m)| *m).collect();
let file = File::create(&file_path).unwrap();
let mut writer =
SerializedFileWriter::new(file, Arc::new(schema), Default::default()).unwrap();
let mut row_group = writer.next_row_group().unwrap();
{
let mut col = row_group.next_column().unwrap().unwrap();
col.typed::<Int96Type>()
.write_batch(&int96_values, Some(&vec![1; test_data.len()]), None)
.unwrap();
col.close().unwrap();
}
{
let mut col = row_group.next_column().unwrap().unwrap();
col.typed::<Int32Type>()
.write_batch(&id_values, None, None)
.unwrap();
col.close().unwrap();
}
row_group.close().unwrap();
writer.close().unwrap();
(file_path, expected_micros)
}
async fn assert_int96_read_matches(
file_path: &str,
schema: SchemaRef,
project_field_ids: Vec<i32>,
expected_micros: &[i64],
) {
use arrow_array::TimestampMicrosecondArray;
let batches = read_int96_batches(file_path, schema, project_field_ids).await;
assert_eq!(batches.len(), 1);
let ts_array = batches[0]
.column(0)
.as_any()
.downcast_ref::<TimestampMicrosecondArray>()
.expect("Expected TimestampMicrosecondArray");
for (i, expected) in expected_micros.iter().enumerate() {
assert_eq!(
ts_array.value(i),
*expected,
"Row {i}: got {}, expected {expected}",
ts_array.value(i)
);
}
}
#[tokio::test]
async fn test_read_with_concurrency_one() {
use arrow_array::Int32Array;
let schema = Arc::new(
Schema::builder()
.with_schema_id(1)
.with_fields(vec![
NestedField::required(1, "id", Type::Primitive(PrimitiveType::Int)).into(),
NestedField::required(2, "file_num", Type::Primitive(PrimitiveType::Int))
.into(),
])
.build()
.unwrap(),
);
let arrow_schema = Arc::new(ArrowSchema::new(vec![
Field::new("id", DataType::Int32, false).with_metadata(HashMap::from([(
PARQUET_FIELD_ID_META_KEY.to_string(),
"1".to_string(),
)])),
Field::new("file_num", DataType::Int32, false).with_metadata(HashMap::from([(
PARQUET_FIELD_ID_META_KEY.to_string(),
"2".to_string(),
)])),
]));
let tmp_dir = TempDir::new().unwrap();
let table_location = tmp_dir.path().to_str().unwrap().to_string();
let file_io = FileIO::new_with_fs();
let props = WriterProperties::builder()
.set_compression(Compression::SNAPPY)
.build();
for file_num in 0..3 {
let id_data = Arc::new(Int32Array::from_iter_values(
file_num * 10..(file_num + 1) * 10,
)) as ArrayRef;
let file_num_data = Arc::new(Int32Array::from(vec![file_num; 10])) as ArrayRef;
let to_write =
RecordBatch::try_new(arrow_schema.clone(), vec![id_data, file_num_data]).unwrap();
let file = File::create(format!("{table_location}/file_{file_num}.parquet")).unwrap();
let mut writer =
ArrowWriter::try_new(file, to_write.schema(), Some(props.clone())).unwrap();
writer.write(&to_write).expect("Writing batch");
writer.close().unwrap();
}
let reader = ArrowReaderBuilder::new(file_io, Runtime::current())
.with_data_file_concurrency_limit(1)
.build();
let tasks = vec![
Ok(FileScanTask::builder()
.with_file_size_in_bytes(
std::fs::metadata(format!("{table_location}/file_0.parquet"))
.unwrap()
.len(),
)
.with_start(0)
.with_length(0)
.with_data_file_path(format!("{table_location}/file_0.parquet"))
.with_data_file_format(DataFileFormat::Parquet)
.with_schema(schema.clone())
.with_project_field_ids(vec![1, 2])
.with_case_sensitive(false)
.build()),
Ok(FileScanTask::builder()
.with_file_size_in_bytes(
std::fs::metadata(format!("{table_location}/file_1.parquet"))
.unwrap()
.len(),
)
.with_start(0)
.with_length(0)
.with_data_file_path(format!("{table_location}/file_1.parquet"))
.with_data_file_format(DataFileFormat::Parquet)
.with_schema(schema.clone())
.with_project_field_ids(vec![1, 2])
.with_case_sensitive(false)
.build()),
Ok(FileScanTask::builder()
.with_file_size_in_bytes(
std::fs::metadata(format!("{table_location}/file_2.parquet"))
.unwrap()
.len(),
)
.with_start(0)
.with_length(0)
.with_data_file_path(format!("{table_location}/file_2.parquet"))
.with_data_file_format(DataFileFormat::Parquet)
.with_schema(schema.clone())
.with_project_field_ids(vec![1, 2])
.with_case_sensitive(false)
.build()),
];
let tasks_stream = Box::pin(futures::stream::iter(tasks)) as FileScanTaskStream;
let result = reader
.read(tasks_stream)
.unwrap()
.stream()
.try_collect::<Vec<RecordBatch>>()
.await
.unwrap();
let total_rows: usize = result.iter().map(|b| b.num_rows()).sum();
assert_eq!(total_rows, 30, "Should have 30 total rows");
let mut all_ids = Vec::new();
let mut all_file_nums = Vec::new();
for batch in &result {
let id_col = batch
.column(0)
.as_primitive::<arrow_array::types::Int32Type>();
let file_num_col = batch
.column(1)
.as_primitive::<arrow_array::types::Int32Type>();
for i in 0..batch.num_rows() {
all_ids.push(id_col.value(i));
all_file_nums.push(file_num_col.value(i));
}
}
assert_eq!(all_ids.len(), 30);
assert_eq!(all_file_nums.len(), 30);
for i in 0..10 {
assert_eq!(all_file_nums[i], 0, "First 10 rows should be from file_0");
assert_eq!(all_ids[i], i as i32, "IDs should be 0-9");
}
for i in 10..20 {
assert_eq!(all_file_nums[i], 1, "Next 10 rows should be from file_1");
assert_eq!(all_ids[i], i as i32, "IDs should be 10-19");
}
for i in 20..30 {
assert_eq!(all_file_nums[i], 2, "Last 10 rows should be from file_2");
assert_eq!(all_ids[i], i as i32, "IDs should be 20-29");
}
}
#[tokio::test]
async fn test_read_int96_timestamps_with_field_ids() {
let schema = Arc::new(
Schema::builder()
.with_schema_id(1)
.with_fields(vec![
NestedField::optional(1, "ts", Type::Primitive(PrimitiveType::Timestamp))
.into(),
NestedField::required(2, "id", Type::Primitive(PrimitiveType::Int)).into(),
])
.build()
.unwrap(),
);
let tmp_dir = TempDir::new().unwrap();
let table_location = tmp_dir.path().to_str().unwrap().to_string();
let (file_path, expected_micros) =
write_int96_parquet_file(&table_location, "with_ids.parquet", true);
assert_int96_read_matches(&file_path, schema, vec![1, 2], &expected_micros).await;
}
#[tokio::test]
async fn test_read_int96_timestamps_without_field_ids() {
let schema = Arc::new(
Schema::builder()
.with_schema_id(1)
.with_fields(vec![
NestedField::optional(1, "ts", Type::Primitive(PrimitiveType::Timestamp))
.into(),
NestedField::required(2, "id", Type::Primitive(PrimitiveType::Int)).into(),
])
.build()
.unwrap(),
);
let tmp_dir = TempDir::new().unwrap();
let table_location = tmp_dir.path().to_str().unwrap().to_string();
let (file_path, expected_micros) =
write_int96_parquet_file(&table_location, "no_ids.parquet", false);
assert_int96_read_matches(&file_path, schema, vec![1, 2], &expected_micros).await;
}
#[tokio::test]
async fn test_read_int96_timestamps_in_struct() {
use arrow_array::{StructArray, TimestampMicrosecondArray};
use parquet::basic::{Repetition, Type as PhysicalType};
use parquet::data_type::Int96Type;
use parquet::file::writer::SerializedFileWriter;
use parquet::schema::types::Type as SchemaType;
let tmp_dir = TempDir::new().unwrap();
let table_location = tmp_dir.path().to_str().unwrap().to_string();
let file_path = format!("{table_location}/struct_int96.parquet");
let ts_type = SchemaType::primitive_type_builder("ts", PhysicalType::INT96)
.with_repetition(Repetition::OPTIONAL)
.with_id(Some(2))
.build()
.unwrap();
let struct_type = SchemaType::group_type_builder("data")
.with_repetition(Repetition::REQUIRED)
.with_id(Some(1))
.with_fields(vec![Arc::new(ts_type)])
.build()
.unwrap();
let parquet_schema = SchemaType::group_type_builder("schema")
.with_fields(vec![Arc::new(struct_type)])
.build()
.unwrap();
let (int96_val, expected_micros) = make_int96_test_value();
let file = File::create(&file_path).unwrap();
let mut writer =
SerializedFileWriter::new(file, Arc::new(parquet_schema), Default::default()).unwrap();
let mut row_group = writer.next_row_group().unwrap();
{
let mut col = row_group.next_column().unwrap().unwrap();
col.typed::<Int96Type>()
.write_batch(&[int96_val], Some(&[1]), None)
.unwrap();
col.close().unwrap();
}
row_group.close().unwrap();
writer.close().unwrap();
let iceberg_schema = Arc::new(
Schema::builder()
.with_schema_id(1)
.with_fields(vec![
NestedField::required(
1,
"data",
Type::Struct(crate::spec::StructType::new(vec![
NestedField::optional(
2,
"ts",
Type::Primitive(PrimitiveType::Timestamp),
)
.into(),
])),
)
.into(),
])
.build()
.unwrap(),
);
let batches = read_int96_batches(&file_path, iceberg_schema, vec![1]).await;
assert_eq!(batches.len(), 1);
let struct_array = batches[0]
.column(0)
.as_any()
.downcast_ref::<StructArray>()
.expect("Expected StructArray");
let ts_array = struct_array
.column(0)
.as_any()
.downcast_ref::<TimestampMicrosecondArray>()
.expect("Expected TimestampMicrosecondArray inside struct");
assert_eq!(
ts_array.value(0),
expected_micros,
"INT96 in struct: got {}, expected {expected_micros}",
ts_array.value(0)
);
}
#[tokio::test]
async fn test_read_int96_timestamps_in_list() {
use arrow_array::{ListArray, TimestampMicrosecondArray};
use parquet::basic::{Repetition, Type as PhysicalType};
use parquet::data_type::Int96Type;
use parquet::file::writer::SerializedFileWriter;
use parquet::schema::types::Type as SchemaType;
let tmp_dir = TempDir::new().unwrap();
let table_location = tmp_dir.path().to_str().unwrap().to_string();
let file_path = format!("{table_location}/list_int96.parquet");
let element_type = SchemaType::primitive_type_builder("element", PhysicalType::INT96)
.with_repetition(Repetition::OPTIONAL)
.with_id(Some(2))
.build()
.unwrap();
let list_group = SchemaType::group_type_builder("list")
.with_repetition(Repetition::REPEATED)
.with_fields(vec![Arc::new(element_type)])
.build()
.unwrap();
let list_type = SchemaType::group_type_builder("timestamps")
.with_repetition(Repetition::OPTIONAL)
.with_id(Some(1))
.with_logical_type(Some(parquet::basic::LogicalType::List))
.with_fields(vec![Arc::new(list_group)])
.build()
.unwrap();
let parquet_schema = SchemaType::group_type_builder("schema")
.with_fields(vec![Arc::new(list_type)])
.build()
.unwrap();
let (int96_val, expected_micros) = make_int96_test_value();
let file = File::create(&file_path).unwrap();
let mut writer =
SerializedFileWriter::new(file, Arc::new(parquet_schema), Default::default()).unwrap();
let mut row_group = writer.next_row_group().unwrap();
{
let mut col = row_group.next_column().unwrap().unwrap();
col.typed::<Int96Type>()
.write_batch(&[int96_val], Some(&[3]), Some(&[0]))
.unwrap();
col.close().unwrap();
}
row_group.close().unwrap();
writer.close().unwrap();
let iceberg_schema = Arc::new(
Schema::builder()
.with_schema_id(1)
.with_fields(vec![
NestedField::optional(
1,
"timestamps",
Type::List(crate::spec::ListType {
element_field: NestedField::optional(
2,
"element",
Type::Primitive(PrimitiveType::Timestamp),
)
.into(),
}),
)
.into(),
])
.build()
.unwrap(),
);
let batches = read_int96_batches(&file_path, iceberg_schema, vec![1]).await;
assert_eq!(batches.len(), 1);
let list_array = batches[0]
.column(0)
.as_any()
.downcast_ref::<ListArray>()
.expect("Expected ListArray");
let ts_array = list_array
.values()
.as_any()
.downcast_ref::<TimestampMicrosecondArray>()
.expect("Expected TimestampMicrosecondArray inside list");
assert_eq!(
ts_array.value(0),
expected_micros,
"INT96 in list: got {}, expected {expected_micros}",
ts_array.value(0)
);
}
#[tokio::test]
async fn test_read_int96_timestamps_in_map() {
use arrow_array::{MapArray, TimestampMicrosecondArray};
use parquet::basic::{Repetition, Type as PhysicalType};
use parquet::data_type::{ByteArrayType, Int96Type};
use parquet::file::writer::SerializedFileWriter;
use parquet::schema::types::Type as SchemaType;
let tmp_dir = TempDir::new().unwrap();
let table_location = tmp_dir.path().to_str().unwrap().to_string();
let file_path = format!("{table_location}/map_int96.parquet");
let key_type = SchemaType::primitive_type_builder("key", PhysicalType::BYTE_ARRAY)
.with_repetition(Repetition::REQUIRED)
.with_logical_type(Some(parquet::basic::LogicalType::String))
.with_id(Some(2))
.build()
.unwrap();
let value_type = SchemaType::primitive_type_builder("value", PhysicalType::INT96)
.with_repetition(Repetition::OPTIONAL)
.with_id(Some(3))
.build()
.unwrap();
let key_value_group = SchemaType::group_type_builder("key_value")
.with_repetition(Repetition::REPEATED)
.with_fields(vec![Arc::new(key_type), Arc::new(value_type)])
.build()
.unwrap();
let map_type = SchemaType::group_type_builder("ts_map")
.with_repetition(Repetition::OPTIONAL)
.with_id(Some(1))
.with_logical_type(Some(parquet::basic::LogicalType::Map))
.with_fields(vec![Arc::new(key_value_group)])
.build()
.unwrap();
let parquet_schema = SchemaType::group_type_builder("schema")
.with_fields(vec![Arc::new(map_type)])
.build()
.unwrap();
let (int96_val, expected_micros) = make_int96_test_value();
let file = File::create(&file_path).unwrap();
let mut writer =
SerializedFileWriter::new(file, Arc::new(parquet_schema), Default::default()).unwrap();
let mut row_group = writer.next_row_group().unwrap();
{
let mut col = row_group.next_column().unwrap().unwrap();
col.typed::<ByteArrayType>()
.write_batch(
&[parquet::data_type::ByteArray::from("event_time")],
Some(&[2]),
Some(&[0]),
)
.unwrap();
col.close().unwrap();
}
{
let mut col = row_group.next_column().unwrap().unwrap();
col.typed::<Int96Type>()
.write_batch(&[int96_val], Some(&[3]), Some(&[0]))
.unwrap();
col.close().unwrap();
}
row_group.close().unwrap();
writer.close().unwrap();
let iceberg_schema = Arc::new(
Schema::builder()
.with_schema_id(1)
.with_fields(vec![
NestedField::optional(
1,
"ts_map",
Type::Map(crate::spec::MapType {
key_field: NestedField::required(
2,
"key",
Type::Primitive(PrimitiveType::String),
)
.into(),
value_field: NestedField::optional(
3,
"value",
Type::Primitive(PrimitiveType::Timestamp),
)
.into(),
}),
)
.into(),
])
.build()
.unwrap(),
);
let batches = read_int96_batches(&file_path, iceberg_schema, vec![1]).await;
assert_eq!(batches.len(), 1);
let map_array = batches[0]
.column(0)
.as_any()
.downcast_ref::<MapArray>()
.expect("Expected MapArray");
let ts_array = map_array
.values()
.as_any()
.downcast_ref::<TimestampMicrosecondArray>()
.expect("Expected TimestampMicrosecondArray as map values");
assert_eq!(
ts_array.value(0),
expected_micros,
"INT96 in map: got {}, expected {expected_micros}",
ts_array.value(0)
);
}
}