Skip to content

Commit 7810abe

Browse files
authored
feat(datafusion): support alter column (#625)
1 parent e5a9087 commit 7810abe

3 files changed

Lines changed: 349 additions & 5 deletions

File tree

crates/integrations/datafusion/src/sql_context.rs

Lines changed: 170 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
//! - `ALTER TABLE db.t ADD COLUMN col TYPE`
2929
//! - `ALTER TABLE db.t DROP COLUMN col`
3030
//! - `ALTER TABLE db.t RENAME COLUMN old TO new`
31+
//! - `ALTER TABLE db.t ALTER COLUMN col TYPE new_type`
32+
//! - `ALTER TABLE db.t ALTER COLUMN col SET|DROP NOT NULL`
3133
//! - `ALTER TABLE db.t RENAME TO new_name`
3234
//! - `ALTER TABLE db.t DROP PARTITION (col = val, ...)`
3335
//! - `CREATE VIEW [IF NOT EXISTS] view [(col, ...)] AS query`
@@ -55,11 +57,11 @@ use datafusion::logical_expr::{Expr as LogicalExpr, LogicalPlan, Volatility};
5557
use datafusion::prelude::{DataFrame, SessionContext};
5658
use datafusion::sql::planner::IdentNormalizer;
5759
use datafusion::sql::sqlparser::ast::{
58-
AlterTableOperation, BinaryLength, CharacterLength, ColumnDef, ColumnOption, CreateFunction,
59-
CreateFunctionBody, CreateTable, CreateTableOptions, CreateView, Delete, Expr as SqlExpr,
60-
FromTable, FunctionBehavior, FunctionReturnType, Insert, Merge, ObjectName, ObjectType,
61-
RenameTableNameKind, Reset, ResetStatement, Set, ShowCreateObject, SqlOption, Statement,
62-
TableFactor, TableObject, Truncate, Update, Value as SqlValue,
60+
AlterColumnOperation, AlterTableOperation, BinaryLength, CharacterLength, ColumnDef,
61+
ColumnOption, CreateFunction, CreateFunctionBody, CreateTable, CreateTableOptions, CreateView,
62+
Delete, Expr as SqlExpr, FromTable, FunctionBehavior, FunctionReturnType, Insert, Merge,
63+
ObjectName, ObjectType, RenameTableNameKind, Reset, ResetStatement, Set, ShowCreateObject,
64+
SqlOption, Statement, TableFactor, TableObject, Truncate, Update, Value as SqlValue,
6365
};
6466
use datafusion::sql::sqlparser::dialect::GenericDialect;
6567
use datafusion::sql::sqlparser::keywords::Keyword;
@@ -1030,6 +1032,20 @@ impl SQLContext {
10301032
Self::ensure_main_branch_write_target(name, "ALTER TABLE")?;
10311033
let identifier = self.resolve_table_name(name)?;
10321034

1035+
if operations.len() > 1
1036+
&& operations.iter().any(|operation| {
1037+
matches!(
1038+
operation,
1039+
AlterTableOperation::RenameTable { .. }
1040+
| AlterTableOperation::DropPartitions { .. }
1041+
)
1042+
})
1043+
{
1044+
return Err(DataFusionError::Plan(
1045+
"ALTER TABLE RENAME TO and DROP PARTITION must be used alone".to_string(),
1046+
));
1047+
}
1048+
10331049
let mut changes = Vec::new();
10341050
let mut rename_to: Option<Identifier> = None;
10351051

@@ -1052,6 +1068,9 @@ impl SQLContext {
10521068
new_column_name.value.clone(),
10531069
));
10541070
}
1071+
AlterTableOperation::AlterColumn { column_name, op } => {
1072+
changes.push(alter_column_to_schema_change(&column_name.value, op)?);
1073+
}
10551074
AlterTableOperation::RenameTable { table_name } => {
10561075
let new_name = match table_name {
10571076
RenameTableNameKind::To(name) | RenameTableNameKind::As(name) => {
@@ -2380,6 +2399,41 @@ fn column_def_to_add_column(col: &ColumnDef) -> DFResult<SchemaChange> {
23802399
})
23812400
}
23822401

2402+
fn alter_column_to_schema_change(
2403+
column_name: &str,
2404+
operation: &AlterColumnOperation,
2405+
) -> DFResult<SchemaChange> {
2406+
match operation {
2407+
AlterColumnOperation::SetNotNull => Ok(SchemaChange::update_column_nullability(
2408+
column_name.to_string(),
2409+
false,
2410+
)),
2411+
AlterColumnOperation::DropNotNull => Ok(SchemaChange::update_column_nullability(
2412+
column_name.to_string(),
2413+
true,
2414+
)),
2415+
AlterColumnOperation::SetDataType {
2416+
data_type, using, ..
2417+
} => {
2418+
if using.is_some() {
2419+
return Err(DataFusionError::Plan(
2420+
"ALTER COLUMN TYPE USING is not supported".to_string(),
2421+
));
2422+
}
2423+
let new_data_type = sql_data_type_to_paimon_type(data_type, true)?;
2424+
Ok(SchemaChange::UpdateColumnType {
2425+
field_names: vec![column_name.to_string()],
2426+
new_data_type,
2427+
// A type-only SQL change must not change the column's nullability.
2428+
keep_nullability: true,
2429+
})
2430+
}
2431+
other => Err(DataFusionError::Plan(format!(
2432+
"Unsupported ALTER COLUMN operation: {other}"
2433+
))),
2434+
}
2435+
}
2436+
23832437
fn column_def_to_paimon_type(col: &ColumnDef) -> DFResult<PaimonDataType> {
23842438
sql_data_type_to_paimon_type(&col.data_type, column_def_nullable(col))
23852439
}
@@ -3273,6 +3327,7 @@ mod tests {
32733327

32743328
use async_trait::async_trait;
32753329
use datafusion::arrow::array::StringViewArray;
3330+
use datafusion::sql::sqlparser::dialect::PostgreSqlDialect;
32763331
use paimon::catalog::Database;
32773332
use paimon::spec::{
32783333
DataField as PaimonDataField, DataType as PaimonDataType, IntType, Schema as PaimonSchema,
@@ -6180,6 +6235,116 @@ mod tests {
61806235
}
61816236
}
61826237

6238+
#[tokio::test]
6239+
async fn test_alter_table_update_column_type_preserves_nullability() {
6240+
let catalog = Arc::new(MockCatalog::new());
6241+
let sql_context = make_sql_context(catalog.clone()).await;
6242+
6243+
for sql in [
6244+
"ALTER TABLE mydb.t1 ALTER COLUMN value TYPE BIGINT",
6245+
"ALTER TABLE mydb.t1 ALTER COLUMN value SET DATA TYPE BIGINT",
6246+
] {
6247+
sql_context.sql(sql).await.unwrap();
6248+
6249+
let calls = catalog.take_calls();
6250+
assert_eq!(calls.len(), 1);
6251+
if let CatalogCall::AlterTable { changes, .. } = &calls[0] {
6252+
assert_eq!(changes.len(), 1);
6253+
assert!(matches!(
6254+
&changes[0],
6255+
SchemaChange::UpdateColumnType {
6256+
field_names,
6257+
new_data_type: PaimonDataType::BigInt(_),
6258+
keep_nullability: true,
6259+
} if field_names.first().map(String::as_str) == Some("value")
6260+
));
6261+
} else {
6262+
panic!("expected AlterTable call");
6263+
}
6264+
}
6265+
}
6266+
6267+
#[tokio::test]
6268+
async fn test_alter_table_update_column_nullability() {
6269+
let catalog = Arc::new(MockCatalog::new());
6270+
let sql_context = make_sql_context(catalog.clone()).await;
6271+
6272+
for (sql, expected_nullability) in [
6273+
("ALTER TABLE mydb.t1 ALTER COLUMN value SET NOT NULL", false),
6274+
("ALTER TABLE mydb.t1 ALTER COLUMN value DROP NOT NULL", true),
6275+
] {
6276+
sql_context.sql(sql).await.unwrap();
6277+
6278+
let calls = catalog.take_calls();
6279+
assert_eq!(calls.len(), 1);
6280+
if let CatalogCall::AlterTable { changes, .. } = &calls[0] {
6281+
assert_eq!(changes.len(), 1);
6282+
assert!(matches!(
6283+
&changes[0],
6284+
SchemaChange::UpdateColumnNullability {
6285+
field_names,
6286+
new_nullability,
6287+
} if field_names.first().map(String::as_str) == Some("value")
6288+
&& *new_nullability == expected_nullability
6289+
));
6290+
} else {
6291+
panic!("expected AlterTable call");
6292+
}
6293+
}
6294+
}
6295+
6296+
#[test]
6297+
fn test_alter_table_update_column_type_rejects_using() {
6298+
let statements = Parser::parse_sql(
6299+
&PostgreSqlDialect {},
6300+
"ALTER TABLE mydb.t1 ALTER COLUMN value \
6301+
TYPE BIGINT USING CAST(value AS BIGINT)",
6302+
)
6303+
.unwrap();
6304+
let Statement::AlterTable(alter_table) = &statements[0] else {
6305+
panic!("expected ALTER TABLE statement");
6306+
};
6307+
let AlterTableOperation::AlterColumn { column_name, op } = &alter_table.operations[0]
6308+
else {
6309+
panic!("expected ALTER COLUMN operation");
6310+
};
6311+
6312+
let err = alter_column_to_schema_change(&column_name.value, op).unwrap_err();
6313+
6314+
assert!(err.to_string().contains("USING is not supported"));
6315+
}
6316+
6317+
#[tokio::test]
6318+
async fn test_alter_table_update_column_default_is_unsupported() {
6319+
let catalog = Arc::new(MockCatalog::new());
6320+
let sql_context = make_sql_context(catalog.clone()).await;
6321+
6322+
let err = sql_context
6323+
.sql("ALTER TABLE mydb.t1 ALTER COLUMN value SET DEFAULT 1")
6324+
.await
6325+
.unwrap_err();
6326+
6327+
assert!(err
6328+
.to_string()
6329+
.contains("Unsupported ALTER COLUMN operation"));
6330+
assert!(catalog.take_calls().is_empty());
6331+
}
6332+
6333+
#[tokio::test]
6334+
async fn test_alter_table_rejects_mixed_special_operations_before_catalog_calls() {
6335+
let catalog = Arc::new(MockCatalog::new());
6336+
let sql_context = make_sql_context(catalog.clone()).await;
6337+
6338+
for sql in [
6339+
"ALTER TABLE mydb.t1 RENAME TO t2, ADD COLUMN age INT",
6340+
"ALTER TABLE mydb.t1 DROP PARTITION (pt = 'a'), ADD COLUMN age INT",
6341+
] {
6342+
let err = sql_context.sql(sql).await.unwrap_err();
6343+
assert!(err.to_string().contains("must be used alone"));
6344+
assert!(catalog.take_calls().is_empty());
6345+
}
6346+
}
6347+
61836348
#[tokio::test]
61846349
async fn test_alter_table_rename_table() {
61856350
let catalog = Arc::new(MockCatalog::new());

crates/integrations/datafusion/tests/sql_context_tests.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1163,6 +1163,53 @@ async fn test_alter_table_add_column() {
11631163
assert_eq!(names, vec!["id", "name", "age"]);
11641164
}
11651165

1166+
#[tokio::test]
1167+
async fn test_alter_table_update_column_type_and_nullability() {
1168+
let (_tmp, catalog) = create_test_env();
1169+
let sql_context = create_sql_context(catalog.clone()).await;
1170+
let identifier = Identifier::new("mydb", "alter_column_test");
1171+
1172+
catalog
1173+
.create_database("mydb", false, Default::default())
1174+
.await
1175+
.unwrap();
1176+
let schema = paimon::spec::Schema::builder()
1177+
.column("id", DataType::Int(IntType::new()))
1178+
.column("value", DataType::Int(IntType::new()))
1179+
.option("alter-column-null-to-not-null.disabled", "false")
1180+
.build()
1181+
.unwrap();
1182+
catalog
1183+
.create_table(&identifier, schema, false)
1184+
.await
1185+
.unwrap();
1186+
1187+
sql_context
1188+
.sql("ALTER TABLE mydb.alter_column_test ALTER COLUMN value SET NOT NULL")
1189+
.await
1190+
.expect("ALTER COLUMN SET NOT NULL should succeed");
1191+
let table = catalog.get_table(&identifier).await.unwrap();
1192+
assert!(!table.schema().fields()[1].data_type().is_nullable());
1193+
1194+
sql_context
1195+
.sql("ALTER TABLE mydb.alter_column_test ALTER COLUMN value TYPE BIGINT")
1196+
.await
1197+
.expect("ALTER COLUMN TYPE should succeed");
1198+
let table = catalog.get_table(&identifier).await.unwrap();
1199+
assert!(matches!(
1200+
table.schema().fields()[1].data_type(),
1201+
DataType::BigInt(_)
1202+
));
1203+
assert!(!table.schema().fields()[1].data_type().is_nullable());
1204+
1205+
sql_context
1206+
.sql("ALTER TABLE mydb.alter_column_test ALTER COLUMN value DROP NOT NULL")
1207+
.await
1208+
.expect("ALTER COLUMN DROP NOT NULL should succeed");
1209+
let table = catalog.get_table(&identifier).await.unwrap();
1210+
assert!(table.schema().fields()[1].data_type().is_nullable());
1211+
}
1212+
11661213
#[tokio::test]
11671214
async fn test_alter_table_rename() {
11681215
let (_tmp, catalog) = create_test_env();

0 commit comments

Comments
 (0)