Skip to content

Commit 9105f96

Browse files
committed
handle date_trunc(time_part, time) and clean up
1 parent 5dec790 commit 9105f96

File tree

2 files changed

+50
-38
lines changed

2 files changed

+50
-38
lines changed

sqlglot/dialects/duckdb.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2456,7 +2456,12 @@ def datetrunc_sql(self, expression: exp.DateTrunc) -> str:
24562456
date = expression.this
24572457
result = self.func("DATE_TRUNC", unit, date)
24582458

2459-
if expression.args.get("cast_to_granularity_type") and date.type:
2459+
if not date.type:
2460+
from sqlglot.optimizer.annotate_types import annotate_types
2461+
2462+
date = annotate_types(date, dialect=self.dialect)
2463+
2464+
if date.type and expression.args.get("cast_to_granularity_type"):
24602465
return self.sql(exp.Cast(this=result, to=date.type))
24612466
return result
24622467

@@ -2475,7 +2480,23 @@ def timestamptrunc_sql(self, expression: exp.TimestampTrunc) -> str:
24752480
return self.sql(exp.AtTimeZone(this=result_sql, zone=zone))
24762481

24772482
result = self.func("DATE_TRUNC", unit, timestamp)
2478-
if expression.args.get("cast_to_granularity_type") and timestamp.type:
2483+
if not timestamp.type:
2484+
from sqlglot.optimizer.annotate_types import annotate_types
2485+
2486+
timestamp = annotate_types(timestamp, dialect=self.dialect)
2487+
2488+
if timestamp.type and timestamp.is_type(
2489+
exp.DataType.Type.TIME, exp.DataType.Type.TIMETZ
2490+
):
2491+
dummy_date = exp.Cast(
2492+
this=exp.Literal.string("1970-01-01"),
2493+
to=exp.DataType(this=exp.DataType.Type.DATE),
2494+
)
2495+
date_time = exp.Add(this=dummy_date, expression=timestamp)
2496+
result = self.func("DATE_TRUNC", unit, date_time)
2497+
return self.sql(exp.Cast(this=result, to=timestamp.type))
2498+
2499+
if timestamp.type and expression.args.get("cast_to_granularity_type"):
24792500
return self.sql(exp.Cast(this=result, to=timestamp.type))
24802501
return result
24812502

tests/dialects/test_snowflake.py

Lines changed: 27 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2676,56 +2676,47 @@ def test_timestamps(self):
26762676
},
26772677
)
26782678
self.validate_all(
2679-
"DATE_TRUNC('YEAR', CAST('2026-01-01 00:00:00' AS TIMESTAMP))",
2679+
"DATE_TRUNC('HOUR', CAST('2026-01-01 00:00:00' AS TIMESTAMP))",
26802680
write={
2681-
"snowflake": "DATE_TRUNC('YEAR', CAST('2026-01-01 00:00:00' AS TIMESTAMP))",
2682-
"duckdb": "DATE_TRUNC('YEAR', CAST('2026-01-01 00:00:00' AS TIMESTAMP))",
2681+
"snowflake": "DATE_TRUNC('HOUR', CAST('2026-01-01 00:00:00' AS TIMESTAMP))",
2682+
"duckdb": "DATE_TRUNC('HOUR', CAST('2026-01-01 00:00:00' AS TIMESTAMP))",
26832683
},
26842684
)
2685+
# Snowflake's DATE_TRUNC return type matches type of the expresison
2686+
# DuckDB's DATE_TRUNC return type matches type of granularity part.
2687+
# In Snowflake --> DuckDB, DATE_TRUNC(date_part, timestamp) should be cast to timestamp to preserve Snowflake behavior.
26852688
self.validate_all(
2686-
"DATE_TRUNC('HOUR', CAST('2026-01-01' AS DATE))",
2689+
"DATE_TRUNC(YEAR, TIMESTAMP '2026-01-01 00:00:00')",
26872690
write={
2688-
"snowflake": "DATE_TRUNC('HOUR', CAST('2026-01-01' AS DATE))",
2689-
"duckdb": "DATE_TRUNC('HOUR', CAST('2026-01-01' AS DATE))",
2691+
"snowflake": "DATE_TRUNC('YEAR', CAST('2026-01-01 00:00:00' AS TIMESTAMP))",
2692+
"duckdb": "CAST(DATE_TRUNC('YEAR', CAST('2026-01-01 00:00:00' AS TIMESTAMP)) AS TIMESTAMP)",
26902693
},
26912694
)
26922695
self.validate_all(
2693-
"DATE_TRUNC('HOUR', CAST('2026-01-01 00:00:00' AS TIMESTAMP))",
2696+
"DATE_TRUNC(MONTH, CAST('2024-06-15 14:23:45' AS TIMESTAMPTZ))",
26942697
write={
2695-
"snowflake": "DATE_TRUNC('HOUR', CAST('2026-01-01 00:00:00' AS TIMESTAMP))",
2696-
"duckdb": "DATE_TRUNC('HOUR', CAST('2026-01-01 00:00:00' AS TIMESTAMP))",
2698+
"snowflake": "DATE_TRUNC('MONTH', CAST('2024-06-15 14:23:45' AS TIMESTAMPTZ))",
2699+
"duckdb": "CAST(DATE_TRUNC('MONTH', CAST('2024-06-15 14:23:45' AS TIMESTAMPTZ)) AS TIMESTAMPTZ)",
26972700
},
26982701
)
2699-
# Snowflake's DATE_TRUNC return type matches type of the expresison
2700-
# DuckDB's DATE_TRUNC return type matches type of granularity part.
2701-
# In Snowflake --> DuckDB, DATE_TRUNC(date_part, timestamp) should be cast to timestamp to preserve Snowflake behavior.
2702-
expr = self.parse_one("DATE_TRUNC(YEAR, TIMESTAMP '2026-01-01 00:00:00')")
2703-
annotated = annotate_types(expr, dialect="snowflake")
2704-
self.assertEqual(
2705-
annotated.sql("duckdb"),
2706-
"CAST(DATE_TRUNC('YEAR', CAST('2026-01-01 00:00:00' AS TIMESTAMP)) AS TIMESTAMP)",
2707-
)
27082702

