feat: trigger alter parse (#6553)

* feat: support trigger alter

* fix: cargo fmt

* fix: clippy

* fix: some docs

* fix: cr

* fix: ON -> RENAME
This commit is contained in:
fys
2025-07-29 19:07:31 +08:00
committed by GitHub
parent f07b1daed4
commit a10b1d9885
15 changed files with 1210 additions and 31 deletions

View File

@@ -29,9 +29,11 @@ excludes = [
# enterprise
"src/common/meta/src/rpc/ddl/trigger.rs",
"src/operator/src/expr_helper/trigger.rs",
"src/sql/src/statements/alter/trigger.rs",
"src/sql/src/statements/create/trigger.rs",
"src/sql/src/statements/show/trigger.rs",
"src/sql/src/statements/drop/trigger.rs",
"src/sql/src/parsers/alter_parser/trigger.rs",
"src/sql/src/parsers/create_parser/trigger.rs",
"src/sql/src/parsers/show_parser/trigger.rs",
"src/mito2/src/extension.rs",

View File

@@ -678,6 +678,8 @@ pub fn check_permission(
Statement::AlterTable(stmt) => {
validate_param(stmt.table_name(), query_ctx)?;
}
#[cfg(feature = "enterprise")]
Statement::AlterTrigger(_) => {}
// set/show variable now only alter/show variable in session
Statement::SetVariables(_) | Statement::ShowVariables(_) => {}
// show charset and show collation won't be checked

View File

@@ -274,6 +274,11 @@ impl StatementExecutor {
self.alter_database(alter_database, query_ctx).await
}
#[cfg(feature = "enterprise")]
Statement::AlterTrigger(alter_trigger) => {
self.alter_trigger(alter_trigger, query_ctx).await
}
Statement::DropTable(stmt) => {
let mut table_names = Vec::with_capacity(stmt.table_names().len());
for table_name_stmt in stmt.table_names() {

View File

@@ -64,6 +64,8 @@ use session::context::QueryContextRef;
use session::table_name::table_idents_to_full_name;
use snafu::{ensure, OptionExt, ResultExt};
use sql::parser::{ParseOptions, ParserContext};
#[cfg(feature = "enterprise")]
use sql::statements::alter::trigger::AlterTrigger;
use sql::statements::alter::{AlterDatabase, AlterTable};
#[cfg(feature = "enterprise")]
use sql::statements::create::trigger::CreateTrigger;
@@ -1306,6 +1308,19 @@ impl StatementExecutor {
Ok(Output::new_with_affected_rows(0))
}
#[cfg(feature = "enterprise")]
#[tracing::instrument(skip_all)]
pub async fn alter_trigger(
&self,
_alter_expr: AlterTrigger,
_query_context: QueryContextRef,
) -> Result<Output> {
crate::error::NotSupportedSnafu {
feat: "alter trigger",
}
.fail()
}
#[tracing::instrument(skip_all)]
pub async fn alter_database(
&self,

View File

@@ -336,6 +336,14 @@ pub enum Error {
#[snafu(implicit)]
location: Location,
},
#[cfg(feature = "enterprise")]
#[snafu(display("Duplicate clauses `{}` in a statement", clause))]
DuplicateClause {
clause: String,
#[snafu(implicit)]
location: Location,
},
}
impl ErrorExt for Error {
@@ -357,7 +365,9 @@ impl ErrorExt for Error {
| InvalidDefault { .. } => StatusCode::InvalidSyntax,
#[cfg(feature = "enterprise")]
MissingClause { .. } | MissingNotifyChannel { .. } => StatusCode::InvalidSyntax,
MissingClause { .. } | MissingNotifyChannel { .. } | DuplicateClause { .. } => {
StatusCode::InvalidSyntax
}
InvalidColumnOption { .. }
| InvalidTableOptionValue { .. }

View File

@@ -12,6 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
#[cfg(feature = "enterprise")]
pub mod trigger;
use std::collections::HashMap;
use common_query::AddColumnLocation;
@@ -43,6 +46,11 @@ impl ParserContext<'_> {
Token::Word(w) => match w.keyword {
Keyword::DATABASE => self.parse_alter_database().map(Statement::AlterDatabase),
Keyword::TABLE => self.parse_alter_table().map(Statement::AlterTable),
#[cfg(feature = "enterprise")]
Keyword::TRIGGER => {
self.parser.next_token();
self.parse_alter_trigger()
}
_ => self.expected("DATABASE or TABLE after ALTER", self.parser.peek_token()),
},
unexpected => self.unsupported(unexpected.to_string()),

View File

@@ -0,0 +1,791 @@
use snafu::{ensure, ResultExt};
use sqlparser::ast::Ident;
use sqlparser::parser::Parser;
use sqlparser::tokenizer::Token;
use crate::error::{self, DuplicateClauseSnafu, InvalidSqlSnafu, Result};
use crate::parser::ParserContext;
use crate::parsers::create_parser::trigger::{ANNOTATIONS, LABELS, NOTIFY, ON};
use crate::statements::alter::trigger::{
AlterTrigger, AlterTriggerOperation, AnnotationChange, AnnotationOperations, LabelChange,
LabelOperations, NotifyChannelChange, NotifyChannelOperations,
};
use crate::statements::create::trigger::NotifyChannel;
use crate::statements::statement::Statement;
use crate::statements::OptionMap;
/// Some keywords about trigger.
pub const RENAME: &str = "RENAME";
pub const TO: &str = "TO";
pub const SET: &str = "SET";
pub const ADD: &str = "ADD";
pub const MODIFY: &str = "MODIFY";
pub const DROP: &str = "DROP";
impl<'a> ParserContext<'a> {
/// Parses an `ALTER TRIGGER` statement.
///
/// ```sql
/// ALTER TRIGGER <trigger_name>
/// [alter_option [alter_option] ...]
///
/// alter_option: {
/// RENAME TO <new_trigger_name>
/// | ON (<query_expression>) EVERY <interval_expression>
/// | [SET] LABELS (<label_name>=<label_val>, ...)
/// | ADD LABELS (<label_name>=<label_val>, ...)
/// | MODIFY LABELS (<label_name>=<label_val>, ...)
/// | DROP LABELS (<label_name1>, <label_name2>, ...)
/// | [SET] ANNOTATIONS (<annotation_name>=<annotation_val>, ...)
/// | ADD ANNOTATIONS (<annotation_name>=<annotation_val>, ...)
/// | MODIFY ANNOTATIONS (<annotation_name>=<annotation_val>, ...)
/// | DROP ANNOTATIONS (<annotation_name1>, <annotation_name2>, ...)
/// | [SET] NOTIFY(
/// WEBHOOK <notify_name1> URL '<url1>' [WITH (<parameter1>=<value1>, ...)],
/// WEBHOOK <notify_name2> URL '<url2>' [WITH (<parameter2>=<value2>, ...)]
/// )
/// | ADD NOTIFY(
/// WEBHOOK <notify_name1> URL '<url1>' [WITH (<parameter1>=<value1>, ...)],
/// WEBHOOK <notify_name2> URL '<url2>' [WITH (<parameter2>=<value2>, ...)]
/// )
/// | DROP NOTIFY (<notify_name1>, <notify_name2>)
/// }
/// ```
pub(super) fn parse_alter_trigger(&mut self) -> Result<Statement> {
let trigger_name = self.intern_parse_table_name()?;
let mut new_trigger_name = None;
let mut new_query = None;
let mut new_interval = None;
let mut label_ops = None;
let mut annotation_ops = None;
let mut notify_ops = None;
loop {
let next_token = self.parser.peek_token();
match next_token.token {
Token::Word(w) if w.value.eq_ignore_ascii_case(RENAME) => {
self.parser.next_token();
let name = self.parse_rename_to(true)?;
let name = Self::canonicalize_identifier(name);
ensure!(
new_trigger_name.is_none(),
DuplicateClauseSnafu {
clause: "RENAME TO"
}
);
new_trigger_name.replace(name.value);
}
Token::Word(w) if w.value.eq_ignore_ascii_case(ON) => {
self.parser.next_token();
let (query, interval) = self.parse_trigger_on(true)?;
ensure!(
new_query.is_none() && new_interval.is_none(),
DuplicateClauseSnafu { clause: ON }
);
new_query.replace(query);
new_interval.replace(interval);
}
Token::Word(w) if w.value.eq_ignore_ascii_case(LABELS) => {
self.parser.next_token();
let labels = self.parse_trigger_labels(true)?;
apply_label_replacement(&mut label_ops, labels)?;
}
Token::Word(w) if w.value.eq_ignore_ascii_case(ANNOTATIONS) => {
self.parser.next_token();
let annotations = self.parse_trigger_annotations(true)?;
apply_annotation_replacement(&mut annotation_ops, annotations)?;
}
Token::Word(w) if w.value.eq_ignore_ascii_case(NOTIFY) => {
self.parser.next_token();
let channels = self.parse_trigger_notify(true)?;
apply_notify_replacement(&mut notify_ops, channels)?;
}
Token::Word(w) if w.value.eq_ignore_ascii_case(SET) => {
self.parser.next_token();
let next_token = self.parser.peek_token();
match next_token.token {
Token::Word(w) if w.value.eq_ignore_ascii_case(LABELS) => {
self.parser.next_token();
let labels = self.parse_trigger_labels(true)?;
apply_label_replacement(&mut label_ops, labels)?;
}
Token::Word(w) if w.value.eq_ignore_ascii_case(ANNOTATIONS) => {
self.parser.next_token();
let annotations = self.parse_trigger_annotations(true)?;
apply_annotation_replacement(&mut annotation_ops, annotations)?;
}
Token::Word(w) if w.value.eq_ignore_ascii_case(NOTIFY) => {
self.parser.next_token();
let channels = self.parse_trigger_notify(true)?;
apply_notify_replacement(&mut notify_ops, channels)?;
}
_ => {
return self.expected(
"`LABELS`, `ANNOTATIONS` or `NOTIFY` keyword after `SET`",
next_token,
)
}
}
}
Token::Word(w) if w.value.eq_ignore_ascii_case(ADD) => {
self.parser.next_token();
let next_token = self.parser.peek_token();
match next_token.token {
Token::Word(w) if w.value.eq_ignore_ascii_case(LABELS) => {
self.parser.next_token();
let labels = self.parse_trigger_labels(true)?;
apply_label_change(&mut label_ops, LabelChange::Add(labels))?;
}
Token::Word(w) if w.value.eq_ignore_ascii_case(ANNOTATIONS) => {
self.parser.next_token();
let annotations = self.parse_trigger_annotations(true)?;
apply_annotation_change(
&mut annotation_ops,
AnnotationChange::Add(annotations),
)?;
}
Token::Word(w) if w.value.eq_ignore_ascii_case(NOTIFY) => {
self.parser.next_token();
let channels = self.parse_trigger_notify(true)?;
apply_notify_change(
&mut notify_ops,
NotifyChannelChange::Add(channels),
)?;
}
_ => {
return self.expected(
"`LABELS`, `ANNOTATIONS` or `NOTIFY` keyword after `ADD`",
next_token,
);
}
}
}
Token::Word(w) if w.value.eq_ignore_ascii_case(MODIFY) => {
self.parser.next_token();
let next_token = self.parser.peek_token();
match next_token.token {
Token::Word(w) if w.value.eq_ignore_ascii_case(LABELS) => {
self.parser.next_token();
let labels = self.parse_trigger_labels(true)?;
apply_label_change(&mut label_ops, LabelChange::Modify(labels))?;
}
Token::Word(w) if w.value.eq_ignore_ascii_case(ANNOTATIONS) => {
self.parser.next_token();
let annotations = self.parse_trigger_annotations(true)?;
apply_annotation_change(
&mut annotation_ops,
AnnotationChange::Modify(annotations),
)?;
}
_ => {
return self.expected(
"`LABELS` or `ANNOTATIONS` keyword after `MODIFY`",
next_token,
);
}
}
}
Token::Word(w) if w.value.eq_ignore_ascii_case(DROP) => {
self.parser.next_token();
let next_token = self.parser.peek_token();
match next_token.token {
Token::Word(w) if w.value.eq_ignore_ascii_case(LABELS) => {
self.parser.next_token();
let names = self.parse_trigger_label_names(true)?;
apply_label_change(&mut label_ops, LabelChange::Drop(names))?;
}
Token::Word(w) if w.value.eq_ignore_ascii_case(ANNOTATIONS) => {
self.parser.next_token();
let names = self.parse_trigger_annotation_names(true)?;
apply_annotation_change(
&mut annotation_ops,
AnnotationChange::Drop(names),
)?;
}
Token::Word(w) if w.value.eq_ignore_ascii_case(NOTIFY) => {
self.parser.next_token();
let channels = self.parse_trigger_notify_names(true)?;
apply_notify_change(
&mut notify_ops,
NotifyChannelChange::Drop(channels),
)?;
}
_ => {
return self.expected(
"`LABELS`, `ANNOTATIONS` or `NOTIFY` keyword after `DROP`",
next_token,
);
}
}
}
Token::EOF => break,
_ => {
return self.expected(
"`ON` or `SET` or `ADD` or `MODIFY` or `DROP` keyword",
next_token,
);
}
}
}
if new_trigger_name.is_none()
&& new_query.is_none()
&& new_interval.is_none()
&& label_ops.is_none()
&& annotation_ops.is_none()
&& notify_ops.is_none()
{
return self.expected("alter option", self.parser.peek_token());
}
let operation = AlterTriggerOperation {
rename: new_trigger_name,
new_query,
new_interval,
label_operations: label_ops,
annotation_operations: annotation_ops,
notify_channel_operations: notify_ops,
};
let alter_trigger = AlterTrigger {
trigger_name,
operation,
};
Ok(Statement::AlterTrigger(alter_trigger))
}
/// The SQL format as follows:
///
/// ```sql
/// RENAME TO <new_trigger_name>
/// ```
///
/// ## Parameters
///
/// - `is_first_keyword_matched`: indicates whether the first keyword `RENAME`
/// has been matched.
fn parse_rename_to(&mut self, is_first_keyword_matched: bool) -> Result<Ident> {
if !is_first_keyword_matched {
if let Token::Word(w) = self.parser.peek_token().token
&& w.value.eq_ignore_ascii_case(RENAME)
{
self.parser.next_token();
} else {
return self.expected("`RENAME` keyword", self.parser.peek_token());
}
}
let next_token = self.parser.peek_token();
match next_token.token {
Token::Word(w) if w.value.eq_ignore_ascii_case(TO) => {
self.parser.next_token();
self.parser.parse_identifier().context(error::SyntaxSnafu)
}
_ => self.expected("`TO` keyword after `RENAME`", next_token),
}
}
/// The SQL format as follows:
///
/// ```sql
/// LABELS (key1, key2)
/// ```
///
/// ## Parameters
///
/// - `is_first_keyword_matched`: indicates whether the first keyword `LABELS`
/// has been matched.
fn parse_trigger_label_names(&mut self, is_first_keyword_matched: bool) -> Result<Vec<String>> {
if !is_first_keyword_matched {
if let Token::Word(w) = self.parser.peek_token().token
&& w.value.eq_ignore_ascii_case(LABELS)
{
self.parser.next_token();
} else {
return self.expected("`LABELS` keyword", self.parser.peek_token());
}
}
if let Token::LParen = self.parser.peek_token().token {
self.parser.next_token();
} else {
return self.expected("`(` after `LABELS`", self.parser.peek_token());
}
let label_names = self
.parser
.parse_comma_separated0(Parser::parse_identifier, Token::RParen)
.context(error::SyntaxSnafu)?
.into_iter()
.map(|ident| ident.value.to_lowercase())
.collect::<Vec<_>>();
self.parser
.expect_token(&Token::RParen)
.context(error::SyntaxSnafu)?;
Ok(label_names)
}
/// The SQL format as follows:
///
/// ```sql
/// ANNOTATIONS (key1, key2)
/// ```
///
/// ## Parameters
///
/// - `is_first_keyword_matched`: indicates whether the first keyword `ANNOTATIONS`
/// has been matched.
fn parse_trigger_annotation_names(
&mut self,
is_first_keyword_matched: bool,
) -> Result<Vec<String>> {
if !is_first_keyword_matched {
if let Token::Word(w) = self.parser.peek_token().token
&& w.value.eq_ignore_ascii_case(ANNOTATIONS)
{
self.parser.next_token();
} else {
return self.expected("`ANNOTATIONS` keyword", self.parser.peek_token());
}
}
if let Token::LParen = self.parser.peek_token().token {
self.parser.next_token();
} else {
return self.expected("`(` after `ANNOTATIONS`", self.parser.peek_token());
}
let annotation_names = self
.parser
.parse_comma_separated0(Parser::parse_identifier, Token::RParen)
.context(error::SyntaxSnafu)?
.into_iter()
.map(|ident| ident.value.to_lowercase())
.collect::<Vec<_>>();
self.parser
.expect_token(&Token::RParen)
.context(error::SyntaxSnafu)?;
Ok(annotation_names)
}
/// The SQL format as follows:
///
/// ```sql
/// NOTIFY (key1, key2)
/// ```
///
/// ## Parameters
///
/// - `is_first_keyword_matched`: indicates whether the first keyword `NOTIFY`
/// has been matched.
fn parse_trigger_notify_names(
&mut self,
is_first_keyword_matched: bool,
) -> Result<Vec<String>> {
if !is_first_keyword_matched {
if let Token::Word(w) = self.parser.peek_token().token
&& w.value.eq_ignore_ascii_case(NOTIFY)
{
self.parser.next_token();
} else {
return self.expected("`NOTIFY` keyword", self.parser.peek_token());
}
}
if let Token::LParen = self.parser.peek_token().token {
self.parser.next_token();
} else {
return self.expected("`(` after `NOTIFY`", self.parser.peek_token());
}
let notify_names = self
.parser
.parse_comma_separated0(Parser::parse_identifier, Token::RParen)
.context(error::SyntaxSnafu)?
.into_iter()
.map(|ident| ident.value.to_lowercase())
.collect::<Vec<_>>();
self.parser
.expect_token(&Token::RParen)
.context(error::SyntaxSnafu)?;
Ok(notify_names)
}
}
fn apply_label_replacement(
label_ops: &mut Option<LabelOperations>,
labels: OptionMap,
) -> Result<()> {
match label_ops {
Some(LabelOperations::ReplaceAll(_)) => DuplicateClauseSnafu {
clause: "SET LABELS",
}
.fail(),
Some(LabelOperations::PartialChanges(_)) => InvalidSqlSnafu {
msg: "SET LABELS cannot be used with ADD/MODIFY/DROP LABELS",
}
.fail(),
None => {
*label_ops = Some(LabelOperations::ReplaceAll(labels));
Ok(())
}
}
}
fn apply_annotation_replacement(
annotation_ops: &mut Option<AnnotationOperations>,
annotations: OptionMap,
) -> Result<()> {
match annotation_ops {
Some(AnnotationOperations::ReplaceAll(_)) => DuplicateClauseSnafu {
clause: "SET ANNOTATIONS",
}
.fail(),
Some(AnnotationOperations::PartialChanges(_)) => InvalidSqlSnafu {
msg: "SET ANNOTATIONS cannot be used with ADD/MODIFY/DROP ANNOTATIONS",
}
.fail(),
None => {
*annotation_ops = Some(AnnotationOperations::ReplaceAll(annotations));
Ok(())
}
}
}
fn apply_notify_replacement(
notify_channel_ops: &mut Option<NotifyChannelOperations>,
channels: Vec<NotifyChannel>,
) -> Result<()> {
match notify_channel_ops {
Some(NotifyChannelOperations::ReplaceAll(_)) => DuplicateClauseSnafu {
clause: "SET NOTIFY",
}
.fail(),
Some(NotifyChannelOperations::PartialChanges(_)) => InvalidSqlSnafu {
msg: "SET NOTIFY cannot be used with ADD/DROP NOTIFY",
}
.fail(),
None => {
*notify_channel_ops = Some(NotifyChannelOperations::ReplaceAll(channels));
Ok(())
}
}
}
fn apply_label_change(
label_ops: &mut Option<LabelOperations>,
label_change: LabelChange,
) -> Result<()> {
match label_ops {
Some(LabelOperations::ReplaceAll(_)) => InvalidSqlSnafu {
msg: "SET LABELS cannot be used with ADD/MODIFY/DROP LABELS",
}
.fail(),
Some(LabelOperations::PartialChanges(label_changes)) => {
label_changes.push(label_change);
Ok(())
}
None => {
*label_ops = Some(LabelOperations::PartialChanges(vec![label_change]));
Ok(())
}
}
}
fn apply_annotation_change(
annotation_ops: &mut Option<AnnotationOperations>,
annotation_change: AnnotationChange,
) -> Result<()> {
match annotation_ops {
Some(AnnotationOperations::ReplaceAll(_)) => InvalidSqlSnafu {
msg: "SET ANNOTATIONS cannot be used with ADD/MODIFY/DROP ANNOTATIONS",
}
.fail(),
Some(AnnotationOperations::PartialChanges(label_changes)) => {
label_changes.push(annotation_change);
Ok(())
}
None => {
*annotation_ops = Some(AnnotationOperations::PartialChanges(vec![
annotation_change,
]));
Ok(())
}
}
}
fn apply_notify_change(
ops: &mut Option<NotifyChannelOperations>,
change: NotifyChannelChange,
) -> Result<()> {
match ops {
Some(NotifyChannelOperations::ReplaceAll(_)) => InvalidSqlSnafu {
msg: "SET NOTIFY cannot be used with ADD/DROP NOTIFY",
}
.fail(),
Some(NotifyChannelOperations::PartialChanges(changes)) => {
changes.push(change);
Ok(())
}
None => {
*ops = Some(NotifyChannelOperations::PartialChanges(vec![change]));
Ok(())
}
}
}
#[cfg(test)]
mod tests {
use crate::dialect::GreptimeDbDialect;
use crate::parser::ParserContext;
use crate::parsers::alter_parser::trigger::{apply_label_change, apply_label_replacement};
use crate::statements::alter::trigger::{LabelChange, LabelOperations};
use crate::statements::statement::Statement;
use crate::statements::OptionMap;
#[test]
fn test_parse_alter_without_alter_options() {
// Failed case: No alter options.
// Note: "ALTER TRIGGER" is matched.
let sql = r#"public.old_trigger"#;
let mut ctx = ParserContext::new(&GreptimeDbDialect {}, sql).unwrap();
let result = ctx.parse_alter_trigger();
assert!(result.is_err());
}
#[test]
fn test_parse_set_query() {
// Passed case: SET QUERY.
// Note: "ALTER TRIGGER" is matched.
let sql = r#"public.old_trigger ON (SELECT * FROM test_table) EVERY '5 minute'::INTERVAL"#;
let mut ctx = ParserContext::new(&GreptimeDbDialect {}, sql).unwrap();
let stmt = ctx.parse_alter_trigger().unwrap();
let Statement::AlterTrigger(alter) = stmt else {
panic!("Expected AlterTrigger statement");
};
assert!(alter.operation.new_query.is_some());
assert!(alter.operation.new_interval.is_some());
assert_eq!(alter.operation.new_interval.unwrap(), 300);
assert!(alter.operation.rename.is_none());
assert!(alter.operation.label_operations.is_none());
assert!(alter.operation.annotation_operations.is_none());
assert!(alter.operation.notify_channel_operations.is_none());
// Failed case: multi SET QUERY.
let sql = r#"public.old_trigger ON (SELECT * FROM test_table) EVERY '5 minute'::INTERVAL
ON (SELECT * FROM another_table) EVERY '10 minute'::INTERVAL"#;
let mut ctx = ParserContext::new(&GreptimeDbDialect {}, sql).unwrap();
let result = ctx.parse_alter_trigger();
assert!(result.is_err());
}
#[test]
fn test_parse_alter_trigger_rename() {
// Note: "ALTER TRIGGER" is matched.
let sql = r#"public.old_trigger RENAME TO `newTrigger`"#;
let mut ctx = ParserContext::new(&GreptimeDbDialect {}, sql).unwrap();
let stmt = ctx.parse_alter_trigger().unwrap();
let Statement::AlterTrigger(alter) = stmt else {
panic!("Expected AlterTrigger statement");
};
let trigger_name = alter.trigger_name.0;
assert_eq!(trigger_name.len(), 2);
assert_eq!(trigger_name[0].value, "public");
assert_eq!(trigger_name[1].value, "old_trigger");
assert_eq!(alter.operation.rename, Some("newTrigger".to_string()));
}
#[test]
fn test_parse_alter_trigger_labels() {
// Passed case: SET LABELS.
// Note: "ALTER TRIGGER" is matched.
let sql = r#"test_trigger SET LABELS (Key1='value1', 'KEY2'='value2')"#;
let mut ctx = ParserContext::new(&GreptimeDbDialect {}, sql).unwrap();
let stmt = ctx.parse_alter_trigger().unwrap();
let Statement::AlterTrigger(alter) = stmt else {
panic!("Expected AlterTrigger statement");
};
let Some(LabelOperations::ReplaceAll(labels)) = alter.operation.label_operations else {
panic!("Expected ReplaceAll label operations");
};
assert_eq!(labels.get("key1"), Some(&"value1".to_string()));
assert_eq!(labels.get("KEY2"), Some(&"value2".to_string()));
// Passed case: multiple ADD/DROP/MODIFY LABELS.
let sql = r#"test_trigger ADD LABELS (key1='value1') MODIFY LABELS (key2='value2') DROP LABELS ('key3')"#;
let mut ctx = ParserContext::new(&GreptimeDbDialect {}, sql).unwrap();
let stmt = ctx.parse_alter_trigger().unwrap();
let Statement::AlterTrigger(alter) = stmt else {
panic!("Expected AlterTrigger statement");
};
let Some(LabelOperations::PartialChanges(changes)) = alter.operation.label_operations
else {
panic!("Expected PartialChanges label operations");
};
assert_eq!(changes.len(), 3);
let expected_changes = vec![
LabelChange::Add(OptionMap::from([(
"key1".to_string(),
"value1".to_string(),
)])),
LabelChange::Modify(OptionMap::from([(
"key2".to_string(),
"value2".to_string(),
)])),
LabelChange::Drop(vec!["key3".to_string()]),
];
assert_eq!(changes, expected_changes);
// Failed case: Duplicate SET LABELS.
let sql =
r#"test_trigger SET LABELS (key1='value1', key2='value2') SET LABELS (key3='value3')"#;
let mut ctx = ParserContext::new(&GreptimeDbDialect {}, sql).unwrap();
let result = ctx.parse_alter_trigger();
assert!(result.is_err());
// Failed case: SET LABELS with ADD/MODIFY/DROP LABELS.
let sql =
r#"test_trigger SET LABELS (key1='value1', key2='value2') ADD LABELS (key3='value3')"#;
let mut ctx = ParserContext::new(&GreptimeDbDialect {}, sql).unwrap();
let result = ctx.parse_alter_trigger();
assert!(result.is_err());
let sql = r#"test_trigger SET LABELS (key1='value1', key2='value2') DROP LABELS ('key3')"#;
let mut ctx = ParserContext::new(&GreptimeDbDialect {}, sql).unwrap();
let result = ctx.parse_alter_trigger();
assert!(result.is_err());
}
#[test]
fn test_parse_rename_to() {
let sql = r#"RENAME TO new_trigger_name"#;
let mut ctx = ParserContext::new(&GreptimeDbDialect {}, sql).unwrap();
let new_trigger_name = ctx.parse_rename_to(false).unwrap();
assert_eq!(new_trigger_name.value, "new_trigger_name");
assert_eq!(new_trigger_name.quote_style, None);
let sql = r#"RENAME TO "New_Trigger_Name""#;
let mut ctx = ParserContext::new(&GreptimeDbDialect {}, sql).unwrap();
let new_trigger_name = ctx.parse_rename_to(false).unwrap();
assert_eq!(new_trigger_name.value, "New_Trigger_Name");
assert_eq!(new_trigger_name.quote_style, Some('"'));
// Failed case: Missing new_trigger_name.
let sql = r#"RENAME TO"#;
let mut ctx = ParserContext::new(&GreptimeDbDialect {}, sql).unwrap();
let result = ctx.parse_rename_to(false);
assert!(result.is_err());
// Failed case: Missing `TO` keyword.
let sql = r#"RENAME"#;
let mut ctx = ParserContext::new(&GreptimeDbDialect {}, sql).unwrap();
let result = ctx.parse_rename_to(false);
assert!(result.is_err());
}
#[test]
fn test_parse_trigger_label_names() {
let sql = r#"LABELS (key1, KEY2, key3)"#;
let mut ctx = ParserContext::new(&GreptimeDbDialect {}, sql).unwrap();
let labels = ctx.parse_trigger_label_names(false).unwrap();
let expected_labels = vec!["key1".to_string(), "key2".to_string(), "key3".to_string()];
assert_eq!(labels, expected_labels,);
let sql = r#"LABELS ('key1', `key2`, "key3",)"#;
let mut ctx = ParserContext::new(&GreptimeDbDialect {}, sql).unwrap();
let labels = ctx.parse_trigger_label_names(false).unwrap();
let expected_labels = vec!["key1".to_string(), "key2".to_string(), "key3".to_string()];
assert_eq!(labels, expected_labels,);
let sql = r#"LABELS ()"#;
let mut ctx = ParserContext::new(&GreptimeDbDialect {}, sql).unwrap();
let labels = ctx.parse_trigger_label_names(false).unwrap();
assert!(labels.is_empty());
}
#[test]
fn test_parse_trigger_annotation_names() {
let sql = r#"ANNOTATIONS (key1, KEY2, key3)"#;
let mut ctx = ParserContext::new(&GreptimeDbDialect {}, sql).unwrap();
let annotations = ctx.parse_trigger_annotation_names(false).unwrap();
let expected_annotations = vec!["key1".to_string(), "key2".to_string(), "key3".to_string()];
assert_eq!(annotations, expected_annotations);
let sql = r#"ANNOTATIONS (key1, `Key2`, "key3")"#;
let mut ctx = ParserContext::new(&GreptimeDbDialect {}, sql).unwrap();
let annotations = ctx.parse_trigger_annotation_names(false).unwrap();
let expected_annotations = vec!["key1".to_string(), "key2".to_string(), "key3".to_string()];
assert_eq!(annotations, expected_annotations);
let sql = r#"ANNOTATIONS ()"#;
let mut ctx = ParserContext::new(&GreptimeDbDialect {}, sql).unwrap();
let annotations = ctx.parse_trigger_annotation_names(false).unwrap();
assert!(annotations.is_empty());
}
#[test]
fn test_parse_trigger_notify_names() {
let sql = r#"NOTIFY (key1, KEY2, key3)"#;
let mut ctx = ParserContext::new(&GreptimeDbDialect {}, sql).unwrap();
let notify_names = ctx.parse_trigger_notify_names(false).unwrap();
let expected_notify_names =
vec!["key1".to_string(), "key2".to_string(), "key3".to_string()];
assert_eq!(notify_names, expected_notify_names);
let sql = r#"NOTIFY (key1, key2,)"#;
let mut ctx = ParserContext::new(&GreptimeDbDialect {}, sql).unwrap();
let notify_names = ctx.parse_trigger_notify_names(false).unwrap();
let expected_notify_names = vec!["key1".to_string(), "key2".to_string()];
assert_eq!(notify_names, expected_notify_names);
}
#[test]
fn test_apply_label_changes() {
let mut label_ops = None;
let mut labels = OptionMap::default();
labels.insert("key1".to_string(), "value1".to_string());
labels.insert("key2".to_string(), "value2".to_string());
apply_label_replacement(&mut label_ops, labels.clone()).unwrap();
assert!(label_ops.is_some());
// Set operations are mutually exclusive.
let result = apply_label_replacement(&mut label_ops, labels.clone());
assert!(result.is_err());
// Set operations and Change operations are mutually exclusive.
let result =
apply_label_change(&mut label_ops, LabelChange::Drop(vec!["key1".to_string()]));
assert!(result.is_err());
let mut label_ops = None;
let result = apply_label_change(&mut label_ops, LabelChange::Add(labels.clone()));
assert!(result.is_ok());
// Partial changes are not mutually exclusive.
let result = apply_label_change(&mut label_ops, LabelChange::Modify(labels));
assert!(result.is_ok());
let result =
apply_label_change(&mut label_ops, LabelChange::Drop(vec!["key1".to_string()]));
assert!(result.is_ok());
let ops = label_ops.unwrap();
if let LabelOperations::PartialChanges(changes) = ops {
assert_eq!(changes.len(), 3);
assert!(matches!(changes[0], LabelChange::Add(_)));
assert!(matches!(changes[1], LabelChange::Modify(_)));
assert!(matches!(changes[2], LabelChange::Drop(_)));
} else {
panic!("Expected PartialChanges, got {:?}", ops);
}
}
}

View File

@@ -732,8 +732,8 @@ impl<'a> ParserContext<'a> {
msg: "dimension should be a positive integer",
})?;
let options = HashMap::from_iter([(VECTOR_OPT_DIM.to_string(), dimension.to_string())]);
column_extensions.vector_options = Some(options.into());
let options = OptionMap::from([(VECTOR_OPT_DIM.to_string(), dimension.to_string())]);
column_extensions.vector_options = Some(options);
}
// parse index options in column definition

View File

@@ -82,9 +82,7 @@ impl<'a> ParserContext<'a> {
let channels = self.parse_trigger_notify(true)?;
notify_channels.extend(channels);
}
Token::EOF => {
break;
}
Token::EOF => break,
_ => {
return self.expected(
"`ON` or `LABELS` or `ANNOTATIONS` or `NOTIFY` keyword",
@@ -127,7 +125,10 @@ impl<'a> ParserContext<'a> {
///
/// - `is_first_keyword_matched`: indicates whether the first keyword `ON`
/// has been matched.
fn parse_trigger_on(&mut self, is_first_keyword_matched: bool) -> Result<(Box<Query>, u64)> {
pub(crate) fn parse_trigger_on(
&mut self,
is_first_keyword_matched: bool,
) -> Result<(Box<Query>, u64)> {
if !is_first_keyword_matched {
if let Token::Word(w) = self.parser.peek_token().token
&& w.value.eq_ignore_ascii_case(ON)
@@ -166,7 +167,10 @@ impl<'a> ParserContext<'a> {
///
/// - `is_first_keyword_matched`: indicates whether the first keyword `LABELS`
/// has been matched.
fn parse_trigger_labels(&mut self, is_first_keyword_matched: bool) -> Result<OptionMap> {
pub(crate) fn parse_trigger_labels(
&mut self,
is_first_keyword_matched: bool,
) -> Result<OptionMap> {
if !is_first_keyword_matched {
if let Token::Word(w) = self.parser.peek_token().token
&& w.value.eq_ignore_ascii_case(LABELS)
@@ -204,7 +208,10 @@ impl<'a> ParserContext<'a> {
///
/// - `is_first_keyword_matched`: indicates whether the first keyword
/// `ANNOTATIONS` has been matched.
fn parse_trigger_annotations(&mut self, is_first_keyword_matched: bool) -> Result<OptionMap> {
pub(crate) fn parse_trigger_annotations(
&mut self,
is_first_keyword_matched: bool,
) -> Result<OptionMap> {
if !is_first_keyword_matched {
if let Token::Word(w) = self.parser.peek_token().token
&& w.value.eq_ignore_ascii_case(ANNOTATIONS)
@@ -245,7 +252,7 @@ impl<'a> ParserContext<'a> {
///
/// - `is_first_keyword_matched`: indicates whether the first keyword `NOTIFY`
/// has been matched.
fn parse_trigger_notify(
pub(crate) fn parse_trigger_notify(
&mut self,
is_first_keyword_matched: bool,
) -> Result<Vec<NotifyChannel>> {
@@ -330,6 +337,7 @@ impl<'a> ParserContext<'a> {
}
let notify_ident = self.parser.parse_identifier().context(error::SyntaxSnafu)?;
let notify_ident = Self::canonicalize_identifier(notify_ident);
if let Token::Word(w) = self.parser.peek_token().token
&& w.value.eq_ignore_ascii_case(URL)

View File

@@ -318,8 +318,6 @@ pub fn concrete_data_type_to_sql_data_type(data_type: &ConcreteDataType) -> Resu
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use api::v1::ColumnDataType;
use datatypes::schema::{
FulltextAnalyzer, COLUMN_FULLTEXT_OPT_KEY_ANALYZER, COLUMN_FULLTEXT_OPT_KEY_CASE_SENSITIVE,
@@ -674,19 +672,16 @@ mod tests {
options: vec![],
},
extensions: ColumnExtensions {
fulltext_index_options: Some(
HashMap::from_iter([
(
COLUMN_FULLTEXT_OPT_KEY_ANALYZER.to_string(),
"English".to_string(),
),
(
COLUMN_FULLTEXT_OPT_KEY_CASE_SENSITIVE.to_string(),
"true".to_string(),
),
])
.into(),
),
fulltext_index_options: Some(OptionMap::from([
(
COLUMN_FULLTEXT_OPT_KEY_ANALYZER.to_string(),
"English".to_string(),
),
(
COLUMN_FULLTEXT_OPT_KEY_CASE_SENSITIVE.to_string(),
"true".to_string(),
),
])),
vector_options: None,
skipping_index_options: None,
inverted_index_options: None,

View File

@@ -12,6 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
#[cfg(feature = "enterprise")]
pub mod trigger;
use std::fmt::{Debug, Display};
use api::v1;

View File

@@ -0,0 +1,332 @@
use std::fmt::{Display, Formatter};
use serde::Serialize;
use sqlparser::ast::{ObjectName, Query};
use sqlparser_derive::{Visit, VisitMut};
use crate::statements::create::trigger::NotifyChannel;
use crate::statements::OptionMap;
#[derive(Debug, Clone, PartialEq, Eq, Visit, VisitMut, Serialize)]
pub struct AlterTrigger {
pub trigger_name: ObjectName,
pub operation: AlterTriggerOperation,
}
#[derive(Debug, Default, Clone, PartialEq, Eq, Visit, VisitMut, Serialize)]
pub struct AlterTriggerOperation {
pub rename: Option<String>,
pub new_query: Option<Box<Query>>,
/// The new interval of exec query. Unit is second.
pub new_interval: Option<u64>,
pub label_operations: Option<LabelOperations>,
pub annotation_operations: Option<AnnotationOperations>,
pub notify_channel_operations: Option<NotifyChannelOperations>,
}
impl Display for AlterTrigger {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "ALTER TRIGGER {}", self.trigger_name)?;
let operation = &self.operation;
if let Some(new_name) = &operation.rename {
writeln!(f)?;
write!(f, "RENAME TO {}", new_name)?;
}
if let Some((new_query, new_interval)) =
operation.new_query.as_ref().zip(operation.new_interval)
{
writeln!(f)?;
write!(f, "ON {}", new_query)?;
write!(f, " EVERY {} SECONDS", new_interval)?;
}
if let Some(label_ops) = &operation.label_operations {
match label_ops {
LabelOperations::ReplaceAll(map) => {
writeln!(f)?;
write!(f, "SET LABELS ({})", map.kv_pairs().join(", "))?
}
LabelOperations::PartialChanges(changes) => {
for change in changes {
writeln!(f)?;
write!(f, "{}", change)?;
}
}
}
}
if let Some(annotation_ops) = &operation.annotation_operations {
match annotation_ops {
AnnotationOperations::ReplaceAll(map) => {
writeln!(f)?;
write!(f, "SET ANNOTATIONS ({})", map.kv_pairs().join(", "))?
}
AnnotationOperations::PartialChanges(changes) => {
for change in changes {
writeln!(f)?;
write!(f, "{}", change)?;
}
}
}
}
if let Some(notify_channel_ops) = &operation.notify_channel_operations {
match notify_channel_ops {
NotifyChannelOperations::ReplaceAll(channels) => {
if !channels.is_empty() {
writeln!(f)?;
writeln!(f, "SET NOTIFY")?;
for channel in channels {
write!(f, " {}", channel)?;
}
}
}
NotifyChannelOperations::PartialChanges(changes) => {
for change in changes {
writeln!(f)?;
write!(f, "{}", change)?;
}
}
}
}
Ok(())
}
}
/// The operations which describe how to update labels.
#[derive(Debug, Clone, PartialEq, Eq, Visit, VisitMut, Serialize)]
pub enum LabelOperations {
ReplaceAll(OptionMap),
PartialChanges(Vec<LabelChange>),
}
#[derive(Debug, Clone, PartialEq, Eq, Visit, VisitMut, Serialize)]
pub enum LabelChange {
/// Add new labels.
///
/// Note: if the labels to add already exists, an error will be reported.
Add(OptionMap),
/// Modify existing labels.
///
/// Note: if the labels to update does not exist, an error will be reported.
Modify(OptionMap),
/// Drop specified labels.
///
/// Note: if the labels to drop does not exist, an error will be reported.
Drop(Vec<String>),
}
impl Display for LabelChange {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
LabelChange::Add(map) => {
if map.is_empty() {
return Ok(());
}
write!(f, "ADD LABELS ({})", map.kv_pairs().join(", "))
}
LabelChange::Modify(map) => {
if map.is_empty() {
return Ok(());
}
write!(f, "MODIFY LABELS ({})", map.kv_pairs().join(", "))
}
LabelChange::Drop(names) => {
if names.is_empty() {
return Ok(());
}
write!(f, "DROP LABELS ({})", names.join(", "))
}
}
}
}
/// The operations which describe how to update annotations.
#[derive(Debug, Clone, PartialEq, Eq, Visit, VisitMut, Serialize)]
pub enum AnnotationOperations {
ReplaceAll(OptionMap),
PartialChanges(Vec<AnnotationChange>),
}
#[derive(Debug, Clone, PartialEq, Eq, Visit, VisitMut, Serialize)]
pub enum AnnotationChange {
/// Add new annotations.
///
/// Note: if the annotations to add already exists, an error will be reported.
Add(OptionMap),
/// Modify existing annotations.
///
/// Note: if the annotations to update does not exist, an error will be
/// reported.
Modify(OptionMap),
/// Drop specified annotations.
///
/// Note: if the annotations to drop does not exist, an error will be
/// reported.
Drop(Vec<String>),
}
impl Display for AnnotationChange {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
AnnotationChange::Add(map) => {
if map.is_empty() {
return Ok(());
}
write!(f, "ADD ANNOTATIONS ({})", map.kv_pairs().join(", "))
}
AnnotationChange::Modify(map) => {
if map.is_empty() {
return Ok(());
}
write!(f, "MODIFY ANNOTATIONS ({})", map.kv_pairs().join(", "))
}
AnnotationChange::Drop(names) => {
if names.is_empty() {
return Ok(());
}
write!(f, "DROP ANNOTATIONS ({})", names.join(", "))
}
}
}
}
/// The operations which describe how to update notify channels.
#[derive(Debug, Clone, PartialEq, Eq, Visit, VisitMut, Serialize)]
pub enum NotifyChannelOperations {
ReplaceAll(Vec<NotifyChannel>),
PartialChanges(Vec<NotifyChannelChange>),
}
#[derive(Debug, Clone, PartialEq, Eq, Visit, VisitMut, Serialize)]
pub enum NotifyChannelChange {
/// Add new NotifyChannel's.
///
/// Note: if the NotifyChannel to add already exists, an error will be
/// reported.
Add(Vec<NotifyChannel>),
/// Drop specified NotifyChannel's.
///
/// Note: if the NotifyChannel to drop does not exist, an error will be
/// reported.
Drop(Vec<String>),
}
impl Display for NotifyChannelChange {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
NotifyChannelChange::Add(channels) => {
if channels.is_empty() {
return Ok(());
}
write!(f, "ADD NOTIFY(")?;
for (idx, channel) in channels.iter().enumerate() {
writeln!(f)?;
write!(f, " {}", channel)?;
if idx < channels.len() - 1 {
write!(f, ",")?;
}
}
write!(f, ")")?;
}
NotifyChannelChange::Drop(names) => {
if names.is_empty() {
return Ok(());
}
write!(f, "DROP NOTIFY ({})", names.join(", "))?;
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use sqlparser::ast::Ident;
use super::*;
use crate::dialect::GreptimeDbDialect;
use crate::parser::{ParseOptions, ParserContext};
use crate::statements::create::trigger::{AlertManagerWebhook, ChannelType};
use crate::statements::statement::Statement;
#[test]
fn test_display_label_change() {
let add = LabelChange::Add(OptionMap::from([("k1".to_string(), "v1".to_string())]));
let modify = LabelChange::Modify(OptionMap::from([("k2".to_string(), "v2".to_string())]));
let drop = LabelChange::Drop(vec!["k3".to_string(), "k4".to_string()]);
assert_eq!(add.to_string(), "ADD LABELS (k1 = 'v1')");
assert_eq!(modify.to_string(), "MODIFY LABELS (k2 = 'v2')");
assert_eq!(drop.to_string(), "DROP LABELS (k3, k4)");
}
#[test]
fn test_display_annotation_change() {
let add = AnnotationChange::Add(OptionMap::from([("a1".to_string(), "v1".to_string())]));
let modify =
AnnotationChange::Modify(OptionMap::from([("a2".to_string(), "v2".to_string())]));
let drop = AnnotationChange::Drop(vec!["a3".to_string(), "a4".to_string()]);
assert_eq!(add.to_string(), "ADD ANNOTATIONS (a1 = 'v1')");
assert_eq!(modify.to_string(), "MODIFY ANNOTATIONS (a2 = 'v2')");
assert_eq!(drop.to_string(), "DROP ANNOTATIONS (a3, a4)");
}
#[test]
fn test_display_notify_channel_change() {
let add_channel = NotifyChannel {
name: Ident::new("webhook1"),
channel_type: ChannelType::Webhook(AlertManagerWebhook {
url: Ident::new("http://example.com"),
options: OptionMap::default(),
}),
};
let add = NotifyChannelChange::Add(vec![add_channel]);
let expected = r#"ADD NOTIFY(
WEBHOOK webhook1 URL http://example.com)"#;
assert_eq!(expected, add.to_string(),);
let drop = NotifyChannelChange::Drop(vec!["webhook2".to_string(), "webhook3".to_string()]);
assert_eq!(drop.to_string(), "DROP NOTIFY (webhook2, webhook3)");
}
#[test]
fn test_display_alter_trigger() {
let sql = r#"ALTER TRIGGER my_trigger
ON (SELECT host AS host_label, cpu, memory FROM machine_monitor WHERE cpu > 2) EVERY '5 minute'::INTERVAL
RENAME TO new_trigger
ADD LABELS (k1 = 'v1', k2 = 'v2')
DROP LABELS (k3, k4)
SET ANNOTATIONS (a1 = 'v1', a2 = 'v2')
DROP NOTIFY (webhook1, webhook2)
ADD NOTIFY
(WEBHOOK webhook3 URL 'http://new3.com',
WEBHOOK webhook4 URL 'http://new4.com')"#;
let result =
ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default())
.unwrap();
assert_eq!(1, result.len());
let Statement::AlterTrigger(trigger) = &result[0] else {
panic!("Expected AlterTrigger statement");
};
let formatted = format!("{}", trigger);
let expected = r#"ALTER TRIGGER my_trigger
RENAME TO new_trigger
ON (SELECT host AS host_label, cpu, memory FROM machine_monitor WHERE cpu > 2) EVERY 300 SECONDS
ADD LABELS (k1 = 'v1', k2 = 'v2')
DROP LABELS (k3, k4)
SET ANNOTATIONS (a1 = 'v1', a2 = 'v2')
DROP NOTIFY (webhook1, webhook2)
ADD NOTIFY(
WEBHOOK webhook3 URL 'http://new3.com',
WEBHOOK webhook4 URL 'http://new4.com')"#;
assert_eq!(formatted, expected);
}
}

View File

@@ -98,10 +98,10 @@ impl OptionMap {
}
}
impl From<HashMap<String, String>> for OptionMap {
fn from(value: HashMap<String, String>) -> Self {
impl<I: IntoIterator<Item = (String, String)>> From<I> for OptionMap {
fn from(value: I) -> Self {
let mut result = OptionMap::default();
for (k, v) in value.into_iter() {
for (k, v) in value {
result.insert(k, v);
}
result

View File

@@ -83,6 +83,9 @@ pub enum Statement {
AlterTable(AlterTable),
/// ALTER DATABASE
AlterDatabase(AlterDatabase),
/// ALTER TRIGGER
#[cfg(feature = "enterprise")]
AlterTrigger(crate::statements::alter::trigger::AlterTrigger),
// Databases.
ShowDatabases(ShowDatabases),
// SHOW TABLES
@@ -203,6 +206,9 @@ impl Statement {
| Statement::Kill(_)
| Statement::Admin(_) => false,
#[cfg(feature = "enterprise")]
Statement::AlterTrigger(_) => false,
#[cfg(feature = "enterprise")]
Statement::CreateTrigger(_) | Statement::DropTrigger(_) => false,
}
@@ -230,6 +236,8 @@ impl Display for Statement {
Statement::CreateDatabase(s) => s.fmt(f),
Statement::AlterTable(s) => s.fmt(f),
Statement::AlterDatabase(s) => s.fmt(f),
#[cfg(feature = "enterprise")]
Statement::AlterTrigger(s) => s.fmt(f),
Statement::ShowDatabases(s) => s.fmt(f),
Statement::ShowTables(s) => s.fmt(f),
Statement::ShowTableStatus(s) => s.fmt(f),

View File

@@ -139,20 +139,20 @@ mod tests {
SELECT *
FROM
t
WHERE a =
WHERE a =
1
",
r"SELECT *
FROM
t
WHERE a =
WHERE a =
1
",
r"
SELECT *
FROM
t
WHERE a =
WHERE a =
1",
];