summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2015-04-20 17:38:03 -0400
committerMike Bayer <mike_mp@zzzcomputing.com>2015-04-20 17:38:03 -0400
commita3af638e1a95d42075e25e87474663348dcf5c14 (patch)
tree4549f2261067856a251367d49e97e40d0bb84b2c
parentbd61e7a3287079cf742f4df698bfe3628c090522 (diff)
downloadsqlalchemy-a3af638e1a95d42075e25e87474663348dcf5c14.tar.gz
- Fixed more regressions caused by NEVER_SET; comparisons
to transient objects with attributes unset would leak NEVER_SET, and negated_contains_or_equals would do so for any transient object as the comparison used only the committed value. Repaired the NEVER_SET cases, fixes #3371, and also made negated_contains_or_equals() use state_attr_by_column() just like a non-negated comparison, fixes #3374
-rw-r--r--doc/build/changelog/changelog_10.rst38
-rw-r--r--doc/build/changelog/migration_10.rst111
-rw-r--r--lib/sqlalchemy/orm/attributes.py2
-rw-r--r--lib/sqlalchemy/orm/mapper.py11
-rw-r--r--lib/sqlalchemy/orm/persistence.py2
-rw-r--r--lib/sqlalchemy/orm/relationships.py47
-rw-r--r--lib/sqlalchemy/orm/strategies.py7
-rw-r--r--lib/sqlalchemy/orm/sync.py4
-rw-r--r--test/ext/test_associationproxy.py23
-rw-r--r--test/orm/test_attributes.py7
-rw-r--r--test/orm/test_query.py270
11 files changed, 458 insertions, 64 deletions
diff --git a/doc/build/changelog/changelog_10.rst b/doc/build/changelog/changelog_10.rst
index cff3f1b5c..86bf7df64 100644
--- a/doc/build/changelog/changelog_10.rst
+++ b/doc/build/changelog/changelog_10.rst
@@ -20,6 +20,44 @@
.. change::
:tags: bug, orm
+ :tickets: 3374
+
+ A query of the form ``query(B).filter(B.a != A(id=7))`` would
+ render the ``NEVER_SET`` symbol, even in older versions as far back
+ as 0.8, for a transient object; for a persistent object, it would
+ always use the persisisted database value and not the currently set
+ value. Assuming autoflush is turned on, this usually would not be
+ apparent for persistent values, as the pending changes would be
+ flushed first. However, this is inconsistent vs. the logic used for
+ the non-negated comparison, ``query(B).filter(B.a == A(id=7))``, which
+ does use the current value and additionally allows comparisons
+ to transient objects. The comparison now uses the current value
+ and not the database-persisted value.
+
+ .. seealso::
+
+ :ref:`bug_3374`
+
+ .. change::
+ :tags: bug, orm
+ :tickets: 3371
+
+ Fixed a regression cause by :ticket:`3061` where the NEVER_SET
+ symbol could leak into relationship-oriented queries, including
+ ``filter()`` and ``with_parent()`` queries. The ``None`` symbol
+ is returned in all cases, however many of these queries have never
+ been correctly supported in any case, and produce comparisons
+ to NULL without using the IS operator. For this reason, a warning
+ is also added to that subset of relationship queries that don't
+ currently provide for ``IS NULL``.
+
+ .. seealso::
+
+ :ref:`bug_3371`
+
+
+ .. change::
+ :tags: bug, orm
:tickets: 3368
Fixed a critical regression caused by :ticket:`3061` where the
diff --git a/doc/build/changelog/migration_10.rst b/doc/build/changelog/migration_10.rst
index da00f1c63..a7a668fbe 100644
--- a/doc/build/changelog/migration_10.rst
+++ b/doc/build/changelog/migration_10.rst
@@ -955,6 +955,117 @@ to by string name as well::
:ticket:`3228`
+.. _bug_3371:
+
+Warnings emitted when comparing objects with None values to relationships
+-------------------------------------------------------------------------
+
+This change is new as of 1.0.1. Some users are performing
+queries that are essentially of this form::
+
+ session.query(Address).filter(Address.user == User(id=None))
+
+This pattern is not currently supported in SQLAlchemy. For all versions,
+it emits SQL resembling::
+
+ SELECT address.id AS address_id, address.user_id AS address_user_id,
+ address.email_address AS address_email_address
+ FROM address WHERE ? = address.user_id
+ (None,)
+
+Note above, there is a comparison ``WHERE ? = address.user_id`` where the
+bound value ``?`` is receving ``None``, or ``NULL`` in SQL. **This will
+always return False in SQL**. The comparison here would in theory
+generate SQL as follows::
+
+ SELECT address.id AS address_id, address.user_id AS address_user_id,
+ address.email_address AS address_email_address
+ FROM address WHERE address.user_id IS NULL
+
+But right now, **it does not**. Applications which are relying upon the
+fact that "NULL = NULL" produces False in all cases run the risk that
+someday, SQLAlchemy might fix this issue to generate "IS NULL", and the queries
+will then produce different results. Therefore with this kind of operation,
+you will see a warning::
+
+ SAWarning: Got None for value of column user.id; this is unsupported
+ for a relationship comparison and will not currently produce an
+ IS comparison (but may in a future release)
+
+Note that this pattern was broken in most cases for release 1.0.0 including
+all of the betas; a value like ``SYMBOL('NEVER_SET')`` would be generated.
+This issue has been fixed, but as a result of identifying this pattern,
+the warning is now there so that we can more safely repair this broken
+behavior (now captured in :ticket:`3373`) in a future release.
+
+:ticket:`3371`
+
+.. _bug_3374:
+
+A "negated contains or equals" relationship comparison will use the current value of attributes, not the database value
+-------------------------------------------------------------------------------------------------------------------------
+
+This change is new as of 1.0.1; while we would have preferred for this to be in 1.0.0,
+it only became apparent as a result of :ticket`3371`.
+
+Given a mapping::
+
+ class A(Base):
+ __tablename__ = 'a'
+ id = Column(Integer, primary_key=True)
+
+ class B(Base):
+ __tablename__ = 'b'
+ id = Column(Integer, primary_key=True)
+ a_id = Column(ForeignKey('a.id'))
+ a = relationship("A")
+
+Given ``A``, with primary key of 7, but which we changed to be 10
+without committing::
+
+ s = Session(autoflush=False)
+ a1 = A(id=7)
+ s.add(a1)
+ s.commit()
+
+ a1.id = 10
+
+A query against a many-to-one relationship with this object as the target
+will use the value 10 in the bound parameters::
+
+ s.query(B).filter(B.a == a1)
+
+Produces::
+
+ SELECT b.id AS b_id, b.a_id AS b_a_id
+ FROM b
+ WHERE ? = b.a_id
+ (10,)
+
+However, before this change, the negation of this criteria would **not** use
+10, it would use 7, unless the object were flushed first::
+
+ s.query(B).filter(B.a != a1)
+
+Produces (in 0.9 and all versions prior to 1.0.1)::
+
+ SELECT b.id AS b_id, b.a_id AS b_a_id
+ FROM b
+ WHERE b.a_id != ? OR b.a_id IS NULL
+ (7,)
+
+For a transient object, it would produce a broken query::
+
+ SELECT b.id, b.a_id
+ FROM b
+ WHERE b.a_id != :a_id_1 OR b.a_id IS NULL
+ {u'a_id_1': symbol('NEVER_SET')}
+
+This inconsistency has been repaired, and in all queries the current attribute
+value, in this example ``10``, will now be used.
+
+:ticket:`3374`
+
.. _migration_3061:
Changes to attribute events and other operations regarding attributes that have no pre-existing value
diff --git a/lib/sqlalchemy/orm/attributes.py b/lib/sqlalchemy/orm/attributes.py
index 41803c8bf..a45c22394 100644
--- a/lib/sqlalchemy/orm/attributes.py
+++ b/lib/sqlalchemy/orm/attributes.py
@@ -619,7 +619,7 @@ class AttributeImpl(object):
if self.key in state.committed_state:
value = state.committed_state[self.key]
- if value is NO_VALUE:
+ if value in (NO_VALUE, NEVER_SET):
return None
else:
return value
diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py
index 4554f78f9..924130874 100644
--- a/lib/sqlalchemy/orm/mapper.py
+++ b/lib/sqlalchemy/orm/mapper.py
@@ -2377,15 +2377,15 @@ class Mapper(InspectionAttr):
"""
state = attributes.instance_state(instance)
- return self._primary_key_from_state(state)
+ return self._primary_key_from_state(state, attributes.PASSIVE_OFF)
- def _primary_key_from_state(self, state):
+ def _primary_key_from_state(
+ self, state, passive=attributes.PASSIVE_RETURN_NEVER_SET):
dict_ = state.dict
manager = state.manager
return [
manager[prop.key].
- impl.get(state, dict_,
- attributes.PASSIVE_RETURN_NEVER_SET)
+ impl.get(state, dict_, passive)
for prop in self._identity_key_props
]
@@ -2428,7 +2428,8 @@ class Mapper(InspectionAttr):
def _get_committed_attr_by_column(self, obj, column):
state = attributes.instance_state(obj)
dict_ = attributes.instance_dict(obj)
- return self._get_committed_state_attr_by_column(state, dict_, column)
+ return self._get_committed_state_attr_by_column(
+ state, dict_, column, passive=attributes.PASSIVE_OFF)
def _get_committed_state_attr_by_column(
self, state, dict_, column,
diff --git a/lib/sqlalchemy/orm/persistence.py b/lib/sqlalchemy/orm/persistence.py
index c2527ee3d..34c37dab6 100644
--- a/lib/sqlalchemy/orm/persistence.py
+++ b/lib/sqlalchemy/orm/persistence.py
@@ -531,7 +531,7 @@ def _collect_post_update_commands(base_mapper, uowtransaction, table,
params[col._label] = \
mapper._get_state_attr_by_column(
state,
- state_dict, col)
+ state_dict, col, passive=attributes.PASSIVE_OFF)
elif col in post_update_cols:
prop = mapper._columntoproperty[col]
diff --git a/lib/sqlalchemy/orm/relationships.py b/lib/sqlalchemy/orm/relationships.py
index b649c9e21..d1c5e5e51 100644
--- a/lib/sqlalchemy/orm/relationships.py
+++ b/lib/sqlalchemy/orm/relationships.py
@@ -1233,11 +1233,15 @@ class RelationshipProperty(StrategizedProperty):
state = attributes.instance_state(other)
def state_bindparam(x, state, col):
- o = state.obj() # strong ref
+ dict_ = state.dict
return sql.bindparam(
- x, unique=True, callable_=lambda:
- self.property.mapper.
- _get_committed_attr_by_column(o, col))
+ x, unique=True,
+ callable_=self.property._get_attr_w_warn_on_none(
+ col,
+ self.property.mapper._get_state_attr_by_column,
+ state, dict_, col, passive=attributes.PASSIVE_OFF
+ )
+ )
def adapt(col):
if self.adapter:
@@ -1252,13 +1256,14 @@ class RelationshipProperty(StrategizedProperty):
adapt(x) == None)
for (x, y) in self.property.local_remote_pairs])
- criterion = sql.and_(*[x == y for (x, y) in
- zip(
- self.property.mapper.primary_key,
- self.property.
- mapper.
- primary_key_from_instance(other))
+ criterion = sql.and_(*[
+ x == y for (x, y) in
+ zip(
+ self.property.mapper.primary_key,
+ self.property.mapper.primary_key_from_instance(other)
+ )
])
+
return ~self._criterion_exists(criterion)
def __ne__(self, other):
@@ -1357,10 +1362,12 @@ class RelationshipProperty(StrategizedProperty):
def visit_bindparam(bindparam):
if bindparam._identifying_key in bind_to_col:
- bindparam.callable = \
- lambda: mapper._get_state_attr_by_column(
- state, dict_,
- bind_to_col[bindparam._identifying_key])
+ bindparam.callable = self._get_attr_w_warn_on_none(
+ bind_to_col[bindparam._identifying_key],
+ mapper._get_state_attr_by_column,
+ state, dict_,
+ bind_to_col[bindparam._identifying_key],
+ passive=attributes.PASSIVE_OFF)
if self.secondary is not None and alias_secondary:
criterion = ClauseAdapter(
@@ -1374,6 +1381,18 @@ class RelationshipProperty(StrategizedProperty):
criterion = adapt_source(criterion)
return criterion
+ def _get_attr_w_warn_on_none(self, column, fn, *arg, **kw):
+ def _go():
+ value = fn(*arg, **kw)
+ if value is None:
+ util.warn(
+ "Got None for value of column %s; this is unsupported "
+ "for a relationship comparison and will not "
+ "currently produce an IS comparison "
+ "(but may in a future release)" % column)
+ return value
+ return _go
+
def _lazy_none_clause(self, reverse_direction=False, adapt_source=None):
if not reverse_direction:
criterion, bind_to_col = \
diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py
index 5c6618686..85d233a05 100644
--- a/lib/sqlalchemy/orm/strategies.py
+++ b/lib/sqlalchemy/orm/strategies.py
@@ -456,15 +456,18 @@ class LazyLoader(AbstractRelationshipLoader, util.MemoizedSlots):
o = state.obj() # strong ref
dict_ = attributes.instance_dict(o)
+ if passive & attributes.INIT_OK:
+ passive ^= attributes.INIT_OK
+
params = {}
for key, ident, value in param_keys:
if ident is not None:
if passive and passive & attributes.LOAD_AGAINST_COMMITTED:
value = mapper._get_committed_state_attr_by_column(
- state, dict_, ident)
+ state, dict_, ident, passive)
else:
value = mapper._get_state_attr_by_column(
- state, dict_, ident)
+ state, dict_, ident, passive)
params[key] = value
diff --git a/lib/sqlalchemy/orm/sync.py b/lib/sqlalchemy/orm/sync.py
index e9a745cc0..e8e273a86 100644
--- a/lib/sqlalchemy/orm/sync.py
+++ b/lib/sqlalchemy/orm/sync.py
@@ -85,7 +85,7 @@ def update(source, source_mapper, dest, old_prefix, synchronize_pairs):
oldvalue = source_mapper._get_committed_attr_by_column(
source.obj(), l)
value = source_mapper._get_state_attr_by_column(
- source, source.dict, l)
+ source, source.dict, l, passive=attributes.PASSIVE_OFF)
except exc.UnmappedColumnError:
_raise_col_to_prop(False, source_mapper, l, None, r)
dest[r.key] = value
@@ -96,7 +96,7 @@ def populate_dict(source, source_mapper, dict_, synchronize_pairs):
for l, r in synchronize_pairs:
try:
value = source_mapper._get_state_attr_by_column(
- source, source.dict, l)
+ source, source.dict, l, passive=attributes.PASSIVE_OFF)
except exc.UnmappedColumnError:
_raise_col_to_prop(False, source_mapper, l, None, r)
diff --git a/test/ext/test_associationproxy.py b/test/ext/test_associationproxy.py
index 9e328a35f..34c1a8322 100644
--- a/test/ext/test_associationproxy.py
+++ b/test/ext/test_associationproxy.py
@@ -13,6 +13,7 @@ from sqlalchemy.testing import fixtures, AssertsCompiledSQL
from sqlalchemy import testing
from sqlalchemy.testing.schema import Table, Column
from sqlalchemy.testing.mock import Mock, call
+from sqlalchemy.testing.assertions import expect_warnings
class DictCollection(dict):
@collection.appender
@@ -1300,16 +1301,18 @@ class ComparatorTest(fixtures.MappedTest, AssertsCompiledSQL):
def test_filter_contains_nul_ul(self):
User, Singular = self.classes.User, self.classes.Singular
- self._equivalent(
- self.session.query(User).filter(
- User.singular_keywords.contains(self.kw)
- ),
- self.session.query(User).filter(
- User.singular.has(
- Singular.keywords.contains(self.kw)
- )
- ),
- )
+ with expect_warnings(
+ "Got None for value of column keywords.singular_id;"):
+ self._equivalent(
+ self.session.query(User).filter(
+ User.singular_keywords.contains(self.kw)
+ ),
+ self.session.query(User).filter(
+ User.singular.has(
+ Singular.keywords.contains(self.kw)
+ )
+ ),
+ )
def test_filter_eq_nul_nul(self):
Keyword = self.classes.Keyword
diff --git a/test/orm/test_attributes.py b/test/orm/test_attributes.py
index b22fff1a9..96c6451d0 100644
--- a/test/orm/test_attributes.py
+++ b/test/orm/test_attributes.py
@@ -1524,6 +1524,13 @@ class HistoryTest(fixtures.TestBase):
f.someattr = 3
eq_(self._someattr_committed_state(f), None)
+ def test_committed_value_set_active_hist(self):
+ Foo = self._fixture(uselist=False, useobject=False,
+ active_history=True)
+ f = Foo()
+ f.someattr = 3
+ eq_(self._someattr_committed_state(f), None)
+
def test_committed_value_set_commit(self):
Foo = self._fixture(uselist=False, useobject=False,
active_history=False)
diff --git a/test/orm/test_query.py b/test/orm/test_query.py
index 4c909d6aa..4021cdfbb 100644
--- a/test/orm/test_query.py
+++ b/test/orm/test_query.py
@@ -742,7 +742,7 @@ class OperatorTest(QueryTest, AssertsCompiledSQL):
__dialect__ = 'default'
- def _test(self, clause, expected, entity=None):
+ def _test(self, clause, expected, entity=None, checkparams=None):
dialect = default.DefaultDialect()
if entity is not None:
# specify a lead entity, so that when we are testing
@@ -754,9 +754,11 @@ class OperatorTest(QueryTest, AssertsCompiledSQL):
lead = context.statement.compile(dialect=dialect)
expected = (str(lead) + " WHERE " + expected).replace("\n", "")
clause = sess.query(entity).filter(clause)
- self.assert_compile(clause, expected)
+ self.assert_compile(clause, expected, checkparams=checkparams)
- def _test_filter_aliases(self, clause, expected, from_, onclause):
+ def _test_filter_aliases(
+ self,
+ clause, expected, from_, onclause, checkparams=None):
dialect = default.DefaultDialect()
sess = Session()
lead = sess.query(from_).join(onclause, aliased=True)
@@ -766,7 +768,7 @@ class OperatorTest(QueryTest, AssertsCompiledSQL):
lead = context.statement.compile(dialect=dialect)
expected = (str(lead) + " WHERE " + expected).replace("\n", "")
- self.assert_compile(full, expected)
+ self.assert_compile(full, expected, checkparams=checkparams)
def test_arithmetic(self):
User = self.classes.User
@@ -933,65 +935,126 @@ class OperatorTest(QueryTest, AssertsCompiledSQL):
def test_m2o_compare_instance(self):
User, Address = self.classes.User, self.classes.Address
- u7 = User(id=7)
+ u7 = User(id=5)
attributes.instance_state(u7)._commit_all(attributes.instance_dict(u7))
+ u7.id = 7
self._test(Address.user == u7, ":param_1 = addresses.user_id")
def test_m2o_compare_instance_negated(self):
User, Address = self.classes.User, self.classes.Address
- u7 = User(id=7)
+ u7 = User(id=5)
attributes.instance_state(u7)._commit_all(attributes.instance_dict(u7))
+ u7.id = 7
self._test(
Address.user != u7,
- "addresses.user_id != :user_id_1 OR addresses.user_id IS NULL")
+ "addresses.user_id != :user_id_1 OR addresses.user_id IS NULL",
+ checkparams={'user_id_1': 7})
def test_m2o_compare_instance_orm_adapt(self):
User, Address = self.classes.User, self.classes.Address
- u7 = User(id=7)
+ u7 = User(id=5)
attributes.instance_state(u7)._commit_all(attributes.instance_dict(u7))
+ u7.id = 7
self._test_filter_aliases(
Address.user == u7,
- ":param_1 = addresses_1.user_id", User, User.addresses
+ ":param_1 = addresses_1.user_id", User, User.addresses,
+ checkparams={'param_1': 7}
)
+ def test_m2o_compare_instance_negated_warn_on_none(self):
+ User, Address = self.classes.User, self.classes.Address
+
+ u7_transient = User(id=None)
+
+ with expect_warnings("Got None for value of column users.id; "):
+ self._test_filter_aliases(
+ Address.user != u7_transient,
+ "addresses_1.user_id != :user_id_1 "
+ "OR addresses_1.user_id IS NULL",
+ User, User.addresses,
+ checkparams={'user_id_1': None}
+ )
+
def test_m2o_compare_instance_negated_orm_adapt(self):
User, Address = self.classes.User, self.classes.Address
- u7 = User(id=7)
+ u7 = User(id=5)
attributes.instance_state(u7)._commit_all(attributes.instance_dict(u7))
+ u7.id = 7
+
+ u7_transient = User(id=7)
self._test_filter_aliases(
Address.user != u7,
"addresses_1.user_id != :user_id_1 OR addresses_1.user_id IS NULL",
- User, User.addresses
+ User, User.addresses,
+ checkparams={'user_id_1': 7}
)
self._test_filter_aliases(
~(Address.user == u7), ":param_1 != addresses_1.user_id",
- User, User.addresses
+ User, User.addresses,
+ checkparams={'param_1': 7}
)
self._test_filter_aliases(
~(Address.user != u7),
"NOT (addresses_1.user_id != :user_id_1 "
- "OR addresses_1.user_id IS NULL)", User, User.addresses
+ "OR addresses_1.user_id IS NULL)", User, User.addresses,
+ checkparams={'user_id_1': 7}
+ )
+
+ self._test_filter_aliases(
+ Address.user != u7_transient,
+ "addresses_1.user_id != :user_id_1 OR addresses_1.user_id IS NULL",
+ User, User.addresses,
+ checkparams={'user_id_1': 7}
+ )
+
+ self._test_filter_aliases(
+ ~(Address.user == u7_transient), ":param_1 != addresses_1.user_id",
+ User, User.addresses,
+ checkparams={'param_1': 7}
+ )
+
+ self._test_filter_aliases(
+ ~(Address.user != u7_transient),
+ "NOT (addresses_1.user_id != :user_id_1 "
+ "OR addresses_1.user_id IS NULL)", User, User.addresses,
+ checkparams={'user_id_1': 7}
)
def test_m2o_compare_instance_aliased(self):
User, Address = self.classes.User, self.classes.Address
- u7 = User(id=7)
+ u7 = User(id=5)
attributes.instance_state(u7)._commit_all(attributes.instance_dict(u7))
+ u7.id = 7
+
+ u7_transient = User(id=7)
a1 = aliased(Address)
self._test(
a1.user == u7,
- ":param_1 = addresses_1.user_id")
+ ":param_1 = addresses_1.user_id",
+ checkparams={'param_1': 7})
self._test(
a1.user != u7,
- "addresses_1.user_id != :user_id_1 OR addresses_1.user_id IS NULL")
+ "addresses_1.user_id != :user_id_1 OR addresses_1.user_id IS NULL",
+ checkparams={'user_id_1': 7})
+
+ a1 = aliased(Address)
+ self._test(
+ a1.user == u7_transient,
+ ":param_1 = addresses_1.user_id",
+ checkparams={'param_1': 7})
+
+ self._test(
+ a1.user != u7_transient,
+ "addresses_1.user_id != :user_id_1 OR addresses_1.user_id IS NULL",
+ checkparams={'user_id_1': 7})
def test_selfref_relationship(self):
@@ -1004,7 +1067,8 @@ class OperatorTest(QueryTest, AssertsCompiledSQL):
Node.children.any(Node.data == 'n1'),
"EXISTS (SELECT 1 FROM nodes AS nodes_1 WHERE "
"nodes.id = nodes_1.parent_id AND nodes_1.data = :data_1)",
- entity=Node
+ entity=Node,
+ checkparams={'data_1': 'n1'}
)
# needs autoaliasing
@@ -1012,36 +1076,43 @@ class OperatorTest(QueryTest, AssertsCompiledSQL):
Node.children == None,
"NOT (EXISTS (SELECT 1 FROM nodes AS nodes_1 "
"WHERE nodes.id = nodes_1.parent_id))",
- entity=Node
+ entity=Node,
+ checkparams={}
)
self._test(
Node.parent == None,
- "nodes.parent_id IS NULL"
+ "nodes.parent_id IS NULL",
+ checkparams={}
)
self._test(
nalias.parent == None,
- "nodes_1.parent_id IS NULL"
+ "nodes_1.parent_id IS NULL",
+ checkparams={}
)
self._test(
nalias.parent != None,
- "nodes_1.parent_id IS NOT NULL"
+ "nodes_1.parent_id IS NOT NULL",
+ checkparams={}
)
self._test(
nalias.children == None,
"NOT (EXISTS ("
"SELECT 1 FROM nodes WHERE nodes_1.id = nodes.parent_id))",
- entity=nalias
+ entity=nalias,
+ checkparams={}
)
self._test(
nalias.children.any(Node.data == 'some data'),
"EXISTS (SELECT 1 FROM nodes WHERE "
"nodes_1.id = nodes.parent_id AND nodes.data = :data_1)",
- entity=nalias)
+ entity=nalias,
+ checkparams={'data_1': 'some data'}
+ )
# this fails because self-referential any() is auto-aliasing;
# the fact that we use "nalias" here means we get two aliases.
@@ -1056,33 +1127,48 @@ class OperatorTest(QueryTest, AssertsCompiledSQL):
nalias.parent.has(Node.data == 'some data'),
"EXISTS (SELECT 1 FROM nodes WHERE nodes.id = nodes_1.parent_id "
"AND nodes.data = :data_1)",
- entity=nalias
+ entity=nalias,
+ checkparams={'data_1': 'some data'}
)
self._test(
Node.parent.has(Node.data == 'some data'),
"EXISTS (SELECT 1 FROM nodes AS nodes_1 WHERE "
"nodes_1.id = nodes.parent_id AND nodes_1.data = :data_1)",
- entity=Node
+ entity=Node,
+ checkparams={'data_1': 'some data'}
)
self._test(
Node.parent == Node(id=7),
- ":param_1 = nodes.parent_id"
+ ":param_1 = nodes.parent_id",
+ checkparams={"param_1": 7}
)
self._test(
nalias.parent == Node(id=7),
- ":param_1 = nodes_1.parent_id"
+ ":param_1 = nodes_1.parent_id",
+ checkparams={"param_1": 7}
+ )
+
+ self._test(
+ nalias.parent != Node(id=7),
+ 'nodes_1.parent_id != :parent_id_1 '
+ 'OR nodes_1.parent_id IS NULL',
+ checkparams={"parent_id_1": 7}
)
self._test(
nalias.parent != Node(id=7),
- 'nodes_1.parent_id != :parent_id_1 OR nodes_1.parent_id IS NULL'
+ 'nodes_1.parent_id != :parent_id_1 '
+ 'OR nodes_1.parent_id IS NULL',
+ checkparams={"parent_id_1": 7}
)
self._test(
- nalias.children.contains(Node(id=7)), "nodes_1.id = :param_1"
+ nalias.children.contains(Node(id=7, parent_id=12)),
+ "nodes_1.id = :param_1",
+ checkparams={"param_1": 12}
)
def test_multilevel_any(self):
@@ -2944,6 +3030,7 @@ class ParentTest(QueryTest, AssertsCompiledSQL):
o.all()
)
+
def test_with_pending_autoflush(self):
Order, User = self.classes.Order, self.classes.User
@@ -3018,6 +3105,131 @@ class ParentTest(QueryTest, AssertsCompiledSQL):
)
+class WithTransientOnNone(_fixtures.FixtureTest, AssertsCompiledSQL):
+ run_inserts = None
+ __dialect__ = 'default'
+
+ def _fixture1(self):
+ User, Address = self.classes.User, self.classes.Address
+ users, addresses = self.tables.users, self.tables.addresses
+
+ mapper(User, users)
+ mapper(Address, addresses, properties={
+ 'user': relationship(User),
+ 'special_user': relationship(
+ User, primaryjoin=and_(
+ users.c.id == addresses.c.user_id,
+ users.c.name == addresses.c.email_address))
+ })
+
+ def test_filter_with_transient_assume_pk(self):
+ self._fixture1()
+ User, Address = self.classes.User, self.classes.Address
+
+ sess = Session()
+
+ q = sess.query(Address).filter(Address.user == User())
+ with expect_warnings("Got None for value of column "):
+ self.assert_compile(
+ q,
+ "SELECT addresses.id AS addresses_id, "
+ "addresses.user_id AS addresses_user_id, "
+ "addresses.email_address AS addresses_email_address "
+ "FROM addresses WHERE :param_1 = addresses.user_id",
+ checkparams={'param_1': None}
+ )
+
+ def test_filter_with_transient_warn_for_none_against_non_pk(self):
+ self._fixture1()
+ User, Address = self.classes.User, self.classes.Address
+
+ s = Session()
+ q = s.query(Address).filter(Address.special_user == User())
+ with expect_warnings("Got None for value of column"):
+
+ self.assert_compile(
+ q,
+ "SELECT addresses.id AS addresses_id, "
+ "addresses.user_id AS addresses_user_id, "
+ "addresses.email_address AS addresses_email_address "
+ "FROM addresses WHERE :param_1 = addresses.user_id "
+ "AND :param_2 = addresses.email_address",
+ checkparams={"param_1": None, "param_2": None}
+ )
+
+ def test_with_parent_with_transient_assume_pk(self):
+ self._fixture1()
+ User, Address = self.classes.User, self.classes.Address
+
+ sess = Session()
+
+ q = sess.query(User).with_parent(Address(), "user")
+ with expect_warnings("Got None for value of column"):
+ self.assert_compile(
+ q,
+ "SELECT users.id AS users_id, users.name AS users_name "
+ "FROM users WHERE users.id = :param_1",
+ checkparams={'param_1': None}
+ )
+
+ def test_with_parent_with_transient_warn_for_none_against_non_pk(self):
+ self._fixture1()
+ User, Address = self.classes.User, self.classes.Address
+
+ s = Session()
+ q = s.query(User).with_parent(Address(), "special_user")
+ with expect_warnings("Got None for value of column"):
+
+ self.assert_compile(
+ q,
+ "SELECT users.id AS users_id, users.name AS users_name "
+ "FROM users WHERE users.id = :param_1 "
+ "AND users.name = :param_2",
+ checkparams={"param_1": None, "param_2": None}
+ )
+
+ def test_negated_contains_or_equals_plain_m2o(self):
+ self._fixture1()
+ User, Address = self.classes.User, self.classes.Address
+
+ s = Session()
+ q = s.query(Address).filter(Address.user != User())
+ with expect_warnings("Got None for value of column"):
+ self.assert_compile(
+ q,
+
+ "SELECT addresses.id AS addresses_id, "
+ "addresses.user_id AS addresses_user_id, "
+ "addresses.email_address AS addresses_email_address "
+ "FROM addresses "
+ "WHERE addresses.user_id != :user_id_1 "
+ "OR addresses.user_id IS NULL",
+ checkparams={'user_id_1': None}
+ )
+
+ def test_negated_contains_or_equals_complex_rel(self):
+ self._fixture1()
+ User, Address = self.classes.User, self.classes.Address
+
+ s = Session()
+
+ # this one does *not* warn because we do the criteria
+ # without deferral
+ q = s.query(Address).filter(Address.special_user != User())
+ self.assert_compile(
+ q,
+ "SELECT addresses.id AS addresses_id, "
+ "addresses.user_id AS addresses_user_id, "
+ "addresses.email_address AS addresses_email_address "
+ "FROM addresses "
+ "WHERE NOT (EXISTS (SELECT 1 "
+ "FROM users "
+ "WHERE users.id = addresses.user_id AND "
+ "users.name = addresses.email_address AND users.id IS NULL))",
+ checkparams={}
+ )
+
+
class SynonymTest(QueryTest):
@classmethod