diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 6bbb0af9e..7e1ac08c1 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -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, @@ -5055,6 +5055,15 @@ pub enum Statement { show_options: ShowStatementOptions, }, /// ```sql + /// SHOW [TERSE] STAGES [ LIKE '' ] [ 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] ... /// ``` /// See @@ -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, diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 4c88ab5f9..ac79be863 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -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(), diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 2f88415e2..f912a942e 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -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(); @@ -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 { - // [ 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 = ''` / `FILE_FORMAT = ` + // is sugar for `FILE_FORMAT = (FORMAT_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 = ''` / `FILE_FORMAT = ` is - // sugar for `FILE_FORMAT = (FORMAT_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, @@ -2472,6 +2508,15 @@ fn parse_show_file_formats(terse: bool, parser: &mut Parser) -> Result Result { + let show_options = parser.parse_show_stmt_options()?; + Ok(Statement::ShowStages { + terse, + show_options, + }) +} + /// Parse `DESC[RIBE] WAREHOUSE ` fn parse_describe_warehouse(parser: &mut Parser) -> Result { let name = parser.parse_object_name(false)?; diff --git a/src/keywords.rs b/src/keywords.rs index a17e20d0a..743329d97 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -1025,6 +1025,7 @@ define_keywords!( SRID, STABLE, STAGE, + STAGES, START, STARTS, STATEMENT, diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 655d9c88c..42ec1957e 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -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!(), + } +}