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]