Skip to content
Open
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
139 changes: 139 additions & 0 deletions src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4536,6 +4536,52 @@ pub enum Statement {
comment: Option<String>,
},
/// ```sql
/// CREATE [OR REPLACE] EXTERNAL VOLUME [IF NOT EXISTS] <name>
/// ```
/// See <https://docs.snowflake.com/en/sql-reference/sql/create-external-volume>
CreateExternalVolume {
/// `OR REPLACE` flag.
or_replace: bool,
/// `IF NOT EXISTS` flag.
if_not_exists: bool,
/// External volume name.
name: ObjectName,
/// Storage locations, each a parenthesized list of key-value options
/// (e.g. `(NAME='loc1' STORAGE_PROVIDER='S3' STORAGE_BASE_URL='s3://bucket/')`).
storage_locations: Vec<KeyValueOptions>,
/// Optional `ALLOW_WRITES` setting.
allow_writes: Option<bool>,
/// Optional comment.
comment: Option<String>,
},
/// ```sql
/// ALTER EXTERNAL VOLUME [IF EXISTS] <name> ...
/// ```
AlterExternalVolume {
/// External volume name.
name: ObjectName,
/// `IF EXISTS` flag.
if_exists: bool,
/// The alter operation.
operation: AlterExternalVolumeOperation,
},
/// ```sql
/// DESC[RIBE] EXTERNAL VOLUME <name>
/// ```
DescribeExternalVolume {
/// The keyword used, `DESC` or `DESCRIBE`.
describe_alias: DescribeAlias,
/// External volume name.
name: ObjectName,
},
/// ```sql
/// SHOW EXTERNAL VOLUMES [LIKE '<pattern>']
/// ```
ShowExternalVolumes {
/// Optional filter (e.g. `LIKE`).
filter: Option<ShowStatementFilter>,
},
/// ```sql
/// ASSERT <condition> [AS <message>]
/// ```
Assert {
Expand Down Expand Up @@ -6261,6 +6307,59 @@ 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} STORAGE_LOCATIONS = (",
or_replace = if *or_replace { "OR REPLACE " } else { "" },
if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" },
)?;
for (i, loc) in storage_locations.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "({loc})")?;
}
write!(f, ")")?;
if let Some(val) = allow_writes {
write!(f, " ALLOW_WRITES = {}", if *val { "TRUE" } else { "FALSE" })?;
}
if let Some(ref c) = comment {
write!(f, " COMMENT = '{}'", value::escape_single_quote_string(c))?;
}
Ok(())
}
Statement::AlterExternalVolume {
name,
if_exists,
operation,
} => {
write!(
f,
"ALTER EXTERNAL VOLUME {if_exists}{name} {operation}",
if_exists = if *if_exists { "IF EXISTS " } else { "" },
)
}
Statement::DescribeExternalVolume {
describe_alias,
name,
} => {
write!(f, "{describe_alias} EXTERNAL VOLUME {name}")
}
Statement::ShowExternalVolumes { filter } => {
write!(f, "SHOW EXTERNAL VOLUMES")?;
if let Some(ref filter) = filter {
write!(f, " {filter}")?;
}
Ok(())
}
Statement::CopyIntoSnowflake {
kind,
into,
Expand Down Expand Up @@ -8579,6 +8678,8 @@ pub enum ObjectType {
User,
/// A stream.
Stream,
/// A Snowflake external volume.
ExternalVolume,
}

impl fmt::Display for ObjectType {
Expand All @@ -8597,6 +8698,7 @@ impl fmt::Display for ObjectType {
ObjectType::Type => "TYPE",
ObjectType::User => "USER",
ObjectType::Stream => "STREAM",
ObjectType::ExternalVolume => "EXTERNAL VOLUME",
})
}
}
Expand Down Expand Up @@ -11021,6 +11123,43 @@ pub struct ShowObjects {
pub show_options: ShowStatementOptions,
}

/// Operations for `ALTER EXTERNAL VOLUME`.
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
pub enum AlterExternalVolumeOperation {
/// `ADD STORAGE_LOCATION = ( ... )`
AddStorageLocation(KeyValueOptions),
/// `SET ALLOW_WRITES = TRUE|FALSE`
SetAllowWrites(bool),
/// `REMOVE STORAGE_LOCATION '<name>'`
RemoveStorageLocation(String),
}

impl fmt::Display for AlterExternalVolumeOperation {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
AlterExternalVolumeOperation::AddStorageLocation(loc) => {
write!(f, "ADD STORAGE_LOCATION = ({loc})")
}
AlterExternalVolumeOperation::SetAllowWrites(val) => {
write!(
f,
"SET ALLOW_WRITES = {}",
if *val { "TRUE" } else { "FALSE" }
)
}
AlterExternalVolumeOperation::RemoveStorageLocation(name) => {
write!(
f,
"REMOVE STORAGE_LOCATION '{}'",
value::escape_single_quote_string(name)
)
}
}
}
}

/// MSSQL's json null clause
///
/// ```plaintext
Expand Down
4 changes: 4 additions & 0 deletions src/ast/spans.rs
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,10 @@ impl Spanned for Statement {
Statement::Vacuum(..) => Span::empty(),
Statement::AlterUser(..) => Span::empty(),
Statement::Reset(..) => Span::empty(),
Statement::CreateExternalVolume { .. } => Span::empty(),
Statement::AlterExternalVolume { .. } => Span::empty(),
Statement::DescribeExternalVolume { .. } => Span::empty(),
Statement::ShowExternalVolumes { .. } => Span::empty(),
}
}
}
Expand Down
155 changes: 147 additions & 8 deletions src/dialect/snowflake.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,15 @@ use crate::ast::helpers::stmt_data_loading::{
FileStagingCommand, StageLoadSelectItem, StageLoadSelectItemKind, StageParamsObject,
};
use crate::ast::{
AlterTable, AlterTableOperation, AlterTableType, CatalogSyncNamespaceMode, ColumnOption,
ColumnPolicy, ColumnPolicyProperty, ContactEntry, CopyIntoSnowflakeKind, CreateTable,
CreateTableLikeKind, DollarQuotedString, Ident, IdentityParameters, IdentityProperty,
IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, InitializeKind,
Insert, MultiTableInsertIntoClause, MultiTableInsertType, MultiTableInsertValue,
MultiTableInsertValues, MultiTableInsertWhenClause, ObjectName, ObjectNamePart,
RefreshModeKind, RowAccessPolicy, ShowObjects, SqlOption, Statement, StorageLifecyclePolicy,
StorageSerializationPolicy, TableObject, TagsColumnOption, Value, WrappedCollection,
AlterExternalVolumeOperation, AlterTable, AlterTableOperation, AlterTableType,
CatalogSyncNamespaceMode, ColumnOption, ColumnPolicy, ColumnPolicyProperty, ContactEntry,
CopyIntoSnowflakeKind, CreateTable, CreateTableLikeKind, DescribeAlias, DollarQuotedString,
Ident, IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind,
IdentityPropertyOrder, InitializeKind, Insert, MultiTableInsertIntoClause,
MultiTableInsertType, MultiTableInsertValue, MultiTableInsertValues,
MultiTableInsertWhenClause, ObjectName, ObjectNamePart, RefreshModeKind, RowAccessPolicy,
ShowObjects, SqlOption, Statement, StorageLifecyclePolicy, StorageSerializationPolicy,
TableObject, TagsColumnOption, Value, WrappedCollection,
};
use crate::dialect::{Dialect, Precedence};
use crate::keywords::Keyword;
Expand Down Expand Up @@ -266,6 +267,11 @@ impl Dialect for SnowflakeDialect {
return Some(parse_alter_dynamic_table(parser));
}

if parser.parse_keywords(&[Keyword::ALTER, Keyword::EXTERNAL, Keyword::VOLUME]) {
// ALTER EXTERNAL VOLUME
return Some(parse_alter_external_volume(parser));
}

