Skip to content

Commit ec60ac7

Browse files
Snowflake: Add ALTER EXTERNAL VOLUME
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent f770b81 commit ec60ac7

5 files changed

Lines changed: 245 additions & 9 deletions

File tree

src/ast/mod.rs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4563,6 +4563,11 @@ pub enum Statement {
45634563
/// See <https://docs.snowflake.com/en/sql-reference/sql/create-external-volume>
45644564
CreateExternalVolume(CreateExternalVolume),
45654565
/// ```sql
4566+
/// ALTER EXTERNAL VOLUME [IF EXISTS] <name> ...
4567+
/// ```
4568+
/// See <https://docs.snowflake.com/en/sql-reference/sql/alter-external-volume>
4569+
AlterExternalVolume(AlterExternalVolume),
4570+
/// ```sql
45664571
/// CREATE [ OR REPLACE ] WAREHOUSE [ IF NOT EXISTS ] <name>
45674572
/// [ [ WITH ] <property> = <value> [ ... ] ]
45684573
/// ```
@@ -6299,6 +6304,7 @@ impl fmt::Display for Statement {
62996304
Ok(())
63006305
}
63016306
Statement::CreateExternalVolume(s) => write!(f, "{s}"),
6307+
Statement::AlterExternalVolume(s) => write!(f, "{s}"),
63026308
Statement::CreateWarehouse(s) => write!(f, "{s}"),
63036309
Statement::CopyIntoSnowflake {
63046310
kind,
@@ -11193,6 +11199,71 @@ impl fmt::Display for CreateExternalVolume {
1119311199
}
1119411200
}
1119511201

11202+
/// ```sql
11203+
/// ALTER EXTERNAL VOLUME [IF EXISTS] <name> ...
11204+
/// ```
11205+
/// See <https://docs.snowflake.com/en/sql-reference/sql/alter-external-volume>
11206+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
11207+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
11208+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
11209+
pub struct AlterExternalVolume {
11210+
/// External volume name.
11211+
pub name: ObjectName,
11212+
/// `IF EXISTS` flag.
11213+
pub if_exists: bool,
11214+
/// The alter operation.
11215+
pub operation: AlterExternalVolumeOperation,
11216+
}
11217+
11218+
impl fmt::Display for AlterExternalVolume {
11219+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
11220+
write!(
11221+
f,
11222+
"ALTER EXTERNAL VOLUME {if_exists}{name} {operation}",
11223+
if_exists = if self.if_exists { "IF EXISTS " } else { "" },
11224+
name = self.name,
11225+
operation = self.operation,
11226+
)
11227+
}
11228+
}
11229+
11230+
/// Operations for `ALTER EXTERNAL VOLUME`.
11231+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
11232+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
11233+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
11234+
pub enum AlterExternalVolumeOperation {
11235+
/// `ADD STORAGE_LOCATION = ( ... )`
11236+
AddStorageLocation(KeyValueOptions),
11237+
/// `SET ALLOW_WRITES = TRUE|FALSE`
11238+
SetAllowWrites(bool),
11239+
/// `REMOVE STORAGE_LOCATION '<name>'`
11240+
RemoveStorageLocation(String),
11241+
}
11242+
11243+
impl fmt::Display for AlterExternalVolumeOperation {
11244+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
11245+
match self {
11246+
AlterExternalVolumeOperation::AddStorageLocation(loc) => {
11247+
write!(f, "ADD STORAGE_LOCATION = ({loc})")
11248+
}
11249+
AlterExternalVolumeOperation::SetAllowWrites(val) => {
11250+
write!(
11251+
f,
11252+
"SET ALLOW_WRITES = {}",
11253+
if *val { "TRUE" } else { "FALSE" }
11254+
)
11255+
}
11256+
AlterExternalVolumeOperation::RemoveStorageLocation(name) => {
11257+
write!(
11258+
f,
11259+
"REMOVE STORAGE_LOCATION '{}'",
11260+
value::escape_single_quote_string(name)
11261+
)
11262+
}
11263+
}
11264+
}
11265+
}
11266+
1119611267
/// MSSQL's json null clause
1119711268
///
1119811269
/// ```plaintext
@@ -12694,6 +12765,12 @@ impl From<CreateExternalVolume> for Statement {
1269412765
}
1269512766
}
1269612767

12768+
impl From<AlterExternalVolume> for Statement {
12769+
fn from(a: AlterExternalVolume) -> Self {
12770+
Self::AlterExternalVolume(a)
12771+
}
12772+
}
12773+
1269712774
impl From<CreateWarehouse> for Statement {
1269812775
fn from(c: CreateWarehouse) -> Self {
1269912776
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::AlterUser(..) => Span::empty(),
525525
Statement::Reset(..) => Span::empty(),
526526
Statement::CreateExternalVolume(..) => Span::empty(),
527+
Statement::AlterExternalVolume(..) => Span::empty(),
527528
}
528529
}
529530
}

