use std::sync::Arc;
use arrow::array::{
Array, ArrayRef, AsArray, ListArray, RecordBatch, RecordBatchOptions, StructArray,
};
use arrow::compute::{CastOptions, cast_with_options};
use arrow::datatypes::{DataType, Field, FieldRef, Fields, Schema};
use crate::error::Result;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum Casting {
#[default]
No,
Safe,
SameKind,
Promote,
}
pub(crate) fn cast_to(
batch: RecordBatch,
target: &Schema,
casting: Casting,
) -> Result<RecordBatch> {
if casting == Casting::No {
return Ok(batch);
}
let mut fields = Vec::with_capacity(batch.num_columns());
let mut columns = Vec::with_capacity(batch.num_columns());
for (field, column) in batch.schema().fields().iter().zip(batch.columns()) {
let (field, column) = match target.field_with_name(field.name()) {
Ok(target) => cast_field(field, column, target, casting)?,
Err(_) => (field.clone(), column.clone()),
};
fields.push(field);
columns.push(column);
}
let schema = Schema::new_with_metadata(fields, batch.schema().metadata().clone());
let options = RecordBatchOptions::new().with_row_count(Some(batch.num_rows()));
Ok(RecordBatch::try_new_with_options(
Arc::new(schema),
columns,
&options,
)?)
}
fn cast_field(
field: &FieldRef,
array: &ArrayRef,
target: &Field,
casting: Casting,
) -> Result<(FieldRef, ArrayRef)> {
let typed = |data_type: DataType| Arc::new(field.as_ref().clone().with_data_type(data_type));
match (field.data_type(), target.data_type()) {
(from, to) if from == to => Ok((field.clone(), array.clone())),
(DataType::Struct(children), DataType::Struct(targets)) => {
let source = array.as_struct();
let mut fields = Vec::with_capacity(children.len());
let mut columns = Vec::with_capacity(children.len());
for (child, column) in children.iter().zip(source.columns()) {
let (child, column) = match targets.find(child.name()) {
Some((_, target)) => cast_field(child, column, target, casting)?,
None => (child.clone(), column.clone()),
};
fields.push(child);
columns.push(column);
}
let fields = Fields::from(fields);
let array = StructArray::try_new(fields.clone(), columns, source.nulls().cloned())?;
Ok((typed(DataType::Struct(fields)), Arc::new(array)))
}
(DataType::List(item), DataType::List(target)) => {
let source = array.as_list::<i32>();
let (item, values) = cast_field(item, source.values(), target, casting)?;
let array = ListArray::try_new(
item.clone(),
source.offsets().clone(),
values,
source.nulls().cloned(),
)?;
Ok((typed(DataType::List(item)), Arc::new(array)))
}
(from, to) if allowed(from, to, casting) => {
let options = CastOptions {
safe: false,
..Default::default()
};
let array = cast_with_options(array, to, &options)?;
Ok((typed(to.clone()), array))
}
_ => Ok((field.clone(), array.clone())),
}
}
pub(crate) fn allowed(from: &DataType, to: &DataType, casting: Casting) -> bool {
let text =
|t: &DataType| matches!(t, DataType::Utf8 | DataType::LargeUtf8 | DataType::Utf8View);
match casting {
Casting::No => false,
_ if text(from) && text(to) => true,
Casting::Safe | Casting::Promote => is_safe(from, to),
Casting::SameKind => {
is_safe(from, to)
|| kind(from)
.zip(kind(to))
.is_some_and(|(from, to)| from <= to)
}
}
}
pub(crate) fn promote_type(a: &DataType, b: &DataType) -> Option<DataType> {
use DataType::*;
if a == b {
return Some(a.clone());
}
let holds = |from: &DataType, to: &DataType| from == to || is_safe(from, to);
[
UInt8, UInt16, UInt32, UInt64, Int8, Int16, Int32, Int64, Float16, Float32, Float64,
]
.into_iter()
.find(|to| holds(a, to) && holds(b, to))
}
fn kind(data_type: &DataType) -> Option<u8> {
match data_type {
DataType::Boolean => Some(0),
t if t.is_unsigned_integer() => Some(1),
t if t.is_signed_integer() => Some(2),
t if t.is_floating() => Some(3),
_ => None,
}
}
fn is_safe(from: &DataType, to: &DataType) -> bool {
let bits = |t: &DataType| t.primitive_width().map_or(0, |bytes| bytes * 8);
match (kind(from), kind(to)) {
(Some(0), Some(_)) => true,
(Some(f), Some(t)) if f == t => bits(to) > bits(from),
(Some(1), Some(2)) => bits(to) > bits(from),
(Some(1 | 2), Some(3)) => match bits(to) {
16 => bits(from) <= 8,
32 => bits(from) <= 16,
_ => true,
},
_ => false,
}
}