]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- fix the labeled column with column_expression() issue, finishes [ticket:1534]
authorMike Bayer <mike_mp@zzzcomputing.com>
Sat, 18 Aug 2012 04:54:00 +0000 (00:54 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sat, 18 Aug 2012 04:54:00 +0000 (00:54 -0400)
- epic documentation sweep for new operator system, making ORM links consistent
and complete, full documentation and examples for type/SQL expression feature
- type_coerce() explicitly accepts BindParamClause objects
- change UserDefinedType to coerce the other side to itself by default as this
is much more likely what's desired
- make coerce_compared_type() fully public on all types
- have profiling run the test no matter what so that the test_zoomarks don't fail
when callcounts are missing

16 files changed:
doc/build/core/tutorial.rst
doc/build/core/types.rst
doc/build/orm/examples.rst
doc/build/orm/mapper_config.rst
lib/sqlalchemy/orm/__init__.py
lib/sqlalchemy/orm/descriptor_props.py
lib/sqlalchemy/orm/interfaces.py
lib/sqlalchemy/orm/properties.py
lib/sqlalchemy/sql/compiler.py
lib/sqlalchemy/sql/expression.py
lib/sqlalchemy/sql/operators.py
lib/sqlalchemy/types.py
test/lib/profiles.txt
test/lib/profiling.py
test/sql/test_type_expressions.py
test/sql/test_types.py

index dad1aa68d66299d848db750a8fd37dd1bcd6eabe..6d55a1ecc3e8cbd2ac7b760958bbc52058051b17 100644 (file)
@@ -1144,7 +1144,7 @@ single named value is needed in the execute parameters:
 Functions
 ---------
 
-SQL functions are created using the :attr:`~.expression.func` keyword, which
+SQL functions are created using the :data:`~.expression.func` keyword, which
 generates functions using attribute access:
 
 .. sourcecode:: pycon+sql
@@ -1231,13 +1231,13 @@ of our selectable:
     >>> s.compile().params
     {u'x_2': 5, u'y_2': 12, u'y_1': 45, u'x_1': 17}
 
-See also :attr:`sqlalchemy.sql.expression.func`.
+See also :data:`~.expression.func`.
 
 Window Functions
 -----------------
 
 Any :class:`.FunctionElement`, including functions generated by
-:attr:`~.expression.func`, can be turned into a "window function", that is an
+:data:`~.expression.func`, can be turned into a "window function", that is an
 OVER clause, using the :meth:`~.FunctionElement.over` method:
 
 .. sourcecode:: pycon+sql
index 6518a5e9c74f8365f8162049526b74cca6772b42..d3278e06750d92a5c58240b517b33bc5b5eb74d1 100644 (file)
@@ -279,6 +279,8 @@ in which case it will produce ``BLOB``.
 See the section :ref:`type_compilation_extension`, a subsection of
 :ref:`sqlalchemy.ext.compiler_toplevel`, for additional examples.
 
+.. _types_typedecorator:
+
 Augmenting Existing Types
 ~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -488,6 +490,145 @@ cursor directly::
       def adapt(self, impltype):
           return MySpecialTime(self.special_argument)
 
+.. _types_sql_value_processing:
+
+Applying SQL-level Bind/Result Processing
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+As seen in the sections :ref:`types_typedecorator` and :ref:`replacing_processors`,
+SQLAlchemy allows Python functions to be invoked both when parameters are sent
+to a statement, as well as when result rows are loaded from the database, to apply
+transformations to the values as they are sent to or from the database.   It is also
+possible to define SQL-level transformations as well.  The rationale here is when
+only the relational database contains a particular series of functions that are necessary
+to coerce incoming and outgoing data between an application and persistence format.
+Examples include using database-defined encryption/decryption functions, as well
+as stored procedures that handle geographic data.  The Postgis extension to Postgresql
+includes an extensive array of SQL functions that are necessary for coercing
+data into particular formats.
+
+Any :class:`.TypeEngine`, :class:`.UserDefinedType` or :class:`.TypeDecorator` subclass
+can include implementations of
+:meth:`.TypeEngine.bind_expression` and/or :meth:`.TypeEngine.column_expression`, which
+when defined to return a non-``None`` value should return a :class:`.ColumnElement`
+expression to be injected into the SQL statement, either surrounding
+bound parameters or a column expression.  For example, to build a ``Geometry``
+type which will apply the Postgis function ``ST_GeomFromText`` to all outgoing
+values and the function ``ST_AsText`` to all incoming data, we can create
+our own subclass of :class:`.UserDefinedType` which provides these methods
+in conjunction with :data:`~.sqlalchemy.sql.expression.func`::
+
+    from sqlalchemy import func
+    from sqlalchemy.types import UserDefinedType
+
+    class Geometry(UserDefinedType):
+        def get_col_spec(self):
+            return "GEOMETRY"
+
+        def bind_expression(self, bindvalue):
+            return func.ST_GeomFromText(bindvalue, type_=self)
+
+        def column_expression(self, col):
+            return func.ST_AsText(col, type_=self)
+
+We can apply the ``Geometry`` type into :class:`.Table` metadata
+and use it in a :func:`.select` construct::
+
+    geometry = Table('geometry', metadata,
+                  Column('geom_id', Integer, primary_key=True),
+                  Column('geom_data', Geometry)
+                )
+
+    print select([geometry]).where(
+      geometry.c.geom_data == 'LINESTRING(189412 252431,189631 259122)')
+
+The resulting SQL embeds both functions as appropriate.   ``ST_AsText``
+is applied to the columns clause so that the return value is run through
+the function before passing into a result set, and ``ST_GeomFromText``
+is run on the bound parameter so that the passed-in value is converted::
+
+    SELECT geometry.geom_id, ST_AsText(geometry.geom_data) AS geom_data_1
+    FROM geometry
+    WHERE geometry.geom_data = ST_GeomFromText(:geom_data_2)
+
+The :meth:`.TypeEngine.column_expression` method interacts with the
+mechanics of the compiler such that the SQL expression does not interfere
+with the labeling of the wrapped expression.   Such as, if we rendered
+a :func:`.select` against a :func:`.label` of our expression, the string
+label is moved to the outside of the wrapped expression::
+
+    print select([geometry.c.geom_data.label('my_data')])
+
+Output::
+
+    SELECT ST_AsText(geometry.geom_data) AS my_data
+    FROM geometry
+
+For an example of subclassing a built in type directly, we subclass
+:class:`.postgresql.BYTEA` to provide a ``PGPString``, which will make use of the
+Postgresql ``pgcrypto`` extension to encrpyt/decrypt values
+transparently::
+
+    from sqlalchemy import create_engine, String, select, func, \
+            MetaData, Table, Column, type_coerce
+
+    from sqlalchemy.dialects.postgresql import BYTEA
+
+    class PGPString(BYTEA):
+        def __init__(self, passphrase, length=None):
+            super(PGPString, self).__init__(length)
+            self.passphrase = passphrase
+
+        def bind_expression(self, bindvalue):
+            # convert the bind's type from PGPString to
+            # String, so that it's passed to psycopg2 as is without
+            # a dbapi.Binary wrapper
+            bindvalue = type_coerce(bindvalue, String)
+            return func.pgp_sym_encrypt(bindvalue, self.passphrase)
+
+        def column_expression(self, col):
+            return func.pgp_sym_decrypt(col, self.passphrase)
+
+    metadata = MetaData()
+    message = Table('message', metadata,
+                    Column('username', String(50)),
+                    Column('message',
+                        PGPString("this is my passphrase", length=1000)),
+                )
+
+    engine = create_engine("postgresql://scott:tiger@localhost/test", echo=True)
+    with engine.begin() as conn:
+        metadata.create_all(conn)
+
+        conn.execute(message.insert(), username="some user",
+                                    message="this is my message")
+
+        print conn.scalar(
+                select([message.c.message]).\
+                    where(message.c.username == "some user")
+            )
+
+The ``pgp_sym_encrypt`` and ``pgp_sym_decrypt`` functions are applied
+to the INSERT and SELECT statements::
+
+  INSERT INTO message (username, message)
+    VALUES (%(username)s, pgp_sym_encrypt(%(message)s, %(pgp_sym_encrypt_1)s))
+    {'username': 'some user', 'message': 'this is my message',
+      'pgp_sym_encrypt_1': 'this is my passphrase'}
+
+  SELECT pgp_sym_decrypt(message.message, %(pgp_sym_decrypt_1)s) AS message_1
+    FROM message
+    WHERE message.username = %(username_1)s
+    {'pgp_sym_decrypt_1': 'this is my passphrase', 'username_1': 'some user'}
+
+
+.. versionadded:: 0.8  Added the :meth:`.TypeEngine.bind_expression` and
+   :meth:`.TypeEngine.column_expression` methods.
+
+See also:
+
+:ref:`examples_postgis`
+
 .. _types_operators:
 
 Redefining and Creating New Operators
@@ -497,7 +638,7 @@ SQLAlchemy Core defines a fixed set of expression operators available to all col
 Some of these operations have the effect of overloading Python's built in operators;
 examples of such operators include
 :meth:`.ColumnOperators.__eq__` (``table.c.somecolumn == 'foo'``),
-:meth:`.ColumnOperators.neg` (``~table.c.flag``),
+:meth:`.ColumnOperators.__invert__` (``~table.c.flag``),
 and :meth:`.ColumnOperators.__add__` (``table.c.x + table.c.y``).  Other operators are exposed as
 explicit methods on column expressions, such as
 :meth:`.ColumnOperators.in_` (``table.c.value.in_(['x', 'y'])``) and :meth:`.ColumnOperators.like`
index fcee004347007fbb1d43ec90a4c6be2276d632ed..6560547cdc188ee2dd2206ad18fff85377a1c331 100644 (file)
@@ -102,6 +102,8 @@ Polymorphic Associations
 
 See :ref:`examples_generic_associations` for a modern version of polymorphic associations.
 
+.. _examples_postgis:
+
 PostGIS Integration
 -------------------
 
index 62515d214199ddeeb63f430367b18045fa796c15..be0274abd67391a1ca9dd2f4a5348c6c079a30f4 100644 (file)
@@ -746,6 +746,13 @@ which take place for column expressions are most directly redefined at the
 type level -  see the
 section :ref:`types_operators` for a description.
 
+ORM level functions like :func:`.column_property`, :func:`.relationship`,
+and :func:`.composite` also provide for operator redefinition at the ORM
+level, by passing a :class:`.PropComparator` subclass to the ``comparator_factory``
+argument of each function.  Customization of operators at this level is a
+rare use case.  See the documentation at :class:`.PropComparator`
+for an overview.
+
 .. _mapper_composite:
 
 Composite Column Types
@@ -846,6 +853,7 @@ using the ``.start`` and ``.end`` attributes against ad-hoc ``Point`` instances:
 
 .. autofunction:: composite
 
+
 Tracking In-Place Mutations on Composites
 -----------------------------------------
 
@@ -857,14 +865,20 @@ to associate each user-defined composite object with all parent associations.
 Please see the example in :ref:`mutable_composites`.
 
 .. versionchanged:: 0.7
-    No automatic tracking of in-place changes to an existing composite value.
+    In-place changes to an existing composite value are no longer
+    tracked automatically; the functionality is superseded by the
+    :class:`.MutableComposite` class.
+
+.. _composite_operations:
 
 Redefining Comparison Operations for Composites
 -----------------------------------------------
 
 The "equals" comparison operation by default produces an AND of all
 corresponding columns equated to one another. This can be changed using
-the ``comparator_factory``, described in :ref:`custom_comparators`.
+the ``comparator_factory`` argument to :func:`.composite`, where we
+specify a custom :class:`.CompositeProperty.Comparator` class
+to define existing or new operations.
 Below we illustrate the "greater than" operator, implementing
 the same expression that the base "greater than" does::
 
index 1a22fe3d13720d8995664c83a7824598ec9e8e41..2078b2396125ca9ee2d7426258473465d743f658 100644 (file)
@@ -760,6 +760,9 @@ def composite(class_, *cols, **kwargs):
     See the mapping documentation section :ref:`mapper_composite` for a full
     usage example.
 
+    The :class:`.MapperProperty` returned by :func:`.composite`
+    is the :class:`.CompositeProperty`.
+
     :param class\_:
       The "composite type" class.
 
index 91717974dad6132c26ca3b202b125901bcb727f4..f4c2e1a9063ac1ac9fb4a3d0e12379ed675ca07e 100644 (file)
@@ -73,7 +73,17 @@ class DescriptorProperty(MapperProperty):
 
 
 class CompositeProperty(DescriptorProperty):
+    """Defines a "composite" mapped attribute, representing a collection
+    of columns as one attribute.
 
+    :class:`.CompositeProperty` is constructed using the :func:`.composite`
+    function.
+
+    See also:
+
+    :ref:`mapper_composite`
+
+    """
     def __init__(self, class_, *attrs, **kwargs):
         self.attrs = attrs
         self.composite_class = class_
@@ -279,6 +289,23 @@ class CompositeProperty(DescriptorProperty):
         return self.comparator_factory(self)
 
     class Comparator(PropComparator):
+        """Produce boolean, comparison, and other operators for
+        :class:`.CompositeProperty` attributes.
+
+        See the example in :ref:`composite_operations` for an overview
+        of usage , as well as the documentation for :class:`.PropComparator`.
+
+        See also:
+
+        :class:`.PropComparator`
+
+        :class:`.ColumnOperators`
+
+        :ref:`types_operators`
+
+        :attr:`.TypeEngine.comparator_factory`
+
+        """
         def __init__(self, prop, adapter=None):
             self.prop = self.property = prop
             self.adapter = adapter
index d0732b91359264127ed3de6009877e1aacdb8529..f41c5894e5d4cf16c4a577d7f1b670372e793f09 100644 (file)
@@ -196,28 +196,91 @@ class MapperProperty(_InspectionAttr):
         return operator(self.comparator, value)
 
 class PropComparator(operators.ColumnOperators):
-    """Defines comparison operations for MapperProperty objects.
+    """Defines boolean, comparison, and other operators for
+    :class:`.MapperProperty` objects.
+
+    SQLAlchemy allows for operators to
+    be redefined at both the Core and ORM level.  :class:`.PropComparator`
+    is the base class of operator redefinition for ORM-level operations,
+    including those of :class:`.ColumnProperty`, :class:`.RelationshipProperty`,
+    and :class:`.CompositeProperty`.
+
+    .. note:: With the advent of Hybrid properties introduced in SQLAlchemy
+       0.7, as well as Core-level operator redefinition in
+       SQLAlchemy 0.8, the use case for user-defined :class:`.PropComparator`
+       instances is extremely rare.  See :ref:`hybrids_toplevel` as well
+       as :ref:`types_operators`.
 
     User-defined subclasses of :class:`.PropComparator` may be created. The
     built-in Python comparison and math operator methods, such as
-    ``__eq__()``, ``__lt__()``, ``__add__()``, can be overridden to provide
+    :meth:`.operators.ColumnOperators.__eq__`,
+    :meth:`.operators.ColumnOperators.__lt__`, and
+    :meth:`.operators.ColumnOperators.__add__`, can be overridden to provide
     new operator behavior. The custom :class:`.PropComparator` is passed to
-    the mapper property via the ``comparator_factory`` argument. In each case,
+    the :class:`.MapperProperty` instance via the ``comparator_factory``
+    argument. In each case,
     the appropriate subclass of :class:`.PropComparator` should be used::
 
+        # definition of custom PropComparator subclasses
+
         from sqlalchemy.orm.properties import \\
                                 ColumnProperty,\\
                                 CompositeProperty,\\
                                 RelationshipProperty
 
         class MyColumnComparator(ColumnProperty.Comparator):
-            pass
+            def __eq__(self, other):
+                return self.__clause_element__() == other
+
+        class MyRelationshipComparator(RelationshipProperty.Comparator):
+            def any(self, expression):
+                "define the 'any' operation"
+                # ...
 
         class MyCompositeComparator(CompositeProperty.Comparator):
-            pass
+            def __gt__(self, other):
+                "redefine the 'greater than' operation"
 
-        class MyRelationshipComparator(RelationshipProperty.Comparator):
-            pass
+                return sql.and_(*[a>b for a, b in
+                                  zip(self.__clause_element__().clauses,
+                                      other.__composite_values__())])
+
+
+        # application of custom PropComparator subclasses
+
+        from sqlalchemy.orm import column_property, relationship, composite
+        from sqlalchemy import Column, String
+
+        class SomeMappedClass(Base):
+            some_column = column_property(Column("some_column", String),
+                                    comparator_factory=MyColumnComparator)
+
+            some_relationship = relationship(SomeOtherClass,
+                                    comparator_factory=MyRelationshipComparator)
+
+            some_composite = composite(
+                    Column("a", String), Column("b", String),
+                    comparator_factory=MyCompositeComparator
+                )
+
+    Note that for column-level operator redefinition, it's usually
+    simpler to define the operators at the Core level, using the
+    :attr:`.TypeEngine.comparator_factory` attribute.  See
+    :ref:`types_operators` for more detail.
+
+    See also:
+
+    :class:`.ColumnProperty.Comparator`
+
+    :class:`.RelationshipProperty.Comparator`
+
+    :class:`.CompositeProperty.Comparator`
+
+    :class:`.ColumnOperators`
+
+    :ref:`types_operators`
+
+    :attr:`.TypeEngine.comparator_factory`
 
     """
 
index 62e4672d316a3b2577c1ed82a60fea492cb474a5..f52e914f73e0cd1e34cbca0083f63409f4110dff 100644 (file)
@@ -161,6 +161,22 @@ class ColumnProperty(StrategizedProperty):
             dest_state._expire_attributes(dest_dict, [self.key])
 
     class Comparator(PropComparator):
+        """Produce boolean, comparison, and other operators for
+        :class:`.ColumnProperty` attributes.
+
+        See the documentation for :class:`.PropComparator` for a brief overview.
+
+        See also:
+
+        :class:`.PropComparator`
+
+        :class:`.ColumnOperators`
+
+        :ref:`types_operators`
+
+        :attr:`.TypeEngine.comparator_factory`
+
+        """
         @util.memoized_instancemethod
         def __clause_element__(self):
             if self.adapter:
@@ -198,9 +214,9 @@ class RelationshipProperty(StrategizedProperty):
 
     Public constructor is the :func:`.orm.relationship` function.
 
-    Of note here is the :class:`.RelationshipProperty.Comparator`
-    class, which implements comparison operations for scalar-
-    and collection-referencing mapped attributes.
+    See also:
+
+    :ref:`relationship_config_toplevel`
 
     """
 
@@ -304,8 +320,25 @@ class RelationshipProperty(StrategizedProperty):
             )
 
     class Comparator(PropComparator):
