INSERTs and UPDATEs break when polymorphic discriminator is part of the primary key

Issue #1162 resolved
Former user created an issue

It appears that INSERTs and UPDATEs do not work under the following conditions: 1. Table with composite primary key. 2. A hierarchy of classes polymorphically mapped to the table (single inheritance) 3. The polymorphic discriminator column is part of the primary key.

Also, while the 'insert' problem can be worked around (see bellow), fixing the 'update' requires patching SQLAlchemy's code.

Here's a test case:

from sqlalchemy import *
from sqlalchemy.orm import *

engine = create_engine('sqlite://')
metadata = MetaData()

test_table = Table("test", metadata,
    Column("id1", Integer, primary_key=True),
    Column("id2", UnicodeText, primary_key=True),
    Column("data", UnicodeText)
)

class TestBase(object):
    pass

class TestDeriv(TestBase):
    pass

mapper(TestBase, test_table,
    polymorphic_on=test_table.c.id2,
    polymorphic_identity=u"base")

mapper(TestDeriv,
    inherits=TestBase,
    polymorphic_on=test_table.c.id2,
    polymorphic_identity=u"deriv")

metadata.create_all(bind=engine)
session = sessionmaker(bind=engine, autoflush=False)()

t1 = TestDeriv()
t1.id1 = 0
# id2 is implied by instance's type, but we still
# have to assign it explicitly in order the insert to work
t1.id2 = u"deriv"

session.add(t1)
session.commit()
session.clear()
del t1

t1 = session.query(TestDeriv).get((0, u"deriv"))
t1.data = u"foo bar"

# The following line raises ConcurrentModificationError
# Logged SQL shows that id2 is bound to None
session.commit()

The fix (see the attached file, works against rev. 5098) is next to trivial (it only modifies two lines), but I have no way to be sure that it won't break something else.

Comments (4)

  1. Mike Bayer repo owner

    I haven't had time to add new unit tests for this use case, but this approach allows all existing inheritance tests to pass...

    Index: lib/sqlalchemy/orm/mapper.py
    ===================================================================
    --- lib/sqlalchemy/orm/mapper.py        (revision 5092)
    +++ lib/sqlalchemy/orm/mapper.py        (working copy)
    @@ -1114,10 +1114,6 @@
                         for col in mapper._cols_by_table[table](table):
                             if col is mapper.version_id_col:
                                 params[col.key](col.key) = 1
    -                        elif col in pks:
    -                            value = mapper._get_state_attr_by_column(state, col)
    -                            if value is not None:
    -                                params[col.key](col.key) = value
                             elif mapper.polymorphic_on and mapper.polymorphic_on.shares_lineage(col):
                                 if self._should_log_debug:
                                     self.__log_debug("Using polymorphic identity '%s' for insert column '%s'" % (mapper.polymorphic_identity, col.key))
    @@ -1126,6 +1122,10 @@
                                      col.server_default is None) or
                                     value is not None):
                                     params[col.key](col.key) = value
    +                        elif col in pks:
    +                            value = mapper._get_state_attr_by_column(state, col)
    +                            if value is not None:
    +                                params[col.key](col.key) = value
                             else:
                                 value = mapper._get_state_attr_by_column(state, col)
                                 if ((col.default is None and
    @@ -1145,8 +1145,8 @@
                                     (added, unchanged, deleted) = attributes.get_history(state, prop.key, passive=True)
                                     if added:
                                         hasdata = True
    -                        elif mapper.polymorphic_on and mapper.polymorphic_on.shares_lineage(col):
    -                            pass
    +#                        elif mapper.polymorphic_on and mapper.polymorphic_on.shares_lineage(col):
    +#                            pass
                             else:
                                 if post_update_cols is not None and col not in post_update_cols:
                                     if col in pks:
    
  2. Log in to comment