Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 69 additions & 2 deletions sqlglot/dialects/duckdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from sqlglot import exp, generator, parser, tokens, transforms

from sqlglot.dialects.dialect import (
DATETIME_DELTA,
Dialect,
JSON_EXTRACT_TYPE,
NormalizationStrategy,
Expand All @@ -17,7 +18,7 @@
bool_xor_sql,
build_default_decimal_type,
count_if_to_sum,
date_delta_to_binary_interval_op,
date_delta_to_binary_interval_op as base_date_delta_to_binary_interval_op,
date_trunc_to_time,
datestrtodate_sql,
no_datetime_sql,
Expand Down Expand Up @@ -142,6 +143,42 @@ def _last_day_sql(self: DuckDB.Generator, expression: exp.LastDay) -> str:
return self.function_fallback_sql(expression)


def _is_nanosecond_unit(unit: t.Optional[exp.Expression]) -> bool:
return isinstance(unit, (exp.Var, exp.Literal)) and unit.name.upper() == "NANOSECOND"


def _handle_nanosecond_diff(
self: DuckDB.Generator,
end_time: exp.Expression,
start_time: exp.Expression,
) -> str:
"""Generate NANOSECOND diff using EPOCH_NS since DATE_DIFF doesn't support it."""
end_ns = exp.cast(end_time, exp.DataType.Type.TIMESTAMP_NS)
start_ns = exp.cast(start_time, exp.DataType.Type.TIMESTAMP_NS)

# Build expression tree: EPOCH_NS(end) - EPOCH_NS(start)
return self.sql(
exp.Sub(this=exp.func("EPOCH_NS", end_ns), expression=exp.func("EPOCH_NS", start_ns))
)


def _handle_nanosecond_add(
self: DuckDB.Generator,
timestamp: exp.Expression,
nanoseconds: exp.Expression,
) -> str:
"""Generate NANOSECOND add using EPOCH_NS and make_timestamp_ns since INTERVAL doesn't support it."""
timestamp_ns = exp.cast(timestamp, exp.DataType.Type.TIMESTAMP_NS)

# Build expression tree: make_timestamp_ns(EPOCH_NS(timestamp) + nanoseconds)
return self.sql(
exp.func(
"make_timestamp_ns",
exp.Add(this=exp.func("EPOCH_NS", timestamp_ns), expression=nanoseconds),
)
)


def _to_boolean_sql(self: DuckDB.Generator, expression: exp.ToBoolean) -> str:
"""
Transpile TO_BOOLEAN and TRY_TO_BOOLEAN functions from Snowflake to DuckDB equivalent.
Expand Down Expand Up @@ -215,6 +252,11 @@ def _date_sql(self: DuckDB.Generator, expression: exp.Date) -> str:

# BigQuery -> DuckDB conversion for the TIME_DIFF function
def _timediff_sql(self: DuckDB.Generator, expression: exp.TimeDiff) -> str:
unit = expression.unit

if _is_nanosecond_unit(unit):
return _handle_nanosecond_diff(self, expression.expression, expression.this)

this = exp.cast(expression.this, exp.DataType.Type.TIME)
expr = exp.cast(expression.expression, exp.DataType.Type.TIME)

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


def date_delta_to_binary_interval_op(
cast: bool = True,
) -> t.Callable[[DuckDB.Generator, DATETIME_DELTA], str]:
"""DuckDB override to handle NANOSECOND operations; delegates other units to base."""
base_impl = base_date_delta_to_binary_interval_op(cast=cast)
Comment on lines +268 to +272
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets prefix the function with _ since its local to DuckDB, by doing so we can also leave the import name as is e.g:

```suggestion
def _date_delta_to_binary_interval_op(
    cast: bool = True,
) -> t.Callable[[DuckDB.Generator, DATETIME_DELTA], str]:
    """DuckDB override to handle NANOSECOND operations; delegates other units to base."""
    base_impl = date_delta_to_binary_interval_op(cast=cast)


def duckdb_date_delta_sql(self: DuckDB.Generator, expression: DATETIME_DELTA) -> str:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Nit] Lets also prefix this with _

unit = expression.unit

# Handle NANOSECOND unit (DuckDB doesn't support INTERVAL ... NANOSECOND)
if _is_nanosecond_unit(unit):
interval_value = expression.expression
if isinstance(interval_value, exp.Interval):
interval_value = interval_value.this
return _handle_nanosecond_add(self, expression.this, interval_value)

return base_impl(self, expression)

return duckdb_date_delta_sql


@unsupported_args(("expression", "DuckDB's ARRAY_SORT does not support a comparator."))
def _array_sort_sql(self: DuckDB.Generator, expression: exp.ArraySort) -> str:
return self.func("ARRAY_SORT", expression.this)
Expand Down Expand Up @@ -439,9 +502,13 @@ def _build_week_trunc_expression(date_expr: exp.Expression, start_dow: int) -> e


def _date_diff_sql(self: DuckDB.Generator, expression: exp.DateDiff) -> str:
unit = expression.unit

if _is_nanosecond_unit(unit):
return _handle_nanosecond_diff(self, expression.this, expression.expression)
Comment on lines +507 to +508
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Nit] We can inline the implementations of both _handle_nanosecond_diff and _handle_nanosecond_add directly under their branches now that they're both small enough after the recent changes, leaving it up to you


this = _implicit_datetime_cast(expression.this)
expr = _implicit_datetime_cast(expression.expression)
unit = expression.args.get("unit")

# DuckDB's WEEK diff does not respect Monday crossing (week boundaries), it checks (end_day - start_day) / 7:
# SELECT DATE_DIFF('WEEK', CAST('2024-12-13' AS DATE), CAST('2024-12-17' AS DATE)) --> 0 (Monday crossed)
Expand Down
63 changes: 63 additions & 0 deletions tests/dialects/test_snowflake.py
Original file line number Diff line number Diff line change
Expand Up @@ -2475,6 +2475,69 @@ def test_timestamps(self):
},
)

# Test DATEDIFF with NANOSECOND - DuckDB uses EPOCH_NS since DATE_DIFF doesn't support NANOSECOND
self.validate_all(
"DATEDIFF(NANOSECOND, '2023-01-01 10:00:00.000000000', '2023-01-01 10:00:00.123456789')",
write={
"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))",
"snowflake": "DATEDIFF(NANOSECOND, '2023-01-01 10:00:00.000000000', '2023-01-01 10:00:00.123456789')",
},
)

# Test DATEDIFF with NANOSECOND on columns
self.validate_all(
"DATEDIFF(NANOSECOND, start_time, end_time)",
write={
"duckdb": "EPOCH_NS(CAST(end_time AS TIMESTAMP_NS)) - EPOCH_NS(CAST(start_time AS TIMESTAMP_NS))",
"snowflake": "DATEDIFF(NANOSECOND, start_time, end_time)",
},
)

# Test DATEADD with NANOSECOND - DuckDB uses MAKE_TIMESTAMP_NS since INTERVAL doesn't support NANOSECOND
self.validate_all(
"DATEADD(NANOSECOND, 123456789, '2023-01-01 10:00:00.000000000')",
write={
"duckdb": "MAKE_TIMESTAMP_NS(EPOCH_NS(CAST('2023-01-01 10:00:00.000000000' AS TIMESTAMP_NS)) + 123456789)",
"snowflake": "DATEADD(NANOSECOND, 123456789, '2023-01-01 10:00:00.000000000')",
},
)

# Test DATEADD with NANOSECOND on columns
self.validate_all(
"DATEADD(NANOSECOND, nano_offset, timestamp_col)",
write={
"duckdb": "MAKE_TIMESTAMP_NS(EPOCH_NS(CAST(timestamp_col AS TIMESTAMP_NS)) + nano_offset)",
"snowflake": "DATEADD(NANOSECOND, nano_offset, timestamp_col)",
},
)

# Test negative NANOSECOND values (subtraction)
self.validate_all(
"DATEADD(NANOSECOND, -123456789, '2023-01-01 10:00:00.500000000')",
write={
"duckdb": "MAKE_TIMESTAMP_NS(EPOCH_NS(CAST('2023-01-01 10:00:00.500000000' AS TIMESTAMP_NS)) + -123456789)",
"snowflake": "DATEADD(NANOSECOND, -123456789, '2023-01-01 10:00:00.500000000')",
},
)

# Test TIMESTAMPDIFF with NANOSECOND - Snowflake parser converts to DATEDIFF
self.validate_all(
"TIMESTAMPDIFF(NANOSECOND, '2023-01-01 10:00:00.000000000', '2023-01-01 10:00:00.123456789')",
write={
"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))",
"snowflake": "DATEDIFF(NANOSECOND, '2023-01-01 10:00:00.000000000', '2023-01-01 10:00:00.123456789')",
},
)

# Test TIMESTAMPADD with NANOSECOND - Snowflake parser converts to DATEADD
self.validate_all(
"TIMESTAMPADD(NANOSECOND, 123456789, '2023-01-01 10:00:00.000000000')",
write={
"duckdb": "MAKE_TIMESTAMP_NS(EPOCH_NS(CAST('2023-01-01 10:00:00.000000000' AS TIMESTAMP_NS)) + 123456789)",
"snowflake": "DATEADD(NANOSECOND, 123456789, '2023-01-01 10:00:00.000000000')",
},
)

self.validate_identity("DATEADD(y, 5, x)", "DATEADD(YEAR, 5, x)")
self.validate_identity("DATEADD(y, 5, x)", "DATEADD(YEAR, 5, x)")
self.validate_identity("DATE_PART(yyy, x)", "DATE_PART(YEAR, x)")
Expand Down