Skip to content

Add support for GO batch delimiter in SQL Server #1809

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

Open
wants to merge 16 commits into
base: main
Choose a base branch
from

Conversation

aharpervc
Copy link
Contributor

@aharpervc aharpervc commented Apr 11, 2025

Reference: https://learn.microsoft.com/en-us/sql/t-sql/language-elements/sql-server-utilities-statements-go

Lots of conventional SQL Server tooling supports GO, so it seems really useful & prudent to be able to parse it. I realize that strictly speaking, it isn't a SQL "statement". However, I think it makes sense to model here as a statement since it acts as one in all regards except documentation.

In terms of parsing, GO has some peculiarities I tried to express with the new tests, such as with regards to position and basically treating newline as a delimiter. This also starts to nudge on #1800, where newlines may possibly be interpreted as statement delimiters in a general case. However, my approach here for GO is isolated to only that statement, at least for the time being.


Also, this will conflict with #1808 when either is merged, because I'm adding things that end up being adjacent to each other which can confuse git, depending on merge strategy. There's no actual conflict.

@aharpervc aharpervc marked this pull request as ready for review April 11, 2025 16:52
src/ast/mod.rs Outdated
Comment on lines 4058 to 4060
Go {
count: Option<u64>,
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Repr wise can we wrap the statement body in explicit structs? we're transitioning away from anonymous structs in order to make the API more ergonomic. Something like

struct GoStatement {
    count: Option<u64>
}
Statement::Go(GoStatement)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done 👍

Comment on lines 478 to 481
// Treat batch delimiter as an end of statement
if expecting_statement_delimiter && dialect_of!(self is MsSqlDialect) {
if let Some(Statement::Go { count: _ }) = stmts.last() {
expecting_statement_delimiter = false;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure I followed this part, what was the intention?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to explain this in the commit message & the "multiple_gos" example in the test. Basically, we want this to parse as 4 statements:

SELECT 1;
GO
SELECT 2;
GO

The second GO is terminated by EOF, so no problem. But the first GO cannot end with a semi colon (This overlaps with the ongoing "no semi-colons for statements" discussion but from a different angle). So we need to figure out how to parse it all out without the existing conventional semi-colon logic.

What I came up with is to basically just look for a GO and have that signal that we aren't then also looking for statement delimiter (eg, semi-colon). This also makes sense semantically because statements can't extend between batches, so any batch delimiter also functions as a statement delimiter.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In terms of the code, I worked out a perhaps slightly less confusing way to set whether or not a semi-colon is expected, here.

This branch can be updated accordingly if we agree over there

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've now done that due to another problem I noticed, for IF statements (new test case also added...)

@@ -617,6 +623,9 @@ impl<'a> Parser<'a> {
}
// `COMMENT` is snowflake specific https://docs.snowflake.com/en/sql-reference/sql/comment
Keyword::COMMENT if self.dialect.supports_comment_on() => self.parse_comment(),
Keyword::GO if dialect_of!(self is MsSqlDialect) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Keyword::GO if dialect_of!(self is MsSqlDialect) => {
Keyword::GO => {

I think it should be fine to let the parser always accept the statement if it shows up. Technically if we wanted to gate it behind a feature then we could use a self.dialect.supports flag since the dialect_of macro is deprecated, but I think it makes more sense to let the parser accept unconditionally

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done 👍

@@ -15017,6 +15026,48 @@ impl<'a> Parser<'a> {
}
}

fn parse_go(&mut self) -> Result<Statement, ParserError> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fn parse_go(&mut self) -> Result<Statement, ParserError> {
/// Parse [Statement::Go]
fn parse_go(&mut self) -> Result<Statement, ParserError> {

@@ -15017,6 +15026,48 @@ impl<'a> Parser<'a> {
}
}

fn parse_go(&mut self) -> Result<Statement, ParserError> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing we could do, in the parse_stmt() function could we call self.prev_token() to rewind the Go keyword so that this function becomes self contained in being able to parse a full Go statement?

@@ -15017,6 +15026,48 @@ impl<'a> Parser<'a> {
}
}

fn parse_go(&mut self) -> Result<Statement, ParserError> {
// previous token should be a newline (skipping non-newline whitespace)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm I didn't look too closely but not sure I followed the function body on a high level, from the docs and the representation in the parser the Go statement only contains an optional int, so that I imagined all the function needs to do would be to maybe_parse(|parser| parser.parse_number_value()) or similar? The function is currently managing whitespace and semicolon but not super clear to me why that is required

Copy link
Contributor Author

@aharpervc aharpervc Apr 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does my comment above & the code comment below, "using this peek function because we want to halt this statement parsing upon newline" help?

-- this should parse as `Go { Some(4) }`
GO 4

-- this should parse as something like `Go { None }`, `Expr(4)`
GO
4

Eg, the whitespace is meaningful for parsing

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah interesting, I think it would be very helpful to add this example to the code to illustrate the implications,

Then for the function, I think using the peek_token_no_skip could simplify the impl. Something roughly like the following? to avoid manually tracking token counts and lookbacks

let count = loop {
  match self.peek_token_no_skip() {
    Token::Whitespace(Whitespace::Newline) | Token::Whitespace(Whitespace::SingleLineComment) | Token::EOF => {
      self.advance_token();
      break None
    }
    Token::Whitespace(_) => {
      self.advance_token();
    }
    _ => {
      break Some(self.parse_number())
    }
  }
};

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I have something like that already for parsing the batch count, which doesn't need to look backwards and already uses peek_token_no_skip. I think between here & your other comment on the alias function, the concern is regarding the newline logic which looks backwards. I'll try and move that into a helper so it can be simplified.

Comment on lines 2100 to 2108
assert!(err.is_err());
assert_eq!(
err.unwrap_err().to_string(),
"sql parser error: Expected: newline before GO, found: ;"
);

let invalid_go_count = "SELECT 1\nGO x";
let err = ms().parse_sql_statements(invalid_go_count);
assert!(err.is_err());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assert!(err.is_err());
assert_eq!(
err.unwrap_err().to_string(),
"sql parser error: Expected: newline before GO, found: ;"
);
let invalid_go_count = "SELECT 1\nGO x";
let err = ms().parse_sql_statements(invalid_go_count);
assert!(err.is_err());
assert_eq!(
err.unwrap_err().to_string(),
"sql parser error: Expected: newline before GO, found: ;"
);
let invalid_go_count = "SELECT 1\nGO x";
let err = ms().parse_sql_statements(invalid_go_count);

since we're already unwrapping the error

Copy link
Contributor Author

@aharpervc aharpervc Apr 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes great point, done 👍

@aharpervc aharpervc force-pushed the mssql-go-keyword branch 2 times, most recently from 3dea67c to ea7db7d Compare April 14, 2025 15:19
@aharpervc aharpervc requested a review from iffyio April 14, 2025 15:23
@aharpervc aharpervc force-pushed the mssql-go-keyword branch 6 times, most recently from afe44a3 to fa964b5 Compare April 15, 2025 15:48
@aharpervc
Copy link
Contributor Author

Rebased on main & updated

@aharpervc aharpervc force-pushed the mssql-go-keyword branch 2 times, most recently from dc0bf6a to ed181fd Compare April 17, 2025 17:44
@@ -15017,6 +15026,48 @@ impl<'a> Parser<'a> {
}
}

fn parse_go(&mut self) -> Result<Statement, ParserError> {
// previous token should be a newline (skipping non-newline whitespace)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah interesting, I think it would be very helpful to add this example to the code to illustrate the implications,

Then for the function, I think using the peek_token_no_skip could simplify the impl. Something roughly like the following? to avoid manually tracking token counts and lookbacks

let count = loop {
  match self.peek_token_no_skip() {
    Token::Whitespace(Whitespace::Newline) | Token::Whitespace(Whitespace::SingleLineComment) | Token::EOF => {
      self.advance_token();
      break None
    }
    Token::Whitespace(_) => {
      self.advance_token();
    }
    _ => {
      break Some(self.parse_number())
    }
  }
};


#[test]
fn parse_mssql_go_keyword() {
let single_go_keyword = "USE some_database;\nGO";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have one of the test cases use ms().one_statement_parses_to(sql, canonical)? looking at the tests its unclear what the behavior of the parser is in terms of serializing an AST that has GO statements in it, and that it needs special behavior to inject new lines manually I imagine

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm unsure the best way to approach this question. one_statement_parses_to requires one statement, right? But most of these SQL text examples are multiple statements. I didn't see anything in TestUtils for a series of statements, so I added a new helper accordingly with canonical strings, let me know if that covers it

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yeah a new helper sounds reasonable to me thanks!

assert_eq!(stmts.len(), 2);
assert_eq!(stmts[1], Statement::Go(GoStatement { count: None }));

let comment_following_go = "USE some_database;\nGO -- okay";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we can add a multine_line_comment_following_go test case? e.g. GO /* whitespace */ 42

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done 👍

src/ast/mod.rs Outdated
Comment on lines 4058 to 4061
///
/// GO is not a Transact-SQL statement; it is a command recognized by various tools as a batch delimiter
///
/// See <https://learn.microsoft.com/en-us/sql/t-sql/language-elements/sql-server-utilities-statements-go>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For visibility we can move the comment to the struct definition similar to the print statement?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is already the same as print 🤔. Are you saying both comments should move to their respective structs?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah indeed, ideally both would have the doc comments on their respective structs, I think I meant to reference return statement in the other PR as the example

// - looking backwards there's only (any) whitespace preceded by a newline
// then: `GO` iSN'T a column alias
if kw == &Keyword::GO {
let mut look_back_count = 2;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

double checking, why does the index start at 2 vs 1?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's similar to parse_go, which references the existing previous token logic which also starts by subtracting 2. My understanding is that 0 is the next token, -1 is the current token, and -2 is the first previous token.

// then: `GO` iSN'T a column alias
if kw == &Keyword::GO {
let mut look_back_count = 2;
loop {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have this logic as a helper function?

fn prev_nth_token_no_skip(&self, mut n: usize) -> Option<&TokenWithSpan> {
  // ...
}

like a combination of peek_nth_token and prev_token

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a new helper, done 👍

@aharpervc aharpervc force-pushed the mssql-go-keyword branch 6 times, most recently from 7689e0c to b8a184c Compare April 18, 2025 18:27
@aharpervc aharpervc requested a review from iffyio April 18, 2025 18:30
@@ -166,6 +168,32 @@ impl TestedDialects {
only_statement
}

/// The same as [`one_statement_parses_to`] but it works for a multiple statements
pub fn multiple_statements_parse_to(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pub fn multiple_statements_parse_to(
pub fn statements_parse_to(

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done 👍


#[test]
fn parse_mssql_go_keyword() {
let single_go_keyword = "USE some_database;\nGO";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yeah a new helper sounds reasonable to me thanks!

@@ -4055,6 +4070,44 @@ impl<'a> Parser<'a> {
)
}

/// Look backwards in the token stream and expect that there was only whitespace tokens until the previous newline
pub fn expect_previously_only_whitespace_until_newline(&mut self) -> Result<(), ParserError> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pub fn expect_previously_only_whitespace_until_newline(&mut self) -> Result<(), ParserError> {
pub(crate) fn expect_previously_only_whitespace_until_newline(&mut self) -> Result<(), ParserError> {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done 👍

@@ -618,6 +632,7 @@ impl<'a> Parser<'a> {
// `COMMENT` is snowflake specific https://docs.snowflake.com/en/sql-reference/sql/comment
Keyword::COMMENT if self.dialect.supports_comment_on() => self.parse_comment(),
Keyword::PRINT => self.parse_print(),
Keyword::GO => self.parse_go(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we rewind with self.prev_token() here before calling parse_go? so that the parse_go function is self contained and can parse a full GO statement. (I also realise now we missed that for print)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that some of the statement types in parse_statement are doing that but most aren't. What's the strategy on this? How do you know when you're supposed to do it or not?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed the code is inconsistent in that regard historically, for newer statements ideally the parser functions are self contained when possible

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, that makes sense. I've updated it to rewind before parsing (using raiserror as a reference)

self.expect_previously_only_whitespace_until_newline()?;

let count = loop {
// using this peek function because we want to halt this statement parsing upon newline
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we include the example you had in the comment earlier, explicitly highlighting why this statement is special? I think otherwise it would not be obvious to folks that come across the code later on why the current code is a special case

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I can add that comment. However there is also test coverage for this behavior, so it should be protected from future refactoring

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done 👍

@@ -15064,6 +15117,38 @@ impl<'a> Parser<'a> {
}))
}

/// Parse [Statement::Go]
fn parse_go(&mut self) -> Result<Statement, ParserError> {
self.expect_previously_only_whitespace_until_newline()?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm this call look equivalent to the loop below that peeks forward to skip newlines? unclear why we're peeking backwards here right after we parsed the GO keyword (so that this will always be infallible?)

Copy link
Contributor Author

@aharpervc aharpervc Apr 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm this call look equivalent to the loop below that peeks forward to skip newlines? unclear why we're peeking backwards here right after we parsed the GO keyword (so that this will always be infallible?)

The too loops aren't the same. The reason we need to peek look backwards here is to forbid this syntax:

-- `GO` is disallowed
select 1; go

If you actually run this on a real SQL Server instance, you will also get a similar error: Incorrect syntax near 'go'.. Therefore it seems prudent to forbid this syntax from parsing here in this library. This situation has test coverage with the invalid_go_position example.


Note that the extremely similar syntax select 1 go is allowed, but in this example go is a column alias rather than a batch delimiter.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah that makes sense! And the added comment clarifies as well thanks!

Comment on lines 124 to 127
.expect_previously_only_whitespace_until_newline()
.is_ok()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if the intent is for the condition to return a bool, then we can update this function to return a bool instead of a result (where the latter is flagging an error to be handled)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case, it's most useful to know if there was previously only whitespace until a newline. However in the other call of this function in parse_go, we legitimately do want a parser error. Therefore we have choices:

  1. expect_previously_only_whitespace_until_newline raises a parse error which makes sense in parse_go but is converted to a boolean here
  2. expect_previously_only_whitespace_until_newline returns a boolean which makes sense here and then parse_go needs to separately raise a parse error

Which is the best choice for the time being?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see, 2. sounds desirable to return a boolean, in that the function is only checking if a condition holds, then interpretation of the condition is left to the caller of the function. in the other usage the caller does e.g.

if !self.prev_whitespace_only_is_newline() {
    return self.expected("newline ...")
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha, I will update it to do that

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done 👍

@@ -4055,6 +4070,44 @@ impl<'a> Parser<'a> {
)
}

/// Look backwards in the token stream and expect that there was only whitespace tokens until the previous newline
pub fn expect_previously_only_whitespace_until_newline(&mut self) -> Result<(), ParserError> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm so i think it would be good to write this function in a reusable manner if possible since it feels similar to the peek_*, prev_* helper functions that we have. Would it be possible to introduce a generic prev_nth_token_no_skip(n: usize) that can be called by parser and dialect methods?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done 👍

@aharpervc aharpervc requested a review from iffyio April 21, 2025 21:40
@@ -15064,6 +15117,38 @@ impl<'a> Parser<'a> {
}))
}

/// Parse [Statement::Go]
fn parse_go(&mut self) -> Result<Statement, ParserError> {
self.expect_previously_only_whitespace_until_newline()?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah that makes sense! And the added comment clarifies as well thanks!

@@ -618,6 +632,7 @@ impl<'a> Parser<'a> {
// `COMMENT` is snowflake specific https://docs.snowflake.com/en/sql-reference/sql/comment
Keyword::COMMENT if self.dialect.supports_comment_on() => self.parse_comment(),
Keyword::PRINT => self.parse_print(),
Keyword::GO => self.parse_go(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed the code is inconsistent in that regard historically, for newer statements ideally the parser functions are self contained when possible

Comment on lines 491 to 498
expecting_statement_delimiter = match &statement {
Statement::If(s) => match &s.if_block.conditional_statements {
// the `END` keyword doesn't need to be followed by a statement delimiter, so it shouldn't be expected here
ConditionalStatements::BeginEnd { .. } => false,
// parsing the statement sequence consumes the statement delimiter, so it shouldn't be expected here
ConditionalStatements::Sequence { .. } => false,
},
// Treat batch delimiter as an end of statement, so no additional statement delimiter expected here
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will this be replaced by similar solution as in #1808?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect so... need to investigate. (I need to rebuild my mental model on this first 😆).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've simplified this to reduce it down to only concerns within this PR 👍

Comment on lines +4104 to +4092
// special consideration required for single line comments since that string includes the newline
Whitespace::SingleLineComment { comment, prefix: _ } => {
if comment.ends_with('\n') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

double checking: is there a scenario where a single line comment doesn't end with a new line? spontaneously sounds like that should always hold true so that the manual newline check would not be required

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

double checking: is there a scenario where a single line comment doesn't end with a new line? spontaneously sounds like that should always hold true so that the manual newline check would not be required

I actually don't know, I was surprised to find that the newline is actually part of the comment text in this library. It seemed prudent to be defensive in this new code, since that comment parsing behavior maybe isn't particularly intentional.

@@ -3939,6 +3954,26 @@ impl<'a> Parser<'a> {
})
}

/// Return nth previous token, possibly whitespace
/// (or [`Token::EOF`] when before the beginning of the stream).
pub fn peek_prev_nth_token_no_skip(&self, n: usize) -> TokenWithSpan {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pub fn peek_prev_nth_token_no_skip(&self, n: usize) -> TokenWithSpan {
pub(crate) fn peek_prev_nth_token_no_skip(&self, n: usize) -> &TokenWithSpan {

we can probably start with the API being non-public. Then can we switch to returning a reference to avoid the mandatory cloning?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done 👍

Comment on lines 124 to 127
.expect_previously_only_whitespace_until_newline()
.is_ok()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see, 2. sounds desirable to return a boolean, in that the function is only checking if a condition holds, then interpretation of the condition is left to the caller of the function. in the other usage the caller does e.g.

if !self.prev_whitespace_only_is_newline() {
    return self.expected("newline ...")
}

@aharpervc
Copy link
Contributor Author

aharpervc commented Apr 23, 2025

Here's another example case this PR should parse properly, before merging (on my todo list...)

USE some_database;
GO

;WITH cte AS (
  SELECT 1 x
)
SELECT *
FROM cte;

The peek logic in parse_go should be seeing that newline, but it's accidentally advancing past the whitespace, seeing the semi-colon, and failing.

Done 👍

- per documentation, "not a statement" but acts like one in all other regards
- since it's a batch delimiter and statements can't extend beyond a batch, it also acts as a statement delimiter
…a bool

- in `parse_go` we don't have a direct way to find what that previous non-whitespace token was, so the error message is adjusted accordingly
@aharpervc aharpervc requested a review from iffyio April 23, 2025 22:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants