Old relationship collection is modified when assigning a new list

Issue #3913 resolved
Adrian created an issue

I noticed this strange behavior in some code that logs changes and thus stores the old value before updating. Here's a simple case that reproduces it:

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

Base = declarative_base()


class Foo(Base):
    __tablename__ = 'foo'
    id = Column(Integer, primary_key=True)

    def __repr__(self):
        return '<Foo {}>'.format(self.id)


class Bar(Base):
    __tablename__ = 'bar'
    id = Column(Integer, primary_key=True)
    foo_id = Column(Integer, ForeignKey('foo.id'))
    foo = relationship(Foo, lazy=False, backref=backref('bars', lazy=False))

    def __repr__(self):
        return '<Bar {}>'.format(self.id)


e = create_engine('sqlite:///', echo=False)
Base.metadata.create_all(e)
s = Session(e, autoflush=False)

s.add(Foo(bars=[Bar(), Bar()]))
s.commit()

foo = s.query(Foo).first()
print('initial', foo.bars)
old = foo.bars
foo.bars = [Bar(), Bar()]
print('old    ', old)
print('new    ', foo.bars)

s.expire_all()

print('\n--------\n')

foo = s.query(Foo).first()
print('initial', foo.bars)
old = foo.bars
foo.bars = [old[0], Bar()]
print('old    ', old)
print('new    ', foo.bars)

Output:

('initial', [<Bar 1>, <Bar 2>])
('old    ', [])
('new    ', [<Bar None>, <Bar None>])

--------

('initial', [<Bar 1>, <Bar 2>])
('old    ', [<Bar 1>])
('new    ', [<Bar 1>, <Bar None>])

I'd expect the list in 'old' to be the same as it is in 'initial'. After all, I'm assigning a new one so the old one should not be modified by that. Of course I could work around it by unconditionally calling copy() on the old attribute but it feels a bit like a bug to me.

Comments (4)

  1. Mike Bayer repo owner

    I'd advise you assign using list(foo.bars) for now because there is no chance any of these collection mechanics can be changed right now.

  2. Adrian reporter

    Yeah, I'm using copy() atm since my code is generic and can deal with all kinds of attributes - not necessarily relationships. Just wanted to mention it here in case it's a bug.

  3. Mike Bayer repo owner
    • changed milestone to 1.2

    tentative 1.2. this is an issue that impacts almost nobody, except those who care about this behavior, and those people have likely developed their applications to expect the collection to be empty. Therefore a tricky one to push out without consideration.

    https://gerrit.sqlalchemy.org/310

  4. Mike Bayer repo owner

    Don't mutate old collection on bulk replace

    For a bulk replace, assume the old collection is no longer useful to the attribute system and only send the removal events, not actually mutated the collection.

    this changes behavior significantly and also means that dispose_collection now receives the old collection intact.

    Change-Id: Ic2685c85438191f07797d9ef97833a2cfdc4fcc2 Fixes: #3913

    → <<cset 2bfe19152d49>>

  5. Log in to comment