From dabc7f0932dd13104411469fdc5327474b2bc8e3 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Thu, 30 Apr 2015 11:33:58 -0400 Subject: - Added support for type comparison functions to be not just per environment, but also present on the custom types themselves, by supplying a method ``compare_against_backend``. Added a new documentation section :ref:`compare_types` describing type comparison fully. fixes #296 --- alembic/ddl/impl.py | 6 ++++ alembic/environment.py | 34 +++---------------- docs/build/autogenerate.rst | 81 ++++++++++++++++++++++++++++++++++++++++++--- docs/build/changelog.rst | 10 ++++++ tests/test_autogenerate.py | 26 +++++++++++++++ 5 files changed, 124 insertions(+), 33 deletions(-) diff --git a/alembic/ddl/impl.py b/alembic/ddl/impl.py index 176079c..3cca1ef 100644 --- a/alembic/ddl/impl.py +++ b/alembic/ddl/impl.py @@ -247,6 +247,12 @@ class DefaultImpl(with_metaclass(ImplMeta)): # fixed in 0.7.4 metadata_impl.__dict__.pop('_type_affinity', None) + if hasattr(metadata_impl, "compare_against_backend"): + comparison = metadata_impl.compare_against_backend( + self.dialect, conn_type) + if comparison is not None: + return not comparison + if conn_type._compare_type_affinity( metadata_impl ): diff --git a/alembic/environment.py b/alembic/environment.py index 130a50f..860315b 100644 --- a/alembic/environment.py +++ b/alembic/environment.py @@ -417,38 +417,14 @@ class EnvironmentContext(object): operation. Defaults to ``False`` which disables type comparison. Set to ``True`` to turn on default type comparison, which has varied - accuracy depending on backend. - - To customize type comparison behavior, a callable may be - specified which - can filter type comparisons during an autogenerate operation. - The format of this callable is:: - - def my_compare_type(context, inspected_column, - metadata_column, inspected_type, metadata_type): - # return True if the types are different, - # False if not, or None to allow the default implementation - # to compare these types - return None - - context.configure( - # ... - compare_type = my_compare_type - ) - - - ``inspected_column`` is a :class:`sqlalchemy.schema.Column` as - returned by - :meth:`sqlalchemy.engine.reflection.Inspector.reflecttable`, - whereas ``metadata_column`` is a - :class:`sqlalchemy.schema.Column` from the local model - environment. - - A return value of ``None`` indicates to allow default type - comparison to proceed. + accuracy depending on backend. See :ref:`compare_types` + for an example as well as information on other type + comparison options. .. seealso:: + :ref:`compare_types` + :paramref:`.EnvironmentContext.configure.compare_server_default` :param compare_server_default: Indicates server default comparison diff --git a/docs/build/autogenerate.rst b/docs/build/autogenerate.rst index 8ad79ed..93f6000 100644 --- a/docs/build/autogenerate.rst +++ b/docs/build/autogenerate.rst @@ -126,7 +126,7 @@ Autogenerate can **optionally detect**: The feature works well in most cases, but is off by default so that it can be tested on the target schema first. It can also be customized by passing a callable here; see the - function's documentation for details. + section :ref:`compare_types` for details. * Change of server default. This will occur if you set the :paramref:`.EnvironmentContext.configure.compare_server_default` parameter to ``True``, or to a custom callable function. @@ -170,10 +170,10 @@ Autogenerate can't currently, but **will eventually detect**: * Sequence additions, removals - not yet implemented. -Rendering Types ----------------- +Comparing and Rendering Types +------------------------------ -The area of autogenerate's behavior of rendering Python-based type objects +The area of autogenerate's behavior of comparing and rendering Python-based type objects in migration scripts presents a challenge, in that there's a very wide variety of types to be rendered in scripts, including those part of SQLAlchemy as well as user-defined types. A few options @@ -345,3 +345,76 @@ The finished migration script will include our imports where the op.add_column('sometable', Column('mycolumn', types.MySpecialType())) +.. _compare_types: + +Comparing Types +^^^^^^^^^^^^^^^^ + +The default type comparison logic will work for SQLAlchemy built in types as +well as basic user defined types. This logic is only enabled if the +:paramref:`.EnvironmentContext.configure.compare_type` parameter +is set to True:: + + context.configure( + # ... + compare_type = True + ) + +Alternatively, the :paramref:`.EnvironmentContext.configure.compare_type` +parameter accepts a callable function which may be used to implement custom type +comparison logic, for cases such as where special user defined types +are being used:: + + def my_compare_type(context, inspected_column, + metadata_column, inspected_type, metadata_type): + # return True if the types are different, + # False if not, or None to allow the default implementation + # to compare these types + return None + + context.configure( + # ... + compare_type = my_compare_type + ) + +Above, ``inspected_column`` is a :class:`sqlalchemy.schema.Column` as +returned by +:meth:`sqlalchemy.engine.reflection.Inspector.reflecttable`, whereas +``metadata_column`` is a :class:`sqlalchemy.schema.Column` from the +local model environment. A return value of ``None`` indicates that default +type comparison to proceed. + +Additionally, custom types that are part of imported or third party +packages which have special behaviors such as per-dialect behavior +should implement a method called ``compare_against_backend()`` +on their SQLAlchemy type. If this method is present, it will be called +where it can also return True or False to specify the types compare as +equivalent or not; if it returns None, default type comparison logic +will proceed:: + + class MySpecialType(TypeDecorator): + + # ... + + def compare_against_backend(self, dialect, conn_type): + # return True if the types are different, + # False if not, or None to allow the default implementation + # to compare these types + if dialect.name == 'postgresql': + return isinstance(conn_type, postgresql.UUID) + else: + return isinstance(conn_type, String) + +The order of precedence regarding the +:paramref:`.EnvironmentContext.configure.compare_type` callable vs. the +type itself implementing ``compare_against_backend`` is that the +:paramref:`.EnvironmentContext.configure.compare_type` callable is favored +first; if it returns ``None``, then the ``compare_against_backend`` method +will be used, if present on the metadata type. If that reutrns ``None``, +then a basic check for type equivalence is run. + +.. versionadded:: 0.7.6 - added support for the ``compare_against_backend()`` + method. + + + diff --git a/docs/build/changelog.rst b/docs/build/changelog.rst index dbfd323..8e3824c 100644 --- a/docs/build/changelog.rst +++ b/docs/build/changelog.rst @@ -6,6 +6,16 @@ Changelog .. changelog:: :version: 0.7.6 + .. change:: + :tags: feature, autogenerate + :tickets: 296 + + Added support for type comparison functions to be not just per + environment, but also present on the custom types themselves, by + supplying a method ``compare_against_backend``. + Added a new documentation section :ref:`compare_types` describing + type comparison fully. + .. change:: :tags: feature, operations :tickets: 255 diff --git a/tests/test_autogenerate.py b/tests/test_autogenerate.py index e9ffe8b..a089b42 100644 --- a/tests/test_autogenerate.py +++ b/tests/test_autogenerate.py @@ -773,6 +773,32 @@ nullable=True)) ) assert not diff + def test_custom_type_compare(self): + class MyType(TypeDecorator): + impl = Integer + + def compare_against_backend(self, dialect, conn_type): + return isinstance(conn_type, Integer) + + diff = [] + autogenerate.compare._compare_type(None, "sometable", "somecol", + Column("somecol", INTEGER()), + Column("somecol", MyType()), + diff, self.autogen_context + ) + assert not diff + + diff = [] + autogenerate.compare._compare_type(None, "sometable", "somecol", + Column("somecol", String()), + Column("somecol", MyType()), + diff, self.autogen_context + ) + eq_( + diff[0][0:4], + ('modify_type', None, 'sometable', 'somecol') + ) + def test_affinity_typedec(self): class MyType(TypeDecorator): impl = CHAR -- cgit v1.2.1