Snowflake ALTER TABLE clustering options (#1579)

diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs
index 6c930a4..849b583 100644
--- a/src/ast/ddl.rs
+++ b/src/ast/ddl.rs
@@ -70,7 +70,10 @@
     ///
     /// Note: this is a ClickHouse-specific operation.
     /// Please refer to [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/alter/projection#drop-projection)
-    DropProjection { if_exists: bool, name: Ident },
+    DropProjection {
+        if_exists: bool,
+        name: Ident,
+    },
 
     /// `MATERIALIZE PROJECTION [IF EXISTS] name [IN PARTITION partition_name]`
     ///
@@ -99,11 +102,15 @@
     /// `DISABLE RULE rewrite_rule_name`
     ///
     /// Note: this is a PostgreSQL-specific operation.
-    DisableRule { name: Ident },
+    DisableRule {
+        name: Ident,
+    },
     /// `DISABLE TRIGGER [ trigger_name | ALL | USER ]`
     ///
     /// Note: this is a PostgreSQL-specific operation.
-    DisableTrigger { name: Ident },
+    DisableTrigger {
+        name: Ident,
+    },
     /// `DROP CONSTRAINT [ IF EXISTS ] <name>`
     DropConstraint {
         if_exists: bool,
@@ -152,19 +159,27 @@
     /// `ENABLE ALWAYS RULE rewrite_rule_name`
     ///
     /// Note: this is a PostgreSQL-specific operation.
-    EnableAlwaysRule { name: Ident },
+    EnableAlwaysRule {
+        name: Ident,
+    },
     /// `ENABLE ALWAYS TRIGGER trigger_name`
     ///
     /// Note: this is a PostgreSQL-specific operation.
-    EnableAlwaysTrigger { name: Ident },
+    EnableAlwaysTrigger {
+        name: Ident,
+    },
     /// `ENABLE REPLICA RULE rewrite_rule_name`
     ///
     /// Note: this is a PostgreSQL-specific operation.
-    EnableReplicaRule { name: Ident },
+    EnableReplicaRule {
+        name: Ident,
+    },
     /// `ENABLE REPLICA TRIGGER trigger_name`
     ///
     /// Note: this is a PostgreSQL-specific operation.
-    EnableReplicaTrigger { name: Ident },
+    EnableReplicaTrigger {
+        name: Ident,
+    },
     /// `ENABLE ROW LEVEL SECURITY`
     ///
     /// Note: this is a PostgreSQL-specific operation.
@@ -172,11 +187,15 @@
     /// `ENABLE RULE rewrite_rule_name`
     ///
     /// Note: this is a PostgreSQL-specific operation.
-    EnableRule { name: Ident },
+    EnableRule {
+        name: Ident,
+    },
     /// `ENABLE TRIGGER [ trigger_name | ALL | USER ]`
     ///
     /// Note: this is a PostgreSQL-specific operation.
-    EnableTrigger { name: Ident },
+    EnableTrigger {
+        name: Ident,
+    },
     /// `RENAME TO PARTITION (partition=val)`
     RenamePartitions {
         old_partitions: Vec<Expr>,
@@ -197,7 +216,9 @@
         new_column_name: Ident,
     },
     /// `RENAME TO <table_name>`
-    RenameTable { table_name: ObjectName },
+    RenameTable {
+        table_name: ObjectName,
+    },
     // CHANGE [ COLUMN ] <old_name> <new_name> <data_type> [ <options> ]
     ChangeColumn {
         old_name: Ident,
@@ -218,7 +239,10 @@
     /// `RENAME CONSTRAINT <old_constraint_name> TO <new_constraint_name>`
     ///
     /// Note: this is a PostgreSQL-specific operation.
-    RenameConstraint { old_name: Ident, new_name: Ident },
+    RenameConstraint {
+        old_name: Ident,
+        new_name: Ident,
+    },
     /// `ALTER [ COLUMN ]`
     AlterColumn {
         column_name: Ident,
@@ -227,14 +251,27 @@
     /// 'SWAP WITH <table_name>'
     ///
     /// Note: this is Snowflake specific <https://docs.snowflake.com/en/sql-reference/sql/alter-table>
-    SwapWith { table_name: ObjectName },
+    SwapWith {
+        table_name: ObjectName,
+    },
     /// 'SET TBLPROPERTIES ( { property_key [ = ] property_val } [, ...] )'
-    SetTblProperties { table_properties: Vec<SqlOption> },
-
+    SetTblProperties {
+        table_properties: Vec<SqlOption>,
+    },
     /// `OWNER TO { <new_owner> | CURRENT_ROLE | CURRENT_USER | SESSION_USER }`
     ///
     /// Note: this is PostgreSQL-specific <https://www.postgresql.org/docs/current/sql-altertable.html>
-    OwnerTo { new_owner: Owner },
+    OwnerTo {
+        new_owner: Owner,
+    },
+    /// Snowflake table clustering options
+    /// <https://docs.snowflake.com/en/sql-reference/sql/alter-table#clustering-actions-clusteringaction>
+    ClusterBy {
+        exprs: Vec<Expr>,
+    },
+    DropClusteringKey,
+    SuspendRecluster,
+    ResumeRecluster,
 }
 
 /// An `ALTER Policy` (`Statement::AlterPolicy`) operation
@@ -548,6 +585,22 @@
                 }
                 Ok(())
             }
+            AlterTableOperation::ClusterBy { exprs } => {
+                write!(f, "CLUSTER BY ({})", display_comma_separated(exprs))?;
+                Ok(())
+            }
+            AlterTableOperation::DropClusteringKey => {
+                write!(f, "DROP CLUSTERING KEY")?;
+                Ok(())
+            }
+            AlterTableOperation::SuspendRecluster => {
+                write!(f, "SUSPEND RECLUSTER")?;
+                Ok(())
+            }
+            AlterTableOperation::ResumeRecluster => {
+                write!(f, "RESUME RECLUSTER")?;
+                Ok(())
+            }
         }
     }
 }
