From c82397000cbcb0746a9513fff14e894866e3ba68 Mon Sep 17 00:00:00 2001 From: xring Date: Fri, 15 Mar 2024 08:48:00 +0800 Subject: [PATCH 1/2] feat: add [FIRST | AFTER column_name] support in MySQL alter table --- src/ast/ddl.rs | 23 ++++- src/ast/mod.rs | 26 +++++ src/keywords.rs | 1 + src/parser/mod.rs | 22 +++++ tests/sqlparser_common.rs | 2 + tests/sqlparser_mysql.rs | 191 ++++++++++++++++++++++++++++++++++++ tests/sqlparser_postgres.rs | 2 + 7 files changed, 262 insertions(+), 5 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 9e3137d944..9da831a03e 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -26,7 +26,7 @@ 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, + SqlOption, MySQLColumnPosition }; 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 3e8354e154..538505e14e 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -5924,6 +5924,32 @@ 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), +visit(with = "visit_statement") +)] +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 c94a6227c5..872cf50b5a 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 a7190563fd..685a32f923 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5275,10 +5275,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, } } } @@ -5407,11 +5411,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 ] @@ -9469,6 +9476,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 62d5f29623..cb10b3bbbe 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -3511,11 +3511,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 8ffb78ae21..91339c916b 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_eq!(if_exists, false); + assert_eq!(only, false); + 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_eq!(if_exists, false); + assert_eq!(only, false); + 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_eq!(if_exists, false); + assert_eq!(only, false); + 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,82 @@ 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 9de4b981ff..c9b4421113 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, }, ] ); From 116c41f566a4119f9af2fadd9b97ad09288f82a1 Mon Sep 17 00:00:00 2001 From: xring Date: Sat, 16 Mar 2024 23:40:49 +0800 Subject: [PATCH 2/2] fix pull workflow issue --- src/ast/ddl.rs | 4 +- src/ast/mod.rs | 8 +--- tests/sqlparser_mysql.rs | 90 ++++++++++++++++++++-------------------- 3 files changed, 48 insertions(+), 54 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 9da831a03e..080e8c4dab 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, MySQLColumnPosition + display_comma_separated, display_separated, DataType, Expr, Ident, MySQLColumnPosition, + ObjectName, SequenceOptions, SqlOption, }; use crate::tokenizer::Token; diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 538505e14e..d2b61cb796 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -5928,14 +5928,10 @@ impl fmt::Display for HiveSetLocation { #[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), -visit(with = "visit_statement") -)] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum MySQLColumnPosition { First, - After(Ident) + After(Ident), } impl Display for MySQLColumnPosition { diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 91339c916b..59314c1d9e 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1886,23 +1886,21 @@ fn parse_alter_table_add_column() { location: _, } => { assert_eq!(name.to_string(), "tab"); - assert_eq!(if_exists, false); - assert_eq!(only, false); + 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), + 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!(), @@ -1917,25 +1915,24 @@ fn parse_alter_table_add_column() { location: _, } => { assert_eq!(name.to_string(), "tab"); - assert_eq!(if_exists, false); - assert_eq!(only, false); + 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} - )), + 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!(), @@ -1944,7 +1941,9 @@ fn parse_alter_table_add_column() { #[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") { + match mysql() + .verified_stmt("ALTER TABLE tab ADD COLUMN a TEXT FIRST, ADD COLUMN b INT AFTER foo") + { Statement::AlterTable { name, if_exists, @@ -1953,8 +1952,8 @@ fn parse_alter_table_add_columns() { location: _, } => { assert_eq!(name.to_string(), "tab"); - assert_eq!(if_exists, false); - assert_eq!(only, false); + assert!(!if_exists); + assert!(!only); assert_eq!( operations, vec![ @@ -1978,9 +1977,10 @@ fn parse_alter_table_add_columns() { collation: None, options: vec![], }, - column_position: Some(MySQLColumnPosition::After( - Ident {value: String::from("foo"), quote_style :None} - )), + column_position: Some(MySQLColumnPosition::After(Ident { + value: String::from("foo"), + quote_style: None, + })), }, ] ); @@ -2037,16 +2037,15 @@ fn parse_alter_table_change_column() { 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} - )), + 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] @@ -2077,9 +2076,10 @@ fn parse_alter_table_change_column_with_column_position() { 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} - )), + 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"; @@ -2093,8 +2093,6 @@ fn parse_alter_table_change_column_with_column_position() { &expected_name.to_string(), ); assert_eq!(expected_operation_after, operation); - - } #[test]