Commits

Mike Bayer committed 7afd5ab

- added unittest for sharding module
- fixed shard test/example datatype to "Float" since Numeric
behavior has changed
- added docstrings to sqlalchemy/orm/__init__.py functions;
still need to proof within generated HTML pages/fix errors
- added engine.threadlocal to docs, removed old mods.threadlocal

Comments (0)

Files changed (11)

doc/build/gen_docstrings.py

 
 from sqlalchemy import schema, types, ansisql, engine, sql, pool, orm, exceptions, databases
 import sqlalchemy.ext.sessioncontext as sessioncontext
-import sqlalchemy.mods.threadlocal as threadlocal
 import sqlalchemy.ext.selectresults as selectresults
 import sqlalchemy.ext.orderinglist as orderinglist
 import sqlalchemy.ext.associationproxy as associationproxy
         make_doc(obj=exceptions),
         make_doc(obj=pool),
         make_doc(obj=sessioncontext),
-        make_doc(obj=threadlocal),
         make_doc(obj=selectresults),
         make_doc(obj=orderinglist, classes=[orderinglist.OrderingList]),
         make_doc(obj=associationproxy, classes=[associationproxy.AssociationProxy]),

examples/sharding/attribute_shard.py

 weather_reports = Table("weather_reports", meta,
     Column('id', Integer, primary_key=True),
     Column('location_id', Integer, ForeignKey('weather_locations.id')),
-    Column('temperature', Numeric),
+    Column('temperature', Float),
     Column('report_time', DateTime, default=datetime.datetime.now),
 )
 

lib/sqlalchemy/orm/__init__.py

 from sqlalchemy import exceptions
 from sqlalchemy import util as sautil
 from sqlalchemy.orm.mapper import Mapper, object_mapper, class_mapper, mapper_registry
-from sqlalchemy.orm.interfaces import SynonymProperty, MapperExtension, EXT_PASS, ExtensionOption
+from sqlalchemy.orm.interfaces import SynonymProperty, MapperExtension, EXT_PASS, ExtensionOption, PropComparator
 from sqlalchemy.orm.properties import PropertyLoader, ColumnProperty, CompositeProperty, BackRef
 from sqlalchemy.orm import mapper as mapperlib
 from sqlalchemy.orm import collections, strategies
            'compile_mappers', 'class_mapper', 'object_mapper',
            'MapperExtension', 'Query', 'polymorphic_union', 'create_session',
            'synonym', 'contains_alias', 'contains_eager', 'EXT_PASS',
-           'object_session'
+           'object_session', 'PropComparator'
            ]
 
