Skip to content

Commit 2eecf96

Browse files
committed
Add CREATE EXTERNAL VOLUME sql
1 parent 12655c2 commit 2eecf96

File tree

5 files changed

+295
-6
lines changed

5 files changed

+295
-6
lines changed

src/ast/mod.rs

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3283,6 +3283,18 @@ pub enum Statement {
32833283
option: Option<ReferentialAction>,
32843284
},
32853285
/// ```sql
3286+
/// CREATE EXTERNAL VOLUME
3287+
/// ```
3288+
/// See <https://docs.snowflake.com/en/sql-reference/sql/create-external-volume>
3289+
CreateExternalVolume {
3290+
or_replace: bool,
3291+
if_not_exists: bool,
3292+
name: ObjectName,
3293+
storage_locations: Vec<CloudProviderParams>,
3294+
allow_writes: Option<bool>,
3295+
comment: Option<String>,
3296+
},
3297+
/// ```sql
32863298
/// CREATE PROCEDURE
32873299
/// ```
32883300
CreateProcedure {
@@ -4171,6 +4183,39 @@ impl fmt::Display for Statement {
41714183
}
41724184
Ok(())
41734185
}
4186+
Statement::CreateExternalVolume {
4187+
or_replace,
4188+
if_not_exists,
4189+
name,
4190+
storage_locations,
4191+
allow_writes,
4192+
comment,
4193+
} => {
4194+
write!(
4195+
f,
4196+
"CREATE {or_replace}EXTERNAL VOLUME {if_not_exists}{name}",
4197+
or_replace = if *or_replace { "OR REPLACE " } else { "" },
4198+
if_not_exists = if *if_not_exists { " IF NOT EXISTS" } else { "" },
4199+
)?;
4200+
if !storage_locations.is_empty() {
4201+
write!(
4202+
f,
4203+
" STORAGE_LOCATIONS = ({})",
4204+
storage_locations
4205+
.iter()
4206+
.map(|loc| format!("({})", loc))
4207+
.collect::<Vec<_>>()
4208+
.join(", ")
4209+
)?;
4210+
}
4211+
if let Some(true) = allow_writes {
4212+
write!(f, " ALLOW_WRITES = TRUE")?;
4213+
}
4214+
if let Some(c) = comment {
4215+
write!(f, " COMMENT = '{}'", c.replace('\'', "''"))?;
4216+
}
4217+
Ok(())
4218+
}
41744219
Statement::CreateProcedure {
41754220
name,
41764221
or_alter,
@@ -8871,6 +8916,74 @@ impl fmt::Display for NullInclusion {
88718916
}
88728917
}
88738918

8919+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
8920+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
8921+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
8922+
pub struct CloudProviderParams {
8923+
pub name: String,
8924+
pub provider: String,
8925+
pub base_url: Option<String>,
8926+
pub aws_role_arn: Option<String>,
8927+
pub aws_access_point_arn: Option<String>,
8928+
pub aws_external_id: Option<String>,
8929+
pub azure_tenant_id: Option<String>,
8930+
pub storage_endpoint: Option<String>,
8931+
pub use_private_link_endpoint: Option<bool>,
8932+
pub encryption: KeyValueOptions,
8933+
pub credentials: KeyValueOptions,
8934+
}
8935+
8936+
impl fmt::Display for CloudProviderParams {
8937+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
8938+
write!(
8939+
f,
8940+
"NAME = '{}' STORAGE_PROVIDER = '{}'",
8941+
self.name, self.provider
8942+
)?;
8943+
8944+
if let Some(base_url) = &self.base_url {
8945+
write!(f, " STORAGE_BASE_URL = '{base_url}'")?;
8946+
}
8947+
8948+
if let Some(arn) = &self.aws_role_arn {
8949+
write!(f, " STORAGE_AWS_ROLE_ARN = '{arn}'")?;
8950+
}
8951+
8952+
if let Some(ap_arn) = &self.aws_access_point_arn {
8953+
write!(f, " STORAGE_AWS_ACCESS_POINT_ARN = '{ap_arn}'")?;
8954+
}
8955+
8956+
if let Some(ext_id) = &self.aws_external_id {
8957+
write!(f, " STORAGE_AWS_EXTERNAL_ID = '{ext_id}'")?;
8958+
}
8959+
8960+
if let Some(tenant_id) = &self.azure_tenant_id {
8961+
write!(f, " AZURE_TENANT_ID = '{tenant_id}'")?;
8962+
}
8963+
8964+
if let Some(endpoint) = &self.storage_endpoint {
8965+
write!(f, " STORAGE_ENDPOINT = '{endpoint}'")?;
8966+
}
8967+
8968+
if let Some(use_pl) = self.use_private_link_endpoint {
8969+
write!(
8970+
f,
8971+
" USE_PRIVATELINK_ENDPOINT = {}",
8972+
if use_pl { "TRUE" } else { "FALSE" }
8973+
)?;
8974+
}
8975+
8976+
if !self.encryption.options.is_empty() {
8977+
write!(f, " ENCRYPTION=({})", self.encryption)?;
8978+
}
8979+
8980+
if !self.credentials.options.is_empty() {
8981+
write!(f, " CREDENTIALS=({})", self.credentials)?;
8982+
}
8983+
Ok(())
8984+
}
8985+
}
8986+
88748987
#[cfg(test)]
88758988
mod tests {
88768989
use super::*;

src/ast/spans.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@ impl Spanned for Values {
251251
/// - [Statement::CreateFunction]
252252
/// - [Statement::CreateTrigger]
253253
/// - [Statement::DropTrigger]
254+
/// - [Statement::CreateExternalVolume]
254255
/// - [Statement::CreateProcedure]
255256
/// - [Statement::CreateMacro]
256257
/// - [Statement::CreateStage]
@@ -468,6 +469,7 @@ impl Spanned for Statement {
468469
Statement::CreateFunction { .. } => Span::empty(),
469470
Statement::CreateTrigger { .. } => Span::empty(),
470471
Statement::DropTrigger { .. } => Span::empty(),
472+
Statement::CreateExternalVolume { .. } => Span::empty(),
471473
Statement::CreateProcedure { .. } => Span::empty(),
472474
Statement::CreateMacro { .. } => Span::empty(),
473475
Statement::CreateStage { .. } => Span::empty(),

src/dialect/snowflake.rs

Lines changed: 152 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
// specific language governing permissions and limitations
1616
// under the License.
1717

18+
use super::keywords::RESERVED_FOR_IDENTIFIER;
1819
#[cfg(not(feature = "std"))]
1920
use crate::alloc::string::ToString;
2021
use crate::ast::helpers::key_value_options::{KeyValueOption, KeyValueOptionType, KeyValueOptions};
@@ -24,10 +25,11 @@ use crate::ast::helpers::stmt_data_loading::{
2425
FileStagingCommand, StageLoadSelectItem, StageParamsObject,
2526
};
2627
use crate::ast::{
27-
CatalogSyncNamespaceMode, ColumnOption, ColumnPolicy, ColumnPolicyProperty, ContactEntry,
28-
CopyIntoSnowflakeKind, Ident, IdentityParameters, IdentityProperty, IdentityPropertyFormatKind,
29-
IdentityPropertyKind, IdentityPropertyOrder, ObjectName, RowAccessPolicy, ShowObjects,
30-
Statement, StorageSerializationPolicy, TagsColumnOption, WrappedCollection,
28+
CatalogSyncNamespaceMode, CloudProviderParams, ColumnOption, ColumnPolicy,
29+
ColumnPolicyProperty, ContactEntry, CopyIntoSnowflakeKind, Ident, IdentityParameters,
30+
IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder,
31+
ObjectName, RowAccessPolicy, ShowObjects, Statement, StorageSerializationPolicy,
32+
TagsColumnOption, WrappedCollection,
3133
};
3234
use crate::dialect::{Dialect, Precedence};
3335
use crate::keywords::Keyword;
@@ -42,8 +44,6 @@ use alloc::vec::Vec;
4244
#[cfg(not(feature = "std"))]
4345
use alloc::{format, vec};
4446

45-
use super::keywords::RESERVED_FOR_IDENTIFIER;
46-
4747
/// A [`Dialect`] for [Snowflake](https://www.snowflake.com/)
4848
#[derive(Debug, Default)]
4949
pub struct SnowflakeDialect;
@@ -179,6 +179,12 @@ impl Dialect for SnowflakeDialect {
179179
));
180180
} else if parser.parse_keyword(Keyword::DATABASE) {
181181
return Some(parse_create_database(or_replace, transient, parser));
182+
} else if parser.parse_keyword(Keyword::EXTERNAL) {
183+
if parser.parse_keyword(Keyword::VOLUME) {
184+
return Some(parse_create_external_volume(or_replace, parser));
185+
} else {
186+
parser.prev_token();
187+
}
182188
} else {
183189
// need to go back with the cursor
184190
let mut back = 1;
@@ -702,6 +708,146 @@ pub fn parse_create_database(
702708
Ok(builder.build())
703709
}
704710

711+
fn parse_create_external_volume(
712+
or_replace: bool,
713+
parser: &mut Parser,
714+
) -> Result<Statement, ParserError> {
715+
let if_not_exists = parser.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]);
716+
let name = parser.parse_object_name(false)?;
717+
let mut comment = None;
718+
let mut allow_writes = None;
719+
let mut storage_locations = Vec::new();
720+
721+
// STORAGE_LOCATIONS (...)
722+
if parser.parse_keywords(&[Keyword::STORAGE_LOCATIONS]) {
723+
parser.expect_token(&Token::Eq)?;
724+
storage_locations = parse_storage_locations(parser)?;
725+
};
726+
727+
// ALLOW_WRITES [ = true | false ]
728+
if parser.parse_keyword(Keyword::ALLOW_WRITES) {
729+
parser.expect_token(&Token::Eq)?;
730+
allow_writes = Some(parser.parse_boolean_string()?);
731+
}
732+
733+
// COMMENT = '...'
734+
if parser.parse_keyword(Keyword::COMMENT) {
735+
parser.expect_token(&Token::Eq)?;
736+
comment = Some(parser.parse_literal_string()?);
737+
}
738+
739+
if storage_locations.is_empty() {
740+
return Err(ParserError::ParserError(
741+
"STORAGE_LOCATIONS is required for CREATE EXTERNAL VOLUME".to_string(),
742+
));
743+
}
744+
745+
Ok(Statement::CreateExternalVolume {
746+
or_replace,
747+
if_not_exists,
748+
name,
749+
allow_writes,
750+
comment,
751+
storage_locations,
752+
})
753+
}
754+
755+
fn parse_storage_locations(parser: &mut Parser) -> Result<Vec<CloudProviderParams>, ParserError> {
756+
let mut locations = Vec::new();
757+
parser.expect_token(&Token::LParen)?;
758+
759+
loop {
760+
parser.expect_token(&Token::LParen)?;
761+
762+
// START OF ONE CloudProviderParams BLOCK
763+
let mut name = None;
764+
let mut provider = None;
765+
let mut base_url = None;
766+
let mut aws_role_arn = None;
767+
let mut aws_access_point_arn = None;
768+
let mut aws_external_id = None;
769+
let mut azure_tenant_id = None;
770+
let mut storage_endpoint = None;
771+
let mut use_private_link_endpoint = None;
772+
let mut encryption: KeyValueOptions = KeyValueOptions { options: vec![] };
773+
let mut credentials: KeyValueOptions = KeyValueOptions { options: vec![] };
774+
775+
loop {
776+
if parser.parse_keyword(Keyword::NAME) {
777+
parser.expect_token(&Token::Eq)?;
778+
name = Some(parser.parse_literal_string()?);
779+
} else if parser.parse_keyword(Keyword::STORAGE_PROVIDER) {
780+
parser.expect_token(&Token::Eq)?;
781+
provider = Some(parser.parse_literal_string()?);
782+
} else if parser.parse_keyword(Keyword::STORAGE_BASE_URL) {
783+
parser.expect_token(&Token::Eq)?;
784+
base_url = Some(parser.parse_literal_string()?);
785+
} else if parser.parse_keyword(Keyword::STORAGE_AWS_ROLE_ARN) {
786+
parser.expect_token(&Token::Eq)?;
787+
aws_role_arn = Some(parser.parse_literal_string()?);
788+
} else if parser.parse_keyword(Keyword::STORAGE_AWS_ACCESS_POINT_ARN) {
789+
parser.expect_token(&Token::Eq)?;
790+
aws_access_point_arn = Some(parser.parse_literal_string()?);
791+
} else if parser.parse_keyword(Keyword::STORAGE_AWS_EXTERNAL_ID) {
792+
parser.expect_token(&Token::Eq)?;
793+
aws_external_id = Some(parser.parse_literal_string()?);
794+
} else if parser.parse_keyword(Keyword::AZURE_TENANT_ID) {
795+
parser.expect_token(&Token::Eq)?;
796+
azure_tenant_id = Some(parser.parse_literal_string()?);
797+
} else if parser.parse_keyword(Keyword::STORAGE_ENDPOINT) {
798+
parser.expect_token(&Token::Eq)?;
799+
storage_endpoint = Some(parser.parse_literal_string()?);
800+
} else if parser.parse_keyword(Keyword::USE_PRIVATELINK_ENDPOINT) {
801+
parser.expect_token(&Token::Eq)?;
802+
use_private_link_endpoint = Some(parser.parse_boolean_string()?);
803+
} else if parser.parse_keyword(Keyword::ENCRYPTION) {
804+
parser.expect_token(&Token::Eq)?;
805+
encryption = KeyValueOptions {
806+
options: parse_parentheses_options(parser)?,
807+
};
808+
} else if parser.parse_keyword(Keyword::CREDENTIALS) {
809+
parser.expect_token(&Token::Eq)?;
810+
credentials = KeyValueOptions {
811+
options: parse_parentheses_options(parser)?,
812+
};
813+
} else if parser.consume_token(&Token::RParen) {
814+
break;
815+
} else {
816+
return parser.expected("a valid key or closing paren", parser.peek_token());
817+
}
818+
}
819+
820+
let Some(name) = name else {
821+
return parser.expected("NAME = '...'", parser.peek_token());
822+
};
823+
824+
let Some(provider) = provider else {
825+
return parser.expected("STORAGE_PROVIDER = '...'", parser.peek_token());
826+
};
827+
828+
locations.push(CloudProviderParams {
829+
name,
830+
provider,
831+
base_url,
832+
aws_role_arn,
833+
aws_access_point_arn,
834+
aws_external_id,
835+
azure_tenant_id,
836+
storage_endpoint,
837+
use_private_link_endpoint,
838+
encryption,
839+
credentials,
840+
});
841+
// EXIT if next token is RParen
842+
if parser.consume_token(&Token::RParen) {
843+
break;
844+
}
845+
// Otherwise expect a comma before next object
846+
parser.expect_token(&Token::Comma)?;
847+
}
848+
Ok(locations)
849+
}
850+
705851
pub fn parse_storage_serialization_policy(
706852
parser: &mut Parser,
707853
) -> Result<StorageSerializationPolicy, ParserError> {

src/keywords.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ define_keywords!(
8989
ALIAS,
9090
ALL,
9191
ALLOCATE,
92+
ALLOW_WRITES,
9293
ALTER,
9394
ALWAYS,
9495
ANALYZE,
@@ -119,6 +120,7 @@ define_keywords!(
119120
AUTO_INCREMENT,
120121
AVG,
121122
AVRO,
123+
AZURE_TENANT_ID,
122124
BACKWARD,
123125
BASE64,
124126
BASE_LOCATION,
@@ -473,6 +475,7 @@ define_keywords!(
473475
KEY,
474476
KEYS,
475477
KILL,
478+
KMS_KEY_ID,
476479
LAG,
477480
LANGUAGE,
478481
LARGE,
@@ -829,7 +832,14 @@ define_keywords!(
829832
STDIN,
830833
STDOUT,
831834
STEP,
835+
STORAGE_AWS_ACCESS_POINT_ARN,
836+
STORAGE_AWS_EXTERNAL_ID,
837+
STORAGE_AWS_ROLE_ARN,
838+
STORAGE_BASE_URL,
839+
STORAGE_ENDPOINT,
832840
STORAGE_INTEGRATION,
841+
STORAGE_LOCATIONS,
842+
STORAGE_PROVIDER,
833843
STORAGE_SERIALIZATION_POLICY,
834844
STORED,
835845
STRICT,
@@ -930,6 +940,7 @@ define_keywords!(
930940
URL,
931941
USAGE,
932942
USE,
943+
USE_PRIVATELINK_ENDPOINT,
933944
USER,
934945
USER_RESOURCES,
935946
USING,

0 commit comments

Comments
 (0)