Skip to content
Closed
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
68 changes: 68 additions & 0 deletions src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4558,6 +4558,11 @@ 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(CreateExternalVolume),
/// ```sql
/// CREATE [ OR REPLACE ] WAREHOUSE [ IF NOT EXISTS ] <name>
/// [ [ WITH ] <property> = <value> [ ... ] ]
/// ```
Expand Down Expand Up @@ -6293,6 +6298,7 @@ impl fmt::Display for Statement {
}
Ok(())
}
Statement::CreateExternalVolume(s) => write!(f, "{s}"),
Statement::CreateWarehouse(s) => write!(f, "{s}"),
Statement::CopyIntoSnowflake {
kind,
Expand Down Expand Up @@ -8686,6 +8692,8 @@ pub enum ObjectType {
User,
/// A stream.
Stream,
/// A Snowflake external volume.
ExternalVolume,
/// A warehouse.
Warehouse,
}
Expand All @@ -8706,6 +8714,7 @@ impl fmt::Display for ObjectType {
ObjectType::Type => "TYPE",
ObjectType::User => "USER",
ObjectType::Stream => "STREAM",
ObjectType::ExternalVolume => "EXTERNAL VOLUME",
ObjectType::Warehouse => "WAREHOUSE",
})
}
Expand Down Expand Up @@ -11131,6 +11140,59 @@ pub struct ShowObjects {
pub show_options: ShowStatementOptions,
}

/// ```sql
/// CREATE [OR REPLACE] EXTERNAL VOLUME [IF NOT EXISTS] <name>
/// ```
/// See <https://docs.snowflake.com/en/sql-reference/sql/create-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 struct CreateExternalVolume {
/// `OR REPLACE` flag.
pub or_replace: bool,
/// `IF NOT EXISTS` flag.
pub if_not_exists: bool,
/// External volume name.
pub name: ObjectName,
/// Storage locations, each a parenthesized list of key-value options
/// (e.g. `(NAME='loc1' STORAGE_PROVIDER='S3' STORAGE_BASE_URL='s3://bucket/')`).
pub storage_locations: Vec<KeyValueOptions>,
/// Optional `ALLOW_WRITES` setting.
pub allow_writes: Option<bool>,
/// Optional comment.
pub comment: Option<String>,
}

impl fmt::Display for CreateExternalVolume {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"CREATE {or_replace}EXTERNAL VOLUME {if_not_exists}{name} STORAGE_LOCATIONS = (",
or_replace = if self.or_replace { "OR REPLACE " } else { "" },
if_not_exists = if self.if_not_exists {
"IF NOT EXISTS "
} else {
""
},
name = self.name,
)?;
for (i, loc) in self.storage_locations.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "({loc})")?;
}
write!(f, ")")?;
if let Some(val) = self.allow_writes {
write!(f, " ALLOW_WRITES = {}", if val { "TRUE" } else { "FALSE" })?;
}
if let Some(ref c) = self.comment {
write!(f, " COMMENT = '{}'", value::escape_single_quote_string(c))?;
}
Ok(())
}
}

/// MSSQL's json null clause
///
/// ```plaintext
Expand Down Expand Up @@ -12626,6 +12688,12 @@ impl From<CreateUser> for Statement {
}
}

impl From<CreateExternalVolume> for Statement {
fn from(c: CreateExternalVolume) -> Self {
Self::CreateExternalVolume(c)
}
}

impl From<CreateWarehouse> for Statement {
fn from(c: CreateWarehouse) -> Self {
Self::CreateWarehouse(c)
Expand Down
1 change: 1 addition & 0 deletions src/ast/spans.rs
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,7 @@ impl Spanned for Statement {
Statement::Vacuum(..) => Span::empty(),
Statement::AlterUser(..) => Span::empty(),
Statement::Reset(..) => Span::empty(),
Statement::CreateExternalVolume(..) => Span::empty(),
}
}
}
Expand Down
81 changes: 74 additions & 7 deletions src/dialect/snowflake.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,14 @@ use crate::ast::helpers::stmt_data_loading::{
};
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,
ColumnPolicy, ColumnPolicyProperty, ContactEntry, CopyIntoSnowflakeKind, CreateExternalVolume,
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,
};
use crate::dialect::{Dialect, Precedence};
use crate::keywords::Keyword;
Expand Down Expand Up @@ -290,6 +291,12 @@ impl Dialect for SnowflakeDialect {
// 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 @@ -1988,3 +1995,63 @@ 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(CreateExternalVolume {
or_replace,
if_not_exists,
name,
storage_locations,
allow_writes,
comment,
}
.into())
}

/// 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)
}
2 changes: 2 additions & 0 deletions src/keywords.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ define_keywords!(
ALL,
ALLOCATE,
ALLOWOVERWRITE,
ALLOW_WRITES,
ALTER,
ALWAYS,
ANALYZE,
Expand Down Expand Up @@ -1006,6 +1007,7 @@ define_keywords!(
STEP,
STORAGE,
STORAGE_INTEGRATION,
STORAGE_LOCATIONS,
STORAGE_SERIALIZATION_POLICY,
STORED,
STRAIGHT_JOIN,
Expand Down
4 changes: 3 additions & 1 deletion src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7627,6 +7627,8 @@ impl<'a> Parser<'a> {
ObjectType::User
} else if self.parse_keyword(Keyword::STREAM) {
ObjectType::Stream
} else if self.parse_keywords(&[Keyword::EXTERNAL, Keyword::VOLUME]) {
ObjectType::ExternalVolume
} else if self.parse_keyword(Keyword::WAREHOUSE) {
ObjectType::Warehouse
} else if self.parse_keyword(Keyword::FUNCTION) {
Expand Down Expand Up @@ -7656,7 +7658,7 @@ impl<'a> Parser<'a> {
};
} else {
return self.expected_ref(
"COLLATION, CONNECTOR, DATABASE, EXTENSION, FUNCTION, INDEX, OPERATOR, POLICY, PROCEDURE, ROLE, SCHEMA, SECRET, SEQUENCE, STAGE, TABLE, TRIGGER, TYPE, VIEW, MATERIALIZED VIEW, USER or WAREHOUSE after DROP",
"COLLATION, CONNECTOR, DATABASE, EXTENSION, EXTERNAL VOLUME, FUNCTION, INDEX, OPERATOR, POLICY, PROCEDURE, ROLE, SCHEMA, SECRET, SEQUENCE, STAGE, TABLE, TRIGGER, TYPE, VIEW, MATERIALIZED VIEW, USER or WAREHOUSE after DROP",
self.peek_token_ref(),
);
};
Expand Down
Loading
Loading