Skip to content

Commit 896abcd

Browse files
Snowflake: Add CREATE and DROP EXTERNAL VOLUME
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 5d653a2 commit 896abcd

6 files changed

Lines changed: 485 additions & 8 deletions

File tree

‎src/ast/mod.rs‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4608,6 +4608,11 @@ pub enum Statement {
46084608
comment: Option<String>,
46094609
},
46104610
/// ```sql
4611+
/// CREATE [OR REPLACE] EXTERNAL VOLUME [IF NOT EXISTS] <name>
4612+
/// ```
4613+
/// See <https://docs.snowflake.com/en/sql-reference/sql/create-external-volume>
4614+
CreateExternalVolume(CreateExternalVolume),
4615+
/// ```sql
46114616
/// CREATE [ OR REPLACE ] WAREHOUSE [ IF NOT EXISTS ] <name>
46124617
/// [ [ WITH ] <property> = <value> [ ... ] ]
46134618
/// ```
@@ -6344,6 +6349,7 @@ impl fmt::Display for Statement {
63446349
}
63456350
Ok(())
63466351
}
6352+
Statement::CreateExternalVolume(s) => write!(f, "{s}"),
63476353
Statement::CreateWarehouse(s) => write!(f, "{s}"),
63486354
Statement::CopyIntoSnowflake {
63496355
kind,
@@ -8738,6 +8744,8 @@ pub enum ObjectType {
87388744
User,
87398745
/// A stream.
87408746
Stream,
8747+
/// A Snowflake external volume.
8748+
ExternalVolume,
87418749
/// A warehouse.
87428750
Warehouse,
87438751
}
@@ -8758,6 +8766,7 @@ impl fmt::Display for ObjectType {
87588766
ObjectType::Type => "TYPE",
87598767
ObjectType::User => "USER",
87608768
ObjectType::Stream => "STREAM",
8769+
ObjectType::ExternalVolume => "EXTERNAL VOLUME",
87618770
ObjectType::Warehouse => "WAREHOUSE",
87628771
})
87638772
}
@@ -11180,6 +11189,59 @@ pub struct ShowObjects {
1118011189
pub show_options: ShowStatementOptions,
1118111190
}
1118211191

11192+
/// ```sql
11193+
/// CREATE [OR REPLACE] EXTERNAL VOLUME [IF NOT EXISTS] <name>
11194+
/// ```
11195+
/// See <https://docs.snowflake.com/en/sql-reference/sql/create-external-volume>
11196+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
11197+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
11198+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
11199+
pub struct CreateExternalVolume {
11200+
/// `OR REPLACE` flag.
11201+
pub or_replace: bool,
11202+
/// `IF NOT EXISTS` flag.
11203+
pub if_not_exists: bool,
11204+
/// External volume name.
11205+
pub name: ObjectName,
11206+
/// Storage locations, each a parenthesized list of key-value options
11207+
/// (e.g. `(NAME='loc1' STORAGE_PROVIDER='S3' STORAGE_BASE_URL='s3://bucket/')`).
11208+
pub storage_locations: Vec<KeyValueOptions>,
11209+
/// Optional `ALLOW_WRITES` setting.
11210+
pub allow_writes: Option<bool>,
11211+
/// Optional comment.
11212+
pub comment: Option<String>,
11213+
}
11214+
11215+
impl fmt::Display for CreateExternalVolume {
11216+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
11217+
write!(
11218+
f,
11219+
"CREATE {or_replace}EXTERNAL VOLUME {if_not_exists}{name} STORAGE_LOCATIONS = (",
11220+
or_replace = if self.or_replace { "OR REPLACE " } else { "" },
11221+
if_not_exists = if self.if_not_exists {
11222+
"IF NOT EXISTS "
11223+
} else {
11224+
""
11225+
},
11226+
name = self.name,
11227+
)?;
11228+
for (i, loc) in self.storage_locations.iter().enumerate() {
11229+
if i > 0 {
11230+
write!(f, ", ")?;
11231+
}
11232+
write!(f, "({loc})")?;
11233+
}
11234+
write!(f, ")")?;
11235+
if let Some(val) = self.allow_writes {
11236+
write!(f, " ALLOW_WRITES = {}", if val { "TRUE" } else { "FALSE" })?;
11237+
}
11238+
if let Some(ref c) = self.comment {
11239+
write!(f, " COMMENT = '{}'", value::escape_single_quote_string(c))?;
11240+
}
11241+
Ok(())
11242+
}
11243+
}
11244+
1118311245
/// MSSQL's json null clause
1118411246
///
1118511247
/// ```plaintext
@@ -12675,6 +12737,12 @@ impl From<CreateUser> for Statement {
1267512737
}
1267612738
}
1267712739

