diff --git a/src/dialect/keywords.rs b/src/dialect/keywords.rs index fb0873e1..8af3216e 100644 --- a/src/dialect/keywords.rs +++ b/src/dialect/keywords.rs @@ -77,6 +77,7 @@ define_keywords!( CAST, CEIL, CEILING, + CHAIN, CHAR, CHAR_LENGTH, CHARACTER, @@ -89,6 +90,7 @@ define_keywords!( COLLECT, COLUMN, COMMIT, + COMMITTED, CONDITION, CONNECT, CONSTRAINT, @@ -194,6 +196,7 @@ define_keywords!( INTERVAL, INTO, IS, + ISOLATION, JOIN, KEY, LAG, @@ -204,6 +207,7 @@ define_keywords!( LEAD, LEADING, LEFT, + LEVEL, LIKE, LIKE_REGEX, LIMIT, @@ -277,6 +281,7 @@ define_keywords!( PROCEDURE, RANGE, RANK, + READ, READS, REAL, RECURSIVE, @@ -294,6 +299,7 @@ define_keywords!( REGR_SXY, REGR_SYY, RELEASE, + REPEATABLE, RESTRICT, RESULT, RETURN, @@ -312,6 +318,7 @@ define_keywords!( SECOND, SELECT, SENSITIVE, + SERIALIZABLE, SESSION_USER, SET, SIMILAR, @@ -350,6 +357,7 @@ define_keywords!( TIMEZONE_MINUTE, TO, TRAILING, + TRANSACTION, TRANSLATE, TRANSLATE_REGEX, TRANSLATION, @@ -361,6 +369,7 @@ define_keywords!( TRUE, UESCAPE, UNBOUNDED, + UNCOMMITTED, UNION, UNIQUE, UNKNOWN, @@ -388,6 +397,8 @@ define_keywords!( WITH, WITHIN, WITHOUT, + WRITE, + WORK, YEAR, ZONE, END_EXEC = "END-EXEC" diff --git a/src/sqlast/mod.rs b/src/sqlast/mod.rs index c112a153..27003bc5 100644 --- a/src/sqlast/mod.rs +++ b/src/sqlast/mod.rs @@ -416,6 +416,14 @@ pub enum SQLStatement { names: Vec, cascade: bool, }, + /// { BEGIN [ TRANSACTION | WORK ] | START TRANSACTION } ... + SQLStartTransaction { modes: Vec }, + /// SET TRANSACTION ... + SQLSetTransaction { modes: Vec }, + /// COMMIT [ TRANSACTION | WORK ] [ AND [ NO ] CHAIN ] + SQLCommit { chain: bool }, + /// ROLLBACK [ TRANSACTION | WORK ] [ AND [ NO ] CHAIN ] + SQLRollback { chain: bool }, } impl ToString for SQLStatement { @@ -555,6 +563,28 @@ impl ToString for SQLStatement { comma_separated_string(names), if *cascade { " CASCADE" } else { "" }, ), + SQLStatement::SQLStartTransaction { modes } => format!( + "START TRANSACTION{}", + if modes.is_empty() { + "".into() + } else { + format!(" {}", comma_separated_string(modes)) + } + ), + SQLStatement::SQLSetTransaction { modes } => format!( + "SET TRANSACTION{}", + if modes.is_empty() { + "".into() + } else { + format!(" {}", comma_separated_string(modes)) + } + ), + SQLStatement::SQLCommit { chain } => { + format!("COMMIT{}", if *chain { " AND CHAIN" } else { "" },) + } + SQLStatement::SQLRollback { chain } => { + format!("ROLLBACK{}", if *chain { " AND CHAIN" } else { "" },) + } } } } @@ -706,3 +736,55 @@ impl ToString for SQLOption { format!("{} = {}", self.name.to_string(), self.value.to_string()) } } + +#[derive(Debug, Clone, PartialEq, Hash)] +pub enum TransactionMode { + AccessMode(TransactionAccessMode), + IsolationLevel(TransactionIsolationLevel), +} + +impl ToString for TransactionMode { + fn to_string(&self) -> String { + use TransactionMode::*; + match self { + AccessMode(access_mode) => access_mode.to_string(), + IsolationLevel(iso_level) => format!("ISOLATION LEVEL {}", iso_level.to_string()), + } + } +} + +#[derive(Debug, Clone, PartialEq, Hash)] +pub enum TransactionAccessMode { + ReadOnly, + ReadWrite, +} + +impl ToString for TransactionAccessMode { + fn to_string(&self) -> String { + use TransactionAccessMode::*; + match self { + ReadOnly => "READ ONLY".into(), + ReadWrite => "READ WRITE".into(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Hash)] +pub enum TransactionIsolationLevel { + ReadUncommitted, + ReadCommitted, + RepeatableRead, + Serializable, +} + +impl ToString for TransactionIsolationLevel { + fn to_string(&self) -> String { + use TransactionIsolationLevel::*; + match self { + ReadUncommitted => "READ UNCOMMITTED".into(), + ReadCommitted => "READ COMMITTED".into(), + RepeatableRead => "REPEATABLE READ".into(), + Serializable => "SERIALIZABLE".into(), + } + } +} diff --git a/src/sqlparser.rs b/src/sqlparser.rs index fd4d05ba..b887eef0 100644 --- a/src/sqlparser.rs +++ b/src/sqlparser.rs @@ -120,6 +120,14 @@ impl Parser { "UPDATE" => Ok(self.parse_update()?), "ALTER" => Ok(self.parse_alter()?), "COPY" => Ok(self.parse_copy()?), + "START" => Ok(self.parse_start_transaction()?), + "SET" => Ok(self.parse_set_transaction()?), + // `BEGIN` is a nonstandard but common alias for the + // standard `START TRANSACTION` statement. It is supported + // by at least PostgreSQL and MySQL. + "BEGIN" => Ok(self.parse_begin()?), + "COMMIT" => Ok(self.parse_commit()?), + "ROLLBACK" => Ok(self.parse_rollback()?), _ => parser_err!(format!( "Unexpected keyword {:?} at the beginning of a statement", w.to_string() @@ -1843,6 +1851,86 @@ impl Parser { } Ok(SQLValues(values)) } + + pub fn parse_start_transaction(&mut self) -> Result { + self.expect_keyword("TRANSACTION")?; + Ok(SQLStatement::SQLStartTransaction { + modes: self.parse_transaction_modes()?, + }) + } + + pub fn parse_begin(&mut self) -> Result { + let _ = self.parse_one_of_keywords(&["TRANSACTION", "WORK"]); + Ok(SQLStatement::SQLStartTransaction { + modes: self.parse_transaction_modes()?, + }) + } + + pub fn parse_set_transaction(&mut self) -> Result { + self.expect_keyword("TRANSACTION")?; + Ok(SQLStatement::SQLSetTransaction { + modes: self.parse_transaction_modes()?, + }) + } + + pub fn parse_transaction_modes(&mut self) -> Result, ParserError> { + let mut modes = vec![]; + let mut required = false; + loop { + let mode = if self.parse_keywords(vec!["ISOLATION", "LEVEL"]) { + let iso_level = if self.parse_keywords(vec!["READ", "UNCOMMITTED"]) { + TransactionIsolationLevel::ReadUncommitted + } else if self.parse_keywords(vec!["READ", "COMMITTED"]) { + TransactionIsolationLevel::ReadCommitted + } else if self.parse_keywords(vec!["REPEATABLE", "READ"]) { + TransactionIsolationLevel::RepeatableRead + } else if self.parse_keyword("SERIALIZABLE") { + TransactionIsolationLevel::Serializable + } else { + self.expected("isolation level", self.peek_token())? + }; + TransactionMode::IsolationLevel(iso_level) + } else if self.parse_keywords(vec!["READ", "ONLY"]) { + TransactionMode::AccessMode(TransactionAccessMode::ReadOnly) + } else if self.parse_keywords(vec!["READ", "WRITE"]) { + TransactionMode::AccessMode(TransactionAccessMode::ReadWrite) + } else if required || self.peek_token().is_some() { + self.expected("transaction mode", self.peek_token())? + } else { + break; + }; + modes.push(mode); + // ANSI requires a comma after each transaction mode, but + // PostgreSQL, for historical reasons, does not. We follow + // PostgreSQL in making the comma optional, since that is strictly + // more general. + required = self.consume_token(&Token::Comma); + } + Ok(modes) + } + + pub fn parse_commit(&mut self) -> Result { + Ok(SQLStatement::SQLCommit { + chain: self.parse_commit_rollback_chain()?, + }) + } + + pub fn parse_rollback(&mut self) -> Result { + Ok(SQLStatement::SQLRollback { + chain: self.parse_commit_rollback_chain()?, + }) + } + + pub fn parse_commit_rollback_chain(&mut self) -> Result { + let _ = self.parse_one_of_keywords(&["TRANSACTION", "WORK"]); + if self.parse_keyword("AND") { + let chain = !self.parse_keyword("NO"); + self.expect_keyword("CHAIN")?; + Ok(chain) + } else { + Ok(false) + } + } } impl SQLWord { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 5253139f..8998dca5 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -2199,6 +2199,126 @@ fn lateral_derived() { ); } +#[test] +fn parse_start_transaction() { + match verified_stmt("START TRANSACTION READ ONLY, READ WRITE, ISOLATION LEVEL SERIALIZABLE") { + SQLStatement::SQLStartTransaction { modes } => assert_eq!( + modes, + vec![ + TransactionMode::AccessMode(TransactionAccessMode::ReadOnly), + TransactionMode::AccessMode(TransactionAccessMode::ReadWrite), + TransactionMode::IsolationLevel(TransactionIsolationLevel::Serializable), + ] + ), + _ => unreachable!(), + } + + // For historical reasons, PostgreSQL allows the commas between the modes to + // be omitted. + match one_statement_parses_to( + "START TRANSACTION READ ONLY READ WRITE ISOLATION LEVEL SERIALIZABLE", + "START TRANSACTION READ ONLY, READ WRITE, ISOLATION LEVEL SERIALIZABLE", + ) { + SQLStatement::SQLStartTransaction { modes } => assert_eq!( + modes, + vec![ + TransactionMode::AccessMode(TransactionAccessMode::ReadOnly), + TransactionMode::AccessMode(TransactionAccessMode::ReadWrite), + TransactionMode::IsolationLevel(TransactionIsolationLevel::Serializable), + ] + ), + _ => unreachable!(), + } + + verified_stmt("START TRANSACTION"); + one_statement_parses_to("BEGIN", "START TRANSACTION"); + one_statement_parses_to("BEGIN WORK", "START TRANSACTION"); + one_statement_parses_to("BEGIN TRANSACTION", "START TRANSACTION"); + + verified_stmt("START TRANSACTION ISOLATION LEVEL READ UNCOMMITTED"); + verified_stmt("START TRANSACTION ISOLATION LEVEL READ COMMITTED"); + verified_stmt("START TRANSACTION ISOLATION LEVEL REPEATABLE READ"); + verified_stmt("START TRANSACTION ISOLATION LEVEL SERIALIZABLE"); + + let res = parse_sql_statements("START TRANSACTION ISOLATION LEVEL BAD"); + assert_eq!( + ParserError::ParserError("Expected isolation level, found: BAD".to_string()), + res.unwrap_err() + ); + + let res = parse_sql_statements("START TRANSACTION BAD"); + assert_eq!( + ParserError::ParserError("Expected transaction mode, found: BAD".to_string()), + res.unwrap_err() + ); + + let res = parse_sql_statements("START TRANSACTION READ ONLY,"); + assert_eq!( + ParserError::ParserError("Expected transaction mode, found: EOF".to_string()), + res.unwrap_err() + ); +} + +#[test] +fn parse_set_transaction() { + // SET TRANSACTION shares transaction mode parsing code with START + // TRANSACTION, so no need to duplicate the tests here. We just do a quick + // sanity check. + match verified_stmt("SET TRANSACTION READ ONLY, READ WRITE, ISOLATION LEVEL SERIALIZABLE") { + SQLStatement::SQLSetTransaction { modes } => assert_eq!( + modes, + vec![ + TransactionMode::AccessMode(TransactionAccessMode::ReadOnly), + TransactionMode::AccessMode(TransactionAccessMode::ReadWrite), + TransactionMode::IsolationLevel(TransactionIsolationLevel::Serializable), + ] + ), + _ => unreachable!(), + } +} + +#[test] +fn parse_commit() { + match verified_stmt("COMMIT") { + SQLStatement::SQLCommit { chain: false } => (), + _ => unreachable!(), + } + + match verified_stmt("COMMIT AND CHAIN") { + SQLStatement::SQLCommit { chain: true } => (), + _ => unreachable!(), + } + + one_statement_parses_to("COMMIT AND NO CHAIN", "COMMIT"); + one_statement_parses_to("COMMIT WORK AND NO CHAIN", "COMMIT"); + one_statement_parses_to("COMMIT TRANSACTION AND NO CHAIN", "COMMIT"); + one_statement_parses_to("COMMIT WORK AND CHAIN", "COMMIT AND CHAIN"); + one_statement_parses_to("COMMIT TRANSACTION AND CHAIN", "COMMIT AND CHAIN"); + one_statement_parses_to("COMMIT WORK", "COMMIT"); + one_statement_parses_to("COMMIT TRANSACTION", "COMMIT"); +} + +#[test] +fn parse_rollback() { + match verified_stmt("ROLLBACK") { + SQLStatement::SQLRollback { chain: false } => (), + _ => unreachable!(), + } + + match verified_stmt("ROLLBACK AND CHAIN") { + SQLStatement::SQLRollback { chain: true } => (), + _ => unreachable!(), + } + + one_statement_parses_to("ROLLBACK AND NO CHAIN", "ROLLBACK"); + one_statement_parses_to("ROLLBACK WORK AND NO CHAIN", "ROLLBACK"); + one_statement_parses_to("ROLLBACK TRANSACTION AND NO CHAIN", "ROLLBACK"); + one_statement_parses_to("ROLLBACK WORK AND CHAIN", "ROLLBACK AND CHAIN"); + one_statement_parses_to("ROLLBACK TRANSACTION AND CHAIN", "ROLLBACK AND CHAIN"); + one_statement_parses_to("ROLLBACK WORK", "ROLLBACK"); + one_statement_parses_to("ROLLBACK TRANSACTION", "ROLLBACK"); +} + #[test] #[should_panic( expected = "Parse results with GenericSqlDialect are different from PostgreSqlDialect"