use crate::cast::cast_supported;
use crate::{spark_cast, SparkCastOptions};
use arrow_array::{new_null_array, Array, RecordBatch, RecordBatchOptions};
use arrow_schema::{Schema, SchemaRef};
use datafusion::datasource::schema_adapter::{SchemaAdapter, SchemaAdapterFactory, SchemaMapper};
use datafusion_common::plan_err;
use datafusion_expr::ColumnarValue;
use std::sync::Arc;
#[derive(Clone, Debug)]
pub struct SparkSchemaAdapterFactory {
cast_options: SparkCastOptions,
}
impl SparkSchemaAdapterFactory {
pub fn new(options: SparkCastOptions) -> Self {
Self {
cast_options: options,
}
}
}
impl SchemaAdapterFactory for SparkSchemaAdapterFactory {
fn create(
&self,
required_schema: SchemaRef,
table_schema: SchemaRef,
) -> Box<dyn SchemaAdapter> {
Box::new(SparkSchemaAdapter {
required_schema,
table_schema,
cast_options: self.cast_options.clone(),
})
}
}
#[derive(Clone, Debug)]
pub struct SparkSchemaAdapter {
required_schema: SchemaRef,
table_schema: SchemaRef,
cast_options: SparkCastOptions,
}
impl SchemaAdapter for SparkSchemaAdapter {
fn map_column_index(&self, index: usize, file_schema: &Schema) -> Option<usize> {
let field = self.required_schema.field(index);
Some(file_schema.fields.find(field.name())?.0)
}
fn map_schema(
&self,
file_schema: &Schema,
) -> datafusion_common::Result<(Arc<dyn SchemaMapper>, Vec<usize>)> {
let mut projection = Vec::with_capacity(file_schema.fields().len());
let mut field_mappings = vec![None; self.required_schema.fields().len()];
for (file_idx, file_field) in file_schema.fields.iter().enumerate() {
if let Some((table_idx, table_field)) =
self.required_schema.fields().find(file_field.name())
{
if cast_supported(
file_field.data_type(),
table_field.data_type(),
&self.cast_options,
) {
field_mappings[table_idx] = Some(projection.len());
projection.push(file_idx);
} else {
return plan_err!(
"Cannot cast file schema field {} of type {:?} to required schema field of type {:?}",
file_field.name(),
file_field.data_type(),
table_field.data_type()
);
}
}
}
Ok((
Arc::new(SchemaMapping {
required_schema: Arc::<Schema>::clone(&self.required_schema),
field_mappings,
table_schema: Arc::<Schema>::clone(&self.table_schema),
cast_options: self.cast_options.clone(),
}),
projection,
))
}
}
#[derive(Debug)]
pub struct SchemaMapping {
required_schema: SchemaRef,
field_mappings: Vec<Option<usize>>,
table_schema: SchemaRef,
cast_options: SparkCastOptions,
}
impl SchemaMapper for SchemaMapping {
fn map_batch(&self, batch: RecordBatch) -> datafusion_common::Result<RecordBatch> {
let batch_rows = batch.num_rows();
let batch_cols = batch.columns().to_vec();
let cols = self
.required_schema
.fields()
.iter()
.zip(&self.field_mappings)
.map(|(field, file_idx)| {
file_idx.map_or_else(
|| Ok(new_null_array(field.data_type(), batch_rows)),
|batch_idx| {
spark_cast(
ColumnarValue::Array(Arc::clone(&batch_cols[batch_idx])),
field.data_type(),
&self.cast_options,
)?
.into_array(batch_rows)
},
)
})
.collect::<datafusion_common::Result<Vec<_>, _>>()?;
let options = RecordBatchOptions::new().with_row_count(Some(batch.num_rows()));
let schema = Arc::<Schema>::clone(&self.required_schema);
let record_batch = RecordBatch::try_new_with_options(schema, cols, &options)?;
Ok(record_batch)
}
fn map_partial_batch(&self, batch: RecordBatch) -> datafusion_common::Result<RecordBatch> {
let batch_cols = batch.columns().to_vec();
let schema = batch.schema();
let (cols, fields) = schema
.fields()
.iter()
.zip(batch_cols.iter())
.flat_map(|(field, batch_col)| {
self.table_schema
.field_with_name(field.name())
.ok()
.map(|table_field| {
spark_cast(
ColumnarValue::Array(Arc::clone(batch_col)),
table_field.data_type(),
&self.cast_options,
)?
.into_array(batch_col.len())
.map(|new_col| (new_col, table_field.clone()))
})
})
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.unzip::<_, _, Vec<_>, Vec<_>>();
let options = RecordBatchOptions::new().with_row_count(Some(batch.num_rows()));
let schema = Arc::new(Schema::new_with_metadata(fields, schema.metadata().clone()));
let record_batch = RecordBatch::try_new_with_options(schema, cols, &options)?;
Ok(record_batch)
}
}
#[cfg(test)]
mod test {
use crate::test_common::file_util::get_temp_filename;
use crate::{EvalMode, SparkCastOptions, SparkSchemaAdapterFactory};
use arrow::array::{Int32Array, StringArray};
use arrow::datatypes::{DataType, Field, Schema};
use arrow::record_batch::RecordBatch;
use arrow_array::UInt32Array;
use arrow_schema::SchemaRef;
use datafusion::datasource::listing::PartitionedFile;
use datafusion::datasource::physical_plan::{FileScanConfig, ParquetExec};
use datafusion::execution::object_store::ObjectStoreUrl;
use datafusion::execution::TaskContext;
use datafusion::physical_plan::ExecutionPlan;
use datafusion_common::DataFusionError;
use futures::StreamExt;
use parquet::arrow::ArrowWriter;
use std::fs::File;
use std::sync::Arc;
#[tokio::test]
async fn parquet_roundtrip_int_as_string() -> Result<(), DataFusionError> {
let file_schema = Arc::new(Schema::new(vec![
Field::new("id", DataType::Int32, false),
Field::new("name", DataType::Utf8, false),
]));
let ids = Arc::new(Int32Array::from(vec![1, 2, 3])) as Arc<dyn arrow::array::Array>;
let names = Arc::new(StringArray::from(vec!["Alice", "Bob", "Charlie"]))
as Arc<dyn arrow::array::Array>;
let batch = RecordBatch::try_new(Arc::clone(&file_schema), vec![ids, names])?;
let required_schema = Arc::new(Schema::new(vec![
Field::new("id", DataType::Utf8, false),
Field::new("name", DataType::Utf8, false),
]));
let _ = roundtrip(&batch, required_schema).await?;
Ok(())
}
#[tokio::test]
async fn parquet_roundtrip_unsigned_int() -> Result<(), DataFusionError> {
let file_schema = Arc::new(Schema::new(vec![Field::new("id", DataType::UInt32, false)]));
let ids = Arc::new(UInt32Array::from(vec![1, 2, 3])) as Arc<dyn arrow::array::Array>;
let batch = RecordBatch::try_new(Arc::clone(&file_schema), vec![ids])?;
let required_schema = Arc::new(Schema::new(vec![Field::new("id", DataType::Int32, false)]));
let _ = roundtrip(&batch, required_schema).await?;
Ok(())
}
async fn roundtrip(
batch: &RecordBatch,
required_schema: SchemaRef,
) -> Result<RecordBatch, DataFusionError> {
let filename = get_temp_filename();
let filename = filename.as_path().as_os_str().to_str().unwrap().to_string();
let file = File::create(&filename)?;
let mut writer = ArrowWriter::try_new(file, Arc::clone(&batch.schema()), None)?;
writer.write(batch)?;
writer.close()?;
let object_store_url = ObjectStoreUrl::local_filesystem();
let file_scan_config = FileScanConfig::new(object_store_url, required_schema)
.with_file_groups(vec![vec![PartitionedFile::from_path(
filename.to_string(),
)?]]);
let mut spark_cast_options = SparkCastOptions::new(EvalMode::Legacy, "UTC", false);
spark_cast_options.allow_cast_unsigned_ints = true;
let parquet_exec = ParquetExec::builder(file_scan_config)
.with_schema_adapter_factory(Arc::new(SparkSchemaAdapterFactory::new(
spark_cast_options,
)))
.build();
let mut stream = parquet_exec
.execute(0, Arc::new(TaskContext::default()))
.unwrap();
stream.next().await.unwrap()
}
}