diff options
-rw-r--r-- | doc/build/core/tutorial.rst | 6 | ||||
-rw-r--r-- | doc/build/core/types.rst | 143 | ||||
-rw-r--r-- | doc/build/orm/examples.rst | 2 | ||||
-rw-r--r-- | doc/build/orm/mapper_config.rst | 18 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/__init__.py | 3 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/descriptor_props.py | 27 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/interfaces.py | 77 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/properties.py | 43 | ||||
-rw-r--r-- | lib/sqlalchemy/sql/compiler.py | 11 | ||||
-rw-r--r-- | lib/sqlalchemy/sql/expression.py | 19 | ||||
-rw-r--r-- | lib/sqlalchemy/sql/operators.py | 16 | ||||
-rw-r--r-- | lib/sqlalchemy/types.py | 75 | ||||
-rw-r--r-- | test/lib/profiles.txt | 7 | ||||
-rw-r--r-- | test/lib/profiling.py | 3 | ||||
-rw-r--r-- | test/sql/test_type_expressions.py | 61 | ||||
-rw-r--r-- | test/sql/test_types.py | 13 |
16 files changed, 449 insertions, 75 deletions
diff --git a/doc/build/core/tutorial.rst b/doc/build/core/tutorial.rst index dad1aa68d..6d55a1ecc 100644 --- a/doc/build/core/tutorial.rst +++ b/doc/build/core/tutorial.rst @@ -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 diff --git a/doc/build/core/types.rst b/doc/build/core/types.rst index 6518a5e9c..d3278e067 100644 --- a/doc/build/core/types.rst +++ b/doc/build/core/types.rst @@ -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` diff --git a/doc/build/orm/examples.rst b/doc/build/orm/examples.rst index fcee00434..6560547cd 100644 --- a/doc/build/orm/examples.rst +++ b/doc/build/orm/examples.rst @@ -102,6 +102,8 @@ Polymorphic Associations See :ref:`examples_generic_associations` for a modern version of polymorphic associations. +.. _examples_postgis: + PostGIS Integration ------------------- diff --git a/doc/build/orm/mapper_config.rst b/doc/build/orm/mapper_config.rst index 62515d214..be0274abd 100644 --- a/doc/build/orm/mapper_config.rst +++ b/doc/build/orm/mapper_config.rst @@ -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:: diff --git a/lib/sqlalchemy/orm/__init__.py b/lib/sqlalchemy/orm/__init__.py index 1a22fe3d1..2078b2396 100644 --- a/lib/sqlalchemy/orm/__init__.py +++ b/lib/sqlalchemy/orm/__init__.py @@ -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. diff --git a/lib/sqlalchemy/orm/descriptor_props.py b/lib/sqlalchemy/orm/descriptor_props.py index 91717974d..f4c2e1a90 100644 --- a/lib/sqlalchemy/orm/descriptor_props.py +++ b/lib/sqlalchemy/orm/descriptor_props.py @@ -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 diff --git a/lib/sqlalchemy/orm/interfaces.py b/lib/sqlalchemy/orm/interfaces.py index d0732b913..f41c5894e 100644 --- a/lib/sqlalchemy/orm/interfaces.py +++ b/lib/sqlalchemy/orm/interfaces.py @@ -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` """ diff --git a/lib/sqlalchemy/orm/properties.py b/lib/sqlalchemy/orm/properties.py index 62e4672d3..f52e914f7 100644 --- a/lib/sqlalchemy/orm/properties.py +++ b/lib/sqlalchemy/orm/properties.py @@ -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` diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index f975225d6..49df9322e 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -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 \ diff --git a/lib/sqlalchemy/sql/expression.py b/lib/sqlalchemy/sql/expression.py index 63fa23c15..e8905ccec 100644 --- a/lib/sqlalchemy/sql/expression.py +++ b/lib/sqlalchemy/sql/expression.py @@ -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. """ diff --git a/lib/sqlalchemy/sql/operators.py b/lib/sqlalchemy/sql/operators.py index ba1117ef6..07fb417fb 100644 --- a/lib/sqlalchemy/sql/operators.py +++ b/lib/sqlalchemy/sql/operators.py @@ -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` """ diff --git a/lib/sqlalchemy/types.py b/lib/sqlalchemy/types.py index ee262b56b..87d656546 100644 --- a/lib/sqlalchemy/types.py +++ b/lib/sqlalchemy/types.py @@ -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): diff --git a/test/lib/profiles.txt b/test/lib/profiles.txt index 707ecb621..841609ece 100644 --- a/test/lib/profiles.txt +++ b/test/lib/profiles.txt @@ -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 diff --git a/test/lib/profiling.py b/test/lib/profiling.py index 6ca28d462..e02c4ce46 100644 --- a/test/lib/profiling.py +++ b/test/lib/profiling.py @@ -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 " diff --git a/test/sql/test_type_expressions.py b/test/sql/test_type_expressions.py index d64ee7f0e..1445ee4f9 100644 --- a/test/sql/test_type_expressions.py +++ b/test/sql/test_type_expressions.py @@ -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', diff --git a/test/sql/test_types.py b/test/sql/test_types.py index 279ae36a0..74047c7ca 100644 --- a/test/sql/test_types.py +++ b/test/sql/test_types.py @@ -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 |