You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@datafusion.apache.org by lu...@apache.org on 2026/10/07 16:27:59 UTC

(datafusion-sqlparser-rs) 01/01: Generic: Parse schema-qualified calls to keyword functions like floor

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

LucaCappelletti94 pushed a commit to branch upstream/qualified-keyword-functions
in repository https://gitbox.apache.org/repos/asf/datafusion-sqlparser-rs.git

commit b61158cd871a430a2afba6d2f5c155a340fd409c
Author: LucaCappelletti94 <ca...@gmail.com>
AuthorDate: Wed Oct 7 18:27:38 2026 +0200

    Generic: Parse schema-qualified calls to keyword functions like floor
---
 src/parser/mod.rs         | 16 +++++++-----
 tests/sqlparser_common.rs | 62 +++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 72 insertions(+), 6 deletions(-)

diff --git a/src/parser/mod.rs b/src/parser/mod.rs
index 1a46c6e5..5e0943ac 100644
--- a/src/parser/mod.rs
+++ b/src/parser/mod.rs
@@ -2172,13 +2172,17 @@ impl<'a> Parser<'a> {
                             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.
+                            // Falls back to identifier parsing for keyword suffixes
+                            // like `T.interval`; a following `(` makes it a
+                            // qualified call instead, e.g. `pg_catalog.floor(3)`.
                             None => {
-                                chain.push(AccessExpr::Dot(Expr::Identifier(
-                                    self.parse_identifier()?,
-                                )));
+                                let ident = self.parse_identifier()?;
+                                let field = if self.peek_token_ref().token == Token::LParen {
+                                    self.parse_function(ObjectName::from(vec![ident]))?
+                                } else {
+                                    Expr::Identifier(ident)
+                                };
+                                chain.push(AccessExpr::Dot(field));
                             }
                         }
                     }
diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs
index 8e08c657..bcf69522 100644
--- a/tests/sqlparser_common.rs
+++ b/tests/sqlparser_common.rs
@@ -16090,6 +16090,68 @@ fn test_keywords_as_column_names_after_dot() {
     }
 }
 
+#[test]
+fn test_qualified_keyword_function_calls() {
+    let pg = TestedDialects::new(vec![Box::new(PostgreSqlDialect {})]);
+
+    for (qualifier, func) in [
+        ("public", "floor"),
+        ("pg_catalog", "floor"),
+        ("pg_catalog", "ceil"),
+    ] {
+        let sql = format!("SELECT {qualifier}.{func}(3)");
+        match expr_from_projection(&pg.verified_only_select(&sql).projection[0]) {
+            Expr::Function(Function {
+                name,
+                args: FunctionArguments::List(FunctionArgumentList { args, .. }),
+                ..
+            }) => {
+                assert_eq!(
+                    name,
+                    &ObjectName::from(vec![Ident::new(qualifier), Ident::new(func)])
+                );
+                assert_eq!(
+                    args,
+                    &[FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::value(
+                        number("3")
+                    )))]
+                );
+            }
+            other => panic!("Expected a qualified function call, got: {other:?}"),
+        }
+    }
+
+    // EXTRACT has its own `EXTRACT(field FROM source)` grammar; schema-qualified
+    // it is an ordinary function call like any other qualified name.
+    match expr_from_projection(
+        &pg.verified_only_select("SELECT pg_catalog.extract(3)")
+            .projection[0],
+    ) {
+        Expr::Function(Function { name, .. }) => assert_eq!(
+            name,
+            &ObjectName::from(vec![Ident::new("pg_catalog"), Ident::new("extract")])
+        ),
+        other => panic!("Expected a qualified function call, got: {other:?}"),
+    }
+
+    // Unqualified calls still parse as the dedicated FLOOR/CEIL expression.
+    match expr_from_projection(&pg.verified_only_select("SELECT FLOOR(3)").projection[0]) {
+        Expr::Floor { .. } => {}
+        other => panic!("Expected Expr::Floor, got: {other:?}"),
+    }
+
+    // Keyword column identifiers after a dot, without parentheses, are untouched.
+    match expr_from_projection(
+        &pg.verified_only_select("SELECT t.interval FROM tbl t")
+            .projection[0],
+    ) {
+        Expr::CompoundIdentifier(idents) => {
+            assert_eq!(idents, &vec![Ident::new("t"), Ident::new("interval")])
+        }
+        other => panic!("Expected CompoundIdentifier, got: {other:?}"),
+    }
+}
+
 #[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: commits-unsubscribe@datafusion.apache.org
For additional commands, e-mail: commits-help@datafusion.apache.org