12740+
impl From<CreateExternalVolume> for Statement {
12741+
fn from(c: CreateExternalVolume) -> Self {
12742+
Self::CreateExternalVolume(c)
12743+
}
12744+
}
12745+
1267812746
impl From<CreateWarehouse> for Statement {
1267912747
fn from(c: CreateWarehouse) -> Self {
1268012748
Self::CreateWarehouse(c)

‎src/ast/spans.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,7 @@ impl Spanned for Statement {
524524
Statement::Vacuum(..) => Span::empty(),
525525
Statement::AlterUser(..) => Span::empty(),
526526
Statement::Reset(..) => Span::empty(),
527+
Statement::CreateExternalVolume(..) => Span::empty(),
527528
}
528529
}
529530
}

‎src/dialect/snowflake.rs‎

Lines changed: 74 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,14 @@ use crate::ast::helpers::stmt_data_loading::{
2828
};
2929
use crate::ast::{
3030
AlterTable, AlterTableOperation, AlterTableType, CatalogSyncNamespaceMode, ColumnOption,
31-
ColumnPolicy, ColumnPolicyProperty, ContactEntry, CopyIntoSnowflakeKind, CreateTable,
32-
CreateTableLikeKind, DollarQuotedString, Ident, IdentityParameters, IdentityProperty,
33-
IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, InitializeKind,
34-
Insert, MultiTableInsertIntoClause, MultiTableInsertType, MultiTableInsertValue,
35-
MultiTableInsertValues, MultiTableInsertWhenClause, ObjectName, ObjectNamePart,
36-
RefreshModeKind, RowAccessPolicy, ShowObjects, SqlOption, Statement, StorageLifecyclePolicy,
37-
StorageSerializationPolicy, TableObject, TagsColumnOption, Value, WrappedCollection,
31+
ColumnPolicy, ColumnPolicyProperty, ContactEntry, CopyIntoSnowflakeKind, CreateExternalVolume,
32+
CreateTable, CreateTableLikeKind, DollarQuotedString, Ident, IdentityParameters,
33+
IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder,
34+
InitializeKind, Insert, MultiTableInsertIntoClause, MultiTableInsertType,
35+
MultiTableInsertValue, MultiTableInsertValues, MultiTableInsertWhenClause, ObjectName,
36+
ObjectNamePart, RefreshModeKind, RowAccessPolicy, ShowObjects, SqlOption, Statement,
37+
StorageLifecyclePolicy, StorageSerializationPolicy, TableObject, TagsColumnOption, Value,
38+
WrappedCollection,
3839
};
3940
use crate::dialect::{Dialect, Precedence};
4041
use crate::keywords::Keyword;
@@ -312,6 +313,12 @@ impl Dialect for SnowflakeDialect {
312313
// possibly CREATE STAGE
313314
//[ OR REPLACE ]
314315
let or_replace = parser.parse_keywords(&[Keyword::OR, Keyword::REPLACE]);
316+
317+
// CREATE [OR REPLACE] EXTERNAL VOLUME
318+
if parser.parse_keywords(&[Keyword::EXTERNAL, Keyword::VOLUME]) {
319+
return Some(parse_create_external_volume(or_replace, parser));
320+
}
321+
315322
// LOCAL | GLOBAL
316323
let global = match parser.parse_one_of_keywords(&[Keyword::LOCAL, Keyword::GLOBAL]) {
317324
Some(Keyword::LOCAL) => Some(false),
@@ -2018,3 +2025,63 @@ fn parse_multi_table_insert_when_clauses(
20182025

20192026
Ok((when_clauses, else_clause))
20202027
}
2028+
2029+
/// Parse `CREATE [OR REPLACE] EXTERNAL VOLUME [IF NOT EXISTS] <name> ...`
2030+
///
2031+
/// Each storage location is parsed by [`parse_external_volume_storage_location`];
2032+
/// the trailing `ALLOW_WRITES` and `COMMENT` properties are accepted in any
2033+
/// order.
2034+
fn parse_create_external_volume(
2035+
or_replace: bool,
2036+
parser: &mut Parser,
2037+
) -> Result<Statement, ParserError> {
2038+
let if_not_exists = parser.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]);
2039+
let name = parser.parse_object_name(false)?;
2040+
2041+
parser.expect_keyword_is(Keyword::STORAGE_LOCATIONS)?;
2042+
parser.expect_token(&Token::Eq)?;
2043+
parser.expect_token(&Token::LParen)?;
2044+
2045+
let storage_locations = parser.parse_comma_separated(parse_external_volume_storage_location)?;
2046+
parser.expect_token(&Token::RParen)?;
2047+
2048+
let mut allow_writes = None;
2049+
let mut comment = None;
2050+
2051+
loop {
2052+
if parser.parse_keyword(Keyword::ALLOW_WRITES) {
2053+
parser.expect_token(&Token::Eq)?;
2054+
allow_writes = Some(parser.parse_boolean_string()?);
2055+
} else if parser.parse_keyword(Keyword::COMMENT) {
2056+
parser.expect_token(&Token::Eq)?;
2057+
comment = Some(parser.parse_comment_value()?);
2058+
} else {
2059+
break;
2060+
}
2061+
}
2062+
2063+
Ok(CreateExternalVolume {
2064+
or_replace,
2065+
if_not_exists,
2066+
name,
2067+
storage_locations,
2068+
allow_writes,
2069+
comment,
2070+
}
2071+
.into())
2072+
}
2073+
2074+
/// Parse one parenthesized storage-location option list, e.g.
2075+
/// `(NAME='loc1' STORAGE_PROVIDER='S3' ...)`. The options (and the
2076+
/// `ENCRYPTION = (...)` sub-list) are parsed generically via
2077+
/// [`Parser::parse_key_value_options`]; only an empty list is rejected,
2078+
/// field order and the exact option set are left to the consumer.
2079+
fn parse_external_volume_storage_location(
2080+
parser: &mut Parser,
2081+
) -> Result<KeyValueOptions, ParserError> {
2082+
let location = parser.parse_key_value_options(true, &[])?;
2083+
if location.options.is_empty() {
2084+
return parser.expected("storage location options", parser.peek_token());
2085+
}
2086+
Ok(location)
2087+
}

