diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 656a90d2f..d74e9e361 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3081,6 +3081,29 @@ pub enum DeclareType { Exception, } +/// SQL Server cursor options that appear after the `CURSOR` keyword. +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum MsSqlCursorOption { + /// `LOCAL` cursor scope. + Local, + /// `GLOBAL` cursor scope. + Global, + /// `FAST_FORWARD` forward-only, read-only cursor. + FastForward, +} + +impl fmt::Display for MsSqlCursorOption { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + MsSqlCursorOption::Local => f.write_str("LOCAL"), + MsSqlCursorOption::Global => f.write_str("GLOBAL"), + MsSqlCursorOption::FastForward => f.write_str("FAST_FORWARD"), + } + } +} + impl fmt::Display for DeclareType { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { @@ -3123,6 +3146,8 @@ pub struct Declare { pub assignment: Option, /// Represents the type of the declared variable. pub declare_type: Option, + /// SQL Server cursor options following the `CURSOR` keyword. + pub cursor_options: Vec, /// Causes the cursor to return data in binary rather than in text format. pub binary: Option, /// None = Not specified @@ -3148,6 +3173,7 @@ impl fmt::Display for Declare { data_type, assignment, declare_type, + cursor_options, binary, sensitive, scroll, @@ -3180,6 +3206,10 @@ impl fmt::Display for Declare { write!(f, " {declare_type}")?; } + if !cursor_options.is_empty() { + write!(f, " {}", display_separated(cursor_options, " "))?; + } + if let Some(hold) = hold { if *hold { write!(f, " WITH HOLD")?; diff --git a/src/keywords.rs b/src/keywords.rs index 0c50703c3..67d13fe83 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -420,6 +420,7 @@ define_keywords!( FALLBACK, FALSE, FAMILY, + FAST_FORWARD, FETCH, FIELDS, FILE, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 272de79d8..f6a577ed8 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -7885,6 +7885,7 @@ impl<'a> Parser<'a> { data_type: None, assignment: None, declare_type, + cursor_options: vec![], binary, sensitive, scroll, @@ -7928,6 +7929,7 @@ impl<'a> Parser<'a> { data_type, assignment: expr.map(|expr| DeclareAssignment::Default(Box::new(expr))), declare_type: None, + cursor_options: vec![], binary: None, sensitive: None, scroll: None, @@ -8022,6 +8024,7 @@ impl<'a> Parser<'a> { data_type, assignment: assigned_expr, declare_type, + cursor_options: vec![], binary: None, sensitive: None, scroll: None, @@ -8097,10 +8100,12 @@ impl<'a> Parser<'a> { } }?; + let mut cursor_options = vec![]; let (declare_type, data_type) = match &self.peek_token_ref().token { Token::Word(w) => match w.keyword { Keyword::CURSOR => { self.next_token(); + cursor_options = self.parse_mssql_cursor_options(); (Some(DeclareType::Cursor), None) } Keyword::AS => { @@ -8126,6 +8131,7 @@ impl<'a> Parser<'a> { data_type, assignment, declare_type, + cursor_options, binary: None, sensitive: None, scroll: None, @@ -8134,6 +8140,21 @@ impl<'a> Parser<'a> { }) } + fn parse_mssql_cursor_options(&mut self) -> Vec { + let mut options = vec![]; + + match self.parse_one_of_keywords(&[Keyword::LOCAL, Keyword::GLOBAL]) { + Some(Keyword::LOCAL) => options.push(MsSqlCursorOption::Local), + Some(Keyword::GLOBAL) => options.push(MsSqlCursorOption::Global), + _ => {} + } + if self.parse_keyword(Keyword::FAST_FORWARD) { + options.push(MsSqlCursorOption::FastForward); + } + + options + } + /// Parses the assigned expression in a variable declaration. /// /// Syntax: diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 3faf56f0d..8adbdbe35 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -1429,6 +1429,7 @@ fn parse_mssql_declare() { data_type: None, assignment: None, declare_type: Some(DeclareType::Cursor), + cursor_options: vec![], binary: None, sensitive: None, scroll: None, @@ -1444,6 +1445,7 @@ fn parse_mssql_declare() { data_type: Some(Int(None)), assignment: None, declare_type: None, + cursor_options: vec![], binary: None, sensitive: None, scroll: None, @@ -1461,6 +1463,7 @@ fn parse_mssql_declare() { (SingleQuotedString("foobar".to_string())).with_empty_span() )))), declare_type: None, + cursor_options: vec![], binary: None, sensitive: None, scroll: None, @@ -1482,6 +1485,7 @@ fn parse_mssql_declare() { data_type: Some(Int(None)), assignment: None, declare_type: None, + cursor_options: vec![], binary: None, sensitive: None, scroll: None, @@ -2925,3 +2929,33 @@ fn parse_mssql_money_constants() { expr_from_projection(only(&select.projection)), ); } + +#[test] +fn parse_mssql_cursor_options() { + for (sql, expected_options) in [ + ( + "DECLARE local_cursor CURSOR LOCAL FOR SELECT name FROM dbo.reports", + vec![MsSqlCursorOption::Local], + ), + ( + "DECLARE fast_cursor CURSOR FAST_FORWARD FOR SELECT name FROM dbo.reports", + vec![MsSqlCursorOption::FastForward], + ), + ( + "DECLARE global_cursor CURSOR GLOBAL FAST_FORWARD FOR SELECT name FROM dbo.reports", + vec![MsSqlCursorOption::Global, MsSqlCursorOption::FastForward], + ), + ] { + let Statement::Declare { stmts } = ms().verified_stmt(sql) else { + panic!("expected DECLARE statement"); + }; + assert_eq!(only(&stmts).cursor_options, expected_options); + } + + TestedDialects::new(vec![Box::new(GenericDialect {})]) + .parse_sql_statements( + "DECLARE report_cursor CURSOR LOCAL FAST_FORWARD FOR SELECT name FROM reports", + ) + .expect_err("MSSQL cursor options should remain dialect-specific"); + ms_and_generic().verified_stmt("SELECT fast_forward FROM jobs"); +}