diff --git a/sqlglot/dialects/duckdb.py b/sqlglot/dialects/duckdb.py index cab31571e4..e6002832fa 100644 --- a/sqlglot/dialects/duckdb.py +++ b/sqlglot/dialects/duckdb.py @@ -1684,6 +1684,48 @@ def zipf_sql(self: DuckDB.Generator, expression: exp.Zipf) -> str: replacements = {"s": s, "n": n, "random_expr": random_expr} return f"({self.sql(exp.replace_placeholders(self.ZIPF_TEMPLATE, **replacements))})" + def unixtodate_sql(self: DuckDB.Generator, expression: exp.UnixToDate) -> str: + """ + Transpile UnixToDate to DuckDB equivalent using make_timestamp_ns with Snowflake logic. + + Converts Unix timestamps following Snowflake's interpretation rules: + - Values <= 31536000000 (Jan 1, 1971): treated as seconds, multiply by 1000000000 + - Values <= 31536000000000 (Jan 1, 1971 in ms): treated as milliseconds, multiply by 1000000 + - Values <= 31536000000000000 (Jan 1, 1971 in μs): treated as microseconds, multiply by 1000 + - Larger values: treated as nanoseconds, use as-is + """ + value = expression.this + + # Convert input to BIGINT first for comparisons + bigint_value = exp.cast(value, exp.DataType.Type.BIGINT) + + # Use CASE to determine precision based on Snowflake thresholds and multiply accordingly so that the value is in nanoseconds + case_expr = ( + exp.case() + .when( + exp.LT(this=bigint_value, expression=exp.Literal.number(31536000000)), + exp.Mul( + this=bigint_value, expression=exp.Literal.number(1000000000) + ), # seconds + ) + .when( + exp.LT(this=bigint_value, expression=exp.Literal.number(31536000000000)), + exp.Mul( + this=bigint_value, expression=exp.Literal.number(1000000) + ), # milliseconds + ) + .when( + exp.LT(this=bigint_value, expression=exp.Literal.number(31536000000000000)), + exp.Mul(this=bigint_value, expression=exp.Literal.number(1000)), # microseconds + ) + .else_(bigint_value) + ) # nanoseconds, use as-is + + timestamp_expr = exp.func("MAKE_TIMESTAMP_NS", case_expr) + date_expr = exp.cast(timestamp_expr, exp.DataType.Type.DATE) + + return self.sql(date_expr) + def tobinary_sql(self: DuckDB.Generator, expression: exp.ToBinary) -> str: """ TO_BINARY and TRY_TO_BINARY transpilation: diff --git a/sqlglot/dialects/snowflake.py b/sqlglot/dialects/snowflake.py index d3e4ada469..8cb0f6408c 100644 --- a/sqlglot/dialects/snowflake.py +++ b/sqlglot/dialects/snowflake.py @@ -115,6 +115,9 @@ def _builder(args: t.List) -> exp.Func: formatted_exp.set("safe", safe) return formatted_exp + if kind == exp.DataType.Type.DATE and int_value: + return exp.UnixToDate(this=value, safe=safe) + return exp.Anonymous(this=name, expressions=args) return _builder @@ -1579,6 +1582,7 @@ class Generator(generator.Generator): exp.TsOrDsToDate: lambda self, e: self.func( f"{'TRY_' if e.args.get('safe') else ''}TO_DATE", e.this, self.format_time(e) ), + exp.UnixToDate: rename_func("TO_DATE"), exp.TsOrDsToTime: lambda self, e: self.func( f"{'TRY_' if e.args.get('safe') else ''}TO_TIME", e.this, self.format_time(e) ), diff --git a/sqlglot/expressions.py b/sqlglot/expressions.py index 24f798ba7c..62dc050708 100644 --- a/sqlglot/expressions.py +++ b/sqlglot/expressions.py @@ -8172,6 +8172,10 @@ class UnixToStr(Func): arg_types = {"this": True, "format": False} +class UnixToDate(Func): + arg_types = {"this": True, "safe": False} + + # https://prestodb.io/docs/current/functions/datetime.html # presto has weird zone/hours/minutes class UnixToTime(Func): diff --git a/sqlglot/typing/__init__.py b/sqlglot/typing/__init__.py index 86af31b949..16e6b0d223 100644 --- a/sqlglot/typing/__init__.py +++ b/sqlglot/typing/__init__.py @@ -73,6 +73,7 @@ exp.StrToDate, exp.TimeStrToDate, exp.TsOrDsToDate, + exp.UnixToDate, } }, **{ diff --git a/tests/dialects/test_snowflake.py b/tests/dialects/test_snowflake.py index 0a05bdf4d7..a7d494fbcf 100644 --- a/tests/dialects/test_snowflake.py +++ b/tests/dialects/test_snowflake.py @@ -2548,7 +2548,7 @@ def test_timestamps(self): self.validate_identity("DATE_PART(yyy, x)", "DATE_PART(YEAR, x)") self.validate_identity("DATE_TRUNC(yr, x)", "DATE_TRUNC('YEAR', x)") - self.validate_identity("TO_DATE('12345')").assert_is(exp.Anonymous) + self.validate_identity("TO_DATE('12345')").assert_is(exp.UnixToDate) self.validate_identity( "SELECT TO_DATE('2019-02-28') + INTERVAL '1 day, 1 year'", @@ -2565,6 +2565,20 @@ def test_timestamps(self): "snowflake": "TO_DATE(x)", }, ) + self.validate_all( + "TO_DATE('1640995200')", + write={ + "snowflake": "TO_DATE('1640995200')", + "duckdb": "CAST(MAKE_TIMESTAMP_NS(CASE WHEN CAST('1640995200' AS BIGINT) < 31536000000 THEN CAST('1640995200' AS BIGINT) * 1000000000 WHEN CAST('1640995200' AS BIGINT) < 31536000000000 THEN CAST('1640995200' AS BIGINT) * 1000000 WHEN CAST('1640995200' AS BIGINT) < 31536000000000000 THEN CAST('1640995200' AS BIGINT) * 1000 ELSE CAST('1640995200' AS BIGINT) END) AS DATE)", + }, + ) + self.validate_all( + "DATE('1640995200')", + write={ + "snowflake": "TO_DATE('1640995200')", + "duckdb": "CAST(MAKE_TIMESTAMP_NS(CASE WHEN CAST('1640995200' AS BIGINT) < 31536000000 THEN CAST('1640995200' AS BIGINT) * 1000000000 WHEN CAST('1640995200' AS BIGINT) < 31536000000000 THEN CAST('1640995200' AS BIGINT) * 1000000 WHEN CAST('1640995200' AS BIGINT) < 31536000000000000 THEN CAST('1640995200' AS BIGINT) * 1000 ELSE CAST('1640995200' AS BIGINT) END) AS DATE)", + }, + ) self.validate_all( "TO_DATE(x, 'MM-DD-YYYY')", write={