Skip to content

Add support of COMMENT ON syntax for Snowflake #1516

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading