diff --git a/sqlglot/dialects/duckdb.py b/sqlglot/dialects/duckdb.py index 2007cfe660..6bf0cb85d2 100644 --- a/sqlglot/dialects/duckdb.py +++ b/sqlglot/dialects/duckdb.py @@ -8,6 +8,7 @@ from sqlglot import exp, generator, parser, tokens, transforms from sqlglot.dialects.dialect import ( + DATETIME_DELTA, Dialect, JSON_EXTRACT_TYPE, NormalizationStrategy, @@ -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, @@ -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. @@ -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) @@ -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) + + def duckdb_date_delta_sql(self: DuckDB.Generator, expression: DATETIME_DELTA) -> str: + 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) @@ -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) + 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) diff --git a/tests/dialects/test_snowflake.py b/tests/dialects/test_snowflake.py index 5365796d86..f7ad99ec1a 100644 --- a/tests/dialects/test_snowflake.py +++ b/tests/dialects/test_snowflake.py @@ -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)")