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
2 changes: 1 addition & 1 deletion sqlglot/expressions/temporal.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ class TimestampTrunc(Expression, Func, TimeUnit):


class TimeSlice(Expression, Func, TimeUnit):
arg_types = {"this": True, "expression": True, "unit": True, "kind": False}
arg_types = {"this": True, "expression": True, "unit": True, "kind": False, "origin": False}


class TimeTrunc(Expression, Func, TimeUnit):
Expand Down
34 changes: 20 additions & 14 deletions sqlglot/generators/duckdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
getbit_sql,
groupconcat_sql,
inline_array_unless_query,
map_date_part,
months_between_sql,
no_datetime_sql,
no_comment_column_constraint_sql,
Expand Down Expand Up @@ -2425,33 +2426,38 @@ def timeslice_sql(self, expression: exp.TimeSlice) -> str:
Transform Snowflake's TIME_SLICE to DuckDB's time_bucket.

Snowflake: TIME_SLICE(date_expr, slice_length, 'UNIT' [, 'START'|'END'])
DuckDB: time_bucket(INTERVAL 'slice_length' UNIT, date_expr)
DuckDB: time_bucket(INTERVAL 'slice_length' UNIT, date_expr, origin)

For 'END' kind, add the interval to get the end of the slice.
For DATE type with 'END', cast result back to DATE to preserve type.
Snowflake aligns slices to the 1970-01-01 epoch, so an explicit origin is
required because DuckDB's default origin differs. For 'END' kind, add the
interval to get the next slice's start; for DATE input, cast back to DATE.
"""
date_expr = expression.this
slice_length = expression.expression
unit = expression.unit
# Normalize unit aliases (e.g. W/WK/WOY -> WEEK) so the interval and origin agree
unit = map_date_part(expression.unit, self.dialect)
kind = expression.text("kind").upper()
is_date = date_expr.is_type(exp.DType.DATE)

# Create INTERVAL expression: INTERVAL 'N' UNIT
interval_expr = exp.Interval(this=slice_length, unit=unit)

# Create base time_bucket expression
time_bucket_expr = exp.func("time_bucket", interval_expr, date_expr)
origin = expression.args.get("origin")
if origin is None:
# Snowflake aligns to 1970-01-01; WEEK to the Monday of the epoch week (WEEK_START=0)
is_week = bool(unit and unit.name.upper() == "WEEK")
origin = exp.Literal.string("1969-12-29" if is_week else "1970-01-01")
# A DATE origin keeps a DATE input a DATE; DuckDB widens it for TIMESTAMP inputs
origin_expr = exp.cast(origin, exp.DType.DATE)

time_bucket_expr = exp.func("time_bucket", interval_expr, date_expr, origin_expr)

# Check if we need the end of the slice (default is start)
if not kind == "END":
# For 'START', return time_bucket directly
if kind != "END":
return self.sql(time_bucket_expr)

# For 'END', add the interval to get end of slice
add_expr = exp.Add(this=time_bucket_expr, expression=interval_expr.copy())

# If input is DATE type, cast result back to DATE to preserve type
# DuckDB converts DATE to TIMESTAMP when adding intervals
if date_expr.is_type(exp.DType.DATE):
# DuckDB widens DATE to TIMESTAMP when adding intervals, so cast back
if is_date:
return self.sql(exp.cast(add_expr, exp.DType.DATE))

return self.sql(add_expr)
Expand Down
3 changes: 3 additions & 0 deletions sqlglot/parsers/starrocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ def _build_time_slice(args: list[exp.Expr]) -> exp.TimeSlice | None:
time_slice.set(
"kind", exp.Literal.string("END" if boundary.name.upper() == "CEIL" else "START")
)

# StarRocks anchors slices at 0001-01-01, unlike Snowflake's 1970-01-01 epoch
time_slice.set("origin", exp.Literal.string("0001-01-01"))
return time_slice


Expand Down
53 changes: 46 additions & 7 deletions tests/dialects/test_snowflake.py
Original file line number Diff line number Diff line change
Expand Up @@ -3192,49 +3192,88 @@ def test_snowflake(self):
"SELECT TIME_SLICE(TIMESTAMP '2024-03-15 14:37:42', 1, 'HOUR')",
write={
"snowflake": "SELECT TIME_SLICE(CAST('2024-03-15 14:37:42' AS TIMESTAMP), 1, 'HOUR')",
"duckdb": "SELECT TIME_BUCKET(INTERVAL 1 HOUR, CAST('2024-03-15 14:37:42' AS TIMESTAMP))",
"duckdb": "SELECT TIME_BUCKET(INTERVAL 1 HOUR, CAST('2024-03-15 14:37:42' AS TIMESTAMP), CAST('1970-01-01' AS DATE))",
},
)
self.validate_all(
"SELECT TIME_SLICE(TIMESTAMP '2024-03-15 14:37:42', 1, 'HOUR', 'END')",
write={
"snowflake": "SELECT TIME_SLICE(CAST('2024-03-15 14:37:42' AS TIMESTAMP), 1, 'HOUR', 'END')",
"duckdb": "SELECT TIME_BUCKET(INTERVAL 1 HOUR, CAST('2024-03-15 14:37:42' AS TIMESTAMP)) + INTERVAL 1 HOUR",
"duckdb": "SELECT TIME_BUCKET(INTERVAL 1 HOUR, CAST('2024-03-15 14:37:42' AS TIMESTAMP), CAST('1970-01-01' AS DATE)) + INTERVAL 1 HOUR",
},
)
self.validate_all(
"SELECT TIME_SLICE(DATE '2024-03-15', 1, 'DAY')",
write={
"snowflake": "SELECT TIME_SLICE(CAST('2024-03-15' AS DATE), 1, 'DAY')",
"duckdb": "SELECT TIME_BUCKET(INTERVAL 1 DAY, CAST('2024-03-15' AS DATE))",
"duckdb": "SELECT TIME_BUCKET(INTERVAL 1 DAY, CAST('2024-03-15' AS DATE), CAST('1970-01-01' AS DATE))",
},
)
self.validate_all(
"SELECT TIME_SLICE(DATE '2024-03-15', 1, 'DAY', 'END')",
write={
"snowflake": "SELECT TIME_SLICE(CAST('2024-03-15' AS DATE), 1, 'DAY', 'END')",
"duckdb": "SELECT CAST(TIME_BUCKET(INTERVAL 1 DAY, CAST('2024-03-15' AS DATE)) + INTERVAL 1 DAY AS DATE)",
"duckdb": "SELECT CAST(TIME_BUCKET(INTERVAL 1 DAY, CAST('2024-03-15' AS DATE), CAST('1970-01-01' AS DATE)) + INTERVAL 1 DAY AS DATE)",
},
)
self.validate_all(
"SELECT TIME_SLICE(TIMESTAMP '2024-03-15 14:37:42', 15, 'MINUTE')",
write={
"snowflake": "SELECT TIME_SLICE(CAST('2024-03-15 14:37:42' AS TIMESTAMP), 15, 'MINUTE')",
"duckdb": "SELECT TIME_BUCKET(INTERVAL 15 MINUTE, CAST('2024-03-15 14:37:42' AS TIMESTAMP))",
"duckdb": "SELECT TIME_BUCKET(INTERVAL 15 MINUTE, CAST('2024-03-15 14:37:42' AS TIMESTAMP), CAST('1970-01-01' AS DATE))",
},
)
self.validate_all(
"SELECT TIME_SLICE(TIMESTAMP '2024-03-15 14:37:42', 1, 'QUARTER')",
write={
"snowflake": "SELECT TIME_SLICE(CAST('2024-03-15 14:37:42' AS TIMESTAMP), 1, 'QUARTER')",
"duckdb": "SELECT TIME_BUCKET(INTERVAL 1 QUARTER, CAST('2024-03-15 14:37:42' AS TIMESTAMP))",
"duckdb": "SELECT TIME_BUCKET(INTERVAL 1 QUARTER, CAST('2024-03-15 14:37:42' AS TIMESTAMP), CAST('1970-01-01' AS DATE))",
},
)
# 8-day slices must align to Snowflake's 1970-01-01 origin, not DuckDB's default
self.validate_all(
"SELECT TIME_SLICE(DATE '2021-03-18', 8, 'DAY')",
write={
"snowflake": "SELECT TIME_SLICE(CAST('2021-03-18' AS DATE), 8, 'DAY')",
"duckdb": "SELECT TIME_BUCKET(INTERVAL 8 DAY, CAST('2021-03-18' AS DATE), CAST('1970-01-01' AS DATE))",
},
)
self.validate_all(
"SELECT TIME_SLICE(DATE '2021-03-18', 8, 'DAY', 'END')",
write={
"snowflake": "SELECT TIME_SLICE(CAST('2021-03-18' AS DATE), 8, 'DAY', 'END')",
"duckdb": "SELECT CAST(TIME_BUCKET(INTERVAL 8 DAY, CAST('2021-03-18' AS DATE), CAST('1970-01-01' AS DATE)) + INTERVAL 8 DAY AS DATE)",
},
)
# WEEK aligns to the Monday of the epoch's week (1969-12-29) under WEEK_START=0
self.validate_all(
"SELECT TIME_SLICE(DATE '2024-03-15', 2, 'WEEK')",
write={
"snowflake": "SELECT TIME_SLICE(CAST('2024-03-15' AS DATE), 2, 'WEEK')",
"duckdb": "SELECT TIME_BUCKET(INTERVAL 2 WEEK, CAST('2024-03-15' AS DATE), CAST('1969-12-29' AS DATE))",
},
)
self.validate_all(
"SELECT TIME_SLICE(DATE '2024-03-15', 1, 'WEEK', 'END')",
write={
"snowflake": "SELECT TIME_SLICE(CAST('2024-03-15' AS DATE), 1, 'WEEK', 'END')",
"duckdb": "SELECT CAST(TIME_BUCKET(INTERVAL 1 WEEK, CAST('2024-03-15' AS DATE)) + INTERVAL 1 WEEK AS DATE)",
"duckdb": "SELECT CAST(TIME_BUCKET(INTERVAL 1 WEEK, CAST('2024-03-15' AS DATE), CAST('1969-12-29' AS DATE)) + INTERVAL 1 WEEK AS DATE)",
},
)
# A DATE origin keeps an unannotated DATE column a DATE in DuckDB, not a TIMESTAMP
self.validate_all(
"SELECT TIME_SLICE(d, 1, 'DAY') FROM t",
write={
"snowflake": "SELECT TIME_SLICE(d, 1, 'DAY') FROM t",
"duckdb": "SELECT TIME_BUCKET(INTERVAL 1 DAY, d, CAST('1970-01-01' AS DATE)) FROM t",
},
)
# Week aliases (e.g. WK) normalize to WEEK and use the epoch-week origin, not the epoch
self.validate_all(
"SELECT TIME_SLICE(CAST('2024-03-15' AS DATE), 1, 'WK')",
write={
"snowflake": "SELECT TIME_SLICE(CAST('2024-03-15' AS DATE), 1, 'WK')",
"duckdb": "SELECT TIME_BUCKET(INTERVAL 1 WEEK, CAST('2024-03-15' AS DATE), CAST('1969-12-29' AS DATE))",
},
)

Expand Down
19 changes: 17 additions & 2 deletions tests/dialects/test_starrocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -570,7 +570,7 @@ def test_time_slice(self):
write={
"starrocks": "SELECT TIME_SLICE(dt, INTERVAL 5 MINUTE, CEIL)",
"snowflake": "SELECT TIME_SLICE(dt, 5, 'MINUTE', 'END')",
"duckdb": "SELECT TIME_BUCKET(INTERVAL 5 MINUTE, dt) + INTERVAL 5 MINUTE",
"duckdb": "SELECT TIME_BUCKET(INTERVAL 5 MINUTE, dt, CAST('0001-01-01' AS DATE)) + INTERVAL 5 MINUTE",
},
)
self.validate_all(
Expand All @@ -581,7 +581,22 @@ def test_time_slice(self):
write={
"starrocks": "SELECT TIME_SLICE(dt, INTERVAL 1 DAY, FLOOR)",
"snowflake": "SELECT TIME_SLICE(dt, 1, 'DAY', 'START')",
"duckdb": "SELECT TIME_BUCKET(INTERVAL 1 DAY, dt)",
"duckdb": "SELECT TIME_BUCKET(INTERVAL 1 DAY, dt, CAST('0001-01-01' AS DATE))",
},
)
# StarRocks anchors at 0001-01-01, not Snowflake's epoch; 7-day slices expose the difference
self.validate_all(
"SELECT TIME_SLICE(dt, INTERVAL 7 DAY)",
write={
"starrocks": "SELECT TIME_SLICE(dt, INTERVAL 7 DAY)",
"duckdb": "SELECT TIME_BUCKET(INTERVAL 7 DAY, dt, CAST('0001-01-01' AS DATE))",
},
)
self.validate_all(
"SELECT TIME_SLICE(dt, INTERVAL 7 DAY, CEIL)",
write={
"starrocks": "SELECT TIME_SLICE(dt, INTERVAL 7 DAY, CEIL)",
"duckdb": "SELECT TIME_BUCKET(INTERVAL 7 DAY, dt, CAST('0001-01-01' AS DATE)) + INTERVAL 7 DAY",
},
)

Expand Down
Loading