Repository: googleapis/python-spanner-django

Issue:
django: `AttributeError: 'tuple' object has no attribute 'items'` on trying to login
When I go to login, I get

```shell
[19/Nov/2019 04:20:14] "GET /admin/login/?next=/admin/ HTTP/1.1" 200 1819
Internal Server Error: /admin/login/
Traceback (most recent call last):
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/django/core/handlers/exception.py", line 34, in inner
    response = get_response(request)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/django/core/handlers/base.py", line 115, in _get_response
    response = self.process_exception_by_middleware(e, request)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/django/core/handlers/base.py", line 113, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/django/views/decorators/cache.py", line 44, in _wrapped_view_func
    response = view_func(request, *args, **kwargs)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/django/contrib/admin/sites.py", line 399, in login
    return LoginView.as_view(**defaults)(request)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/django/views/generic/base.py", line 71, in view
    return self.dispatch(request, *args, **kwargs)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/django/utils/decorators.py", line 45, in _wrapper
    return bound_method(*args, **kwargs)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/django/views/decorators/debug.py", line 76, in sensitive_post_parameters_wrapper
    return view(request, *args, **kwargs)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/django/utils/decorators.py", line 45, in _wrapper
    return bound_method(*args, **kwargs)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/django/utils/decorators.py", line 142, in _wrapped_view
    response = view_func(request, *args, **kwargs)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/django/utils/decorators.py", line 45, in _wrapper
    return bound_method(*args, **kwargs)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/django/views/decorators/cache.py", line 44, in _wrapped_view_func
    response = view_func(request, *args, **kwargs)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/django/contrib/auth/views.py", line 61, in dispatch
    return super().dispatch(request, *args, **kwargs)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/django/views/generic/base.py", line 97, in dispatch
    return handler(request, *args, **kwargs)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/django/views/generic/edit.py", line 142, in post
    return self.form_valid(form)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/django/contrib/auth/views.py", line 90, in form_valid
    auth_login(self.request, form.get_user())
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/django/contrib/auth/__init__.py", line 132, in login
    user_logged_in.send(sender=user.__class__, request=request, user=user)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/django/dispatch/dispatcher.py", line 175, in send
    for receiver in self._live_receivers(sender)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/django/dispatch/dispatcher.py", line 175, in <listcomp>
    for receiver in self._live_receivers(sender)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/django/contrib/auth/models.py", line 20, in update_last_login
    user.save(update_fields=['last_login'])
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/django/contrib/auth/base_user.py", line 66, in save
    super().save(*args, **kwargs)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/django/db/models/base.py", line 741, in save
    force_update=force_update, update_fields=update_fields)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/django/db/models/base.py", line 779, in save_base
    force_update, using, update_fields,
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/django/db/models/base.py", line 851, in _save_table
    forced_update)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/django/db/models/base.py", line 900, in _do_update
    return filtered._update(values) > 0
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/django/db/models/query.py", line 760, in _update
    return query.get_compiler(self.db).execute_sql(CURSOR)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/django/db/models/sql/compiler.py", line 1429, in execute_sql
    cursor = super().execute_sql(result_type)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/django/db/models/sql/compiler.py", line 1100, in execute_sql
    cursor.execute(sql, params)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/django/db/backends/utils.py", line 99, in execute
    return super().execute(sql, params)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/django/db/backends/utils.py", line 67, in execute
    return self._execute_with_wrappers(sql, params, many=False, executor=self._execute)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/django/db/backends/utils.py", line 76, in _execute_with_wrappers
    return executor(sql, params, many, context)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/django/db/backends/utils.py", line 84, in _execute
    return self.cursor.execute(sql, params)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/spanner/dbapi/cursor.py", line 98, in execute
    param_types=param_types,
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/google/cloud/spanner_v1/session.py", line 299, in run_in_transaction
    return_value = func(txn, *args, **kw)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/spanner/dbapi/cursor.py", line 111, in __do_execute_update
    res = transaction.execute_update(sql, *args, **kwargs)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/google/cloud/spanner_v1/transaction.py", line 188, in execute_update
    params_pb = self._make_params_pb(params, param_types)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/google/cloud/spanner_v1/transaction.py", line 157, in _make_params_pb
    fields={key: _make_value_pb(value) for key, value in params.items()}
AttributeError: 'tuple' object has no attribute 'items'
[19/Nov/2019 04:20:32] "POST /admin/login/?next=/admin/ HTTP/1.1" 500 238450
```