-        """Produce comparison operations for :func:`~.orm.relationship`-based
-         attributes."""
+        """Produce boolean, comparison, and other operators for
+        :class:`.RelationshipProperty` attributes.
+
+        See the documentation for :class:`.PropComparator` for a brief overview
+        of ORM level operator definition.
+
+        See also:
+
+        :class:`.PropComparator`
+
+        :class:`.ColumnProperty.Comparator`
+
+        :class:`.ColumnOperators`
+
+        :ref:`types_operators`
+
+        :attr:`.TypeEngine.comparator_factory`
+
+        """
 
         def __init__(self, prop, mapper, of_type=None, adapter=None):
             """Construction of :class:`.RelationshipProperty.Comparator`
index f975225d603079f596e4e3365187472018bac803..49df9322e88b18534f312ab03ed0c7f375840787 100644 (file)
@@ -948,8 +948,15 @@ class SQLCompiler(engine.Compiled):
             else:
                 add_to_result_map = None
 
-        if isinstance(col_expr, sql.Label):
-            result_expr = col_expr
+        if isinstance(column, sql.Label):
+            if col_expr is not column:
+                result_expr = _CompileLabel(
+                        col_expr,
+                        column.name,
+                        alt_names=(column.element,)
+                    )
+            else:
+                result_expr = col_expr
 
         elif select is not None and \
                 select.use_labels and \
index 63fa23c15d4a13bdb51f7da779108d91c9f6c8ec..e8905ccec52b6d57462cff7c27d804b87604fdde 100644 (file)
@@ -174,8 +174,8 @@ def select(columns=None, whereclause=None, from_obj=[], **kwargs):
 
     See also:
 
-    :ref:`coretutorial_selecting` - Core Tutorial description
-     of :func:`.select`.
+    :ref:`coretutorial_selecting` - Core Tutorial description of
+    :func:`.select`.
 
     :param columns:
       A list of :class:`.ClauseElement` objects, typically
@@ -899,9 +899,14 @@ def type_coerce(expr, type_):
         )
 
     """
