Commits

Mike Bayer committed a985240

- more docs
- got from_statement() to actually work with query, tests were not covering
- added auto-labeling of anonymous columns sent to add_column(), tests

Comments (0)

Files changed (5)

doc/build/content/adv_datamapping.txt

     myparent.myclasses.append(MyClass('this is myclass'))
     myclass = myparent.myclasses['this is myclass']
 
+Note: SQLAlchemy 0.4 has an overhauled and much improved implementation for custom list classes, with some slight API changes.
 
 #### Custom Join Conditions {@name=customjoin}
 

doc/build/content/datamapping.txt

     {python}
     session.query(User).add_entity(Address).join('addresses').all()
     
+Theres also a way to combine scalar results with objects, using `add_column()`.  This is often used for functions and aggregates.  
+
+    {python}
+    r = session.query(User).add_column(func.max(users_table.c.name)).group_by([c for c in users_table.c]).all()
+    for r in result:
+        print "user:", r[0]
+        print "max name:", r[1]
+    
 To join across multiple relationships, specify them in a list.  Below, we load a `ShoppingCart`, limiting its `cartitems` collection to the single item which has a `price` object whose `amount` column is 47.95:
 
     {python}

doc/build/content/unitofwork.txt

 
 Session Facts:
 
- * the Session object is **not threadsafe**.  For thread-local management of Sessions, the recommended approch is to use the [plugins_sessioncontext](rel:plugins_sessioncontext) extension module.
+ * the Session object is **not threadsafe**.  For thread-local management of Sessions, the recommended approach is to use the [plugins_sessioncontext](rel:plugins_sessioncontext) extension module.
 
 We will now cover some of the key concepts used by Sessions and its underlying Unit of Work.
 
     mymapper = mapper(MyClass, mytable)
     
     session = create_session()
-    obj1 = session.query(MyClass).selectfirst(mytable.c.id==15)
-    obj2 = session.query(MyClass).selectfirst(mytable.c.id==15)
+    obj1 = session.query(MyClass).filter(mytable.c.id==15).first()
+    obj2 = session.query(MyClass).filter(mytable.c.id==15).first()
     
     >>> obj1 is obj2
     True
     
-The Identity Map is an instance of `dict` by default.  (This is new as of version 0.3.2).  As an option, you can specify the flag `weak_identity_map=True` to the `create_session` function so that it will use a `weakref.WeakValueDictionary`, so that when an in-memory object falls out of scope, it will be removed automatically, thereby providing some automatic management of memory.   However, this may not be instant if there are circular references upon the object.  To guarantee that an instance is removed from the identity map before removing references to it, use the `expunge()` method, described later, to remove it.  Additionally, note that an object that has changes marked on it (i.e. "dirty") can still fall out of scope when using `weak_identity_map`.
+The Identity Map is an instance of `dict` by default.  As an option, you can specify the flag `weak_identity_map=True` to the `create_session` function so that it will use a `weakref.WeakValueDictionary`, so that when an in-memory object falls out of scope, it will be removed automatically, thereby providing some automatic management of memory.   However, this may not be instant if there are circular references upon the object.  To guarantee that an instance is removed from the identity map before removing references to it, use the `expunge()` method, described later, to remove it.  Additionally, note that an object that has changes marked on it (i.e. "dirty") can still fall out of scope when using `weak_identity_map`.
 
 The Session supports an iterator interface in order to see all objects in the identity map:
 
 
 #### query() {@name=query}
 
-The `query()` function takes a class or `Mapper` as an argument, along with an optional `entity_name` parameter, and returns a new `Query` object which will issue mapper queries within the context of this Session.  If a Mapper is passed, then the Query uses that mapper.  Otherwise, if a class is sent, it will locate the primary mapper for that class which is used to construct the Query.  
+The `query()` function takes one or more classes and/or mappers, along with an optional `entity_name` parameter, and returns a new `Query` object which will issue mapper queries within the context of this Session.  For each mapper is passed, the Query uses that mapper.  For each class, the Query will locate the primary mapper for the class using `class_mapper()`.
 
     {python}
     # query from a class
-    session.query(User).select_by(name='ed')
+    session.query(User).filter_by(name='ed').all()
+
+    # query with multiple classes, returns tuples
+    session.query(User, Address).join('addresses').filter_by(name='ed').all()
     
     # query from a mapper
     query = session.query(usermapper)
     
     # query from a class mapped with entity name 'alt_users'
     q = session.query(User, entity_name='alt_users')
-    y = q.options(eagerload('orders')).select()
+    y = q.options(eagerload('orders')).all()
     
 `entity_name` is an optional keyword argument sent with a class object, in order to further qualify which primary mapper to be used; this only applies if there was a `Mapper` created with that particular class/entity name combination, else an exception is raised.  All of the methods on Session which take a class or mapper argument also take the `entity_name` argument, so that a given class can be properly matched to the desired primary mapper.
 
 
 #### merge() {@name=merge}
 
