88from sqlglot import exp , generator , parser , tokens , transforms
99
1010from sqlglot .dialects .dialect import (
11+ DATETIME_DELTA ,
1112 Dialect ,
1213 JSON_EXTRACT_TYPE ,
1314 NormalizationStrategy ,
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+
145193def _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
217265def _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." ))
227303def _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
441517def _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)
0 commit comments