summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2018-06-24 22:50:06 -0400
committerMike Bayer <mike_mp@zzzcomputing.com>2018-06-26 15:41:00 -0400
commitf243c00dda1484da97e706b7237670cdce6f10b9 (patch)
tree217a8209e012abd07d72dfdf3559a5832c0cd902
parent677818707471db670d5c3a83b10ebbc65f871881 (diff)
downloadsqlalchemy-f243c00dda1484da97e706b7237670cdce6f10b9.tar.gz
Ensure BakedQuery is cloned before we add options to it
Fixed bug in new polymorphic selectin loading where the BakedQuery used internally would be mutated by the given loader options, which would both inappropriately mutate the subclass query as well as carry over the effect to subsequent queries. Change-Id: Iaceecb50557f78484d09e55b3029a0483dfe873f Fixes: #4286
-rw-r--r--doc/build/changelog/unreleased_12/4286.rst8
-rw-r--r--lib/sqlalchemy/ext/baked.py7
-rw-r--r--lib/sqlalchemy/orm/loading.py6
-rw-r--r--test/orm/inheritance/test_poly_loading.py170
4 files changed, 186 insertions, 5 deletions
diff --git a/doc/build/changelog/unreleased_12/4286.rst b/doc/build/changelog/unreleased_12/4286.rst
new file mode 100644
index 000000000..ff3cc81ed
--- /dev/null
+++ b/doc/build/changelog/unreleased_12/4286.rst
@@ -0,0 +1,8 @@
+.. change::
+ :tags: bug, orm
+ :tickets: 4286
+
+ Fixed bug in new polymorphic selectin loading where the BakedQuery used
+ internally would be mutated by the given loader options, which would both
+ inappropriately mutate the subclass query as well as carry over the effect
+ to subsequent queries.
diff --git a/lib/sqlalchemy/ext/baked.py b/lib/sqlalchemy/ext/baked.py
index 79457e86e..addec90da 100644
--- a/lib/sqlalchemy/ext/baked.py
+++ b/lib/sqlalchemy/ext/baked.py
@@ -154,6 +154,13 @@ class BakedQuery(object):
self._spoiled = True
return self
+ def _with_lazyload_options(self, options, effective_path, cache_path=None):
+ """Cloning version of _add_lazyload_options.
+ """
+ q = self._clone()
+ q._add_lazyload_options(options, effective_path, cache_path=cache_path)
+ return q
+
def _add_lazyload_options(self, options, effective_path, cache_path=None):
"""Used by per-state lazy loaders to add options to the
"lazy load" query from a parent query.
diff --git a/lib/sqlalchemy/orm/loading.py b/lib/sqlalchemy/orm/loading.py
index a169845d4..0a6f8023a 100644
--- a/lib/sqlalchemy/orm/loading.py
+++ b/lib/sqlalchemy/orm/loading.py
@@ -580,17 +580,17 @@ def _load_subclass_via_in(context, path, entity):
def do_load(context, path, states, load_only, effective_entity):
orig_query = context.query
- q._add_lazyload_options(
+ q2 = q._with_lazyload_options(
(enable_opt, ) + orig_query._with_options + (disable_opt, ),
path.parent, cache_path=path
)
if orig_query._populate_existing:
- q.add_criteria(
+ q2.add_criteria(
lambda q: q.populate_existing()
)
- q(context.session).params(
+ q2(context.session).params(
primary_keys=[
state.key[1][0] if zero_idx else state.key[1]
for state, load_attrs in states
diff --git a/test/orm/inheritance/test_poly_loading.py b/test/orm/inheritance/test_poly_loading.py
index b07b498a6..b724e8dba 100644
--- a/test/orm/inheritance/test_poly_loading.py
+++ b/test/orm/inheritance/test_poly_loading.py
@@ -1,6 +1,6 @@
from sqlalchemy import String, Integer, Column, ForeignKey
-from sqlalchemy.orm import relationship, Session, \
- selectin_polymorphic, selectinload, with_polymorphic
+from sqlalchemy.orm import relationship, Session, joinedload, \
+ selectin_polymorphic, selectinload, with_polymorphic, backref
from sqlalchemy.testing import fixtures
from sqlalchemy import testing
from sqlalchemy.testing import eq_
@@ -497,3 +497,169 @@ class TestGeometries(GeometryFixtureBase):
# the poly loader won't locate a state limited to the "a1" mapper,
# needs to test that it has states
sess.query(a)._with_invoke_all_eagers(False).all()
+
+
+class LoaderOptionsTest(
+ fixtures.DeclarativeMappedTest, testing.AssertsExecutionResults):
+ @classmethod
+ def setup_classes(cls):
+ Base = cls.DeclarativeBasic
+
+ class Parent(fixtures.ComparableEntity, Base):
+ __tablename__ = 'parent'
+ id = Column(Integer, primary_key=True)
+
+ class Child(fixtures.ComparableEntity, Base):
+ __tablename__ = 'child'
+ id = Column(Integer, primary_key=True)
+ parent_id = Column(Integer, ForeignKey('parent.id'))
+ parent = relationship('Parent', backref=backref('children'))
+
+ type = Column(String(50), nullable=False)
+ __mapper_args__ = {
+ 'polymorphic_on': type,
+ }
+
+ class ChildSubclass1(Child):
+ __tablename__ = 'child_subclass1'
+ id = Column(Integer, ForeignKey('child.id'), primary_key=True)
+ __mapper_args__ = {
+ 'polymorphic_identity': 'subclass1',
+ 'polymorphic_load': 'selectin'
+ }
+
+ class Other(fixtures.ComparableEntity, Base):
+ __tablename__ = 'other'
+
+ id = Column(Integer, primary_key=True)
+ child_subclass_id = Column(Integer,
+ ForeignKey('child_subclass1.id'))
+ child_subclass = relationship('ChildSubclass1',
+ backref=backref('others'))
+
+ @classmethod
+ def insert_data(cls):
+ Parent, ChildSubclass1, Other = cls.classes(
+ "Parent", "ChildSubclass1", "Other")
+ session = Session()
+
+ parent = Parent(id=1)
+ subclass1 = ChildSubclass1(id=1, parent=parent)
+ other = Other(id=1, child_subclass=subclass1)
+ session.add_all([parent, subclass1, other])
+ session.commit()
+
+ def test_options_dont_pollute_baked(self):
+ self._test_options_dont_pollute(True)
+
+ def test_options_dont_pollute_unbaked(self):
+ self._test_options_dont_pollute(False)
+
+ def _test_options_dont_pollute(self, enable_baked):
+ Parent, ChildSubclass1, Other = self.classes(
+ "Parent", "ChildSubclass1", "Other")
+ session = Session(enable_baked_queries=enable_baked)
+
+ def no_opt():
+ q = session.query(Parent).options(
+ joinedload(Parent.children.of_type(ChildSubclass1)))
+
+ return self.assert_sql_execution(
+ testing.db,
+ q.all,
+ CompiledSQL(
+ "SELECT parent.id AS parent_id, "
+ "anon_1.child_id AS anon_1_child_id, "
+ "anon_1.child_parent_id AS anon_1_child_parent_id, "
+ "anon_1.child_type AS anon_1_child_type, "
+ "anon_1.child_subclass1_id AS anon_1_child_subclass1_id "
+ "FROM parent "
+ "LEFT OUTER JOIN (SELECT child.id AS child_id, "
+ "child.parent_id AS child_parent_id, "
+ "child.type AS child_type, "
+ "child_subclass1.id AS child_subclass1_id "
+ "FROM child "
+ "LEFT OUTER JOIN child_subclass1 "
+ "ON child.id = child_subclass1.id) AS anon_1 "
+ "ON parent.id = anon_1.child_parent_id",
+ {}
+ ),
+ CompiledSQL(
+ "SELECT child_subclass1.id AS child_subclass1_id, "
+ "child.id AS child_id, "
+ "child.parent_id AS child_parent_id, "
+ "child.type AS child_type "
+ "FROM child JOIN child_subclass1 "
+ "ON child.id = child_subclass1.id "
+ "WHERE child.id IN ([EXPANDING_primary_keys]) "
+ "ORDER BY child.id",
+ [{'primary_keys': [1]}]
+ ),
+ )
+
+ result = no_opt()
+ with self.assert_statement_count(testing.db, 1):
+ eq_(
+ result,
+ [Parent(children=[ChildSubclass1(others=[Other()])])]
+ )
+
+ session.expunge_all()
+
+ q = session.query(Parent).options(
+ joinedload(Parent.children.of_type(ChildSubclass1))
+ .joinedload(ChildSubclass1.others)
+ )
+
+ result = self.assert_sql_execution(
+ testing.db,
+ q.all,
+ CompiledSQL(
+ "SELECT parent.id AS parent_id, "
+ "anon_1.child_id AS anon_1_child_id, "
+ "anon_1.child_parent_id AS anon_1_child_parent_id, "
+ "anon_1.child_type AS anon_1_child_type, "
+ "anon_1.child_subclass1_id AS anon_1_child_subclass1_id, "
+ "other_1.id AS other_1_id, "
+ "other_1.child_subclass_id AS other_1_child_subclass_id "
+ "FROM parent LEFT OUTER JOIN "
+ "(SELECT child.id AS child_id, "
+ "child.parent_id AS child_parent_id, "
+ "child.type AS child_type, "
+ "child_subclass1.id AS child_subclass1_id "
+ "FROM child LEFT OUTER JOIN child_subclass1 "
+ "ON child.id = child_subclass1.id) AS anon_1 "
+ "ON parent.id = anon_1.child_parent_id "
+ "LEFT OUTER JOIN other AS other_1 "
+ "ON anon_1.child_subclass1_id = other_1.child_subclass_id",
+ {}
+ ),
+ CompiledSQL(
+ "SELECT child_subclass1.id AS child_subclass1_id, "
+ "child.id AS child_id, child.parent_id AS child_parent_id, "
+ "child.type AS child_type, other_1.id AS other_1_id, "
+ "other_1.child_subclass_id AS other_1_child_subclass_id "
+ "FROM child JOIN child_subclass1 "
+ "ON child.id = child_subclass1.id "
+ "LEFT OUTER JOIN other AS other_1 "
+ "ON child_subclass1.id = other_1.child_subclass_id "
+ "WHERE child.id IN ([EXPANDING_primary_keys]) "
+ "ORDER BY child.id",
+ [{'primary_keys': [1]}]
+ )
+ )
+
+ with self.assert_statement_count(testing.db, 0):
+ eq_(
+ result,
+ [Parent(children=[ChildSubclass1(others=[Other()])])]
+ )
+
+ session.expunge_all()
+
+ result = no_opt()
+ with self.assert_statement_count(testing.db, 1):
+ eq_(
+ result,
+ [Parent(children=[ChildSubclass1(others=[Other()])])]
+ )