Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 130 additions & 8 deletions src/ast/ddl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -371,10 +371,50 @@ pub enum AlterTableOperation {
DropClusteringKey,
SuspendRecluster,
ResumeRecluster,
/// `REFRESH`
/// `REFRESH [ '<subpath>' ]`
///
/// Note: this is Snowflake specific for dynamic tables <https://docs.snowflake.com/en/sql-reference/sql/alter-table>
Refresh,
/// Note: this is Snowflake specific for dynamic/external tables
/// <https://docs.snowflake.com/en/sql-reference/sql/alter-dynamic-table>
/// <https://docs.snowflake.com/en/sql-reference/sql/alter-external-table>
Refresh {
/// Optional subpath for external table refresh
subpath: Option<String>,
},
/// `ADD PARTITION COLUMN <column_name> <data_type>`
///
/// Note: this is Snowflake specific for external tables <https://docs.snowflake.com/en/sql-reference/sql/alter-external-table>
AddPartitionColumn {
column_name: Ident,
data_type: DataType,
},
/// `ADD FILES ( '<path>' [, '<path>', ...] )`
///
/// Note: this is Snowflake specific for external tables <https://docs.snowflake.com/en/sql-reference/sql/alter-external-table>
AddFiles {
files: Vec<String>,
},
/// `REMOVE FILES ( '<path>' [, '<path>', ...] )`
///
/// Note: this is Snowflake specific for external tables <https://docs.snowflake.com/en/sql-reference/sql/alter-external-table>
RemoveFiles {
files: Vec<String>,
},
/// `ADD PARTITION ( <part_col_name> = '<string>' [, ...] ) LOCATION '<path>'`
///
/// Note: this is Snowflake specific for external tables <https://docs.snowflake.com/en/sql-reference/sql/alter-external-table>
AddPartition {
/// Partition column values as key-value pairs
partition: Vec<(Ident, String)>,
/// Location path
location: String,
},
/// `DROP PARTITION LOCATION '<path>'`
///
/// Note: this is Snowflake specific for external tables <https://docs.snowflake.com/en/sql-reference/sql/alter-external-table>
DropPartitionLocation {
/// Location path
location: String,
},
/// `SUSPEND`
///
/// Note: this is Snowflake specific for dynamic tables <https://docs.snowflake.com/en/sql-reference/sql/alter-table>
Expand Down Expand Up @@ -425,6 +465,16 @@ pub enum AlterTableOperation {
SetOptionsParens {
options: Vec<SqlOption>,
},
/// `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<SqlOption>,
},
}

/// An `ALTER Policy` (`Statement::AlterPolicy`) operation
Expand Down Expand Up @@ -863,8 +913,58 @@ 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,
data_type,
} => {
write!(f, "ADD PARTITION COLUMN {column_name} {data_type}")
}
AlterTableOperation::AddFiles { files } => {
write!(
f,
"ADD FILES ({})",
files
.iter()
.map(|f| format!("'{f}'"))
.collect::<Vec<_>>()
.join(", ")
)
}
AlterTableOperation::RemoveFiles { files } => {
write!(
f,
"REMOVE FILES ({})",
files
.iter()
.map(|f| format!("'{f}'"))
.collect::<Vec<_>>()
.join(", ")
)
}
AlterTableOperation::AddPartition {
partition,
location,
} => {
write!(
f,
"ADD PARTITION ({}) LOCATION '{}'",
partition
.iter()
.map(|(k, v)| format!("{k} = '{v}'"))
.collect::<Vec<_>>()
.join(", "),
location
)
}
AlterTableOperation::DropPartitionLocation { location } => {
write!(f, "DROP PARTITION LOCATION '{location}'")
}
AlterTableOperation::Suspend => {
write!(f, "SUSPEND")
Expand Down Expand Up @@ -892,6 +992,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))
}
}
}
}
Expand Down Expand Up @@ -3880,8 +3983,11 @@ pub enum AlterTableType {
/// <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>
/// <https://docs.snowflake.com/en/sql-reference/sql/alter-dynamic-table>
Dynamic,
/// External table type
/// <https://docs.snowflake.com/en/sql-reference/sql/alter-external-table>
External,
}

