Added the rest of alter external table spec

This commit is contained in:
Andriy Romanov 2025-12-03 12:17:09 -08:00
parent 791a9aea2d
commit 568c8e5651
4 changed files with 190 additions and 4 deletions

View file

@ -387,6 +387,34 @@ pub enum AlterTableOperation {
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>
@ -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::<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")
}
@ -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} ")?;
}

View file

@ -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(),

View file

@ -657,9 +657,15 @@ 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> {
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<Statement, ParserEr
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![];
@ -698,7 +725,7 @@ fn parse_alter_external_table(parser: &mut Parser) -> Result<Statement, ParserEr
AlterTableOperation::SetOptions { options }
} else {
return parser.expected(
"REFRESH, RENAME TO, ADD PARTITION COLUMN, or SET after ALTER EXTERNAL TABLE",
"REFRESH, RENAME TO, ADD, DROP, or SET after ALTER EXTERNAL TABLE",
parser.peek_token(),
);
};
@ -721,6 +748,50 @@ fn parse_alter_external_table(parser: &mut Parser) -> Result<Statement, ParserEr
}))
}
/// 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> {

View file

@ -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/'",
);
}