Discussion:
I've diagnosed the cause of this issue. We need to also invoke the same logic as goes into `parse_insert` (in PR https://github.com/orijtech/spanner-orm/pull/45) because
```python
SQL: UPDATE auth_user SET last_login = %s WHERE auth_user.id = %s
Args: (('2019-11-19 17:26:14.478324+00:00', 4297733461848466419),)
Kwargs: {'param_types': {}}
```
is the statement, but really it should be rewritten to
```python
res = transaction.insert_or_update(
    table='auth_user',
    columns=['last_login', 'auth_user_id'],
    values=[
        ['2019-11-19 17:26:14.478324+00:00', 4297733461848466419]
    ])
```
Actually what it should be is using `sql_pyformat_args_to_spanner`

to Transform:

### Original
```
SQL: UPDATE auth_user SET last_login = '2019-12-03 18:09:15.052992+00:00' WHERE 
auth_user.id = 4297733461848466419
```
```
SQL: UPDATE django_session SET
session_data = 'YTdjMTUxZGFmMzk0MTQ3ZjgyMmVkODQxYTc3ODZjZjcwNGQyYzhhYjp7Il9hdXRo
X3VzZXJfaWQiOiI0Mjk3NzMzNDYxODQ4NDY2NDE5IiwiX2F1dGhfdXNlcl9iYWNrZW
5kIjoiZGphbmdvLmNvbnRyaWIuYXV0aC5iYWNrZW5kcy5Nb2RlbEJhY2tlbmQiLCJfY
XV0aF91c2VyX2hhc2giOiI3ZmRiNmRmM2M5ZDI5NjI1NzcxODg4NTY4ZWRlM2U1OWI0ODhlMmYzIn0=',
expire_date = '2019-12-03 18:09:15.052992+00:00'
WHERE django_session.session_key = 'biy906fz448gny147pndiqape8sgpwtj'
```

### Transformed into
```
SQL: UPDATE auth_user SET last_login = @a0 WHERE auth_user.id = @a1
Args: {'a0': '2019-11-19 18:09:14.267085+00:00', 'a1': 4297733461848466419}
Kwargs: {'param_types': {}}
```
```
SQL: UPDATE django_session SET session_data = @a0, expire_date = @a1
WHERE django_session.session_key = @a2
Args: {'a0': 'YTdjMTUxZGFmMzk0MTQ3ZjgyMmVkODQxYTc3ODZjZjcwNGQyYzhhYjp7Il9hdXRoX3VzZXJfaW
QiOiI0Mjk3NzMzNDYxODQ4NDY2NDE5IiwiX2F1dGhfdXNlcl9iYWNrZW5kIjoiZGphbmdvLmNvbnR
yaWIuYXV0aC5iYWNrZW5kcy5Nb2RlbEJhY2tlbmQiLCJfYXV0aF91c2VyX2hhc2giOiI3ZmRiNmRm
M2M5ZDI5NjI1NzcxODg4NTY4ZWRlM2U1OWI0ODhlMmYzIn0=',
'a1': '2019-12-03 18:09:15.052992+00:00', 'a2': 'biy906fz448gny147pndiqape8sgpwtj'}
Kwargs: {'param_types': {}}
```

Resolving patch:
diff --git a/spanner/dbapi/cursor.py b/spanner/dbapi/cursor.py
--- a/spanner/dbapi/cursor.py
+++ b/spanner/dbapi/cursor.py
@@ -107,8 +107,9 @@ def execute(self, sql, args=None):
         except grpc_exceptions.InternalServerError as e:
             raise OperationalError(e.details if hasattr(e, 'details') else e)
 
-    def __do_execute_update(self, transaction, sql, *args, **kwargs):
-        res = transaction.execute_update(sql, *args, **kwargs)
+    def __do_execute_update(self, transaction, sql, params, **kwargs):
+        sql, params = sql_pyformat_args_to_spanner(sql, params)
+        res = transaction.execute_update(sql, params=params, **kwargs)
         self.__itr = None
         if type(res) == int:
             self.__row_count = res


Test patch: