Skip to content

Commit 46f28fa

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 cad4923 commit 46f28fa

File tree

6 files changed

+196
-33
lines changed

6 files changed

+196
-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: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@
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};
22+
use crate::tokenizer::Token;
2123

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

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

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

6574
fn is_identifier_part(&self, ch: char) -> bool {
@@ -119,3 +128,64 @@ impl Dialect for BigQueryDialect {
119128
!RESERVED_FOR_COLUMN_ALIAS.contains(kw)
120129
}
121130
}
131+
132+
impl BigQueryDialect {
133+
fn maybe_parse_statement(&self, parser: &mut Parser) -> Option<Result<Statement, ParserError>> {
134+
if parser.peek_keyword(Keyword::BEGIN) {
135+
return Some(self.parse_begin(parser));
136+
}
137+
None
138+
}
139+
140+
/// Parse a `BEGIN` statement.
141+
/// <https://cloud.google.com/bigquery/docs/reference/standard-sql/procedural-language#beginexceptionend>
142+
fn parse_begin(&self, parser: &mut Parser) -> Result<Statement, ParserError> {
143+
parser.expect_keyword(Keyword::BEGIN)?;
144+
145+
let statements = self.parse_statement_list(parser, &[Keyword::EXCEPTION, Keyword::END])?;
146+
147+
let has_exception_when_clause = parser.parse_keywords(&[
148+
Keyword::EXCEPTION,
149+
Keyword::WHEN,
150+
Keyword::ERROR,
151+
Keyword::THEN,
152+
]);
153+
let mut exception_statements = vec![];
154+
if has_exception_when_clause && !parser.peek_keyword(Keyword::END) {
155+
exception_statements = self.parse_statement_list(parser, &[Keyword::END])?;
156+
}
157+
158+
parser.expect_keyword(Keyword::END)?;
159+
160+
Ok(Statement::StartTransaction {
161+
begin: true,
162+
statements,
163+
exception_statements,
164+
has_exception_when_clause,
165+
has_end_keyword: true,
166+
transaction: None,
167+
modifier: None,
168+
modes: vec![],
169+
})
170+
}
171+
172+
/// Parses 0 or more statements, each followed by a semicolon.
173+
fn parse_statement_list(
174+
&self,
175+
parser: &mut Parser,
176+
terminal_keywords: &[Keyword],
177+
) -> Result<Vec<Statement>, ParserError> {
178+
let mut values = vec![];
179+
loop {
180+
if let Token::Word(w) = &parser.peek_nth_token_ref(0).token {
181+
if w.quote_style.is_none() && terminal_keywords.contains(&w.keyword) {
182+
break;
183+
}
184+
}
185+
186+
values.push(parser.parse_statement()?);
187+
parser.expect_token(&Token::SemiColon)?;
188+
}
189+
Ok(values)
190+
}
191+
}

src/parser/mod.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13524,6 +13524,10 @@ impl<'a> Parser<'a> {
1352413524
begin: false,
1352513525
transaction: Some(BeginTransactionKind::Transaction),
1352613526
modifier: None,
13527+
statements: vec![],
13528+
exception_statements: vec![],
13529+
has_exception_when_clause: false,
13530+
has_end_keyword: false,
1352713531
})
1352813532
}
1352913533

