From 0f4a063e2dcde9cc8985e2f7cb0d28464750090a Mon Sep 17 00:00:00 2001 From: Andriy Romanov Date: Wed, 3 Dec 2025 11:39:25 -0800 Subject: [PATCH 1/6] Fixing ALTER EXTERNAL TABLE queries for snowflake --- src/ast/ddl.rs | 6 +++++- src/dialect/snowflake.rs | 36 ++++++++++++++++++++++++++++++++++++ src/parser/mod.rs | 20 ++++++++++++-------- tests/sqlparser_snowflake.rs | 6 ++++++ 4 files changed, 59 insertions(+), 9 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index ad3191ebc..e12364c09 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -3880,8 +3880,11 @@ pub enum AlterTableType { /// Iceberg, /// Dynamic table type - /// + /// Dynamic, + /// External table type + /// + External, } /// ALTER TABLE statement @@ -3911,6 +3914,7 @@ impl fmt::Display for AlterTable { match &self.table_type { Some(AlterTableType::Iceberg) => write!(f, "ALTER ICEBERG TABLE ")?, Some(AlterTableType::Dynamic) => write!(f, "ALTER DYNAMIC TABLE ")?, + Some(AlterTableType::External) => write!(f, "ALTER EXTERNAL TABLE ")?, None => write!(f, "ALTER TABLE ")?, } diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 4cfaddceb..d2b714f24 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -221,6 +221,11 @@ impl Dialect for SnowflakeDialect { return Some(parse_alter_dynamic_table(parser)); } + if parser.parse_keywords(&[Keyword::ALTER, Keyword::EXTERNAL, Keyword::TABLE]) { + // ALTER EXTERNAL TABLE + return Some(parse_alter_external_table(parser)); + } + if parser.parse_keywords(&[Keyword::ALTER, Keyword::SESSION]) { // ALTER SESSION let set = match parser.parse_one_of_keywords(&[Keyword::SET, Keyword::UNSET]) { @@ -649,6 +654,37 @@ fn parse_alter_dynamic_table(parser: &mut Parser) -> Result +fn parse_alter_external_table(parser: &mut Parser) -> Result { + let if_exists = parser.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); + let table_name = parser.parse_object_name(true)?; + + // Parse the operation (REFRESH for now, can be extended) + let operation = if parser.parse_keyword(Keyword::REFRESH) { + AlterTableOperation::Refresh + } else { + return parser.expected("REFRESH after ALTER EXTERNAL 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, + only: false, + operations: vec![operation], + location: None, + on_cluster: None, + table_type: Some(AlterTableType::External), + end_token: AttachedToken(end_token), + })) +} + /// Parse snowflake alter session. /// fn parse_alter_session(parser: &mut Parser, set: bool) -> Result { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index b2fa3b169..fc104a5fa 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -9778,6 +9778,7 @@ impl<'a> Parser<'a> { Keyword::POLICY, Keyword::CONNECTOR, Keyword::ICEBERG, + Keyword::EXTERNAL, Keyword::SCHEMA, Keyword::USER, ])?; @@ -9789,10 +9790,14 @@ impl<'a> Parser<'a> { } Keyword::VIEW => self.parse_alter_view(), Keyword::TYPE => self.parse_alter_type(), - Keyword::TABLE => self.parse_alter_table(false), + Keyword::TABLE => self.parse_alter_table(None), Keyword::ICEBERG => { self.expect_keyword(Keyword::TABLE)?; - self.parse_alter_table(true) + self.parse_alter_table(Some(AlterTableType::Iceberg)) + } + Keyword::EXTERNAL => { + self.expect_keyword(Keyword::TABLE)?; + self.parse_alter_table(Some(AlterTableType::External)) } Keyword::INDEX => { let index_name = self.parse_object_name(false)?; @@ -9822,7 +9827,10 @@ impl<'a> Parser<'a> { } /// Parse a [Statement::AlterTable] - pub fn parse_alter_table(&mut self, iceberg: bool) -> Result { + pub fn parse_alter_table( + &mut self, + table_type: Option, + ) -> Result { let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); let only = self.parse_keyword(Keyword::ONLY); // [ ONLY ] let table_name = self.parse_object_name(false)?; @@ -9855,11 +9863,7 @@ impl<'a> Parser<'a> { operations, location, on_cluster, - table_type: if iceberg { - Some(AlterTableType::Iceberg) - } else { - None - }, + table_type, end_token: AttachedToken(end_token), } .into()) diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 22a632666..398ab7630 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -4635,3 +4635,9 @@ fn test_alter_dynamic_table() { snowflake().verified_stmt("ALTER DYNAMIC TABLE my_dyn_table SUSPEND"); snowflake().verified_stmt("ALTER DYNAMIC TABLE my_dyn_table RESUME"); } + +#[test] +fn test_alter_external_table() { + snowflake().verified_stmt("ALTER EXTERNAL TABLE some_table REFRESH"); + snowflake().verified_stmt("ALTER EXTERNAL TABLE my_database.my_schema.my_external_table REFRESH"); +} From da84b910cbbfb2db1b1060c804b95d399985ff7d Mon Sep 17 00:00:00 2001 From: Andriy Romanov Date: Wed, 3 Dec 2025 11:45:39 -0800 Subject: [PATCH 2/6] Added alter external table rename to support for snowflake --- src/dialect/snowflake.rs | 14 +++++++++++--- tests/sqlparser_snowflake.rs | 1 + 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index d2b714f24..57a79589b 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -31,7 +31,7 @@ use crate::ast::{ ColumnPolicy, ColumnPolicyProperty, ContactEntry, CopyIntoSnowflakeKind, CreateTableLikeKind, DollarQuotedString, Ident, IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, InitializeKind, ObjectName, ObjectNamePart, - RefreshModeKind, RowAccessPolicy, ShowObjects, SqlOption, Statement, + RefreshModeKind, RenameTableNameKind, RowAccessPolicy, ShowObjects, SqlOption, Statement, StorageSerializationPolicy, TagsColumnOption, Value, WrappedCollection, }; use crate::dialect::{Dialect, Precedence}; @@ -660,11 +660,19 @@ fn parse_alter_external_table(parser: &mut Parser) -> Result Date: Wed, 3 Dec 2025 11:46:02 -0800 Subject: [PATCH 3/6] Added alter external table .. add partition column support --- src/ast/ddl.rs | 13 +++++++++++++ src/ast/spans.rs | 1 + src/dialect/snowflake.rs | 9 ++++++++- tests/sqlparser_snowflake.rs | 1 + 4 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index e12364c09..95e04bd3e 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -375,6 +375,13 @@ pub enum AlterTableOperation { /// /// Note: this is Snowflake specific for dynamic tables Refresh, + /// `ADD PARTITION COLUMN ` + /// + /// Note: this is Snowflake specific for external tables + AddPartitionColumn { + column_name: Ident, + data_type: DataType, + }, /// `SUSPEND` /// /// Note: this is Snowflake specific for dynamic tables @@ -866,6 +873,12 @@ impl fmt::Display for AlterTableOperation { AlterTableOperation::Refresh => { write!(f, "REFRESH") } + AlterTableOperation::AddPartitionColumn { + column_name, + data_type, + } => { + write!(f, "ADD PARTITION COLUMN {column_name} {data_type}") + } AlterTableOperation::Suspend => { write!(f, "SUSPEND") } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 684cc5b02..65cbb57bc 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1146,6 +1146,7 @@ impl Spanned for AlterTableOperation { AlterTableOperation::SuspendRecluster => Span::empty(), AlterTableOperation::ResumeRecluster => Span::empty(), AlterTableOperation::Refresh => Span::empty(), + AlterTableOperation::AddPartitionColumn { column_name, .. } => column_name.span, AlterTableOperation::Suspend => Span::empty(), AlterTableOperation::Resume => Span::empty(), AlterTableOperation::Algorithm { .. } => Span::empty(), diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 57a79589b..d4ab18c7f 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -668,9 +668,16 @@ fn parse_alter_external_table(parser: &mut Parser) -> Result Date: Wed, 3 Dec 2025 11:50:43 -0800 Subject: [PATCH 4/6] Added relative path to refresh parsing --- src/ast/ddl.rs | 19 ++++++++++++++----- src/ast/spans.rs | 2 +- src/dialect/snowflake.rs | 12 ++++++++++-- tests/sqlparser_snowflake.rs | 1 + 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 95e04bd3e..059b312ec 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -371,10 +371,15 @@ pub enum AlterTableOperation { DropClusteringKey, SuspendRecluster, ResumeRecluster, - /// `REFRESH` + /// `REFRESH [ '' ]` /// - /// Note: this is Snowflake specific for dynamic tables - Refresh, + /// Note: this is Snowflake specific for dynamic/external tables + /// + /// + Refresh { + /// Optional subpath for external table refresh + subpath: Option, + }, /// `ADD PARTITION COLUMN ` /// /// Note: this is Snowflake specific for external tables @@ -870,8 +875,12 @@ impl fmt::Display for AlterTableOperation { write!(f, "RESUME RECLUSTER")?; Ok(()) } - AlterTableOperation::Refresh => { - write!(f, "REFRESH") + AlterTableOperation::Refresh { subpath } => { + write!(f, "REFRESH")?; + if let Some(path) = subpath { + write!(f, " '{path}'")?; + } + Ok(()) } AlterTableOperation::AddPartitionColumn { column_name, diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 65cbb57bc..d703e8259 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1145,7 +1145,7 @@ impl Spanned for AlterTableOperation { AlterTableOperation::DropClusteringKey => Span::empty(), AlterTableOperation::SuspendRecluster => Span::empty(), AlterTableOperation::ResumeRecluster => Span::empty(), - AlterTableOperation::Refresh => Span::empty(), + AlterTableOperation::Refresh { .. } => Span::empty(), AlterTableOperation::AddPartitionColumn { column_name, .. } => column_name.span, AlterTableOperation::Suspend => Span::empty(), AlterTableOperation::Resume => Span::empty(), diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index d4ab18c7f..c866e213c 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -624,7 +624,7 @@ fn parse_alter_dynamic_table(parser: &mut Parser) -> Result Result { + parser.next_token(); + Some(s) + } + _ => None, + }; + AlterTableOperation::Refresh { subpath } } else if parser.parse_keywords(&[Keyword::RENAME, Keyword::TO]) { let new_table_name = parser.parse_object_name(false)?; AlterTableOperation::RenameTable { diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 24425ebaa..755ed50c5 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -4639,6 +4639,7 @@ fn test_alter_dynamic_table() { #[test] fn test_alter_external_table() { snowflake().verified_stmt("ALTER EXTERNAL TABLE some_table REFRESH"); + snowflake().verified_stmt("ALTER EXTERNAL TABLE some_table REFRESH 'year=2025/month=12/'"); snowflake().verified_stmt("ALTER EXTERNAL TABLE my_database.my_schema.my_external_table REFRESH"); snowflake().verified_stmt("ALTER EXTERNAL TABLE some_table RENAME TO new_table_name"); snowflake().verified_stmt("ALTER EXTERNAL TABLE some_table ADD PARTITION COLUMN column_name VARCHAR"); From 791a9aea2d10a59a003b63e9e6ae595318e9179c Mon Sep 17 00:00:00 2001 From: Andriy Romanov Date: Wed, 3 Dec 2025 11:59:58 -0800 Subject: [PATCH 5/6] Added set auto_refresh parsing for alter external table --- src/ast/ddl.rs | 13 +++++++++++++ src/ast/spans.rs | 3 +++ src/dialect/snowflake.rs | 15 ++++++++++++++- tests/sqlparser_snowflake.rs | 1 + 4 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 059b312ec..eff328d5f 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -437,6 +437,16 @@ pub enum AlterTableOperation { SetOptionsParens { options: Vec, }, + /// `SET key = value` options without parentheses. + /// + /// Example: + /// ```sql + /// SET AUTO_REFRESH = TRUE + /// ``` + /// [Snowflake](https://docs.snowflake.com/en/sql-reference/sql/alter-external-table) + SetOptions { + options: Vec, + }, } /// An `ALTER Policy` (`Statement::AlterPolicy`) operation @@ -914,6 +924,9 @@ impl fmt::Display for AlterTableOperation { AlterTableOperation::SetOptionsParens { options } => { write!(f, "SET ({})", display_comma_separated(options)) } + AlterTableOperation::SetOptions { options } => { + write!(f, "SET {}", display_comma_separated(options)) + } } } } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index d703e8259..0f971a548 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1157,6 +1157,9 @@ impl Spanned for AlterTableOperation { AlterTableOperation::SetOptionsParens { options } => { union_spans(options.iter().map(|i| i.span())) } + AlterTableOperation::SetOptions { options } => { + union_spans(options.iter().map(|i| i.span())) + } } } } diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index c866e213c..a76d13993 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -683,9 +683,22 @@ fn parse_alter_external_table(parser: &mut Parser) -> Result Date: Wed, 3 Dec 2025 12:17:09 -0800 Subject: [PATCH 6/6] Added the rest of alter external table spec --- src/ast/ddl.rs | 87 +++++++++++++++++++++++++++++++++++- src/ast/spans.rs | 6 +++ src/dialect/snowflake.rs | 75 ++++++++++++++++++++++++++++++- tests/sqlparser_snowflake.rs | 26 +++++++++++ 4 files changed, 190 insertions(+), 4 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index eff328d5f..eda182cff 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -387,6 +387,34 @@ pub enum AlterTableOperation { column_name: Ident, data_type: DataType, }, + /// `ADD FILES ( '' [, '', ...] )` + /// + /// Note: this is Snowflake specific for external tables + AddFiles { + files: Vec, + }, + /// `REMOVE FILES ( '' [, '', ...] )` + /// + /// Note: this is Snowflake specific for external tables + RemoveFiles { + files: Vec, + }, + /// `ADD PARTITION ( = '' [, ...] ) LOCATION ''` + /// + /// Note: this is Snowflake specific for external tables + AddPartition { + /// Partition column values as key-value pairs + partition: Vec<(Ident, String)>, + /// Location path + location: String, + }, + /// `DROP PARTITION LOCATION ''` + /// + /// Note: this is Snowflake specific for external tables + DropPartitionLocation { + /// Location path + location: String, + }, /// `SUSPEND` /// /// Note: this is Snowflake specific for dynamic tables @@ -898,6 +926,46 @@ impl fmt::Display for AlterTableOperation { } => { write!(f, "ADD PARTITION COLUMN {column_name} {data_type}") } + AlterTableOperation::AddFiles { files } => { + write!( + f, + "ADD FILES ({})", + files + .iter() + .map(|f| format!("'{f}'")) + .collect::>() + .join(", ") + ) + } + AlterTableOperation::RemoveFiles { files } => { + write!( + f, + "REMOVE FILES ({})", + files + .iter() + .map(|f| format!("'{f}'")) + .collect::>() + .join(", ") + ) + } + AlterTableOperation::AddPartition { + partition, + location, + } => { + write!( + f, + "ADD PARTITION ({}) LOCATION '{}'", + partition + .iter() + .map(|(k, v)| format!("{k} = '{v}'")) + .collect::>() + .join(", "), + location + ) + } + AlterTableOperation::DropPartitionLocation { location } => { + write!(f, "DROP PARTITION LOCATION '{location}'") + } AlterTableOperation::Suspend => { write!(f, "SUSPEND") } @@ -3953,13 +4021,28 @@ impl fmt::Display for AlterTable { None => write!(f, "ALTER TABLE ")?, } - if self.if_exists { + // For external table ADD PARTITION / DROP PARTITION operations, + // IF EXISTS comes after the table name per Snowflake syntax + let if_exists_after_table_name = self.table_type == Some(AlterTableType::External) + && self.operations.iter().any(|op| { + matches!( + op, + AlterTableOperation::AddPartition { .. } + | AlterTableOperation::DropPartitionLocation { .. } + ) + }); + + if self.if_exists && !if_exists_after_table_name { write!(f, "IF EXISTS ")?; } if self.only { write!(f, "ONLY ")?; } - write!(f, "{} ", &self.name)?; + write!(f, "{}", &self.name)?; + if self.if_exists && if_exists_after_table_name { + write!(f, " IF EXISTS")?; + } + write!(f, " ")?; if let Some(cluster) = &self.on_cluster { write!(f, "ON CLUSTER {cluster} ")?; } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 0f971a548..96feab899 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1147,6 +1147,12 @@ impl Spanned for AlterTableOperation { AlterTableOperation::ResumeRecluster => Span::empty(), AlterTableOperation::Refresh { .. } => Span::empty(), AlterTableOperation::AddPartitionColumn { column_name, .. } => column_name.span, + AlterTableOperation::AddFiles { .. } => Span::empty(), + AlterTableOperation::RemoveFiles { .. } => Span::empty(), + AlterTableOperation::AddPartition { partition, .. } => { + union_spans(partition.iter().map(|(k, _)| k.span)) + } + AlterTableOperation::DropPartitionLocation { .. } => Span::empty(), AlterTableOperation::Suspend => Span::empty(), AlterTableOperation::Resume => Span::empty(), AlterTableOperation::Algorithm { .. } => Span::empty(), diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index a76d13993..0665ddb80 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -657,9 +657,15 @@ fn parse_alter_dynamic_table(parser: &mut Parser) -> Result fn parse_alter_external_table(parser: &mut Parser) -> Result { - let if_exists = parser.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); + // IF EXISTS can appear before the table name for most operations + let mut if_exists = parser.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); let table_name = parser.parse_object_name(true)?; + // IF EXISTS can also appear after the table name for ADD/DROP PARTITION operations + if !if_exists { + if_exists = parser.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); + } + // Parse the operation let operation = if parser.parse_keyword(Keyword::REFRESH) { // Optional subpath for refreshing specific partitions @@ -683,6 +689,27 @@ fn parse_alter_external_table(parser: &mut Parser) -> Result = '' [, ...] ) LOCATION '' + let partition = parse_partition_key_values(parser)?; + parser.expect_keyword(Keyword::LOCATION)?; + let location = parse_single_quoted_string(parser)?; + AlterTableOperation::AddPartition { + partition, + location, + } + } else if parser.parse_keywords(&[Keyword::DROP, Keyword::PARTITION, Keyword::LOCATION]) { + // DROP PARTITION LOCATION '' + let location = parse_single_quoted_string(parser)?; + AlterTableOperation::DropPartitionLocation { location } + } else if parser.parse_keywords(&[Keyword::ADD, Keyword::FILES]) { + // Parse ADD FILES ( '' [, '', ...] ) + let files = parse_parenthesized_file_list(parser)?; + AlterTableOperation::AddFiles { files } + } else if parser.parse_keywords(&[Keyword::REMOVE, Keyword::FILES]) { + // Parse REMOVE FILES ( '' [, '', ...] ) + let files = parse_parenthesized_file_list(parser)?; + AlterTableOperation::RemoveFiles { files } } else if parser.parse_keyword(Keyword::SET) { // Parse SET key = value options (e.g., SET AUTO_REFRESH = TRUE) let mut options = vec![]; @@ -698,7 +725,7 @@ fn parse_alter_external_table(parser: &mut Parser) -> Result Result Result, ParserError> { + parser.expect_token(&Token::LParen)?; + let mut files = vec![]; + loop { + match parser.next_token().token { + Token::SingleQuotedString(s) => files.push(s), + _ => { + return parser.expected("a single-quoted string", parser.peek_token()); + } + } + if !parser.consume_token(&Token::Comma) { + break; + } + } + parser.expect_token(&Token::RParen)?; + Ok(files) +} + +/// Parse partition key-value pairs: ( = '' [, = '', ...] ) +fn parse_partition_key_values(parser: &mut Parser) -> Result, ParserError> { + parser.expect_token(&Token::LParen)?; + let mut pairs = vec![]; + loop { + let key = parser.parse_identifier()?; + parser.expect_token(&Token::Eq)?; + let value = parse_single_quoted_string(parser)?; + pairs.push((key, value)); + if !parser.consume_token(&Token::Comma) { + break; + } + } + parser.expect_token(&Token::RParen)?; + Ok(pairs) +} + +/// Parse a single-quoted string and return its content. +fn parse_single_quoted_string(parser: &mut Parser) -> Result { + match parser.next_token().token { + Token::SingleQuotedString(s) => Ok(s), + _ => parser.expected("a single-quoted string", parser.peek_token()), + } +} + /// Parse snowflake alter session. /// fn parse_alter_session(parser: &mut Parser, set: bool) -> Result { diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 005bd7c32..e64fc133c 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -4644,4 +4644,30 @@ fn test_alter_external_table() { snowflake().verified_stmt("ALTER EXTERNAL TABLE some_table RENAME TO new_table_name"); snowflake().verified_stmt("ALTER EXTERNAL TABLE some_table ADD PARTITION COLUMN column_name VARCHAR"); snowflake().verified_stmt("ALTER EXTERNAL TABLE some_table SET AUTO_REFRESH = true"); + snowflake().verified_stmt("ALTER EXTERNAL TABLE some_table ADD FILES ('file1.parquet')"); + snowflake().verified_stmt( + "ALTER EXTERNAL TABLE some_table ADD FILES ('path/file1.parquet', 'path/file2.parquet')", + ); + snowflake().verified_stmt("ALTER EXTERNAL TABLE some_table REMOVE FILES ('file1.parquet')"); + snowflake().verified_stmt( + "ALTER EXTERNAL TABLE some_table REMOVE FILES ('path/file1.parquet', 'path/file2.parquet')", + ); + // ADD PARTITION with location + snowflake() + .verified_stmt("ALTER EXTERNAL TABLE some_table ADD PARTITION (year = '2024') LOCATION 's3://bucket/path/'"); + snowflake().verified_stmt( + "ALTER EXTERNAL TABLE some_table ADD PARTITION (year = '2024', month = '12') LOCATION 's3://bucket/path/'", + ); + // DROP PARTITION location + snowflake() + .verified_stmt("ALTER EXTERNAL TABLE some_table DROP PARTITION LOCATION 's3://bucket/path/'"); + // Test IF EXISTS (before table name for most operations) + snowflake().verified_stmt("ALTER EXTERNAL TABLE IF EXISTS some_table REFRESH"); + // Test IF EXISTS (after table name for ADD/DROP PARTITION per Snowflake syntax) + snowflake().verified_stmt( + "ALTER EXTERNAL TABLE some_table IF EXISTS ADD PARTITION (year = '2024') LOCATION 's3://bucket/path/'", + ); + snowflake().verified_stmt( + "ALTER EXTERNAL TABLE some_table IF EXISTS DROP PARTITION LOCATION 's3://bucket/path/'", + ); }