-
Notifications
You must be signed in to change notification settings - Fork 629
Expand parse without semicolons #1949
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
base: main
Are you sure you want to change the base?
Conversation
069ce0e
to
9be2dad
Compare
- a corresponding `supports_statements_without_semicolon_delimiter` Dialect trait function - this is optional for SQL Server, so it's set to `true` for that dialect - for the implementation, `RETURN` parsing needs to be tightened up to avoid ambiguity & tests that formerly asserted "end of statement" now maybe need to assert "an SQL statement" - a new `assert_err_parse_statements` splits the dialects based on semicolon requirements & asserts the expected error message accordingly
- at least all of select/insert/update/delete (plus exec) can be added
9be2dad
to
53d7930
Compare
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.
Thanks @aharpervc -- sorry for the delay in reviewing. I left some comments. Let me know what you think
@@ -123,6 +123,10 @@ impl Dialect for MsSqlDialect { | |||
true | |||
} | |||
|
|||
fn supports_statements_without_semicolon_delimiter(&self) -> bool { |
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.
🤦 MS SQL!!!!
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.
🤷♂️🤣
@@ -1136,6 +1142,11 @@ pub trait Dialect: Debug + Any { | |||
fn supports_notnull_operator(&self) -> bool { | |||
false | |||
} | |||
|
|||
/// Returns true if the dialect supports parsing statements without a semicolon delimiter. |
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.
Would it be possible to give an example here?
Something like
/// Returns true if the dialect supports parsing statements without a semicolon delimiter. | |
/// Returns true if the dialect supports parsing statements without a semicolon delimiter. | |
/// | |
/// If returns true, the following SQL will not parse. If returns `false` the SQL will parse | |
/// | |
/// ```sql | |
/// SELECT 1 | |
/// SELECT 2 | |
/// ``` |
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 👍
@@ -266,6 +266,22 @@ impl ParserOptions { | |||
self.unescape = unescape; | |||
self | |||
} | |||
|
|||
/// Set if semicolon statement delimiters are required. |
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.
What is the usecase to have the same configuration information on the ParserOptions
and the Dialect
?
It seems there are a few places in the Parser that inconsistently check the options and/or the dialect flag. If we had only one way to specify the option, there would not be any room for inconsistencies
Or maybe I misunderstand what is going on here
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.
What is the usecase to have the same configuration information on the ParserOptions and the Dialect?
It's similar to the existing with_trailing_commas
logic, and has several practical use cases:
- can be set to
true
for SQL Server & leftfalse
for all others (until if/when someone discovers this is supported in other dialects) - integration with existing testing patterns. eg, we can have
all_dialects_requiring_semicolon_statement_delimiter
&all_dialects_not_requiring_semicolon_statement_delimiter
- used to set the corresponding default parser option
It seems like testing parsing without semicolons would get a lot worse if it wasn't a dialect option
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 yeah the with_trailing_commas
option was introduced as parser setting originally and later had to be added as a dialect option since it was dialect specific, I figure ideally it would have been present only on the dialect.
My current thinking is be that ideally we have one way to configure the semicolon option since it would be nice to not have two knobs for the flag. In terms of which way to configure, I'm guessing it could make more sense to have the flag as a parser setting - i.e. whether or not semicolon is required is more reflecting the tool that originally processed the input file, vs an aspect of the sql language/dialect itself, so that it can differ in certain contexts for the same dialect.
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.
In terms of which way to configure, I'm guessing it could make more sense to have the flag as a parser setting
This seems reasonable to me in terms of philosophy. However, I don't know how that could be implemented practically. I.e., how would you write that code in this project to be able to have the same test coverage?
The benefit of setting the parse option based on the dialect configuration is:
- not all dialects support parsing without semicolons (so far, only one)
- for dialects that require semicolons, we probably don't want anything to change (as is implemented here in the PR)
- this project's tests are oriented around dialect features, so implementing the semicolon requirement as a dialect feature makes it more clear for how to run the tests
src/parser/mod.rs
Outdated
@@ -4541,6 +4561,18 @@ impl<'a> Parser<'a> { | |||
return Ok(vec![]); | |||
} | |||
|
|||
if end_token == Token::SemiColon | |||
&& self |
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.
do we also have to check the parser options here too?
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 might be that we should only be checking the parser options. I'll investigate
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.
Switched to parser options 👍
| Expr::Cast { .. } | ||
| Expr::Convert { .. } | ||
| Expr::Subquery(_) => Ok(expr), | ||
// todo: how to retstrict to variables? |
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 seems more like a semantic check -- this crate is focused on syntax parsing.
Read more hre:
https://github.com/apache/datafusion-sqlparser-rs?tab=readme-ov-file#syntax-vs-semantics
I think the error is inappropriate in this 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.
I think the error is inappropriate in this case
What do you suggest?
This seems more like a semantic check -- this crate is focused on syntax parsing.
The difficulty here is that without semicolon tokens the return statement is ambiguous. Consider the following perfectly valid T-SQL statement (which is also a test case example) which has several variations of return
:
CREATE OR ALTER PROCEDURE example_sp
AS
IF USER_NAME() = 'X'
RETURN
IF 1 = 2
RETURN (SELECT 1)
RETURN CONVERT(INT, 123)
If you revert this change and run that test, you get this error:
thread 'test_supports_statements_without_semicolon_delimiter' panicked at tests/sqlparser_mssql.rs:2533:14:
called `Result::unwrap()` on an `Err` value: ParserError("Expected: an SQL statement, found: 1 at Line: 1, Column: 72")
The reason is that for the first return
, the self.maybe_parse(|p| p.parse_expr())?
"successfully" parses/consumes the IF
keyword. However, this is contrary to the keyword documentation for SQL Server (ref) which requires an "integer expression".
I think I had said when introducing parse_return in a previous PR that we'd have to come back and tighten it up eventually, but I can't find that discussion 😐.
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.
@aharpervc in the example, is the ambiguity stemming from tsql not supporting IF
expressions but only IF
statements? If so we could flag that difference in the parser dialect, avoiding this diff. If it does support IF
statements then the grammar indeed looks either ambiguous or relying on the newline character to differentiate between the two scenarios?
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 don't see this being about the if statement, but rather about the return statement. The question for the parse is, given a return keyword, how many more tokens (or similarly, which expr's) should be considered part of that ReturnStatement? For T-SQL, there is a restricted answer to that question, but it is somewhat awkward to express here.
if it does support IF statements then the grammar indeed looks either ambiguous or relying on the newline character to differentiate between the two scenarios?
Newlines don't really help you here, I put them in the example above for readability, but this should parse identically (and does if you run on a real SQL Server instance):
CREATE OR ALTER PROCEDURE example_sp AS IF USER_NAME() = 'X' RETURN IF 1 = 2 RETURN (SELECT 1) RETURN CONVERT(INT, 123)
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 right, makes sense that they're not newline dependent. To clarify what I meant in the IF
scenario the idea would be to look whether the next token can not be an expression - from a look at their docs the options seem limited to
https://learn.microsoft.com/en-us/sql/t-sql/language-elements/control-of-flow?view=sql-server-ver15
if self.peek_one_of_keywords(
// hmm noticed while enumerating this list that the only ambiguous case really
// seems to be IF - since offhand the others can't be an expression.
// So that the list might not be necessary after all.
[BEGIN, BREAK, CONTINUE, GOTO, IF, RETURN, THROW, TRY, WAITFOR, WHILE]
) {
// bare return statement
}
// else maybe_parse_expr
would something like this work?
src/test_utils.rs
Outdated
.replace(" ;", " ") | ||
.replace(";\n", "\n") | ||
.replace("\n;", "\n") | ||
.replace(";", " "); |
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.
wouldn't this last statement (replace ";"
with " "
handle all the above? Or is the idea that newlines are important ?
Can you please add comments explaining the rationale for these substitutions?
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'm preserving the newline/whitespace around the statement delimiter because I'm trying to avoid being opinionated on whether they're meaningful or not in this helper. i.e., the helper should only strip semicolons and should avoid other transformations. I can add a comment
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.
Good point. At some point I was probably testing this in conjunction with #1809 and might have gotten it from there. Regardless, I'll simplify it here per your suggestion.
@@ -272,20 +275,39 @@ fn parse_insert_default_values() { | |||
"INSERT INTO test_table DEFAULT VALUES (some_column)"; | |||
assert_eq!( | |||
ParserError::ParserError("Expected: end of statement, found: (".to_string()), | |||
parse_sql_statements(insert_with_default_values_and_hive_after_columns).unwrap_err() | |||
all_dialects_requiring_semicolon_statement_delimiter() |
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 query doesn't seem to have multiple statements. Why does the test need to be changed?
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.
Turning on the option to make semicolons optional results in this test failures:
thread 'parse_insert_default_values' panicked at src/test_utils.rs:101:21:
assertion `left == right` failed: Parse results with PostgreSqlDialect are different from MsSqlDialect
left: Err(ParserError("Expected: end of statement, found: ("))
right: Err(ParserError("Expected: SELECT, VALUES, or a subquery in the query body, found: some_column"))
Since that's a parser error in both cases (but different message) I split the assertion out into two -- one for semicolons required that asserts the former error, and one for semicolons optional that asserts the other error.
- the dialect's original support informs the parser option, but the parser behavior itself should just check it's own options
9a52518
to
4bde5b4
Compare
/// When the dialect supports statements without semicolon delimiter, actual keywords aren't parsed as aliases. | ||
fn is_table_factor_alias(&self, explicit: bool, kw: &Keyword, _parser: &mut Parser) -> bool { | ||
if self.supports_statements_without_semicolon_delimiter() { | ||
kw == &Keyword::NoKeyword | ||
} else { | ||
explicit || self.is_table_alias(kw, _parser) | ||
} |
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 I followed the intent here, would you be able to provide example sqls to illustrate and how they should be parsed with vs without the flag?
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.
Yes, the example sql is from the sqlparse_common test case parse_select_with_table_alias_keyword
& parse_invalid_limit_by
, such as SELECT a FROM lineitem DECLARE
. Commenting out the new logic and running the tests shows the difficulty. My thinking here is that if semicolons are optional, we can forbid keywords as table aliases. Folks who use this option are going to be opting in to a certain level of ambiguity regardless, but having the parser do reasonable things (with test coverage) is my objective here.
Looking at this code again I think this should be checking the actual configured parser option rather than the dialect's capability, similar to the other feedback comment. I will change it.
@@ -266,6 +266,22 @@ impl ParserOptions { | |||
self.unescape = unescape; | |||
self | |||
} | |||
|
|||
/// Set if semicolon statement delimiters are required. |
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 yeah the with_trailing_commas
option was introduced as parser setting originally and later had to be added as a dialect option since it was dialect specific, I figure ideally it would have been present only on the dialect.
My current thinking is be that ideally we have one way to configure the semicolon option since it would be nice to not have two knobs for the flag. In terms of which way to configure, I'm guessing it could make more sense to have the flag as a parser setting - i.e. whether or not semicolon is required is more reflecting the tool that originally processed the input file, vs an aspect of the sql language/dialect itself, so that it can differ in certain contexts for the same dialect.
| Expr::Cast { .. } | ||
| Expr::Convert { .. } | ||
| Expr::Subquery(_) => Ok(expr), | ||
// todo: how to retstrict to variables? |
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.
@aharpervc in the example, is the ambiguity stemming from tsql not supporting IF
expressions but only IF
statements? If so we could flag that difference in the parser dialect, avoiding this diff. If it does support IF
statements then the grammar indeed looks either ambiguous or relying on the newline character to differentiate between the two scenarios?
This PR is a followup (ref) to recent work on parsing without requiring semicolon statement delimiters. It's the same content as my former PR which was stale after #1937.
This PR is rebased on latest master and brings forward the increased test coverage and additional parsing fixes from the previous branch.
Particularly, this PR expands support as follows:
supports_statements_without_semicolon_delimiter
dialect trait function (default false, but true for SQL Server). Now we can set the default parser option based on the dialectstatements_without_semicolons_parse_to
which deletes semicolons from the string, then parses it (simplifies testing parsing with & without),all_dialects_requiring_semicolon_statement_delimiter
&all_dialects_not_requiring_semicolon_statement_delimiter
to find configured dialects, andassert_err_parse_statements
to simplify asserting "end of statement" vs "an SQL statement" errors. A lot of test assertions that fail when requiring semicolons also fail when not requiring semicolons, but what precisely the parser will complain about could be different (again, if you write SQL this way, you accept that possibility). The helper functions enable running existing tests on dialects that don't require semicolons with minimal changes. The main usage there is in the "common" tests.