evaluator.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. # orm/evaluator.py
  2. # Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
  3. # <see AUTHORS file>
  4. #
  5. # This module is part of SQLAlchemy and is released under
  6. # the MIT License: https://www.opensource.org/licenses/mit-license.php
  7. # mypy: ignore-errors
  8. """Evaluation functions used **INTERNALLY** by ORM DML use cases.
  9. This module is **private, for internal use by SQLAlchemy**.
  10. .. versionchanged:: 2.0.4 renamed ``EvaluatorCompiler`` to
  11. ``_EvaluatorCompiler``.
  12. """
  13. from __future__ import annotations
  14. from typing import Type
  15. from . import exc as orm_exc
  16. from .base import LoaderCallableStatus
  17. from .base import PassiveFlag
  18. from .. import exc
  19. from .. import inspect
  20. from ..sql import and_
  21. from ..sql import operators
  22. from ..sql.sqltypes import Integer
  23. from ..sql.sqltypes import Numeric
  24. from ..util import warn_deprecated
  25. class UnevaluatableError(exc.InvalidRequestError):
  26. pass
  27. class _NoObject(operators.ColumnOperators):
  28. def operate(self, *arg, **kw):
  29. return None
  30. def reverse_operate(self, *arg, **kw):
  31. return None
  32. class _ExpiredObject(operators.ColumnOperators):
  33. def operate(self, *arg, **kw):
  34. return self
  35. def reverse_operate(self, *arg, **kw):
  36. return self
  37. _NO_OBJECT = _NoObject()
  38. _EXPIRED_OBJECT = _ExpiredObject()
  39. class _EvaluatorCompiler:
  40. def __init__(self, target_cls=None):
  41. self.target_cls = target_cls
  42. def process(self, clause, *clauses):
  43. if clauses:
  44. clause = and_(clause, *clauses)
  45. meth = getattr(self, f"visit_{clause.__visit_name__}", None)
  46. if not meth:
  47. raise UnevaluatableError(
  48. f"Cannot evaluate {type(clause).__name__}"
  49. )
  50. return meth(clause)
  51. def visit_grouping(self, clause):
  52. return self.process(clause.element)
  53. def visit_null(self, clause):
  54. return lambda obj: None
  55. def visit_false(self, clause):
  56. return lambda obj: False
  57. def visit_true(self, clause):
  58. return lambda obj: True
  59. def visit_column(self, clause):
  60. try:
  61. parentmapper = clause._annotations["parentmapper"]
  62. except KeyError as ke:
  63. raise UnevaluatableError(
  64. f"Cannot evaluate column: {clause}"
  65. ) from ke
  66. if self.target_cls and not issubclass(
  67. self.target_cls, parentmapper.class_
  68. ):
  69. raise UnevaluatableError(
  70. "Can't evaluate criteria against "
  71. f"alternate class {parentmapper.class_}"
  72. )
  73. parentmapper._check_configure()
  74. # we'd like to use "proxy_key" annotation to get the "key", however
  75. # in relationship primaryjoin cases proxy_key is sometimes deannotated
  76. # and sometimes apparently not present in the first place (?).
  77. # While I can stop it from being deannotated (though need to see if
  78. # this breaks other things), not sure right now about cases where it's
  79. # not there in the first place. can fix at some later point.
  80. # key = clause._annotations["proxy_key"]
  81. # for now, use the old way
  82. try:
  83. key = parentmapper._columntoproperty[clause].key
  84. except orm_exc.UnmappedColumnError as err:
  85. raise UnevaluatableError(
  86. f"Cannot evaluate expression: {err}"
  87. ) from err
  88. # note this used to fall back to a simple `getattr(obj, key)` evaluator
  89. # if impl was None; as of #8656, we ensure mappers are configured
  90. # so that impl is available
  91. impl = parentmapper.class_manager[key].impl
  92. def get_corresponding_attr(obj):
  93. if obj is None:
  94. return _NO_OBJECT
  95. state = inspect(obj)
  96. dict_ = state.dict
  97. value = impl.get(
  98. state, dict_, passive=PassiveFlag.PASSIVE_NO_FETCH
  99. )
  100. if value is LoaderCallableStatus.PASSIVE_NO_RESULT:
  101. return _EXPIRED_OBJECT
  102. return value
  103. return get_corresponding_attr
  104. def visit_tuple(self, clause):
  105. return self.visit_clauselist(clause)
  106. def visit_expression_clauselist(self, clause):
  107. return self.visit_clauselist(clause)
  108. def visit_clauselist(self, clause):
  109. evaluators = [self.process(clause) for clause in clause.clauses]
  110. dispatch = (
  111. f"visit_{clause.operator.__name__.rstrip('_')}_clauselist_op"
  112. )
  113. meth = getattr(self, dispatch, None)
  114. if meth:
  115. return meth(clause.operator, evaluators, clause)
  116. else:
  117. raise UnevaluatableError(
  118. f"Cannot evaluate clauselist with operator {clause.operator}"
  119. )
  120. def visit_binary(self, clause):
  121. eval_left = self.process(clause.left)
  122. eval_right = self.process(clause.right)
  123. dispatch = f"visit_{clause.operator.__name__.rstrip('_')}_binary_op"
  124. meth = getattr(self, dispatch, None)
  125. if meth:
  126. return meth(clause.operator, eval_left, eval_right, clause)
  127. else:
  128. raise UnevaluatableError(
  129. f"Cannot evaluate {type(clause).__name__} with "
  130. f"operator {clause.operator}"
  131. )
  132. def visit_or_clauselist_op(self, operator, evaluators, clause):
  133. def evaluate(obj):
  134. has_null = False
  135. for sub_evaluate in evaluators:
  136. value = sub_evaluate(obj)
  137. if value is _EXPIRED_OBJECT:
  138. return _EXPIRED_OBJECT
  139. elif value:
  140. return True
  141. has_null = has_null or value is None
  142. if has_null:
  143. return None
  144. return False
  145. return evaluate
  146. def visit_and_clauselist_op(self, operator, evaluators, clause):
  147. def evaluate(obj):
  148. for sub_evaluate in evaluators:
  149. value = sub_evaluate(obj)
  150. if value is _EXPIRED_OBJECT:
  151. return _EXPIRED_OBJECT
  152. if not value:
  153. if value is None or value is _NO_OBJECT:
  154. return None
  155. return False
  156. return True
  157. return evaluate
  158. def visit_comma_op_clauselist_op(self, operator, evaluators, clause):
  159. def evaluate(obj):
  160. values = []
  161. for sub_evaluate in evaluators:
  162. value = sub_evaluate(obj)
  163. if value is _EXPIRED_OBJECT:
  164. return _EXPIRED_OBJECT
  165. elif value is None or value is _NO_OBJECT:
  166. return None
  167. values.append(value)
  168. return tuple(values)
  169. return evaluate
  170. def visit_custom_op_binary_op(
  171. self, operator, eval_left, eval_right, clause
  172. ):
  173. if operator.python_impl:
  174. return self._straight_evaluate(
  175. operator, eval_left, eval_right, clause
  176. )
  177. else:
  178. raise UnevaluatableError(
  179. f"Custom operator {operator.opstring!r} can't be evaluated "
  180. "in Python unless it specifies a callable using "
  181. "`.python_impl`."
  182. )
  183. def visit_is_binary_op(self, operator, eval_left, eval_right, clause):
  184. def evaluate(obj):
  185. left_val = eval_left(obj)
  186. right_val = eval_right(obj)
  187. if left_val is _EXPIRED_OBJECT or right_val is _EXPIRED_OBJECT:
  188. return _EXPIRED_OBJECT
  189. return left_val == right_val
  190. return evaluate
  191. def visit_is_not_binary_op(self, operator, eval_left, eval_right, clause):
  192. def evaluate(obj):
  193. left_val = eval_left(obj)
  194. right_val = eval_right(obj)
  195. if left_val is _EXPIRED_OBJECT or right_val is _EXPIRED_OBJECT:
  196. return _EXPIRED_OBJECT
  197. return left_val != right_val
  198. return evaluate
  199. def _straight_evaluate(self, operator, eval_left, eval_right, clause):
  200. def evaluate(obj):
  201. left_val = eval_left(obj)
  202. right_val = eval_right(obj)
  203. if left_val is _EXPIRED_OBJECT or right_val is _EXPIRED_OBJECT:
  204. return _EXPIRED_OBJECT
  205. elif left_val is None or right_val is None:
  206. return None
  207. return operator(eval_left(obj), eval_right(obj))
  208. return evaluate
  209. def _straight_evaluate_numeric_only(
  210. self, operator, eval_left, eval_right, clause
  211. ):
  212. if clause.left.type._type_affinity not in (
  213. Numeric,
  214. Integer,
  215. ) or clause.right.type._type_affinity not in (Numeric, Integer):
  216. raise UnevaluatableError(
  217. f'Cannot evaluate math operator "{operator.__name__}" for '
  218. f"datatypes {clause.left.type}, {clause.right.type}"
  219. )
  220. return self._straight_evaluate(operator, eval_left, eval_right, clause)
  221. visit_add_binary_op = _straight_evaluate_numeric_only
  222. visit_mul_binary_op = _straight_evaluate_numeric_only
  223. visit_sub_binary_op = _straight_evaluate_numeric_only
  224. visit_mod_binary_op = _straight_evaluate_numeric_only
  225. visit_truediv_binary_op = _straight_evaluate_numeric_only
  226. visit_lt_binary_op = _straight_evaluate
  227. visit_le_binary_op = _straight_evaluate
  228. visit_ne_binary_op = _straight_evaluate
  229. visit_gt_binary_op = _straight_evaluate
  230. visit_ge_binary_op = _straight_evaluate
  231. visit_eq_binary_op = _straight_evaluate
  232. def visit_in_op_binary_op(self, operator, eval_left, eval_right, clause):
  233. return self._straight_evaluate(
  234. lambda a, b: a in b if a is not _NO_OBJECT else None,
  235. eval_left,
  236. eval_right,
  237. clause,
  238. )
  239. def visit_not_in_op_binary_op(
  240. self, operator, eval_left, eval_right, clause
  241. ):
  242. return self._straight_evaluate(
  243. lambda a, b: a not in b if a is not _NO_OBJECT else None,
  244. eval_left,
  245. eval_right,
  246. clause,
  247. )
  248. def visit_concat_op_binary_op(
  249. self, operator, eval_left, eval_right, clause
  250. ):
  251. return self._straight_evaluate(
  252. lambda a, b: a + b, eval_left, eval_right, clause
  253. )
  254. def visit_startswith_op_binary_op(
  255. self, operator, eval_left, eval_right, clause
  256. ):
  257. return self._straight_evaluate(
  258. lambda a, b: a.startswith(b), eval_left, eval_right, clause
  259. )
  260. def visit_endswith_op_binary_op(
  261. self, operator, eval_left, eval_right, clause
  262. ):
  263. return self._straight_evaluate(
  264. lambda a, b: a.endswith(b), eval_left, eval_right, clause
  265. )
  266. def visit_unary(self, clause):
  267. eval_inner = self.process(clause.element)
  268. if clause.operator is operators.inv:
  269. def evaluate(obj):
  270. value = eval_inner(obj)
  271. if value is _EXPIRED_OBJECT:
  272. return _EXPIRED_OBJECT
  273. elif value is None:
  274. return None
  275. return not value
  276. return evaluate
  277. raise UnevaluatableError(
  278. f"Cannot evaluate {type(clause).__name__} "
  279. f"with operator {clause.operator}"
  280. )
  281. def visit_bindparam(self, clause):
  282. if clause.callable:
  283. val = clause.callable()
  284. else:
  285. val = clause.value
  286. return lambda obj: val
  287. def __getattr__(name: str) -> Type[_EvaluatorCompiler]:
  288. if name == "EvaluatorCompiler":
  289. warn_deprecated(
  290. "Direct use of 'EvaluatorCompiler' is not supported, and this "
  291. "name will be removed in a future release. "
  292. "'_EvaluatorCompiler' is for internal use only",
  293. "2.0",
  294. )
  295. return _EvaluatorCompiler
  296. else:
  297. raise AttributeError(f"module {__name__!r} has no attribute {name!r}")