use std::collections::{HashMap, HashSet};
use std::str::FromStr;
use std::sync::Arc;
use arrow_schema::{Field, Schema as ArrowSchema, SchemaRef as ArrowSchemaRef};
use parquet::arrow::{PARQUET_FIELD_ID_META_KEY, ProjectionMask};
use parquet::schema::types::{SchemaDescriptor, Type as ParquetType};
use super::{ArrowReader, CollectFieldIdVisitor};
use crate::arrow::arrow_schema_to_schema;
use crate::error::Result;
use crate::expr::BoundPredicate;
use crate::expr::visitors::bound_predicate_visitor::visit;
use crate::spec::{NameMapping, NestedField, PrimitiveType, Schema, Type};
use crate::{Error, ErrorKind};
impl ArrowReader {
pub(super) fn build_field_id_set_and_map(
parquet_schema: &SchemaDescriptor,
arrow_schema: &ArrowSchemaRef,
predicate: &BoundPredicate,
use_position_fallback: bool,
) -> Result<(HashSet<i32>, HashMap<i32, usize>)> {
let mut collector = CollectFieldIdVisitor {
field_ids: HashSet::default(),
};
visit(&mut collector, predicate)?;
let iceberg_field_ids = collector.field_ids();
let field_id_map = match build_field_id_map(parquet_schema)? {
Some(map) => map,
None if use_position_fallback => build_fallback_field_id_map(parquet_schema),
None => build_field_id_map_from_arrow_schema(arrow_schema),
};
Ok((iceberg_field_ids, field_id_map))
}
fn include_leaf_field_id(field: &NestedField, field_ids: &mut Vec<i32>) {
match field.field_type.as_ref() {
Type::Primitive(_) => {
field_ids.push(field.id);
}
Type::Struct(struct_type) => {
for nested_field in struct_type.fields() {
Self::include_leaf_field_id(nested_field, field_ids);
}
}
Type::List(list_type) => {
Self::include_leaf_field_id(&list_type.element_field, field_ids);
}
Type::Map(map_type) => {
Self::include_leaf_field_id(&map_type.key_field, field_ids);
Self::include_leaf_field_id(&map_type.value_field, field_ids);
}
}
}
pub(super) fn get_arrow_projection_mask(
field_ids: &[i32],
iceberg_schema_of_task: &Schema,
parquet_schema: &SchemaDescriptor,
arrow_schema: &ArrowSchemaRef,
use_fallback: bool, ) -> Result<ProjectionMask> {
fn type_promotion_is_valid(
file_type: Option<&PrimitiveType>,
projected_type: Option<&PrimitiveType>,
) -> bool {
match (file_type, projected_type) {
(Some(lhs), Some(rhs)) if lhs == rhs => true,
(Some(PrimitiveType::Int), Some(PrimitiveType::Long)) => true,
(Some(PrimitiveType::Float), Some(PrimitiveType::Double)) => true,
(
Some(PrimitiveType::Decimal {
precision: file_precision,
scale: file_scale,
}),
Some(PrimitiveType::Decimal {
precision: requested_precision,
scale: requested_scale,
}),
) if requested_precision >= file_precision && file_scale == requested_scale => true,
(Some(PrimitiveType::Fixed(16)), Some(PrimitiveType::Uuid)) => true,
_ => false,
}
}
if field_ids.is_empty() {
return Ok(ProjectionMask::all());
}
if use_fallback {
Self::get_arrow_projection_mask_fallback(field_ids, parquet_schema)
} else {
let mut leaf_field_ids = vec![];
for field_id in field_ids {
let field = iceberg_schema_of_task.field_by_id(*field_id);
if let Some(field) = field {
Self::include_leaf_field_id(field, &mut leaf_field_ids);
}
}
Self::get_arrow_projection_mask_with_field_ids(
&leaf_field_ids,
iceberg_schema_of_task,
parquet_schema,
arrow_schema,
type_promotion_is_valid,
)
}
}
fn get_arrow_projection_mask_with_field_ids(
leaf_field_ids: &[i32],
iceberg_schema_of_task: &Schema,
parquet_schema: &SchemaDescriptor,
arrow_schema: &ArrowSchemaRef,
type_promotion_is_valid: fn(Option<&PrimitiveType>, Option<&PrimitiveType>) -> bool,
) -> Result<ProjectionMask> {
let mut column_map = HashMap::new();
let fields = arrow_schema.fields();
let mut projected_fields: HashMap<arrow_schema::FieldRef, i32> = HashMap::new();
let projected_arrow_schema = ArrowSchema::new_with_metadata(
fields.filter_leaves(|_, f| {
f.metadata()
.get(PARQUET_FIELD_ID_META_KEY)
.and_then(|field_id| i32::from_str(field_id).ok())
.is_some_and(|field_id| {
projected_fields.insert((*f).clone(), field_id);
leaf_field_ids.contains(&field_id)
})
}),
arrow_schema.metadata().clone(),
);
let iceberg_schema = arrow_schema_to_schema(&projected_arrow_schema)?;
fields.filter_leaves(|idx, field| {
let Some(field_id) = projected_fields.get(field).cloned() else {
return false;
};
let iceberg_field = iceberg_schema_of_task.field_by_id(field_id);
let parquet_iceberg_field = iceberg_schema.field_by_id(field_id);
if iceberg_field.is_none() || parquet_iceberg_field.is_none() {
return false;
}
if !type_promotion_is_valid(
parquet_iceberg_field
.unwrap()
.field_type
.as_primitive_type(),
iceberg_field.unwrap().field_type.as_primitive_type(),
) {
return false;
}
column_map.insert(field_id, idx);
true
});
let mut indices = vec![];
for field_id in leaf_field_ids {
if let Some(col_idx) = column_map.get(field_id) {
indices.push(*col_idx);
}
}
if indices.is_empty() {
Ok(ProjectionMask::all())
} else {
Ok(ProjectionMask::leaves(parquet_schema, indices))
}
}
fn get_arrow_projection_mask_fallback(
field_ids: &[i32],
parquet_schema: &SchemaDescriptor,
) -> Result<ProjectionMask> {
let parquet_root_fields = parquet_schema.root_schema().get_fields();
let mut root_indices = vec![];
for field_id in field_ids.iter() {
let parquet_pos = (*field_id - 1) as usize;
if parquet_pos < parquet_root_fields.len() {
root_indices.push(parquet_pos);
}
}
if root_indices.is_empty() {
Ok(ProjectionMask::all())
} else {
Ok(ProjectionMask::roots(parquet_schema, root_indices))
}
}
}
pub(super) fn build_field_id_map(
parquet_schema: &SchemaDescriptor,
) -> Result<Option<HashMap<i32, usize>>> {
let mut column_map = HashMap::new();
for (idx, field) in parquet_schema.columns().iter().enumerate() {
let field_type = field.self_type();
match field_type {
ParquetType::PrimitiveType { basic_info, .. } => {
if !basic_info.has_id() {
return Ok(None);
}
column_map.insert(basic_info.id(), idx);
}
ParquetType::GroupType { .. } => {
return Err(Error::new(
ErrorKind::DataInvalid,
format!(
"Leaf column in schema should be primitive type but got {field_type:?}"
),
));
}
};
}
Ok(Some(column_map))
}
fn leaf_count(ty: &parquet::schema::types::Type) -> usize {
if ty.is_primitive() {
1
} else {
ty.get_fields().iter().map(|f| leaf_count(f)).sum()
}
}
pub(super) fn build_fallback_field_id_map(
parquet_schema: &SchemaDescriptor,
) -> HashMap<i32, usize> {
let mut column_map = HashMap::new();
let mut leaf_idx = 0;
for (top_pos, field) in parquet_schema.root_schema().get_fields().iter().enumerate() {
let field_id = (top_pos + 1) as i32;
if field.is_primitive() {
column_map.insert(field_id, leaf_idx);
}
leaf_idx += leaf_count(field);
}
column_map
}
fn build_field_id_map_from_arrow_schema(arrow_schema: &ArrowSchemaRef) -> HashMap<i32, usize> {
let mut column_map = HashMap::new();
arrow_schema.fields().filter_leaves(|idx, field| {
if let Some(field_id) = field
.metadata()
.get(PARQUET_FIELD_ID_META_KEY)
.and_then(|value| i32::from_str(value).ok())
{
column_map.insert(field_id, idx);
}
false
});
column_map
}
pub(super) fn apply_name_mapping_to_arrow_schema(
arrow_schema: ArrowSchemaRef,
name_mapping: &NameMapping,
) -> Result<Arc<ArrowSchema>> {
debug_assert!(
arrow_schema
.fields()
.iter()
.next()
.is_none_or(|f| f.metadata().get(PARQUET_FIELD_ID_META_KEY).is_none()),
"Schema already has field IDs - name mapping should not be applied"
);
let fields_with_mapped_ids: Vec<_> = arrow_schema
.fields()
.iter()
.map(|field| {
let mapped_field_opt = name_mapping
.fields()
.iter()
.find(|f| f.names().contains(&field.name().to_string()));
let mut metadata = field.metadata().clone();
if let Some(mapped_field) = mapped_field_opt
&& let Some(field_id) = mapped_field.field_id()
{
metadata.insert(PARQUET_FIELD_ID_META_KEY.to_string(), field_id.to_string());
}
Field::new(field.name(), field.data_type().clone(), field.is_nullable())
.with_metadata(metadata)
})
.collect();
Ok(Arc::new(ArrowSchema::new_with_metadata(
fields_with_mapped_ids,
arrow_schema.metadata().clone(),
)))
}
pub(super) fn add_fallback_field_ids_to_arrow_schema(
arrow_schema: &ArrowSchemaRef,
) -> Arc<ArrowSchema> {
debug_assert!(
arrow_schema
.fields()
.iter()
.next()
.is_none_or(|f| f.metadata().get(PARQUET_FIELD_ID_META_KEY).is_none()),
"Schema already has field IDs"
);
let fields_with_fallback_ids: Vec<_> = arrow_schema
.fields()
.iter()
.enumerate()
.map(|(pos, field)| {
let mut metadata = field.metadata().clone();
let field_id = (pos + 1) as i32; metadata.insert(PARQUET_FIELD_ID_META_KEY.to_string(), field_id.to_string());
Field::new(field.name(), field.data_type().clone(), field.is_nullable())
.with_metadata(metadata)
})
.collect();
Arc::new(ArrowSchema::new_with_metadata(
fields_with_fallback_ids,
arrow_schema.metadata().clone(),
))
}
#[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, StringArray};
use arrow_schema::{DataType, Field, Schema as ArrowSchema, TimeUnit};
use futures::TryStreamExt;
use parquet::arrow::{ArrowWriter, PARQUET_FIELD_ID_META_KEY, ProjectionMask};
use parquet::basic::Compression;
use parquet::file::properties::WriterProperties;
use parquet::schema::parser::parse_message_type;
use parquet::schema::types::SchemaDescriptor;
use tempfile::TempDir;
use crate::arrow::{ArrowReader, ArrowReaderBuilder};
use crate::expr::{Bind, Reference};
use crate::io::FileIO;
use crate::scan::{FileScanTask, FileScanTaskStream};
use crate::spec::{
DataFileFormat, Datum, MappedField, NameMapping, NestedField, PrimitiveType, Schema, Type,
};
use crate::{ErrorKind, Runtime};
#[test]
fn test_arrow_projection_mask() {
let schema = Arc::new(
Schema::builder()
.with_schema_id(1)
.with_identifier_field_ids(vec![1])
.with_fields(vec![
NestedField::required(1, "c1", Type::Primitive(PrimitiveType::String)).into(),
NestedField::optional(2, "c2", Type::Primitive(PrimitiveType::Int)).into(),
NestedField::optional(
3,
"c3",
Type::Primitive(PrimitiveType::Decimal {
precision: 38,
scale: 3,
}),
)
.into(),
])
.build()
.unwrap(),
);
let arrow_schema = Arc::new(ArrowSchema::new(vec![
Field::new("c1", DataType::Utf8, false).with_metadata(HashMap::from([(
PARQUET_FIELD_ID_META_KEY.to_string(),
"1".to_string(),
)])),
Field::new("c2", DataType::Duration(TimeUnit::Microsecond), true).with_metadata(
HashMap::from([(PARQUET_FIELD_ID_META_KEY.to_string(), "2".to_string())]),
),
Field::new("c3", DataType::Decimal128(39, 3), true).with_metadata(HashMap::from([(
PARQUET_FIELD_ID_META_KEY.to_string(),
"3".to_string(),
)])),
]));
let message_type = "
message schema {
required binary c1 (STRING) = 1;
optional int32 c2 (INTEGER(8,true)) = 2;
optional fixed_len_byte_array(17) c3 (DECIMAL(39,3)) = 3;
}
";
let parquet_type = parse_message_type(message_type).expect("should parse schema");
let parquet_schema = SchemaDescriptor::new(Arc::new(parquet_type));
let err = ArrowReader::get_arrow_projection_mask(
&[1, 2, 3],
&schema,
&parquet_schema,
&arrow_schema,
false,
)
.unwrap_err();
assert_eq!(err.kind(), ErrorKind::DataInvalid);
assert_eq!(
err.to_string(),
"DataInvalid => Unsupported Arrow data type: Duration(µs)".to_string()
);
let err = ArrowReader::get_arrow_projection_mask(
&[1, 3],
&schema,
&parquet_schema,
&arrow_schema,
false,
)
.unwrap_err();
assert_eq!(err.kind(), ErrorKind::DataInvalid);
assert_eq!(
err.to_string(),
"DataInvalid => Failed to create decimal type, source: DataInvalid => Decimals with precision larger than 38 are not supported: 39".to_string()
);
let mask = ArrowReader::get_arrow_projection_mask(
&[1],
&schema,
&parquet_schema,
&arrow_schema,
false,
)
.expect("Some ProjectionMask");
assert_eq!(mask, ProjectionMask::leaves(&parquet_schema, vec![0]));
}
#[tokio::test]
async fn test_schema_evolution_add_column() {
use arrow_array::{Array, Int32Array};
let new_schema = Arc::new(
Schema::builder()
.with_schema_id(2)
.with_fields(vec![
NestedField::required(1, "a", Type::Primitive(PrimitiveType::Int)).into(),
NestedField::optional(2, "b", Type::Primitive(PrimitiveType::Int)).into(),
])
.build()
.unwrap(),
);
let arrow_schema_old = Arc::new(ArrowSchema::new(vec![
Field::new("a", DataType::Int32, false).with_metadata(HashMap::from([(
PARQUET_FIELD_ID_META_KEY.to_string(),
"1".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 data_a = Arc::new(Int32Array::from(vec![1, 2, 3])) as ArrayRef;
let to_write = RecordBatch::try_new(arrow_schema_old.clone(), vec![data_a]).unwrap();
let props = WriterProperties::builder()
.set_compression(Compression::SNAPPY)
.build();
let file = File::create(format!("{table_location}/old_file.parquet")).unwrap();
let mut writer = ArrowWriter::try_new(file, to_write.schema(), Some(props)).unwrap();
writer.write(&to_write).expect("Writing batch");
writer.close().unwrap();
let reader = ArrowReaderBuilder::new(file_io, Runtime::current()).build();
let tasks = Box::pin(futures::stream::iter(
vec![Ok(FileScanTask::builder()
.with_file_size_in_bytes(
std::fs::metadata(format!("{table_location}/old_file.parquet"))
.unwrap()
.len(),
)
.with_start(0)
.with_length(0)
.with_data_file_path(format!("{table_location}/old_file.parquet"))
.with_data_file_format(DataFileFormat::Parquet)
.with_schema(new_schema.clone())
.with_project_field_ids(vec![1, 2]) .with_case_sensitive(false)
.build())]
.into_iter(),
)) as FileScanTaskStream;
let result = reader
.read(tasks)
.unwrap()
.stream()
.try_collect::<Vec<RecordBatch>>()
.await
.unwrap();
assert_eq!(result.len(), 1);
let batch = &result[0];
assert_eq!(batch.num_columns(), 2);
assert_eq!(batch.num_rows(), 3);
let col_a = batch
.column(0)
.as_primitive::<arrow_array::types::Int32Type>();
assert_eq!(col_a.values(), &[1, 2, 3]);
let col_b = batch
.column(1)
.as_primitive::<arrow_array::types::Int32Type>();
assert_eq!(col_b.null_count(), 3);
assert!(col_b.is_null(0));
assert!(col_b.is_null(1));
assert!(col_b.is_null(2));
}
#[tokio::test]
async fn test_read_parquet_file_without_field_ids() {
let schema = Arc::new(
Schema::builder()
.with_schema_id(1)
.with_fields(vec![
NestedField::required(1, "name", Type::Primitive(PrimitiveType::String)).into(),
NestedField::required(2, "age", Type::Primitive(PrimitiveType::Int)).into(),
])
.build()
.unwrap(),
);
let arrow_schema = Arc::new(ArrowSchema::new(vec![
Field::new("name", DataType::Utf8, false),
Field::new("age", DataType::Int32, false),
]));
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 name_data = vec!["Alice", "Bob", "Charlie"];
let age_data = vec![30, 25, 35];
use arrow_array::Int32Array;
let name_col = Arc::new(StringArray::from(name_data.clone())) as ArrayRef;
let age_col = Arc::new(Int32Array::from(age_data.clone())) as ArrayRef;
let to_write = RecordBatch::try_new(arrow_schema.clone(), vec![name_col, age_col]).unwrap();
let props = WriterProperties::builder()
.set_compression(Compression::SNAPPY)
.build();
let file = File::create(format!("{table_location}/1.parquet")).unwrap();
let mut writer = ArrowWriter::try_new(file, to_write.schema(), Some(props)).unwrap();
writer.write(&to_write).expect("Writing batch");
writer.close().unwrap();
let reader = ArrowReaderBuilder::new(file_io, Runtime::current()).build();
let tasks = Box::pin(futures::stream::iter(
vec![Ok(FileScanTask::builder()
.with_file_size_in_bytes(
std::fs::metadata(format!("{table_location}/1.parquet"))
.unwrap()
.len(),
)
.with_start(0)
.with_length(0)
.with_data_file_path(format!("{table_location}/1.parquet"))
.with_data_file_format(DataFileFormat::Parquet)
.with_schema(schema.clone())
.with_project_field_ids(vec![1, 2])
.with_case_sensitive(false)
.build())]
.into_iter(),
)) as FileScanTaskStream;
let result = reader
.read(tasks)
.unwrap()
.stream()
.try_collect::<Vec<RecordBatch>>()
.await
.unwrap();
assert_eq!(result.len(), 1);
let batch = &result[0];
assert_eq!(batch.num_rows(), 3);
assert_eq!(batch.num_columns(), 2);
let name_array = batch.column(0).as_string::<i32>();
assert_eq!(name_array.value(0), "Alice");
assert_eq!(name_array.value(1), "Bob");
assert_eq!(name_array.value(2), "Charlie");
let age_array = batch
.column(1)
.as_primitive::<arrow_array::types::Int32Type>();
assert_eq!(age_array.value(0), 30);
assert_eq!(age_array.value(1), 25);
assert_eq!(age_array.value(2), 35);
}
#[tokio::test]
async fn test_read_parquet_with_name_mapping_uses_mapped_field_ids() {
let schema = Arc::new(
Schema::builder()
.with_schema_id(1)
.with_fields(vec![
NestedField::optional(1, "id", Type::Primitive(PrimitiveType::Long)).into(),
NestedField::optional(2, "name", Type::Primitive(PrimitiveType::String)).into(),
NestedField::optional(3, "dept", Type::Primitive(PrimitiveType::String)).into(),
NestedField::optional(4, "subdept", Type::Primitive(PrimitiveType::String))
.into(),
])
.build()
.unwrap(),
);
let arrow_schema = Arc::new(ArrowSchema::new(vec![
Field::new("name", DataType::Utf8, true),
Field::new("subdept", DataType::Utf8, true),
]));
let name_mapping = Arc::new(NameMapping::new(vec![
MappedField::new(Some(2), vec!["name".to_string()], vec![]),
MappedField::new(Some(4), vec!["subdept".to_string()], vec![]),
]));
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 name_col = Arc::new(StringArray::from(vec!["Alice", "Bob", "Charlie"])) as ArrayRef;
let subdept_col = Arc::new(StringArray::from(vec!["comms", "tax", "audit"])) as ArrayRef;
let to_write =
RecordBatch::try_new(arrow_schema.clone(), vec![name_col, subdept_col]).unwrap();
let props = WriterProperties::builder()
.set_compression(Compression::SNAPPY)
.build();
let file = File::create(format!("{table_location}/1.parquet")).unwrap();
let mut writer = ArrowWriter::try_new(file, to_write.schema(), Some(props)).unwrap();
writer.write(&to_write).expect("Writing batch");
writer.close().unwrap();
let reader = ArrowReaderBuilder::new(file_io, Runtime::current()).build();
let tasks = Box::pin(futures::stream::iter(
vec![Ok(FileScanTask::builder()
.with_file_size_in_bytes(
std::fs::metadata(format!("{table_location}/1.parquet"))
.unwrap()
.len(),
)
.with_start(0)
.with_length(0)
.with_data_file_path(format!("{table_location}/1.parquet"))
.with_data_file_format(DataFileFormat::Parquet)
.with_schema(schema.clone())
.with_project_field_ids(vec![2, 4])
.with_case_sensitive(false)
.with_name_mapping(Some(name_mapping))
.build())]
.into_iter(),
)) as FileScanTaskStream;
let result = reader
.read(tasks)
.unwrap()
.stream()
.try_collect::<Vec<RecordBatch>>()
.await
.unwrap();
assert_eq!(result.len(), 1);
let batch = &result[0];
assert_eq!(batch.num_rows(), 3);
assert_eq!(batch.num_columns(), 2);
let name_array = batch.column(0).as_string::<i32>();
assert_eq!(
name_array.null_count(),
0,
"`name` was NULL-filled: name mapping was ignored and position fallback was used"
);
assert_eq!(name_array.value(0), "Alice");
assert_eq!(name_array.value(1), "Bob");
assert_eq!(name_array.value(2), "Charlie");
let subdept_array = batch.column(1).as_string::<i32>();
assert_eq!(subdept_array.null_count(), 0);
assert_eq!(subdept_array.value(0), "comms");
assert_eq!(subdept_array.value(1), "tax");
assert_eq!(subdept_array.value(2), "audit");
}
#[tokio::test]
async fn test_predicate_on_name_mapped_file_uses_mapped_field_ids() {
let schema = Arc::new(
Schema::builder()
.with_schema_id(1)
.with_fields(vec![
NestedField::optional(1, "id", Type::Primitive(PrimitiveType::Long)).into(),
NestedField::optional(2, "name", Type::Primitive(PrimitiveType::String)).into(),
NestedField::optional(3, "dept", Type::Primitive(PrimitiveType::String)).into(),
NestedField::optional(4, "subdept", Type::Primitive(PrimitiveType::String))
.into(),
])
.build()
.unwrap(),
);
let arrow_schema = Arc::new(ArrowSchema::new(vec![
Field::new("name", DataType::Utf8, true),
Field::new("subdept", DataType::Utf8, true),
]));
let name_mapping = Arc::new(NameMapping::new(vec![
MappedField::new(Some(2), vec!["name".to_string()], vec![]),
MappedField::new(Some(4), vec!["subdept".to_string()], vec![]),
]));
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 name_col = Arc::new(StringArray::from(vec!["Alice", "Bob", "Sue"])) as ArrayRef;
let subdept_col = Arc::new(StringArray::from(vec!["Bob", "Alice", "Alice"])) as ArrayRef;
let to_write =
RecordBatch::try_new(arrow_schema.clone(), vec![name_col, subdept_col]).unwrap();
let props = WriterProperties::builder()
.set_compression(Compression::SNAPPY)
.build();
let file = File::create(format!("{table_location}/1.parquet")).unwrap();
let mut writer = ArrowWriter::try_new(file, to_write.schema(), Some(props)).unwrap();
writer.write(&to_write).expect("Writing batch");
writer.close().unwrap();
let predicate = Reference::new("name").equal_to(Datum::string("Alice"));
let reader = ArrowReaderBuilder::new(file_io, Runtime::current())
.with_row_group_filtering_enabled(true)
.with_row_selection_enabled(true)
.build();
let tasks = Box::pin(futures::stream::iter(
vec![Ok(FileScanTask::builder()
.with_file_size_in_bytes(
std::fs::metadata(format!("{table_location}/1.parquet"))
.unwrap()
.len(),
)
.with_start(0)
.with_length(0)
.with_data_file_path(format!("{table_location}/1.parquet"))
.with_data_file_format(DataFileFormat::Parquet)
.with_schema(schema.clone())
.with_project_field_ids(vec![2, 4])
.with_case_sensitive(false)
.with_name_mapping(Some(name_mapping))
.with_predicate(Some(predicate.bind(schema, true).unwrap()))
.build())]
.into_iter(),
)) as FileScanTaskStream;
let result = reader
.read(tasks)
.unwrap()
.stream()
.try_collect::<Vec<RecordBatch>>()
.await
.unwrap();
let total_rows: usize = result.iter().map(|b| b.num_rows()).sum();
assert_eq!(
total_rows, 1,
"filter `name = \"Alice\"` matched the wrong rows: predicate was evaluated \
against the wrong physical column"
);
let batch = &result[0];
let name_array = batch.column(0).as_string::<i32>();
assert_eq!(name_array.value(0), "Alice");
let subdept_array = batch.column(1).as_string::<i32>();
assert_eq!(subdept_array.value(0), "Bob");
}
#[tokio::test]
async fn test_read_parquet_without_field_ids_partial_projection() {
use arrow_array::Int32Array;
let schema = Arc::new(
Schema::builder()
.with_schema_id(1)
.with_fields(vec![
NestedField::required(1, "col1", Type::Primitive(PrimitiveType::String)).into(),
NestedField::required(2, "col2", Type::Primitive(PrimitiveType::Int)).into(),
NestedField::required(3, "col3", Type::Primitive(PrimitiveType::String)).into(),
NestedField::required(4, "col4", Type::Primitive(PrimitiveType::Int)).into(),
])
.build()
.unwrap(),
);
let arrow_schema = Arc::new(ArrowSchema::new(vec![
Field::new("col1", DataType::Utf8, false),
Field::new("col2", DataType::Int32, false),
Field::new("col3", DataType::Utf8, false),
Field::new("col4", DataType::Int32, false),
]));
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 col1_data = Arc::new(StringArray::from(vec!["a", "b"])) as ArrayRef;
let col2_data = Arc::new(Int32Array::from(vec![10, 20])) as ArrayRef;
let col3_data = Arc::new(StringArray::from(vec!["c", "d"])) as ArrayRef;
let col4_data = Arc::new(Int32Array::from(vec![30, 40])) as ArrayRef;
let to_write = RecordBatch::try_new(arrow_schema.clone(), vec![
col1_data, col2_data, col3_data, col4_data,
])
.unwrap();
let props = WriterProperties::builder()
.set_compression(Compression::SNAPPY)
.build();
let file = File::create(format!("{table_location}/1.parquet")).unwrap();
let mut writer = ArrowWriter::try_new(file, to_write.schema(), Some(props)).unwrap();
writer.write(&to_write).expect("Writing batch");
writer.close().unwrap();
let reader = ArrowReaderBuilder::new(file_io, Runtime::current()).build();
let tasks = Box::pin(futures::stream::iter(
vec![Ok(FileScanTask::builder()
.with_file_size_in_bytes(
std::fs::metadata(format!("{table_location}/1.parquet"))
.unwrap()
.len(),
)
.with_start(0)
.with_length(0)
.with_data_file_path(format!("{table_location}/1.parquet"))
.with_data_file_format(DataFileFormat::Parquet)
.with_schema(schema.clone())
.with_project_field_ids(vec![1, 3])
.with_case_sensitive(false)
.build())]
.into_iter(),
)) as FileScanTaskStream;
let result = reader
.read(tasks)
.unwrap()
.stream()
.try_collect::<Vec<RecordBatch>>()
.await
.unwrap();
assert_eq!(result.len(), 1);
let batch = &result[0];
assert_eq!(batch.num_rows(), 2);
assert_eq!(batch.num_columns(), 2);
let col1_array = batch.column(0).as_string::<i32>();
assert_eq!(col1_array.value(0), "a");
assert_eq!(col1_array.value(1), "b");
let col3_array = batch.column(1).as_string::<i32>();
assert_eq!(col3_array.value(0), "c");
assert_eq!(col3_array.value(1), "d");
}
#[tokio::test]
async fn test_read_parquet_without_field_ids_schema_evolution() {
use arrow_array::{Array, Int32Array};
let schema = Arc::new(
Schema::builder()
.with_schema_id(1)
.with_fields(vec![
NestedField::required(1, "name", Type::Primitive(PrimitiveType::String)).into(),
NestedField::required(2, "age", Type::Primitive(PrimitiveType::Int)).into(),
NestedField::optional(3, "city", Type::Primitive(PrimitiveType::String)).into(),
])
.build()
.unwrap(),
);
let arrow_schema = Arc::new(ArrowSchema::new(vec![
Field::new("name", DataType::Utf8, false),
Field::new("age", DataType::Int32, false),
]));
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 name_data = Arc::new(StringArray::from(vec!["Alice", "Bob"])) as ArrayRef;
let age_data = Arc::new(Int32Array::from(vec![30, 25])) as ArrayRef;
let to_write =
RecordBatch::try_new(arrow_schema.clone(), vec![name_data, age_data]).unwrap();
let props = WriterProperties::builder()
.set_compression(Compression::SNAPPY)
.build();
let file = File::create(format!("{table_location}/1.parquet")).unwrap();
let mut writer = ArrowWriter::try_new(file, to_write.schema(), Some(props)).unwrap();
writer.write(&to_write).expect("Writing batch");
writer.close().unwrap();
let reader = ArrowReaderBuilder::new(file_io, Runtime::current()).build();
let tasks = Box::pin(futures::stream::iter(
vec![Ok(FileScanTask::builder()
.with_file_size_in_bytes(
std::fs::metadata(format!("{table_location}/1.parquet"))
.unwrap()
.len(),
)
.with_start(0)
.with_length(0)
.with_data_file_path(format!("{table_location}/1.parquet"))
.with_data_file_format(DataFileFormat::Parquet)
.with_schema(schema.clone())
.with_project_field_ids(vec![1, 2, 3])
.with_case_sensitive(false)
.build())]
.into_iter(),
)) as FileScanTaskStream;
let result = reader
.read(tasks)
.unwrap()
.stream()
.try_collect::<Vec<RecordBatch>>()
.await
.unwrap();
assert_eq!(result.len(), 1);
let batch = &result[0];
assert_eq!(batch.num_rows(), 2);
assert_eq!(batch.num_columns(), 3);
let name_array = batch.column(0).as_string::<i32>();
assert_eq!(name_array.value(0), "Alice");
assert_eq!(name_array.value(1), "Bob");
let age_array = batch
.column(1)
.as_primitive::<arrow_array::types::Int32Type>();
assert_eq!(age_array.value(0), 30);
assert_eq!(age_array.value(1), 25);
let city_array = batch.column(2).as_string::<i32>();
assert_eq!(city_array.null_count(), 2);
assert!(city_array.is_null(0));
assert!(city_array.is_null(1));
}
#[tokio::test]
async fn test_read_parquet_without_field_ids_multiple_row_groups() {
use arrow_array::Int32Array;
let schema = Arc::new(
Schema::builder()
.with_schema_id(1)
.with_fields(vec![
NestedField::required(1, "name", Type::Primitive(PrimitiveType::String)).into(),
NestedField::required(2, "value", Type::Primitive(PrimitiveType::Int)).into(),
])
.build()
.unwrap(),
);
let arrow_schema = Arc::new(ArrowSchema::new(vec![
Field::new("name", DataType::Utf8, false),
Field::new("value", DataType::Int32, false),
]));
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)
.set_write_batch_size(2)
.set_max_row_group_row_count(Some(2))
.build();
let file = File::create(format!("{table_location}/1.parquet")).unwrap();
let mut writer = ArrowWriter::try_new(file, arrow_schema.clone(), Some(props)).unwrap();
for batch_num in 0..3 {
let name_data = Arc::new(StringArray::from(vec![
format!("name_{}", batch_num * 2),
format!("name_{}", batch_num * 2 + 1),
])) as ArrayRef;
let value_data =
Arc::new(Int32Array::from(vec![batch_num * 2, batch_num * 2 + 1])) as ArrayRef;
let batch =
RecordBatch::try_new(arrow_schema.clone(), vec![name_data, value_data]).unwrap();
writer.write(&batch).expect("Writing batch");
}
writer.close().unwrap();
let reader = ArrowReaderBuilder::new(file_io, Runtime::current()).build();
let tasks = Box::pin(futures::stream::iter(
vec![Ok(FileScanTask::builder()
.with_file_size_in_bytes(
std::fs::metadata(format!("{table_location}/1.parquet"))
.unwrap()
.len(),
)
.with_start(0)
.with_length(0)
.with_data_file_path(format!("{table_location}/1.parquet"))
.with_data_file_format(DataFileFormat::Parquet)
.with_schema(schema.clone())
.with_project_field_ids(vec![1, 2])
.with_case_sensitive(false)
.build())]
.into_iter(),
)) as FileScanTaskStream;
let result = reader
.read(tasks)
.unwrap()
.stream()
.try_collect::<Vec<RecordBatch>>()
.await
.unwrap();
assert!(!result.is_empty());
let mut all_names = Vec::new();
let mut all_values = Vec::new();
for batch in &result {
let name_array = batch.column(0).as_string::<i32>();
let value_array = batch
.column(1)
.as_primitive::<arrow_array::types::Int32Type>();
for i in 0..batch.num_rows() {
all_names.push(name_array.value(i).to_string());
all_values.push(value_array.value(i));
}
}
assert_eq!(all_names.len(), 6);
assert_eq!(all_values.len(), 6);
for i in 0..6 {
assert_eq!(all_names[i], format!("name_{i}"));
assert_eq!(all_values[i], i as i32);
}
}
#[tokio::test]
async fn test_read_parquet_without_field_ids_with_struct() {
use arrow_array::{Int32Array, StructArray};
use arrow_schema::Fields;
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,
"person",
Type::Struct(crate::spec::StructType::new(vec![
NestedField::required(
3,
"name",
Type::Primitive(PrimitiveType::String),
)
.into(),
NestedField::required(4, "age", Type::Primitive(PrimitiveType::Int))
.into(),
])),
)
.into(),
])
.build()
.unwrap(),
);
let arrow_schema = Arc::new(ArrowSchema::new(vec![
Field::new("id", DataType::Int32, false),
Field::new(
"person",
DataType::Struct(Fields::from(vec![
Field::new("name", DataType::Utf8, false),
Field::new("age", DataType::Int32, false),
])),
false,
),
]));
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 id_data = Arc::new(Int32Array::from(vec![1, 2])) as ArrayRef;
let name_data = Arc::new(StringArray::from(vec!["Alice", "Bob"])) as ArrayRef;
let age_data = Arc::new(Int32Array::from(vec![30, 25])) as ArrayRef;
let person_data = Arc::new(StructArray::from(vec![
(
Arc::new(Field::new("name", DataType::Utf8, false)),
name_data,
),
(
Arc::new(Field::new("age", DataType::Int32, false)),
age_data,
),
])) as ArrayRef;
let to_write =
RecordBatch::try_new(arrow_schema.clone(), vec![id_data, person_data]).unwrap();
let props = WriterProperties::builder()
.set_compression(Compression::SNAPPY)
.build();
let file = File::create(format!("{table_location}/1.parquet")).unwrap();
let mut writer = ArrowWriter::try_new(file, to_write.schema(), Some(props)).unwrap();
writer.write(&to_write).expect("Writing batch");
writer.close().unwrap();
let reader = ArrowReaderBuilder::new(file_io, Runtime::current()).build();
let tasks = Box::pin(futures::stream::iter(
vec![Ok(FileScanTask::builder()
.with_file_size_in_bytes(
std::fs::metadata(format!("{table_location}/1.parquet"))
.unwrap()
.len(),
)
.with_start(0)
.with_length(0)
.with_data_file_path(format!("{table_location}/1.parquet"))
.with_data_file_format(DataFileFormat::Parquet)
.with_schema(schema.clone())
.with_project_field_ids(vec![1, 2])
.with_case_sensitive(false)
.build())]
.into_iter(),
)) as FileScanTaskStream;
let result = reader
.read(tasks)
.unwrap()
.stream()
.try_collect::<Vec<RecordBatch>>()
.await
.unwrap();
assert_eq!(result.len(), 1);
let batch = &result[0];
assert_eq!(batch.num_rows(), 2);
assert_eq!(batch.num_columns(), 2);
let id_array = batch
.column(0)
.as_primitive::<arrow_array::types::Int32Type>();
assert_eq!(id_array.value(0), 1);
assert_eq!(id_array.value(1), 2);
let person_array = batch.column(1).as_struct();
assert_eq!(person_array.num_columns(), 2);
let name_array = person_array.column(0).as_string::<i32>();
assert_eq!(name_array.value(0), "Alice");
assert_eq!(name_array.value(1), "Bob");
let age_array = person_array
.column(1)
.as_primitive::<arrow_array::types::Int32Type>();
assert_eq!(age_array.value(0), 30);
assert_eq!(age_array.value(1), 25);
}
#[tokio::test]
async fn test_read_parquet_without_field_ids_schema_evolution_add_column_in_middle() {
use arrow_array::{Array, Int32Array};
let arrow_schema_old = Arc::new(ArrowSchema::new(vec![
Field::new("col0", DataType::Int32, true),
Field::new("col1", DataType::Int32, true),
]));
let schema = Arc::new(
Schema::builder()
.with_schema_id(1)
.with_fields(vec![
NestedField::optional(1, "col0", Type::Primitive(PrimitiveType::Int)).into(),
NestedField::optional(5, "newCol", Type::Primitive(PrimitiveType::Int)).into(),
NestedField::optional(2, "col1", 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_io = FileIO::new_with_fs();
let col0_data = Arc::new(Int32Array::from(vec![1, 2])) as ArrayRef;
let col1_data = Arc::new(Int32Array::from(vec![10, 20])) as ArrayRef;
let to_write =
RecordBatch::try_new(arrow_schema_old.clone(), vec![col0_data, col1_data]).unwrap();
let props = WriterProperties::builder()
.set_compression(Compression::SNAPPY)
.build();
let file = File::create(format!("{table_location}/1.parquet")).unwrap();
let mut writer = ArrowWriter::try_new(file, to_write.schema(), Some(props)).unwrap();
writer.write(&to_write).expect("Writing batch");
writer.close().unwrap();
let reader = ArrowReaderBuilder::new(file_io, Runtime::current()).build();
let tasks = Box::pin(futures::stream::iter(
vec![Ok(FileScanTask::builder()
.with_file_size_in_bytes(
std::fs::metadata(format!("{table_location}/1.parquet"))
.unwrap()
.len(),
)
.with_start(0)
.with_length(0)
.with_data_file_path(format!("{table_location}/1.parquet"))
.with_data_file_format(DataFileFormat::Parquet)
.with_schema(schema.clone())
.with_project_field_ids(vec![1, 5, 2])
.with_case_sensitive(false)
.build())]
.into_iter(),
)) as FileScanTaskStream;
let result = reader
.read(tasks)
.unwrap()
.stream()
.try_collect::<Vec<RecordBatch>>()
.await
.unwrap();
assert_eq!(result.len(), 1);
let batch = &result[0];
assert_eq!(batch.num_rows(), 2);
assert_eq!(batch.num_columns(), 3);
let result_col0 = batch
.column(0)
.as_primitive::<arrow_array::types::Int32Type>();
assert_eq!(result_col0.value(0), 1);
assert_eq!(result_col0.value(1), 2);
let result_newcol = batch
.column(1)
.as_primitive::<arrow_array::types::Int32Type>();
assert_eq!(result_newcol.null_count(), 2);
assert!(result_newcol.is_null(0));
assert!(result_newcol.is_null(1));
let result_col1 = batch
.column(2)
.as_primitive::<arrow_array::types::Int32Type>();
assert_eq!(result_col1.value(0), 10);
assert_eq!(result_col1.value(1), 20);
}
#[tokio::test]
async fn test_read_parquet_without_field_ids_filter_eliminates_all_rows() {
use arrow_array::{Float64Array, 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, "name", Type::Primitive(PrimitiveType::String)).into(),
NestedField::required(3, "value", Type::Primitive(PrimitiveType::Double))
.into(),
])
.build()
.unwrap(),
);
let arrow_schema = Arc::new(ArrowSchema::new(vec![
Field::new("id", DataType::Int32, false),
Field::new("name", DataType::Utf8, false),
Field::new("value", DataType::Float64, false),
]));
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 id_data = Arc::new(Int32Array::from(vec![10, 11, 12])) as ArrayRef;
let name_data = Arc::new(StringArray::from(vec!["a", "b", "c"])) as ArrayRef;
let value_data = Arc::new(Float64Array::from(vec![100.0, 200.0, 300.0])) as ArrayRef;
let to_write =
RecordBatch::try_new(arrow_schema.clone(), vec![id_data, name_data, value_data])
.unwrap();
let props = WriterProperties::builder()
.set_compression(Compression::SNAPPY)
.build();
let file = File::create(format!("{table_location}/1.parquet")).unwrap();
let mut writer = ArrowWriter::try_new(file, to_write.schema(), Some(props)).unwrap();
writer.write(&to_write).expect("Writing batch");
writer.close().unwrap();
let predicate = Reference::new("id").less_than(Datum::int(5));
let reader = ArrowReaderBuilder::new(file_io, Runtime::current())
.with_row_group_filtering_enabled(true)
.with_row_selection_enabled(true)
.build();
let tasks = Box::pin(futures::stream::iter(
vec![Ok(FileScanTask::builder()
.with_file_size_in_bytes(
std::fs::metadata(format!("{table_location}/1.parquet"))
.unwrap()
.len(),
)
.with_start(0)
.with_length(0)
.with_data_file_path(format!("{table_location}/1.parquet"))
.with_data_file_format(DataFileFormat::Parquet)
.with_schema(schema.clone())
.with_project_field_ids(vec![1, 2, 3])
.with_case_sensitive(false)
.with_predicate(Some(predicate.bind(schema, true).unwrap()))
.build())]
.into_iter(),
)) as FileScanTaskStream;
let result = reader
.read(tasks)
.unwrap()
.stream()
.try_collect::<Vec<RecordBatch>>()
.await
.unwrap();
assert!(result.is_empty() || result.iter().all(|batch| batch.num_rows() == 0));
}
#[tokio::test]
async fn test_bucket_partitioning_reads_source_column_from_file() {
use arrow_array::Int32Array;
use crate::spec::{Literal, PartitionSpec, Struct, Transform};
let schema = Arc::new(
Schema::builder()
.with_schema_id(0)
.with_fields(vec![
NestedField::required(1, "id", Type::Primitive(PrimitiveType::Int)).into(),
NestedField::optional(2, "name", Type::Primitive(PrimitiveType::String)).into(),
])
.build()
.unwrap(),
);
let partition_spec = Arc::new(
PartitionSpec::builder(schema.clone())
.with_spec_id(0)
.add_partition_field("id", "id_bucket", Transform::Bucket(4))
.unwrap()
.build()
.unwrap(),
);
let partition_data = Struct::from_iter(vec![Some(Literal::int(1))]);
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("name", DataType::Utf8, true).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 id_data = Arc::new(Int32Array::from(vec![1, 5, 9, 13])) as ArrayRef;
let name_data =
Arc::new(StringArray::from(vec!["Alice", "Bob", "Charlie", "Dave"])) as ArrayRef;
let to_write =
RecordBatch::try_new(arrow_schema.clone(), vec![id_data, name_data]).unwrap();
let props = WriterProperties::builder()
.set_compression(Compression::SNAPPY)
.build();
let file = File::create(format!("{}/data.parquet", &table_location)).unwrap();
let mut writer = ArrowWriter::try_new(file, to_write.schema(), Some(props)).unwrap();
writer.write(&to_write).expect("Writing batch");
writer.close().unwrap();
let reader = ArrowReaderBuilder::new(file_io, Runtime::current()).build();
let tasks = Box::pin(futures::stream::iter(
vec![Ok(FileScanTask::builder()
.with_file_size_in_bytes(
std::fs::metadata(format!("{table_location}/data.parquet"))
.unwrap()
.len(),
)
.with_start(0)
.with_length(0)
.with_data_file_path(format!("{table_location}/data.parquet"))
.with_data_file_format(DataFileFormat::Parquet)
.with_schema(schema.clone())
.with_project_field_ids(vec![1, 2])
.with_case_sensitive(false)
.with_partition(Some(partition_data))
.with_partition_spec(Some(partition_spec))
.build())]
.into_iter(),
)) as FileScanTaskStream;
let result = reader
.read(tasks)
.unwrap()
.stream()
.try_collect::<Vec<RecordBatch>>()
.await
.unwrap();
assert_eq!(result.len(), 1);
let batch = &result[0];
assert_eq!(batch.num_columns(), 2);
assert_eq!(batch.num_rows(), 4);
let id_col = batch
.column(0)
.as_primitive::<arrow_array::types::Int32Type>();
assert_eq!(id_col.value(0), 1);
assert_eq!(id_col.value(1), 5);
assert_eq!(id_col.value(2), 9);
assert_eq!(id_col.value(3), 13);
let name_col = batch.column(1).as_string::<i32>();
assert_eq!(name_col.value(0), "Alice");
assert_eq!(name_col.value(1), "Bob");
assert_eq!(name_col.value(2), "Charlie");
assert_eq!(name_col.value(3), "Dave");
}
#[tokio::test]
async fn test_predicate_on_migrated_file_with_nested_types() {
use serde::{Deserialize, Serialize};
use serde_arrow::schema::{SchemaLike, TracingOptions};
#[derive(Serialize, Deserialize)]
struct Person {
name: String,
age: i32,
}
#[derive(Serialize, Deserialize)]
struct Row {
person: Person,
people: Vec<Person>,
props: std::collections::BTreeMap<String, String>,
id: i32,
}
let rows = vec![
Row {
person: Person {
name: "Alice".into(),
age: 30,
},
people: vec![Person {
name: "Alice".into(),
age: 30,
}],
props: [("k1".into(), "v1".into())].into(),
id: 1,
},
Row {
person: Person {
name: "Bob".into(),
age: 25,
},
people: vec![Person {
name: "Bob".into(),
age: 25,
}],
props: [("k2".into(), "v2".into())].into(),
id: 2,
},
Row {
person: Person {
name: "Carol".into(),
age: 40,
},
people: vec![Person {
name: "Carol".into(),
age: 40,
}],
props: [("k3".into(), "v3".into())].into(),
id: 3,
},
];
let tracing_options = TracingOptions::default()
.map_as_struct(false)
.strings_as_large_utf8(false)
.sequence_as_large_list(false);
let fields = Vec::<arrow_schema::FieldRef>::from_type::<Row>(tracing_options).unwrap();
let arrow_schema = Arc::new(ArrowSchema::new(fields.clone()));
let batch = serde_arrow::to_record_batch(&fields, &rows).unwrap();
let iceberg_schema = Arc::new(
Schema::builder()
.with_schema_id(1)
.with_fields(vec![
NestedField::required(
1,
"person",
Type::Struct(crate::spec::StructType::new(vec![
NestedField::required(
5,
"name",
Type::Primitive(PrimitiveType::String),
)
.into(),
NestedField::required(6, "age", Type::Primitive(PrimitiveType::Int))
.into(),
])),
)
.into(),
NestedField::required(
2,
"people",
Type::List(crate::spec::ListType {
element_field: NestedField::required(
7,
"element",
Type::Struct(crate::spec::StructType::new(vec![
NestedField::required(
8,
"name",
Type::Primitive(PrimitiveType::String),
)
.into(),
NestedField::required(
9,
"age",
Type::Primitive(PrimitiveType::Int),
)
.into(),
])),
)
.into(),
}),
)
.into(),
NestedField::required(
3,
"props",
Type::Map(crate::spec::MapType {
key_field: NestedField::required(
10,
"key",
Type::Primitive(PrimitiveType::String),
)
.into(),
value_field: NestedField::required(
11,
"value",
Type::Primitive(PrimitiveType::String),
)
.into(),
}),
)
.into(),
NestedField::required(4, "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 = format!("{table_location}/1.parquet");
let props = WriterProperties::builder()
.set_compression(Compression::SNAPPY)
.build();
let file = File::create(&file_path).unwrap();
let mut writer = ArrowWriter::try_new(file, arrow_schema, Some(props)).unwrap();
writer.write(&batch).expect("Writing batch");
writer.close().unwrap();
let predicate = Reference::new("id").greater_than(Datum::int(1));
let reader = ArrowReaderBuilder::new(FileIO::new_with_fs(), Runtime::current())
.with_row_group_filtering_enabled(true)
.with_row_selection_enabled(true)
.build();
let tasks = Box::pin(futures::stream::iter(
vec![Ok(FileScanTask::builder()
.with_file_size_in_bytes(std::fs::metadata(&file_path).unwrap().len())
.with_start(0)
.with_length(0)
.with_data_file_path(file_path)
.with_data_file_format(DataFileFormat::Parquet)
.with_schema(iceberg_schema.clone())
.with_project_field_ids(vec![4])
.with_case_sensitive(false)
.with_predicate(Some(predicate.bind(iceberg_schema, true).unwrap()))
.build())]
.into_iter(),
)) as FileScanTaskStream;
let result = reader
.read(tasks)
.unwrap()
.stream()
.try_collect::<Vec<RecordBatch>>()
.await
.unwrap();
let ids: Vec<i32> = result
.iter()
.flat_map(|b| {
b.column(0)
.as_primitive::<arrow_array::types::Int32Type>()
.values()
.iter()
.copied()
})
.collect();
assert_eq!(ids, vec![2, 3]);
}
}