]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- comparator_factory is accepted by all MapperProperty constructors. [ticket:1149]
authorMike Bayer <mike_mp@zzzcomputing.com>
Mon, 24 Nov 2008 01:14:32 +0000 (01:14 +0000)
committerMike Bayer <mike_mp@zzzcomputing.com>
Mon, 24 Nov 2008 01:14:32 +0000 (01:14 +0000)
- added other unit tests as per [ticket:1149]
- rewrote most of the "joined table inheritance" documentation section, removed badly out of
date "polymorphic_fetch" and "select_table" arguments.
- "select_table" raises a deprecation warning.  converted unit tests to not use it.
- removed all references to "ORDER BY table.oid" from mapping docs.
- renamed PropertyLoader to RelationProperty.  Old symbol remains.
- renamed ColumnProperty.ColumnComparator to ColumnProperty.Comparator.  Old symbol remains.

14 files changed:
CHANGES
doc/build/content/mappers.txt
lib/sqlalchemy/orm/__init__.py
lib/sqlalchemy/orm/mapper.py
lib/sqlalchemy/orm/properties.py
test/ext/declarative.py
test/orm/compile.py
test/orm/inheritance/abc_inheritance.py
test/orm/inheritance/abc_polymorphic.py
test/orm/inheritance/magazine.py
test/orm/inheritance/polymorph.py
test/orm/inheritance/polymorph2.py
test/orm/inheritance/selects.py
test/orm/mapper.py

diff --git a/CHANGES b/CHANGES
index 774799e83f5e0b661e0d2a5443538f280b64609d..5041ee9326ba4c67166e332f82bee88ba336a9a6 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -22,7 +22,17 @@ CHANGES
       primaryjoin/secondaryjoin are ClauseElement 
       instances, to prevent more confusing errors later 
       on.
-       
+    
+    - comparator_factory argument is now documented
+      and supported by all MapperProperty types,
+      including column_property(), relation(), 
+      backref(), and synonym() [ticket:5051].
+      
+    - Changed the name of PropertyLoader to
+      RelationProperty, to be consistent
+      with all the other names.  PropertyLoader is 
+      still present as a synonym.
+      
 - sql
     - Fixed the import weirdness in sqlalchemy.sql
       to not export __names__ [ticket:1215].
@@ -37,7 +47,7 @@ CHANGES
       is correctly propagated to the attribute manager.
 
 - documentation
-    - Tickets [ticket:1200].
+    - Tickets [ticket:1200] [ticket:1149].
       
     - Added note about create_session() defaults.
     
@@ -48,7 +58,15 @@ CHANGES
     - Rewrote the "threadlocal" strategy section of 
       the docs due to recent confusion over this 
       feature.
+     
+    - Removed badly out of date 'polymorphic_fetch'
+      and 'select_table' docs from inheritance, 
+      reworked the second half of "joined table
+      inheritance".
       
+    - Documented `comparator_factory` kwarg, added
+      new doc section "Custom Comparators".
+    
 - postgres
     - Calling alias.execute() in conjunction with
       server_side_cursors won't raise AttributeError.
index bd3c2faa42a3e745a79c5b68a7f330d9a77e9e7c..d645849165984b2701e2c1c32e6d292b1129e1a8 100644 (file)
@@ -203,6 +203,39 @@ The `email` attribute is now usable in the same way as any other mapped attribut
 
 If the mapped class does not provide a property, the `synonym()` construct will create a default getter/setter object automatically.
 
+##### Custom Comparators {@name=comparators}
+
+The expressions returned by comparison operations, such as `User.name=='ed'`, can be customized.  SQLAlchemy attributes generate these expressions using [docstrings_sqlalchemy.orm.interfaces_PropComparator](rel:docstrings_sqlalchemy.orm.interfaces_PropComparator) objects, which provide common Python expression overrides including `__eq__()`, `__ne__()`, `__lt__()`, and so on.  Any mapped attribute can be passed a user-defined class via the `comparator_factory` keyword argument, which subclasses the appropriate `PropComparator` in use, which can provide any or all of these methods:
+
+    {python}
+    from sqlalchemy.orm.properties import ColumnProperty
+    class MyComparator(ColumnProperty.Comparator):
+        def __eq__(self, other):
+            return func.lower(self.__clause_element__()) == func.lower(other)
+
+    mapper(EmailAddress, addresses_table, properties={
+        'email':column_property(addresses_table.c.email, comparator_factory=MyComparator)
+    })
+
+Above, comparisons on the `email` column are wrapped in the SQL lower() function to produce case-insensitive matching:
+
+    {python}
+    >>> str(EmailAddress.email == 'SomeAddress@foo.com')
+    lower(addresses.email) = lower(:lower_1)
+
+The `__clause_element__()` method is provided by the base `Comparator` class in use, and represents the SQL element which best matches what this attribute represents.  For a column-based attribute, it's the mapped column.  For a composite attribute, it's a `sqlalchemy.sql.expression.ClauseList` consisting of each column represented.  For a relation, it's the table mapped by the local mapper (not the remote mapper).  `__clause_element__()` should be honored by the custom comparator class in most cases since the resulting element will be applied any translations which are in effect, such as the correctly aliased member when using an `aliased()` construct or certain `with_polymorphic()` scenarios.
+
+There are four kinds of `Comparator` classes which may be subclassed, as according to the type of mapper property configured:
+
+  * `column_property()` attribute - `sqlalchemy.orm.properties.ColumnProperty.Comparator`
+  * `composite()` attribute - `sqlalchemy.orm.properties.CompositeProperty.Comparator`
+  * `relation()` attribute - `sqlalchemy.orm.properties.RelationProperty.Comparator`
+  * `comparable_property()` attribute - `sqlalchemy.orm.interfaces.PropComparator`
+
+When using `comparable_property()`, which is a mapper property that isn't tied to any column or mapped table, the `__clause_element__()` method of `PropComparator` should also be implemented.
+  
+The `comparator_factory` argument is accepted by all `MapperProperty`-producing functions:  `column_property()`, `composite()`, `comparable_property()`, `synonym()`, `relation()`, `backref()`, `deferred()`, and `dynamic_loader()`.
+
 #### Composite Column Types {@name=composite}
 
 Sets of columns can be associated with a single datatype.  The ORM treats the group of columns like a single column which accepts and returns objects using the custom datatype you provide.  In this example, we'll create a table `vertices` which stores a pair of x/y coordinates, and a custom datatype `Point` which is a composite type of an x and y column:
@@ -347,7 +380,7 @@ In joined table inheritance, each class along a particular classes' list of pare
 
 The table also has a column called `type`.  It is strongly advised in both single- and joined- table inheritance scenarios that the root table contains a column whose sole purpose is that of the **discriminator**; it stores a value which indicates the type of object represented within the row.  The column may be of any desired datatype.  While there are some "tricks" to work around the requirement that there be a discriminator column, they are more complicated to configure when one wishes to load polymorphically.
 
