From ceeac2a45563346b2b7741fb85d3c65828e52904 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Thu, 16 Jul 2015 19:00:14 -0400 Subject: - build out custom autogenerate compare hooks - new documentation for autogenerate customization --- alembic/autogenerate/__init__.py | 2 +- alembic/autogenerate/compare.py | 104 ++++++++-------- alembic/util/langhelpers.py | 11 +- docs/build/api/autogenerate.rst | 255 +++++++++++++++++++++++++++++++++++---- docs/build/api/operations.rst | 66 ++++++++-- docs/build/changelog.rst | 11 +- tests/test_autogen_diffs.py | 49 ++++---- tests/test_postgresql.py | 6 +- 8 files changed, 386 insertions(+), 118 deletions(-) diff --git a/alembic/autogenerate/__init__.py b/alembic/autogenerate/__init__.py index 8deef1e..78520a8 100644 --- a/alembic/autogenerate/__init__.py +++ b/alembic/autogenerate/__init__.py @@ -3,5 +3,5 @@ from .api import ( # noqa produce_migrations, render_python_code, RevisionContext ) -from .compare import _produce_net_changes # noqa +from .compare import _produce_net_changes, comparators # noqa from .render import render_op_text, renderers # noqa \ No newline at end of file diff --git a/alembic/autogenerate/compare.py b/alembic/autogenerate/compare.py index 7a3828b..fdc3cae 100644 --- a/alembic/autogenerate/compare.py +++ b/alembic/autogenerate/compare.py @@ -3,6 +3,7 @@ from sqlalchemy.engine.reflection import Inspector from sqlalchemy import event from ..operations import ops import logging +from .. import util from ..util import compat from ..util import sqla_compat from sqlalchemy.util import OrderedSet @@ -19,6 +20,9 @@ def _populate_migration_script(autogen_context, migration_script): migration_script.upgrade_ops.reverse_into(migration_script.downgrade_ops) +comparators = util.Dispatcher(uselist=True) + + def _produce_net_changes(autogen_context, upgrade_ops): connection = autogen_context.connection @@ -37,10 +41,13 @@ def _produce_net_changes(autogen_context, upgrade_ops): else: schemas = [None] - _autogen_for_tables(autogen_context, schemas, upgrade_ops) + comparators.dispatch("schema", autogen_context.dialect.name)( + autogen_context, upgrade_ops, schemas + ) -def _autogen_for_tables(autogen_context, schemas, upgrade_ops): +@comparators.dispatch_for("schema") +def _autogen_for_tables(autogen_context, upgrade_ops, schemas): inspector = autogen_context.inspector metadata = autogen_context.metadata @@ -105,11 +112,11 @@ def _compare_tables(conn_table_names, metadata_table_names, ops.CreateTableOp.from_table(metadata_table)) log.info("Detected added table %r", name) modify_table_ops = ops.ModifyTableOps(tname, [], schema=s) - _compare_indexes_and_uniques(s, tname, - None, - metadata_table, - modify_table_ops, - autogen_context, inspector) + + comparators.dispatch("table")( + autogen_context, modify_table_ops, + s, tname, None, metadata_table + ) if not modify_table_ops.is_empty(): upgrade_ops.ops.append(modify_table_ops) @@ -165,23 +172,15 @@ def _compare_tables(conn_table_names, metadata_table_names, conn_table, metadata_table, modify_table_ops, autogen_context, inspector): - _compare_indexes_and_uniques(s, tname, - conn_table, - metadata_table, - modify_table_ops, - autogen_context, inspector) - _compare_foreign_keys(s, tname, conn_table, - metadata_table, - modify_table_ops, autogen_context, - inspector) + + comparators.dispatch("table")( + autogen_context, modify_table_ops, + s, tname, conn_table, metadata_table + ) if not modify_table_ops.is_empty(): upgrade_ops.ops.append(modify_table_ops) - # TODO: - # table constraints - # sequences - def _make_index(params, conn_table): # TODO: add .info such as 'duplicates_constraint' @@ -246,23 +245,12 @@ def _compare_columns(schema, tname, conn_table, metadata_table, continue alter_column_op = ops.AlterColumnOp( tname, colname, schema=schema) - _compare_type(schema, tname, colname, - conn_col, - metadata_col, - alter_column_op, autogen_context - ) - # work around SQLAlchemy issue #3023 - if not metadata_col.primary_key: - _compare_nullable(schema, tname, colname, - conn_col, - metadata_col.nullable, - alter_column_op, autogen_context - ) - _compare_server_default(schema, tname, colname, - conn_col, - metadata_col, - alter_column_op, autogen_context - ) + + comparators.dispatch("column")( + autogen_context, alter_column_op, + schema, tname, colname, conn_col, metadata_col + ) + if alter_column_op.has_changes(): modify_table_ops.ops.append(alter_column_op) @@ -334,10 +322,12 @@ class _fk_constraint_sig(_constraint_sig): ) -def _compare_indexes_and_uniques(schema, tname, conn_table, - metadata_table, modify_ops, - autogen_context, inspector): +@comparators.dispatch_for("table") +def _compare_indexes_and_uniques( + autogen_context, modify_ops, schema, tname, conn_table, + metadata_table): + inspector = autogen_context.inspector is_create_table = conn_table is None # 1a. get raw indexes and unique constraints from metadata ... @@ -568,9 +558,16 @@ def _compare_indexes_and_uniques(schema, tname, conn_table, obj_added(unnamed_metadata_uniques[uq_sig]) -def _compare_nullable(schema, tname, cname, conn_col, - metadata_col_nullable, alter_column_op, - autogen_context): +@comparators.dispatch_for("column") +def _compare_nullable( + autogen_context, alter_column_op, schema, tname, cname, conn_col, + metadata_col): + + # work around SQLAlchemy issue #3023 + if metadata_col.primary_key: + return + + metadata_col_nullable = metadata_col.nullable conn_col_nullable = conn_col.nullable alter_column_op.existing_nullable = conn_col_nullable @@ -583,9 +580,10 @@ def _compare_nullable(schema, tname, cname, conn_col, ) -def _compare_type(schema, tname, cname, conn_col, - metadata_col, alter_column_op, - autogen_context): +@comparators.dispatch_for("column") +def _compare_type( + autogen_context, alter_column_op, schema, tname, cname, conn_col, + metadata_col): conn_type = conn_col.type alter_column_op.existing_type = conn_type @@ -632,8 +630,10 @@ def _render_server_default_for_compare(metadata_default, return None -def _compare_server_default(schema, tname, cname, conn_col, metadata_col, - alter_column_op, autogen_context): +@comparators.dispatch_for("column") +def _compare_server_default( + autogen_context, alter_column_op, schema, tname, cname, + conn_col, metadata_col): metadata_default = metadata_col.server_default conn_col_default = conn_col.server_default @@ -659,15 +659,17 @@ def _compare_server_default(schema, tname, cname, conn_col, metadata_col, tname, cname) -def _compare_foreign_keys(schema, tname, conn_table, - metadata_table, modify_table_ops, - autogen_context, inspector): +@comparators.dispatch_for("table") +def _compare_foreign_keys( + autogen_context, modify_table_ops, schema, tname, conn_table, + metadata_table): # if we're doing CREATE TABLE, all FKs are created # inline within the table def if conn_table is None: return + inspector = autogen_context.inspector metadata_fks = set( fk for fk in metadata_table.constraints if isinstance(fk, sa_schema.ForeignKeyConstraint) diff --git a/alembic/util/langhelpers.py b/alembic/util/langhelpers.py index 3d1befb..6c92e3c 100644 --- a/alembic/util/langhelpers.py +++ b/alembic/util/langhelpers.py @@ -263,7 +263,6 @@ class Dispatcher(object): def dispatch_for(self, target, qualifier='default'): def decorate(fn): - assert isinstance(target, type) if self.uselist: assert target not in self._registry self._registry.setdefault((target, qualifier), []).append(fn) @@ -274,7 +273,15 @@ class Dispatcher(object): return decorate def dispatch(self, obj, qualifier='default'): - for spcls in type(obj).__mro__: + + if isinstance(obj, string_types): + targets = [obj] + elif isinstance(obj, type): + targets = obj.__mro__ + else: + targets = type(obj).__mro__ + + for spcls in targets: if qualifier != 'default' and (spcls, qualifier) in self._registry: return self._fn_or_list(self._registry[(spcls, qualifier)]) elif (spcls, 'default') in self._registry: diff --git a/docs/build/api/autogenerate.rst b/docs/build/api/autogenerate.rst index b60d858..8b026e8 100644 --- a/docs/build/api/autogenerate.rst +++ b/docs/build/api/autogenerate.rst @@ -4,7 +4,8 @@ Autogeneration ============== -The autogenerate system has two areas of API that are public: +The autogeneration system has a wide degree of public API, including +the following areas: 1. The ability to do a "diff" of a :class:`~sqlalchemy.schema.MetaData` object against a database, and receive a data structure back. This structure @@ -15,9 +16,22 @@ The autogenerate system has two areas of API that are public: revision scripts, including support for multiple revision scripts generated in one pass. +3. The ability to add new operation directives to autogeneration, including + custom schema/model comparison functions and revision script rendering. + Getting Diffs ============== +The simplest API autogenerate provides is the "schema comparison" API; +these are simple functions that will run all registered "comparison" functions +between a :class:`~sqlalchemy.schema.MetaData` object and a database +backend to produce a structure showing how they differ. The two +functions provided are :func:`.compare_metadata`, which is more of the +"legacy" function that produces diff tuples, and :func:`.produce_migrations`, +which produces a structure consisting of operation directives detailed in +:ref:`alembic.operations.toplevel`. + + .. autofunction:: alembic.autogenerate.compare_metadata .. autofunction:: alembic.autogenerate.produce_migrations @@ -184,6 +198,8 @@ to whatever is in this list. .. autofunction:: alembic.autogenerate.render_python_code +.. _autogen_custom_ops: + Autogenerating Custom Operation Directives ========================================== @@ -192,16 +208,180 @@ subclasses of :class:`.MigrateOperation` in order to add new ``op.`` directives. In the preceding section :ref:`customizing_revision`, we also learned that these same :class:`.MigrateOperation` structures are at the base of how the autogenerate system knows what Python code to render. -How to connect these two systems, so that our own custom operation -directives can be used? First off, we'd probably be implementing -a :paramref:`.EnvironmentContext.configure.process_revision_directives` -plugin as described previously, so that we can add our own directives -to the autogenerate stream. What if we wanted to add our ``CreateSequenceOp`` -to the autogenerate structure? We basically need to define an autogenerate -renderer for it, as follows:: +Using this knowledge, we can create additional functions that plug into +the autogenerate system so that our new operations can be generated +into migration scripts when ``alembic revision --autogenerate`` is run. + +The following sections will detail an example of this using the +the ``CreateSequenceOp`` and ``DropSequenceOp`` directives +we created in :ref:`operation_plugins`, which correspond to the +SQLAlchemy :class:`~sqlalchemy.schema.Sequence` construct. + +.. versionadded:: 0.8.0 - custom operations can be added to the + autogenerate system to support new kinds of database objects. + +Tracking our Object with the Model +---------------------------------- + +The basic job of an autogenerate comparison function is to inspect +a series of objects in the database and compare them against a series +of objects defined in our model. By "in our model", we mean anything +defined in Python code that we want to track, however most commonly +we're talking about a series of :class:`~sqlalchemy.schema.Table` +objects present in a :class:`~sqlalchemy.schema.MetaData` collection. + +Let's propose a simple way of seeing what :class:`~sqlalchemy.schema.Sequence` +objects we want to ensure exist in the database when autogenerate +runs. While these objects do have some integrations with +:class:`~sqlalchemy.schema.Table` and :class:`~sqlalchemy.schema.MetaData` +already, let's assume they don't, as the example here intends to illustrate +how we would do this for most any kind of custom construct. We +associate the object with the :attr:`~sqlalchemy.schema.MetaData.info` +collection of :class:`~sqlalchemy.schema.MetaData`, which is a dictionary +we can use for anything, which we also know will be passed to the autogenerate +process:: + + from sqlalchemy.schema import Sequence + + def add_sequence_to_model(sequence, metadata): + metadata.info.setdefault("sequences", set()).add( + (sequence.schema, sequence.name) + ) + + my_seq = Sequence("my_sequence") + add_sequence_to_model(my_seq, model_metadata) + +The :attr:`~sqlalchemy.schema.MetaData.info` +dictionary is a good place to put things that we want our autogeneration +routines to be able to locate, which can include any object such as +custom DDL objects representing views, triggers, special constraints, +or anything else we want to support. + + +Registering a Comparison Function +--------------------------------- + +We now need to register a comparison hook, which will be used +to compare the database to our model and produce ``CreateSequenceOp`` +and ``DropSequenceOp`` directives to be included in our migration +script. Note that we are assuming a +Postgresql backend:: + + from alembic.autogenerate import comparators + + @comparators.dispatch_for("schema") + def compare_sequences(autogen_context, upgrade_ops, schemas): + all_conn_sequences = set() + + for sch in schemas: + + all_conn_sequences.update([ + (sch, row[0]) for row in + autogen_context.connection.execute( + "SELECT relname FROM pg_class c join " + "pg_namespace n on n.oid=c.relnamespace where " + "relkind='S' and n.nspname=%(nspname)s", + + # note that we consider a schema of 'None' in our + # model to be the "default" name in the PG database; + # this usually is the name 'public' + nspname=autogen_context.dialect.default_schema_name + if sch is None else sch + ) + ]) + + # get the collection of Sequence objects we're storing with + # our MetaData + metadata_sequences = autogen_context.metadata.info.setdefault( + "sequences", set()) + + # for new names, produce CreateSequenceOp directives + for sch, name in metadata_sequences.difference(all_conn_sequences): + upgrade_ops.ops.append( + CreateSequenceOp(name, schema=sch) + ) + + # for names that are going away, produce DropSequenceOp + # directives + for sch, name in all_conn_sequences.difference(metadata_sequences): + upgrade_ops.ops.append( + DropSequenceOp(name, schema=sch) + ) + +Above, we've built a new function ``compare_sequences()`` and registered +it as a "schema" level comparison function with autogenerate. The +job that it performs is that it compares the list of sequence names +present in each database schema with that of a list of sequence names +that we are maintaining in our :class:`~sqlalchemy.schema.MetaData` object. + +When autogenerate completes, it will have a series of +``CreateSequenceOp`` and ``DropSequenceOp`` directives in the list of +"upgrade" operations; the list of "downgrade" operations is generated +directly from these using the +``CreateSequenceOp.reverse()`` and ``DropSequenceOp.reverse()`` methods +that we've implemented on these objects. + +The registration of our function at the scope of "schema" means our +autogenerate comparison function is called outside of the context +of any specific table or column. The three available scopes +are "schema", "table", and "column", summarized as follows: + +* **Schema level** - these hooks are passed a :class:`.AutogenContext`, + an :class:`.UpgradeOps` collection, and a collection of string schema + names to be operated upon. If the + :class:`.UpgradeOps` collection contains changes after all + hooks are run, it is included in the migration script: + + :: + + @comparators.dispatch_for("schema") + def compare_schema_level(autogen_context, upgrade_ops, schemas): + pass + +* **Table level** - these hooks are passed a :class:`.AutogenContext`, + a :class:`.ModifyTableOps` collection, a schema name, table name, + a :class:`~sqlalchemy.schema.Table` reflected from the database if any + or ``None``, and a :class:`~sqlalchemy.schema.Table` present in the + local :class:`~sqlalchemy.schema.MetaData`. If the + :class:`.ModifyTableOps` collection contains changes after all + hooks are run, it is included in the migration script: + + :: + + @comparators.dispatch_for("table") + def compare_table_level(autogen_context, modify_ops, + schemaname, tablename, conn_table, metadata_table): + pass + +* **Column level** - these hooks are passed a :class:`.AutogenContext`, + an :class:`.AlterColumnOp` object, a schema name, table name, + column name, a :class:`~sqlalchemy.schema.Column` reflected from the + database and a :class:`~sqlalchemy.schema.Column` present in the + local table. If the :class:`.AlterColumnOp` contains changes after + all hooks are run, it is included in the migration script; + a "change" is considered to be present if any of the ``modify_`` attributes + are set to a non-default value, or there are any keys + in the ``.kw`` collection with the prefix ``"modify_"``: + + :: + + @comparators.dispatch_for("column") + def compare_column_level(autogen_context, alter_column_op, + schemaname, tname, cname, conn_col, metadata_col): + pass + +The :class:`.AutogenContext` passed to these hooks is documented below. + +.. autoclass:: alembic.autogenerate.api.AutogenContext + :members: - # note: this is a continuation of the example from the - # "Operation Plugins" section +Creating a Render Function +-------------------------- + +The second autogenerate integration hook is to provide a "render" function; +since the autogenerate +system renders Python code, we need to build a function that renders +the correct "op" instructions for our directive:: from alembic.autogenerate import renderers @@ -209,29 +389,52 @@ renderer for it, as follows:: def render_create_sequence(autogen_context, op): return "op.create_sequence(%r, **%r)" % ( op.sequence_name, - op.kw + {"schema": op.schema} ) -With our render function established, we can our ``CreateSequenceOp`` -generated in an autogenerate context using the :func:`.render_python_code` -debugging function in conjunction with an :class:`.UpgradeOps` structure:: - from alembic.operations import ops - from alembic.autogenerate import render_python_code + @renderers.dispatch_for(DropSequenceOp) + def render_drop_sequence(autogen_context, op): + return "op.drop_sequence(%r, **%r)" % ( + op.sequence_name, + {"schema": op.schema} + ) - upgrade_ops = ops.UpgradeOps( - ops=[ - CreateSequenceOp("my_seq") - ] - ) +The above functions will render Python code corresponding to the +presence of ``CreateSequenceOp`` and ``DropSequenceOp`` instructions +in the list that our comparison function generates. - print(render_python_code(upgrade_ops)) +Running It +---------- -Which produces:: +All the above code can be organized however the developer sees fit; +the only thing that needs to make it work is that when the +Alembic environment ``env.py`` is invoked, it either imports modules +which contain all the above routines, or they are locally present, +or some combination thereof. - ### commands auto generated by Alembic - please adjust! ### - op.create_sequence('my_seq', **{}) +If we then have code in our model (which of course also needs to be invoked +when ``env.py`` runs!) like this:: + + from sqlalchemy.schema import Sequence + + my_seq_1 = Sequence("my_sequence_1") + add_sequence_to_model(my_seq_1, target_metadata) + +When we first run ``alembic revision --autogenerate``, we'll see this +in our migration file:: + + def upgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.create_sequence('my_sequence_1', **{'schema': None}) ### end Alembic commands ### -.. autoclass:: alembic.autogenerate.api.AutogenContext + def downgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.drop_sequence('my_sequence_1', **{'schema': None}) + ### end Alembic commands ### + +These are our custom directives that will invoke when ``alembic upgrade`` +or ``alembic downgrade`` is run. + diff --git a/docs/build/api/operations.rst b/docs/build/api/operations.rst index d9ff238..2eb8358 100644 --- a/docs/build/api/operations.rst +++ b/docs/build/api/operations.rst @@ -1,7 +1,7 @@ .. _alembic.operations.toplevel: ===================== -The Operations Object +Operation Directives ===================== Within migration scripts, actual database migration operations are handled @@ -48,9 +48,9 @@ migration scripts:: class CreateSequenceOp(MigrateOperation): """Create a SEQUENCE.""" - def __init__(self, sequence_name, **kw): + def __init__(self, sequence_name, schema=None): self.sequence_name = sequence_name - self.kw = kw + self.schema = schema @classmethod def create_sequence(cls, operations, sequence_name, **kw): @@ -59,20 +59,58 @@ migration scripts:: op = CreateSequenceOp(sequence_name, **kw) return operations.invoke(op) -Above, the ``CreateSequenceOp`` class represents a new operation that will -be available as ``op.create_sequence()``. The reason the operation -is represented as a stateful class is so that an operation and a specific + def reverse(self): + # only needed to support autogenerate + return DropSequenceOp(self.sequence_name, schema=self.schema) + + @Operations.register_operation("drop_sequence") + class DropSequenceOp(MigrateOperation): + """Drop a SEQUENCE.""" + + def __init__(self, sequence_name, schema=None): + self.sequence_name = sequence_name + self.schema = schema + + @classmethod + def drop_sequence(cls, operations, sequence_name, **kw): + """Issue a "DROP SEQUENCE" instruction.""" + + op = DropSequenceOp(sequence_name, **kw) + return operations.invoke(op) + + def reverse(self): + # only needed to support autogenerate + return CreateSequenceOp(self.sequence_name, schema=self.schema) + +Above, the ``CreateSequenceOp`` and ``DropSequenceOp`` classes represent +new operations that will +be available as ``op.create_sequence()`` and ``op.drop_sequence()``. +The reason the operations +are represented as stateful classes is so that an operation and a specific set of arguments can be represented generically; the state can then correspond to different kinds of operations, such as invoking the instruction against a database, or autogenerating Python code for the operation into a script. -In order to establish the migrate-script behavior of the new operation, +In order to establish the migrate-script behavior of the new operations, we use the :meth:`.Operations.implementation_for` decorator:: @Operations.implementation_for(CreateSequenceOp) def create_sequence(operations, operation): - operations.execute("CREATE SEQUENCE %s" % operation.sequence_name) + if operation.schema is not None: + name = "%s.%s" % (operation.schema, operation.sequence_name) + else: + name = operation.sequence_name + operations.execute("CREATE SEQUENCE %s" % name) + + + @Operations.implementation_for(DropSequenceOp) + def drop_sequence(operations, operation): + if operation.schema is not None: + name = "%s.%s" % (operation.schema, operation.sequence_name) + else: + name = operation.sequence_name + operations.execute("DROP SEQUENCE %s" % name) Above, we use the simplest possible technique of invoking our DDL, which is just to call :meth:`.Operations.execute` with literal SQL. If this is @@ -80,16 +118,24 @@ all a custom operation needs, then this is fine. However, options for more comprehensive support include building out a custom SQL construct, as documented at :ref:`sqlalchemy.ext.compiler_toplevel`. -With the above two steps, a migration script can now use a new method -``op.create_sequence()`` that will proxy to our object as a classmethod:: +With the above two steps, a migration script can now use new methods +``op.create_sequence()`` and ``op.drop_sequence()`` that will proxy to +our object as a classmethod:: def upgrade(): op.create_sequence("my_sequence") + def downgrade(): + op.drop_sequence("my_sequence") + The registration of new operations only needs to occur in time for the ``env.py`` script to invoke :meth:`.MigrationContext.run_migrations`; within the module level of the ``env.py`` script is sufficient. +.. seealso:: + + :ref:`autogen_custom_ops` - how to add autogenerate support to + custom operations. .. versionadded:: 0.8 - the migration operations available via the :class:`.Operations` class as well as the ``alembic.op`` namespace diff --git a/docs/build/changelog.rst b/docs/build/changelog.rst index 8232c47..f6982b2 100644 --- a/docs/build/changelog.rst +++ b/docs/build/changelog.rst @@ -21,7 +21,7 @@ Changelog .. change:: :tags: feature, autogenerate - :tickets: 301 + :tickets: 301, 306 The internal system for autogenerate been reworked to build upon the extensible system of operation objects present in @@ -32,9 +32,12 @@ Changelog :paramref:`.EnvironmentContext.configure.process_revision_directives` allows end-user code to fully customize what autogenerate will do, including not just full manipulation of the Python steps to take - but also what file or files will be written and where. It is also - possible to write a system that reads an autogenerate stream and - invokes it directly against a database without writing any files. + but also what file or files will be written and where. Additionally, + autogenerate is now extensible as far as database objects compared + and rendered into scripts; any new operation directive can also be + registered into a series of hooks that allow custom database/model + comparison functions to run as well as to render new operation + directives into autogenerate scripts. .. seealso:: diff --git a/tests/test_autogen_diffs.py b/tests/test_autogen_diffs.py index 196ba23..d176b91 100644 --- a/tests/test_autogen_diffs.py +++ b/tests/test_autogen_diffs.py @@ -396,21 +396,23 @@ class AutogenerateDiffTest(ModelOne, AutogenTest, TestBase): def test_skip_null_type_comparison_reflected(self): ac = ops.AlterColumnOp("sometable", "somecol") - autogenerate.compare._compare_type(None, "sometable", "somecol", - Column("somecol", NULLTYPE), - Column("somecol", Integer()), - ac, self.autogen_context - ) + autogenerate.compare._compare_type( + self.autogen_context, ac, + None, "sometable", "somecol", + Column("somecol", NULLTYPE), + Column("somecol", Integer()), + ) diff = ac.to_diff_tuple() assert not diff def test_skip_null_type_comparison_local(self): ac = ops.AlterColumnOp("sometable", "somecol") - autogenerate.compare._compare_type(None, "sometable", "somecol", - Column("somecol", Integer()), - Column("somecol", NULLTYPE), - ac, self.autogen_context - ) + autogenerate.compare._compare_type( + self.autogen_context, ac, + None, "sometable", "somecol", + Column("somecol", Integer()), + Column("somecol", NULLTYPE), + ) diff = ac.to_diff_tuple() assert not diff @@ -422,19 +424,22 @@ class AutogenerateDiffTest(ModelOne, AutogenTest, TestBase): return isinstance(conn_type, Integer) ac = ops.AlterColumnOp("sometable", "somecol") - autogenerate.compare._compare_type(None, "sometable", "somecol", - Column("somecol", INTEGER()), - Column("somecol", MyType()), - ac, self.autogen_context - ) + autogenerate.compare._compare_type( + self.autogen_context, ac, + None, "sometable", "somecol", + Column("somecol", INTEGER()), + Column("somecol", MyType()), + ) + assert not ac.has_changes() ac = ops.AlterColumnOp("sometable", "somecol") - autogenerate.compare._compare_type(None, "sometable", "somecol", - Column("somecol", String()), - Column("somecol", MyType()), - ac, self.autogen_context - ) + autogenerate.compare._compare_type( + self.autogen_context, ac, + None, "sometable", "somecol", + Column("somecol", String()), + Column("somecol", MyType()), + ) diff = ac.to_diff_tuple() eq_( diff[0][0:4], @@ -453,10 +458,10 @@ class AutogenerateDiffTest(ModelOne, AutogenTest, TestBase): uo = ops.AlterColumnOp('sometable', 'somecol') autogenerate.compare._compare_type( + self.autogen_context, uo, None, "sometable", "somecol", Column("somecol", Integer, nullable=True), - Column("somecol", MyType()), - uo, self.autogen_context + Column("somecol", MyType()) ) assert not uo.has_changes() diff --git a/tests/test_postgresql.py b/tests/test_postgresql.py index 7fb514f..576d957 100644 --- a/tests/test_postgresql.py +++ b/tests/test_postgresql.py @@ -203,8 +203,10 @@ class PostgresqlDefaultCompareTest(TestBase): insp_col = Column("somecol", cols[0]['type'], server_default=text(cols[0]['default'])) op = ops.AlterColumnOp("test", "somecol") - _compare_server_default(None, "test", "somecol", insp_col, - t2.c.somecol, op, self.autogen_context) + _compare_server_default( + self.autogen_context, op, + None, "test", "somecol", insp_col, t2.c.somecol) + diffs = op.to_diff_tuple() eq_(bool(diffs), diff_expected) -- cgit v1.2.1