summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2018-10-23 19:38:46 -0400
committerMike Bayer <mike_mp@zzzcomputing.com>2018-10-26 16:34:33 -0400
commit90574aabef36fd59841b7df7d8ac30e2030e9854 (patch)
treeae94a1693474a4bbbdde26b9262fac14e8ebfbf1 /lib/sqlalchemy
parentac358a04a7b077602ac668c19c3c40389d9e77e4 (diff)
downloadsqlalchemy-90574aabef36fd59841b7df7d8ac30e2030e9854.tar.gz
Create object- and column-oriented versions of AssociationProxyInstance
The :class:`.AssociationProxy` now has standard column comparison operations such as :meth:`.ColumnOperators.like` and :meth:`.ColumnOperators.startswith` available when the target attribute is a plain column - the EXISTS expression that joins to the target table is rendered as usual, but the column expression is then use within the WHERE criteria of the EXISTS. Note that this alters the behavior of the ``.contains()`` method on the association proxy to make use of :meth:`.ColumnOperators.contains` when used on a column-based attribute. Fixes: #4351 Change-Id: I310941f4e8f778c200f8144a26a89e5364cd4dfb
Diffstat (limited to 'lib/sqlalchemy')
-rw-r--r--lib/sqlalchemy/ext/associationproxy.py100
1 files changed, 81 insertions, 19 deletions
diff --git a/lib/sqlalchemy/ext/associationproxy.py b/lib/sqlalchemy/ext/associationproxy.py
index 1c28b10a1..629b4ac64 100644
--- a/lib/sqlalchemy/ext/associationproxy.py
+++ b/lib/sqlalchemy/ext/associationproxy.py
@@ -17,6 +17,7 @@ import operator
from .. import exc, orm, util
from ..orm import collections, interfaces
from ..sql import or_
+from ..sql.operators import ColumnOperators
from .. import inspect
@@ -217,7 +218,7 @@ class AssociationProxy(interfaces.InspectionAttrInfo):
except KeyError:
owner = self._calc_owner(class_)
if owner is not None:
- result = AssociationProxyInstance(self, owner)
+ result = AssociationProxyInstance.for_proxy(self, owner)
setattr(class_, self.key + "_inst", result)
return result
else:
@@ -283,13 +284,49 @@ class AssociationProxyInstance(object):
"""
- def __init__(self, parent, owning_class):
+ def __init__(self, parent, owning_class, target_class, value_attr):
self.parent = parent
self.key = parent.key
self.owning_class = owning_class
self.target_collection = parent.target_collection
self.value_attr = parent.value_attr
self.collection_class = None
+ self.target_class = target_class
+ self.value_attr = value_attr
+
+ target_class = None
+ """The intermediary class handled by this
+ :class:`.AssociationProxyInstance`.
+
+ Intercepted append/set/assignment events will result
+ in the generation of new instances of this class.
+
+ """
+
+ @classmethod
+ def for_proxy(cls, parent, owning_class):
+ target_collection = parent.target_collection
+ value_attr = parent.value_attr
+ prop = orm.class_mapper(owning_class).\
+ get_property(target_collection)
+ target_class = prop.mapper.class_
+
+ target_assoc = cls._cls_unwrap_target_assoc_proxy(
+ target_class, value_attr)
+ if target_assoc is not None:
+ return ObjectAssociationProxyInstance(
+ parent, owning_class, target_class, value_attr
+ )
+
+ is_object = getattr(target_class, value_attr).impl.uses_objects
+ if is_object:
+ return ObjectAssociationProxyInstance(
+ parent, owning_class, target_class, value_attr
+ )
+ else:
+ return ColumnAssociationProxyInstance(
+ parent, owning_class, target_class, value_attr
+ )
def _get_property(self):
return orm.class_mapper(self.owning_class).\
@@ -299,13 +336,18 @@ class AssociationProxyInstance(object):
def _comparator(self):
return self._get_property().comparator
- @util.memoized_property
- def _unwrap_target_assoc_proxy(self):
- attr = getattr(self.target_class, self.value_attr)
+ @classmethod
+ def _cls_unwrap_target_assoc_proxy(cls, target_class, value_attr):
+ attr = getattr(target_class, value_attr)
if isinstance(attr, (AssociationProxy, AssociationProxyInstance)):
return attr
return None
+ @util.memoized_property
+ def _unwrap_target_assoc_proxy(self):
+ return self._cls_unwrap_target_assoc_proxy(
+ self.target_class, self.value_attr)
+
@property
def remote_attr(self):
"""The 'remote' :class:`.MapperProperty` referenced by this
@@ -353,17 +395,6 @@ class AssociationProxyInstance(object):
return (self.local_attr, self.remote_attr)
@util.memoized_property
- def target_class(self):
- """The intermediary class handled by this
- :class:`.AssociationProxyInstance`.
-
- Intercepted append/set/assignment events will result
- in the generation of new instances of this class.
-
- """
- return self._get_property().mapper.class_
-
- @util.memoized_property
def scalar(self):
"""Return ``True`` if this :class:`.AssociationProxyInstance`
proxies a scalar relationship on the local side."""
@@ -378,9 +409,9 @@ class AssociationProxyInstance(object):
return not self._get_property().\
mapper.get_property(self.value_attr).uselist
- @util.memoized_property
+ @property
def _target_is_object(self):
- return getattr(self.target_class, self.value_attr).impl.uses_objects
+ raise NotImplementedError()
def _initialize_scalar_accessors(self):
if self.parent.getset_factory:
@@ -587,6 +618,12 @@ class AssociationProxyInstance(object):
return self._criterion_exists(
criterion=criterion, is_has=True, **kwargs)
+
+class ObjectAssociationProxyInstance(AssociationProxyInstance):
+ """an :class:`.AssociationProxyInstance` that has an object as a target.
+ """
+ _target_is_object = True
+
def contains(self, obj):
"""Produce a proxied 'contains' expression using EXISTS.
@@ -611,7 +648,7 @@ class AssociationProxyInstance(object):
elif self._target_is_object and self.scalar and \
self._value_is_scalar:
raise exc.InvalidRequestError(
- "contains() doesn't apply to a scalar endpoint; use ==")
+ "contains() doesn't apply to a scalar object endpoint; use ==")
else:
return self._comparator._criterion_exists(**{self.value_attr: obj})
@@ -634,6 +671,31 @@ class AssociationProxyInstance(object):
getattr(self.target_class, self.value_attr) != obj)
+class ColumnAssociationProxyInstance(
+ ColumnOperators, AssociationProxyInstance):
+ """an :class:`.AssociationProxyInstance` that has a database column as a
+ target.
+ """
+ _target_is_object = False
+
+ def __eq__(self, other):
+ # special case "is None" to check for no related row as well
+ expr = self._criterion_exists(
+ self.remote_attr.operate(operator.eq, other)
+ )
+ if other is None:
+ return or_(
+ expr, self._comparator == None
+ )
+ else:
+ return expr
+
+ def operate(self, op, *other, **kwargs):
+ return self._criterion_exists(
+ self.remote_attr.operate(op, *other, **kwargs)
+ )
+
+
class _lazy_collection(object):
def __init__(self, obj, target):
self.parent = obj