diff options
author | Mike Bayer <mike_mp@zzzcomputing.com> | 2015-04-20 17:38:03 -0400 |
---|---|---|
committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2015-04-20 17:38:03 -0400 |
commit | a3af638e1a95d42075e25e87474663348dcf5c14 (patch) | |
tree | 4549f2261067856a251367d49e97e40d0bb84b2c | |
parent | bd61e7a3287079cf742f4df698bfe3628c090522 (diff) | |
download | sqlalchemy-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.rst | 38 | ||||
-rw-r--r-- | doc/build/changelog/migration_10.rst | 111 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/attributes.py | 2 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/mapper.py | 11 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/persistence.py | 2 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/relationships.py | 47 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/strategies.py | 7 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/sync.py | 4 | ||||
-rw-r--r-- | test/ext/test_associationproxy.py | 23 | ||||
-rw-r--r-- | test/orm/test_attributes.py | 7 | ||||
-rw-r--r-- | test/orm/test_query.py | 270 |
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 |