From ac298a6bbc6ce64fad5a8d2a31d165bdc54fbc7d Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Fri, 25 Apr 2025 13:17:25 -0400 Subject: [PATCH 01/12] Add additional cursor parsing support for SQL Server - parse `OPEN cursor_name` statements - enable `FETCH` statements to parse `FROM cursor_name`, in addition to the existing `IN` parsing --- src/ast/mod.rs | 33 ++++++++++++++++++++++++++++++++- src/ast/spans.rs | 1 + src/parser/mod.rs | 21 ++++++++++++++++++++- tests/sqlparser_mssql.rs | 18 ++++++++++++++++++ 4 files changed, 71 insertions(+), 2 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 45924579b..ee8931c12 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3032,6 +3032,14 @@ pub enum Statement { partition: Option>, }, /// ```sql + /// OPEN cursor_name + /// ``` + /// Opens a cursor. + Open { + /// Cursor name + cursor_name: Ident, + }, + /// ```sql /// CLOSE /// ``` /// Closes the portal underlying an open cursor. @@ -3403,6 +3411,10 @@ pub enum Statement { /// Cursor name name: Ident, direction: FetchDirection, + /// Differentiate between dialects that fetch `FROM` vs fetch `IN` + /// + /// [MsSql](https://learn.microsoft.com/en-us/sql/t-sql/language-elements/fetch-transact-sql) + from_or_in: AttachedToken, /// Optional, It's possible to fetch rows form cursor to the table into: Option, }, @@ -4225,11 +4237,25 @@ impl fmt::Display for Statement { Statement::Fetch { name, direction, + from_or_in, into, } => { write!(f, "FETCH {direction} ")?; - write!(f, "IN {name}")?; + match &from_or_in.0.token { + Token::Word(w) => match w.keyword { + Keyword::FROM => { + write!(f, "FROM {name}")?; + } + Keyword::IN => { + write!(f, "IN {name}")?; + } + _ => unreachable!(), + }, + _ => { + unreachable!() + } + } if let Some(into) = into { write!(f, " INTO {into}")?; @@ -4488,6 +4514,11 @@ impl fmt::Display for Statement { Ok(()) } Statement::Delete(delete) => write!(f, "{delete}"), + Statement::Open { cursor_name } => { + write!(f, "OPEN {cursor_name}")?; + + Ok(()) + } Statement::Close { cursor } => { write!(f, "CLOSE {cursor}")?; diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 93de5fff2..fd6fdcdbc 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -364,6 +364,7 @@ impl Spanned for Statement { from_query: _, partition: _, } => Span::empty(), + Statement::Open { cursor_name } => cursor_name.span, Statement::Close { cursor } => match cursor { CloseCursor::All => Span::empty(), CloseCursor::Specific { name } => name.span, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 0546548af..b808c6476 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -570,6 +570,10 @@ impl<'a> Parser<'a> { Keyword::ALTER => self.parse_alter(), Keyword::CALL => self.parse_call(), Keyword::COPY => self.parse_copy(), + Keyword::OPEN => { + self.prev_token(); + self.parse_open() + } Keyword::CLOSE => self.parse_close(), Keyword::SET => self.parse_set(), Keyword::SHOW => self.parse_show(), @@ -6609,7 +6613,13 @@ impl<'a> Parser<'a> { } }; - self.expect_one_of_keywords(&[Keyword::FROM, Keyword::IN])?; + let from_or_in_token = if self.peek_keyword(Keyword::FROM) { + self.expect_keyword(Keyword::FROM)? + } else if self.peek_keyword(Keyword::IN) { + self.expect_keyword(Keyword::IN)? + } else { + return parser_err!("Expected FROM or IN", self.peek_token().span.start); + }; let name = self.parse_identifier()?; @@ -6622,6 +6632,7 @@ impl<'a> Parser<'a> { Ok(Statement::Fetch { name, direction, + from_or_in: AttachedToken(from_or_in_token), into, }) } @@ -8735,6 +8746,14 @@ impl<'a> Parser<'a> { }) } + /// Parse [Statement::Open] + fn parse_open(&mut self) -> Result { + self.expect_keyword(Keyword::OPEN)?; + Ok(Statement::Open { + cursor_name: self.parse_identifier()?, + }) + } + pub fn parse_close(&mut self) -> Result { let cursor = if self.parse_keyword(Keyword::ALL) { CloseCursor::All diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index ef6103474..a8e88032c 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -1393,6 +1393,24 @@ fn parse_mssql_declare() { let _ = ms().verified_stmt(declare_cursor_for_select); } +#[test] +fn test_mssql_cursor() { + let full_cursor_usage = "\ + DECLARE Employee_Cursor CURSOR FOR \ + SELECT LastName, FirstName \ + FROM AdventureWorks2022.HumanResources.vEmployee \ + WHERE LastName LIKE 'B%'; \ + \ + OPEN Employee_Cursor; \ + \ + FETCH NEXT FROM Employee_Cursor; \ + \ + CLOSE Employee_Cursor; \ + DEALLOCATE Employee_Cursor\ + "; + let _ = ms().statements_parse_to(full_cursor_usage, 5, ""); +} + #[test] fn test_parse_raiserror() { let sql = r#"RAISERROR('This is a test', 16, 1)"#; From f1e8ac7b356320fb6f1c9496c9d6cb996603a41d Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Mon, 28 Apr 2025 13:30:22 -0400 Subject: [PATCH 02/12] Add `statements_parse_to` helper --- src/test_utils.rs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/test_utils.rs b/src/test_utils.rs index 6270ac42b..ca5311900 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -151,6 +151,8 @@ impl TestedDialects { /// /// 2. re-serializing the result of parsing `sql` produces the same /// `canonical` sql string + /// + /// For multiple statements, use [`statements_parse_to`]. pub fn one_statement_parses_to(&self, sql: &str, canonical: &str) -> Statement { let mut statements = self.parse_sql_statements(sql).expect(sql); assert_eq!(statements.len(), 1); @@ -166,6 +168,30 @@ impl TestedDialects { only_statement } + /// The same as [`one_statement_parses_to`] but it works for a multiple statements + pub fn statements_parse_to( + &self, + sql: &str, + statement_count: usize, + canonical: &str, + ) -> Vec { + let statements = self.parse_sql_statements(sql).expect(sql); + assert_eq!(statements.len(), statement_count); + if !canonical.is_empty() && sql != canonical { + assert_eq!(self.parse_sql_statements(canonical).unwrap(), statements); + } else { + assert_eq!( + sql, + statements + .iter() + .map(|s| s.to_string()) + .collect::>() + .join("; ") + ); + } + statements + } + /// Ensures that `sql` parses as an [`Expr`], and that /// re-serializing the parse result produces canonical pub fn expr_parses_to(&self, sql: &str, canonical: &str) -> Expr { From 5ec1463d7488337077879e058bd47488d00b7f05 Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Fri, 25 Apr 2025 13:40:07 -0400 Subject: [PATCH 03/12] Add support for parsing `WHILE` statements - it's a conditional block alongside IF & CASE --- src/ast/mod.rs | 41 +++++++++++++++++++++++++++++++++++++++- src/ast/spans.rs | 11 ++++++++++- src/keywords.rs | 1 + src/parser/mod.rs | 39 +++++++++++++++++++++++++++++++++++--- tests/sqlparser_mssql.rs | 30 ++++++++++++++++++++++++++++- 5 files changed, 116 insertions(+), 6 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index ee8931c12..d3171d53e 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -2226,7 +2226,33 @@ impl fmt::Display for IfStatement { } } -/// A block within a [Statement::Case] or [Statement::If]-like statement +/// A `WHILE` statement. +/// +/// Example: +/// ```sql +/// WHILE @@FETCH_STATUS = 0 +/// BEGIN +/// FETCH NEXT FROM c1 INTO @var1, @var2; +/// END +/// ``` +/// +/// [MsSql](https://learn.microsoft.com/en-us/sql/t-sql/language-elements/while-transact-sql) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct WhileStatement { + pub while_block: ConditionalStatementBlock, +} + +impl fmt::Display for WhileStatement { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let WhileStatement { while_block } = self; + write!(f, "{while_block}")?; + Ok(()) + } +} + +/// A block within a [Statement::Case] or [Statement::If] or [Statement::While]-like statement /// /// Example 1: /// ```sql @@ -2242,6 +2268,14 @@ impl fmt::Display for IfStatement { /// ```sql /// ELSE SELECT 1; SELECT 2; /// ``` +/// +/// Example 4: +/// ```sql +/// WHILE @@FETCH_STATUS = 0 +/// BEGIN +/// FETCH NEXT FROM c1 INTO @var1, @var2; +/// END +/// ``` #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -2981,6 +3015,8 @@ pub enum Statement { Case(CaseStatement), /// An `IF` statement. If(IfStatement), + /// A `WHILE` statement. + While(WhileStatement), /// A `RAISE` statement. Raise(RaiseStatement), /// ```sql @@ -4345,6 +4381,9 @@ impl fmt::Display for Statement { Statement::If(stmt) => { write!(f, "{stmt}") } + Statement::While(stmt) => { + write!(f, "{stmt}") + } Statement::Raise(stmt) => { write!(f, "{stmt}") } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index fd6fdcdbc..116844f0f 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -36,7 +36,7 @@ use super::{ ReferentialAction, RenameSelectItem, ReplaceSelectElement, ReplaceSelectItem, Select, SelectInto, SelectItem, SetExpr, SqlOption, Statement, Subscript, SymbolDefinition, TableAlias, TableAliasColumnDef, TableConstraint, TableFactor, TableObject, TableOptionsClustered, - TableWithJoins, UpdateTableFromKind, Use, Value, Values, ViewColumnDef, + TableWithJoins, UpdateTableFromKind, Use, Value, Values, ViewColumnDef, WhileStatement, WildcardAdditionalOptions, With, WithFill, }; @@ -338,6 +338,7 @@ impl Spanned for Statement { } => source.span(), Statement::Case(stmt) => stmt.span(), Statement::If(stmt) => stmt.span(), + Statement::While(stmt) => stmt.span(), Statement::Raise(stmt) => stmt.span(), Statement::Call(function) => function.span(), Statement::Copy { @@ -775,6 +776,14 @@ impl Spanned for IfStatement { } } +impl Spanned for WhileStatement { + fn span(&self) -> Span { + let WhileStatement { while_block } = self; + + while_block.span() + } +} + impl Spanned for ConditionalStatements { fn span(&self) -> Span { match self { diff --git a/src/keywords.rs b/src/keywords.rs index 4eaad7ed2..c2985283c 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -981,6 +981,7 @@ define_keywords!( WHEN, WHENEVER, WHERE, + WHILE, WIDTH_BUCKET, WINDOW, WITH, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index b808c6476..c827665bd 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -536,6 +536,10 @@ impl<'a> Parser<'a> { self.prev_token(); self.parse_if_stmt() } + Keyword::WHILE => { + self.prev_token(); + self.parse_while() + } Keyword::RAISE => { self.prev_token(); self.parse_raise_stmt() @@ -704,8 +708,18 @@ impl<'a> Parser<'a> { })) } + /// Parse a `WHILE` statement. + /// + /// See [Statement::While] + fn parse_while(&mut self) -> Result { + self.expect_keyword_is(Keyword::WHILE)?; + let while_block = self.parse_conditional_statement_block(&[Keyword::END])?; + + Ok(Statement::While(WhileStatement { while_block })) + } + /// Parses an expression and associated list of statements - /// belonging to a conditional statement like `IF` or `WHEN`. + /// belonging to a conditional statement like `IF` or `WHEN` or `WHILE`. /// /// Example: /// ```sql @@ -720,6 +734,10 @@ impl<'a> Parser<'a> { let condition = match &start_token.token { Token::Word(w) if w.keyword == Keyword::ELSE => None, + Token::Word(w) if w.keyword == Keyword::WHILE => { + let expr = self.parse_expr()?; + Some(expr) + } _ => { let expr = self.parse_expr()?; then_token = Some(AttachedToken(self.expect_keyword(Keyword::THEN)?)); @@ -727,13 +745,25 @@ impl<'a> Parser<'a> { } }; - let statements = self.parse_statement_list(terminal_keywords)?; + let conditional_statements = if self.peek_keyword(Keyword::BEGIN) { + let begin_token = self.expect_keyword(Keyword::BEGIN)?; + let statements = self.parse_statement_list(terminal_keywords)?; + let end_token = self.expect_keyword(Keyword::END)?; + ConditionalStatements::BeginEnd(BeginEndStatements { + begin_token: AttachedToken(begin_token), + statements, + end_token: AttachedToken(end_token), + }) + } else { + let statements = self.parse_statement_list(terminal_keywords)?; + ConditionalStatements::Sequence { statements } + }; Ok(ConditionalStatementBlock { start_token: AttachedToken(start_token), condition, then_token, - conditional_statements: ConditionalStatements::Sequence { statements }, + conditional_statements, }) } @@ -4457,6 +4487,9 @@ impl<'a> Parser<'a> { break; } } + if let Token::EOF = self.peek_nth_token_ref(0).token { + break; + } values.push(self.parse_statement()?); self.expect_token(&Token::SemiColon)?; } diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index a8e88032c..78cdeb7e4 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -1405,10 +1405,38 @@ fn test_mssql_cursor() { \ FETCH NEXT FROM Employee_Cursor; \ \ + WHILE @@FETCH_STATUS = 0 \ + BEGIN \ + FETCH NEXT FROM Employee_Cursor; \ + END; \ + \ CLOSE Employee_Cursor; \ DEALLOCATE Employee_Cursor\ "; - let _ = ms().statements_parse_to(full_cursor_usage, 5, ""); + let _ = ms().statements_parse_to(full_cursor_usage, 6, ""); +} + +#[test] +fn test_mssql_while_statement() { + let while_single_statement = "WHILE 1 = 0 PRINT 'Hello World';"; + let _ = ms().verified_stmt(while_single_statement); + + let while_begin_end = "\ + WHILE @@FETCH_STATUS = 0 \ + BEGIN \ + FETCH NEXT FROM Employee_Cursor; \ + END\ + "; + let _ = ms().verified_stmt(while_begin_end); + + let while_begin_end_multiple_statements = "\ + WHILE @@FETCH_STATUS = 0 \ + BEGIN \ + FETCH NEXT FROM Employee_Cursor; \ + PRINT 'Hello World'; \ + END\ + "; + let _ = ms().verified_stmt(while_begin_end_multiple_statements); } #[test] From a72e1c13add1426800e3dad0ed645768ea6ff6b7 Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Tue, 29 Apr 2025 13:05:43 -0400 Subject: [PATCH 04/12] Make `OPEN` reserved for table aliases - this is useful since opening a cursor typically happens immediately after declaring the cursor's query --- src/keywords.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/keywords.rs b/src/keywords.rs index c2985283c..9986de97a 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -1065,6 +1065,8 @@ pub const RESERVED_FOR_TABLE_ALIAS: &[Keyword] = &[ Keyword::SAMPLE, Keyword::TABLESAMPLE, Keyword::FROM, + // Reserved for SQL Server cursors + Keyword::OPEN, ]; /// Can't be used as a column alias, so that `SELECT alias` From 01d85a0c1bb07f421f2ca8fa4f4f91cbfc053989 Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Tue, 29 Apr 2025 16:36:37 -0400 Subject: [PATCH 05/12] Introduce `FetchPosition` to simplify FROM vs IN --- src/ast/mod.rs | 46 +++++++++++++++++++++++++--------------------- src/parser/mod.rs | 10 ++++++---- 2 files changed, 31 insertions(+), 25 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index d3171d53e..b7bf7b5d9 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3447,10 +3447,7 @@ pub enum Statement { /// Cursor name name: Ident, direction: FetchDirection, - /// Differentiate between dialects that fetch `FROM` vs fetch `IN` - /// - /// [MsSql](https://learn.microsoft.com/en-us/sql/t-sql/language-elements/fetch-transact-sql) - from_or_in: AttachedToken, + position: FetchPosition, /// Optional, It's possible to fetch rows form cursor to the table into: Option, }, @@ -4273,25 +4270,10 @@ impl fmt::Display for Statement { Statement::Fetch { name, direction, - from_or_in, + position, into, } => { - write!(f, "FETCH {direction} ")?; - - match &from_or_in.0.token { - Token::Word(w) => match w.keyword { - Keyword::FROM => { - write!(f, "FROM {name}")?; - } - Keyword::IN => { - write!(f, "IN {name}")?; - } - _ => unreachable!(), - }, - _ => { - unreachable!() - } - } + write!(f, "FETCH {direction} {position} {name}")?; if let Some(into) = into { write!(f, " INTO {into}")?; @@ -6232,6 +6214,28 @@ impl fmt::Display for FetchDirection { } } +/// The "position" for a FETCH statement. +/// +/// [MsSql](https://learn.microsoft.com/en-us/sql/t-sql/language-elements/fetch-transact-sql) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum FetchPosition { + From, + In, +} + +impl fmt::Display for FetchPosition { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + FetchPosition::From => f.write_str("FROM")?, + FetchPosition::In => f.write_str("IN")?, + }; + + Ok(()) + } +} + /// A privilege on a database object (table, sequence, etc.). #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] diff --git a/src/parser/mod.rs b/src/parser/mod.rs index c827665bd..7f358b598 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -6646,10 +6646,12 @@ impl<'a> Parser<'a> { } }; - let from_or_in_token = if self.peek_keyword(Keyword::FROM) { - self.expect_keyword(Keyword::FROM)? + let position = if self.peek_keyword(Keyword::FROM) { + self.expect_keyword(Keyword::FROM)?; + FetchPosition::From } else if self.peek_keyword(Keyword::IN) { - self.expect_keyword(Keyword::IN)? + self.expect_keyword(Keyword::IN)?; + FetchPosition::In } else { return parser_err!("Expected FROM or IN", self.peek_token().span.start); }; @@ -6665,7 +6667,7 @@ impl<'a> Parser<'a> { Ok(Statement::Fetch { name, direction, - from_or_in: AttachedToken(from_or_in_token), + position, into, }) } From 99657770ca628488f74e5d77d1633c81a9672ca8 Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Tue, 29 Apr 2025 16:37:27 -0400 Subject: [PATCH 06/12] Remove unnecessary comment Co-authored-by: Ifeanyi Ubah --- src/keywords.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/keywords.rs b/src/keywords.rs index 9986de97a..eff0dad28 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -1065,7 +1065,6 @@ pub const RESERVED_FOR_TABLE_ALIAS: &[Keyword] = &[ Keyword::SAMPLE, Keyword::TABLESAMPLE, Keyword::FROM, - // Reserved for SQL Server cursors Keyword::OPEN, ]; From 648c0244ec0a9690a25a1a47691d2367345aef8b Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Tue, 29 Apr 2025 16:49:13 -0400 Subject: [PATCH 07/12] Introduce a `OpenStatement` struct for the `OPEN` statement --- src/ast/mod.rs | 26 +++++++++++++++++--------- src/ast/spans.rs | 23 +++++++++++++++-------- src/parser/mod.rs | 4 ++-- 3 files changed, 34 insertions(+), 19 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index b7bf7b5d9..b93b2eaa6 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3071,10 +3071,7 @@ pub enum Statement { /// OPEN cursor_name /// ``` /// Opens a cursor. - Open { - /// Cursor name - cursor_name: Ident, - }, + Open(OpenStatement), /// ```sql /// CLOSE /// ``` @@ -4535,11 +4532,7 @@ impl fmt::Display for Statement { Ok(()) } Statement::Delete(delete) => write!(f, "{delete}"), - Statement::Open { cursor_name } => { - write!(f, "OPEN {cursor_name}")?; - - Ok(()) - } + Statement::Open(open) => write!(f, "{open}"), Statement::Close { cursor } => { write!(f, "CLOSE {cursor}")?; @@ -9390,6 +9383,21 @@ pub enum ReturnStatementValue { Expr(Expr), } +/// Represents an `OPEN` statement. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct OpenStatement { + /// Cursor name + pub cursor_name: Ident, +} + +impl fmt::Display for OpenStatement { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "OPEN {}", self.cursor_name) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 116844f0f..13de56854 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -31,13 +31,13 @@ use super::{ FunctionArguments, GroupByExpr, HavingBound, IfStatement, IlikeSelectItem, Insert, Interpolate, InterpolateExpr, Join, JoinConstraint, JoinOperator, JsonPath, JsonPathElem, LateralView, LimitClause, MatchRecognizePattern, Measure, NamedWindowDefinition, ObjectName, ObjectNamePart, - Offset, OnConflict, OnConflictAction, OnInsert, OrderBy, OrderByExpr, OrderByKind, Partition, - PivotValueSource, ProjectionSelect, Query, RaiseStatement, RaiseStatementValue, - ReferentialAction, RenameSelectItem, ReplaceSelectElement, ReplaceSelectItem, Select, - SelectInto, SelectItem, SetExpr, SqlOption, Statement, Subscript, SymbolDefinition, TableAlias, - TableAliasColumnDef, TableConstraint, TableFactor, TableObject, TableOptionsClustered, - TableWithJoins, UpdateTableFromKind, Use, Value, Values, ViewColumnDef, WhileStatement, - WildcardAdditionalOptions, With, WithFill, + Offset, OnConflict, OnConflictAction, OnInsert, OpenStatement, OrderBy, OrderByExpr, + OrderByKind, Partition, PivotValueSource, ProjectionSelect, Query, RaiseStatement, + RaiseStatementValue, ReferentialAction, RenameSelectItem, ReplaceSelectElement, + ReplaceSelectItem, Select, SelectInto, SelectItem, SetExpr, SqlOption, Statement, Subscript, + SymbolDefinition, TableAlias, TableAliasColumnDef, TableConstraint, TableFactor, TableObject, + TableOptionsClustered, TableWithJoins, UpdateTableFromKind, Use, Value, Values, ViewColumnDef, + WhileStatement, WildcardAdditionalOptions, With, WithFill, }; /// Given an iterator of spans, return the [Span::union] of all spans. @@ -365,7 +365,7 @@ impl Spanned for Statement { from_query: _, partition: _, } => Span::empty(), - Statement::Open { cursor_name } => cursor_name.span, + Statement::Open(open) => open.span(), Statement::Close { cursor } => match cursor { CloseCursor::All => Span::empty(), CloseCursor::Specific { name } => name.span, @@ -2305,6 +2305,13 @@ impl Spanned for BeginEndStatements { } } +impl Spanned for OpenStatement { + fn span(&self) -> Span { + let OpenStatement { cursor_name } = self; + cursor_name.span + } +} + #[cfg(test)] pub mod tests { use crate::dialect::{Dialect, GenericDialect, SnowflakeDialect}; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 7f358b598..8357a8a12 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8784,9 +8784,9 @@ impl<'a> Parser<'a> { /// Parse [Statement::Open] fn parse_open(&mut self) -> Result { self.expect_keyword(Keyword::OPEN)?; - Ok(Statement::Open { + Ok(Statement::Open(OpenStatement { cursor_name: self.parse_identifier()?, - }) + })) } pub fn parse_close(&mut self) -> Result { From bf0036a5323ec29048052ca43edefdd391965ce7 Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Tue, 29 Apr 2025 17:31:12 -0400 Subject: [PATCH 08/12] Expand test example to assert AST --- tests/sqlparser_mssql.rs | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 78cdeb7e4..4e6639026 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -23,7 +23,8 @@ mod test_utils; use helpers::attached_token::AttachedToken; -use sqlparser::tokenizer::{Location, Span}; +use sqlparser::keywords::Keyword; +use sqlparser::tokenizer::{Location, Span, Token, TokenWithSpan, Word}; use test_utils::*; use sqlparser::ast::DataType::{Int, Text, Varbinary}; @@ -1419,7 +1420,40 @@ fn test_mssql_cursor() { #[test] fn test_mssql_while_statement() { let while_single_statement = "WHILE 1 = 0 PRINT 'Hello World';"; - let _ = ms().verified_stmt(while_single_statement); + let stmt = ms().verified_stmt(while_single_statement); + assert_eq!( + stmt, + Statement::While(sqlparser::ast::WhileStatement { + while_block: ConditionalStatementBlock { + start_token: AttachedToken(TokenWithSpan { + token: Token::Word(Word { + value: "WHILE".to_string(), + quote_style: None, + keyword: Keyword::WHILE + }), + span: Span::empty() + }), + condition: Some(Expr::BinaryOp { + left: Box::new(Expr::Value( + (Value::Number("1".parse().unwrap(), false)).with_empty_span() + )), + op: BinaryOperator::Eq, + right: Box::new(Expr::Value( + (Value::Number("0".parse().unwrap(), false)).with_empty_span() + )), + }), + then_token: None, + conditional_statements: ConditionalStatements::Sequence { + statements: vec![Statement::Print(PrintStatement { + message: Box::new(Expr::Value( + (Value::SingleQuotedString("Hello World".to_string())) + .with_empty_span() + )), + }),], + } + } + }) + ); let while_begin_end = "\ WHILE @@FETCH_STATUS = 0 \ From 2941291b336d4bffb0097c7d106804fbb55d80e7 Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Tue, 29 Apr 2025 17:36:36 -0400 Subject: [PATCH 09/12] Add test for `OPEN` statement --- tests/sqlparser_common.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index fa2346c2c..0d5850ab3 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -15103,3 +15103,15 @@ fn parse_return() { let _ = all_dialects().verified_stmt("RETURN 1"); } + +#[test] +fn test_open() { + let open_cursor = "OPEN Employee_Cursor"; + let stmt = all_dialects().verified_stmt(open_cursor); + assert_eq!( + stmt, + Statement::Open(OpenStatement { + cursor_name: Ident::new("Employee_Cursor"), + }) + ); +} From 3d2001f9b3ed6b26db35a36bf93e915bc0ba40ca Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Tue, 29 Apr 2025 17:40:21 -0400 Subject: [PATCH 10/12] Merge conditions into single `match` --- src/parser/mod.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 8357a8a12..a4720ad5b 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -4482,14 +4482,16 @@ impl<'a> Parser<'a> { ) -> Result, ParserError> { let mut values = vec![]; loop { - if let Token::Word(w) = &self.peek_nth_token_ref(0).token { - if w.quote_style.is_none() && terminal_keywords.contains(&w.keyword) { - break; + match &self.peek_nth_token_ref(0).token { + Token::EOF => break, + Token::Word(w) => { + if w.quote_style.is_none() && terminal_keywords.contains(&w.keyword) { + break; + } } + _ => {} } - if let Token::EOF = self.peek_nth_token_ref(0).token { - break; - } + values.push(self.parse_statement()?); self.expect_token(&Token::SemiColon)?; } From dbf7606b4053afb9f206d43c3b925f6cdd2e3365 Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Tue, 29 Apr 2025 18:08:04 -0400 Subject: [PATCH 11/12] Fix incorrect trailing comma --- tests/sqlparser_mssql.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 4e6639026..81afc5699 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -223,7 +223,7 @@ fn parse_create_function() { value: Some(ReturnStatementValue::Expr(Expr::Value( (number("1")).with_empty_span() ))), - }),], + })], end_token: AttachedToken::empty(), })), behavior: None, @@ -1449,7 +1449,7 @@ fn test_mssql_while_statement() { (Value::SingleQuotedString("Hello World".to_string())) .with_empty_span() )), - }),], + })], } } }) From 3608d8c736747cde836264f4588a702491486e62 Mon Sep 17 00:00:00 2001 From: Andrew Harper Date: Wed, 30 Apr 2025 10:34:11 -0400 Subject: [PATCH 12/12] Remove statement count assertion from `statements_parse_to` --- src/test_utils.rs | 8 +------- tests/sqlparser_mssql.rs | 2 +- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/test_utils.rs b/src/test_utils.rs index ca5311900..3c22fa911 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -169,14 +169,8 @@ impl TestedDialects { } /// The same as [`one_statement_parses_to`] but it works for a multiple statements - pub fn statements_parse_to( - &self, - sql: &str, - statement_count: usize, - canonical: &str, - ) -> Vec { + pub fn statements_parse_to(&self, sql: &str, canonical: &str) -> Vec { let statements = self.parse_sql_statements(sql).expect(sql); - assert_eq!(statements.len(), statement_count); if !canonical.is_empty() && sql != canonical { assert_eq!(self.parse_sql_statements(canonical).unwrap(), statements); } else { diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 81afc5699..36317f3dc 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -1414,7 +1414,7 @@ fn test_mssql_cursor() { CLOSE Employee_Cursor; \ DEALLOCATE Employee_Cursor\ "; - let _ = ms().statements_parse_to(full_cursor_usage, 6, ""); + let _ = ms().statements_parse_to(full_cursor_usage, ""); } #[test]