]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- Added a new directive used within the scope of an attribute "set" operation
authorMike Bayer <mike_mp@zzzcomputing.com>
Sat, 1 Feb 2014 00:57:38 +0000 (19:57 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sat, 1 Feb 2014 00:57:38 +0000 (19:57 -0500)
to disable autoflush, in the case that the attribute needs to lazy-load
the "old" value, as in when replacing one-to-one values or some
kinds of many-to-one.  A flush at this point otherwise occurs
at the point that the attribute is None and can cause NULL violations.
[ticket:2921]

doc/build/changelog/changelog_09.rst
lib/sqlalchemy/orm/attributes.py
lib/sqlalchemy/orm/base.py
lib/sqlalchemy/orm/strategies.py
test/orm/test_cascade.py

index 4a4ec10089a253c8ab06ff50b2422967295314c1..8995f7d399e3a4ff2d41fe50373ea53e4354f471 100644 (file)
 .. changelog::
     :version: 0.9.2
 
+    .. change::
+        :tags: bug, orm
+        :tickets: 2921
+
+        Added a new directive used within the scope of an attribute "set" operation
+        to disable autoflush, in the case that the attribute needs to lazy-load
+        the "old" value, as in when replacing one-to-one values or some
+        kinds of many-to-one.  A flush at this point otherwise occurs
+        at the point that the attribute is None and can cause NULL violations.
+
     .. change::
         :tags: feature, orm
 
index e5f8550abded72b713ee9e6457f27db75e753122..7647bf9d0722f630933a4a1ca4e3e00d2610a79a 100644 (file)
@@ -23,7 +23,7 @@ from .base import PASSIVE_NO_RESULT, ATTR_WAS_SET, ATTR_EMPTY, NO_VALUE,\
             NEVER_SET, NO_CHANGE, CALLABLES_OK, SQL_OK, RELATED_OBJECT_OK,\
             INIT_OK, NON_PERSISTENT_OK, LOAD_AGAINST_COMMITTED, PASSIVE_OFF,\
             PASSIVE_RETURN_NEVER_SET, PASSIVE_NO_INITIALIZE, PASSIVE_NO_FETCH,\
-            PASSIVE_NO_FETCH_RELATED, PASSIVE_ONLY_PERSISTENT
+            PASSIVE_NO_FETCH_RELATED, PASSIVE_ONLY_PERSISTENT, NO_AUTOFLUSH
 from .base import state_str, instance_str
 
 @inspection._self_inspects
@@ -761,7 +761,7 @@ class ScalarObjectAttributeImpl(ScalarAttributeImpl):
 
         """
         if self.dispatch._active_history:
-            old = self.get(state, dict_, passive=PASSIVE_ONLY_PERSISTENT)
+            old = self.get(state, dict_, passive=PASSIVE_ONLY_PERSISTENT | NO_AUTOFLUSH)
         else:
             old = self.get(state, dict_, passive=PASSIVE_NO_FETCH)
 
index 577f9ff764df876a4bbd267598d4f08d73a262c7..e973de8972859aab4c3b2d063998edc325a6e398 100644 (file)
@@ -80,6 +80,10 @@ LOAD_AGAINST_COMMITTED = util.symbol("LOAD_AGAINST_COMMITTED",
 """, canonical=32
 )
 
+NO_AUTOFLUSH = util.symbol("NO_AUTOFLUSH",
+"""loader callables should disable autoflush.
+""", canonical=64)
+
 # pre-packaged sets of flags used as inputs
 PASSIVE_OFF = util.symbol("PASSIVE_OFF",
     "Callables can be emitted in all cases.",
index bd9b02d24b8af83d1873a8a586ee4bec735308e0..2c18e81293efc6ff30a866183a2940e05ed63ecd 100644 (file)
@@ -536,9 +536,10 @@ class LazyLoader(AbstractRelationshipLoader):
         pending = not state.key
 
         # don't autoflush on pending
-        if pending:
+        if pending or passive & attributes.NO_AUTOFLUSH:
             q = q.autoflush(False)
 
+
         if state.load_path:
             q = q._with_current_path(state.load_path[self.parent_property])
 
@@ -568,6 +569,7 @@ class LazyLoader(AbstractRelationshipLoader):
 
         q = q.filter(lazy_clause)
 
+
         result = q.all()
         if self.uselist:
             return result
index 615ae815d10dc968ec03c29e505c40c6d6eb3b11..bd6a172866099cb01b1d27493b529fdf2a2caffd 100644 (file)
@@ -552,6 +552,56 @@ class O2OSingleParentTest(_fixtures.FixtureTest):
         assert u1.address is not a1
         assert a1.user is None
 
+class O2OSingleParentNoFlushTest(fixtures.MappedTest):
+    run_inserts = None
+
+    @classmethod
+    def define_tables(cls, metadata):
+        Table('users', metadata,
+              Column('id', Integer, primary_key=True, test_needs_autoincrement=True),
+              Column('name', String(30), nullable=False),
+        )
+
+        Table('addresses', metadata,
+              Column('id', Integer, primary_key=True, test_needs_autoincrement=True),
+              Column('user_id', None, ForeignKey('users.id'), nullable=False),
+              Column('email_address', String(50), nullable=False),
+        )
+
+    @classmethod
+    def setup_classes(cls):
+        class User(cls.Comparable):
+            pass
+        class Address(cls.Comparable):
+            pass
+
+    @classmethod
+    def setup_mappers(cls):
+        Address, addresses, users, User = (cls.classes.Address,
+                                cls.tables.addresses,
+                                cls.tables.users,
+                                cls.classes.User)
+
+        mapper(Address, addresses)
+        mapper(User, users, properties={'address'
+               : relationship(Address, backref=backref('user',
+               single_parent=True, cascade="all, delete-orphan"),
+               uselist=False)})
+
+    def test_replace_attribute_no_flush(self):
+        # test [ticket:2921]
+
+        User, Address = self.classes.User, self.classes.Address
+        a1 = Address(email_address='some address')
+        u1 = User(name='u1', address=a1)
+        sess = Session()
+        sess.add(u1)
+        sess.commit()
+
+        a2 = Address(email_address='asdf')
+        sess.add(a2)
+        u1.address = a2
+
 class NoSaveCascadeFlushTest(_fixtures.FixtureTest):
     """Test related item not present in session, commit proceeds."""
 
@@ -1429,6 +1479,7 @@ class M2OCascadeDeleteOrphanTestTwo(fixtures.MappedTest):
         eq_(sess.query(T2).all(), [])
         eq_(sess.query(T3).all(), [])
 
+
     def test_finds_orphans_twolevel(self):
         T2, T3, T1 = (self.classes.T2,
                                 self.classes.T3,