-Feature Status: [Alpha Implementation][alpha_implementation] 
-
 `merge()` is used to return the persistent version of an instance that is not attached to this Session.  When passed an instance, if an instance with its database identity already exists within this Session, it is returned.  If the instance does not exist in this Session, it is loaded from the database and then returned.  
 
 A future version of `merge()` will also update the Session's instance with the state of the given instance (hence the name "merge").
 
 ### Cascade rules {@name=cascade}
 
-Feature Status: [Alpha Implementation][alpha_implementation] 
-
 Mappers support the concept of configurable *cascade* behavior on `relation()`s.  This behavior controls how the Session should treat the instances that have a parent-child relationship with another instance that is operated upon by the Session.  Cascade is indicated as a comma-separated list of string keywords, with the possible values `all`, `delete`, `save-update`, `refresh-expire`, `merge`, `expunge`, and `delete-orphan`.
 
 Cascading is configured by setting the `cascade` keyword argument on a `relation()`:

lib/sqlalchemy/orm/query.py

 from sqlalchemy import sql, util, exceptions, sql_util, logging, schema
 from sqlalchemy.orm import mapper, class_mapper, object_mapper
 from sqlalchemy.orm.interfaces import OperationContext, SynonymProperty
+import random
 
 __all__ = ['Query', 'QueryContext', 'SelectionContext']
 
         self._func = None
         self._joinpoint = self.mapper
         self._from_obj = [self.table]
-
+        self._statement = None
+        
         for opt in util.flatten_iterator(self.with_options):
             opt.process_query(self)
         
         q._from_obj = list(self._from_obj)
         q._joinpoint = self._joinpoint
         q._criterion = self._criterion
+        q._statement = self._statement
         q._col = self._col
         q._func = self._func
         return q
         of this Query along with the additional entities.  The Query selects
         from all tables with no joining criterion by default.
         
-        When tuple-based results are returned, the 'uniquing' of returned entities
-        is disabled to maintain grouping.
-
             entity
                 a class or mapper which will be added to the results.
                 
         table or selectable that is not the primary mapped selectable.  The Query selects
         from all tables with no joining criterion by default.
         
-        When tuple-based results are returned, the 'uniquing' of returned entities
-        is disabled to maintain grouping.
-
             column
                 a string column name or sql.ColumnElement to be added to the results.
                 
         """
         
         q = self._clone()
+        
+        # alias non-labeled column elements. 
+        # TODO: make the generation deterministic
+        if isinstance(column, sql.ColumnElement) and not hasattr(column, '_label'):
+            column = column.label("anon_" + hex(random.randint(0, 65535))[2:])
+            
         q._entities.append(column)
         return q
         
                         process.append((proc, appender))
                     x(m)
                 elif isinstance(m, sql.ColumnElement) or isinstance(m, basestring):
+                    print "M IS", m
                     def y(m):
                         res = []
                         def proc(context, row):
+                            print "ROW VAL", m, "KEYS", row.keys()
                             res.append(row[m])
                         process.append((proc, res))
                     y(m)
         the arguments to this function are deprecated and are removed in version 0.4.
         """
 
+        if self._statement:
+            self._statement.use_labels = True
+            return self._statement
+
         if self._criterion:
             whereclause = sql.and_(self._criterion, whereclause)
 

test/orm/query.py

 
     def test_contains_eager(self):
 
-        selectquery = users.outerjoin(addresses).select(use_labels=True, order_by=[users.c.id, addresses.c.id])
+        selectquery = users.outerjoin(addresses).select(users.c.id<10, use_labels=True, order_by=[users.c.id, addresses.c.id])
         q = create_session().query(User)
 
         def go():
             l = q.options(contains_eager('addresses')).instances(selectquery.execute())
-            assert fixtures.user_address_result == l
+            assert fixtures.user_address_result[0:3] == l
         self.assert_sql_count(testbase.db, go, 1)
 
         def go():
             l = q.options(contains_eager('addresses')).from_statement(selectquery).all()
-            assert fixtures.user_address_result == l
+            assert fixtures.user_address_result[0:3] == l
         self.assert_sql_count(testbase.db, go, 1)
 
     def test_contains_eager_alias(self):
 
         s = select([users, func.count(addresses.c.id).label('count')], from_obj=[users.outerjoin(addresses)], group_by=[c for c in users.c], order_by=users.c.id)
         q = sess.query(User)
-        l = q.instances(s.execute(), "count")
+        l = q.add_column("count").from_statement(s).all()
         assert l == expected
 
     @testbase.unsupported('mysql') # only because of "+" operator requiring "concat" in mysql (fix #475)
 
         s = select([users, func.count(addresses.c.id).label('count'), ("Name:" + users.c.name).label('concat')], from_obj=[users.outerjoin(addresses)], group_by=[c for c in users.c], order_by=[users.c.id])
         q = create_session().query(User)
-        l = q.instances(s.execute(), "count", "concat")
+        l = q.add_column("count").add_column("concat").from_statement(s).all()
         assert l == expected
+        
+        q = create_session().query(User).add_column(func.count(addresses.c.id))\
+            .add_column(("Name:" + users.c.name)).select_from(users.outerjoin(addresses))\
+            .group_by([c for c in users.c]).order_by(users.c.id)
+            
+        assert q.all() == expected
 
 
 if __name__ == '__main__':