Repository: tobymao/sqlglot

Issue:
Support for Snowflake TO_TIME , TIME
**Is your feature request related to a problem? Please describe.**
The executor is not able to run queries containing `TO_TIME` , or `TIME` calls when the reader is set to `sqlglot.dialects.Snowflake`

Example:
```python
def test_time_function():
    query = "SELECT TO_TIME('13:30:00');"
    assert executor.execute(query, read=Snowflake)
```
raises:
```
>               raise ExecuteError(f"Step '{node.id}' failed: {e}") from e
E               sqlglot.errors.ExecuteError: Step 'Scan: (4541617904)' failed: name 'TO_TIME' is not defined
```

**Describe the solution you'd like**
I would like support for this function

**Describe alternatives you've considered**
I would gladly take on authoring a PR (including testing) if documentation was provided for how to implement this feature. Even if those steps were provided in this GH issue I could add them to the readme

**Additional context**
N/A



Discussion:
Hi Erik,

Thanks for reporting this issue, we will look into it. Indeed, documentation could be improved in this aspect, it's something that we are looking at lately and will be happy to accept PR's to increase it's scope. 

Resolving patch:
diff --git a/sqlglot/dialects/snowflake.py b/sqlglot/dialects/snowflake.py
--- a/sqlglot/dialects/snowflake.py
+++ b/sqlglot/dialects/snowflake.py
@@ -20,8 +20,7 @@
     timestrtotime_sql,
     var_map_sql,
 )
-from sqlglot.expressions import Literal
-from sqlglot.helper import flatten, is_int, seq_get
+from sqlglot.helper import flatten, is_float, is_int, seq_get
 from sqlglot.tokens import TokenType
 
 if t.TYPE_CHECKING:
@@ -29,33 +28,28 @@
 
 
 # from https://docs.snowflake.com/en/sql-reference/functions/to_timestamp.html
-def _build_to_timestamp(args: t.List) -> t.Union[exp.StrToTime, exp.UnixToTime, exp.TimeStrToTime]:
-    if len(args) == 2:
-        first_arg, second_arg = args
-        if second_arg.is_string:
-            # case: <string_expr> [ , <format> ]
-            return build_formatted_time(exp.StrToTime, "snowflake")(args)
-        return exp.UnixToTime(this=first_arg, scale=second_arg)
+def _build_timestamp(name: str, kind: exp.DataType.Type) -> t.Callable[[t.List], exp.Func]:
+    def _builder(args: t.List) -> exp.Func:
+        value = seq_get(args, 0)
 
-    from sqlglot.optimizer.simplify import simplify_literals
+        if isinstance(value, exp.Literal):
+            int_value = is_int(value.this)
 
-    # The first argument might be an expression like 40 * 365 * 86400, so we try to
-    # reduce it using `simplify_literals` first and then check if it's a Literal.
-    first_arg = seq_get(args, 0)
-    if not isinstance(simplify_literals(first_arg, root=True), Literal):
-        # case: <variant_expr> or other expressions such as columns
-        return exp.TimeStrToTime.from_arg_list(args)
+            # Converts calls like `TO_TIME('01:02:03')` into casts
+            if len(args) == 1 and value.is_string and not int_value:
+                return exp.cast(value, kind)
 
-    if first_arg.is_string:
-        if is_int(first_arg.this):
-            # case: <integer>
-            return exp.UnixToTime.from_arg_list(args)
+            # Handles `TO_TIMESTAMP(str, fmt)` and `TO_TIMESTAMP(num, scale)` as special
+            # cases so we can transpile them, since they're relatively common
+            if kind == exp.DataType.Type.TIMESTAMP:
+                if int_value:
+                    return exp.UnixToTime(this=value, scale=seq_get(args, 1))
+                if not is_float(value.this):
+                    return build_formatted_time(exp.StrToTime, "snowflake")(args)
 
-        # case: <date_expr>
-        return build_formatted_time(exp.StrToTime, "snowflake", default=True)(args)
+        return exp.Anonymous(this=name, expressions=args)
 
-    # case: <numeric_expr>
-    return exp.UnixToTime.from_arg_list(args)
+    return _builder
 
 
 def _build_object_construct(args: t.List) -> t.Union[exp.StarMap, exp.Struct]:
@@ -364,7 +358,13 @@ class Parser(parser.Parser):
                 precision=seq_get(args, 2),
                 scale=seq_get(args, 3),
             ),
-            "TO_TIMESTAMP": _build_to_timestamp,
+            "TO_TIME": _build_timestamp("TO_TIME", exp.DataType.Type.TIME),
+            "TO_TIMESTAMP": _build_timestamp("TO_TIMESTAMP", exp.DataType.Type.TIMESTAMP),
+            "TO_TIMESTAMP_LTZ": _build_timestamp(
+                "TO_TIMESTAMP_LTZ", exp.DataType.Type.TIMESTAMPLTZ
+            ),
+            "TO_TIMESTAMP_NTZ": _build_timestamp("TO_TIMESTAMP_NTZ", exp.DataType.Type.TIMESTAMP),
+            "TO_TIMESTAMP_TZ": _build_timestamp("TO_TIMESTAMP_TZ", exp.DataType.Type.TIMESTAMPTZ),
             "TO_VARCHAR": exp.ToChar.from_arg_list,
             "ZEROIFNULL": _build_if_from_zeroifnull,
         }
