Skip to content

Commit 890fe6b

Browse files
osipovartemVedin
authored andcommitted
Merge pull request #15 from Embucket/issues/1418_external_volume
CREATE EXTERNAL VOLUME sql
1 parent 292df6d commit 890fe6b

File tree

7 files changed

+299
-13
lines changed

7 files changed

+299
-13
lines changed

.github/workflows/rust.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ jobs:
3838
- uses: actions/checkout@v4
3939
- name: Setup Rust Toolchain
4040
uses: ./.github/actions/setup-builder
41+
with:
42+
rust-version: "1.86.0"
4143
- run: cargo clippy --all-targets --all-features -- -D warnings
4244

4345
benchmark-lint:
@@ -46,6 +48,8 @@ jobs:
4648
- uses: actions/checkout@v4
4749
- name: Setup Rust Toolchain
4850
uses: ./.github/actions/setup-builder
51+
with:
52+
rust-version: "1.86.0"
4953
- run: cd sqlparser_bench && cargo clippy --all-targets --all-features -- -D warnings
5054

5155
compile:

src/ast/mod.rs

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3990,6 +3990,18 @@ pub enum Statement {
39903990
option: Option<ReferentialAction>,
39913991
},
39923992
/// ```sql
3993+
/// CREATE EXTERNAL VOLUME
3994+
/// ```
3995+
/// See <https://docs.snowflake.com/en/sql-reference/sql/create-external-volume>
3996+
CreateExternalVolume {
3997+
or_replace: bool,
3998+
if_not_exists: bool,
3999+
name: ObjectName,
4000+
storage_locations: Vec<CloudProviderParams>,
4001+
allow_writes: Option<bool>,
4002+
comment: Option<String>,
4003+
},
4004+
/// ```sql
39934005
/// CREATE PROCEDURE
39944006
/// ```
39954007
CreateProcedure {
@@ -5002,6 +5014,39 @@ impl fmt::Display for Statement {
50025014
}
50035015
Ok(())
50045016
}
5017+
Statement::CreateExternalVolume {
5018+
or_replace,
5019+
if_not_exists,
5020+
name,
5021+
storage_locations,
5022+
allow_writes,
5023+
comment,
5024+
} => {
5025+
write!(
5026+
f,
5027+
"CREATE {or_replace}EXTERNAL VOLUME {if_not_exists}{name}",
5028+
or_replace = if *or_replace { "OR REPLACE " } else { "" },
5029+
if_not_exists = if *if_not_exists { " IF NOT EXISTS" } else { "" },
5030+
)?;
5031+
if !storage_locations.is_empty() {
5032+
write!(
5033+
f,
5034+
" STORAGE_LOCATIONS = ({})",
5035+
storage_locations
5036+
.iter()
5037+
.map(|loc| format!("({})", loc))
5038+
.collect::<Vec<_>>()
5039+
.join(", ")
5040+
)?;
5041+
}
5042+
if let Some(true) = allow_writes {
5043+
write!(f, " ALLOW_WRITES = TRUE")?;
5044+
}
5045+
if let Some(c) = comment {
5046+
write!(f, " COMMENT = '{c}'")?;
5047+
}
5048+
Ok(())
5049+
}
50055050
Statement::CreateProcedure {
50065051
name,
50075052
or_alter,
@@ -10223,6 +10268,74 @@ impl fmt::Display for MemberOf {
1022310268
}
1022410269
}
1022510270

