: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
__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
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)
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
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):
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
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.
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):
o.all()
)
+
def test_with_pending_autoflush(self):
Order, User = self.classes.Order, self.classes.User
)
+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