From 1318e497ac4798e9c3203f20f9393dd86ac2fa43 Mon Sep 17 00:00:00 2001 From: Takahiro Ebato Date: Fri, 15 Dec 2023 23:26:31 +0900 Subject: [PATCH 1/4] add support for BEGIN TRANSACTION modifiers in sqlite Support the following syntaxes - BEGIN DEFERRED - BEGIN IMMEDIATE - BEGIN EXCLUSIVE --- src/ast/mod.rs | 32 +++++++++++++++++++++++++++++++- src/keywords.rs | 3 +++ src/parser/mod.rs | 13 +++++++++++++ tests/sqlparser_common.rs | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 81 insertions(+), 1 deletion(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 4769ea9bd..95491ad7b 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1819,6 +1819,8 @@ pub enum Statement { StartTransaction { modes: Vec, begin: bool, + // Only for sqlite + modifier: Option, }, /// `SET TRANSACTION ...` SetTransaction { @@ -3107,9 +3109,14 @@ impl fmt::Display for Statement { Statement::StartTransaction { modes, begin: syntax_begin, + modifier, } => { if *syntax_begin { - write!(f, "BEGIN TRANSACTION")?; + if let Some(modifier) = *modifier { + write!(f, "BEGIN {} TRANSACTION", modifier)?; + } else { + write!(f, "BEGIN TRANSACTION")?; + } } else { write!(f, "START TRANSACTION")?; } @@ -4292,6 +4299,29 @@ impl fmt::Display for TransactionIsolationLevel { } } +/// Sqlite specific syntax +/// +/// https://sqlite.org/lang_transaction.html +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum TransactionModifier { + Deferred, + Immediate, + Exclusive, +} + +impl fmt::Display for TransactionModifier { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use TransactionModifier::*; + f.write_str(match self { + Deferred => "DEFERRED", + Immediate => "IMMEDIATE", + Exclusive => "EXCLUSIVE", + }) + } +} + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] diff --git a/src/keywords.rs b/src/keywords.rs index 2de36562f..7f13e3990 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -209,6 +209,7 @@ define_keywords!( DECIMAL, DECLARE, DEFAULT, + DEFERRED, DELETE, DELIMITED, DELIMITER, @@ -254,6 +255,7 @@ define_keywords!( EVERY, EXCEPT, EXCLUDE, + EXCLUSIVE, EXEC, EXECUTE, EXISTS, @@ -321,6 +323,7 @@ define_keywords!( IF, IGNORE, ILIKE, + IMMEDIATE, IMMUTABLE, IN, INCLUDE, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index d9d4761c3..9842ca4ba 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -7834,14 +7834,27 @@ impl<'a> Parser<'a> { Ok(Statement::StartTransaction { modes: self.parse_transaction_modes()?, begin: false, + modifier: None, }) } pub fn parse_begin(&mut self) -> Result { + let modifier = if !dialect_of!(self is SQLiteDialect) { + None + } else if self.parse_keyword(Keyword::DEFERRED) { + Some(TransactionModifier::Deferred) + } else if self.parse_keyword(Keyword::IMMEDIATE) { + Some(TransactionModifier::Immediate) + } else if self.parse_keyword(Keyword::EXCLUSIVE) { + Some(TransactionModifier::Exclusive) + } else { + None + }; let _ = self.parse_one_of_keywords(&[Keyword::TRANSACTION, Keyword::WORK]); Ok(Statement::StartTransaction { modes: self.parse_transaction_modes()?, begin: true, + modifier, }) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 1d0923b4f..fc21a686e 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -6232,6 +6232,40 @@ fn parse_start_transaction() { ); } +#[test] +fn parse_start_transaction_sqlite() { + let dialect = SQLiteDialect {}; + + let check = |sql: &str, expected_modifier: Option| match Parser::parse_sql( + &dialect, &sql, + ) + .unwrap() + .pop() + .unwrap() + { + Statement::StartTransaction { modifier, .. } => assert_eq!(modifier, expected_modifier), + _ => panic!("{}", sql), + }; + + let sql = "BEGIN DEFERRED"; + check(sql, Some(TransactionModifier::Deferred)); + + let sql = "BEGIN DEFERRED TRANSACTION"; + check(sql, Some(TransactionModifier::Deferred)); + + let sql = "BEGIN IMMEDIATE"; + check(sql, Some(TransactionModifier::Immediate)); + + let sql = "BEGIN IMMEDIATE TRANSACTION"; + check(sql, Some(TransactionModifier::Immediate)); + + let sql = "BEGIN EXCLUSIVE"; + check(sql, Some(TransactionModifier::Exclusive)); + + let sql = "BEGIN EXCLUSIVE TRANSACTION"; + check(sql, Some(TransactionModifier::Exclusive)); +} + #[test] fn parse_set_transaction() { // SET TRANSACTION shares transaction mode parsing code with START From 374985dcd45f6bc55dacf27991785b967b153008 Mon Sep 17 00:00:00 2001 From: Takahiro Ebato Date: Sun, 17 Dec 2023 16:03:58 +0900 Subject: [PATCH 2/4] refactor BEGIN TRANSACTION statements in SQLite --- src/ast/mod.rs | 2 +- src/dialect/generic.rs | 4 ++++ src/dialect/mod.rs | 4 ++++ src/dialect/sqlite.rs | 4 ++++ src/parser/mod.rs | 2 +- tests/sqlparser_common.rs | 34 ---------------------------------- tests/sqlparser_sqlite.rs | 10 ++++++++++ 7 files changed, 24 insertions(+), 36 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 95491ad7b..4dc03e9cb 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1819,7 +1819,7 @@ pub enum Statement { StartTransaction { modes: Vec, begin: bool, - // Only for sqlite + /// Only for SQLite modifier: Option, }, /// `SET TRANSACTION ...` diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index ea5cc6c34..afdaba82d 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -38,4 +38,8 @@ impl Dialect for GenericDialect { fn supports_group_by_expr(&self) -> bool { true } + + fn supports_start_transaction_modifier(&self) -> bool { + true + } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 53bb891de..a10b21254 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -128,6 +128,10 @@ pub trait Dialect: Debug + Any { fn supports_in_empty_list(&self) -> bool { false } + /// Returns true if the dialect supports `BEGIN {DEFERRED | IMMEDIATE | EXCLUSIVE} [TRANSACTION]` statements + fn supports_start_transaction_modifier(&self) -> bool { + false + } /// Returns true if the dialect has a CONVERT function which accepts a type first /// and an expression second, e.g. `CONVERT(varchar, 1)` fn convert_type_before_value(&self) -> bool { diff --git a/src/dialect/sqlite.rs b/src/dialect/sqlite.rs index c9e9ab185..0640466c8 100644 --- a/src/dialect/sqlite.rs +++ b/src/dialect/sqlite.rs @@ -40,6 +40,10 @@ impl Dialect for SQLiteDialect { true } + fn supports_start_transaction_modifier(&self) -> bool { + true + } + fn is_identifier_part(&self, ch: char) -> bool { self.is_identifier_start(ch) || ch.is_ascii_digit() } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 9842ca4ba..69788acd0 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -7839,7 +7839,7 @@ impl<'a> Parser<'a> { } pub fn parse_begin(&mut self) -> Result { - let modifier = if !dialect_of!(self is SQLiteDialect) { + let modifier = if !self.dialect.supports_start_transaction_modifier() { None } else if self.parse_keyword(Keyword::DEFERRED) { Some(TransactionModifier::Deferred) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index fc21a686e..1d0923b4f 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -6232,40 +6232,6 @@ fn parse_start_transaction() { ); } -#[test] -fn parse_start_transaction_sqlite() { - let dialect = SQLiteDialect {}; - - let check = |sql: &str, expected_modifier: Option| match Parser::parse_sql( - &dialect, &sql, - ) - .unwrap() - .pop() - .unwrap() - { - Statement::StartTransaction { modifier, .. } => assert_eq!(modifier, expected_modifier), - _ => panic!("{}", sql), - }; - - let sql = "BEGIN DEFERRED"; - check(sql, Some(TransactionModifier::Deferred)); - - let sql = "BEGIN DEFERRED TRANSACTION"; - check(sql, Some(TransactionModifier::Deferred)); - - let sql = "BEGIN IMMEDIATE"; - check(sql, Some(TransactionModifier::Immediate)); - - let sql = "BEGIN IMMEDIATE TRANSACTION"; - check(sql, Some(TransactionModifier::Immediate)); - - let sql = "BEGIN EXCLUSIVE"; - check(sql, Some(TransactionModifier::Exclusive)); - - let sql = "BEGIN EXCLUSIVE TRANSACTION"; - check(sql, Some(TransactionModifier::Exclusive)); -} - #[test] fn parse_set_transaction() { // SET TRANSACTION shares transaction mode parsing code with START diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index cc0d53b14..68b51857e 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -431,6 +431,16 @@ fn invalid_empty_list() { ); } +#[test] +fn parse_start_transaction_with_modifier() { + sqlite_and_generic().verified_stmt("BEGIN DEFERRED TRANSACTION"); + sqlite_and_generic().verified_stmt("BEGIN IMMEDIATE TRANSACTION"); + sqlite_and_generic().verified_stmt("BEGIN EXCLUSIVE TRANSACTION"); + sqlite_and_generic().one_statement_parses_to("BEGIN DEFERRED", "BEGIN DEFERRED TRANSACTION"); + sqlite_and_generic().one_statement_parses_to("BEGIN IMMEDIATE", "BEGIN IMMEDIATE TRANSACTION"); + sqlite_and_generic().one_statement_parses_to("BEGIN EXCLUSIVE", "BEGIN EXCLUSIVE TRANSACTION"); +} + fn sqlite() -> TestedDialects { TestedDialects { dialects: vec![Box::new(SQLiteDialect {})], From f2ad35e9d232cd2a4afd4b5715be6135db59416b Mon Sep 17 00:00:00 2001 From: Takahiro Ebato Date: Mon, 18 Dec 2023 00:52:52 +0900 Subject: [PATCH 3/4] added tests and a helper function --- src/test_utils.rs | 44 +++++++++++++++++++++++++++++++++++++++ tests/sqlparser_sqlite.rs | 33 ++++++++++++++++++++++------- 2 files changed, 70 insertions(+), 7 deletions(-) diff --git a/src/test_utils.rs b/src/test_utils.rs index 26cfec463..2217bbaf2 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -212,6 +212,50 @@ pub fn all_dialects() -> TestedDialects { } } +static ALL_DIALECT_NAMES: &[&str] = &[ + "generic", + "mysql", + "postgresql", + "hive", + "sqlite", + "snowflake", + "redshift", + "mssql", + "clickhouse", + "bigquery", + "ansi", + "duckdb", +]; + +pub fn partition_all_dialects_by_inclusion( + dialect_names: Vec<&str>, +) -> (TestedDialects, TestedDialects) { + for dialect_name in &dialect_names { + assert!( + ALL_DIALECT_NAMES.contains(dialect_name), + "Unknown dialect: {}", + dialect_name + ); + } + + let (included_dialect_names, excluded_dialect_names) = ALL_DIALECT_NAMES + .iter() + .partition(|&dialect_name| dialect_names.contains(dialect_name)); + + let build_tested_dialects = |names: Vec<&str>| TestedDialects { + dialects: names + .iter() + .map(|&name| dialect_from_str(name).unwrap()) + .collect(), + options: None, + }; + + ( + build_tested_dialects(included_dialect_names), + build_tested_dialects(excluded_dialect_names), + ) +} + pub fn assert_eq_vec(expected: &[&str], actual: &[T]) { assert_eq!( expected, diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index 68b51857e..8baa39ad8 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -22,7 +22,7 @@ use test_utils::*; use sqlparser::ast::SelectItem::UnnamedExpr; use sqlparser::ast::*; use sqlparser::dialect::{GenericDialect, SQLiteDialect}; -use sqlparser::parser::ParserOptions; +use sqlparser::parser::{ParserError, ParserOptions}; use sqlparser::tokenizer::Token; #[test] @@ -433,12 +433,31 @@ fn invalid_empty_list() { #[test] fn parse_start_transaction_with_modifier() { - sqlite_and_generic().verified_stmt("BEGIN DEFERRED TRANSACTION"); - sqlite_and_generic().verified_stmt("BEGIN IMMEDIATE TRANSACTION"); - sqlite_and_generic().verified_stmt("BEGIN EXCLUSIVE TRANSACTION"); - sqlite_and_generic().one_statement_parses_to("BEGIN DEFERRED", "BEGIN DEFERRED TRANSACTION"); - sqlite_and_generic().one_statement_parses_to("BEGIN IMMEDIATE", "BEGIN IMMEDIATE TRANSACTION"); - sqlite_and_generic().one_statement_parses_to("BEGIN EXCLUSIVE", "BEGIN EXCLUSIVE TRANSACTION"); + let (supported_dialects, unsupported_dialects) = + partition_all_dialects_by_inclusion(vec!["generic", "sqlite"]); + + supported_dialects.verified_stmt("BEGIN DEFERRED TRANSACTION"); + supported_dialects.verified_stmt("BEGIN IMMEDIATE TRANSACTION"); + supported_dialects.verified_stmt("BEGIN EXCLUSIVE TRANSACTION"); + supported_dialects.one_statement_parses_to("BEGIN DEFERRED", "BEGIN DEFERRED TRANSACTION"); + supported_dialects.one_statement_parses_to("BEGIN IMMEDIATE", "BEGIN IMMEDIATE TRANSACTION"); + supported_dialects.one_statement_parses_to("BEGIN EXCLUSIVE", "BEGIN EXCLUSIVE TRANSACTION"); + + let res = unsupported_dialects.parse_sql_statements("BEGIN DEFERRED"); + assert_eq!( + ParserError::ParserError("Expected end of statement, found: DEFERRED".to_string()), + res.unwrap_err(), + ); + let res = unsupported_dialects.parse_sql_statements("BEGIN IMMEDIATE"); + assert_eq!( + ParserError::ParserError("Expected end of statement, found: IMMEDIATE".to_string()), + res.unwrap_err(), + ); + let res = unsupported_dialects.parse_sql_statements("BEGIN EXCLUSIVE"); + assert_eq!( + ParserError::ParserError("Expected end of statement, found: EXCLUSIVE".to_string()), + res.unwrap_err(), + ); } fn sqlite() -> TestedDialects { From c5357bf2fe9d50905fe138422c51c119553bc3c0 Mon Sep 17 00:00:00 2001 From: Takahiro Ebato Date: Wed, 20 Dec 2023 19:40:13 +0900 Subject: [PATCH 4/4] fix tests and documentation --- src/ast/mod.rs | 4 ++-- src/test_utils.rs | 44 --------------------------------------- tests/sqlparser_sqlite.rs | 25 +++++++++++++--------- 3 files changed, 17 insertions(+), 56 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 4dc03e9cb..a6bede5e7 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -4299,9 +4299,9 @@ impl fmt::Display for TransactionIsolationLevel { } } -/// Sqlite specific syntax +/// SQLite specific syntax /// -/// https://sqlite.org/lang_transaction.html +/// #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] diff --git a/src/test_utils.rs b/src/test_utils.rs index 2217bbaf2..26cfec463 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -212,50 +212,6 @@ pub fn all_dialects() -> TestedDialects { } } -static ALL_DIALECT_NAMES: &[&str] = &[ - "generic", - "mysql", - "postgresql", - "hive", - "sqlite", - "snowflake", - "redshift", - "mssql", - "clickhouse", - "bigquery", - "ansi", - "duckdb", -]; - -pub fn partition_all_dialects_by_inclusion( - dialect_names: Vec<&str>, -) -> (TestedDialects, TestedDialects) { - for dialect_name in &dialect_names { - assert!( - ALL_DIALECT_NAMES.contains(dialect_name), - "Unknown dialect: {}", - dialect_name - ); - } - - let (included_dialect_names, excluded_dialect_names) = ALL_DIALECT_NAMES - .iter() - .partition(|&dialect_name| dialect_names.contains(dialect_name)); - - let build_tested_dialects = |names: Vec<&str>| TestedDialects { - dialects: names - .iter() - .map(|&name| dialect_from_str(name).unwrap()) - .collect(), - options: None, - }; - - ( - build_tested_dialects(included_dialect_names), - build_tested_dialects(excluded_dialect_names), - ) -} - pub fn assert_eq_vec(expected: &[&str], actual: &[T]) { assert_eq!( expected, diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index 8baa39ad8..311085c0a 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -433,16 +433,21 @@ fn invalid_empty_list() { #[test] fn parse_start_transaction_with_modifier() { - let (supported_dialects, unsupported_dialects) = - partition_all_dialects_by_inclusion(vec!["generic", "sqlite"]); - - supported_dialects.verified_stmt("BEGIN DEFERRED TRANSACTION"); - supported_dialects.verified_stmt("BEGIN IMMEDIATE TRANSACTION"); - supported_dialects.verified_stmt("BEGIN EXCLUSIVE TRANSACTION"); - supported_dialects.one_statement_parses_to("BEGIN DEFERRED", "BEGIN DEFERRED TRANSACTION"); - supported_dialects.one_statement_parses_to("BEGIN IMMEDIATE", "BEGIN IMMEDIATE TRANSACTION"); - supported_dialects.one_statement_parses_to("BEGIN EXCLUSIVE", "BEGIN EXCLUSIVE TRANSACTION"); - + sqlite_and_generic().verified_stmt("BEGIN DEFERRED TRANSACTION"); + sqlite_and_generic().verified_stmt("BEGIN IMMEDIATE TRANSACTION"); + sqlite_and_generic().verified_stmt("BEGIN EXCLUSIVE TRANSACTION"); + sqlite_and_generic().one_statement_parses_to("BEGIN DEFERRED", "BEGIN DEFERRED TRANSACTION"); + sqlite_and_generic().one_statement_parses_to("BEGIN IMMEDIATE", "BEGIN IMMEDIATE TRANSACTION"); + sqlite_and_generic().one_statement_parses_to("BEGIN EXCLUSIVE", "BEGIN EXCLUSIVE TRANSACTION"); + + let unsupported_dialects = TestedDialects { + dialects: all_dialects() + .dialects + .into_iter() + .filter(|x| !(x.is::() || x.is::())) + .collect(), + options: None, + }; let res = unsupported_dialects.parse_sql_statements("BEGIN DEFERRED"); assert_eq!( ParserError::ParserError("Expected end of statement, found: DEFERRED".to_string()),