Skip to content

Commit

Permalink
BigQuery: Parse optional DELETE FROM statement
Browse files Browse the repository at this point in the history
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
  • Loading branch information
iffyio committed Jan 31, 2024
1 parent bcecd85 commit d8f501c
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 27 deletions.
28 changes: 26 additions & 2 deletions src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<TableWithJoins>),
/// BigQuery: `FROM` keyword was omitted.
/// <https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax#delete_statement>
WithoutKeyword(Vec<TableWithJoins>),
}

/// A top-level statement (SELECT, INSERT, CREATE, etc.)
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
Expand Down Expand Up @@ -1599,7 +1616,7 @@ pub enum Statement {
/// Multi tables delete are supported in mysql
tables: Vec<ObjectName>,
/// FROM
from: Vec<TableWithJoins>,
from: FromTable,
/// USING (Snowflake, Postgres, MySQL)
using: Option<Vec<TableWithJoins>>,
/// WHERE
Expand Down Expand Up @@ -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))?;
}
Expand Down
22 changes: 16 additions & 6 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6324,12 +6324,18 @@ impl<'a> Parser<'a> {
}

pub fn parse_delete(&mut self) -> Result<Statement, ParserError> {
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)?;
Expand Down Expand Up @@ -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,
Expand Down
40 changes: 27 additions & 13 deletions src/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<F>(except: F) -> TestedDialects
where
F: Fn(&dyn Dialect) -> bool,
{
let all_dialects = vec![
Box::new(GenericDialect {}) as Box<dyn Dialect>,
Box::new(PostgreSqlDialect {}) as Box<dyn Dialect>,
Box::new(MsSqlDialect {}) as Box<dyn Dialect>,
Box::new(AnsiDialect {}) as Box<dyn Dialect>,
Box::new(SnowflakeDialect {}) as Box<dyn Dialect>,
Box::new(HiveDialect {}) as Box<dyn Dialect>,
Box::new(RedshiftSqlDialect {}) as Box<dyn Dialect>,
Box::new(MySqlDialect {}) as Box<dyn Dialect>,
Box::new(BigQueryDialect {}) as Box<dyn Dialect>,
Box::new(SQLiteDialect {}) as Box<dyn Dialect>,
Box::new(DuckDbDialect {}) as Box<dyn Dialect>,
];
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,
}
}
Expand Down
24 changes: 24 additions & 0 deletions tests/sqlparser_bigquery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!(
Expand Down
33 changes: 27 additions & 6 deletions tests/sqlparser_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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")]),
Expand All @@ -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::<BigQueryDialect>() || d.is::<GenericDialect>());
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::<BigQueryDialect>() || d.is::<GenericDialect>());
match dialects.verified_stmt(sql) {
Statement::Delete {
tables,
from: FromTable::WithFromKeyword(from),
..
} => {
assert_eq!(
ObjectName(vec![Ident::new("schema1"), Ident::new("table1")]),
tables[0]
Expand Down Expand Up @@ -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),
..
} => {
Expand Down Expand Up @@ -646,7 +667,7 @@ fn parse_where_delete_statement() {
match verified_stmt(sql) {
Statement::Delete {
tables: _,
from,
from: FromTable::WithFromKeyword(from),
using,
selection,
returning,
Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit d8f501c

Please sign in to comment.