From e1ce7c4b05f8a6ae083a2b7e4ab5dfca83acaa3f Mon Sep 17 00:00:00 2001 From: Corey Fritz Date: Sun, 26 Apr 2026 06:25:45 -0600 Subject: [PATCH 1/2] feat(snowflake): accept COPY GRANTS after CREATE VIEW column list Snowflake documents `COPY GRANTS` as appearing after the column list on `CREATE VIEW`. The parser already accepted it before the column list; also accept it after, normalizing Display to the pre-columns form. --- src/parser/mod.rs | 8 ++++++- tests/sqlparser_snowflake.rs | 46 ++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 668c520e5..612cc9be4 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -6533,10 +6533,16 @@ impl<'a> Parser<'a> { let name_before_not_exists = !if_not_exists_first && self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); let if_not_exists = if_not_exists_first || name_before_not_exists; - let copy_grants = self.parse_keywords(&[Keyword::COPY, Keyword::GRANTS]); + let mut copy_grants = self.parse_keywords(&[Keyword::COPY, Keyword::GRANTS]); // Many dialects support `OR ALTER` right after `CREATE`, but we don't (yet). // ANSI SQL and Postgres support RECURSIVE here, but we don't support it either. let columns = self.parse_view_columns()?; + // Snowflake also documents `COPY GRANTS` *after* the column list; accept + // either position, but not both. + // + if !copy_grants { + copy_grants = self.parse_keywords(&[Keyword::COPY, Keyword::GRANTS]); + } let mut options = CreateTableOptions::None; let with_options = self.parse_options(Keyword::WITH)?; if !with_options.is_empty() { diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 790bf1515..70a56a098 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -4764,6 +4764,52 @@ fn test_snowflake_create_view_copy_grants() { ); } +#[test] +fn test_snowflake_create_view_copy_grants_after_columns() { + // Snowflake's documented placement for `COPY GRANTS` on `CREATE VIEW` is + // *after* the column list. Display normalizes to the pre-columns form + // already supported, so use `one_statement_parses_to` to assert the + // post-columns input is accepted and the AST flag is set. + // + let cases = [ + ( + "CREATE OR REPLACE VIEW v (a, b) COPY GRANTS AS SELECT a, b FROM t", + "CREATE OR REPLACE VIEW v COPY GRANTS (a, b) AS SELECT a, b FROM t", + ), + ( + "CREATE OR REPLACE SECURE VIEW v (a, b) COPY GRANTS AS SELECT a, b FROM t", + "CREATE OR REPLACE SECURE VIEW v COPY GRANTS (a, b) AS SELECT a, b FROM t", + ), + ( + "CREATE MATERIALIZED VIEW v (a) COPY GRANTS AS SELECT a FROM t", + "CREATE MATERIALIZED VIEW v COPY GRANTS (a) AS SELECT a FROM t", + ), + ]; + for (sql, parsed) in cases { + match snowflake().one_statement_parses_to(sql, parsed) { + Statement::CreateView(CreateView { + name, + copy_grants, + columns, + .. + }) => { + assert_eq!("v", name.to_string()); + assert!(copy_grants, "copy_grants should be true for {sql:?}"); + assert!(!columns.is_empty(), "columns should be set for {sql:?}"); + } + _ => unreachable!(), + } + } + + // Baseline: the same query without COPY GRANTS must not flip the flag. + match snowflake().verified_stmt("CREATE OR REPLACE VIEW v (a) AS SELECT a FROM t") { + Statement::CreateView(CreateView { copy_grants, .. }) => { + assert!(!copy_grants); + } + _ => unreachable!(), + } +} + #[test] fn test_snowflake_identifier_function() { // Using IDENTIFIER to reference a column From fb32fd5a49b70874abcbf8a0caf21fcbd5de96b7 Mon Sep 17 00:00:00 2001 From: Corey Fritz Date: Wed, 29 Apr 2026 09:34:08 -0600 Subject: [PATCH 2/2] Drop AST asserts and header comment in COPY GRANTS test --- tests/sqlparser_snowflake.rs | 28 ++-------------------------- 1 file changed, 2 insertions(+), 26 deletions(-) diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 70a56a098..992129e4e 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -4766,11 +4766,6 @@ fn test_snowflake_create_view_copy_grants() { #[test] fn test_snowflake_create_view_copy_grants_after_columns() { - // Snowflake's documented placement for `COPY GRANTS` on `CREATE VIEW` is - // *after* the column list. Display normalizes to the pre-columns form - // already supported, so use `one_statement_parses_to` to assert the - // post-columns input is accepted and the AST flag is set. - // let cases = [ ( "CREATE OR REPLACE VIEW v (a, b) COPY GRANTS AS SELECT a, b FROM t", @@ -4786,28 +4781,9 @@ fn test_snowflake_create_view_copy_grants_after_columns() { ), ]; for (sql, parsed) in cases { - match snowflake().one_statement_parses_to(sql, parsed) { - Statement::CreateView(CreateView { - name, - copy_grants, - columns, - .. - }) => { - assert_eq!("v", name.to_string()); - assert!(copy_grants, "copy_grants should be true for {sql:?}"); - assert!(!columns.is_empty(), "columns should be set for {sql:?}"); - } - _ => unreachable!(), - } - } - - // Baseline: the same query without COPY GRANTS must not flip the flag. - match snowflake().verified_stmt("CREATE OR REPLACE VIEW v (a) AS SELECT a FROM t") { - Statement::CreateView(CreateView { copy_grants, .. }) => { - assert!(!copy_grants); - } - _ => unreachable!(), + snowflake().one_statement_parses_to(sql, parsed); } + snowflake().verified_stmt("CREATE OR REPLACE VIEW v (a) AS SELECT a FROM t"); } #[test]