Skip to content

Commit e9f78d8

Browse files
wojpadloWojciech Padlo
andauthored
Snowflake: SHOW [TERSE] STAGES + order-independent CREATE/ALTER STAGE options (#10)
* Snowflake: parse SHOW [TERSE] STAGES Add Statement::ShowStages { terse, show_options } mirroring ShowFileFormats, plus the STAGES keyword and parse_show_stages dispatch in the Snowflake SHOW block. * Snowflake: order-independent CREATE/ALTER STAGE option groups parse_stage_properties matched the property groups in a fixed sequence, rejecting valid Snowflake like FILE_FORMAT=(...) URL='...'. Match the groups in a loop instead so they may appear in any order (each once). * Format: rewrap ddl re-export list (rustfmt) --------- Co-authored-by: Wojciech Padlo <wojciech.padlo@localstack.cloud>
1 parent a0bc366 commit e9f78d8

5 files changed

Lines changed: 146 additions & 46 deletions

File tree

src/ast/mod.rs

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,11 @@ pub use self::ddl::{
8080
IdentityPropertyKind, IdentityPropertyOrder, IndexColumn, IndexOption, IndexType,
8181
KeyOrIndexDisplay, Msck, NullsDistinctOption, OperatorArgTypes, OperatorClassItem,
8282
OperatorFamilyDropItem, OperatorFamilyItem, OperatorOption, OperatorPurpose, Owner, Partition,
83-
PartitionBoundValue, ProcedureExecuteAs, ProcedureParam, ReferentialAction, RenameTableNameKind,
84-
ReplicaIdentity,
85-
TagsColumnOption, TriggerObjectKind, Truncate, UserDefinedTypeCompositeAttributeDef,
86-
UserDefinedTypeInternalLength, UserDefinedTypeRangeOption, UserDefinedTypeRepresentation,
87-
UserDefinedTypeSqlDefinitionOption, UserDefinedTypeStorage, ViewColumnDef,
83+
PartitionBoundValue, ProcedureExecuteAs, ProcedureParam, ReferentialAction,
84+
RenameTableNameKind, ReplicaIdentity, TagsColumnOption, TriggerObjectKind, Truncate,
85+
UserDefinedTypeCompositeAttributeDef, UserDefinedTypeInternalLength,
86+
UserDefinedTypeRangeOption, UserDefinedTypeRepresentation, UserDefinedTypeSqlDefinitionOption,
87+
UserDefinedTypeStorage, ViewColumnDef,
8888
};
8989
pub use self::dml::{
9090
Delete, Insert, Merge, MergeAction, MergeClause, MergeClauseKind, MergeInsertExpr,
@@ -5055,6 +5055,15 @@ pub enum Statement {
50555055
show_options: ShowStatementOptions,
50565056
},
50575057
/// ```sql
5058+
/// SHOW [TERSE] STAGES [ LIKE '<pattern>' ] [ IN ... ] ...
5059+
/// ```
5060+
ShowStages {
5061+
/// Whether to show terse output.
5062+
terse: bool,
5063+
/// Options controlling the SHOW output (`LIKE` / `IN` / `LIMIT` / ...).
5064+
show_options: ShowStatementOptions,
5065+
},
5066+
/// ```sql
50585067
/// CREATE [OR REPLACE] CATALOG INTEGRATION [IF NOT EXISTS] <name> ...
50595068
/// ```
50605069
/// See <https://docs.snowflake.com/en/sql-reference/sql/create-catalog-integration>
@@ -7169,6 +7178,16 @@ impl fmt::Display for Statement {
71697178
terse = if *terse { "TERSE " } else { "" },
71707179
)
71717180
}
7181+
Statement::ShowStages {
7182+
terse,
7183+
show_options,
7184+
} => {
7185+
write!(
7186+
f,
7187+
"SHOW {terse}STAGES{show_options}",
7188+
terse = if *terse { "TERSE " } else { "" },
7189+
)
7190+
}
71727191
Statement::CreateCatalogIntegration {
71737192
or_replace,
71747193
if_not_exists,

src/ast/spans.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,7 @@ impl Spanned for Statement {
551551
Statement::DropFileFormat { .. } => Span::empty(),
552552
Statement::DescribeFileFormat { .. } => Span::empty(),
553553
Statement::ShowFileFormats { .. } => Span::empty(),
554+
Statement::ShowStages { .. } => Span::empty(),
554555
Statement::CreateCatalogIntegration { .. } => Span::empty(),
555556
Statement::DropCatalogIntegration { .. } => Span::empty(),
556557
Statement::ShowCatalogIntegrations { .. } => Span::empty(),

src/dialect/snowflake.rs

Lines changed: 86 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,9 @@ impl Dialect for SnowflakeDialect {
458458
if parser.parse_keywords(&[Keyword::FILE, Keyword::FORMATS]) {
459459
return Some(parse_show_file_formats(terse, parser));
460460
}
461+
if parser.parse_keyword(Keyword::STAGES) {
462+
return Some(parse_show_stages(terse, parser));
463+
}
461464
//Give back Keyword::TERSE
462465
if terse {
463466
parser.prev_token();
@@ -1333,58 +1336,91 @@ struct StageProperties {
13331336
/// Parse the stage property groups (`internalStageParams`/`externalStageParams`,
13341337
/// `DIRECTORY`, `FILE_FORMAT`, `COPY_OPTIONS`, `COMMENT`) shared by
13351338
/// `CREATE STAGE` and `ALTER STAGE ... SET`.
1339+
///
1340+
/// Snowflake accepts these groups in any order (e.g. `FILE_FORMAT = (...) URL =
1341+
/// '...'`), so the groups are matched in a loop until none remains rather than
1342+
/// in a fixed sequence. Each group may appear at most once.
13361343
fn parse_stage_properties(parser: &mut Parser) -> Result<StageProperties, ParserError> {
1337-
// [ internalStageParams | externalStageParams ]
1338-
let stage_params = parse_stage_params(parser)?;
1339-
1344+
let empty_options = || KeyValueOptions {
1345+
options: vec![],
1346+
delimiter: KeyValueOptionsDelimiter::Space,
1347+
};
1348+
let (mut url, mut storage_integration, mut endpoint) = (None, None, None);
1349+
let mut encryption = empty_options();
1350+
let mut credentials = empty_options();
13401351
let mut directory_table_params = Vec::new();
13411352
let mut file_format = Vec::new();
13421353
let mut copy_options = Vec::new();
13431354
let mut comment = None;
13441355

1345-
// [ directoryTableParams ]
1346-
if parser.parse_keyword(Keyword::DIRECTORY) {
1347-
parser.expect_token(&Token::Eq)?;
1348-
directory_table_params = parser.parse_key_value_options(true, &[])?.options;
1349-
}
1350-
1351-
// [ file_format]
1352-
if parser.parse_keyword(Keyword::FILE_FORMAT) {
1353-
parser.expect_token(&Token::Eq)?;
1354-
if parser.peek_token().token == Token::LParen {
1355-
file_format = parser.parse_key_value_options(true, &[])?.options;
1356+
loop {
1357+
// [ internalStageParams | externalStageParams ]
1358+
if url.is_none() && parser.parse_keyword(Keyword::URL) {
1359+
parser.expect_token(&Token::Eq)?;
1360+
url = Some(match parser.next_token().token {
1361+
Token::SingleQuotedString(word) => Ok(word),
1362+
_ => parser.expected_ref("a URL statement", parser.peek_token_ref()),
1363+
}?);
1364+
} else if storage_integration.is_none()
1365+
&& parser.parse_keyword(Keyword::STORAGE_INTEGRATION)
1366+
{
1367+
parser.expect_token(&Token::Eq)?;
1368+
storage_integration = Some(parser.next_token().token.to_string());
1369+
} else if endpoint.is_none() && parser.parse_keyword(Keyword::ENDPOINT) {
1370+
parser.expect_token(&Token::Eq)?;
1371+
endpoint = Some(match parser.next_token().token {
1372+
Token::SingleQuotedString(word) => Ok(word),
1373+
_ => parser.expected_ref("an endpoint statement", parser.peek_token_ref()),
1374+
}?);
1375+
} else if credentials.options.is_empty() && parser.parse_keyword(Keyword::CREDENTIALS) {
1376+
parser.expect_token(&Token::Eq)?;
1377+
credentials.options = parser.parse_key_value_options(true, &[])?.options;
1378+
} else if encryption.options.is_empty() && parser.parse_keyword(Keyword::ENCRYPTION) {
1379+
parser.expect_token(&Token::Eq)?;
1380+
encryption.options = parser.parse_key_value_options(true, &[])?.options;
1381+
} else if directory_table_params.is_empty() && parser.parse_keyword(Keyword::DIRECTORY) {
1382+
parser.expect_token(&Token::Eq)?;
1383+
directory_table_params = parser.parse_key_value_options(true, &[])?.options;
1384+
} else if file_format.is_empty() && parser.parse_keyword(Keyword::FILE_FORMAT) {
1385+
parser.expect_token(&Token::Eq)?;
1386+
if parser.peek_token().token == Token::LParen {
1387+
file_format = parser.parse_key_value_options(true, &[])?.options;
1388+
} else {
1389+
// Shorthand `FILE_FORMAT = '<name>'` / `FILE_FORMAT = <ident>`
1390+
// is sugar for `FILE_FORMAT = (FORMAT_NAME = <name>)` —
1391+
// normalize it.
1392+
let tok = parser.peek_token();
1393+
let value = match tok.token {
1394+
Token::Word(w) => {
1395+
parser.next_token();
1396+
Value::Placeholder(w.value.clone()).with_span(tok.span)
1397+
}
1398+
_ => parser.parse_value()?,
1399+
};
1400+
file_format = vec![KeyValueOption {
1401+
option_name: "FORMAT_NAME".to_string(),
1402+
option_value: KeyValueOptionKind::Single(value),
1403+
}];
1404+
}
1405+
} else if copy_options.is_empty() && parser.parse_keyword(Keyword::COPY_OPTIONS) {
1406+
parser.expect_token(&Token::Eq)?;
1407+
copy_options = parser.parse_key_value_options(true, &[])?.options;
1408+
} else if comment.is_none() && parser.parse_keyword(Keyword::COMMENT) {
1409+
parser.expect_token(&Token::Eq)?;
1410+
comment = Some(parser.parse_comment_value()?);
13561411
} else {
1357-
// Shorthand `FILE_FORMAT = '<name>'` / `FILE_FORMAT = <ident>` is
1358-
// sugar for `FILE_FORMAT = (FORMAT_NAME = <name>)` — normalize it.
1359-
let tok = parser.peek_token();
1360-
let value = match tok.token {
1361-
Token::Word(w) => {
1362-
parser.next_token();
1363-
Value::Placeholder(w.value.clone()).with_span(tok.span)
1364-
}
1365-
_ => parser.parse_value()?,
1366-
};
1367-
file_format = vec![KeyValueOption {
1368-
option_name: "FORMAT_NAME".to_string(),
1369-
option_value: KeyValueOptionKind::Single(value),
1370-
}];
1412+
break;
13711413
}
13721414
}
13731415

1374-
// [ copy_options ]
1375-
if parser.parse_keyword(Keyword::COPY_OPTIONS) {
1376-
parser.expect_token(&Token::Eq)?;
1377-
copy_options = parser.parse_key_value_options(true, &[])?.options;
1378-
}
1379-
1380-
// [ comment ]
1381-
if parser.parse_keyword(Keyword::COMMENT) {
1382-
parser.expect_token(&Token::Eq)?;
1383-
comment = Some(parser.parse_comment_value()?);
1384-
}
1385-
13861416
Ok(StageProperties {
1387-
stage_params,
1417+
stage_params: StageParamsObject {
1418+
url,
1419+
encryption,
1420+
endpoint,
1421+
storage_integration,
1422+
credentials,
1423+
},
13881424
directory_table_params: KeyValueOptions {
13891425
options: directory_table_params,
13901426
delimiter: KeyValueOptionsDelimiter::Space,
@@ -2472,6 +2508,15 @@ fn parse_show_file_formats(terse: bool, parser: &mut Parser) -> Result<Statement
24722508
})
24732509
}
24742510

2511+
/// Parse `SHOW [TERSE] STAGES [ ... ]`
2512+
fn parse_show_stages(terse: bool, parser: &mut Parser) -> Result<Statement, ParserError> {
2513+
let show_options = parser.parse_show_stmt_options()?;
2514+
Ok(Statement::ShowStages {
2515+
terse,
2516+
show_options,
2517+
})
2518+
}
2519+
24752520
/// Parse `DESC[RIBE] WAREHOUSE <name>`
24762521
fn parse_describe_warehouse(parser: &mut Parser) -> Result<Statement, ParserError> {
24772522
let name = parser.parse_object_name(false)?;

src/keywords.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1025,6 +1025,7 @@ define_keywords!(
10251025
SRID,
10261026
STABLE,
10271027
STAGE,
1028+
STAGES,
10281029
START,
10291030
STARTS,
10301031
STATEMENT,

tests/sqlparser_snowflake.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7951,3 +7951,37 @@ fn test_show_terse_file_formats() {
79517951
_ => unreachable!(),
79527952
}
79537953
}
7954+
7955+
#[test]
7956+
fn test_create_stage_options_any_order() {
7957+
// Snowflake accepts the stage property groups in any order.
7958+
snowflake().one_statement_parses_to(
7959+
"CREATE OR REPLACE STAGE s FILE_FORMAT = (TYPE=CSV) URL = 's3://test/'",
7960+
"CREATE OR REPLACE STAGE s URL='s3://test/' FILE_FORMAT=(TYPE=CSV)",
7961+
);
7962+
}
7963+
7964+
#[test]
7965+
fn test_show_stages() {
7966+
match snowflake().verified_stmt("SHOW STAGES") {
7967+
Statement::ShowStages { terse, .. } => {
7968+
assert!(!terse);
7969+
}
7970+
_ => unreachable!(),
7971+
}
7972+
}
7973+
7974+
#[test]
7975+
fn test_show_stages_like_in_schema() {
7976+
snowflake().verified_stmt("SHOW STAGES LIKE 'pat%' IN SCHEMA db.sch");
7977+
}
7978+
7979+
#[test]
7980+
fn test_show_terse_stages() {
7981+
match snowflake().verified_stmt("SHOW TERSE STAGES") {
7982+
Statement::ShowStages { terse, .. } => {
7983+
assert!(terse);
7984+
}
7985+
_ => unreachable!(),
7986+
}
7987+
}

0 commit comments

Comments
 (0)