‎src/keywords.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ define_keywords!(
112112
ALL,
113113
ALLOCATE,
114114
ALLOWOVERWRITE,
115+
ALLOW_WRITES,
115116
ALTER,
116117
ALWAYS,
117118
ANALYZE,
@@ -1008,6 +1009,7 @@ define_keywords!(
10081009
STEP,
10091010
STORAGE,
10101011
STORAGE_INTEGRATION,
1012+
STORAGE_LOCATIONS,
10111013
STORAGE_SERIALIZATION_POLICY,
10121014
STORED,
10131015
STRAIGHT_JOIN,

‎src/parser/mod.rs‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7745,6 +7745,8 @@ impl<'a> Parser<'a> {
77457745
ObjectType::User
77467746
} else if self.parse_keyword(Keyword::STREAM) {
77477747
ObjectType::Stream
7748+
} else if self.parse_keywords(&[Keyword::EXTERNAL, Keyword::VOLUME]) {
7749+
ObjectType::ExternalVolume
77487750
} else if self.parse_keyword(Keyword::WAREHOUSE) {
77497751
ObjectType::Warehouse
77507752
} else if self.parse_keyword(Keyword::FUNCTION) {
@@ -7774,7 +7776,7 @@ impl<'a> Parser<'a> {
77747776
};
77757777
} else {
77767778
return self.expected_ref(
7777-
"COLLATION, CONNECTOR, DATABASE, EXTENSION, FUNCTION, INDEX, OPERATOR, POLICY, PROCEDURE, ROLE, SCHEMA, SECRET, SEQUENCE, STAGE, TABLE, TRIGGER, TYPE, VIEW, MATERIALIZED VIEW, USER or WAREHOUSE after DROP",
7779+
"COLLATION, CONNECTOR, DATABASE, EXTENSION, EXTERNAL VOLUME, FUNCTION, INDEX, OPERATOR, POLICY, PROCEDURE, ROLE, SCHEMA, SECRET, SEQUENCE, STAGE, TABLE, TRIGGER, TYPE, VIEW, MATERIALIZED VIEW, USER or WAREHOUSE after DROP",
77787780
self.peek_token_ref(),
77797781
);
77807782
};

0 commit comments

Comments
 (0)