use crate::error::{Error, Result};
use crate::metadata::{Schema, UNKNOWN_COLUMN_ID};
use std::collections::{HashMap, HashSet};
pub(crate) const UNEXIST_MAPPING: i32 = -1;
pub(crate) fn index_mapping(origin_schema: &Schema, expected_schema: &Schema) -> Result<Vec<i32>> {
let origin_columns = origin_schema.columns();
let mut origin_id_to_index: HashMap<i32, usize> = HashMap::with_capacity(origin_columns.len());
for (i, col) in origin_columns.iter().enumerate() {
if col.id() == UNKNOWN_COLUMN_ID {
return Err(Error::RowConvertError {
message: format!(
"origin schema column '{}' has no assigned id; cannot build index mapping",
col.name()
),
});
}
if origin_id_to_index.insert(col.id(), i).is_some() {
return Err(Error::RowConvertError {
message: format!("duplicate column id {} in origin schema", col.id()),
});
}
}
let expected_columns = expected_schema.columns();
let mut mapping = Vec::with_capacity(expected_columns.len());
let mut expected_seen: HashSet<i32> = HashSet::with_capacity(expected_columns.len());
for expected in expected_columns {
if expected.id() == UNKNOWN_COLUMN_ID {
return Err(Error::RowConvertError {
message: format!(
"expected schema column '{}' has no assigned id; cannot build index mapping",
expected.name()
),
});
}
if !expected_seen.insert(expected.id()) {
return Err(Error::RowConvertError {
message: format!("duplicate column id {} in expected schema", expected.id()),
});
}
match origin_id_to_index.get(&expected.id()) {
None => mapping.push(UNEXIST_MAPPING),
Some(&idx) => {
let origin = &origin_columns[idx];
if !origin.data_type().eq_ignore_nullable(expected.data_type()) {
return Err(Error::RowConvertError {
message: format!(
"Expected datatype of column(id={},name={}) is [{}], while the actual datatype is [{}]",
expected.id(),
expected.name(),
expected.data_type(),
origin.data_type()
),
});
}
mapping.push(idx as i32);
}
}
}
Ok(mapping)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::metadata::{Column, DataType, DataTypes};
fn schema_auto(columns: &[(&str, DataType)]) -> Schema {
let mut b = Schema::builder();
for (name, dt) in columns {
b = b.column(*name, dt.clone());
}
b.build().expect("schema build")
}
fn schema_with_ids(columns: &[(i32, &str, DataType)]) -> Schema {
let cols: Vec<Column> = columns
.iter()
.map(|(id, name, dt)| Column::new(*name, dt.clone()).with_id(*id))
.collect();
Schema::builder()
.with_columns(cols)
.build()
.expect("schema build")
}
#[test]
fn identity_mapping_when_schemas_equal() {
let s = schema_auto(&[
("a", DataTypes::bigint()),
("b", DataTypes::string()),
("c", DataTypes::int()),
]);
assert_eq!(index_mapping(&s, &s).unwrap(), vec![0, 1, 2]);
}
#[test]
fn projection_subset_in_order() {
let origin = schema_auto(&[
("a", DataTypes::bigint()),
("b", DataTypes::string()),
("c", DataTypes::int()),
]);
let expected =
schema_with_ids(&[(0, "a", DataTypes::bigint()), (2, "c", DataTypes::int())]);
assert_eq!(index_mapping(&origin, &expected).unwrap(), vec![0, 2]);
}
#[test]
fn reorder_mapping() {
let origin = schema_auto(&[
("a", DataTypes::bigint()),
("b", DataTypes::string()),
("c", DataTypes::int()),
]);
let expected = schema_with_ids(&[
(2, "c", DataTypes::int()),
(0, "a", DataTypes::bigint()),
(1, "b", DataTypes::string()),
]);
assert_eq!(index_mapping(&origin, &expected).unwrap(), vec![2, 0, 1]);
}
#[test]
fn missing_column_returns_sentinel() {
let origin = schema_auto(&[("a", DataTypes::bigint())]);
let expected = schema_with_ids(&[
(0, "a", DataTypes::bigint()),
(1, "new_col", DataTypes::string()),
]);
assert_eq!(
index_mapping(&origin, &expected).unwrap(),
vec![0, UNEXIST_MAPPING]
);
}
#[test]
fn rename_preserves_mapping_when_id_matches() {
let origin = schema_with_ids(&[(0, "old_name", DataTypes::int())]);
let expected = schema_with_ids(&[(0, "new_name", DataTypes::int())]);
assert_eq!(index_mapping(&origin, &expected).unwrap(), vec![0]);
}
#[test]
fn drop_then_add_with_same_name_does_not_alias() {
let origin = schema_with_ids(&[(0, "a", DataTypes::int())]);
let expected = schema_with_ids(&[(5, "a", DataTypes::int())]);
assert_eq!(
index_mapping(&origin, &expected).unwrap(),
vec![UNEXIST_MAPPING]
);
}
#[test]
fn datatype_mismatch_returns_error() {
let origin = schema_auto(&[("a", DataTypes::bigint())]);
let expected = schema_with_ids(&[(0, "a", DataTypes::int())]);
let err = index_mapping(&origin, &expected).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("id=0"), "{msg}");
assert!(msg.contains("name=a"), "{msg}");
assert!(msg.contains("INT"), "{msg}");
assert!(msg.contains("BIGINT"), "{msg}");
}
#[test]
fn nullability_difference_does_not_error() {
let origin = Schema::builder()
.column("a", DataTypes::int())
.primary_key(["a"])
.unwrap()
.build()
.unwrap();
let expected = schema_with_ids(&[(0, "a", DataTypes::int())]);
assert_eq!(index_mapping(&origin, &expected).unwrap(), vec![0]);
}
}