Skip to content

Commit 568c8e5

Browse files
committed
Added the rest of alter external table spec
1 parent 791a9ae commit 568c8e5

File tree

4 files changed

+190
-4
lines changed

4 files changed

+190
-4
lines changed

src/ast/ddl.rs

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,34 @@ pub enum AlterTableOperation {
387387
column_name: Ident,
388388
data_type: DataType,
389389
},
390+
/// `ADD FILES ( '<path>' [, '<path>', ...] )`
391+
///
392+
/// Note: this is Snowflake specific for external tables <https://docs.snowflake.com/en/sql-reference/sql/alter-external-table>
393+
AddFiles {
394+
files: Vec<String>,
395+
},
396+
/// `REMOVE FILES ( '<path>' [, '<path>', ...] )`
397+
///
398+
/// Note: this is Snowflake specific for external tables <https://docs.snowflake.com/en/sql-reference/sql/alter-external-table>
399+
RemoveFiles {
400+
files: Vec<String>,
401+
},
402+
/// `ADD PARTITION ( <part_col_name> = '<string>' [, ...] ) LOCATION '<path>'`
403+
///
404+
/// Note: this is Snowflake specific for external tables <https://docs.snowflake.com/en/sql-reference/sql/alter-external-table>
405+
AddPartition {
406+
/// Partition column values as key-value pairs
407+
partition: Vec<(Ident, String)>,
408+
/// Location path
409+
location: String,
410+
},
411+
/// `DROP PARTITION LOCATION '<path>'`
412+
///
413+
/// Note: this is Snowflake specific for external tables <https://docs.snowflake.com/en/sql-reference/sql/alter-external-table>
414+
DropPartitionLocation {
415+
/// Location path
416+
location: String,
417+
},
390418
/// `SUSPEND`
391419
///
392420
/// 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 {
898926
} => {
899927
write!(f, "ADD PARTITION COLUMN {column_name} {data_type}")
900928
}
929+
AlterTableOperation::AddFiles { files } => {
930+
write!(
931+
f,
932+
"ADD FILES ({})",
933+
files
934+
.iter()
935+
.map(|f| format!("'{f}'"))
936+
.collect::<Vec<_>>()
937+
.join(", ")
938+
)
939+
}
940+
AlterTableOperation::RemoveFiles { files } => {
941+
write!(
942+
f,
943+
"REMOVE FILES ({})",
944+
files
945+
.iter()
946+
.map(|f| format!("'{f}'"))
947+
.collect::<Vec<_>>()
948+
.join(", ")
949+
)
950+
}
951+
AlterTableOperation::AddPartition {
952+
partition,
953+
location,
954+
} => {
955+
write!(
956+
f,
957+
"ADD PARTITION ({}) LOCATION '{}'",
958+
partition
959+
.iter()
960+
.map(|(k, v)| format!("{k} = '{v}'"))
961+
.collect::<Vec<_>>()
962+
.join(", "),
963+
location
964+
)
965+
}
966+
AlterTableOperation::DropPartitionLocation { location } => {
967+
write!(f, "DROP PARTITION LOCATION '{location}'")
968+
}
901969
AlterTableOperation::Suspend => {
902970
write!(f, "SUSPEND")
903971
}
@@ -3953,13 +4021,28 @@ impl fmt::Display for AlterTable {
39534021
None => write!(f, "ALTER TABLE ")?,
39544022
}
39554023