@@ -13553,6 +13557,10 @@ impl<'a> Parser<'a> {
1355313557
begin: true,
1355413558
transaction,
1355513559
modifier,
13560+
statements: vec![],
13561+
exception_statements: vec![],
13562+
has_exception_when_clause: false,
13563+
has_end_keyword: false,
1355613564
})
1355713565
}
1355813566

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
@@ -8028,7 +8028,12 @@ fn lateral_function() {
80288028

80298029
#[test]
80308030
fn parse_start_transaction() {
8031-
match verified_stmt("START TRANSACTION READ ONLY, READ WRITE, ISOLATION LEVEL SERIALIZABLE") {
8031+
let dialects = all_dialects_except(|d|
8032+
// BigQuery does not support this syntax
8033+
d.is::<BigQueryDialect>());
8034+
match dialects
8035+
.verified_stmt("START TRANSACTION READ ONLY, READ WRITE, ISOLATION LEVEL SERIALIZABLE")
8036+
{
80328037
Statement::StartTransaction { modes, .. } => assert_eq!(
80338038
modes,
80348039
vec![
@@ -8042,7 +8047,7 @@ fn parse_start_transaction() {
80428047

80438048
// For historical reasons, PostgreSQL allows the commas between the modes to
80448049
// be omitted.
8045-
match one_statement_parses_to(
8050+
match dialects.one_statement_parses_to(
80468051
"START TRANSACTION READ ONLY READ WRITE ISOLATION LEVEL SERIALIZABLE",
80478052
"START TRANSACTION READ ONLY, READ WRITE, ISOLATION LEVEL SERIALIZABLE",
80488053
) {
@@ -8057,40 +8062,40 @@ fn parse_start_transaction() {
80578062
_ => unreachable!(),
80588063
}
80598064

8060-
verified_stmt("START TRANSACTION");
8061-
verified_stmt("BEGIN");
8062-
verified_stmt("BEGIN WORK");
8063-
verified_stmt("BEGIN TRANSACTION");
8065+
dialects.verified_stmt("START TRANSACTION");
8066+
dialects.verified_stmt("BEGIN");
8067+
dialects.verified_stmt("BEGIN WORK");
8068+
dialects.verified_stmt("BEGIN TRANSACTION");
80648069

8065-
verified_stmt("START TRANSACTION ISOLATION LEVEL READ UNCOMMITTED");
8066-
verified_stmt("START TRANSACTION ISOLATION LEVEL READ COMMITTED");
8067-
verified_stmt("START TRANSACTION ISOLATION LEVEL REPEATABLE READ");
8068-
verified_stmt("START TRANSACTION ISOLATION LEVEL SERIALIZABLE");
8070+
dialects.verified_stmt("START TRANSACTION ISOLATION LEVEL READ UNCOMMITTED");
8071+
dialects.verified_stmt("START TRANSACTION ISOLATION LEVEL READ COMMITTED");
8072+
dialects.verified_stmt("START TRANSACTION ISOLATION LEVEL REPEATABLE READ");
8073+
dialects.verified_stmt("START TRANSACTION ISOLATION LEVEL SERIALIZABLE");
80698074

80708075
// Regression test for https://github.com/sqlparser-rs/sqlparser-rs/pull/139,
80718076
// in which START TRANSACTION would fail to parse if followed by a statement
80728077
// terminator.
80738078
assert_eq!(
8074-
parse_sql_statements("START TRANSACTION; SELECT 1"),
8079+
dialects.parse_sql_statements("START TRANSACTION; SELECT 1"),
80758080
Ok(vec![
80768081
verified_stmt("START TRANSACTION"),
80778082
verified_stmt("SELECT 1"),
80788083
])
80798084
);
80808085

8081-
let res = parse_sql_statements("START TRANSACTION ISOLATION LEVEL BAD");
8086+
let res = dialects.parse_sql_statements("START TRANSACTION ISOLATION LEVEL BAD");
80828087
assert_eq!(
80838088
ParserError::ParserError("Expected: isolation level, found: BAD".to_string()),
80848089
res.unwrap_err()
80858090
);
80868091

8087-
let res = parse_sql_statements("START TRANSACTION BAD");
8092+
let res = dialects.parse_sql_statements("START TRANSACTION BAD");
80888093
assert_eq!(
80898094
ParserError::ParserError("Expected: end of statement, found: BAD".to_string()),
80908095
res.unwrap_err()
80918096
);
80928097

8093-
let res = parse_sql_statements("START TRANSACTION READ ONLY,");
8098+
let res = dialects.parse_sql_statements("START TRANSACTION READ ONLY,");
80948099
assert_eq!(
80958100
ParserError::ParserError("Expected: transaction mode, found: EOF".to_string()),
80968101
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)