Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
11 changes: 5 additions & 6 deletions sqlparser_bench/benches/sqlparser_bench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,25 +45,24 @@ fn basic_queries(c: &mut Criterion) {

let large_statement = {
let expressions = (0..1000)
.map(|n| format!("FN_{}(COL_{})", n, n))
.map(|n| format!("FN_{n}(COL_{n})"))
.collect::<Vec<_>>()
.join(", ");
let tables = (0..1000)
.map(|n| format!("TABLE_{}", n))
.map(|n| format!("TABLE_{n}"))
.collect::<Vec<_>>()
.join(" JOIN ");
let where_condition = (0..1000)
.map(|n| format!("COL_{} = {}", n, n))
.map(|n| format!("COL_{n} = {n}"))
.collect::<Vec<_>>()
.join(" OR ");
let order_condition = (0..1000)
.map(|n| format!("COL_{} DESC", n))
.map(|n| format!("COL_{n} DESC"))
.collect::<Vec<_>>()
.join(", ");

format!(
"SELECT {} FROM {} WHERE {} ORDER BY {}",
expressions, tables, where_condition, order_condition
"SELECT {expressions} FROM {tables} WHERE {where_condition} ORDER BY {order_condition}",
)
};

Expand Down
113 changes: 113 additions & 0 deletions src/ast/mod.rs
Original file line number Diff line number Diff line change
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 @@ -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