diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 3294acf6a..7e0f7b090 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -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, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 365bddb0f..7906db46a 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -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))";