Repository: tobymao/sqlglot

Issue:
TIMESTAMP_scale functions are incorrectly converted when targeting DuckDB
**Before you file an issue**
- Make sure you specify the "read" dialect eg. parse_one(sql, read="spark") yes
- Check if the issue still exists on main yes

**Fully reproducible code snippet**
Please include a fully reproducible code snippet or the input sql, dialect, and expected output.

TIMESTAMP_scale functions are improperly converted when DuckDB is the target dialect:
```
>>> sqlglot.transpile("SELECT TIMESTAMP_MILLIS(10000) AS ts", read="bigquery", write="duckdb")
['SELECT TIMESTAMP_MILLIS(10000) AS ts']
```
The following output is expected:
```
SELECT EPOCH_MS(10000) as TS
```

**Official Documentation**
Please include links to official SQL documentation related to your issue.

DuckDB timestamp functions are detailed [here](https://duckdb.org/docs/sql/functions/timestamp.html). 



Discussion:
Hey @j1ah0ng I'll be able to take a look soon, though it'd be very helpful if you opened PRs for issues like this which are relatively simple. If you wanna take a look before I (or someone else) jump on it, my guess is that you just need to add a few new expressions to represent these functions (if they don't exist already) and parse/generate them correctly in the dialects of interest.

If you're interested to contribute don't hesitate to join our [Slack](https://tobikodata.com/slack) to ask for guidance.
ack - thanks for the pointer! I'll circle back to this today and get something up. 
Sounds good. For this one in particular you can use an existing `Expression` which is called `UnixToTime`, so the fix would entail mapping `"TIMESTAMP_MILLIS"` in BQ to it in `BigQuery.Parser.FUNCTIONS` and then making sure that expression is also renamed to `"TIMESTAMP_MILLIS"` to make sure that a BQ parse/generate roundtrip will preserve the function.

Other `TIMESTAMP_*` functions might not have `Expression`s so you'd need to make those.

Resolving patch:
diff --git a/sqlglot/dialects/bigquery.py b/sqlglot/dialects/bigquery.py
--- a/sqlglot/dialects/bigquery.py
+++ b/sqlglot/dialects/bigquery.py
@@ -196,6 +196,20 @@ def _ts_or_ds_diff_sql(self: BigQuery.Generator, expression: exp.TsOrDsDiff) ->
     return self.func("DATE_DIFF", expression.this, expression.expression, unit)
 
 
+def _unix_to_time_sql(self: BigQuery.Generator, expression: exp.UnixToTime) -> str:
+    scale = expression.args.get("scale")
+    timestamp = self.sql(expression, "this")
+    if scale in {None, exp.UnixToTime.SECONDS}:
+        return f"TIMESTAMP_SECONDS({timestamp})"
+    if scale == exp.UnixToTime.MILLIS:
+        return f"TIMESTAMP_MILLIS({timestamp})"
+    if scale == exp.UnixToTime.MICROS:
+        return f"TIMESTAMP_MICROS({timestamp})"
+
+    self.unsupported("Improper scale for timestamp")
+    return ""
+
+
 class BigQuery(Dialect):
     UNNEST_COLUMN_ONLY = True
     SUPPORTS_USER_DEFINED_TYPES = False
@@ -337,6 +351,15 @@ class Parser(parser.Parser):
             "TIME_SUB": parse_date_delta_with_interval(exp.TimeSub),
             "TIMESTAMP_ADD": parse_date_delta_with_interval(exp.TimestampAdd),
             "TIMESTAMP_SUB": parse_date_delta_with_interval(exp.TimestampSub),
+            "TIMESTAMP_MICROS": lambda args: exp.UnixToTime(
+                this=seq_get(args, 0), scale=exp.UnixToTime.MICROS
+            ),
+            "TIMESTAMP_MILLIS": lambda args: exp.UnixToTime(
+                this=seq_get(args, 0), scale=exp.UnixToTime.MILLIS
+            ),
+            "TIMESTAMP_SECONDS": lambda args: exp.UnixToTime(
+                this=seq_get(args, 0), scale=exp.UnixToTime.SECONDS
+            ),
             "TO_JSON_STRING": exp.JSONFormat.from_arg_list,
         }
 
@@ -538,6 +561,7 @@ class Generator(generator.Generator):
             exp.TsOrDsDiff: _ts_or_ds_diff_sql,
             exp.TsOrDsToDate: ts_or_ds_to_date_sql("bigquery"),
             exp.Unhex: rename_func("FROM_HEX"),
+            exp.UnixToTime: _unix_to_time_sql,
             exp.Values: _derived_table_values_to_unnest,
             exp.VariancePop: rename_func("VAR_POP"),
         }
diff --git a/sqlglot/dialects/duckdb.py b/sqlglot/dialects/duckdb.py
--- a/sqlglot/dialects/duckdb.py
+++ b/sqlglot/dialects/duckdb.py
@@ -107,6 +107,19 @@ def _json_format_sql(self: DuckDB.Generator, expression: exp.JSONFormat) -> str:
     return f"CAST({sql} AS TEXT)"
 
 
+def _unix_to_time_sql(self: DuckDB.Generator, expression: exp.UnixToTime) -> str:
+    scale = expression.args.get("scale")
+    timestamp = self.sql(expression, "this")
+    if scale in {None, exp.UnixToTime.SECONDS}:
+        return f"TO_TIMESTAMP({timestamp})"
+    if scale == exp.UnixToTime.MILLIS:
+        return f"EPOCH_MS({timestamp})"
+    if scale == exp.UnixToTime.MICROS:
+        return f"MAKE_TIMESTAMP({timestamp})"
+
+    raise ValueError("Improper scale for timestamp")
+
+
 class DuckDB(Dialect):
     NULL_ORDERING = "nulls_are_last"
     SUPPORTS_USER_DEFINED_TYPES = False
@@ -159,11 +172,15 @@ class Parser(parser.Parser):
             "DATETRUNC": date_trunc_to_time,
             "EPOCH": exp.TimeToUnix.from_arg_list,
             "EPOCH_MS": lambda args: exp.UnixToTime(
-                this=exp.Div(this=seq_get(args, 0), expression=exp.Literal.number(1000))
+                this=seq_get(args, 0),
+                scale=exp.UnixToTime.MILLIS,
             ),
             "LIST_REVERSE_SORT": _sort_array_reverse,
             "LIST_SORT": exp.SortArray.from_arg_list,
             "LIST_VALUE": exp.Array.from_arg_list,
+            "MAKE_TIMESTAMP": lambda args: exp.UnixToTime(
+                this=seq_get(args, 0), scale=exp.UnixToTime.MICROS
+            ),
             "MEDIAN": lambda args: exp.PercentileCont(
                 this=seq_get(args, 0), expression=exp.Literal.number(0.5)
             ),
@@ -334,7 +351,7 @@ class Generator(generator.Generator):
             ),
             exp.TsOrDsToDate: ts_or_ds_to_date_sql("duckdb"),
             exp.UnixToStr: lambda self, e: f"STRFTIME(TO_TIMESTAMP({self.sql(e, 'this')}), {self.format_time(e)})",