-def relation(*args, **kwargs):
+def relation(argument, secondary=None, **kwargs):
     """Provide a relationship of a primary Mapper to a secondary Mapper.
 
-    This corresponds to a parent-child or associative table relationship.
+    This corresponds to a parent-child or associative table relationship.  
+    The constructed class is an instance of [sqlalchemy.orm.properties#PropertyLoader].
+
+      argument
+          a class or Mapper instance, representing the target of the relation.
+
+      secondary
+        for a many-to-many relationship, specifies the intermediary table. The
+        `secondary` keyword argument should generally only be used for a table
+        that is not otherwise expressed in any class mapping. In particular,
+        using the Association Object Pattern is
+        generally mutually exclusive against using the `secondary` keyword
+        argument.
+
+      \**kwargs follow:
+
+        association
+          Deprecated; as of version 0.3.0 the association keyword is synonomous
+          with applying the "all, delete-orphan" cascade to a "one-to-many"
+          relationship. SA can now automatically reconcile a "delete" and
+          "insert" operation of two objects with the same "identity" in a flush()
+          operation into a single "update" statement, which is the pattern that
+          "association" used to indicate. See the updated example of association
+          mappings in [datamapping_association](rel:datamapping_association).
+      
+        backref
+          indicates the name of a property to be placed on the related mapper's
+          class that will handle this relationship in the other direction,
+          including synchronizing the object attributes on both sides of the
+          relation. Can also point to a `backref()` construct for more
+          configurability. 
+      
+        cascade
+          a string list of cascade rules which determines how persistence
+          operations should be "cascaded" from parent to child. 
+      
+        collection_class
+          a class or function that returns a new list-holding object. will be
+          used in place of a plain list for storing elements. 
+      
+        foreign_keys
+          a list of columns which are to be used as "foreign key" columns.
+          this parameter should be used in conjunction with explicit
+          `primaryjoin` and `secondaryjoin` (if needed) arguments, and the
+          columns within the `foreign_keys` list should be present within
+          those join conditions. Normally, `relation()` will inspect the
+          columns within the join conditions to determine which columns are
+          the "foreign key" columns, based on information in the `Table`
+          metadata. Use this argument when no ForeignKey's are present in the
+          join condition, or to override the table-defined foreign keys.
+
+        foreignkey
+          deprecated. use the `foreign_keys` argument for foreign key
+          specification, or `remote_side` for "directional" logic.
+
+        lazy=True
+          specifies how the related items should be loaded. a value of True
+          indicates they should be loaded lazily when the property is first
+          accessed. A value of False indicates they should be loaded by joining
+          against the parent object query, so parent and child are loaded in one
+          round trip (i.e. eagerly). A value of None indicates the related items
+          are not loaded by the mapper in any case; the application will manually
+          insert items into the list in some other way. In all cases, items added
+          or removed to the parent object's collection (or scalar attribute) will
+          cause the appropriate updates and deletes upon flush(), i.e. this
+          option only affects load operations, not save operations.
+
+        order_by
+          indicates the ordering that should be applied when loading these items.
+
+        passive_deletes=False
+          Indicates if lazy-loaders should not be executed during the `flush()`
+          process, which normally occurs in order to locate all existing child
+          items when a parent item is to be deleted. Setting this flag to True is
+          appropriate when `ON DELETE CASCADE` rules have been set up on the
+          actual tables so that the database may handle cascading deletes
+          automatically. This strategy is useful particularly for handling the
+          deletion of objects that have very large (and/or deep) child-object
+          collections. 
+
+        post_update
+          this indicates that the relationship should be handled by a second
+          UPDATE statement after an INSERT or before a DELETE. Currently, it also
+          will issue an UPDATE after the instance was UPDATEd as well, although
+          this technically should be improved. This flag is used to handle saving
+          bi-directional dependencies between two individual rows (i.e. each row
+          references the other), where it would otherwise be impossible to INSERT
+          or DELETE both rows fully since one row exists before the other. Use
+          this flag when a particular mapping arrangement will incur two rows
+          that are dependent on each other, such as a table that has a
+          one-to-many relationship to a set of child rows, and also has a column
+          that references a single child row within that list (i.e. both tables
+          contain a foreign key to each other). If a `flush()` operation returns
+          an error that a "cyclical dependency" was detected, this is a cue that
+          you might want to use `post_update` to "break" the cycle.
+
+        primaryjoin
+          a ClauseElement that will be used as the primary join of this child
+          object against the parent object, or in a many-to-many relationship the
+          join of the primary object to the association table. By default, this
+          value is computed based on the foreign key relationships of the parent
+          and child tables (or association table).
+
+        private=False
+          deprecated. setting `private=True` is the equivalent of setting
+          `cascade="all, delete-orphan"`, and indicates the lifecycle of child
+          objects should be contained within that of the parent. 
+
+        remote_side
+          used for self-referential relationships, indicates the column or list
+          of columns that form the "remote side" of the relationship. 
+
+        secondaryjoin
+          a ClauseElement that will be used as the join of an association table
+          to the child object. By default, this value is computed based on the
+          foreign key relationships of the association and child tables.
+
+        uselist=(True|False)
+          a boolean that indicates if this property should be loaded as a list or
+          a scalar. In most cases, this value is determined automatically by
+          `relation()`, based on the type and direction of the relationship - one
+          to many forms a list, many to one forms a scalar, many to many is a
+          list. If a scalar is desired where normally a list would be present,
+          such as a bi-directional one-to-one relationship, set uselist to False.
+
+        viewonly=False
+          when set to True, the relation is used only for loading objects within
+          the relationship, and has no effect on the unit-of-work flush process.
+          Relations with viewonly can specify any kind of join conditions to
+          provide additional views of related objects onto a parent object. Note
+          that the functionality of a viewonly relationship has its limits -
+          complicated join conditions may not compile into eager or lazy loaders
+          properly. If this is the case, use an alternative method.
+
     """
 
