Repository: googleapis/python-spanner-django

Issue:
django: Spanner's EXTRACT() 'week' differs from Django
[Django](https://docs.djangoproject.com/en/stable/ref/models/database-functions/): The week lookup_type is calculated based on ISO-8601, i.e., a week starts on a Monday. The first week of a year is the one that contains the year’s first Thursday, i.e. the first week has the majority (four or more) of its days in the year. The value returned is in the range 1 to 52 or 53.

[Spanner](https://cloud.google.com/spanner/docs/functions-and-operators#extract_2): Returns the week number of the date in the range [0, 53]. Weeks begin with Sunday, and dates prior to the first Sunday of the year are in week 0.
```
======================================================================
FAIL: test_extract_func (db_functions.datetime.test_extract_trunc.DateFunctionTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/tim/code/django/tests/db_functions/datetime/test_extract_trunc.py", line 191, in test_extract_func
    lambda m: (m.start_datetime, m.extracted)
  File "/home/tim/code/django/django/test/testcases.py", line 1056, in assertQuerysetEqual
    return self.assertEqual(list(items), values, msg=msg)
AssertionError: Lists differ: [(dat[42 chars]1), 24), (datetime.datetime(2016, 6, 15, 14, 10, 50, 123), 24)] != [(dat[42 chars]1), 25), (datetime.datetime(2016, 6, 15, 14, 10, 50, 123), 24)]

First differing element 0:
(datetime.datetime(2015, 6, 15, 14, 30, 50, 321), 24)
(datetime.datetime(2015, 6, 15, 14, 30, 50, 321), 25)

- [(datetime.datetime(2015, 6, 15, 14, 30, 50, 321), 24),
?                                                     ^

+ [(datetime.datetime(2015, 6, 15, 14, 30, 50, 321), 25),
?                                                     ^

   (datetime.datetime(2016, 6, 15, 14, 10, 50, 123), 24)]

======================================================================
FAIL: test_extract_week_func (db_functions.datetime.test_extract_trunc.DateFunctionTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/tim/code/django/tests/db_functions/datetime/test_extract_trunc.py", line 373, in test_extract_week_func
    lambda m: (m.start_datetime, m.extracted)
  File "/home/tim/code/django/django/test/testcases.py", line 1056, in assertQuerysetEqual
    return self.assertEqual(list(items), values, msg=msg)
AssertionError: Lists differ: [(dat[42 chars]1), 24), (datetime.datetime(2016, 6, 15, 14, 10, 50, 123), 24)] != [(dat[42 chars]1), 25), (datetime.datetime(2016, 6, 15, 14, 10, 50, 123), 24)]

First differing element 0:
(datetime.datetime(2015, 6, 15, 14, 30, 50, 321), 24)
(datetime.datetime(2015, 6, 15, 14, 30, 50, 321), 25)

- [(datetime.datetime(2015, 6, 15, 14, 30, 50, 321), 24),
?                                                     ^

+ [(datetime.datetime(2015, 6, 15, 14, 30, 50, 321), 25),
?                                                     ^

   (datetime.datetime(2016, 6, 15, 14, 10, 50, 123), 24)]

======================================================================
FAIL: test_extract_week_func_boundaries (db_functions.datetime.test_extract_trunc.DateFunctionTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/tim/code/django/tests/db_functions/datetime/test_extract_trunc.py", line 448, in test_extract_week_func_boundaries
    ], lambda m: (m.start_datetime, m.extracted))
  File "/home/tim/code/django/django/test/testcases.py", line 1056, in assertQuerysetEqual
    return self.assertEqual(list(items), values, msg=msg)
AssertionError: Lists differ: [(dat[33 chars]0), 51), (datetime.datetime(2014, 12, 31, 13, [49 chars] 52)] != [(dat[33 chars]0), 52), (datetime.datetime(2014, 12, 31, 13, [48 chars] 53)]

First differing element 0:
(datetime.datetime(2014, 12, 27, 13, 0), 51)
(datetime.datetime(2014, 12, 27, 13, 0), 52)

- [(datetime.datetime(2014, 12, 27, 13, 0), 51),
?                                            ^

+ [(datetime.datetime(2014, 12, 27, 13, 0), 52),
?                                            ^

-  (datetime.datetime(2014, 12, 31, 13, 0), 52),
?                                           ^^

+  (datetime.datetime(2014, 12, 31, 13, 0), 1),
?                                           ^

-  (datetime.datetime(2015, 12, 31, 13, 0), 52)]
?                                            ^

+  (datetime.datetime(2015, 12, 31, 13, 0), 53)]
?                                            ^


======================================================================
FAIL: test_extract_func (db_functions.datetime.test_extract_trunc.DateFunctionWithTimeZoneTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/tim/code/django/tests/db_functions/datetime/test_extract_trunc.py", line 191, in test_extract_func
    lambda m: (m.start_datetime, m.extracted)
  File "/home/tim/code/django/django/test/testcases.py", line 1056, in assertQuerysetEqual
    return self.assertEqual(list(items), values, msg=msg)
AssertionError: Lists differ: [(dat[56 chars]>), 24), (datetime.datetime(2016, 6, 15, 14, 1[26 chars] 24)] != [(dat[56 chars]>), 25), (datetime.datetime(2016, 6, 15, 14, 1[26 chars] 24)]

First differing element 0:
(datetime.datetime(2015, 6, 15, 14, 30, 50, 321, tzinfo=<UTC>), 24)
(datetime.datetime(2015, 6, 15, 14, 30, 50, 321, tzinfo=<UTC>), 25)

- [(datetime.datetime(2015, 6, 15, 14, 30, 50, 321, tzinfo=<UTC>), 24),
?                                                                   ^

+ [(datetime.datetime(2015, 6, 15, 14, 30, 50, 321, tzinfo=<UTC>), 25),
?                                                                   ^

   (datetime.datetime(2016, 6, 15, 14, 10, 50, 123, tzinfo=<UTC>), 24)]

======================================================================
FAIL: test_extract_func_with_timezone (db_functions.datetime.test_extract_trunc.DateFunctionWithTimeZoneTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/tim/code/django/tests/db_functions/datetime/test_extract_trunc.py", line 988, in test_extract_func_with_timezone
    self.assertEqual(utc_model.week, 25)
AssertionError: 24 != 25

======================================================================
FAIL: test_extract_week_func (db_functions.datetime.test_extract_trunc.DateFunctionWithTimeZoneTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/tim/code/django/tests/db_functions/datetime/test_extract_trunc.py", line 373, in test_extract_week_func
    lambda m: (m.start_datetime, m.extracted)
  File "/home/tim/code/django/django/test/testcases.py", line 1056, in assertQuerysetEqual
    return self.assertEqual(list(items), values, msg=msg)
AssertionError: Lists differ: [(dat[56 chars]>), 24), (datetime.datetime(2016, 6, 15, 14, 1[26 chars] 24)] != [(dat[56 chars]>), 25), (datetime.datetime(2016, 6, 15, 14, 1[26 chars] 24)]

First differing element 0:
(datetime.datetime(2015, 6, 15, 14, 30, 50, 321, tzinfo=<UTC>), 24)
(datetime.datetime(2015, 6, 15, 14, 30, 50, 321, tzinfo=<UTC>), 25)

- [(datetime.datetime(2015, 6, 15, 14, 30, 50, 321, tzinfo=<UTC>), 24),
?                                                                   ^

+ [(datetime.datetime(2015, 6, 15, 14, 30, 50, 321, tzinfo=<UTC>), 25),
?                                                                   ^

   (datetime.datetime(2016, 6, 15, 14, 10, 50, 123, tzinfo=<UTC>), 24)]

======================================================================
FAIL: test_extract_week_func_boundaries (db_functions.datetime.test_extract_trunc.DateFunctionWithTimeZoneTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/tim/code/django/tests/db_functions/datetime/test_extract_trunc.py", line 448, in test_extract_week_func_boundaries
    ], lambda m: (m.start_datetime, m.extracted))
  File "/home/tim/code/django/django/test/testcases.py", line 1056, in assertQuerysetEqual
    return self.assertEqual(list(items), values, msg=msg)
AssertionError: Lists differ: [(dat[47 chars]>), 51), (datetime.datetime(2014, 12, 31, 13, [77 chars] 52)] != [(dat[47 chars]>), 52), (datetime.datetime(2014, 12, 31, 13, [76 chars] 53)]

First differing element 0:
(datetime.datetime(2014, 12, 27, 13, 0, tzinfo=<UTC>), 51)
(datetime.datetime(2014, 12, 27, 13, 0, tzinfo=<UTC>), 52)

Diff is 748 characters long. Set self.maxDiff to None to see it.
```


Discussion:
Cloud Spanner has ISOWEEK, ISOYEAR that can be passed into EXTRACT per
https://cloud.google.com/spanner/docs/functions-and-operators#extract, perhaps that could be helpful here?
I can confirm that it seems like ISOWEEK works properly

```sql
SELECT
	EXTRACT(week from DATE(dt.start_datetime)) as week,
    EXTRACT(isoweek from DATE(dt.start_datetime)) as isoweek,
    dt.start_datetime as full_date
FROM
	db_functions_dtmodel as dt
```

which produces


week | isoweek | full_date
-- | -- | --
24 | 24 | 2016-06-15T14:10:50.000123Z
50 | 51 | 2019-12-17T19:54:39Z
24 | 25 | 2015-06-15T14:30:50.000321Z

and I believe our test requires a value of 25, instead of 24, and would expect 51 instead of 50 where the values in columns isoweek vs week respectively

Resolving patch:
diff --git a/django_spanner/features.py b/django_spanner/features.py
--- a/django_spanner/features.py
+++ b/django_spanner/features.py
@@ -178,15 +178,6 @@ class DatabaseFeatures(BaseDatabaseFeatures):
         'timezones.tests.NewDatabaseTests.test_query_datetimes',
         # To be investigated: https://github.com/orijtech/spanner-orm/issues/135
         'admin_changelist.tests.ChangeListTests.test_multiuser_edit',
-        # Spanner's EXTRACT() 'week' differs from Django:
-        # https://github.com/orijtech/spanner-orm/issues/263
-        'db_functions.datetime.test_extract_trunc.DateFunctionTests.test_extract_func',
-        'db_functions.datetime.test_extract_trunc.DateFunctionTests.test_extract_week_func',
-        'db_functions.datetime.test_extract_trunc.DateFunctionTests.test_extract_week_func_boundaries',
-        'db_functions.datetime.test_extract_trunc.DateFunctionWithTimeZoneTests.test_extract_func',
-        'db_functions.datetime.test_extract_trunc.DateFunctionWithTimeZoneTests.test_extract_func_with_timezone',
-        'db_functions.datetime.test_extract_trunc.DateFunctionWithTimeZoneTests.test_extract_week_func',
-        'db_functions.datetime.test_extract_trunc.DateFunctionWithTimeZoneTests.test_extract_week_func_boundaries',
         # using NULL with + crashes: https://github.com/orijtech/spanner-orm/issues/201
         'annotations.tests.NonAggregateAnnotationTestCase.test_combined_annotation_commutative',
         # Spanner loses DecimalField precision due to conversion to float:
diff --git a/django_spanner/operations.py b/django_spanner/operations.py
--- a/django_spanner/operations.py
+++ b/django_spanner/operations.py
@@ -30,9 +30,9 @@ class DatabaseOperations(BaseDatabaseOperations):
     # EXTRACT() function.
     # https://cloud.google.com/spanner/docs/functions-and-operators#extract
     extract_names = {
-        'week_day': 'dayofweek',
-        'iso_week': 'isoweek',
         'iso_year': 'isoyear',
+        'week': 'isoweek',
+        'week_day': 'dayofweek',
     }
 
     def max_name_length(self):


Test patch: