-
Notifications
You must be signed in to change notification settings - Fork 606
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 { | ||
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. I placed this next to 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. Could we wrap this new statement in a named struct? 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 👍. I didn't do that originally so as to more closely mimic the existing code |
||
/// 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, | ||
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. Not sure what's best here, it could also be two separate Optional fields 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. Could we represent it with an explicit enum? enum FetchPosition {
From
In
} 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 👍 |
||
/// 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; | ||
} | ||
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. can we collapse this into above to use a match statement? match &self.peek_nth_token_ref(0).token {
Token::Word(n) if ...
Token::Eof
} 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 👍. I was probably trying to minimize the diff for review here |
||
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); | ||
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. this assertion seems to already be covered by the if/else below? so that we can skip the statement_count argument requirement? 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. Hm, I don't fully understand. Without this line you can't guarantee that the string you feed in has exactly the number of statements you intend it to parse. Also, 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. Ah so I meant that in both cases when asserting : in this case we're already explicitly check that both statements lists are identical assert_eq!(self.parse_sql_statements(canonical).unwrap(), statements); Then in this case, we're doing so implicitly, reconstructing the input sql based off the returned statement assert_eq!(
sql,
statements
.iter()
.map(|s| s.to_string())
.collect::<Vec<_>>()
.join("; ")
); So that i imagine it shouldn't be possible for the count assertion to fail and either of the subsequent assertion to pass?
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.
I will remove the assertion here to get this branch merged. However, I think removing it removes a level of safety that is beneficial. Part of my thinking here is motivated by my upcoming branch on making semi colon statement delimiters optional. So any code that is making assumptions about "number of statements" becomes even more useful. But perhaps that branch can re-introduce that assertion if necessary. 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. BTW, this helper was introduced over on the GO branch, does your opinion change at all seeing the usage over there? |
||
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.