Skip to content

Commit e1ee4ee

Browse files
committed
fix: qualified column names with SQL keywords parse as identifiers
Since v0.55.0, qualified column names using SQL keywords failed to parse. For example, `T.interval` in `PARTITION BY T.key, T.interval ORDER BY ...` was incorrectly interpreted as an INTERVAL expression instead of a column. In `3e90a18` changed `parse_compound_expr` to use `parse_subexpr()` for tokens after `.`, causing keywords like INTERVAL, CASE, CAST, etc. to be treated as expression starters. Explicitly handle `Token::Word` in `parse_compound_expr` by treating it as an identifier. If followed by `(` (excluding the `(+)` outer join operator), parse as a method call. This restores the original behavior where words after `.` were always converted to identifiers.
1 parent 3af9988 commit e1ee4ee

2 files changed

Lines changed: 60 additions & 0 deletions

File tree

src/parser/mod.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1856,6 +1856,21 @@ impl<'a> Parser<'a> {
18561856
chain.push(AccessExpr::Dot(expr));
18571857
self.advance_token(); // The consumed string
18581858
}
1859+
// Handle words (including keywords like INTERVAL) as identifiers
1860+
// when they appear after a period. This ensures `T.interval` is
1861+
// parsed as a compound identifier, not as an interval expression.
1862+
// If followed by `(`, parse as a method call (but not for `(+)`
1863+
// which is the outer join operator in some dialects).
1864+
Token::Word(w) => {
1865+
let ident = w.clone().into_ident(next_token.span);
1866+
self.advance_token();
1867+
if self.peek_token() == Token::LParen && !self.peek_outer_join_operator() {
1868+
let expr = self.parse_function(ObjectName::from(vec![ident]))?;
1869+
chain.push(AccessExpr::Dot(expr));
1870+
} else {
1871+
chain.push(AccessExpr::Dot(Expr::Identifier(ident)));
1872+
}
1873+
}
18591874
// Fallback to parsing an arbitrary expression.
18601875
_ => match self.parse_subexpr(self.dialect.prec_value(Precedence::Period))? {
18611876
// If we get back a compound field access or identifier,

tests/sqlparser_common.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15009,6 +15009,51 @@ fn test_reserved_keywords_for_identifiers() {
1500915009
dialects.parse_sql_statements(sql).unwrap();
1501015010
}
1501115011

15012+
#[test]
15013+
fn test_keywords_as_column_names_after_dot() {
15014+
// Test various keywords that have special meaning when standalone
15015+
// but should be treated as identifiers after a dot.
15016+
let keywords = [
15017+
"interval", // INTERVAL '1' DAY
15018+
"case", // CASE WHEN ... END
15019+
"cast", // CAST(x AS y)
15020+
"extract", // EXTRACT(DAY FROM ...)
15021+
"trim", // TRIM(...)
15022+
"substring", // SUBSTRING(...)
15023+
"left", // LEFT(str, n)
15024+
"right", // RIGHT(str, n)
15025+
];
15026+
15027+
for kw in keywords {
15028+
let sql = format!("SELECT T.{kw} FROM T");
15029+
verified_stmt(&sql);
15030+
15031+
let sql = format!("SELECT SUM(x) OVER (PARTITION BY T.{kw} ORDER BY T.id) FROM T");
15032+
verified_stmt(&sql);
15033+
15034+
let sql = format!("SELECT T.{kw}, S.{kw} FROM T, S WHERE T.{kw} = S.{kw}");
15035+
verified_stmt(&sql);
15036+
}
15037+
15038+
let select = verified_only_select("SELECT T.interval, T.case FROM T");
15039+
match &select.projection[0] {
15040+
SelectItem::UnnamedExpr(Expr::CompoundIdentifier(idents)) => {
15041+
assert_eq!(idents.len(), 2);
15042+
assert_eq!(idents[0].value, "T");
15043+
assert_eq!(idents[1].value, "interval");
15044+
}
15045+
_ => panic!("Expected CompoundIdentifier for T.interval"),
15046+
}
15047+
match &select.projection[1] {
15048+
SelectItem::UnnamedExpr(Expr::CompoundIdentifier(idents)) => {
15049+
assert_eq!(idents.len(), 2);
15050+
assert_eq!(idents[0].value, "T");
15051+
assert_eq!(idents[1].value, "case");
15052+
}
15053+
_ => panic!("Expected CompoundIdentifier for T.case"),
15054+
}
15055+
}
15056+
1501215057
#[test]
1501315058
fn parse_create_table_with_bit_types() {
1501415059
let sql = "CREATE TABLE t (a BIT, b BIT VARYING, c BIT(42), d BIT VARYING(43))";

0 commit comments

Comments
 (0)