-Next we define individual tables for each of `Engineer` and `Manager`, which each contain columns that represent the attributes unique to the subclass they represent.  Each table also must contain a primary key column (or columns), and in most cases a foreign key reference to the parent table.  It is  standard practice that the same column is used for both of these roles, and that the column is also named the same as that of the parent table.  However this is optional in SQLAlchemy; separate columns may be used for primary key and parent-relation, the column may be named differently than that of the parent, and even a custom join condition can be specified between parent and child tables instead of using a foreign key.  In joined table inheritance, the primary key of an instance is always represented by the primary key of the base table only (new in SQLAlchemy 0.4).
+Next we define individual tables for each of `Engineer` and `Manager`, which contain columns that represent the attributes unique to the subclass they represent.  Each table also must contain a primary key column (or columns), and in most cases a foreign key reference to the parent table.  It is  standard practice that the same column is used for both of these roles, and that the column is also named the same as that of the parent table.  However this is optional in SQLAlchemy; separate columns may be used for primary key and parent-relation, the column may be named differently than that of the parent, and even a custom join condition can be specified between parent and child tables instead of using a foreign key. 
 
     {python}
     engineers = Table('engineers', metadata,
@@ -360,6 +393,8 @@ Next we define individual tables for each of `Engineer` and `Manager`, which eac
        Column('manager_data', String(50)),
     )
 
+One natural effect of the joined table inheritance configuration is that the identity of any mapped object can be determined entirely from the base table.  This has obvious advantages, so SQLAlchemy always considers the primary key columns of a joined inheritance class to be those of the base table only, unless otherwise manually configured.  In other words, the `employee_id` column of both the `engineers` and `managers` table is not used to locate the `Engineer` or `Manager` object itself - only the value in `employees.employee_id` is considered, and the primary key in this case is non-composite.  `engineers.employee_id` and `managers.employee_id` are still of course critical to the proper operation of the pattern overall as they are used to locate the joined row, once the parent row has been determined, either through a distinct SELECT statement or all at once within a JOIN.
+
 We then configure mappers as usual, except we use some additional arguments to indicate the inheritance relationship, the polymorphic discriminator column, and the **polymorphic identity** of each class; this is the value that will be stored in the polymorphic discriminator column.
 
     {python}
@@ -367,30 +402,93 @@ We then configure mappers as usual, except we use some additional arguments to i
     mapper(Engineer, engineers, inherits=Employee, polymorphic_identity='engineer')
     mapper(Manager, managers, inherits=Employee, polymorphic_identity='manager')
 
-And that's it.  Querying against `Employee` will return a combination of `Employee`, `Engineer` and `Manager` objects.
+And that's it.  Querying against `Employee` will return a combination of `Employee`, `Engineer` and `Manager` objects.   Newly saved `Engineer`, `Manager`, and `Employee` objects will automatically populate the `employees.type` column with `engineer`, `manager`, or `employee`, as appropriate.
+
+###### Controlling Which Tables are Queried {@name=with_polymorphic}
 
-###### Polymorphic Querying Strategies {@name=querying}
+The `with_polymorphic()` method of `Query` affects the specific subclass tables which the Query selects from.  Normally, a query such as this:
 
-The `Query` object includes some helper functionality when dealing with joined-table inheritance mappings.  These are the `with_polymorphic()` and `of_type()` methods, both of which are introduced in version 0.4.4.
+    {python}
+    session.query(Employee).all()
 
-The `with_polymorphic()` method affects the specific subclass tables which the Query selects from.  Normally, a query such as this:
+...selects only from the `employees` table.   When loading fresh from the database, our joined-table setup will query from the parent table only, using SQL such as this:
 
     {python}
-    session.query(Employee).filter(Employee.name=='ed')
+    {opensql}
+    SELECT employees.employee_id AS employees_employee_id, employees.name AS employees_name, employees.type AS employees_type
+    FROM employees
+    []
 
-Selects only from the `employees` table.  The criterion we use in `filter()` and other methods will generate WHERE criterion against this table.  What if we wanted to load `Employee` objects but also wanted to use criterion against `Engineer` ?  We could just query against the `Engineer` class instead.  But, if we were using criterion which filters among more than one subclass (subclasses which do not inherit directly from one to the other), we'd like to select from an outer join of all those tables.  The `with_polymorphic()` method can tell `Query` which joined-table subclasses we want to select for:
+As attributes are requested from those `Employee` objects which are represented in either the `engineers` or `managers` child tables, a second load is issued for the columns in that related row, if the data was not already loaded.  So above, after accessing the objects you'd see further SQL issued along the lines of:
 
     {python}
-    session.query(Employee).with_polymorphic(Engineer).filter(Engineer.engineer_info=='some info')
+    {opensql}
+    SELECT managers.employee_id AS managers_employee_id, managers.manager_data AS managers_manager_data
+    FROM managers
+    WHERE ? = managers.employee_id
+    [5]
+    SELECT engineers.employee_id AS engineers_employee_id, engineers.engineer_info AS engineers_engineer_info
+    FROM engineers
+    WHERE ? = engineers.employee_id
+    [2]
+
+This behavior works well when issuing searches for small numbers of items, such as when using `get()`, since the full range of joined tables are not pulled in to the SQL statement unnecessarily.  But when querying a larger span of rows which are known to be of many types, you may want to actively join to some or all of the joined tables.  The `with_polymorphic` feature of `Query` and `mapper` provides this.
+
+Telling our query to polymorphically load `Engineer` and `Manager` objects:
+
+    {python}
+    query = session.query(Employee).with_polymorphic([Engineer, Manager])
+
+produces a query which joins the `employees` table to both the `engineers` and `managers` tables like the following:
+
+    {python}
+    query.all()
+    {opensql}
+    SELECT employees.employee_id AS employees_employee_id, engineers.employee_id AS engineers_employee_id, managers.employee_id AS managers_employee_id, employees.name AS employees_name, employees.type AS employees_type, engineers.engineer_info AS engineers_engineer_info, managers.manager_data AS managers_manager_data
+    FROM employees LEFT OUTER JOIN engineers ON employees.employee_id = engineers.employee_id LEFT OUTER JOIN managers ON employees.employee_id = managers.employee_id
+    []
 
-Even without criterion, the `with_polymorphic()` method has the added advantage that instances are loaded from all of their tables in one result set.  Such as, to optimize the loading of all `Employee` objects, `with_polymorphic()` accepts `'*'` as a wildcard indicating that all subclass tables should be joined:
+`with_polymorphic()` accepts a single class or mapper, a list of classes/mappers, or the string `'*'` to indicate all subclasses:
 
     {python}
-    session.query(Employee).with_polymorphic('*').all()
+    # join to the engineers table
+    query.with_polymorphic(Engineer)
+    
+    # join to the engineers and managers tables
+    query.with_polymorphic([Engineer, Manager])
+    
+    # join to all subclass tables
+    query.with_polymorphic('*')
+    
+It also accepts a second argument `selectable` which replaces the automatic join creation and instead selects directly from the selectable given.  This feature is normally used with "concrete" inheritance, described later, but can be used with any kind of inheritance setup in the case that specialized SQL should be used to load polymorphically:
 
-`with_polymorphic()` is an effective query-level alternative to the existing `select_table` option available on `mapper()`.
+    {python}
+    # custom selectable
+    query.with_polymorphic([Engineer, Manager], employees.outerjoin(managers).outerjoin(engineers))
+
+`with_polymorphic()` is also needed when you wish to add filter criterion that is specific to one or more subclasses, so that those columns are available to the WHERE clause:
+
+    {python}
+    session.query(Employee).with_polymorphic([Engineer, Manager]).\
+        filter(or_(Engineer.engineer_info=='w', Manager.manager_data=='q'))
+        
+Note that if you only need to load a single subtype, such as just the `Engineer` objects, `with_polymorphic()` is not needed since you would query against the `Engineer` class directly.
 
