Skip to content

Commit dcbada6

Browse files
Snowflake EXTERNAL VOLUME: reuse KeyValueOptions and generic DROP
Address review feedback by building on existing infrastructure instead of bespoke AST: - Parse storage locations (and the nested ENCRYPTION clause) via Parser::parse_key_value_options, the same path CREATE STAGE uses. Storage locations are now Vec<KeyValueOptions>; the ExternalVolumeStorageLocation and ExternalVolumeEncryption structs and their hand-written Display and parser code are removed. Parsing is syntax-only, so field order is preserved and semantic checks (required STORAGE_BASE_URL, duplicate fields) are left to the consumer. - Route DROP EXTERNAL VOLUME through the generic Statement::Drop machinery via a new ObjectType::ExternalVolume, matching how STAGE/STREAM are handled, and drop the dedicated DropExternalVolume statement variant. - Use expect_keyword_is instead of expect_keyword to match the file's convention, and remove the now-unused option-name keywords. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 1858c13 commit dcbada6

6 files changed

Lines changed: 265 additions & 608 deletions

File tree

src/ast/mod.rs

Lines changed: 7 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -4546,8 +4546,9 @@ pub enum Statement {
45464546
if_not_exists: bool,
45474547
/// External volume name.
45484548
name: ObjectName,
4549-
/// Storage locations.
4550-
storage_locations: Vec<ExternalVolumeStorageLocation>,
4549+
/// Storage locations, each a parenthesized list of key-value options
4550+
/// (e.g. `(NAME='loc1' STORAGE_PROVIDER='S3' STORAGE_BASE_URL='s3://bucket/')`).
4551+
storage_locations: Vec<KeyValueOptions>,
45514552
/// Optional `ALLOW_WRITES` setting.
45524553
allow_writes: Option<bool>,
45534554
/// Optional comment.
@@ -4565,15 +4566,6 @@ pub enum Statement {
45654566
operation: AlterExternalVolumeOperation,
45664567
},
45674568
/// ```sql
4568-
/// DROP EXTERNAL VOLUME [IF EXISTS] <name>
4569-
/// ```
4570-
DropExternalVolume {
4571-
/// External volume name.
4572-
name: ObjectName,
4573-
/// `IF EXISTS` flag.
4574-
if_exists: bool,
4575-
},
4576-
/// ```sql
45774569
/// DESC[RIBE] EXTERNAL VOLUME <name>
45784570
/// ```
45794571
DescribeExternalVolume {
@@ -6353,13 +6345,6 @@ impl fmt::Display for Statement {
63536345
if_exists = if *if_exists { "IF EXISTS " } else { "" },
63546346
)
63556347
}
6356-
Statement::DropExternalVolume { name, if_exists } => {
6357-
write!(
6358-
f,
6359-
"DROP EXTERNAL VOLUME {if_exists}{name}",
6360-
if_exists = if *if_exists { "IF EXISTS " } else { "" },
6361-
)
6362-
}
63636348
Statement::DescribeExternalVolume { name } => {
63646349
write!(f, "DESCRIBE EXTERNAL VOLUME {name}")
63656350
}
@@ -8688,6 +8673,8 @@ pub enum ObjectType {
86888673
User,
86898674
/// A stream.
86908675
Stream,
8676+
/// A Snowflake external volume.
8677+
ExternalVolume,
86918678
}
86928679

86938680
impl fmt::Display for ObjectType {
@@ -8706,6 +8693,7 @@ impl fmt::Display for ObjectType {
87068693
ObjectType::Type => "TYPE",
87078694
ObjectType::User => "USER",
87088695
ObjectType::Stream => "STREAM",
8696+
ObjectType::ExternalVolume => "EXTERNAL VOLUME",
87098697
})
87108698
}
87118699
}
@@ -11130,91 +11118,13 @@ pub struct ShowObjects {
1113011118
pub show_options: ShowStatementOptions,
1113111119
}
1113211120

