diff --git a/src/ast/mod.rs b/src/ast/mod.rs index e7cc9099d..0042a476a 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -7903,6 +7903,8 @@ pub enum FunctionArgExpr { Expr(Expr), /// Qualified wildcard, e.g. `alias.*` or `schema.table.*`. QualifiedWildcard(ObjectName), + /// Qualified wildcard with additional options, e.g. `alias.* EXCLUDE(col)`. + QualifiedWildcardWithOptions(ObjectName, WildcardAdditionalOptions), /// An unqualified `*` wildcard. Wildcard, /// An unqualified `*` wildcard with additional options, e.g. `* EXCLUDE(col)`. @@ -7926,6 +7928,9 @@ impl fmt::Display for FunctionArgExpr { match self { FunctionArgExpr::Expr(expr) => write!(f, "{expr}"), FunctionArgExpr::QualifiedWildcard(prefix) => write!(f, "{prefix}.*"), + FunctionArgExpr::QualifiedWildcardWithOptions(prefix, opts) => { + write!(f, "{prefix}.*{opts}") + } FunctionArgExpr::Wildcard => f.write_str("*"), FunctionArgExpr::WildcardWithOptions(opts) => write!(f, "*{opts}"), } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index f67e5085f..90fe079e9 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -2176,6 +2176,9 @@ impl Spanned for FunctionArgExpr { FunctionArgExpr::QualifiedWildcard(object_name) => { union_spans(object_name.0.iter().map(|i| i.span())) } + FunctionArgExpr::QualifiedWildcardWithOptions(object_name, options) => { + union_spans([object_name.span(), options.span()].into_iter()) + } FunctionArgExpr::Wildcard => Span::empty(), FunctionArgExpr::WildcardWithOptions(_) => Span::empty(), } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 30a538253..a5b167a17 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -183,6 +183,15 @@ pub enum WildcardExpr { Wildcard, } +fn wildcard_options_are_empty(options: &WildcardAdditionalOptions) -> bool { + options.opt_ilike.is_none() + && options.opt_exclude.is_none() + && options.opt_except.is_none() + && options.opt_replace.is_none() + && options.opt_rename.is_none() + && options.opt_alias.is_none() +} + impl From for ParserError { fn from(e: TokenizerError) -> Self { ParserError::TokenizerError(e.to_string()) @@ -18271,6 +18280,24 @@ impl<'a> Parser<'a> { /// Parse a single function argument, handling named and unnamed variants. pub fn parse_function_args(&mut self) -> Result { + if self.dialect.supports_select_wildcard_exclude() { + if let Some(arg) = self.maybe_parse(|parser| { + parser.expect_token(&Token::LParen)?; + let Expr::QualifiedWildcard(prefix, token) = parser.parse_wildcard_expr()? else { + return parser.expected_ref("a qualified wildcard", parser.peek_token_ref()); + }; + let options = parser.parse_wildcard_additional_options(token.0)?; + parser.expect_token(&Token::RParen)?; + Ok(if wildcard_options_are_empty(&options) { + FunctionArgExpr::QualifiedWildcard(prefix) + } else { + FunctionArgExpr::QualifiedWildcardWithOptions(prefix, options) + }) + })? { + return Ok(FunctionArg::Unnamed(arg)); + } + } + let arg = if self.dialect.supports_named_fn_args_with_expr_name() { self.maybe_parse(|p| { let name = p.parse_expr()?; @@ -18303,17 +18330,22 @@ impl<'a> Parser<'a> { // Support `* EXCLUDE(col1, col2, ...)` inside function calls (e.g. Snowflake's // `HASH(* EXCLUDE(col))`). Parse the options the same way SELECT items do. let opts = self.parse_wildcard_additional_options(token.0.clone())?; - if opts.opt_exclude.is_some() - || opts.opt_except.is_some() - || opts.opt_replace.is_some() - || opts.opt_rename.is_some() - || opts.opt_ilike.is_some() - { + if !wildcard_options_are_empty(&opts) { FunctionArgExpr::WildcardWithOptions(opts) } else { wildcard_expr.into() } } + Expr::QualifiedWildcard(prefix, token) + if self.dialect.supports_select_wildcard_exclude() => + { + let opts = self.parse_wildcard_additional_options(token.0)?; + if wildcard_options_are_empty(&opts) { + FunctionArgExpr::QualifiedWildcard(prefix) + } else { + FunctionArgExpr::QualifiedWildcardWithOptions(prefix, opts) + } + } other => other.into(), }; Ok(FunctionArg::Unnamed(arg_expr)) diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index ae53ca060..2661eb035 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -1586,6 +1586,22 @@ fn snowflake_and_generic() -> TestedDialects { ]) } +#[test] +fn parse_parenthesized_qualified_wildcard_function_argument() { + snowflake().one_statement_parses_to( + "SELECT SEARCH((lines.*), 'king') FROM lines", + "SELECT SEARCH(lines.*, 'king') FROM lines", + ); + snowflake().one_statement_parses_to( + "SELECT SEARCH((lines.* ILIKE '%line'), 'king') FROM lines", + "SELECT SEARCH(lines.* ILIKE '%line', 'king') FROM lines", + ); + snowflake().one_statement_parses_to( + "SELECT SEARCH((lines.* EXCLUDE character), 'king') FROM lines", + "SELECT SEARCH(lines.* EXCLUDE character, 'king') FROM lines", + ); +} + #[test] fn test_select_wildcard_with_exclude() { let select = snowflake_and_generic().verified_only_select("SELECT * EXCLUDE (col_a) FROM data");