@@ -152,32 +152,6 @@ def _pushdown_cte_column_names(expression: exp.Expression) -> exp.Expression:
152152 return expression
153153
154154
155- def _normalize_week_units (expression : exp .Expression ) -> exp .Expression :
156- """
157- Normalize WEEK and ISOWEEK units in DATE_DIFF to WeekStart expressions.
158-
159- Transformations:
160- - WEEK -> WeekStart(this=Var('SUNDAY'))
161- - ISOWEEK -> WeekStart(this=Var('MONDAY'))
162-
163- Note: WEEK(day) is already parsed as WeekStart by the parser.
164- """
165- if isinstance (expression , exp .DateDiff ):
166- unit = expression .args .get ("unit" )
167-
168- if isinstance (unit , exp .Var ):
169- unit_name = unit .this .upper () if isinstance (unit .this , str ) else str (unit .this )
170-
171- if unit_name == "WEEK" :
172- # BigQuery's WEEK uses Sunday as the start of the week
173- expression .set ("unit" , exp .WeekStart (this = exp .var ("SUNDAY" )))
174- elif unit_name == "ISOWEEK" :
175- # ISOWEEK uses Monday as the start of the week
176- expression .set ("unit" , exp .WeekStart (this = exp .var ("MONDAY" )))
177-
178- return expression
179-
180-
181155def _build_parse_timestamp (args : t .List ) -> exp .StrToTime :
182156 this = build_formatted_time (exp .StrToTime , "bigquery" )([seq_get (args , 1 ), seq_get (args , 0 )])
183157 this .set ("zone" , seq_get (args , 2 ))
@@ -234,6 +208,34 @@ def _ts_or_ds_diff_sql(self: BigQuery.Generator, expression: exp.TsOrDsDiff) ->
234208 return self .func ("DATE_DIFF" , expression .this , expression .expression , unit )
235209
236210
211+ def _serialize_bq_datetime_diff_unit (self : BigQuery .Generator , expression : exp .Expression ) -> str :
212+ """
213+ Serialize unit for *_DIFF functions, converting Week expressions to BigQuery syntax.
214+
215+ Canonical form -> BigQuery syntax:
216+ - Week(SUNDAY) -> WEEK (BigQuery's default)
217+ - Week(MONDAY) -> ISOWEEK
218+ - Week(other day) -> WEEK(day)
219+ - Other units -> use unit_to_var
220+
221+ """
222+ from sqlglot .dialects .dialect import extract_week_unit_info
223+
224+ unit = expression .args .get ("unit" )
225+ day_name = extract_week_unit_info (unit , include_dow = False )
226+
227+ if day_name and isinstance (day_name , str ):
228+ if day_name == "SUNDAY" :
229+ return self .sql (exp .var ("WEEK" ))
230+ elif day_name == "MONDAY" :
231+ return self .sql (exp .var ("ISOWEEK" ))
232+ else :
233+ return self .sql (exp .Week (this = exp .var (day_name )))
234+
235+ unit_expr = unit_to_var (expression )
236+ return self .sql (unit_expr ) if unit_expr else "DAY"
237+
238+
237239def _unix_to_time_sql (self : BigQuery .Generator , expression : exp .UnixToTime ) -> str :
238240 scale = expression .args .get ("scale" )
239241 timestamp = expression .this
@@ -267,6 +269,46 @@ def _build_datetime(args: t.List) -> exp.Func:
267269 return exp .TimestampFromParts .from_arg_list (args )
268270
269271
272+ def _normalize_week_unit (unit : t .Optional [exp .Expression ]) -> t .Optional [exp .Expression ]:
273+ """
274+ In BigQuery, plain WEEK defaults to Sunday-start weeks.
275+ Normalize plain WEEK to WEEK(SUNDAY) to preserve the semantic in the AST for correct cross-dialect transpilation.
276+ """
277+ unit_name = None
278+
279+ if isinstance (unit , exp .Var ):
280+ unit_name = str (unit .this )
281+ elif isinstance (unit , exp .Column ) and isinstance (unit .this , exp .Identifier ):
282+ unit_name = str (unit .this .this )
283+
284+ if unit_name and unit_name .upper () == "WEEK" :
285+ return exp .Week (this = exp .var ("SUNDAY" ))
286+
287+ return unit
288+
289+
290+ def build_date_time_diff_with_week_normalization (
291+ exp_class : t .Type [E ],
292+ ) -> t .Callable [[t .List ], E ]:
293+ """
294+ Factory for *_DIFF functions that normalizes plain WEEK units to WEEK(SUNDAY).
295+
296+ These functions have signature: FUNC(expr1, expr2, date_part)
297+ where date_part is at argument index 2.
298+
299+ Supports: DATE_DIFF, DATETIME_DIFF, TIME_DIFF, TIMESTAMP_DIFF
300+ """
301+
302+ def _builder (args : t .List ) -> E :
303+ return exp_class (
304+ this = seq_get (args , 0 ),
305+ expression = seq_get (args , 1 ),
306+ unit = _normalize_week_unit (seq_get (args , 2 )),
307+ )
308+
309+ return _builder
310+
311+
270312def _build_regexp_extract (
271313 expr_type : t .Type [E ], default_group : t .Optional [exp .Expression ] = None
272314) -> t .Callable [[t .List ], E ]:
@@ -435,17 +477,6 @@ class BigQuery(Dialect):
435477
436478 EXPRESSION_METADATA = EXPRESSION_METADATA .copy ()
437479
438- def parse (self , sql : str , ** opts ) -> t .List [t .Optional [exp .Expression ]]:
439- """Parse SQL and normalize BigQuery-specific constructs to canonical form."""
440- expressions = super ().parse (sql , ** opts )
441-
442- # Normalize WEEK units in DATE_DIFF to canonical WeekStart expressions
443- for expression in expressions :
444- if expression :
445- expression .transform (_normalize_week_units , copy = False )
446-
447- return expressions
448-
449480 def normalize_identifier (self , expression : E ) -> E :
450481 if (
451482 isinstance (expression , exp .Identifier )
@@ -569,6 +600,7 @@ class Parser(parser.Parser):
569600 "CONTAINS_SUBSTR" : _build_contains_substring ,
570601 "DATE" : _build_date ,
571602 "DATE_ADD" : build_date_delta_with_interval (exp .DateAdd ),
603+ "DATE_DIFF" : build_date_time_diff_with_week_normalization (exp .DateDiff ),
572604 "DATE_SUB" : build_date_delta_with_interval (exp .DateSub ),
573605 "DATE_TRUNC" : lambda args : exp .DateTrunc (
574606 unit = seq_get (args , 1 ),
@@ -1101,7 +1133,7 @@ class Generator(generator.Generator):
11011133 exp .CTE : transforms .preprocess ([_pushdown_cte_column_names ]),
11021134 exp .DateAdd : date_add_interval_sql ("DATE" , "ADD" ),
11031135 exp .DateDiff : lambda self , e : self .func (
1104- "DATE_DIFF" , e .this , e .expression , self . _date_diff_unit_sql ( e )
1136+ "DATE_DIFF" , e .this , e .expression , _serialize_bq_datetime_diff_unit ( self , e )
11051137 ),
11061138 exp .DateFromParts : rename_func ("DATE" ),
11071139 exp .DateStrToDate : datestrtodate_sql ,
@@ -1354,40 +1386,6 @@ class Generator(generator.Generator):
13541386 "within" ,
13551387 }
13561388
1357- def _date_diff_unit_sql (self , expression : exp .DateDiff ) -> str :
1358- """
1359- Convert canonical WeekStart expression back to BigQuery syntax.
1360-
1361- Canonical form -> BigQuery syntax:
1362- - WeekStart(SUNDAY) -> WEEK (BigQuery's default)
1363- - WeekStart(MONDAY) -> ISOWEEK
1364- - WeekStart(other day) -> WEEK(day)
1365- - Other units -> use unit_to_var as normal
1366- """
1367- unit = expression .args .get ("unit" )
1368-
1369- if isinstance (unit , exp .WeekStart ):
1370- # Extract the day from WeekStart
1371- day_var = unit .this
1372- if isinstance (day_var , exp .Var ):
1373- day_name = (
1374- day_var .this .upper () if isinstance (day_var .this , str ) else str (day_var .this )
1375- )
1376-
1377- if day_name == "SUNDAY" :
1378- # BigQuery's default WEEK is Sunday-start
1379- return self .sql (exp .var ("WEEK" ))
1380- elif day_name == "MONDAY" :
1381- # Use ISOWEEK for Monday-start
1382- return self .sql (exp .var ("ISOWEEK" ))
1383- else :
1384- # For other days, use WEEK(day) syntax
1385- return self .sql (exp .Week (this = day_var ))
1386-
1387- # For other units, use default behavior
1388- unit_expr = unit_to_var (expression )
1389- return self .sql (unit_expr ) if unit_expr else "DAY"
1390-
13911389 def datetrunc_sql (self , expression : exp .DateTrunc ) -> str :
13921390 unit = expression .unit
13931391 unit_sql = unit .name if unit .is_string else self .sql (unit )
0 commit comments