-Next is a way to join along `relation` paths while narrowing the criterion to specific subclasses.  Suppose the `employees` table represents a collection of employees which are associated with a `Company` object.  We'll add a `company_id` column to the `employees` table and a new table `companies`:
+The mapper also accepts `with_polymorphic` as a configurational argument so that the joined-style load will be issued automatically.  This argument may be the string `'*'`, a list of classes, or a tuple consisting of either, followed by a selectable.
+
+    {python}
+    mapper(Employee, employees, polymorphic_on=employees.c.type, \
+        polymorphic_identity='employee', with_polymorphic='*')
+    mapper(Engineer, engineers, inherits=Employee, polymorphic_identity='engineer')
+    mapper(Manager, managers, inherits=Employee, polymorphic_identity='manager')
+
+The above mapping will produce a query similar to that of `with_polymorphic('*')` for every query of `Employee` objects.
+
+Using `with_polymorphic()` with `Query` will override the mapper-level `with_polymorphic` setting.
+
+###### Creating Joins to Specific Subtypes {@name=joins}
+
+The `of_type()` method is a helper which allows the construction of joins along `relation` paths while narrowing the criterion to specific subclasses.  Suppose the `employees` table represents a collection of employees which are associated with a `Company` object.  We'll add a `company_id` column to the `employees` table and a new table `companies`:
 
     {python}
     companies = Table('companies', metadata,
@@ -412,20 +510,20 @@ Next is a way to join along `relation` paths while narrowing the criterion to sp
         'employees': relation(Employee)
     })
 
-If we wanted to join from `Company` to not just `Employee` but specifically `Engineers`, using the `join()` method or `any()` or `has()` operators will by default create a join from `companies` to `employees`, without including `engineers` or `managers` in the mix.  If we wish to have criterion which is specifically against the `Engineer` class, we can tell those methods to join or subquery against the full set of tables representing the subclass using the `of_type()` operator:
+When querying from `Company` onto the `Employee` relation, the `join()` method as well as the `any()` and `has()` operators will create a join from `companies` to `employees`, without including `engineers` or `managers` in the mix.  If we wish to have criterion which is specifically against the `Engineer` class, we can tell those methods to join or subquery against the joined table representing the subclass using the `of_type()` operator:
 
     {python}
     session.query(Company).join(Company.employees.of_type(Engineer)).filter(Engineer.engineer_info=='someinfo')
 
-A longhand notation, introduced in 0.4.3, is also available, which involves spelling out the full target selectable within a 2-tuple:
+A longhand version of this would involve spelling out the full target selectable within a 2-tuple:
 
     {python}
-    session.query(Company).join(('employees', employees.join(engineers))).filter(Engineer.engineer_info=='someinfo')
+    session.query(Company).join((employees.join(engineers), Company.employees)).filter(Engineer.engineer_info=='someinfo')
 
-The second notation allows more flexibility, such as joining to any group of subclass tables:
+Currently, `of_type()` accepts a single class argument.  It may be expanded later on to accept multiple classes.  For now, to join to any group of subclasses, the longhand notation allows this flexibility:
 
     {python}
-    session.query(Company).join(('employees', employees.outerjoin(engineers).outerjoin(managers))).\
+    session.query(Company).join((employees.outerjoin(engineers).outerjoin(managers), Company.employees)).\
         filter(or_(Engineer.engineer_info=='someinfo', Manager.manager_data=='somedata'))
 
 The `any()` and `has()` operators also can be used with `of_type()` when the embedded criterion is in terms of a subclass:
@@ -445,75 +543,6 @@ Note that the `any()` and `has()` are both shorthand for a correlated EXISTS que
 
 The EXISTS subquery above selects from the join of `employees` to `engineers`, and also specifies criterion which correlates the EXISTS subselect back to the parent `companies` table.
 
-###### Optimizing Joined Table Loads {@name=optimizing}
-
-When loading fresh from the database, the joined-table setup above will query from the parent table first, then for each row will issue a second query to the child table.  For example, for a load of five rows with `Employee` id 3, `Manager` ids 1 and 5 and `Engineer` ids 2 and 4, will produce queries along the lines of this example:
-
-    {python}
-    session.query(Employee).all()
-    {opensql}
-    SELECT employees.employee_id AS employees_employee_id, employees.name AS employees_name, employees.type AS employees_type
-    FROM employees ORDER BY employees.oid
-    []
-    SELECT managers.employee_id AS managers_employee_id, managers.manager_data AS managers_manager_data
-    FROM managers
-    WHERE ? = managers.employee_id
-    [5]
-    SELECT engineers.employee_id AS engineers_employee_id, engineers.engineer_info AS engineers_engineer_info
-    FROM engineers
-    WHERE ? = engineers.employee_id
-    [2]
-    SELECT engineers.employee_id AS engineers_employee_id, engineers.engineer_info AS engineers_engineer_info
-    FROM engineers
-    WHERE ? = engineers.employee_id
-    [4]
-    SELECT managers.employee_id AS managers_employee_id, managers.manager_data AS managers_manager_data
-    FROM managers
-    WHERE ? = managers.employee_id
-    [1]
-
-The above query works well for a `get()` operation, since it limits the queries to only the tables directly involved in fetching a single instance.  For instances which are already present in the session, the secondary table load is not needed.  However, the above loading style is not efficient for loading large groups of objects, as it incurs separate queries for each parent row.
-
-One way to reduce the number of "secondary" loads of child rows is to "defer" them, using `polymorphic_fetch='deferred'`:
-
-    {python}
-    mapper(Employee, employees, polymorphic_on=employees.c.type, \
-        polymorphic_identity='employee', polymorphic_fetch='deferred')
-    mapper(Engineer, engineers, inherits=Employee, polymorphic_identity='engineer')
-    mapper(Manager, managers, inherits=Employee, polymorphic_identity='manager')
-
-The above configuration queries in the same manner as earlier, except the load of each "secondary" table occurs only when attributes referencing those columns are first referenced on the loaded instance.  This style of loading is very efficient for cases where large selects of items occur, but a detailed "drill down" of extra inherited properties is less common.
-
-More commonly, an all-at-once load may be achieved by constructing a query which combines all three tables together.  The easiest way to do this as of version 0.4.4 is to use the `with_polymorphic()` query method which will automatically join in the classes desired:
-
-    {python}
-    query = session.query(Employee).with_polymorphic([Engineer, Manager])
-
-Which produces a query like the following:
-
-    {python}
-    query.all()
-    {opensql}
-    SELECT employees.employee_id AS employees_employee_id, engineers.employee_id AS engineers_employee_id, managers.employee_id AS managers_employee_id, employees.name AS employees_name, employees.type AS employees_type, engineers.engineer_info AS engineers_engineer_info, managers.manager_data AS managers_manager_data
-    FROM employees LEFT OUTER JOIN engineers ON employees.employee_id = engineers.employee_id LEFT OUTER JOIN managers ON employees.employee_id = managers.employee_id ORDER BY employees.oid
-    []
-
-`with_polymorphic()` accepts a single class or mapper, a list of classes/mappers, or the string `'*'` to indicate all subclasses.  It also accepts a second argument `selectable` which replaces the automatic join creation and instead selects directly from the selectable given.  This can allow polymorphic loads from a variety of inheritance schemes including concrete tables, if the appropriate unions are constructed.
-
-Similar behavior as provided by `with_polymorphic()` can be configured at the mapper level so that any user-defined query is used by default in order to load instances.  The `select_table` argument references an arbitrary selectable which the mapper will use for load operations (it has no impact on save operations).  Any selectable can be used for this, such as a UNION of tables.  For joined table inheritance, the easiest method is to use OUTER JOIN:
-
-    {python}
-    join = employees.outerjoin(engineers).outerjoin(managers)
-
-    mapper(Employee, employees, polymorphic_on=employees.c.type, \
-        polymorphic_identity='employee', select_table=join)
-    mapper(Engineer, engineers, inherits=Employee, polymorphic_identity='engineer')
-    mapper(Manager, managers, inherits=Employee, polymorphic_identity='manager')
-
-The above mapping will produce a query similar to that of `with_polymorphic('*')` for every query of `Employee` objects.
-
-When `select_table` is used, `with_polymorphic()` still overrides its usage at the query level.  For example, if `select_table` were configured to load from a join of multiple tables, using `with_polymorphic(Employee)` will limit the list of tables selected from to just the base table (as always, tables which don't get loaded in the first pass will be loaded on an as-needed basis).
-
 ##### Single Table Inheritance
 
 Single table inheritance is where the attributes of the base class as well as all subclasses are represented within a single table.  A column is present in the table for every attribute mapped to the base class and all subclasses; the columns which correspond to a single subclass are nullable.  This configuration looks much like joined-table inheritance except there's only one table.  In this case, a `type` column is required, as there would be no other way to discriminate between classes.  The table is specified in the base mapper only; for the inheriting classes, leave their `table` parameter blank:
@@ -563,7 +592,7 @@ Notice in this case there is no `type` column.  If polymorphic loading is not re
     mapper(Manager, managers_table)
     mapper(Engineer, engineers_table)
 
-To load polymorphically, the `select_table` argument is currently required.  In this case we must construct a UNION of all three tables.  SQLAlchemy includes a helper function to create these called `polymorphic_union`, which will map all the different columns into a structure of selects with the same numbers and names of columns, and also generate a virtual `type` column for each subselect:
+To load polymorphically, the `with_polymorphic` argument is required, along with a selectable indicating how rows should be loaded.  In this case we must construct a UNION of all three tables.  SQLAlchemy includes a helper function to create these called `polymorphic_union`, which will map all the different columns into a structure of selects with the same numbers and names of columns, and also generate a virtual `type` column for each subselect:
 
     {python}
     pjoin = polymorphic_union({
@@ -572,7 +601,7 @@ To load polymorphically, the `select_table` argument is currently required.  In
         'engineer': engineers_table
     }, 'type', 'pjoin')
 
-    employee_mapper = mapper(Employee, employees_table, select_table=pjoin, \
+    employee_mapper = mapper(Employee, employees_table, with_polymorphic=('*', pjoin), \
         polymorphic_on=pjoin.c.type, polymorphic_identity='employee')
     manager_mapper = mapper(Manager, managers_table, inherits=employee_mapper, \
         concrete=True, polymorphic_identity='manager')
@@ -598,7 +627,7 @@ Upon select, the polymorphic union produces a query like this:
         SELECT engineers.employee_id AS employee_id, CAST(NULL AS VARCHAR(50)) AS manager_data, engineers.name AS name,
         engineers.engineer_info AS engineer_info, 'engineer' AS type
         FROM engineers
-    ) AS pjoin ORDER BY pjoin.oid
+    ) AS pjoin
     []
 
 ##### Using Relations with Inheritance {@name=relations}