11133-
/// A storage location within a Snowflake `EXTERNAL VOLUME`.
11134-
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
11135-
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
11136-
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
11137-
pub struct ExternalVolumeStorageLocation {
11138-
/// The `NAME` of the storage location.
11139-
pub name: String,
11140-
/// The `STORAGE_PROVIDER` (e.g. `'S3'`).
11141-
pub storage_provider: String,
11142-
/// The `STORAGE_BASE_URL` (e.g. `'s3://bucket/path/'`).
11143-
pub storage_base_url: String,
11144-
/// Optional `STORAGE_AWS_ROLE_ARN`.
11145-
pub storage_aws_role_arn: Option<String>,
11146-
/// Optional `STORAGE_AWS_EXTERNAL_ID`.
11147-
pub storage_aws_external_id: Option<String>,
11148-
/// Optional `ENCRYPTION` settings.
11149-
pub encryption: Option<ExternalVolumeEncryption>,
11150-
}
11151-
11152-
impl fmt::Display for ExternalVolumeStorageLocation {
11153-
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
11154-
write!(
11155-
f,
11156-
"NAME = '{}' STORAGE_PROVIDER = '{}' STORAGE_BASE_URL = '{}'",
11157-
value::escape_single_quote_string(&self.name),
11158-
value::escape_single_quote_string(&self.storage_provider),
11159-
value::escape_single_quote_string(&self.storage_base_url),
11160-
)?;
11161-
if let Some(ref arn) = self.storage_aws_role_arn {
11162-
write!(
11163-
f,
11164-
" STORAGE_AWS_ROLE_ARN = '{}'",
11165-
value::escape_single_quote_string(arn)
11166-
)?;
11167-
}
11168-
if let Some(ref ext_id) = self.storage_aws_external_id {
11169-
write!(
11170-
f,
11171-
" STORAGE_AWS_EXTERNAL_ID = '{}'",
11172-
value::escape_single_quote_string(ext_id)
11173-
)?;
11174-
}
11175-
if let Some(ref enc) = self.encryption {
11176-
write!(f, " ENCRYPTION = ({enc})")?;
11177-
}
11178-
Ok(())
11179-
}
11180-
}
11181-
11182-
/// Encryption settings for an `EXTERNAL VOLUME` storage location.
11183-
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
11184-
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
11185-
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
11186-
pub struct ExternalVolumeEncryption {
11187-
/// Encryption type: `'AWS_SSE_S3'`, `'AWS_SSE_KMS'`, or `'NONE'`.
11188-
pub kind: String,
11189-
/// Optional `KMS_KEY_ID`.
11190-
pub kms_key_id: Option<String>,
11191-
}
11192-
11193-
impl fmt::Display for ExternalVolumeEncryption {
11194-
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
11195-
write!(
11196-
f,
11197-
"TYPE = '{}'",
11198-
value::escape_single_quote_string(&self.kind)
11199-
)?;
11200-
if let Some(ref key_id) = self.kms_key_id {
11201-
write!(
11202-
f,
11203-
" KMS_KEY_ID = '{}'",
11204-
value::escape_single_quote_string(key_id)
11205-
)?;
11206-
}
11207-
Ok(())
11208-
}
11209-
}
11210-
1121111121
/// Operations for `ALTER EXTERNAL VOLUME`.
1121211122
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
1121311123
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1121411124
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
1121511125
pub enum AlterExternalVolumeOperation {
1121611126
/// `ADD STORAGE_LOCATION = ( ... )`
11217-
AddStorageLocation(ExternalVolumeStorageLocation),
11127+
AddStorageLocation(KeyValueOptions),
1121811128
/// `SET ALLOW_WRITES = TRUE|FALSE`
1121911129
SetAllowWrites(bool),
1122011130
/// `REMOVE STORAGE_LOCATION '<name>'`

src/ast/spans.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -524,7 +524,6 @@ impl Spanned for Statement {
524524
Statement::Reset(..) => Span::empty(),
525525
Statement::CreateExternalVolume { .. } => Span::empty(),
526526
Statement::AlterExternalVolume { .. } => Span::empty(),
527-
Statement::DropExternalVolume { .. } => Span::empty(),
528527
Statement::DescribeExternalVolume { .. } => Span::empty(),
529528
Statement::ShowExternalVolumes { .. } => Span::empty(),
530529
}

src/dialect/snowflake.rs

Lines changed: 18 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,13 @@ use crate::ast::helpers::stmt_data_loading::{
2929
use crate::ast::{
3030
AlterExternalVolumeOperation, AlterTable, AlterTableOperation, AlterTableType,
3131
CatalogSyncNamespaceMode, ColumnOption, ColumnPolicy, ColumnPolicyProperty, ContactEntry,
32-
CopyIntoSnowflakeKind, CreateTable, CreateTableLikeKind, DollarQuotedString,
33-
ExternalVolumeEncryption, ExternalVolumeStorageLocation, Ident, IdentityParameters,
34-
IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder,
35-
InitializeKind, Insert, MultiTableInsertIntoClause, MultiTableInsertType,
36-
MultiTableInsertValue, MultiTableInsertValues, MultiTableInsertWhenClause, ObjectName,
37-
ObjectNamePart, RefreshModeKind, RowAccessPolicy, ShowObjects, SqlOption, Statement,
38-
StorageLifecyclePolicy, StorageSerializationPolicy, TableObject, TagsColumnOption, Value,
39-
WrappedCollection,
32+
CopyIntoSnowflakeKind, CreateTable, CreateTableLikeKind, DollarQuotedString, Ident,
33+
IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind,
34+
IdentityPropertyOrder, InitializeKind, Insert, MultiTableInsertIntoClause,
35+
MultiTableInsertType, MultiTableInsertValue, MultiTableInsertValues,
36+
MultiTableInsertWhenClause, ObjectName, ObjectNamePart, RefreshModeKind, RowAccessPolicy,
37+
ShowObjects, SqlOption, Statement, StorageLifecyclePolicy, StorageSerializationPolicy,
38+
TableObject, TagsColumnOption, Value, WrappedCollection,
4039
};
4140
use crate::dialect::{Dialect, Precedence};
4241
use crate::keywords::Keyword;
@@ -288,11 +287,6 @@ impl Dialect for SnowflakeDialect {
288287
return Some(parse_alter_session(parser, set));
289288
}
290289

291-
if parser.parse_keywords(&[Keyword::DROP, Keyword::EXTERNAL, Keyword::VOLUME]) {
292-
// DROP EXTERNAL VOLUME
293-
return Some(parse_drop_external_volume(parser));
294-
}
295-
296290
if parser
297291
.parse_one_of_keywords(&[Keyword::DESC, Keyword::DESCRIBE])
298292
.is_some()
@@ -2018,22 +2012,25 @@ fn parse_multi_table_insert_when_clauses(
20182012
}
20192013

20202014
/// Parse `CREATE [OR REPLACE] EXTERNAL VOLUME [IF NOT EXISTS] <name> ...`
2015+
///
2016+
/// Each storage location is a parenthesized `KEY = value` option list; the
2017+
/// options (and the `ENCRYPTION = (...)` sub-list) are parsed generically via
2018+
/// [`Parser::parse_key_value_options`], so field order and the exact option
2019+
/// set are left to the caller rather than validated here.
20212020
fn parse_create_external_volume(
20222021
or_replace: bool,
20232022
parser: &mut Parser,
20242023
) -> Result<Statement, ParserError> {
20252024
let if_not_exists = parser.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]);
20262025
let name = parser.parse_object_name(false)?;
20272026

2028-
parser.expect_keyword(Keyword::STORAGE_LOCATIONS)?;
2027+
parser.expect_keyword_is(Keyword::STORAGE_LOCATIONS)?;
20292028
parser.expect_token(&Token::Eq)?;
20302029
parser.expect_token(&Token::LParen)?;
20312030

20322031
let mut storage_locations = vec![];
20332032
loop {
2034-
parser.expect_token(&Token::LParen)?;
2035-
storage_locations.push(parse_external_volume_storage_location(parser)?);
2036-
parser.expect_token(&Token::RParen)?;
2033+
storage_locations.push(parser.parse_key_value_options(true, &[])?);
20372034
if !parser.consume_token(&Token::Comma) {
20382035
break;
20392036
}
@@ -2063,126 +2060,21 @@ fn parse_create_external_volume(
20632060
})
20642061
}
20652062

