Skip to content

Commit dba3bb8

Browse files
feat(duckdb): Addressed review comments
1 parent 76386d2 commit dba3bb8

File tree

5 files changed

+44
-71
lines changed

5 files changed

+44
-71
lines changed

sqlglot/dialects/bigquery.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -214,26 +214,34 @@ def _serialize_bq_datetime_diff_unit(self: BigQuery.Generator, expression: exp.E
214214
215215
Canonical form -> BigQuery syntax:
216216
- Week(SUNDAY) -> WEEK (BigQuery's default)
217-
- Week(MONDAY) -> WEEK(MONDAY)
218-
- ISOWEEK -> ISOWEEK
217+
- Week(MONDAY) -> WEEK(MONDAY) (preserved during round-trip)
218+
- Var(ISOWEEK) -> ISOWEEK (preserved as-is)
219+
- WeekStart -> preserved as-is
219220
- Week(other day) -> WEEK(day)
220221
- Other units -> use unit_to_var
221222
222223
"""
223224
from sqlglot.dialects.dialect import extract_week_unit_info
224225

225226
unit = expression.args.get("unit")
226-
day_name = extract_week_unit_info(unit, include_dow=False)
227227

228-
if day_name and isinstance(day_name, str):
228+
# Preserve ISOWEEK/WEEKISO as-is (don't convert to WEEK(MONDAY))
229+
if isinstance(unit, exp.Var):
230+
unit_name = unit.this.upper() if isinstance(unit.this, str) else str(unit.this)
231+
if unit_name in ("ISOWEEK", "WEEKISO"):
232+
return self.sql(unit)
233+
234+
week_info = extract_week_unit_info(unit)
235+
236+
if week_info:
237+
day_name, _ = week_info # Extract day name, ignore DOW number
229238
if day_name == "SUNDAY":
230239
return self.sql(exp.var("WEEK"))
231240
elif day_name == "MONDAY":
232-
# Preserve the original form: WEEK(MONDAY) vs ISOWEEK
233241
if isinstance(unit, exp.WeekStart):
234242
return self.sql(unit)
235243
else:
236-
return self.sql(exp.var("ISOWEEK"))
244+
return self.sql(exp.Week(this=exp.var(day_name)))
237245
else:
238246
return self.sql(exp.Week(this=exp.var(day_name)))
239247

sqlglot/dialects/dialect.py

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -811,21 +811,23 @@ class Dialect(metaclass=_Dialect):
811811
"CENTURIES": "CENTURY",
812812
}
813813

814+
# Mapping of week unit names to (start_day, dow_number) tuples
815+
# dow_number is the numeric day-of-week value (0=Sunday, 1=Monday, etc.)
814816
WEEK_UNIT_SEMANTICS = {
815817
"WEEK": ("SUNDAY", 0),
816818
"ISOWEEK": ("MONDAY", 1),
817819
"WEEKISO": ("MONDAY", 1),
818820
}
819821

820-
# Days of week to DOW numbers (ISO standard: Monday=1, Sunday=0)
822+
# Days of week to DOW numbers (ISO standard: Sunday=0, Monday=1)
821823
WEEK_START_DAY_TO_DOW = {
824+
"SUNDAY": 0,
822825
"MONDAY": 1,
823826
"TUESDAY": 2,
824827
"WEDNESDAY": 3,
825828
"THURSDAY": 4,
826829
"FRIDAY": 5,
827830
"SATURDAY": 6,
828-
"SUNDAY": 0,
829831
}
830832

831833
# Specifies what types a given type can be coerced into
@@ -1702,47 +1704,42 @@ def map_date_part(part, dialect: DialectType = Dialect):
17021704

17031705

17041706
def extract_week_unit_info(
1705-
unit: t.Optional[exp.Expression], include_dow: bool = False
1706-
) -> t.Optional[t.Union[str, t.Tuple[str, int]]]:
1707+
unit: t.Optional[exp.Expression], dialect: DialectType = Dialect
1708+
) -> t.Optional[t.Tuple[str, int]]:
17071709
"""
17081710
Extract week unit information from AST node.
17091711
17101712
This helper provides a unified way to handle week units across dialects.
17111713
17121714
Args:
17131715
unit: The unit expression (Var, Week, or WeekStart)
1714-
include_dow: If True, return (day_name, dow_number) tuple for transformations
1715-
If False, return just day_name string for serialization
1716+
dialect: Dialect class or instance for accessing WEEK_UNIT_SEMANTICS and WEEK_START_DAY_TO_DOW
17161717
17171718
Returns:
1718-
- If include_dow=False: day_name (e.g., "SUNDAY")
1719-
- If include_dow=True: (day_name, dow_number) (e.g., ("SUNDAY", 0))
1720-
- None if not a week unit
1719+
- (day_name, dow_number) tuple (e.g., ("SUNDAY", 0) or ("MONDAY", 1))
1720+
- None if not a week unit or if day is dynamic (not a constant)
17211721
17221722
"""
1723+
dialect_instance = Dialect.get_or_raise(dialect)
1724+
17231725
# Handle plain Var expressions for ISOWEEK/WEEKISO only
17241726
# NOTE: Plain Var('WEEK') is NOT handled to avoid breaking other dialects
17251727
if isinstance(unit, exp.Var):
17261728
unit_name = unit.this.upper() if isinstance(unit.this, str) else str(unit.this)
17271729
# Only handle ISOWEEK/WEEKISO variants, not plain WEEK
17281730
if unit_name in ("ISOWEEK", "WEEKISO"):
1729-
week_info = Dialect.WEEK_UNIT_SEMANTICS.get(unit_name)
1731+
week_info = dialect_instance.WEEK_UNIT_SEMANTICS.get(unit_name)
17301732
if week_info:
1731-
day_name, dow = week_info
1732-
return (day_name, dow) if include_dow else day_name
1733+
return week_info
17331734
return None
17341735

17351736
# Handle Week/WeekStart expressions with explicit day
17361737
if isinstance(unit, (exp.Week, exp.WeekStart)):
17371738
day_var = unit.this
17381739
if isinstance(day_var, exp.Var):
17391740
day_name = day_var.this.upper() if isinstance(day_var.this, str) else str(day_var.this)
1740-
1741-
if include_dow:
1742-
dow_value = Dialect.WEEK_START_DAY_TO_DOW.get(day_name)
1743-
return (day_name, dow_value) if dow_value is not None else None
1744-
1745-
return day_name
1741+
dow_value = dialect_instance.WEEK_START_DAY_TO_DOW.get(day_name)
1742+
return (day_name, dow_value) if dow_value is not None else None
17461743

17471744
return None
17481745

sqlglot/dialects/duckdb.py

Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -249,34 +249,14 @@ def _implicit_datetime_cast(
249249
return arg
250250

251251

252-
def _extract_week_start_day(unit: t.Optional[exp.Expression]) -> t.Optional[t.Tuple[str, int]]:
253-
"""
254-
Extract week start day name and DOW number from a Week, WeekStart, or plain Var expression.
255-
256-
Uses extract_week_unit_info(include_dow=True) for uniform week unit handling.
257-
258-
Handles:
259-
- Var('WEEK') -> ('SUNDAY', 0) # BigQuery default
260-
- Var('ISOWEEK') -> ('MONDAY', 1)
261-
- Week(Var('day')) -> ('day', dow)
262-
- WeekStart(Var('day')) -> ('day', dow)
263-
"""
264-
from sqlglot.dialects.dialect import extract_week_unit_info
265-
266-
# Use shared helper with include_dow=True to get (day_name, dow_number)
267-
result = extract_week_unit_info(unit, include_dow=True)
268-
# When include_dow=True, result is either None or Tuple[str, int]
269-
return result if result is None or isinstance(result, tuple) else None
270-
271-
272252
def _build_week_trunc_expression(date_expr: exp.Expression, start_dow: int) -> exp.Expression:
273253
"""
274254
Build DATE_TRUNC expression for week boundaries with custom start day.
275255
Formula: shift = 1 - start_dow, then DATE_TRUNC('week', date + INTERVAL shift DAY)
276256
"""
277257
if start_dow == 1:
278258
# No shift needed for Monday-based weeks (ISO standard)
279-
return exp.Anonymous(this="DATE_TRUNC", expressions=[exp.Literal.string("week"), date_expr])
259+
return exp.DateTrunc(unit=exp.var("week"), this=date_expr)
280260

281261
# Shift date to align week boundaries with ISO Monday
282262
shift_days = 1 - start_dow
@@ -285,18 +265,24 @@ def _build_week_trunc_expression(date_expr: exp.Expression, start_dow: int) -> e
285265
expression=exp.Interval(this=exp.Literal.string(str(shift_days)), unit=exp.var("DAY")),
286266
)
287267

288-
return exp.Anonymous(this="DATE_TRUNC", expressions=[exp.Literal.string("week"), shifted_date])
268+
return exp.DateTrunc(unit=exp.var("week"), this=shifted_date)
289269

290270

291271
def _date_diff_sql(self: DuckDB.Generator, expression: exp.DateDiff) -> str:
292272
"""
293273
Generate DATE_DIFF SQL for DuckDB using DATE_TRUNC-based week alignment.
274+
275+
Note: When week start day is dynamic (from a column), week_start will be None
276+
and we fall back to standard DATE_DIFF, since compile-time offsets are required.
294277
"""
278+
from sqlglot.dialects.dialect import extract_week_unit_info
279+
295280
this = _implicit_datetime_cast(expression.this)
296281
expr = _implicit_datetime_cast(expression.expression)
297282
unit = expression.args.get("unit")
298283

299-
week_start = _extract_week_start_day(unit)
284+
# Extract week start day; returns None if day is dynamic (column reference)
285+
week_start = extract_week_unit_info(unit)
300286
if week_start and this and expr:
301287
_, start_dow = week_start
302288

tests/dialects/test_bigquery.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3225,47 +3225,47 @@ def test_week(self):
32253225
"DATE_DIFF('2024-01-15', '2024-01-08', WEEK(MONDAY))",
32263226
write={
32273227
"bigquery": "DATE_DIFF('2024-01-15', '2024-01-08', WEEK(MONDAY))",
3228-
"duckdb": "DATE_DIFF('DAY', DATE_TRUNC('week', CAST('2024-01-08' AS DATE)), DATE_TRUNC('week', CAST('2024-01-15' AS DATE))) // 7",
3228+
"duckdb": "DATE_DIFF('DAY', DATE_TRUNC('WEEK', CAST('2024-01-08' AS DATE)), DATE_TRUNC('WEEK', CAST('2024-01-15' AS DATE))) // 7",
32293229
},
32303230
)
32313231
# Test WEEK(SUNDAY) - Sunday-based week (normalized to WEEK)
32323232
self.validate_all(
32333233
"DATE_DIFF('2024-01-15', '2024-01-08', WEEK(SUNDAY))",
32343234
write={
32353235
"bigquery": "DATE_DIFF('2024-01-15', '2024-01-08', WEEK)",
3236-
"duckdb": "DATE_DIFF('DAY', DATE_TRUNC('week', CAST('2024-01-08' AS DATE) + INTERVAL '1' DAY), DATE_TRUNC('week', CAST('2024-01-15' AS DATE) + INTERVAL '1' DAY)) // 7",
3236+
"duckdb": "DATE_DIFF('DAY', DATE_TRUNC('WEEK', CAST('2024-01-08' AS DATE) + INTERVAL '1' DAY), DATE_TRUNC('WEEK', CAST('2024-01-15' AS DATE) + INTERVAL '1' DAY)) // 7",
32373237
},
32383238
)
32393239
# Test WEEK(SATURDAY) - Saturday-based week calculation
32403240
self.validate_all(
32413241
"DATE_DIFF('2024-01-15', '2024-01-08', WEEK(SATURDAY))",
32423242
write={
32433243
"bigquery": "DATE_DIFF('2024-01-15', '2024-01-08', WEEK(SATURDAY))",
3244-
"duckdb": "DATE_DIFF('DAY', DATE_TRUNC('week', CAST('2024-01-08' AS DATE) + INTERVAL '-5' DAY), DATE_TRUNC('week', CAST('2024-01-15' AS DATE) + INTERVAL '-5' DAY)) // 7",
3244+
"duckdb": "DATE_DIFF('DAY', DATE_TRUNC('WEEK', CAST('2024-01-08' AS DATE) + INTERVAL '-5' DAY), DATE_TRUNC('WEEK', CAST('2024-01-15' AS DATE) + INTERVAL '-5' DAY)) // 7",
32453245
},
32463246
)
32473247
# Test WEEK - Default Sunday-based week calculation
32483248
self.validate_all(
32493249
"DATE_DIFF('2024-01-15', '2024-01-08', WEEK)",
32503250
write={
32513251
"bigquery": "DATE_DIFF('2024-01-15', '2024-01-08', WEEK)",
3252-
"duckdb": "DATE_DIFF('DAY', DATE_TRUNC('week', CAST('2024-01-08' AS DATE) + INTERVAL '1' DAY), DATE_TRUNC('week', CAST('2024-01-15' AS DATE) + INTERVAL '1' DAY)) // 7",
3252+
"duckdb": "DATE_DIFF('DAY', DATE_TRUNC('WEEK', CAST('2024-01-08' AS DATE) + INTERVAL '1' DAY), DATE_TRUNC('WEEK', CAST('2024-01-15' AS DATE) + INTERVAL '1' DAY)) // 7",
32533253
},
32543254
)
32553255
# Test ISOWEEK - ISO 8601 Monday-based week calculation
32563256
self.validate_all(
32573257
"DATE_DIFF('2024-01-15', '2024-01-08', ISOWEEK)",
32583258
write={
32593259
"bigquery": "DATE_DIFF('2024-01-15', '2024-01-08', ISOWEEK)",
3260-
"duckdb": "DATE_DIFF('DAY', DATE_TRUNC('week', CAST('2024-01-08' AS DATE)), DATE_TRUNC('week', CAST('2024-01-15' AS DATE))) // 7",
3260+
"duckdb": "DATE_DIFF('DAY', DATE_TRUNC('WEEK', CAST('2024-01-08' AS DATE)), DATE_TRUNC('WEEK', CAST('2024-01-15' AS DATE))) // 7",
32613261
},
32623262
)
32633263
# Test with DATE literal - Explicit DATE casting
32643264
self.validate_all(
32653265
"DATE_DIFF(DATE '2024-01-15', DATE '2024-01-08', WEEK(MONDAY))",
32663266
write={
32673267
"bigquery": "DATE_DIFF(CAST('2024-01-15' AS DATE), CAST('2024-01-08' AS DATE), WEEK(MONDAY))",
3268-
"duckdb": "DATE_DIFF('DAY', DATE_TRUNC('week', CAST('2024-01-08' AS DATE)), DATE_TRUNC('week', CAST('2024-01-15' AS DATE))) // 7",
3268+
"duckdb": "DATE_DIFF('DAY', DATE_TRUNC('WEEK', CAST('2024-01-08' AS DATE)), DATE_TRUNC('WEEK', CAST('2024-01-15' AS DATE))) // 7",
32693269
},
32703270
)
32713271

tests/dialects/test_duckdb.py

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1983,24 +1983,6 @@ def test_extract_date_parts(self):
19831983
# All of these should remain as is, they don't have synonyms
19841984
self.validate_identity(f"EXTRACT({part} FROM foo)")
19851985

1986-
def test_date_diff_week(self):
1987-
# Test Monday-based week (ISO week) - no offset needed
1988-
self.validate_identity(
1989-
"DATE_DIFF('DAY', DATE_TRUNC('WEEK', CAST('2024-01-08' AS DATE)), DATE_TRUNC('WEEK', CAST('2024-01-15' AS DATE))) // 7"
1990-
)
1991-
# Test Sunday-based week - shift by +1 day to align with ISO Monday
1992-
self.validate_identity(
1993-
"DATE_DIFF('DAY', DATE_TRUNC('WEEK', CAST('2024-01-08' AS DATE) + INTERVAL '1' DAY), DATE_TRUNC('WEEK', CAST('2024-01-15' AS DATE) + INTERVAL '1' DAY)) // 7"
1994-
)
1995-
# Test Saturday-based week - shift by -5 days to align with ISO Monday
1996-
self.validate_identity(
1997-
"DATE_DIFF('DAY', DATE_TRUNC('WEEK', CAST('2024-01-08' AS DATE) + INTERVAL '-5' DAY), DATE_TRUNC('WEEK', CAST('2024-01-15' AS DATE) + INTERVAL '-5' DAY)) // 7"
1998-
)
1999-
# Test zero weeks between dates in same week
2000-
self.validate_identity(
2001-
"DATE_DIFF('DAY', DATE_TRUNC('WEEK', CAST('2024-01-01' AS DATE)), DATE_TRUNC('WEEK', CAST('2024-01-01' AS DATE))) // 7"
2002-
)
2003-
20041986
def test_set_item(self):
20051987
self.validate_identity("SET memory_limit = '10GB'")
20061988
self.validate_identity("SET SESSION default_collation = 'nocase'")

0 commit comments

Comments
 (0)