From: Mike Bayer Date: Wed, 25 Jul 2007 18:57:56 +0000 (+0000) Subject: - added unittest for sharding module X-Git-Tag: rel_0_4_6~27 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=e3908c916e0fe308c7e58d9563bfe5176b03d81e;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - 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 --- diff --git a/doc/build/gen_docstrings.py b/doc/build/gen_docstrings.py index 435e1628b8..de162d94b3 100644 --- a/doc/build/gen_docstrings.py +++ b/doc/build/gen_docstrings.py @@ -4,7 +4,6 @@ import re 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 @@ -38,7 +37,6 @@ def make_all_docs(): 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]), diff --git a/examples/sharding/attribute_shard.py b/examples/sharding/attribute_shard.py index 1f80c5445e..e95b978aec 100644 --- a/examples/sharding/attribute_shard.py +++ b/examples/sharding/attribute_shard.py @@ -77,7 +77,7 @@ weather_locations = Table("weather_locations", meta, 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), ) diff --git a/lib/sqlalchemy/orm/__init__.py b/lib/sqlalchemy/orm/__init__.py index 6a790def40..b41d766c69 100644 --- a/lib/sqlalchemy/orm/__init__.py +++ b/lib/sqlalchemy/orm/__init__.py @@ -12,7 +12,7 @@ packages and tying operations to class properties and constructors. 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 @@ -27,18 +27,153 @@ __all__ = ['relation', 'column_property', 'composite', 'backref', 'eagerload', '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. @@ -51,6 +186,19 @@ def column_property(*args, **kwargs): 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) @@ -63,12 +211,30 @@ def composite(class_, *cols, **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 @@ -90,13 +256,139 @@ def deferred(*columns, **kwargs): return ColumnProperty(deferred=True, *columns, **kwargs) -def mapper(class_, table=None, *args, **params): - """Return a new ``Mapper`` object. - - See the ``Mapper`` class for a description of arguments. +def mapper(class_, local_table=None, *args, **params): + """Return a new [sqlalchemy.orm#Mapper] object. + + 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``. diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index e035537351..76cc412890 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -65,133 +65,8 @@ class Mapper(object): 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): diff --git a/lib/sqlalchemy/orm/properties.py b/lib/sqlalchemy/orm/properties.py index e0500f2bd1..d3150c6cb0 100644 --- a/lib/sqlalchemy/orm/properties.py +++ b/lib/sqlalchemy/orm/properties.py @@ -81,7 +81,7 @@ class CompositeProperty(ColumnProperty): 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) @@ -118,7 +118,7 @@ class PropertyLoader(StrategizedProperty): 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 diff --git a/test/orm/alltests.py b/test/orm/alltests.py index 35d6da6b6e..4f8f4b6b7a 100644 --- a/test/orm/alltests.py +++ b/test/orm/alltests.py @@ -2,6 +2,7 @@ import testbase import unittest import inheritance.alltests as inheritance +import sharding.alltests as sharding def suite(): modules_to_test = ( @@ -38,6 +39,7 @@ def suite(): mod = getattr(mod, token) alltests.addTest(unittest.findTestCases(mod, suiteClass=None)) alltests.addTest(inheritance.suite()) + alltests.addTest(sharding.suite()) return alltests diff --git a/test/orm/mapper.py b/test/orm/mapper.py index f9c3aac841..b72a10516a 100644 --- a/test/orm/mapper.py +++ b/test/orm/mapper.py @@ -820,7 +820,7 @@ class CompositeTypesTest(ORMTest): 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): diff --git a/test/orm/sharding/__init__.py b/test/orm/sharding/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/orm/sharding/alltests.py b/test/orm/sharding/alltests.py new file mode 100644 index 0000000000..563a6d5cb2 --- /dev/null +++ b/test/orm/sharding/alltests.py @@ -0,0 +1,21 @@ +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()) diff --git a/test/orm/sharding/shard.py b/test/orm/sharding/shard.py new file mode 100644 index 0000000000..faa980cc27 --- /dev/null +++ b/test/orm/sharding/shard.py @@ -0,0 +1,154 @@ +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() + \ No newline at end of file diff --git a/test/testlib/tables.py b/test/testlib/tables.py index 2dc9200f35..69c84c5b34 100644 --- a/test/testlib/tables.py +++ b/test/testlib/tables.py @@ -2,6 +2,10 @@ import testbase 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,