use std::fmt;
use std::sync::Arc;
use arrow_array::{ArrayRef, RecordBatch};
use arrow_schema::{DataType, Field as ArrowField, FieldRef, SchemaRef};
use lance_arrow::BLOB_V2_EXT_NAME;
use lance_core::{Error, Result, datatypes::Schema};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FieldTypeMismatch {
pub field_path: String,
pub expected: FieldRef,
pub actual: FieldRef,
}
impl fmt::Display for FieldTypeMismatch {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"field `{}` does not match the file schema: expected {}, got {}",
self.field_path,
FieldType(&self.expected),
FieldType(&self.actual)
)
}
}
impl std::error::Error for FieldTypeMismatch {}
struct FieldType<'a>(&'a ArrowField);
impl fmt::Display for FieldType<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0.data_type())?;
match (
self.0.extension_type_name(),
self.0.extension_type_metadata(),
) {
(Some(name), Some(metadata)) => write!(f, " (extension {name}, metadata {metadata})"),
(Some(name), None) => write!(f, " (extension {name})"),
_ => write!(f, " (no extension)"),
}
}
}
pub struct ExpectedTypes {
fields: Vec<FieldRef>,
accepted: Option<SchemaRef>,
}
impl ExpectedTypes {
pub fn new(schema: &Schema) -> Self {
Self {
fields: schema
.fields
.iter()
.map(|field| Arc::new(ArrowField::from(field)))
.collect(),
accepted: None,
}
}
pub fn check_batch(&mut self, batch: &RecordBatch) -> Result<()> {
let batch_schema = batch.schema();
if self.accepted.as_ref() == Some(&batch_schema) {
return Ok(());
}
for (index, expected) in self.fields.iter().enumerate() {
let actual = batch_schema
.fields()
.get(index)
.filter(|actual| actual.name() == expected.name())
.or_else(|| {
let (index, _) = batch_schema.column_with_name(expected.name())?;
batch_schema.fields().get(index)
});
if let Some(actual) = actual {
check_field(expected, actual, true).map_err(into_error)?;
}
}
self.accepted = Some(batch_schema);
Ok(())
}
pub fn check_column(&self, index: usize, array: &ArrayRef) -> Result<()> {
let expected = &self.fields[index];
let actual = Arc::new(ArrowField::new(
expected.name(),
array.data_type().clone(),
true,
));
check_field(expected, &actual, false).map_err(into_error)
}
}
fn into_error(mismatch: FieldTypeMismatch) -> Error {
Error::invalid_input_source(Box::new(mismatch))
}
fn check_field(
expected: &FieldRef,
actual: &FieldRef,
compare_extension: bool,
) -> std::result::Result<(), FieldTypeMismatch> {
let mismatch = || FieldTypeMismatch {
field_path: expected.name().clone(),
expected: expected.clone(),
actual: actual.clone(),
};
let expected_metadata = expected.extension_type_metadata();
let actual_metadata = actual.extension_type_metadata();
let metadata_matches = expected_metadata == actual_metadata
|| (expected.extension_type_name() == Some(BLOB_V2_EXT_NAME)
&& expected_metadata.unwrap_or_default() == actual_metadata.unwrap_or_default());
if compare_extension
&& (expected.extension_type_name() != actual.extension_type_name() || !metadata_matches)
{
return Err(mismatch());
}
let children = match (expected.data_type(), actual.data_type()) {
(DataType::Struct(_), DataType::Struct(_))
if expected.extension_type_name() == Some(BLOB_V2_EXT_NAME) =>
{
return Ok(());
}
(DataType::Struct(expected_children), DataType::Struct(actual_children))
if expected_children.len() == actual_children.len() =>
{
expected_children
.iter()
.zip(actual_children.iter())
.collect()
}
(DataType::List(expected_item), DataType::List(actual_item))
| (DataType::LargeList(expected_item), DataType::LargeList(actual_item))
| (DataType::Map(expected_item, _), DataType::Map(actual_item, _)) => {
vec![(expected_item, actual_item)]
}
(
DataType::FixedSizeList(expected_item, expected_size),
DataType::FixedSizeList(actual_item, actual_size),
) if expected_size == actual_size => vec![(expected_item, actual_item)],
(DataType::Utf8, DataType::Utf8View) | (DataType::Binary, DataType::BinaryView) => {
return Ok(());
}
(expected_type, actual_type) if expected_type == actual_type => return Ok(()),
_ => return Err(mismatch()),
};
for (expected_child, actual_child) in children {
check_field(expected_child, actual_child, true).map_err(|mut mismatch| {
mismatch.field_path = format!("{}.{}", expected.name(), mismatch.field_path);
mismatch
})?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use arrow_array::new_empty_array;
use arrow_schema::Schema as ArrowSchema;
use lance_arrow::{ARROW_EXT_META_KEY, ARROW_EXT_NAME_KEY};
use lance_core::datatypes::{BLOB_V2_LOGICAL_MINIMAL_FIELDS, BLOB_V2_PREPARED_TYPE};
use rstest::rstest;
use std::collections::HashMap;
#[rstest]
#[case::python_to_rust(BLOB_V2_EXT_NAME, BLOB_V2_EXT_NAME, Some(""), None, true)]
#[case::rust_to_python(BLOB_V2_EXT_NAME, BLOB_V2_EXT_NAME, None, Some(""), true)]
#[case::nonempty_expected(BLOB_V2_EXT_NAME, BLOB_V2_EXT_NAME, Some("parameters"), None, false)]
#[case::nonempty_actual(BLOB_V2_EXT_NAME, BLOB_V2_EXT_NAME, None, Some("parameters"), false)]
#[case::different_name(BLOB_V2_EXT_NAME, "other.extension", None, None, false)]
#[case::other_extension("other.extension", "other.extension", Some(""), None, false)]
fn blob_extension_metadata(
#[case] expected_name: &str,
#[case] actual_name: &str,
#[case] expected_metadata: Option<&str>,
#[case] actual_metadata: Option<&str>,
#[case] accepted: bool,
) {
let nested_field = |data_type, name: &str, metadata: Option<&str>| {
let mut values = HashMap::from([(ARROW_EXT_NAME_KEY.to_string(), name.to_string())]);
if let Some(metadata) = metadata {
values.insert(ARROW_EXT_META_KEY.to_string(), metadata.to_string());
}
ArrowField::new(
"parent",
DataType::Struct(
vec![ArrowField::new("blob", data_type, true).with_metadata(values)].into(),
),
true,
)
};
let expected = nested_field(
DataType::Struct(BLOB_V2_LOGICAL_MINIMAL_FIELDS.clone()),
expected_name,
expected_metadata,
);
let actual_type = if expected_name == BLOB_V2_EXT_NAME {
BLOB_V2_PREPARED_TYPE.clone()
} else {
DataType::Struct(BLOB_V2_LOGICAL_MINIMAL_FIELDS.clone())
};
let actual = nested_field(actual_type, actual_name, actual_metadata);
let schema = Schema::try_from(&ArrowSchema::new(vec![expected])).unwrap();
let array = new_empty_array(actual.data_type());
let batch = RecordBatch::try_new(
Arc::new(ArrowSchema::new(vec![actual])),
vec![array.clone()],
)
.unwrap();
let mut expected_types = ExpectedTypes::new(&schema);
for result in [
expected_types.check_batch(&batch),
expected_types.check_column(0, &array),
] {
if accepted {
result.unwrap();
} else {
let error = result.unwrap_err();
assert!(matches!(error, Error::InvalidInput { .. }));
assert!(error.to_string().contains("parent.blob"), "{error}");
}
}
}
}