blob: ea9450b34de22f4308f0b4548c2467a2a3708a82 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//! Schema evolution utilities for mapping between table schema and data file schema.
//!
//! Reference: [org.apache.paimon.schema.SchemaEvolutionUtil](https://github.com/apache/paimon/blob/release-1.3/paimon-core/src/main/java/org/apache/paimon/schema/SchemaEvolutionUtil.java)
use crate::spec::DataField;
use std::collections::HashMap;
/// Sentinel value indicating a field does not exist in the data schema.
pub const NULL_FIELD_INDEX: i32 = -1;
/// Create index mapping from table fields to underlying data fields using field IDs.
///
/// For example, the table and data fields are as follows:
/// - table fields: `1->c, 6->b, 3->a`
/// - data fields: `1->a, 3->c`
///
/// We get the index mapping `[0, -1, 1]`, where:
/// - `0` is the index of table field `1->c` in data fields
/// - `-1` means field `6->b` does not exist in data fields
/// - `1` is the index of table field `3->a` in data fields
///
/// Returns `None` if the mapping is identity (no evolution needed).
///
/// Reference: [SchemaEvolutionUtil.createIndexMapping](https://github.com/apache/paimon/blob/release-1.3/paimon-core/src/main/java/org/apache/paimon/schema/SchemaEvolutionUtil.java)
pub fn create_index_mapping(
table_fields: &[DataField],
data_fields: &[DataField],
) -> Option<Vec<i32>> {
let mut field_id_to_index: HashMap<i32, i32> = HashMap::with_capacity(data_fields.len());
for (i, field) in data_fields.iter().enumerate() {
field_id_to_index.insert(field.id(), i as i32);
}
let mut index_mapping = Vec::with_capacity(table_fields.len());
for field in table_fields {
let data_index = field_id_to_index
.get(&field.id())
.copied()
.unwrap_or(NULL_FIELD_INDEX);
index_mapping.push(data_index);
}
// Check if mapping is identity (no evolution needed).
let is_identity = index_mapping.len() == data_fields.len()
&& index_mapping
.iter()
.enumerate()
.all(|(i, &idx)| idx == i as i32);
if is_identity {
None
} else {
Some(index_mapping)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::spec::{DataType, IntType, VarCharType};
fn field(id: i32, name: &str) -> DataField {
DataField::new(id, name.to_string(), DataType::Int(IntType::new()))
}
#[test]
fn test_identity_mapping() {
let table_fields = vec![field(0, "a"), field(1, "b"), field(2, "c")];
let data_fields = vec![field(0, "a"), field(1, "b"), field(2, "c")];
assert_eq!(create_index_mapping(&table_fields, &data_fields), None);
}
#[test]
fn test_added_column() {
// Table has 3 fields, data file only has the first 2
let table_fields = vec![field(0, "a"), field(1, "b"), field(2, "c")];
let data_fields = vec![field(0, "a"), field(1, "b")];
assert_eq!(
create_index_mapping(&table_fields, &data_fields),
Some(vec![0, 1, -1])
);
}
#[test]
fn test_reordered_fields() {
let table_fields = vec![field(1, "c"), field(6, "b"), field(3, "a")];
let data_fields = vec![field(1, "a"), field(3, "c")];
assert_eq!(
create_index_mapping(&table_fields, &data_fields),
Some(vec![0, -1, 1])
);
}
#[test]
fn test_renamed_column() {
// Field ID stays the same even if name changed
let table_fields = vec![field(0, "id"), field(1, "new_name")];
let data_fields = vec![field(0, "id"), field(1, "old_name")];
// Identity mapping since field IDs match positionally
assert_eq!(create_index_mapping(&table_fields, &data_fields), None);
}
#[test]
fn test_empty_data_fields() {
let table_fields = vec![field(0, "a"), field(1, "b")];
let data_fields: Vec<DataField> = vec![];
assert_eq!(
create_index_mapping(&table_fields, &data_fields),
Some(vec![-1, -1])
);
}
#[test]
fn test_type_promotion_same_mapping() {
// Type promotion doesn't affect index mapping — only field IDs matter
let table_fields = vec![
DataField::new(0, "id".to_string(), DataType::Int(IntType::new())),
DataField::new(
1,
"name".to_string(),
DataType::VarChar(VarCharType::string_type()),
),
];
let data_fields = vec![
DataField::new(0, "id".to_string(), DataType::Int(IntType::new())),
DataField::new(
1,
"name".to_string(),
DataType::VarChar(VarCharType::string_type()),
),
];
assert_eq!(create_index_mapping(&table_fields, &data_fields), None);
}
}