--- /dev/null
+.. change::
+ :tags: bug, orm, regression
+ :tickets: 6055
+
+ Fixed a critical regression in the relationship lazy loader where the SQL
+ criteria used to fetch a related many-to-one object could go stale in
+ relation to other memoized structures within the loader if the mapper had
+ configuration changes, such as can occur when mappers are late configured
+ or configured on demand, producing a comparison to None and returning no
+ object. Huge thanks to Alan Hamlett for their help tracking this down late
+ into the night.
+
+
lambda q: q.where(
sql_util._deep_annotate(_get_clause, {"_orm_adapt": True})
),
+ # this track_on will allow the lambda to refresh if
+ # _get_clause goes stale due to reconfigured mapper.
+ # however, it's not needed as the lambda otherwise tracks
+ # on the SQL cache key of the expression. the main thing
+ # is that the bindparam.key stays the same if the cache key
+ # stays the same, as we are referring to the .key explicitly
+ # in the params.
+ # track_on=[id(_get_clause)]
)
else:
q._where_criteria = (
"""
params = [
- (primary_key, sql.bindparam(None, type_=primary_key.type))
- for primary_key in self.primary_key
+ (
+ primary_key,
+ sql.bindparam("pk_%d" % idx, type_=primary_key.type),
+ )
+ for idx, primary_key in enumerate(self.primary_key, 1)
]
return (
sql.and_(*[k == v for (k, v) in params]),
and self.entity._get_clause[0].compare(
self._lazywhere,
use_proxies=True,
+ compare_keys=False,
equivalents=self.mapper._equivalent_columns,
)
)
self.omit_join = self.parent._get_clause[0].compare(
lazyloader._rev_lazywhere,
use_proxies=True,
+ compare_keys=False,
equivalents=self.parent._equivalent_columns,
)
else:
self.type = type_
- def _with_value(self, value, maintain_key=False):
+ def _with_value(self, value, maintain_key=False, required=NO_ARG):
"""Return a copy of this :class:`.BindParameter` with the given value
set.
"""
cloned = self._clone(maintain_key=maintain_key)
cloned.value = value
cloned.callable = None
- cloned.required = False
+ cloned.required = required if required is not NO_ARG else self.required
if cloned.type is type_api.NULLTYPE:
cloned.type = type_api._resolve_value_to_type(value)
return cloned
replace_context=err,
)
else:
- new_params[key] = existing._with_value(value)
+ new_params[key] = existing._with_value(value, required=False)
@util.preload_module("sqlalchemy.sql.selectable")
def columns(self, *cols, **types):
to_evaluate = object.__getattribute__(self, "_to_evaluate")
if param is None:
name = object.__getattribute__(self, "_name")
- self._param = param = elements.BindParameter(name, unique=True)
+ self._param = param = elements.BindParameter(
+ name, required=False, unique=True
+ )
self._has_param = True
param.type = type_api._resolve_value_to_type(to_evaluate)
return param._with_value(to_evaluate, maintain_key=True)
return COMPARE_FAILED
def compare_bindparam(self, left, right, **kw):
+ compare_keys = kw.pop("compare_keys", True)
compare_values = kw.pop("compare_values", True)
+
if compare_values:
- return []
+ omit = []
else:
# this means, "skip these, we already compared"
- return ["callable", "value"]
+ omit = ["callable", "value"]
+
+ if not compare_keys:
+ omit.append("key")
+
+ return omit
class ColIdentityComparatorStrategy(TraversalComparatorStrategy):
u1 = bq(sess).get(7)
eq_(u1.name, "jack")
sess.close()
- eq_(len(bq._bakery), 4)
+
+ # this went from 4 to 3 as a result of #6055. by giving a name
+ # to the bind param in mapper._get_clause, while the baked cache
+ # here grows by one element, the SQL compiled_cache no longer
+ # changes because the keys of the bindparam() objects are passed
+ # explicitly as params to the execute() call as a result of
+ # _load_on_pk_identity() (either the one in baked or the one in
+ # loading.py), which then puts them
+ # in column_keys which makes them part of the cache key. These
+ # were previously anon names, now they are explicit so they
+ # stay across resets
+ eq_(len(bq._bakery), 3)
class ResultPostCriteriaTest(BakedTest):
asserter.assert_(
CompiledSQL(
"SELECT a.id AS a_id, a.type AS a_type "
- "FROM a WHERE a.id = :param_1",
- [{"param_1": 1}],
+ "FROM a WHERE a.id = :pk_1",
+ [{"pk_1": 1}],
),
CompiledSQL("DELETE FROM a WHERE a.id = :id", [{"id": 1}]),
)
asserter.assert_(
CompiledSQL(
"SELECT a.id AS a_id, a.type AS a_type "
- "FROM a WHERE a.id = :param_1",
- [{"param_1": 1}],
+ "FROM a WHERE a.id = :pk_1",
+ [{"pk_1": 1}],
),
CompiledSQL("DELETE FROM a WHERE a.id = :id", [{"id": 1}]),
)
asserter.assert_(
CompiledSQL(
"SELECT a.id AS a_id, a.type AS a_type "
- "FROM a WHERE a.id = :param_1",
- [{"param_1": 1}],
+ "FROM a WHERE a.id = :pk_1",
+ [{"pk_1": 1}],
),
CompiledSQL("DELETE FROM a WHERE a.id = :id", [{"id": 1}]),
)
"base.data AS base_data, base.type AS base_type, "
"sub.sub AS sub_sub, sub.subcounter2 AS sub_subcounter2 "
"FROM base LEFT OUTER JOIN sub ON base.id = sub.id "
- "WHERE base.id = :param_1",
- {"param_1": sjb_id},
+ "WHERE base.id = :pk_1",
+ {"pk_1": sjb_id},
),
)
"SELECT base.counter AS base_counter, "
"sub.subcounter AS sub_subcounter, "
"sub.subcounter2 AS sub_subcounter2 FROM base JOIN sub "
- "ON base.id = sub.id WHERE base.id = :param_1",
- lambda ctx: {"param_1": s1.id},
+ "ON base.id = sub.id WHERE base.id = :pk_1",
+ lambda ctx: {"pk_1": s1.id},
),
)
),
CompiledSQL(
"SELECT test.bar AS test_bar FROM test "
- "WHERE test.id = :param_1",
- [{"param_1": 1}],
+ "WHERE test.id = :pk_1",
+ [{"pk_1": 1}],
),
CompiledSQL(
"SELECT test.bar AS test_bar FROM test "
- "WHERE test.id = :param_1",
- [{"param_1": 2}],
+ "WHERE test.id = :pk_1",
+ [{"pk_1": 2}],
),
],
)
),
CompiledSQL(
"SELECT test.bar AS test_bar FROM test "
- "WHERE test.id = :param_1",
- [{"param_1": 1}],
+ "WHERE test.id = :pk_1",
+ [{"pk_1": 1}],
),
CompiledSQL(
"SELECT test.bar AS test_bar FROM test "
- "WHERE test.id = :param_1",
- [{"param_1": 2}],
+ "WHERE test.id = :pk_1",
+ [{"pk_1": 2}],
),
)
else:
),
CompiledSQL(
"SELECT test.bar AS test_bar FROM test "
- "WHERE test.id = :param_1",
- [{"param_1": 1}],
+ "WHERE test.id = :pk_1",
+ [{"pk_1": 1}],
),
CompiledSQL(
"SELECT test.bar AS test_bar FROM test "
- "WHERE test.id = :param_1",
- [{"param_1": 2}],
+ "WHERE test.id = :pk_1",
+ [{"pk_1": 2}],
),
)
),
(
"SELECT orders.description AS orders_description "
- "FROM orders WHERE orders.id = :param_1",
- {"param_1": 3},
+ "FROM orders WHERE orders.id = :pk_1",
+ {"pk_1": 3},
),
],
)
"orders.address_id AS orders_address_id, "
"orders.description AS orders_description, "
"orders.isopen AS orders_isopen "
- "FROM orders WHERE orders.id = :param_1",
- {"param_1": 3},
+ "FROM orders WHERE orders.id = :pk_1",
+ {"pk_1": 3},
),
],
)
),
(
"SELECT orders.user_id AS orders_user_id "
- "FROM orders WHERE orders.id = :param_1",
- {"param_1": 1},
+ "FROM orders WHERE orders.id = :pk_1",
+ {"pk_1": 1},
),
],
)
sess.flush,
CompiledSQL(
"SELECT users.id AS users_id, users.name AS users_name "
- "FROM users WHERE users.id = :param_1",
- lambda ctx: [{"param_1": u1_id}],
+ "FROM users WHERE users.id = :pk_1",
+ lambda ctx: [{"pk_1": u1_id}],
),
CompiledSQL(
"INSERT INTO addresses (user_id, email_address) "
CompiledSQL(
"SELECT addresses.id AS addresses_id, addresses.email_address "
"AS addresses_email_address FROM addresses "
- "WHERE addresses.id = :param_1",
- lambda ctx: [{"param_1": a2_id}],
+ "WHERE addresses.id = :pk_1",
+ lambda ctx: [{"pk_1": a2_id}],
),
CompiledSQL(
"UPDATE addresses SET user_id=:user_id WHERE addresses.id = "
),
CompiledSQL(
"SELECT users.id AS users_id, users.name AS users_name "
- "FROM users WHERE users.id = :param_1",
- lambda ctx: [{"param_1": u1_id}],
+ "FROM users WHERE users.id = :pk_1",
+ lambda ctx: [{"pk_1": u1_id}],
),
)
"SELECT child2.id AS child2_id, base.id AS base_id, "
"base.type AS base_type "
"FROM base JOIN child2 ON base.id = child2.id "
- "WHERE base.id = :param_1",
- {"param_1": 4},
+ "WHERE base.id = :pk_1",
+ {"pk_1": 4},
),
)
"SELECT child2.id AS child2_id, base.id AS base_id, "
"base.type AS base_type "
"FROM base JOIN child2 ON base.id = child2.id "
- "WHERE base.id = :param_1",
- {"param_1": 4},
+ "WHERE base.id = :pk_1",
+ {"pk_1": 4},
),
)
"related_1.id AS related_1_id FROM base JOIN child2 "
"ON base.id = child2.id "
"LEFT OUTER JOIN related AS related_1 "
- "ON base.id = related_1.id WHERE base.id = :param_1",
- {"param_1": 4},
+ "ON base.id = related_1.id WHERE base.id = :pk_1",
+ {"pk_1": 4},
),
)
self.assert_sql_count(testing.db, go, 0)
sa.orm.clear_mappers()
+ def test_use_get_lambda_key_wont_go_stale(self):
+ """test [ticket:6055]"""
+ Address, addresses, users, User = (
+ self.classes.Address,
+ self.tables.addresses,
+ self.tables.users,
+ self.classes.User,
+ )
+ um = mapper(User, users)
+ am = mapper(
+ Address, addresses, properties={"user": relationship(User)}
+ )
+
+ is_true(am.relationships.user._lazy_strategy.use_get)
+
+ with fixture_session() as sess:
+ a1 = sess.get(Address, 2)
+
+ eq_(a1.user.id, 8)
+
+ um._reset_memoizations()
+
+ with fixture_session() as sess:
+ a1 = sess.get(Address, 2)
+
+ eq_(a1.user.id, 8)
+
def test_uses_get_compatible_types(self):
"""test the use_get optimization with compatible
but non-identical types"""
[
(
"SELECT book.summary AS book_summary "
- "FROM book WHERE book.id = :param_1",
- {"param_1": 1},
+ "FROM book WHERE book.id = :pk_1",
+ {"pk_1": 1},
),
(
"SELECT book.excerpt AS book_excerpt "
- "FROM book WHERE book.id = :param_1",
- {"param_1": 1},
+ "FROM book WHERE book.id = :pk_1",
+ {"pk_1": 1},
),
],
)
[
(
"SELECT book.summary AS book_summary "
- "FROM book WHERE book.id = :param_1",
- {"param_1": 1},
+ "FROM book WHERE book.id = :pk_1",
+ {"pk_1": 1},
),
(
"SELECT book.excerpt AS book_excerpt "
- "FROM book WHERE book.id = :param_1",
- {"param_1": 1},
+ "FROM book WHERE book.id = :pk_1",
+ {"pk_1": 1},
),
],
)
"addresses_user_id, addresses.email_address AS "
"addresses_email_address FROM addresses "
"WHERE addresses.id = "
- ":param_1",
- lambda ctx: {"param_1": c1id},
+ ":pk_1",
+ lambda ctx: {"pk_1": c1id},
),
CompiledSQL(
"SELECT addresses.id AS addresses_id, "
"addresses_user_id, addresses.email_address AS "
"addresses_email_address FROM addresses "
"WHERE addresses.id = "
- ":param_1",
- lambda ctx: {"param_1": c2id},
+ ":pk_1",
+ lambda ctx: {"pk_1": c2id},
),
CompiledSQL(
"SELECT users.id AS users_id, users.name AS users_name "
- "FROM users WHERE users.id = :param_1",
- lambda ctx: {"param_1": pid},
+ "FROM users WHERE users.id = :pk_1",
+ lambda ctx: {"pk_1": pid},
),
CompiledSQL(
"DELETE FROM addresses WHERE addresses.id = :id",
"addresses_user_id, addresses.email_address AS "
"addresses_email_address FROM addresses "
"WHERE addresses.id = "
- ":param_1",
- lambda ctx: {"param_1": c1id},
+ ":pk_1",
+ lambda ctx: {"pk_1": c1id},
),
CompiledSQL(
"SELECT addresses.id AS addresses_id, "
"addresses_user_id, addresses.email_address AS "
"addresses_email_address FROM addresses "
"WHERE addresses.id = "
- ":param_1",
- lambda ctx: {"param_1": c2id},
+ ":pk_1",
+ lambda ctx: {"pk_1": c2id},
),
),
CompiledSQL(
"addresses_user_id, addresses.email_address AS "
"addresses_email_address FROM addresses "
"WHERE addresses.id = "
- ":param_1",
- lambda ctx: {"param_1": c1id},
+ ":pk_1",
+ lambda ctx: {"pk_1": c1id},
),
CompiledSQL(
"SELECT addresses.id AS addresses_id, "
"addresses_user_id, addresses.email_address AS "
"addresses_email_address FROM addresses "
"WHERE addresses.id = "
- ":param_1",
- lambda ctx: {"param_1": c2id},
+ ":pk_1",
+ lambda ctx: {"pk_1": c2id},
),
),
CompiledSQL(
"SELECT nodes.id AS nodes_id, nodes.parent_id AS "
"nodes_parent_id, "
"nodes.data AS nodes_data FROM nodes "
- "WHERE nodes.id = :param_1",
- lambda ctx: {"param_1": pid},
+ "WHERE nodes.id = :pk_1",
+ lambda ctx: {"pk_1": pid},
),
CompiledSQL(
"SELECT nodes.id AS nodes_id, nodes.parent_id AS "
"nodes_parent_id, "
"nodes.data AS nodes_data FROM nodes "
- "WHERE nodes.id = :param_1",
- lambda ctx: {"param_1": c1id},
+ "WHERE nodes.id = :pk_1",
+ lambda ctx: {"pk_1": c1id},
),
CompiledSQL(
"SELECT nodes.id AS nodes_id, nodes.parent_id AS "
"nodes_parent_id, "
"nodes.data AS nodes_data FROM nodes "
- "WHERE nodes.id = :param_1",
- lambda ctx: {"param_1": c2id},
+ "WHERE nodes.id = :pk_1",
+ lambda ctx: {"pk_1": c2id},
),
AllOf(
CompiledSQL(
),
CompiledSQL(
"SELECT test.foo AS test_foo FROM test "
- "WHERE test.id = :param_1",
- [{"param_1": 1}],
+ "WHERE test.id = :pk_1",
+ [{"pk_1": 1}],
),
CompiledSQL(
"SELECT test.foo AS test_foo FROM test "
- "WHERE test.id = :param_1",
- [{"param_1": 2}],
+ "WHERE test.id = :pk_1",
+ [{"pk_1": 2}],
),
)
),
CompiledSQL(
"SELECT test.foo AS test_foo FROM test "
- "WHERE test.id = :param_1",
- [{"param_1": 1}],
+ "WHERE test.id = :pk_1",
+ [{"pk_1": 1}],
),
CompiledSQL(
"SELECT test.foo AS test_foo FROM test "
- "WHERE test.id = :param_1",
- [{"param_1": 2}],
+ "WHERE test.id = :pk_1",
+ [{"pk_1": 2}],
),
],
),
),
CompiledSQL(
"SELECT test2.bar AS test2_bar FROM test2 "
- "WHERE test2.id = :param_1",
- [{"param_1": 1}],
+ "WHERE test2.id = :pk_1",
+ [{"pk_1": 1}],
),
CompiledSQL(
"SELECT test2.bar AS test2_bar FROM test2 "
- "WHERE test2.id = :param_1",
- [{"param_1": 3}],
+ "WHERE test2.id = :pk_1",
+ [{"pk_1": 3}],
),
],
),
),
CompiledSQL(
"SELECT test2.bar AS test2_bar FROM test2 "
- "WHERE test2.id = :param_1",
- [{"param_1": 1}],
+ "WHERE test2.id = :pk_1",
+ [{"pk_1": 1}],
),
CompiledSQL(
"SELECT test2.bar AS test2_bar FROM test2 "
- "WHERE test2.id = :param_1",
- [{"param_1": 3}],
+ "WHERE test2.id = :pk_1",
+ [{"pk_1": 3}],
),
CompiledSQL(
"SELECT test2.bar AS test2_bar FROM test2 "
- "WHERE test2.id = :param_1",
- [{"param_1": 4}],
+ "WHERE test2.id = :pk_1",
+ [{"pk_1": 4}],
),
)
CompiledSQL(
"SELECT users.age_int AS users_age_int, "
"users.name AS users_name FROM users "
- "WHERE users.id = :param_1",
- [{"param_1": 4}],
+ "WHERE users.id = :pk_1",
+ [{"pk_1": 4}],
),
CompiledSQL(
"UPDATE users "
# key
CompiledSQL(
"SELECT users.name AS users_name FROM users "
- "WHERE users.id = :param_1",
- [{"param_1": 4}],
+ "WHERE users.id = :pk_1",
+ [{"pk_1": 4}],
),
CompiledSQL(
"UPDATE users SET "
CompiledSQL(
"SELECT users.age_int AS users_age_int, "
"users.id AS users_id, users.name AS users_name FROM users "
- "WHERE users.id = :param_1",
- [{"param_1": 1}],
+ "WHERE users.id = :pk_1",
+ [{"pk_1": 1}],
),
# refresh jill
CompiledSQL(
"SELECT users.age_int AS users_age_int, "
"users.id AS users_id, users.name AS users_name FROM users "
- "WHERE users.id = :param_1",
- [{"param_1": 3}],
+ "WHERE users.id = :pk_1",
+ [{"pk_1": 3}],
),
]
CompiledSQL(
"SELECT users.age_int AS users_age_int, "
"users.name AS users_name FROM users "
- "WHERE users.id = :param_1",
- [{"param_1": 4}],
+ "WHERE users.id = :pk_1",
+ [{"pk_1": 4}],
)
)
asserter.assert_(*to_assert)
CompiledSQL(
"SELECT users.age_int AS users_age_int, "
"users.id AS users_id, users.name AS users_name FROM users "
- "WHERE users.id = :param_1",
- [{"param_1": 1}],
+ "WHERE users.id = :pk_1",
+ [{"pk_1": 1}],
),
# refresh jill
CompiledSQL(
"SELECT users.age_int AS users_age_int, "
"users.id AS users_id, users.name AS users_name FROM users "
- "WHERE users.id = :param_1",
- [{"param_1": 3}],
+ "WHERE users.id = :pk_1",
+ [{"pk_1": 3}],
),
)
CompiledSQL(
"SELECT version_table.version_id "
"AS version_table_version_id "
- "FROM version_table WHERE version_table.id = :param_1",
- lambda ctx: [{"param_1": 1}],
+ "FROM version_table WHERE version_table.id = :pk_1",
+ lambda ctx: [{"pk_1": 1}],
)
)
self.assert_sql_execution(testing.db, sess.flush, *statements)
CompiledSQL(
"SELECT version_table.version_id "
"AS version_table_version_id "
- "FROM version_table WHERE version_table.id = :param_1",
- lambda ctx: [{"param_1": 1}],
+ "FROM version_table WHERE version_table.id = :pk_1",
+ lambda ctx: [{"pk_1": 1}],
)
)
with conditional_sane_rowcount_warnings(
CompiledSQL(
"SELECT version_table.version_id "
"AS version_table_version_id "
- "FROM version_table WHERE version_table.id = :param_1",
- lambda ctx: [{"param_1": 1}],
+ "FROM version_table WHERE version_table.id = :pk_1",
+ lambda ctx: [{"pk_1": 1}],
),
CompiledSQL(
"SELECT version_table.version_id "
"AS version_table_version_id "
- "FROM version_table WHERE version_table.id = :param_1",
- lambda ctx: [{"param_1": 2}],
+ "FROM version_table WHERE version_table.id = :pk_1",
+ lambda ctx: [{"pk_1": 2}],
),
CompiledSQL(
"SELECT version_table.version_id "
"AS version_table_version_id "
- "FROM version_table WHERE version_table.id = :param_1",
- lambda ctx: [{"param_1": 3}],
+ "FROM version_table WHERE version_table.id = :pk_1",
+ lambda ctx: [{"pk_1": 3}],
),
]
)
from sqlalchemy.testing import fixtures
from sqlalchemy.testing import is_
from sqlalchemy.testing import ne_
+from sqlalchemy.testing.assertions import expect_raises_message
from sqlalchemy.testing.assertsql import CompiledSQL
from sqlalchemy.types import Boolean
from sqlalchemy.types import Integer
s5 = go(oldc1, oldc2)
self.assert_compile(s5, "SELECT x WHERE y > :y_1")
+ def test_maintain_required_bindparam(self):
+ """test that the "required" flag doesn't go away for bound
+ parameters"""
+
+ def go():
+ col_expr = column("x")
+ stmt = lambdas.lambda_stmt(lambda: select(col_expr))
+ stmt += lambda stmt: stmt.where(col_expr == bindparam(None))
+
+ return stmt
+
+ s1 = go()
+
+ with expect_raises_message(
+ exc.InvalidRequestError, "A value is required for bind parameter"
+ ):
+ s1.compile().construct_params({})
+ s2 = go()
+ with expect_raises_message(
+ exc.InvalidRequestError, "A value is required for bind parameter"
+ ):
+ s2.compile().construct_params({})
+
def test_stmt_lambda_w_additional_hascachekey_variants(self):
def go(col_expr, q):
stmt = lambdas.lambda_stmt(lambda: select(col_expr))