-    if len(args) > 1 and isinstance(args[0], type):
-        raise exceptions.ArgumentError("relation(class, table, **kwargs) is deprecated.  Please use relation(class, **kwargs) or relation(mapper, **kwargs).")
-    return _relation_loader(*args, **kwargs)
+    return PropertyLoader(argument, secondary=secondary, **kwargs)
+
+#    return _relation_loader(argument, secondary=secondary, **kwargs)
+
+#def _relation_loader(mapper, secondary=None, primaryjoin=None, secondaryjoin=None, lazy=True, **kwargs):
 
 def column_property(*args, **kwargs):
     """Provide a column-level property for use with a Mapper.
 
     Columns that arent present in the mapper's selectable won't be persisted
     by the mapper and are effectively "read-only" attributes.
+
+      \*cols
+          list of Column objects to be mapped.
+    
+      group
+          a group name for this property when marked as deferred.
+        
+      deferred
+          when True, the column property is "deferred", meaning that
+          it does not load immediately, and is instead loaded when the
+          attribute is first accessed on an instance.  See also 
+          [sqlalchemy.orm#deferred()].
+
     """
     
     return ColumnProperty(*args, **kwargs)
     implement a constructor with positional arguments matching the order of 
     columns given, as well as a __colset__() method which returns its attributes 
     in column order.
