sql/parsers/
comment_parser.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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(); // consume COMMENT
29
30        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}