Skip to content

Commit d67abcd

Browse files
author
Wojciech Padlo
committed
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).
1 parent 225f43b commit d67abcd

2 files changed

Lines changed: 83 additions & 41 deletions

File tree

src/dialect/snowflake.rs

Lines changed: 74 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1336,58 +1336,91 @@ struct StageProperties {
13361336
/// Parse the stage property groups (`internalStageParams`/`externalStageParams`,
13371337
/// `DIRECTORY`, `FILE_FORMAT`, `COPY_OPTIONS`, `COMMENT`) shared by
13381338
/// `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.
13391343
fn parse_stage_properties(parser: &mut Parser) -> Result<StageProperties, ParserError> {
1340-
// [ internalStageParams | externalStageParams ]
1341-
let stage_params = parse_stage_params(parser)?;
1342-
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();
13431351
let mut directory_table_params = Vec::new();
13441352
let mut file_format = Vec::new();
13451353
let mut copy_options = Vec::new();
13461354
let mut comment = None;
13471355

1348-
// [ directoryTableParams ]
1349-
if parser.parse_keyword(Keyword::DIRECTORY) {
1350-
parser.expect_token(&Token::Eq)?;
1351-
directory_table_params = parser.parse_key_value_options(true, &[])?.options;
1352-
}
1353-
1354-
// [ file_format]
1355-
if parser.parse_keyword(Keyword::FILE_FORMAT) {
1356-
parser.expect_token(&Token::Eq)?;
1357-
if parser.peek_token().token == Token::LParen {
1358-
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()?);
13591411
} else {
1360-
// Shorthand `FILE_FORMAT = '<name>'` / `FILE_FORMAT = <ident>` is
1361-
// sugar for `FILE_FORMAT = (FORMAT_NAME = <name>)` — normalize it.
1362-
let tok = parser.peek_token();
1363-
let value = match tok.token {
1364-
Token::Word(w) => {
1365-
parser.next_token();
1366-
Value::Placeholder(w.value.clone()).with_span(tok.span)
1367-
}
1368-
_ => parser.parse_value()?,
1369-
};
1370-
file_format = vec![KeyValueOption {
1371-
option_name: "FORMAT_NAME".to_string(),
1372-
option_value: KeyValueOptionKind::Single(value),
1373-
}];
1412+
break;
13741413
}
13751414
}
13761415

1377-
// [ copy_options ]
1378-
if parser.parse_keyword(Keyword::COPY_OPTIONS) {
1379-
parser.expect_token(&Token::Eq)?;
1380-
copy_options = parser.parse_key_value_options(true, &[])?.options;
1381-
}
1382-
1383-
// [ comment ]
1384-
if parser.parse_keyword(Keyword::COMMENT) {
1385-
parser.expect_token(&Token::Eq)?;
1386-
comment = Some(parser.parse_comment_value()?);
1387-
}
1388-
13891416
Ok(StageProperties {
1390-
stage_params,
1417+
stage_params: StageParamsObject {
1418+
url,
1419+
encryption,
1420+
endpoint,
1421+
storage_integration,
1422+
credentials,
1423+
},
13911424
directory_table_params: KeyValueOptions {
13921425
options: directory_table_params,
13931426
delimiter: KeyValueOptionsDelimiter::Space,

tests/sqlparser_snowflake.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7952,6 +7952,15 @@ fn test_show_terse_file_formats() {
79527952
}
79537953
}
79547954

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+
79557964
#[test]
79567965
fn test_show_stages() {
79577966
match snowflake().verified_stmt("SHOW STAGES") {

0 commit comments

Comments
 (0)