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


Comments:

Patch:
diff --git a/sqlglot/dialects/snowflake.py b/sqlglot/dialects/snowflake.py
index 804775c3..18154779 100644
--- a/sqlglot/dialects/snowflake.py
+++ b/sqlglot/dialects/snowflake.py
@@ -20,8 +20,7 @@ from sqlglot.dialects.dialect import (
     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 @@ if t.TYPE_CHECKING:
 
 
 # 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 Snowflake(Dialect):
                 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
index 0d0d4813..c51049bb 100644
--- 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
index 0d4547fa..bcc68c3c 100644
--- 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
index e4cec3a9..9d5a93be 100644
--- a/tests/dialects/test_snowflake.py
+++ b/tests/dialects/test_snowflake.py
@@ -40,6 +40,12 @@ WHERE
   )""",
         )
 
+        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 @@ WHERE
             "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 @@ WHERE
         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
index 4b81359c..1eaca14f 100644
--- 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 @@ class TestExecutor(unittest.TestCase):
             ("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"),