Skip to content

Commit f018574

Browse files
committed
BigQuery: Add support for BEGIN
Adds support for the `BEGIN ... EXCEPTION ... END` syntax in BigQuery: ```sql BEGIN SELECT 1; SELECT 2; EXCEPTION WHEN ERROR THEN SELECT 3; SELECT 4; END ``` https://cloud.google.com/bigquery/docs/reference/standard-sql/procedural-language#beginexceptionend
1 parent 46cfcfe commit f018574

File tree

6 files changed

+198
-33
lines changed

6 files changed

+198
-33
lines changed

src/ast/mod.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3058,6 +3058,33 @@ pub enum Statement {
30583058
begin: bool,
30593059
transaction: Option<BeginTransactionKind>,
30603060
modifier: Option<TransactionModifier>,
3061+
/// List of statements belonging to the `BEGIN` block.
3062+
/// Example:
3063+
/// ```sql
3064+
/// BEGIN
3065+
/// SELECT 1;
3066+
/// SELECT 2;
3067+
/// END;
3068+
/// ```
3069+
statements: Vec<Statement>,
3070+
/// TRUE if the statement has a
3071+
/// `EXCEPTION WHEN ERROR THEN` clause
3072+
/// Example:
3073+
/// ```sql
3074+
/// BEGIN
3075+
/// SELECT 1;
3076+
/// SELECT 2;
3077+
/// EXCEPTION WHEN ERROR THEN
3078+
/// SELECT 3;
3079+
/// END;
3080+
/// ```
3081+
/// <https://cloud.google.com/bigquery/docs/reference/standard-sql/procedural-language#beginexceptionend>
3082+
has_exception_when_clause: bool,
3083+
/// Statements of an exception clause.
3084+
/// <https://cloud.google.com/bigquery/docs/reference/standard-sql/procedural-language#beginexceptionend>
3085+
exception_statements: Vec<Statement>,
3086+
/// TRUE if the statement has an `END` keyword.
3087+
has_end_keyword: bool,
30613088
},
30623089
/// ```sql
30633090
/// SET TRANSACTION ...
@@ -4772,6 +4799,10 @@ impl fmt::Display for Statement {
47724799
begin: syntax_begin,
47734800
transaction,
47744801
modifier,
4802+
statements,
4803+
exception_statements,
4804+
has_exception_when_clause,
4805+
has_end_keyword,
47754806
} => {
47764807
if *syntax_begin {
47774808
if let Some(modifier) = *modifier {
@@ -4788,6 +4819,24 @@ impl fmt::Display for Statement {
47884819
if !modes.is_empty() {
47894820
write!(f, " {}", display_comma_separated(modes))?;
47904821
}
4822+
if !statements.is_empty() {
4823+
write!(f, " {}", display_separated(statements, "; "))?;
4824+
// We manually insert semicolon for the last statement,
4825+
// since display_separated doesn't handle that case.
4826+
write!(f, ";")?;
4827+
}
4828+
if *has_exception_when_clause {
4829+
write!(f, " EXCEPTION WHEN ERROR THEN")?;
4830+
}
4831+
if !exception_statements.is_empty() {
4832+
write!(f, " {}", display_separated(exception_statements, "; "))?;
4833+
// We manually insert semicolon for the last statement,
4834+
// since display_separated doesn't handle that case.
4835+
write!(f, ";")?;
4836+
}
4837+
if *has_end_keyword {
4838+
write!(f, " END")?;
4839+
}
47914840
Ok(())
47924841
}
47934842
Statement::SetTransaction {

src/dialect/bigquery.rs

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@
1515
// specific language governing permissions and limitations
1616
// under the License.
1717

18+
use crate::ast::Statement;
1819
use crate::dialect::Dialect;
1920
use crate::keywords::Keyword;
20-
use crate::parser::Parser;
21+
use crate::parser::{Parser, ParserError};
2122

2223
/// These keywords are disallowed as column identifiers. Such that
2324
/// `SELECT 5 AS <col> FROM T` is rejected by BigQuery.
@@ -44,7 +45,11 @@ const RESERVED_FOR_COLUMN_ALIAS: &[Keyword] = &[
4445
pub struct BigQueryDialect;
4546

4647
impl Dialect for BigQueryDialect {
47-
// See https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#identifiers
48+
fn parse_statement(&self, parser: &mut Parser) -> Option<Result<Statement, ParserError>> {
49+
self.maybe_parse_statement(parser)
50+
}
51+
52+
/// See <https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#identifiers>
4853
fn is_delimited_identifier_start(&self, ch: char) -> bool {
4954
ch == '`'
5055
}
@@ -60,6 +65,9 @@ impl Dialect for BigQueryDialect {
6065

6166
fn is_identifier_start(&self, ch: char) -> bool {
6267
ch.is_ascii_lowercase() || ch.is_ascii_uppercase() || ch == '_'
68+
// BigQuery supports `@@foo.bar` variable syntax in its procedural language.
69+
// https://cloud.google.com/bigquery/docs/reference/standard-sql/procedural-language#beginexceptionend
70+
|| ch == '@'
6371
}
6472

6573
fn is_identifier_part(&self, ch: char) -> bool {
@@ -119,3 +127,46 @@ impl Dialect for BigQueryDialect {
119127
!RESERVED_FOR_COLUMN_ALIAS.contains(kw)
120128
}
121129
}
130+
131+
impl BigQueryDialect {
132+
fn maybe_parse_statement(&self, parser: &mut Parser) -> Option<Result<Statement, ParserError>> {
133+
if parser.peek_keyword(Keyword::BEGIN) {
134+
return Some(self.parse_begin(parser));
135+
}
136+
None
137+
}
138+
139+
/// Parse a `BEGIN` statement.
140+
/// <https://cloud.google.com/bigquery/docs/reference/standard-sql/procedural-language#beginexceptionend>
141+
fn parse_begin(&self, parser: &mut Parser) -> Result<Statement, ParserError> {
142+
parser.expect_keyword(Keyword::BEGIN)?;
143+
144+
let statements = parser.parse_statement_list(&[Keyword::EXCEPTION, Keyword::END])?;
145+
146+
let has_exception_when_clause = parser.parse_keywords(&[
147+
Keyword::EXCEPTION,
148+
Keyword::WHEN,
149+
Keyword::ERROR,
150+
Keyword::THEN,
151+
]);
152+
let exception_statements =
153+
if has_exception_when_clause && !parser.peek_keyword(Keyword::END) {
154+
parser.parse_statement_list(&[Keyword::END])?
155+
} else {
156+
Default::default()
157+
};
158+
159+
parser.expect_keyword(Keyword::END)?;
160+
161+
Ok(Statement::StartTransaction {
162+
begin: true,
163+
statements,
164+
exception_statements,
165+
has_exception_when_clause,
166+
has_end_keyword: true,
167+
transaction: None,
168+
modifier: None,
169+
modes: Default::default(),
170+
})
171+
}
172+
}

src/parser/mod.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4140,6 +4140,27 @@ impl<'a> Parser<'a> {
41404140
self.parse_comma_separated(f)
41414141
}
41424142

4143+
/// Parses 0 or more statements, each followed by a semicolon.
4144+
/// If the next token is any of `terminal_keywords` then no more
4145+
/// statements will be parsed.
4146+
pub(crate) fn parse_statement_list(
4147+
&mut self,
4148+
terminal_keywords: &[Keyword],
4149+
) -> Result<Vec<Statement>, ParserError> {
4150+
let mut values = vec![];
4151+
loop {
4152+
if let Token::Word(w) = &self.peek_nth_token_ref(0).token {
4153+
if w.quote_style.is_none() && terminal_keywords.contains(&w.keyword) {
4154+
break;
4155+
}
4156+
}
4157+
4158+
values.push(self.parse_statement()?);
4159+
self.expect_token(&Token::SemiColon)?;
4160+
}
4161+
Ok(values)
4162+
}
4163+
41434164
/// Default implementation of a predicate that returns true if
41444165
/// the specified keyword is reserved for column alias.
41454166
/// See [Dialect::is_column_alias]
@@ -13566,6 +13587,10 @@ impl<'a> Parser<'a> {
1356613587
begin: false,
1356713588
transaction: Some(BeginTransactionKind::Transaction),
1356813589
modifier: None,
13590+
statements: vec![],
13591+
exception_statements: vec![],
13592+
has_exception_when_clause: false,
13593+
has_end_keyword: false,
1356913594
})
1357013595
}
1357113596

@@ -13595,6 +13620,10 @@ impl<'a> Parser<'a> {
1359513620
begin: true,
1359613621
transaction,
1359713622
modifier,
13623+
statements: vec![],
13624+
exception_statements: vec![],
13625+
has_exception_when_clause: false,
13626+
has_end_keyword: false,
1359813627
})
1359913628
}
1360013629

tests/sqlparser_bigquery.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,54 @@ fn parse_big_query_non_reserved_column_alias() {
236236
bigquery().verified_stmt(sql);
237237
}
238238

239+
#[test]
240+
fn parse_at_at_identifier() {
241+
bigquery().verified_stmt("SELECT @@error.stack_trace, @@error.message");
242+
}
243+
244+
#[test]
245+
fn parse_begin() {
246+
let sql = r#"BEGIN SELECT 1; EXCEPTION WHEN ERROR THEN SELECT 2; END"#;
247+
let Statement::StartTransaction {
248+
statements,
249+
exception_statements,
250+
has_exception_when_clause,
251+
has_end_keyword,
252+
..
253+
} = bigquery().verified_stmt(sql)
254+
else {
255+
unreachable!();
256+
};
257+
assert_eq!(1, statements.len());
258+
assert_eq!(1, exception_statements.len());
259+
assert!(has_exception_when_clause);
260+
assert!(has_end_keyword);
261+
262+
bigquery().verified_stmt(
263+
"BEGIN SELECT 1; SELECT 2; EXCEPTION WHEN ERROR THEN SELECT 2; SELECT 4; END",
264+
);
265+
bigquery()
266+
.verified_stmt("BEGIN SELECT 1; EXCEPTION WHEN ERROR THEN SELECT @@error.stack_trace; END");
267+
bigquery().verified_stmt("BEGIN EXCEPTION WHEN ERROR THEN SELECT 2; END");
268+
bigquery().verified_stmt("BEGIN SELECT 1; SELECT 2; EXCEPTION WHEN ERROR THEN END");
269+
bigquery().verified_stmt("BEGIN EXCEPTION WHEN ERROR THEN END");
270+
bigquery().verified_stmt("BEGIN SELECT 1; SELECT 2; END");
271+
bigquery().verified_stmt("BEGIN END");
272+
273+
assert_eq!(
274+
bigquery()
275+
.parse_sql_statements("BEGIN SELECT 1; SELECT 2 END")
276+
.unwrap_err(),
277+
ParserError::ParserError("Expected: ;, found: END".to_string())
278+
);
279+
assert_eq!(
280+
bigquery()
281+
.parse_sql_statements("BEGIN SELECT 1; EXCEPTION WHEN ERROR THEN SELECT 2 END")
282+
.unwrap_err(),
283+
ParserError::ParserError("Expected: ;, found: END".to_string())
284+
);
285+
}
286+
239287
#[test]
240288
fn parse_delete_statement() {
241289
let sql = "DELETE \"table\" WHERE 1";

tests/sqlparser_common.rs

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8032,7 +8032,12 @@ fn lateral_function() {
80328032

80338033
#[test]
80348034
fn parse_start_transaction() {
8035-
match verified_stmt("START TRANSACTION READ ONLY, READ WRITE, ISOLATION LEVEL SERIALIZABLE") {
8035+
let dialects = all_dialects_except(|d|
8036+
// BigQuery does not support this syntax
8037+
d.is::<BigQueryDialect>());
8038+
match dialects
8039+
.verified_stmt("START TRANSACTION READ ONLY, READ WRITE, ISOLATION LEVEL SERIALIZABLE")
8040+
{
80368041
Statement::StartTransaction { modes, .. } => assert_eq!(
80378042
modes,
80388043
vec![
@@ -8046,7 +8051,7 @@ fn parse_start_transaction() {
80468051

80478052
// For historical reasons, PostgreSQL allows the commas between the modes to
80488053
// be omitted.
8049-
match one_statement_parses_to(
8054+
match dialects.one_statement_parses_to(
80508055
"START TRANSACTION READ ONLY READ WRITE ISOLATION LEVEL SERIALIZABLE",
80518056
"START TRANSACTION READ ONLY, READ WRITE, ISOLATION LEVEL SERIALIZABLE",
80528057
) {
@@ -8061,40 +8066,40 @@ fn parse_start_transaction() {
80618066
_ => unreachable!(),
80628067
}
80638068

8064-
verified_stmt("START TRANSACTION");
8065-
verified_stmt("BEGIN");
8066-
verified_stmt("BEGIN WORK");
8067-
verified_stmt("BEGIN TRANSACTION");
8069+
dialects.verified_stmt("START TRANSACTION");
8070+
dialects.verified_stmt("BEGIN");
8071+
dialects.verified_stmt("BEGIN WORK");
8072+
dialects.verified_stmt("BEGIN TRANSACTION");
80688073

8069-
verified_stmt("START TRANSACTION ISOLATION LEVEL READ UNCOMMITTED");
8070-
verified_stmt("START TRANSACTION ISOLATION LEVEL READ COMMITTED");
8071-
verified_stmt("START TRANSACTION ISOLATION LEVEL REPEATABLE READ");
8072-
verified_stmt("START TRANSACTION ISOLATION LEVEL SERIALIZABLE");
8074+
dialects.verified_stmt("START TRANSACTION ISOLATION LEVEL READ UNCOMMITTED");
8075+
dialects.verified_stmt("START TRANSACTION ISOLATION LEVEL READ COMMITTED");
8076+
dialects.verified_stmt("START TRANSACTION ISOLATION LEVEL REPEATABLE READ");
8077+
dialects.verified_stmt("START TRANSACTION ISOLATION LEVEL SERIALIZABLE");
80738078

80748079
// Regression test for https://github.com/sqlparser-rs/sqlparser-rs/pull/139,
80758080
// in which START TRANSACTION would fail to parse if followed by a statement
80768081
// terminator.
80778082
assert_eq!(
8078-
parse_sql_statements("START TRANSACTION; SELECT 1"),
8083+
dialects.parse_sql_statements("START TRANSACTION; SELECT 1"),
80798084
Ok(vec![
80808085
verified_stmt("START TRANSACTION"),
80818086
verified_stmt("SELECT 1"),
80828087
])
80838088
);
80848089

8085-
let res = parse_sql_statements("START TRANSACTION ISOLATION LEVEL BAD");
8090+
let res = dialects.parse_sql_statements("START TRANSACTION ISOLATION LEVEL BAD");
80868091
assert_eq!(
80878092
ParserError::ParserError("Expected: isolation level, found: BAD".to_string()),
80888093
res.unwrap_err()
80898094
);
80908095

8091-
let res = parse_sql_statements("START TRANSACTION BAD");
8096+
let res = dialects.parse_sql_statements("START TRANSACTION BAD");
80928097
assert_eq!(
80938098
ParserError::ParserError("Expected: end of statement, found: BAD".to_string()),
80948099
res.unwrap_err()
80958100
);
80968101

8097-
let res = parse_sql_statements("START TRANSACTION READ ONLY,");
8102+
let res = dialects.parse_sql_statements("START TRANSACTION READ ONLY,");
80988103
assert_eq!(
80998104
ParserError::ParserError("Expected: transaction mode, found: EOF".to_string()),
81008105
res.unwrap_err()

tests/sqlparser_sqlite.rs

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -522,23 +522,6 @@ fn parse_start_transaction_with_modifier() {
522522
sqlite_and_generic().verified_stmt("BEGIN DEFERRED");
523523
sqlite_and_generic().verified_stmt("BEGIN IMMEDIATE");
524524
sqlite_and_generic().verified_stmt("BEGIN EXCLUSIVE");
525-
526-
let unsupported_dialects = all_dialects_except(|d| d.supports_start_transaction_modifier());
527-
let res = unsupported_dialects.parse_sql_statements("BEGIN DEFERRED");
528-
assert_eq!(
529-
ParserError::ParserError("Expected: end of statement, found: DEFERRED".to_string()),
530-
res.unwrap_err(),
531-
);
532-
let res = unsupported_dialects.parse_sql_statements("BEGIN IMMEDIATE");
533-
assert_eq!(
534-
ParserError::ParserError("Expected: end of statement, found: IMMEDIATE".to_string()),
535-
res.unwrap_err(),
536-
);
537-
let res = unsupported_dialects.parse_sql_statements("BEGIN EXCLUSIVE");
538-
assert_eq!(
539-
ParserError::ParserError("Expected: end of statement, found: EXCLUSIVE".to_string()),
540-
res.unwrap_err(),
541-
);
542525
}
543526

544527
#[test]

0 commit comments

Comments
 (0)