]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- The "evaulator" for query.update()/delete() won't work with multi-table
authorMike Bayer <mike_mp@zzzcomputing.com>
Tue, 8 Jul 2014 23:17:45 +0000 (19:17 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Tue, 8 Jul 2014 23:17:45 +0000 (19:17 -0400)
updates, and needs to be set to `synchronize_session=False` or
`synchronize_session='fetch'`; this is a warning in 0.9.7, an
exception in 1.0.

fixes #3117

doc/build/changelog/changelog_09.rst
lib/sqlalchemy/orm/evaluator.py
lib/sqlalchemy/orm/persistence.py
test/orm/test_update_delete.py

index 687281c51d5c9e1bf59f24975dfd35327e740b43..d202fe804b37680067a7fdd7e8f8ed71b9118ba1 100644 (file)
     :version: 0.9.7
     :released:
 
+    .. change::
+        :tags: bug, orm
+        :tickets: 3117
+
+        The "evaulator" for query.update()/delete() won't work with multi-table
+        updates, and needs to be set to `synchronize_session=False` or
+        `synchronize_session='fetch'`; a warning is now emitted.  In
+        1.0 this will be promoted to a full exception.
+
     .. change::
         :tags: feature, postgresql
         :versions: 1.0.0
index e1dd9606883339bf2545a6654e5d9b37eb1d005b..8a7e4ac5357270d9516c238ecd377d5bbd5bc968 100644 (file)
@@ -6,7 +6,7 @@
 
 import operator
 from ..sql import operators
-
+from .. import util
 
 class UnevaluatableError(Exception):
     pass
@@ -25,6 +25,9 @@ _notimplemented_ops = set(getattr(operators, op)
 
 
 class EvaluatorCompiler(object):
+    def __init__(self, target_cls=None):
+        self.target_cls = target_cls
+
     def process(self, clause):
         meth = getattr(self, "visit_%s" % clause.__visit_name__, None)
         if not meth:
@@ -46,10 +49,22 @@ class EvaluatorCompiler(object):
 
     def visit_column(self, clause):
         if 'parentmapper' in clause._annotations:
-            key = clause._annotations['parentmapper'].\
-              _columntoproperty[clause].key
+            parentmapper = clause._annotations['parentmapper']
+            if self.target_cls and not issubclass(
+                    self.target_cls, parentmapper.class_):
+                util.warn(
+                    "Can't do in-Python evaluation of criteria against "
+                    "alternate class %s; "
+                    "expiration of objects will not be accurate "
+                    "and/or may fail.  synchronize_session should be set to "
+                    "False or 'fetch'. "
+                    "This warning will be an exception "
+                    "in 1.0." % parentmapper.class_
+                )
+            key = parentmapper._columntoproperty[clause].key
         else:
             key = clause.key
+
         get_corresponding_attr = operator.attrgetter(key)
         return lambda obj: get_corresponding_attr(obj)
 
index 95a37ef2cb0ae58c2d8bff96da0e84f45a7e5bd6..a61ac75d9011af057f00e77a165b6d9780dfcb1e 100644 (file)
@@ -927,8 +927,10 @@ class BulkEvaluate(BulkUD):
 
     def _do_pre_synchronize(self):
         query = self.query
+        target_cls = query._mapper_zero().class_
+
         try:
-            evaluator_compiler = evaluator.EvaluatorCompiler()
+            evaluator_compiler = evaluator.EvaluatorCompiler(target_cls)
             if query.whereclause is not None:
                 eval_condition = evaluator_compiler.process(
                                                 query.whereclause)
@@ -943,7 +945,6 @@ class BulkEvaluate(BulkUD):
                     "Could not evaluate current criteria in Python. "
                     "Specify 'fetch' or False for the "
                     "synchronize_session parameter.")
-        target_cls = query._mapper_zero().class_
 
         #TODO: detect when the where clause is a trivial primary key match
         self.matched_objects = [
index bf72b49f6f20a4789624e2d5ea80a22897554536..f16339faea2ae1f3910fc9989698271436ff5de5 100644 (file)
@@ -21,12 +21,10 @@ class UpdateDeleteTest(fixtures.MappedTest):
                         test_needs_autoincrement=True),
               Column('name', String(32)),
               Column('age', Integer))
-
     @classmethod
     def setup_classes(cls):
         class User(cls.Comparable):
             pass
-
     @classmethod
     def insert_data(cls):
         users = cls.tables.users
@@ -619,6 +617,27 @@ class UpdateDeleteFromTest(fixtures.MappedTest):
                 ])
         )
 
+    def test_no_eval_against_multi_table_criteria(self):
+        User = self.classes.User
+        Document = self.classes.Document
+
+        s = Session()
+
+        q = s.query(User).filter(User.id == Document.user_id)
+        assert_raises_message(
+            exc.SAWarning,
+            r"Can't do in-Python evaluation of criteria against alternate "
+            r"class .*Document.*; "
+            "expiration of objects will not be accurate "
+            "and/or may fail.  synchronize_session should be set to "
+            "False or 'fetch'. "
+            "This warning will be an exception "
+            "in 1.0.",
+            q.update,
+            {"name": "ed"}
+        )
+
+
     @testing.requires.update_where_target_in_subquery
     def test_update_using_in(self):
         Document = self.classes.Document
@@ -675,7 +694,7 @@ class UpdateDeleteFromTest(fixtures.MappedTest):
             filter(User.id == 2).update({
                     Document.samename: 'd_samename',
                     User.samename: 'u_samename'
-                }
+                }, synchronize_session=False
             )
         eq_(
             s.query(User.id, Document.samename, User.samename).