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:
Ruihang Xia
2025-12-10 23:08:47 +08:00
committed by GitHub
parent f1abe5d215
commit 564cc0c750
33 changed files with 1840 additions and 316 deletions

View File

@@ -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(),

View File

@@ -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;

View 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"),
}
}
}

View File

@@ -14,6 +14,7 @@
pub mod admin;
pub mod alter;
pub mod comment;
pub mod copy;
pub mod create;
pub mod cursor;

View 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),
}
}
}

View File

@@ -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}")
}