-            exp.UnixToTime: rename_func("TO_TIMESTAMP"),
+            exp.UnixToTime: _unix_to_time_sql,
             exp.UnixToTimeStr: lambda self, e: f"CAST(TO_TIMESTAMP({self.sql(e, 'this')}) AS TEXT)",
             exp.VariancePop: rename_func("VAR_POP"),
             exp.WeekOfYear: rename_func("WEEKOFYEAR"),


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
@@ -109,6 +109,9 @@ def test_bigquery(self):
         self.validate_identity("ROLLBACK TRANSACTION")
         self.validate_identity("CAST(x AS BIGNUMERIC)")
         self.validate_identity("SELECT y + 1 FROM x GROUP BY y + 1 ORDER BY 1")
+        self.validate_identity("SELECT TIMESTAMP_SECONDS(2) AS t")
+        self.validate_identity("SELECT TIMESTAMP_MICROS(2) AS t")
+        self.validate_identity("SELECT TIMESTAMP_MILLIS(2) AS t")
         self.validate_identity(
             "FOR record IN (SELECT word, word_count FROM bigquery-public-data.samples.shakespeare LIMIT 5) DO SELECT record.word, record.word_count"
         )
diff --git a/tests/dialects/test_duckdb.py b/tests/dialects/test_duckdb.py
--- a/tests/dialects/test_duckdb.py
+++ b/tests/dialects/test_duckdb.py
@@ -109,6 +109,9 @@ def test_duckdb(self):
             parse_one("a // b", read="duckdb").assert_is(exp.IntDiv).sql(dialect="duckdb"), "a // b"
         )
 