2709-
expr = self.parse_one("DATE_TRUNC(MONTH, CAST('2024-06-15 14:23:45' AS TIMESTAMPTZ))")
2710-
annotated = annotate_types(expr, dialect="snowflake")
2711-
self.assertEqual(
2712-
annotated.sql("duckdb"),
2713-
"CAST(DATE_TRUNC('MONTH', CAST('2024-06-15 14:23:45' AS TIMESTAMPTZ)) AS TIMESTAMPTZ)",
2714-
)
2715-
# Time unit and timestamp should not trigger extra type casting
2716-
expr = self.parse_one("DATE_TRUNC(HOUR, TIMESTAMP '2024-06-15 14:23:45')")
2717-
annotated = annotate_types(expr, dialect="snowflake")
2718-
self.assertEqual(
2719-
annotated.sql("duckdb"),
2720-
"DATE_TRUNC('HOUR', CAST('2024-06-15 14:23:45' AS TIMESTAMP))",
2703+
# In Snowflake --> DuckDB, DATE_TRUNC(time_part, date) should be cast to date to preserve Snowflake behavior.
2704+
self.validate_all(
2705+
"DATE_TRUNC('HOUR', CAST('2026-01-01' AS DATE))",
2706+
write={
2707+
"snowflake": "DATE_TRUNC('HOUR', CAST('2026-01-01' AS DATE))",
2708+
"duckdb": "CAST(DATE_TRUNC('HOUR', CAST('2026-01-01' AS DATE)) AS DATE)",
2709+
},
27212710
)
27222711

2723-
# In Snowflake --> DuckDB, DATE_TRUNC(time_part, date) should be cast to date to preserve Snowflake behavior.
2724-
expr = self.parse_one("DATE_TRUNC('HOUR', DATE '2026-01-01')")
2725-
annotated = annotate_types(expr, dialect="snowflake")
2726-
self.assertEqual(
2727-
annotated.sql("duckdb"),
2728-
"CAST(DATE_TRUNC('HOUR', CAST('2026-01-01' AS DATE)) AS DATE)",
2712+
# DuckDB does not support DATE_TRUNC(time_part, time), so we add a dummy date to generate DATE_TRUNC(time_part, date) --> DATE in DuckDB
2713+
# Then it is casted to a time (HH:MM:SS) to match Snowflake.
2714+
self.validate_all(
2715+
"DATE_TRUNC('HOUR', CAST('14:23:45.123456' AS TIME))",
2716+
write={
2717+
"snowflake": "DATE_TRUNC('HOUR', CAST('14:23:45.123456' AS TIME))",
2718+
"duckdb": "CAST(DATE_TRUNC('HOUR', CAST('1970-01-01' AS DATE) + CAST('14:23:45.123456' AS TIME)) AS TIME)",
2719+
},
27292720
)
27302721
self.validate_identity("TO_DATE('12345')").assert_is(exp.Anonymous)
27312722

0 commit comments

Comments
 (0)