From d8f501c7ae4ba0c0a6519c10615a45e2c1ef882c Mon Sep 17 00:00:00 2001 From: ifeanyi Date: Wed, 31 Jan 2024 20:07:37 +0100 Subject: [PATCH] BigQuery: Parse optional `DELETE FROM` statement The `FROM` keyword is optional is a `DELETE` statement for [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax#delete_statement) This adds parser support to handle such statements --- src/ast/mod.rs | 28 ++++++++++++++++++++++++-- src/parser/mod.rs | 22 ++++++++++++++------ src/test_utils.rs | 40 +++++++++++++++++++++++++------------ tests/sqlparser_bigquery.rs | 24 ++++++++++++++++++++++ tests/sqlparser_common.rs | 33 ++++++++++++++++++++++++------ 5 files changed, 120 insertions(+), 27 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index a2c28c810..7da1bfbd3 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1436,6 +1436,23 @@ impl fmt::Display for CreateTableOptions { } } +/// A `FROM` clause within a `DELETE` statement. +/// +/// Syntax +/// ```sql +/// [FROM] table +/// ``` +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum FromTable { + /// An explicit `FROM` keyword was specified. + WithFromKeyword(Vec), + /// BigQuery: `FROM` keyword was omitted. + /// + WithoutKeyword(Vec), +} + /// A top-level statement (SELECT, INSERT, CREATE, etc.) #[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] @@ -1599,7 +1616,7 @@ pub enum Statement { /// Multi tables delete are supported in mysql tables: Vec, /// FROM - from: Vec, + from: FromTable, /// USING (Snowflake, Postgres, MySQL) using: Option>, /// WHERE @@ -2692,7 +2709,14 @@ impl fmt::Display for Statement { if !tables.is_empty() { write!(f, "{} ", display_comma_separated(tables))?; } - write!(f, "FROM {}", display_comma_separated(from))?; + match from { + FromTable::WithFromKeyword(from) => { + write!(f, "FROM {}", display_comma_separated(from))?; + } + FromTable::WithoutKeyword(from) => { + write!(f, "{}", display_comma_separated(from))?; + } + } if let Some(using) = using { write!(f, " USING {}", display_comma_separated(using))?; } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 36ac2fd28..1e6695f45 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -6324,12 +6324,18 @@ impl<'a> Parser<'a> { } pub fn parse_delete(&mut self) -> Result { - let tables = if !self.parse_keyword(Keyword::FROM) { - let tables = self.parse_comma_separated(|p| p.parse_object_name(false))?; - self.expect_keyword(Keyword::FROM)?; - tables + let (tables, with_from_keyword) = if !self.parse_keyword(Keyword::FROM) { + // `FROM` keyword is optional in BigQuery SQL. + // https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax#delete_statement + if dialect_of!(self is BigQueryDialect | GenericDialect) { + (vec![], false) + } else { + let tables = self.parse_comma_separated(|p| p.parse_object_name(false))?; + self.expect_keyword(Keyword::FROM)?; + (tables, true) + } } else { - vec![] + (vec![], true) }; let from = self.parse_comma_separated(Parser::parse_table_and_joins)?; @@ -6361,7 +6367,11 @@ impl<'a> Parser<'a> { Ok(Statement::Delete { tables, - from, + from: if with_from_keyword { + FromTable::WithFromKeyword(from) + } else { + FromTable::WithoutKeyword(from) + }, using, selection, returning, diff --git a/src/test_utils.rs b/src/test_utils.rs index 26cfec463..e72403f65 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -193,21 +193,35 @@ impl TestedDialects { } } +/// Returns all available dialects. pub fn all_dialects() -> TestedDialects { + all_dialects_except(|_| false) +} + +/// Returns available dialects. The `except` predicate is used +/// to filter out specific dialects. +pub fn all_dialects_except(except: F) -> TestedDialects +where + F: Fn(&dyn Dialect) -> bool, +{ + let all_dialects = vec![ + Box::new(GenericDialect {}) as Box, + Box::new(PostgreSqlDialect {}) as Box, + Box::new(MsSqlDialect {}) as Box, + Box::new(AnsiDialect {}) as Box, + Box::new(SnowflakeDialect {}) as Box, + Box::new(HiveDialect {}) as Box, + Box::new(RedshiftSqlDialect {}) as Box, + Box::new(MySqlDialect {}) as Box, + Box::new(BigQueryDialect {}) as Box, + Box::new(SQLiteDialect {}) as Box, + Box::new(DuckDbDialect {}) as Box, + ]; TestedDialects { - dialects: vec![ - Box::new(GenericDialect {}), - Box::new(PostgreSqlDialect {}), - Box::new(MsSqlDialect {}), - Box::new(AnsiDialect {}), - Box::new(SnowflakeDialect {}), - Box::new(HiveDialect {}), - Box::new(RedshiftSqlDialect {}), - Box::new(MySqlDialect {}), - Box::new(BigQueryDialect {}), - Box::new(SQLiteDialect {}), - Box::new(DuckDbDialect {}), - ], + dialects: all_dialects + .into_iter() + .filter(|d| !except(d.as_ref())) + .collect(), options: None, } } diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index a2309a430..3080acec6 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -86,6 +86,30 @@ fn parse_raw_literal() { panic!("invalid query") } +#[test] +fn parse_delete_statement() { + let sql = "DELETE \"table\" WHERE 1"; + match bigquery_and_generic().verified_stmt(sql) { + Statement::Delete { + from: FromTable::WithoutKeyword(from), + .. + } => { + assert_eq!( + TableFactor::Table { + name: ObjectName(vec![Ident::with_quote('"', "table")]), + alias: None, + args: None, + with_hints: vec![], + version: None, + partitions: vec![], + }, + from[0].relation + ); + } + _ => unreachable!(), + } +} + #[test] fn parse_create_view_with_options() { let sql = concat!( diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index fcacb3459..09b03be46 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -42,6 +42,7 @@ mod test_utils; #[cfg(test)] use pretty_assertions::assert_eq; +use sqlparser::test_utils::all_dialects_except; #[test] fn parse_insert_values() { @@ -523,7 +524,10 @@ fn parse_no_table_name() { fn parse_delete_statement() { let sql = "DELETE FROM \"table\""; match verified_stmt(sql) { - Statement::Delete { from, .. } => { + Statement::Delete { + from: FromTable::WithFromKeyword(from), + .. + } => { assert_eq!( TableFactor::Table { name: ObjectName(vec![Ident::with_quote('"', "table")]), @@ -540,11 +544,28 @@ fn parse_delete_statement() { } } +#[test] +fn parse_delete_without_from_error() { + let sql = "DELETE \"table\" WHERE 1"; + + let dialects = all_dialects_except(|d| d.is::() || d.is::()); + let res = dialects.parse_sql_statements(sql); + assert_eq!( + ParserError::ParserError("Expected FROM, found: WHERE".to_string()), + res.unwrap_err() + ); +} + #[test] fn parse_delete_statement_for_multi_tables() { let sql = "DELETE schema1.table1, schema2.table2 FROM schema1.table1 JOIN schema2.table2 ON schema2.table2.col1 = schema1.table1.col1 WHERE schema2.table2.col2 = 1"; - match verified_stmt(sql) { - Statement::Delete { tables, from, .. } => { + let dialects = all_dialects_except(|d| d.is::() || d.is::()); + match dialects.verified_stmt(sql) { + Statement::Delete { + tables, + from: FromTable::WithFromKeyword(from), + .. + } => { assert_eq!( ObjectName(vec![Ident::new("schema1"), Ident::new("table1")]), tables[0] @@ -585,7 +606,7 @@ fn parse_delete_statement_for_multi_tables_with_using() { let sql = "DELETE FROM schema1.table1, schema2.table2 USING schema1.table1 JOIN schema2.table2 ON schema2.table2.pk = schema1.table1.col1 WHERE schema2.table2.col2 = 1"; match verified_stmt(sql) { Statement::Delete { - from, + from: FromTable::WithFromKeyword(from), using: Some(using), .. } => { @@ -646,7 +667,7 @@ fn parse_where_delete_statement() { match verified_stmt(sql) { Statement::Delete { tables: _, - from, + from: FromTable::WithFromKeyword(from), using, selection, returning, @@ -687,7 +708,7 @@ fn parse_where_delete_with_alias_statement() { match verified_stmt(sql) { Statement::Delete { tables: _, - from, + from: FromTable::WithFromKeyword(from), using, selection, returning,