+        self.validate_identity("SELECT EPOCH_MS(10) AS t")
+        self.validate_identity("SELECT MAKE_TIMESTAMP(10) AS t")
+        self.validate_identity("SELECT TO_TIMESTAMP(10) AS t")
         self.validate_identity("SELECT UNNEST(column, recursive := TRUE) FROM table")
         self.validate_identity("VAR_POP(a)")
         self.validate_identity("SELECT * FROM foo ASOF LEFT JOIN bar ON a = b")
@@ -551,10 +554,10 @@ def test_time(self):
         self.validate_all(
             "EPOCH_MS(x)",
             write={
-                "bigquery": "UNIX_TO_TIME(x / 1000)",
-                "duckdb": "TO_TIMESTAMP(x / 1000)",
-                "presto": "FROM_UNIXTIME(CAST(x AS DOUBLE) / 1000)",
-                "spark": "CAST(FROM_UNIXTIME(x / 1000) AS TIMESTAMP)",
+                "bigquery": "TIMESTAMP_MILLIS(x)",
+                "duckdb": "EPOCH_MS(x)",
+                "presto": "FROM_UNIXTIME(x, 'millis')",
+                "spark": "TIMESTAMP_MILLIS(x)",
             },
         )
         self.validate_all(
@@ -589,7 +592,7 @@ def test_time(self):
         self.validate_all(
             "TO_TIMESTAMP(x)",
             write={
-                "bigquery": "UNIX_TO_TIME(x)",
+                "bigquery": "TIMESTAMP_SECONDS(x)",
                 "duckdb": "TO_TIMESTAMP(x)",
                 "presto": "FROM_UNIXTIME(x)",
                 "hive": "FROM_UNIXTIME(x)",
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
@@ -375,7 +375,7 @@ def test_snowflake(self):
         self.validate_all(
             "SELECT TO_TIMESTAMP(1659981729)",
             write={
-                "bigquery": "SELECT UNIX_TO_TIME(1659981729)",
+                "bigquery": "SELECT TIMESTAMP_SECONDS(1659981729)",
                 "snowflake": "SELECT TO_TIMESTAMP(1659981729)",
                 "spark": "SELECT CAST(FROM_UNIXTIME(1659981729) AS TIMESTAMP)",
             },
@@ -383,7 +383,7 @@ def test_snowflake(self):
         self.validate_all(
             "SELECT TO_TIMESTAMP(1659981729000, 3)",
             write={
-                "bigquery": "SELECT UNIX_TO_TIME(1659981729000, 'millis')",
+                "bigquery": "SELECT TIMESTAMP_MILLIS(1659981729000)",
                 "snowflake": "SELECT TO_TIMESTAMP(1659981729000, 3)",
                 "spark": "SELECT TIMESTAMP_MILLIS(1659981729000)",
             },
@@ -391,7 +391,7 @@ def test_snowflake(self):
         self.validate_all(
             "SELECT TO_TIMESTAMP('1659981729')",
             write={
-                "bigquery": "SELECT UNIX_TO_TIME('1659981729')",
+                "bigquery": "SELECT TIMESTAMP_SECONDS('1659981729')",
                 "snowflake": "SELECT TO_TIMESTAMP('1659981729')",
                 "spark": "SELECT CAST(FROM_UNIXTIME('1659981729') AS TIMESTAMP)",
             },
@@ -399,7 +399,7 @@ def test_snowflake(self):
         self.validate_all(
             "SELECT TO_TIMESTAMP(1659981729000000000, 9)",
             write={
-                "bigquery": "SELECT UNIX_TO_TIME(1659981729000000000, 'micros')",
+                "bigquery": "SELECT TIMESTAMP_MICROS(1659981729000000000)",
                 "snowflake": "SELECT TO_TIMESTAMP(1659981729000000000, 9)",
                 "spark": "SELECT TIMESTAMP_MICROS(1659981729000000000)",
             },