src/dialect/snowflake.rs

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,15 @@ use crate::ast::helpers::stmt_data_loading::{
2727
FileStagingCommand, StageLoadSelectItem, StageLoadSelectItemKind, StageParamsObject,
2828
};
2929
use crate::ast::{
30-
AlterTable, AlterTableOperation, AlterTableType, CatalogSyncNamespaceMode, ColumnOption,
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,
30+
AlterExternalVolume, AlterExternalVolumeOperation, AlterTable, AlterTableOperation,
31+
AlterTableType, CatalogSyncNamespaceMode, ColumnOption, ColumnPolicy, ColumnPolicyProperty,
32+
ContactEntry, CopyIntoSnowflakeKind, CreateExternalVolume, CreateTable, CreateTableLikeKind,
33+
DollarQuotedString, Ident, IdentityParameters, IdentityProperty, IdentityPropertyFormatKind,
34+
IdentityPropertyKind, IdentityPropertyOrder, InitializeKind, Insert,
35+
MultiTableInsertIntoClause, MultiTableInsertType, MultiTableInsertValue,
36+
MultiTableInsertValues, MultiTableInsertWhenClause, ObjectName, ObjectNamePart,
37+
RefreshModeKind, RowAccessPolicy, ShowObjects, SqlOption, Statement, StorageLifecyclePolicy,
38+
StorageSerializationPolicy, TableObject, TagsColumnOption, Value, WrappedCollection,
3939
};
4040
use crate::dialect::{Dialect, Precedence};
4141
use crate::keywords::Keyword;
@@ -272,6 +272,11 @@ impl Dialect for SnowflakeDialect {
272272
return Some(parse_alter_dynamic_table(parser));
273273
}
274274

275+
if parser.parse_keywords(&[Keyword::ALTER, Keyword::EXTERNAL, Keyword::VOLUME]) {
276+
// ALTER EXTERNAL VOLUME
277+
return Some(parse_alter_external_volume(parser));
278+
}
279+
275280
if parser.parse_keywords(&[Keyword::ALTER, Keyword::EXTERNAL, Keyword::TABLE]) {
276281
// ALTER EXTERNAL TABLE
277282
return Some(parse_alter_external_table(parser));
@@ -2041,6 +2046,40 @@ fn parse_create_external_volume(
20412046
.into())
20422047
}
20432048

2049+
/// Parse `ALTER EXTERNAL VOLUME [IF EXISTS] <name> ...`
2050+
fn parse_alter_external_volume(parser: &mut Parser) -> Result<Statement, ParserError> {
2051+
let if_exists = parser.parse_keywords(&[Keyword::IF, Keyword::EXISTS]);
2052+
let name = parser.parse_object_name(false)?;
2053+
2054+
let operation = if parser.parse_keyword(Keyword::ADD) {
2055+
parser.expect_keyword_is(Keyword::STORAGE_LOCATION)?;
2056+
parser.expect_token(&Token::Eq)?;
2057+
AlterExternalVolumeOperation::AddStorageLocation(parse_external_volume_storage_location(
2058+
parser,
2059+
)?)
2060+
} else if parser.parse_keyword(Keyword::SET) {
2061+
parser.expect_keyword_is(Keyword::ALLOW_WRITES)?;
2062+
parser.expect_token(&Token::Eq)?;
2063+
AlterExternalVolumeOperation::SetAllowWrites(parser.parse_boolean_string()?)
2064+
} else if parser.parse_keyword(Keyword::REMOVE) {
2065+
parser.expect_keyword_is(Keyword::STORAGE_LOCATION)?;
2066+
let loc_name = parser.parse_literal_string()?;
2067+
AlterExternalVolumeOperation::RemoveStorageLocation(loc_name)
2068+
} else {
2069+
return parser.expected(
2070+
"ADD, SET, or REMOVE after ALTER EXTERNAL VOLUME <name>",
2071+
parser.peek_token(),
2072+
);
2073+
};
2074+
2075+
Ok(AlterExternalVolume {
2076+
name,
2077+
if_exists,
2078+
operation,
2079+
}
2080+
.into())
2081+
}
2082+
20442083
/// Parse one parenthesized storage-location option list, e.g.
20452084
/// `(NAME='loc1' STORAGE_PROVIDER='S3' ...)`. The options (and the
20462085
/// `ENCRYPTION = (...)` sub-list) are parsed generically via

