]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Ensure the "orm" plugin is used unconditionally for bundles
authorMike Bayer <mike_mp@zzzcomputing.com>
Mon, 16 Nov 2020 15:15:17 +0000 (10:15 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Mon, 16 Nov 2020 15:45:07 +0000 (10:45 -0500)
This also introduces that the "orm" plugin may be used
when the plugin_subject is None.

Fixed regression where the :paramref:`.Bundle.single_entity` flag would
take effect for a :class:`.Bundle` even though it were not set.
Additionally, this flag is legacy as it only makes sense for the
:class:`_orm.Query` object and not 2.0 style execution.  a deprecation
warning is emitted when used with new-style execution.

Fixes: #5702
Change-Id: If9f43365b9d966cb398890aeba2efa555658a7e5

doc/build/changelog/unreleased_14/5702.rst [new file with mode: 0644]
lib/sqlalchemy/orm/context.py
lib/sqlalchemy/orm/strategies.py
lib/sqlalchemy/orm/util.py
lib/sqlalchemy/sql/traversals.py
test/orm/test_bundle.py

diff --git a/doc/build/changelog/unreleased_14/5702.rst b/doc/build/changelog/unreleased_14/5702.rst
new file mode 100644 (file)
index 0000000..e0a9561
--- /dev/null
@@ -0,0 +1,9 @@
+.. change::
+    :tags: bug, orm
+    :tickets: 5702
+
+    Fixed regression where the :paramref:`.Bundle.single_entity` flag would
+    take effect for a :class:`.Bundle` even though it were not set.
+    Additionally, this flag is legacy as it only makes sense for the
+    :class:`_orm.Query` object and not 2.0 style execution.  a deprecation
+    warning is emitted when used with new-style execution.
index 12759f018c5a3bf0302598579a1d8d9775c059c5..cd654ed3d6eefdc54c77a4dd6a9a4c6dff0a22dd 100644 (file)
@@ -242,7 +242,8 @@ class ORMCompileState(CompileState):
         except KeyError:
             assert False, "statement had 'orm' plugin but no plugin_subject"
         else:
-            bind_arguments["mapper"] = plugin_subject.mapper
+            if plugin_subject:
+                bind_arguments["mapper"] = plugin_subject.mapper
 
         if load_options._autoflush:
             session._autoflush()
@@ -342,10 +343,10 @@ class ORMFromStatementCompileState(ORMCompileState):
         self._polymorphic_adapters = {}
         self._no_yield_pers = set()
 
-        _QueryEntity.to_compile_state(self, statement_container._raw_columns)
-
         self.compile_options = statement_container._compile_options
 
+        _QueryEntity.to_compile_state(self, statement_container._raw_columns)
+
         self.current_path = statement_container._compile_options._current_path
 
         if toplevel and statement_container._with_options:
@@ -494,10 +495,10 @@ class ORMSelectCompileState(ORMCompileState, SelectState):
             )
             self._setup_with_polymorphics()
 
-        _QueryEntity.to_compile_state(self, select_statement._raw_columns)
-
         self.compile_options = select_statement._compile_options
 
+        _QueryEntity.to_compile_state(self, select_statement._raw_columns)
+
         # determine label style.   we can make different decisions here.
         # at the moment, trying to see if we can always use DISAMBIGUATE_ONLY
         # rather than LABEL_STYLE_NONE, and if we can use disambiguate style
@@ -2365,6 +2366,14 @@ class _BundleEntity(_QueryEntity):
                     )
 
         self.supports_single_entity = self.bundle.single_entity
+        if (
+            self.supports_single_entity
+            and not compile_state.compile_options._use_legacy_query_style
+        ):
+            util.warn_deprecated_20(
+                "The Bundle.single_entity flag has no effect when "
+                "using 2.0 style execution."
+            )
 
     @property
     def mapper(self):
index 1d4709726689900e4767d62eb868c21333224f42..7f7bab68255d7260d497c27c787b206ab6586052 100644 (file)
@@ -2730,7 +2730,14 @@ class SelectInLoader(PostLoader, util.MemoizedSlots):
         q = sql.lambda_stmt(
             lambda: sql.select(
                 orm_util.Bundle("pk", *pk_cols), effective_entity
-            ).apply_labels(),
+            )
+            .apply_labels()
+            ._set_propagate_attrs(
+                {
+                    "compile_state_plugin": "orm",
+                    "plugin_subject": effective_entity,
+                }
+            ),
             lambda_cache=self._query_cache,
             global_track_bound_values=False,
             track_on=(self, effective_entity) + tuple(pk_cols),
index 7d1650a1a1fac8830cb3e0448bba9a931c49f0ab..4502d5b8919edc5e58d1113c3d1b50944bba34a0 100644 (file)
@@ -1361,11 +1361,26 @@ class Bundle(ORMColumnsClauseRole, SupportsCloneAnnotations, InspectionAttr):
         # ensure existing entity_namespace remains
         annotations = {"bundle": self, "entity_namespace": self}
         annotations.update(self._annotations)
-        return expression.ClauseList(
-            _literal_as_text_role=roles.ColumnsClauseRole,
-            group=False,
-            *[e._annotations.get("bundle", e) for e in self.exprs]
-        )._annotate(annotations)
+
+        plugin_subject = self.exprs[0]._propagate_attrs.get(
+            "plugin_subject", self.entity
+        )
+        return (
+            expression.ClauseList(
+                _literal_as_text_role=roles.ColumnsClauseRole,
+                group=False,
+                *[e._annotations.get("bundle", e) for e in self.exprs]
+            )
+            ._annotate(annotations)
+            ._set_propagate_attrs(
+                # the Bundle *must* use the orm plugin no matter what.  the
+                # subject can be None but it's much better if it's not.
+                {
+                    "compile_state_plugin": "orm",
+                    "plugin_subject": plugin_subject,
+                }
+            )
+        )
 
     @property
     def clauses(self):
