.. changelog::
:version: 0.8.2
+ .. change::
+ :tags: bug, orm
+ :tickets: 2750
+
+ A warning is emitted when trying to flush an object of an inherited
+ class where the polymorphic discriminator has been assigned
+ to a value that is invalid for the class.
+
.. change::
:tags: bug, postgresql
:tickets: 2740
from .interfaces import MapperProperty, _InspectionAttr, _MappedAttribute
from .util import _INSTRUMENTOR, _class_to_mapper, \
- _state_mapper, class_mapper, \
- PathRegistry
+ _state_mapper, class_mapper, \
+ PathRegistry, state_str
import sys
properties = util.importlater("sqlalchemy.orm", "properties")
descriptor_props = util.importlater("sqlalchemy.orm", "descriptor_props")
if self.polymorphic_on is not None:
self._set_polymorphic_identity = \
mapper._set_polymorphic_identity
+ self._validate_polymorphic_identity = \
+ mapper._validate_polymorphic_identity
else:
self._set_polymorphic_identity = None
return
state.get_impl(polymorphic_key).set(state, dict_,
state.manager.mapper.polymorphic_identity, None)
+ def _validate_polymorphic_identity(mapper, state, dict_):
+ if dict_[polymorphic_key] not in \
+ mapper._acceptable_polymorphic_identities:
+ util.warn(
+ "Flushing object %s with "
+ "incompatible polymorphic identity %r; the "
+ "object may not refresh and/or load correctly" % (
+ state_str(state),
+ dict_[polymorphic_key]
+ )
+ )
+
self._set_polymorphic_identity = _set_polymorphic_identity
+ self._validate_polymorphic_identity = _validate_polymorphic_identity
else:
self._set_polymorphic_identity = None
+
+ _validate_polymorphic_identity = None
+
+ @_memoized_configured_property
+ def _acceptable_polymorphic_identities(self):
+ identities = set()
+
+ stack = deque([self])
+ while stack:
+ item = stack.popleft()
+ if item.mapped_table is self.mapped_table:
+ identities.add(item.polymorphic_identity)
+ stack.extend(item._inheriting_mappers)
+
+ return identities
+
def _adapt_inherited_property(self, key, prop, init):
if not self.concrete:
self._configure_property(key, prop, init=False, setparent=False)
else:
mapper.dispatch.before_update(mapper, connection, state)
+ if mapper._validate_polymorphic_identity:
+ mapper._validate_polymorphic_identity(mapper, state, dict_)
+
# detect if we have a "pending" instance (i.e. has
# no instance_key attached to it), and another instance
# with the same identity key already exists as persistent.
from sqlalchemy import *
from sqlalchemy import exc as sa_exc, util, event
from sqlalchemy.orm import *
+from sqlalchemy.orm.util import instance_str
from sqlalchemy.orm import exc as orm_exc, attributes
from sqlalchemy.testing.assertsql import AllOf, CompiledSQL
from sqlalchemy.sql import table, column
pass
class C(B):
pass
+ class D(B):
+ pass
mapper(A, table_a,
polymorphic_on=table_a.c.class_name,
polymorphic_identity='b')
mapper(C, table_c, inherits=B,
polymorphic_identity='c')
+ mapper(D, inherits=B,
+ polymorphic_identity='d')
def test_poly_configured_immediate(self):
A, C, B = (self.classes.A,
assert isinstance(sess.query(A).first(), C)
def test_assignment(self):
- C, B = self.classes.C, self.classes.B
+ D, B = self.classes.D, self.classes.B
sess = Session()
b1 = B()
- b1.class_name = 'c'
+ b1.class_name = 'd'
sess.add(b1)
sess.commit()
sess.close()
- assert isinstance(sess.query(B).first(), C)
+ assert isinstance(sess.query(B).first(), D)
+
+ def test_invalid_assignment(self):
+ C, B = self.classes.C, self.classes.B
+
+ sess = Session()
+ c1 = C()
+ c1.class_name = 'b'
+ sess.add(c1)
+ assert_raises_message(
+ sa_exc.SAWarning,
+ "Flushing object %s with incompatible "
+ "polymorphic identity 'b'; the object may not "
+ "refresh and/or load correctly" % instance_str(c1),
+ sess.flush
+ )
class CascadeTest(fixtures.MappedTest):
"""that cascades on polymorphic relationships continue