Repository: googleapis/python-spanner-django

Issue:
django: power operator doesn't work
e.g. `Number.objects.filter(pk=self.n.pk).update(integer=F('integer') ** 2, float=F('float') ** 1.5)`

Traceback:
```
======================================================================
ERROR: test_lefthand_power (expressions.tests.ExpressionOperatorTests)
----------------------------------------------------------------------
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 = "Bitwise operator ^ requires two integer/BYTES arguments of the same type, but saw DOUBLE and DOUBLE [at 1:98]\n...expressions_number.the_integer ^ @a0), the_float = (expressions_number.the...\n                                                       ^"
	debug_error_string = "{"created":"@1584199412.671626668","description":"Error received from peer ipv4:172.217.6.202:443","file":"src/core/lib/surface/call.cc","file_line":1055,"grpc_message":"Bitwise operator ^ requires two integer/BYTES arguments of the same type, but saw DOUBLE and DOUBLE [at 1:98]\n...expressions_number.the_integer ^ @a0), the_float = (expressions_number.the...\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-django/spanner/dbapi/autocommit_on_cursor.py", line 95, in execute
    self.__handle_update(sql, args or None)
  File "/home/tim/code/spanner-django/spanner/dbapi/autocommit_on_cursor.py", line 106, in __handle_update
    sql, params,
  File "/home/tim/code/spanner-django/spanner/dbapi/autocommit_on_connection.py", line 74, 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-django/spanner/dbapi/autocommit_on_cursor.py", line 113, 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 Bitwise operator ^ requires two integer/BYTES arguments of the same type, but saw DOUBLE and DOUBLE [at 1:98]\n...expressions_number.the_integer ^ @a0), the_float = (expressions_number.the...\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-django/spanner/dbapi/autocommit_on_cursor.py", line 99, in execute
    raise ProgrammingError(e.details if hasattr(e, 'details') else e)
spanner.dbapi.exceptions.ProgrammingError: 400 Bitwise operator ^ requires two integer/BYTES arguments of the same type, but saw DOUBLE and DOUBLE [at 1:98]\n...expressions_number.the_integer ^ @a0), the_float = (expressions_number.the...\n                                                       ^

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/tim/code/django/tests/expressions/tests.py", line 1014, in test_lefthand_power
    Number.objects.filter(pk=self.n.pk).update(integer=F('integer') ** 2, float=F('float') ** 1.5)
  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 1469, in execute_sql
    cursor = super().execute_sql(result_type)
  File "/home/tim/code/django/django/db/models/sql/compiler.py", line 1140, 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-django/spanner/dbapi/autocommit_on_cursor.py", line 99, in execute
    raise ProgrammingError(e.details if hasattr(e, 'details') else e)
django.db.utils.ProgrammingError: 400 Bitwise operator ^ requires two integer/BYTES arguments of the same type, but saw DOUBLE and DOUBLE [at 1:98]\n...expressions_number.the_integer ^ @a0), the_float = (expressions_number.the...\n                                                       ^
```


Discussion:

Resolving patch:
diff --git a/django_spanner/features.py b/django_spanner/features.py
--- a/django_spanner/features.py
+++ b/django_spanner/features.py
@@ -257,8 +257,9 @@ class DatabaseFeatures(BaseDatabaseFeatures):
         # https://github.com/orijtech/django-spanner/issues/331
         'expressions.tests.ExpressionOperatorTests.test_lefthand_division',
         'expressions.tests.ExpressionOperatorTests.test_right_hand_division',
-        # power operator doesn't work:
-        # https://github.com/orijtech/django-spanner/issues/333
+        # power operator produces a float result, which can't be assigned to
+        # an integer column:
+        # https://github.com/orijtech/django-spanner/issues/331
         'expressions.tests.ExpressionOperatorTests.test_lefthand_power',
         'expressions.tests.ExpressionOperatorTests.test_righthand_power',
         # Cloud Spanner's docs: "The rows that are returned by LIMIT and OFFSET
diff --git a/django_spanner/operations.py b/django_spanner/operations.py
--- a/django_spanner/operations.py
+++ b/django_spanner/operations.py
@@ -214,6 +214,8 @@ def format_for_duration_arithmetic(self, sql):
     def combine_expression(self, connector, sub_expressions):
         if connector == '%%':
             return 'MOD(%s)' % ', '.join(sub_expressions)
+        elif connector == '^':
+            return 'POWER(%s)' % ', '.join(sub_expressions)
         return super().combine_expression(connector, sub_expressions)
 
     def combine_duration_expression(self, connector, sub_expressions):


Test patch: