use std::collections::HashMap;
use std::sync::Arc;
use arrow_array::RecordBatch;
use arrow_schema::{Field, Schema};
use super::error::{IcebergError, Result};
pub trait ArrowSerializable {
fn arrow_schema(field_id_map: &FieldIdMap) -> Result<Arc<Schema>>;
fn to_record_batch(&self, field_id_map: &FieldIdMap) -> Result<RecordBatch>;
}
#[derive(Debug, Clone)]
pub struct FieldIdMap {
column_to_field_id: HashMap<String, i32>,
table_name: String,
}
impl FieldIdMap {
pub fn get_field_id(&self, column_path: &str) -> Result<&i32> {
self.column_to_field_id.get(column_path).ok_or_else(|| {
IcebergError::ColumnNotFound {
column_name: column_path.to_owned(),
table_name: self.table_name.clone(),
}
.into()
})
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.column_to_field_id.is_empty()
}
#[must_use]
pub fn len(&self) -> usize {
self.column_to_field_id.len()
}
#[must_use]
pub const fn new(table_name: String, column_to_field_id: HashMap<String, i32>) -> Self {
Self {
column_to_field_id,
table_name,
}
}
#[must_use]
pub fn table_name(&self) -> &str {
&self.table_name
}
pub(crate) fn validate_columns(&self, required_columns: &[&str]) -> Result<()> {
let missing_columns: Vec<String> = required_columns
.iter()
.filter_map(|column| {
if self.column_to_field_id.contains_key(*column) {
None
} else {
Some((*column).to_owned())
}
})
.collect();
if missing_columns.is_empty() {
Ok(())
} else {
Err(IcebergError::MissingColumns {
missing_columns,
table_name: self.table_name.clone(),
}
.into())
}
}
}
pub(crate) fn update_field_id(field: &mut Field, field_id: i32) {
field
.metadata_mut()
.insert("PARQUET:field_id".to_owned(), field_id.to_string());
}
#[cfg(test)]
#[allow(
clippy::expect_used,
clippy::panic,
reason = "tests use expect/panic for brevity"
)]
mod tests {
use super::*;
use crate::types::error::HorizonError;
fn sample_map() -> FieldIdMap {
let mut mapping = HashMap::new();
mapping.insert("datetime".to_owned(), 1_i32);
mapping.insert("vector".to_owned(), 2_i32);
mapping.insert("vector.element".to_owned(), 3_i32);
FieldIdMap::new("test.table".to_owned(), mapping)
}
#[test]
fn validate_columns_accepts_matching_top_level_fields() {
let map = sample_map();
map.validate_columns(&["datetime", "vector"])
.expect("matching columns should validate");
}
#[test]
fn validate_columns_accepts_empty_required_list() {
let map = sample_map();
map.validate_columns(&[])
.expect("empty required list should validate");
}
#[test]
fn validate_columns_reports_missing_columns_with_table_name() {
let map = sample_map();
let error = map
.validate_columns(&["datetime", "ghost"])
.expect_err("missing column should error");
let HorizonError::Iceberg(IcebergError::MissingColumns {
missing_columns,
table_name,
}) = error
else {
panic!("expected IcebergError::MissingColumns variant");
};
assert_eq!(missing_columns, vec!["ghost".to_owned()]);
assert_eq!(table_name, "test.table");
}
#[test]
fn get_field_id_returns_column_not_found_for_missing_column() {
let map = sample_map();
let error = map
.get_field_id("ghost")
.expect_err("missing column should error");
let HorizonError::Iceberg(IcebergError::ColumnNotFound {
column_name,
table_name,
}) = error
else {
panic!("expected IcebergError::ColumnNotFound variant");
};
assert_eq!(column_name, "ghost");
assert_eq!(table_name, "test.table");
}
}