diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 99d8521cd..7cdf04096 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -39,6 +39,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))] @@ -149,6 +151,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. @@ -692,6 +705,20 @@ impl fmt::Display for AlterTableOperation { value ) } + 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/ast/query.rs b/src/ast/query.rs index 1b30dcf17..e904c4e93 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -1129,6 +1129,8 @@ pub enum TableFactor { /// Optional index hints(mysql) /// See: index_hints: Vec, + /// Clickhouse specific + with_final: bool, }, Derived { lateral: bool, @@ -1711,6 +1713,7 @@ impl fmt::Display for TableFactor { json_path, sample, index_hints, + with_final, } => { write!(f, "{name}")?; if let Some(json_path) = json_path { @@ -1733,6 +1736,9 @@ impl fmt::Display for TableFactor { if *with_ordinality { write!(f, " WITH ORDINALITY")?; } + if *with_final { + write!(f, " FINAL")?; + } if let Some(TableSampleKind::BeforeTableAlias(sample)) = sample { write!(f, "{sample}")?; } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index a4f5eb46c..7322241a8 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1069,6 +1069,7 @@ impl Spanned for AlterTableOperation { AlterTableOperation::ResumeRecluster => Span::empty(), AlterTableOperation::Algorithm { .. } => Span::empty(), AlterTableOperation::AutoIncrement { value, .. } => value.span(), + AlterTableOperation::Update { .. } => Span::empty(), } } } @@ -1764,6 +1765,7 @@ impl Spanned for TableFactor { json_path: _, sample: _, index_hints: _, + with_final: _, } => union_spans( name.0 .iter() diff --git a/src/dialect/clickhouse.rs b/src/dialect/clickhouse.rs index f5e70c309..132446b4e 100644 --- a/src/dialect/clickhouse.rs +++ b/src/dialect/clickhouse.rs @@ -31,6 +31,11 @@ impl Dialect for ClickHouseDialect { self.is_identifier_start(ch) || ch.is_ascii_digit() } + // See https://clickhouse.com/docs/en/sql-reference/statements/alter/update + 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 c13d5aa69..7b89614bf 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 8d4557e2f..1384f962d 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: @@ -183,6 +183,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 @@ -1000,6 +1005,10 @@ pub trait Dialect: Debug + Any { fn supports_set_names(&self) -> bool { false } + + fn supports_select_table_final(&self) -> bool { + false + } } /// This represents the operators for which precedence must be defined diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index 9b08b8f32..ec37a4cc2 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -29,6 +29,7 @@ use log::debug; use crate::dialect::{Dialect, Precedence}; +use crate::keywords; use crate::keywords::Keyword; use crate::parser::{Parser, ParserError}; use crate::tokenizer::Token; @@ -258,4 +259,12 @@ impl Dialect for PostgreSqlDialect { fn supports_set_names(&self) -> bool { true } + + fn is_table_factor_alias(&self, explicit: bool, kw: &Keyword, _parser: &mut Parser) -> bool { + explicit || (!keywords::RESERVED_FOR_TABLE_ALIAS.contains(kw) && *kw != Keyword::FINAL) + } + + fn supports_select_table_final(&self) -> bool { + true + } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index d3c48a6e7..e3abdadb8 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8214,6 +8214,35 @@ impl<'a> Parser<'a> { let equals = self.consume_token(&Token::Eq); let value = self.parse_number_value()?; AlterTableOperation::AutoIncrement { equals, value } + } 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)?; + // NOTE: Maybe it's better to save the index before parse_subexpr to do a real look + // ahead. + 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()?) + } 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])?; @@ -11949,6 +11978,9 @@ impl<'a> Parser<'a> { } } + let with_final = + self.dialect.supports_select_table_final() && self.parse_keyword(Keyword::FINAL); + let mut table = TableFactor::Table { name, alias, @@ -11960,6 +11992,7 @@ impl<'a> Parser<'a> { json_path, sample, index_hints, + with_final, }; while let Some(kw) = self.parse_one_of_keywords(&[Keyword::PIVOT, Keyword::UNPIVOT]) { diff --git a/src/test_utils.rs b/src/test_utils.rs index 6270ac42b..b2ef34f41 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -363,6 +363,7 @@ pub fn table(name: impl Into) -> TableFactor { json_path: None, sample: None, index_hints: vec![], + with_final: false, } } @@ -378,6 +379,7 @@ pub fn table_from_name(name: ObjectName) -> TableFactor { json_path: None, sample: None, index_hints: vec![], + with_final: false, } } @@ -396,6 +398,7 @@ pub fn table_with_alias(name: impl Into, alias: impl Into) -> Ta json_path: None, sample: None, index_hints: vec![], + with_final: false, } } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index b5d42ea6c..9f19210bf 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -14720,3 +14720,23 @@ fn parse_set_time_zone_alias() { _ => unreachable!(), } } + +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()); + } + } +}