Add snowflake dynamic table parsing (#2083)

diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs
index fd48121..1ae24a7 100644
--- a/src/ast/ddl.rs
+++ b/src/ast/ddl.rs
@@ -365,6 +365,18 @@
     DropClusteringKey,
     SuspendRecluster,
     ResumeRecluster,
+    /// `REFRESH`
+    ///
+    /// Note: this is Snowflake specific for dynamic tables <https://docs.snowflake.com/en/sql-reference/sql/alter-table>
+    Refresh,
+    /// `SUSPEND`
+    ///
+    /// Note: this is Snowflake specific for dynamic tables <https://docs.snowflake.com/en/sql-reference/sql/alter-table>
+    Suspend,
+    /// `RESUME`
+    ///
+    /// Note: this is Snowflake specific for dynamic tables <https://docs.snowflake.com/en/sql-reference/sql/alter-table>
+    Resume,
     /// `ALGORITHM [=] { DEFAULT | INSTANT | INPLACE | COPY }`
     ///
     /// [MySQL]-specific table alter algorithm.
@@ -845,6 +857,15 @@
                 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 @@
     }
 }
 
+/// 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
+    /// <https://docs.snowflake.com/en/sql-reference/sql/alter-iceberg-table>
+    Iceberg,
+    /// Dynamic table type
+    /// <https://docs.snowflake.com/en/sql-reference/sql/alter-table>
+    Dynamic,
+}
+
 /// ALTER TABLE statement
 #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
@@ -3548,19 +3583,18 @@
     /// 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<Ident>,
-    /// Snowflake "ICEBERG" clause for Iceberg tables
-    /// <https://docs.snowflake.com/en/sql-reference/sql/alter-iceberg-table>
-    pub iceberg: bool,
+    /// Table type: None for regular tables, Some(AlterTableType) for Iceberg or Dynamic tables
+    pub table_type: Option<AlterTableType>,
     /// 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 4636e4b..b32697f 100644
--- a/src/ast/mod.rs
+++ b/src/ast/mod.rs
@@ -61,7 +61,7 @@
 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,
diff --git a/src/ast/spans.rs b/src/ast/spans.rs
index 34edabd..80244e6 100644
--- a/src/ast/spans.rs
+++ b/src/ast/spans.rs
@@ -1108,6 +1108,9 @@
             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 825fd45..bb0d4f1 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 @@
     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 @@
             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 @@
     }
 }
 
+/// Parse snowflake alter dynamic table.
+/// <https://docs.snowflake.com/en/sql-reference/sql/alter-table>
+fn parse_alter_dynamic_table(parser: &mut Parser) -> Result<Statement, ParserError> {
+    // 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.
 /// <https://docs.snowflake.com/en/sql-reference/sql/alter-session>
 fn parse_alter_session(parser: &mut Parser, set: bool) -> Result<Statement, ParserError> {
diff --git a/src/keywords.rs b/src/keywords.rs
index 319c578..dc4ecd2 100644
--- a/src/keywords.rs
+++ b/src/keywords.rs
@@ -783,6 +783,7 @@
     REF,
     REFERENCES,
     REFERENCING,
+    REFRESH,
     REFRESH_MODE,
     REGCLASS,
     REGEXP,
diff --git a/src/parser/mod.rs b/src/parser/mod.rs
index f43329b..026f624 100644
--- a/src/parser/mod.rs
+++ b/src/parser/mod.rs
@@ -9462,7 +9462,11 @@
             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 a8c8afd..b6100d4 100644
--- a/src/test_utils.rs
+++ b/src/test_utils.rs
@@ -347,7 +347,7 @@
             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 e43df87..86c1013 100644
--- a/tests/sqlparser_mysql.rs
+++ b/tests/sqlparser_mysql.rs
@@ -2746,14 +2746,14 @@
             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 2be5eae..f187af1 100644
--- a/tests/sqlparser_snowflake.rs
+++ b/tests/sqlparser_snowflake.rs
@@ -4662,3 +4662,11 @@
     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");
+}