]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- [feature] polymorphic_on now accepts many
authorMike Bayer <mike_mp@zzzcomputing.com>
Fri, 9 Dec 2011 05:56:12 +0000 (00:56 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Fri, 9 Dec 2011 05:56:12 +0000 (00:56 -0500)
new kinds of values:

- standalone expressions that aren't
otherwise mapped
- column_property() objects
- string names of any column_property()
or attribute name of a mapped Column

The docs include an example using
the case() construct, which is likely to be
a common constructed used here.
[ticket:2345] and part of [ticket:2238]

CHANGES
doc/build/orm/inheritance.rst
doc/build/orm/mapper_config.rst
doc/build/orm/relationships.rst
lib/sqlalchemy/orm/__init__.py
lib/sqlalchemy/orm/mapper.py
lib/sqlalchemy/orm/properties.py
test/orm/inheritance/test_basic.py

diff --git a/CHANGES b/CHANGES
index 791547e00188af79b96c459d1c205b0c28522e78..83626cf38df9aa4a6c19e1a08b91b914a3333405 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -6,95 +6,95 @@ CHANGES
 0.7.4
 =====
 - orm
-   - [bug] Fixed backref behavior when "popping" the 
-     value off of a many-to-one in response to 
-     a removal from a stale one-to-many - the operation
-     is skipped, since the many-to-one has since
-     been updated.  [ticket:2315]
-
-   - [bug] After some years of not doing this, added
-     more granularity to the "is X a parent of Y" 
-     functionality, which is used when determining
-     if the FK on "Y" needs to be "nulled out" as well
-     as if "Y" should be deleted with delete-orphan
-     cascade.   The test now takes into account the
-     Python identity of the parent as well its identity 
-     key, to see if the last known parent of Y is
-     definitely X.   If a decision
-     can't be made, a StaleDataError is raised.  The
-     conditions where this error is raised are fairly
-     rare, requiring that the previous parent was
-     garbage collected, and previously
-     could very well inappropriately update/delete
-     a record that's since moved onto a new parent,
-     though there may be some cases where 
-     "silent success" occurred previously that will now 
-     raise in the face of ambiguity.
-     Expiring "Y" resets the "parent" tracker, meaning
-     X.remove(Y) could then end up deleting Y even 
-     if X is stale, but this is the same behavior
-     as before; it's advised to expire X also in that 
-     case.  [ticket:2264]
-
-   - [bug] fixed inappropriate evaluation of user-mapped
-     object in a boolean context within query.get()
-     [ticket:2310].  Also in 0.6.9.
-
-   - [bug] Added missing comma to PASSIVE_RETURN_NEVER_SET
-     symbol [ticket:2304]
-
-   - [bug] Cls.column.collate("some collation") now
-     works.  [ticket:1776]  Also in 0.6.9
-
-   - [bug] the value of a composite attribute is now
-     expired after an insert or update operation, instead
-     of regenerated in place.  This ensures that a 
-     column value which is expired within a flush
-     will be loaded first, before the composite
-     is regenerated using that value.  [ticket:2309]
-
-   - [bug] The fix in [ticket:2309] also emits the
-     "refresh" event when the composite value is
-     loaded on access, even if all column
-     values were already present, as is appropriate.
-     This fixes the "mutable" extension which relies
-     upon the "load" event to ensure the _parents 
-     dictionary is up to date, fixes [ticket:2308].
-     Thanks to Scott Torborg for the test case here.
-
-   - [bug] Fixed bug whereby a subclass of a subclass
-     using concrete inheritance in conjunction with
-     the new ConcreteBase or AbstractConcreteBase
-     would fail to apply the subclasses deeper than
-     one level to the "polymorphic loader" of each
-     base  [ticket:2312]
-
-   - [bug] Fixed bug whereby a subclass of a subclass
-     using the new AbstractConcreteBase would fail
-     to acquire the correct "base_mapper" attribute
-     when the "base" mapper was generated, thereby
-     causing failures later on.  [ticket:2312]
-
-   - [bug] Fixed bug whereby column_property() created
-     against ORM-level column could be treated as
-     a distinct entity when producing certain
-     kinds of joined-inh joins.  [ticket:2316]
-
-   - [bug] Fixed the error formatting raised when 
-     a tuple is inadvertently passed to session.query()
-     [ticket:2297].  Also in 0.6.9.
-
-   - [bug] Calls to query.join() to a single-table
-     inheritance subclass are now tracked, and
-     are used to eliminate the additional WHERE..
-     IN criterion normally tacked on with single
-     table inheritance, since the join should
-     accommodate it.  This allows OUTER JOIN
-     to a single table subclass to produce
-     the correct results, and overall will produce
-     fewer WHERE criterion when dealing with 
-     single table inheritance joins.  
-     [ticket:2328]
+  - [bug] Fixed backref behavior when "popping" the 
+    value off of a many-to-one in response to 
+    a removal from a stale one-to-many - the operation
+    is skipped, since the many-to-one has since
+    been updated.  [ticket:2315]
+
+  - [bug] After some years of not doing this, added
+    more granularity to the "is X a parent of Y" 
+    functionality, which is used when determining
+    if the FK on "Y" needs to be "nulled out" as well
+    as if "Y" should be deleted with delete-orphan
+    cascade.   The test now takes into account the
+    Python identity of the parent as well its identity 
+    key, to see if the last known parent of Y is
+    definitely X.   If a decision
+    can't be made, a StaleDataError is raised.  The
+    conditions where this error is raised are fairly
+    rare, requiring that the previous parent was
+    garbage collected, and previously
+    could very well inappropriately update/delete
+    a record that's since moved onto a new parent,
+    though there may be some cases where 
+    "silent success" occurred previously that will now 
+    raise in the face of ambiguity.
+    Expiring "Y" resets the "parent" tracker, meaning
+    X.remove(Y) could then end up deleting Y even 
+    if X is stale, but this is the same behavior
+    as before; it's advised to expire X also in that 
+    case.  [ticket:2264]
+
+  - [bug] fixed inappropriate evaluation of user-mapped
+    object in a boolean context within query.get()
+    [ticket:2310].  Also in 0.6.9.
+
+  - [bug] Added missing comma to PASSIVE_RETURN_NEVER_SET
+    symbol [ticket:2304]
+
+  - [bug] Cls.column.collate("some collation") now
+    works.  [ticket:1776]  Also in 0.6.9
+
+  - [bug] the value of a composite attribute is now
+    expired after an insert or update operation, instead
+    of regenerated in place.  This ensures that a 
+    column value which is expired within a flush
+    will be loaded first, before the composite
+    is regenerated using that value.  [ticket:2309]
+
+  - [bug] The fix in [ticket:2309] also emits the
+    "refresh" event when the composite value is
+    loaded on access, even if all column
+    values were already present, as is appropriate.
+    This fixes the "mutable" extension which relies
+    upon the "load" event to ensure the _parents 
+    dictionary is up to date, fixes [ticket:2308].
+    Thanks to Scott Torborg for the test case here.
+
+  - [bug] Fixed bug whereby a subclass of a subclass
+    using concrete inheritance in conjunction with
+    the new ConcreteBase or AbstractConcreteBase
+    would fail to apply the subclasses deeper than
+    one level to the "polymorphic loader" of each
+    base  [ticket:2312]
+
+  - [bug] Fixed bug whereby a subclass of a subclass
+    using the new AbstractConcreteBase would fail
+    to acquire the correct "base_mapper" attribute
+    when the "base" mapper was generated, thereby
+    causing failures later on.  [ticket:2312]
+
+  - [bug] Fixed bug whereby column_property() created
+    against ORM-level column could be treated as
+    a distinct entity when producing certain
+    kinds of joined-inh joins.  [ticket:2316]
+
+  - [bug] Fixed the error formatting raised when 
+    a tuple is inadvertently passed to session.query()
+    [ticket:2297].  Also in 0.6.9.
+
+  - [bug] Calls to query.join() to a single-table
+    inheritance subclass are now tracked, and
+    are used to eliminate the additional WHERE..
+    IN criterion normally tacked on with single
+    table inheritance, since the join should
+    accommodate it.  This allows OUTER JOIN
+    to a single table subclass to produce
+    the correct results, and overall will produce
+    fewer WHERE criterion when dealing with 
+    single table inheritance joins.
+    [ticket:2328]
 
   - [bug] __table_args__ can now be passed as 
     an empty tuple as well as an empty dict.
@@ -108,6 +108,20 @@ CHANGES
     this might be better as an exception but
     it's not critical either way.  [ticket:2325]
 
+  - [feature] polymorphic_on now accepts many
+    new kinds of values:
+
+      - standalone expressions that aren't
+        otherwise mapped
+      - column_property() objects
+      - string names of any column_property()
+        or attribute name of a mapped Column
+
+    The docs include an example using 
+    the case() construct, which is likely to be
+    a common constructed used here.
+    [ticket:2345] and part of [ticket:2238]
+
   - [feature] IdentitySet supports the - operator
     as the same as difference(), handy when dealing 
     with Session.dirty etc. [ticket:2301]
@@ -133,7 +147,7 @@ CHANGES
      need for a "warning" if a column is attached
      to a table after it was already used in an 
      expression - the select() construct will now
-     always produce the correct expression.  
+     always produce the correct expression.
      There's probably no real-world
      performance hit here; select() objects are 
      almost always made ad-hoc, and systems that 
@@ -386,7 +400,7 @@ CHANGES
     import [ticket:2253]
 
   - Reinstated "comparator_factory" argument to 
-    composite(), removed when 0.7 was released.  
+    composite(), removed when 0.7 was released.
     [ticket:2248]
 
   - Fixed bug in query.join() which would occur
@@ -415,7 +429,7 @@ CHANGES
     ArgumentError, rather than UnmappedClassError.
     [ticket:2196]
 
-  - New event hook, MapperEvents.after_configured().  
+  - New event hook, MapperEvents.after_configured().
     Called after a configure() step has completed and
     mappers were in fact affected.   Theoretically this
     event is called once per application, unless new mappings
@@ -507,7 +521,7 @@ CHANGES
     long lists of bound parameter sets will be 
     compressed with an informative indicator
     of the compression taking place.  Exception
-    messages use the same improved formatting.  
+    messages use the same improved formatting.
     [ticket:2243]
 
   - Added optional "sa_pool_key" argument to 
@@ -598,7 +612,7 @@ CHANGES
   - The behavior of =/!= when comparing a scalar select
     to a value will no longer produce IN/NOT IN as of 0.8;
     this behavior is a little too heavy handed (use in_() if
-    you want to emit IN) and now emits a deprecation warning.   
+    you want to emit IN) and now emits a deprecation warning.
     To get the 0.8 behavior immediately and remove the warning, 
     a compiler recipe is given at 
     http://www.sqlalchemy.org/docs/07/dialects/mssql.html#scalar-select-comparisons
