mirror of
https://github.com/GreptimeTeam/greptimedb.git
synced 2026-05-23 00:10:38 +00:00
feat: table/column/flow COMMENT (#7060)
* initial impl Signed-off-by: Ruihang Xia <waynestxia@gmail.com> * simplify impl Signed-off-by: Ruihang Xia <waynestxia@gmail.com> * sqlness test Signed-off-by: Ruihang Xia <waynestxia@gmail.com> * avoid unimplemented panic Signed-off-by: Ruihang Xia <waynestxia@gmail.com> * validate flow Signed-off-by: Ruihang Xia <waynestxia@gmail.com> * update sqlness result Signed-off-by: Ruihang Xia <waynestxia@gmail.com> * fix table column comment Signed-off-by: Ruihang Xia <waynestxia@gmail.com> * table level comment Signed-off-by: Ruihang Xia <waynestxia@gmail.com> * simplify table info serde Signed-off-by: Ruihang Xia <waynestxia@gmail.com> * don't txn Signed-off-by: Ruihang Xia <waynestxia@gmail.com> * remove empty trait Signed-off-by: Ruihang Xia <waynestxia@gmail.com> * wip: procedure Signed-off-by: Ruihang Xia <waynestxia@gmail.com> * update proto Signed-off-by: Ruihang Xia <waynestxia@gmail.com> * grpc support Signed-off-by: Ruihang Xia <waynestxia@gmail.com> * Apply suggestions from code review Co-authored-by: dennis zhuang <killme2008@gmail.com> Co-authored-by: LFC <990479+MichaelScofield@users.noreply.github.com> Signed-off-by: Ruihang Xia <waynestxia@gmail.com> * try from pb struct Signed-off-by: Ruihang Xia <waynestxia@gmail.com> * doc comment Signed-off-by: Ruihang Xia <waynestxia@gmail.com> * check unchanged fast case Signed-off-by: Ruihang Xia <waynestxia@gmail.com> * tune errors Signed-off-by: Ruihang Xia <waynestxia@gmail.com> * fix merge error Signed-off-by: Ruihang Xia <waynestxia@gmail.com> * use try_as_raw_value Signed-off-by: Ruihang Xia <waynestxia@gmail.com> --------- Signed-off-by: Ruihang Xia <waynestxia@gmail.com> Co-authored-by: dennis zhuang <killme2008@gmail.com> Co-authored-by: LFC <990479+MichaelScofield@users.noreply.github.com>
This commit is contained in:
@@ -163,6 +163,8 @@ impl ParserContext<'_> {
|
||||
|
||||
Keyword::TRUNCATE => self.parse_truncate(),
|
||||
|
||||
Keyword::COMMENT => self.parse_comment(),
|
||||
|
||||
Keyword::SET => self.parse_set_variables(),
|
||||
|
||||
Keyword::ADMIN => self.parse_admin_command(),
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
pub(crate) mod admin_parser;
|
||||
mod alter_parser;
|
||||
pub(crate) mod comment_parser;
|
||||
pub(crate) mod copy_parser;
|
||||
pub(crate) mod create_parser;
|
||||
pub(crate) mod cursor_parser;
|
||||
|
||||
196
src/sql/src/parsers/comment_parser.rs
Normal file
196
src/sql/src/parsers/comment_parser.rs
Normal file
@@ -0,0 +1,196 @@
|
||||
// Copyright 2023 Greptime Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use snafu::{ResultExt, ensure};
|
||||
use sqlparser::ast::ObjectName;
|
||||
use sqlparser::keywords::Keyword;
|
||||
use sqlparser::tokenizer::Token;
|
||||
|
||||
use crate::ast::{Ident, ObjectNamePart};
|
||||
use crate::error::{self, InvalidSqlSnafu, Result};
|
||||
use crate::parser::{FLOW, ParserContext};
|
||||
use crate::statements::comment::{Comment, CommentObject};
|
||||
use crate::statements::statement::Statement;
|
||||
|
||||
impl ParserContext<'_> {
|
||||
pub(crate) fn parse_comment(&mut self) -> Result<Statement> {
|
||||
let _ = self.parser.next_token(); // consume COMMENT
|
||||
|
||||
if !self.parser.parse_keyword(Keyword::ON) {
|
||||
return self.expected("ON", self.parser.peek_token());
|
||||
}
|
||||
|
||||
let target_token = self.parser.next_token();
|
||||
let comment = match target_token.token {
|
||||
Token::Word(word) if word.keyword == Keyword::TABLE => {
|
||||
let raw_table =
|
||||
self.parse_object_name()
|
||||
.with_context(|_| error::UnexpectedSnafu {
|
||||
expected: "a table name",
|
||||
actual: self.peek_token_as_string(),
|
||||
})?;
|
||||
let table = Self::canonicalize_object_name(raw_table)?;
|
||||
CommentObject::Table(table)
|
||||
}
|
||||
Token::Word(word) if word.keyword == Keyword::COLUMN => {
|
||||
self.parse_column_comment_target()?
|
||||
}
|
||||
Token::Word(word)
|
||||
if word.keyword == Keyword::NoKeyword && word.value.eq_ignore_ascii_case(FLOW) =>
|
||||
{
|
||||
let raw_flow =
|
||||
self.parse_object_name()
|
||||
.with_context(|_| error::UnexpectedSnafu {
|
||||
expected: "a flow name",
|
||||
actual: self.peek_token_as_string(),
|
||||
})?;
|
||||
let flow = Self::canonicalize_object_name(raw_flow)?;
|
||||
CommentObject::Flow(flow)
|
||||
}
|
||||
_ => return self.expected("TABLE, COLUMN or FLOW", target_token),
|
||||
};
|
||||
|
||||
if !self.parser.parse_keyword(Keyword::IS) {
|
||||
return self.expected("IS", self.parser.peek_token());
|
||||
}
|
||||
|
||||
let comment_value = if self.parser.parse_keyword(Keyword::NULL) {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
self.parser
|
||||
.parse_literal_string()
|
||||
.context(error::SyntaxSnafu)?,
|
||||
)
|
||||
};
|
||||
|
||||
Ok(Statement::Comment(Comment {
|
||||
object: comment,
|
||||
comment: comment_value,
|
||||
}))
|
||||
}
|
||||
|
||||
fn parse_column_comment_target(&mut self) -> Result<CommentObject> {
|
||||
let raw = self
|
||||
.parse_object_name()
|
||||
.with_context(|_| error::UnexpectedSnafu {
|
||||
expected: "a column reference",
|
||||
actual: self.peek_token_as_string(),
|
||||
})?;
|
||||
let canonical = Self::canonicalize_object_name(raw)?;
|
||||
|
||||
let mut parts = canonical.0;
|
||||
ensure!(
|
||||
parts.len() >= 2,
|
||||
InvalidSqlSnafu {
|
||||
msg: "COMMENT ON COLUMN expects <table>.<column>".to_string(),
|
||||
}
|
||||
);
|
||||
|
||||
let column_part = parts.pop().unwrap();
|
||||
let ObjectNamePart::Identifier(column_ident) = column_part else {
|
||||
unreachable!("canonicalized object name should only contain identifiers");
|
||||
};
|
||||
|
||||
let column = ParserContext::canonicalize_identifier(column_ident);
|
||||
|
||||
let mut table_idents: Vec<Ident> = Vec::with_capacity(parts.len());
|
||||
for part in parts {
|
||||
match part {
|
||||
ObjectNamePart::Identifier(ident) => table_idents.push(ident),
|
||||
ObjectNamePart::Function(_) => {
|
||||
unreachable!("canonicalized object name should only contain identifiers")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ensure!(
|
||||
!table_idents.is_empty(),
|
||||
InvalidSqlSnafu {
|
||||
msg: "Table name is required before column name".to_string(),
|
||||
}
|
||||
);
|
||||
|
||||
let table = ObjectName::from(table_idents);
|
||||
|
||||
Ok(CommentObject::Column { table, column })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::assert_matches::assert_matches;
|
||||
|
||||
use crate::dialect::GreptimeDbDialect;
|
||||
use crate::parser::{ParseOptions, ParserContext};
|
||||
use crate::statements::comment::CommentObject;
|
||||
use crate::statements::statement::Statement;
|
||||
|
||||
fn parse(sql: &str) -> Statement {
|
||||
let mut stmts =
|
||||
ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
|
||||
.unwrap();
|
||||
assert_eq!(stmts.len(), 1);
|
||||
stmts.pop().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_comment_on_table() {
|
||||
let stmt = parse("COMMENT ON TABLE mytable IS 'test';");
|
||||
match stmt {
|
||||
Statement::Comment(comment) => {
|
||||
assert_matches!(comment.object, CommentObject::Table(ref name) if name.to_string() == "mytable");
|
||||
assert_eq!(comment.comment.as_deref(), Some("test"));
|
||||
}
|
||||
_ => panic!("expected comment statement"),
|
||||
}
|
||||
|
||||
let stmt = parse("COMMENT ON TABLE mytable IS NULL;");
|
||||
match stmt {
|
||||
Statement::Comment(comment) => {
|
||||
assert_matches!(comment.object, CommentObject::Table(ref name) if name.to_string() == "mytable");
|
||||
assert!(comment.comment.is_none());
|
||||
}
|
||||
_ => panic!("expected comment statement"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_comment_on_column() {
|
||||
let stmt = parse("COMMENT ON COLUMN my_schema.my_table.my_col IS 'desc';");
|
||||
match stmt {
|
||||
Statement::Comment(comment) => match comment.object {
|
||||
CommentObject::Column { table, column } => {
|
||||
assert_eq!(table.to_string(), "my_schema.my_table");
|
||||
assert_eq!(column.value, "my_col");
|
||||
assert_eq!(comment.comment.as_deref(), Some("desc"));
|
||||
}
|
||||
_ => panic!("expected column comment"),
|
||||
},
|
||||
_ => panic!("expected comment statement"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_comment_on_flow() {
|
||||
let stmt = parse("COMMENT ON FLOW my_flow IS 'desc';");
|
||||
match stmt {
|
||||
Statement::Comment(comment) => {
|
||||
assert_matches!(comment.object, CommentObject::Flow(ref name) if name.to_string() == "my_flow");
|
||||
assert_eq!(comment.comment.as_deref(), Some("desc"));
|
||||
}
|
||||
_ => panic!("expected comment statement"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
pub mod admin;
|
||||
pub mod alter;
|
||||
pub mod comment;
|
||||
pub mod copy;
|
||||
pub mod create;
|
||||
pub mod cursor;
|
||||
|
||||
67
src/sql/src/statements/comment.rs
Normal file
67
src/sql/src/statements/comment.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
// Copyright 2023 Greptime Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
|
||||
use serde::Serialize;
|
||||
use sqlparser_derive::{Visit, VisitMut};
|
||||
|
||||
use crate::ast::{Ident, ObjectName};
|
||||
|
||||
/// Represents a SQL COMMENT statement for adding or removing comments on database objects.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```sql
|
||||
/// COMMENT ON TABLE my_table IS 'This is a table comment';
|
||||
/// COMMENT ON COLUMN my_table.my_column IS 'This is a column comment';
|
||||
/// COMMENT ON FLOW my_flow IS NULL;
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Visit, VisitMut, Serialize)]
|
||||
pub struct Comment {
|
||||
pub object: CommentObject,
|
||||
pub comment: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Visit, VisitMut, Serialize)]
|
||||
pub enum CommentObject {
|
||||
Table(ObjectName),
|
||||
Column { table: ObjectName, column: Ident },
|
||||
Flow(ObjectName),
|
||||
}
|
||||
|
||||
impl Display for Comment {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "COMMENT ON {} IS ", self.object)?;
|
||||
match &self.comment {
|
||||
Some(comment) => {
|
||||
let escaped = comment.replace('\'', "''");
|
||||
write!(f, "'{}'", escaped)
|
||||
}
|
||||
None => f.write_str("NULL"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for CommentObject {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
CommentObject::Table(name) => write!(f, "TABLE {}", name),
|
||||
CommentObject::Column { table, column } => {
|
||||
write!(f, "COLUMN {}.{}", table, column)
|
||||
}
|
||||
CommentObject::Flow(name) => write!(f, "FLOW {}", name),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ use sqlparser_derive::{Visit, VisitMut};
|
||||
use crate::error::{ConvertToDfStatementSnafu, Error};
|
||||
use crate::statements::admin::Admin;
|
||||
use crate::statements::alter::{AlterDatabase, AlterTable};
|
||||
use crate::statements::comment::Comment;
|
||||
use crate::statements::copy::Copy;
|
||||
use crate::statements::create::{
|
||||
CreateDatabase, CreateExternalTable, CreateFlow, CreateTable, CreateTableLike, CreateView,
|
||||
@@ -137,6 +138,8 @@ pub enum Statement {
|
||||
SetVariables(SetVariables),
|
||||
// SHOW VARIABLES
|
||||
ShowVariables(ShowVariables),
|
||||
// COMMENT ON
|
||||
Comment(Comment),
|
||||
// USE
|
||||
Use(String),
|
||||
// Admin statement(extension)
|
||||
@@ -204,6 +207,7 @@ impl Statement {
|
||||
| Statement::Copy(_)
|
||||
| Statement::TruncateTable(_)
|
||||
| Statement::SetVariables(_)
|
||||
| Statement::Comment(_)
|
||||
| Statement::Use(_)
|
||||
| Statement::DeclareCursor(_)
|
||||
| Statement::CloseCursor(_)
|
||||
@@ -267,6 +271,7 @@ impl Display for Statement {
|
||||
Statement::TruncateTable(s) => s.fmt(f),
|
||||
Statement::SetVariables(s) => s.fmt(f),
|
||||
Statement::ShowVariables(s) => s.fmt(f),
|
||||
Statement::Comment(s) => s.fmt(f),
|
||||
Statement::ShowCharset(kind) => {
|
||||
write!(f, "SHOW CHARSET {kind}")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user