]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
perf tweaks
authorMike Bayer <mike_mp@zzzcomputing.com>
Fri, 19 Jun 2020 04:32:00 +0000 (00:32 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Fri, 19 Jun 2020 04:34:20 +0000 (00:34 -0400)
- avoid abc checks in distill_20
- ColumnEntity subclasses are unique to their compile state and
have no querycontext specific state.  They can do a simple memoize of their
fetch_column without using attributes, and they can memoize their
_getter() too so that it goes into the cache, just like
instance_processor() does.
- unify ORMColumnEntity and RawColumnEntity for the row processor part,
add some test coverage for the case where it is used in a from_statement
- do a faster generate if there are no memoized entries
- query._params is always immutabledict
Change-Id: I1e2dfe607a1749b5b434fc11f9348ee631501dfa

lib/sqlalchemy/engine/util.py
lib/sqlalchemy/orm/context.py
lib/sqlalchemy/orm/query.py
lib/sqlalchemy/sql/base.py
test/orm/test_froms.py
test/orm/test_lazy_relations.py
test/orm/test_query.py

index 8fb04646f4e93efa94792e3edef8069e82a4d1ba..fc0260ae24556f9092cb54abd9f41711d1933da6 100644 (file)
@@ -8,6 +8,7 @@
 from .. import exc
 from .. import util
 from ..util import collections_abc
+from ..util import immutabledict
 
 
 def connection_memoize(key):
@@ -85,9 +86,11 @@ _no_kw = util.immutabledict()
 
 
 def _distill_params_20(params):
+    # TODO: this has to be in C
     if params is None:
         return _no_tuple, _no_kw, []
-    elif isinstance(params, collections_abc.MutableSequence):  # list
+    elif isinstance(params, list):
+        # collections_abc.MutableSequence): # avoid abc.__instancecheck__
         if params and not isinstance(
             params[0], (collections_abc.Mapping, tuple)
         ):
@@ -99,7 +102,9 @@ def _distill_params_20(params):
         return tuple(params), _no_kw, params
     elif isinstance(
         params,
-        (collections_abc.Sequence, collections_abc.Mapping),  # tuple or dict
+        (tuple, dict, immutabledict),
+        # avoid abc.__instancecheck__
+        # (collections_abc.Sequence, collections_abc.Mapping),
     ):
         return _no_tuple, params, [params]
     else:
index 588b83571499db3762b654fec2618e7eae11d815..f380229e1d0f8606c345e414f6aaf7090542089d 100644 (file)
@@ -2245,7 +2245,7 @@ class _BundleEntity(_QueryEntity):
 
 
 class _ColumnEntity(_QueryEntity):
-    __slots__ = ()
+    __slots__ = ("_fetch_column", "_row_processor")
 
     @classmethod
     def _for_columns(cls, compile_state, columns, parent_bundle=None):
@@ -2275,6 +2275,44 @@ class _ColumnEntity(_QueryEntity):
     def use_id_for_hash(self):
         return not self.column.type.hashable
 
+    def row_processor(self, context, result):
+        compile_state = context.compile_state
+
+        # the resulting callable is entirely cacheable so just return
+        # it if we already made one
+        if self._row_processor is not None:
+            return self._row_processor
+
+        # retrieve the column that would have been set up in
+        # setup_compile_state, to avoid doing redundant work
+        if self._fetch_column is not None:
+            column = self._fetch_column
+        else:
+            # fetch_column will be None when we are doing a from_statement
+            # and setup_compile_state may not have been called.
+            column = self.column
+
+            # previously, the RawColumnEntity didn't look for from_obj_alias
+            # however I can't think of a case where we would be here and
+            # we'd want to ignore it if this is the from_statement use case.
+            # it's not really a use case to have raw columns + from_statement
+            if compile_state._from_obj_alias:
+                column = compile_state._from_obj_alias.columns[column]
+
+            if column._annotations:
+                # annotated columns perform more slowly in compiler and
+                # result due to the __eq__() method, so use deannotated
+                column = column._deannotate()
+
+        if compile_state.compound_eager_adapter:
+            column = compile_state.compound_eager_adapter.columns[column]
+
+        getter = result._getter(column)
+
+        ret = getter, self._label_name, self._extra_entities
+        self._row_processor = ret
+        return ret
+
 
 class _RawColumnEntity(_ColumnEntity):
     entity_zero = None
@@ -2303,28 +2341,11 @@ class _RawColumnEntity(_ColumnEntity):
             self.column._from_objects[0] if self.column._from_objects else None
         )
         self._extra_entities = (self.expr, self.column)