if parser.parse_keywords(&[Keyword::ALTER, Keyword::EXTERNAL, Keyword::TABLE]) {
// ALTER EXTERNAL TABLE
return Some(parse_alter_external_table(parser));
Expand All @@ -281,10 +287,30 @@ impl Dialect for SnowflakeDialect {
return Some(parse_alter_session(parser, set));
}

if let Some(kw) = parser.parse_one_of_keywords(&[Keyword::DESC, Keyword::DESCRIBE]) {
if parser.parse_keywords(&[Keyword::EXTERNAL, Keyword::VOLUME]) {
// DESC[RIBE] EXTERNAL VOLUME
let describe_alias = if kw == Keyword::DESC {
DescribeAlias::Desc
} else {
DescribeAlias::Describe
};
return Some(parse_describe_external_volume(describe_alias, parser));
}
// not EXTERNAL VOLUME — put back DESC/DESCRIBE
parser.prev_token();
}

if parser.parse_keyword(Keyword::CREATE) {
// possibly CREATE STAGE
//[ OR REPLACE ]
let or_replace = parser.parse_keywords(&[Keyword::OR, Keyword::REPLACE]);

// CREATE [OR REPLACE] EXTERNAL VOLUME
if parser.parse_keywords(&[Keyword::EXTERNAL, Keyword::VOLUME]) {
return Some(parse_create_external_volume(or_replace, parser));
}

// LOCAL | GLOBAL
let global = match parser.parse_one_of_keywords(&[Keyword::LOCAL, Keyword::GLOBAL]) {
Some(Keyword::LOCAL) => Some(false),
Expand Down Expand Up @@ -363,6 +389,9 @@ impl Dialect for SnowflakeDialect {
}

if parser.parse_keyword(Keyword::SHOW) {
if parser.parse_keywords(&[Keyword::EXTERNAL, Keyword::VOLUMES]) {
return Some(parse_show_external_volumes(parser));
}
let terse = parser.parse_keyword(Keyword::TERSE);
if parser.parse_keyword(Keyword::OBJECTS) {
return Some(parse_show_objects(terse, parser));
Expand Down Expand Up @@ -1983,3 +2012,113 @@ fn parse_multi_table_insert_when_clauses(

Ok((when_clauses, else_clause))
}

/// Parse `CREATE [OR REPLACE] EXTERNAL VOLUME [IF NOT EXISTS] <name> ...`
///
/// Each storage location is parsed by [`parse_external_volume_storage_location`];
/// the trailing `ALLOW_WRITES` and `COMMENT` properties are accepted in any
/// order.
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)?;

parser.expect_keyword_is(Keyword::STORAGE_LOCATIONS)?;
parser.expect_token(&Token::Eq)?;
parser.expect_token(&Token::LParen)?;

let storage_locations = parser.parse_comma_separated(parse_external_volume_storage_location)?;
parser.expect_token(&Token::RParen)?;

let mut allow_writes = None;
let mut comment = None;

loop {
if parser.parse_keyword(Keyword::ALLOW_WRITES) {
parser.expect_token(&Token::Eq)?;
allow_writes = Some(parser.parse_boolean_string()?);
} else if parser.parse_keyword(Keyword::COMMENT) {
parser.expect_token(&Token::Eq)?;
comment = Some(parser.parse_comment_value()?);
} else {
break;
}
}

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

/// Parse `ALTER EXTERNAL VOLUME [IF EXISTS] <name> ...`
fn parse_alter_external_volume(parser: &mut Parser) -> Result<Statement, ParserError> {
let if_exists = parser.parse_keywords(&[Keyword::IF, Keyword::EXISTS]);
let name = parser.parse_object_name(false)?;

let operation = if parser.parse_keyword(Keyword::ADD) {
parser.expect_keyword_is(Keyword::STORAGE_LOCATION)?;
parser.expect_token(&Token::Eq)?;
AlterExternalVolumeOperation::AddStorageLocation(parse_external_volume_storage_location(
parser,
)?)
} else if parser.parse_keyword(Keyword::SET) {
parser.expect_keyword_is(Keyword::ALLOW_WRITES)?;
parser.expect_token(&Token::Eq)?;
AlterExternalVolumeOperation::SetAllowWrites(parser.parse_boolean_string()?)
} else if parser.parse_keyword(Keyword::REMOVE) {
parser.expect_keyword_is(Keyword::STORAGE_LOCATION)?;
let loc_name = parser.parse_literal_string()?;
AlterExternalVolumeOperation::RemoveStorageLocation(loc_name)
} else {
return parser.expected(
"ADD, SET, or REMOVE after ALTER EXTERNAL VOLUME <name>",
parser.peek_token(),
);
};

Ok(Statement::AlterExternalVolume {
name,
if_exists,
operation,
})
}

/// Parse one parenthesized storage-location option list, e.g.
/// `(NAME='loc1' STORAGE_PROVIDER='S3' ...)`. The options (and the
/// `ENCRYPTION = (...)` sub-list) are parsed generically via
/// [`Parser::parse_key_value_options`]; only an empty list is rejected,
/// field order and the exact option set are left to the consumer.
fn parse_external_volume_storage_location(
parser: &mut Parser,
) -> Result<KeyValueOptions, ParserError> {
let location = parser.parse_key_value_options(true, &[])?;
if location.options.is_empty() {
return parser.expected("storage location options", parser.peek_token());
}
Ok(location)
}

/// Parse `DESC[RIBE] EXTERNAL VOLUME <name>`
fn parse_describe_external_volume(
describe_alias: DescribeAlias,
parser: &mut Parser,
) -> Result<Statement, ParserError> {
let name = parser.parse_object_name(false)?;
Ok(Statement::DescribeExternalVolume {
describe_alias,
name,
})
}

/// Parse `SHOW EXTERNAL VOLUMES [LIKE '<pattern>']`
fn parse_show_external_volumes(parser: &mut Parser) -> Result<Statement, ParserError> {
let filter = parser.parse_show_statement_filter()?;
Ok(Statement::ShowExternalVolumes { filter })
}
Loading
Loading