@@ -759,7 +773,7 @@ CHANGES
 
   - Added the same "columns-only" check to 
     mapper.polymorphic_on as used when
-    receiving user arguments to  
+    receiving user arguments to
     relationship.order_by, foreign_keys,
     remote_side, etc.
 
@@ -832,7 +846,7 @@ CHANGES
     execution.
 
   - StatementException wrapping will display the
-    original exception class in the message.  
+    original exception class in the message.
 
   - Failures on connect which raise dbapi.Error
     will forward the error to dialect.is_disconnect()
@@ -844,7 +858,7 @@ CHANGES
 - sqlite
   - SQLite dialect no longer strips quotes
     off of reflected default value, allowing
-    a round trip CREATE TABLE to work.  
+    a round trip CREATE TABLE to work.
     This is consistent with other dialects
     that also maintain the exact form of
     the default.  [ticket:2189]
index 5f9a28671a357d47820e132788b8a0365bb29f7c..8d73ceecab91cdb8b6e98844fb48f9c039121c6a 100644 (file)
@@ -134,6 +134,8 @@ And that's it. Querying against ``Employee`` will return a combination of
 ``employees.type`` column with ``engineer``, ``manager``, or ``employee``, as
 appropriate.
 
+.. _with_polymorphic:
+
 Basic Control of Which Tables are Queried
 ++++++++++++++++++++++++++++++++++++++++++
 