src/keywords.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1007,6 +1007,7 @@ define_keywords!(
10071007
STEP,
10081008
STORAGE,
10091009
STORAGE_INTEGRATION,
1010+
STORAGE_LOCATION,
10101011
STORAGE_LOCATIONS,
10111012
STORAGE_SERIALIZATION_POLICY,
10121013
STORED,

tests/sqlparser_snowflake.rs

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5249,3 +5249,121 @@ fn test_drop_external_volume_if_exists() {
52495249
_ => unreachable!(),
52505250
}
52515251
}
5252+
5253+
#[test]
5254+
fn test_alter_external_volume_add_storage_location() {
5255+
let sql = "ALTER EXTERNAL VOLUME my_vol ADD STORAGE_LOCATION = \
5256+
(NAME='loc2' STORAGE_PROVIDER='S3' STORAGE_BASE_URL='s3://bucket2/')";
5257+
match snowflake().verified_stmt(sql) {
5258+
Statement::AlterExternalVolume(AlterExternalVolume {
5259+
name,
5260+
if_exists,
5261+
operation,
5262+
}) => {
5263+
assert_eq!("my_vol", name.to_string());
5264+
assert!(!if_exists);
5265+
match operation {
5266+
AlterExternalVolumeOperation::AddStorageLocation(loc) => {
5267+
assert_eq!(Some("loc2"), ext_vol_option(&loc.options, "NAME"));
5268+
assert_eq!(Some("S3"), ext_vol_option(&loc.options, "STORAGE_PROVIDER"));
5269+
}
5270+
_ => unreachable!(),
5271+
}
5272+
}
5273+
_ => unreachable!(),
5274+
}
5275+
}
5276+
5277+
#[test]
5278+
fn test_alter_external_volume_add_storage_location_full() {
5279+
// The ADD path reuses the same option parser, so exercise the optional
5280+
// fields (external id + encryption) through it too.
5281+
snowflake().verified_stmt(
5282+
"ALTER EXTERNAL VOLUME my_vol ADD STORAGE_LOCATION = \
5283+
(NAME='loc2' STORAGE_PROVIDER='S3' STORAGE_BASE_URL='s3://bucket2/' \
5284+
STORAGE_AWS_ROLE_ARN='arn:aws:iam::role/r' \
5285+
STORAGE_AWS_EXTERNAL_ID='ext-id' \
5286+
ENCRYPTION=(TYPE='AWS_SSE_KMS' KMS_KEY_ID='key'))",
5287+
);
5288+
}
5289+
5290+
#[test]
5291+
fn test_alter_external_volume_set_allow_writes() {
5292+
match snowflake().verified_stmt("ALTER EXTERNAL VOLUME my_vol SET ALLOW_WRITES = TRUE") {
5293+
Statement::AlterExternalVolume(AlterExternalVolume { operation, .. }) => {
5294+
assert_eq!(
5295+
AlterExternalVolumeOperation::SetAllowWrites(true),
5296+
operation
5297+
);
5298+
}
5299+
_ => unreachable!(),
5300+
}
5301+
5302+
match snowflake().verified_stmt("ALTER EXTERNAL VOLUME my_vol SET ALLOW_WRITES = FALSE") {
5303+
Statement::AlterExternalVolume(AlterExternalVolume { operation, .. }) => {
5304+
assert_eq!(
5305+
AlterExternalVolumeOperation::SetAllowWrites(false),
5306+
operation
5307+
);
5308+
}
5309+
_ => unreachable!(),
5310+
}
5311+
}
5312+
5313+
#[test]
5314+
fn test_alter_external_volume_if_exists() {
5315+
match snowflake()
5316+
.verified_stmt("ALTER EXTERNAL VOLUME IF EXISTS my_vol SET ALLOW_WRITES = TRUE")
5317+
{
5318+
Statement::AlterExternalVolume(AlterExternalVolume { if_exists, .. }) => {
5319+
assert!(if_exists);
5320+
}
5321+
_ => unreachable!(),
5322+
}
5323+
}
5324+
5325+
#[test]
5326+
fn test_alter_external_volume_remove_storage_location() {
5327+
match snowflake().verified_stmt("ALTER EXTERNAL VOLUME my_vol REMOVE STORAGE_LOCATION 'loc1'") {
5328+
Statement::AlterExternalVolume(AlterExternalVolume { operation, .. }) => {
5329+
assert_eq!(
5330+
AlterExternalVolumeOperation::RemoveStorageLocation("loc1".to_string()),
5331+
operation
5332+
);
5333+
}
5334+
_ => unreachable!(),
5335+
}
5336+
}
5337+
5338+
#[test]
5339+
fn test_alter_external_volume_add_empty_storage_location() {
5340+
let err = snowflake()
5341+
.parse_sql_statements("ALTER EXTERNAL VOLUME my_vol ADD STORAGE_LOCATION = ()")
5342+
.expect_err("parser must reject an empty storage location");
5343+
assert!(
5344+
err.to_string().contains("storage location options"),
5345+
"unexpected error: {err}"
5346+
);
5347+
}
5348+
5349+
#[test]
5350+
fn test_alter_external_volume_allow_writes_non_boolean() {
5351+
let err = snowflake()
5352+
.parse_sql_statements("ALTER EXTERNAL VOLUME my_vol SET ALLOW_WRITES = 1")
5353+
.expect_err("parser must reject non-boolean ALLOW_WRITES");
5354+
assert!(
5355+
err.to_string().contains("TRUE or FALSE"),
5356+
"unexpected error: {err}"
5357+
);
5358+
}
5359+
5360+
#[test]
5361+
fn test_alter_external_volume_missing_operation() {
5362+
let err = snowflake()
5363+
.parse_sql_statements("ALTER EXTERNAL VOLUME my_vol")
5364+
.expect_err("parser must reject ALTER EXTERNAL VOLUME without an operation");
5365+
assert!(
5366+
err.to_string().contains("ADD, SET, or REMOVE"),
5367+
"unexpected error: {err}"
5368+
);
5369+
}

0 commit comments

Comments
 (0)