sql/parsers/
comment_parser.rs1use snafu::{ResultExt, ensure};
16use sqlparser::ast::ObjectName;
17use sqlparser::keywords::Keyword;
18use sqlparser::tokenizer::Token;
19
20use crate::ast::{Ident, ObjectNamePart};
21use crate::error::{self, InvalidSqlSnafu, Result};
22use crate::parser::{FLOW, ParserContext};
23use crate::statements::comment::{Comment, CommentObject};
24use crate::statements::statement::Statement;
25
26impl ParserContext<'_> {
27 pub(crate) fn parse_comment(&mut self) -> Result<Statement> {
28 let _ = self.parser.next_token(); if !self.parser.parse_keyword(Keyword::ON) {
31 return self.expected("ON", self.parser.peek_token());
32 }
33
34 let target_token = self.parser.next_token();
35 let comment = match target_token.token {
36 Token::Word(word) if word.keyword == Keyword::TABLE => {
37 let raw_table =
38 self.parse_object_name()
39 .with_context(|_| error::UnexpectedSnafu {
40 expected: "a table name",
41 actual: self.peek_token_as_string(),
42 })?;
43 let table = Self::canonicalize_object_name(raw_table)?;
44 CommentObject::Table(table)
45 }
46 Token::Word(word) if word.keyword == Keyword::COLUMN => {
47 self.parse_column_comment_target()?
48 }
49 Token::Word(word)
50 if word.keyword == Keyword::NoKeyword && word.value.eq_ignore_ascii_case(FLOW) =>
51 {
52 let raw_flow =
53 self.parse_object_name()
54 .with_context(|_| error::UnexpectedSnafu {
55 expected: "a flow name",
56 actual: self.peek_token_as_string(),
57 })?;
58 let flow = Self::canonicalize_object_name(raw_flow)?;
59 CommentObject::Flow(flow)
60 }
61 _ => return self.expected("TABLE, COLUMN or FLOW", target_token),
62 };
63
64 if !self.parser.parse_keyword(Keyword::IS) {
65 return self.expected("IS", self.parser.peek_token());
66 }
67
68 let comment_value = if self.parser.parse_keyword(Keyword::NULL) {
69 None
70 } else {
71 Some(
72 self.parser
73 .parse_literal_string()
74 .context(error::SyntaxSnafu)?,
75 )
76 };
77
78 Ok(Statement::Comment(Comment {
79 object: comment,
80 comment: comment_value,
81 }))
82 }
83
84 fn parse_column_comment_target(&mut self) -> Result<CommentObject> {
85 let raw = self
86 .parse_object_name()
87 .with_context(|_| error::UnexpectedSnafu {
88 expected: "a column reference",
89 actual: self.peek_token_as_string(),
90 })?;
91 let canonical = Self::canonicalize_object_name(raw)?;
92
93 let mut parts = canonical.0;
94 ensure!(
95 parts.len() >= 2,
96 InvalidSqlSnafu {
97 msg: "COMMENT ON COLUMN expects <table>.<column>".to_string(),
98 }
99 );
100
101 let column_part = parts.pop().unwrap();
102 let ObjectNamePart::Identifier(column_ident) = column_part else {
103 unreachable!("canonicalized object name should only contain identifiers");
104 };
105
106 let column = ParserContext::canonicalize_identifier(column_ident);
107
108 let mut table_idents: Vec<Ident> = Vec::with_capacity(parts.len());
109 for part in parts {
110 match part {
111 ObjectNamePart::Identifier(ident) => table_idents.push(ident),
112 ObjectNamePart::Function(_) => {
113 unreachable!("canonicalized object name should only contain identifiers")
114 }
115 }
116 }
117
118 ensure!(
119 !table_idents.is_empty(),
120 InvalidSqlSnafu {
121 msg: "Table name is required before column name".to_string(),
122 }
123 );
124
125 let table = ObjectName::from(table_idents);
126
127 Ok(CommentObject::Column { table, column })
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use std::assert_matches::assert_matches;
134
135 use crate::dialect::GreptimeDbDialect;
136 use crate::parser::{ParseOptions, ParserContext};
137 use crate::statements::comment::CommentObject;
138 use crate::statements::statement::Statement;
139
140 fn parse(sql: &str) -> Statement {
141 let mut stmts =
142 ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
143 .unwrap();
144 assert_eq!(stmts.len(), 1);
145 stmts.pop().unwrap()
146 }
147
148 #[test]
149 fn test_parse_comment_on_table() {
150 let stmt = parse("COMMENT ON TABLE mytable IS 'test';");
151 match stmt {
152 Statement::Comment(comment) => {
153 assert_matches!(comment.object, CommentObject::Table(ref name) if name.to_string() == "mytable");
154 assert_eq!(comment.comment.as_deref(), Some("test"));
155 }
156 _ => panic!("expected comment statement"),
157 }
158
159 let stmt = parse("COMMENT ON TABLE mytable IS NULL;");
160 match stmt {
161 Statement::Comment(comment) => {
162 assert_matches!(comment.object, CommentObject::Table(ref name) if name.to_string() == "mytable");
163 assert!(comment.comment.is_none());
164 }
165 _ => panic!("expected comment statement"),
166 }
167 }
168
169 #[test]
170 fn test_parse_comment_on_column() {
171 let stmt = parse("COMMENT ON COLUMN my_schema.my_table.my_col IS 'desc';");
172 match stmt {
173 Statement::Comment(comment) => match comment.object {
174 CommentObject::Column { table, column } => {
175 assert_eq!(table.to_string(), "my_schema.my_table");
176 assert_eq!(column.value, "my_col");
177 assert_eq!(comment.comment.as_deref(), Some("desc"));
178 }
179 _ => panic!("expected column comment"),
180 },
181 _ => panic!("expected comment statement"),
182 }
183 }
184
185 #[test]
186 fn test_parse_comment_on_flow() {
187 let stmt = parse("COMMENT ON FLOW my_flow IS 'desc';");
188 match stmt {
189 Statement::Comment(comment) => {
190 assert_matches!(comment.object, CommentObject::Flow(ref name) if name.to_string() == "my_flow");
191 assert_eq!(comment.comment.as_deref(), Some("desc"));
192 }
193 _ => panic!("expected comment statement"),
194 }
195 }
196}