use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use std::rc::Rc;
use std::sync::Arc;
use arrow::datatypes::{DataType, Field, FieldRef, Schema, TimeUnit};
use parquet::basic::Type;
use parquet::schema::types::SchemaDescriptor;
pub fn apply_file_schema_type_coercions(
table_schema: &Schema,
file_schema: &Schema,
) -> Option<Schema> {
let mut needs_view_transform = false;
let mut needs_string_transform = false;
let table_fields: HashMap<_, _> = table_schema
.fields()
.iter()
.map(|f| {
let dt = f.data_type();
if matches!(dt, &DataType::Utf8View | &DataType::BinaryView) {
needs_view_transform = true;
}
if matches!(
dt,
&DataType::Utf8 | &DataType::LargeUtf8 | &DataType::Utf8View
) {
needs_string_transform = true;
}
(f.name(), dt)
})
.collect();
if !needs_view_transform && !needs_string_transform {
return None;
}
let transformed_fields: Vec<Arc<Field>> = file_schema
.fields()
.iter()
.map(|field| {
let field_name = field.name();
let field_type = field.data_type();
if let Some(table_type) = table_fields.get(field_name) {
match (table_type, field_type) {
(
&DataType::Utf8,
DataType::Binary | DataType::LargeBinary | DataType::BinaryView,
) => {
return field_with_new_type(field, DataType::Utf8);
}
(
&DataType::LargeUtf8,
DataType::Binary | DataType::LargeBinary | DataType::BinaryView,
) => {
return field_with_new_type(field, DataType::LargeUtf8);
}
(
&DataType::Utf8View,
DataType::Binary | DataType::LargeBinary | DataType::BinaryView,
) => {
return field_with_new_type(field, DataType::Utf8View);
}
(&DataType::Utf8View, DataType::Utf8 | DataType::LargeUtf8) => {
return field_with_new_type(field, DataType::Utf8View);
}
(&DataType::BinaryView, DataType::Binary | DataType::LargeBinary) => {
return field_with_new_type(field, DataType::BinaryView);
}
_ => {}
}
}
Arc::clone(field)
})
.collect();
Some(Schema::new_with_metadata(
transformed_fields,
file_schema.metadata.clone(),
))
}
#[deprecated(since = "53.2.0", note = "use `Int96Coercer` instead")]
pub fn coerce_int96_to_resolution(
parquet_schema: &SchemaDescriptor,
file_schema: &Schema,
time_unit: &TimeUnit,
) -> Option<Schema> {
Int96Coercer::new(parquet_schema, file_schema, time_unit).coerce()
}
pub struct Int96Coercer<'a> {
parquet_schema: &'a SchemaDescriptor,
file_schema: &'a Schema,
time_unit: &'a TimeUnit,
timezone: Option<Arc<str>>,
}
impl<'a> Int96Coercer<'a> {
pub fn new(
parquet_schema: &'a SchemaDescriptor,
file_schema: &'a Schema,
time_unit: &'a TimeUnit,
) -> Self {
Self {
parquet_schema,
file_schema,
time_unit,
timezone: None,
}
}
pub fn with_timezone(mut self, timezone: Option<Arc<str>>) -> Self {
self.timezone = timezone;
self
}
pub fn coerce(self) -> Option<Schema> {
let Self {
parquet_schema,
file_schema,
time_unit,
timezone,
} = self;
coerce_int96_to_resolution_impl(
parquet_schema,
file_schema,
time_unit,
timezone.as_ref(),
)
}
}
fn coerce_int96_to_resolution_impl(
parquet_schema: &SchemaDescriptor,
file_schema: &Schema,
time_unit: &TimeUnit,
timezone: Option<&Arc<str>>,
) -> Option<Schema> {
let int96_fields: HashSet<_> = parquet_schema
.columns()
.iter()
.filter(|f| f.physical_type() == Type::INT96)
.map(|f| f.path().string())
.collect();
if int96_fields.is_empty() {
return None;
}
type NestedFields = Rc<RefCell<Vec<FieldRef>>>;
type StackContext<'a> = (
Vec<&'a str>, &'a FieldRef, NestedFields, Option<NestedFields>, );
let fields = Rc::new(RefCell::new(Vec::with_capacity(file_schema.fields.len())));
let transformed_schema = {
let mut stack: Vec<StackContext> = file_schema
.fields()
.iter()
.rev()
.map(|f| (vec![f.name().as_str()], f, Rc::clone(&fields), None))
.collect();
while let Some((parquet_path, current_field, parent_fields, child_fields)) =
stack.pop()
{
match (current_field.data_type(), child_fields) {
(DataType::Struct(unprocessed_children), None) => {
let child_fields = Rc::new(RefCell::new(Vec::with_capacity(
unprocessed_children.len(),
)));
stack.push((
parquet_path.clone(),
current_field,
parent_fields,
Some(Rc::clone(&child_fields)),
));
for child in unprocessed_children.into_iter().rev() {
let mut child_path = parquet_path.clone();
child_path.push(".");
child_path.push(child.name());
stack.push((child_path, child, Rc::clone(&child_fields), None));
}
}
(DataType::Struct(unprocessed_children), Some(processed_children)) => {
let processed_children = processed_children.borrow();
assert_eq!(processed_children.len(), unprocessed_children.len());
let processed_struct = Field::new_struct(
current_field.name(),
processed_children.as_slice(),
current_field.is_nullable(),
);
parent_fields.borrow_mut().push(Arc::new(processed_struct));
}
(DataType::List(unprocessed_child), None) => {
let child_fields = Rc::new(RefCell::new(Vec::with_capacity(1)));
stack.push((
parquet_path.clone(),
current_field,
parent_fields,
Some(Rc::clone(&child_fields)),
));
let mut child_path = parquet_path.clone();
child_path.push(".list.");
child_path.push(unprocessed_child.name());
stack.push((
child_path.clone(),
unprocessed_child,
Rc::clone(&child_fields),
None,
));
}
(DataType::List(_), Some(processed_children)) => {
let processed_children = processed_children.borrow();
assert_eq!(processed_children.len(), 1);
let processed_list = Field::new_list(
current_field.name(),
Arc::clone(&processed_children[0]),
current_field.is_nullable(),
);
parent_fields.borrow_mut().push(Arc::new(processed_list));
}
(DataType::Map(unprocessed_child, _), None) => {
let child_fields = Rc::new(RefCell::new(Vec::with_capacity(1)));
stack.push((
parquet_path.clone(),
current_field,
parent_fields,
Some(Rc::clone(&child_fields)),
));
let mut child_path = parquet_path.clone();
child_path.push(".");
child_path.push(unprocessed_child.name());
stack.push((
child_path.clone(),
unprocessed_child,
Rc::clone(&child_fields),
None,
));
}
(DataType::Map(_, sorted), Some(processed_children)) => {
let processed_children = processed_children.borrow();
assert_eq!(processed_children.len(), 1);
let processed_map = Field::new(
current_field.name(),
DataType::Map(Arc::clone(&processed_children[0]), *sorted),
current_field.is_nullable(),
);
parent_fields.borrow_mut().push(Arc::new(processed_map));
}
(DataType::Timestamp(TimeUnit::Nanosecond, None), None)
if int96_fields.contains(parquet_path.concat().as_str()) =>
{
parent_fields.borrow_mut().push(field_with_new_type(
current_field,
DataType::Timestamp(*time_unit, timezone.cloned()),
));
}
_ => parent_fields.borrow_mut().push(Arc::clone(current_field)),
}
}
assert_eq!(fields.borrow().len(), file_schema.fields.len());
Schema::new_with_metadata(
fields.borrow_mut().clone(),
file_schema.metadata.clone(),
)
};
Some(transformed_schema)
}
fn field_with_new_type(field: &FieldRef, new_type: DataType) -> FieldRef {
Arc::new(field.as_ref().clone().with_data_type(new_type))
}
pub fn transform_schema_to_view(schema: &Schema) -> Schema {
let transformed_fields: Vec<Arc<Field>> = schema
.fields
.iter()
.map(|field| match field.data_type() {
DataType::Utf8 | DataType::LargeUtf8 => {
field_with_new_type(field, DataType::Utf8View)
}
DataType::Binary | DataType::LargeBinary => {
field_with_new_type(field, DataType::BinaryView)
}
_ => Arc::clone(field),
})
.collect();
Schema::new_with_metadata(transformed_fields, schema.metadata.clone())
}
pub fn transform_binary_to_string(schema: &Schema) -> Schema {
let transformed_fields: Vec<Arc<Field>> = schema
.fields
.iter()
.map(|field| match field.data_type() {
DataType::Binary => field_with_new_type(field, DataType::Utf8),
DataType::LargeBinary => field_with_new_type(field, DataType::LargeUtf8),
DataType::BinaryView => field_with_new_type(field, DataType::Utf8View),
_ => Arc::clone(field),
})
.collect();
Schema::new_with_metadata(transformed_fields, schema.metadata.clone())
}
#[cfg(test)]
mod tests {
use parquet::arrow::parquet_to_arrow_schema;
use super::*;
use parquet::schema::parser::parse_message_type;
#[test]
fn coerce_int96_to_resolution_with_mixed_timestamps() {
let spark_schema = "
message spark_schema {
optional int96 c0;
optional int64 c1 (TIMESTAMP(NANOS,true));
optional int64 c2 (TIMESTAMP(NANOS,false));
optional int64 c3 (TIMESTAMP(MILLIS,true));
optional int64 c4 (TIMESTAMP(MILLIS,false));
optional int64 c5 (TIMESTAMP(MICROS,true));
optional int64 c6 (TIMESTAMP(MICROS,false));
}
";
let schema = parse_message_type(spark_schema).expect("should parse schema");
let descr = SchemaDescriptor::new(Arc::new(schema));
let arrow_schema = parquet_to_arrow_schema(&descr, None).unwrap();
let result = Int96Coercer::new(&descr, &arrow_schema, &TimeUnit::Microsecond)
.coerce()
.unwrap();
let expected_schema = Schema::new(vec![
Field::new("c0", DataType::Timestamp(TimeUnit::Microsecond, None), true),
Field::new(
"c1",
DataType::Timestamp(TimeUnit::Nanosecond, Some("UTC".into())),
true,
),
Field::new("c2", DataType::Timestamp(TimeUnit::Nanosecond, None), true),
Field::new(
"c3",
DataType::Timestamp(TimeUnit::Millisecond, Some("UTC".into())),
true,
),
Field::new("c4", DataType::Timestamp(TimeUnit::Millisecond, None), true),
Field::new(
"c5",
DataType::Timestamp(TimeUnit::Microsecond, Some("UTC".into())),
true,
),
Field::new("c6", DataType::Timestamp(TimeUnit::Microsecond, None), true),
]);
assert_eq!(result, expected_schema);
}
#[test]
fn coerce_int96_to_resolution_with_tz_applies_timezone() {
let spark_schema = "
message spark_schema {
optional int96 c0;
optional int64 c1 (TIMESTAMP(NANOS,true));
optional int64 c2 (TIMESTAMP(NANOS,false));
optional int64 c3 (TIMESTAMP(MILLIS,true));
optional int64 c4 (TIMESTAMP(MILLIS,false));
optional int64 c5 (TIMESTAMP(MICROS,true));
optional int64 c6 (TIMESTAMP(MICROS,false));
}
";
let schema = parse_message_type(spark_schema).expect("should parse schema");
let descr = SchemaDescriptor::new(Arc::new(schema));
let arrow_schema = parquet_to_arrow_schema(&descr, None).unwrap();
let result = Int96Coercer::new(&descr, &arrow_schema, &TimeUnit::Microsecond)
.with_timezone(Some(Arc::from("UTC")))
.coerce()
.unwrap();
let expected_schema = Schema::new(vec![
Field::new(
"c0",
DataType::Timestamp(TimeUnit::Microsecond, Some("UTC".into())),
true,
),
Field::new(
"c1",
DataType::Timestamp(TimeUnit::Nanosecond, Some("UTC".into())),
true,
),
Field::new("c2", DataType::Timestamp(TimeUnit::Nanosecond, None), true),
Field::new(
"c3",
DataType::Timestamp(TimeUnit::Millisecond, Some("UTC".into())),
true,
),
Field::new("c4", DataType::Timestamp(TimeUnit::Millisecond, None), true),
Field::new(
"c5",
DataType::Timestamp(TimeUnit::Microsecond, Some("UTC".into())),
true,
),
Field::new("c6", DataType::Timestamp(TimeUnit::Microsecond, None), true),
]);
assert_eq!(result, expected_schema);
}
#[test]
fn coerce_int96_to_resolution_with_nested_types() {
let spark_schema = "
message spark_schema {
optional int96 c0;
optional group c1 {
optional int96 c0;
}
optional group c2 {
optional group c0 (LIST) {
repeated group list {
optional int96 element;
}
}
}
optional group c3 (LIST) {
repeated group list {
optional int96 element;
}
}
optional group c4 (LIST) {
repeated group list {
optional group element {
optional int96 c0;
optional int96 c1;
}
}
}
optional group c5 (MAP) {
repeated group key_value {
required int96 key;
optional int96 value;
}
}
optional group c6 (LIST) {
repeated group list {
optional group element (MAP) {
repeated group key_value {
required int96 key;
optional int96 value;
}
}
}
}
}
";
let schema = parse_message_type(spark_schema).expect("should parse schema");
let descr = SchemaDescriptor::new(Arc::new(schema));
let arrow_schema = parquet_to_arrow_schema(&descr, None).unwrap();
let result = Int96Coercer::new(&descr, &arrow_schema, &TimeUnit::Microsecond)
.coerce()
.unwrap();
let expected_schema = Schema::new(vec![
Field::new("c0", DataType::Timestamp(TimeUnit::Microsecond, None), true),
Field::new_struct(
"c1",
vec![Field::new(
"c0",
DataType::Timestamp(TimeUnit::Microsecond, None),
true,
)],
true,
),
Field::new_struct(
"c2",
vec![Field::new_list(
"c0",
Field::new(
"element",
DataType::Timestamp(TimeUnit::Microsecond, None),
true,
),
true,
)],
true,
),
Field::new_list(
"c3",
Field::new(
"element",
DataType::Timestamp(TimeUnit::Microsecond, None),
true,
),
true,
),
Field::new_list(
"c4",
Field::new_struct(
"element",
vec![
Field::new(
"c0",
DataType::Timestamp(TimeUnit::Microsecond, None),
true,
),
Field::new(
"c1",
DataType::Timestamp(TimeUnit::Microsecond, None),
true,
),
],
true,
),
true,
),
Field::new_map(
"c5",
"key_value",
Field::new(
"key",
DataType::Timestamp(TimeUnit::Microsecond, None),
false,
),
Field::new(
"value",
DataType::Timestamp(TimeUnit::Microsecond, None),
true,
),
false,
true,
),
Field::new_list(
"c6",
Field::new_map(
"element",
"key_value",
Field::new(
"key",
DataType::Timestamp(TimeUnit::Microsecond, None),
false,
),
Field::new(
"value",
DataType::Timestamp(TimeUnit::Microsecond, None),
true,
),
false,
true,
),
true,
),
]);
assert_eq!(result, expected_schema);
}
}