10271+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
10272+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
10273+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
10274+
pub struct CloudProviderParams {
10275+
pub name: String,
10276+
pub provider: String,
10277+
pub base_url: Option<String>,
10278+
pub aws_role_arn: Option<String>,
10279+
pub aws_access_point_arn: Option<String>,
10280+
pub aws_external_id: Option<String>,
10281+
pub azure_tenant_id: Option<String>,
10282+
pub storage_endpoint: Option<String>,
10283+
pub use_private_link_endpoint: Option<bool>,
10284+
pub encryption: KeyValueOptions,
10285+
pub credentials: KeyValueOptions,
10286+
}
10287+
10288+
impl fmt::Display for CloudProviderParams {
10289+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
10290+
write!(
10291+
f,
10292+
"NAME = '{}' STORAGE_PROVIDER = '{}'",
10293+
self.name, self.provider
10294+
)?;
10295+
10296+
if let Some(base_url) = &self.base_url {
10297+
write!(f, " STORAGE_BASE_URL = '{base_url}'")?;
10298+
}
10299+
10300+
if let Some(arn) = &self.aws_role_arn {
10301+
write!(f, " STORAGE_AWS_ROLE_ARN = '{arn}'")?;
10302+
}
10303+
10304+
if let Some(ap_arn) = &self.aws_access_point_arn {
10305+
write!(f, " STORAGE_AWS_ACCESS_POINT_ARN = '{ap_arn}'")?;
10306+
}
10307+
10308+
if let Some(ext_id) = &self.aws_external_id {
10309+
write!(f, " STORAGE_AWS_EXTERNAL_ID = '{ext_id}'")?;
10310+
}
10311+
10312+
if let Some(tenant_id) = &self.azure_tenant_id {
10313+
write!(f, " AZURE_TENANT_ID = '{tenant_id}'")?;
10314+
}
10315+
10316+
if let Some(endpoint) = &self.storage_endpoint {
10317+
write!(f, " STORAGE_ENDPOINT = '{endpoint}'")?;
10318+
}
10319+
10320+
if let Some(use_pl) = self.use_private_link_endpoint {
10321+
write!(
10322+
f,
10323+
" USE_PRIVATELINK_ENDPOINT = {}",
10324+
if use_pl { "TRUE" } else { "FALSE" }
10325+
)?;
10326+
}
10327+
10328+
if !self.encryption.options.is_empty() {
10329+
write!(f, " ENCRYPTION=({})", self.encryption)?;
10330+
}
10331+
10332+
if !self.credentials.options.is_empty() {
10333+
write!(f, " CREDENTIALS=({})", self.credentials)?;
10334+
}
10335+
Ok(())
10336+
}
10337+
}
10338+
1022610339
#[cfg(test)]
1022710340
mod tests {
1022810341
use crate::tokenizer::Location;

src/ast/spans.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ impl Spanned for Values {
267267
/// - [Statement::CreateFunction]
268268
/// - [Statement::CreateTrigger]
269269
/// - [Statement::DropTrigger]
270+
/// - [Statement::CreateExternalVolume]
270271
/// - [Statement::CreateProcedure]
271272
/// - [Statement::CreateMacro]
272273
/// - [Statement::CreateStage]
@@ -488,6 +489,7 @@ impl Spanned for Statement {
488489
Statement::CreateDomain { .. } => Span::empty(),
489490
Statement::CreateTrigger { .. } => Span::empty(),
490491
Statement::DropTrigger { .. } => Span::empty(),
492+
Statement::CreateExternalVolume { .. } => Span::empty(),
491493
Statement::CreateProcedure { .. } => Span::empty(),
492494
Statement::CreateMacro { .. } => Span::empty(),
493495
Statement::CreateStage { .. } => Span::empty(),

src/dialect/snowflake.rs

Lines changed: 150 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,21 @@
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;
20-
use crate::ast::helpers::key_value_options::{
21-
KeyValueOption, KeyValueOptionType, KeyValueOptions,
22-
};
21+
use crate::ast::helpers::key_value_options::{KeyValueOption, KeyValueOptionType, KeyValueOptions};
2322
use crate::ast::helpers::stmt_create_database::CreateDatabaseBuilder;
2423
use crate::ast::helpers::stmt_create_table::CreateTableBuilder;
2524
use crate::ast::helpers::stmt_data_loading::{
2625
FileStagingCommand, StageLoadSelectItem, StageLoadSelectItemKind, StageParamsObject,
2726
};
2827
use crate::ast::{
29-
CatalogSyncNamespaceMode, ColumnOption, ColumnPolicy, ColumnPolicyProperty, ContactEntry,
30-
CopyIntoSnowflakeKind, DollarQuotedString, Ident, IdentityParameters, IdentityProperty,
31-
IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, ObjectName,
32-
ObjectNamePart, RowAccessPolicy, ShowObjects, SqlOption, Statement, StorageSerializationPolicy,
33-
TagsColumnOption, WrappedCollection,
28+
CatalogSyncNamespaceMode, CloudProviderParams, ColumnOption, ColumnPolicy,
29+
ColumnPolicyProperty, ContactEntry, CopyIntoSnowflakeKind, DollarQuotedString, Ident,
30+
IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind,
31+
IdentityPropertyOrder, ObjectName, ObjectNamePart, RowAccessPolicy, ShowObjects, SqlOption,
32+
Statement, StorageSerializationPolicy, TagsColumnOption, WrappedCollection,
3433
};
3534
use crate::dialect::{Dialect, Precedence};
3635
use crate::keywords::Keyword;
@@ -45,8 +44,6 @@ use alloc::vec::Vec;
4544
#[cfg(not(feature = "std"))]
4645
use alloc::{format, vec};
4746

48-
use super::keywords::RESERVED_FOR_IDENTIFIER;
49-
5047
const RESERVED_KEYWORDS_FOR_SELECT_ITEM_OPERATOR: [Keyword; 1] = [Keyword::CONNECT_BY_ROOT];
5148
/// A [`Dialect`] for [Snowflake](https://www.snowflake.com/)
5249
#[derive(Debug, Default)]
@@ -187,6 +184,8 @@ impl Dialect for SnowflakeDialect {
187184
));
188185
} else if parser.parse_keyword(Keyword::DATABASE) {
189186
return Some(parse_create_database(or_replace, transient, parser));
187+
} else if parser.parse_keywords(&[Keyword::EXTERNAL, Keyword::VOLUME]) {
188+
return Some(parse_create_external_volume(or_replace, parser));
190189
} else {
191190
// need to go back with the cursor
192191
let mut back = 1;
@@ -323,7 +322,7 @@ impl Dialect for SnowflakeDialect {
323322
Keyword::LIMIT | Keyword::OFFSET if peek_for_limit_options(parser) => false,
324323

325324
// `FETCH` can be considered an alias as long as it's not followed by `FIRST`` or `NEXT`
326-
// which would give it a different meanings, for example:
325+
// which would give it a different meanings, for example:
327326
// `SELECT 1 FETCH FIRST 10 ROWS` - not an alias
328327
// `SELECT 1 FETCH 10` - not an alias
329328
Keyword::FETCH if parser.peek_one_of_keywords(&[Keyword::FIRST, Keyword::NEXT]).is_some()
@@ -840,6 +839,146 @@ pub fn parse_create_database(
840839
Ok(builder.build())
841840
}
842841

842+
fn parse_create_external_volume(
843+
or_replace: bool,
844+
parser: &mut Parser,
845+
) -> Result<Statement, ParserError> {
846+
let if_not_exists = parser.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]);
847+
let name = parser.parse_object_name(false)?;
848+
let mut comment = None;
849+
let mut allow_writes = None;
850+
let mut storage_locations = Vec::new();
851+
852+
// STORAGE_LOCATIONS (...)
853+
if parser.parse_keywords(&[Keyword::STORAGE_LOCATIONS]) {
854+
parser.expect_token(&Token::Eq)?;
855+
storage_locations = parse_storage_locations(parser)?;
856+
};
857+
858+
// ALLOW_WRITES [ = true | false ]
859+
if parser.parse_keyword(Keyword::ALLOW_WRITES) {
860+
parser.expect_token(&Token::Eq)?;
861+
allow_writes = Some(parser.parse_boolean_string()?);
862+
}
863+
864+
// COMMENT = '...'
865+
if parser.parse_keyword(Keyword::COMMENT) {
866+
parser.expect_token(&Token::Eq)?;
867+
comment = Some(parser.parse_literal_string()?);
868+
}
869+
870+
if storage_locations.is_empty() {
871+
return Err(ParserError::ParserError(
872+
"STORAGE_LOCATIONS is required for CREATE EXTERNAL VOLUME".to_string(),
873+
));
874+
}
875+
876+
Ok(Statement::CreateExternalVolume {
877+
or_replace,
878+
if_not_exists,
879+
name,
880+
allow_writes,
881+
comment,
882+
storage_locations,
883+
})
884+
}
885+
886+
fn parse_storage_locations(parser: &mut Parser) -> Result<Vec<CloudProviderParams>, ParserError> {
887+
let mut locations = Vec::new();
888+
parser.expect_token(&Token::LParen)?;
889+
890+
loop {
891+
parser.expect_token(&Token::LParen)?;
892+
893+
// START OF ONE CloudProviderParams BLOCK
894+
let mut name = None;
895+
let mut provider = None;
896+
let mut base_url = None;
897+
let mut aws_role_arn = None;
898+
let mut aws_access_point_arn = None;
899+
let mut aws_external_id = None;
900+
let mut azure_tenant_id = None;
901+
let mut storage_endpoint = None;
902+
let mut use_private_link_endpoint = None;
903+
let mut encryption: KeyValueOptions = KeyValueOptions { options: vec![] };
904+
let mut credentials: KeyValueOptions = KeyValueOptions { options: vec![] };
905+
906+
loop {
907+
if parser.parse_keyword(Keyword::NAME) {
908+
parser.expect_token(&Token::Eq)?;
909+
name = Some(parser.parse_literal_string()?);
910+
} else if parser.parse_keyword(Keyword::STORAGE_PROVIDER) {
911+
parser.expect_token(&Token::Eq)?;
912+
provider = Some(parser.parse_literal_string()?);
913+
} else if parser.parse_keyword(Keyword::STORAGE_BASE_URL) {
914+
parser.expect_token(&Token::Eq)?;
915+
base_url = Some(parser.parse_literal_string()?);
916+
} else if parser.parse_keyword(Keyword::STORAGE_AWS_ROLE_ARN) {
917+
parser.expect_token(&Token::Eq)?;
918+
aws_role_arn = Some(parser.parse_literal_string()?);
919+
} else if parser.parse_keyword(Keyword::STORAGE_AWS_ACCESS_POINT_ARN) {
920+
parser.expect_token(&Token::Eq)?;
921+
aws_access_point_arn = Some(parser.parse_literal_string()?);
922+
} else if parser.parse_keyword(Keyword::STORAGE_AWS_EXTERNAL_ID) {
923+
parser.expect_token(&Token::Eq)?;
924+
aws_external_id = Some(parser.parse_literal_string()?);
925+
} else if parser.parse_keyword(Keyword::AZURE_TENANT_ID) {
926+
parser.expect_token(&Token::Eq)?;
927+
azure_tenant_id = Some(parser.parse_literal_string()?);
928+
} else if parser.parse_keyword(Keyword::STORAGE_ENDPOINT) {
929+
parser.expect_token(&Token::Eq)?;
930+
storage_endpoint = Some(parser.parse_literal_string()?);
931+
} else if parser.parse_keyword(Keyword::USE_PRIVATELINK_ENDPOINT) {
932+
parser.expect_token(&Token::Eq)?;
933+
use_private_link_endpoint = Some(parser.parse_boolean_string()?);
934+
} else if parser.parse_keyword(Keyword::ENCRYPTION) {
935+
parser.expect_token(&Token::Eq)?;
936+
encryption = KeyValueOptions {
937+
options: parse_parentheses_options(parser)?,
938+
};
939+
} else if parser.parse_keyword(Keyword::CREDENTIALS) {
940+
parser.expect_token(&Token::Eq)?;
941+
credentials = KeyValueOptions {
942+
options: parse_parentheses_options(parser)?,
943+
};
944+
} else if parser.consume_token(&Token::RParen) {
945+
break;
946+
} else {
947+
return parser.expected("a valid key or closing paren", parser.peek_token());
948+
}
949+
}
950+
951+
let Some(name) = name else {
952+
return parser.expected("NAME = '...'", parser.peek_token());
953+
};
954+
955+
let Some(provider) = provider else {
956+
return parser.expected("STORAGE_PROVIDER = '...'", parser.peek_token());
957+
};
958+
959+
locations.push(CloudProviderParams {
960+
name,
961+
provider,
962+
base_url,
963+
aws_role_arn,
964+
aws_access_point_arn,
965+
aws_external_id,
966+
azure_tenant_id,
967+
storage_endpoint,
968+
use_private_link_endpoint,
969+
encryption,
970+
credentials,
971+
});
972+
// EXIT if next token is RParen
973+
if parser.consume_token(&Token::RParen) {
974+
break;
975+
}
976+
// Otherwise expect a comma before next object
977+
parser.expect_token(&Token::Comma)?;
978+
}
979+
Ok(locations)
980+
}
981+
843982
pub fn parse_storage_serialization_policy(
844983
parser: &mut Parser,
845984
) -> Result<StorageSerializationPolicy, ParserError> {

0 commit comments

Comments
 (0)