From a10b1d9885a3be53d1af9a8cab69d5bb71ac7bf1 Mon Sep 17 00:00:00 2001 From: fys <40801205+fengys1996@users.noreply.github.com> Date: Tue, 29 Jul 2025 19:07:31 +0800 Subject: [PATCH] feat: trigger alter parse (#6553) * feat: support trigger alter * fix: cargo fmt * fix: clippy * fix: some docs * fix: cr * fix: ON -> RENAME --- licenserc.toml | 2 + src/frontend/src/instance.rs | 2 + src/operator/src/statement.rs | 5 + src/operator/src/statement/ddl.rs | 15 + src/sql/src/error.rs | 12 +- src/sql/src/parsers/alter_parser.rs | 8 + src/sql/src/parsers/alter_parser/trigger.rs | 791 +++++++++++++++++++ src/sql/src/parsers/create_parser.rs | 4 +- src/sql/src/parsers/create_parser/trigger.rs | 22 +- src/sql/src/statements.rs | 25 +- src/sql/src/statements/alter.rs | 3 + src/sql/src/statements/alter/trigger.rs | 332 ++++++++ src/sql/src/statements/option_map.rs | 6 +- src/sql/src/statements/statement.rs | 8 + src/sql/src/util.rs | 6 +- 15 files changed, 1210 insertions(+), 31 deletions(-) create mode 100644 src/sql/src/parsers/alter_parser/trigger.rs create mode 100644 src/sql/src/statements/alter/trigger.rs diff --git a/licenserc.toml b/licenserc.toml index 3330dabe34..7bebc5fd23 100644 --- a/licenserc.toml +++ b/licenserc.toml @@ -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", diff --git a/src/frontend/src/instance.rs b/src/frontend/src/instance.rs index a0dc811051..02a36c0ad8 100644 --- a/src/frontend/src/instance.rs +++ b/src/frontend/src/instance.rs @@ -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 diff --git a/src/operator/src/statement.rs b/src/operator/src/statement.rs index 235bb4980d..b0e556a9b7 100644 --- a/src/operator/src/statement.rs +++ b/src/operator/src/statement.rs @@ -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() { diff --git a/src/operator/src/statement/ddl.rs b/src/operator/src/statement/ddl.rs index 707c8b90ae..f0cd784dae 100644 --- a/src/operator/src/statement/ddl.rs +++ b/src/operator/src/statement/ddl.rs @@ -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 { + crate::error::NotSupportedSnafu { + feat: "alter trigger", + } + .fail() + } + #[tracing::instrument(skip_all)] pub async fn alter_database( &self, diff --git a/src/sql/src/error.rs b/src/sql/src/error.rs index b38b1cbf78..c840311591 100644 --- a/src/sql/src/error.rs +++ b/src/sql/src/error.rs @@ -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 { .. } diff --git a/src/sql/src/parsers/alter_parser.rs b/src/sql/src/parsers/alter_parser.rs index ce2df198ab..00fd6f3eb1 100644 --- a/src/sql/src/parsers/alter_parser.rs +++ b/src/sql/src/parsers/alter_parser.rs @@ -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()), diff --git a/src/sql/src/parsers/alter_parser/trigger.rs b/src/sql/src/parsers/alter_parser/trigger.rs new file mode 100644 index 0000000000..6f4fb01a9b --- /dev/null +++ b/src/sql/src/parsers/alter_parser/trigger.rs @@ -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 + /// [alter_option [alter_option] ...] + /// + /// alter_option: { + /// RENAME TO + /// | ON () EVERY + /// | [SET] LABELS (=, ...) + /// | ADD LABELS (=, ...) + /// | MODIFY LABELS (=, ...) + /// | DROP LABELS (, , ...) + /// | [SET] ANNOTATIONS (=, ...) + /// | ADD ANNOTATIONS (=, ...) + /// | MODIFY ANNOTATIONS (=, ...) + /// | DROP ANNOTATIONS (, , ...) + /// | [SET] NOTIFY( + /// WEBHOOK URL '' [WITH (=, ...)], + /// WEBHOOK URL '' [WITH (=, ...)] + /// ) + /// | ADD NOTIFY( + /// WEBHOOK URL '' [WITH (=, ...)], + /// WEBHOOK URL '' [WITH (=, ...)] + /// ) + /// | DROP NOTIFY (, ) + /// } + /// ``` + pub(super) fn parse_alter_trigger(&mut self) -> Result { + 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 + /// ``` + /// + /// ## 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 { + 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> { + 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::>(); + + 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> { + 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::>(); + + 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> { + 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::>(); + + self.parser + .expect_token(&Token::RParen) + .context(error::SyntaxSnafu)?; + + Ok(notify_names) + } +} + +fn apply_label_replacement( + label_ops: &mut Option, + 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, + 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, + channels: Vec, +) -> 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, + 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, + 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, + 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); + } + } +} diff --git a/src/sql/src/parsers/create_parser.rs b/src/sql/src/parsers/create_parser.rs index 5c64aec469..30446ee73f 100644 --- a/src/sql/src/parsers/create_parser.rs +++ b/src/sql/src/parsers/create_parser.rs @@ -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 diff --git a/src/sql/src/parsers/create_parser/trigger.rs b/src/sql/src/parsers/create_parser/trigger.rs index 24d9b27d76..26fced155a 100644 --- a/src/sql/src/parsers/create_parser/trigger.rs +++ b/src/sql/src/parsers/create_parser/trigger.rs @@ -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, u64)> { + pub(crate) fn parse_trigger_on( + &mut self, + is_first_keyword_matched: bool, + ) -> Result<(Box, 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 { + pub(crate) fn parse_trigger_labels( + &mut self, + is_first_keyword_matched: bool, + ) -> Result { 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 { + pub(crate) fn parse_trigger_annotations( + &mut self, + is_first_keyword_matched: bool, + ) -> Result { 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> { @@ -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) diff --git a/src/sql/src/statements.rs b/src/sql/src/statements.rs index 14646732c0..25088ee5ef 100644 --- a/src/sql/src/statements.rs +++ b/src/sql/src/statements.rs @@ -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, diff --git a/src/sql/src/statements/alter.rs b/src/sql/src/statements/alter.rs index 63d2bb2b9f..ba9712a403 100644 --- a/src/sql/src/statements/alter.rs +++ b/src/sql/src/statements/alter.rs @@ -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; diff --git a/src/sql/src/statements/alter/trigger.rs b/src/sql/src/statements/alter/trigger.rs new file mode 100644 index 0000000000..e7488cbeba --- /dev/null +++ b/src/sql/src/statements/alter/trigger.rs @@ -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, + pub new_query: Option>, + /// The new interval of exec query. Unit is second. + pub new_interval: Option, + pub label_operations: Option, + pub annotation_operations: Option, + pub notify_channel_operations: Option, +} + +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), +} + +#[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), +} + +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), +} + +#[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), +} + +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), + PartialChanges(Vec), +} + +#[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), + /// Drop specified NotifyChannel's. + /// + /// Note: if the NotifyChannel to drop does not exist, an error will be + /// reported. + Drop(Vec), +} + +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); + } +} diff --git a/src/sql/src/statements/option_map.rs b/src/sql/src/statements/option_map.rs index 2b4b4e771b..0177624a13 100644 --- a/src/sql/src/statements/option_map.rs +++ b/src/sql/src/statements/option_map.rs @@ -98,10 +98,10 @@ impl OptionMap { } } -impl From> for OptionMap { - fn from(value: HashMap) -> Self { +impl> From 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 diff --git a/src/sql/src/statements/statement.rs b/src/sql/src/statements/statement.rs index 63464e15c0..2979a225cb 100644 --- a/src/sql/src/statements/statement.rs +++ b/src/sql/src/statements/statement.rs @@ -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), diff --git a/src/sql/src/util.rs b/src/sql/src/util.rs index 07106cdae6..2888bc2655 100644 --- a/src/sql/src/util.rs +++ b/src/sql/src/util.rs @@ -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", ];