- [bug] Cls.column.collate("some collation") now
works. [ticket:1776] Also in 0.6.9
+ - [bug] the value of a composite attribute is now
+ expired after an insert or update operation, instead
+ of regenerated in place. This ensures that a
+ column value which is expired within a flush
+ will be loaded first, before the composite
+ is regenerated using that value. [ticket:2309]
+
+ - [bug] The fix in [ticket:2309] also emits the
+ "refresh" event when the composite value is
+ loaded on access, even if all column
+ values were already present, as is appropriate.
+ This fixes the "mutable" extension which relies
+ upon the "load" event to ensure the _parents
+ dictionary is up to date, fixes [ticket:2308].
+ Thanks to Scott Torborg for the test case here.
+
- sql
- [feature] Added accessor to types called "python_type",
returns the rudimentary Python type object
def fget(instance):
dict_ = attributes.instance_dict(instance)
+ state = attributes.instance_state(instance)
if self.key not in dict_:
# key not present. Iterate through related
# ensures they all load.
values = [getattr(instance, key) for key in self._attribute_keys]
- # usually, the load() event will have loaded our key
- # at this point, unless we only loaded relationship()
- # attributes above. Populate here if that's the case.
- if self.key not in dict_ and not _none_set.issuperset(values):
+ # current expected behavior here is that the composite is
+ # created on access if the object is persistent or if
+ # col attributes have non-None. This would be better
+ # if the composite were created unconditionally,
+ # but that would be a behavioral change.
+ if self.key not in dict_ and (
+ state.key is not None or
+ not _none_set.issuperset(values)
+ ):
dict_[self.key] = self.composite_class(*values)
+ state.manager.dispatch.refresh(state, None, [self.key])
return dict_.get(self.key, None)
if k not in dict_:
return
+ #assert self.key not in dict_
dict_[self.key] = self.composite_class(
*[state.dict[key] for key in
self._attribute_keys]
state.dict.pop(self.key, None)
def insert_update_handler(mapper, connection, state):
- state.dict[self.key] = self.composite_class(
- *[state.dict.get(key, None) for key in
- self._attribute_keys]
- )
+ """After an insert or update, some columns may be expired due
+ to server side defaults, or re-populated due to client side
+ defaults. Pop out the composite value here so that it
+ recreates.
+
+ """
+
+ state.dict.pop(self.key, None)
event.listen(self.parent, 'after_insert',
insert_update_handler, raw=True)
-from sqlalchemy import Integer, ForeignKey
+from sqlalchemy import Integer, ForeignKey, String
from sqlalchemy.types import PickleType, TypeDecorator, VARCHAR
from sqlalchemy.orm import mapper, Session, composite
from sqlalchemy.orm.mapper import Mapper
f2.data['a'] = 'c'
assert f2 in sess.dirty
+ def test_unrelated_flush(self):
+ sess = Session()
+ f1 = Foo(data={"a":"b"}, unrelated_data="unrelated")
+ sess.add(f1)
+ sess.flush()
+ f1.unrelated_data = "unrelated 2"
+ sess.flush()
+ f1.data["a"] = "c"
+ sess.commit()
+ eq_(f1.data["a"], "c")
+
def _test_non_mutable(self):
sess = Session()
Column('id', Integer, primary_key=True, test_needs_autoincrement=True),
Column('skip', mutable_pickle),
Column('data', mutable_pickle),
- Column('non_mutable_data', PickleType)
+ Column('non_mutable_data', PickleType),
+ Column('unrelated_data', String(50))
)
def test_non_mutable(self):
Table('foo', metadata,
Column('id', Integer, primary_key=True, test_needs_autoincrement=True),
Column('data', MutationDict.as_mutable(JSONEncodedDict)),
- Column('non_mutable_data', JSONEncodedDict)
+ Column('non_mutable_data', JSONEncodedDict),
+ Column('unrelated_data', String(50))
)
def test_non_mutable(self):
Table('foo', metadata,
Column('id', Integer, primary_key=True, test_needs_autoincrement=True),
Column('data', PickleType),
- Column('non_mutable_data', PickleType)
+ Column('non_mutable_data', PickleType),
+ Column('unrelated_data', String(50))
)
Table('subfoo', metadata,
Table('foo', metadata,
Column('id', Integer, primary_key=True, test_needs_autoincrement=True),
Column('skip', PickleType),
- Column('data', PickleType)
+ Column('data', PickleType),
+ Column('unrelated_data', String(50))
)
class MutableAssociationScalarJSONTest(_MutableDictTestBase, fixtures.MappedTest):
Table('foo', metadata,
Column('id', Integer, primary_key=True, test_needs_autoincrement=True),
- Column('data', JSONEncodedDict)
+ Column('data', JSONEncodedDict),
+ Column('unrelated_data', String(50))
)
Table('foo', metadata,
Column('id', Integer, primary_key=True, test_needs_autoincrement=True),
Column('x', Integer),
- Column('y', Integer)
+ Column('y', Integer),
+ Column('unrelated_data', String(50))
)
def teardown(self):
setattr, f1, 'data', 'foo'
)
+ def test_unrelated_flush(self):
+ sess = Session()
+ f1 = Foo(data=Point(3, 4), unrelated_data="unrelated")
+ sess.add(f1)
+ sess.flush()
+ f1.unrelated_data = "unrelated 2"
+ sess.flush()
+ f1.data.x = 5
+ sess.commit()
+
+ eq_(f1.data.x, 5)
+
class MutableInheritedCompositesTest(_CompositeTestBase, fixtures.MappedTest):
@classmethod
def define_tables(cls, metadata):
Column('id', Integer, primary_key=True,
test_needs_autoincrement=True),
Column('graph_id', Integer,
- ForeignKey('graphs.id'),
- nullable=False),
+ ForeignKey('graphs.id')),
Column('x1', Integer),
Column('y1', Integer),
Column('x2', Integer),
e = sess.query(Edge).get(g.edges[1].id)
eq_(e.end, Point(18, 4))
+ def test_not_none(self):
+ Graph, Edge, Point = (self.classes.Graph,
+ self.classes.Edge,
+ self.classes.Point)
+
+ # current contract. the composite is None
+ # when hasn't been populated etc. on a
+ # pending/transient object.
+ e1 = Edge()
+ assert e1.end is None
+ sess = Session()
+ sess.add(e1)
+
+ # however, once it's persistent, the code as of 0.7.3
+ # would unconditionally populate it, even though it's
+ # all None. I think this usage contract is inconsistent,
+ # and it would be better that the composite is just
+ # created unconditionally in all cases.
+ # but as we are just trying to fix [ticket:2308] and
+ # [ticket:2309] without changing behavior we maintain
+ # that only "persistent" gets the composite with the
+ # Nones
+
+ sess.flush()
+ assert e1.end is not None
+
def test_eager_load(self):
Graph, Point = self.classes.Graph, self.classes.Point
test_needs_autoincrement=True),
Column('x1', Integer, default=2),
Column('x2', Integer),
- Column('x3', Integer, default=15),
+ Column('x3', Integer, server_default="15"),
Column('x4', Integer)
)
class FBComposite(cls.Comparable):
def __init__(self, x1, x2, x3, x4):
- self.x1 = x1
+ self.goofy_x1 = x1
self.x2 = x2
self.x3 = x3
self.x4 = x4
def __composite_values__(self):
- return self.x1, self.x2, self.x3, self.x4
+ return self.goofy_x1, self.x2, self.x3, self.x4
__hash__ = None
def __eq__(self, other):
- return other.x1 == self.x1 and \
+ return other.goofy_x1 == self.goofy_x1 and \
other.x2 == self.x2 and \
other.x3 == self.x3 and \
other.x4 == self.x4
def __ne__(self, other):
return not self.__eq__(other)
-
+ def __repr__(self):
+ return "FBComposite(%r, %r, %r, %r)" % (
+ self.goofy_x1, self.x2, self.x3, self.x4
+ )
mapper(Foobar, foobars, properties=dict(
foob=sa.orm.composite(FBComposite,
foobars.c.x1,
sess.add(f1)
sess.flush()
- assert f1.foob == FBComposite(2, 5, 15, None)
+ eq_(f1.foob, FBComposite(2, 5, 15, None))
f2 = Foobar()
sess.add(f2)
sess.flush()
- assert f2.foob == FBComposite(2, None, 15, None)
+ eq_(f2.foob, FBComposite(2, None, 15, None))
def test_set_composite_values(self):
Foobar, FBComposite = self.classes.Foobar, self.classes.FBComposite
sess.add(f1)
sess.flush()
- assert f1.foob == FBComposite(2, 5, 15, None)
+ eq_(f1.foob, FBComposite(2, 5, 15, None))
+
class MappedSelectTest(fixtures.MappedTest):
@classmethod