use std::collections::HashMap;
use std::sync::Arc;
use arrow::datatypes::{DataType, Field, FieldRef, Fields, Schema};
use arrow::error::ArrowError;
use arrow_schema::extension::{
EXTENSION_TYPE_METADATA_KEY, EXTENSION_TYPE_NAME_KEY, ExtensionType,
};
use serde::{Deserialize, Serialize};
use crate::error::{Result, nd_err};
pub const VALUES_FIELD: &str = "values";
pub const DIM_NAMES_FIELD: &str = "dim_names";
pub const DIM_SIZES_FIELD: &str = "dim_sizes";
pub const ATTRS_FIELD: &str = "attrs";
pub const ITEM_FIELD: &str = "item";
pub const ENCODING_VERSION: u32 = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct NdArrayMetadata {
pub version: u32,
}
impl Default for NdArrayMetadata {
fn default() -> Self {
Self {
version: ENCODING_VERSION,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NdArrayType {
value_field: FieldRef,
attrs: Fields,
metadata: NdArrayMetadata,
}
impl NdArrayType {
pub fn new(value_type: DataType, attrs: Fields) -> Self {
Self::with_value_field(Arc::new(Field::new(ITEM_FIELD, value_type, true)), attrs)
}
pub fn with_value_field(value_field: FieldRef, attrs: Fields) -> Self {
let attrs = attrs
.iter()
.map(|f| {
if f.is_nullable() {
f.clone()
} else {
Arc::new(f.as_ref().clone().with_nullable(true))
}
})
.collect();
Self {
value_field,
attrs,
metadata: NdArrayMetadata::default(),
}
}
pub fn value_field(&self) -> &FieldRef {
&self.value_field
}
pub fn value_type(&self) -> &DataType {
self.value_field.data_type()
}
pub fn attribute_fields(&self) -> &Fields {
&self.attrs
}
pub fn storage_type(&self) -> DataType {
DataType::Struct(self.storage_fields())
}
pub fn has_attributes(&self) -> bool {
!self.attrs.is_empty()
}
pub(crate) fn storage_fields(&self) -> Fields {
let mut fields = vec![
Field::new(
VALUES_FIELD,
DataType::List(self.value_field.clone()),
false,
),
Field::new(DIM_NAMES_FIELD, dim_names_type(), false),
Field::new(DIM_SIZES_FIELD, dim_sizes_type(), false),
];
if self.has_attributes() {
fields.push(Field::new(
ATTRS_FIELD,
DataType::Struct(self.attrs.clone()),
false,
));
}
fields.into()
}
pub fn try_from_storage_type(data_type: &DataType) -> Result<Self> {
let DataType::Struct(fields) = data_type else {
return Err(nd_err!(
InvalidEncoding,
"storage type must be a Struct, got {data_type}"
));
};
let names: Vec<&str> = fields.iter().map(|f| f.name().as_str()).collect();
let expected = [VALUES_FIELD, DIM_NAMES_FIELD, DIM_SIZES_FIELD, ATTRS_FIELD];
if !(names == expected[..3] || names == expected) {
return Err(nd_err!(
InvalidEncoding,
"storage fields must be {expected:?}, got {names:?}"
));
}
let DataType::List(value_field) = fields[0].data_type() else {
return Err(nd_err!(
InvalidEncoding,
"'{VALUES_FIELD}' must be a List, got {}",
fields[0].data_type()
));
};
expect_list_of(&fields[1], &DataType::Utf8)?;
expect_list_of(&fields[2], &DataType::UInt32)?;
let attrs = match fields.get(3).map(|f| f.data_type()) {
None => Fields::empty(),
Some(DataType::Struct(attrs)) => attrs.clone(),
Some(other) => {
return Err(nd_err!(
InvalidEncoding,
"'{ATTRS_FIELD}' must be a Struct, got {other}"
));
}
};
Ok(Self::with_value_field(value_field.clone(), attrs))
}
pub fn to_field(&self, name: impl Into<String>, nullable: bool) -> Field {
Field::new(name, self.storage_type(), nullable).with_extension_type(self.clone())
}
}
fn dim_names_type() -> DataType {
DataType::List(Arc::new(Field::new(ITEM_FIELD, DataType::Utf8, false)))
}
fn dim_sizes_type() -> DataType {
DataType::List(Arc::new(Field::new(ITEM_FIELD, DataType::UInt32, false)))
}
fn expect_list_of(field: &Field, item_type: &DataType) -> Result<()> {
match field.data_type() {
DataType::List(item) if item.data_type() == item_type => Ok(()),
other => Err(nd_err!(
InvalidEncoding,
"'{}' must be List<{item_type}>, got {other}",
field.name()
)),
}
}
impl ExtensionType for NdArrayType {
const NAME: &'static str = "nd_arrow.array";
type Metadata = NdArrayMetadata;
fn metadata(&self) -> &Self::Metadata {
&self.metadata
}
fn serialize_metadata(&self) -> Option<String> {
Some(serde_json::to_string(&self.metadata).expect("metadata serializes"))
}
fn deserialize_metadata(metadata: Option<&str>) -> Result<Self::Metadata, ArrowError> {
let metadata = match metadata.map(str::trim) {
None | Some("") | Some("{}") => NdArrayMetadata::default(),
Some(json) => serde_json::from_str(json).map_err(|e| {
ArrowError::InvalidArgumentError(format!("invalid nd extension metadata: {e}"))
})?,
};
if metadata.version != ENCODING_VERSION {
return Err(ArrowError::InvalidArgumentError(format!(
"unsupported nd encoding version {}, expected {ENCODING_VERSION}",
metadata.version
)));
}
Ok(metadata)
}
fn supports_data_type(&self, data_type: &DataType) -> Result<(), ArrowError> {
let parsed = Self::try_from_storage_type(data_type)?;
if parsed.value_field != self.value_field || parsed.attrs != self.attrs {
return Err(ArrowError::InvalidArgumentError(format!(
"nd extension type does not match storage type {data_type}"
)));
}
Ok(())
}
fn try_new(data_type: &DataType, metadata: Self::Metadata) -> Result<Self, ArrowError> {
let mut parsed = Self::try_from_storage_type(data_type)?;
parsed.metadata = metadata;
Ok(parsed)
}
}
pub const GLOBAL_ATTRIBUTES_EXTENSION: &str = "nd_arrow.global_attributes";
pub const GLOBAL_ATTRIBUTES_FIELD: &str = "__global_attrs__";
pub fn global_attributes_field(fields: Fields) -> Field {
Field::new(GLOBAL_ATTRIBUTES_FIELD, DataType::Struct(fields), false).with_metadata(
HashMap::from([
(
EXTENSION_TYPE_NAME_KEY.to_string(),
GLOBAL_ATTRIBUTES_EXTENSION.to_string(),
),
(
EXTENSION_TYPE_METADATA_KEY.to_string(),
serde_json::to_string(&NdArrayMetadata::default()).expect("metadata serializes"),
),
]),
)
}
pub fn is_global_attributes_field(field: &Field) -> bool {
field.extension_type_name() == Some(GLOBAL_ATTRIBUTES_EXTENSION)
&& matches!(field.data_type(), DataType::Struct(_))
}
pub fn is_nd_field(field: &Field) -> bool {
field.extension_type_name() == Some(NdArrayType::NAME)
}
pub fn encoded_field(logical: &Field) -> Field {
let field = NdArrayType::new(logical.data_type().clone(), Fields::empty())
.to_field(logical.name(), true);
with_base_metadata(field, logical.metadata())
}
pub fn logical_field(encoded: &Field) -> Result<Field> {
let ext: NdArrayType = encoded.try_extension_type()?;
let mut metadata = encoded.metadata().clone();
metadata.remove(EXTENSION_TYPE_NAME_KEY);
metadata.remove(EXTENSION_TYPE_METADATA_KEY);
Ok(Field::new(
encoded.name(),
ext.value_type().clone(),
encoded.is_nullable(),
)
.with_metadata(metadata))
}
pub fn encoded_schema(logical: &Schema) -> Schema {
let fields: Vec<Field> = logical.fields().iter().map(|f| encoded_field(f)).collect();
Schema::new_with_metadata(fields, logical.metadata().clone())
}
pub fn logical_schema(encoded: &Schema) -> Result<Schema> {
let fields = encoded
.fields()
.iter()
.map(|f| logical_field(f))
.collect::<Result<Vec<_>>>()?;
Ok(Schema::new_with_metadata(
fields,
encoded.metadata().clone(),
))
}
pub(crate) fn with_base_metadata(field: Field, base: &HashMap<String, String>) -> Field {
let mut metadata = base.clone();
metadata.extend(field.metadata().clone());
field.with_metadata(metadata)
}
#[cfg(test)]
mod tests {
use super::*;
fn sample() -> NdArrayType {
NdArrayType::new(
DataType::Float64,
Fields::from(vec![Field::new("units", DataType::Utf8, true)]),
)
}
#[test]
fn storage_type_round_trips() {
let ext = sample();
let parsed = NdArrayType::try_from_storage_type(&ext.storage_type()).unwrap();
assert_eq!(parsed, ext);
}
#[test]
fn field_is_tagged_and_parses_back() {
let field = sample().to_field("sst", true);
assert!(is_nd_field(&field));
let ext: NdArrayType = field.try_extension_type().unwrap();
assert_eq!(ext, sample());
assert_eq!(
field.metadata().get(EXTENSION_TYPE_METADATA_KEY).unwrap(),
r#"{"version":1}"#
);
}
#[test]
fn logical_and_encoded_fields_round_trip() {
let logical = Field::new("sst", DataType::Float32, true)
.with_metadata(HashMap::from([("k".to_string(), "v".to_string())]));
let encoded = encoded_field(&logical);
assert!(is_nd_field(&encoded));
assert_eq!(encoded.metadata().get("k").unwrap(), "v");
assert_eq!(logical_field(&encoded).unwrap(), logical);
}
#[test]
fn invalid_storage_rejected() {
assert!(NdArrayType::try_from_storage_type(&DataType::Float64).is_err());
let wrong_order = DataType::Struct(Fields::from(vec![
Field::new(DIM_NAMES_FIELD, dim_names_type(), false),
Field::new(
VALUES_FIELD,
DataType::new_list(DataType::Int32, true),
false,
),
Field::new(DIM_SIZES_FIELD, dim_sizes_type(), false),
Field::new(ATTRS_FIELD, DataType::Struct(Fields::empty()), false),
]));
assert!(NdArrayType::try_from_storage_type(&wrong_order).is_err());
}
#[test]
fn unknown_version_rejected() {
assert!(NdArrayType::deserialize_metadata(Some(r#"{"version":2}"#)).is_err());
assert!(NdArrayType::deserialize_metadata(None).is_ok());
}
}