diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index f99cbe2ea..57a4a5d7a 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1494,6 +1494,12 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if the dialect supports multiple column definitions after + /// a single `ALTER TABLE ... ADD` clause. + fn supports_alter_table_add_multiple_columns(&self) -> bool { + false + } + /// Returns true if the dialect considers the specified ident as a function /// that returns an identifier. Typically used to generate identifiers /// programmatically. diff --git a/src/dialect/mssql.rs b/src/dialect/mssql.rs index 980b63d28..fb813bc32 100644 --- a/src/dialect/mssql.rs +++ b/src/dialect/mssql.rs @@ -88,6 +88,10 @@ impl Dialect for MsSqlDialect { false } + fn supports_alter_table_add_multiple_columns(&self) -> bool { + true + } + fn supports_named_fn_args_with_colon_operator(&self) -> bool { true } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 272de79d8..0538df334 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -10654,55 +10654,7 @@ impl<'a> Parser<'a> { /// Parse a single `ALTER TABLE` operation and return an `AlterTableOperation`. pub fn parse_alter_table_operation(&mut self) -> Result { let operation = if self.parse_keyword(Keyword::ADD) { - if let Some(constraint) = self.parse_optional_table_constraint()? { - let not_valid = self.parse_keywords(&[Keyword::NOT, Keyword::VALID]); - AlterTableOperation::AddConstraint { - constraint, - not_valid, - } - } else if dialect_of!(self is ClickHouseDialect|GenericDialect) - && self.parse_keyword(Keyword::PROJECTION) - { - return self.parse_alter_table_add_projection(); - } else { - let if_not_exists = - self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); - let mut new_partitions = vec![]; - loop { - if self.parse_keyword(Keyword::PARTITION) { - new_partitions.push(self.parse_partition()?); - } else { - break; - } - } - if !new_partitions.is_empty() { - AlterTableOperation::AddPartitions { - if_not_exists, - new_partitions, - } - } else { - let column_keyword = self.parse_keyword(Keyword::COLUMN); - - let if_not_exists = if dialect_of!(self is PostgreSqlDialect | BigQueryDialect | DuckDbDialect | GenericDialect) - { - self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]) - || if_not_exists - } else { - false - }; - - 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, - } - } - } + self.parse_alter_table_add_operation()? } else if self.parse_keyword(Keyword::RENAME) { if dialect_of!(self is PostgreSqlDialect) && self.parse_keyword(Keyword::CONSTRAINT) { let old_name = self.parse_identifier()?; @@ -11133,6 +11085,109 @@ impl<'a> Parser<'a> { Ok(operation) } + fn parse_alter_table_add_operation(&mut self) -> Result { + if let Some(constraint) = self.parse_optional_table_constraint()? { + let not_valid = self.parse_keywords(&[Keyword::NOT, Keyword::VALID]); + return Ok(AlterTableOperation::AddConstraint { + constraint, + not_valid, + }); + } + + if dialect_of!(self is ClickHouseDialect | GenericDialect) + && self.parse_keyword(Keyword::PROJECTION) + { + return self.parse_alter_table_add_projection(); + } + + let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); + let mut new_partitions = vec![]; + while self.parse_keyword(Keyword::PARTITION) { + new_partitions.push(self.parse_partition()?); + } + if !new_partitions.is_empty() { + return Ok(AlterTableOperation::AddPartitions { + if_not_exists, + new_partitions, + }); + } + + self.parse_alter_table_add_column(if_not_exists) + } + + fn parse_alter_table_add_column( + &mut self, + if_not_exists: bool, + ) -> Result { + let column_keyword = self.parse_keyword(Keyword::COLUMN); + let if_not_exists = if dialect_of!(self is PostgreSqlDialect | BigQueryDialect | DuckDbDialect | GenericDialect) + { + self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]) || if_not_exists + } else { + false + }; + let column_def = self.parse_column_def()?; + let column_position = self.parse_column_position()?; + + Ok(AlterTableOperation::AddColumn { + column_keyword, + if_not_exists, + column_def, + column_position, + }) + } + + fn parse_alter_table_add_item(&mut self) -> Result { + if let Some(constraint) = self.parse_optional_table_constraint()? { + let not_valid = self.parse_keywords(&[Keyword::NOT, Keyword::VALID]); + return Ok(AlterTableOperation::AddConstraint { + constraint, + not_valid, + }); + } + + self.parse_alter_table_add_column(false) + } + + fn can_parse_implicit_alter_table_add_item(&self, operations: &[AlterTableOperation]) -> bool { + self.dialect.supports_alter_table_add_multiple_columns() + && matches!( + operations.last(), + Some( + AlterTableOperation::AddColumn { .. } + | AlterTableOperation::AddConstraint { .. } + ) + ) + && !self.is_alter_table_operation_starter() + } + + fn is_alter_table_operation_starter(&self) -> bool { + if self + .peek_one_of_keywords(&[ + Keyword::ADD, + Keyword::ALTER, + Keyword::DISABLE, + Keyword::DROP, + Keyword::ENABLE, + Keyword::PARTITION, + Keyword::RENAME, + Keyword::REPLICA, + Keyword::SET, + Keyword::SWAP, + ]) + .is_some() + { + return true; + } + + matches!( + &self.peek_token_ref().token, + Token::Word(word) + if word.quote_style.is_none() + && matches!(word.value.to_ascii_uppercase().as_str(), "NOCHECK" | "SWITCH") + ) + } + fn parse_set_data_type(&mut self, had_set: bool) -> Result { let data_type = self.parse_data_type()?; let using = if self.dialect.supports_alter_column_type_using() @@ -11467,7 +11522,18 @@ impl<'a> Parser<'a> { let only = self.parse_keyword(Keyword::ONLY); // [ ONLY ] let table_name = self.parse_object_name(false)?; let on_cluster = self.parse_optional_on_cluster()?; - let operations = self.parse_comma_separated(Parser::parse_alter_table_operation)?; + let mut operations = vec![]; + loop { + let operation = if self.can_parse_implicit_alter_table_add_item(&operations) { + self.parse_alter_table_add_item()? + } else { + self.parse_alter_table_operation()? + }; + operations.push(operation); + if self.is_parse_comma_separated_end() { + break; + } + } let mut location = None; if self.parse_keyword(Keyword::LOCATION) { diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 3faf56f0d..41d601951 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -31,7 +31,7 @@ use sqlparser::ast::DataType::{Int, Text, Varbinary}; use sqlparser::ast::DeclareAssignment::MsSqlAssignment; use sqlparser::ast::Value::SingleQuotedString; use sqlparser::ast::*; -use sqlparser::dialect::{GenericDialect, MsSqlDialect}; +use sqlparser::dialect::{GenericDialect, MsSqlDialect, PostgreSqlDialect}; use sqlparser::parser::{Parser, ParserError, ParserOptions}; #[test] @@ -2925,3 +2925,30 @@ fn parse_mssql_money_constants() { expr_from_projection(only(&select.projection)), ); } + +#[test] +fn parse_mssql_alter_table_add_multiple_columns() { + ms().one_statement_parses_to( + "ALTER TABLE dbo.demo ADD first_flag BIT NULL, second_flag BIT NULL", + "ALTER TABLE dbo.demo ADD first_flag BIT NULL, ADD second_flag BIT NULL", + ); + ms().one_statement_parses_to( + "ALTER TABLE [dbo].[demo] ADD amount DECIMAL(10, 2) DEFAULT (0), [display_name] NVARCHAR(50) NULL", + "ALTER TABLE [dbo].[demo] ADD amount DECIMAL(10,2) DEFAULT (0), ADD [display_name] NVARCHAR(50) NULL", + ); + ms().one_statement_parses_to( + "ALTER TABLE dbo.demo ADD enabled BIT NULL, CHECK (enabled IN (0, 1))", + "ALTER TABLE dbo.demo ADD enabled BIT NULL, ADD CHECK (enabled IN (0, 1))", + ); + ms().verified_stmt("ALTER TABLE dbo.demo ADD first_flag BIT NULL, ADD second_flag BIT NULL"); + assert!(Parser::parse_sql( + &MsSqlDialect {}, + "ALTER TABLE dbo.demo ADD first_flag BIT NULL second_flag BIT NULL", + ) + .is_err()); + assert!(Parser::parse_sql( + &PostgreSqlDialect {}, + "ALTER TABLE demo ADD first_flag BOOLEAN, second_flag BOOLEAN", + ) + .is_err()); +}