honor passive on scalar backref assignment

Issue #1483 resolved
Mike Bayer repo owner created an issue

patch:

Index: lib/sqlalchemy/orm/dynamic.py
===================================================================
--- lib/sqlalchemy/orm/dynamic.py   (revision 6172)
+++ lib/sqlalchemy/orm/dynamic.py   (working copy)
@@ -100,7 +100,7 @@
         dict_[self.key](self.key) = True
         return state.committed_state[self.key](self.key)

-    def set(self, state, dict_, value, initiator):
+    def set(self, state, dict_, value, initiator, passive=attributes.PASSIVE_OFF):
         if initiator is self:
             return

Index: lib/sqlalchemy/orm/attributes.py
===================================================================
--- lib/sqlalchemy/orm/attributes.py    (revision 6172)
+++ lib/sqlalchemy/orm/attributes.py    (working copy)
@@ -383,12 +383,12 @@
             return self.initialize(state, dict_)

     def append(self, state, dict_, value, initiator, passive=PASSIVE_OFF):
-        self.set(state, dict_, value, initiator)
+        self.set(state, dict_, value, initiator, passive=passive)

     def remove(self, state, dict_, value, initiator, passive=PASSIVE_OFF):
-        self.set(state, dict_, None, initiator)
+        self.set(state, dict_, None, initiator, passive=passive)

-    def set(self, state, dict_, value, initiator):
+    def set(self, state, dict_, value, initiator, passive=PASSIVE_OFF):
         raise NotImplementedError()

     def get_committed_value(self, state, dict_, passive=PASSIVE_OFF):
@@ -436,7 +436,7 @@
         return History.from_attribute(
             self, state, dict_.get(self.key, NO_VALUE))

-    def set(self, state, dict_, value, initiator):
+    def set(self, state, dict_, value, initiator, passive=PASSIVE_OFF):
         if initiator is self:
             return

@@ -511,7 +511,7 @@
         ScalarAttributeImpl.delete(self, state, dict_)
         state.mutable_dict.pop(self.key)

-    def set(self, state, dict_, value, initiator):
+    def set(self, state, dict_, value, initiator, passive=PASSIVE_OFF):
         if initiator is self:
             return

@@ -559,7 +559,7 @@
             else:
                 return History.from_attribute(self, state, current)

-    def set(self, state, dict_, value, initiator):
+    def set(self, state, dict_, value, initiator, passive=PASSIVE_OFF):
         """Set a value on the given InstanceState.

         `initiator` is the ``InstrumentedAttribute`` that initiated the
@@ -570,8 +570,10 @@
         if initiator is self:
             return

-        # may want to add options to allow the get() here to be passive
-        old = self.get(state, dict_)
+        old = self.get(state, dict_, passive=passive)
+        if old is PASSIVE_NORESULT:
+            old = None
+             
         value = self.fire_replace_event(state, dict_, value, old, initiator)
         dict_[self.key](self.key) = value

@@ -707,7 +709,7 @@
         else:
             collection.remove_with_event(value, initiator)

-    def set(self, state, dict_, value, initiator):
+    def set(self, state, dict_, value, initiator, passive=PASSIVE_OFF):
         """Set a value on the given object.

         `initiator` is the ``InstrumentedAttribute`` that initiated the

here is a test for the common case, throws the usual orphan error without the patch:

from sqlalchemy import *
from sqlalchemy.orm import *
from sqlalchemy.ext.declarative import declarative_base


Base = declarative_base()


class User(Base):
    __tablename__ = 'user'
    id = Column(Integer, primary_key=True)
    name = Column(String)

    site = relation("Site", uselist=False, cascade="all, delete-orphan", backref="user")

class Site(Base):
    __tablename__ = 'site'
    id = Column(Integer, primary_key=True)
    user_id = Column(Integer, ForeignKey('user.id'))
    title = Column(String)

engine = create_engine('sqlite://', echo=True)

Base.metadata.create_all(engine)

sess = sessionmaker(bind=engine)()
v = User(name='victor')
sess.add(v)
sess.commit()

print "---------------"
s = Site(title='asdf')
sess.add(s)
s.user = v
sess.commit()

however, need to ensure things go well for every combination of forwards/backwards ref here. Also is None the most appropriate thing to be using here (i.e. not NO_VALUE, etc.)

Comments (3)

  1. Mike Bayer reporter

    particularly, if you did the really rare thing of setting up a one-to-many based on an association table, then moved an item from one "many" collection to another. does the old behavior result in the association table being updated due to the backref ? if so then this results in backwards incompatible behavior (again very rare).

  2. Log in to comment