Skip to content

Commit ee32ab2

Browse files
committed
Add support of COMMENT ON syntax for Snowflake
The COMMENT ON syntax has been supported in PostgreSQL. This PR only moves it into the common and adds `supports_comment_on` to enable this feature. Also, this PR extends `SCHEMA`, `DATABASE`, `USER`, `ROLE` as its allowed object type. This closes #1511.
1 parent 334a5bf commit ee32ab2

File tree

8 files changed

+164
-102
lines changed

8 files changed

+164
-102
lines changed

src/ast/mod.rs

+8
Original file line numberDiff line numberDiff line change
@@ -1884,6 +1884,10 @@ pub enum CommentObject {
18841884
Column,
18851885
Table,
18861886
Extension,
1887+
Schema,
1888+
Database,
1889+
User,
1890+
Role,
18871891
}
18881892

18891893
impl fmt::Display for CommentObject {
@@ -1892,6 +1896,10 @@ impl fmt::Display for CommentObject {
18921896
CommentObject::Column => f.write_str("COLUMN"),
18931897
CommentObject::Table => f.write_str("TABLE"),
18941898
CommentObject::Extension => f.write_str("EXTENSION"),
1899+
CommentObject::Schema => f.write_str("SCHEMA"),
1900+
CommentObject::Database => f.write_str("DATABASE"),
1901+
CommentObject::User => f.write_str("USER"),
1902+
CommentObject::Role => f.write_str("ROLE"),
18951903
}
18961904
}
18971905
}

src/dialect/generic.rs

+4
Original file line numberDiff line numberDiff line change
@@ -111,4 +111,8 @@ impl Dialect for GenericDialect {
111111
fn supports_try_convert(&self) -> bool {
112112
true
113113
}
114+
115+
fn supports_comment_on(&self) -> bool {
116+
true
117+
}
114118
}

src/dialect/mod.rs

+6-1
Original file line numberDiff line numberDiff line change
@@ -601,11 +601,16 @@ pub trait Dialect: Debug + Any {
601601
false
602602
}
603603

604-
/// Returns true if this dialect expects the the `TOP` option
604+
/// Returns true if this dialect expects the `TOP` option
605605
/// before the `ALL`/`DISTINCT` options in a `SELECT` statement.
606606
fn supports_top_before_distinct(&self) -> bool {
607607
false
608608
}
609+
610+
/// Returns true if this dialect supports the `COMMENT` statement
611+
fn supports_comment_on(&self) -> bool {
612+
false
613+
}
609614
}
610615

611616
/// This represents the operators for which precedence must be defined

src/dialect/postgresql.rs

+6-39
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
// limitations under the License.
2929
use log::debug;
3030

31-
use crate::ast::{CommentObject, ObjectName, Statement, UserDefinedTypeRepresentation};
31+
use crate::ast::{ObjectName, Statement, UserDefinedTypeRepresentation};
3232
use crate::dialect::{Dialect, Precedence};
3333
use crate::keywords::Keyword;
3434
use crate::parser::{Parser, ParserError};
@@ -136,9 +136,7 @@ impl Dialect for PostgreSqlDialect {
136136
}
137137

