Repository: tobymao/sqlglot

Issue:
User defined functions with reserved keyword names raise ParseError in BigQuery dialect
While BigQuery doesn't allow reserved keywords to be used as standalone identifiers unless they're quoted, it does allow unquoted reserved keywords to be used as part of a qualified identifier (e.g. a UDF qualified with its dataset, or a subfield qualified with its parent fields).

This was originally reported in #1535 "_User defined functions with keyword or built in function names throw ParseError for BigQuery dialect_" (though the code snippet there only demonstrated the built-in function case, not the keyword case).

**Fully reproducible code snippet**
```python
sqlglot.parse_one("SELECT assert.true(1=1)", read="bigquery")
```

**Official Documentation**
- https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#path_expressions
  > Subsequent parts of a path expression can include non-identifiers, such as reserved keywords.


Discussion:

Patch:
diff --git a/sqlglot/dialects/clickhouse.py b/sqlglot/dialects/clickhouse.py
index 110ca543..a6fb0032 100644
--- a/sqlglot/dialects/clickhouse.py
+++ b/sqlglot/dialects/clickhouse.py
@@ -461,9 +461,13 @@ class ClickHouse(Dialect):
             functions: t.Optional[t.Dict[str, t.Callable]] = None,
             anonymous: bool = False,
             optional_parens: bool = True,
+            any_token: bool = False,
         ) -> t.Optional[exp.Expression]:
             func = super()._parse_function(
-                functions=functions, anonymous=anonymous, optional_parens=optional_parens
+                functions=functions,
+                anonymous=anonymous,
+                optional_parens=optional_parens,
+                any_token=any_token,
             )
 
             if isinstance(func, exp.Anonymous):
diff --git a/sqlglot/parser.py b/sqlglot/parser.py
index 22b52087..294c142e 100644
--- a/sqlglot/parser.py
+++ b/sqlglot/parser.py
@@ -4232,7 +4232,7 @@ class Parser(metaclass=_Parser):
             elif op and self._curr:
                 field = self._parse_column_reference()
             else:
-                field = self._parse_field(anonymous_func=True, any_token=True)
+                field = self._parse_field(any_token=True, anonymous_func=True)
 
             if isinstance(field, exp.Func) and this:
                 # bigquery allows function calls like x.y.count(...)
@@ -4320,17 +4320,23 @@ class Parser(metaclass=_Parser):
         tokens: t.Optional[t.Collection[TokenType]] = None,
         anonymous_func: bool = False,
     ) -> t.Optional[exp.Expression]:
-        return (
-            self._parse_primary()
-            or self._parse_function(anonymous=anonymous_func)
-            or self._parse_id_var(any_token=any_token, tokens=tokens)
-        )
+        if anonymous_func:
+            field = (
+                self._parse_function(anonymous=anonymous_func, any_token=any_token)
+                or self._parse_primary()
+            )
+        else:
+            field = self._parse_primary() or self._parse_function(
+                anonymous=anonymous_func, any_token=any_token
+            )
+        return field or self._parse_id_var(any_token=any_token, tokens=tokens)
 
     def _parse_function(
         self,
         functions: t.Optional[t.Dict[str, t.Callable]] = None,
         anonymous: bool = False,
         optional_parens: bool = True,
+        any_token: bool = False,
     ) -> t.Optional[exp.Expression]:
         # This allows us to also parse {fn <function>} syntax (Snowflake, MySQL support this)
         # See: https://community.snowflake.com/s/article/SQL-Escape-Sequences
@@ -4344,7 +4350,10 @@ class Parser(metaclass=_Parser):
             fn_syntax = True
 
         func = self._parse_function_call(
-            functions=functions, anonymous=anonymous, optional_parens=optional_parens
+            functions=functions,
+            anonymous=anonymous,
+            optional_parens=optional_parens,
+            any_token=any_token,
         )
 
         if fn_syntax:
@@ -4357,6 +4366,7 @@ class Parser(metaclass=_Parser):
         functions: t.Optional[t.Dict[str, t.Callable]] = None,
         anonymous: bool = False,
         optional_parens: bool = True,
+        any_token: bool = False,
     ) -> t.Optional[exp.Expression]:
         if not self._curr:
             return None
@@ -4378,7 +4388,7 @@ class Parser(metaclass=_Parser):
 
             return None
 
-        if token_type not in self.FUNC_TOKENS:
+        if not any_token and token_type not in self.FUNC_TOKENS:
             return None
 
         self._advance(2)


Test patch:
diff --git a/tests/dialects/test_bigquery.py b/tests/dialects/test_bigquery.py
index 820e75f6..e4553405 100644
--- a/tests/dialects/test_bigquery.py
+++ b/tests/dialects/test_bigquery.py
@@ -67,6 +67,7 @@ class TestBigQuery(Validator):
         select_with_quoted_udf = self.validate_identity("SELECT `p.d.UdF`(data) FROM `p.d.t`")
         self.assertEqual(select_with_quoted_udf.selects[0].name, "p.d.UdF")
 
+        self.validate_identity("assert.true(1 = 1)")
         self.validate_identity("SELECT ARRAY_TO_STRING(list, '--') AS text")
         self.validate_identity("SELECT jsondoc['some_key']")
         self.validate_identity("SELECT `p.d.UdF`(data).* FROM `p.d.t`")