Repository: googleapis/python-spanner-django

Issue:
django-test-suite: timezones.tests.LegacyDatabaseTests.test_cursor_execute_returns_naive_datetime comparison bypasses conversions
The code for `timezones.tests.LegacyDatabaseTests.test_cursor_execute_returns_naive_datetime`
performs a cursor.fetchall bypassing any conversions by our Django adapter as per https://github.com/timgraham/django/blob/9763540cd3b68f49610bede78ddef09d8e3cccdd/tests/timezones/tests.py#L211-L216
![test_cursor_execute_returns_native_dateime-bypass](https://user-images.githubusercontent.com/4898263/72143203-1f5d6680-334b-11ea-93f2-3419e7c19345.png)

Let's skip this test


Discussion:

Resolving patch:
diff --git a/spanner/dbapi/parse_utils.py b/spanner/dbapi/parse_utils.py
--- a/spanner/dbapi/parse_utils.py
+++ b/spanner/dbapi/parse_utils.py
@@ -12,6 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import datetime
 import decimal
 import re
 from functools import reduce
@@ -373,9 +374,9 @@ def get_param_types(params):
             param_types[key] = spanner.param_types.FLOAT64
         elif isinstance(value, int):
             param_types[key] = spanner.param_types.INT64
-        elif isinstance(value, TimestampStr):
+        elif isinstance(value, (TimestampStr, datetime.datetime,)):
             param_types[key] = spanner.param_types.TIMESTAMP
-        elif isinstance(value, DateStr):
+        elif isinstance(value, (DateStr, datetime.date,)):
             param_types[key] = spanner.param_types.DATE
         elif isinstance(value, str):
             param_types[key] = spanner.param_types.STRING
diff --git a/spanner/django/__init__.py b/spanner/django/__init__.py
--- a/spanner/django/__init__.py
+++ b/spanner/django/__init__.py
@@ -12,11 +12,14 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import datetime
 # Monkey-patch AutoField to generate a random value since Cloud Spanner can't
 # do that.
 from uuid import uuid4
 
 from django.db.models.fields import AutoField, Field
+# Monkey-patch google.DatetimeWithNanoseconds's __eq__ compare against datetime.datetime.
+from google.api_core.datetime_helpers import DatetimeWithNanoseconds
 
 from .expressions import register_expressions
 from .functions import register_functions
@@ -39,3 +42,35 @@ def autofield_init(self, *args, **kwargs):
 
 
 AutoField.__init__ = autofield_init
+
+old_datetimewithnanoseconds_eq = getattr(DatetimeWithNanoseconds, '__eq__', None)
+
+
+def datetimewithnanoseconds_eq(self, other):
+    if old_datetimewithnanoseconds_eq:
+        equal = old_datetimewithnanoseconds_eq(self, other)
+        if equal:
+            return True
+        elif type(self) is type(other):
+            return False
+
+    # Otherwise try to convert them to an equvialent form.
+    # See https://github.com/orijtech/spanner-orm/issues/272
+    if isinstance(other, datetime.datetime):
+        return self.ctime() == other.ctime()
+
+    return False
+
+
+DatetimeWithNanoseconds.__eq__ = datetimewithnanoseconds_eq
+
+# Sanity check here since tests can't easily be run for this file:
+if __name__ == '__main__':
+    from django.utils import timezone
+    UTC = timezone.utc
+
+    dt = datetime.datetime(2020, 1, 10, 2, 44, 57, 999, UTC)
+    dtns = DatetimeWithNanoseconds(2020, 1, 10, 2, 44, 57, 999, UTC)
+    equal = dtns == dt
+    if not equal:
+        raise Exception('%s\n!=\n%s' % (dtns, dt))


Test patch:
diff --git a/tests/spanner/dbapi/test_parse_utils.py b/tests/spanner/dbapi/test_parse_utils.py
--- a/tests/spanner/dbapi/test_parse_utils.py
+++ b/tests/spanner/dbapi/test_parse_utils.py
@@ -12,6 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import datetime
 import decimal
 from unittest import TestCase
 
@@ -296,6 +297,17 @@ def test_get_param_types(self):
                 {'a1': 10, 'b1': '2019-11-26T02:55:41.000000Z'},
                 {'a1': param_types.INT64, 'b1': param_types.STRING},
             ),
+            (
+                {
+                    'a1': 10, 'b1': TimestampStr('2019-11-26T02:55:41.000000Z'),
+                    'dt1': datetime.datetime(2011, 9, 1, 13, 20, 30),
+                    'd1': datetime.date(2011, 9, 1),
+                },
+                {
+                    'a1': param_types.INT64, 'b1': param_types.TIMESTAMP,
+                    'dt1': param_types.TIMESTAMP, 'd1': param_types.DATE,
+                },
+            ),
             (
                 {'a1': 10, 'b1': TimestampStr('2019-11-26T02:55:41.000000Z')},
                 {'a1': param_types.INT64, 'b1': param_types.TIMESTAMP},