diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 0677e63bf..f065af084 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -36,6 +36,8 @@ use crate::ast::{ use crate::keywords::Keyword; use crate::tokenizer::Token; +use super::Assignment; + /// An `ALTER TABLE` (`Statement::AlterTable`) operation #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -143,6 +145,17 @@ pub enum AlterTableOperation { partition: Partition, with_name: Option, }, + /// `UPDATE = [, ...] [IN PARTITION partition_id] WHERE ` + /// Note: this is a ClickHouse-specific operation, please refer to + /// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/alter/update) + Update { + /// Column assignments + assignments: Vec, + /// PARTITION + partition_id: Option, + /// WHERE + selection: Option, + }, /// `DROP PRIMARY KEY` /// /// Note: this is a MySQL-specific operation. @@ -546,6 +559,16 @@ impl fmt::Display for AlterTableOperation { } Ok(()) } + AlterTableOperation::Update { assignments, partition_id, selection } => { + write!(f, "UPDATE {}", display_comma_separated(assignments))?; + if let Some(partition_id) = partition_id { + write!(f, " IN PARTITION {}", partition_id)?; + } + if let Some(selection) = selection { + write!(f, " WHERE {}", selection)?; + } + Ok(()) + } } } } diff --git a/src/dialect/clickhouse.rs b/src/dialect/clickhouse.rs index 0c8f08040..b2d00b8b3 100644 --- a/src/dialect/clickhouse.rs +++ b/src/dialect/clickhouse.rs @@ -31,6 +31,10 @@ impl Dialect for ClickHouseDialect { self.is_identifier_start(ch) || ch.is_ascii_digit() } + fn supports_alter_table_update(&self) -> bool { + true + } + fn supports_string_literal_backslash_escape(&self) -> bool { true } diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index 92d720a0b..e92e98e1a 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -40,6 +40,10 @@ impl Dialect for GenericDialect { || ch == '_' } + fn supports_alter_table_update(&self) -> bool { + true + } + fn supports_unicode_string_literal(&self) -> bool { true } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 744f5a8c8..53ffcf1ea 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -60,7 +60,7 @@ use alloc::boxed::Box; /// Convenience check if a [`Parser`] uses a certain dialect. /// -/// Note: when possible please the new style, adding a method to the [`Dialect`] +/// Note: when possible please use the new style, adding a method to the [`Dialect`] /// trait rather than using this macro. /// /// The benefits of adding a method on `Dialect` over this macro are: @@ -149,6 +149,11 @@ pub trait Dialect: Debug + Any { false } + /// Determine if the dialect supports `ALTER TABLE ... UPDATE ...` statements. + fn supports_alter_table_update(&self) -> bool { + false + } + /// Determine if the dialect supports escaping characters via '\' in string literals. /// /// Some dialects like BigQuery and Snowflake support this while others like diff --git a/src/parser/mod.rs b/src/parser/mod.rs index b4c0487b4..39cf38e5a 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1081,7 +1081,7 @@ impl<'a> Parser<'a> { self.parse_bigquery_struct_literal() } Keyword::PRIOR if matches!(self.state, ParserState::ConnectBy) => { - let expr = self.parse_subexpr(self.dialect.prec_value(Precedence::PlusMinus))?; + let expr = self.parse_subexpr(self.dialect.prec_value(Precedence::Pipe))?; Ok(Expr::Prior(Box::new(expr))) } Keyword::MAP if self.peek_token() == Token::LBrace && self.dialect.support_map_literal_syntax() => { @@ -7156,6 +7156,33 @@ impl<'a> Parser<'a> { partition, with_name, } + } else if self.dialect.supports_alter_table_update() && self.parse_keyword(Keyword::UPDATE) + { + let mut assignments = vec![]; + loop { + let target = self.parse_assignment_target()?; + self.expect_token(&Token::Eq)?; + let value = self.parse_subexpr(self.dialect.prec_value(Precedence::Between))?; + assignments.push(Assignment { target, value }); + if self.is_parse_comma_separated_end() { + break; + } + } + let partition_id = if self.parse_keywords(&[Keyword::IN, Keyword::PARTITION]) { + Some(self.parse_identifier(false)?) + } else { + None + }; + let selection = if self.parse_keyword(Keyword::WHERE) { + Some(self.parse_expr()?) + } else { + None + }; + AlterTableOperation::Update { + assignments, + partition_id, + selection, + } } else { let options: Vec = self.parse_options_with_keywords(&[Keyword::SET, Keyword::TBLPROPERTIES])?; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 55ab3ddc3..ef675cd21 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -11432,3 +11432,24 @@ fn test_any_some_all_comparison() { verified_stmt("SELECT c1 FROM tbl WHERE c1 <> SOME(SELECT c2 FROM tbl)"); verified_stmt("SELECT 1 = ANY(WITH x AS (SELECT 1) SELECT * FROM x)"); } + +#[test] +fn test_parse_alter_table_update() { + let dialects = all_dialects_where(|d| d.supports_alter_table_update()); + let cases = [ + ( + "ALTER TABLE t UPDATE col1 = 1, col2 = col3 + col4 WHERE cod4 = 1", + true, + ), + ("ALTER TABLE t UPDATE c = 0 IN PARTITION abc", true), + ("ALTER TABLE t UPDATE", false), + ("ALTER TABLE t UPDATE c WHERE 1 = 1", false), + ]; + for (sql, is_valid) in cases { + if is_valid { + dialects.verified_stmt(sql); + } else { + assert!(dialects.parse_sql_statements(sql).is_err()); + } + } +}