+    
+      class_
+        the "composite type" class.
+          
+      \*cols
+        list of Column objects to be mapped.
+      
+      group
+        a group name for this property when marked as deferred.
+          
+      deferred
+        when True, the column property is "deferred", meaning that
+        it does not load immediately, and is instead loaded when the
+        attribute is first accessed on an instance.  See also 
+        [sqlalchemy.orm#deferred()].
+          
+      comparator
+        an optional instance of [sqlalchemy.orm#PropComparator] which
+        provides SQL expression generation functions for this composite
+        type.
     """
     
     return CompositeProperty(class_, *cols, **kwargs)
     
-def _relation_loader(mapper, secondary=None, primaryjoin=None, secondaryjoin=None, lazy=True, **kwargs):
-    return PropertyLoader(mapper, secondary, primaryjoin, secondaryjoin, lazy=lazy, **kwargs)
 
 def backref(name, **kwargs):
     """Create a BackRef object with explicit arguments, which are the same arguments one
 
     return ColumnProperty(deferred=True, *columns, **kwargs)
 
-def mapper(class_, table=None, *args, **params):
-    """Return a new ``Mapper`` object.
+def mapper(class_, local_table=None, *args, **params):
+    """Return a new [sqlalchemy.orm#Mapper] object.
 
-    See the ``Mapper`` class for a description of arguments.
+      class\_
+        The class to be mapped.
+
+      local_table
+        The table to which the class is mapped, or None if this
+        mapper inherits from another mapper using concrete table
+        inheritance.
+
+      entity_name
+        A name to be associated with the `class`, to allow alternate
+        mappings for a single class.
+
+      always_refresh
+        If True, all query operations for this mapped class will
+        overwrite all data within object instances that already
+        exist within the session, erasing any in-memory changes with
+        whatever information was loaded from the database.  Usage
+        of this flag is highly discouraged; as an alternative, 
+        see the method `populate_existing()` on [sqlalchemy.orm.query#Query].
+
+      allow_column_override
+        If True, allows the usage of a ``relation()`` which has the
+        same name as a column in the mapped table.  The table column
+        will no longer be mapped.
+
+      allow_null_pks
+        Indicates that composite primary keys where one or more (but
+        not all) columns contain NULL is a valid primary key.
+        Primary keys which contain NULL values usually indicate that
+        a result row does not contain an entity and should be
+        skipped.
+
+      batch
+        Indicates that save operations of multiple entities can be
+        batched together for efficiency.  setting to False indicates
+        that an instance will be fully saved before saving the next
+        instance, which includes inserting/updating all table rows
+        corresponding to the entity as well as calling all
+        ``MapperExtension`` methods corresponding to the save
+        operation.
+
+      column_prefix
+        A string which will be prepended to the `key` name of all
+        Columns when creating column-based properties from the given
+        Table.  Does not affect explicitly specified column-based
+        properties
+
+      concrete
+        If True, indicates this mapper should use concrete table
+        inheritance with its parent mapper.
+
+      extension
+        A [sqlalchemy.orm#MapperExtension] instance or list of
+        ``MapperExtension`` instances which will be applied to all
+        operations by this ``Mapper``.
+
+      inherits
+        Another ``Mapper`` for which this ``Mapper`` will have an
+        inheritance relationship with.
+
+      inherit_condition
+        For joined table inheritance, a SQL expression (constructed
+        ``ClauseElement``) which will define how the two tables are
+        joined; defaults to a natural join between the two tables.
+
+      order_by
+        A single ``Column`` or list of ``Columns`` for which
+        selection operations should use as the default ordering for
+        entities.  Defaults to the OID/ROWID of the table if any, or
+        the first primary key column of the table.
+
+      non_primary
+        Construct a ``Mapper`` that will define only the selection
+        of instances, not their persistence.  Any number of non_primary
+        mappers may be created for a particular class.
+
+      polymorphic_on
+        Used with mappers in an inheritance relationship, a ``Column``
+        which will identify the class/mapper combination to be used
+        with a particular row.  requires the polymorphic_identity
+        value to be set for all mappers in the inheritance
+        hierarchy.
+
+      _polymorphic_map
+        Used internally to propigate the full map of polymorphic
+        identifiers to surrogate mappers.
+
+      polymorphic_identity
+        A value which will be stored in the Column denoted by
+        polymorphic_on, corresponding to the *class identity* of
+        this mapper.
+
+      polymorphic_fetch
+        specifies how subclasses mapped through joined-table 
+        inheritance will be fetched.  options are 'union', 
+        'select', and 'deferred'.  if the select_table argument 
+        is present, defaults to 'union', otherwise defaults to
+        'select'.
+
+      properties
+        A dictionary mapping the string names of object attributes
+        to ``MapperProperty`` instances, which define the
+        persistence behavior of that attribute.  Note that the
+        columns in the mapped table are automatically converted into
+        ``ColumnProperty`` instances based on the `key` property of
+        each ``Column`` (although they can be overridden using this
+        dictionary).
+
+      primary_key
+        A list of ``Column`` objects which define the *primary key*
+        to be used against this mapper's selectable unit.  This is
+        normally simply the primary key of the `local_table`, but
+        can be overridden here.
+
+      select_table
+        A [sqlalchemy.schema#Table] or any [sqlalchemy.sql#Selectable] 
+        which will be used to select instances of this mapper's class.  
+        usually used to provide polymorphic loading among several 
+        classes in an inheritance hierarchy.
+
+      version_id_col
+        A ``Column`` which must have an integer type that will be
+        used to keep a running *version id* of mapped entities in
+        the database.  this is used during save operations to ensure
+        that no other thread or process has updated the instance
+        during the lifetime of the entity, else a
+        ``ConcurrentModificationError`` exception is thrown.
     """
 
-    return Mapper(class_, table, *args, **params)
+    return Mapper(class_, local_table, *args, **params)
 
 def synonym(name, proxy=False):
     """Set up `name` as a synonym to another ``MapperProperty``.

lib/sqlalchemy/orm/mapper.py

                 column_prefix=None):
         """Construct a new mapper.
 
-        All arguments may be sent to the ``sqlalchemy.orm.mapper()``
-        function where they are passed through to here.
-
-        class\_
-          The class to be mapped.
-
-        local_table
-          The table to which the class is mapped, or None if this
-          mapper inherits from another mapper using concrete table
-          inheritance.
-
-        properties
-          A dictionary mapping the string names of object attributes
-          to ``MapperProperty`` instances, which define the
-          persistence behavior of that attribute.  Note that the
-          columns in the mapped table are automatically converted into
-          ``ColumnProperty`` instances based on the `key` property of
-          each ``Column`` (although they can be overridden using this
-          dictionary).
-
-        primary_key
-          A list of ``Column`` objects which define the *primary key*
-          to be used against this mapper's selectable unit.  This is
-          normally simply the primary key of the `local_table`, but
-          can be overridden here.
-
-        non_primary
-          Construct a ``Mapper`` that will define only the selection
-          of instances, not their persistence.
-
-        inherits
-          Another ``Mapper`` for which this ``Mapper`` will have an
-          inheritance relationship with.
-
-        inherit_condition
-          For joined table inheritance, a SQL expression (constructed
-          ``ClauseElement``) which will define how the two tables are
-          joined; defaults to a natural join between the two tables.
-
-        extension
-          A ``MapperExtension`` instance or list of
-          ``MapperExtension`` instances which will be applied to all
-          operations by this ``Mapper``.
-
-        order_by
-          A single ``Column`` or list of ``Columns`` for which
-          selection operations should use as the default ordering for
-          entities.  Defaults to the OID/ROWID of the table if any, or
-          the first primary key column of the table.
-
-        allow_column_override
-          If True, allows the usage of a ``relation()`` which has the
-          same name as a column in the mapped table.  The table column
-          will no longer be mapped.
-
-        entity_name
-          A name to be associated with the `class`, to allow alternate
-          mappings for a single class.
-
-        always_refresh
-          If True, all query operations for this mapped class will
-          overwrite all data within object instances that already
-          exist within the session, erasing any in-memory changes with
-          whatever information was loaded from the database.
-
-        version_id_col
-          A ``Column`` which must have an integer type that will be
-          used to keep a running *version id* of mapped entities in
-          the database.  this is used during save operations to ensure
-          that no other thread or process has updated the instance
-          during the lifetime of the entity, else a
-          ``ConcurrentModificationError`` exception is thrown.
-
-        polymorphic_on
-          Used with mappers in an inheritance relationship, a ``Column``
-          which will identify the class/mapper combination to be used
-          with a particular row.  requires the polymorphic_identity
-          value to be set for all mappers in the inheritance
-          hierarchy.
-
-        _polymorphic_map
-          Used internally to propigate the full map of polymorphic
-          identifiers to surrogate mappers.
-
-        polymorphic_identity
-          A value which will be stored in the Column denoted by
-          polymorphic_on, corresponding to the *class identity* of
-          this mapper.
-        
-        polymorphic_fetch
-          specifies how subclasses mapped through joined-table 
-          inheritance will be fetched.  options are 'union', 
-          'select', and 'deferred'.  if the select_table argument 
-          is present, defaults to 'union', otherwise defaults to
-          'select'.
-          
-        concrete
-          If True, indicates this mapper should use concrete table
-          inheritance with its parent mapper.
-
-        select_table
-          A ``Table`` or (more commonly) ``Selectable`` which will be
-          used to select instances of this mapper's class.  usually
-          used to provide polymorphic loading among several classes in
-          an inheritance hierarchy.
-
-        allow_null_pks
-          Indicates that composite primary keys where one or more (but
-          not all) columns contain NULL is a valid primary key.
-          Primary keys which contain NULL values usually indicate that
-          a result row does not contain an entity and should be
-          skipped.
-
-        batch
-          Indicates that save operations of multiple entities can be
-          batched together for efficiency.  setting to False indicates
-          that an instance will be fully saved before saving the next
-          instance, which includes inserting/updating all table rows
-          corresponding to the entity as well as calling all
-          ``MapperExtension`` methods corresponding to the save
-          operation.
-
-        column_prefix
-          A string which will be prepended to the `key` name of all
-          Columns when creating column-based properties from the given
-          Table.  Does not affect explicitly specified column-based
-          properties
+        Mappers are normally constructed via the [sqlalchemy.orm#mapper()] 
+        function.  See for details.
         """
 
         if not issubclass(class_, object):

lib/sqlalchemy/orm/properties.py

     def __init__(self, class_, *columns, **kwargs):
         super(CompositeProperty, self).__init__(*columns, **kwargs)
         self.composite_class = class_
-        self.comparator = CompositeProperty.Comparator(self)
+        self.comparator = kwargs.pop('comparator', CompositeProperty.Comparator(self))
         
     def copy(self):
         return CompositeProperty(deferred=self.deferred, group=self.group, composite_class=self.composite_class, *self.columns)
     of items that correspond to a related database table.
     """
 
-    def __init__(self, argument, secondary, primaryjoin, secondaryjoin, entity_name=None, foreign_keys=None, foreignkey=None, uselist=None, private=False, association=None, order_by=False, attributeext=None, backref=None, is_backref=False, post_update=False, cascade=None, viewonly=False, lazy=True, collection_class=None, passive_deletes=False, remote_side=None, enable_typechecks=True, join_depth=None):
+    def __init__(self, argument, secondary=None, primaryjoin=None, secondaryjoin=None, entity_name=None, foreign_keys=None, foreignkey=None, uselist=None, private=False, association=None, order_by=False, attributeext=None, backref=None, is_backref=False, post_update=False, cascade=None, viewonly=False, lazy=True, collection_class=None, passive_deletes=False, remote_side=None, enable_typechecks=True, join_depth=None):
         self.uselist = uselist
         self.argument = argument
         self.entity_name = entity_name

test/orm/alltests.py

 import unittest
 
 import inheritance.alltests as inheritance
+import sharding.alltests as sharding
 
 def suite():
     modules_to_test = (
             mod = getattr(mod, token)
         alltests.addTest(unittest.findTestCases(mod, suiteClass=None))
     alltests.addTest(inheritance.suite())
+    alltests.addTest(sharding.suite())
     return alltests
 
 

test/orm/mapper.py

                 return other.x == self.x and other.y == self.y
             def __ne__(self, other):
                 return not self.__eq__(other)
-                    
+
         class Graph(object):
             pass
         class Edge(object):

test/orm/sharding/__init__.py

Empty file added.

test/orm/sharding/alltests.py

+import testbase
+import unittest
+
+import inheritance.alltests as inheritance
+
+def suite():
+    modules_to_test = (
+        'orm.sharding.shard',
+        )
+    alltests = unittest.TestSuite()
+    for name in modules_to_test:
+        mod = __import__(name)
+        for token in name.split('.')[1:]:
+            mod = getattr(mod, token)
+        alltests.addTest(unittest.findTestCases(mod, suiteClass=None))
+    alltests.addTest(inheritance.suite())
+    return alltests
+
+
+if __name__ == '__main__':
+    testbase.main(suite())

test/orm/sharding/shard.py

+import testbase
+from sqlalchemy import *
+from sqlalchemy.orm import *
+
+from sqlalchemy.orm.shard import ShardedSession
+from sqlalchemy.sql import ColumnOperators
+import datetime, operator, os
+from testlib import PersistTest
+
+# TODO: ShardTest can be turned into a base for further subclasses
+
+class ShardTest(PersistTest):
+    def setUpAll(self):
+        global db1, db2, db3, db4, weather_locations, weather_reports
+        
+        db1 = create_engine('sqlite:///shard1.db')
+        db2 = create_engine('sqlite:///shard2.db')
+        db3 = create_engine('sqlite:///shard3.db')
+        db4 = create_engine('sqlite:///shard4.db')
+
+        meta = MetaData()
+        ids = Table('ids', meta,
+            Column('nextid', Integer, nullable=False))
+
+        def id_generator(ctx):
+            # in reality, might want to use a separate transaction for this.
+            c = db1.connect()
+            nextid = c.execute(ids.select(for_update=True)).scalar()
+            c.execute(ids.update(values={ids.c.nextid : ids.c.nextid + 1}))
+            return nextid
+
+        weather_locations = Table("weather_locations", meta,
+                Column('id', Integer, primary_key=True, default=id_generator),
+                Column('continent', String(30), nullable=False),
+                Column('city', String(50), nullable=False)
+            )
+
+        weather_reports = Table("weather_reports", meta,
+            Column('id', Integer, primary_key=True),
+            Column('location_id', Integer, ForeignKey('weather_locations.id')),
+            Column('temperature', Float),
+            Column('report_time', DateTime, default=datetime.datetime.now),
+        )
+        
+        for db in (db1, db2, db3, db4):
+            meta.create_all(db)
+        
+        db1.execute(ids.insert(), nextid=1)
+
+        self.setup_session()
+        self.setup_mappers()
+        
+    def tearDownAll(self):
+        for i in range(1,5):
+            os.remove("shard%d.db" % i)
+
+    def setup_session(self):
+        global create_session
+
+        shard_lookup = {
+            'North America':'north_america',
+            'Asia':'asia',
+            'Europe':'europe',
+            'South America':'south_america'
+        }
+        
+        def shard_chooser(mapper, instance):
+            if isinstance(instance, WeatherLocation):
+                return shard_lookup[instance.continent]
+            else:
+                return shard_chooser(mapper, instance.location)
+
+        def id_chooser(ident):
+            return ['north_america', 'asia', 'europe', 'south_america']
+
+        def query_chooser(query):
+            ids = []
+
+            class FindContinent(sql.ClauseVisitor):
+                def visit_binary(self, binary):
+                    if binary.left is weather_locations.c.continent:
+                        if binary.operator == operator.eq:
+                            ids.append(shard_lookup[binary.right.value])
+                        elif binary.operator == ColumnOperators.in_op:
+                            for bind in binary.right.clauses:
+                                ids.append(shard_lookup[bind.value])
+
+            FindContinent().traverse(query._criterion)
+            if len(ids) == 0:
+                return ['north_america', 'asia', 'europe', 'south_america']
+            else:
+                return ids
+
+        def create_session():
+            s = ShardedSession(shard_chooser, id_chooser, query_chooser)
+            s.bind_shard('north_america', db1)
+            s.bind_shard('asia', db2)
+            s.bind_shard('europe', db3)
+            s.bind_shard('south_america', db4)
+            return s
+
+    def setup_mappers(self):
+        global WeatherLocation, Report
+        
+        class WeatherLocation(object):
+            def __init__(self, continent, city):
+                self.continent = continent
+                self.city = city
+
+        class Report(object):
+            def __init__(self, temperature):
+                self.temperature = temperature
+
+        mapper(WeatherLocation, weather_locations, properties={
+            'reports':relation(Report, backref='location')
+        })
+
+        mapper(Report, weather_reports)    
+
+    def test_roundtrip(self):
+        tokyo = WeatherLocation('Asia', 'Tokyo')
+        newyork = WeatherLocation('North America', 'New York')
+        toronto = WeatherLocation('North America', 'Toronto')
+        london = WeatherLocation('Europe', 'London')
+        dublin = WeatherLocation('Europe', 'Dublin')
+        brasilia = WeatherLocation('South America', 'Brasila')
+        quito = WeatherLocation('South America', 'Quito')
+
+        tokyo.reports.append(Report(80.0))
+        newyork.reports.append(Report(75))
+        quito.reports.append(Report(85))
+
+        sess = create_session()
+        for c in [tokyo, newyork, toronto, london, dublin, brasilia, quito]:
+            sess.save(c)
+        sess.flush()
+
+        sess.clear()
+
+        t = sess.query(WeatherLocation).get(tokyo.id)
+        assert t.city == tokyo.city
+        assert t.reports[0].temperature == 80.0
+
+        north_american_cities = sess.query(WeatherLocation).filter(WeatherLocation.continent == 'North America')
+        assert set([c.city for c in north_american_cities]) == set(['New York', 'Toronto'])
+
+        asia_and_europe = sess.query(WeatherLocation).filter(WeatherLocation.continent.in_('Europe', 'Asia'))
+        assert set([c.city for c in asia_and_europe]) == set(['Tokyo', 'London', 'Dublin'])
+
+
+
+if __name__ == '__main__':
+    testbase.main()
+    

test/testlib/tables.py

 from sqlalchemy import *
 from testlib.schema import Table, Column
 
+
+# these are older test fixtures, used primarily by test/orm/mapper.py and test/orm/unitofwork.py.
+# newer unit tests make usage of test/orm/fixtures.py.
+
 metadata = MetaData()
 
 users = Table('users', metadata,