138138
fn parse_statement(&self, parser: &mut Parser) -> Option<Result<Statement, ParserError>> {
139-
if parser.parse_keyword(Keyword::COMMENT) {
140-
Some(parse_comment(parser))
141-
} else if parser.parse_keyword(Keyword::CREATE) {
139+
if parser.parse_keyword(Keyword::CREATE) {
142140
parser.prev_token(); // unconsume the CREATE in case we don't end up parsing anything
143141
parse_create(parser)
144142
} else {
@@ -201,42 +199,11 @@ impl Dialect for PostgreSqlDialect {
201199
fn supports_notify(&self) -> bool {
202200
true
203201
}
204-
}
205-
206-
pub fn parse_comment(parser: &mut Parser) -> Result<Statement, ParserError> {
207-
let if_exists = parser.parse_keywords(&[Keyword::IF, Keyword::EXISTS]);
208-
209-
parser.expect_keyword(Keyword::ON)?;
210-
let token = parser.next_token();
211-
212-
let (object_type, object_name) = match token.token {
213-
Token::Word(w) if w.keyword == Keyword::COLUMN => {
214-
let object_name = parser.parse_object_name(false)?;
215-
(CommentObject::Column, object_name)
216-
}
217-
Token::Word(w) if w.keyword == Keyword::TABLE => {
218-
let object_name = parser.parse_object_name(false)?;
219-
(CommentObject::Table, object_name)
220-
}
221-
Token::Word(w) if w.keyword == Keyword::EXTENSION => {
222-
let object_name = parser.parse_object_name(false)?;
223-
(CommentObject::Extension, object_name)
224-
}
225-
_ => parser.expected("comment object_type", token)?,
226-
};
227202

228-
parser.expect_keyword(Keyword::IS)?;
229-
let comment = if parser.parse_keyword(Keyword::NULL) {
230-
None
231-
} else {
232-
Some(parser.parse_literal_string()?)
233-
};
234-
Ok(Statement::Comment {
235-
object_type,
236-
object_name,
237-
comment,
238-
if_exists,
239-
})
203+
/// see <https://www.postgresql.org/docs/current/sql-comment.html>
204+
fn supports_comment_on(&self) -> bool {
205+
true
206+
}
240207
}
241208

242209
pub fn parse_create(parser: &mut Parser) -> Option<Result<Statement, ParserError>> {

src/dialect/snowflake.rs

+5
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,11 @@ impl Dialect for SnowflakeDialect {
9696
true
9797
}
9898

99+
/// See [doc](https://docs.snowflake.com/en/sql-reference/sql/comment)
100+
fn supports_comment_on(&self) -> bool {
101+
true
102+
}
103+
99104
fn parse_statement(&self, parser: &mut Parser) -> Option<Result<Statement, ParserError>> {
100105
if parser.parse_keyword(Keyword::CREATE) {
101106
// possibly CREATE STAGE

src/parser/mod.rs

+47
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,8 @@ impl<'a> Parser<'a> {
551551
Keyword::OPTIMIZE if dialect_of!(self is ClickHouseDialect | GenericDialect) => {
552552
self.parse_optimize_table()
553553
}
554+
// `COMMENT` is snowflake specific https://docs.snowflake.com/en/sql-reference/sql/comment
555+
Keyword::COMMENT if self.dialect.supports_comment_on() => self.parse_comment(),
554556
_ => self.expected("an SQL statement", next_token),
555557
},
556558
Token::LParen => {
@@ -561,6 +563,51 @@ impl<'a> Parser<'a> {
561563
}
562564
}
563565

566+
pub fn parse_comment(&mut self) -> Result<Statement, ParserError> {
567+
let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]);
568+
569+
self.expect_keyword(Keyword::ON)?;
570+
let token = self.next_token();
571+
572+
let (object_type, object_name) = match token.token {
573+
Token::Word(w) if w.keyword == Keyword::COLUMN => {
574+
(CommentObject::Column, self.parse_object_name(false)?)
575+
}
576+
Token::Word(w) if w.keyword == Keyword::TABLE => {
577+
(CommentObject::Table, self.parse_object_name(false)?)
578+
}
579+
Token::Word(w) if w.keyword == Keyword::EXTENSION => {
580+
(CommentObject::Extension, self.parse_object_name(false)?)
581+
}
582+
Token::Word(w) if w.keyword == Keyword::SCHEMA => {
583+
(CommentObject::Schema, self.parse_object_name(false)?)
584+
}
585+
Token::Word(w) if w.keyword == Keyword::DATABASE => {
586+
(CommentObject::Database, self.parse_object_name(false)?)
587+
}
588+
Token::Word(w) if w.keyword == Keyword::USER => {
589+
(CommentObject::User, self.parse_object_name(false)?)
590+
}
591+
Token::Word(w) if w.keyword == Keyword::ROLE => {
592+
(CommentObject::Role, self.parse_object_name(false)?)
593+
}
594+
_ => self.expected("comment object_type", token)?,
595+
};
596+
597+
self.expect_keyword(Keyword::IS)?;
598+
let comment = if self.parse_keyword(Keyword::NULL) {
599+
None
600+
} else {
601+
Some(self.parse_literal_string()?)
602+
};
603+
Ok(Statement::Comment {
604+
object_type,
605+
object_name,
606+
comment,
607+
if_exists,
608+
})
609+
}
610+
564611
pub fn parse_flush(&mut self) -> Result<Statement, ParserError> {
565612
let mut channel = None;
566613
let mut tables: Vec<ObjectName> = vec![];

tests/sqlparser_common.rs

+88
Original file line numberDiff line numberDiff line change
@@ -11532,3 +11532,91 @@ fn test_select_top() {
1153211532
dialects.verified_stmt("SELECT TOP 3 DISTINCT * FROM tbl");
1153311533
dialects.verified_stmt("SELECT TOP 3 DISTINCT a, b, c FROM tbl");
1153411534
}
11535+
11536+
#[test]
11537+
fn parse_comments() {
11538+
match all_dialects_where(|d| d.supports_comment_on())
11539+
.verified_stmt("COMMENT ON COLUMN tab.name IS 'comment'")
11540+
{
11541+
Statement::Comment {
11542+
object_type,
11543+
object_name,
11544+
comment: Some(comment),
11545+
if_exists,
11546+
} => {
11547+
assert_eq!("comment", comment);
11548+
assert_eq!("tab.name", object_name.to_string());
11549+
assert_eq!(CommentObject::Column, object_type);
11550+
assert!(!if_exists);
11551+
}
11552+
_ => unreachable!(),
11553+
}
11554+
11555+
let object_types = [
11556+
("COLUMN", CommentObject::Column),
11557+
("EXTENSION", CommentObject::Extension),
11558+
("TABLE", CommentObject::Table),
11559+
("SCHEMA", CommentObject::Schema),
11560+
("DATABASE", CommentObject::Database),
11561+
("USER", CommentObject::User),
11562+
("ROLE", CommentObject::Role),
11563+
];
11564+
for (keyword, expected_object_type) in object_types.iter() {
11565+
match all_dialects_where(|d| d.supports_comment_on())
11566+
.verified_stmt(format!("COMMENT IF EXISTS ON {keyword} db.t0 IS 'comment'").as_str())
11567+
{
11568+
Statement::Comment {
11569+
object_type,
11570+
object_name,
11571+
comment: Some(comment),
11572+
if_exists,
11573+
} => {
11574+
assert_eq!("comment", comment);
11575+
assert_eq!("db.t0", object_name.to_string());
11576+
assert_eq!(*expected_object_type, object_type);
11577+
assert!(if_exists);
11578+
}
11579+
_ => unreachable!(),
11580+
}
11581+
}
11582+
11583+
match all_dialects_where(|d| d.supports_comment_on())
11584+
.verified_stmt("COMMENT IF EXISTS ON TABLE public.tab IS NULL")
11585+
{
11586+
Statement::Comment {
11587+
object_type,
11588+
object_name,
11589+
comment: None,
11590+
if_exists,
11591+
} => {
11592+
assert_eq!("public.tab", object_name.to_string());
11593+
assert_eq!(CommentObject::Table, object_type);
11594+
assert!(if_exists);
11595+
}
11596+
_ => unreachable!(),
11597+
}
11598+
11599+
// missing IS statement
11600+
assert_eq!(
11601+
all_dialects_where(|d| d.supports_comment_on())
11602+
.parse_sql_statements("COMMENT ON TABLE t0")
11603+
.unwrap_err(),
11604+
ParserError::ParserError("Expected: IS, found: EOF".to_string())
11605+
);
11606+
11607+
// missing comment literal
11608+
assert_eq!(
11609+
all_dialects_where(|d| d.supports_comment_on())
11610+
.parse_sql_statements("COMMENT ON TABLE t0 IS")
11611+
.unwrap_err(),
11612+
ParserError::ParserError("Expected: literal string, found: EOF".to_string())
11613+
);
11614+
11615+
// unknown object type
11616+
assert_eq!(
11617+
all_dialects_where(|d| d.supports_comment_on())
11618+
.parse_sql_statements("COMMENT ON UNKNOWN t0 IS 'comment'")
11619+
.unwrap_err(),
11620+
ParserError::ParserError("Expected: comment object_type, found: UNKNOWN".to_string())
11621+
);
11622+
}

tests/sqlparser_postgres.rs

-62
Original file line numberDiff line numberDiff line change
@@ -2891,68 +2891,6 @@ fn test_composite_value() {
28912891
);
28922892
}
28932893

2894-
#[test]
2895-
fn parse_comments() {
2896-
match pg().verified_stmt("COMMENT ON COLUMN tab.name IS 'comment'") {
2897-
Statement::Comment {
2898-
object_type,
2899-
object_name,
2900-
comment: Some(comment),
2901-
if_exists,
2902-
} => {
2903-
assert_eq!("comment", comment);
2904-
assert_eq!("tab.name", object_name.to_string());
2905-
assert_eq!(CommentObject::Column, object_type);
2906-
assert!(!if_exists);
2907-
}
2908-
_ => unreachable!(),
2909-
}
2910-
2911-
match pg().verified_stmt("COMMENT ON EXTENSION plpgsql IS 'comment'") {
2912-
Statement::Comment {
2913-
object_type,
2914-
object_name,
2915-
comment: Some(comment),
2916-
if_exists,
2917-
} => {
2918-
assert_eq!("comment", comment);
2919-
assert_eq!("plpgsql", object_name.to_string());
2920-
assert_eq!(CommentObject::Extension, object_type);
2921-
assert!(!if_exists);
2922-
}
2923-
_ => unreachable!(),
2924-
}
2925-
2926-
match pg().verified_stmt("COMMENT ON TABLE public.tab IS 'comment'") {
2927-
Statement::Comment {
2928-
object_type,
2929-
object_name,
2930-
comment: Some(comment),
2931-
if_exists,
2932-
} => {
2933-
assert_eq!("comment", comment);
2934-
assert_eq!("public.tab", object_name.to_string());
2935-
assert_eq!(CommentObject::Table, object_type);
2936-
assert!(!if_exists);
2937-
}
2938-
_ => unreachable!(),
2939-
}
2940-
2941-
match pg().verified_stmt("COMMENT IF EXISTS ON TABLE public.tab IS NULL") {
2942-
Statement::Comment {
2943-
object_type,
2944-
object_name,
2945-
comment: None,
2946-
if_exists,
2947-
} => {
2948-
assert_eq!("public.tab", object_name.to_string());
2949-
assert_eq!(CommentObject::Table, object_type);
2950-
assert!(if_exists);
2951-
}
2952-
_ => unreachable!(),
2953-
}
2954-
}
2955-
29562894
#[test]
29572895
fn parse_quoted_identifier() {
29582896
pg_and_generic().verified_stmt(r#"SELECT "quoted "" ident""#);

0 commit comments

Comments
 (0)