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
15 changes: 15 additions & 0 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1856,6 +1856,21 @@ impl<'a> Parser<'a> {
chain.push(AccessExpr::Dot(expr));
self.advance_token(); // The consumed string
}
// Handle words (including keywords like INTERVAL) as identifiers
// when they appear after a period. This ensures `T.interval` is
// parsed as a compound identifier, not as an interval expression.
// If followed by `(`, parse as a method call (but not for `(+)`
// which is the outer join operator in some dialects).
Token::Word(w) => {
let ident = w.clone().into_ident(next_token.span);
self.advance_token();
if self.peek_token() == Token::LParen && !self.peek_outer_join_operator() {
let expr = self.parse_function(ObjectName::from(vec![ident]))?;
chain.push(AccessExpr::Dot(expr));
} else {
chain.push(AccessExpr::Dot(Expr::Identifier(ident)));
}
}
// Fallback to parsing an arbitrary expression.
_ => match self.parse_subexpr(self.dialect.prec_value(Precedence::Period))? {
// If we get back a compound field access or identifier,
Expand Down
45 changes: 45 additions & 0 deletions tests/sqlparser_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15009,6 +15009,51 @@ fn test_reserved_keywords_for_identifiers() {
dialects.parse_sql_statements(sql).unwrap();
}

#[test]
fn test_keywords_as_column_names_after_dot() {
// Test various keywords that have special meaning when standalone
// but should be treated as identifiers after a dot.
let keywords = [
"interval", // INTERVAL '1' DAY
"case", // CASE WHEN ... END
"cast", // CAST(x AS y)
"extract", // EXTRACT(DAY FROM ...)
"trim", // TRIM(...)
"substring", // SUBSTRING(...)
"left", // LEFT(str, n)
"right", // RIGHT(str, n)
];

for kw in keywords {
let sql = format!("SELECT T.{kw} FROM T");
verified_stmt(&sql);

let sql = format!("SELECT SUM(x) OVER (PARTITION BY T.{kw} ORDER BY T.id) FROM T");
verified_stmt(&sql);

let sql = format!("SELECT T.{kw}, S.{kw} FROM T, S WHERE T.{kw} = S.{kw}");
verified_stmt(&sql);
}

let select = verified_only_select("SELECT T.interval, T.case FROM T");
match &select.projection[0] {
SelectItem::UnnamedExpr(Expr::CompoundIdentifier(idents)) => {
assert_eq!(idents.len(), 2);
assert_eq!(idents[0].value, "T");
assert_eq!(idents[1].value, "interval");
}
_ => panic!("Expected CompoundIdentifier for T.interval"),
}
match &select.projection[1] {
SelectItem::UnnamedExpr(Expr::CompoundIdentifier(idents)) => {
assert_eq!(idents.len(), 2);
assert_eq!(idents[0].value, "T");
assert_eq!(idents[1].value, "case");
}
_ => panic!("Expected CompoundIdentifier for T.case"),
}
}

#[test]
fn parse_create_table_with_bit_types() {
let sql = "CREATE TABLE t (a BIT, b BIT VARYING, c BIT(42), d BIT VARYING(43))";
Expand Down