use std::sync::Arc;
use arrow::datatypes::{Schema, SchemaRef};
use arrow_avro::reader::{Reader, ReaderBuilder};
use datafusion_common::error::Result;
use datafusion_datasource::TableSchema;
use datafusion_datasource::file::FileSource;
use datafusion_datasource::file_scan_config::FileScanConfig;
use datafusion_datasource::file_stream::FileOpener;
use datafusion_datasource::projection::{ProjectionOpener, SplitProjection};
use datafusion_physical_expr_adapter::BatchAdapterFactory;
use datafusion_physical_plan::metrics::ExecutionPlanMetricsSet;
use datafusion_physical_plan::projection::ProjectionExprs;
use object_store::ObjectStore;
#[derive(Clone)]
pub struct AvroSource {
table_schema: TableSchema,
batch_size: Option<usize>,
projection: SplitProjection,
metrics: ExecutionPlanMetricsSet,
}
impl AvroSource {
pub fn new(table_schema: impl Into<TableSchema>) -> Self {
let table_schema = table_schema.into();
Self {
projection: SplitProjection::unprojected(&table_schema),
table_schema,
batch_size: None,
metrics: ExecutionPlanMetricsSet::new(),
}
}
fn open<R: std::io::BufRead>(
&self,
reader: R,
projection: Option<Vec<usize>>,
) -> Result<Reader<R>> {
let mut builder = ReaderBuilder::new()
.with_batch_size(self.batch_size.expect("Batch size must set before open"));
if let Some(projection) = projection {
builder = builder.with_projection(projection);
}
builder.build(reader).map_err(Into::into)
}
fn projected_file_schema(&self) -> SchemaRef {
let file_schema = self.table_schema.file_schema();
if self.projection.file_indices.is_empty() {
return Arc::clone(file_schema);
}
Arc::new(Schema::new(
self.projection
.file_indices
.iter()
.map(|idx| file_schema.field(*idx).clone())
.collect::<Vec<_>>(),
))
}
fn writer_projection_for_schema(
&self,
writer_schema: &Schema,
target_schema: &Schema,
) -> Option<Vec<usize>> {
let projection = target_schema
.fields()
.iter()
.filter_map(|field| {
writer_schema
.column_with_name(field.name())
.map(|(idx, _)| idx)
})
.collect::<Vec<_>>();
let identity_projection = projection.len() == writer_schema.fields().len()
&& projection
.iter()
.enumerate()
.all(|(idx, value)| idx == *value);
(!identity_projection).then_some(projection)
}
}
impl FileSource for AvroSource {
fn create_file_opener(
&self,
object_store: Arc<dyn ObjectStore>,
_base_config: &FileScanConfig,
_partition: usize,
) -> Result<Arc<dyn FileOpener>> {
let mut opener = Arc::new(private::AvroOpener {
config: Arc::new(self.clone()),
object_store,
}) as Arc<dyn FileOpener>;
opener = ProjectionOpener::try_new(
self.projection.clone(),
Arc::clone(&opener),
self.table_schema.file_schema(),
)?;
Ok(opener)
}
fn table_schema(&self) -> &TableSchema {
&self.table_schema
}
fn with_batch_size(&self, batch_size: usize) -> Arc<dyn FileSource> {
let mut conf = self.clone();
conf.batch_size = Some(batch_size);
Arc::new(conf)
}
fn try_pushdown_projection(
&self,
projection: &ProjectionExprs,
) -> Result<Option<Arc<dyn FileSource>>> {
let mut source = self.clone();
let new_projection = self.projection.source.try_merge(projection)?;
let split_projection =
SplitProjection::new(self.table_schema.file_schema(), &new_projection);
source.projection = split_projection;
Ok(Some(Arc::new(source)))
}
fn projection(&self) -> Option<&ProjectionExprs> {
Some(&self.projection.source)
}
fn metrics(&self) -> &ExecutionPlanMetricsSet {
&self.metrics
}
fn file_type(&self) -> &str {
"avro"
}
fn supports_repartitioning(&self) -> bool {
false
}
}
mod private {
use super::*;
use std::io::BufReader;
use std::io::Seek;
use bytes::Buf;
use datafusion_datasource::{PartitionedFile, file_stream::FileOpenFuture};
use futures::StreamExt;
use object_store::{GetResultPayload, ObjectStore, ObjectStoreExt};
pub struct AvroOpener {
pub config: Arc<AvroSource>,
pub object_store: Arc<dyn ObjectStore>,
}
impl FileOpener for AvroOpener {
fn open(&self, partitioned_file: PartitionedFile) -> Result<FileOpenFuture> {
let object_store = Arc::clone(&self.object_store);
let config = Arc::clone(&self.config);
let projected_file_schema = config.projected_file_schema();
Ok(Box::pin(async move {
let r = object_store
.get(&partitioned_file.object_meta.location)
.await?;
match r.payload {
GetResultPayload::File(mut file, _) => {
let probe_reader =
config.open(BufReader::new(file.try_clone()?), None)?;
let writer_projection = config.writer_projection_for_schema(
probe_reader.schema().as_ref(),
projected_file_schema.as_ref(),
);
file.rewind()?;
let reader =
config.open(BufReader::new(file), writer_projection)?;
let batch_adapter =
BatchAdapterFactory::new(Arc::clone(&projected_file_schema))
.make_adapter(&reader.schema())?;
Ok(futures::stream::iter(reader)
.map(move |r| {
r.map_err(Into::into)
.and_then(|batch| batch_adapter.adapt_batch(&batch))
})
.boxed())
}
GetResultPayload::Stream(_) => {
let bytes = r.bytes().await?;
let probe_reader =
config.open(BufReader::new(bytes.clone().reader()), None)?;
let writer_projection = config.writer_projection_for_schema(
probe_reader.schema().as_ref(),
projected_file_schema.as_ref(),
);
let reader = config
.open(BufReader::new(bytes.reader()), writer_projection)?;
let batch_adapter =
BatchAdapterFactory::new(Arc::clone(&projected_file_schema))
.make_adapter(&reader.schema())?;
Ok(futures::stream::iter(reader)
.map(move |r| {
r.map_err(Into::into)
.and_then(|batch| batch_adapter.adapt_batch(&batch))
})
.boxed())
}
}
}))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use arrow::array::*;
use arrow::array::{
BinaryArray, BooleanArray, Float32Array, Float64Array, Int32Array, Int64Array,
TimestampMicrosecondArray, TimestampMillisecondArray,
};
use arrow::datatypes::TimeUnit;
use arrow::datatypes::{DataType, Field};
use std::fs::File;
use std::io::BufReader;
fn build_reader(
name: &'_ str,
projection: Option<Vec<usize>>,
) -> Reader<BufReader<File>> {
let testdata = datafusion_common::test_util::arrow_test_data();
let filename = format!("{testdata}/avro/{name}");
let mut builder = ReaderBuilder::new().with_batch_size(64);
if let Some(proj) = projection {
builder = builder.with_projection(proj);
}
builder
.build(BufReader::new(File::open(filename).unwrap()))
.unwrap()
}
fn get_col<'a, T: 'static>(
batch: &'a RecordBatch,
col: (usize, &Field),
) -> Option<&'a T> {
batch.column(col.0).as_any().downcast_ref::<T>()
}
#[test]
fn test_avro_basic() {
let mut reader = build_reader("alltypes_dictionary.avro", None);
let batch = reader.next().unwrap().unwrap();
assert_eq!(11, batch.num_columns());
assert_eq!(2, batch.num_rows());
let schema = reader.schema();
let batch_schema = batch.schema();
assert_eq!(schema, batch_schema);
let id = schema.column_with_name("id").unwrap();
assert_eq!(0, id.0);
assert_eq!(&DataType::Int32, id.1.data_type());
let col = get_col::<Int32Array>(&batch, id).unwrap();
assert_eq!(0, col.value(0));
assert_eq!(1, col.value(1));
let bool_col = schema.column_with_name("bool_col").unwrap();
assert_eq!(1, bool_col.0);
assert_eq!(&DataType::Boolean, bool_col.1.data_type());
let col = get_col::<BooleanArray>(&batch, bool_col).unwrap();
assert!(col.value(0));
assert!(!col.value(1));
let tinyint_col = schema.column_with_name("tinyint_col").unwrap();
assert_eq!(2, tinyint_col.0);
assert_eq!(&DataType::Int32, tinyint_col.1.data_type());
let col = get_col::<Int32Array>(&batch, tinyint_col).unwrap();
assert_eq!(0, col.value(0));
assert_eq!(1, col.value(1));
let smallint_col = schema.column_with_name("smallint_col").unwrap();
assert_eq!(3, smallint_col.0);
assert_eq!(&DataType::Int32, smallint_col.1.data_type());
let col = get_col::<Int32Array>(&batch, smallint_col).unwrap();
assert_eq!(0, col.value(0));
assert_eq!(1, col.value(1));
let int_col = schema.column_with_name("int_col").unwrap();
assert_eq!(4, int_col.0);
let col = get_col::<Int32Array>(&batch, int_col).unwrap();
assert_eq!(0, col.value(0));
assert_eq!(1, col.value(1));
assert_eq!(&DataType::Int32, int_col.1.data_type());
let col = get_col::<Int32Array>(&batch, int_col).unwrap();
assert_eq!(0, col.value(0));
assert_eq!(1, col.value(1));
let bigint_col = schema.column_with_name("bigint_col").unwrap();
assert_eq!(5, bigint_col.0);
let col = get_col::<Int64Array>(&batch, bigint_col).unwrap();
assert_eq!(0, col.value(0));
assert_eq!(10, col.value(1));
assert_eq!(&DataType::Int64, bigint_col.1.data_type());
let float_col = schema.column_with_name("float_col").unwrap();
assert_eq!(6, float_col.0);
let col = get_col::<Float32Array>(&batch, float_col).unwrap();
assert_eq!(0.0, col.value(0));
assert_eq!(1.1, col.value(1));
assert_eq!(&DataType::Float32, float_col.1.data_type());
let col = get_col::<Float32Array>(&batch, float_col).unwrap();
assert_eq!(0.0, col.value(0));
assert_eq!(1.1, col.value(1));
let double_col = schema.column_with_name("double_col").unwrap();
assert_eq!(7, double_col.0);
assert_eq!(&DataType::Float64, double_col.1.data_type());
let col = get_col::<Float64Array>(&batch, double_col).unwrap();
assert_eq!(0.0, col.value(0));
assert_eq!(10.1, col.value(1));
let date_string_col = schema.column_with_name("date_string_col").unwrap();
assert_eq!(8, date_string_col.0);
assert_eq!(&DataType::Binary, date_string_col.1.data_type());
let col = get_col::<BinaryArray>(&batch, date_string_col).unwrap();
assert_eq!("01/01/09".as_bytes(), col.value(0));
assert_eq!("01/01/09".as_bytes(), col.value(1));
let string_col = schema.column_with_name("string_col").unwrap();
assert_eq!(9, string_col.0);
assert_eq!(&DataType::Binary, string_col.1.data_type());
let col = get_col::<BinaryArray>(&batch, string_col).unwrap();
assert_eq!("0".as_bytes(), col.value(0));
assert_eq!("1".as_bytes(), col.value(1));
let timestamp_col = schema.column_with_name("timestamp_col").unwrap();
assert_eq!(10, timestamp_col.0);
assert_eq!(
&DataType::Timestamp(TimeUnit::Microsecond, Some("+00:00".into())),
timestamp_col.1.data_type()
);
let col = get_col::<TimestampMicrosecondArray>(&batch, timestamp_col).unwrap();
assert_eq!(1230768000000000, col.value(0));
assert_eq!(1230768060000000, col.value(1));
}
#[test]
fn test_avro_with_projection() {
let projection = vec![9, 7, 1];
let mut reader = build_reader("alltypes_dictionary.avro", Some(projection));
let batch = reader.next().unwrap().unwrap();
assert_eq!(3, batch.num_columns());
assert_eq!(2, batch.num_rows());
let schema = reader.schema();
let batch_schema = batch.schema();
assert_eq!(schema, batch_schema);
assert_eq!("string_col", schema.field(0).name());
assert_eq!(&DataType::Binary, schema.field(0).data_type());
let col = batch
.column(0)
.as_any()
.downcast_ref::<BinaryArray>()
.unwrap();
assert_eq!("0".as_bytes(), col.value(0));
assert_eq!("1".as_bytes(), col.value(1));
assert_eq!("double_col", schema.field(1).name());
assert_eq!(&DataType::Float64, schema.field(1).data_type());
let col = batch
.column(1)
.as_any()
.downcast_ref::<Float64Array>()
.unwrap();
assert_eq!(0.0, col.value(0));
assert_eq!(10.1, col.value(1));
assert_eq!("bool_col", schema.field(2).name());
assert_eq!(&DataType::Boolean, schema.field(2).data_type());
let col = batch
.column(2)
.as_any()
.downcast_ref::<BooleanArray>()
.unwrap();
assert!(col.value(0));
assert!(!col.value(1));
}
#[test]
fn test_avro_timestamp_logical_types() {
let mut reader = build_reader("timestamp_logical_types.avro", None);
let batch = reader.next().unwrap().unwrap();
assert_eq!(7, batch.num_columns());
assert_eq!(2, batch.num_rows());
let schema = reader.schema();
let ts_millis = schema.column_with_name("ts_millis").unwrap();
assert_eq!(1, ts_millis.0);
assert_eq!(
&DataType::Timestamp(TimeUnit::Millisecond, Some("+00:00".into())),
ts_millis.1.data_type()
);
let col = get_col::<TimestampMillisecondArray>(&batch, ts_millis).unwrap();
assert_eq!(0, col.value(0));
assert_eq!(1_000, col.value(1));
let ts_micros = schema.column_with_name("ts_micros").unwrap();
assert_eq!(2, ts_micros.0);
assert_eq!(
&DataType::Timestamp(TimeUnit::Microsecond, Some("+00:00".into())),
ts_micros.1.data_type()
);
let col = get_col::<TimestampMicrosecondArray>(&batch, ts_micros).unwrap();
assert_eq!(0, col.value(0));
assert_eq!(1_000_000, col.value(1));
let ts_nanos = schema.column_with_name("ts_nanos").unwrap();
assert_eq!(3, ts_nanos.0);
assert_eq!(
&DataType::Timestamp(TimeUnit::Nanosecond, Some("+00:00".into())),
ts_nanos.1.data_type()
);
let col = get_col::<TimestampNanosecondArray>(&batch, ts_nanos).unwrap();
assert_eq!(0, col.value(0));
assert_eq!(1_000_000_000, col.value(1));
let local_ts_millis = schema.column_with_name("local_ts_millis").unwrap();
assert_eq!(4, local_ts_millis.0);
assert_eq!(
&DataType::Timestamp(TimeUnit::Millisecond, None),
local_ts_millis.1.data_type()
);
let col = get_col::<TimestampMillisecondArray>(&batch, local_ts_millis).unwrap();
assert_eq!(0, col.value(0));
assert_eq!(1_000, col.value(1));
let local_ts_micros = schema.column_with_name("local_ts_micros").unwrap();
assert_eq!(5, local_ts_micros.0);
assert_eq!(
&DataType::Timestamp(TimeUnit::Microsecond, None),
local_ts_micros.1.data_type()
);
let col = get_col::<TimestampMicrosecondArray>(&batch, local_ts_micros).unwrap();
assert_eq!(0, col.value(0));
assert_eq!(1_000_000, col.value(1));
let local_ts_nanos = schema.column_with_name("local_ts_nanos").unwrap();
assert_eq!(6, local_ts_nanos.0);
assert_eq!(
&DataType::Timestamp(TimeUnit::Nanosecond, None),
local_ts_nanos.1.data_type()
);
let col = get_col::<TimestampNanosecondArray>(&batch, local_ts_nanos).unwrap();
assert_eq!(0, col.value(0));
assert_eq!(1_000_000_000, col.value(1));
}
}