@@ -673,7 +702,7 @@ Let's crank it up and try loading with an eager load:
     managers.manager_data AS manager_data, managers.name AS name, managers.company_id AS company_id, 'manager' AS type
     FROM managers UNION ALL SELECT engineers.engineer_info AS engineer_info, engineers.employee_id AS employee_id,
     CAST(NULL AS VARCHAR(50)) AS manager_data, engineers.name AS name, engineers.company_id AS company_id, 'engineer' AS type
-    FROM engineers) AS anon_1 ON companies.id = anon_1.company_id ORDER BY companies.oid, anon_1.oid
+    FROM engineers) AS anon_1 ON companies.id = anon_1.company_id
     []
 
 The big limitation with concrete table inheritance is that relation()s placed on each concrete mapper do **not** propagate to child mappers.  If you want to have the same relation()s set up on all concrete mappers, they must be configured manually on each.
@@ -1081,7 +1110,7 @@ On the subject of joins, i.e. those described in [datamapping_joins](rel:datamap
         filter(and_(Node.parent_id==nodealias.c.id, nodealias.c.data=='child2')).all()
     SELECT treenodes.id AS treenodes_id, treenodes.parent_id AS treenodes_parent_id, treenodes.data AS treenodes_data
     FROM treenodes, treenodes AS treenodes_1
-    WHERE treenodes.data = ? AND treenodes.parent_id = treenodes_1.id AND treenodes_1.data = ? ORDER BY treenodes.oid
+    WHERE treenodes.data = ? AND treenodes.parent_id = treenodes_1.id AND treenodes_1.data = ?
     ['subchild1', 'child2']
 
 or automatically, using `join()` with `aliased=True`:
@@ -1092,7 +1121,7 @@ or automatically, using `join()` with `aliased=True`:
         join('parent', aliased=True).filter(Node.data=='child2').all()
     SELECT treenodes.id AS treenodes_id, treenodes.parent_id AS treenodes_parent_id, treenodes.data AS treenodes_data
     FROM treenodes JOIN treenodes AS treenodes_1 ON treenodes_1.id = treenodes.parent_id
-    WHERE treenodes.data = ? AND treenodes_1.data = ? ORDER BY treenodes.oid
+    WHERE treenodes.data = ? AND treenodes_1.data = ?
     ['subchild1', 'child2']
 
 To add criterion to multiple points along a longer join, use `from_joinpoint=True`:
@@ -1104,7 +1133,7 @@ To add criterion to multiple points along a longer join, use `from_joinpoint=Tru
         join('parent', aliased=True, from_joinpoint=True).filter(Node.data=='root').all()
     SELECT treenodes.id AS treenodes_id, treenodes.parent_id AS treenodes_parent_id, treenodes.data AS treenodes_data
     FROM treenodes JOIN treenodes AS treenodes_1 ON treenodes_1.id = treenodes.parent_id JOIN treenodes AS treenodes_2 ON treenodes_2.id = treenodes_1.parent_id
-    WHERE treenodes.data = ? AND treenodes_1.data = ? AND treenodes_2.data = ? ORDER BY treenodes.oid
+    WHERE treenodes.data = ? AND treenodes_1.data = ? AND treenodes_2.data = ?
     ['subchild1', 'child2', 'root']
 
 ##### Configuring Eager Loading {@name=eagerloading}
@@ -1118,7 +1147,7 @@ Eager loading of relations occurs using joins or outerjoins from parent to child
 
     {sql}session.query(Node).all()
     SELECT treenodes_1.id AS treenodes_1_id, treenodes_1.parent_id AS treenodes_1_parent_id, treenodes_1.data AS treenodes_1_data, treenodes_2.id AS treenodes_2_id, treenodes_2.parent_id AS treenodes_2_parent_id, treenodes_2.data AS treenodes_2_data, treenodes.id AS treenodes_id, treenodes.parent_id AS treenodes_parent_id, treenodes.data AS treenodes_data
-    FROM treenodes LEFT OUTER JOIN treenodes AS treenodes_2 ON treenodes.id = treenodes_2.parent_id LEFT OUTER JOIN treenodes AS treenodes_1 ON treenodes_2.id = treenodes_1.parent_id ORDER BY treenodes.oid, treenodes_2.oid, treenodes_1.oid
+    FROM treenodes LEFT OUTER JOIN treenodes AS treenodes_2 ON treenodes.id = treenodes_2.parent_id LEFT OUTER JOIN treenodes AS treenodes_1 ON treenodes_2.id = treenodes_1.parent_id
     []
 
 #### Specifying Alternate Join Conditions to relation() {@name=customjoin}
@@ -1364,7 +1393,7 @@ In the [datamapping](rel:datamapping), we introduced the concept of **Eager Load
     addresses_1.user_id AS addresses_1_user_id, users.id AS users_id, users.name AS users_name,
     users.fullname AS users_fullname, users.password AS users_password
     FROM users LEFT OUTER JOIN addresses AS addresses_1 ON users.id = addresses_1.user_id
-    WHERE users.name = ? ORDER BY users.oid, addresses_1.oid
+    WHERE users.name = ?
     ['jack']
 
 By default, all relations are **lazy loading**.  The scalar or collection attribute associated with a `relation()` contains a trigger which fires the first time the attribute is accessed, which issues a SQL call at that point:
@@ -1373,7 +1402,7 @@ By default, all relations are **lazy loading**.  The scalar or collection attrib
     {sql}>>> jack.addresses
     SELECT addresses.id AS addresses_id, addresses.email_address AS addresses_email_address, addresses.user_id AS addresses_user_id
     FROM addresses
-    WHERE ? = addresses.user_id ORDER BY addresses.oid
+    WHERE ? = addresses.user_id
     [5]
     {stop}[<Address(u'jack@google.com')>, <Address(u'j25@yahoo.com')>]
 
index 4496c21e4f5819607cb0acbd1b2cce8e6c471d02..c9066fb6c7e64d1783ef66ea85a80d34a3de20a2 100644 (file)
@@ -41,6 +41,7 @@ from sqlalchemy.orm.properties import (
      ColumnProperty,
      ComparableProperty,
      CompositeProperty,
+     RelationProperty,
      PropertyLoader,
      SynonymProperty,
      )
@@ -159,7 +160,7 @@ def relation(argument, secondary=None, **kwargs):
 
     This corresponds to a parent-child or associative table relationship.  The
     constructed class is an instance of
-    [sqlalchemy.orm.properties#PropertyLoader].
+    [sqlalchemy.orm.properties#RelationProperty].
 
       argument
           a class or Mapper instance, representing the target of the relation.
@@ -209,6 +210,10 @@ def relation(argument, secondary=None, **kwargs):
           a class or function that returns a new list-holding object. will be
           used in place of a plain list for storing elements.
 
+        comparator_factory
+          a class which extends sqlalchemy.orm.properties.RelationProperty.Comparator
+          which provides custom SQL clause generation for comparison operations.
+          
         extension
           an [sqlalchemy.orm.interfaces#AttributeExtension] instance, 
           or list of extensions, which will be prepended to the list of 
@@ -359,11 +364,11 @@ def relation(argument, secondary=None, **kwargs):
           use an alternative method.
 
     """
-    return PropertyLoader(argument, secondary=secondary, **kwargs)
+    return RelationProperty(argument, secondary=secondary, **kwargs)
 
 def dynamic_loader(argument, secondary=None, primaryjoin=None, secondaryjoin=None, 
     foreign_keys=None, backref=None, post_update=False, cascade=False, remote_side=None, enable_typechecks=True,
-    passive_deletes=False, order_by=None):
+    passive_deletes=False, order_by=None, comparator_factory=None):
     """Construct a dynamically-loading mapper property.
 
     This property is similar to relation(), except read operations return an
@@ -377,10 +382,10 @@ def dynamic_loader(argument, secondary=None, primaryjoin=None, secondaryjoin=Non
     """
     from sqlalchemy.orm.dynamic import DynaLoader
 
-    return PropertyLoader(argument, secondary=secondary, primaryjoin=primaryjoin,
+    return RelationProperty(argument, secondary=secondary, primaryjoin=primaryjoin,
             secondaryjoin=secondaryjoin, foreign_keys=foreign_keys, backref=backref,
             post_update=post_update, cascade=cascade, remote_side=remote_side, enable_typechecks=enable_typechecks,
-            passive_deletes=passive_deletes, order_by=order_by,
+            passive_deletes=passive_deletes, order_by=order_by, comparator_factory=comparator_factory,
             strategy_class=DynaLoader)
 
 def column_property(*args, **kwargs):
@@ -398,6 +403,10 @@ def column_property(*args, **kwargs):
       \*cols
           list of Column objects to be mapped.
 
+      comparator_factory
+        a class which extends sqlalchemy.orm.properties.RelationProperty.Comparator
+        which provides custom SQL clause generation for comparison operations.
+
       group
           a group name for this property when marked as deferred.
 
index 37a43101286cb812d290b6cb9253ae056fad8a28..a6b49ab6b65a8199ba9406d5ee4f7ef13a56450d 100644 (file)
@@ -139,6 +139,9 @@ class Mapper(object):
 
         self.select_table = select_table
         if select_table:
+            util.warn_deprecated('select_table option is deprecated.  Use with_polymorphic=("*", selectable) '
+                            'instead.')
+
             if with_polymorphic:
                 raise sa_exc.ArgumentError("select_table can't be used with "
                             "with_polymorphic (they define conflicting settings)")
index bd493aaf25219334feec8b845d4f4a31b62de7ae..714b18047413ec83da0f159c995f176bf9a3bc10 100644 (file)
@@ -25,7 +25,7 @@ from sqlalchemy.orm.interfaces import (
     )
 
 __all__ = ('ColumnProperty', 'CompositeProperty', 'SynonymProperty',
-           'ComparableProperty', 'PropertyLoader', 'BackRef')
+           'ComparableProperty', 'RelationProperty', 'BackRef')
 
 
 class ColumnProperty(StrategizedProperty):
@@ -82,7 +82,7 @@ class ColumnProperty(StrategizedProperty):
     def get_col_value(self, column, value):
         return value
 
-    class ColumnComparator(PropComparator):
+    class Comparator(PropComparator):
         @util.memoized_instancemethod
         def __clause_element__(self):
             if self.adapter:
@@ -96,7 +96,9 @@ class ColumnProperty(StrategizedProperty):
         def reverse_operate(self, op, other, **kwargs):
             col = self.__clause_element__()
             return op(col._bind_param(other), col, **kwargs)
-
+    
+    ColumnComparator = Comparator
+    
     def __str__(self):
         return str(self.parent.class_.__name__) + "." + self.key
 
@@ -244,7 +246,7 @@ class ComparableProperty(MapperProperty):
         pass
 
 
-class PropertyLoader(StrategizedProperty):
+class RelationProperty(StrategizedProperty):
     """Describes an object property that holds a single item or list
     of items that correspond to a related database table.
     """
@@ -263,6 +265,7 @@ class PropertyLoader(StrategizedProperty):
         collection_class=None, passive_deletes=False,
         passive_updates=True, remote_side=None,
         enable_typechecks=True, join_depth=None,
+        comparator_factory=None,
         strategy_class=None, _local_remote_pairs=None):
 
         self.uselist = uselist
@@ -280,12 +283,13 @@ class PropertyLoader(StrategizedProperty):
         self.passive_updates = passive_updates
         self.remote_side = remote_side
         self.enable_typechecks = enable_typechecks
-        self.comparator = PropertyLoader.Comparator(self, None)
+        
         self.join_depth = join_depth
         self.local_remote_pairs = _local_remote_pairs
         self.extension = extension
         self.__join_cache = {}
-        self.comparator_factory = PropertyLoader.Comparator
+        self.comparator_factory = comparator_factory or RelationProperty.Comparator
+        self.comparator = self.comparator_factory(self, None)
         util.set_creation_order(self)
 
         if strategy_class:
@@ -337,7 +341,7 @@ class PropertyLoader(StrategizedProperty):
             on the local side of generated expressions.
 
             """
-            return PropertyLoader.Comparator(self.prop, self.mapper, getattr(self, '_of_type', None), adapter)
+            return self.__class__(self.prop, self.mapper, getattr(self, '_of_type', None), adapter)
             
         @property
         def parententity(self):
@@ -357,7 +361,7 @@ class PropertyLoader(StrategizedProperty):
             return op(self, *other, **kwargs)
 
         def of_type(self, cls):
-            return PropertyLoader.Comparator(self.prop, self.mapper, cls)
+            return RelationProperty.Comparator(self.prop, self.mapper, cls)
 
         def in_(self, other):
             raise NotImplementedError("in_() not yet supported for relations.  For a "
@@ -843,7 +847,7 @@ class PropertyLoader(StrategizedProperty):
                 "added to the primary mapper, i.e. the very first "
                 "mapper created for class '%s' " % (self.key, self.parent.class_.__name__, self.parent.class_.__name__))
 
-        super(PropertyLoader, self).do_init()
+        super(RelationProperty, self).do_init()
 
     def _refers_to_parent_table(self):
         return self.parent.mapped_table is self.target or self.parent.mapped_table is self.target
@@ -946,12 +950,13 @@ class PropertyLoader(StrategizedProperty):
         if not self.viewonly:
             self._dependency_processor.register_dependencies(uowcommit)
 
-log.class_logger(PropertyLoader)
+PropertyLoader = RelationProperty
+log.class_logger(RelationProperty)
 
 class BackRef(object):
-    """Attached to a PropertyLoader to indicate a complementary reverse relationship.
+    """Attached to a RelationProperty to indicate a complementary reverse relationship.
 
-    Can optionally create the complementing PropertyLoader if one does not exist already."""
+    Can optionally create the complementing RelationProperty if one does not exist already."""
 
     def __init__(self, key, _prop=None, **kwargs):
         self.key = key
@@ -982,7 +987,7 @@ class BackRef(object):
             self.kwargs.setdefault('viewonly', prop.viewonly)
             self.kwargs.setdefault('post_update', prop.post_update)
 
-            relation = PropertyLoader(parent, prop.secondary, pj, sj,
+            relation = RelationProperty(parent, prop.secondary, pj, sj,
                                       backref=BackRef(prop.key, _prop=prop),
                                       _is_backref=True,
                                       **self.kwargs)
index ae83b1c999dadd5695d0281f19c79c584c67d3ba..4d94953a22303b216aeafdb609daa637497ec8d7 100644 (file)
@@ -539,7 +539,29 @@ class DeclarativeTest(testing.TestBase, testing.AssertsExecutionResults):
         sess.add(u1)
         sess.flush()
         eq_(sess.query(User).filter(User.name == "SOMENAME someuser").one(), u1)
+    
+    def test_synonym_no_descriptor(self):
+        from sqlalchemy.orm.properties import ColumnProperty
+        
+        class CustomCompare(ColumnProperty.Comparator):
+            def __eq__(self, other):
+                return self.__clause_element__() == other + ' FOO'
+                
+        class User(Base, ComparableEntity):
+            __tablename__ = 'users'
 
+            id = Column('id', Integer, primary_key=True)
+            _name = Column('name', String(50))
+            name = sa.orm.synonym('_name', comparator_factory=CustomCompare)
+        
+        Base.metadata.create_all()
+
+        sess = create_session()
+        u1 = User(name='someuser FOO')
+        sess.add(u1)
+        sess.flush()
+        eq_(sess.query(User).filter(User.name == "someuser").one(), u1)
+        
     def test_synonym_added(self):
         class User(Base, ComparableEntity):
             __tablename__ = 'users'
index aa429bc29ae3b010a43c1897dcbfffb634bbe82b..7c9bed4ecc7039e213b36da292c32ad196dcf7ef 100644 (file)
@@ -49,7 +49,7 @@ class CompileTest(_base.ORMTest):
         order_join = order.select().alias('pjoin')
 
         order_mapper = mapper(Order, order,
-            select_table=order_join,
+            with_polymorphic=('*', order_join),
             polymorphic_on=order_join.c.type,
             polymorphic_identity='order',
             properties={
@@ -102,7 +102,7 @@ class CompileTest(_base.ORMTest):
         order_join = order.select().alias('pjoin')
 
         order_mapper = mapper(Order, order,
-            select_table=order_join,
+            with_polymorphic=('*', order_join),
             polymorphic_on=order_join.c.type,
             polymorphic_identity='order',
             properties={
index 1a91df98759710f9927ea39a1c477dd1705ac456..7b14ca929c9573127f7c5e3799596f5c47000542 100644 (file)
@@ -92,8 +92,8 @@ def produce_test(parent, child, direction):
             class B(A):pass
             class C(B):pass
 
-            mapper(A, ta, polymorphic_on=abcjoin.c.type, select_table=abcjoin, polymorphic_identity="a")
-            mapper(B, tb, polymorphic_on=bcjoin.c.type, select_table=bcjoin, polymorphic_identity="b", inherits=A, inherit_condition=atob)
+            mapper(A, ta, polymorphic_on=abcjoin.c.type, with_polymorphic=('*', abcjoin), polymorphic_identity="a")
+            mapper(B, tb, polymorphic_on=bcjoin.c.type, with_polymorphic=('*', bcjoin), polymorphic_identity="b", inherits=A, inherit_condition=atob)
             mapper(C, tc, polymorphic_identity="c", inherits=B, inherit_condition=btoc)
 
             parent_mapper = class_mapper({ta:A, tb:B, tc:C}[parent_table])
index 367c2e73cc0747b97eb967e607815232774d1ed6..05df5d51ee4858f5ac90b65746dc8a22192c87db 100644 (file)
@@ -32,8 +32,8 @@ class ABCTest(ORMTest):
             else:
                 abc = bc = None
 
-            mapper(A, a, select_table=abc, polymorphic_on=a.c.type, polymorphic_identity='a')
-            mapper(B, b, select_table=bc, inherits=A, polymorphic_identity='b')
+            mapper(A, a, with_polymorphic=('*', abc), polymorphic_on=a.c.type, polymorphic_identity='a')
+            mapper(B, b, with_polymorphic=('*', bc), inherits=A, polymorphic_identity='b')
             mapper(C, c, inherits=B, polymorphic_identity='c')
 
             a1 = A(adata='a1')
index 621f9639f41a6c41f7bc35c025b55bd2f9652011..a0444269d1f285574b995d93ce35b9cf0b86087f 100644 (file)
@@ -148,10 +148,10 @@ def generate_round_trip_test(use_unions=False, use_joins=False):
                     'c': page_table.join(magazine_page_table).join(classified_page_table),
                     'p': page_table.select(page_table.c.type=='p'),
                 }, None, 'page_join')
-            page_mapper = mapper(Page, page_table, select_table=page_join, polymorphic_on=page_join.c.type, polymorphic_identity='p')
+            page_mapper = mapper(Page, page_table, with_polymorphic=('*', page_join), polymorphic_on=page_join.c.type, polymorphic_identity='p')
         elif use_joins:
             page_join = page_table.outerjoin(magazine_page_table).outerjoin(classified_page_table)
-            page_mapper = mapper(Page, page_table, select_table=page_join, polymorphic_on=page_table.c.type, polymorphic_identity='p')
+            page_mapper = mapper(Page, page_table, with_polymorphic=('*', page_join), polymorphic_on=page_table.c.type, polymorphic_identity='p')
         else:
             page_mapper = mapper(Page, page_table, polymorphic_on=page_table.c.type, polymorphic_identity='p')
 
@@ -161,12 +161,12 @@ def generate_round_trip_test(use_unions=False, use_joins=False):
                     'm': page_table.join(magazine_page_table),
                     'c': page_table.join(magazine_page_table).join(classified_page_table),
                 }, None, 'page_join')
-            magazine_page_mapper = mapper(MagazinePage, magazine_page_table, select_table=magazine_join, inherits=page_mapper, polymorphic_identity='m', properties={
+            magazine_page_mapper = mapper(MagazinePage, magazine_page_table, with_polymorphic=('*', magazine_join), inherits=page_mapper, polymorphic_identity='m', properties={
                 'magazine': relation(Magazine, backref=backref('pages', order_by=magazine_join.c.page_no))
             })
         elif use_joins:
             magazine_join = page_table.join(magazine_page_table).outerjoin(classified_page_table)
-            magazine_page_mapper = mapper(MagazinePage, magazine_page_table, select_table=magazine_join, inherits=page_mapper, polymorphic_identity='m', properties={
+            magazine_page_mapper = mapper(MagazinePage, magazine_page_table, with_polymorphic=('*', magazine_join), inherits=page_mapper, polymorphic_identity='m', properties={
                 'magazine': relation(Magazine, backref=backref('pages', order_by=page_table.c.page_no))
             })
         else:
index c82916be89c76c8395cf9e2dbeac5a2c4efd5322..cbdbd4c008cccde217e298adf075f9a3f317fd53 100644 (file)
@@ -64,7 +64,7 @@ class InsertOrderTest(PolymorphTest):
                 'person':people.select(people.c.type=='person'),
             }, None, 'pjoin')
 
-        person_mapper = mapper(Person, people, select_table=person_join, polymorphic_on=person_join.c.type, polymorphic_identity='person')
+        person_mapper = mapper(Person, people, with_polymorphic=('*', person_join), polymorphic_on=person_join.c.type, polymorphic_identity='person')
 
         mapper(Engineer, engineers, inherits=person_mapper, polymorphic_identity='engineer')
         mapper(Manager, managers, inherits=person_mapper, polymorphic_identity='manager')
index fc65e22b53d60d32cdc6fcd6e2d462301c9c90ed..78081d506dbcd8a49b0b27cc5680f1620ba3faa9 100644 (file)
@@ -156,7 +156,7 @@ class RelationTest2(ORMTest):
                     self.data = data
             mapper(Data, data)
 
-        mapper(Person, people, select_table=poly_union, polymorphic_identity='person', polymorphic_on=polymorphic_on)
+        mapper(Person, people, with_polymorphic=('*', poly_union), polymorphic_identity='person', polymorphic_on=polymorphic_on)
 
         if usedata:
             mapper(Manager, managers, inherits=Person, inherit_condition=people.c.person_id==managers.c.person_id, polymorphic_identity='manager',
@@ -241,14 +241,14 @@ def generate_test(jointype="join1", usedata=False):
             mapper(Data, data)
 
         if usedata:
-            mapper(Person, people, select_table=poly_union, polymorphic_identity='person', polymorphic_on=people.c.type,
+            mapper(Person, people, with_polymorphic=('*', poly_union), polymorphic_identity='person', polymorphic_on=people.c.type,
                   properties={
                     'colleagues':relation(Person, primaryjoin=people.c.colleague_id==people.c.person_id, remote_side=people.c.colleague_id, uselist=True),
                     'data':relation(Data, uselist=False)
                     }
             )
         else:
-            mapper(Person, people, select_table=poly_union, polymorphic_identity='person', polymorphic_on=people.c.type,
+            mapper(Person, people, with_polymorphic=('*', poly_union), polymorphic_identity='person', polymorphic_on=people.c.type,
                   properties={
                     'colleagues':relation(Person, primaryjoin=people.c.colleague_id==people.c.person_id,
                         remote_side=people.c.colleague_id, uselist=True)
@@ -348,7 +348,7 @@ class RelationTest4(ORMTest):
                 'manager':people.join(managers),
             }, "type", 'employee_join')
 
-        person_mapper   = mapper(Person, people, select_table=employee_join,polymorphic_on=employee_join.c.type, polymorphic_identity='person')
+        person_mapper   = mapper(Person, people, with_polymorphic=('*', employee_join), polymorphic_on=employee_join.c.type, polymorphic_identity='person')
         engineer_mapper = mapper(Engineer, engineers, inherits=person_mapper, polymorphic_identity='engineer')
         manager_mapper  = mapper(Manager, managers, inherits=person_mapper, polymorphic_identity='manager')
         car_mapper      = mapper(Car, cars, properties= {'employee':relation(person_mapper)})
@@ -579,12 +579,12 @@ class RelationTest7(ORMTest):
             }, "type", 'car_join')
 
         car_mapper  = mapper(Car, cars,
-                select_table=car_join,polymorphic_on=car_join.c.type,
+                with_polymorphic=('*', car_join) ,polymorphic_on=car_join.c.type,
                 polymorphic_identity='car',
                 )
         offroad_car_mapper = mapper(Offraod_Car, offroad_cars, inherits=car_mapper, polymorphic_identity='offroad')
         person_mapper = mapper(Person, people,
-                select_table=employee_join,polymorphic_on=employee_join.c.type,
+                with_polymorphic=('*', employee_join), polymorphic_on=employee_join.c.type,
                 polymorphic_identity='person',
                 properties={
                     'car':relation(car_mapper)
@@ -686,7 +686,7 @@ class GenerativeTest(TestBase, AssertsExecutionResults):
 
         status_mapper   = mapper(Status, status)
         person_mapper   = mapper(Person, people,
-            select_table=employee_join,polymorphic_on=employee_join.c.type,
+            with_polymorphic=('*', employee_join), polymorphic_on=employee_join.c.type,
             polymorphic_identity='person', properties={'status':relation(status_mapper)})
         engineer_mapper = mapper(Engineer, engineers, inherits=person_mapper, polymorphic_identity='engineer')
         manager_mapper  = mapper(Manager, managers, inherits=person_mapper, polymorphic_identity='manager')
@@ -781,7 +781,7 @@ class MultiLevelTest(ORMTest):
         mapper_Employee = mapper( Employee, table_Employee,
                     polymorphic_identity= 'Employee',
                     polymorphic_on= pu_Employee.c.atype,
-                    select_table= pu_Employee,
+                    with_polymorphic=('*', pu_Employee),
                 )
 
         pu_Engineer = polymorphic_union( {
@@ -793,7 +793,7 @@ class MultiLevelTest(ORMTest):
                     inherits= mapper_Employee,
                     polymorphic_identity= 'Engineer',
                     polymorphic_on= pu_Engineer.c.atype,
-                    select_table= pu_Engineer,
+                    with_polymorphic=('*', pu_Engineer),
                 )
 
         mapper_Manager = mapper( Manager, table_Manager,
@@ -851,7 +851,7 @@ class ManyToManyPolyTest(ORMTest):
 
         mapper(
             BaseItem, base_item_table,
-            select_table=item_join,
+            with_polymorphic=('*', item_join),
             polymorphic_on=base_item_table.c.child_name,
             polymorphic_identity='BaseItem',
             properties=dict(collections=relation(Collection, secondary=base_item_collection_table, backref="items")))
@@ -892,7 +892,7 @@ class CustomPKTest(ORMTest):
         d['t2'] = t1.join(t2)
         pjoin = polymorphic_union(d, None, 'pjoin')
 
-        mapper(T1, t1, polymorphic_on=t1.c.type, polymorphic_identity='t1', select_table=pjoin, primary_key=[pjoin.c.id])
+        mapper(T1, t1, polymorphic_on=t1.c.type, polymorphic_identity='t1', with_polymorphic=('*', pjoin), primary_key=[pjoin.c.id])
         mapper(T2, t2, inherits=T1, polymorphic_identity='t2')
         print [str(c) for c in class_mapper(T1).primary_key]
         ot1 = T1()
@@ -927,7 +927,7 @@ class CustomPKTest(ORMTest):
         d['t2'] = t1.join(t2)
         pjoin = polymorphic_union(d, None, 'pjoin')
 
-        mapper(T1, t1, polymorphic_on=t1.c.type, polymorphic_identity='t1', select_table=pjoin)
+        mapper(T1, t1, polymorphic_on=t1.c.type, polymorphic_identity='t1', with_polymorphic=('*', pjoin))
         mapper(T2, t2, inherits=T1, polymorphic_identity='t2')
         assert len(class_mapper(T1).primary_key) == 1
 
@@ -1043,7 +1043,7 @@ class MissingPolymorphicOnTest(ORMTest):
         poly_select = select([tablea, tableb.c.data.label('discriminator')], from_obj=tablea.join(tableb)).alias('poly')
         
         mapper(B, tableb)
-        mapper(A, tablea, select_table=poly_select, polymorphic_on=poly_select.c.discriminator, properties={
+        mapper(A, tablea, with_polymorphic=('*', poly_select), polymorphic_on=poly_select.c.discriminator, properties={
             'b':relation(B, uselist=False)
         })
         mapper(C, tablec, inherits=A,polymorphic_identity='c')
index b3a343e3878c5e05c527b9fd02138b1a328b195d..6d516301d465ec5aea3a31cf241457eb95faf663 100644 (file)
@@ -29,14 +29,14 @@ class InheritingSelectablesTest(ORMTest):
         mapper(Foo, foo, polymorphic_on=foo.c.b)
 
         mapper(Baz, baz,
-                    select_table=foo.join(baz, foo.c.b=='baz').alias('baz'),
+                    with_polymorphic=('*', foo.join(baz, foo.c.b=='baz').alias('baz')),
                     inherits=Foo,
                     inherit_condition=(foo.c.a==baz.c.a),
                     inherit_foreign_keys=[baz.c.a],
                     polymorphic_identity='baz')
 
         mapper(Bar, bar,
-                    select_table=foo.join(bar, foo.c.b=='bar').alias('bar'),
+                    with_polymorphic=('*', foo.join(bar, foo.c.b=='bar').alias('bar')),
                     inherits=Foo,
                     inherit_condition=(foo.c.a==bar.c.a),
                     inherit_foreign_keys=[bar.c.a],
index 7c6984919ce9eea4a5efb753552f628a9e85291f..8cf4fec06e6b229b2e868730c50a26a16c259728 100644 (file)
@@ -2,10 +2,11 @@
 
 import testenv; testenv.configure_for_tests()
 from testlib import sa, testing
-from testlib.sa import MetaData, Table, Column, Integer, String, ForeignKey
-from testlib.sa.orm import mapper, relation, backref, create_session, class_mapper, reconstructor, validates
-from testlib.sa.orm import defer, deferred, synonym, attributes
-from testlib.testing import eq_
+from testlib.sa import MetaData, Table, Column, Integer, String, ForeignKey, func
+from testlib.sa.engine import default
+from testlib.sa.orm import mapper, relation, backref, create_session, class_mapper, reconstructor, validates, aliased
+from testlib.sa.orm import defer, deferred, synonym, attributes, column_property, composite, relation, dynamic_loader, comparable_property
+from testlib.testing import eq_, AssertsCompiledSQL
 import pickleable
 from orm import _base, _fixtures
 
@@ -1128,7 +1129,80 @@ class ValidatorTest(_fixtures.FixtureTest):
             sess.query(User).filter_by(name='edward').one(), 
             User(name='edward', addresses=[Address(email_address='foo@bar.com')])
         )
+
+class ComparatorFactoryTest(_fixtures.FixtureTest, AssertsCompiledSQL):
+    @testing.resolve_artifact_names
+    def test_kwarg_accepted(self):
+        class DummyComposite(object):
+            def __init__(self, x, y):
+                pass
+        
+        from sqlalchemy.orm.interfaces import PropComparator
+        
+        class MyFactory(PropComparator):
+            pass
+            
+        for args in (
+            (column_property, users.c.name),
+            (deferred, users.c.name),
+            (synonym, 'name'),
+            (composite, DummyComposite, users.c.id, users.c.name),
+            (relation, Address),
+            (backref, 'address'),
+            (comparable_property, ),
+            (dynamic_loader, Address)
+        ):
+            fn = args[0]
+            args = args[1:]
+            fn(comparator_factory=MyFactory, *args)
         
+    @testing.resolve_artifact_names
+    def test_column(self):
+        from sqlalchemy.orm.properties import ColumnProperty
+        
+        class MyFactory(ColumnProperty.Comparator):
+            def __eq__(self, other):
+                return func.foobar(self.__clause_element__()) == func.foobar(other)
+        mapper(User, users, properties={'name':column_property(users.c.name, comparator_factory=MyFactory)})
+        self.assert_compile(User.name == 'ed', "foobar(users.name) = foobar(:foobar_1)", dialect=default.DefaultDialect())
+        self.assert_compile(aliased(User).name == 'ed', "foobar(users_1.name) = foobar(:foobar_1)", dialect=default.DefaultDialect())
+
+    @testing.resolve_artifact_names
+    def test_synonym(self):
+        from sqlalchemy.orm.properties import ColumnProperty
+        class MyFactory(ColumnProperty.Comparator):
+            def __eq__(self, other):
+                return func.foobar(self.__clause_element__()) == func.foobar(other)
+        mapper(User, users, properties={'name':synonym('_name', map_column=True, comparator_factory=MyFactory)})
+        self.assert_compile(User.name == 'ed', "foobar(users.name) = foobar(:foobar_1)", dialect=default.DefaultDialect())
+        self.assert_compile(aliased(User).name == 'ed', "foobar(users_1.name) = foobar(:foobar_1)", dialect=default.DefaultDialect())
+
+    @testing.resolve_artifact_names
+    def test_relation(self):
+        from sqlalchemy.orm.properties import PropertyLoader
+
+        class MyFactory(PropertyLoader.Comparator):
+            def __eq__(self, other):
+                return func.foobar(self.__clause_element__().c.user_id) == func.foobar(other.id)
+
+        class MyFactory2(PropertyLoader.Comparator):
+            def __eq__(self, other):
+                return func.foobar(self.__clause_element__().c.id) == func.foobar(other.user_id)
+                
+        mapper(User, users)
+        mapper(Address, addresses, properties={
+            'user':relation(User, comparator_factory=MyFactory, 
+                backref=backref("addresses", comparator_factory=MyFactory2)
+            )
+            }
+        )
+        self.assert_compile(Address.user == User(id=5), "foobar(addresses.user_id) = foobar(:foobar_1)", dialect=default.DefaultDialect())
+        self.assert_compile(User.addresses == Address(id=5, user_id=7), "foobar(users.id) = foobar(:foobar_1)", dialect=default.DefaultDialect())
+
+        self.assert_compile(aliased(Address).user == User(id=5), "foobar(addresses_1.user_id) = foobar(:foobar_1)", dialect=default.DefaultDialect())
+        self.assert_compile(aliased(User).addresses == Address(id=5, user_id=7), "foobar(users_1.id) = foobar(:foobar_1)", dialect=default.DefaultDialect())
+
+    
 class DeferredTest(_fixtures.FixtureTest):
 
     @testing.resolve_artifact_names