]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Support state expiration for with_expression(); rename deferred_expression
authorMike Bayer <mike_mp@zzzcomputing.com>
Mon, 26 Jun 2017 17:24:22 +0000 (13:24 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Mon, 26 Jun 2017 17:49:49 +0000 (13:49 -0400)
The attributeimpl for a deferred_expression does not
support a scalar loader, add new configurability so that
the impl can have this flag turned off.   Document
that the with_expression() system currently does not
offer any deferred loading.

To eliminate confusion over "deferred", which refers to
lazy loading of column attributes, and "with_expression",
which refers to an attribute that is explicitly at
query time only, rename deferred_expression to query_expression.

Change-Id: I07c4a050ed68c79ccbde9492e9de1630b7470d74

doc/build/changelog/migration_12.rst
doc/build/orm/loading_columns.rst
doc/build/orm/mapped_sql_expr.rst
lib/sqlalchemy/orm/__init__.py
lib/sqlalchemy/orm/attributes.py
lib/sqlalchemy/orm/dynamic.py
lib/sqlalchemy/orm/strategies.py
lib/sqlalchemy/orm/strategy_options.py
test/orm/test_deferred.py

index add12a50c304d73ad3c3ea30fe63b9c2aced95ec..52990d3fbb9ee8f62a2c5764c05a893a7d209e0f 100644 (file)
@@ -218,12 +218,12 @@ are loaded with additional SELECT statements:
 ORM attributes that can receive ad-hoc SQL expressions
 ------------------------------------------------------
 
-A new ORM attribute type :func:`.orm.deferred_expression` is added which
+A new ORM attribute type :func:`.orm.query_expression` is added which
 is similar to :func:`.orm.deferred`, except its SQL expression
 is determined at query time using a new option :func:`.orm.with_expression`;
 if not specified, the attribute defaults to ``None``::
 
-    from sqlalchemy.orm import deferred_expression
+    from sqlalchemy.orm import query_expression
     from sqlalchemy.orm import with_expression
 
     class A(Base):
@@ -233,7 +233,7 @@ if not specified, the attribute defaults to ``None``::
         y = Column(Integer)
 
         # will be None normally...
-        expr = deferred_expression()
+        expr = query_expression()
 
     # but let's give it x + y
     a1 = session.query(A).options(
@@ -242,7 +242,7 @@ if not specified, the attribute defaults to ``None``::
 
 .. seealso::
 
-    :ref:`mapper_deferred_expression`
+    :ref:`mapper_query_expression`
 
 :ticket:`3058`
 
index 289ba5c332e8a6e05ba2a2f8ce5f46a168e5874d..8766dac1b483d02fbddcaa97213d43564b145f3d 100644 (file)
@@ -136,7 +136,7 @@ Column Deferral API
 
 .. autofunction:: deferred
 
-.. autofunction:: deferred_expression
+.. autofunction:: query_expression
 
 .. autofunction:: load_only
 
index 7d0481d9cc5b75880bd5d12e1b9f56c92880a1f4..425a4ece82b2d84a41c31c00a3f8299fbca55b54 100644 (file)
@@ -206,7 +206,7 @@ The plain descriptor approach is useful as a last resort, but is less performant
 in the usual case than both the hybrid and column property approaches, in that
 it needs to emit a SQL query upon each access.
 
-.. _mapper_deferred_expression:
+.. _mapper_querytime_expression:
 
 Query-time SQL expressions as mapped attributes
 -----------------------------------------------
@@ -223,11 +223,11 @@ The above query returns tuples of the form ``(A object, integer)``.
 An option exists which can apply the ad-hoc ``A.x + A.y`` expression to the
 returned ``A`` objects instead of as a separate tuple entry; this is the
 :func:`.with_expression` query option in conjunction with the
-:func:`.deferred_expression` attribute mapping.    The class is mapped
+:func:`.query_expression` attribute mapping.    The class is mapped
 to include a placeholder attribute where any particular SQL expression
 may be applied::
 
-    from sqlalchemy.orm import deferred_expression
+    from sqlalchemy.orm import query_expression
 
     class A(Base):
         __tablename__ = 'a'
@@ -235,7 +235,7 @@ may be applied::
         x = Column(Integer)
         y = Column(Integer)
 
-        expr = deferred_expression()
+        expr = query_expression()
 
 We can then query for objects of type ``A``, applying an arbitrary
 SQL expression to be populated into ``A.expr``::
@@ -244,27 +244,37 @@ SQL expression to be populated into ``A.expr``::
     q = session.query(A).options(
         with_expression(A.expr, A.x + A.y))
 
-The :func:`.deferred_expression` mapping has these caveats:
+The :func:`.query_expression` mapping has these caveats:
 
-* On an object where :func:`.deferred_expression` were not used to populate
+* On an object where :func:`.query_expression` were not used to populate
   the attribute, the attribute on an object instance will have the value
   ``None``.
 
+* The query_expression value **does not refresh when the object is
+  expired**.  Once the object is expired, either via :meth:`.Session.expire`
+  or via the expire_on_commit behavior of :meth:`.Session.commit`, the value is
+  removed from the attribute and will return ``None`` on subsequent access.
+  Only by running a new :class:`.Query` that touches the object which includes
+  a new :func:`.with_expression` directive will the attribute be set to a
+  non-None value.
+
 * The mapped attribute currently **cannot** be applied to other parts of the
-  query and make use of the ad-hoc expression; that is, this won't work::
+  query, such as the WHERE clause, the ORDER BY clause, and make use of the
+  ad-hoc expression; that is, this won't work::
 
+    # wont work
     q = session.query(A).options(
         with_expression(A.expr, A.x + A.y)
-    ).filter(A.expr > 5)
+    ).filter(A.expr > 5).order_by(A.expr)
 
-  The ``A.expr`` expression will resolve to NULL in the above WHERE clause.
-  To use the expression throughout the query, assign to a variable and
-  use that::
+  The ``A.expr`` expression will resolve to NULL in the above WHERE clause
+  and ORDER BY clause. To use the expression throughout the query, assign to a
+  variable and use that::
 
     a_expr = A.x + A.y
     q = session.query(A).options(
         with_expression(A.expr, a_expr)
-    ).filter(a_expr > 5)
+    ).filter(a_expr > 5).order_by(a_expr)
 
 .. versionadded:: 1.2
 