index b37e813518cfd182c860612acb085608e41bc525..118b52f046acaee5b41a7f88c5526d8f2f331f61 100644 (file)
@@ -120,6 +120,8 @@ with the desired key::
        'name': user_table.c.user_name,
     })
 
+.. _column_prefix:
+
 Naming All Columns with a Prefix
 --------------------------------
 
@@ -188,6 +190,8 @@ See examples of this usage at :ref:`mapper_sql_expressions`.
 
 .. autofunction:: column_property
 
+.. _include_exclude_cols:
+
 Mapping a Subset of Table Columns
 ---------------------------------
 
index 337f9c73f1c7bc645f9669cf5048d0cad046695f..f4c43fbc5128070152e1d3330b4be77814b1eda0 100644 (file)
@@ -1171,6 +1171,7 @@ that ``Widget.widget_id`` remains an "autoincrementing" column we specify
 :func:`.relationship` we must limit those columns considered as part of
 the foreign key for the purposes of joining and cross-population.
 
+.. _passive_updates:
 
 Mutable Primary Keys / Update Cascades
 ---------------------------------------
index afb64d491880199792a9615a40831b5c8878023c..f1092d557b1e4da7408335baaac28ebbfcae8934 100644 (file)
@@ -787,12 +787,62 @@ def deferred(*columns, **kwargs):
 
 def mapper(class_, local_table=None, *args, **params):
     """Return a new :class:`~.Mapper` object.
-
-        :param class\_: The class to be mapped.
-
-        :param local_table: The table to which the class is mapped, or None if
-           this mapper inherits from another mapper using concrete table
-           inheritance.
+    
+        This function is typically used behind the scenes
+        via the Declarative extension.   When using Declarative,
+        many of the usual :func:`.mapper` arguments are handled
+        by the Declarative extension itself, including ``class_``,
+        ``local_table``, ``properties``, and  ``inherits``.
+        Other options are passed to :func:`.mapper` using 
+        the ``__mapper_args__`` class variable::
+   
+           class MyClass(Base):
+               __tablename__ = 'my_table'
+               id = Column(Integer, primary_key=True)
+               type = Column(String(50))
+               alt = Column("some_alt", Integer)
+               
+               __mapper_args__ = {
+                   'polymorphic_on' : type
+               }
+
+
+        Explicit use of :func:`.mapper`
+        is often referred to as *classical mapping*.  The above 
+        declarative example is equivalent in classical form to::
+        
+            my_table = Table("my_table", metadata,
+                Column('id', Integer, primary_key=True),
+                Column('type', String(50)),
+                Column("some_alt", Integer)
+            )
+            
+            class MyClass(object):
+                pass
+            
+            mapper(MyClass, my_table, 
+                polymorphic_on=my_table.c.type, 
+                properties={
+                    'alt':my_table.c.some_alt
+                })
+        
+        See also:
+        
+        :ref:`classical_mapping` - discussion of direct usage of
+        :func:`.mapper`
+
+        :param class\_: The class to be mapped.  When using Declarative,
+          this argument is automatically passed as the declared class
+          itself.
+
+        :param local_table: The :class:`.Table` or other selectable 
+           to which the class is mapped.  May be ``None`` if 
+           this mapper inherits from another mapper using single-table
+           inheritance.   When using Declarative, this argument is 
+           automatically passed by the extension, based on what
+           is configured via the ``__table__`` argument or via the :class:`.Table`
+           produced as a result of the ``__tablename__`` and :class:`.Column`
+           arguments present.
 
         :param always_refresh: If True, all query operations for this mapped
            class will overwrite all data within object instances that already
@@ -812,31 +862,31 @@ def mapper(class_, local_table=None, *args, **params):
            particular primary key value. A "partial primary key" can occur if
            one has mapped to an OUTER JOIN, for example.
 
-        :param batch: Indicates that save operations of multiple entities 
-           can be batched together for efficiency. setting to False indicates
+        :param batch: Defaults to ``True``, indicating 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
-           :class:`.MapperExtension` methods corresponding to the save
-           operation.
-
-        :param column_prefix: A string which will be prepended to the `key`
-           name of all :class:`.Column` objects when creating 
-           column-based properties from the
-           given :class:`.Table`. Does not affect explicitly specified 
-           column-based properties
+           instance.  This is used in the extremely rare case that a 
+           :class:`.MapperEvents` listener requires being called 
+           in between individual row persistence operations.
+
+        :param column_prefix: A string which will be prepended 
+           to the mapped attribute name when :class:`.Column`
+           objects are automatically assigned as attributes to the
+           mapped class.  Does not affect explicitly specified 
+           column-based properties.   
+           
+           See the section :ref:`column_prefix` for an example.
 
         :param concrete: If True, indicates this mapper should use concrete
            table inheritance with its parent mapper.
+           
+           See the section :ref:`concrete_inheritance` for an example.
 
         :param exclude_properties: A list or set of string column names to 
-          be excluded from mapping. As of SQLAlchemy 0.6.4, this collection
-          may also include :class:`.Column` objects. Columns named or present
-          in this list will not be automatically mapped. Note that neither
-          this option nor include_properties will allow one to circumvent plan
-          Python inheritance - if mapped class ``B`` inherits from mapped
-          class ``A``, no combination of includes or excludes will allow ``B``
-          to have fewer properties than its superclass, ``A``.
+          be excluded from mapping.  
+          
+          See :ref:`include_exclude_cols` for an example.
 
         :param extension: A :class:`.MapperExtension` instance or
            list of :class:`.MapperExtension`
@@ -844,97 +894,183 @@ def mapper(class_, local_table=None, *args, **params):
            :class:`.Mapper`.  **Deprecated.**  Please see :class:`.MapperEvents`.
 
         :param include_properties: An inclusive list or set of string column
-          names to map. As of SQLAlchemy 0.6.4, this collection may also
-          include :class:`.Column` objects in order to disambiguate between
-          same-named columns in a selectable (such as a
-          :func:`~.expression.join()`). If this list is not ``None``, columns
-          present in the mapped table but not named or present in this list
-          will not be automatically mapped. See also "exclude_properties".
-
-        :param inherits: Another :class:`.Mapper` for which 
-            this :class:`.Mapper` will have an inheritance
-            relationship with.
-
+          names to map.   
+          
+          See :ref:`include_exclude_cols` for an example.
+
+        :param inherits: A mapped class or the corresponding :class:`.Mapper` 
+          of one indicating a superclass to which this :class:`.Mapper`
+          should *inherit* from.   The mapped class here must be a subclass of the
+          other mapper's class.   When using Declarative, this argument
+          is passed automatically as a result of the natural class
+          hierarchy of the declared classes.   
+          
+          See also:
+          
+          :ref:`inheritance_toplevel`
+          
         :param inherit_condition: For joined table inheritance, a SQL
-           expression (constructed
-           :class:`.ClauseElement`) which will
+           expression which will
            define how the two tables are joined; defaults to a natural join
            between the two tables.
 
-        :param inherit_foreign_keys: When inherit_condition is used and the
-           condition contains no ForeignKey columns, specify the "foreign"
-           columns of the join condition in this list. else leave as None.
+        :param inherit_foreign_keys: When ``inherit_condition`` is used and the
+           columns present are missing a :class:`.ForeignKey` configuration, 
+           this parameter can be used to specify which columns are "foreign".  
+           In most cases can be left as ``None``.
 
-        :param non_primary: Construct a :class:`.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.
+        :param non_primary: Specify that this :class:`.Mapper` is in addition
+          to the "primary" mapper, that is, the one used for persistence.
+          The :class:`.Mapper` created here may be used for ad-hoc
+          mapping of the class to an alternate selectable, for loading
+          only.   
+          
+          The ``non_primary`` feature is rarely needed with modern
+          usage.
 
         :param order_by: A single :class:`.Column` or list of :class:`.Column`
            objects 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.
+           ordering for entities.  By default mappers have no pre-defined 
+           ordering.
 
-        :param passive_updates: Indicates UPDATE behavior of foreign key
-           when a primary key changes on a joined-table inheritance or other
-           joined table mapping.
+        :param passive_updates: Indicates UPDATE behavior of foreign key
+           columns when a primary key column changes on a joined-table inheritance 
+           mapping.   Defaults to ``True``.
 
            When True, it is assumed that ON UPDATE CASCADE is configured on
            the foreign key in the database, and that the database will handle
-           propagation of an UPDATE from a source column to dependent rows.
-           Note that with databases which enforce referential integrity (i.e.
-           PostgreSQL, MySQL with InnoDB tables), ON UPDATE CASCADE is
-           required for this operation. The relationship() will update the
-           value of the attribute on related items which are locally present
-           in the session during a flush.
+           propagation of an UPDATE from a source column to dependent columns
+           on joined-table rows.
 
            When False, it is assumed that the database does not enforce
            referential integrity and will not be issuing its own CASCADE
-           operation for an update. The relationship() will issue the
-           appropriate UPDATE statements to the database in response to the
-           change of a referenced key, and items locally present in the
-           session during a flush will also be refreshed.
-
-           This flag should probably be set to False if primary key changes
-           are expected and the database in use doesn't support CASCADE (i.e.
-           SQLite, MySQL MyISAM tables).
-
-            Also see the passive_updates flag on :func:`relationship()`.
-
-           A future SQLAlchemy release will provide a "detect" feature for
-           this flag.
-
-        :param polymorphic_on: Used with mappers in an inheritance
-           relationship, a :class:`.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. The column specified by ``polymorphic_on``
-           is usually a column that resides directly within the base mapper's
-           mapped table; alternatively, it may be a column that is only
-           present within the <selectable> portion of the ``with_polymorphic``
-           argument.
-
-        :param polymorphic_identity: A value which will be stored in the
-           Column denoted by polymorphic_on, corresponding to the class
-           identity of this mapper.
+           operation for an update.  The :class:`.Mapper` here will
+           emit an UPDATE statement for the dependent columns during a
+           primary key change.
+           
+           See also:
+           
+           :ref:`passive_updates` - description of a similar feature as 
+           used with :func:`.relationship`
+
+        :param polymorphic_on: Specifies the column, attribute, or 
+          SQL expression used to determine the target class for an 
+          incoming row, when inheriting classes are present.
+          
+          This value is commonly a :class:`.Column` object that's
+          present in the mapped :class:`.Table`::
+          
+            class Employee(Base):
+                __tablename__ = 'employee'
+                
+                id = Column(Integer, primary_key=True)
+                discriminator = Column(String(50))
+                
+                __mapper_args__ = {
+                    "polymorphic_on":discriminator,
+                    "polymorphic_identity":"employee"
+                }
+        
+          As of SQLAlchemy 0.7.4, it may also be specified
+          as a SQL expression, as in this example where we 
+          use the :func:`.case` construct to provide a conditional
+          approach::
+
+            class Employee(Base):
+                __tablename__ = 'employee'
+                
+                id = Column(Integer, primary_key=True)
+                discriminator = Column(String(50))
+                
+                __mapper_args__ = {
+                    "polymorphic_on":case([
+                        (discriminator == "EN", "engineer"),
+                        (discriminator == "MA", "manager"),
+                    ], else_="employee"),
+                    "polymorphic_identity":"employee"
+                }
+        
+          Also as of 0.7.4, it may also refer to any attribute 
+          configured with :func:`.column_property`, or to the
+          string name of one::
+            
+                class Employee(Base):
+                    __tablename__ = 'employee'
+                
+                    id = Column(Integer, primary_key=True)
+                    discriminator = Column(String(50))
+                    employee_type = column_property(
+                        case([
+                            (discriminator == "EN", "engineer"),
+                            (discriminator == "MA", "manager"),
+                        ], else_="employee")
+                    )
+                
+                    __mapper_args__ = {
+                        "polymorphic_on":employee_type,
+                        "polymorphic_identity":"employee"
+                    }
+            
+          When setting ``polymorphic_on`` to reference an
+          attribute or expression that's not present in the
+          locally mapped :class:`.Table`, yet the value 
+          of the discriminator should be persisted to the database, 
+          the value of the
+          discriminator is not automatically set on new
+          instances; this must be handled by the user,
+          either through manual means or via event listeners.
+          A typical approach to establishing such a listener
+          looks like::
+
+                from sqlalchemy import event
+                from sqlalchemy.orm import object_mapper
+            
+                @event.listens_for(Employee, "init", propagate=True)
+                def set_identity(instance, *arg, **kw):
+                    mapper = object_mapper(instance)
+                    instance.discriminator = mapper.polymorphic_identity
+        
+          Where above, we assign the value of ``polymorphic_identity``
+          for the mapped class to the ``discriminator`` attribute,
+          thus persisting the value to the ``discriminator`` column
+          in the database.
+      
+          See also:
+      
+          :ref:`inheritance_toplevel`
+        
+        :param polymorphic_identity: Specifies the value which 
+          identifies this particular class as returned by the
+          column expression referred to by the ``polymorphic_on``
+          setting.  As rows are received, the value corresponding
+          to the ``polymorphic_on`` column expression is compared
+          to this value, indicating which subclass should 
+          be used for the newly reconstructed object.
 
         :param 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
-           :class:`.Column` (although they can be overridden using this dictionary).
+           attributes to :class:`.MapperProperty` instances, which define the
+           persistence behavior of that attribute.  Note that :class:`.Column`
+           objects present in
+           the mapped :class:`.Table` are automatically placed into
+           ``ColumnProperty`` instances upon mapping, unless overridden.
+           When using Declarative, this argument is passed automatically,
+           based on all those :class:`.MapperProperty` instances declared
+           in the declared class body.
 
         :param primary_key: A list of :class:`.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.
 
-        :param version_id_col: A :class:`.Column` which must have an integer type
+        :param version_id_col: A :class:`.Column` 
            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
+           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 :class:`.StaleDataError` exception is
-           thrown.
+           lifetime of the entity, else a :class:`~sqlalchemy.orm.exc.StaleDataError` 
+           exception is
+           thrown.  By default the column must be of :class:`.Integer` type,
+           unless ``version_id_generator`` specifies a new generation
+           algorithm.
 
         :param version_id_generator: A callable which defines the algorithm
             used to generate new version ids. Defaults to an integer
@@ -943,10 +1079,15 @@ def mapper(class_, local_table=None, *args, **params):
 
                 import uuid
 
-                mapper(Cls, table, 
-                version_id_col=table.c.version_uuid,
-                version_id_generator=lambda version:uuid.uuid4().hex
-                )
+                class MyClass(Base):
+                    __tablename__ = 'mytable'
+                    id = Column(Integer, primary_key=True)
+                    version_uuid = Column(String(32))
+                    
+                    __mapper_args__ = {
+                        'version_id_col':version_uuid,
+                        'version_id_generator':lambda version:uuid.uuid4().hex
+                    }
 
             The callable receives the current version identifier as its 
             single argument.
@@ -959,14 +1100,16 @@ def mapper(class_, local_table=None, *args, **params):
             ``'*'`` may be used to indicate all descending classes should be
             loaded immediately. The second tuple argument <selectable>
             indicates a selectable that will be used to query for multiple
-            classes. Normally, it is left as None, in which case this mapper
-            will form an outer join from the base mapper's table to that of
-            all desired sub-mappers. When specified, it provides the
-            selectable to be used for polymorphic loading. When
-            with_polymorphic includes mappers which load from a "concrete"
-            inheriting table, the <selectable> argument is required, since it
-            usually requires more complex UNION queries.
-
+            classes. 
+            
+            See also:
+            
+            :ref:`concrete_inheritance` - typically uses ``with_polymorphic``
+            to specify a UNION statement to select from.
+            
+            :ref:`with_polymorphic` - usage example of the related 
+            :meth:`.Query.with_polymorphic` method
+            
     """
     return Mapper(class_, local_table, *args, **params)
 