/// ALTER TABLE statement
Expand Down Expand Up @@ -3911,16 +4017,32 @@ 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 ")?,
}

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} ")?;
}
Expand Down
12 changes: 11 additions & 1 deletion src/ast/spans.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1145,7 +1145,14 @@ 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::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(),
Expand All @@ -1156,6 +1163,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()))
}
}
}
}
Expand Down
147 changes: 145 additions & 2 deletions src/dialect/snowflake.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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]) {
Expand Down Expand Up @@ -619,7 +624,7 @@ fn parse_alter_dynamic_table(parser: &mut Parser) -> Result<Statement, ParserErr

// Parse the operation (REFRESH, SUSPEND, or RESUME)
let operation = if parser.parse_keyword(Keyword::REFRESH) {
AlterTableOperation::Refresh
AlterTableOperation::Refresh { subpath: None }
} else if parser.parse_keyword(Keyword::SUSPEND) {
AlterTableOperation::Suspend
} else if parser.parse_keyword(Keyword::RESUME) {
Expand Down Expand Up @@ -649,6 +654,144 @@ fn parse_alter_dynamic_table(parser: &mut Parser) -> Result<Statement, ParserErr
}))
}

/// Parse snowflake alter external table.
/// <https://docs.snowflake.com/en/sql-reference/sql/alter-external-table>
fn parse_alter_external_table(parser: &mut Parser) -> Result<Statement, ParserError> {
// 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
let subpath = match parser.peek_token().token {
Token::SingleQuotedString(s) => {
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 {
table_name: RenameTableNameKind::To(new_table_name),
}
} else if parser.parse_keywords(&[Keyword::ADD, Keyword::PARTITION, Keyword::COLUMN]) {
let column_name = parser.parse_identifier()?;
let data_type = parser.parse_data_type()?;
AlterTableOperation::AddPartitionColumn {
column_name,
data_type,
}
} else if parser.parse_keywords(&[Keyword::ADD, Keyword::PARTITION]) {
// ADD PARTITION ( <col> = '<val>' [, ...] ) LOCATION '<path>'
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 '<path>'
let location = parse_single_quoted_string(parser)?;
AlterTableOperation::DropPartitionLocation { location }
} else if parser.parse_keywords(&[Keyword::ADD, Keyword::FILES]) {
// Parse ADD FILES ( '<path>' [, '<path>', ...] )
let files = parse_parenthesized_file_list(parser)?;
AlterTableOperation::AddFiles { files }
} else if parser.parse_keywords(&[Keyword::REMOVE, Keyword::FILES]) {
// Parse REMOVE FILES ( '<path>' [, '<path>', ...] )
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![];
loop {
let key = parser.parse_identifier()?;
parser.expect_token(&Token::Eq)?;
let value = parser.parse_expr()?;
options.push(SqlOption::KeyValue { key, value });
if !parser.consume_token(&Token::Comma) {
break;
}
}
AlterTableOperation::SetOptions { options }
} else {
return parser.expected(
"REFRESH, RENAME TO, ADD, DROP, or SET 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 a parenthesized list of single-quoted file paths.
fn parse_parenthesized_file_list(parser: &mut Parser) -> Result<Vec<String>, 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: ( <col> = '<val>' [, <col> = '<val>', ...] )
fn parse_partition_key_values(parser: &mut Parser) -> Result<Vec<(Ident, String)>, 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<String, ParserError> {
match parser.next_token().token {
Token::SingleQuotedString(s) => Ok(s),
_ => parser.expected("a single-quoted string", parser.peek_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> {
Expand Down
Loading
Loading