summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2015-07-16 19:00:14 -0400
committerMike Bayer <mike_mp@zzzcomputing.com>2015-07-16 19:00:14 -0400
commitceeac2a45563346b2b7741fb85d3c65828e52904 (patch)
tree9a437d10d61c957bc0cf7ebd718ec519991733b3
parent1e2848d6744188f9915ecd17028835e9e1ae86b1 (diff)
downloadalembic-ticket_306.tar.gz
- build out custom autogenerate compare hooksticket_306
- new documentation for autogenerate customization
-rw-r--r--alembic/autogenerate/__init__.py2
-rw-r--r--alembic/autogenerate/compare.py104
-rw-r--r--alembic/util/langhelpers.py11
-rw-r--r--docs/build/api/autogenerate.rst255
-rw-r--r--docs/build/api/operations.rst66
-rw-r--r--docs/build/changelog.rst11
-rw-r--r--tests/test_autogen_diffs.py49
-rw-r--r--tests/test_postgresql.py6
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)