Skip to content

Commit 2296a92

Browse files
feat(duckdb): Add transpilation support for nanoseconds used in date/time functions
1 parent 870d600 commit 2296a92

File tree

2 files changed

+159
-2
lines changed

2 files changed

+159
-2
lines changed

sqlglot/dialects/duckdb.py

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from sqlglot import exp, generator, parser, tokens, transforms
99

1010
from sqlglot.dialects.dialect import (
11+
DATETIME_DELTA,
1112
Dialect,
1213
JSON_EXTRACT_TYPE,
1314
NormalizationStrategy,
@@ -17,7 +18,7 @@
1718
bool_xor_sql,
1819
build_default_decimal_type,
1920
count_if_to_sum,
20-
date_delta_to_binary_interval_op,
21+
date_delta_to_binary_interval_op as base_date_delta_to_binary_interval_op,
2122
date_trunc_to_time,
2223
datestrtodate_sql,
2324
no_datetime_sql,
@@ -142,6 +143,53 @@ def _last_day_sql(self: DuckDB.Generator, expression: exp.LastDay) -> str:
142143
return self.function_fallback_sql(expression)
143144

144145

146+
def _unwrap_cast(expr: exp.Expression) -> exp.Expression:
147+
"""Unwrap Cast expression to avoid double-casting that loses nanosecond precision.
148+
149+
Nested casts can lose precision when converting between
150+
timestamp types. By unwrapping the inner cast, we go directly from the source
151+
expression to TIMESTAMP_NS, preserving nanosecond precision.
152+
"""
153+
return expr.this if isinstance(expr, exp.Cast) else expr
154+
155+
156+
def _is_nanosecond_unit(unit: t.Optional[exp.Expression]) -> bool:
157+
"""Check if unit is NANOSECOND."""
158+
return isinstance(unit, (exp.Var, exp.Literal)) and unit.name.upper() == "NANOSECOND"
159+
160+
161+
def _handle_nanosecond_diff(
162+
self: DuckDB.Generator,
163+
end_time: exp.Expression,
164+
start_time: exp.Expression,
165+
) -> str:
166+
"""Generate NANOSECOND diff using EPOCH_NS since DATE_DIFF doesn't support it."""
167+
end_ns = exp.cast(_unwrap_cast(end_time), exp.DataType.Type.TIMESTAMP_NS)
168+
start_ns = exp.cast(_unwrap_cast(start_time), exp.DataType.Type.TIMESTAMP_NS)
169+
170+
# Build expression tree: EPOCH_NS(end) - EPOCH_NS(start)
171+
return self.sql(
172+
exp.Sub(this=exp.func("EPOCH_NS", end_ns), expression=exp.func("EPOCH_NS", start_ns))
173+
)
174+
175+
176+
def _handle_nanosecond_add(
177+
self: DuckDB.Generator,
178+
timestamp: exp.Expression,
179+
nanoseconds: exp.Expression,
180+
) -> str:
181+
"""Generate NANOSECOND add using EPOCH_NS and make_timestamp_ns since INTERVAL doesn't support it."""
182+
timestamp_ns = exp.cast(_unwrap_cast(timestamp), exp.DataType.Type.TIMESTAMP_NS)
183+
184+
# Build expression tree: make_timestamp_ns(EPOCH_NS(timestamp) + nanoseconds)
185+
return self.sql(
186+
exp.func(
187+
"make_timestamp_ns",
188+
exp.Add(this=exp.func("EPOCH_NS", timestamp_ns), expression=nanoseconds),
189+
)
190+
)
191+
192+
145193
def _to_boolean_sql(self: DuckDB.Generator, expression: exp.ToBoolean) -> str:
146194
"""
147195
Transpile TO_BOOLEAN and TRY_TO_BOOLEAN functions from Snowflake to DuckDB equivalent.
@@ -215,6 +263,13 @@ def _date_sql(self: DuckDB.Generator, expression: exp.Date) -> str:
215263

216264
# BigQuery -> DuckDB conversion for the TIME_DIFF function
217265
def _timediff_sql(self: DuckDB.Generator, expression: exp.TimeDiff) -> str:
266+
unit = expression.args.get("unit")
267+
268+
if _is_nanosecond_unit(unit):
269+
this_ts = exp.cast(expression.this, exp.DataType.Type.TIMESTAMP_NS)
270+
expr_ts = exp.cast(expression.expression, exp.DataType.Type.TIMESTAMP_NS)
271+
return _handle_nanosecond_diff(self, expr_ts, this_ts)
272+
218273
this = exp.cast(expression.this, exp.DataType.Type.TIME)
219274
expr = exp.cast(expression.expression, exp.DataType.Type.TIME)
220275

@@ -223,6 +278,27 @@ def _timediff_sql(self: DuckDB.Generator, expression: exp.TimeDiff) -> str:
223278
return self.func("DATE_DIFF", unit_to_str(expression), expr, this)
224279

225280

281+
def date_delta_to_binary_interval_op(
282+
cast: bool = True,
283+
) -> t.Callable[[DuckDB.Generator, DATETIME_DELTA], str]:
284+
"""DuckDB override to handle NANOSECOND operations; delegates other units to base."""
285+
base_impl = base_date_delta_to_binary_interval_op(cast=cast)
286+
287+
def duckdb_date_delta_sql(self: DuckDB.Generator, expression: DATETIME_DELTA) -> str:
288+
unit = expression.args.get("unit")
289+
290+
# Handle NANOSECOND unit (DuckDB doesn't support INTERVAL ... NANOSECOND)
291+
if _is_nanosecond_unit(unit):
292+
interval_value = expression.expression
293+
if isinstance(interval_value, exp.Interval):
294+
interval_value = interval_value.this
295+
return _handle_nanosecond_add(self, expression.this, interval_value)
296+
297+
return base_impl(self, expression)
298+
299+
return duckdb_date_delta_sql
300+
301+
226302
@unsupported_args(("expression", "DuckDB's ARRAY_SORT does not support a comparator."))
227303
def _array_sort_sql(self: DuckDB.Generator, expression: exp.ArraySort) -> str:
228304
return self.func("ARRAY_SORT", expression.this)
@@ -439,9 +515,13 @@ def _build_week_trunc_expression(date_expr: exp.Expression, start_dow: int) -> e
439515

440516

441517
def _date_diff_sql(self: DuckDB.Generator, expression: exp.DateDiff) -> str:
518+
unit = expression.args.get("unit")
519+
520+
if _is_nanosecond_unit(unit):
521+
return _handle_nanosecond_diff(self, expression.this, expression.expression)
522+
442523
this = _implicit_datetime_cast(expression.this)
443524
expr = _implicit_datetime_cast(expression.expression)
444-
unit = expression.args.get("unit")
445525

446526
# DuckDB's WEEK diff does not respect Monday crossing (week boundaries), it checks (end_day - start_day) / 7:
447527
# SELECT DATE_DIFF('WEEK', CAST('2024-12-13' AS DATE), CAST('2024-12-17' AS DATE)) --> 0 (Monday crossed)

tests/dialects/test_snowflake.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2475,6 +2475,83 @@ def test_timestamps(self):
24752475
},
24762476
)
24772477

2478+
# Test DATEDIFF with NANOSECOND - DuckDB uses EPOCH_NS since DATE_DIFF doesn't support NANOSECOND
2479+
self.validate_all(
2480+
"DATEDIFF(NANOSECOND, '2023-01-01 10:00:00.000000000', '2023-01-01 10:00:00.123456789')",
2481+
write={
2482+
"duckdb": "EPOCH_NS(CAST('2023-01-01 10:00:00.123456789' AS TIMESTAMP_NS)) - EPOCH_NS(CAST('2023-01-01 10:00:00.000000000' AS TIMESTAMP_NS))",
2483+
"snowflake": "DATEDIFF(NANOSECOND, '2023-01-01 10:00:00.000000000', '2023-01-01 10:00:00.123456789')",
2484+
},
2485+
)
2486+
self.validate_all(
2487+
"DATEDIFF(NANOSECOND, CAST('2023-01-01 10:00:00.000000000' AS TIMESTAMP), CAST('2023-01-01 10:00:00.987654321' AS TIMESTAMP))",
2488+
write={
2489+
"duckdb": "EPOCH_NS(CAST('2023-01-01 10:00:00.987654321' AS TIMESTAMP_NS)) - EPOCH_NS(CAST('2023-01-01 10:00:00.000000000' AS TIMESTAMP_NS))",
2490+
"snowflake": "DATEDIFF(NANOSECOND, CAST('2023-01-01 10:00:00.000000000' AS TIMESTAMP), CAST('2023-01-01 10:00:00.987654321' AS TIMESTAMP))",
2491+
},
2492+
)
2493+
2494+
# Test TIMEDIFF with NANOSECOND - DuckDB uses EPOCH_NS for nanosecond precision
2495+
self.validate_all(
2496+
"TIMEDIFF(NANOSECOND, '10:00:00.000000000', '10:00:00.123456789')",
2497+
write={
2498+
"duckdb": "EPOCH_NS(CAST('10:00:00.123456789' AS TIMESTAMP_NS)) - EPOCH_NS(CAST('10:00:00.000000000' AS TIMESTAMP_NS))",
2499+
"snowflake": "DATEDIFF(NANOSECOND, '10:00:00.000000000', '10:00:00.123456789')",
2500+
},
2501+
)
2502+
2503+
# Test DATEADD with NANOSECOND - DuckDB uses MAKE_TIMESTAMP_NS since INTERVAL doesn't support NANOSECOND
2504+
self.validate_all(
2505+
"DATEADD(NANOSECOND, 123456789, '2023-01-01 10:00:00.000000000')",
2506+
write={
2507+
"duckdb": "MAKE_TIMESTAMP_NS(EPOCH_NS(CAST('2023-01-01 10:00:00.000000000' AS TIMESTAMP_NS)) + 123456789)",
2508+
"snowflake": "DATEADD(NANOSECOND, 123456789, '2023-01-01 10:00:00.000000000')",
2509+
},
2510+
)
2511+
self.validate_all(
2512+
"DATEADD(NANOSECOND, 999999999, CAST('2023-01-01 10:00:00.000000000' AS TIMESTAMP))",
2513+
write={
2514+
"duckdb": "MAKE_TIMESTAMP_NS(EPOCH_NS(CAST('2023-01-01 10:00:00.000000000' AS TIMESTAMP_NS)) + 999999999)",
2515+
"snowflake": "DATEADD(NANOSECOND, 999999999, CAST('2023-01-01 10:00:00.000000000' AS TIMESTAMP))",
2516+
},
2517+
)
2518+
2519+
# Test TIMEADD with NANOSECOND - DuckDB uses MAKE_TIMESTAMP_NS
2520+
self.validate_all(
2521+
"TIMEADD(NANOSECOND, 123456789, '10:00:00.000000000')",
2522+
write={
2523+
"duckdb": "MAKE_TIMESTAMP_NS(EPOCH_NS(CAST('10:00:00.000000000' AS TIMESTAMP_NS)) + 123456789)",
2524+
"snowflake": "TIMEADD(NANOSECOND, 123456789, '10:00:00.000000000')",
2525+
},
2526+
)
2527+
2528+
# Test negative NANOSECOND values (subtraction)
2529+
self.validate_all(
2530+
"DATEADD(NANOSECOND, -123456789, '2023-01-01 10:00:00.500000000')",
2531+
write={
2532+
"duckdb": "MAKE_TIMESTAMP_NS(EPOCH_NS(CAST('2023-01-01 10:00:00.500000000' AS TIMESTAMP_NS)) + -123456789)",
2533+
"snowflake": "DATEADD(NANOSECOND, -123456789, '2023-01-01 10:00:00.500000000')",
2534+
},
2535+
)
2536+
2537+
# Test TIMESTAMPDIFF with NANOSECOND - Snowflake parser converts to DATEDIFF
2538+
self.validate_all(
2539+
"TIMESTAMPDIFF(NANOSECOND, '2023-01-01 10:00:00.000000000', '2023-01-01 10:00:00.123456789')",
2540+
write={
2541+
"duckdb": "EPOCH_NS(CAST('2023-01-01 10:00:00.123456789' AS TIMESTAMP_NS)) - EPOCH_NS(CAST('2023-01-01 10:00:00.000000000' AS TIMESTAMP_NS))",
2542+
"snowflake": "DATEDIFF(NANOSECOND, '2023-01-01 10:00:00.000000000', '2023-01-01 10:00:00.123456789')",
2543+
},
2544+
)
2545+
2546+
# Test TIMESTAMPADD with NANOSECOND - Snowflake parser converts to DATEADD
2547+
self.validate_all(
2548+
"TIMESTAMPADD(NANOSECOND, 123456789, '2023-01-01 10:00:00.000000000')",
2549+
write={
2550+
"duckdb": "MAKE_TIMESTAMP_NS(EPOCH_NS(CAST('2023-01-01 10:00:00.000000000' AS TIMESTAMP_NS)) + 123456789)",
2551+
"snowflake": "DATEADD(NANOSECOND, 123456789, '2023-01-01 10:00:00.000000000')",
2552+
},
2553+
)
2554+
24782555
self.validate_identity("DATEADD(y, 5, x)", "DATEADD(YEAR, 5, x)")
24792556
self.validate_identity("DATEADD(y, 5, x)", "DATEADD(YEAR, 5, x)")
24802557
self.validate_identity("DATE_PART(yyy, x)", "DATE_PART(YEAR, x)")

0 commit comments

Comments
 (0)