1. idank
  2. sqlalchemy

Commits

Mike Bayer  committed b23a967

- 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).

  • Participants
  • Parent commits fefad8d
  • Branches default

Comments (0)

Files changed (6)

File CHANGES

View file
       #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

File lib/sqlalchemy/orm/__init__.py

View file
 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)
 

File lib/sqlalchemy/orm/mapper.py

View file
             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

File lib/sqlalchemy/orm/strategies.py

View file
     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)
 

File lib/sqlalchemy/sql.py

View file
             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:

File test/orm/mapper.py

View file
         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()