summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy/sql
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2022-01-04 14:04:15 -0500
committerMike Bayer <mike_mp@zzzcomputing.com>2022-01-04 16:40:35 -0500
commit63eeec396e5d180e11a342920e7c3e49be434eb1 (patch)
tree02e203f26828018189ef20d0ff73fcc70f61c386 /lib/sqlalchemy/sql
parent400bf1716f84821d63daaf0a988d725dfd55d6a0 (diff)
downloadsqlalchemy-63eeec396e5d180e11a342920e7c3e49be434eb1.tar.gz
implement python_impl to custom_op for basic ORM evaluator extensibility
Added new parameter :paramref:`_sql.Operators.op.python_impl`, available from :meth:`_sql.Operators.op` and also when using the :class:`_sql.Operators.custom_op` constructor directly, which allows an in-Python evaluation function to be provided along with the custom SQL operator. This evaluation function becomes the implementation used when the operator object is used given plain Python objects as operands on both sides, and in particular is compatible with the ``synchronize_session='evaluate'`` option used with :ref:`orm_expression_update_delete`. Fixes: #3162 Change-Id: If46ba6a0e303e2180a177ba418a8cafe9b42608e
Diffstat (limited to 'lib/sqlalchemy/sql')
-rw-r--r--lib/sqlalchemy/sql/operators.py70
1 files changed, 65 insertions, 5 deletions
diff --git a/lib/sqlalchemy/sql/operators.py b/lib/sqlalchemy/sql/operators.py
index 74eb73e46..8006d6145 100644
--- a/lib/sqlalchemy/sql/operators.py
+++ b/lib/sqlalchemy/sql/operators.py
@@ -31,6 +31,7 @@ from operator import rshift
from operator import sub
from operator import truediv
+from .. import exc
from .. import util
@@ -117,7 +118,12 @@ class Operators:
return self.operate(inv)
def op(
- self, opstring, precedence=0, is_comparison=False, return_type=None
+ self,
+ opstring,
+ precedence=0,
+ is_comparison=False,
+ return_type=None,
+ python_impl=None,
):
"""Produce a generic operator function.
@@ -164,6 +170,26 @@ class Operators:
:class:`.Boolean`, and those that do not will be of the same
type as the left-hand operand.
+ :param python_impl: an optional Python function that can evaluate
+ two Python values in the same way as this operator works when
+ run on the database server. Useful for in-Python SQL expression
+ evaluation functions, such as for ORM hybrid attributes, and the
+ ORM "evaluator" used to match objects in a session after a multi-row
+ update or delete.
+
+ e.g.::
+
+ >>> expr = column('x').op('+', python_impl=lambda a, b: a + b)('y')
+
+ The operator for the above expression will also work for non-SQL
+ left and right objects::
+
+ >>> expr.operator(5, 10)
+ 15
+
+ .. versionadded:: 2.0
+
+
.. seealso::
:ref:`types_operators`
@@ -171,14 +197,20 @@ class Operators:
:ref:`relationship_custom_operator`
"""
- operator = custom_op(opstring, precedence, is_comparison, return_type)
+ operator = custom_op(
+ opstring,
+ precedence,
+ is_comparison,
+ return_type,
+ python_impl=python_impl,
+ )
def against(other):
return operator(self, other)
return against
- def bool_op(self, opstring, precedence=0):
+ def bool_op(self, opstring, precedence=0, python_impl=None):
"""Return a custom boolean operator.
This method is shorthand for calling
@@ -191,7 +223,12 @@ class Operators:
:meth:`.Operators.op`
"""
- return self.op(opstring, precedence=precedence, is_comparison=True)
+ return self.op(
+ opstring,
+ precedence=precedence,
+ is_comparison=True,
+ python_impl=python_impl,
+ )
def operate(self, op, *other, **kwargs):
r"""Operate on an argument.
@@ -219,6 +256,8 @@ class Operators:
"""
raise NotImplementedError(str(op))
+ __sa_operate__ = operate
+
def reverse_operate(self, op, other, **kwargs):
"""Reverse operate on an argument.
@@ -256,6 +295,16 @@ class custom_op:
__name__ = "custom_op"
+ __slots__ = (
+ "opstring",
+ "precedence",
+ "is_comparison",
+ "natural_self_precedent",
+ "eager_grouping",
+ "return_type",
+ "python_impl",
+ )
+
def __init__(
self,
opstring,
@@ -264,6 +313,7 @@ class custom_op:
return_type=None,
natural_self_precedent=False,
eager_grouping=False,
+ python_impl=None,
):
self.opstring = opstring
self.precedence = precedence
@@ -273,6 +323,7 @@ class custom_op:
self.return_type = (
return_type._to_instance(return_type) if return_type else None
)
+ self.python_impl = python_impl
def __eq__(self, other):
return isinstance(other, custom_op) and other.opstring == self.opstring
@@ -281,7 +332,16 @@ class custom_op:
return id(self)
def __call__(self, left, right, **kw):
- return left.operate(self, right, **kw)
+ if hasattr(left, "__sa_operate__"):
+ return left.operate(self, right, **kw)
+ elif self.python_impl:
+ return self.python_impl(left, right, **kw)
+ else:
+ raise exc.InvalidRequestError(
+ f"Custom operator {self.opstring!r} can't be used with "
+ "plain Python objects unless it includes the "
+ "'python_impl' parameter."
+ )
class ColumnOperators(Operators):