Skip to content

Commit

Permalink
Add support of COMMENT ON syntax for Snowflake (#1516)
Browse files Browse the repository at this point in the history
  • Loading branch information
git-hulk authored Nov 13, 2024
1 parent 76322ba commit 6d907d3
Show file tree
Hide file tree
Showing 8 changed files with 164 additions and 102 deletions.
8 changes: 8 additions & 0 deletions src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1884,6 +1884,10 @@ pub enum CommentObject {
Column,
Table,
Extension,
Schema,
Database,
User,
Role,
}

impl fmt::Display for CommentObject {
Expand All @@ -1892,6 +1896,10 @@ impl fmt::Display for CommentObject {
CommentObject::Column => f.write_str("COLUMN"),
CommentObject::Table => f.write_str("TABLE"),
CommentObject::Extension => f.write_str("EXTENSION"),
CommentObject::Schema => f.write_str("SCHEMA"),
CommentObject::Database => f.write_str("DATABASE"),
CommentObject::User => f.write_str("USER"),
CommentObject::Role => f.write_str("ROLE"),
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/dialect/generic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,8 @@ impl Dialect for GenericDialect {
fn supports_try_convert(&self) -> bool {
true
}

fn supports_comment_on(&self) -> bool {
true
}
}
7 changes: 6 additions & 1 deletion src/dialect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -611,7 +611,7 @@ pub trait Dialect: Debug + Any {
false
}

/// Returns true if this dialect expects the the `TOP` option
/// Returns true if this dialect expects the `TOP` option
/// before the `ALL`/`DISTINCT` options in a `SELECT` statement.
fn supports_top_before_distinct(&self) -> bool {
false
Expand All @@ -628,6 +628,11 @@ pub trait Dialect: Debug + Any {
fn supports_show_like_before_in(&self) -> bool {
false
}

/// Returns true if this dialect supports the `COMMENT` statement
fn supports_comment_on(&self) -> bool {
false
}
}

/// This represents the operators for which precedence must be defined
Expand Down
45 changes: 6 additions & 39 deletions src/dialect/postgresql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
// limitations under the License.
use log::debug;

use crate::ast::{CommentObject, ObjectName, Statement, UserDefinedTypeRepresentation};
use crate::ast::{ObjectName, Statement, UserDefinedTypeRepresentation};
use crate::dialect::{Dialect, Precedence};
use crate::keywords::Keyword;
use crate::parser::{Parser, ParserError};
Expand Down Expand Up @@ -136,9 +136,7 @@ impl Dialect for PostgreSqlDialect {
}

fn parse_statement(&self, parser: &mut Parser) -> Option<Result<Statement, ParserError>> {
if parser.parse_keyword(Keyword::COMMENT) {
Some(parse_comment(parser))
} else if parser.parse_keyword(Keyword::CREATE) {
if parser.parse_keyword(Keyword::CREATE) {
parser.prev_token(); // unconsume the CREATE in case we don't end up parsing anything
parse_create(parser)
} else {
Expand Down Expand Up @@ -206,42 +204,11 @@ impl Dialect for PostgreSqlDialect {
fn supports_factorial_operator(&self) -> bool {
true
}
}

pub fn parse_comment(parser: &mut Parser) -> Result<Statement, ParserError> {
let if_exists = parser.parse_keywords(&[Keyword::IF, Keyword::EXISTS]);

parser.expect_keyword(Keyword::ON)?;
let token = parser.next_token();

let (object_type, object_name) = match token.token {
Token::Word(w) if w.keyword == Keyword::COLUMN => {
let object_name = parser.parse_object_name(false)?;
(CommentObject::Column, object_name)
}
Token::Word(w) if w.keyword == Keyword::TABLE => {
let object_name = parser.parse_object_name(false)?;
(CommentObject::Table, object_name)
}
Token::Word(w) if w.keyword == Keyword::EXTENSION => {
let object_name = parser.parse_object_name(false)?;
(CommentObject::Extension, object_name)
}
_ => parser.expected("comment object_type", token)?,
};

parser.expect_keyword(Keyword::IS)?;
let comment = if parser.parse_keyword(Keyword::NULL) {
None
} else {
Some(parser.parse_literal_string()?)
};
Ok(Statement::Comment {
object_type,
object_name,
comment,
if_exists,
})
/// see <https://www.postgresql.org/docs/current/sql-comment.html>
fn supports_comment_on(&self) -> bool {
true
}
}

pub fn parse_create(parser: &mut Parser) -> Option<Result<Statement, ParserError>> {
Expand Down
5 changes: 5 additions & 0 deletions src/dialect/snowflake.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ impl Dialect for SnowflakeDialect {
true
}

/// See [doc](https://docs.snowflake.com/en/sql-reference/sql/comment)
fn supports_comment_on(&self) -> bool {
true
}

fn parse_statement(&self, parser: &mut Parser) -> Option<Result<Statement, ParserError>> {
if parser.parse_keyword(Keyword::CREATE) {
// possibly CREATE STAGE
Expand Down
47 changes: 47 additions & 0 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,8 @@ impl<'a> Parser<'a> {
Keyword::OPTIMIZE if dialect_of!(self is ClickHouseDialect | GenericDialect) => {
self.parse_optimize_table()
}
// `COMMENT` is snowflake specific https://docs.snowflake.com/en/sql-reference/sql/comment
Keyword::COMMENT if self.dialect.supports_comment_on() => self.parse_comment(),
_ => self.expected("an SQL statement", next_token),
},
Token::LParen => {
Expand All @@ -561,6 +563,51 @@ impl<'a> Parser<'a> {
}
}

pub fn parse_comment(&mut self) -> Result<Statement, ParserError> {
let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]);

self.expect_keyword(Keyword::ON)?;
let token = self.next_token();

let (object_type, object_name) = match token.token {
Token::Word(w) if w.keyword == Keyword::COLUMN => {
(CommentObject::Column, self.parse_object_name(false)?)
}
Token::Word(w) if w.keyword == Keyword::TABLE => {
(CommentObject::Table, self.parse_object_name(false)?)
}
Token::Word(w) if w.keyword == Keyword::EXTENSION => {
(CommentObject::Extension, self.parse_object_name(false)?)
}
Token::Word(w) if w.keyword == Keyword::SCHEMA => {
(CommentObject::Schema, self.parse_object_name(false)?)
}
Token::Word(w) if w.keyword == Keyword::DATABASE => {
(CommentObject::Database, self.parse_object_name(false)?)
}
Token::Word(w) if w.keyword == Keyword::USER => {
(CommentObject::User, self.parse_object_name(false)?)
}
Token::Word(w) if w.keyword == Keyword::ROLE => {
(CommentObject::Role, self.parse_object_name(false)?)
}
_ => 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,
if_exists,
})
}

pub fn parse_flush(&mut self) -> Result<Statement, ParserError> {
let mut channel = None;
let mut tables: Vec<ObjectName> = vec![];
Expand Down
88 changes: 88 additions & 0 deletions tests/sqlparser_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11629,3 +11629,91 @@ fn parse_factorial_operator() {
);
}
}

#[test]
fn parse_comments() {
match all_dialects_where(|d| d.supports_comment_on())
.verified_stmt("COMMENT ON COLUMN tab.name IS 'comment'")
{
Statement::Comment {
object_type,
object_name,
comment: Some(comment),
if_exists,
} => {
assert_eq!("comment", comment);
assert_eq!("tab.name", object_name.to_string());
assert_eq!(CommentObject::Column, object_type);
assert!(!if_exists);
}
_ => unreachable!(),
}

let object_types = [
("COLUMN", CommentObject::Column),
("EXTENSION", CommentObject::Extension),
("TABLE", CommentObject::Table),
("SCHEMA", CommentObject::Schema),
("DATABASE", CommentObject::Database),
("USER", CommentObject::User),
("ROLE", CommentObject::Role),
];
for (keyword, expected_object_type) in object_types.iter() {
match all_dialects_where(|d| d.supports_comment_on())
.verified_stmt(format!("COMMENT IF EXISTS ON {keyword} db.t0 IS 'comment'").as_str())
{
Statement::Comment {
object_type,
object_name,
comment: Some(comment),
if_exists,
} => {
assert_eq!("comment", comment);
assert_eq!("db.t0", object_name.to_string());
assert_eq!(*expected_object_type, object_type);
assert!(if_exists);
}
_ => unreachable!(),
}
}

match all_dialects_where(|d| d.supports_comment_on())
.verified_stmt("COMMENT IF EXISTS ON TABLE public.tab IS NULL")
{
Statement::Comment {
object_type,
object_name,
comment: None,
if_exists,
} => {
assert_eq!("public.tab", object_name.to_string());
assert_eq!(CommentObject::Table, object_type);
assert!(if_exists);
}
_ => unreachable!(),
}

// missing IS statement
assert_eq!(
all_dialects_where(|d| d.supports_comment_on())
.parse_sql_statements("COMMENT ON TABLE t0")
.unwrap_err(),
ParserError::ParserError("Expected: IS, found: EOF".to_string())
);

// missing comment literal
assert_eq!(
all_dialects_where(|d| d.supports_comment_on())
.parse_sql_statements("COMMENT ON TABLE t0 IS")
.unwrap_err(),
ParserError::ParserError("Expected: literal string, found: EOF".to_string())
);

// unknown object type
assert_eq!(
all_dialects_where(|d| d.supports_comment_on())
.parse_sql_statements("COMMENT ON UNKNOWN t0 IS 'comment'")
.unwrap_err(),
ParserError::ParserError("Expected: comment object_type, found: UNKNOWN".to_string())
);
}
62 changes: 0 additions & 62 deletions tests/sqlparser_postgres.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2891,68 +2891,6 @@ fn test_composite_value() {
);
}

#[test]
fn parse_comments() {
match pg().verified_stmt("COMMENT ON COLUMN tab.name IS 'comment'") {
Statement::Comment {
object_type,
object_name,
comment: Some(comment),
if_exists,
} => {
assert_eq!("comment", comment);
assert_eq!("tab.name", object_name.to_string());
assert_eq!(CommentObject::Column, object_type);
assert!(!if_exists);
}
_ => unreachable!(),
}

match pg().verified_stmt("COMMENT ON EXTENSION plpgsql IS 'comment'") {
Statement::Comment {
object_type,
object_name,
comment: Some(comment),
if_exists,
} => {
assert_eq!("comment", comment);
assert_eq!("plpgsql", object_name.to_string());
assert_eq!(CommentObject::Extension, object_type);
assert!(!if_exists);
}
_ => unreachable!(),
}

match pg().verified_stmt("COMMENT ON TABLE public.tab IS 'comment'") {
Statement::Comment {
object_type,
object_name,
comment: Some(comment),
if_exists,
} => {
assert_eq!("comment", comment);
assert_eq!("public.tab", object_name.to_string());
assert_eq!(CommentObject::Table, object_type);
assert!(!if_exists);
}
_ => unreachable!(),
}

match pg().verified_stmt("COMMENT IF EXISTS ON TABLE public.tab IS NULL") {
Statement::Comment {
object_type,
object_name,
comment: None,
if_exists,
} => {
assert_eq!("public.tab", object_name.to_string());
assert_eq!(CommentObject::Table, object_type);
assert!(if_exists);
}
_ => unreachable!(),
}
}

#[test]
fn parse_quoted_identifier() {
pg_and_generic().verified_stmt(r#"SELECT "quoted "" ident""#);
Expand Down

0 comments on commit 6d907d3

Please sign in to comment.