diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index d48400b1..78ec2e0c 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -67,6 +67,15 @@ pub enum AlterTableOperation { data_type: DataType, options: Vec, }, + /// `RENAME CONSTRAINT TO ` + /// + /// Note: this is a PostgreSQL-specific operation. + RenameConstraint { old_name: Ident, new_name: Ident }, + /// `ALTER [ COLUMN ]` + AlterColumn { + column_name: Ident, + op: AlterColumnOperation, + }, } impl fmt::Display for AlterTableOperation { @@ -85,6 +94,9 @@ impl fmt::Display for AlterTableOperation { AlterTableOperation::AddColumn { column_def } => { write!(f, "ADD COLUMN {}", column_def.to_string()) } + AlterTableOperation::AlterColumn { column_name, op } => { + write!(f, "ALTER COLUMN {} {}", column_name, op) + } AlterTableOperation::DropPartitions { partitions, if_exists, @@ -139,6 +151,51 @@ impl fmt::Display for AlterTableOperation { write!(f, " {}", display_separated(options, " ")) } } + AlterTableOperation::RenameConstraint { old_name, new_name } => { + write!(f, "RENAME CONSTRAINT {} TO {}", old_name, new_name) + } + } + } +} + +/// An `ALTER COLUMN` (`Statement::AlterTable`) operation +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum AlterColumnOperation { + /// `SET NOT NULL` + SetNotNull, + /// `DROP NOT NULL` + DropNotNull, + /// `SET DEFAULT ` + SetDefault { value: Expr }, + /// `DROP DEFAULT` + DropDefault, + /// `[SET DATA] TYPE [USING ]` + SetDataType { + data_type: DataType, + /// PostgreSQL specific + using: Option, + }, +} + +impl fmt::Display for AlterColumnOperation { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + AlterColumnOperation::SetNotNull => write!(f, "SET NOT NULL",), + AlterColumnOperation::DropNotNull => write!(f, "DROP NOT NULL",), + AlterColumnOperation::SetDefault { value } => { + write!(f, "SET DEFAULT {}", value) + } + AlterColumnOperation::DropDefault {} => { + write!(f, "DROP DEFAULT") + } + AlterColumnOperation::SetDataType { data_type, using } => { + if let Some(expr) = using { + write!(f, "SET DATA TYPE {} USING {}", data_type, expr) + } else { + write!(f, "SET DATA TYPE {}", data_type) + } + } } } } diff --git a/src/ast/mod.rs b/src/ast/mod.rs index e7b8e7a0..a1a4453b 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -31,8 +31,8 @@ use serde::{Deserialize, Serialize}; pub use self::data_type::DataType; pub use self::ddl::{ - AlterTableOperation, ColumnDef, ColumnOption, ColumnOptionDef, ReferentialAction, - TableConstraint, + AlterColumnOperation, AlterTableOperation, ColumnDef, ColumnOption, ColumnOptionDef, + ReferentialAction, TableConstraint, }; pub use self::operator::{BinaryOperator, UnaryOperator}; pub use self::query::{ @@ -592,6 +592,22 @@ impl fmt::Display for ShowCreateObject { } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum CommentObject { + Column, + Table, +} + +impl fmt::Display for CommentObject { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + CommentObject::Column => f.write_str("COLUMN"), + CommentObject::Table => f.write_str("TABLE"), + } + } +} + /// A top-level statement (SELECT, INSERT, CREATE, etc.) #[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -779,6 +795,14 @@ pub enum Statement { snapshot: Option, session: bool, }, + /// `COMMENT ON ...` + /// + /// Note: this is a PostgreSQL-specific statement. + Comment { + object_type: CommentObject, + object_name: ObjectName, + comment: Option, + }, /// `COMMIT [ TRANSACTION | WORK ] [ AND [ NO ] CHAIN ]` Commit { chain: bool }, /// `ROLLBACK [ TRANSACTION | WORK ] [ AND [ NO ] CHAIN ]` @@ -1459,6 +1483,18 @@ impl fmt::Display for Statement { } write!(f, "AS {}", statement) } + Statement::Comment { + object_type, + object_name, + comment, + } => { + write!(f, "COMMENT ON {} {} IS ", object_type, object_name)?; + if let Some(c) = comment { + write!(f, "'{}'", c) + } else { + write!(f, "NULL") + } + } } } } diff --git a/src/keywords.rs b/src/keywords.rs index 0695395c..56f77cc2 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -128,6 +128,7 @@ define_keywords!( COLLECT, COLUMN, COLUMNS, + COMMENT, COMMIT, COMMITTED, COMPUTE, @@ -161,6 +162,7 @@ define_keywords!( CURRENT_USER, CURSOR, CYCLE, + DATA, DATABASE, DATE, DAY, @@ -469,6 +471,7 @@ define_keywords!( TRUE, TRUNCATE, TRY_CAST, + TYPE, UESCAPE, UNBOUNDED, UNCOMMITTED, diff --git a/src/parser.rs b/src/parser.rs index b01c6090..a90db514 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -191,6 +191,9 @@ impl<'a> Parser<'a> { self.prev_token(); Ok(self.parse_insert()?) } + Keyword::COMMENT if dialect_of!(self is PostgreSqlDialect) => { + Ok(self.parse_comment()?) + } _ => self.expected("an SQL statement", Token::Word(w)), }, Token::LParen => { @@ -1944,7 +1947,12 @@ impl<'a> Parser<'a> { } } } else if self.parse_keyword(Keyword::RENAME) { - if self.parse_keyword(Keyword::TO) { + if dialect_of!(self is PostgreSqlDialect) && self.parse_keyword(Keyword::CONSTRAINT) { + let old_name = self.parse_identifier()?; + self.expect_keyword(Keyword::TO)?; + let new_name = self.parse_identifier()?; + AlterTableOperation::RenameConstraint { old_name, new_name } + } else if self.parse_keyword(Keyword::TO) { let table_name = self.parse_object_name()?; AlterTableOperation::RenameTable { table_name } } else { @@ -2014,6 +2022,38 @@ impl<'a> Parser<'a> { data_type, options, } + } else if self.parse_keyword(Keyword::ALTER) { + let _ = self.parse_keyword(Keyword::COLUMN); + let column_name = self.parse_identifier()?; + let is_postgresql = dialect_of!(self is PostgreSqlDialect); + + let op = if self.parse_keywords(&[Keyword::SET, Keyword::NOT, Keyword::NULL]) { + AlterColumnOperation::SetNotNull {} + } else if self.parse_keywords(&[Keyword::DROP, Keyword::NOT, Keyword::NULL]) { + AlterColumnOperation::DropNotNull {} + } else if self.parse_keywords(&[Keyword::SET, Keyword::DEFAULT]) { + AlterColumnOperation::SetDefault { + value: self.parse_expr()?, + } + } else if self.parse_keywords(&[Keyword::DROP, Keyword::DEFAULT]) { + AlterColumnOperation::DropDefault {} + } else if self.parse_keywords(&[Keyword::SET, Keyword::DATA, Keyword::TYPE]) + || (is_postgresql && self.parse_keyword(Keyword::TYPE)) + { + let data_type = self.parse_data_type()?; + let using = if is_postgresql && self.parse_keyword(Keyword::USING) { + Some(self.parse_expr()?) + } else { + None + }; + AlterColumnOperation::SetDataType { data_type, using } + } else { + return self.expected( + "SET/DROP NOT NULL, SET DEFAULT, SET DATA TYPE after ALTER COLUMN", + self.peek_token(), + ); + }; + AlterTableOperation::AlterColumn { column_name, op } } else { return self.expected( "ADD, RENAME, PARTITION or DROP after ALTER TABLE", @@ -3547,6 +3587,35 @@ impl<'a> Parser<'a> { statement, }) } + + fn parse_comment(&mut self) -> Result { + self.expect_keyword(Keyword::ON)?; + let token = self.next_token(); + + let (object_type, object_name) = match token { + Token::Word(w) if w.keyword == Keyword::COLUMN => { + let object_name = self.parse_object_name()?; + (CommentObject::Column, object_name) + } + Token::Word(w) if w.keyword == Keyword::TABLE => { + let object_name = self.parse_object_name()?; + (CommentObject::Table, object_name) + } + _ => self.expected("comment object_type", token)?, + }; + + self.expect_keyword(Keyword::IS)?; + let comment = if self.parse_keyword(Keyword::NULL) { + None + } else { + Some(self.parse_literal_string()?) + }; + Ok(Statement::Comment { + object_type, + object_name, + comment, + }) + } } impl Word { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 4cbcd8e0..07a0db52 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1950,6 +1950,108 @@ fn parse_alter_table_drop_column() { } } +#[test] +fn parse_alter_table_alter_column() { + let alter_stmt = "ALTER TABLE tab"; + match verified_stmt(&format!( + "{} ALTER COLUMN is_active SET NOT NULL", + alter_stmt + )) { + Statement::AlterTable { + name, + operation: AlterTableOperation::AlterColumn { column_name, op }, + } => { + assert_eq!("tab", name.to_string()); + assert_eq!("is_active", column_name.to_string()); + assert_eq!(op, AlterColumnOperation::SetNotNull {}); + } + _ => unreachable!(), + } + + one_statement_parses_to( + "ALTER TABLE tab ALTER is_active DROP NOT NULL", + "ALTER TABLE tab ALTER COLUMN is_active DROP NOT NULL", + ); + + match verified_stmt(&format!( + "{} ALTER COLUMN is_active SET DEFAULT false", + alter_stmt + )) { + Statement::AlterTable { + name, + operation: AlterTableOperation::AlterColumn { column_name, op }, + } => { + assert_eq!("tab", name.to_string()); + assert_eq!("is_active", column_name.to_string()); + assert_eq!( + op, + AlterColumnOperation::SetDefault { + value: Expr::Value(Value::Boolean(false)) + } + ); + } + _ => unreachable!(), + } + + match verified_stmt(&format!( + "{} ALTER COLUMN is_active DROP DEFAULT", + alter_stmt + )) { + Statement::AlterTable { + name, + operation: AlterTableOperation::AlterColumn { column_name, op }, + } => { + assert_eq!("tab", name.to_string()); + assert_eq!("is_active", column_name.to_string()); + assert_eq!(op, AlterColumnOperation::DropDefault {}); + } + _ => unreachable!(), + } +} + +#[test] +fn parse_alter_table_alter_column_type() { + let alter_stmt = "ALTER TABLE tab"; + match verified_stmt("ALTER TABLE tab ALTER COLUMN is_active SET DATA TYPE TEXT") { + Statement::AlterTable { + name, + operation: AlterTableOperation::AlterColumn { column_name, op }, + } => { + assert_eq!("tab", name.to_string()); + assert_eq!("is_active", column_name.to_string()); + assert_eq!( + op, + AlterColumnOperation::SetDataType { + data_type: DataType::Text, + using: None, + } + ); + } + _ => unreachable!(), + } + + let res = Parser::parse_sql( + &GenericDialect {}, + &format!("{} ALTER COLUMN is_active TYPE TEXT", alter_stmt), + ); + assert_eq!( + ParserError::ParserError("Expected SET/DROP NOT NULL, SET DEFAULT, SET DATA TYPE after ALTER COLUMN, found: TYPE".to_string()), + res.unwrap_err() + ); + + let res = Parser::parse_sql( + &GenericDialect {}, + &format!( + "{} ALTER COLUMN is_active SET DATA TYPE TEXT USING 'text'", + alter_stmt + ), + ); + assert_eq!( + ParserError::ParserError("Expected end of statement, found: USING".to_string()), + res.unwrap_err() + ); +} + #[test] fn parse_bad_constraint() { let res = parse_sql_statements("ALTER TABLE tab ADD"); diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 941f9c5d..bff21da2 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -263,6 +263,50 @@ fn parse_create_table_constraints_only() { }; } +#[test] +fn parse_alter_table_constraints_rename() { + match pg().verified_stmt("ALTER TABLE tab RENAME CONSTRAINT old_name TO new_name") { + Statement::AlterTable { + name, + operation: AlterTableOperation::RenameConstraint { old_name, new_name }, + } => { + assert_eq!("tab", name.to_string()); + assert_eq!(old_name.to_string(), "old_name"); + assert_eq!(new_name.to_string(), "new_name"); + } + _ => unreachable!(), + } +} + +#[test] +fn parse_alter_table_alter_column() { + pg().one_statement_parses_to( + "ALTER TABLE tab ALTER COLUMN is_active TYPE TEXT USING 'text'", + "ALTER TABLE tab ALTER COLUMN is_active SET DATA TYPE TEXT USING 'text'", + ); + + match pg() + .verified_stmt("ALTER TABLE tab ALTER COLUMN is_active SET DATA TYPE TEXT USING 'text'") + { + Statement::AlterTable { + name, + operation: AlterTableOperation::AlterColumn { column_name, op }, + } => { + assert_eq!("tab", name.to_string()); + assert_eq!("is_active", column_name.to_string()); + let using_expr = Expr::Value(Value::SingleQuotedString("text".to_string())); + assert_eq!( + op, + AlterColumnOperation::SetDataType { + data_type: DataType::Text, + using: Some(using_expr), + } + ); + } + _ => unreachable!(), + } +} + #[test] fn parse_create_table_if_not_exists() { let sql = "CREATE TABLE IF NOT EXISTS uk_cities ()"; @@ -749,6 +793,47 @@ fn test_transaction_statement() { ); } +#[test] +fn parse_comments() { + match pg().verified_stmt("COMMENT ON COLUMN tab.name IS 'comment'") { + Statement::Comment { + object_type, + object_name, + comment: Some(comment), + } => { + assert_eq!("comment", comment); + assert_eq!("tab.name", object_name.to_string()); + assert_eq!(CommentObject::Column, object_type); + } + _ => unreachable!(), + } + + match pg().verified_stmt("COMMENT ON TABLE public.tab IS 'comment'") { + Statement::Comment { + object_type, + object_name, + comment: Some(comment), + } => { + assert_eq!("comment", comment); + assert_eq!("public.tab", object_name.to_string()); + assert_eq!(CommentObject::Table, object_type); + } + _ => unreachable!(), + } + + match pg().verified_stmt("COMMENT ON TABLE public.tab IS NULL") { + Statement::Comment { + object_type, + object_name, + comment: None, + } => { + assert_eq!("public.tab", object_name.to_string()); + assert_eq!(CommentObject::Table, object_type); + } + _ => unreachable!(), + } +} + fn pg() -> TestedDialects { TestedDialects { dialects: vec![Box::new(PostgreSqlDialect {})],