diff --git a/fuzz/fuzz_seeds/postgres_2505_deferrable_transaction.sql b/fuzz/fuzz_seeds/postgres_2505_deferrable_transaction.sql new file mode 100644 index 0000000000..f9677fd432 --- /dev/null +++ b/fuzz/fuzz_seeds/postgres_2505_deferrable_transaction.sql @@ -0,0 +1,5 @@ +START TRANSACTION ISOLATION LEVEL SERIALIZABLE, READ ONLY, DEFERRABLE; +BEGIN TRANSACTION NOT DEFERRABLE; +START TRANSACTION NOT DEFERRABLE READ WRITE; +SET TRANSACTION DEFERRABLE; +SET SESSION CHARACTERISTICS AS TRANSACTION NOT DEFERRABLE; diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 8e798524c9..e341dd0388 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -9312,12 +9312,14 @@ impl fmt::Display for AttachDuckDBDatabaseOption { #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -/// Mode for transactions: access mode or isolation level. +/// Mode for transactions: access mode, isolation level or deferrability. pub enum TransactionMode { /// Access mode for a transaction (e.g. `READ ONLY` / `READ WRITE`). AccessMode(TransactionAccessMode), /// Isolation level for a transaction (e.g. `SERIALIZABLE`). IsolationLevel(TransactionIsolationLevel), + /// PostgreSQL transaction deferrability for [`START TRANSACTION`](https://www.postgresql.org/docs/17/sql-start-transaction.html) and [`SET TRANSACTION`](https://www.postgresql.org/docs/17/sql-set-transaction.html), with `true` for `DEFERRABLE` and `false` for `NOT DEFERRABLE`. + Deferrable(bool), } impl fmt::Display for TransactionMode { @@ -9326,6 +9328,11 @@ impl fmt::Display for TransactionMode { match self { AccessMode(access_mode) => write!(f, "{access_mode}"), IsolationLevel(iso_level) => write!(f, "ISOLATION LEVEL {iso_level}"), + Deferrable(deferrable) => f.write_str(if *deferrable { + "DEFERRABLE" + } else { + "NOT DEFERRABLE" + }), } } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 3efb22a349..89b90e66a3 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -20562,6 +20562,11 @@ impl<'a> Parser<'a> { TransactionMode::AccessMode(TransactionAccessMode::ReadOnly) } else if self.parse_keywords(&[Keyword::READ, Keyword::WRITE]) { TransactionMode::AccessMode(TransactionAccessMode::ReadWrite) + } else if self.parse_keyword(Keyword::DEFERRABLE) { + TransactionMode::Deferrable(true) + } else if self.parse_keyword(Keyword::NOT) { + self.expect_keyword_is(Keyword::DEFERRABLE)?; + TransactionMode::Deferrable(false) } else if required { self.expected_ref("transaction mode", self.peek_token_ref())? } else { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index f71869e820..7c47870aca 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -10159,6 +10159,128 @@ fn parse_abort() { ); } +#[test] +fn parse_transaction_deferrable_mode() { + let dialects = all_dialects_except(|d| + // BigQuery and Snowflake do not support this syntax + d.is::() || d.is::()); + + let Statement::StartTransaction { modes, .. } = + dialects.verified_stmt("START TRANSACTION DEFERRABLE") + else { + unreachable!() + }; + assert_eq!(modes, vec![TransactionMode::Deferrable(true)]); + + dialects.verified_stmt("BEGIN TRANSACTION NOT DEFERRABLE"); + + // PostgreSQL's `BEGIN` needs no transaction keyword. MsSql is excluded because + // it reads a bare `BEGIN` followed by anything else as a `BEGIN ... END` block. + all_dialects_except(|d| { + d.is::() || d.is::() || d.is::() + }) + .verified_stmt("BEGIN DEFERRABLE"); + + let Statement::StartTransaction { modes, .. } = dialects + .verified_stmt("START TRANSACTION ISOLATION LEVEL SERIALIZABLE, READ ONLY, DEFERRABLE") + else { + unreachable!() + }; + assert_eq!( + modes, + vec![ + TransactionMode::IsolationLevel(TransactionIsolationLevel::Serializable), + TransactionMode::AccessMode(TransactionAccessMode::ReadOnly), + TransactionMode::Deferrable(true), + ] + ); + + // As with the other modes, PostgreSQL allows the commas to be omitted. + let Statement::StartTransaction { modes, .. } = dialects.one_statement_parses_to( + "START TRANSACTION NOT DEFERRABLE READ WRITE", + "START TRANSACTION NOT DEFERRABLE, READ WRITE", + ) else { + unreachable!() + }; + assert_eq!( + modes, + vec![ + TransactionMode::Deferrable(false), + TransactionMode::AccessMode(TransactionAccessMode::ReadWrite), + ] + ); + + let Statement::Set(Set::SetTransaction { modes, .. }) = + verified_stmt("SET TRANSACTION DEFERRABLE") + else { + unreachable!() + }; + assert_eq!(modes, vec![TransactionMode::Deferrable(true)]); + + let Statement::Set(Set::SetTransaction { modes, session, .. }) = + verified_stmt("SET SESSION CHARACTERISTICS AS TRANSACTION NOT DEFERRABLE") + else { + unreachable!() + }; + assert_eq!(modes, vec![TransactionMode::Deferrable(false)]); + assert!(session); + + assert_eq!( + ParserError::ParserError("Expected: DEFERRABLE, found: SERIALIZABLE".to_string()), + dialects + .parse_sql_statements("START TRANSACTION NOT SERIALIZABLE") + .unwrap_err() + ); + + assert_eq!( + ParserError::ParserError("Expected: transaction mode, found: EOF".to_string()), + dialects + .parse_sql_statements("START TRANSACTION DEFERRABLE,") + .unwrap_err() + ); +} + +#[test] +fn parse_transaction_deferrable_mode_malformed_not() { + let dialects = all_dialects_except(|d| + // BigQuery and Snowflake do not support this syntax + d.is::() || d.is::()); + + assert_eq!( + ParserError::ParserError("Expected: DEFERRABLE, found: ,".to_string()), + dialects + .parse_sql_statements("START TRANSACTION NOT,") + .unwrap_err() + ); + + let Statement::StartTransaction { modes, .. } = + dialects.verified_stmt("START TRANSACTION NOT DEFERRABLE, READ ONLY") + else { + unreachable!() + }; + assert_eq!( + modes, + vec![ + TransactionMode::Deferrable(false), + TransactionMode::AccessMode(TransactionAccessMode::ReadOnly), + ] + ); +} + +#[test] +#[cfg(all(feature = "serde", feature = "json_example"))] +fn serde_roundtrip_transaction_deferrable_mode() { + for sql in [ + "START TRANSACTION DEFERRABLE", + "START TRANSACTION NOT DEFERRABLE", + ] { + let stmt = verified_stmt(sql); + let json = serde_json::to_string(&stmt).unwrap(); + let roundtripped: Statement = serde_json::from_str(&json).unwrap(); + assert_eq!(stmt, roundtripped); + } +} + #[test] #[should_panic(expected = "Parse results with GenericDialect are different from PostgreSqlDialect")] fn ensure_multiple_dialects_are_tested() {