From 0a55634ee3f6f796704345507e8d8ffd4081fc5a Mon Sep 17 00:00:00 2001 From: Yoa Bot Date: Sun, 22 Feb 2026 20:04:56 +0100 Subject: [PATCH 1/5] Snowflake: support wildcard with EXCLUDE in function arguments (e.g. HASH(* EXCLUDE(col))) --- src/ast/mod.rs | 5 +++++ src/ast/spans.rs | 2 ++ src/parser/mod.rs | 21 ++++++++++++++++++++- tests/sqlparser_snowflake.rs | 26 ++++++++++++++++++++++++++ 4 files changed, 53 insertions(+), 1 deletion(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 61b0f65b2..318cd57db 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -7614,6 +7614,10 @@ pub enum FunctionArgExpr { QualifiedWildcard(ObjectName), /// An unqualified `*` wildcard. Wildcard, + /// An unqualified `*` wildcard with additional options, e.g. `* EXCLUDE(col)`. + /// + /// Used in Snowflake to support expressions like `HASH(* EXCLUDE(col))`. + WildcardWithOptions(WildcardAdditionalOptions), } impl From for FunctionArgExpr { @@ -7632,6 +7636,7 @@ impl fmt::Display for FunctionArgExpr { FunctionArgExpr::Expr(expr) => write!(f, "{expr}"), FunctionArgExpr::QualifiedWildcard(prefix) => write!(f, "{prefix}.*"), FunctionArgExpr::Wildcard => f.write_str("*"), + FunctionArgExpr::WildcardWithOptions(opts) => write!(f, "*{opts}"), } } } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 128fe01be..4d178fce0 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -2128,6 +2128,7 @@ impl Spanned for FunctionArg { /// /// Missing spans: /// - [FunctionArgExpr::Wildcard] +/// - [FunctionArgExpr::WildcardWithOptions] impl Spanned for FunctionArgExpr { fn span(&self) -> Span { match self { @@ -2136,6 +2137,7 @@ impl Spanned for FunctionArgExpr { union_spans(object_name.0.iter().map(|i| i.span())) } FunctionArgExpr::Wildcard => Span::empty(), + FunctionArgExpr::WildcardWithOptions(_) => Span::empty(), } } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 16eb7a8b1..635f345f1 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -17559,7 +17559,26 @@ impl<'a> Parser<'a> { if let Some(arg) = arg { return Ok(arg); } - Ok(FunctionArg::Unnamed(self.parse_wildcard_expr()?.into())) + let wildcard_expr = self.parse_wildcard_expr()?; + let arg_expr: FunctionArgExpr = match wildcard_expr { + Expr::Wildcard(ref token) if self.dialect.supports_select_wildcard_exclude() => { + // 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() + { + FunctionArgExpr::WildcardWithOptions(opts) + } else { + wildcard_expr.into() + } + } + other => other.into(), + }; + Ok(FunctionArg::Unnamed(arg_expr)) } fn parse_function_named_arg_operator(&mut self) -> Result { diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 43444016f..868aa0222 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -4890,3 +4890,29 @@ fn test_select_dollar_column_from_stage() { // With table function args, without alias snowflake().verified_stmt("SELECT $1, $2 FROM @mystage1(file_format => 'myformat')"); } + +#[test] +fn test_hash_wildcard_exclude() { + // The canonical form normalizes `EXCLUDE(cols)` → `EXCLUDE (cols)` (space before parens), + // matching how WildcardAdditionalOptions / ExcludeSelectItem::Multiple is displayed. + let sql = r#"SELECT * FROM (SELECT *, HASH(*) HASH FROM (SELECT CREATED_AT::TIMESTAMP_NTZ AS CREATED_AT, UPDATED_AT::TIMESTAMP_NTZ AS UPDATED_AT, * EXCLUDE(SYNC_DATE, CREATED_AT, UPDATED_AT) FROM CMP_PROD.PUBLIC.CMP_AUDIT_LOG_HARD_DELETED)) S FULL JOIN (SELECT *, HASH(* EXCLUDE(SYNC_DATE)) HASH FROM CMP_PROD.ARCHIVE.CMP_AUDIT_LOG_HARD_DELETE_20260219) T USING(HASH) WHERE T.HASH IS NULL"#; + let canonical = r#"SELECT * FROM (SELECT *, HASH(*) AS HASH FROM (SELECT CREATED_AT::TIMESTAMP_NTZ AS CREATED_AT, UPDATED_AT::TIMESTAMP_NTZ AS UPDATED_AT, * EXCLUDE (SYNC_DATE, CREATED_AT, UPDATED_AT) FROM CMP_PROD.PUBLIC.CMP_AUDIT_LOG_HARD_DELETED)) S FULL JOIN (SELECT *, HASH(* EXCLUDE (SYNC_DATE)) AS HASH FROM CMP_PROD.ARCHIVE.CMP_AUDIT_LOG_HARD_DELETE_20260219) T USING(HASH) WHERE T.HASH IS NULL"#; + snowflake().verified_query_with_canonical(sql, canonical); +} + +#[test] +fn test_hash_star_simple() { + // HASH(*) — wildcard as a function argument. + snowflake().verified_expr("HASH(*)"); +} + +#[test] +fn test_hash_star_exclude_simple() { + // HASH(* EXCLUDE (col)) — wildcard with EXCLUDE in a function argument (Snowflake). + // Canonical form has a space before the parenthesised column list. + snowflake().one_statement_parses_to( + "SELECT HASH(* EXCLUDE(SYNC_DATE)) FROM t", + "SELECT HASH(* EXCLUDE (SYNC_DATE)) FROM t", + ); + snowflake().verified_expr("HASH(* EXCLUDE (SYNC_DATE))"); +} From b7f187941535cf88c8d49b5b2e70d91e0cd6ee25 Mon Sep 17 00:00:00 2001 From: Yoa Bot Date: Sun, 22 Feb 2026 23:02:18 +0100 Subject: [PATCH 2/5] Anonymize table/column names in tests --- tests/sqlparser_snowflake.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 868aa0222..4a234f674 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -4895,8 +4895,8 @@ fn test_select_dollar_column_from_stage() { fn test_hash_wildcard_exclude() { // The canonical form normalizes `EXCLUDE(cols)` → `EXCLUDE (cols)` (space before parens), // matching how WildcardAdditionalOptions / ExcludeSelectItem::Multiple is displayed. - let sql = r#"SELECT * FROM (SELECT *, HASH(*) HASH FROM (SELECT CREATED_AT::TIMESTAMP_NTZ AS CREATED_AT, UPDATED_AT::TIMESTAMP_NTZ AS UPDATED_AT, * EXCLUDE(SYNC_DATE, CREATED_AT, UPDATED_AT) FROM CMP_PROD.PUBLIC.CMP_AUDIT_LOG_HARD_DELETED)) S FULL JOIN (SELECT *, HASH(* EXCLUDE(SYNC_DATE)) HASH FROM CMP_PROD.ARCHIVE.CMP_AUDIT_LOG_HARD_DELETE_20260219) T USING(HASH) WHERE T.HASH IS NULL"#; - let canonical = r#"SELECT * FROM (SELECT *, HASH(*) AS HASH FROM (SELECT CREATED_AT::TIMESTAMP_NTZ AS CREATED_AT, UPDATED_AT::TIMESTAMP_NTZ AS UPDATED_AT, * EXCLUDE (SYNC_DATE, CREATED_AT, UPDATED_AT) FROM CMP_PROD.PUBLIC.CMP_AUDIT_LOG_HARD_DELETED)) S FULL JOIN (SELECT *, HASH(* EXCLUDE (SYNC_DATE)) AS HASH FROM CMP_PROD.ARCHIVE.CMP_AUDIT_LOG_HARD_DELETE_20260219) T USING(HASH) WHERE T.HASH IS NULL"#; + let sql = r#"SELECT * FROM (SELECT *, HASH(*) HASH FROM (SELECT CREATED_AT::TIMESTAMP_NTZ AS CREATED_AT, UPDATED_AT::TIMESTAMP_NTZ AS UPDATED_AT, * EXCLUDE(LOAD_DATE, CREATED_AT, UPDATED_AT) FROM mydb.public.events)) S FULL JOIN (SELECT *, HASH(* EXCLUDE(LOAD_DATE)) HASH FROM mydb.archive.events_20260101) T USING(HASH) WHERE T.HASH IS NULL"#; + let canonical = r#"SELECT * FROM (SELECT *, HASH(*) AS HASH FROM (SELECT CREATED_AT::TIMESTAMP_NTZ AS CREATED_AT, UPDATED_AT::TIMESTAMP_NTZ AS UPDATED_AT, * EXCLUDE (LOAD_DATE, CREATED_AT, UPDATED_AT) FROM mydb.public.events)) S FULL JOIN (SELECT *, HASH(* EXCLUDE (LOAD_DATE)) AS HASH FROM mydb.archive.events_20260101) T USING(HASH) WHERE T.HASH IS NULL"#; snowflake().verified_query_with_canonical(sql, canonical); } @@ -4911,8 +4911,8 @@ fn test_hash_star_exclude_simple() { // HASH(* EXCLUDE (col)) — wildcard with EXCLUDE in a function argument (Snowflake). // Canonical form has a space before the parenthesised column list. snowflake().one_statement_parses_to( - "SELECT HASH(* EXCLUDE(SYNC_DATE)) FROM t", - "SELECT HASH(* EXCLUDE (SYNC_DATE)) FROM t", + "SELECT HASH(* EXCLUDE(col1)) FROM t", + "SELECT HASH(* EXCLUDE (col1)) FROM t", ); - snowflake().verified_expr("HASH(* EXCLUDE (SYNC_DATE))"); + snowflake().verified_expr("HASH(* EXCLUDE (col1))"); } From cb34af3902fdfd0c18d40699a313dce091f8c239 Mon Sep 17 00:00:00 2001 From: Yoa Bot Date: Mon, 23 Feb 2026 10:37:03 +0100 Subject: [PATCH 3/5] Address review: remove redundant test, merge simple tests into test_wildcard_func_arg in sqlparser_common.rs --- tests/sqlparser_common.rs | 19 +++++++++++++++++++ tests/sqlparser_snowflake.rs | 26 -------------------------- 2 files changed, 19 insertions(+), 26 deletions(-) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index a3b5404d3..265e009ba 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -18563,3 +18563,22 @@ fn parse_array_subscript() { dialects.verified_stmt("SELECT arr[1][2]"); dialects.verified_stmt("SELECT arr[:][:]"); } + +#[test] +fn test_wildcard_func_arg() { + // Wildcard (*) and wildcard with EXCLUDE as a function argument. + // Documented for Snowflake's HASH function but parsed for any dialect that + // supports the wildcard-EXCLUDE select syntax. + let dialects = all_dialects_where(|d| d.supports_select_wildcard_exclude()); + + // Plain wildcard argument: HASH(*) + dialects.verified_expr("HASH(*)"); + + // Wildcard with EXCLUDE — canonical form has a space before the parenthesised column list. + dialects.one_statement_parses_to( + "SELECT HASH(* EXCLUDE(col1)) FROM t", + "SELECT HASH(* EXCLUDE (col1)) FROM t", + ); + dialects.verified_expr("HASH(* EXCLUDE (col1))"); + dialects.verified_expr("HASH(* EXCLUDE (col1, col2))"); +} diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 4a234f674..43444016f 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -4890,29 +4890,3 @@ fn test_select_dollar_column_from_stage() { // With table function args, without alias snowflake().verified_stmt("SELECT $1, $2 FROM @mystage1(file_format => 'myformat')"); } - -#[test] -fn test_hash_wildcard_exclude() { - // The canonical form normalizes `EXCLUDE(cols)` → `EXCLUDE (cols)` (space before parens), - // matching how WildcardAdditionalOptions / ExcludeSelectItem::Multiple is displayed. - let sql = r#"SELECT * FROM (SELECT *, HASH(*) HASH FROM (SELECT CREATED_AT::TIMESTAMP_NTZ AS CREATED_AT, UPDATED_AT::TIMESTAMP_NTZ AS UPDATED_AT, * EXCLUDE(LOAD_DATE, CREATED_AT, UPDATED_AT) FROM mydb.public.events)) S FULL JOIN (SELECT *, HASH(* EXCLUDE(LOAD_DATE)) HASH FROM mydb.archive.events_20260101) T USING(HASH) WHERE T.HASH IS NULL"#; - let canonical = r#"SELECT * FROM (SELECT *, HASH(*) AS HASH FROM (SELECT CREATED_AT::TIMESTAMP_NTZ AS CREATED_AT, UPDATED_AT::TIMESTAMP_NTZ AS UPDATED_AT, * EXCLUDE (LOAD_DATE, CREATED_AT, UPDATED_AT) FROM mydb.public.events)) S FULL JOIN (SELECT *, HASH(* EXCLUDE (LOAD_DATE)) AS HASH FROM mydb.archive.events_20260101) T USING(HASH) WHERE T.HASH IS NULL"#; - snowflake().verified_query_with_canonical(sql, canonical); -} - -#[test] -fn test_hash_star_simple() { - // HASH(*) — wildcard as a function argument. - snowflake().verified_expr("HASH(*)"); -} - -#[test] -fn test_hash_star_exclude_simple() { - // HASH(* EXCLUDE (col)) — wildcard with EXCLUDE in a function argument (Snowflake). - // Canonical form has a space before the parenthesised column list. - snowflake().one_statement_parses_to( - "SELECT HASH(* EXCLUDE(col1)) FROM t", - "SELECT HASH(* EXCLUDE (col1)) FROM t", - ); - snowflake().verified_expr("HASH(* EXCLUDE (col1))"); -} From cfdd82b682fbe2ac1aa9c3e4c8a1bfb59e7df3b8 Mon Sep 17 00:00:00 2001 From: Yoa Bot Date: Mon, 23 Feb 2026 11:43:12 +0100 Subject: [PATCH 4/5] Remove test_wildcard_func_arg from sqlparser_common.rs --- tests/sqlparser_common.rs | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 265e009ba..a3b5404d3 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -18563,22 +18563,3 @@ fn parse_array_subscript() { dialects.verified_stmt("SELECT arr[1][2]"); dialects.verified_stmt("SELECT arr[:][:]"); } - -#[test] -fn test_wildcard_func_arg() { - // Wildcard (*) and wildcard with EXCLUDE as a function argument. - // Documented for Snowflake's HASH function but parsed for any dialect that - // supports the wildcard-EXCLUDE select syntax. - let dialects = all_dialects_where(|d| d.supports_select_wildcard_exclude()); - - // Plain wildcard argument: HASH(*) - dialects.verified_expr("HASH(*)"); - - // Wildcard with EXCLUDE — canonical form has a space before the parenthesised column list. - dialects.one_statement_parses_to( - "SELECT HASH(* EXCLUDE(col1)) FROM t", - "SELECT HASH(* EXCLUDE (col1)) FROM t", - ); - dialects.verified_expr("HASH(* EXCLUDE (col1))"); - dialects.verified_expr("HASH(* EXCLUDE (col1, col2))"); -} From ce91e5352bd7632e43e4aa2aa829af1711dca3d6 Mon Sep 17 00:00:00 2001 From: Yoa Bot Date: Mon, 23 Feb 2026 11:45:27 +0100 Subject: [PATCH 5/5] Restore test_wildcard_func_arg, remove only the HASH(*) assertion --- tests/sqlparser_common.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index a3b5404d3..64e972f29 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -18563,3 +18563,19 @@ fn parse_array_subscript() { dialects.verified_stmt("SELECT arr[1][2]"); dialects.verified_stmt("SELECT arr[:][:]"); } + +#[test] +fn test_wildcard_func_arg() { + // Wildcard (*) and wildcard with EXCLUDE as a function argument. + // Documented for Snowflake's HASH function but parsed for any dialect that + // supports the wildcard-EXCLUDE select syntax. + let dialects = all_dialects_where(|d| d.supports_select_wildcard_exclude()); + + // Wildcard with EXCLUDE — canonical form has a space before the parenthesised column list. + dialects.one_statement_parses_to( + "SELECT HASH(* EXCLUDE(col1)) FROM t", + "SELECT HASH(* EXCLUDE (col1)) FROM t", + ); + dialects.verified_expr("HASH(* EXCLUDE (col1))"); + dialects.verified_expr("HASH(* EXCLUDE (col1, col2))"); +}