From 26f26802dc0aa8d548d0047b9acb8081d772c46f Mon Sep 17 00:00:00 2001 From: Etgar Perets Date: Mon, 21 Jul 2025 10:38:46 +0300 Subject: [PATCH 1/3] SGA-11419 Added snowflake ability for if not exists after create view, also added ability to write view name before if not exists in snowflake as it is implemented, replaced dialect of with trait functions --- src/ast/mod.rs | 16 +++++++++++++--- src/ast/spans.rs | 1 + src/dialect/bigquery.rs | 5 +++++ src/dialect/generic.rs | 8 ++++++++ src/dialect/mod.rs | 12 ++++++++++++ src/dialect/snowflake.rs | 10 ++++++++++ src/dialect/sqlite.rs | 5 +++++ src/parser/mod.rs | 27 +++++++++++++++++++++++---- tests/sqlparser_common.rs | 25 +++++++++++++++++++++++++ 9 files changed, 102 insertions(+), 7 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 7b401606ed..df2889daad 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3263,6 +3263,8 @@ pub enum Statement { materialized: bool, /// View name name: ObjectName, + // Name IF NOT EXIST instead of IF NOT EXIST name + name_before_not_exists: bool, columns: Vec, query: Box, options: CreateTableOptions, @@ -4987,6 +4989,7 @@ impl fmt::Display for Statement { temporary, to, params, + name_before_not_exists, } => { write!( f, @@ -4999,11 +5002,18 @@ impl fmt::Display for Statement { } write!( f, - "{materialized}{temporary}VIEW {if_not_exists}{name}{to}", + "{materialized}{temporary}VIEW {if_not_and_name}{to}", + if_not_and_name = if *if_not_exists { + if *name_before_not_exists { + format!("{name} IF NOT EXISTS") + } else { + format!("IF NOT EXISTS {name}") + } + } else { + format!("{name}") + }, materialized = if *materialized { "MATERIALIZED " } else { "" }, - name = name, temporary = if *temporary { "TEMPORARY " } else { "" }, - if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }, to = to .as_ref() .map(|to| format!(" TO {to}")) diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 91523925e2..f5558356fa 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -400,6 +400,7 @@ impl Spanned for Statement { if_not_exists: _, temporary: _, to, + name_before_not_exists: _, params: _, } => union_spans( core::iter::once(name.span()) diff --git a/src/dialect/bigquery.rs b/src/dialect/bigquery.rs index 27fd3cca3b..2adcfb1bd3 100644 --- a/src/dialect/bigquery.rs +++ b/src/dialect/bigquery.rs @@ -116,6 +116,11 @@ impl Dialect for BigQueryDialect { true } + // See https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#:~:text=CREATE%20%5B%20OR%20REPLACE%20%5D%20VIEW%20%5B%20IF%20NOT%20EXISTS%20%5D + fn create_view_if_not_exists_supported(&self) -> bool { + true + } + fn require_interval_qualifier(&self) -> bool { true } diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index be2cc00761..ac5a6ea93b 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -88,6 +88,14 @@ impl Dialect for GenericDialect { true } + fn create_view_if_not_exists_supported(&self) -> bool { + true + } + + fn create_view_name_before_if_not_exists_supported(&self) -> bool { + true + } + fn support_map_literal_syntax(&self) -> bool { true } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index c78b000337..2bf60bfe16 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -247,6 +247,18 @@ pub trait Dialect: Debug + Any { false } + /// Does the dialect support sql statements such as: + /// CREATE VIEW IF NOT EXISTS view_name AS SELECT * FROM table_name + fn create_view_if_not_exists_supported(&self) -> bool { + false + } + + /// Does the dialect support view_name before IF NOT EXISTS in CREATE VIEW: + /// CREATE VIEW IF NOT EXISTS view_name AS SELECT * FROM table_name + fn create_view_name_before_if_not_exists_supported(&self) -> bool { + false + } + /// Returns true if the dialect supports referencing another named window /// within a window clause declaration. /// diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 26432b3f00..c0c61ceb61 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -285,6 +285,16 @@ impl Dialect for SnowflakeDialect { true } + // See https://docs.snowflake.com/en/sql-reference/sql/create-view + fn create_view_if_not_exists_supported(&self) -> bool { + true + } + + // Snowflake allows table name before if not exists in CREATE VIEW + fn create_view_name_before_if_not_exists_supported(&self) -> bool { + true + } + fn supports_left_associative_joins_without_parens(&self) -> bool { false } diff --git a/src/dialect/sqlite.rs b/src/dialect/sqlite.rs index 64a8d532f5..7fc9993039 100644 --- a/src/dialect/sqlite.rs +++ b/src/dialect/sqlite.rs @@ -57,6 +57,11 @@ impl Dialect for SQLiteDialect { true } + // See https://www.sqlite.org/lang_createview.html + fn create_view_if_not_exists_supported(&self) -> bool { + true + } + fn supports_start_transaction_modifier(&self) -> bool { true } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index d35d7880f8..a0e6bbd6e4 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5768,12 +5768,30 @@ impl<'a> Parser<'a> { ) -> Result { let materialized = self.parse_keyword(Keyword::MATERIALIZED); self.expect_keyword_is(Keyword::VIEW)?; - let if_not_exists = dialect_of!(self is BigQueryDialect|SQLiteDialect|GenericDialect) - && self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); + let allow_unquoted_hyphen = dialect_of!(self is BigQueryDialect); + let mut if_not_exists = false; + let name: ObjectName; + let mut name_before_not_exists = false; + if self.peek_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]) { + // Possible syntax -> ... IF NOT EXISTS + if self.dialect.create_view_if_not_exists_supported() { + if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); + } + name = self.parse_object_name(allow_unquoted_hyphen)?; + } else { + // Possible syntax -> ... IF NOT EXISTS + name = self.parse_object_name(allow_unquoted_hyphen)?; + if self + .dialect + .create_view_name_before_if_not_exists_supported() + && self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]) + { + if_not_exists = true; + name_before_not_exists = true; + } + } // 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 allow_unquoted_hyphen = dialect_of!(self is BigQueryDialect); - let name = self.parse_object_name(allow_unquoted_hyphen)?; let columns = self.parse_view_columns()?; let mut options = CreateTableOptions::None; let with_options = self.parse_options(Keyword::WITH)?; @@ -5840,6 +5858,7 @@ impl<'a> Parser<'a> { temporary, to, params: create_view_params, + name_before_not_exists, }) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index e6548d3e04..28168e72df 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -8040,6 +8040,7 @@ fn parse_create_view() { temporary, to, params, + name_before_not_exists: _, } => { assert_eq!(or_alter, false); assert_eq!("myschema.myview", name.to_string()); @@ -8108,6 +8109,7 @@ fn parse_create_view_with_columns() { temporary, to, params, + name_before_not_exists: _, } => { assert_eq!(or_alter, false); assert_eq!("v", name.to_string()); @@ -8157,6 +8159,7 @@ fn parse_create_view_temporary() { temporary, to, params, + name_before_not_exists: _, } => { assert_eq!(or_alter, false); assert_eq!("myschema.myview", name.to_string()); @@ -8196,6 +8199,7 @@ fn parse_create_or_replace_view() { temporary, to, params, + name_before_not_exists: _, } => { assert_eq!(or_alter, false); assert_eq!("v", name.to_string()); @@ -8239,6 +8243,7 @@ fn parse_create_or_replace_materialized_view() { temporary, to, params, + name_before_not_exists: _, } => { assert_eq!(or_alter, false); assert_eq!("v", name.to_string()); @@ -8278,6 +8283,7 @@ fn parse_create_materialized_view() { temporary, to, params, + name_before_not_exists: _, } => { assert_eq!(or_alter, false); assert_eq!("myschema.myview", name.to_string()); @@ -8317,6 +8323,7 @@ fn parse_create_materialized_view_with_cluster_by() { temporary, to, params, + name_before_not_exists: _, } => { assert_eq!(or_alter, false); assert_eq!("myschema.myview", name.to_string()); @@ -16377,3 +16384,21 @@ fn parse_drop_stream() { } verified_stmt("DROP STREAM IF EXISTS s1"); } + +#[test] +fn parse_create_view_if_not_exists() { + let sql = "CREATE VIEW IF NOT EXISTS v AS SELECT 1"; + let dialects = TestedDialects::new(vec![ + Box::new(SnowflakeDialect {}), + Box::new(GenericDialect {}), + Box::new(SQLiteDialect {}), + Box::new(BigQueryDialect {}), + ]); + let _ = dialects.verified_stmt(sql); + let sql = "CREATE VIEW v IF NOT EXISTS AS SELECT 1"; + let dialects = TestedDialects::new(vec![ + Box::new(SnowflakeDialect {}), + Box::new(GenericDialect {}), + ]); + let _ = dialects.verified_stmt(sql); +} From 46ce27fdba60b855322d817f32eff7a5d5903c6e Mon Sep 17 00:00:00 2001 From: Etgar Perets Date: Sun, 27 Jul 2025 10:54:53 +0300 Subject: [PATCH 2/3] SGA-11419 Skipping dialect methods and adding a comment on name if not exists supported by snowflake --- src/dialect/bigquery.rs | 5 ----- src/dialect/generic.rs | 8 -------- src/dialect/mod.rs | 12 ------------ src/dialect/snowflake.rs | 10 ---------- src/dialect/sqlite.rs | 5 ----- src/parser/mod.rs | 11 +++-------- tests/sqlparser_common.rs | 16 +++------------- 7 files changed, 6 insertions(+), 61 deletions(-) diff --git a/src/dialect/bigquery.rs b/src/dialect/bigquery.rs index 2adcfb1bd3..27fd3cca3b 100644 --- a/src/dialect/bigquery.rs +++ b/src/dialect/bigquery.rs @@ -116,11 +116,6 @@ impl Dialect for BigQueryDialect { true } - // See https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#:~:text=CREATE%20%5B%20OR%20REPLACE%20%5D%20VIEW%20%5B%20IF%20NOT%20EXISTS%20%5D - fn create_view_if_not_exists_supported(&self) -> bool { - true - } - fn require_interval_qualifier(&self) -> bool { true } diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index ac5a6ea93b..be2cc00761 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -88,14 +88,6 @@ impl Dialect for GenericDialect { true } - fn create_view_if_not_exists_supported(&self) -> bool { - true - } - - fn create_view_name_before_if_not_exists_supported(&self) -> bool { - true - } - fn support_map_literal_syntax(&self) -> bool { true } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 2bf60bfe16..c78b000337 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -247,18 +247,6 @@ pub trait Dialect: Debug + Any { false } - /// Does the dialect support sql statements such as: - /// CREATE VIEW IF NOT EXISTS view_name AS SELECT * FROM table_name - fn create_view_if_not_exists_supported(&self) -> bool { - false - } - - /// Does the dialect support view_name before IF NOT EXISTS in CREATE VIEW: - /// CREATE VIEW IF NOT EXISTS view_name AS SELECT * FROM table_name - fn create_view_name_before_if_not_exists_supported(&self) -> bool { - false - } - /// Returns true if the dialect supports referencing another named window /// within a window clause declaration. /// diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index c0c61ceb61..26432b3f00 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -285,16 +285,6 @@ impl Dialect for SnowflakeDialect { true } - // See https://docs.snowflake.com/en/sql-reference/sql/create-view - fn create_view_if_not_exists_supported(&self) -> bool { - true - } - - // Snowflake allows table name before if not exists in CREATE VIEW - fn create_view_name_before_if_not_exists_supported(&self) -> bool { - true - } - fn supports_left_associative_joins_without_parens(&self) -> bool { false } diff --git a/src/dialect/sqlite.rs b/src/dialect/sqlite.rs index 7fc9993039..64a8d532f5 100644 --- a/src/dialect/sqlite.rs +++ b/src/dialect/sqlite.rs @@ -57,11 +57,6 @@ impl Dialect for SQLiteDialect { true } - // See https://www.sqlite.org/lang_createview.html - fn create_view_if_not_exists_supported(&self) -> bool { - true - } - fn supports_start_transaction_modifier(&self) -> bool { true } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a0e6bbd6e4..1bd5a9d028 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5774,18 +5774,13 @@ impl<'a> Parser<'a> { let mut name_before_not_exists = false; if self.peek_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]) { // Possible syntax -> ... IF NOT EXISTS - if self.dialect.create_view_if_not_exists_supported() { - if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); - } + if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); name = self.parse_object_name(allow_unquoted_hyphen)?; } else { // Possible syntax -> ... IF NOT EXISTS + // Supported by snowflake but is undocumented name = self.parse_object_name(allow_unquoted_hyphen)?; - if self - .dialect - .create_view_name_before_if_not_exists_supported() - && self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]) - { + if self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]) { if_not_exists = true; name_before_not_exists = true; } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 28168e72df..6b2fa1a202 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -16387,18 +16387,8 @@ fn parse_drop_stream() { #[test] fn parse_create_view_if_not_exists() { - let sql = "CREATE VIEW IF NOT EXISTS v AS SELECT 1"; - let dialects = TestedDialects::new(vec![ - Box::new(SnowflakeDialect {}), - Box::new(GenericDialect {}), - Box::new(SQLiteDialect {}), - Box::new(BigQueryDialect {}), - ]); - let _ = dialects.verified_stmt(sql); + let sql: &'static str = "CREATE VIEW IF NOT EXISTS v AS SELECT 1"; + let _ = all_dialects().verified_stmt(sql); let sql = "CREATE VIEW v IF NOT EXISTS AS SELECT 1"; - let dialects = TestedDialects::new(vec![ - Box::new(SnowflakeDialect {}), - Box::new(GenericDialect {}), - ]); - let _ = dialects.verified_stmt(sql); + let _ = all_dialects().verified_stmt(sql); } From ce9864dc3bcd77ff6d39941acec8e805a90fbb49 Mon Sep 17 00:00:00 2001 From: Etgar Perets Date: Tue, 29 Jul 2025 14:39:02 +0300 Subject: [PATCH 3/3] SGA-11419 improved comments, replaced if else flow, added assert to test --- src/ast/mod.rs | 11 ++++++++++- src/parser/mod.rs | 24 ++++++++---------------- tests/sqlparser_common.rs | 9 +++++++++ 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index df2889daad..10f9643ac9 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3263,7 +3263,16 @@ pub enum Statement { materialized: bool, /// View name name: ObjectName, - // Name IF NOT EXIST instead of IF NOT EXIST name + /// If `if_not_exists` is true, this flag is set to true if the view name comes before the `IF NOT EXISTS` clause. + /// Example: + /// ```sql + /// CREATE VIEW myview IF NOT EXISTS AS SELECT 1` + /// ``` + /// Otherwise, the flag is set to false if the view name comes after the clause + /// Example: + /// ```sql + /// CREATE VIEW IF NOT EXISTS myview AS SELECT 1` + /// ``` name_before_not_exists: bool, columns: Vec, query: Box, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 1bd5a9d028..cfa98025b0 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5769,22 +5769,14 @@ impl<'a> Parser<'a> { let materialized = self.parse_keyword(Keyword::MATERIALIZED); self.expect_keyword_is(Keyword::VIEW)?; let allow_unquoted_hyphen = dialect_of!(self is BigQueryDialect); - let mut if_not_exists = false; - let name: ObjectName; - let mut name_before_not_exists = false; - if self.peek_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]) { - // Possible syntax -> ... IF NOT EXISTS - if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); - name = self.parse_object_name(allow_unquoted_hyphen)?; - } else { - // Possible syntax -> ... IF NOT EXISTS - // Supported by snowflake but is undocumented - name = self.parse_object_name(allow_unquoted_hyphen)?; - if self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]) { - if_not_exists = true; - name_before_not_exists = true; - } - } + // Tries to parse IF NOT EXISTS either before name or after name + // Name before IF NOT EXISTS is supported by snowflake but undocumented + let if_not_exists_first = + self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); + let name = self.parse_object_name(allow_unquoted_hyphen)?; + 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; // 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()?; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 6b2fa1a202..71d6dc4502 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -16387,8 +16387,17 @@ fn parse_drop_stream() { #[test] fn parse_create_view_if_not_exists() { + // Name after IF NOT EXISTS let sql: &'static str = "CREATE VIEW IF NOT EXISTS v AS SELECT 1"; let _ = all_dialects().verified_stmt(sql); + // Name before IF NOT EXISTS let sql = "CREATE VIEW v IF NOT EXISTS AS SELECT 1"; let _ = all_dialects().verified_stmt(sql); + // Name missing from query + let sql = "CREATE VIEW IF NOT EXISTS AS SELECT 1"; + let res = all_dialects().parse_sql_statements(sql); + assert_eq!( + ParserError::ParserError("Expected: AS, found: SELECT".to_string()), + res.unwrap_err() + ); }