2066-
/// Parse a single storage location: `NAME = '...' STORAGE_PROVIDER = '...' ...`
2067-
///
2068-
/// `NAME` and `STORAGE_PROVIDER` must appear first (in that order). After
2069-
/// those two, `STORAGE_BASE_URL` (required) and the optional fields
2070-
/// (`STORAGE_AWS_ROLE_ARN`, `STORAGE_AWS_EXTERNAL_ID`, `ENCRYPTION`) may
2071-
/// appear in any order, separated by commas or whitespace. Real Snowflake
2072-
/// accepts the flexible ordering — mirror that here so fixtures like
2073-
/// `NAME = '…' STORAGE_PROVIDER = '…' STORAGE_AWS_ROLE_ARN = '…'
2074-
/// STORAGE_BASE_URL = '…'` parse correctly.
2075-
fn parse_external_volume_storage_location(
2076-
parser: &mut Parser,
2077-
) -> Result<ExternalVolumeStorageLocation, ParserError> {
2078-
parser.expect_keyword(Keyword::NAME)?;
2079-
parser.expect_token(&Token::Eq)?;
2080-
let name = parser.parse_literal_string()?;
2081-
2082-
// separators may be commas or whitespace — consume optional comma
2083-
let _ = parser.consume_token(&Token::Comma);
2084-
2085-
parser.expect_keyword(Keyword::STORAGE_PROVIDER)?;
2086-
parser.expect_token(&Token::Eq)?;
2087-
let storage_provider = parser.parse_literal_string()?;
2088-
2089-
let mut storage_base_url: Option<String> = None;
2090-
let mut storage_aws_role_arn = None;
2091-
let mut storage_aws_external_id = None;
2092-
let mut encryption = None;
2093-
2094-
// Parse the remaining fields (STORAGE_BASE_URL + optionals) in any order
2095-
// (comma-or-whitespace separated). Duplicates are rejected.
2096-
loop {
2097-
let _ = parser.consume_token(&Token::Comma);
2098-
if parser.parse_keyword(Keyword::STORAGE_BASE_URL) {
2099-
if storage_base_url.is_some() {
2100-
return Err(ParserError::ParserError(
2101-
"duplicate STORAGE_BASE_URL in STORAGE_LOCATION".to_string(),
2102-
));
2103-
}
2104-
parser.expect_token(&Token::Eq)?;
2105-
storage_base_url = Some(parser.parse_literal_string()?);
2106-
} else if parser.parse_keyword(Keyword::STORAGE_AWS_ROLE_ARN) {
2107-
if storage_aws_role_arn.is_some() {
2108-
return Err(ParserError::ParserError(
2109-
"duplicate STORAGE_AWS_ROLE_ARN in STORAGE_LOCATION".to_string(),
2110-
));
2111-
}
2112-
parser.expect_token(&Token::Eq)?;
2113-
storage_aws_role_arn = Some(parser.parse_literal_string()?);
2114-
} else if parser.parse_keyword(Keyword::STORAGE_AWS_EXTERNAL_ID) {
2115-
if storage_aws_external_id.is_some() {
2116-
return Err(ParserError::ParserError(
2117-
"duplicate STORAGE_AWS_EXTERNAL_ID in STORAGE_LOCATION".to_string(),
2118-
));
2119-
}
2120-
parser.expect_token(&Token::Eq)?;
2121-
storage_aws_external_id = Some(parser.parse_literal_string()?);
2122-
} else if parser.parse_keyword(Keyword::ENCRYPTION) {
2123-
if encryption.is_some() {
2124-
return Err(ParserError::ParserError(
2125-
"duplicate ENCRYPTION in STORAGE_LOCATION".to_string(),
2126-
));
2127-
}
2128-
parser.expect_token(&Token::Eq)?;
2129-
parser.expect_token(&Token::LParen)?;
2130-
encryption = Some(parse_external_volume_encryption(parser)?);
2131-
parser.expect_token(&Token::RParen)?;
2132-
} else {
2133-
break;
2134-
}
2135-
}
2136-
2137-
let storage_base_url = storage_base_url.ok_or_else(|| {
2138-
ParserError::ParserError("STORAGE_BASE_URL is required in STORAGE_LOCATION".to_string())
2139-
})?;
2140-
2141-
Ok(ExternalVolumeStorageLocation {
2142-
name,
2143-
storage_provider,
2144-
storage_base_url,
2145-
storage_aws_role_arn,
2146-
storage_aws_external_id,
2147-
encryption,
2148-
})
2149-
}
2150-
2151-
/// Parse encryption options: `TYPE = '...' [KMS_KEY_ID = '...']`
2152-
fn parse_external_volume_encryption(
2153-
parser: &mut Parser,
2154-
) -> Result<ExternalVolumeEncryption, ParserError> {
2155-
parser.expect_keyword(Keyword::TYPE)?;
2156-
parser.expect_token(&Token::Eq)?;
2157-
let kind = parser.parse_literal_string()?;
2158-
2159-
let mut kms_key_id = None;
2160-
if parser.parse_keyword(Keyword::KMS_KEY_ID) {
2161-
parser.expect_token(&Token::Eq)?;
2162-
kms_key_id = Some(parser.parse_literal_string()?);
2163-
}
2164-
2165-
Ok(ExternalVolumeEncryption { kind, kms_key_id })
2166-
}
2167-
21682063
/// Parse `ALTER EXTERNAL VOLUME [IF EXISTS] <name> ...`
21692064
fn parse_alter_external_volume(parser: &mut Parser) -> Result<Statement, ParserError> {
21702065
let if_exists = parser.parse_keywords(&[Keyword::IF, Keyword::EXISTS]);
21712066
let name = parser.parse_object_name(false)?;
21722067

21732068
let operation = if parser.parse_keyword(Keyword::ADD) {
2174-
parser.expect_keyword(Keyword::STORAGE_LOCATION)?;
2069+
parser.expect_keyword_is(Keyword::STORAGE_LOCATION)?;
21752070
parser.expect_token(&Token::Eq)?;
2176-
parser.expect_token(&Token::LParen)?;
2177-
let loc = parse_external_volume_storage_location(parser)?;
2178-
parser.expect_token(&Token::RParen)?;
2179-
AlterExternalVolumeOperation::AddStorageLocation(loc)
2071+
AlterExternalVolumeOperation::AddStorageLocation(parser.parse_key_value_options(true, &[])?)
21802072
} else if parser.parse_keyword(Keyword::SET) {
2181-
parser.expect_keyword(Keyword::ALLOW_WRITES)?;
2073+
parser.expect_keyword_is(Keyword::ALLOW_WRITES)?;
21822074
parser.expect_token(&Token::Eq)?;
21832075
AlterExternalVolumeOperation::SetAllowWrites(parser.parse_boolean_string()?)
21842076
} else if parser.parse_keyword(Keyword::REMOVE) {
2185-
parser.expect_keyword(Keyword::STORAGE_LOCATION)?;
2077+
parser.expect_keyword_is(Keyword::STORAGE_LOCATION)?;
21862078
let loc_name = parser.parse_literal_string()?;
21872079
AlterExternalVolumeOperation::RemoveStorageLocation(loc_name)
21882080
} else {
@@ -2199,13 +2091,6 @@ fn parse_alter_external_volume(parser: &mut Parser) -> Result<Statement, ParserE
21992091
})
22002092
}
22012093