+    type_ = sqltypes.to_instance(type_)
+
     if hasattr(expr, '__clause_expr__'):
         return type_coerce(expr.__clause_expr__())
-
+    elif isinstance(expr, BindParameter):
+        bp = expr._clone()
+        bp.type = type_
+        return bp
     elif not isinstance(expr, Visitable):
         if expr is None:
             return null()
@@ -1177,7 +1182,7 @@ def over(func, partition_by=None, order_by=None):
     Would produce "ROW_NUMBER() OVER(ORDER BY x)".
 
     :param func: a :class:`.FunctionElement` construct, typically
-     generated by :attr:`~.expression.func`.
+     generated by :data:`~.expression.func`.
     :param partition_by: a column element or string, or a list
      of such, that will be used as the PARTITION BY clause
      of the OVER construct.
@@ -1185,7 +1190,7 @@ def over(func, partition_by=None, order_by=None):
      of such, that will be used as the ORDER BY clause
      of the OVER construct.
 
-    This function is also available from the :attr:`~.expression.func`
+    This function is also available from the :data:`~.expression.func`
     construct itself via the :meth:`.FunctionElement.over` method.
 
     .. versionadded:: 0.7
@@ -2867,7 +2872,7 @@ class BindParameter(ColumnElement):
         if type_ is None:
             if _compared_to_type is not None:
                 self.type = \
