From 3b4dc0f2272b636e741cdd23512e766659670a75 Mon Sep 17 00:00:00 2001 From: Yoav Cohen <59807311+yoavcloud@users.noreply.github.com> Date: Sat, 11 Jan 2025 10:51:01 +0100 Subject: [PATCH] MsSQL SET for session params (#1646) --- src/ast/mod.rs | 127 +++++++++++++++++++++++++++++++++++++++ src/ast/spans.rs | 1 + src/dialect/mod.rs | 6 ++ src/dialect/mssql.rs | 5 ++ src/keywords.rs | 5 ++ src/parser/mod.rs | 66 ++++++++++++++++++++ tests/sqlparser_mssql.rs | 61 +++++++++++++++++++ 7 files changed, 271 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 1f8df3529..2d79f7d6b 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3437,6 +3437,10 @@ pub enum Statement { /// Snowflake `REMOVE` /// See: Remove(FileStagingCommand), + /// MS-SQL session + /// + /// See + SetSessionParam(SetSessionParamKind), } impl fmt::Display for Statement { @@ -5024,6 +5028,7 @@ impl fmt::Display for Statement { } Statement::List(command) => write!(f, "LIST {command}"), Statement::Remove(command) => write!(f, "REMOVE {command}"), + Statement::SetSessionParam(kind) => write!(f, "SET {kind}"), } } } @@ -6441,6 +6446,7 @@ pub enum TransactionIsolationLevel { ReadCommitted, RepeatableRead, Serializable, + Snapshot, } impl fmt::Display for TransactionIsolationLevel { @@ -6451,6 +6457,7 @@ impl fmt::Display for TransactionIsolationLevel { ReadCommitted => "READ COMMITTED", RepeatableRead => "REPEATABLE READ", Serializable => "SERIALIZABLE", + Snapshot => "SNAPSHOT", }) } } @@ -7937,6 +7944,126 @@ impl fmt::Display for TableObject { } } +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum SetSessionParamKind { + Generic(SetSessionParamGeneric), + IdentityInsert(SetSessionParamIdentityInsert), + Offsets(SetSessionParamOffsets), + Statistics(SetSessionParamStatistics), +} + +impl fmt::Display for SetSessionParamKind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + SetSessionParamKind::Generic(x) => write!(f, "{x}"), + SetSessionParamKind::IdentityInsert(x) => write!(f, "{x}"), + SetSessionParamKind::Offsets(x) => write!(f, "{x}"), + SetSessionParamKind::Statistics(x) => write!(f, "{x}"), + } + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct SetSessionParamGeneric { + pub names: Vec, + pub value: String, +} + +impl fmt::Display for SetSessionParamGeneric { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} {}", display_comma_separated(&self.names), self.value) + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct SetSessionParamIdentityInsert { + pub obj: ObjectName, + pub value: SessionParamValue, +} + +impl fmt::Display for SetSessionParamIdentityInsert { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "IDENTITY_INSERT {} {}", self.obj, self.value) + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct SetSessionParamOffsets { + pub keywords: Vec, + pub value: SessionParamValue, +} + +impl fmt::Display for SetSessionParamOffsets { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "OFFSETS {} {}", + display_comma_separated(&self.keywords), + self.value + ) + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct SetSessionParamStatistics { + pub topic: SessionParamStatsTopic, + pub value: SessionParamValue, +} + +impl fmt::Display for SetSessionParamStatistics { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "STATISTICS {} {}", self.topic, self.value) + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum SessionParamStatsTopic { + IO, + Profile, + Time, + Xml, +} + +impl fmt::Display for SessionParamStatsTopic { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + SessionParamStatsTopic::IO => write!(f, "IO"), + SessionParamStatsTopic::Profile => write!(f, "PROFILE"), + SessionParamStatsTopic::Time => write!(f, "TIME"), + SessionParamStatsTopic::Xml => write!(f, "XML"), + } + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum SessionParamValue { + On, + Off, +} + +impl fmt::Display for SessionParamValue { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + SessionParamValue::On => write!(f, "ON"), + SessionParamValue::Off => write!(f, "OFF"), + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 19f6074b3..183bebf8c 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -496,6 +496,7 @@ impl Spanned for Statement { Statement::UNLISTEN { .. } => Span::empty(), Statement::RenameTable { .. } => Span::empty(), Statement::List(..) | Statement::Remove(..) => Span::empty(), + Statement::SetSessionParam { .. } => Span::empty(), } } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 32b0ed482..c66982d1f 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -802,6 +802,12 @@ pub trait Dialect: Debug + Any { fn supports_insert_format(&self) -> bool { false } + + /// Returns true if this dialect supports `SET` statements without an explicit + /// assignment operator such as `=`. For example: `SET SHOWPLAN_XML ON`. + fn supports_set_stmt_without_operator(&self) -> bool { + false + } } /// This represents the operators for which precedence must be defined diff --git a/src/dialect/mssql.rs b/src/dialect/mssql.rs index fa77bdc1e..67a648944 100644 --- a/src/dialect/mssql.rs +++ b/src/dialect/mssql.rs @@ -85,4 +85,9 @@ impl Dialect for MsSqlDialect { fn supports_end_transaction_modifier(&self) -> bool { true } + + /// See: + fn supports_set_stmt_without_operator(&self) -> bool { + true + } } diff --git a/src/keywords.rs b/src/keywords.rs index 8c8860e12..8c8077f51 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -388,6 +388,7 @@ define_keywords!( HOURS, ID, IDENTITY, + IDENTITY_INSERT, IF, IGNORE, ILIKE, @@ -426,6 +427,7 @@ define_keywords!( INTERVAL, INTO, INVOKER, + IO, IS, ISODOW, ISOLATION, @@ -557,7 +559,9 @@ define_keywords!( OCTETS, OCTET_LENGTH, OF, + OFF, OFFSET, + OFFSETS, OLD, OMIT, ON, @@ -623,6 +627,7 @@ define_keywords!( PRIOR, PRIVILEGES, PROCEDURE, + PROFILE, PROGRAM, PROJECTION, PUBLIC, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index c17402515..3cf3c585e 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -10428,11 +10428,75 @@ impl<'a> Parser<'a> { snapshot: None, session: false, }) + } else if self.dialect.supports_set_stmt_without_operator() { + self.prev_token(); + self.parse_set_session_params() } else { self.expected("equals sign or TO", self.peek_token()) } } + pub fn parse_set_session_params(&mut self) -> Result { + if self.parse_keyword(Keyword::STATISTICS) { + let topic = match self.parse_one_of_keywords(&[ + Keyword::IO, + Keyword::PROFILE, + Keyword::TIME, + Keyword::XML, + ]) { + Some(Keyword::IO) => SessionParamStatsTopic::IO, + Some(Keyword::PROFILE) => SessionParamStatsTopic::Profile, + Some(Keyword::TIME) => SessionParamStatsTopic::Time, + Some(Keyword::XML) => SessionParamStatsTopic::Xml, + _ => return self.expected("IO, PROFILE, TIME or XML", self.peek_token()), + }; + let value = self.parse_session_param_value()?; + Ok(Statement::SetSessionParam(SetSessionParamKind::Statistics( + SetSessionParamStatistics { topic, value }, + ))) + } else if self.parse_keyword(Keyword::IDENTITY_INSERT) { + let obj = self.parse_object_name(false)?; + let value = self.parse_session_param_value()?; + Ok(Statement::SetSessionParam( + SetSessionParamKind::IdentityInsert(SetSessionParamIdentityInsert { obj, value }), + )) + } else if self.parse_keyword(Keyword::OFFSETS) { + let keywords = self.parse_comma_separated(|parser| { + let next_token = parser.next_token(); + match &next_token.token { + Token::Word(w) => Ok(w.to_string()), + _ => parser.expected("SQL keyword", next_token), + } + })?; + let value = self.parse_session_param_value()?; + Ok(Statement::SetSessionParam(SetSessionParamKind::Offsets( + SetSessionParamOffsets { keywords, value }, + ))) + } else { + let names = self.parse_comma_separated(|parser| { + let next_token = parser.next_token(); + match next_token.token { + Token::Word(w) => Ok(w.to_string()), + _ => parser.expected("Session param name", next_token), + } + })?; + let value = self.parse_expr()?.to_string(); + Ok(Statement::SetSessionParam(SetSessionParamKind::Generic( + SetSessionParamGeneric { names, value }, + ))) + } + } + + fn parse_session_param_value(&mut self) -> Result { + if self.parse_keyword(Keyword::ON) { + Ok(SessionParamValue::On) + } else if self.parse_keyword(Keyword::OFF) { + Ok(SessionParamValue::Off) + } else { + self.expected("ON or OFF", self.peek_token()) + } + } + pub fn parse_show(&mut self) -> Result { let terse = self.parse_keyword(Keyword::TERSE); let extended = self.parse_keyword(Keyword::EXTENDED); @@ -13004,6 +13068,8 @@ impl<'a> Parser<'a> { TransactionIsolationLevel::RepeatableRead } else if self.parse_keyword(Keyword::SERIALIZABLE) { TransactionIsolationLevel::Serializable + } else if self.parse_keyword(Keyword::SNAPSHOT) { + TransactionIsolationLevel::Snapshot } else { self.expected("isolation level", self.peek_token())? }; diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index ecc874af8..567cd5382 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -1679,6 +1679,67 @@ fn parse_true_false_as_identifiers() { ); } +#[test] +fn parse_mssql_set_session_value() { + ms().verified_stmt( + "SET OFFSETS SELECT, FROM, ORDER, TABLE, PROCEDURE, STATEMENT, PARAM, EXECUTE ON", + ); + ms().verified_stmt("SET IDENTITY_INSERT dbo.Tool ON"); + ms().verified_stmt("SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED"); + ms().verified_stmt("SET TRANSACTION ISOLATION LEVEL READ COMMITTED"); + ms().verified_stmt("SET TRANSACTION ISOLATION LEVEL REPEATABLE READ"); + ms().verified_stmt("SET TRANSACTION ISOLATION LEVEL SNAPSHOT"); + ms().verified_stmt("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE"); + ms().verified_stmt("SET STATISTICS IO ON"); + ms().verified_stmt("SET STATISTICS XML ON"); + ms().verified_stmt("SET STATISTICS PROFILE ON"); + ms().verified_stmt("SET STATISTICS TIME ON"); + ms().verified_stmt("SET DATEFIRST 7"); + ms().verified_stmt("SET DATEFIRST @xxx"); + ms().verified_stmt("SET DATEFIRST @@xxx"); + ms().verified_stmt("SET DATEFORMAT dmy"); + ms().verified_stmt("SET DATEFORMAT @datevar"); + ms().verified_stmt("SET DATEFORMAT @@datevar"); + ms().verified_stmt("SET DEADLOCK_PRIORITY 'LOW'"); + ms().verified_stmt("SET DEADLOCK_PRIORITY LOW"); + ms().verified_stmt("SET DEADLOCK_PRIORITY 8"); + ms().verified_stmt("SET DEADLOCK_PRIORITY -8"); + ms().verified_stmt("SET DEADLOCK_PRIORITY @xxx"); + ms().verified_stmt("SET DEADLOCK_PRIORITY @@xxx"); + ms().verified_stmt("SET LOCK_TIMEOUT 1800"); + ms().verified_stmt("SET CONCAT_NULL_YIELDS_NULL ON"); + ms().verified_stmt("SET CURSOR_CLOSE_ON_COMMIT ON"); + ms().verified_stmt("SET FIPS_FLAGGER 'level'"); + ms().verified_stmt("SET FIPS_FLAGGER OFF"); + ms().verified_stmt("SET LANGUAGE Italian"); + ms().verified_stmt("SET QUOTED_IDENTIFIER ON"); + ms().verified_stmt("SET ARITHABORT ON"); + ms().verified_stmt("SET ARITHIGNORE OFF"); + ms().verified_stmt("SET FMTONLY ON"); + ms().verified_stmt("SET NOCOUNT OFF"); + ms().verified_stmt("SET NOEXEC ON"); + ms().verified_stmt("SET NUMERIC_ROUNDABORT ON"); + ms().verified_stmt("SET QUERY_GOVERNOR_COST_LIMIT 11"); + ms().verified_stmt("SET ROWCOUNT 4"); + ms().verified_stmt("SET ROWCOUNT @xxx"); + ms().verified_stmt("SET ROWCOUNT @@xxx"); + ms().verified_stmt("SET TEXTSIZE 11"); + ms().verified_stmt("SET ANSI_DEFAULTS ON"); + ms().verified_stmt("SET ANSI_NULL_DFLT_OFF ON"); + ms().verified_stmt("SET ANSI_NULL_DFLT_ON ON"); + ms().verified_stmt("SET ANSI_NULLS ON"); + ms().verified_stmt("SET ANSI_PADDING ON"); + ms().verified_stmt("SET ANSI_WARNINGS ON"); + ms().verified_stmt("SET FORCEPLAN ON"); + ms().verified_stmt("SET SHOWPLAN_ALL ON"); + ms().verified_stmt("SET SHOWPLAN_TEXT ON"); + ms().verified_stmt("SET SHOWPLAN_XML ON"); + ms().verified_stmt("SET IMPLICIT_TRANSACTIONS ON"); + ms().verified_stmt("SET REMOTE_PROC_TRANSACTIONS ON"); + ms().verified_stmt("SET XACT_ABORT ON"); + ms().verified_stmt("SET ANSI_NULLS, ANSI_PADDING ON"); +} + fn ms() -> TestedDialects { TestedDialects::new(vec![Box::new(MsSqlDialect {})]) }