2202-
/// Parse `DROP EXTERNAL VOLUME [IF EXISTS] <name>`
2203-
fn parse_drop_external_volume(parser: &mut Parser) -> Result<Statement, ParserError> {
2204-
let if_exists = parser.parse_keywords(&[Keyword::IF, Keyword::EXISTS]);
2205-
let name = parser.parse_object_name(false)?;
2206-
Ok(Statement::DropExternalVolume { name, if_exists })
2207-
}
2208-
22092094
/// Parse `DESC[RIBE] EXTERNAL VOLUME <name>`
22102095
fn parse_describe_external_volume(parser: &mut Parser) -> Result<Statement, ParserError> {
22112096
let name = parser.parse_object_name(false)?;

src/keywords.rs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -570,7 +570,6 @@ define_keywords!(
570570
KEYS,
571571
KEY_BLOCK_SIZE,
572572
KILL,
573-
KMS_KEY_ID,
574573
LAG,
575574
LAMBDA,
576575
LANGUAGE,
@@ -1006,13 +1005,9 @@ define_keywords!(
10061005
STDOUT,
10071006
STEP,
10081007
STORAGE,
1009-
STORAGE_AWS_EXTERNAL_ID,
1010-
STORAGE_AWS_ROLE_ARN,
1011-
STORAGE_BASE_URL,
10121008
STORAGE_INTEGRATION,
10131009
STORAGE_LOCATION,
10141010
STORAGE_LOCATIONS,
1015-
STORAGE_PROVIDER,
10161011
STORAGE_SERIALIZATION_POLICY,
10171012
STORED,
10181013
STRAIGHT_JOIN,

0 commit comments

Comments
 (0)