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};
5557use datafusion:: prelude:: { DataFrame , SessionContext } ;
5658use datafusion:: sql:: planner:: IdentNormalizer ;
5759use 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} ;
6466use datafusion:: sql:: sqlparser:: dialect:: GenericDialect ;
6567use 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+
23832437fn 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 ( ) ) ;
0 commit comments