use std::collections::HashMap;
use std::sync::Arc;
use arrow::compute;
use arrow::datatypes::{DataType, SchemaRef, TimeUnit};
use arrow::record_batch::{RecordBatch, RecordBatchOptions};
use datafusion::common::DataFusionError;
pub(crate) fn convert_timestamp_column(batch: RecordBatch) -> Result<RecordBatch, DataFusionError> {
if batch.num_rows() == 0 {
return Ok(batch);
}
let schema = batch.schema();
let mut new_fields = Vec::with_capacity(schema.fields().len());
let mut new_columns = Vec::with_capacity(batch.num_columns());
for (i, field) in schema.fields().iter().enumerate() {
let col = batch.column(i);
if field.name() == chronix_core::TIME_COLUMN && *field.data_type() == DataType::Int64 {
let ts = compute::cast(col, &DataType::Timestamp(TimeUnit::Nanosecond, None))
.map_err(|e| DataFusionError::ArrowError(Box::new(e), None))?;
new_fields.push(
arrow::datatypes::Field::new(
chronix_core::TIME_COLUMN,
DataType::Timestamp(TimeUnit::Nanosecond, None),
false,
)
.into(),
);
new_columns.push(ts);
} else {
new_fields.push(field.clone());
new_columns.push(col.clone());
}
}
let new_schema = Arc::new(arrow::datatypes::Schema::new(new_fields));
RecordBatch::try_new(new_schema, new_columns)
.map_err(|e| DataFusionError::ArrowError(Box::new(e), None))
}
pub(crate) fn align_batch_to_schema(
batch: &RecordBatch,
target: &SchemaRef,
) -> Result<RecordBatch, DataFusionError> {
let src = batch.schema();
if src.as_ref() == target.as_ref() {
return Ok(batch.clone());
}
let index: HashMap<&str, usize> = src
.fields()
.iter()
.enumerate()
.map(|(i, f)| (f.name().as_str(), i))
.collect();
let mut columns = Vec::with_capacity(target.fields().len());
for field in target.fields() {
match index.get(field.name().as_str()) {
Some(&i) => {
let col = batch.column(i);
if col.data_type() == field.data_type() {
columns.push(Arc::clone(col));
} else {
columns.push(
compute::cast(col, field.data_type())
.map_err(|e| DataFusionError::ArrowError(Box::new(e), None))?,
);
}
}
None => columns.push(arrow::array::new_null_array(
field.data_type(),
batch.num_rows(),
)),
}
}
let options = RecordBatchOptions::new().with_row_count(Some(batch.num_rows()));
RecordBatch::try_new_with_options(Arc::clone(target), columns, &options)
.map_err(|e| DataFusionError::ArrowError(Box::new(e), None))
}
#[cfg(feature = "object-store")]
pub(crate) fn to_archive_batch(
batch: &RecordBatch,
target: &SchemaRef,
) -> Result<RecordBatch, DataFusionError> {
let converted = convert_timestamp_column(batch.clone())?;
align_batch_to_schema(&converted, target)
}
#[cfg(test)]
mod align_tests {
#![allow(clippy::unwrap_used, clippy::expect_used)]
use super::*;
use arrow::array::Int64Array;
use arrow::datatypes::{Field, Schema};
#[test]
fn the_result_carries_the_target_schema_even_when_the_names_match() {
let src = Arc::new(Schema::new(vec![
Field::new("v", DataType::Int64, true)
.with_metadata(crate::db::role_metadata(chronix_core::ColumnRole::Field)),
]));
let batch =
RecordBatch::try_new(src, vec![Arc::new(Int64Array::from(vec![1_i64, 2]))]).unwrap();
let target: SchemaRef = Arc::new(Schema::new(vec![Field::new("v", DataType::Int64, true)]));
let aligned = align_batch_to_schema(&batch, &target).unwrap();
assert_eq!(aligned.schema().as_ref(), target.as_ref());
assert_eq!(aligned.num_rows(), 2);
}
#[test]
fn an_identical_schema_is_returned_unchanged() {
let schema: SchemaRef = Arc::new(Schema::new(vec![Field::new("v", DataType::Int64, true)]));
let batch = RecordBatch::try_new(
Arc::clone(&schema),
vec![Arc::new(Int64Array::from(vec![1_i64]))],
)
.unwrap();
let aligned = align_batch_to_schema(&batch, &schema).unwrap();
assert_eq!(aligned.schema().as_ref(), schema.as_ref());
}
}
#[cfg(all(test, feature = "object-store"))]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used)]
use super::*;
use arrow::array::{Float64Array, Int64Array, StringArray};
use arrow::datatypes::{Field, Schema};
fn storage_batch() -> RecordBatch {
let schema = Arc::new(Schema::new(vec![
Field::new(chronix_core::TIME_COLUMN, DataType::Int64, false),
Field::new("host", DataType::Utf8, true),
Field::new("watts", DataType::Float64, true),
]));
RecordBatch::try_new(
schema,
vec![
Arc::new(Int64Array::from(vec![1_i64, 2])),
Arc::new(StringArray::from(vec!["a", "b"])),
Arc::new(Float64Array::from(vec![1.0, 2.0])),
],
)
.unwrap()
}
#[test]
fn archive_batch_matches_the_target_schema() {
let target: SchemaRef = Arc::new(Schema::new(vec![
Field::new(
"_time",
DataType::Timestamp(TimeUnit::Nanosecond, None),
false,
),
Field::new("host", DataType::Utf8, true),
Field::new("amps", DataType::Float64, true),
Field::new("watts", DataType::Float64, true),
]));
let out = to_archive_batch(&storage_batch(), &target).unwrap();
assert_eq!(out.schema(), target, "the archive schema is the contract");
assert_eq!(out.num_rows(), 2);
assert!(
out.column(2).null_count() == 2,
"a field the batch predates must be null, not absent"
);
}
}