3956-
if self.if_exists {
4024+
// For external table ADD PARTITION / DROP PARTITION operations,
4025+
// IF EXISTS comes after the table name per Snowflake syntax
4026+
let if_exists_after_table_name = self.table_type == Some(AlterTableType::External)
4027+
&& self.operations.iter().any(|op| {
4028+
matches!(
4029+
op,
4030+
AlterTableOperation::AddPartition { .. }
4031+
| AlterTableOperation::DropPartitionLocation { .. }
4032+
)
4033+
});
4034+
4035+
if self.if_exists && !if_exists_after_table_name {
39574036
write!(f, "IF EXISTS ")?;
39584037
}
39594038
if self.only {
39604039
write!(f, "ONLY ")?;
39614040
}
3962-
write!(f, "{} ", &self.name)?;
4041+
write!(f, "{}", &self.name)?;
4042+
if self.if_exists && if_exists_after_table_name {
4043+
write!(f, " IF EXISTS")?;
4044+
}
4045+
write!(f, " ")?;
39634046
if let Some(cluster) = &self.on_cluster {
39644047
write!(f, "ON CLUSTER {cluster} ")?;
39654048
}

src/ast/spans.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1147,6 +1147,12 @@ impl Spanned for AlterTableOperation {
11471147
AlterTableOperation::ResumeRecluster => Span::empty(),
11481148
AlterTableOperation::Refresh { .. } => Span::empty(),
11491149
AlterTableOperation::AddPartitionColumn { column_name, .. } => column_name.span,
1150+
AlterTableOperation::AddFiles { .. } => Span::empty(),
1151+
AlterTableOperation::RemoveFiles { .. } => Span::empty(),
1152+
AlterTableOperation::AddPartition { partition, .. } => {
1153+
union_spans(partition.iter().map(|(k, _)| k.span))
1154+
}
1155+
AlterTableOperation::DropPartitionLocation { .. } => Span::empty(),
11501156
AlterTableOperation::Suspend => Span::empty(),
11511157
AlterTableOperation::Resume => Span::empty(),
11521158
AlterTableOperation::Algorithm { .. } => Span::empty(),

src/dialect/snowflake.rs

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -657,9 +657,15 @@ fn parse_alter_dynamic_table(parser: &mut Parser) -> Result<Statement, ParserErr
657657
/// Parse snowflake alter external table.
658658
/// <https://docs.snowflake.com/en/sql-reference/sql/alter-external-table>
659659
fn parse_alter_external_table(parser: &mut Parser) -> Result<Statement, ParserError> {
660-
let if_exists = parser.parse_keywords(&[Keyword::IF, Keyword::EXISTS]);
660+
// IF EXISTS can appear before the table name for most operations
661+
let mut if_exists = parser.parse_keywords(&[Keyword::IF, Keyword::EXISTS]);
661662
let table_name = parser.parse_object_name(true)?;
662663

664+
// IF EXISTS can also appear after the table name for ADD/DROP PARTITION operations
665+
if !if_exists {
666+
if_exists = parser.parse_keywords(&[Keyword::IF, Keyword::EXISTS]);
667+
}
668+
663669
// Parse the operation
664670
let operation = if parser.parse_keyword(Keyword::REFRESH) {
665671
// Optional subpath for refreshing specific partitions
@@ -683,6 +689,27 @@ fn parse_alter_external_table(parser: &mut Parser) -> Result<Statement, ParserEr
683689
column_name,
684690
data_type,
685691
}
692+
} else if parser.parse_keywords(&[Keyword::ADD, Keyword::PARTITION]) {
693+
// ADD PARTITION ( <col> = '<val>' [, ...] ) LOCATION '<path>'
694+
let partition = parse_partition_key_values(parser)?;
695+
parser.expect_keyword(Keyword::LOCATION)?;
696+
let location = parse_single_quoted_string(parser)?;
697+
AlterTableOperation::AddPartition {
698+
partition,
699+
location,
700+
}
701+
} else if parser.parse_keywords(&[Keyword::DROP, Keyword::PARTITION, Keyword::LOCATION]) {
702+
// DROP PARTITION LOCATION '<path>'
703+
let location = parse_single_quoted_string(parser)?;
704+
AlterTableOperation::DropPartitionLocation { location }
705+
} else if parser.parse_keywords(&[Keyword::ADD, Keyword::FILES]) {
706+
// Parse ADD FILES ( '<path>' [, '<path>', ...] )
707+
let files = parse_parenthesized_file_list(parser)?;
708+
AlterTableOperation::AddFiles { files }
709+
} else if parser.parse_keywords(&[Keyword::REMOVE, Keyword::FILES]) {
710+
// Parse REMOVE FILES ( '<path>' [, '<path>', ...] )
711+
let files = parse_parenthesized_file_list(parser)?;
712+
AlterTableOperation::RemoveFiles { files }
686713
} else if parser.parse_keyword(Keyword::SET) {
687714
// Parse SET key = value options (e.g., SET AUTO_REFRESH = TRUE)
688715
let mut options = vec![];
@@ -698,7 +725,7 @@ fn parse_alter_external_table(parser: &mut Parser) -> Result<Statement, ParserEr
698725
AlterTableOperation::SetOptions { options }
699726
} else {
700727
return parser.expected(
701-
"REFRESH, RENAME TO, ADD PARTITION COLUMN, or SET after ALTER EXTERNAL TABLE",
728+
"REFRESH, RENAME TO, ADD, DROP, or SET after ALTER EXTERNAL TABLE",
702729
parser.peek_token(),
703730
);
704731
};
@@ -721,6 +748,50 @@ fn parse_alter_external_table(parser: &mut Parser) -> Result<Statement, ParserEr
721748
}))
722749
}
723750