diff --git a/sqlglot/executor/env.py b/sqlglot/executor/env.py
--- a/sqlglot/executor/env.py
+++ b/sqlglot/executor/env.py
@@ -106,6 +106,13 @@ def cast(this, to):
             return this
         if isinstance(this, str):
             return datetime.date.fromisoformat(this)
+    if to == exp.DataType.Type.TIME:
+        if isinstance(this, datetime.datetime):
+            return this.time()
+        if isinstance(this, datetime.time):
+            return this
+        if isinstance(this, str):
+            return datetime.time.fromisoformat(this)
     if to in (exp.DataType.Type.DATETIME, exp.DataType.Type.TIMESTAMP):
         if isinstance(this, datetime.datetime):
             return this
diff --git a/sqlglot/helper.py b/sqlglot/helper.py
--- a/sqlglot/helper.py
+++ b/sqlglot/helper.py
@@ -317,8 +317,16 @@ def find_new_name(taken: t.Collection[str], base: str) -> str:
 
 
 def is_int(text: str) -> bool:
+    return is_type(text, int)
+
+
+def is_float(text: str) -> bool:
+    return is_type(text, float)
+
+
+def is_type(text: str, target_type: t.Type) -> bool:
     try:
-        int(text)
+        target_type(text)
         return True
     except ValueError:
         return False


Test patch:
diff --git a/tests/dialects/test_snowflake.py b/tests/dialects/test_snowflake.py
--- a/tests/dialects/test_snowflake.py
+++ b/tests/dialects/test_snowflake.py
@@ -40,6 +40,12 @@ def test_snowflake(self):
   )""",
         )
 
+        self.validate_identity("SELECT TO_TIMESTAMP(123.4)").selects[0].assert_is(exp.Anonymous)
+        self.validate_identity("SELECT TO_TIME(x) FROM t")
+        self.validate_identity("SELECT TO_TIMESTAMP(x) FROM t")
+        self.validate_identity("SELECT TO_TIMESTAMP_NTZ(x) FROM t")
+        self.validate_identity("SELECT TO_TIMESTAMP_LTZ(x) FROM t")
+        self.validate_identity("SELECT TO_TIMESTAMP_TZ(x) FROM t")
         self.validate_identity("TO_DECIMAL(expr, fmt, precision, scale)")
         self.validate_identity("ALTER TABLE authors ADD CONSTRAINT c1 UNIQUE (id, email)")
         self.validate_identity("RM @parquet_stage", check_command_warning=True)
@@ -198,10 +204,6 @@ def test_snowflake(self):
             "SELECT {fn CEILING(5.3)}",
             "SELECT CEIL(5.3)",
         )
-        self.validate_identity(
-            "SELECT TO_TIMESTAMP(x) FROM t",
-            "SELECT CAST(x AS TIMESTAMPNTZ) FROM t",
-        )
         self.validate_identity(
             "CAST(x AS BYTEINT)",
             "CAST(x AS INT)",
@@ -632,9 +634,16 @@ def test_snowflake(self):
         self.validate_all(
             "SELECT TO_TIMESTAMP('2013-04-05 01:02:03')",
             write={
-                "bigquery": "SELECT PARSE_TIMESTAMP('%Y-%m-%d %H:%M:%S', '2013-04-05 01:02:03')",
-                "snowflake": "SELECT TO_TIMESTAMP('2013-04-05 01:02:03', 'yyyy-mm-DD hh24:mi:ss')",
-                "spark": "SELECT TO_TIMESTAMP('2013-04-05 01:02:03', 'yyyy-MM-dd HH:mm:ss')",
+                "bigquery": "SELECT CAST('2013-04-05 01:02:03' AS DATETIME)",
+                "snowflake": "SELECT CAST('2013-04-05 01:02:03' AS TIMESTAMPNTZ)",
+                "spark": "SELECT CAST('2013-04-05 01:02:03' AS TIMESTAMP)",
+            },
+        )
+        self.validate_all(
+            "SELECT TO_TIME('12:05:00')",
+            write={
+                "bigquery": "SELECT CAST('12:05:00' AS TIME)",
+                "snowflake": "SELECT CAST('12:05:00' AS TIME)",
             },
         )
         self.validate_all(
diff --git a/tests/test_executor.py b/tests/test_executor.py
--- a/tests/test_executor.py
+++ b/tests/test_executor.py
@@ -1,7 +1,7 @@
 import os
 import datetime
 import unittest
-from datetime import date
+from datetime import date, time
 from multiprocessing import Pool
 
 import duckdb
@@ -640,6 +640,7 @@ def test_scalar_functions(self):
             ("CAST(1 AS TEXT)", "1"),
             ("CAST('1' AS LONG)", 1),
             ("CAST('1.1' AS FLOAT)", 1.1),
+            ("CAST('12:05:01' AS TIME)", time(12, 5, 1)),
             ("COALESCE(NULL)", None),
             ("COALESCE(NULL, NULL)", None),
             ("COALESCE(NULL, 'b')", "b"),