on update cascade changes PK, mapper can't find the row

Issue #1671 resolved
Mike Bayer repo owner created an issue

technically, this can't work in any case due to version_id_col, which will be inconsistent. but when removed, the mapper still attempts to update the rows whose PKs have changed - it any case, this needs to be supported so that other row values can be reached on update.

basically this is the one case where _save_obj() has to look at history.added instead of history.deleted when looking for a mutated primary key value. Somehow the dependency rules need to put a clue in there.

import sqlalchemy as sql
from sqlalchemy import orm
from sqlalchemy.ext.declarative import declarative_base

__metaclass__ = type

engine = sql.create_engine("postgresql://scott:tiger@localhost/test", echo=True)
metadata = sql.MetaData(bind=engine)
db = orm.create_session(bind=engine, autocommit=False)
T = declarative_base(metadata=metadata)

class P(T):
   __tablename__ = 'p'
   id = sql.Column(sql.String, primary_key=True)
   version = sql.Column(sql.Integer, nullable=False, default=1)
   cc = orm.relation('C', backref='parent', passive_updates=True)
   __mapper_args__ = {'version_id_col': version}

class C(T):
   __tablename__ = 'c'
   i = sql.Column(sql.String, primary_key=True)
   p = sql.Column(sql.String,
                  sql.ForeignKey('p.id', 
                                onupdate='cascade',
                                ondelete='cascade'),
                                primary_key=False)
   version = sql.Column(sql.Integer, nullable=False, default=1)

   __mapper_args__ = {'version_id_col': version}

metadata.create_all()
P.__table__.delete().execute()

with db.transaction:
   p = P(id='P1', cc=[C(i='C.2')](C(i='C.1'),))
   db.add(p)

#db.expunge_all()
#p = db.query(P).first()

#with db.transaction:
#   p.id = 'P2'
   # ok, no ConcModError

#db.expunge_all()
#p = db.query(P).first()

with db.transaction:
   p.id = 'P3'
   p.cc
   # issues spurious updates, throws ConcModError

Comments (6)

  1. Mike Bayer reporter

    line 229 of OneToManyDP is where the pk sync happens:

    if self._pks_changed(uowcommit, state):
       for child in history.unchanged:
          self._synchronize(state, child, None, False, uowcommit)
    
  2. Mike Bayer reporter

    the attached patch works around, but I'd like to see if the mechanism can be built into sync.populate, possibly place notes into the state itself not the UOW (might not be important), and the approach here can be generalized to fix #1362 as well since sync.populate is used by both inheritance sync as well as dependency sync

  3. Log in to comment