index 80c27e392de9dc37fd9aec3ace623b82b2026377..846d2c2a759a379b9f13df0fe3a2c4daf4a51db7 100644 (file)
@@ -178,18 +178,18 @@ def deferred(*columns, **kw):
     return ColumnProperty(deferred=True, *columns, **kw)
 
 
-def deferred_expression():
+def query_expression():
     """Indicate an attribute that populates from a query-time SQL expression.
 
     .. versionadded:: 1.2
 
     .. seealso::
 
-        :ref:`mapper_deferred_expression`
+        :ref:`mapper_query_expression`
 
     """
     prop = ColumnProperty(_sql.null())
-    prop.strategy_key = (("deferred_expression", True),)
+    prop.strategy_key = (("query_expression", True),)
     return prop
 
 mapper = public_factory(Mapper, ".orm.mapper")
index f3a5c4735397a64ed0f3497f4bd6f0c0e8156b47..01cb0056ee9bb771cb3d199bb9dd122b62bc3747 100644 (file)
@@ -388,7 +388,7 @@ class AttributeImpl(object):
                  callable_, dispatch, trackparent=False, extension=None,
                  compare_function=None, active_history=False,
                  parent_token=None, expire_missing=True,
-                 send_modified_events=True,
+                 send_modified_events=True, accepts_scalar_loader=None,
                  **kwargs):
         r"""Construct an AttributeImpl.
 
@@ -449,6 +449,11 @@ class AttributeImpl(object):
         else:
             self.is_equal = compare_function
 
+        if accepts_scalar_loader is not None:
+            self.accepts_scalar_loader = accepts_scalar_loader
+        else:
+            self.accepts_scalar_loader = self.default_accepts_scalar_loader
+
         # TODO: pass in the manager here
         # instead of doing a lookup
         attr = manager_of_class(class_)[key]
@@ -465,7 +470,7 @@ class AttributeImpl(object):
     __slots__ = (
         'class_', 'key', 'callable_', 'dispatch', 'trackparent',
         'parent_token', 'send_modified_events', 'is_equal', 'expire_missing',
-        '_modified_token'
+        '_modified_token', 'accepts_scalar_loader'
     )
 
     def _init_modified_token(self):
@@ -657,7 +662,7 @@ class AttributeImpl(object):
 class ScalarAttributeImpl(AttributeImpl):
     """represents a scalar value-holding InstrumentedAttribute."""
 
-    accepts_scalar_loader = True
+    default_accepts_scalar_loader = True
     uses_objects = False
     supports_population = True
     collection = False
@@ -743,7 +748,7 @@ class ScalarObjectAttributeImpl(ScalarAttributeImpl):
 
     """
 
-    accepts_scalar_loader = False
+    default_accepts_scalar_loader = False
     uses_objects = True
     supports_population = True
     collection = False
