Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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