index 3a363e73133ce441f3aa734376a7479d345ab9ec..f8e20c969e7f39bb1468c93d42f7199114e43ce2 100644 (file)
@@ -128,9 +128,7 @@ class Mapper(object):
         self.batch = batch
         self.eager_defaults = eager_defaults
         self.column_prefix = column_prefix
-        self.polymorphic_on = expression._only_column_elements_or_none(
-                                        polymorphic_on, 
-                                        "polymorphic_on")
+        self.polymorphic_on = polymorphic_on
         self._dependency_processors = []
         self.validators = util.immutabledict()
         self.passive_updates = passive_updates
@@ -882,34 +880,67 @@ class Mapper(object):
         if self.polymorphic_on is not None:
             setter = True
 
-            if self.polymorphic_on not in self._columntoproperty:
+            if isinstance(self.polymorphic_on, basestring):
+                try:
+                    self.polymorphic_on = self._props[self.polymorphic_on]
+                except KeyError:
+                    raise sa_exc.ArgumentError(
+                                "Can't determine polymorphic_on "
+                                "value '%s' - no attribute is "
+                                "mapped to this name." % self.polymorphic_on)
+
+            if self.polymorphic_on in self._columntoproperty:
+                prop = self._columntoproperty[self.polymorphic_on]
+                polymorphic_key = prop.key
+                self.polymorphic_on = prop.columns[0]
+            elif isinstance(self.polymorphic_on, MapperProperty):
+                if not isinstance(self.polymorphic_on, properties.ColumnProperty):
+                    raise sa_exc.ArgumentError(
+                            "Only direct column-mapped "
+                            "property or SQL expression "
+                            "can be passed for polymorphic_on")
+                prop = self.polymorphic_on
+                self.polymorphic_on = prop.columns[0]
+                polymorphic_key = prop.key
+            elif not expression.is_column(self.polymorphic_on):
+                raise sa_exc.ArgumentError(
+                    "Only direct column-mapped "
+                    "property or SQL expression "
+                    "can be passed for polymorphic_on"
+                )
+            else:
                 col = self.mapped_table.corresponding_column(self.polymorphic_on)
                 if col is None:
                     setter = False
                     instrument = False
                     col = self.polymorphic_on
