Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
95 changes: 83 additions & 12 deletions src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2982,6 +2982,74 @@ impl From<Set> for Statement {
}
}

/// An exception representing exception handling with the `EXCEPTION` keyword.
///
/// Snowflake: <https://docs.snowflake.com/en/sql-reference/snowflake-scripting/exception>
/// BigQuery: <https://cloud.google.com/bigquery/docs/reference/standard-sql/procedural-language#beginexceptionend>
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
pub struct ExceptionClause {
/// When represents each `WHEN` case
pub when: Vec<ExceptionWhen>,
/// The exception that is being raised or the current exception being handled if `None`.
///
/// Example
/// RAISE;
/// RAISE MY_EXCEPTION;
/// RAISE USING MESSAGE = "Some error";
///
/// BigQuery: <https://cloud.google.com/bigquery/docs/reference/standard-sql/procedural-language#raise>
/// Snowflake: <https://docs.snowflake.com/en/sql-reference/snowflake-scripting/raise>
// pub raises: Option<Option<Ident>>,
pub raises: Option<Box<Statement>>,
}

impl Display for ExceptionClause {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, " EXCEPTION")?;
for w in &self.when {
write!(f, "{w}")?;
}

if let Some(raises) = &self.raises {
write!(f, " {raises};")?;
}

Ok(())
}
}

/// A representation of a `WHEN` arm with all the identifiers catched and the statements to execute
/// for the arm.
///
/// Snowflake: <https://docs.snowflake.com/en/sql-reference/snowflake-scripting/exception>
/// BigQuery: <https://cloud.google.com/bigquery/docs/reference/standard-sql/procedural-language#beginexceptionend>
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
pub struct ExceptionWhen {
pub idents: Vec<Ident>,
pub statements: Vec<Statement>,
}

impl Display for ExceptionWhen {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
" WHEN {idents} THEN",
idents = display_separated(&self.idents, " OR ")
)?;

if !self.statements.is_empty() {
write!(f, " ")?;
format_statement_list(f, &self.statements)?;
}

Ok(())
}
}

