Repository: googleapis/python-spanner-django

Issue:
Creating a model with DateField crashes: Unable to infer type for parameter
We need to set `params_type` as in https://github.com/orijtech/spanner-orm/pull/83.
```
======================================================================
ERROR: test_add (many_to_one.tests.ManyToOneTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/tim/.virtualenvs/django37/lib/python3.7/site-packages/google/api_core/grpc_helpers.py", line 57, in error_remapped_callable
    return callable_(*args, **kwargs)
  File "/home/tim/.virtualenvs/django37/lib/python3.7/site-packages/grpc/_channel.py", line 690, in __call__
    return _end_unary_response_blocking(state, call, False, None)
  File "/home/tim/.virtualenvs/django37/lib/python3.7/site-packages/grpc/_channel.py", line 592, in _end_unary_response_blocking
    raise _Rendezvous(state, None, None, deadline)
grpc._channel._Rendezvous: <_Rendezvous of RPC that terminated with:
	status = StatusCode.INVALID_ARGUMENT
	details = "Unable to infer type for parameter: a1. Provide the parameter type in the parameter binding."
	debug_error_string = "{"created":"@1575243539.099247432","description":"Error received from peer ipv4:172.217.10.10:443","file":"src/core/lib/surface/call.cc","file_line":1055,"grpc_message":"Unable to infer type for parameter: a1. Provide the parameter type in the parameter binding.","grpc_status":3}"
>

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/tim/code/django/tests/many_to_one/tests.py", line 25, in setUp
    self.a.save()
  File "/home/tim/code/django/django/db/models/base.py", line 741, in save
    force_update=force_update, update_fields=update_fields)
  File "/home/tim/code/django/django/db/models/base.py", line 779, in save_base
    force_update, using, update_fields,
  File "/home/tim/code/django/django/db/models/base.py", line 851, in _save_table
    forced_update)
  File "/home/tim/code/django/django/db/models/base.py", line 900, in _do_update
    return filtered._update(values) > 0
  File "/home/tim/code/django/django/db/models/query.py", line 760, in _update
    return query.get_compiler(self.db).execute_sql(CURSOR)
  File "/home/tim/code/django/django/db/models/sql/compiler.py", line 1429, in execute_sql
    cursor = super().execute_sql(result_type)
  File "/home/tim/code/django/django/db/models/sql/compiler.py", line 1100, in execute_sql
    cursor.execute(sql, params)
  File "/home/tim/code/django/django/db/backends/utils.py", line 67, in execute
    return self._execute_with_wrappers(sql, params, many=False, executor=self._execute)
  File "/home/tim/code/django/django/db/backends/utils.py", line 76, in _execute_with_wrappers
    return executor(sql, params, many, context)
  File "/home/tim/code/django/django/db/backends/utils.py", line 84, in _execute
    return self.cursor.execute(sql, params)
  File "/home/tim/code/spanner-orm/spanner/dbapi/cursor.py", line 98, in execute
    param_types=param_types,
  File "/home/tim/.virtualenvs/django37/lib/python3.7/site-packages/google/cloud/spanner_v1/session.py", line 300, in run_in_transaction
    return_value = func(txn, *args, **kw)
  File "/home/tim/code/spanner-orm/spanner/dbapi/cursor.py", line 123, in __do_execute_update
    res = transaction.execute_update(sql, params=params, param_types=param_types)
  File "/home/tim/.virtualenvs/django37/lib/python3.7/site-packages/google/cloud/spanner_v1/transaction.py", line 202, in execute_update
    metadata=metadata,
  File "/home/tim/.virtualenvs/django37/lib/python3.7/site-packages/google/cloud/spanner_v1/gapic/spanner_client.py", line 810, in execute_sql
    request, retry=retry, timeout=timeout, metadata=metadata
  File "/home/tim/.virtualenvs/django37/lib/python3.7/site-packages/google/api_core/gapic_v1/method.py", line 143, in __call__
    return wrapped_func(*args, **kwargs)
  File "/home/tim/.virtualenvs/django37/lib/python3.7/site-packages/google/api_core/retry.py", line 277, in retry_wrapped_func
    on_error=on_error,
  File "/home/tim/.virtualenvs/django37/lib/python3.7/site-packages/google/api_core/retry.py", line 182, in retry_target
    return target()
  File "/home/tim/.virtualenvs/django37/lib/python3.7/site-packages/google/api_core/timeout.py", line 214, in func_with_timeout
    return func(*args, **kwargs)
  File "/home/tim/.virtualenvs/django37/lib/python3.7/site-packages/google/api_core/grpc_helpers.py", line 59, in error_remapped_callable
    six.raise_from(exceptions.from_grpc_error(exc), exc)
  File "<string>", line 3, in raise_from
google.api_core.exceptions.InvalidArgument: 400 Unable to infer type for parameter: a1. Provide the parameter type in the parameter binding.
```


