#552
- fix to using distinct() or distinct=True in combination with
join() and similar
- - corresponding to label/bindparam name generataion, eager loaders
+ - corresponding to label/bindparam name generation, eager loaders
generate deterministic names for the aliases they create using
md5 hashes.
- improved/fixed custom collection classes when giving it "set"/
"sets.Set" classes or subclasses (was still looking for append()
methods on them during lazy loads)
+ - restored old "column_property()" ORM function (used to be called
+ "column()") to force any column expression to be added as a property
+ on a mapper, particularly those that aren't present in the mapped
+ selectable. this allows "scalar expressions" of any kind to be
+ added as relations (though they have issues with eager loads).
- fix to many-to-many relationships targeting polymorphic mappers
[ticket:533]
- making progress with session.merge() as well as combining its
from sqlalchemy.orm.session import Session as create_session
from sqlalchemy.orm.session import object_session, attribute_manager
-__all__ = ['relation', 'backref', 'eagerload', 'lazyload', 'noload', 'deferred', 'defer', 'undefer', 'extension',
+__all__ = ['relation', 'column_property', 'backref', 'eagerload', 'lazyload', 'noload', 'deferred', 'defer', 'undefer', 'extension',
'mapper', 'clear_mappers', 'compile_mappers', 'clear_mapper', 'class_mapper', 'object_mapper', 'MapperExtension', 'Query',
'cascade_mappers', 'polymorphic_union', 'create_session', 'synonym', 'contains_alias', 'contains_eager', 'EXT_PASS', 'object_session'
]
raise exceptions.ArgumentError("relation(class, table, **kwargs) is deprecated. Please use relation(class, **kwargs) or relation(mapper, **kwargs).")
return _relation_loader(*args, **kwargs)
+def column_property(*args, **kwargs):
+ """Provide a column-level property for use with a Mapper.
+
+ Normally, custom column-level properties that represent columns
+ directly or indirectly present within the mapped selectable
+ can just be added to the ``properties`` dictionary directly,
+ in which case this function's usage is not necessary.
+
+ In the case of a ``ColumnElement`` directly present within the
+ ``properties`` dictionary, the given column is converted to be the exact column
+ located within the mapped selectable, in the case that the mapped selectable
+ is not the exact parent selectable of the given column, but shares a common
+ base table relationship with that column.
+
+ Use this function when the column expression being added does not
+ correspond to any single column within the mapped selectable,
+ such as a labeled function or scalar-returning subquery, to force the element
+ to become a mapped property regardless of it not being present within the
+ mapped selectable.
+
+ Note that persistence of instances is driven from the collection of columns
+ within the mapped selectable, so column properties attached to a Mapper which have
+ no direct correspondence to the mapped selectable will effectively be non-persisted
+ attributes.
+ """
+ return properties.ColumnProperty(*args, **kwargs)
+
def _relation_loader(mapper, secondary=None, primaryjoin=None, secondaryjoin=None, lazy=True, **kwargs):
return properties.PropertyLoader(mapper, secondary, primaryjoin, secondaryjoin, lazy=lazy, **kwargs)
self._compile_all()
self._compile_property(key, prop, init=True)
- def _create_prop_from_column(self, column, skipmissing=False):
- if sql.is_column(column):
- try:
- column = self.mapped_table.corresponding_column(column)
- except KeyError:
- if skipmissing:
- return
- raise exceptions.ArgumentError("Column '%s' is not represented in mapper's table" % prop._label)
- return ColumnProperty(column)
- elif isinstance(column, list) and sql.is_column(column[0]):
- try:
- column = [self.mapped_table.corresponding_column(c) for c in column]
- except KeyError, e:
- # TODO: want to take the columns we have from this
- if skipmissing:
- return
- raise exceptions.ArgumentError("Column '%s' is not represented in mapper's table" % e.args[0])
- return ColumnProperty(*column)
- else:
+ def _create_prop_from_column(self, column):
+ column = util.to_list(column)
+ if not sql.is_column(column[0]):
return None
+ mapped_column = []
+ for c in column:
+ mc = self.mapped_table.corresponding_column(c, raiseerr=False)
+ if not mc:
+ raise exceptions.ArgumentError("Column '%s' is not represented in mapper's table. Use the `column_property()` function to force this column to be mapped as a read-only attribute." % str(c))
+ mapped_column.append(mc)
+ return ColumnProperty(*mapped_column)
def _adapt_inherited_property(self, key, prop):
if not self.concrete:
self._compile_property(key, prop, init=False, setparent=False)
# TODO: concrete properties dont adapt at all right now....will require copies of relations() etc.
- def _compile_property(self, key, prop, init=True, skipmissing=False, setparent=True):
+ def _compile_property(self, key, prop, init=True, setparent=True):
"""Add a ``MapperProperty`` to this or another ``Mapper``,
including configuration of the property.
self.__log("_compile_property(%s, %s)" % (key, prop.__class__.__name__))
if not isinstance(prop, MapperProperty):
- col = self._create_prop_from_column(prop, skipmissing=skipmissing)
+ col = self._create_prop_from_column(prop)
if col is None:
raise exceptions.ArgumentError("%s=%r is not an instance of MapperProperty or Column" % (key, prop))
prop = col
def setup_query(self, context, eagertable=None, **kwargs):
for c in self.columns:
if eagertable is not None:
- context.statement.append_column(eagertable.corresponding_column(c))
+ conv = eagertable.corresponding_column(c, raiseerr=False)
+ if conv:
+ context.statement.append_column(conv)
+ else:
+ context.statement.append_column(c)
else:
context.statement.append_column(c)
if not raiseerr:
return None
else:
- raise exceptions.InvalidRequestError("Given column '%s', attached to table '%s', failed to locate a corresponding column from table '%s'" % (str(column), str(column.table), self.name))
+ raise exceptions.InvalidRequestError("Given column '%s', attached to table '%s', failed to locate a corresponding column from table '%s'" % (str(column), str(getattr(column, 'table', None)), self.name))
def _get_exported_attribute(self, name):
try:
pass
class MapperTest(MapperSuperTest):
+ # TODO: MapperTest has grown much larger than it originally was and needs
+ # to be broken up among various functions, including querying, session operations,
+ # mapper configurational issues
def testget(self):
s = create_session()
mapper(User, users)
s.refresh(u) #hangs
- def testmagic(self):
- """not sure what this is really testing."""
- mapper(User, users, properties = {
- 'addresses' : relation(mapper(Address, addresses))
- })
- sess = create_session()
- l = sess.query(User).select_by(user_name='fred')
- self.assert_result(l, User, *[{'user_id':9}])
- u = l[0]
-
- u2 = sess.query(User).get_by_user_name('fred')
- self.assert_(u is u2)
-
- l = sess.query(User).select_by(email_address='ed@bettyboop.com')
- self.assert_result(l, User, *[{'user_id':8}])
-
- l = sess.query(User).select_by(User.c.user_name=='fred', addresses.c.email_address!='ed@bettyboop.com', user_id=9)
-
def testprops(self):
"""tests the various attributes of the properties attached to classes"""
m = mapper(User, users, properties = {
}).compile()
self.assert_(User.addresses.property is m.props['addresses'])
- def testload(self):
- """tests loading rows with a mapper and producing object instances"""
+ def testquery(self):
+ """test a basic Query.select() operation."""
mapper(User, users)
l = create_session().query(User).select()
self.assert_result(l, User, *user_result)
print "User", u.user_id, u.user_name, u.concat, u.count
assert l[0].concat == l[0].user_id * 2 == 14
assert l[1].concat == l[1].user_id * 2 == 16
+
+ def testexternalcolumns(self):
+ """test creating mappings that reference external columns or functions"""
+
+ f = (users.c.user_id *2).label('concat')
+ try:
+ mapper(User, users, properties={
+ 'concat': f,
+ })
+ class_mapper(User)
+ except exceptions.ArgumentError, e:
+ assert str(e) == "Column '%s' is not represented in mapper's table. Use the `column_property()` function to force this column to be mapped as a read-only attribute." % str(f)
+ clear_mappers()
+
+ mapper(Address, addresses, properties={
+ 'user':relation(User, lazy=False)
+ })
+
+ mapper(User, users, properties={
+ 'concat': column_property(f),
+ 'count': column_property(select([func.count(addresses.c.address_id)], users.c.user_id==addresses.c.user_id, scalar=True).label('count'))
+ })
+
+ sess = create_session()
+ l = sess.query(User).select()
+ for u in l:
+ print "User", u.user_id, u.user_name, u.concat, u.count
+ assert l[0].concat == l[0].user_id * 2 == 14
+ assert l[1].concat == l[1].user_id * 2 == 16
+
+ ### eager loads, not really working across all DBs, no column aliasing in place so
+ # results still wont be good for larger situations
+ #l = sess.query(Address).select()
+ l = sess.query(Address).options(lazyload('user')).select()
+ for a in l:
+ print "User", a.user.user_id, a.user.user_name, a.user.concat, a.user.count
+ assert l[0].user.concat == l[0].user.user_id * 2 == 14
+ assert l[1].user.concat == l[1].user.user_id * 2 == 16
+
@testbase.unsupported('firebird')
def testcount(self):
assert not hasattr(l.addresses[0], 'TEST')
assert not hasattr(l.addresses[0], 'TEST2')
-
-
-
def testeageroptions(self):
"""tests that a lazy relation can be upgraded to an eager relation via the options method"""
sess = create_session()