-                    if self.with_polymorphic is None \
-                        or self.with_polymorphic[1].corresponding_column(col) \
-                        is None:
-                        raise sa_exc.InvalidRequestError("Could not map polymorphic_on column "
-                                  "'%s' to the mapped table - polymorphic "
-                                  "loads will not function properly"
-                                  % col.description)
+                    if isinstance(col, schema.Column) and (
+                        self.with_polymorphic is  None or \
+                       self.with_polymorphic[1].corresponding_column(col) is None
+                        ):
+                        raise sa_exc.InvalidRequestError(
+                            "Could not map polymorphic_on column "
+                            "'%s' to the mapped table - polymorphic "
+                            "loads will not function properly"
+                                 % col.description)
                 else:
                     instrument = True
 
-                if self._should_exclude(col.key, col.key, False, col):
-                    raise sa_exc.InvalidRequestError(
+                key = getattr(col, 'key', None)
+                if key:
+                    if self._should_exclude(col.key, col.key, False, col):
+                        raise sa_exc.InvalidRequestError(
                         "Cannot exclude or override the discriminator column %r" %
                         col.key)
+                else:
+                    col = col.label(None)
+                    key = col.key
 
                 self._configure_property(
-                                col.key, 
+                                key, 
                                 properties.ColumnProperty(col, _instrument=instrument),
                                 init=init, setparent=True)
-                polymorphic_key = col.key
-            else:
-                polymorphic_key = self._columntoproperty[self.polymorphic_on].key
+                polymorphic_key = key
 
         if setter:
             def _set_polymorphic_identity(state):
@@ -1045,7 +1076,7 @@ class Mapper(object):
                                     prop.columns[0] is self.polymorphic_on)
 
             self.columns[key] = col