-                    _compared_to_type._coerce_compared_value(
+                    _compared_to_type.coerce_compared_value(
                         _compared_to_operator, value)
             else:
                 self.type = sqltypes._type_map.get(type(value),
@@ -3470,7 +3475,7 @@ class Function(FunctionElement):
     def __init__(self, name, *clauses, **kw):
         """Construct a :class:`.Function`.
 
-        The :attr:`.func` construct is normally used to construct
+        The :data:`.func` construct is normally used to construct
         new :class:`.Function` instances.
 
         """
index ba1117ef69033a7e2148be1686f956648c01bf76..07fb417fb0df0831c05944c0b1f6e90d64f6e91c 100644 (file)
@@ -205,7 +205,8 @@ class custom_op(object):
 
 
 class ColumnOperators(Operators):
-    """Defines comparison and math operations.
+    """Defines boolean, comparison, and other operators for
+    :class:`.ColumnElement` expressions.
 
     By default, all methods call down to
     :meth:`.operate` or :meth:`.reverse_operate`,
@@ -229,10 +230,15 @@ class ColumnOperators(Operators):
     so that the ``==`` operation above is replaced by a clause
     construct.
 
-    The docstrings here will describe column-oriented
-    behavior of each operator.  For ORM-based operators
-    on related objects and collections, see
-    :class:`.RelationshipProperty.Comparator`.
+    See also:
+
+    :ref:`types_operators`
+
+    :attr:`.TypeEngine.comparator_factory`
+
+    :class:`.ColumnOperators`
+
+    :class:`.PropComparator`
 
     """
 
index ee262b56b3db969cd0f1fa4a4194b0bca2afe6cc..87d65654629d1b488a41a3e012d7704f7fa3c6cf 100644 (file)
@@ -164,7 +164,24 @@ class TypeEngine(AbstractType):
         return None
 
     def column_expression(self, colexpr):
-        """Given a SELECT column expression, return a wrapping SQL expression."""
+        """Given a SELECT column expression, return a wrapping SQL expression.
+
+        This is typically a SQL function that wraps a column expression
+        as rendered in the columns clause of a SELECT statement.
+        It is used for special data types that require
+        columns to be wrapped in some special database function in order
+        to coerce the value before being sent back to the application.
+        It is the SQL analogue of the :meth:`.TypeEngine.result_processor`
+        method.
+
+        The method is evaluated at statement compile time, as opposed
+        to statement construction time.
+
+        See also:
+
+        :ref:`types_sql_value_processing`
+
+        """
 
         return None
 
@@ -177,17 +194,24 @@ class TypeEngine(AbstractType):
         """"Given a bind value (i.e. a :class:`.BindParameter` instance),
         return a SQL expression in its place.
 
-        This is typically a SQL function that wraps the existing value
-        in a bind.   It is used for special data types that require
-        literals being wrapped in some special database function in all
-        cases, such as Postgis GEOMETRY types.
+        This is typically a SQL function that wraps the existing bound
+        parameter within the statement.  It is used for special data types
+        that require literals being wrapped in some special database function
+        in order to coerce an application-level value into a database-specific
+        format.  It is the SQL analogue of the :meth:`.TypeEngine.bind_processor`
+        method.
 
         The method is evaluated at statement compile time, as opposed
         to statement construction time.
 
         Note that this method, when implemented, should always return
         the exact same structure, without any conditional logic, as it
-        will be used during executemany() calls as well.
+        may be used in an executemany() call against an arbitrary number
+        of bound parameter sets.
+
+        See also:
+
+        :ref:`types_sql_value_processing`
 
         """
         return None
@@ -334,7 +358,7 @@ class TypeEngine(AbstractType):
         """
         return util.constructor_copy(self, cls, **kw)
 
-    def _coerce_compared_value(self, op, value):
+    def coerce_compared_value(self, op, value):
         """Suggest a type for a 'coerced' Python value in an expression.
 
         Given an operator and value, gives the type a chance
@@ -460,6 +484,23 @@ class UserDefinedType(TypeEngine):
 
     comparator_factory = Comparator
 
+    def coerce_compared_value(self, op, value):
+        """Suggest a type for a 'coerced' Python value in an expression.
+
+        Default behavior for :class:`.UserDefinedType` is the
+        same as that of :class:`.TypeDecorator`; by default it returns
+        ``self``, assuming the compared value should be coerced into
+        the same type as this one.  See :meth:`.TypeDecorator.coerce_compared_value`
+        for more detail.
+
+        .. versionchanged:: 0.8 :meth:`.UserDefinedType.coerce_compared_value`
+           now returns ``self`` by default, rather than falling onto the
+           more fundamental behavior of :meth:`.TypeEngine.coerce_compared_value`.
+
+        """
+
+        return self
+
 
 class TypeDecorator(TypeEngine):
     """Allows the creation of types which add additional functionality
@@ -602,7 +643,8 @@ class TypeDecorator(TypeEngine):
         return self.impl._type_affinity
 
     def type_engine(self, dialect):
-        """Return a dialect-specific :class:`.TypeEngine` instance for this :class:`.TypeDecorator`.
+        """Return a dialect-specific :class:`.TypeEngine` instance
+        for this :class:`.TypeDecorator`.
 
         In most cases this returns a dialect-adapted form of
         the :class:`.TypeEngine` type represented by ``self.impl``.
@@ -778,11 +820,6 @@ class TypeDecorator(TypeEngine):
         """
         return self
 
-    def _coerce_compared_value(self, op, value):
-        """See :meth:`.TypeEngine._coerce_compared_value` for a description."""
-
-        return self.coerce_compared_value(op, value)
-
     def copy(self):
         """Produce a copy of this :class:`.TypeDecorator` instance.
 
@@ -1672,13 +1709,13 @@ class _Binary(TypeEngine):
         return process
     # end Py2K
 
-    def _coerce_compared_value(self, op, value):
-        """See :meth:`.TypeEngine._coerce_compared_value` for a description."""
+    def coerce_compared_value(self, op, value):
+        """See :meth:`.TypeEngine.coerce_compared_value` for a description."""
 
         if isinstance(value, basestring):
             return self
         else:
-            return super(_Binary, self)._coerce_compared_value(op, value)
+            return super(_Binary, self).coerce_compared_value(op, value)
 
     def get_dbapi_type(self, dbapi):
         return dbapi.BINARY
@@ -2193,10 +2230,10 @@ class Interval(_DateAffinity, TypeDecorator):
     def _type_affinity(self):
         return Interval
 
-    def _coerce_compared_value(self, op, value):
-        """See :meth:`.TypeEngine._coerce_compared_value` for a description."""
+    def coerce_compared_value(self, op, value):
+        """See :meth:`.TypeEngine.coerce_compared_value` for a description."""
 
-        return self.impl._coerce_compared_value(op, value)
+        return self.impl.coerce_compared_value(op, value)
 
 
 class REAL(Float):
index 707ecb62191da6dda4da3f0fbbe22d6d1dfe2c93..841609ecec5af1522f03d485a08400577195fc7d 100644 (file)
@@ -26,7 +26,10 @@ test.aaa_profiling.test_compiler.CompileTest.test_insert 3.2_sqlite_pysqlite_noc
 
 # TEST: test.aaa_profiling.test_compiler.CompileTest.test_select
 
+test.aaa_profiling.test_compiler.CompileTest.test_select 2.7_mysql_mysqldb_cextensions 133
+test.aaa_profiling.test_compiler.CompileTest.test_select 2.7_postgresql_psycopg2_cextensions 133
 test.aaa_profiling.test_compiler.CompileTest.test_select 2.7_postgresql_psycopg2_nocextensions 133
+test.aaa_profiling.test_compiler.CompileTest.test_select 2.7_sqlite_pysqlite_cextensions 133
 test.aaa_profiling.test_compiler.CompileTest.test_select 2.7_sqlite_pysqlite_nocextensions 133
 
 # TEST: test.aaa_profiling.test_compiler.CompileTest.test_select_second_time
@@ -191,7 +194,10 @@ test.aaa_profiling.test_resultset.ExecutionTest.test_minimal_engine_execute 3.2_
 
 # TEST: test.aaa_profiling.test_resultset.ResultSetTest.test_contains_doesnt_compile
 
+test.aaa_profiling.test_resultset.ResultSetTest.test_contains_doesnt_compile 2.7_mysql_mysqldb_cextensions 14
+test.aaa_profiling.test_resultset.ResultSetTest.test_contains_doesnt_compile 2.7_postgresql_psycopg2_cextensions 14
 test.aaa_profiling.test_resultset.ResultSetTest.test_contains_doesnt_compile 2.7_postgresql_psycopg2_nocextensions 14
+test.aaa_profiling.test_resultset.ResultSetTest.test_contains_doesnt_compile 2.7_sqlite_pysqlite_cextensions 14
 test.aaa_profiling.test_resultset.ResultSetTest.test_contains_doesnt_compile 2.7_sqlite_pysqlite_nocextensions 14
 
 # TEST: test.aaa_profiling.test_resultset.ResultSetTest.test_string
@@ -234,6 +240,7 @@ test.aaa_profiling.test_zoomark.ZooMarkTest.test_profile_2_insert 3.2_postgresql
 
 # TEST: test.aaa_profiling.test_zoomark.ZooMarkTest.test_profile_3_properties
 
+test.aaa_profiling.test_zoomark.ZooMarkTest.test_profile_3_properties 2.7_postgresql_psycopg2_cextensions 3302
 test.aaa_profiling.test_zoomark.ZooMarkTest.test_profile_3_properties 2.7_postgresql_psycopg2_nocextensions 3526
 
 # TEST: test.aaa_profiling.test_zoomark.ZooMarkTest.test_profile_4_expressions
index 6ca28d46205093b19d41f574b155ee3ed491a5d9..e02c4ce46ade366b4bf9e3d78830331494507197 100644 (file)
@@ -229,6 +229,9 @@ def function_call_count(variance=0.05):
                 raise SkipTest("cProfile is not installed")
 
             if not _profile_stats.has_stats() and not _profile_stats.write:
+                # run the function anyway, to support dependent tests
+                # (not a great idea but we have these in test_zoomark)
+                fn(*args, **kw)
                 raise SkipTest("No profiling stats available on this "
                             "platform for this function.  Run tests with "
                             "--write-profiles to add statistics to %s for "
index d64ee7f0e47e2eb71e89a53fa7b2abd0e91b3a57..1445ee4f96d58a52c7c96585a690b3fb2a3f08cb 100644 (file)
@@ -1,4 +1,4 @@
-from sqlalchemy import Table, Column, String, func, MetaData, select
+from sqlalchemy import Table, Column, String, func, MetaData, select, TypeDecorator
 from test.lib import fixtures, AssertsCompiledSQL, testing
 from test.lib.testing import eq_
 
@@ -77,24 +77,7 @@ class SelectTest(fixtures.TestBase, AssertsCompiledSQL):
             "test_table WHERE test_table.y = lower(:y_2)"
         )
 
-class RoundTripTest(fixtures.TablesTest):
-    @classmethod
-    def define_tables(cls, metadata):
-        class MyString(String):
-            def bind_expression(self, bindvalue):
-                return func.lower(bindvalue)
-
-            def column_expression(self, col):
-                return func.upper(col)
-
-        Table(
-                'test_table',
-                metadata,
-                    Column('x', String(50)),
-                    Column('y', MyString(50)
-                )
-        )
-
+class RoundTripTestBase(object):
     def test_round_trip(self):
         testing.db.execute(
             self.tables.test_table.insert(),
@@ -150,8 +133,6 @@ class RoundTripTest(fixtures.TablesTest):
             "Y1"
         )
 
-    @testing.fails_if(lambda: True, "still need to propagate "
-                "result_map more effectively")
     def test_targeting_individual_labels(self):
         testing.db.execute(
             self.tables.test_table.insert(),
@@ -166,6 +147,44 @@ class RoundTripTest(fixtures.TablesTest):
             "Y1"
         )
 
+class StringRoundTripTest(fixtures.TablesTest, RoundTripTestBase):
+    @classmethod
+    def define_tables(cls, metadata):
+        class MyString(String):
+            def bind_expression(self, bindvalue):
+                return func.lower(bindvalue)
+
+            def column_expression(self, col):
+                return func.upper(col)
+
+        Table(
+                'test_table',
+                metadata,
+                    Column('x', String(50)),
+                    Column('y', MyString(50)
+                )
+        )
+
+
+class TypeDecRoundTripTest(fixtures.TablesTest, RoundTripTestBase):
+    @classmethod
+    def define_tables(cls, metadata):
+        class MyString(TypeDecorator):
+            impl = String
+            def bind_expression(self, bindvalue):
+                return func.lower(bindvalue)
+
+            def column_expression(self, col):
+                return func.upper(col)
+
+        Table(
+                'test_table',
+                metadata,
+                    Column('x', String(50)),
+                    Column('y', MyString(50)
+                )
+        )
+
 class ReturningTest(fixtures.TablesTest):
     __requires__ = 'returning',
 
index 279ae36a0a700570c3cb4f6c40fe3786da5aab7e..74047c7cabf040fd42d2d034a1a54ac8d1c9fe0b 100644 (file)
@@ -441,6 +441,13 @@ class UserDefinedTest(fixtures.TablesTest, AssertsCompiledSQL):
             []
         )
 
+        eq_(
+            testing.db.scalar(
+                select([type_coerce(literal('d1BIND_OUT'), MyType)])
+            ),
+            'd1BIND_OUT'
+        )
+
     @classmethod
     def define_tables(cls, metadata):
         class MyType(types.UserDefinedType):
@@ -1300,9 +1307,9 @@ class ExpressionTest(fixtures.TestBase, AssertsExecutionResults, AssertsCompiled
             pass
 
         # unknown type + integer, right hand bind
-        # is an Integer
+        # coerces to given type
         expr = column("foo", MyFoobarType) + 5
-        assert expr.right.type._type_affinity is types.Integer
+        assert expr.right.type._type_affinity is MyFoobarType
 
         # untyped bind - it gets assigned MyFoobarType
         expr = column("foo", MyFoobarType) + bindparam("foo")
@@ -1321,7 +1328,7 @@ class ExpressionTest(fixtures.TestBase, AssertsExecutionResults, AssertsCompiled
         assert expr.right.type._type_affinity is MyFoobarType
 
         expr = column("foo", MyFoobarType) - datetime.date(2010, 8, 25)
-        assert expr.right.type._type_affinity is types.Date
+        assert expr.right.type._type_affinity is MyFoobarType
 
     def test_date_coercion(self):
         from sqlalchemy.sql import column