From 146e0515d8c8f991857fb3cf27a6b9a5583d25d1 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sun, 29 Apr 2007 22:26:39 +0000 Subject: [PATCH] - 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). --- CHANGES | 7 +++- lib/sqlalchemy/orm/__init__.py | 29 +++++++++++++- lib/sqlalchemy/orm/mapper.py | 33 ++++++---------- lib/sqlalchemy/orm/strategies.py | 6 ++- lib/sqlalchemy/sql.py | 2 +- test/orm/mapper.py | 67 +++++++++++++++++++++----------- 6 files changed, 96 insertions(+), 48 deletions(-) diff --git a/CHANGES b/CHANGES index 2932a76a4c..c07e53916c 100644 --- a/CHANGES +++ b/CHANGES @@ -94,12 +94,17 @@ #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 diff --git a/lib/sqlalchemy/orm/__init__.py b/lib/sqlalchemy/orm/__init__.py index 48b69dadf2..7a73ecca5c 100644 --- a/lib/sqlalchemy/orm/__init__.py +++ b/lib/sqlalchemy/orm/__init__.py @@ -19,7 +19,7 @@ from sqlalchemy.orm import properties, strategies, interfaces 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' ] @@ -34,6 +34,33 @@ def relation(*args, **kwargs): 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) diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index ab90f24b6f..af9d53ac9c 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -792,33 +792,24 @@ class Mapper(object): 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. @@ -833,7 +824,7 @@ class Mapper(object): 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 diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py index cdf71e0444..1d7eee56d8 100644 --- a/lib/sqlalchemy/orm/strategies.py +++ b/lib/sqlalchemy/orm/strategies.py @@ -23,7 +23,11 @@ class ColumnLoader(LoaderStrategy): 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) diff --git a/lib/sqlalchemy/sql.py b/lib/sqlalchemy/sql.py index a7ca55e315..6858084def 100644 --- a/lib/sqlalchemy/sql.py +++ b/lib/sqlalchemy/sql.py @@ -1617,7 +1617,7 @@ class FromClause(Selectable): 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: diff --git a/test/orm/mapper.py b/test/orm/mapper.py index 8a0f28c478..99712b082f 100644 --- a/test/orm/mapper.py +++ b/test/orm/mapper.py @@ -21,6 +21,9 @@ class MapperSuperTest(AssertMixin): 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) @@ -248,24 +251,6 @@ class MapperTest(MapperSuperTest): 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 = { @@ -273,8 +258,8 @@ class MapperTest(MapperSuperTest): }).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) @@ -456,6 +441,45 @@ class MapperTest(MapperSuperTest): 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): @@ -563,9 +587,6 @@ class MapperTest(MapperSuperTest): 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() -- 2.47.2