Skip to content

Snowflake: Support CREATE VIEW myview IF NOT EXISTS #1961

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3263,6 +3263,17 @@ pub enum Statement {
materialized: bool,
/// View name
name: ObjectName,
/// 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<ViewColumnDef>,
query: Box<Query>,
options: CreateTableOptions,
Expand Down Expand Up @@ -4987,6 +4998,7 @@ impl fmt::Display for Statement {
temporary,
to,
params,
name_before_not_exists,
} => {
write!(
f,
Expand All @@ -4999,11 +5011,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}"))
Expand Down
1 change: 1 addition & 0 deletions src/ast/spans.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
12 changes: 9 additions & 3 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5768,12 +5768,17 @@ impl<'a> Parser<'a> {
) -> Result<Statement, ParserError> {
let materialized = self.parse_keyword(Keyword::MATERIALIZED);
self.expect_keyword_is(Keyword::VIEW)?;
let if_not_exists = dialect_of!(self is BigQueryDialect|SQLiteDialect|GenericDialect)
let allow_unquoted_hyphen = dialect_of!(self is BigQueryDialect);
// 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 allow_unquoted_hyphen = dialect_of!(self is BigQueryDialect);
let name = self.parse_object_name(allow_unquoted_hyphen)?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it work to only put something like this line after we've parsed? name

let name_before_not_exists = !if_not_exists && self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]);
let if_not_exists = if_not_exists || name_before_not_exists;

thinking if so that would let us avoid the if/else and mut

let columns = self.parse_view_columns()?;
let mut options = CreateTableOptions::None;
let with_options = self.parse_options(Keyword::WITH)?;
Expand Down Expand Up @@ -5840,6 +5845,7 @@ impl<'a> Parser<'a> {
temporary,
to,
params: create_view_params,
name_before_not_exists,
})
}

Expand Down
24 changes: 24 additions & 0 deletions tests/sqlparser_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -16377,3 +16384,20 @@ fn parse_drop_stream() {
}
verified_stmt("DROP STREAM IF EXISTS s1");
}

#[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";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From a quick look I couldn't find which dialects in practice support this syntax? Do you have a link to documentation for one such dialect?

related the MR not super sure it would be necessary to have this behavior covered by a flag and the parser can always look to accept CREATE VIEW IF NOT EXISTS v, as well as the other syntax if its relevant)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, I couldn't find official documentation for this syntax, though we have confirmed it works in practice on Snowflake.

I'm happy to remove the dialect flag and update the parser to support both forms for all dialects. Let me know if you'd like me to proceed with that change.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yeah in that case I think we can remove the dialect flags, not sure that we need to support both forms if CREATE VIEW v IF NOT EXISTS is not supported by any dialect

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's supported in snowflake, but not officially documented.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah alright, in that case yeah I think we could skip the dialect methods entirely and let the parser accept either format if they show up? Also we can add a comment flagging that the second format is supported by snowflake but is undocumented

let _ = all_dialects().verified_stmt(sql);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a test case for CREATE VIEW IF NOT EXISTS AS SELECT 1 to verify how the parser behaves in that scenario?

// 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()
);
}