Discussion:
I think the way to go here is to implement `DatabaseOperations.adapt_datefield_value()` and cast to `DateStr` similar to the existing implementation for `TimestampStr`.

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
@@ -19,7 +19,7 @@
 from google.cloud import spanner_v1 as spanner
 
 from .exceptions import Error
-from .types import TimestampStr
+from .types import DateStr, TimestampStr
 
 
 def resolve_project_id(project_id):
@@ -420,6 +420,8 @@ def infer_param_types(params, param_types):
             param_types = insert_key_in_param_types(key, param_types, spanner.param_types.INT64)
         elif isinstance(value, TimestampStr):
             param_types = insert_key_in_param_types(key, param_types, spanner.param_types.TIMESTAMP)
+        elif isinstance(value, DateStr):
+            param_types = insert_key_in_param_types(key, param_types, spanner.param_types.DATE)
 
     return param_types
 
diff --git a/spanner/dbapi/types.py b/spanner/dbapi/types.py
--- a/spanner/dbapi/types.py
+++ b/spanner/dbapi/types.py
@@ -98,3 +98,13 @@ class TimestampStr(str):
     types that should be TIMESTAMP.
     """
     pass
+
+
+class DateStr(str):
+    """
+    DateStr is a sentinel type to help format Django dates as
+    compatible with Cloud Spanner's DATE type, but right before making
+    queries, it'll help differentiate between normal strings and the case of
+    types that should be DATE.
+    """
+    pass
diff --git a/spanner/django/operations.py b/spanner/django/operations.py
--- a/spanner/django/operations.py
+++ b/spanner/django/operations.py
@@ -3,7 +3,7 @@
 from django.conf import settings
 from django.db.backends.base.operations import BaseDatabaseOperations
 from django.utils import timezone
-from spanner.dbapi.parse_utils import TimestampStr
+from spanner.dbapi.parse_utils import DateStr, TimestampStr
 
 
 class DatabaseOperations(BaseDatabaseOperations):
@@ -41,6 +41,11 @@ def sql_flush(self, style, tables, sequences, allow_cascade=False):
         else:
             return []
 
+    def adapt_datefield_value(self, value):
+        if value is None:
+            return None
+        return DateStr(str(value))
+
     def adapt_datetimefield_value(self, value):
         if value is None:
             return None


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
@@ -17,7 +17,7 @@
 from google.cloud import spanner_v1 as spanner
 from spanner.dbapi.exceptions import Error
 from spanner.dbapi.parse_utils import (
-    STMT_DDL, STMT_NON_UPDATING, TimestampStr, classify_stmt,
+    STMT_DDL, STMT_NON_UPDATING, DateStr, TimestampStr, classify_stmt,
     ensure_where_clause, extract_connection_params, infer_param_types,
     parse_insert, parse_spanner_url, reINSTANCE_CONFIG,
     rows_for_insert_or_update, sql_pyformat_args_to_spanner,
@@ -319,24 +319,29 @@ def test_sql_pyformat_args_to_spanner_invalid(self):
     def test_infer_param_types(self):
         cases = [
             (
-                {'a1': 10, 'b1': '2005-08-30T01:01:01.000001Z'},
+                {'a1': 10, 'b1': '2005-08-30T01:01:01.000001Z', 'c1': '2019-12-05'},
                 None,
                 {'a1': spanner.param_types.INT64},
             ),
             (
-                {'a1': 10, 'b1': TimestampStr('2005-08-30T01:01:01.000001Z')},
+                {'a1': 10, 'b1': TimestampStr('2005-08-30T01:01:01.000001Z'), 'c1': '2019-12-05'},
                 None,
                 {'a1': spanner.param_types.INT64, 'b1': spanner.param_types.TIMESTAMP},
             ),
+            (
+                {'a1': 10, 'b1': '2005-08-30T01:01:01.000001Z', 'c1': DateStr('2019-12-05')},
+                None,
+                {'a1': spanner.param_types.INT64, 'c1': spanner.param_types.DATE},
+            ),
             (
                 {'a1': 10, 'b1': '2005-08-30T01:01:01.000001Z'},
                 {},
                 {'a1': spanner.param_types.INT64},
             ),
             (
-                {'a1': 10, 'b1': TimestampStr('2005-08-30T01:01:01.000001Z')},
+                {'a1': 10, 'b1': TimestampStr('2005-08-30T01:01:01.000001Z'), 'c1': DateStr('2005-08-30')},
                 {},
-                {'a1': spanner.param_types.INT64, 'b1': spanner.param_types.TIMESTAMP},
+                {'a1': spanner.param_types.INT64, 'b1': spanner.param_types.TIMESTAMP, 'c1': spanner.param_types.DATE},
             ),
             ({'a1': 10, 'b1': 'aaaaa08-30T01:01:01.000001Z'}, None, {'a1': spanner.param_types.INT64}),
             (