feat: comment and alter column support (#381)

* feat: add support for postgresql comment keyword

* feat: add alter column and rename constraint
This commit is contained in:
Zach Hamlin 2021-12-17 11:19:08 -06:00 committed by GitHub
parent 823635d2fc
commit 9569d1b215
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 355 additions and 3 deletions

View file

@ -67,6 +67,15 @@ pub enum AlterTableOperation {
data_type: DataType,
options: Vec<ColumnOption>,
},
/// `RENAME CONSTRAINT <old_constraint_name> TO <new_constraint_name>`
///
/// 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 <expr>`
SetDefault { value: Expr },
/// `DROP DEFAULT`
DropDefault,
/// `[SET DATA] TYPE <data_type> [USING <expr>]`
SetDataType {
data_type: DataType,
/// PostgreSQL specific
using: Option<Expr>,
},
}
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)
}
}
}
}
}

View file

@ -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<Value>,
session: bool,
},
/// `COMMENT ON ...`
///
/// Note: this is a PostgreSQL-specific statement.
Comment {
object_type: CommentObject,
object_name: ObjectName,
comment: Option<String>,
},
/// `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")
}
}
}
}
}

View file

@ -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,

View file

@ -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<Statement, ParserError> {
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 {

View file

@ -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");

View file

@ -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 {})],