From c5fb50ff729e18e50ec468be6e316ccf4270107d Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Fri, 2 Oct 2026 18:14:25 +0200 Subject: [PATCH 1/3] Parse the DEFERRABLE transaction mode --- src/ast/mod.rs | 9 ++++- src/parser/mod.rs | 5 +++ tests/sqlparser_common.rs | 83 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+), 1 deletion(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index eb5bc6cc75..d30c3f4166 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -9290,12 +9290,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), + /// `DEFERRABLE` when true, `NOT DEFERRABLE` when false. + Deferrable(bool), } impl fmt::Display for TransactionMode { @@ -9304,6 +9306,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 276b252ed2..ee5718ff44 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -20290,6 +20290,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 87fb61dc17..2f227f6446 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -20496,6 +20496,89 @@ fn parse_stage_table_factor() { ); } +#[test] +fn parse_transaction_deferrable_mode() { + // The third transaction mode of the SQL standard, spelled the same in + // PostgreSQL: + 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_alter_table_column_position() { let dialects = all_dialects_where(|d| d.supports_alter_column_position()); From 38e7f55d0ba71f7bccb5d5d7af8a5b83709bf18d Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Tue, 20 Oct 2026 12:51:23 +0200 Subject: [PATCH 2/3] Cover deferrable transaction boundaries and serialization --- tests/sqlparser_common.rs | 41 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 2f227f6446..fa1e372024 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -20579,6 +20579,47 @@ fn parse_transaction_deferrable_mode() { ); } +#[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] fn parse_alter_table_column_position() { let dialects = all_dialects_where(|d| d.supports_alter_column_position()); From b8cff1f6f3b06dc69b1646c085da2039806f8d23 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Tue, 20 Oct 2026 17:56:02 +0200 Subject: [PATCH 3/3] Remove inaccurate `DEFERRABLE` standard claim --- tests/sqlparser_common.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index fa1e372024..b0f8a793ee 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -20498,8 +20498,6 @@ fn parse_stage_table_factor() { #[test] fn parse_transaction_deferrable_mode() { - // The third transaction mode of the SQL standard, spelled the same in - // PostgreSQL: let dialects = all_dialects_except(|d| // BigQuery and Snowflake do not support this syntax d.is::() || d.is::());