Repository: tobymao/sqlglot

Issue:
Support for GCP BigQuery's `TRIM` native function.
# Feature request

The SQL engine does not support the native function of BigQuery `TRIM`.

[Here](https://cloud.google.com/bigquery/docs/reference/standard-sql/string_functions#trim) is the official documentation for the function.


----

# Test performed

When executing the following query (with `sqlglot 18.8.0`)
```python
from sqlglot.dialects import BigQuery
from sqlglot.executor import execute

execute(sql='SELECT TRIM(" hello ") as field_1', read=BigQuery)
```
I would expect the result to be
```csv
field_1
hello
```
with the string `hello` not containing any leading or trailing whitespaces.

Instead I get the following `ExecuteError`
```
Traceback (most recent call last):
  File "[REDACTED]/.venv/lib/python3.9/site-packages/sqlglot/executor/python.py", line 38, in execute
    contexts[node] = self.scan(node, context)
  File "[REDACTED]/.venv/lib/python3.9/site-packages/sqlglot/executor/python.py", line 106, in scan
    return self.context({step.name: self._project_and_filter(context, step, table_iter)})
  File "[REDACTED]/.venv/lib/python3.9/site-packages/sqlglot/executor/python.py", line 121, in _project_and_filter
    sink.append(context.eval_tuple(projections))
  File "[REDACTED]/.venv/lib/python3.9/site-packages/sqlglot/executor/context.py", line 38, in eval_tuple
    return tuple(self.eval(code) for code in codes)
  File "[REDACTED]/.venv/lib/python3.9/site-packages/sqlglot/executor/context.py", line 38, in <genexpr>
    return tuple(self.eval(code) for code in codes)
  File "[REDACTED]/.venv/lib/python3.9/site-packages/sqlglot/executor/context.py", line 35, in eval
    return eval(code, self.env)
  File "TRIM(' hello ')", line 1, in <module>
NameError: name 'TRIM' is not defined

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "[REDACTED]/__sqlglot_trial.py", line 4, in <module>
    execute(sql='SELECT TRIM(" hello ") as field_1', read=BigQuery)
  File "[REDACTED]/.venv/lib/python3.9/site-packages/sqlglot/executor/__init__.py", line 88, in execute
    result = PythonExecutor(tables=tables_).execute(plan)
  File "[REDACTED]/.venv/lib/python3.9/site-packages/sqlglot/executor/python.py", line 60, in execute
    raise ExecuteError(f"Step '{node.id}' failed: {e}") from e
sqlglot.errors.ExecuteError: Step 'Scan: (4348114064)' failed: name 'TRIM' is not defined```


Discussion:

Resolving patch:
diff --git a/sqlglot/dialects/spark2.py b/sqlglot/dialects/spark2.py
--- a/sqlglot/dialects/spark2.py
+++ b/sqlglot/dialects/spark2.py
@@ -101,6 +101,8 @@ def _unqualify_pivot_columns(expression: exp.Expression) -> exp.Expression:
 
 class Spark2(Hive):
     class Parser(Hive.Parser):
+        TRIM_PATTERN_FIRST = True
+
         FUNCTIONS = {
             **Hive.Parser.FUNCTIONS,
             "AGGREGATE": exp.Reduce.from_arg_list,
diff --git a/sqlglot/executor/env.py b/sqlglot/executor/env.py
--- a/sqlglot/executor/env.py
+++ b/sqlglot/executor/env.py
@@ -202,4 +202,5 @@ def interval(this, unit):
     "CURRENTTIME": datetime.datetime.now,
     "CURRENTDATE": datetime.date.today,
     "STRFTIME": null_if_any(lambda fmt, arg: datetime.datetime.fromisoformat(arg).strftime(fmt)),
+    "TRIM": null_if_any(lambda this, e=None: this.strip(e)),
 }
diff --git a/sqlglot/parser.py b/sqlglot/parser.py
--- a/sqlglot/parser.py
+++ b/sqlglot/parser.py
@@ -888,9 +888,12 @@ class Parser(metaclass=_Parser):
     # Whether or not the table sample clause expects CSV syntax
     TABLESAMPLE_CSV = False
 
-    # Whether or not the SET command needs a delimiter (e.g. "=") for assignments.
+    # Whether or not the SET command needs a delimiter (e.g. "=") for assignments
     SET_REQUIRES_ASSIGNMENT_DELIMITER = True
 
+    # Whether the TRIM function expects the characters to trim as its first argument
+    TRIM_PATTERN_FIRST = False
+
     __slots__ = (
         "error_level",
         "error_message_context",
@@ -4410,16 +4413,18 @@ def _parse_trim(self) -> exp.Trim:
 
         position = None
         collation = None
+        expression = None
 
         if self._match_texts(self.TRIM_TYPES):
             position = self._prev.text.upper()
 
-        expression = self._parse_bitwise()
+        this = self._parse_bitwise()
         if self._match_set((TokenType.FROM, TokenType.COMMA)):
-            this = self._parse_bitwise()
-        else:
-            this = expression
-            expression = None
+            invert_order = self._prev.token_type == TokenType.FROM or self.TRIM_PATTERN_FIRST
+            expression = self._parse_bitwise()
+
+            if invert_order:
+                this, expression = expression, this
 
         if self._match(TokenType.COLLATE):
             collation = self._parse_bitwise()


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
@@ -134,6 +134,18 @@ def test_bigquery(self):
         self.validate_all('x <> """"""', write={"bigquery": "x <> ''"})
         self.validate_all("x <> ''''''", write={"bigquery": "x <> ''"})
         self.validate_all("CAST(x AS DATETIME)", read={"": "x::timestamp"})
+        self.validate_all(
+            "TRIM(item, '*')",
+            read={
+                "snowflake": "TRIM(item, '*')",
+                "spark": "TRIM('*', item)",
+            },
+            write={
+                "bigquery": "TRIM(item, '*')",
+                "snowflake": "TRIM(item, '*')",
+                "spark": "TRIM('*' FROM item)",
+            },
+        )
         self.validate_all(
             "CREATE OR REPLACE TABLE `a.b.c` COPY `a.b.d`",
             write={
diff --git a/tests/test_executor.py b/tests/test_executor.py
--- a/tests/test_executor.py
+++ b/tests/test_executor.py
@@ -624,6 +624,8 @@ def test_scalar_functions(self):
             ("LEFT('12345', 3)", "123"),
             ("RIGHT('12345', 3)", "345"),
             ("DATEDIFF('2022-01-03'::date, '2022-01-01'::TIMESTAMP::DATE)", 2),
+            ("TRIM(' foo ')", "foo"),
+            ("TRIM('afoob', 'ab')", "foo"),
         ]:
             with self.subTest(sql):
                 result = execute(f"SELECT {sql}")