/// 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 @@ -3670,17 +3738,24 @@ pub enum Statement {
/// END;
/// ```
statements: Vec<Statement>,
/// Statements of an exception clause.
/// Exception handling with exception clauses and raises.
/// Example:
/// ```sql
/// BEGIN
/// SELECT 1;
/// EXCEPTION WHEN ERROR THEN
/// SELECT 2;
/// SELECT 3;
/// EXCEPTION
/// WHEN EXCEPTION_1 THEN
/// SELECT 2;
/// WHEN EXCEPTION_2 OR EXCEPTION_3 THEN
/// SELECT 3;
/// WHEN OTHER THEN
/// SELECT 4;
/// RAISE;
/// END;
/// ```
/// <https://cloud.google.com/bigquery/docs/reference/standard-sql/procedural-language#beginexceptionend>
exception_statements: Option<Vec<Statement>>,
/// <https://docs.snowflake.com/en/sql-reference/snowflake-scripting/exception>
exception: Option<ExceptionClause>,
/// TRUE if the statement has an `END` keyword.
has_end_keyword: bool,
},
Expand Down Expand Up @@ -5525,7 +5600,7 @@ impl fmt::Display for Statement {
transaction,
modifier,
statements,
exception_statements,
exception,
has_end_keyword,
} => {
if *syntax_begin {
Expand All @@ -5547,12 +5622,8 @@ impl fmt::Display for Statement {
write!(f, " ")?;
format_statement_list(f, statements)?;
}
if let Some(exception_statements) = exception_statements {
write!(f, " EXCEPTION WHEN ERROR THEN")?;
if !exception_statements.is_empty() {
write!(f, " ")?;
format_statement_list(f, exception_statements)?;
}
if let Some(exception) = exception {
write!(f, "{exception}")?;
}
if *has_end_keyword {
write!(f, " END")?;
Expand Down
51 changes: 5 additions & 46 deletions src/dialect/bigquery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,11 @@ pub struct BigQueryDialect;

impl Dialect for BigQueryDialect {
fn parse_statement(&self, parser: &mut Parser) -> Option<Result<Statement, ParserError>> {
self.maybe_parse_statement(parser)
if parser.parse_keyword(Keyword::BEGIN) {
return Some(parser.parse_begin_exception_end());
}

None
}

/// See <https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#identifiers>
Expand Down Expand Up @@ -141,48 +145,3 @@ impl Dialect for BigQueryDialect {
true
}
}

impl BigQueryDialect {
fn maybe_parse_statement(&self, parser: &mut Parser) -> Option<Result<Statement, ParserError>> {
if parser.peek_keyword(Keyword::BEGIN) {
return Some(self.parse_begin(parser));
}
None
}

/// Parse a `BEGIN` statement.
/// <https://cloud.google.com/bigquery/docs/reference/standard-sql/procedural-language#beginexceptionend>
fn parse_begin(&self, parser: &mut Parser) -> Result<Statement, ParserError> {
parser.expect_keyword(Keyword::BEGIN)?;

let statements = parser.parse_statement_list(&[Keyword::EXCEPTION, Keyword::END])?;

let has_exception_when_clause = parser.parse_keywords(&[
Keyword::EXCEPTION,
Keyword::WHEN,
Keyword::ERROR,
Keyword::THEN,
]);
let exception_statements = if has_exception_when_clause {
if !parser.peek_keyword(Keyword::END) {
Some(parser.parse_statement_list(&[Keyword::END])?)
} else {
Some(Default::default())
}
} else {
None
};

parser.expect_keyword(Keyword::END)?;

Ok(Statement::StartTransaction {
begin: true,
statements,
exception_statements,
has_end_keyword: true,
transaction: None,
modifier: None,
modes: Default::default(),
})
}
}
4 changes: 4 additions & 0 deletions src/dialect/snowflake.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,10 @@ impl Dialect for SnowflakeDialect {
}

fn parse_statement(&self, parser: &mut Parser) -> Option<Result<Statement, ParserError>> {
if parser.parse_keyword(Keyword::BEGIN) {
return Some(parser.parse_begin_exception_end());
}

if parser.parse_keywords(&[Keyword::ALTER, Keyword::SESSION]) {
// ALTER SESSION
let set = match parser.parse_one_of_keywords(&[Keyword::SET, Keyword::UNSET]) {
Expand Down
1 change: 1 addition & 0 deletions src/keywords.rs
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,7 @@ define_keywords!(
ORDER,
ORDINALITY,
ORGANIZATION,
OTHER,
OUT,
OUTER,
OUTPUT,
Expand Down
62 changes: 60 additions & 2 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15096,7 +15096,7 @@ impl<'a> Parser<'a> {
transaction: Some(BeginTransactionKind::Transaction),
modifier: None,
statements: vec![],
exception_statements: None,
exception: None,
has_end_keyword: false,
})
}
Expand Down Expand Up @@ -15128,11 +15128,69 @@ impl<'a> Parser<'a> {
transaction,
modifier,
statements: vec![],
exception_statements: None,
exception: None,
has_end_keyword: false,
})
}

pub fn parse_begin_exception_end(&mut self) -> Result<Statement, ParserError> {
let statements = self.parse_statement_list(&[Keyword::EXCEPTION, Keyword::END])?;

let exception = if self.parse_keyword(Keyword::EXCEPTION) {
let mut when = Vec::new();

// We can have multiple `WHEN` arms so we consume all cases until `END` or an exception
// is `RAISE`ed.
while self
.peek_one_of_keywords(&[Keyword::END, Keyword::RAISE])
.is_none()
{
self.expect_keyword(Keyword::WHEN)?;

// Each `WHEN` case can have one or more conditions, e.g.
// WHEN EXCEPTION_1 [OR EXCEPTION_2] THEN
// So we parse identifiers until the `THEN` keyword.
let mut idents = Vec::new();

while !self.parse_keyword(Keyword::THEN) {
let ident = self.parse_identifier()?;
idents.push(ident);

self.maybe_parse(|p| p.expect_keyword(Keyword::OR))?;
}

let statements =
self.parse_statement_list(&[Keyword::WHEN, Keyword::RAISE, Keyword::END])?;

when.push(ExceptionWhen { idents, statements });
}

let raises = if self.peek_keyword(Keyword::RAISE) {
let raises = Some(Box::new(self.parse_raise_stmt()?));
self.expect_token(&Token::SemiColon)?;
raises
} else {
None
};

Some(ExceptionClause { when, raises })
} else {
None
};

self.expect_keyword(Keyword::END)?;

Ok(Statement::StartTransaction {
begin: true,
statements,
exception,
has_end_keyword: true,
transaction: None,
modifier: None,
modes: Default::default(),
})
}

pub fn parse_end(&mut self) -> Result<Statement, ParserError> {
let modifier = if !self.dialect.supports_end_transaction_modifier() {
None
Expand Down
7 changes: 5 additions & 2 deletions tests/sqlparser_bigquery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -264,15 +264,18 @@ fn parse_begin() {
let sql = r#"BEGIN SELECT 1; EXCEPTION WHEN ERROR THEN SELECT 2; END"#;
let Statement::StartTransaction {
statements,
exception_statements,
exception,
has_end_keyword,
..
} = bigquery().verified_stmt(sql)
else {
unreachable!();
};
assert_eq!(1, statements.len());
assert_eq!(1, exception_statements.unwrap().len());
assert!(exception.is_some());

let exception = exception.unwrap();
assert_eq!(1, exception.when.len());
assert!(has_end_keyword);

bigquery().verified_stmt(
Expand Down
7 changes: 5 additions & 2 deletions tests/sqlparser_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8592,8 +8592,11 @@ fn lateral_function() {
#[test]
fn parse_start_transaction() {
let dialects = all_dialects_except(|d|
// BigQuery does not support this syntax
d.is::<BigQueryDialect>());
// BigQuery and Snowflake does not support this syntax
//
// BigQuery: <https://cloud.google.com/bigquery/docs/reference/standard-sql/procedural-language#begin_transaction>
// Snowflake: <https://docs.snowflake.com/en/sql-reference/sql/begin>
d.is::<BigQueryDialect>() || d.is::<SnowflakeDialect>());
match dialects
.verified_stmt("START TRANSACTION READ ONLY, READ WRITE, ISOLATION LEVEL SERIALIZABLE")
{
Expand Down
64 changes: 64 additions & 0 deletions tests/sqlparser_snowflake.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4066,3 +4066,67 @@ fn parse_connect_by_root_operator() {
"sql parser error: Expected an expression, found: FROM"
);
}

#[test]
fn test_begin_exception_end() {
for sql in [
"BEGIN SELECT 1; EXCEPTION WHEN OTHER THEN SELECT 2; RAISE; END",
"BEGIN SELECT 1; EXCEPTION WHEN OTHER THEN SELECT 2; RAISE EX_1; END",
"BEGIN SELECT 1; EXCEPTION WHEN FOO THEN SELECT 2; WHEN OTHER THEN SELECT 3; RAISE; END",
"BEGIN BEGIN SELECT 1; EXCEPTION WHEN OTHER THEN SELECT 2; RAISE; END; END",
] {
snowflake().verified_stmt(sql);
}

let sql = r#"
DECLARE
EXCEPTION_1 EXCEPTION (-20001, 'I caught the expected exception.');
EXCEPTION_2 EXCEPTION (-20002, 'Not the expected exception!');
EXCEPTION_3 EXCEPTION (-20003, 'The worst exception...');
BEGIN
BEGIN
SELECT 1;
EXCEPTION
WHEN EXCEPTION_1 THEN
SELECT 1;
WHEN EXCEPTION_2 OR EXCEPTION_3 THEN
SELECT 2;
SELECT 3;
WHEN OTHER THEN
SELECT 4;
RAISE;
END;
END
"#;

// Outer `BEGIN` of the two nested `BEGIN` statements.
let Statement::StartTransaction { mut statements, .. } = snowflake()
.parse_sql_statements(sql)
.unwrap()
.pop()
.unwrap()
else {
unreachable!();
};

// Inner `BEGIN` of the two nested `BEGIN` statements.
let Statement::StartTransaction {
statements,
exception,
has_end_keyword,
..
} = statements.pop().unwrap()
else {
unreachable!();
};

assert_eq!(1, statements.len());
assert!(has_end_keyword);

let exception = exception.unwrap();
assert_eq!(3, exception.when.len());
assert_eq!(1, exception.when[0].idents.len());
assert_eq!(1, exception.when[0].statements.len());
assert_eq!(2, exception.when[1].idents.len());
assert_eq!(2, exception.when[1].statements.len());
}