Skip to content

Commit 870dba4

Browse files
feat(snowflake)!: support transpilation of LAST_DAY from Snowflake to Duckdb (#6614)
* support transpilation of LAST_DAY from Snowflake to Duckdb * small refactor * add tests * styling --------- Co-authored-by: Jo <[email protected]>
1 parent 8f38887 commit 870dba4

File tree

3 files changed

+94
-1
lines changed

3 files changed

+94
-1
lines changed

sqlglot/dialects/duckdb.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,59 @@
8989
MAX_BIT_POSITION = exp.Literal.number(32768)
9090

9191

92+
def _last_day_sql(self: DuckDB.Generator, expression: exp.LastDay) -> str:
93+
"""
94+
DuckDB's LAST_DAY only supports finding the last day of a month.
95+
For other date parts (year, quarter, week), we need to implement equivalent logic.
96+
"""
97+
date_expr = expression.this
98+
unit = expression.text("unit")
99+
100+
if not unit or unit.upper() == "MONTH":
101+
# Default behavior - use DuckDB's native LAST_DAY
102+
return self.func("LAST_DAY", date_expr)
103+
104+
if unit.upper() == "YEAR":
105+
# Last day of year: December 31st of the same year
106+
year_expr = exp.func("EXTRACT", "YEAR", date_expr)
107+
make_date_expr = exp.func(
108+
"MAKE_DATE", year_expr, exp.Literal.number(12), exp.Literal.number(31)
109+
)
110+
return self.sql(make_date_expr)
111+
112+
if unit.upper() == "QUARTER":
113+
# Last day of quarter
114+
year_expr = exp.func("EXTRACT", "YEAR", date_expr)
115+
quarter_expr = exp.func("EXTRACT", "QUARTER", date_expr)
116+
117+
# Calculate last month of quarter: quarter * 3. Quarter can be 1 to 4
118+
last_month_expr = exp.Mul(this=quarter_expr, expression=exp.Literal.number(3))
119+
first_day_last_month_expr = exp.func(
120+
"MAKE_DATE", year_expr, last_month_expr, exp.Literal.number(1)
121+
)
122+
123+
# Last day of the last month of the quarter
124+
last_day_expr = exp.func("LAST_DAY", first_day_last_month_expr)
125+
return self.sql(last_day_expr)
126+
127+
if unit.upper() == "WEEK":
128+
# DuckDB DAYOFWEEK: Sunday=0, Monday=1, ..., Saturday=6
129+
dow = exp.func("EXTRACT", "DAYOFWEEK", date_expr)
130+
# Days to the last day of week: (7 - dayofweek) % 7, assuming the last day of week is Sunday (Snowflake)
131+
# Wrap in parentheses to ensure correct precedence
132+
days_to_sunday_expr = exp.Mod(
133+
this=exp.Paren(this=exp.Sub(this=exp.Literal.number(7), expression=dow)),
134+
expression=exp.Literal.number(7),
135+
)
136+
interval_expr = exp.Interval(this=days_to_sunday_expr, unit=exp.var("DAY"))
137+
add_expr = exp.Add(this=date_expr, expression=interval_expr)
138+
cast_expr = exp.cast(add_expr, exp.DataType.Type.DATE)
139+
return self.sql(cast_expr)
140+
141+
self.unsupported(f"Unsupported date part '{unit}' in LAST_DAY function")
142+
return self.function_fallback_sql(expression)
143+
144+
92145
def _to_boolean_sql(self: DuckDB.Generator, expression: exp.ToBoolean) -> str:
93146
"""
94147
Transpile TO_BOOLEAN and TRY_TO_BOOLEAN functions from Snowflake to DuckDB equivalent.
@@ -1323,6 +1376,7 @@ class Generator(generator.Generator):
13231376
exp.JSONObjectAgg: rename_func("JSON_GROUP_OBJECT"),
13241377
exp.JSONBObjectAgg: rename_func("JSON_GROUP_OBJECT"),
13251378
exp.DateBin: rename_func("TIME_BUCKET"),
1379+
exp.LastDay: _last_day_sql,
13261380
}
13271381

13281382
SUPPORTED_JSON_PATH_PARTS = {

tests/dialects/test_bigquery.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -656,7 +656,6 @@ def test_bigquery(self):
656656
"snowflake": "SELECT LAST_DAY(CAST('2008-11-25' AS DATE), QUARTER)",
657657
},
658658
write={
659-
"duckdb": UnsupportedError,
660659
"bigquery": "SELECT LAST_DAY(CAST('2008-11-25' AS DATE), QUARTER)",
661660
"snowflake": "SELECT LAST_DAY(CAST('2008-11-25' AS DATE), QUARTER)",
662661
},

tests/dialects/test_snowflake.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1775,6 +1775,46 @@ def test_snowflake(self):
17751775
},
17761776
)
17771777

1778+
self.validate_all(
1779+
"LAST_DAY(CAST('2023-04-15' AS DATE))",
1780+
write={
1781+
"snowflake": "LAST_DAY(CAST('2023-04-15' AS DATE))",
1782+
"duckdb": "LAST_DAY(CAST('2023-04-15' AS DATE))",
1783+
},
1784+
)
1785+
1786+
self.validate_all(
1787+
"LAST_DAY(CAST('2023-04-15' AS DATE), MONTH)",
1788+
write={
1789+
"snowflake": "LAST_DAY(CAST('2023-04-15' AS DATE), MONTH)",
1790+
"duckdb": "LAST_DAY(CAST('2023-04-15' AS DATE))",
1791+
},
1792+
)
1793+
1794+
self.validate_all(
1795+
"LAST_DAY(CAST('2024-06-15' AS DATE), YEAR)",
1796+
write={
1797+
"snowflake": "LAST_DAY(CAST('2024-06-15' AS DATE), YEAR)",
1798+
"duckdb": "MAKE_DATE(EXTRACT(YEAR FROM CAST('2024-06-15' AS DATE)), 12, 31)",
1799+
},
1800+
)
1801+
1802+
self.validate_all(
1803+
"LAST_DAY(CAST('2024-01-15' AS DATE), QUARTER)",
1804+
write={
1805+
"snowflake": "LAST_DAY(CAST('2024-01-15' AS DATE), QUARTER)",
1806+
"duckdb": "LAST_DAY(MAKE_DATE(EXTRACT(YEAR FROM CAST('2024-01-15' AS DATE)), EXTRACT(QUARTER FROM CAST('2024-01-15' AS DATE)) * 3, 1))",
1807+
},
1808+
)
1809+
1810+
self.validate_all(
1811+
"LAST_DAY(CAST('2025-12-15' AS DATE), WEEK)",
1812+
write={
1813+
"snowflake": "LAST_DAY(CAST('2025-12-15' AS DATE), WEEK)",
1814+
"duckdb": "CAST(CAST('2025-12-15' AS DATE) + INTERVAL ((7 - EXTRACT(DAYOFWEEK FROM CAST('2025-12-15' AS DATE))) % 7) DAY AS DATE)",
1815+
},
1816+
)
1817+
17781818
self.validate_all(
17791819
"SELECT ST_DISTANCE(a, b)",
17801820
write={

0 commit comments

Comments
 (0)