Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/dialect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1526,6 +1526,15 @@ pub trait Dialect: Debug + Any {
explicit || self.is_column_alias(kw, parser)
}

/// Returns true if `OVER` without a following `(` or plain-identifier window name
/// may be used as an implicit column alias rather than starting a window clause.
///
/// SQLite reads `OVER` as a keyword only when `(` or a name follows, see
/// `analyzeOverKeyword` in [tokenize.c](https://sqlite.org/src/file?name=src/tokenize.c)
fn supports_over_keyword_as_column_alias(&self) -> bool {
false
}

/// Returns true if the specified keyword should be parsed as a table factor identifier.
/// See [keywords::RESERVED_FOR_TABLE_FACTOR]
fn is_table_factor(&self, kw: &Keyword, _parser: &mut Parser) -> bool {
Expand Down
24 changes: 24 additions & 0 deletions src/dialect/sqlite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,4 +160,28 @@ impl Dialect for SQLiteDialect {
fn supports_unterminated_block_comments(&self) -> bool {
true
}

fn is_column_alias(&self, kw: &Keyword, _parser: &mut Parser) -> bool {
// https://www.sqlite.org/lang_keywords.html
!matches!(
kw,
Keyword::SELECT
| Keyword::WHERE
| Keyword::GROUP
| Keyword::HAVING
| Keyword::ORDER
| Keyword::LIMIT
| Keyword::UNION
| Keyword::EXCEPT
| Keyword::INTERSECT
| Keyword::RETURNING
| Keyword::VALUES
| Keyword::FROM
| Keyword::INTO
)
}

fn supports_over_keyword_as_column_alias(&self) -> bool {
true
}
}
17 changes: 16 additions & 1 deletion src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2676,7 +2676,22 @@ impl<'a> Parser<'a> {
let window_spec = self.parse_window_spec()?;
Some(WindowType::WindowSpec(window_spec))
} else {
Some(WindowType::NamedWindow(self.parse_identifier()?))
let over_is_alias = self.dialect.supports_over_keyword_as_column_alias()
&& match &self.peek_token_ref().token {
Token::Word(w) => {
let kw = w.keyword;
let dialect = self.dialect;
!dialect.is_column_alias(&kw, self)
}
Token::SingleQuotedString(_) | Token::DoubleQuotedString(_) => false,
_ => true,
};
if over_is_alias {
self.prev_token();
None
} else {
Some(WindowType::NamedWindow(self.parse_identifier()?))
}
}
} else {
None
Expand Down
28 changes: 21 additions & 7 deletions tests/sqlparser_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8387,12 +8387,25 @@ fn parse_multiple_statements() {
// ensure that SELECT/WITH is not parsed as a table or column alias if ';'
// separating the statements is omitted:
test_with("SELECT foo FROM baz", "SELECT", " bar");
test_with("SELECT foo", "WITH", " cte AS (SELECT 1 AS s) SELECT bar");
test_with(
"SELECT foo FROM baz",
"WITH",
" cte AS (SELECT 1 AS s) SELECT bar",
);
// WITH is a valid alias in SQLite so missing-semicolon detection skips it
for sql1 in ["SELECT foo", "SELECT foo FROM baz"] {
let cte_rest = " cte AS (SELECT 1 AS s) SELECT bar";
let res = parse_sql_statements(&(sql1.to_owned() + ";WITH" + cte_rest));
assert_eq!(
vec![
one_statement_parses_to(sql1, ""),
one_statement_parses_to(&("WITH".to_owned() + cte_rest), ""),
],
res.unwrap()
);
one_statement_parses_to(&(sql1.to_owned() + ";"), sql1);
let res = all_dialects_except(|d| d.is::<SQLiteDialect>())
.parse_sql_statements(&(sql1.to_owned() + " WITH" + cte_rest));
assert_eq!(
ParserError::ParserError("Expected: end of statement, found: WITH".to_string()),
res.unwrap_err()
);
}
test_with("DELETE FROM foo", "SELECT", " bar");
test_with("INSERT INTO foo VALUES (1)", "SELECT", " bar");
// Since MySQL supports the `CREATE TABLE SELECT` syntax, this needs to be handled separately
Expand Down Expand Up @@ -9238,7 +9251,8 @@ fn parse_fetch() {
});
let ast = verified_query("SELECT foo FROM bar FETCH FIRST 2 ROWS ONLY");
assert_eq!(ast.fetch, fetch_first_two_rows_only);
let ast = verified_query("SELECT 'foo' FETCH FIRST 2 ROWS ONLY");
let ast = all_dialects_except(|d| d.is::<SQLiteDialect>())
.verified_query("SELECT 'foo' FETCH FIRST 2 ROWS ONLY");
assert_eq!(ast.fetch, fetch_first_two_rows_only);
let ast = verified_query("SELECT foo FROM bar FETCH FIRST ROWS ONLY");
assert_eq!(
Expand Down
64 changes: 64 additions & 0 deletions tests/sqlparser_sqlite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1058,6 +1058,49 @@ fn test_cast_empty_type() {
);
}

#[test]
fn parse_keyword_column_aliases() {
for kw in [
"analyze",
"cluster",
"distribute",
"end",
"exclude",
"explain",
"fetch",
"lateral",
"minus",
"offset",
"sort",
"top",
"view",
"with",
] {
sqlite().one_statement_parses_to(
&format!("SELECT a {kw} FROM t"),
&format!("SELECT a AS {kw} FROM t"),
);
sqlite().one_statement_parses_to(&format!("SELECT 1 {kw}"), &format!("SELECT 1 AS {kw}"));
}
for (sql, error) in [
("SELECT 1 WHERE", "Expected: an expression, found: EOF"),
("SELECT 1 GROUP", "Expected: end of statement, found: GROUP"),
] {
assert_eq!(
sqlite().parse_sql_statements(sql),
Err(ParserError::ParserError(error.to_string())),
"{sql}"
);
}
assert_eq!(
TestedDialects::new(vec![Box::new(GenericDialect {})])
.parse_sql_statements("SELECT a view FROM t"),
Err(ParserError::ParserError(
"Expected: end of statement, found: view".to_string()
))
);
}

#[test]
fn parse_n_prefix_not_national_string() {
// In SQLite, `n'...'` is the identifier `n` followed by a string literal.
Expand Down Expand Up @@ -1161,6 +1204,27 @@ fn parse_unterminated_block_comment() {
);
}

#[test]
fn parse_over_as_column_alias_after_function() {
// In SQLite, OVER is a fallback identifier so `f() OVER` aliases the result as `over`.
sqlite().one_statement_parses_to("SELECT f() OVER", "SELECT f() AS OVER");
// A keyword window name is not mistaken for an alias candidate.
sqlite().verified_stmt("SELECT f() OVER partition");
// Window function syntax still works when OVER is followed by `(`.
sqlite().verified_stmt("SELECT f() OVER ()");
// A quoted string names the window, so OVER cannot be an alias for it
sqlite().verified_stmt("SELECT f() OVER 'partition'");
sqlite().verified_stmt(r#"SELECT f() OVER "partition""#);
// Other dialects keep the greedy OVER parse and reject the bare form.
let generic = TestedDialects::new(vec![Box::new(GenericDialect {})]);
assert_eq!(
generic.parse_sql_statements("SELECT f() OVER"),
Err(ParserError::ParserError(
"Expected: identifier, found: EOF".to_string()
))
);
}

fn sqlite() -> TestedDialects {
TestedDialects::new(vec![Box::new(SQLiteDialect {})])
}
Expand Down
Loading