diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index fd481213f..1ae24a7f4 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -365,6 +365,18 @@ pub enum AlterTableOperation { DropClusteringKey, SuspendRecluster, ResumeRecluster, + /// `REFRESH` + /// + /// Note: this is Snowflake specific for dynamic tables + Refresh, + /// `SUSPEND` + /// + /// Note: this is Snowflake specific for dynamic tables + Suspend, + /// `RESUME` + /// + /// Note: this is Snowflake specific for dynamic tables + Resume, /// `ALGORITHM [=] { DEFAULT | INSTANT | INPLACE | COPY }` /// /// [MySQL]-specific table alter algorithm. @@ -845,6 +857,15 @@ impl fmt::Display for AlterTableOperation { write!(f, "RESUME RECLUSTER")?; Ok(()) } + AlterTableOperation::Refresh => { + write!(f, "REFRESH") + } + AlterTableOperation::Suspend => { + write!(f, "SUSPEND") + } + AlterTableOperation::Resume => { + write!(f, "RESUME") + } AlterTableOperation::AutoIncrement { equals, value } => { write!( f, @@ -3532,6 +3553,20 @@ impl Spanned for DropExtension { } } +/// Table type for ALTER TABLE statements. +/// Used to distinguish between regular tables, Iceberg tables, and Dynamic tables. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum AlterTableType { + /// Iceberg table type + /// + Iceberg, + /// Dynamic table type + /// + Dynamic, +} + /// ALTER TABLE statement #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -3548,19 +3583,18 @@ pub struct AlterTable { /// For example: `ALTER TABLE table_name ON CLUSTER cluster_name ADD COLUMN c UInt32` /// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/alter/update) pub on_cluster: Option, - /// Snowflake "ICEBERG" clause for Iceberg tables - /// - pub iceberg: bool, + /// Table type: None for regular tables, Some(AlterTableType) for Iceberg or Dynamic tables + pub table_type: Option, /// Token that represents the end of the statement (semicolon or EOF) pub end_token: AttachedToken, } impl fmt::Display for AlterTable { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - if self.iceberg { - write!(f, "ALTER ICEBERG TABLE ")?; - } else { - write!(f, "ALTER TABLE ")?; + match &self.table_type { + Some(AlterTableType::Iceberg) => write!(f, "ALTER ICEBERG TABLE ")?, + Some(AlterTableType::Dynamic) => write!(f, "ALTER DYNAMIC TABLE ")?, + None => write!(f, "ALTER TABLE ")?, } if self.if_exists { diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 176d36545..c838b3b68 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -61,7 +61,7 @@ pub use self::dcl::{ pub use self::ddl::{ AlterColumnOperation, AlterConnectorOwner, AlterIndexOperation, AlterPolicyOperation, AlterSchema, AlterSchemaOperation, AlterTable, AlterTableAlgorithm, AlterTableLock, - AlterTableOperation, AlterType, AlterTypeAddValue, AlterTypeAddValuePosition, + AlterTableOperation, AlterTableType, AlterType, AlterTypeAddValue, AlterTypeAddValuePosition, AlterTypeOperation, AlterTypeRename, AlterTypeRenameValue, ClusteredBy, ColumnDef, ColumnOption, ColumnOptionDef, ColumnOptions, ColumnPolicy, ColumnPolicyProperty, ConstraintCharacteristics, CreateConnector, CreateDomain, CreateExtension, CreateFunction, @@ -2787,10 +2787,11 @@ impl fmt::Display for Declare { } /// Sql options of a `CREATE TABLE` statement. -#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash, Default)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum CreateTableOptions { + #[default] None, /// Options specified using the `WITH` keyword. /// e.g. `WITH (description = "123")` @@ -2819,12 +2820,6 @@ pub enum CreateTableOptions { TableProperties(Vec), } -impl Default for CreateTableOptions { - fn default() -> Self { - Self::None - } -} - impl fmt::Display for CreateTableOptions { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 7d2a00095..573f31ba7 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1107,6 +1107,9 @@ impl Spanned for AlterTableOperation { AlterTableOperation::DropClusteringKey => Span::empty(), AlterTableOperation::SuspendRecluster => Span::empty(), AlterTableOperation::ResumeRecluster => Span::empty(), + AlterTableOperation::Refresh => Span::empty(), + AlterTableOperation::Suspend => Span::empty(), + AlterTableOperation::Resume => Span::empty(), AlterTableOperation::Algorithm { .. } => Span::empty(), AlterTableOperation::AutoIncrement { value, .. } => value.span(), AlterTableOperation::Lock { .. } => Span::empty(), diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 825fd45f0..bb0d4f16b 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -17,6 +17,7 @@ #[cfg(not(feature = "std"))] use crate::alloc::string::ToString; +use crate::ast::helpers::attached_token::AttachedToken; use crate::ast::helpers::key_value_options::{ KeyValueOption, KeyValueOptionKind, KeyValueOptions, KeyValueOptionsDelimiter, }; @@ -26,11 +27,12 @@ use crate::ast::helpers::stmt_data_loading::{ FileStagingCommand, StageLoadSelectItem, StageLoadSelectItemKind, StageParamsObject, }; use crate::ast::{ - CatalogSyncNamespaceMode, ColumnOption, ColumnPolicy, ColumnPolicyProperty, ContactEntry, - CopyIntoSnowflakeKind, CreateTableLikeKind, DollarQuotedString, Ident, IdentityParameters, - IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, - InitializeKind, ObjectName, ObjectNamePart, RefreshModeKind, RowAccessPolicy, ShowObjects, - SqlOption, Statement, StorageSerializationPolicy, TagsColumnOption, Value, WrappedCollection, + AlterTable, AlterTableOperation, AlterTableType, CatalogSyncNamespaceMode, ColumnOption, + ColumnPolicy, ColumnPolicyProperty, ContactEntry, CopyIntoSnowflakeKind, CreateTableLikeKind, + DollarQuotedString, Ident, IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, + IdentityPropertyKind, IdentityPropertyOrder, InitializeKind, ObjectName, ObjectNamePart, + RefreshModeKind, RowAccessPolicy, ShowObjects, SqlOption, Statement, + StorageSerializationPolicy, TagsColumnOption, Value, WrappedCollection, }; use crate::dialect::{Dialect, Precedence}; use crate::keywords::Keyword; @@ -214,6 +216,11 @@ impl Dialect for SnowflakeDialect { return Some(parser.parse_begin_exception_end()); } + if parser.parse_keywords(&[Keyword::ALTER, Keyword::DYNAMIC, Keyword::TABLE]) { + // ALTER DYNAMIC TABLE + return Some(parse_alter_dynamic_table(parser)); + } + if parser.parse_keywords(&[Keyword::ALTER, Keyword::SESSION]) { // ALTER SESSION let set = match parser.parse_one_of_keywords(&[Keyword::SET, Keyword::UNSET]) { @@ -604,6 +611,44 @@ fn parse_file_staging_command(kw: Keyword, parser: &mut Parser) -> Result +fn parse_alter_dynamic_table(parser: &mut Parser) -> Result { + // Use parse_object_name(true) to support IDENTIFIER() function + let table_name = parser.parse_object_name(true)?; + + // Parse the operation (REFRESH, SUSPEND, or RESUME) + let operation = if parser.parse_keyword(Keyword::REFRESH) { + AlterTableOperation::Refresh + } else if parser.parse_keyword(Keyword::SUSPEND) { + AlterTableOperation::Suspend + } else if parser.parse_keyword(Keyword::RESUME) { + AlterTableOperation::Resume + } else { + return parser.expected( + "REFRESH, SUSPEND, or RESUME after ALTER DYNAMIC TABLE", + parser.peek_token(), + ); + }; + + let end_token = if parser.peek_token_ref().token == Token::SemiColon { + parser.peek_token_ref().clone() + } else { + parser.get_current_token().clone() + }; + + Ok(Statement::AlterTable(AlterTable { + name: table_name, + if_exists: false, + only: false, + operations: vec![operation], + location: None, + on_cluster: None, + table_type: Some(AlterTableType::Dynamic), + end_token: AttachedToken(end_token), + })) +} + /// Parse snowflake alter session. /// fn parse_alter_session(parser: &mut Parser, set: bool) -> Result { diff --git a/src/keywords.rs b/src/keywords.rs index 319c57827..dc4ecd2f9 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -783,6 +783,7 @@ define_keywords!( REF, REFERENCES, REFERENCING, + REFRESH, REFRESH_MODE, REGCLASS, REGEXP, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 9a01e510b..377819c0b 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -9461,7 +9461,11 @@ impl<'a> Parser<'a> { operations, location, on_cluster, - iceberg, + table_type: if iceberg { + Some(AlterTableType::Iceberg) + } else { + None + }, end_token: AttachedToken(end_token), } .into()) diff --git a/src/test_utils.rs b/src/test_utils.rs index a8c8afd59..b6100d498 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -347,7 +347,7 @@ pub fn alter_table_op_with_name(stmt: Statement, expected_name: &str) -> AlterTa assert_eq!(alter_table.name.to_string(), expected_name); assert!(!alter_table.if_exists); assert!(!alter_table.only); - assert!(!alter_table.iceberg); + assert_eq!(alter_table.table_type, None); only(alter_table.operations) } _ => panic!("Expected ALTER TABLE statement"), diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index e43df87ab..86c1013c3 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -2746,14 +2746,14 @@ fn parse_alter_table_add_column() { if_exists, only, operations, - iceberg, + table_type, location: _, on_cluster: _, end_token: _, }) => { assert_eq!(name.to_string(), "tab"); assert!(!if_exists); - assert!(!iceberg); + assert_eq!(table_type, None); assert!(!only); assert_eq!( operations, diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 2be5eae8c..f187af1bd 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -4662,3 +4662,11 @@ fn test_drop_constraints() { snowflake().verified_stmt("ALTER TABLE tbl DROP FOREIGN KEY k1 RESTRICT"); snowflake().verified_stmt("ALTER TABLE tbl DROP CONSTRAINT c1 CASCADE"); } + +#[test] +fn test_alter_dynamic_table() { + snowflake().verified_stmt("ALTER DYNAMIC TABLE MY_DYNAMIC_TABLE REFRESH"); + snowflake().verified_stmt("ALTER DYNAMIC TABLE my_database.my_schema.my_dynamic_table REFRESH"); + snowflake().verified_stmt("ALTER DYNAMIC TABLE my_dyn_table SUSPEND"); + snowflake().verified_stmt("ALTER DYNAMIC TABLE my_dyn_table RESUME"); +}