Skip to content

CREATE EXTERNAL VOLUME sql #15

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ jobs:
- uses: actions/checkout@v4
- name: Setup Rust Toolchain
uses: ./.github/actions/setup-builder
with:
rust-version: "1.86.0"
- run: cargo clippy --all-targets --all-features -- -D warnings

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

compile:
Expand Down
2 changes: 1 addition & 1 deletion src/ast/ddl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -820,7 +820,7 @@ impl fmt::Display for AlterColumnOperation {
AlterColumnOperation::SetDefault { value } => {
write!(f, "SET DEFAULT {value}")
}
AlterColumnOperation::DropDefault {} => {
AlterColumnOperation::DropDefault => {
write!(f, "DROP DEFAULT")
}
AlterColumnOperation::SetDataType { data_type, using } => {
Expand Down
125 changes: 119 additions & 6 deletions src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -651,17 +651,17 @@ pub enum Expr {
/// such as maps, arrays, and lists:
/// - Array
/// - A 1-dim array `a[1]` will be represented like:
/// `CompoundFieldAccess(Ident('a'), vec![Subscript(1)]`
/// `CompoundFieldAccess(Ident('a'), vec![Subscript(1)]`
/// - A 2-dim array `a[1][2]` will be represented like:
/// `CompoundFieldAccess(Ident('a'), vec![Subscript(1), Subscript(2)]`
/// `CompoundFieldAccess(Ident('a'), vec![Subscript(1), Subscript(2)]`
/// - Map or Struct (Bracket-style)
/// - A map `a['field1']` will be represented like:
/// `CompoundFieldAccess(Ident('a'), vec![Subscript('field')]`
/// `CompoundFieldAccess(Ident('a'), vec![Subscript('field')]`
/// - A 2-dim map `a['field1']['field2']` will be represented like:
/// `CompoundFieldAccess(Ident('a'), vec![Subscript('field1'), Subscript('field2')]`
/// `CompoundFieldAccess(Ident('a'), vec![Subscript('field1'), Subscript('field2')]`
/// - Struct (Dot-style) (only effect when the chain contains both subscript and expr)
/// - A struct access `a[field1].field2` will be represented like:
/// `CompoundFieldAccess(Ident('a'), vec![Subscript('field1'), Ident('field2')]`
/// `CompoundFieldAccess(Ident('a'), vec![Subscript('field1'), Ident('field2')]`
/// - If a struct access likes `a.field1.field2`, it will be represented by CompoundIdentifier([a, field1, field2])
CompoundFieldAccess {
root: Box<Expr>,
Expand Down Expand Up @@ -3283,6 +3283,18 @@ pub enum Statement {
option: Option<ReferentialAction>,
},
/// ```sql
/// CREATE EXTERNAL VOLUME
/// ```
/// See <https://docs.snowflake.com/en/sql-reference/sql/create-external-volume>
CreateExternalVolume {
or_replace: bool,
if_not_exists: bool,
name: ObjectName,
storage_locations: Vec<CloudProviderParams>,
allow_writes: Option<bool>,
comment: Option<String>,
},
/// ```sql
/// CREATE PROCEDURE
/// ```
CreateProcedure {
Expand Down Expand Up @@ -4171,6 +4183,39 @@ impl fmt::Display for Statement {
}
Ok(())
}
Statement::CreateExternalVolume {
or_replace,
if_not_exists,
name,
storage_locations,
allow_writes,
comment,
} => {
write!(
f,
"CREATE {or_replace}EXTERNAL VOLUME {if_not_exists}{name}",
or_replace = if *or_replace { "OR REPLACE " } else { "" },
if_not_exists = if *if_not_exists { " IF NOT EXISTS" } else { "" },
)?;
if !storage_locations.is_empty() {
write!(
f,
" STORAGE_LOCATIONS = ({})",
storage_locations
.iter()
.map(|loc| format!("({})", loc))
.collect::<Vec<_>>()
.join(", ")
)?;
}
if let Some(true) = allow_writes {
write!(f, " ALLOW_WRITES = TRUE")?;
}
if let Some(c) = comment {
write!(f, " COMMENT = '{c}'")?;
}
Ok(())
}
Statement::CreateProcedure {
name,
or_alter,
Expand Down Expand Up @@ -7314,7 +7359,7 @@ impl fmt::Display for CopyTarget {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use CopyTarget::*;
match self {
Stdin { .. } => write!(f, "STDIN"),
Stdin => write!(f, "STDIN"),
Stdout => write!(f, "STDOUT"),
File { filename } => write!(f, "'{}'", value::escape_single_quote_string(filename)),
Program { command } => write!(
Expand Down Expand Up @@ -8871,6 +8916,74 @@ impl fmt::Display for NullInclusion {
}
}

#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
pub struct CloudProviderParams {
pub name: String,
pub provider: String,
pub base_url: Option<String>,
pub aws_role_arn: Option<String>,
pub aws_access_point_arn: Option<String>,
pub aws_external_id: Option<String>,
pub azure_tenant_id: Option<String>,
pub storage_endpoint: Option<String>,
pub use_private_link_endpoint: Option<bool>,
pub encryption: KeyValueOptions,
pub credentials: KeyValueOptions,
}

impl fmt::Display for CloudProviderParams {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"NAME = '{}' STORAGE_PROVIDER = '{}'",
self.name, self.provider
)?;

if let Some(base_url) = &self.base_url {
write!(f, " STORAGE_BASE_URL = '{base_url}'")?;
}

if let Some(arn) = &self.aws_role_arn {
write!(f, " STORAGE_AWS_ROLE_ARN = '{arn}'")?;
}

if let Some(ap_arn) = &self.aws_access_point_arn {
write!(f, " STORAGE_AWS_ACCESS_POINT_ARN = '{ap_arn}'")?;
}

if let Some(ext_id) = &self.aws_external_id {
write!(f, " STORAGE_AWS_EXTERNAL_ID = '{ext_id}'")?;
}

if let Some(tenant_id) = &self.azure_tenant_id {
write!(f, " AZURE_TENANT_ID = '{tenant_id}'")?;
}

if let Some(endpoint) = &self.storage_endpoint {
write!(f, " STORAGE_ENDPOINT = '{endpoint}'")?;
}

if let Some(use_pl) = self.use_private_link_endpoint {
write!(
f,
" USE_PRIVATELINK_ENDPOINT = {}",
if use_pl { "TRUE" } else { "FALSE" }
)?;
}

if !self.encryption.options.is_empty() {
write!(f, " ENCRYPTION=({})", self.encryption)?;
}

if !self.credentials.options.is_empty() {
write!(f, " CREDENTIALS=({})", self.credentials)?;
}
Ok(())
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
2 changes: 2 additions & 0 deletions src/ast/spans.rs
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ impl Spanned for Values {
/// - [Statement::CreateFunction]
/// - [Statement::CreateTrigger]
/// - [Statement::DropTrigger]
/// - [Statement::CreateExternalVolume]
/// - [Statement::CreateProcedure]
/// - [Statement::CreateMacro]
/// - [Statement::CreateStage]
Expand Down Expand Up @@ -468,6 +469,7 @@ impl Spanned for Statement {
Statement::CreateFunction { .. } => Span::empty(),
Statement::CreateTrigger { .. } => Span::empty(),
Statement::DropTrigger { .. } => Span::empty(),
Statement::CreateExternalVolume { .. } => Span::empty(),
Statement::CreateProcedure { .. } => Span::empty(),
Statement::CreateMacro { .. } => Span::empty(),
Statement::CreateStage { .. } => Span::empty(),
Expand Down
154 changes: 148 additions & 6 deletions src/dialect/snowflake.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
// specific language governing permissions and limitations
// under the License.

use super::keywords::RESERVED_FOR_IDENTIFIER;
#[cfg(not(feature = "std"))]
use crate::alloc::string::ToString;
use crate::ast::helpers::key_value_options::{KeyValueOption, KeyValueOptionType, KeyValueOptions};
Expand All @@ -24,10 +25,11 @@ use crate::ast::helpers::stmt_data_loading::{
FileStagingCommand, StageLoadSelectItem, StageParamsObject,
};
use crate::ast::{
CatalogSyncNamespaceMode, ColumnOption, ColumnPolicy, ColumnPolicyProperty, ContactEntry,
CopyIntoSnowflakeKind, Ident, IdentityParameters, IdentityProperty, IdentityPropertyFormatKind,
IdentityPropertyKind, IdentityPropertyOrder, ObjectName, RowAccessPolicy, ShowObjects,
Statement, StorageSerializationPolicy, TagsColumnOption, WrappedCollection,
CatalogSyncNamespaceMode, CloudProviderParams, ColumnOption, ColumnPolicy,
ColumnPolicyProperty, ContactEntry, CopyIntoSnowflakeKind, Ident, IdentityParameters,
IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder,
ObjectName, RowAccessPolicy, ShowObjects, Statement, StorageSerializationPolicy,
TagsColumnOption, WrappedCollection,
};
use crate::dialect::{Dialect, Precedence};
use crate::keywords::Keyword;
Expand All @@ -42,8 +44,6 @@ use alloc::vec::Vec;
#[cfg(not(feature = "std"))]
use alloc::{format, vec};

use super::keywords::RESERVED_FOR_IDENTIFIER;

/// A [`Dialect`] for [Snowflake](https://www.snowflake.com/)
#[derive(Debug, Default)]
pub struct SnowflakeDialect;
Expand Down Expand Up @@ -179,6 +179,8 @@ impl Dialect for SnowflakeDialect {
));
} else if parser.parse_keyword(Keyword::DATABASE) {
return Some(parse_create_database(or_replace, transient, parser));
} else if parser.parse_keywords(&[Keyword::EXTERNAL, Keyword::VOLUME]) {
return Some(parse_create_external_volume(or_replace, parser));
} else {
// need to go back with the cursor
let mut back = 1;
Expand Down Expand Up @@ -702,6 +704,146 @@ pub fn parse_create_database(
Ok(builder.build())
}

fn parse_create_external_volume(
or_replace: bool,
parser: &mut Parser,
) -> Result<Statement, ParserError> {
let if_not_exists = parser.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]);
let name = parser.parse_object_name(false)?;
let mut comment = None;
let mut allow_writes = None;
let mut storage_locations = Vec::new();

// STORAGE_LOCATIONS (...)
if parser.parse_keywords(&[Keyword::STORAGE_LOCATIONS]) {
parser.expect_token(&Token::Eq)?;
storage_locations = parse_storage_locations(parser)?;
};

// ALLOW_WRITES [ = true | false ]
if parser.parse_keyword(Keyword::ALLOW_WRITES) {
parser.expect_token(&Token::Eq)?;
allow_writes = Some(parser.parse_boolean_string()?);
}

// COMMENT = '...'
if parser.parse_keyword(Keyword::COMMENT) {
parser.expect_token(&Token::Eq)?;
comment = Some(parser.parse_literal_string()?);
}

if storage_locations.is_empty() {
return Err(ParserError::ParserError(
"STORAGE_LOCATIONS is required for CREATE EXTERNAL VOLUME".to_string(),
));
}

Ok(Statement::CreateExternalVolume {
or_replace,
if_not_exists,
name,
allow_writes,
comment,
storage_locations,
})
}

fn parse_storage_locations(parser: &mut Parser) -> Result<Vec<CloudProviderParams>, ParserError> {
let mut locations = Vec::new();
parser.expect_token(&Token::LParen)?;

loop {
parser.expect_token(&Token::LParen)?;

// START OF ONE CloudProviderParams BLOCK
let mut name = None;
let mut provider = None;
let mut base_url = None;
let mut aws_role_arn = None;
let mut aws_access_point_arn = None;
let mut aws_external_id = None;
let mut azure_tenant_id = None;
let mut storage_endpoint = None;
let mut use_private_link_endpoint = None;
let mut encryption: KeyValueOptions = KeyValueOptions { options: vec![] };
let mut credentials: KeyValueOptions = KeyValueOptions { options: vec![] };

loop {
if parser.parse_keyword(Keyword::NAME) {
parser.expect_token(&Token::Eq)?;
name = Some(parser.parse_literal_string()?);
} else if parser.parse_keyword(Keyword::STORAGE_PROVIDER) {
parser.expect_token(&Token::Eq)?;
provider = Some(parser.parse_literal_string()?);
} else if parser.parse_keyword(Keyword::STORAGE_BASE_URL) {
parser.expect_token(&Token::Eq)?;
base_url = Some(parser.parse_literal_string()?);
} else if parser.parse_keyword(Keyword::STORAGE_AWS_ROLE_ARN) {
parser.expect_token(&Token::Eq)?;
aws_role_arn = Some(parser.parse_literal_string()?);
} else if parser.parse_keyword(Keyword::STORAGE_AWS_ACCESS_POINT_ARN) {
parser.expect_token(&Token::Eq)?;
aws_access_point_arn = Some(parser.parse_literal_string()?);
} else if parser.parse_keyword(Keyword::STORAGE_AWS_EXTERNAL_ID) {
parser.expect_token(&Token::Eq)?;
aws_external_id = Some(parser.parse_literal_string()?);
} else if parser.parse_keyword(Keyword::AZURE_TENANT_ID) {
parser.expect_token(&Token::Eq)?;
azure_tenant_id = Some(parser.parse_literal_string()?);
} else if parser.parse_keyword(Keyword::STORAGE_ENDPOINT) {
parser.expect_token(&Token::Eq)?;
storage_endpoint = Some(parser.parse_literal_string()?);
} else if parser.parse_keyword(Keyword::USE_PRIVATELINK_ENDPOINT) {
parser.expect_token(&Token::Eq)?;
use_private_link_endpoint = Some(parser.parse_boolean_string()?);
} else if parser.parse_keyword(Keyword::ENCRYPTION) {
parser.expect_token(&Token::Eq)?;
encryption = KeyValueOptions {
options: parse_parentheses_options(parser)?,
};
} else if parser.parse_keyword(Keyword::CREDENTIALS) {
parser.expect_token(&Token::Eq)?;
credentials = KeyValueOptions {
options: parse_parentheses_options(parser)?,
};
} else if parser.consume_token(&Token::RParen) {
break;
} else {
return parser.expected("a valid key or closing paren", parser.peek_token());
}
}

let Some(name) = name else {
return parser.expected("NAME = '...'", parser.peek_token());
};

let Some(provider) = provider else {
return parser.expected("STORAGE_PROVIDER = '...'", parser.peek_token());
};

locations.push(CloudProviderParams {
name,
provider,
base_url,
aws_role_arn,
aws_access_point_arn,
aws_external_id,
azure_tenant_id,
storage_endpoint,
use_private_link_endpoint,
encryption,
credentials,
});
// EXIT if next token is RParen
if parser.consume_token(&Token::RParen) {
break;
}
// Otherwise expect a comma before next object
parser.expect_token(&Token::Comma)?;
}
Ok(locations)
}

pub fn parse_storage_serialization_policy(
parser: &mut Parser,
) -> Result<StorageSerializationPolicy, ParserError> {
Expand Down
Loading
Loading