This is an automated email from the ASF dual-hosted git repository.

github-bot pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion-sqlparser-rs.git


The following commit(s) were added to refs/heads/main by this push:
     new 845e2138 fix: qualified column names with SQL keywords parse as 
identifiers (#2157)
845e2138 is described below

commit 845e2138e27dcaf677b92dc36be32b0ec403ae3d
Author: Simon Sawert <[email protected]>
AuthorDate: Thu Jan 22 17:52:13 2026 +0100

    fix: qualified column names with SQL keywords parse as identifiers (#2157)
---
 src/parser/mod.rs         | 65 +++++++++++++++++++++++++++++++++--------------
 tests/sqlparser_common.rs | 45 ++++++++++++++++++++++++++++++++
 2 files changed, 91 insertions(+), 19 deletions(-)

diff --git a/src/parser/mod.rs b/src/parser/mod.rs
index 733abbbf..882803a5 100644
--- a/src/parser/mod.rs
+++ b/src/parser/mod.rs
@@ -1900,26 +1900,53 @@ impl<'a> Parser<'a> {
                         chain.push(AccessExpr::Dot(expr));
                         self.advance_token(); // The consumed string
                     }
-                    // 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,
-                        // we flatten the nested expression.
-                        // For example if the current root is `foo`
-                        // and we get back a compound identifier expression 
`bar.baz`
-                        // The full expression should be `foo.bar.baz` (i.e.
-                        // a root with an access chain with 2 entries) and not
-                        // `foo.(bar.baz)` (i.e. a root with an access chain 
with
-                        // 1 entry`).
-                        Expr::CompoundFieldAccess { root, access_chain } => {
-                            chain.push(AccessExpr::Dot(*root));
-                            chain.extend(access_chain);
-                        }
-                        Expr::CompoundIdentifier(parts) => chain
-                            
.extend(parts.into_iter().map(Expr::Identifier).map(AccessExpr::Dot)),
-                        expr => {
-                            chain.push(AccessExpr::Dot(expr));
+                    // Fallback to parsing an arbitrary expression, but 
restrict to expression
+                    // types that are valid after the dot operator. This 
ensures that e.g.
+                    // `T.interval` is parsed as a compound identifier, not as 
an interval
+                    // expression.
+                    _ => {
+                        let expr = self.maybe_parse(|parser| {
+                            let expr = parser
+                                
.parse_subexpr(parser.dialect.prec_value(Precedence::Period))?;
+                            match &expr {
+                                Expr::CompoundFieldAccess { .. }
+                                | Expr::CompoundIdentifier(_)
+                                | Expr::Identifier(_)
+                                | Expr::Value(_)
+                                | Expr::Function(_) => Ok(expr),
+                                _ => parser.expected("an identifier or value", 
parser.peek_token()),
+                            }
+                        })?;
+
+                        match expr {
+                            // If we get back a compound field access or 
identifier,
+                            // we flatten the nested expression.
+                            // For example if the current root is `foo`
+                            // and we get back a compound identifier 
expression `bar.baz`
+                            // The full expression should be `foo.bar.baz` 
(i.e.
+                            // a root with an access chain with 2 entries) and 
not
+                            // `foo.(bar.baz)` (i.e. a root with an access 
chain with
+                            // 1 entry`).
+                            Some(Expr::CompoundFieldAccess { root, 
access_chain }) => {
+                                chain.push(AccessExpr::Dot(*root));
+                                chain.extend(access_chain);
+                            }
+                            Some(Expr::CompoundIdentifier(parts)) => 
chain.extend(
+                                
parts.into_iter().map(Expr::Identifier).map(AccessExpr::Dot),
+                            ),
+                            Some(expr) => {
+                                chain.push(AccessExpr::Dot(expr));
+                            }
+                            // If the expression is not a valid suffix, fall 
back to
+                            // parsing as an identifier. This handles cases 
like `T.interval`
+                            // where `interval` is a keyword but should be 
treated as an identifier.
+                            None => {
+                                chain.push(AccessExpr::Dot(Expr::Identifier(
+                                    self.parse_identifier()?,
+                                )));
+                            }
                         }
-                    },
+                    }
                 }
             } else if !self.dialect.supports_partiql()
                 && self.peek_token_ref().token == Token::LBracket
diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs
index c67bcb18..f892bf7a 100644
--- a/tests/sqlparser_common.rs
+++ b/tests/sqlparser_common.rs
@@ -15045,6 +15045,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))";


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to