Repository: googleapis/python-spanner-django

Issue:
django: implement DatabaseOperations.date_interval_sql()
Example failure:
```
======================================================================
ERROR: test_update_with_timedelta (timezones.tests.NewDatabaseTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/tim/code/django/tests/timezones/tests.py", line 515, in test_update_with_timedelta
    Event.objects.update(dt=F('dt') + timedelta(hours=2))
  File "/home/tim/code/django/django/db/models/manager.py", line 82, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "/home/tim/code/django/django/db/models/query.py", line 741, in update
    rows = query.get_compiler(self.db).execute_sql(CURSOR)
  File "/home/tim/code/django/django/db/models/sql/compiler.py", line 1462, in execute_sql
    cursor = super().execute_sql(result_type)
  File "/home/tim/code/django/django/db/models/sql/compiler.py", line 1120, in execute_sql
    sql, params = self.as_sql()
  File "/home/tim/code/django/django/db/models/sql/compiler.py", line 1437, in as_sql
    sql, params = self.compile(val)
  File "/home/tim/code/django/django/db/models/sql/compiler.py", line 405, in compile
    sql, params = node.as_sql(self, self.connection)
  File "/home/tim/code/django/django/db/models/expressions.py", line 428, in as_sql
    return DurationExpression(self.lhs, self.connector, self.rhs).as_sql(compiler, connection)
  File "/home/tim/code/django/django/db/models/expressions.py", line 474, in as_sql
    sql, params = self.compile(self.rhs, compiler, connection)
  File "/home/tim/code/django/django/db/models/expressions.py", line 465, in compile
    return compiler.compile(side)
  File "/home/tim/code/django/django/db/models/sql/compiler.py", line 405, in compile
    sql, params = node.as_sql(self, self.connection)
  File "/home/tim/code/django/django/db/models/expressions.py", line 683, in as_sql
    return connection.ops.date_interval_sql(self.value), []
  File "/home/tim/code/django/django/db/backends/base/operations.py", line 102, in date_interval_sql
    raise NotImplementedError('subclasses of BaseDatabaseOperations may require a date_interval_sql() method')
NotImplementedError: subclasses of BaseDatabaseOperations may require a date_interval_sql() method
```


Discussion:

Resolving patch:
diff --git a/spanner/django/features.py b/spanner/django/features.py
--- a/spanner/django/features.py
+++ b/spanner/django/features.py
@@ -61,9 +61,6 @@ class DatabaseFeatures(BaseDatabaseFeatures):
         # https://github.com/orijtech/spanner-orm/issues/155
         'timezones.tests.LegacyDatabaseTests.test_raw_sql',
         'timezones.tests.NewDatabaseTests.test_raw_sql',
-        # implement DatabaseOperations.date_interval_sql()
-        # https://github.com/orijtech/spanner-orm/issues/184
-        'timezones.tests.NewDatabaseTests.test_update_with_timedelta',
         # Unable to infer type for parameter:
         # https://github.com/orijtech/spanner-orm/issues/185
         'timezones.tests.LegacyDatabaseTests.test_cursor_execute_returns_naive_datetime',
@@ -92,9 +89,6 @@ class DatabaseFeatures(BaseDatabaseFeatures):
         'lookup.test_decimalfield.DecimalFieldLookupTests.test_lt',
         'lookup.test_decimalfield.DecimalFieldLookupTests.test_lte',
         'model_fields.test_decimalfield.DecimalFieldTests.test_filter_with_strings',
-        # annotating DateTimeField + DurationField crashes:
-        # https://github.com/orijtech/spanner-orm/issues/202
-        'annotations.tests.NonAggregateAnnotationTestCase.test_mixed_type_annotation_date_interval',
         # 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/spanner/django/operations.py b/spanner/django/operations.py
--- a/spanner/django/operations.py
+++ b/spanner/django/operations.py
@@ -6,7 +6,9 @@
 
 from django.conf import settings
 from django.db.backends.base.operations import BaseDatabaseOperations
+from django.db.utils import DatabaseError
 from django.utils import timezone
+from django.utils.duration import duration_microseconds
 from spanner.dbapi.parse_utils import DateStr, TimestampStr, escape_name
 
 
@@ -173,6 +175,20 @@ def datetime_trunc_sql(self, lookup_type, field_name, tzname):
             sql = 'TIMESTAMP_ADD(' + sql + ', INTERVAL 1 DAY)'
         return sql
 
+    def date_interval_sql(self, timedelta):
+        return 'INTERVAL %s MICROSECOND' % duration_microseconds(timedelta)
+
+    def format_for_duration_arithmetic(self, sql):
+        return 'INTERVAL %s MICROSECOND' % sql
+
+    def combine_duration_expression(self, connector, sub_expressions):
+        if connector == '+':
+            return 'TIMESTAMP_ADD(' + ', '.join(sub_expressions) + ')'
+        elif connector == '-':
+            return 'TIMESTAMP_SUB(' + ', '.join(sub_expressions) + ')'
+        else:
+            raise DatabaseError('Invalid connector for timedelta: %s.' % connector)
+
     def lookup_cast(self, lookup_type, internal_type=None):
         # Cast text lookups to string to allow things like filter(x__contains=4)
         if lookup_type in ('contains', 'icontains', 'startswith', 'istartswith',


Test patch: