Skip to content

Commit 4a91f2f

Browse files
authored
Merge pull request #15 from Embucket/issues/1418_external_volume
CREATE EXTERNAL VOLUME sql
2 parents 12655c2 + 3ca4928 commit 4a91f2f

File tree

9 files changed

+310
-22
lines changed

9 files changed

+310
-22
lines changed

.github/workflows/rust.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ jobs:
3535
- uses: actions/checkout@v4
3636
- name: Setup Rust Toolchain
3737
uses: ./.github/actions/setup-builder
38+
with:
39+
rust-version: "1.86.0"
3840
- run: cargo clippy --all-targets --all-features -- -D warnings
3941

4042
benchmark-lint:
@@ -43,6 +45,8 @@ jobs:
4345
- uses: actions/checkout@v4
4446
- name: Setup Rust Toolchain
4547
uses: ./.github/actions/setup-builder
48+
with:
49+
rust-version: "1.86.0"
4650
- run: cd sqlparser_bench && cargo clippy --all-targets --all-features -- -D warnings
4751

4852
compile:

src/ast/ddl.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -820,7 +820,7 @@ impl fmt::Display for AlterColumnOperation {
820820
AlterColumnOperation::SetDefault { value } => {
821821
write!(f, "SET DEFAULT {value}")
822822
}
823-
AlterColumnOperation::DropDefault {} => {
823+
AlterColumnOperation::DropDefault => {
824824
write!(f, "DROP DEFAULT")
825825
}
826826
AlterColumnOperation::SetDataType { data_type, using } => {

src/ast/mod.rs

Lines changed: 119 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -651,17 +651,17 @@ pub enum Expr {
651651
/// such as maps, arrays, and lists:
652652
/// - Array
653653
/// - A 1-dim array `a[1]` will be represented like:
654-
/// `CompoundFieldAccess(Ident('a'), vec![Subscript(1)]`
654+
/// `CompoundFieldAccess(Ident('a'), vec![Subscript(1)]`
655655
/// - A 2-dim array `a[1][2]` will be represented like:
656-
/// `CompoundFieldAccess(Ident('a'), vec![Subscript(1), Subscript(2)]`
656+
/// `CompoundFieldAccess(Ident('a'), vec![Subscript(1), Subscript(2)]`
657657
/// - Map or Struct (Bracket-style)
658658
/// - A map `a['field1']` will be represented like:
659-
/// `CompoundFieldAccess(Ident('a'), vec![Subscript('field')]`
659+
/// `CompoundFieldAccess(Ident('a'), vec![Subscript('field')]`
660660
/// - A 2-dim map `a['field1']['field2']` will be represented like:
661-
/// `CompoundFieldAccess(Ident('a'), vec![Subscript('field1'), Subscript('field2')]`
661+
/// `CompoundFieldAccess(Ident('a'), vec![Subscript('field1'), Subscript('field2')]`
662662
/// - Struct (Dot-style) (only effect when the chain contains both subscript and expr)
663663
/// - A struct access `a[field1].field2` will be represented like:
664-
/// `CompoundFieldAccess(Ident('a'), vec![Subscript('field1'), Ident('field2')]`
664+
/// `CompoundFieldAccess(Ident('a'), vec![Subscript('field1'), Ident('field2')]`
665665
/// - If a struct access likes `a.field1.field2`, it will be represented by CompoundIdentifier([a, field1, field2])
666666
CompoundFieldAccess {
667667
root: Box<Expr>,
@@ -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}'")?;
4216+
}
4217+
Ok(())
4218+
}
41744219
Statement::CreateProcedure {
41754220
name,
41764221
or_alter,
@@ -7314,7 +7359,7 @@ impl fmt::Display for CopyTarget {
73147359
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
73157360
use CopyTarget::*;
73167361
match self {
7317-
Stdin { .. } => write!(f, "STDIN"),
7362+
Stdin => write!(f, "STDIN"),
73187363
Stdout => write!(f, "STDOUT"),
73197364
File { filename } => write!(f, "'{}'", value::escape_single_quote_string(filename)),
73207365
Program { command } => write!(
@@ -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: 148 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,8 @@ 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_keywords(&[Keyword::EXTERNAL, Keyword::VOLUME]) {
183+
return Some(parse_create_external_volume(or_replace, parser));
182184
} else {
183185
// need to go back with the cursor
184186
let mut back = 1;
@@ -702,6 +704,146 @@ pub fn parse_create_database(
702704
Ok(builder.build())
703705
}
704706

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

0 commit comments

Comments
 (0)