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 7b401606e..df2889daa 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 91523925e..f5558356f 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 27fd3cca3..2adcfb1bd 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 be2cc0076..ac5a6ea93 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 c78b00033..2bf60bfe1 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 26432b3f0..c0c61ceb6 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 64a8d532f..7fc999303 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 d35d7880f..a0e6bbd6e 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 e6548d3e0..28168e72d 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 2adcfb1bd..27fd3cca3 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 ac5a6ea93..be2cc0076 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 2bf60bfe1..c78b00033 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 c0c61ceb6..26432b3f0 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 7fc999303..64a8d532f 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 a0e6bbd6e..1bd5a9d02 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 28168e72d..6b2fa1a20 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 df2889daa..10f9643ac 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 1bd5a9d02..cfa98025b 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 6b2fa1a20..71d6dc450 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() + ); }