mirror of
https://github.com/GreptimeTeam/greptimedb.git
synced 2025-12-22 22:20:02 +00:00
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:
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 { .. }
|
||||
|
||||
@@ -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()),
|
||||
|
||||
791
src/sql/src/parsers/alter_parser/trigger.rs
Normal file
791
src/sql/src/parsers/alter_parser/trigger.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
332
src/sql/src/statements/alter/trigger.rs
Normal file
332
src/sql/src/statements/alter/trigger.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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",
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user