diff options
author | mike bayer <mike_mp@zzzcomputing.com> | 2018-06-26 18:00:47 -0400 |
---|---|---|
committer | Gerrit Code Review <gerrit@ci.zzzcomputing.com> | 2018-06-26 18:00:47 -0400 |
commit | a0d1030096af8f6cdd2e2fb7668224ade2d87c8f (patch) | |
tree | dd30091b0d583122c8e1735d31b00357938f7e29 | |
parent | 1827af37cfc7494143ae290da435029043af2372 (diff) | |
parent | f243c00dda1484da97e706b7237670cdce6f10b9 (diff) | |
download | sqlalchemy-a0d1030096af8f6cdd2e2fb7668224ade2d87c8f.tar.gz |
Merge "Ensure BakedQuery is cloned before we add options to it"
-rw-r--r-- | doc/build/changelog/unreleased_12/4286.rst | 8 | ||||
-rw-r--r-- | lib/sqlalchemy/ext/baked.py | 7 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/loading.py | 6 | ||||
-rw-r--r-- | test/orm/inheritance/test_poly_loading.py | 170 |
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()])])] + ) |