751+
/// Parse a parenthesized list of single-quoted file paths.
752+
fn parse_parenthesized_file_list(parser: &mut Parser) -> Result<Vec<String>, ParserError> {
753+
parser.expect_token(&Token::LParen)?;
754+
let mut files = vec![];
755+
loop {
756+
match parser.next_token().token {
757+
Token::SingleQuotedString(s) => files.push(s),
758+
_ => {
759+
return parser.expected("a single-quoted string", parser.peek_token());
760+
}
761+
}
762+
if !parser.consume_token(&Token::Comma) {
763+
break;
764+
}
765+
}
766+
parser.expect_token(&Token::RParen)?;
767+
Ok(files)
768+
}
769+
770+
/// Parse partition key-value pairs: ( <col> = '<val>' [, <col> = '<val>', ...] )
771+
fn parse_partition_key_values(parser: &mut Parser) -> Result<Vec<(Ident, String)>, ParserError> {
772+
parser.expect_token(&Token::LParen)?;
773+
let mut pairs = vec![];
774+
loop {
775+
let key = parser.parse_identifier()?;
776+
parser.expect_token(&Token::Eq)?;
777+
let value = parse_single_quoted_string(parser)?;
778+
pairs.push((key, value));
779+
if !parser.consume_token(&Token::Comma) {
780+
break;
781+
}
782+
}
783+
parser.expect_token(&Token::RParen)?;
784+
Ok(pairs)
785+
}
786+
787+
/// Parse a single-quoted string and return its content.
788+
fn parse_single_quoted_string(parser: &mut Parser) -> Result<String, ParserError> {
789+
match parser.next_token().token {
790+
Token::SingleQuotedString(s) => Ok(s),
791+
_ => parser.expected("a single-quoted string", parser.peek_token()),
792+
}
793+
}
794+
724795
/// Parse snowflake alter session.
725796
/// <https://docs.snowflake.com/en/sql-reference/sql/alter-session>
726797
fn parse_alter_session(parser: &mut Parser, set: bool) -> Result<Statement, ParserError> {

tests/sqlparser_snowflake.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4644,4 +4644,30 @@ fn test_alter_external_table() {
46444644
snowflake().verified_stmt("ALTER EXTERNAL TABLE some_table RENAME TO new_table_name");
46454645
snowflake().verified_stmt("ALTER EXTERNAL TABLE some_table ADD PARTITION COLUMN column_name VARCHAR");
46464646
snowflake().verified_stmt("ALTER EXTERNAL TABLE some_table SET AUTO_REFRESH = true");
4647+
snowflake().verified_stmt("ALTER EXTERNAL TABLE some_table ADD FILES ('file1.parquet')");
4648+
snowflake().verified_stmt(
4649+
"ALTER EXTERNAL TABLE some_table ADD FILES ('path/file1.parquet', 'path/file2.parquet')",
4650+
);
4651+
snowflake().verified_stmt("ALTER EXTERNAL TABLE some_table REMOVE FILES ('file1.parquet')");
4652+
snowflake().verified_stmt(
4653+
"ALTER EXTERNAL TABLE some_table REMOVE FILES ('path/file1.parquet', 'path/file2.parquet')",
4654+
);
4655+
// ADD PARTITION with location
4656+
snowflake()
4657+
.verified_stmt("ALTER EXTERNAL TABLE some_table ADD PARTITION (year = '2024') LOCATION 's3://bucket/path/'");
4658+
snowflake().verified_stmt(
4659+
"ALTER EXTERNAL TABLE some_table ADD PARTITION (year = '2024', month = '12') LOCATION 's3://bucket/path/'",
4660+
);
4661+
// DROP PARTITION location
4662+
snowflake()
4663+
.verified_stmt("ALTER EXTERNAL TABLE some_table DROP PARTITION LOCATION 's3://bucket/path/'");
4664+
// Test IF EXISTS (before table name for most operations)
4665+
snowflake().verified_stmt("ALTER EXTERNAL TABLE IF EXISTS some_table REFRESH");
4666+
// Test IF EXISTS (after table name for ADD/DROP PARTITION per Snowflake syntax)
4667+
snowflake().verified_stmt(
4668+
"ALTER EXTERNAL TABLE some_table IF EXISTS ADD PARTITION (year = '2024') LOCATION 's3://bucket/path/'",
4669+
);
4670+
snowflake().verified_stmt(
4671+
"ALTER EXTERNAL TABLE some_table IF EXISTS DROP PARTITION LOCATION 's3://bucket/path/'",
4672+
);
46474673
}

0 commit comments

Comments
 (0)