Skip to content
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
29 changes: 24 additions & 5 deletions src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,11 @@ pub use self::ddl::{
IdentityPropertyKind, IdentityPropertyOrder, IndexColumn, IndexOption, IndexType,
KeyOrIndexDisplay, Msck, NullsDistinctOption, OperatorArgTypes, OperatorClassItem,
OperatorFamilyDropItem, OperatorFamilyItem, OperatorOption, OperatorPurpose, Owner, Partition,
PartitionBoundValue, ProcedureExecuteAs, ProcedureParam, ReferentialAction, RenameTableNameKind,
ReplicaIdentity,
TagsColumnOption, TriggerObjectKind, Truncate, UserDefinedTypeCompositeAttributeDef,
UserDefinedTypeInternalLength, UserDefinedTypeRangeOption, UserDefinedTypeRepresentation,
UserDefinedTypeSqlDefinitionOption, UserDefinedTypeStorage, ViewColumnDef,
PartitionBoundValue, ProcedureExecuteAs, ProcedureParam, ReferentialAction,
RenameTableNameKind, ReplicaIdentity, TagsColumnOption, TriggerObjectKind, Truncate,
UserDefinedTypeCompositeAttributeDef, UserDefinedTypeInternalLength,
UserDefinedTypeRangeOption, UserDefinedTypeRepresentation, UserDefinedTypeSqlDefinitionOption,
UserDefinedTypeStorage, ViewColumnDef,
};
pub use self::dml::{
Delete, Insert, Merge, MergeAction, MergeClause, MergeClauseKind, MergeInsertExpr,
Expand Down Expand Up @@ -5055,6 +5055,15 @@ pub enum Statement {
show_options: ShowStatementOptions,
},
/// ```sql
/// SHOW [TERSE] STAGES [ LIKE '<pattern>' ] [ IN ... ] ...
/// ```
ShowStages {
/// Whether to show terse output.
terse: bool,
/// Options controlling the SHOW output (`LIKE` / `IN` / `LIMIT` / ...).
show_options: ShowStatementOptions,
},
/// ```sql
/// CREATE [OR REPLACE] CATALOG INTEGRATION [IF NOT EXISTS] <name> ...
/// ```
/// See <https://docs.snowflake.com/en/sql-reference/sql/create-catalog-integration>
Expand Down Expand Up @@ -7169,6 +7178,16 @@ impl fmt::Display for Statement {
terse = if *terse { "TERSE " } else { "" },
)
}
Statement::ShowStages {
terse,
show_options,
} => {
write!(
f,
"SHOW {terse}STAGES{show_options}",
terse = if *terse { "TERSE " } else { "" },
)
}
Statement::CreateCatalogIntegration {
or_replace,
if_not_exists,
Expand Down
1 change: 1 addition & 0 deletions src/ast/spans.rs
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,7 @@ impl Spanned for Statement {
Statement::DropFileFormat { .. } => Span::empty(),
Statement::DescribeFileFormat { .. } => Span::empty(),
Statement::ShowFileFormats { .. } => Span::empty(),
Statement::ShowStages { .. } => Span::empty(),
Statement::CreateCatalogIntegration { .. } => Span::empty(),
Statement::DropCatalogIntegration { .. } => Span::empty(),
Statement::ShowCatalogIntegrations { .. } => Span::empty(),
Expand Down
127 changes: 86 additions & 41 deletions src/dialect/snowflake.rs
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,9 @@ impl Dialect for SnowflakeDialect {
if parser.parse_keywords(&[Keyword::FILE, Keyword::FORMATS]) {
return Some(parse_show_file_formats(terse, parser));
}
if parser.parse_keyword(Keyword::STAGES) {
return Some(parse_show_stages(terse, parser));
}
//Give back Keyword::TERSE
if terse {
parser.prev_token();
Expand Down Expand Up @@ -1333,58 +1336,91 @@ struct StageProperties {
/// Parse the stage property groups (`internalStageParams`/`externalStageParams`,
/// `DIRECTORY`, `FILE_FORMAT`, `COPY_OPTIONS`, `COMMENT`) shared by
/// `CREATE STAGE` and `ALTER STAGE ... SET`.
///
/// Snowflake accepts these groups in any order (e.g. `FILE_FORMAT = (...) URL =
/// '...'`), so the groups are matched in a loop until none remains rather than
/// in a fixed sequence. Each group may appear at most once.
fn parse_stage_properties(parser: &mut Parser) -> Result<StageProperties, ParserError> {
// [ internalStageParams | externalStageParams ]
let stage_params = parse_stage_params(parser)?;

let empty_options = || KeyValueOptions {
options: vec![],
delimiter: KeyValueOptionsDelimiter::Space,
};
let (mut url, mut storage_integration, mut endpoint) = (None, None, None);
let mut encryption = empty_options();
let mut credentials = empty_options();
let mut directory_table_params = Vec::new();
let mut file_format = Vec::new();
let mut copy_options = Vec::new();
let mut comment = None;

// [ directoryTableParams ]
if parser.parse_keyword(Keyword::DIRECTORY) {
parser.expect_token(&Token::Eq)?;
directory_table_params = parser.parse_key_value_options(true, &[])?.options;
}

// [ file_format]
if parser.parse_keyword(Keyword::FILE_FORMAT) {
parser.expect_token(&Token::Eq)?;
if parser.peek_token().token == Token::LParen {
file_format = parser.parse_key_value_options(true, &[])?.options;
loop {
// [ internalStageParams | externalStageParams ]
if url.is_none() && parser.parse_keyword(Keyword::URL) {
parser.expect_token(&Token::Eq)?;
url = Some(match parser.next_token().token {
Token::SingleQuotedString(word) => Ok(word),
_ => parser.expected_ref("a URL statement", parser.peek_token_ref()),
}?);
} else if storage_integration.is_none()
&& parser.parse_keyword(Keyword::STORAGE_INTEGRATION)
{
parser.expect_token(&Token::Eq)?;
storage_integration = Some(parser.next_token().token.to_string());
} else if endpoint.is_none() && parser.parse_keyword(Keyword::ENDPOINT) {
parser.expect_token(&Token::Eq)?;
endpoint = Some(match parser.next_token().token {
Token::SingleQuotedString(word) => Ok(word),
_ => parser.expected_ref("an endpoint statement", parser.peek_token_ref()),
}?);
} else if credentials.options.is_empty() && parser.parse_keyword(Keyword::CREDENTIALS) {
parser.expect_token(&Token::Eq)?;
credentials.options = parser.parse_key_value_options(true, &[])?.options;
} else if encryption.options.is_empty() && parser.parse_keyword(Keyword::ENCRYPTION) {
parser.expect_token(&Token::Eq)?;
encryption.options = parser.parse_key_value_options(true, &[])?.options;
} else if directory_table_params.is_empty() && parser.parse_keyword(Keyword::DIRECTORY) {
parser.expect_token(&Token::Eq)?;
directory_table_params = parser.parse_key_value_options(true, &[])?.options;
} else if file_format.is_empty() && parser.parse_keyword(Keyword::FILE_FORMAT) {
parser.expect_token(&Token::Eq)?;
if parser.peek_token().token == Token::LParen {
file_format = parser.parse_key_value_options(true, &[])?.options;
} else {
// Shorthand `FILE_FORMAT = '<name>'` / `FILE_FORMAT = <ident>`
// is sugar for `FILE_FORMAT = (FORMAT_NAME = <name>)` —
// normalize it.
let tok = parser.peek_token();
let value = match tok.token {
Token::Word(w) => {
parser.next_token();
Value::Placeholder(w.value.clone()).with_span(tok.span)
}
_ => parser.parse_value()?,
};
file_format = vec![KeyValueOption {
option_name: "FORMAT_NAME".to_string(),
option_value: KeyValueOptionKind::Single(value),
}];
}
} else if copy_options.is_empty() && parser.parse_keyword(Keyword::COPY_OPTIONS) {
parser.expect_token(&Token::Eq)?;
copy_options = parser.parse_key_value_options(true, &[])?.options;
} else if comment.is_none() && parser.parse_keyword(Keyword::COMMENT) {
parser.expect_token(&Token::Eq)?;
comment = Some(parser.parse_comment_value()?);
} else {
// Shorthand `FILE_FORMAT = '<name>'` / `FILE_FORMAT = <ident>` is
// sugar for `FILE_FORMAT = (FORMAT_NAME = <name>)` — normalize it.
let tok = parser.peek_token();
let value = match tok.token {
Token::Word(w) => {
parser.next_token();
Value::Placeholder(w.value.clone()).with_span(tok.span)
}
_ => parser.parse_value()?,
};
file_format = vec![KeyValueOption {
option_name: "FORMAT_NAME".to_string(),
option_value: KeyValueOptionKind::Single(value),
}];
break;
}
}

// [ copy_options ]
if parser.parse_keyword(Keyword::COPY_OPTIONS) {
parser.expect_token(&Token::Eq)?;
copy_options = parser.parse_key_value_options(true, &[])?.options;
}

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

Ok(StageProperties {
stage_params,
stage_params: StageParamsObject {
url,
encryption,
endpoint,
storage_integration,
credentials,
},
directory_table_params: KeyValueOptions {
options: directory_table_params,
delimiter: KeyValueOptionsDelimiter::Space,
Expand Down Expand Up @@ -2472,6 +2508,15 @@ fn parse_show_file_formats(terse: bool, parser: &mut Parser) -> Result<Statement
})
}

/// Parse `SHOW [TERSE] STAGES [ ... ]`
fn parse_show_stages(terse: bool, parser: &mut Parser) -> Result<Statement, ParserError> {
let show_options = parser.parse_show_stmt_options()?;
Ok(Statement::ShowStages {
terse,
show_options,
})
}

/// Parse `DESC[RIBE] WAREHOUSE <name>`
fn parse_describe_warehouse(parser: &mut Parser) -> Result<Statement, ParserError> {
let name = parser.parse_object_name(false)?;
Expand Down
1 change: 1 addition & 0 deletions src/keywords.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1025,6 +1025,7 @@ define_keywords!(
SRID,
STABLE,
STAGE,
STAGES,
START,
STARTS,
STATEMENT,
Expand Down
34 changes: 34 additions & 0 deletions tests/sqlparser_snowflake.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7951,3 +7951,37 @@ fn test_show_terse_file_formats() {
_ => unreachable!(),
}
}

#[test]
fn test_create_stage_options_any_order() {
// Snowflake accepts the stage property groups in any order.
snowflake().one_statement_parses_to(
"CREATE OR REPLACE STAGE s FILE_FORMAT = (TYPE=CSV) URL = 's3://test/'",
"CREATE OR REPLACE STAGE s URL='s3://test/' FILE_FORMAT=(TYPE=CSV)",
);
}

#[test]
fn test_show_stages() {
match snowflake().verified_stmt("SHOW STAGES") {
Statement::ShowStages { terse, .. } => {
assert!(!terse);
}
_ => unreachable!(),
}
}

#[test]
fn test_show_stages_like_in_schema() {
snowflake().verified_stmt("SHOW STAGES LIKE 'pat%' IN SCHEMA db.sch");
}

#[test]
fn test_show_terse_stages() {
match snowflake().verified_stmt("SHOW TERSE STAGES") {
Statement::ShowStages { terse, .. } => {
assert!(terse);
}
_ => unreachable!(),
}
}
Loading