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:

Resolving patch:
diff --git a/sqlglot/dialects/clickhouse.py b/sqlglot/dialects/clickhouse.py
--- a/sqlglot/dialects/clickhouse.py
+++ b/sqlglot/dialects/clickhouse.py
@@ -461,9 +461,13 @@ def _parse_function(
             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
--- a/sqlglot/parser.py
+++ b/sqlglot/parser.py
@@ -4232,7 +4232,7 @@ def _parse_column_ops(self, this: t.Optional[exp.Expression]) -> t.Optional[exp.
             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 @@ def _parse_field(
         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 @@ def _parse_function(
             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 @@ def _parse_function_call(
         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 @@ def _parse_function_call(
 
             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
--- a/tests/dialects/test_bigquery.py
+++ b/tests/dialects/test_bigquery.py
@@ -67,6 +67,7 @@ def test_bigquery(self):
         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`")