-            for col in prop.columns:
+            for col in prop.columns + prop._orig_columns:
                 for col in col.proxy_set:
                     self._columntoproperty[col] = prop
 
index 330a851083f37b413d26b3ffac8deb60094867c8..8794cd3dadd637e43d6b9d38c8d8a299e2130743 100644 (file)
@@ -61,6 +61,7 @@ class ColumnProperty(StrategizedProperty):
         :param extension:
 
         """
+        self._orig_columns = [expression._labeled(c) for c in columns]
         self.columns = [expression._labeled(_orm_deannotate(c)) 
                             for c in columns]
         self.group = kwargs.pop('group', None)
index 7b991a618ed1cff1e4d7b23a87fb51ca5f844ca3..c9aa5fc9be62a17e14472bd085f4fb4801c31eb4 100644 (file)
@@ -1,7 +1,7 @@
 import warnings
 from test.lib.testing import eq_, assert_raises, assert_raises_message
 from sqlalchemy import *
-from sqlalchemy import exc as sa_exc, util
+from sqlalchemy import exc as sa_exc, util, event
 from sqlalchemy.orm import *
 from sqlalchemy.orm import exc as orm_exc, attributes
 from test.lib.assertsql import AllOf, CompiledSQL
@@ -86,62 +86,189 @@ class PolymorphicOnNotLocalTest(fixtures.MappedTest):
                 Column('y', String(10)), 
                 Column('xid', ForeignKey('t1.id')))
 
-    def test_non_col_polymorphic_on(self):
-        class InterfaceBase(object):
+    @classmethod
+    def setup_classes(cls):
+        class Parent(cls.Comparable):
+            pass
+        class Child(Parent):
             pass
 
+    def test_non_col_polymorphic_on(self):
+        Parent = self.classes.Parent
+        t2 = self.tables.t2
         assert_raises_message(
             sa_exc.ArgumentError,
-            "Column-based expression object expected "
-            "for argument 'polymorphic_on'; got: "
-            "'im not a column', type",
+            "Can't determine polymorphic_on "
+            "value 'im not a column' - no "
+            "attribute is mapped to this name.",
             mapper,
-            InterfaceBase, t2, polymorphic_on="im not a column"
+            Parent, t2, polymorphic_on="im not a column"
         )
 
-    def test_bad_polymorphic_on(self):
+    def test_polymorphic_on_non_expr_prop(self):
         t2, t1 = self.tables.t2, self.tables.t1
+        Parent = self.classes.Parent
 
-        class InterfaceBase(object):
-            pass
+        t1t2_join = select([t1.c.x], from_obj=[t1.join(t2)]).alias()
+        def go():
+            interface_m = mapper(Parent, t2,
+                                polymorphic_on=lambda:"hi",
+                                polymorphic_identity=0)
 
+        assert_raises_message(
+            sa_exc.ArgumentError,
+            "Only direct column-mapped property or "
+            "SQL expression can be passed for polymorphic_on",
+            go
+        )
+
+    def test_polymorphic_on_not_present_col(self):
+        t2, t1 = self.tables.t2, self.tables.t1
+        Parent = self.classes.Parent
         t1t2_join = select([t1.c.x], from_obj=[t1.join(t2)]).alias()
         def go():
-            interface_m = mapper(InterfaceBase, t2,
+            t1t2_join_2 = select([t1.c.q], from_obj=[t1.join(t2)]).alias()
+            interface_m = mapper(Parent, t2,
                                 polymorphic_on=t1t2_join.c.x,
+                                with_polymorphic=('*', t1t2_join_2),
                                 polymorphic_identity=0)
-
         assert_raises_message(
             sa_exc.InvalidRequestError,
             "Could not map polymorphic_on column 'x' to the mapped table - "
             "polymorphic loads will not function properly",
             go
         )
-        clear_mappers()
 
+    def test_polymorphic_on_only_in_with_poly(self):
+        t2, t1 = self.tables.t2, self.tables.t1
+        Parent = self.classes.Parent
+        t1t2_join = select([t1.c.x], from_obj=[t1.join(t2)]).alias()
         # if its in the with_polymorphic, then its OK
-        interface_m = mapper(InterfaceBase, t2,
+        mapper(Parent, t2,
                                 polymorphic_on=t1t2_join.c.x,
                                 with_polymorphic=('*', t1t2_join),
                                 polymorphic_identity=0)
-        configure_mappers()
 
-        clear_mappers()
+    def test_polymorpic_on_not_in_with_poly(self):
+        t2, t1 = self.tables.t2, self.tables.t1
+        Parent = self.classes.Parent
+
+        t1t2_join = select([t1.c.x], from_obj=[t1.join(t2)]).alias()
 
         # if with_polymorphic, but its not present, not OK
         def go():
             t1t2_join_2 = select([t1.c.q], from_obj=[t1.join(t2)]).alias()
-            interface_m = mapper(InterfaceBase, t2,
+            interface_m = mapper(Parent, t2,
                                 polymorphic_on=t1t2_join.c.x,
                                 with_polymorphic=('*', t1t2_join_2),
                                 polymorphic_identity=0)
         assert_raises_message(
             sa_exc.InvalidRequestError,
-            "Could not map polymorphic_on column 'x' to the mapped table - "
+            "Could not map polymorphic_on column 'x' "
+            "to the mapped table - "
             "polymorphic loads will not function properly",
             go
         )
 
+    def test_polymorphic_on_expr_explicit_map(self):
+        t2, t1 = self.tables.t2, self.tables.t1
+        Parent, Child = self.classes.Parent, self.classes.Child
+        expr = case([
+            (t1.c.x=="p", "parent"),
+            (t1.c.x=="c", "child"),
+        ],else_ = t1.c.x)
+        mapper(Parent, t1, properties={
+            "discriminator":column_property(expr)
+        }, polymorphic_identity="parent",
+            polymorphic_on=expr)
+        mapper(Child, t2, inherits=Parent, 
+                polymorphic_identity="child")
+
+        self._roundtrip()
+
+    def test_polymorphic_on_expr_implicit_map(self):
+        t2, t1 = self.tables.t2, self.tables.t1
+        Parent, Child = self.classes.Parent, self.classes.Child
+        expr = case([
+            (t1.c.x=="p", "parent"),
+            (t1.c.x=="c", "child"),
+        ],else_ = t1.c.x).label("foo")
+        mapper(Parent, t1, polymorphic_identity="parent",
+            polymorphic_on=expr)
+        mapper(Child, t2, inherits=Parent, polymorphic_identity="child")
+
+        self._roundtrip()
+
+    def test_polymorphic_on_column_prop(self):
+        t2, t1 = self.tables.t2, self.tables.t1
+        Parent, Child = self.classes.Parent, self.classes.Child
+        expr = case([
+            (t1.c.x=="p", "parent"),
+            (t1.c.x=="c", "child"),
+        ],else_ = t1.c.x)
+        cprop = column_property(expr)
+        mapper(Parent, t1, properties={
+            "discriminator":cprop
+        }, polymorphic_identity="parent",
+            polymorphic_on=cprop)
+        mapper(Child, t2, inherits=Parent, 
+                polymorphic_identity="child")
+
+        self._roundtrip()
+
+    def test_polymorphic_on_column_str_prop(self):
+        t2, t1 = self.tables.t2, self.tables.t1
+        Parent, Child = self.classes.Parent, self.classes.Child
+        expr = case([
+            (t1.c.x=="p", "parent"),
+            (t1.c.x=="c", "child"),
+        ],else_ = t1.c.x)
+        cprop = column_property(expr)
+        mapper(Parent, t1, properties={
+            "discriminator":cprop
+        }, polymorphic_identity="parent",
+            polymorphic_on="discriminator")
+        mapper(Child, t2, inherits=Parent, 
+                polymorphic_identity="child")
+
+        self._roundtrip()
+
+    def test_polymorphic_on_synonym(self):
+        t2, t1 = self.tables.t2, self.tables.t1
+        Parent, Child = self.classes.Parent, self.classes.Child
+        cprop = column_property(t1.c.x)
+        assert_raises_message(
+            sa_exc.ArgumentError,
+            "Only direct column-mapped property or "
+            "SQL expression can be passed for polymorphic_on",
+            mapper, Parent, t1, properties={
+            "discriminator":cprop,
+            "discrim_syn":synonym(cprop)
+        }, polymorphic_identity="parent",
+            polymorphic_on="discrim_syn")
+
+    def _roundtrip(self, set_event=True):
+        Parent, Child = self.classes.Parent, self.classes.Child
+
+        if set_event:
+            @event.listens_for(Parent, "init", propagate=True)
+            def set_identity(instance, *arg, **kw):
+                instance.x = object_mapper(instance).polymorphic_identity
+
+        s = Session(testing.db)
+        s.add_all([
+            Parent(q="p1"),
+            Child(q="c1", y="c1"),
+            Parent(q="p2"),
+        ])
+        s.commit()
+        s.close()
+
+        eq_(
+            [type(t) for t in s.query(Parent).order_by(Parent.id)],
+            [Parent, Child, Parent]
+        )
+
 
 class FalseDiscriminatorTest(fixtures.MappedTest):
     @classmethod