Repository: googleapis/python-spanner-django

Issue:
django: Case(When(..., then=binary)) crashes
e.g.
```python
CaseTestModel.objects.update(
    binary=Case(
        When(integer=1, then=Value(b'one', output_field=models.BinaryField())),
        When(integer=2, then=Value(b'two', output_field=models.BinaryField())),
        default=Value(b'', output_field=models.BinaryField()),
    ),
)
```
```shell
======================================================================
ERROR: test_update_binary (expressions_case.tests.CaseExpressionTests)
----------------------------------------------------------------------
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 = "Value of type INT64 cannot be assigned to binary, which has type BYTES [at 1:52]\nUPDATE expressions_case_casetestmodel SET binary = CASE WHEN (expressions_cas...\n                                                   ^"
	debug_error_string = "{"created":"@1577632634.714942491","description":"Error received from peer ipv4:172.217.6.202:443","file":"src/core/lib/surface/call.cc","file_line":1055,"grpc_message":"Value of type INT64 cannot be assigned to binary, which has type BYTES [at 1:52]\nUPDATE expressions_case_casetestmodel SET binary = CASE WHEN (expressions_cas...\n                                                   ^","grpc_status":3}"
>

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/tim/code/spanner-orm/spanner/dbapi/cursor.py", line 95, in execute
    self.__handle_update(sql, args or None)
  File "/home/tim/code/spanner-orm/spanner/dbapi/cursor.py", line 107, in __handle_update
    sql, params,
  File "/home/tim/code/spanner-orm/spanner/dbapi/connection.py", line 91, in in_transaction
    return self.__dbhandle.run_in_transaction(fn, *args, **kwargs)
  File "/home/tim/.virtualenvs/django37/lib/python3.7/site-packages/google/cloud/spanner_v1/database.py", line 443, in run_in_transaction
    return session.run_in_transaction(func, *args, **kw)
  File "/home/tim/.virtualenvs/django37/lib/python3.7/site-packages/google/cloud/spanner_v1/session.py", line 299, in run_in_transaction
    return_value = func(txn, *args, **kw)
  File "/home/tim/code/spanner-orm/spanner/dbapi/cursor.py", line 114, in __do_execute_update
    res = transaction.execute_update(sql, params=params, param_types=get_param_types(params))
  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 812, 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 Value of type INT64 cannot be assigned to binary, which has type BYTES [at 1:52]\nUPDATE expressions_case_casetestmodel SET binary = CASE WHEN (expressions_cas...\n                                                   ^

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  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 99, in execute
    raise ProgrammingError(e.details if hasattr(e, 'details') else e)
spanner.dbapi.exceptions.ProgrammingError: 400 Value of type INT64 cannot be assigned to binary, which has type BYTES [at 1:52]\nUPDATE expressions_case_casetestmodel SET binary = CASE WHEN (expressions_cas...\n                                                   ^

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/tim/code/django/tests/expressions_case/tests.py", line 636, in test_update_binary
    default=Value(b'', output_field=models.BinaryField()),
  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 1133, 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/django/django/db/utils.py", line 89, in __exit__
    raise dj_exc_value.with_traceback(traceback) from exc_value
  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 99, in execute
    raise ProgrammingError(e.details if hasattr(e, 'details') else e)
django.db.utils.ProgrammingError: 400 Value of type INT64 cannot be assigned to binary, which has type BYTES [at 1:52]\nUPDATE expressions_case_casetestmodel SET binary = CASE WHEN (expressions_cas...\n 
```


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
@@ -378,6 +378,8 @@ def get_param_types(params):
             param_types[key] = spanner.param_types.DATE
         elif isinstance(value, str):
             param_types[key] = spanner.param_types.STRING
+        elif isinstance(value, bytes):
+            param_types[key] = spanner.param_types.BYTES
     return param_types
 
 


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
@@ -345,6 +345,7 @@ def test_get_param_types(self):
                 {'a1': 10, 'b1': TimestampStr('2019-11-26T02:55:41.000000Z')},
                 {'a1': param_types.INT64, 'b1': param_types.TIMESTAMP},
             ),
+            ({'a1': b'bytes'}, {'a1': param_types.BYTES}),
             ({'a1': 10, 'b1': None}, {'a1': param_types.INT64}),
             (None, None),
         ]