-
Notifications
You must be signed in to change notification settings - Fork 644
Improve support for cursors for SQL Server #1831
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
ac298a6
f1e8ac7
5ec1463
a72e1c1
01d85a0
9965777
648c024
bf0036a
2941291
3d2001f
dbf7606
3608d8c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -3032,6 +3068,14 @@ pub enum Statement { | |
partition: Option<Box<Expr>>, | ||
}, | ||
/// ```sql | ||
/// OPEN cursor_name | ||
/// ``` | ||
/// Opens a cursor. | ||
Open { | ||
|
||
/// Cursor name | ||
cursor_name: Ident, | ||
}, | ||
/// ```sql | ||
/// CLOSE | ||
/// ``` | ||
/// Closes the portal underlying an open cursor. | ||
|
@@ -3403,6 +3447,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<ObjectName>, | ||
}, | ||
|
@@ -4225,11 +4273,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}")?; | ||
|
@@ -4319,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}") | ||
} | ||
|
@@ -4488,6 +4553,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}")?; | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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() | ||
|
@@ -570,6 +574,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(), | ||
|
@@ -700,8 +708,18 @@ impl<'a> Parser<'a> { | |
})) | ||
} | ||
|
||
/// Parse a `WHILE` statement. | ||
/// | ||
/// See [Statement::While] | ||
fn parse_while(&mut self) -> Result<Statement, ParserError> { | ||
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 | ||
|
@@ -716,20 +734,36 @@ 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)?)); | ||
Some(expr) | ||
} | ||
}; | ||
|
||
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)?; | ||
Comment on lines
+749
to
+751
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
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, | ||
}) | ||
} | ||
|
||
|
@@ -4453,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)?; | ||
} | ||
|
@@ -6609,7 +6646,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 +6665,7 @@ impl<'a> Parser<'a> { | |
Ok(Statement::Fetch { | ||
name, | ||
direction, | ||
from_or_in: AttachedToken(from_or_in_token), | ||
into, | ||
}) | ||
} | ||
|
@@ -8735,6 +8779,14 @@ impl<'a> Parser<'a> { | |
}) | ||
} | ||
|
||
/// Parse [Statement::Open] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe I missed it, we seem to be lacking test cases for the open statement feature? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's part of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done 👍 |
||
fn parse_open(&mut self) -> Result<Statement, ParserError> { | ||
self.expect_keyword(Keyword::OPEN)?; | ||
Ok(Statement::Open { | ||
cursor_name: self.parse_identifier()?, | ||
}) | ||
} | ||
|
||
pub fn parse_close(&mut self) -> Result<Statement, ParserError> { | ||
let cursor = if self.parse_keyword(Keyword::ALL) { | ||
CloseCursor::All | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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<Statement> { | ||
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::<Vec<_>>() | ||
.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 { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't absolutely need a WhileStatement struct; we could be doing
Statement::While(ConditionalStatementBlock)
instead. I'm following the example of CASE & IF, which also do it this way.