@@ -867,7 +872,7 @@ class CollectionAttributeImpl(AttributeImpl):
     semantics to the orm layer independent of the user data implementation.
 
     """
-    accepts_scalar_loader = False
+    default_accepts_scalar_loader = False
     uses_objects = True
     supports_population = True
     collection = True
index 9f99740d9a45a496f53fc5389ed2122946508252..bb63067b05f7fb8aba5f67328e273c134d961b9c 100644 (file)
@@ -44,7 +44,7 @@ class DynaLoader(strategies.AbstractRelationshipLoader):
 
 class DynamicAttributeImpl(attributes.AttributeImpl):
     uses_objects = True
-    accepts_scalar_loader = False
+    default_accepts_scalar_loader = False
     supports_population = False
     collection = False
 
index 4e7636cf83b87b50fae58497ed945b5fe01bfa3d..4b9eb3b0fd7736e266d561a8ab2b02d4063d8fc4 100644 (file)
@@ -195,7 +195,7 @@ class ColumnLoader(LoaderStrategy):
 
 
 @log.class_logger
-@properties.ColumnProperty.strategy_for(deferred_expression=True)
+@properties.ColumnProperty.strategy_for(query_expression=True)
 class ExpressionColumnLoader(ColumnLoader):
     def __init__(self, parent, strategy_key):
         super(ExpressionColumnLoader, self).__init__(parent, strategy_key)
@@ -235,6 +235,15 @@ class ExpressionColumnLoader(ColumnLoader):
             else:
                 populators["expire"].append((self.key, True))
 
+    def init_class_attribute(self, mapper):
+        self.is_class_level = True
+
+        _register_attribute(
+            self.parent_property, mapper, useobject=False,
+            compare_function=self.columns[0].type.compare_values,
+            accepts_scalar_loader=False
+        )
+
 
 @log.class_logger
 @properties.ColumnProperty.strategy_for(deferred=True, instrument=True)
index 2cdf6ba957697ecca48eb26ad072662c666064f3..796f859f810fabbbbe27a98f9d4f6e40edc333e4 100644 (file)
@@ -1356,7 +1356,7 @@ from .util import _orm_full_deannotate
 def with_expression(loadopt, key, expression):
     r"""Apply an ad-hoc SQL expression to a "deferred expression" attribute.
 
-    This option is used in conjunction with the :func:`.orm.deferred_expression`
+    This option is used in conjunction with the :func:`.orm.query_expression`
     mapper-level construct that indicates an attribute which should be the
     target of an ad-hoc SQL expression.
 
@@ -1375,7 +1375,7 @@ def with_expression(loadopt, key, expression):
 
     .. seealso::
 
-        :ref:`mapper_deferred_expression`
+        :ref:`mapper_query_expression`
 
     """
 
@@ -1384,7 +1384,7 @@ def with_expression(loadopt, key, expression):
 
     return loadopt.set_column_strategy(
         (key, ),
-        {"deferred_expression": True},
+        {"query_expression": True},
         opts={"expression": expression}
     )
 
index 9c2dc4c8b73e26e442b2f3e7bcb1af44163d0d2e..9295399bccf4b00a9b61aac0f461d2ab961f90d8 100644 (file)
@@ -3,7 +3,7 @@ from sqlalchemy import testing, util
 from sqlalchemy.orm import mapper, deferred, defer, undefer, Load, \
     load_only, undefer_group, create_session, synonym, relationship, Session,\
     joinedload, defaultload, aliased, contains_eager, with_polymorphic, \
-    deferred_expression, with_expression
+    query_expression, with_expression
 from sqlalchemy.testing import eq_, AssertsCompiledSQL, assert_raises_message
 from test.orm import _fixtures
 from sqlalchemy.testing.schema import Column
@@ -886,7 +886,7 @@ class WithExpressionTest(fixtures.DeclarativeMappedTest):
             x = Column(Integer)
             y = Column(Integer)
 
-            my_expr = deferred_expression()
+            my_expr = query_expression()
 
             bs = relationship("B", order_by="B.id")
 
@@ -897,7 +897,7 @@ class WithExpressionTest(fixtures.DeclarativeMappedTest):
             p = Column(Integer)
             q = Column(Integer)
 
-            b_expr = deferred_expression()
+            b_expr = query_expression()
 
     @classmethod
     def insert_data(cls):
@@ -972,4 +972,51 @@ class WithExpressionTest(fixtures.DeclarativeMappedTest):
         def go():
             eq_(a1.my_expr, None)
 
-        self.assert_sql_count(testing.db, go, 0)
\ No newline at end of file
+        self.assert_sql_count(testing.db, go, 0)
+
+    def test_dont_explode_on_expire_individual(self):
+        A = self.classes.A
+
+        s = Session()
+        q = s.query(A).options(
+            with_expression(A.my_expr, A.x + A.y)).filter(A.x > 1).\
+            order_by(A.id)
+
+        a1 = q.first()
+
+        eq_(a1.my_expr, 5)
+
+        s.expire(a1, ['my_expr'])
+
+        eq_(a1.my_expr, None)
+
+        # comes back
+        q = s.query(A).options(
+            with_expression(A.my_expr, A.x + A.y)).filter(A.x > 1).\
+            order_by(A.id)
+        q.first()
+        eq_(a1.my_expr, 5)
+
+    def test_dont_explode_on_expire_whole(self):
+        A = self.classes.A
+
+        s = Session()
+        q = s.query(A).options(
+            with_expression(A.my_expr, A.x + A.y)).filter(A.x > 1).\
+            order_by(A.id)
+
+        a1 = q.first()
+
+        eq_(a1.my_expr, 5)
+
+        s.expire(a1)
+
+        eq_(a1.my_expr, None)
+
+        # comes back
+        q = s.query(A).options(
+            with_expression(A.my_expr, A.x + A.y)).filter(A.x > 1).\
+            order_by(A.id)
+        q.first()
+        eq_(a1.my_expr, 5)
+