From 20c57547847b353a797dc830b1449a3d6b9135ad Mon Sep 17 00:00:00 2001 From: xring Date: Sun, 7 Apr 2024 20:43:23 +0800 Subject: [PATCH] Support `[FIRST | AFTER column_name]` support in `ALTER TABLE` for MySQL (#1180) --- src/ast/ddl.rs | 25 +++-- src/ast/mod.rs | 22 +++++ src/keywords.rs | 1 + src/parser/mod.rs | 22 +++++ tests/sqlparser_common.rs | 2 + tests/sqlparser_mysql.rs | 189 ++++++++++++++++++++++++++++++++++++ tests/sqlparser_postgres.rs | 2 + 7 files changed, 257 insertions(+), 6 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 9e3137d9..080e8c4d 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -25,8 +25,8 @@ use sqlparser_derive::{Visit, VisitMut}; use crate::ast::value::escape_single_quote_string; use crate::ast::{ - display_comma_separated, display_separated, DataType, Expr, Ident, ObjectName, SequenceOptions, - SqlOption, + display_comma_separated, display_separated, DataType, Expr, Ident, MySQLColumnPosition, + ObjectName, SequenceOptions, SqlOption, }; use crate::tokenizer::Token; @@ -45,6 +45,8 @@ pub enum AlterTableOperation { if_not_exists: bool, /// . column_def: ColumnDef, + /// MySQL `ALTER TABLE` only [FIRST | AFTER column_name] + column_position: Option, }, /// `DISABLE ROW LEVEL SECURITY` /// @@ -129,6 +131,8 @@ pub enum AlterTableOperation { new_name: Ident, data_type: DataType, options: Vec, + /// MySQL `ALTER TABLE` only [FIRST | AFTER column_name] + column_position: Option, }, /// `RENAME CONSTRAINT TO ` /// @@ -171,6 +175,7 @@ impl fmt::Display for AlterTableOperation { column_keyword, if_not_exists, column_def, + column_position, } => { write!(f, "ADD")?; if *column_keyword { @@ -181,6 +186,10 @@ impl fmt::Display for AlterTableOperation { } write!(f, " {column_def}")?; + if let Some(position) = column_position { + write!(f, " {position}")?; + } + Ok(()) } AlterTableOperation::AlterColumn { column_name, op } => { @@ -271,13 +280,17 @@ impl fmt::Display for AlterTableOperation { new_name, data_type, options, + column_position, } => { write!(f, "CHANGE COLUMN {old_name} {new_name} {data_type}")?; - if options.is_empty() { - Ok(()) - } else { - write!(f, " {}", display_separated(options, " ")) + if !options.is_empty() { + write!(f, " {}", display_separated(options, " "))?; } + if let Some(position) = column_position { + write!(f, " {position}")?; + } + + Ok(()) } AlterTableOperation::RenameConstraint { old_name, new_name } => { write!(f, "RENAME CONSTRAINT {old_name} TO {new_name}") diff --git a/src/ast/mod.rs b/src/ast/mod.rs index dfdc86e0..2eebbc60 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -6018,6 +6018,28 @@ impl fmt::Display for HiveSetLocation { } } +/// MySQL `ALTER TABLE` only [FIRST | AFTER column_name] +#[allow(clippy::large_enum_variant)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum MySQLColumnPosition { + First, + After(Ident), +} + +impl Display for MySQLColumnPosition { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + MySQLColumnPosition::First => Ok(write!(f, "FIRST")?), + MySQLColumnPosition::After(ident) => { + let column_name = &ident.value; + Ok(write!(f, "AFTER {column_name}")?) + } + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/keywords.rs b/src/keywords.rs index fa7d133e..91842672 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -73,6 +73,7 @@ define_keywords!( ACTION, ADD, ADMIN, + AFTER, AGAINST, ALL, ALLOCATE, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 568d89e3..57e24d21 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5358,10 +5358,14 @@ impl<'a> Parser<'a> { }; let column_def = self.parse_column_def()?; + + let column_position = self.parse_column_position()?; + AlterTableOperation::AddColumn { column_keyword, if_not_exists, column_def, + column_position, } } } @@ -5490,11 +5494,14 @@ impl<'a> Parser<'a> { options.push(option); } + let column_position = self.parse_column_position()?; + AlterTableOperation::ChangeColumn { old_name, new_name, data_type, options, + column_position, } } else if self.parse_keyword(Keyword::ALTER) { let _ = self.parse_keyword(Keyword::COLUMN); // [ COLUMN ] @@ -9608,6 +9615,21 @@ impl<'a> Parser<'a> { Ok(partitions) } + fn parse_column_position(&mut self) -> Result, ParserError> { + if dialect_of!(self is MySqlDialect | GenericDialect) { + if self.parse_keyword(Keyword::FIRST) { + Ok(Some(MySQLColumnPosition::First)) + } else if self.parse_keyword(Keyword::AFTER) { + let ident = self.parse_identifier(false)?; + Ok(Some(MySQLColumnPosition::After(ident))) + } else { + Ok(None) + } + } else { + Ok(None) + } + } + /// Consume the parser and return its underlying token buffer pub fn into_tokens(self) -> Vec { self.tokens diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index f474e116..c8551e1f 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -3512,11 +3512,13 @@ fn parse_alter_table() { column_keyword, if_not_exists, column_def, + column_position, } => { assert!(column_keyword); assert!(!if_not_exists); assert_eq!("foo", column_def.name.to_string()); assert_eq!("TEXT", column_def.data_type.to_string()); + assert_eq!(None, column_position); } _ => unreachable!(), }; diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 8ffb78ae..59314c1d 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1875,6 +1875,120 @@ fn parse_delete_with_limit() { } } +#[test] +fn parse_alter_table_add_column() { + match mysql().verified_stmt("ALTER TABLE tab ADD COLUMN b INT FIRST") { + Statement::AlterTable { + name, + if_exists, + only, + operations, + location: _, + } => { + assert_eq!(name.to_string(), "tab"); + assert!(!if_exists); + assert!(!only); + assert_eq!( + operations, + vec![AlterTableOperation::AddColumn { + column_keyword: true, + if_not_exists: false, + column_def: ColumnDef { + name: "b".into(), + data_type: DataType::Int(None), + collation: None, + options: vec![], + }, + column_position: Some(MySQLColumnPosition::First), + },] + ); + } + _ => unreachable!(), + } + + match mysql().verified_stmt("ALTER TABLE tab ADD COLUMN b INT AFTER foo") { + Statement::AlterTable { + name, + if_exists, + only, + operations, + location: _, + } => { + assert_eq!(name.to_string(), "tab"); + assert!(!if_exists); + assert!(!only); + assert_eq!( + operations, + vec![AlterTableOperation::AddColumn { + column_keyword: true, + if_not_exists: false, + column_def: ColumnDef { + name: "b".into(), + data_type: DataType::Int(None), + collation: None, + options: vec![], + }, + column_position: Some(MySQLColumnPosition::After(Ident { + value: String::from("foo"), + quote_style: None + })), + },] + ); + } + _ => unreachable!(), + } +} + +#[test] +fn parse_alter_table_add_columns() { + match mysql() + .verified_stmt("ALTER TABLE tab ADD COLUMN a TEXT FIRST, ADD COLUMN b INT AFTER foo") + { + Statement::AlterTable { + name, + if_exists, + only, + operations, + location: _, + } => { + assert_eq!(name.to_string(), "tab"); + assert!(!if_exists); + assert!(!only); + assert_eq!( + operations, + vec![ + AlterTableOperation::AddColumn { + column_keyword: true, + if_not_exists: false, + column_def: ColumnDef { + name: "a".into(), + data_type: DataType::Text, + collation: None, + options: vec![], + }, + column_position: Some(MySQLColumnPosition::First), + }, + AlterTableOperation::AddColumn { + column_keyword: true, + if_not_exists: false, + column_def: ColumnDef { + name: "b".into(), + data_type: DataType::Int(None), + collation: None, + options: vec![], + }, + column_position: Some(MySQLColumnPosition::After(Ident { + value: String::from("foo"), + quote_style: None, + })), + }, + ] + ); + } + _ => unreachable!(), + } +} + #[test] fn parse_alter_table_drop_primary_key() { assert_matches!( @@ -1891,6 +2005,7 @@ fn parse_alter_table_change_column() { new_name: Ident::new("desc"), data_type: DataType::Text, options: vec![ColumnOption::NotNull], + column_position: None, }; let sql1 = "ALTER TABLE orders CHANGE COLUMN description desc TEXT NOT NULL"; @@ -1904,6 +2019,80 @@ fn parse_alter_table_change_column() { &expected_name.to_string(), ); assert_eq!(expected_operation, operation); + + let expected_operation = AlterTableOperation::ChangeColumn { + old_name: Ident::new("description"), + new_name: Ident::new("desc"), + data_type: DataType::Text, + options: vec![ColumnOption::NotNull], + column_position: Some(MySQLColumnPosition::First), + }; + let sql3 = "ALTER TABLE orders CHANGE COLUMN description desc TEXT NOT NULL FIRST"; + let operation = + alter_table_op_with_name(mysql().verified_stmt(sql3), &expected_name.to_string()); + assert_eq!(expected_operation, operation); + + let expected_operation = AlterTableOperation::ChangeColumn { + old_name: Ident::new("description"), + new_name: Ident::new("desc"), + data_type: DataType::Text, + options: vec![ColumnOption::NotNull], + column_position: Some(MySQLColumnPosition::After(Ident { + value: String::from("foo"), + quote_style: None, + })), + }; + let sql4 = "ALTER TABLE orders CHANGE COLUMN description desc TEXT NOT NULL AFTER foo"; + let operation = + alter_table_op_with_name(mysql().verified_stmt(sql4), &expected_name.to_string()); + assert_eq!(expected_operation, operation); +} + +#[test] +fn parse_alter_table_change_column_with_column_position() { + let expected_name = ObjectName(vec![Ident::new("orders")]); + let expected_operation_first = AlterTableOperation::ChangeColumn { + old_name: Ident::new("description"), + new_name: Ident::new("desc"), + data_type: DataType::Text, + options: vec![ColumnOption::NotNull], + column_position: Some(MySQLColumnPosition::First), + }; + + let sql1 = "ALTER TABLE orders CHANGE COLUMN description desc TEXT NOT NULL FIRST"; + let operation = + alter_table_op_with_name(mysql().verified_stmt(sql1), &expected_name.to_string()); + assert_eq!(expected_operation_first, operation); + + let sql2 = "ALTER TABLE orders CHANGE description desc TEXT NOT NULL FIRST"; + let operation = alter_table_op_with_name( + mysql().one_statement_parses_to(sql2, sql1), + &expected_name.to_string(), + ); + assert_eq!(expected_operation_first, operation); + + let expected_operation_after = AlterTableOperation::ChangeColumn { + old_name: Ident::new("description"), + new_name: Ident::new("desc"), + data_type: DataType::Text, + options: vec![ColumnOption::NotNull], + column_position: Some(MySQLColumnPosition::After(Ident { + value: String::from("total_count"), + quote_style: None, + })), + }; + + let sql1 = "ALTER TABLE orders CHANGE COLUMN description desc TEXT NOT NULL AFTER total_count"; + let operation = + alter_table_op_with_name(mysql().verified_stmt(sql1), &expected_name.to_string()); + assert_eq!(expected_operation_after, operation); + + let sql2 = "ALTER TABLE orders CHANGE description desc TEXT NOT NULL AFTER total_count"; + let operation = alter_table_op_with_name( + mysql().one_statement_parses_to(sql2, sql1), + &expected_name.to_string(), + ); + assert_eq!(expected_operation_after, operation); } #[test] diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 3747aef7..ea5c9875 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -694,6 +694,7 @@ fn parse_alter_table_add_columns() { collation: None, options: vec![], }, + column_position: None, }, AlterTableOperation::AddColumn { column_keyword: true, @@ -704,6 +705,7 @@ fn parse_alter_table_add_columns() { collation: None, options: vec![], }, + column_position: None, }, ] );