blob: 9f69400bdf87cec8a3c9232f05eafea5b2722e6a [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.
use crate::spec::DataType;
use serde::{Deserialize, Serialize};
/// Schema change to table.
///
/// The JSON wire format is kept compatible with Java Paimon's `SchemaChange`,
/// which is an internally-tagged polymorphic type (`@JsonTypeInfo` with an
/// `"action"` discriminator). Each variant therefore serializes as
/// `{"action": "<name>", ...fields}` with `fieldNames` arrays (a column path;
/// only top-level single-element paths are currently applied — see
/// `TableSchema::apply_changes`).
///
/// Reference: <https://github.com/apache/paimon/blob/master/paimon-api/src/main/java/org/apache/paimon/schema/SchemaChange.java>
//
// Note: `dropPrimaryKey` and `updateColumnDefaultValue` from Java are not yet
// modeled here; they are out of scope for the current alter-table support.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "action", rename_all = "camelCase")]
pub enum SchemaChange {
/// A SchemaChange to set a table option.
SetOption { key: String, value: String },
/// A SchemaChange to remove a table option.
RemoveOption { key: String },
/// A SchemaChange to update a table comment.
UpdateComment { comment: Option<String> },
/// A SchemaChange to add a new field.
#[serde(rename_all = "camelCase")]
AddColumn {
field_names: Vec<String>,
data_type: DataType,
comment: Option<String>,
#[serde(rename = "move")]
column_move: Option<ColumnMove>,
},
/// A SchemaChange to rename a field.
#[serde(rename_all = "camelCase")]
RenameColumn {
field_names: Vec<String>,
new_name: String,
},
/// A SchemaChange to drop a field.
#[serde(rename_all = "camelCase")]
DropColumn { field_names: Vec<String> },
/// A SchemaChange to update the field's type.
#[serde(rename_all = "camelCase")]
UpdateColumnType {
field_names: Vec<String>,
new_data_type: DataType,
/// When true, keep the existing column's nullability instead of taking
/// it from `new_data_type`.
#[serde(default)]
keep_nullability: bool,
},
/// A SchemaChange to update the field's nullability.
#[serde(rename_all = "camelCase")]
UpdateColumnNullability {
field_names: Vec<String>,
new_nullability: bool,
},
/// A SchemaChange to update the (nested) field's comment.
#[serde(rename_all = "camelCase")]
UpdateColumnComment {
field_names: Vec<String>,
new_comment: String,
},
/// A SchemaChange to update the field's position.
#[serde(rename_all = "camelCase")]
UpdateColumnPosition {
#[serde(rename = "move")]
column_move: ColumnMove,
},
}
impl SchemaChange {
/// impl the `set_option`.
pub fn set_option(key: String, value: String) -> Self {
SchemaChange::SetOption { key, value }
}
/// impl the `remove_option`.
pub fn remove_option(key: String) -> Self {
SchemaChange::RemoveOption { key }
}
/// impl the `update_comment`.
pub fn update_comment(comment: Option<String>) -> Self {
SchemaChange::UpdateComment { comment }
}
/// impl the `add_column`.
pub fn add_column(field_name: String, data_type: DataType) -> Self {
SchemaChange::AddColumn {
field_names: vec![field_name],
data_type,
comment: None,
column_move: None,
}
}
/// impl the `add_column_with_description`.
pub fn add_column_with_description(
field_name: String,
data_type: DataType,
description: String,
) -> Self {
SchemaChange::AddColumn {
field_names: vec![field_name],
data_type,
comment: Some(description),
column_move: None,
}
}
/// impl the `add_column_with_description_and_column_move`.
pub fn add_column_with_description_and_column_move(
field_name: String,
data_type: DataType,
description: String,
column_move: ColumnMove,
) -> Self {
SchemaChange::AddColumn {
field_names: vec![field_name],
data_type,
comment: Some(description),
column_move: Some(column_move),
}
}
/// impl the `rename_column`.
pub fn rename_column(field_name: String, new_name: String) -> Self {
SchemaChange::RenameColumn {
field_names: vec![field_name],
new_name,
}
}
/// impl the `drop_column`.
pub fn drop_column(field_name: String) -> Self {
SchemaChange::DropColumn {
field_names: vec![field_name],
}
}
/// impl the `update_column_type`.
pub fn update_column_type(field_name: String, new_data_type: DataType) -> Self {
SchemaChange::UpdateColumnType {
field_names: vec![field_name],
new_data_type,
keep_nullability: false,
}
}
/// impl the `update_column_position`.
pub fn update_column_position(column_move: ColumnMove) -> Self {
SchemaChange::UpdateColumnPosition { column_move }
}
/// impl the `update_column_nullability`.
pub fn update_column_nullability(field_name: String, new_nullability: bool) -> Self {
SchemaChange::UpdateColumnNullability {
field_names: vec![field_name],
new_nullability,
}
}
/// impl the `update_columns_nullability`.
pub fn update_columns_nullability(field_names: Vec<String>, new_nullability: bool) -> Self {
SchemaChange::UpdateColumnNullability {
field_names,
new_nullability,
}
}
/// impl the `update_column_comment`.
pub fn update_column_comment(field_name: String, comment: String) -> Self {
SchemaChange::UpdateColumnComment {
field_names: vec![field_name],
new_comment: comment,
}
}
/// impl the `update_columns_comment`.
pub fn update_columns_comment(field_names: Vec<String>, comment: String) -> Self {
SchemaChange::UpdateColumnComment {
field_names,
new_comment: comment,
}
}
}
/// The type of move.
///
/// Reference: <https://github.com/apache/paimon/blob/master/paimon-api/src/main/java/org/apache/paimon/schema/SchemaChange.java>
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub enum ColumnMoveType {
FIRST,
AFTER,
BEFORE,
LAST,
}
/// Represents a requested column move in a struct.
///
/// Reference: <https://github.com/apache/paimon/blob/master/paimon-api/src/main/java/org/apache/paimon/schema/SchemaChange.java>
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ColumnMove {
pub field_name: String,
/// The anchor column for `AFTER`/`BEFORE` moves (`None` for `FIRST`/`LAST`).
/// Named `referenceFieldName` on the wire to match Java Paimon.
pub reference_field_name: Option<String>,
#[serde(rename = "type")]
pub move_type: ColumnMoveType,
}
impl ColumnMove {
/// Get the field name.
pub fn field_name(&self) -> &str {
&self.field_name
}
/// Get the reference field name.
pub fn reference_field_name(&self) -> Option<&str> {
self.reference_field_name.as_deref()
}
/// Get the move type.
pub fn move_type(&self) -> &ColumnMoveType {
&self.move_type
}
/// Create a new `Move` with `FIRST` move type.
pub fn move_first(field_name: String) -> Self {
ColumnMove {
field_name,
reference_field_name: None,
move_type: ColumnMoveType::FIRST,
}
}
/// Create a new `Move` with `LAST` move type.
pub fn move_last(field_name: String) -> Self {
ColumnMove {
field_name,
reference_field_name: None,
move_type: ColumnMoveType::LAST,
}
}
/// Create a new `Move` with `AFTER` move type.
pub fn move_after(field_name: String, reference_field_name: String) -> Self {
ColumnMove {
field_name,
reference_field_name: Some(reference_field_name),
move_type: ColumnMoveType::AFTER,
}
}
/// Create a new `Move` with `BEFORE` move type.
pub fn move_before(field_name: String, reference_field_name: String) -> Self {
ColumnMove {
field_name,
reference_field_name: Some(reference_field_name),
move_type: ColumnMoveType::BEFORE,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::spec::{DoubleType, IntType};
#[test]
fn test_schema_change_serialize_deserialize() {
// Java-compatible wire format: internally tagged by "action", with
// `fieldNames` arrays and `referenceFieldName` move anchors.
let json_data = r#"
[
{
"action": "setOption",
"key": "snapshot.time-retained",
"value": "2h"
},
{
"action": "removeOption",
"key": "compaction.max.file-num"
},
{
"action": "updateComment",
"comment": "table.comment"
},
{
"action": "addColumn",
"fieldNames": ["col1"],
"dataType": "INT",
"comment": "col1_description",
"move": {
"fieldName": "col1",
"referenceFieldName": null,
"type": "FIRST"
}
},
{
"action": "renameColumn",
"fieldNames": ["col3"],
"newName": "col3_new_name"
},
{
"action": "dropColumn",
"fieldNames": ["col1"]
},
{
"action": "updateColumnType",
"fieldNames": ["col14"],
"newDataType": "DOUBLE",
"keepNullability": false
},
{
"action": "updateColumnPosition",
"move": {
"fieldName": "col4",
"referenceFieldName": "col3",
"type": "AFTER"
}
},
{
"action": "updateColumnNullability",
"fieldNames": ["col5", "f2"],
"newNullability": false
},
{
"action": "updateColumnComment",
"fieldNames": ["col5", "f1"],
"newComment": "col5 f1 field"
}
]"#;
let schema_changes: Vec<SchemaChange> =
serde_json::from_str(json_data).expect("Failed to deserialize SchemaChange.");
assert_eq!(
schema_changes,
vec![
SchemaChange::SetOption {
key: "snapshot.time-retained".to_string(),
value: "2h".to_string(),
},
SchemaChange::RemoveOption {
key: "compaction.max.file-num".to_string(),
},
SchemaChange::UpdateComment {
comment: Some("table.comment".to_string()),
},
SchemaChange::AddColumn {
field_names: vec!["col1".to_string()],
data_type: DataType::Int(IntType::new()),
comment: Some("col1_description".to_string()),
column_move: Some(ColumnMove::move_first("col1".to_string())),
},
SchemaChange::RenameColumn {
field_names: vec!["col3".to_string()],
new_name: "col3_new_name".to_string(),
},
SchemaChange::DropColumn {
field_names: vec!["col1".to_string()],
},
SchemaChange::UpdateColumnType {
field_names: vec!["col14".to_string()],
new_data_type: DataType::Double(DoubleType::new()),
keep_nullability: false,
},
SchemaChange::UpdateColumnPosition {
column_move: ColumnMove::move_after("col4".to_string(), "col3".to_string()),
},
SchemaChange::UpdateColumnNullability {
field_names: vec!["col5".to_string(), "f2".to_string()],
new_nullability: false,
},
SchemaChange::UpdateColumnComment {
field_names: vec!["col5".to_string(), "f1".to_string()],
new_comment: "col5 f1 field".to_string(),
},
]
);
}
#[test]
fn test_schema_change_serialize_shape() {
// Verify the serialized JSON carries the Java "action" discriminator.
let change = SchemaChange::add_column("c".to_string(), DataType::Int(IntType::new()));
let value = serde_json::to_value(&change).unwrap();
assert_eq!(value["action"], "addColumn");
assert_eq!(value["fieldNames"][0], "c");
// Round-trip through JSON.
let round: SchemaChange = serde_json::from_value(value).unwrap();
assert_eq!(round, change);
}
#[test]
fn test_column_move_serialize_deserialize() {
let json_data = r#"
[
{
"fieldName": "col1",
"referenceFieldName": null,
"type": "FIRST"
},
{
"fieldName": "col2_after",
"referenceFieldName": "col2",
"type": "AFTER"
}
]"#;
let column_moves: Vec<ColumnMove> = serde_json::from_str(json_data).unwrap();
assert_eq!(
column_moves,
vec![
ColumnMove::move_first("col1".to_string()),
ColumnMove::move_after("col2_after".to_string(), "col2".to_string()),
]
);
}
}