+        self._fetch_column = self._row_processor = None
 
     def corresponds_to(self, entity):
         return False
 
-    def row_processor(self, context, result):
-        if ("fetch_column", self) in context.attributes:
-            column = context.attributes[("fetch_column", self)]
-        else:
-            column = self.column
-
-            if column._annotations:
-                # annotated columns perform more slowly in compiler and
-                # result due to the __eq__() method, so use deannotated
-                column = column._deannotate()
-
-        compile_state = context.compile_state
-        if compile_state.compound_eager_adapter:
-            column = compile_state.compound_eager_adapter.columns[column]
-
-        getter = result._getter(column)
-        return getter, self._label_name, self._extra_entities
-
     def setup_compile_state(self, compile_state):
         current_adapter = compile_state._get_current_adapter()
         if current_adapter:
@@ -2338,7 +2359,7 @@ class _RawColumnEntity(_ColumnEntity):
             column = column._deannotate()
 
         compile_state.primary_columns.append(column)
-        compile_state.attributes[("fetch_column", self)] = column
+        self._fetch_column = column
 
 
 class _ORMColumnEntity(_ColumnEntity):
@@ -2386,6 +2407,7 @@ class _ORMColumnEntity(_ColumnEntity):
 
         compile_state._has_orm_entities = True
         self.column = column
+        self._fetch_column = self._row_processor = None
 
         self._extra_entities = (self.expr, self.column)
 
@@ -2407,27 +2429,6 @@ class _ORMColumnEntity(_ColumnEntity):
                 self.entity_zero
             ) and entity.common_parent(self.entity_zero)
 
-    def row_processor(self, context, result):
-        compile_state = context.compile_state
-
-        if ("fetch_column", self) in context.attributes:
-            column = context.attributes[("fetch_column", self)]
-        else:
-            column = self.column
-            if compile_state._from_obj_alias:
-                column = compile_state._from_obj_alias.columns[column]
-
-            if column._annotations:
-                # annotated columns perform more slowly in compiler and
-                # result due to the __eq__() method, so use deannotated
-                column = column._deannotate()
-
-        if compile_state.compound_eager_adapter:
-            column = compile_state.compound_eager_adapter.columns[column]
-
-        getter = result._getter(column)
-        return getter, self._label_name, self._extra_entities
-
     def setup_compile_state(self, compile_state):
         current_adapter = compile_state._get_current_adapter()
         if current_adapter:
@@ -2460,5 +2461,4 @@ class _ORMColumnEntity(_ColumnEntity):
             compile_state._fallback_from_clauses.append(ezero.selectable)
 
         compile_state.primary_columns.append(column)
-
-        compile_state.attributes[("fetch_column", self)] = column
+        self._fetch_column = column
index cdad55320c337d19d326216dfaa99428ca31bc6a..5def082b4f22205811a00aaceb75d91607edc3b7 100644 (file)
@@ -1637,8 +1637,7 @@ class Query(
                 "params() takes zero or one positional argument, "
                 "which is a dictionary."
             )
-        params = dict(self.load_options._params)
-        params.update(kwargs)
+        params = self.load_options._params.union(kwargs)
         self.load_options += {"_params": params}
 
     @_generative
