Merge pull request #106 from offscale/transactions

Transaction support
This commit is contained in:
Nikhil Benesch 2019-06-09 01:35:05 -04:00 committed by GitHub
commit 5536cd1f9e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 301 additions and 0 deletions

View file

@ -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"

View file

@ -416,6 +416,14 @@ pub enum SQLStatement {
names: Vec<SQLObjectName>,
cascade: bool,
},
/// { BEGIN [ TRANSACTION | WORK ] | START TRANSACTION } ...
SQLStartTransaction { modes: Vec<TransactionMode> },
/// SET TRANSACTION ...
SQLSetTransaction { modes: Vec<TransactionMode> },
/// 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(),
}
}
}

View file

@ -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<SQLStatement, ParserError> {
self.expect_keyword("TRANSACTION")?;
Ok(SQLStatement::SQLStartTransaction {
modes: self.parse_transaction_modes()?,
})
}
pub fn parse_begin(&mut self) -> Result<SQLStatement, ParserError> {
let _ = self.parse_one_of_keywords(&["TRANSACTION", "WORK"]);
Ok(SQLStatement::SQLStartTransaction {
modes: self.parse_transaction_modes()?,
})
}
pub fn parse_set_transaction(&mut self) -> Result<SQLStatement, ParserError> {
self.expect_keyword("TRANSACTION")?;
Ok(SQLStatement::SQLSetTransaction {
modes: self.parse_transaction_modes()?,
})
}
pub fn parse_transaction_modes(&mut self) -> Result<Vec<TransactionMode>, 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<SQLStatement, ParserError> {
Ok(SQLStatement::SQLCommit {
chain: self.parse_commit_rollback_chain()?,
})
}
pub fn parse_rollback(&mut self) -> Result<SQLStatement, ParserError> {
Ok(SQLStatement::SQLRollback {
chain: self.parse_commit_rollback_chain()?,
})
}
pub fn parse_commit_rollback_chain(&mut self) -> Result<bool, ParserError> {
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 {

View file

@ -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"