From 619da03fc5152b7ac07845572ada24d556dccdd1 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Sat, 26 Sep 2026 22:37:17 +0800 Subject: [PATCH] MSSQL: Support cursor scope and FAST_FORWARD options --- src/ast/mod.rs | 30 ++++++++++++++++++++++++++++++ src/keywords.rs | 1 + src/parser/mod.rs | 25 +++++++++++++++++++++++++ tests/sqlparser_mssql.rs | 38 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 94 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index a899d3ec3a..5e6202dd0d 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3109,6 +3109,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 { @@ -3151,6 +3174,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 @@ -3176,6 +3201,7 @@ impl fmt::Display for Declare { data_type, assignment, declare_type, + cursor_options, binary, sensitive, scroll, @@ -3208,6 +3234,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 deae7bbd94..7bc5ab4c3c 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -421,6 +421,7 @@ define_keywords!( FALLBACK, FALSE, FAMILY, + FAST_FORWARD, FETCH, FIELDS, FILE, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index b8238843a9..ad1a610419 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -7938,6 +7938,7 @@ impl<'a> Parser<'a> { data_type: None, assignment: None, declare_type, + cursor_options: vec![], binary, sensitive, scroll, @@ -7981,6 +7982,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, @@ -8075,6 +8077,7 @@ impl<'a> Parser<'a> { data_type, assignment: assigned_expr, declare_type, + cursor_options: vec![], binary: None, sensitive: None, scroll: None, @@ -8150,10 +8153,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 => { @@ -8179,6 +8184,7 @@ impl<'a> Parser<'a> { data_type, assignment, declare_type, + cursor_options, binary: None, sensitive: None, scroll: None, @@ -8187,6 +8193,25 @@ impl<'a> Parser<'a> { }) } + fn parse_mssql_cursor_options(&mut self) -> Vec { + let mut options = vec![]; + + loop { + options.push( + match self.parse_one_of_keywords(&[ + Keyword::LOCAL, + Keyword::GLOBAL, + Keyword::FAST_FORWARD, + ]) { + Some(Keyword::LOCAL) => MsSqlCursorOption::Local, + Some(Keyword::GLOBAL) => MsSqlCursorOption::Global, + Some(Keyword::FAST_FORWARD) => MsSqlCursorOption::FastForward, + _ => return options, + }, + ); + } + } + /// Parses the assigned expression in a variable declaration. /// /// Syntax: diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index d978f4a69a..dbf30fdc23 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, @@ -2958,3 +2962,37 @@ fn parse_create_proc() { .expect_err("PROC should remain MSSQL-specific"); ms_and_generic().verified_stmt("SELECT proc FROM jobs"); } + +#[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], + ), + ( + "DECLARE local_cursor CURSOR FAST_FORWARD LOCAL FOR SELECT name FROM dbo.reports", + vec![MsSqlCursorOption::FastForward, MsSqlCursorOption::Local], + ), + ] { + 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"); +}