summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2013-06-08 13:23:15 -0400
committerMike Bayer <mike_mp@zzzcomputing.com>2013-06-08 13:23:15 -0400
commit20d1e9c3fa8ccc99207988e27d89e18b1870bc2c (patch)
tree8e1584a5048fbbd6931118785890e5043cc9d5a8
parentd5363fca5400f6c4969c2756fcfcdae6b9703091 (diff)
downloadsqlalchemy-20d1e9c3fa8ccc99207988e27d89e18b1870bc2c.tar.gz
Added additional criterion to the ==, != comparators, used with
scalar values, for comparisons to None to also take into account the association record itself being non-present, in addition to the existing test for the scalar endpoint on the association record being NULL. Previously, comparing ``Cls.scalar == None`` would return records for which ``Cls.associated`` were present and ``Cls.associated.scalar`` is None, but not rows for which ``Cls.associated`` is non-present. More significantly, the inverse operation ``Cls.scalar != None`` *would* return ``Cls`` rows for which ``Cls.associated`` was non-present. Additionally, added a special use case where you can call ``Cls.scalar.has()`` with no arguments, when ``Cls.scalar`` is a column-based value - this returns whether or not ``Cls.associated`` has any rows present, regardless of whether or not ``Cls.associated.scalar`` is NULL or not. [ticket:2751]
-rw-r--r--doc/build/changelog/changelog_09.rst21
-rw-r--r--lib/sqlalchemy/ext/associationproxy.py30
-rw-r--r--test/ext/test_associationproxy.py168
3 files changed, 195 insertions, 24 deletions
diff --git a/doc/build/changelog/changelog_09.rst b/doc/build/changelog/changelog_09.rst
index 9d4bbfea1..0ae1a2322 100644
--- a/doc/build/changelog/changelog_09.rst
+++ b/doc/build/changelog/changelog_09.rst
@@ -7,6 +7,27 @@
:version: 0.9.0
.. change::
+ :tags: bug, ext, associationproxy
+ :tickets: 2751
+
+ Added additional criterion to the ==, != comparators, used with
+ scalar values, for comparisons to None to also take into account
+ the association record itself being non-present, in addition to the
+ existing test for the scalar endpoint on the association record
+ being NULL. Previously, comparing ``Cls.scalar == None`` would return
+ records for which ``Cls.associated`` were present and
+ ``Cls.associated.scalar`` is None, but not rows for which
+ ``Cls.associated`` is non-present. More significantly, the
+ inverse operation ``Cls.scalar != None`` *would* return ``Cls``
+ rows for which ``Cls.associated`` was non-present.
+
+ Additionally, added a special use case where you
+ can call ``Cls.scalar.has()`` with no arguments,
+ when ``Cls.scalar`` is a column-based value - this returns whether or
+ not ``Cls.associated`` has any rows present, regardless of whether
+ or not ``Cls.associated.scalar`` is NULL or not.
+
+ .. change::
:tags: bug, orm
:tickets: 2369
diff --git a/lib/sqlalchemy/ext/associationproxy.py b/lib/sqlalchemy/ext/associationproxy.py
index 0482a9205..fca2f0008 100644
--- a/lib/sqlalchemy/ext/associationproxy.py
+++ b/lib/sqlalchemy/ext/associationproxy.py
@@ -17,7 +17,7 @@ import operator
import weakref
from .. import exc, orm, util
from ..orm import collections, interfaces
-from ..sql import not_
+from ..sql import not_, or_
def association_proxy(target_collection, attr, **kw):
@@ -231,6 +231,10 @@ class AssociationProxy(interfaces._InspectionAttr):
return not self._get_property().\
mapper.get_property(self.value_attr).uselist
+ @util.memoized_property
+ def _target_is_object(self):
+ return getattr(self.target_class, self.value_attr).impl.uses_objects
+
def __get__(self, obj, class_):
if self.owning_class is None:
self.owning_class = class_ and class_ or type(obj)
@@ -388,10 +392,17 @@ class AssociationProxy(interfaces._InspectionAttr):
"""
- return self._comparator.has(
+ if self._target_is_object:
+ return self._comparator.has(
getattr(self.target_class, self.value_attr).\
has(criterion, **kwargs)
)
+ else:
+ if criterion is not None or kwargs:
+ raise exc.ArgumentError(
+ "Non-empty has() not allowed for "
+ "column-targeted association proxy; use ==")
+ return self._comparator.has()
def contains(self, obj):
"""Produce a proxied 'contains' expression using EXISTS.
@@ -411,10 +422,21 @@ class AssociationProxy(interfaces._InspectionAttr):
return self._comparator.any(**{self.value_attr: obj})
def __eq__(self, obj):
- return self._comparator.has(**{self.value_attr: obj})
+ # note the has() here will fail for collections; eq_()
+ # is only allowed with a scalar.
+ if obj is None:
+ return or_(
+ self._comparator.has(**{self.value_attr: obj}),
+ self._comparator == None
+ )
+ else:
+ return self._comparator.has(**{self.value_attr: obj})
def __ne__(self, obj):
- return not_(self.__eq__(obj))
+ # note the has() here will fail for collections; eq_()
+ # is only allowed with a scalar.
+ return self._comparator.has(
+ getattr(self.target_class, self.value_attr) != obj)
class _lazy_collection(object):
diff --git a/test/ext/test_associationproxy.py b/test/ext/test_associationproxy.py
index a5fcc45cc..724f1b215 100644
--- a/test/ext/test_associationproxy.py
+++ b/test/ext/test_associationproxy.py
@@ -1042,6 +1042,7 @@ class ComparatorTest(fixtures.MappedTest, AssertsCompiledSQL):
Table('singular', metadata,
Column('id', Integer,
primary_key=True, test_needs_autoincrement=True),
+ Column('value', String(50))
)
@classmethod
@@ -1059,6 +1060,10 @@ class ComparatorTest(fixtures.MappedTest, AssertsCompiledSQL):
# nonuselist -> uselist
singular_keywords = association_proxy('singular', 'keywords')
+ # m2o -> scalar
+ # nonuselist
+ singular_value = association_proxy('singular', 'value')
+
class Keyword(cls.Comparable):
def __init__(self, keyword):
self.keyword = keyword
@@ -1116,17 +1121,26 @@ class ComparatorTest(fixtures.MappedTest, AssertsCompiledSQL):
'fox', 'jumped', 'over',
'the', 'lazy',
)
- for ii in range(4):
+ for ii in range(16):
user = User('user%d' % ii)
- user.singular = Singular()
+
+ if ii % 2 == 0:
+ user.singular = Singular(value=("singular%d" % ii)
+ if ii % 4 == 0 else None)
session.add(user)
- for jj in words[ii:ii + 3]:
+ for jj in words[(ii % len(words)):((ii + 3) % len(words))]:
k = Keyword(jj)
user.keywords.append(k)
- user.singular.keywords.append(k)
+ if ii % 3 == None:
+ user.singular.keywords.append(k)
+
orphan = Keyword('orphan')
orphan.user_keyword = UserKeyword(keyword=orphan, user=None)
session.add(orphan)
+
+ keyword_with_nothing = Keyword('kwnothing')
+ session.add(keyword_with_nothing)
+
session.commit()
cls.u = user
cls.kw = user.keywords[0]
@@ -1190,12 +1204,10 @@ class ComparatorTest(fixtures.MappedTest, AssertsCompiledSQL):
self.classes.Keyword)
self._equivalent(self.session.query(Keyword).
- filter(Keyword.user.has(User.name
- == 'user2')),
+ filter(Keyword.user.has(User.name == 'user2')),
self.session.query(Keyword).
filter(Keyword.user_keyword.has(
- UserKeyword.user.has(User.name
- == 'user2'))))
+ UserKeyword.user.has(User.name == 'user2'))))
def test_filter_any_criterion_nul_ul(self):
User, Keyword, Singular = (self.classes.User,
@@ -1203,12 +1215,13 @@ class ComparatorTest(fixtures.MappedTest, AssertsCompiledSQL):
self.classes.Singular)
self._equivalent(
- self.session.query(User).\
- filter(User.singular_keywords.any(Keyword.keyword=='jumped')),
- self.session.query(User).\
+ self.session.query(User).
+ filter(User.singular_keywords.any(
+ Keyword.keyword == 'jumped')),
+ self.session.query(User).
filter(
User.singular.has(
- Singular.keywords.any(Keyword.keyword=='jumped')
+ Singular.keywords.any(Keyword.keyword == 'jumped')
)
)
)
@@ -1246,19 +1259,134 @@ class ComparatorTest(fixtures.MappedTest, AssertsCompiledSQL):
def test_filter_ne_nul_nul(self):
Keyword = self.classes.Keyword
- self._equivalent(self.session.query(Keyword).filter(Keyword.user
- != self.u),
+ self._equivalent(self.session.query(Keyword).filter(Keyword.user != self.u),
self.session.query(Keyword).
- filter(not_(Keyword.user_keyword.has(user=self.u))))
+ filter(
+ Keyword.user_keyword.has(Keyword.user != self.u)
+ )
+ )
def test_filter_eq_null_nul_nul(self):
UserKeyword, Keyword = self.classes.UserKeyword, self.classes.Keyword
- self._equivalent(self.session.query(Keyword).filter(Keyword.user
- == None),
- self.session.query(Keyword).
- filter(Keyword.user_keyword.has(UserKeyword.user
- == None)))
+ self._equivalent(
+ self.session.query(Keyword).filter(Keyword.user == None),
+ self.session.query(Keyword).
+ filter(
+ or_(
+ Keyword.user_keyword.has(UserKeyword.user == None),
+ Keyword.user_keyword == None
+ )
+
+ )
+ )
+
+ def test_filter_ne_null_nul_nul(self):
+ UserKeyword, Keyword = self.classes.UserKeyword, self.classes.Keyword
+
+ self._equivalent(
+ self.session.query(Keyword).filter(Keyword.user != None),
+ self.session.query(Keyword).
+ filter(
+ Keyword.user_keyword.has(UserKeyword.user != None),
+ )
+ )
+
+ def test_filter_eq_None_nul(self):
+ User = self.classes.User
+ Singular = self.classes.Singular
+
+ self._equivalent(
+ self.session.query(User).filter(User.singular_value == None),
+ self.session.query(User).filter(
+ or_(
+ User.singular.has(Singular.value==None),
+ User.singular == None
+ )
+ )
+ )
+
+ def test_filter_eq_value_nul(self):
+ User = self.classes.User
+ Singular = self.classes.Singular
+
+ self._equivalent(
+ self.session.query(User).filter(User.singular_value == "singular4"),
+ self.session.query(User).filter(
+ User.singular.has(Singular.value=="singular4"),
+ )
+ )
+
+ def test_filter_ne_None_nul(self):
+ User = self.classes.User
+ Singular = self.classes.Singular
+
+ self._equivalent(
+ self.session.query(User).filter(User.singular_value != None),
+ self.session.query(User).filter(
+ User.singular.has(Singular.value != None),
+ )
+ )
+
+ def test_has_nul(self):
+ # a special case where we provide an empty has() on a
+ # non-object-targeted association proxy.
+ User = self.classes.User
+ Singular = self.classes.Singular
+
+ self._equivalent(
+ self.session.query(User).filter(User.singular_value.has()),
+ self.session.query(User).filter(
+ User.singular.has(),
+ )
+ )
+
+ def test_nothas_nul(self):
+ # a special case where we provide an empty has() on a
+ # non-object-targeted association proxy.
+ User = self.classes.User
+ Singular = self.classes.Singular
+
+ self._equivalent(
+ self.session.query(User).filter(~User.singular_value.has()),
+ self.session.query(User).filter(
+ ~User.singular.has(),
+ )
+ )
+
+ def test_has_criterion_nul(self):
+ # but we don't allow that with any criterion...
+ User = self.classes.User
+ Singular = self.classes.Singular
+
+ assert_raises_message(
+ exc.ArgumentError,
+ "Non-empty has\(\) not allowed",
+ User.singular_value.has,
+ User.singular_value == "singular4"
+ )
+
+ def test_has_kwargs_nul(self):
+ # ... or kwargs
+ User = self.classes.User
+ Singular = self.classes.Singular
+
+ assert_raises_message(
+ exc.ArgumentError,
+ "Non-empty has\(\) not allowed",
+ User.singular_value.has, singular_value="singular4"
+ )
+
+ def test_filter_ne_value_nul(self):
+ User = self.classes.User
+ Singular = self.classes.Singular
+
+ self._equivalent(
+ self.session.query(User).filter(User.singular_value != "singular4"),
+ self.session.query(User).filter(
+ User.singular.has(Singular.value != "singular4"),
+ )
+ )
def test_filter_scalar_contains_fails_nul_nul(self):
Keyword = self.classes.Keyword