index a24d010cddc31aa18cdad006ecdf6963d567df62..2887813adf3763168680e285b481cd796be6fa3c 100644 (file)
@@ -176,7 +176,9 @@ class HasCacheKey(object):
                             obj["compile_state_plugin"],
                             obj["plugin_subject"]._gen_cache_key(
                                 anon_map, bindparams
-                            ),
+                            )
+                            if obj["plugin_subject"]
+                            else None,
                         )
                     elif meth is InternalTraversal.dp_annotations_key:
                         # obj is here is the _annotations dict.   however, we
index 49feb32f0b61d4509c0b0eb7829ab4f339516f96..956645506fc8c00dbceb7e55d5d8fa3f414b26d4 100644 (file)
@@ -247,7 +247,7 @@ class BundleTest(fixtures.MappedTest, AssertsCompiledSQL):
             ],
         )
 
-    def test_single_entity(self):
+    def test_single_entity_legacy_query(self):
         Data = self.classes.Data
         sess = Session()
 
@@ -258,12 +258,80 @@ class BundleTest(fixtures.MappedTest, AssertsCompiledSQL):
             [("d3d1", "d3d2"), ("d4d1", "d4d2"), ("d5d1", "d5d2")],
         )
 
-    def test_single_entity_future(self):
+    def test_labeled_cols_non_single_entity_legacy_query(self):
+        Data = self.classes.Data
+        sess = Session()
+
+        b1 = Bundle("b1", Data.d1.label("x"), Data.d2.label("y"))
+
+        eq_(
+            sess.query(b1).filter(b1.c.x.between("d3d1", "d5d1")).all(),
+            [(("d3d1", "d3d2"),), (("d4d1", "d4d2"),), (("d5d1", "d5d2"),)],
+        )
+
+    def test_labeled_cols_single_entity_legacy_query(self):
+        Data = self.classes.Data
+        sess = Session()
+
+        b1 = Bundle(
+            "b1", Data.d1.label("x"), Data.d2.label("y"), single_entity=True
+        )
+
+        eq_(
+            sess.query(b1).filter(b1.c.x.between("d3d1", "d5d1")).all(),
+            [("d3d1", "d3d2"), ("d4d1", "d4d2"), ("d5d1", "d5d2")],
+        )
+
+    def test_labeled_cols_as_rows_future(self):
+        Data = self.classes.Data
+        sess = Session()
+
+        b1 = Bundle("b1", Data.d1.label("x"), Data.d2.label("y"))
+
+        stmt = select(b1).filter(b1.c.x.between("d3d1", "d5d1"))
+
+        eq_(
+            sess.execute(stmt).all(),
+            [(("d3d1", "d3d2"),), (("d4d1", "d4d2"),), (("d5d1", "d5d2"),)],
+        )
+
+    def test_labeled_cols_as_scalars_future(self):
+        Data = self.classes.Data
+        sess = Session()
+
+        b1 = Bundle("b1", Data.d1.label("x"), Data.d2.label("y"))
+
+        stmt = select(b1).filter(b1.c.x.between("d3d1", "d5d1"))
+        eq_(
+            sess.execute(stmt).scalars().all(),
+            [("d3d1", "d3d2"), ("d4d1", "d4d2"), ("d5d1", "d5d2")],
+        )
+
+    def test_single_entity_flag_is_legacy_w_future(self):
         Data = self.classes.Data
         sess = Session(testing.db, future=True)
 
+        # flag has no effect
         b1 = Bundle("b1", Data.d1, Data.d2, single_entity=True)
 
+        stmt = select(b1).filter(b1.c.d1.between("d3d1", "d5d1"))
+
+        with testing.expect_deprecated_20(
+            "The Bundle.single_entity flag has no effect when "
+            "using 2.0 style execution."
+        ):
+            rows = sess.execute(stmt).all()
+        eq_(
+            rows,
+            [(("d3d1", "d3d2"),), (("d4d1", "d4d2"),), (("d5d1", "d5d2"),)],
+        )
+
+    def test_as_scalars_future(self):
+        Data = self.classes.Data
+        sess = Session(testing.db)
+
+        b1 = Bundle("b1", Data.d1, Data.d2)
+
         stmt = select(b1).filter(b1.c.d1.between("d3d1", "d5d1"))
         eq_(
             sess.execute(stmt).scalars().all(),
@@ -446,3 +514,25 @@ class BundleTest(fixtures.MappedTest, AssertsCompiledSQL):
             "SELECT row_number() OVER (ORDER BY data.id, data.d1, data.d2) "
             "AS anon_1 FROM data",
         )
+
+    def test_non_mapped_columns_non_single_entity(self):
+        data_table = self.tables.data
+
+        b1 = Bundle("b1", data_table.c.d1, data_table.c.d2)
+
+        sess = Session()
+        eq_(
+            sess.query(b1).filter(b1.c.d1.between("d3d1", "d5d1")).all(),
+            [(("d3d1", "d3d2"),), (("d4d1", "d4d2"),), (("d5d1", "d5d2"),)],
+        )
+
+    def test_non_mapped_columns_single_entity(self):
+        data_table = self.tables.data
+
+        b1 = Bundle("b1", data_table.c.d1, data_table.c.d2, single_entity=True)
+
+        sess = Session()
+        eq_(
+            sess.query(b1).filter(b1.c.d1.between("d3d1", "d5d1")).all(),
+            [("d3d1", "d3d2"), ("d4d1", "d4d2"), ("d5d1", "d5d2")],
+        )