diff options
author | Mike Bayer <mike_mp@zzzcomputing.com> | 2015-07-16 19:00:55 -0400 |
---|---|---|
committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2015-07-16 19:00:55 -0400 |
commit | c5be31760e4bc57b898d1d69d4bb0dd7c2dc7eb7 (patch) | |
tree | f158a8b62d7679176f3f60d1b1bf188a6383e03c /docs/build/api/autogenerate.rst | |
parent | 96214629cdb13f1694831f36c48a7ec86dd8c7f6 (diff) | |
download | alembic-c5be31760e4bc57b898d1d69d4bb0dd7c2dc7eb7.tar.gz |
- rework all of autogenerate to build directly on alembic.operations.ops
objects; the "diffs" is now a legacy system that is exported from
the ops. A new model of comparison/rendering/ upgrade/downgrade
composition that is cleaner and much more extensible is introduced.
- 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.
- write all new docs for the new system
fixes #306
Diffstat (limited to 'docs/build/api/autogenerate.rst')
-rw-r--r-- | docs/build/api/autogenerate.rst | 255 |
1 files changed, 230 insertions, 25 deletions
diff --git a/docs/build/api/autogenerate.rst b/docs/build/api/autogenerate.rst index b024ab1..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. + - # note: this is a continuation of the example from the - # "Operation Plugins" section +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: + +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,27 +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 ### + + 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. + |