diff --git a/src/ast/spans.rs b/src/ast/spans.rs
index cd3bda1..de577c9 100644
--- a/src/ast/spans.rs
+++ b/src/ast/spans.rs
@@ -1020,6 +1020,10 @@
                 union_spans(table_properties.iter().map(|i| i.span()))
             }
             AlterTableOperation::OwnerTo { .. } => Span::empty(),
+            AlterTableOperation::ClusterBy { exprs } => union_spans(exprs.iter().map(|e| e.span())),
+            AlterTableOperation::DropClusteringKey => Span::empty(),
+            AlterTableOperation::SuspendRecluster => Span::empty(),
+            AlterTableOperation::ResumeRecluster => Span::empty(),
         }
     }
 }
diff --git a/src/keywords.rs b/src/keywords.rs
index bd97c3c..25a719d 100644
--- a/src/keywords.rs
+++ b/src/keywords.rs
@@ -168,6 +168,7 @@
     CLOSE,
     CLUSTER,
     CLUSTERED,
+    CLUSTERING,
     COALESCE,
     COLLATE,
     COLLATION,
@@ -622,6 +623,7 @@
     READS,
     READ_ONLY,
     REAL,
+    RECLUSTER,
     RECURSIVE,
     REF,
     REFERENCES,
@@ -656,6 +658,7 @@
     RESTRICTIVE,
     RESULT,
     RESULTSET,
+    RESUME,
     RETAIN,
     RETURN,
     RETURNING,
@@ -745,6 +748,7 @@
     SUM,
     SUPER,
     SUPERUSER,
+    SUSPEND,
     SWAP,
     SYMMETRIC,
     SYNC,
diff --git a/src/parser/mod.rs b/src/parser/mod.rs
index b5365b5..ac76f64 100644
--- a/src/parser/mod.rs
+++ b/src/parser/mod.rs
@@ -7273,6 +7273,8 @@
                 let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]);
                 let name = self.parse_identifier(false)?;
                 AlterTableOperation::DropProjection { if_exists, name }
+            } else if self.parse_keywords(&[Keyword::CLUSTERING, Keyword::KEY]) {
+                AlterTableOperation::DropClusteringKey
             } else {
                 let _ = self.parse_keyword(Keyword::COLUMN); // [ COLUMN ]
                 let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]);
@@ -7444,6 +7446,15 @@
                 partition,
                 with_name,
             }
+        } else if self.parse_keywords(&[Keyword::CLUSTER, Keyword::BY]) {
+            self.expect_token(&Token::LParen)?;
+            let exprs = self.parse_comma_separated(|parser| parser.parse_expr())?;
+            self.expect_token(&Token::RParen)?;
+            AlterTableOperation::ClusterBy { exprs }
+        } else if self.parse_keywords(&[Keyword::SUSPEND, Keyword::RECLUSTER]) {
+            AlterTableOperation::SuspendRecluster
+        } else if self.parse_keywords(&[Keyword::RESUME, Keyword::RECLUSTER]) {
+            AlterTableOperation::ResumeRecluster
         } else {
             let options: Vec<SqlOption> =
                 self.parse_options_with_keywords(&[Keyword::SET, Keyword::TBLPROPERTIES])?;
diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs
index fb8a60c..3cbd87b 100644
--- a/tests/sqlparser_snowflake.rs
+++ b/tests/sqlparser_snowflake.rs
@@ -1412,6 +1412,42 @@
 }
 
 #[test]
+fn test_alter_table_clustering() {
+    let sql = r#"ALTER TABLE tab CLUSTER BY (c1, "c2", TO_DATE(c3))"#;
+    match alter_table_op(snowflake_and_generic().verified_stmt(sql)) {
+        AlterTableOperation::ClusterBy { exprs } => {
+            assert_eq!(
+                exprs,
+                [
+                    Expr::Identifier(Ident::new("c1")),
+                    Expr::Identifier(Ident::with_quote('"', "c2")),
+                    Expr::Function(Function {
+                        name: ObjectName(vec![Ident::new("TO_DATE")]),
+                        parameters: FunctionArguments::None,
+                        args: FunctionArguments::List(FunctionArgumentList {
+                            args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(
+                                Expr::Identifier(Ident::new("c3"))
+                            ))],
+                            duplicate_treatment: None,
+                            clauses: vec![],
+                        }),
+                        filter: None,
+                        null_treatment: None,
+                        over: None,
+                        within_group: vec![]
+                    })
+                ],
+            );
+        }
+        _ => unreachable!(),
+    }
+
+    snowflake_and_generic().verified_stmt("ALTER TABLE tbl DROP CLUSTERING KEY");
+    snowflake_and_generic().verified_stmt("ALTER TABLE tbl SUSPEND RECLUSTER");
+    snowflake_and_generic().verified_stmt("ALTER TABLE tbl RESUME RECLUSTER");
+}
+
+#[test]
 fn test_drop_stage() {
     match snowflake_and_generic().verified_stmt("DROP STAGE s1") {
         Statement::Drop {