index 5f2ce8f14fcb0a43669b766cc1b7e15df9f3a4f0..4c603b6ddec852ed7802ca7b04a69752c0c30fc5 100644 (file)
@@ -495,8 +495,14 @@ class Generative(HasMemoized):
 
     def _generate(self):
         skip = self._memoized_keys
-        s = self.__class__.__new__(self.__class__)
-        s.__dict__ = {k: v for k, v in self.__dict__.items() if k not in skip}
+        cls = self.__class__
+        s = cls.__new__(cls)
+        if skip:
+            s.__dict__ = {
+                k: v for k, v in self.__dict__.items() if k not in skip
+            }
+        else:
+            s.__dict__ = self.__dict__.copy()
         return s
 
 
index 1fde343d8c9a6ddd386fdddfa9407555afe77b1e..62227ece277e488297b6227cb9b059cbcca38541 100644 (file)
@@ -2310,6 +2310,18 @@ class MixedEntitiesTest(QueryTest, AssertsCompiledSQL):
         )
         assert result == expected
 
+    def test_multi_columns_3(self):
+        User = self.classes.User
+        users = self.tables.users
+
+        sess = create_session()
+
+        q = sess.query(User.id, User.name)
+        stmt = select([users]).order_by(users.c.id)
+        q = q.from_statement(stmt)
+
+        eq_(q.all(), [(7, "jack"), (8, "ed"), (9, "fred"), (10, "chuck")])
+
     def test_raw_columns(self):
         addresses, users, User = (
             self.tables.addresses,
index 65158dbd4a62ec9e57cc2e6d7995a72cf2522dfb..c30086bea0d0a758a9a76acb6d216c262b9e85d0 100644 (file)
@@ -439,9 +439,7 @@ class LazyTest(_fixtures.FixtureTest):
             def process_query_conditionally(self, query):
                 """process query during a lazyload"""
                 canary()
-                params = dict(query.load_options._params)
-                query.load_options += {"_params": params}
-                query.load_options._params.update(dict(name=self.crit))
+                query.params.non_generative(query, dict(name=self.crit))
 
         s = Session()
         ed = s.query(User).options(MyOption("ed")).filter_by(name="ed").one()
index a7cc82ddfa1d6748b41f062d9967ce5e76d4608f..c2f0c4424b8fb6b1f18b5b102d9066c0fd301421 100644 (file)
@@ -4440,6 +4440,26 @@ class TextTest(QueryTest, AssertsCompiledSQL):
 
         self.assert_sql_count(testing.db, go, 1)
 
+    def test_textual_select_orm_columns(self):
+        # test that columns using column._label match, as well as that
+        # ordering doesn't matter.
+        User = self.classes.User
+        Address = self.classes.Address
+        users = self.tables.users
+        addresses = self.tables.addresses
+
+        s = create_session()
+        q = s.query(User.name, User.id, Address.id).from_statement(
+            text(
+                "select users.name AS users_name, users.id AS users_id, "
+                "addresses.id AS addresses_id FROM users JOIN addresses "
+                "ON users.id = addresses.user_id WHERE users.id=8 "
+                "ORDER BY addresses.id"
+            ).columns(users.c.name, users.c.id, addresses.c.id)
+        )
+
+        eq_(q.all(), [("ed", 8, 2), ("ed", 8, 3), ("ed", 8, 4)])
+
     @testing.combinations(
         (
             False,
@@ -4564,6 +4584,21 @@ class TextTest(QueryTest, AssertsCompiledSQL):
             [User(id=7), User(id=8), User(id=9), User(id=10)],
         )
 
+    def test_columns_via_textasfrom_from_statement(self):
+        User = self.classes.User
+        s = create_session()
+
+        eq_(
+            s.query(User.id, User.name)
+            .from_statement(
+                text("select * from users order by id").columns(
+                    id=Integer, name=String
+                )
+            )
+            .all(),
+            [(7, "jack"), (8, "ed"), (9, "fred"), (10, "chuck")],
+        )
+
     def test_via_textasfrom_use_mapped_columns(self):
         User = self.classes.User
         s = create_session()