-
Notifications
You must be signed in to change notification settings - Fork 602
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
Conversation
- parse `OPEN cursor_name` statements - enable `FETCH` statements to parse `FROM cursor_name`, in addition to the existing `IN` parsing
- it's a conditional block alongside IF & CASE
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] | ||
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] | ||
pub struct WhileStatement { | ||
pub while_block: ConditionalStatementBlock, |
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.
src/ast/mod.rs
Outdated
/// OPEN cursor_name | ||
/// ``` | ||
/// Opens a cursor. | ||
Open { |
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.
I placed this next to CLOSE
because they're semantically paired, rather than alphabetical. Not sure what's preferred on this project
src/ast/mod.rs
Outdated
/// 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 comment
The 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 comment
The reason will be displayed to describe this comment to others. Learn more.
Could we represent it with an explicit enum?
e.g
enum FetchPosition {
From
In
}
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.
Done 👍
- this is useful since opening a cursor typically happens immediately after declaring the cursor's query
src/ast/mod.rs
Outdated
/// OPEN cursor_name | ||
/// ``` | ||
/// Opens a cursor. | ||
Open { |
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.
Could we wrap this new statement in a named struct?
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.
Done 👍. I didn't do that originally so as to more closely mimic the existing code
src/ast/mod.rs
Outdated
/// 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 comment
The reason will be displayed to describe this comment to others. Learn more.
Could we represent it with an explicit enum?
e.g
enum FetchPosition {
From
In
}
|
||
#[test] | ||
fn test_mssql_while_statement() { | ||
let while_single_statement = "WHILE 1 = 0 PRINT 'Hello World';"; |
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.
Can we include a test case with multiple statements in the while block?
Also since this introduces a new statement, can we include a test case that asserts the returned AST?
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.
Can we include a test case with multiple statements in the while block?
This is covered by the additional subsequent examples
Also since this introduces a new statement, can we include a test case that asserts the returned AST?
Yes, I'll do that here for the initial example case
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.
Done 👍
src/test_utils.rs
Outdated
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 comment
The 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 comment
The 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, one_statement_parses_to
has this same assertion before the if/else.
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.
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?
one_statement_parse_to
uses the count internally and there it makes sense to sanity check that since the expected count is always one, in this case we're exposing the count as a function argument which makes for suboptimal API that the user has to manually increment/decrement a counter when the sql changes. So that if the count argument isn't strictly necessary we would want to skip it
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.
Then in this case, we're doing so implicitly, reconstructing the input sql based off the returned statement ... So that i imagine it shouldn't be possible for the count assertion to fail and either of the subsequent assertion to pass?
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 comment
The 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?
@@ -8735,6 +8779,14 @@ impl<'a> Parser<'a> { | |||
}) | |||
} | |||
|
|||
/// Parse [Statement::Open] |
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.
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 comment
The reason will be displayed to describe this comment to others. Learn more.
It's part of test_mssql_cursor
, but I'll make a separate test function just for OPEN
for clarity
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.
Done 👍
src/parser/mod.rs
Outdated
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 comment
The 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 comment
The reason will be displayed to describe this comment to others. Learn more.
Done 👍. I was probably trying to minimize the diff for review here
e0b484e
to
bf0036a
Compare
write!(f, "FETCH {direction} ")?; | ||
|
||
write!(f, "IN {name}")?; | ||
write!(f, "FETCH {direction} {position} {name}")?; |
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.
This could probably be write!(f, "{position} {name}")?;
, not sure what the pro/con is on that
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.
LGTM! Thanks @aharpervc!
cc @alamb
let begin_token = self.expect_keyword(Keyword::BEGIN)?; | ||
let statements = self.parse_statement_list(terminal_keywords)?; | ||
let end_token = self.expect_keyword(Keyword::END)?; |
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.
This PR is a followup to #1821 to address several difficulties I had parsing real world SQL files.
There are 3 related enhancements here:
OPEN
statements, eg:OPEN my_cursor
FETCH
statement parsing support theFROM
keyword. Eg,FETCH NEXT FROM my_cursor
. The logic formerly only supported "FETCH NEXT IN" syntaxWHILE
statements, which is commonly used in conjunction with cursors. EgWHILE @@fetch_status = 0...
. This is a conditional statement block, much like IF & CASE, so that code has been structured similarly and placed adjacent to those statements.(4th thing -- a test helper introduced in this PR was brought over here, because it simplifies validating a test case. If the other PR merges first that commit can be dropped out of this branch).
The effect of these changes is that this cursor example from the SQL Server FETCH documentation page now successfully parses: