From 0e43247da4cfd2d829ee4b350e336364cb8a7ec1 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Fri, 3 Jul 2015 13:10:41 -0400 Subject: [PATCH] - squash merge of ticket_302 branch - The internal system for Alembic operations has been reworked to now build upon an extensible system of operation objects. New operations can be added to the ``op.`` namespace, including that they are available in custom autogenerate schemes. fixes #302 - The internal system for autogenerate been reworked to build upon the extensible system of operation objects present in #302. A new customization hook process_revision_directives is added to allow manipulation of the autogen stream. Fixes #301 --- .gitignore | 1 + alembic/__init__.py | 8 +- alembic/autogenerate/__init__.py | 9 +- alembic/autogenerate/api.py | 339 +-- alembic/autogenerate/compare.py | 54 +- alembic/autogenerate/compose.py | 144 + alembic/autogenerate/generate.py | 92 + alembic/autogenerate/render.py | 669 +++-- alembic/command.py | 47 +- alembic/config.py | 11 +- alembic/context.py | 5 +- alembic/ddl/base.py | 68 +- alembic/ddl/impl.py | 75 +- alembic/ddl/mssql.py | 6 +- alembic/ddl/mysql.py | 9 +- alembic/ddl/postgresql.py | 2 +- alembic/op.py | 6 +- alembic/operations/__init__.py | 6 + alembic/operations/base.py | 442 +++ alembic/{ => operations}/batch.py | 4 +- alembic/{operations.py => operations/ops.py} | 2198 +++++++------- alembic/operations/schemaobj.py | 157 + alembic/operations/toimpl.py | 162 + alembic/runtime/__init__.py | 0 alembic/{ => runtime}/environment.py | 53 +- alembic/{ => runtime}/migration.py | 4 +- alembic/script/__init__.py | 3 + alembic/{script.py => script/base.py} | 6 +- alembic/{ => script}/revision.py | 5 +- alembic/testing/assertions.py | 4 +- alembic/testing/env.py | 6 +- alembic/testing/exclusions.py | 3 +- alembic/testing/fixtures.py | 13 +- alembic/testing/mock.py | 2 +- alembic/testing/provision.py | 6 +- alembic/util.py | 405 --- alembic/util/__init__.py | 20 + alembic/{ => util}/compat.py | 0 alembic/util/langhelpers.py | 275 ++ alembic/util/messaging.py | 94 + alembic/util/pyfiles.py | 80 + alembic/util/sqla_compat.py | 160 + docs/build/api.rst | 217 -- docs/build/api/api_overview.png | Bin 0 -> 123965 bytes docs/build/api/autogenerate.rst | 235 ++ docs/build/api/commands.rst | 38 + docs/build/api/config.rst | 26 + docs/build/api/ddl.rst | 56 + docs/build/api/environment.rst | 19 + docs/build/api/index.rst | 33 + docs/build/api/migration.rst | 8 + docs/build/api/operations.rst | 123 + docs/build/api/overview.rst | 47 + docs/build/api/script.rst | 20 + docs/build/api_overview.png | Bin 64697 -> 0 bytes docs/build/assets/api_overview.graffle | 2654 +++++++++++++---- docs/build/changelog.rst | 38 + docs/build/cookbook.rst | 2 +- docs/build/front.rst | 19 +- docs/build/index.rst | 4 +- docs/build/ops.rst | 18 +- tests/_autogen_fixtures.py | 251 ++ tests/test_autogen_composition.py | 328 ++ ..._autogenerate.py => test_autogen_diffs.py} | 665 +---- tests/test_autogen_fks.py | 4 +- tests/test_autogen_indexes.py | 2 +- tests/test_autogen_render.py | 277 +- tests/test_batch.py | 24 +- tests/test_config.py | 5 +- tests/test_op.py | 39 +- tests/test_revision.py | 2 +- tests/test_script_consumption.py | 3 +- tests/test_script_production.py | 177 +- 73 files changed, 7276 insertions(+), 3711 deletions(-) create mode 100644 alembic/autogenerate/compose.py create mode 100644 alembic/autogenerate/generate.py create mode 100644 alembic/operations/__init__.py create mode 100644 alembic/operations/base.py rename alembic/{ => operations}/batch.py (99%) rename alembic/{operations.py => operations/ops.py} (55%) create mode 100644 alembic/operations/schemaobj.py create mode 100644 alembic/operations/toimpl.py create mode 100644 alembic/runtime/__init__.py rename alembic/{ => runtime}/environment.py (94%) rename alembic/{ => runtime}/migration.py (99%) create mode 100644 alembic/script/__init__.py rename alembic/{script.py => script/base.py} (99%) rename alembic/{ => script}/revision.py (99%) delete mode 100644 alembic/util.py create mode 100644 alembic/util/__init__.py rename alembic/{ => util}/compat.py (100%) create mode 100644 alembic/util/langhelpers.py create mode 100644 alembic/util/messaging.py create mode 100644 alembic/util/pyfiles.py create mode 100644 alembic/util/sqla_compat.py delete mode 100644 docs/build/api.rst create mode 100644 docs/build/api/api_overview.png create mode 100644 docs/build/api/autogenerate.rst create mode 100644 docs/build/api/commands.rst create mode 100644 docs/build/api/config.rst create mode 100644 docs/build/api/ddl.rst create mode 100644 docs/build/api/environment.rst create mode 100644 docs/build/api/index.rst create mode 100644 docs/build/api/migration.rst create mode 100644 docs/build/api/operations.rst create mode 100644 docs/build/api/overview.rst create mode 100644 docs/build/api/script.rst delete mode 100644 docs/build/api_overview.png create mode 100644 tests/_autogen_fixtures.py create mode 100644 tests/test_autogen_composition.py rename tests/{test_autogenerate.py => test_autogen_diffs.py} (52%) diff --git a/.gitignore b/.gitignore index 0875618..5a97f5e 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ alembic.ini .coverage coverage.xml .tox +*.patch diff --git a/alembic/__init__.py b/alembic/__init__.py index f429441..345bf26 100644 --- a/alembic/__init__.py +++ b/alembic/__init__.py @@ -1,9 +1,15 @@ from os import path -__version__ = '0.7.7' +__version__ = '0.8.0' package_dir = path.abspath(path.dirname(__file__)) from . import op # noqa from . import context # noqa + +import sys +from .runtime import environment +from .runtime import migration +sys.modules['alembic.migration'] = migration +sys.modules['alembic.environment'] = environment diff --git a/alembic/autogenerate/__init__.py b/alembic/autogenerate/__init__.py index 2d75912..4272a7e 100644 --- a/alembic/autogenerate/__init__.py +++ b/alembic/autogenerate/__init__.py @@ -1,2 +1,7 @@ -from .api import compare_metadata, _produce_migration_diffs, \ - _produce_net_changes +from .api import ( # noqa + compare_metadata, _render_migration_diffs, + produce_migrations, render_python_code + ) +from .compare import _produce_net_changes # noqa +from .generate import RevisionContext # noqa +from .render import render_op_text, renderers # noqa \ No newline at end of file diff --git a/alembic/autogenerate/api.py b/alembic/autogenerate/api.py index 6281a6c..cff977b 100644 --- a/alembic/autogenerate/api.py +++ b/alembic/autogenerate/api.py @@ -1,26 +1,12 @@ """Provide the 'autogenerate' feature which can produce migration operations automatically.""" -import logging -import itertools -import re - -from ..compat import StringIO - -from mako.pygen import PythonPrinter -from sqlalchemy.engine.reflection import Inspector -from sqlalchemy.util import OrderedSet -from .compare import _compare_tables -from .render import _drop_table, _drop_column, _drop_index, _drop_constraint, \ - _add_table, _add_column, _add_index, _add_constraint, _modify_col, \ - _add_fk_constraint +from ..operations import ops +from . import render +from . import compare +from . import compose from .. import util -log = logging.getLogger(__name__) - -################################################### -# public - def compare_metadata(context, metadata): """Compare a database schema to that given in a @@ -105,9 +91,14 @@ def compare_metadata(context, metadata): :param metadata: a :class:`~sqlalchemy.schema.MetaData` instance. + .. seealso:: + + :func:`.produce_migrations` - produces a :class:`.MigrationScript` + structure based on metadata comparison. + """ - autogen_context, connection = _autogen_context(context, None) + autogen_context = _autogen_context(context, metadata=metadata) # as_sql=True is nonsensical here. autogenerate requires a connection # it can use to run queries against to get the database schema. @@ -118,76 +109,107 @@ def compare_metadata(context, metadata): diffs = [] - object_filters = _get_object_filters(context.opts) - include_schemas = context.opts.get('include_schemas', False) - - _produce_net_changes(connection, metadata, diffs, autogen_context, - object_filters, include_schemas) + compare._produce_net_changes(autogen_context, diffs) return diffs -################################################### -# top level +def produce_migrations(context, metadata): + """Produce a :class:`.MigrationScript` structure based on schema + comparison. -def _produce_migration_diffs(context, template_args, - imports, include_symbol=None, - include_object=None, - include_schemas=False): - opts = context.opts - metadata = opts['target_metadata'] - include_schemas = opts.get('include_schemas', include_schemas) + This function does essentially what :func:`.compare_metadata` does, + but then runs the resulting list of diffs to produce the full + :class:`.MigrationScript` object. For an example of what this looks like, + see the example in :ref:`customizing_revision`. - object_filters = _get_object_filters(opts, include_symbol, include_object) + .. versionadded:: 0.8.0 - if metadata is None: - raise util.CommandError( - "Can't proceed with --autogenerate option; environment " - "script %s does not provide " - "a MetaData object to the context." % ( - context.script.env_py_location - )) - autogen_context, connection = _autogen_context(context, imports) + .. seealso:: + :func:`.compare_metadata` - returns more fundamental "diff" + data from comparing a schema. + + """ + + autogen_context = _autogen_context(context, metadata=metadata) diffs = [] - _produce_net_changes(connection, metadata, diffs, - autogen_context, object_filters, include_schemas) - template_args[opts['upgrade_token']] = _indent(_render_cmd_body( - _produce_upgrade_commands, diffs, autogen_context)) - template_args[opts['downgrade_token']] = _indent(_render_cmd_body( - _produce_downgrade_commands, diffs, autogen_context)) - template_args['imports'] = "\n".join(sorted(imports)) + compare._produce_net_changes(autogen_context, diffs) - -def _indent(text): - text = re.compile(r'^', re.M).sub(" ", text).strip() - text = re.compile(r' +$', re.M).sub("", text) - return text - - -def _render_cmd_body(fn, diffs, autogen_context): - - buf = StringIO() - printer = PythonPrinter(buf) - - printer.writeline( - "### commands auto generated by Alembic - " - "please adjust! ###" + migration_script = ops.MigrationScript( + rev_id=None, + upgrade_ops=ops.UpgradeOps([]), + downgrade_ops=ops.DowngradeOps([]), ) - for line in fn(diffs, autogen_context): - printer.writeline(line) + compose._to_migration_script(autogen_context, migration_script, diffs) - printer.writeline("### end Alembic commands ###") - - return buf.getvalue() + return migration_script -def _get_object_filters( - context_opts, include_symbol=None, include_object=None): - include_symbol = context_opts.get('include_symbol', include_symbol) - include_object = context_opts.get('include_object', include_object) +def render_python_code( + up_or_down_op, + sqlalchemy_module_prefix='sa.', + alembic_module_prefix='op.', + imports=(), + render_item=None, +): + """Render Python code given an :class:`.UpgradeOps` or + :class:`.DowngradeOps` object. + + This is a convenience function that can be used to test the + autogenerate output of a user-defined :class:`.MigrationScript` structure. + + """ + autogen_context = { + 'opts': { + 'sqlalchemy_module_prefix': sqlalchemy_module_prefix, + 'alembic_module_prefix': alembic_module_prefix, + 'render_item': render_item, + }, + 'imports': set(imports) + } + return render._indent(render._render_cmd_body( + up_or_down_op, autogen_context)) + + + + +def _render_migration_diffs(context, template_args, imports): + """legacy, used by test_autogen_composition at the moment""" + + migration_script = produce_migrations(context, None) + + autogen_context = _autogen_context(context, imports=imports) + diffs = [] + + compare._produce_net_changes(autogen_context, diffs) + + migration_script = ops.MigrationScript( + rev_id=None, + imports=imports, + upgrade_ops=ops.UpgradeOps([]), + downgrade_ops=ops.DowngradeOps([]), + ) + + compose._to_migration_script(autogen_context, migration_script, diffs) + + render._render_migration_script( + autogen_context, migration_script, template_args + ) + + +def _autogen_context( + context, imports=None, metadata=None, include_symbol=None, + include_object=None, include_schemas=False): + + opts = context.opts + metadata = opts['target_metadata'] if metadata is None else metadata + include_schemas = opts.get('include_schemas', include_schemas) + + include_symbol = opts.get('include_symbol', include_symbol) + include_object = opts.get('include_object', include_object) object_filters = [] if include_symbol: @@ -200,171 +222,24 @@ def _get_object_filters( if include_object: object_filters.append(include_object) - return object_filters + if metadata is None: + raise util.CommandError( + "Can't proceed with --autogenerate option; environment " + "script %s does not provide " + "a MetaData object to the context." % ( + context.script.env_py_location + )) - -def _autogen_context(context, imports): opts = context.opts connection = context.bind return { - 'imports': imports, + 'imports': imports if imports is not None else set(), 'connection': connection, 'dialect': connection.dialect, 'context': context, - 'opts': opts - }, connection - - -################################################### -# walk structures - - -def _produce_net_changes(connection, metadata, diffs, autogen_context, - object_filters=(), - include_schemas=False): - inspector = Inspector.from_engine(connection) - conn_table_names = set() - - default_schema = connection.dialect.default_schema_name - if include_schemas: - schemas = set(inspector.get_schema_names()) - # replace default schema name with None - schemas.discard("information_schema") - # replace the "default" schema with None - schemas.add(None) - schemas.discard(default_schema) - else: - schemas = [None] - - version_table_schema = autogen_context['context'].version_table_schema - version_table = autogen_context['context'].version_table - - for s in schemas: - tables = set(inspector.get_table_names(schema=s)) - if s == version_table_schema: - tables = tables.difference( - [autogen_context['context'].version_table] - ) - conn_table_names.update(zip([s] * len(tables), tables)) - - metadata_table_names = OrderedSet( - [(table.schema, table.name) for table in metadata.sorted_tables] - ).difference([(version_table_schema, version_table)]) - - _compare_tables(conn_table_names, metadata_table_names, - object_filters, - inspector, metadata, diffs, autogen_context) - - -def _produce_upgrade_commands(diffs, autogen_context): - return _produce_commands("upgrade", diffs, autogen_context) - - -def _produce_downgrade_commands(diffs, autogen_context): - return _produce_commands("downgrade", diffs, autogen_context) - - -def _produce_commands(type_, diffs, autogen_context): - opts = autogen_context['opts'] - render_as_batch = opts.get('render_as_batch', False) - - if diffs: - if type_ == 'downgrade': - diffs = reversed(diffs) - for (schema, table), subdiffs in _group_diffs_by_table(diffs): - if table is not None and render_as_batch: - yield "with op.batch_alter_table"\ - "(%r, schema=%r) as batch_op:" % (table, schema) - autogen_context['batch_prefix'] = 'batch_op.' - for diff in subdiffs: - yield _invoke_command(type_, diff, autogen_context) - if table is not None and render_as_batch: - del autogen_context['batch_prefix'] - yield "" - else: - yield "pass" - - -def _invoke_command(updown, args, autogen_context): - if isinstance(args, tuple): - return _invoke_adddrop_command(updown, args, autogen_context) - else: - return _invoke_modify_command(updown, args, autogen_context) - - -def _invoke_adddrop_command(updown, args, autogen_context): - cmd_type = args[0] - adddrop, cmd_type = cmd_type.split("_") - - cmd_args = args[1:] + (autogen_context,) - - _commands = { - "table": (_drop_table, _add_table), - "column": (_drop_column, _add_column), - "index": (_drop_index, _add_index), - "constraint": (_drop_constraint, _add_constraint), - "fk": (_drop_constraint, _add_fk_constraint) + 'opts': opts, + 'metadata': metadata, + 'object_filters': object_filters, + 'include_schemas': include_schemas } - cmd_callables = _commands[cmd_type] - - if ( - updown == "upgrade" and adddrop == "add" - ) or ( - updown == "downgrade" and adddrop == "remove" - ): - return cmd_callables[1](*cmd_args) - else: - return cmd_callables[0](*cmd_args) - - -def _invoke_modify_command(updown, args, autogen_context): - sname, tname, cname = args[0][1:4] - kw = {} - - _arg_struct = { - "modify_type": ("existing_type", "type_"), - "modify_nullable": ("existing_nullable", "nullable"), - "modify_default": ("existing_server_default", "server_default"), - } - for diff in args: - diff_kw = diff[4] - for arg in ("existing_type", - "existing_nullable", - "existing_server_default"): - if arg in diff_kw: - kw.setdefault(arg, diff_kw[arg]) - old_kw, new_kw = _arg_struct[diff[0]] - if updown == "upgrade": - kw[new_kw] = diff[-1] - kw[old_kw] = diff[-2] - else: - kw[new_kw] = diff[-2] - kw[old_kw] = diff[-1] - - if "nullable" in kw: - kw.pop("existing_nullable", None) - if "server_default" in kw: - kw.pop("existing_server_default", None) - return _modify_col(tname, cname, autogen_context, schema=sname, **kw) - - -def _group_diffs_by_table(diffs): - _adddrop = { - "table": lambda diff: (None, None), - "column": lambda diff: (diff[0], diff[1]), - "index": lambda diff: (diff[0].table.schema, diff[0].table.name), - "constraint": lambda diff: (diff[0].table.schema, diff[0].table.name), - "fk": lambda diff: (diff[0].parent.schema, diff[0].parent.name) - } - - def _derive_table(diff): - if isinstance(diff, tuple): - cmd_type = diff[0] - adddrop, cmd_type = cmd_type.split("_") - return _adddrop[cmd_type](diff[1:]) - else: - sname, tname = diff[0][1:3] - return sname, tname - - return itertools.groupby(diffs, _derive_table) diff --git a/alembic/autogenerate/compare.py b/alembic/autogenerate/compare.py index 2aae962..cd6b696 100644 --- a/alembic/autogenerate/compare.py +++ b/alembic/autogenerate/compare.py @@ -1,7 +1,9 @@ from sqlalchemy import schema as sa_schema, types as sqltypes +from sqlalchemy.engine.reflection import Inspector from sqlalchemy import event import logging -from .. import compat +from ..util import compat +from ..util import sqla_compat from sqlalchemy.util import OrderedSet import re from .render import _user_defined_render @@ -11,6 +13,47 @@ from alembic.ddl.base import _fk_spec log = logging.getLogger(__name__) +def _produce_net_changes(autogen_context, diffs): + + metadata = autogen_context['metadata'] + connection = autogen_context['connection'] + object_filters = autogen_context.get('object_filters', ()) + include_schemas = autogen_context.get('include_schemas', False) + + inspector = Inspector.from_engine(connection) + conn_table_names = set() + + default_schema = connection.dialect.default_schema_name + if include_schemas: + schemas = set(inspector.get_schema_names()) + # replace default schema name with None + schemas.discard("information_schema") + # replace the "default" schema with None + schemas.add(None) + schemas.discard(default_schema) + else: + schemas = [None] + + version_table_schema = autogen_context['context'].version_table_schema + version_table = autogen_context['context'].version_table + + for s in schemas: + tables = set(inspector.get_table_names(schema=s)) + if s == version_table_schema: + tables = tables.difference( + [autogen_context['context'].version_table] + ) + conn_table_names.update(zip([s] * len(tables), tables)) + + metadata_table_names = OrderedSet( + [(table.schema, table.name) for table in metadata.sorted_tables] + ).difference([(version_table_schema, version_table)]) + + _compare_tables(conn_table_names, metadata_table_names, + object_filters, + inspector, metadata, diffs, autogen_context) + + def _run_filters(object_, name, type_, reflected, compare_to, object_filters): for fn in object_filters: if not fn(object_, name, type_, reflected, compare_to): @@ -250,7 +293,7 @@ class _ix_constraint_sig(_constraint_sig): @property def column_names(self): - return _get_index_column_names(self.const) + return sqla_compat._get_index_column_names(self.const) class _fk_constraint_sig(_constraint_sig): @@ -267,13 +310,6 @@ class _fk_constraint_sig(_constraint_sig): ) -def _get_index_column_names(idx): - if compat.sqla_08: - return [getattr(exp, "name", None) for exp in idx.expressions] - else: - return [getattr(col, "name", None) for col in idx.columns] - - def _compare_indexes_and_uniques(schema, tname, object_filters, conn_table, metadata_table, diffs, autogen_context, inspector): diff --git a/alembic/autogenerate/compose.py b/alembic/autogenerate/compose.py new file mode 100644 index 0000000..b42b505 --- /dev/null +++ b/alembic/autogenerate/compose.py @@ -0,0 +1,144 @@ +import itertools +from ..operations import ops + + +def _to_migration_script(autogen_context, migration_script, diffs): + _to_upgrade_op( + autogen_context, + diffs, + migration_script.upgrade_ops, + ) + + _to_downgrade_op( + autogen_context, + diffs, + migration_script.downgrade_ops, + ) + + +def _to_upgrade_op(autogen_context, diffs, upgrade_ops): + return _to_updown_op(autogen_context, diffs, upgrade_ops, "upgrade") + + +def _to_downgrade_op(autogen_context, diffs, downgrade_ops): + return _to_updown_op(autogen_context, diffs, downgrade_ops, "downgrade") + + +def _to_updown_op(autogen_context, diffs, op_container, type_): + if not diffs: + return + + if type_ == 'downgrade': + diffs = reversed(diffs) + + dest = [op_container.ops] + + for (schema, tablename), subdiffs in _group_diffs_by_table(diffs): + subdiffs = list(subdiffs) + if tablename is not None: + table_ops = [] + op = ops.ModifyTableOps(tablename, table_ops, schema=schema) + dest[-1].append(op) + dest.append(table_ops) + for diff in subdiffs: + _produce_command(autogen_context, diff, dest[-1], type_) + if tablename is not None: + dest.pop(-1) + + +def _produce_command(autogen_context, diff, op_list, updown): + if isinstance(diff, tuple): + _produce_adddrop_command(updown, diff, op_list, autogen_context) + else: + _produce_modify_command(updown, diff, op_list, autogen_context) + + +def _produce_adddrop_command(updown, diff, op_list, autogen_context): + cmd_type = diff[0] + adddrop, cmd_type = cmd_type.split("_") + + cmd_args = diff[1:] + + _commands = { + "table": (ops.DropTableOp.from_table, ops.CreateTableOp.from_table), + "column": ( + ops.DropColumnOp.from_column_and_tablename, + ops.AddColumnOp.from_column_and_tablename), + "index": (ops.DropIndexOp.from_index, ops.CreateIndexOp.from_index), + "constraint": ( + ops.DropConstraintOp.from_constraint, + ops.AddConstraintOp.from_constraint), + "fk": ( + ops.DropConstraintOp.from_constraint, + ops.CreateForeignKeyOp.from_constraint) + } + + cmd_callables = _commands[cmd_type] + + if ( + updown == "upgrade" and adddrop == "add" + ) or ( + updown == "downgrade" and adddrop == "remove" + ): + op_list.append(cmd_callables[1](*cmd_args)) + else: + op_list.append(cmd_callables[0](*cmd_args)) + + +def _produce_modify_command(updown, diffs, op_list, autogen_context): + sname, tname, cname = diffs[0][1:4] + kw = {} + + _arg_struct = { + "modify_type": ("existing_type", "modify_type"), + "modify_nullable": ("existing_nullable", "modify_nullable"), + "modify_default": ("existing_server_default", "modify_server_default"), + } + for diff in diffs: + diff_kw = diff[4] + for arg in ("existing_type", + "existing_nullable", + "existing_server_default"): + if arg in diff_kw: + kw.setdefault(arg, diff_kw[arg]) + old_kw, new_kw = _arg_struct[diff[0]] + if updown == "upgrade": + kw[new_kw] = diff[-1] + kw[old_kw] = diff[-2] + else: + kw[new_kw] = diff[-2] + kw[old_kw] = diff[-1] + + if "modify_nullable" in kw: + kw.pop("existing_nullable", None) + if "modify_server_default" in kw: + kw.pop("existing_server_default", None) + + op_list.append( + ops.AlterColumnOp( + tname, cname, schema=sname, + **kw + ) + ) + + +def _group_diffs_by_table(diffs): + _adddrop = { + "table": lambda diff: (None, None), + "column": lambda diff: (diff[0], diff[1]), + "index": lambda diff: (diff[0].table.schema, diff[0].table.name), + "constraint": lambda diff: (diff[0].table.schema, diff[0].table.name), + "fk": lambda diff: (diff[0].parent.schema, diff[0].parent.name) + } + + def _derive_table(diff): + if isinstance(diff, tuple): + cmd_type = diff[0] + adddrop, cmd_type = cmd_type.split("_") + return _adddrop[cmd_type](diff[1:]) + else: + sname, tname = diff[0][1:3] + return sname, tname + + return itertools.groupby(diffs, _derive_table) + diff --git a/alembic/autogenerate/generate.py b/alembic/autogenerate/generate.py new file mode 100644 index 0000000..c686156 --- /dev/null +++ b/alembic/autogenerate/generate.py @@ -0,0 +1,92 @@ +from .. import util +from . import api +from . import compose +from . import compare +from . import render +from ..operations import ops + + +class RevisionContext(object): + def __init__(self, config, script_directory, command_args): + self.config = config + self.script_directory = script_directory + self.command_args = command_args + self.template_args = { + 'config': config # Let templates use config for + # e.g. multiple databases + } + self.generated_revisions = [ + self._default_revision() + ] + + def _to_script(self, migration_script): + template_args = {} + for k, v in self.template_args.items(): + template_args.setdefault(k, v) + + if migration_script._autogen_context is not None: + render._render_migration_script( + migration_script._autogen_context, migration_script, + template_args + ) + + return self.script_directory.generate_revision( + migration_script.rev_id, + migration_script.message, + refresh=True, + head=migration_script.head, + splice=migration_script.splice, + branch_labels=migration_script.branch_label, + version_path=migration_script.version_path, + **template_args) + + def run_autogenerate(self, rev, context): + if self.command_args['sql']: + raise util.CommandError( + "Using --sql with --autogenerate does not make any sense") + if set(self.script_directory.get_revisions(rev)) != \ + set(self.script_directory.get_revisions("heads")): + raise util.CommandError("Target database is not up to date.") + + autogen_context = api._autogen_context(context) + + diffs = [] + compare._produce_net_changes(autogen_context, diffs) + + migration_script = self.generated_revisions[0] + + compose._to_migration_script(autogen_context, migration_script, diffs) + + hook = context.opts.get('process_revision_directives', None) + if hook: + hook(context, rev, self.generated_revisions) + + for migration_script in self.generated_revisions: + migration_script._autogen_context = autogen_context + + def run_no_autogenerate(self, rev, context): + hook = context.opts.get('process_revision_directives', None) + if hook: + hook(context, rev, self.generated_revisions) + + for migration_script in self.generated_revisions: + migration_script._autogen_context = None + + def _default_revision(self): + op = ops.MigrationScript( + rev_id=self.command_args['rev_id'] or util.rev_id(), + message=self.command_args['message'], + imports=set(), + upgrade_ops=ops.UpgradeOps([]), + downgrade_ops=ops.DowngradeOps([]), + head=self.command_args['head'], + splice=self.command_args['splice'], + branch_label=self.command_args['branch_label'], + version_path=self.command_args['version_path'] + ) + op._autogen_context = None + return op + + def generate_scripts(self): + for generated_revision in self.generated_revisions: + yield self._to_script(generated_revision) diff --git a/alembic/autogenerate/render.py b/alembic/autogenerate/render.py index 5007652..c3f3df1 100644 --- a/alembic/autogenerate/render.py +++ b/alembic/autogenerate/render.py @@ -1,11 +1,12 @@ from sqlalchemy import schema as sa_schema, types as sqltypes, sql -import logging -from .. import compat -from ..ddl.base import _table_for_constraint, _fk_spec +from ..operations import ops +from ..util import compat import re -from ..compat import string_types +from ..util.compat import string_types +from .. import util +from mako.pygen import PythonPrinter +from ..util.compat import StringIO -log = logging.getLogger(__name__) MAX_PYTHON_ARGS = 255 @@ -22,6 +23,341 @@ except ImportError: return name +def _indent(text): + text = re.compile(r'^', re.M).sub(" ", text).strip() + text = re.compile(r' +$', re.M).sub("", text) + return text + + +def _render_migration_script(autogen_context, migration_script, template_args): + opts = autogen_context['opts'] + imports = autogen_context['imports'] + template_args[opts['upgrade_token']] = _indent(_render_cmd_body( + migration_script.upgrade_ops, autogen_context)) + template_args[opts['downgrade_token']] = _indent(_render_cmd_body( + migration_script.downgrade_ops, autogen_context)) + template_args['imports'] = "\n".join(sorted(imports)) + + +default_renderers = renderers = util.Dispatcher() + + +def _render_cmd_body(op_container, autogen_context): + + buf = StringIO() + printer = PythonPrinter(buf) + + printer.writeline( + "### commands auto generated by Alembic - " + "please adjust! ###" + ) + + if not op_container.ops: + printer.writeline("pass") + else: + for op in op_container.ops: + lines = render_op(autogen_context, op) + + for line in lines: + printer.writeline(line) + + printer.writeline("### end Alembic commands ###") + + return buf.getvalue() + + +def render_op(autogen_context, op): + renderer = renderers.dispatch(op) + lines = util.to_list(renderer(autogen_context, op)) + return lines + + +def render_op_text(autogen_context, op): + return "\n".join(render_op(autogen_context, op)) + + +@renderers.dispatch_for(ops.ModifyTableOps) +def _render_modify_table(autogen_context, op): + opts = autogen_context['opts'] + render_as_batch = opts.get('render_as_batch', False) + + if op.ops: + lines = [] + if render_as_batch: + lines.append( + "with op.batch_alter_table(%r, schema=%r) as batch_op:" + % (op.table_name, op.schema) + ) + autogen_context['batch_prefix'] = 'batch_op.' + for t_op in op.ops: + t_lines = render_op(autogen_context, t_op) + lines.extend(t_lines) + if render_as_batch: + del autogen_context['batch_prefix'] + lines.append("") + return lines + else: + return [ + "pass" + ] + + +@renderers.dispatch_for(ops.CreateTableOp) +def _add_table(autogen_context, op): + table = op.to_table() + + args = [col for col in + [_render_column(col, autogen_context) for col in table.columns] + if col] + \ + sorted([rcons for rcons in + [_render_constraint(cons, autogen_context) for cons in + table.constraints] + if rcons is not None + ]) + + if len(args) > MAX_PYTHON_ARGS: + args = '*[' + ',\n'.join(args) + ']' + else: + args = ',\n'.join(args) + + text = "%(prefix)screate_table(%(tablename)r,\n%(args)s" % { + 'tablename': _ident(op.table_name), + 'prefix': _alembic_autogenerate_prefix(autogen_context), + 'args': args, + } + if op.schema: + text += ",\nschema=%r" % _ident(op.schema) + for k in sorted(op.kw): + text += ",\n%s=%r" % (k.replace(" ", "_"), op.kw[k]) + text += "\n)" + return text + + +@renderers.dispatch_for(ops.DropTableOp) +def _drop_table(autogen_context, op): + text = "%(prefix)sdrop_table(%(tname)r" % { + "prefix": _alembic_autogenerate_prefix(autogen_context), + "tname": _ident(op.table_name) + } + if op.schema: + text += ", schema=%r" % _ident(op.schema) + text += ")" + return text + + +@renderers.dispatch_for(ops.CreateIndexOp) +def _add_index(autogen_context, op): + index = op.to_index() + + has_batch = 'batch_prefix' in autogen_context + + if has_batch: + tmpl = "%(prefix)screate_index(%(name)r, [%(columns)s], "\ + "unique=%(unique)r%(kwargs)s)" + else: + tmpl = "%(prefix)screate_index(%(name)r, %(table)r, [%(columns)s], "\ + "unique=%(unique)r%(schema)s%(kwargs)s)" + + text = tmpl % { + 'prefix': _alembic_autogenerate_prefix(autogen_context), + 'name': _render_gen_name(autogen_context, index.name), + 'table': _ident(index.table.name), + 'columns': ", ".join( + _get_index_rendered_expressions(index, autogen_context)), + 'unique': index.unique or False, + 'schema': (", schema=%r" % _ident(index.table.schema)) + if index.table.schema else '', + 'kwargs': ( + ', ' + + ', '.join( + ["%s=%s" % + (key, _render_potential_expr(val, autogen_context)) + for key, val in index.kwargs.items()])) + if len(index.kwargs) else '' + } + return text + + +@renderers.dispatch_for(ops.DropIndexOp) +def _drop_index(autogen_context, op): + has_batch = 'batch_prefix' in autogen_context + + if has_batch: + tmpl = "%(prefix)sdrop_index(%(name)r)" + else: + tmpl = "%(prefix)sdrop_index(%(name)r, "\ + "table_name=%(table_name)r%(schema)s)" + + text = tmpl % { + 'prefix': _alembic_autogenerate_prefix(autogen_context), + 'name': _render_gen_name(autogen_context, op.index_name), + 'table_name': _ident(op.table_name), + 'schema': ((", schema=%r" % _ident(op.schema)) + if op.schema else '') + } + return text + + +@renderers.dispatch_for(ops.CreateUniqueConstraintOp) +def _add_unique_constraint(autogen_context, op): + return [_uq_constraint(op.to_constraint(), autogen_context, True)] + + +@renderers.dispatch_for(ops.CreateForeignKeyOp) +def _add_fk_constraint(autogen_context, op): + + args = [ + repr( + _render_gen_name(autogen_context, op.constraint_name)), + repr(_ident(op.source_table)), + repr(_ident(op.referent_table)), + repr([_ident(col) for col in op.local_cols]), + repr([_ident(col) for col in op.remote_cols]) + ] + + for k in ( + 'source_schema', 'referent_schema', + 'onupdate', 'ondelete', 'initially', 'deferrable', 'use_alter' + ): + if k in op.kw: + value = op.kw[k] + if value is not None: + args.append("%s=%r" % (k, value)) + + return "%(prefix)screate_foreign_key(%(args)s)" % { + 'prefix': _alembic_autogenerate_prefix(autogen_context), + 'args': ", ".join(args) + } + + +@renderers.dispatch_for(ops.CreatePrimaryKeyOp) +def _add_pk_constraint(constraint, autogen_context): + raise NotImplementedError() + + +@renderers.dispatch_for(ops.CreateCheckConstraintOp) +def _add_check_constraint(constraint, autogen_context): + raise NotImplementedError() + + +@renderers.dispatch_for(ops.DropConstraintOp) +def _drop_constraint(autogen_context, op): + + if 'batch_prefix' in autogen_context: + template = "%(prefix)sdrop_constraint"\ + "(%(name)r, type_=%(type)r)" + else: + template = "%(prefix)sdrop_constraint"\ + "(%(name)r, '%(table_name)s'%(schema)s, type_=%(type)r)" + + text = template % { + 'prefix': _alembic_autogenerate_prefix(autogen_context), + 'name': _render_gen_name( + autogen_context, op.constraint_name), + 'table_name': _ident(op.table_name), + 'type': op.constraint_type, + 'schema': (", schema='%s'" % _ident(op.schema)) + if op.schema else '', + } + return text + + +@renderers.dispatch_for(ops.AddColumnOp) +def _add_column(autogen_context, op): + + schema, tname, column = op.schema, op.table_name, op.column + if 'batch_prefix' in autogen_context: + template = "%(prefix)sadd_column(%(column)s)" + else: + template = "%(prefix)sadd_column(%(tname)r, %(column)s" + if schema: + template += ", schema=%(schema)r" + template += ")" + text = template % { + "prefix": _alembic_autogenerate_prefix(autogen_context), + "tname": tname, + "column": _render_column(column, autogen_context), + "schema": schema + } + return text + + +@renderers.dispatch_for(ops.DropColumnOp) +def _drop_column(autogen_context, op): + + schema, tname, column_name = op.schema, op.table_name, op.column_name + + if 'batch_prefix' in autogen_context: + template = "%(prefix)sdrop_column(%(cname)r)" + else: + template = "%(prefix)sdrop_column(%(tname)r, %(cname)r" + if schema: + template += ", schema=%(schema)r" + template += ")" + + text = template % { + "prefix": _alembic_autogenerate_prefix(autogen_context), + "tname": _ident(tname), + "cname": _ident(column_name), + "schema": _ident(schema) + } + return text + + +@renderers.dispatch_for(ops.AlterColumnOp) +def _alter_column(autogen_context, op): + + tname = op.table_name + cname = op.column_name + server_default = op.modify_server_default + type_ = op.modify_type + nullable = op.modify_nullable + existing_type = op.existing_type + existing_nullable = op.existing_nullable + existing_server_default = op.existing_server_default + schema = op.schema + + indent = " " * 11 + + if 'batch_prefix' in autogen_context: + template = "%(prefix)salter_column(%(cname)r" + else: + template = "%(prefix)salter_column(%(tname)r, %(cname)r" + + text = template % { + 'prefix': _alembic_autogenerate_prefix( + autogen_context), + 'tname': tname, + 'cname': cname} + text += ",\n%sexisting_type=%s" % ( + indent, + _repr_type(existing_type, autogen_context)) + if server_default is not False: + rendered = _render_server_default( + server_default, autogen_context) + text += ",\n%sserver_default=%s" % (indent, rendered) + + if type_ is not None: + text += ",\n%stype_=%s" % (indent, + _repr_type(type_, autogen_context)) + if nullable is not None: + text += ",\n%snullable=%r" % ( + indent, nullable,) + if existing_nullable is not None: + text += ",\n%sexisting_nullable=%r" % ( + indent, existing_nullable) + if existing_server_default: + rendered = _render_server_default( + existing_server_default, + autogen_context) + text += ",\n%sexisting_server_default=%s" % ( + indent, rendered) + if schema and "batch_prefix" not in autogen_context: + text += ",\n%sschema=%r" % (indent, schema) + text += ")" + return text + + class _f_name(object): def __init__(self, prefix, name): @@ -82,45 +418,6 @@ def _render_potential_expr(value, autogen_context, wrap_in_text=True): return repr(value) -def _add_table(table, autogen_context): - args = [col for col in - [_render_column(col, autogen_context) for col in table.c] - if col] + \ - sorted([rcons for rcons in - [_render_constraint(cons, autogen_context) for cons in - table.constraints] - if rcons is not None - ]) - - if len(args) > MAX_PYTHON_ARGS: - args = '*[' + ',\n'.join(args) + ']' - else: - args = ',\n'.join(args) - - text = "%(prefix)screate_table(%(tablename)r,\n%(args)s" % { - 'tablename': _ident(table.name), - 'prefix': _alembic_autogenerate_prefix(autogen_context), - 'args': args, - } - if table.schema: - text += ",\nschema=%r" % _ident(table.schema) - for k in sorted(table.kwargs): - text += ",\n%s=%r" % (k.replace(" ", "_"), table.kwargs[k]) - text += "\n)" - return text - - -def _drop_table(table, autogen_context): - text = "%(prefix)sdrop_table(%(tname)r" % { - "prefix": _alembic_autogenerate_prefix(autogen_context), - "tname": _ident(table.name) - } - if table.schema: - text += ", schema=%r" % _ident(table.schema) - text += ")" - return text - - def _get_index_rendered_expressions(idx, autogen_context): if compat.sqla_08: return [repr(_ident(getattr(exp, "name", None))) @@ -132,80 +429,6 @@ def _get_index_rendered_expressions(idx, autogen_context): repr(_ident(getattr(col, "name", None))) for col in idx.columns] -def _add_index(index, autogen_context): - """ - Generate Alembic operations for the CREATE INDEX of an - :class:`~sqlalchemy.schema.Index` instance. - """ - - has_batch = 'batch_prefix' in autogen_context - - if has_batch: - tmpl = "%(prefix)screate_index(%(name)r, [%(columns)s], "\ - "unique=%(unique)r%(kwargs)s)" - else: - tmpl = "%(prefix)screate_index(%(name)r, %(table)r, [%(columns)s], "\ - "unique=%(unique)r%(schema)s%(kwargs)s)" - - text = tmpl % { - 'prefix': _alembic_autogenerate_prefix(autogen_context), - 'name': _render_gen_name(autogen_context, index.name), - 'table': _ident(index.table.name), - 'columns': ", ".join( - _get_index_rendered_expressions(index, autogen_context)), - 'unique': index.unique or False, - 'schema': (", schema=%r" % _ident(index.table.schema)) - if index.table.schema else '', - 'kwargs': ( - ', ' + - ', '.join( - ["%s=%s" % - (key, _render_potential_expr(val, autogen_context)) - for key, val in index.kwargs.items()])) - if len(index.kwargs) else '' - } - return text - - -def _drop_index(index, autogen_context): - """ - Generate Alembic operations for the DROP INDEX of an - :class:`~sqlalchemy.schema.Index` instance. - """ - has_batch = 'batch_prefix' in autogen_context - - if has_batch: - tmpl = "%(prefix)sdrop_index(%(name)r)" - else: - tmpl = "%(prefix)sdrop_index(%(name)r, "\ - "table_name=%(table_name)r%(schema)s)" - - text = tmpl % { - 'prefix': _alembic_autogenerate_prefix(autogen_context), - 'name': _render_gen_name(autogen_context, index.name), - 'table_name': _ident(index.table.name), - 'schema': ((", schema=%r" % _ident(index.table.schema)) - if index.table.schema else '') - } - return text - - -def _render_unique_constraint(constraint, autogen_context): - rendered = _user_defined_render("unique", constraint, autogen_context) - if rendered is not False: - return rendered - - return _uq_constraint(constraint, autogen_context, False) - - -def _add_unique_constraint(constraint, autogen_context): - """ - Generate Alembic operations for the ALTER TABLE .. ADD CONSTRAINT ... - UNIQUE of a :class:`~sqlalchemy.schema.UniqueConstraint` instance. - """ - return _uq_constraint(constraint, autogen_context, True) - - def _uq_constraint(constraint, autogen_context, alter): opts = [] @@ -224,7 +447,8 @@ def _uq_constraint(constraint, autogen_context, alter): if alter: args = [ - repr(_render_gen_name(autogen_context, constraint.name))] + repr(_render_gen_name( + autogen_context, constraint.name))] if not has_batch: args += [repr(_ident(constraint.table.name))] args.append(repr([_ident(col.name) for col in constraint.columns])) @@ -242,177 +466,6 @@ def _uq_constraint(constraint, autogen_context, alter): } -def _add_fk_constraint(constraint, autogen_context): - source_schema, source_table, \ - source_columns, target_schema, \ - target_table, target_columns = _fk_spec(constraint) - - args = [ - repr(_render_gen_name(autogen_context, constraint.name)), - repr(_ident(source_table)), - repr(_ident(target_table)), - repr([_ident(col) for col in source_columns]), - repr([_ident(col) for col in target_columns]) - ] - if source_schema: - args.append( - "%s=%r" % ('source_schema', source_schema), - ) - if target_schema: - args.append( - "%s=%r" % ('referent_schema', target_schema) - ) - - opts = [] - _populate_render_fk_opts(constraint, opts) - args.extend(("%s=%s" % (k, v) for (k, v) in opts)) - - return "%(prefix)screate_foreign_key(%(args)s)" % { - 'prefix': _alembic_autogenerate_prefix(autogen_context), - 'args': ", ".join(args) - } - - -def _add_pk_constraint(constraint, autogen_context): - raise NotImplementedError() - - -def _add_check_constraint(constraint, autogen_context): - raise NotImplementedError() - - -def _add_constraint(constraint, autogen_context): - """ - Dispatcher for the different types of constraints. - """ - funcs = { - "unique_constraint": _add_unique_constraint, - "foreign_key_constraint": _add_fk_constraint, - "primary_key_constraint": _add_pk_constraint, - "check_constraint": _add_check_constraint, - "column_check_constraint": _add_check_constraint, - } - return funcs[constraint.__visit_name__](constraint, autogen_context) - - -def _drop_constraint(constraint, autogen_context): - """ - Generate Alembic operations for the ALTER TABLE ... DROP CONSTRAINT - of a :class:`~sqlalchemy.schema.UniqueConstraint` instance. - """ - - types = { - "unique_constraint": "unique", - "foreign_key_constraint": "foreignkey", - "primary_key_constraint": "primary", - "check_constraint": "check", - "column_check_constraint": "check", - } - - if 'batch_prefix' in autogen_context: - template = "%(prefix)sdrop_constraint"\ - "(%(name)r, type_=%(type)r)" - else: - template = "%(prefix)sdrop_constraint"\ - "(%(name)r, '%(table_name)s'%(schema)s, type_=%(type)r)" - - constraint_table = _table_for_constraint(constraint) - text = template % { - 'prefix': _alembic_autogenerate_prefix(autogen_context), - 'name': _render_gen_name(autogen_context, constraint.name), - 'table_name': _ident(constraint_table.name), - 'type': types[constraint.__visit_name__], - 'schema': (", schema='%s'" % _ident(constraint_table.schema)) - if constraint_table.schema else '', - } - return text - - -def _add_column(schema, tname, column, autogen_context): - if 'batch_prefix' in autogen_context: - template = "%(prefix)sadd_column(%(column)s)" - else: - template = "%(prefix)sadd_column(%(tname)r, %(column)s" - if schema: - template += ", schema=%(schema)r" - template += ")" - text = template % { - "prefix": _alembic_autogenerate_prefix(autogen_context), - "tname": tname, - "column": _render_column(column, autogen_context), - "schema": schema - } - return text - - -def _drop_column(schema, tname, column, autogen_context): - if 'batch_prefix' in autogen_context: - template = "%(prefix)sdrop_column(%(cname)r)" - else: - template = "%(prefix)sdrop_column(%(tname)r, %(cname)r" - if schema: - template += ", schema=%(schema)r" - template += ")" - - text = template % { - "prefix": _alembic_autogenerate_prefix(autogen_context), - "tname": _ident(tname), - "cname": _ident(column.name), - "schema": _ident(schema) - } - return text - - -def _modify_col(tname, cname, - autogen_context, - server_default=False, - type_=None, - nullable=None, - existing_type=None, - existing_nullable=None, - existing_server_default=False, - schema=None): - indent = " " * 11 - - if 'batch_prefix' in autogen_context: - template = "%(prefix)salter_column(%(cname)r" - else: - template = "%(prefix)salter_column(%(tname)r, %(cname)r" - - text = template % { - 'prefix': _alembic_autogenerate_prefix( - autogen_context), - 'tname': tname, - 'cname': cname} - text += ",\n%sexisting_type=%s" % ( - indent, - _repr_type(existing_type, autogen_context)) - if server_default is not False: - rendered = _render_server_default( - server_default, autogen_context) - text += ",\n%sserver_default=%s" % (indent, rendered) - - if type_ is not None: - text += ",\n%stype_=%s" % (indent, - _repr_type(type_, autogen_context)) - if nullable is not None: - text += ",\n%snullable=%r" % ( - indent, nullable,) - if existing_nullable is not None: - text += ",\n%sexisting_nullable=%r" % ( - indent, existing_nullable) - if existing_server_default: - rendered = _render_server_default( - existing_server_default, - autogen_context) - text += ",\n%sexisting_server_default=%s" % ( - indent, rendered) - if schema and "batch_prefix" not in autogen_context: - text += ",\n%sschema=%r" % (indent, schema) - text += ")" - return text - - def _user_autogenerate_prefix(autogen_context, target): prefix = autogen_context['opts']['user_module_prefix'] if prefix is None: @@ -508,14 +561,15 @@ def _repr_type(type_, autogen_context): return "%s%r" % (prefix, type_) +_constraint_renderers = util.Dispatcher() + + def _render_constraint(constraint, autogen_context): - renderer = _constraint_renderers.get(type(constraint), None) - if renderer: - return renderer(constraint, autogen_context) - else: - return None + renderer = _constraint_renderers.dispatch(constraint) + return renderer(constraint, autogen_context) +@_constraint_renderers.dispatch_for(sa_schema.PrimaryKeyConstraint) def _render_primary_key(constraint, autogen_context): rendered = _user_defined_render("primary_key", constraint, autogen_context) if rendered is not False: @@ -555,7 +609,8 @@ def _fk_colspec(fk, metadata_schema): # try to resolve the remote table and adjust for column.key parent_metadata = fk.parent.table.metadata if table_fullname in parent_metadata.tables: - colname = _ident(parent_metadata.tables[table_fullname].c[colname].name) + colname = _ident( + parent_metadata.tables[table_fullname].c[colname].name) colspec = "%s.%s" % (table_fullname, colname) @@ -576,6 +631,7 @@ def _populate_render_fk_opts(constraint, opts): opts.append(("use_alter", repr(constraint.use_alter))) +@_constraint_renderers.dispatch_for(sa_schema.ForeignKeyConstraint) def _render_foreign_key(constraint, autogen_context): rendered = _user_defined_render("foreign_key", constraint, autogen_context) if rendered is not False: @@ -602,6 +658,16 @@ def _render_foreign_key(constraint, autogen_context): } +@_constraint_renderers.dispatch_for(sa_schema.UniqueConstraint) +def _render_unique_constraint(constraint, autogen_context): + rendered = _user_defined_render("unique", constraint, autogen_context) + if rendered is not False: + return rendered + + return _uq_constraint(constraint, autogen_context, False) + + +@_constraint_renderers.dispatch_for(sa_schema.CheckConstraint) def _render_check_constraint(constraint, autogen_context): rendered = _user_defined_render("check", constraint, autogen_context) if rendered is not False: @@ -622,7 +688,8 @@ def _render_check_constraint(constraint, autogen_context): ( "name", repr( - _render_gen_name(autogen_context, constraint.name)) + _render_gen_name( + autogen_context, constraint.name)) ) ) return "%(prefix)sCheckConstraint(%(sqltext)s%(opts)s)" % { @@ -633,9 +700,5 @@ def _render_check_constraint(constraint, autogen_context): constraint.sqltext, autogen_context, wrap_in_text=False) } -_constraint_renderers = { - sa_schema.PrimaryKeyConstraint: _render_primary_key, - sa_schema.ForeignKeyConstraint: _render_foreign_key, - sa_schema.UniqueConstraint: _render_unique_constraint, - sa_schema.CheckConstraint: _render_check_constraint -} + +renderers = default_renderers.branch() diff --git a/alembic/command.py b/alembic/command.py index 5ba6d6a..3ce5131 100644 --- a/alembic/command.py +++ b/alembic/command.py @@ -1,8 +1,9 @@ import os from .script import ScriptDirectory -from .environment import EnvironmentContext -from . import util, autogenerate as autogen +from .runtime.environment import EnvironmentContext +from . import util +from . import autogenerate as autogen def list_templates(config): @@ -70,12 +71,16 @@ def revision( version_path=None, rev_id=None): """Create a new revision file.""" - script = ScriptDirectory.from_config(config) - template_args = { - 'config': config # Let templates use config for - # e.g. multiple databases - } - imports = set() + script_directory = ScriptDirectory.from_config(config) + + command_args = dict( + message=message, + autogenerate=autogenerate, + sql=sql, head=head, splice=splice, branch_label=branch_label, + version_path=version_path, rev_id=rev_id + ) + revision_context = autogen.RevisionContext( + config, script_directory, command_args) environment = util.asbool( config.get_main_option("revision_environment") @@ -89,13 +94,11 @@ def revision( "Using --sql with --autogenerate does not make any sense") def retrieve_migrations(rev, context): - if set(script.get_revisions(rev)) != \ - set(script.get_revisions("heads")): - raise util.CommandError("Target database is not up to date.") - autogen._produce_migration_diffs(context, template_args, imports) + revision_context.run_autogenerate(rev, context) return [] elif environment: def retrieve_migrations(rev, context): + revision_context.run_no_autogenerate(rev, context) return [] elif sql: raise util.CommandError( @@ -105,16 +108,22 @@ def revision( if environment: with EnvironmentContext( config, - script, + script_directory, fn=retrieve_migrations, as_sql=sql, - template_args=template_args, + template_args=revision_context.template_args, + revision_context=revision_context ): - script.run_env() - return script.generate_revision( - rev_id or util.rev_id(), message, refresh=True, - head=head, splice=splice, branch_labels=branch_label, - version_path=version_path, **template_args) + script_directory.run_env() + + scripts = [ + script for script in + revision_context.generate_scripts() + ] + if len(scripts) == 1: + return scripts[0] + else: + return scripts def merge(config, revisions, message=None, branch_label=None, rev_id=None): diff --git a/alembic/config.py b/alembic/config.py index 7f813d2..b3fc36f 100644 --- a/alembic/config.py +++ b/alembic/config.py @@ -1,10 +1,13 @@ from argparse import ArgumentParser -from .compat import SafeConfigParser +from .util.compat import SafeConfigParser import inspect import os import sys -from . import command, util, package_dir, compat +from . import command +from . import util +from . import package_dir +from .util import compat class Config(object): @@ -127,7 +130,7 @@ class Config(object): This is a utility dictionary which can include not just strings but engines, connections, schema objects, or anything else. Use this to pass objects into an env.py script, such as passing - a :class:`.Connection` when calling + a :class:`sqlalchemy.engine.base.Connection` when calling commands from :mod:`alembic.command` programmatically. .. versionadded:: 0.7.5 @@ -152,7 +155,7 @@ class Config(object): @util.memoized_property def file_config(self): - """Return the underlying :class:`ConfigParser` object. + """Return the underlying ``ConfigParser`` object. Direct access to the .ini file is available here, though the :meth:`.Config.get_section` and diff --git a/alembic/context.py b/alembic/context.py index 9c0f676..758fca8 100644 --- a/alembic/context.py +++ b/alembic/context.py @@ -1,6 +1,5 @@ -from .environment import EnvironmentContext -from . import util +from .runtime.environment import EnvironmentContext # create proxy functions for # each method on the EnvironmentContext class. -util.create_module_class_proxy(EnvironmentContext, globals(), locals()) +EnvironmentContext.create_module_class_proxy(globals(), locals()) diff --git a/alembic/ddl/base.py b/alembic/ddl/base.py index dbdc991..f4a525f 100644 --- a/alembic/ddl/base.py +++ b/alembic/ddl/base.py @@ -1,13 +1,16 @@ import functools from sqlalchemy.ext.compiler import compiles -from sqlalchemy.schema import DDLElement, Column, \ - ForeignKeyConstraint, CheckConstraint +from sqlalchemy.schema import DDLElement, Column from sqlalchemy import Integer from sqlalchemy import types as sqltypes -from sqlalchemy.sql.visitors import traverse from .. import util +# backwards compat +from ..util.sqla_compat import ( # noqa + _table_for_constraint, + _columns_for_constraint, _fk_spec, _is_type_bound, _find_columns) + if util.sqla_09: from sqlalchemy.sql.elements import quoted_name @@ -154,65 +157,6 @@ def visit_column_default(element, compiler, **kw): ) -def _table_for_constraint(constraint): - if isinstance(constraint, ForeignKeyConstraint): - return constraint.parent - else: - return constraint.table - - -def _columns_for_constraint(constraint): - if isinstance(constraint, ForeignKeyConstraint): - return [fk.parent for fk in constraint.elements] - elif isinstance(constraint, CheckConstraint): - return _find_columns(constraint.sqltext) - else: - return list(constraint.columns) - - -def _fk_spec(constraint): - if util.sqla_100: - source_columns = [ - constraint.columns[key].name for key in constraint.column_keys] - else: - source_columns = [ - element.parent.name for element in constraint.elements] - - source_table = constraint.parent.name - source_schema = constraint.parent.schema - target_schema = constraint.elements[0].column.table.schema - target_table = constraint.elements[0].column.table.name - target_columns = [element.column.name for element in constraint.elements] - - return ( - source_schema, source_table, - source_columns, target_schema, target_table, target_columns) - - -def _is_type_bound(constraint): - # this deals with SQLAlchemy #3260, don't copy CHECK constraints - # that will be generated by the type. - if util.sqla_100: - # new feature added for #3260 - return constraint._type_bound - else: - # old way, look at what we know Boolean/Enum to use - return ( - constraint._create_rule is not None and - isinstance( - getattr(constraint._create_rule, "target", None), - sqltypes.SchemaType) - ) - - -def _find_columns(clause): - """locate Column objects within the given expression.""" - - cols = set() - traverse(clause, {}, {'column': cols.add}) - return cols - - def quote_dotted(name, quote): """quote the elements of a dotted name""" diff --git a/alembic/ddl/impl.py b/alembic/ddl/impl.py index 3cca1ef..debef26 100644 --- a/alembic/ddl/impl.py +++ b/alembic/ddl/impl.py @@ -1,17 +1,13 @@ -from sqlalchemy.sql.expression import _BindParamClause -from sqlalchemy.ext.compiler import compiles -from sqlalchemy import schema, text, sql +from sqlalchemy import schema, text from sqlalchemy import types as sqltypes -from ..compat import string_types, text_type, with_metaclass +from ..util.compat import ( + string_types, text_type, with_metaclass +) +from ..util import sqla_compat from .. import util from . import base -if util.sqla_08: - from sqlalchemy.sql.expression import TextClause -else: - from sqlalchemy.sql.expression import _TextClause as TextClause - class ImplMeta(type): @@ -221,8 +217,10 @@ class DefaultImpl(with_metaclass(ImplMeta)): for row in rows: self._exec(table.insert(inline=True).values(**dict( (k, - _literal_bindparam(k, v, type_=table.c[k].type) - if not isinstance(v, _literal_bindparam) else v) + sqla_compat._literal_bindparam( + k, v, type_=table.c[k].type) + if not isinstance( + v, sqla_compat._literal_bindparam) else v) for k, v in row.items() ))) else: @@ -320,61 +318,6 @@ class DefaultImpl(with_metaclass(ImplMeta)): self.static_output("COMMIT" + self.command_terminator) -class _literal_bindparam(_BindParamClause): - pass - - -@compiles(_literal_bindparam) -def _render_literal_bindparam(element, compiler, **kw): - return compiler.render_literal_bindparam(element, **kw) - - -def _textual_index_column(table, text_): - """a workaround for the Index construct's severe lack of flexibility""" - if isinstance(text_, string_types): - c = schema.Column(text_, sqltypes.NULLTYPE) - table.append_column(c) - return c - elif isinstance(text_, TextClause): - return _textual_index_element(table, text_) - else: - raise ValueError("String or text() construct expected") - - -class _textual_index_element(sql.ColumnElement): - """Wrap around a sqlalchemy text() construct in such a way that - we appear like a column-oriented SQL expression to an Index - construct. - - The issue here is that currently the Postgresql dialect, the biggest - recipient of functional indexes, keys all the index expressions to - the corresponding column expressions when rendering CREATE INDEX, - so the Index we create here needs to have a .columns collection that - is the same length as the .expressions collection. Ultimately - SQLAlchemy should support text() expressions in indexes. - - See https://bitbucket.org/zzzeek/sqlalchemy/issue/3174/\ - support-text-sent-to-indexes - - """ - __visit_name__ = '_textual_idx_element' - - def __init__(self, table, text): - self.table = table - self.text = text - self.key = text.text - self.fake_column = schema.Column(self.text.text, sqltypes.NULLTYPE) - table.append_column(self.fake_column) - - def get_children(self): - return [self.fake_column] - - -@compiles(_textual_index_element) -def _render_textual_index_column(element, compiler, **kw): - return compiler.process(element.text, **kw) - - def _string_compare(t1, t2): return \ t1.length is not None and \ diff --git a/alembic/ddl/mssql.py b/alembic/ddl/mssql.py index f516e9b..f51de33 100644 --- a/alembic/ddl/mssql.py +++ b/alembic/ddl/mssql.py @@ -39,11 +39,10 @@ class MSSQLImpl(DefaultImpl): name=None, type_=None, schema=None, - autoincrement=None, existing_type=None, existing_server_default=None, existing_nullable=None, - existing_autoincrement=None + **kw ): if nullable is not None and existing_type is None: @@ -63,10 +62,9 @@ class MSSQLImpl(DefaultImpl): nullable=nullable, type_=type_, schema=schema, - autoincrement=autoincrement, existing_type=existing_type, existing_nullable=existing_nullable, - existing_autoincrement=existing_autoincrement + **kw ) if server_default is not False: diff --git a/alembic/ddl/mysql.py b/alembic/ddl/mysql.py index 7956185..b1cb324 100644 --- a/alembic/ddl/mysql.py +++ b/alembic/ddl/mysql.py @@ -2,7 +2,7 @@ from sqlalchemy.ext.compiler import compiles from sqlalchemy import types as sqltypes from sqlalchemy import schema -from ..compat import string_types +from ..util.compat import string_types from .. import util from .impl import DefaultImpl from .base import ColumnNullable, ColumnName, ColumnDefault, \ @@ -23,11 +23,12 @@ class MySQLImpl(DefaultImpl): name=None, type_=None, schema=None, - autoincrement=None, existing_type=None, existing_server_default=None, existing_nullable=None, - existing_autoincrement=None + autoincrement=None, + existing_autoincrement=None, + **kw ): if name is not None: self._exec( @@ -284,3 +285,5 @@ def _mysql_drop_constraint(element, compiler, **kw): raise NotImplementedError( "No generic 'DROP CONSTRAINT' in MySQL - " "please specify constraint type") + + diff --git a/alembic/ddl/postgresql.py b/alembic/ddl/postgresql.py index 9f97b34..ea423d7 100644 --- a/alembic/ddl/postgresql.py +++ b/alembic/ddl/postgresql.py @@ -1,6 +1,6 @@ import re -from .. import compat +from ..util import compat from .. import util from .base import compiles, alter_table, format_table_name, RenameTable from .impl import DefaultImpl diff --git a/alembic/op.py b/alembic/op.py index 8e5f777..1f367a1 100644 --- a/alembic/op.py +++ b/alembic/op.py @@ -1,6 +1,6 @@ -from .operations import Operations -from . import util +from .operations.base import Operations # create proxy functions for # each method on the Operations class. -util.create_module_class_proxy(Operations, globals(), locals()) +Operations.create_module_class_proxy(globals(), locals()) + diff --git a/alembic/operations/__init__.py b/alembic/operations/__init__.py new file mode 100644 index 0000000..1f6ee5d --- /dev/null +++ b/alembic/operations/__init__.py @@ -0,0 +1,6 @@ +from .base import Operations, BatchOperations +from .ops import MigrateOperation +from . import toimpl + + +__all__ = ['Operations', 'BatchOperations', 'MigrateOperation'] \ No newline at end of file diff --git a/alembic/operations/base.py b/alembic/operations/base.py new file mode 100644 index 0000000..18710fc --- /dev/null +++ b/alembic/operations/base.py @@ -0,0 +1,442 @@ +from contextlib import contextmanager + +from .. import util +from ..util import sqla_compat +from . import batch +from . import schemaobj +from ..util.compat import exec_ +import textwrap +import inspect + +__all__ = ('Operations', 'BatchOperations') + +try: + from sqlalchemy.sql.naming import conv +except: + conv = None + + +class Operations(util.ModuleClsProxy): + + """Define high level migration operations. + + Each operation corresponds to some schema migration operation, + executed against a particular :class:`.MigrationContext` + which in turn represents connectivity to a database, + or a file output stream. + + While :class:`.Operations` is normally configured as + part of the :meth:`.EnvironmentContext.run_migrations` + method called from an ``env.py`` script, a standalone + :class:`.Operations` instance can be + made for use cases external to regular Alembic + migrations by passing in a :class:`.MigrationContext`:: + + from alembic.migration import MigrationContext + from alembic.operations import Operations + + conn = myengine.connect() + ctx = MigrationContext.configure(conn) + op = Operations(ctx) + + op.alter_column("t", "c", nullable=True) + + Note that as of 0.8, most of the methods on this class are produced + dynamically using the :meth:`.Operations.register_operation` + method. + + """ + + _to_impl = util.Dispatcher() + + def __init__(self, migration_context, impl=None): + """Construct a new :class:`.Operations` + + :param migration_context: a :class:`.MigrationContext` + instance. + + """ + self.migration_context = migration_context + if impl is None: + self.impl = migration_context.impl + else: + self.impl = impl + + self.schema_obj = schemaobj.SchemaObjects(migration_context) + + @classmethod + def register_operation(cls, name, sourcename=None): + """Register a new operation for this class. + + This method is normally used to add new operations + to the :class:`.Operations` class, and possibly the + :class:`.BatchOperations` class as well. All Alembic migration + operations are implemented via this system, however the system + is also available as a public API to facilitate adding custom + operations. + + .. versionadded:: 0.8.0 + + .. seealso:: + + :ref:`operation_plugins` + + + """ + def register(op_cls): + if sourcename is None: + fn = getattr(op_cls, name) + source_name = fn.__name__ + else: + fn = getattr(op_cls, sourcename) + source_name = fn.__name__ + + spec = inspect.getargspec(fn) + + name_args = spec[0] + assert name_args[0:2] == ['cls', 'operations'] + + name_args[0:2] = ['self'] + + args = inspect.formatargspec(*spec) + num_defaults = len(spec[3]) if spec[3] else 0 + if num_defaults: + defaulted_vals = name_args[0 - num_defaults:] + else: + defaulted_vals = () + + apply_kw = inspect.formatargspec( + name_args, spec[1], spec[2], + defaulted_vals, + formatvalue=lambda x: '=' + x) + + func_text = textwrap.dedent("""\ + def %(name)s%(args)s: + %(doc)r + return op_cls.%(source_name)s%(apply_kw)s + """ % { + 'name': name, + 'source_name': source_name, + 'args': args, + 'apply_kw': apply_kw, + 'doc': fn.__doc__, + 'meth': fn.__name__ + }) + globals_ = {'op_cls': op_cls} + lcl = {} + exec_(func_text, globals_, lcl) + setattr(cls, name, lcl[name]) + fn.__func__.__doc__ = "This method is proxied on "\ + "the :class:`.%s` class, via the :meth:`.%s.%s` method." % ( + cls.__name__, cls.__name__, name + ) + return op_cls + return register + + @classmethod + def implementation_for(cls, op_cls): + """Register an implementation for a given :class:`.MigrateOperation`. + + This is part of the operation extensibility API. + + .. seealso:: + + :ref:`operation_plugins` - example of use + + """ + + def decorate(fn): + cls._to_impl.dispatch_for(op_cls)(fn) + return fn + return decorate + + @classmethod + @contextmanager + def context(cls, migration_context): + op = Operations(migration_context) + op._install_proxy() + yield op + op._remove_proxy() + + @contextmanager + def batch_alter_table( + self, table_name, schema=None, recreate="auto", copy_from=None, + table_args=(), table_kwargs=util.immutabledict(), + reflect_args=(), reflect_kwargs=util.immutabledict(), + naming_convention=None): + """Invoke a series of per-table migrations in batch. + + Batch mode allows a series of operations specific to a table + to be syntactically grouped together, and allows for alternate + modes of table migration, in particular the "recreate" style of + migration required by SQLite. + + "recreate" style is as follows: + + 1. A new table is created with the new specification, based on the + migration directives within the batch, using a temporary name. + + 2. the data copied from the existing table to the new table. + + 3. the existing table is dropped. + + 4. the new table is renamed to the existing table name. + + The directive by default will only use "recreate" style on the + SQLite backend, and only if directives are present which require + this form, e.g. anything other than ``add_column()``. The batch + operation on other backends will proceed using standard ALTER TABLE + operations. + + The method is used as a context manager, which returns an instance + of :class:`.BatchOperations`; this object is the same as + :class:`.Operations` except that table names and schema names + are omitted. E.g.:: + + with op.batch_alter_table("some_table") as batch_op: + batch_op.add_column(Column('foo', Integer)) + batch_op.drop_column('bar') + + The operations within the context manager are invoked at once + when the context is ended. When run against SQLite, if the + migrations include operations not supported by SQLite's ALTER TABLE, + the entire table will be copied to a new one with the new + specification, moving all data across as well. + + The copy operation by default uses reflection to retrieve the current + structure of the table, and therefore :meth:`.batch_alter_table` + in this mode requires that the migration is run in "online" mode. + The ``copy_from`` parameter may be passed which refers to an existing + :class:`.Table` object, which will bypass this reflection step. + + .. note:: The table copy operation will currently not copy + CHECK constraints, and may not copy UNIQUE constraints that are + unnamed, as is possible on SQLite. See the section + :ref:`sqlite_batch_constraints` for workarounds. + + :param table_name: name of table + :param schema: optional schema name. + :param recreate: under what circumstances the table should be + recreated. At its default of ``"auto"``, the SQLite dialect will + recreate the table if any operations other than ``add_column()``, + ``create_index()``, or ``drop_index()`` are + present. Other options include ``"always"`` and ``"never"``. + :param copy_from: optional :class:`~sqlalchemy.schema.Table` object + that will act as the structure of the table being copied. If omitted, + table reflection is used to retrieve the structure of the table. + + .. versionadded:: 0.7.6 Fully implemented the + :paramref:`~.Operations.batch_alter_table.copy_from` + parameter. + + .. seealso:: + + :ref:`batch_offline_mode` + + :paramref:`~.Operations.batch_alter_table.reflect_args` + + :paramref:`~.Operations.batch_alter_table.reflect_kwargs` + + :param reflect_args: a sequence of additional positional arguments that + will be applied to the table structure being reflected / copied; + this may be used to pass column and constraint overrides to the + table that will be reflected, in lieu of passing the whole + :class:`~sqlalchemy.schema.Table` using + :paramref:`~.Operations.batch_alter_table.copy_from`. + + .. versionadded:: 0.7.1 + + :param reflect_kwargs: a dictionary of additional keyword arguments + that will be applied to the table structure being copied; this may be + used to pass additional table and reflection options to the table that + will be reflected, in lieu of passing the whole + :class:`~sqlalchemy.schema.Table` using + :paramref:`~.Operations.batch_alter_table.copy_from`. + + .. versionadded:: 0.7.1 + + :param table_args: a sequence of additional positional arguments that + will be applied to the new :class:`~sqlalchemy.schema.Table` when + created, in addition to those copied from the source table. + This may be used to provide additional constraints such as CHECK + constraints that may not be reflected. + :param table_kwargs: a dictionary of additional keyword arguments + that will be applied to the new :class:`~sqlalchemy.schema.Table` + when created, in addition to those copied from the source table. + This may be used to provide for additional table options that may + not be reflected. + + .. versionadded:: 0.7.0 + + :param naming_convention: a naming convention dictionary of the form + described at :ref:`autogen_naming_conventions` which will be applied + to the :class:`~sqlalchemy.schema.MetaData` during the reflection + process. This is typically required if one wants to drop SQLite + constraints, as these constraints will not have names when + reflected on this backend. Requires SQLAlchemy **0.9.4** or greater. + + .. seealso:: + + :ref:`dropping_sqlite_foreign_keys` + + .. versionadded:: 0.7.1 + + .. note:: batch mode requires SQLAlchemy 0.8 or above. + + .. seealso:: + + :ref:`batch_migrations` + + """ + impl = batch.BatchOperationsImpl( + self, table_name, schema, recreate, + copy_from, table_args, table_kwargs, reflect_args, + reflect_kwargs, naming_convention) + batch_op = BatchOperations(self.migration_context, impl=impl) + yield batch_op + impl.flush() + + def get_context(self): + """Return the :class:`.MigrationContext` object that's + currently in use. + + """ + + return self.migration_context + + def invoke(self, operation): + """Given a :class:`.MigrateOperation`, invoke it in terms of + this :class:`.Operations` instance. + + .. versionadded:: 0.8.0 + + """ + fn = self._to_impl.dispatch( + operation, self.migration_context.impl.__dialect__) + return fn(self, operation) + + def f(self, name): + """Indicate a string name that has already had a naming convention + applied to it. + + This feature combines with the SQLAlchemy ``naming_convention`` feature + to disambiguate constraint names that have already had naming + conventions applied to them, versus those that have not. This is + necessary in the case that the ``"%(constraint_name)s"`` token + is used within a naming convention, so that it can be identified + that this particular name should remain fixed. + + If the :meth:`.Operations.f` is used on a constraint, the naming + convention will not take effect:: + + op.add_column('t', 'x', Boolean(name=op.f('ck_bool_t_x'))) + + Above, the CHECK constraint generated will have the name + ``ck_bool_t_x`` regardless of whether or not a naming convention is + in use. + + Alternatively, if a naming convention is in use, and 'f' is not used, + names will be converted along conventions. If the ``target_metadata`` + contains the naming convention + ``{"ck": "ck_bool_%(table_name)s_%(constraint_name)s"}``, then the + output of the following: + + op.add_column('t', 'x', Boolean(name='x')) + + will be:: + + CONSTRAINT ck_bool_t_x CHECK (x in (1, 0))) + + The function is rendered in the output of autogenerate when + a particular constraint name is already converted, for SQLAlchemy + version **0.9.4 and greater only**. Even though ``naming_convention`` + was introduced in 0.9.2, the string disambiguation service is new + as of 0.9.4. + + .. versionadded:: 0.6.4 + + """ + if conv: + return conv(name) + else: + raise NotImplementedError( + "op.f() feature requires SQLAlchemy 0.9.4 or greater.") + + def inline_literal(self, value, type_=None): + """Produce an 'inline literal' expression, suitable for + using in an INSERT, UPDATE, or DELETE statement. + + When using Alembic in "offline" mode, CRUD operations + aren't compatible with SQLAlchemy's default behavior surrounding + literal values, + which is that they are converted into bound values and passed + separately into the ``execute()`` method of the DBAPI cursor. + An offline SQL + script needs to have these rendered inline. While it should + always be noted that inline literal values are an **enormous** + security hole in an application that handles untrusted input, + a schema migration is not run in this context, so + literals are safe to render inline, with the caveat that + advanced types like dates may not be supported directly + by SQLAlchemy. + + See :meth:`.execute` for an example usage of + :meth:`.inline_literal`. + + The environment can also be configured to attempt to render + "literal" values inline automatically, for those simple types + that are supported by the dialect; see + :paramref:`.EnvironmentContext.configure.literal_binds` for this + more recently added feature. + + :param value: The value to render. Strings, integers, and simple + numerics should be supported. Other types like boolean, + dates, etc. may or may not be supported yet by various + backends. + :param ``type_``: optional - a :class:`sqlalchemy.types.TypeEngine` + subclass stating the type of this value. In SQLAlchemy + expressions, this is usually derived automatically + from the Python type of the value itself, as well as + based on the context in which the value is used. + + .. seealso:: + + :paramref:`.EnvironmentContext.configure.literal_binds` + + """ + return sqla_compat._literal_bindparam(None, value, type_=type_) + + def get_bind(self): + """Return the current 'bind'. + + Under normal circumstances, this is the + :class:`~sqlalchemy.engine.Connection` currently being used + to emit SQL to the database. + + In a SQL script context, this value is ``None``. [TODO: verify this] + + """ + return self.migration_context.impl.bind + + +class BatchOperations(Operations): + """Modifies the interface :class:`.Operations` for batch mode. + + This basically omits the ``table_name`` and ``schema`` parameters + from associated methods, as these are a given when running under batch + mode. + + .. seealso:: + + :meth:`.Operations.batch_alter_table` + + Note that as of 0.8, most of the methods on this class are produced + dynamically using the :meth:`.Operations.register_operation` + method. + + """ + + def _noop(self, operation): + raise NotImplementedError( + "The %s method does not apply to a batch table alter operation." + % operation) diff --git a/alembic/batch.py b/alembic/operations/batch.py similarity index 99% rename from alembic/batch.py rename to alembic/operations/batch.py index 1006739..726df78 100644 --- a/alembic/batch.py +++ b/alembic/operations/batch.py @@ -3,8 +3,8 @@ from sqlalchemy import Table, MetaData, Index, select, Column, \ from sqlalchemy import types as sqltypes from sqlalchemy import schema as sql_schema from sqlalchemy.util import OrderedDict -from . import util -from .ddl.base import _columns_for_constraint, _is_type_bound +from .. import util +from ..util.sqla_compat import _columns_for_constraint, _is_type_bound class BatchOperationsImpl(object): diff --git a/alembic/operations.py b/alembic/operations/ops.py similarity index 55% rename from alembic/operations.py rename to alembic/operations/ops.py index 2bf8060..1a38d07 100644 --- a/alembic/operations.py +++ b/alembic/operations/ops.py @@ -1,341 +1,968 @@ -from contextlib import contextmanager - -from sqlalchemy.types import NULLTYPE, Integer -from sqlalchemy import schema as sa_schema - -from . import util, batch -from .compat import string_types -from .ddl import impl - -__all__ = ('Operations', 'BatchOperations') - -try: - from sqlalchemy.sql.naming import conv -except: - conv = None +from .. import util +from ..util import sqla_compat +from . import schemaobj +from sqlalchemy.types import NULLTYPE +from .base import Operations, BatchOperations -class Operations(object): +class MigrateOperation(object): + """base class for migration command and organization objects. - """Define high level migration operations. + This system is part of the operation extensibility API. - Each operation corresponds to some schema migration operation, - executed against a particular :class:`.MigrationContext` - which in turn represents connectivity to a database, - or a file output stream. + .. versionadded:: 0.8.0 - While :class:`.Operations` is normally configured as - part of the :meth:`.EnvironmentContext.run_migrations` - method called from an ``env.py`` script, a standalone - :class:`.Operations` instance can be - made for use cases external to regular Alembic - migrations by passing in a :class:`.MigrationContext`:: + .. seealso:: - from alembic.migration import MigrationContext - from alembic.operations import Operations + :ref:`operation_objects` - conn = myengine.connect() - ctx = MigrationContext.configure(conn) - op = Operations(ctx) + :ref:`operation_plugins` - op.alter_column("t", "c", nullable=True) + :ref:`customizing_revision` """ - def __init__(self, migration_context, impl=None): - """Construct a new :class:`.Operations` - :param migration_context: a :class:`.MigrationContext` - instance. - - """ - self.migration_context = migration_context - if impl is None: - self.impl = migration_context.impl - else: - self.impl = impl +class AddConstraintOp(MigrateOperation): + """Represent an add constraint operation.""" @classmethod - @contextmanager - def context(cls, migration_context): - from .op import _install_proxy, _remove_proxy - op = Operations(migration_context) - _install_proxy(op) - yield op - _remove_proxy() + def from_constraint(cls, constraint): + funcs = { + "unique_constraint": CreateUniqueConstraintOp.from_constraint, + "foreign_key_constraint": CreateForeignKeyOp.from_constraint, + "primary_key_constraint": CreatePrimaryKeyOp.from_constraint, + "check_constraint": CreateCheckConstraintOp.from_constraint, + "column_check_constraint": CreateCheckConstraintOp.from_constraint, + } + return funcs[constraint.__visit_name__](constraint) - def _primary_key_constraint(self, name, table_name, cols, schema=None): - m = self._metadata() - columns = [sa_schema.Column(n, NULLTYPE) for n in cols] - t1 = sa_schema.Table(table_name, m, - *columns, - schema=schema) - p = sa_schema.PrimaryKeyConstraint(*columns, name=name) - t1.append_constraint(p) - return p - def _foreign_key_constraint(self, name, source, referent, - local_cols, remote_cols, - onupdate=None, ondelete=None, - deferrable=None, source_schema=None, - referent_schema=None, initially=None, - match=None, **dialect_kw): - m = self._metadata() - if source == referent: - t1_cols = local_cols + remote_cols - else: - t1_cols = local_cols - sa_schema.Table( - referent, m, - *[sa_schema.Column(n, NULLTYPE) for n in remote_cols], - schema=referent_schema) +@Operations.register_operation("drop_constraint") +@BatchOperations.register_operation("drop_constraint", "batch_drop_constraint") +class DropConstraintOp(MigrateOperation): + """Represent a drop constraint operation.""" - t1 = sa_schema.Table( - source, m, - *[sa_schema.Column(n, NULLTYPE) for n in t1_cols], - schema=source_schema) + def __init__(self, constraint_name, table_name, type_=None, schema=None): + self.constraint_name = constraint_name + self.table_name = table_name + self.constraint_type = type_ + self.schema = schema - tname = "%s.%s" % (referent_schema, referent) if referent_schema \ - else referent + @classmethod + def from_constraint(cls, constraint): + types = { + "unique_constraint": "unique", + "foreign_key_constraint": "foreignkey", + "primary_key_constraint": "primary", + "check_constraint": "check", + "column_check_constraint": "check", + } - if util.sqla_08: - # "match" kw unsupported in 0.7 - dialect_kw['match'] = match - - f = sa_schema.ForeignKeyConstraint(local_cols, - ["%s.%s" % (tname, n) - for n in remote_cols], - name=name, - onupdate=onupdate, - ondelete=ondelete, - deferrable=deferrable, - initially=initially, - **dialect_kw - ) - t1.append_constraint(f) - - return f - - def _unique_constraint(self, name, source, local_cols, schema=None, **kw): - t = sa_schema.Table( - source, self._metadata(), - *[sa_schema.Column(n, NULLTYPE) for n in local_cols], - schema=schema) - kw['name'] = name - uq = sa_schema.UniqueConstraint(*[t.c[n] for n in local_cols], **kw) - # TODO: need event tests to ensure the event - # is fired off here - t.append_constraint(uq) - return uq - - def _check_constraint(self, name, source, condition, schema=None, **kw): - t = sa_schema.Table(source, self._metadata(), - sa_schema.Column('x', Integer), schema=schema) - ck = sa_schema.CheckConstraint(condition, name=name, **kw) - t.append_constraint(ck) - return ck - - def _metadata(self): - kw = {} - if 'target_metadata' in self.migration_context.opts: - mt = self.migration_context.opts['target_metadata'] - if hasattr(mt, 'naming_convention'): - kw['naming_convention'] = mt.naming_convention - return sa_schema.MetaData(**kw) - - def _table(self, name, *columns, **kw): - m = self._metadata() - t = sa_schema.Table(name, m, *columns, **kw) - for f in t.foreign_keys: - self._ensure_table_for_fk(m, f) - return t - - def _column(self, name, type_, **kw): - return sa_schema.Column(name, type_, **kw) - - def _index(self, name, tablename, columns, schema=None, **kw): - t = sa_schema.Table( - tablename or 'no_table', self._metadata(), - schema=schema + constraint_table = sqla_compat._table_for_constraint(constraint) + return cls( + constraint.name, + constraint_table.name, + schema=constraint_table.schema, + type_=types[constraint.__visit_name__] ) - idx = sa_schema.Index( - name, - *[impl._textual_index_column(t, n) for n in columns], - **kw) - return idx - def _parse_table_key(self, table_key): - if '.' in table_key: - tokens = table_key.split('.') - sname = ".".join(tokens[0:-1]) - tname = tokens[-1] - else: - tname = table_key - sname = None - return (sname, tname) + @classmethod + @util._with_legacy_names([("type", "type_")]) + def drop_constraint( + cls, operations, name, table_name, type_=None, schema=None): + """Drop a constraint of the given name, typically via DROP CONSTRAINT. - def _ensure_table_for_fk(self, metadata, fk): - """create a placeholder Table object for the referent of a - ForeignKey. + :param name: name of the constraint. + :param table_name: table name. + :param ``type_``: optional, required on MySQL. can be + 'foreignkey', 'primary', 'unique', or 'check'. + :param schema: Optional schema name to operate within. To control + quoting of the schema outside of the default behavior, use + the SQLAlchemy construct + :class:`~sqlalchemy.sql.elements.quoted_name`. + + .. versionadded:: 0.7.0 'schema' can now accept a + :class:`~sqlalchemy.sql.elements.quoted_name` construct. """ - if isinstance(fk._colspec, string_types): - table_key, cname = fk._colspec.rsplit('.', 1) - sname, tname = self._parse_table_key(table_key) - if table_key not in metadata.tables: - rel_t = sa_schema.Table(tname, metadata, schema=sname) - else: - rel_t = metadata.tables[table_key] - if cname not in rel_t.c: - rel_t.append_column(sa_schema.Column(cname, NULLTYPE)) - @contextmanager - def batch_alter_table( - self, table_name, schema=None, recreate="auto", copy_from=None, - table_args=(), table_kwargs=util.immutabledict(), - reflect_args=(), reflect_kwargs=util.immutabledict(), - naming_convention=None): - """Invoke a series of per-table migrations in batch. + op = cls(name, table_name, type_=type_, schema=schema) + return operations.invoke(op) - Batch mode allows a series of operations specific to a table - to be syntactically grouped together, and allows for alternate - modes of table migration, in particular the "recreate" style of - migration required by SQLite. + @classmethod + def batch_drop_constraint(cls, operations, name, type_=None): + """Issue a "drop constraint" instruction using the + current batch migration context. - "recreate" style is as follows: - - 1. A new table is created with the new specification, based on the - migration directives within the batch, using a temporary name. - - 2. the data copied from the existing table to the new table. - - 3. the existing table is dropped. - - 4. the new table is renamed to the existing table name. - - The directive by default will only use "recreate" style on the - SQLite backend, and only if directives are present which require - this form, e.g. anything other than ``add_column()``. The batch - operation on other backends will proceed using standard ALTER TABLE - operations. - - The method is used as a context manager, which returns an instance - of :class:`.BatchOperations`; this object is the same as - :class:`.Operations` except that table names and schema names - are omitted. E.g.:: - - with op.batch_alter_table("some_table") as batch_op: - batch_op.add_column(Column('foo', Integer)) - batch_op.drop_column('bar') - - The operations within the context manager are invoked at once - when the context is ended. When run against SQLite, if the - migrations include operations not supported by SQLite's ALTER TABLE, - the entire table will be copied to a new one with the new - specification, moving all data across as well. - - The copy operation by default uses reflection to retrieve the current - structure of the table, and therefore :meth:`.batch_alter_table` - in this mode requires that the migration is run in "online" mode. - The ``copy_from`` parameter may be passed which refers to an existing - :class:`.Table` object, which will bypass this reflection step. - - .. note:: The table copy operation will currently not copy - CHECK constraints, and may not copy UNIQUE constraints that are - unnamed, as is possible on SQLite. See the section - :ref:`sqlite_batch_constraints` for workarounds. - - :param table_name: name of table - :param schema: optional schema name. - :param recreate: under what circumstances the table should be - recreated. At its default of ``"auto"``, the SQLite dialect will - recreate the table if any operations other than ``add_column()``, - ``create_index()``, or ``drop_index()`` are - present. Other options include ``"always"`` and ``"never"``. - :param copy_from: optional :class:`~sqlalchemy.schema.Table` object - that will act as the structure of the table being copied. If omitted, - table reflection is used to retrieve the structure of the table. - - .. versionadded:: 0.7.6 Fully implemented the - :paramref:`~.Operations.batch_alter_table.copy_from` - parameter. - - .. seealso:: - - :ref:`batch_offline_mode` - - :paramref:`~.Operations.batch_alter_table.reflect_args` - - :paramref:`~.Operations.batch_alter_table.reflect_kwargs` - - :param reflect_args: a sequence of additional positional arguments that - will be applied to the table structure being reflected / copied; - this may be used to pass column and constraint overrides to the - table that will be reflected, in lieu of passing the whole - :class:`~sqlalchemy.schema.Table` using - :paramref:`~.Operations.batch_alter_table.copy_from`. - - .. versionadded:: 0.7.1 - - :param reflect_kwargs: a dictionary of additional keyword arguments - that will be applied to the table structure being copied; this may be - used to pass additional table and reflection options to the table that - will be reflected, in lieu of passing the whole - :class:`~sqlalchemy.schema.Table` using - :paramref:`~.Operations.batch_alter_table.copy_from`. - - .. versionadded:: 0.7.1 - - :param table_args: a sequence of additional positional arguments that - will be applied to the new :class:`~sqlalchemy.schema.Table` when - created, in addition to those copied from the source table. - This may be used to provide additional constraints such as CHECK - constraints that may not be reflected. - :param table_kwargs: a dictionary of additional keyword arguments - that will be applied to the new :class:`~sqlalchemy.schema.Table` - when created, in addition to those copied from the source table. - This may be used to provide for additional table options that may - not be reflected. - - .. versionadded:: 0.7.0 - - :param naming_convention: a naming convention dictionary of the form - described at :ref:`autogen_naming_conventions` which will be applied - to the :class:`~sqlalchemy.schema.MetaData` during the reflection - process. This is typically required if one wants to drop SQLite - constraints, as these constraints will not have names when - reflected on this backend. Requires SQLAlchemy **0.9.4** or greater. - - .. seealso:: - - :ref:`dropping_sqlite_foreign_keys` - - .. versionadded:: 0.7.1 - - .. note:: batch mode requires SQLAlchemy 0.8 or above. + The batch form of this call omits the ``table_name`` and ``schema`` + arguments from the call. .. seealso:: - :ref:`batch_migrations` + :meth:`.Operations.drop_constraint` """ - impl = batch.BatchOperationsImpl( - self, table_name, schema, recreate, - copy_from, table_args, table_kwargs, reflect_args, - reflect_kwargs, naming_convention) - batch_op = BatchOperations(self.migration_context, impl=impl) - yield batch_op - impl.flush() + op = cls( + name, operations.impl.table_name, + type_=type_, schema=operations.impl.schema + ) + return operations.invoke(op) - def get_context(self): - """Return the :class:`.MigrationContext` object that's - currently in use. + +@Operations.register_operation("create_primary_key") +@BatchOperations.register_operation( + "create_primary_key", "batch_create_primary_key") +class CreatePrimaryKeyOp(AddConstraintOp): + """Represent a create primary key operation.""" + + def __init__( + self, constraint_name, table_name, columns, schema=None, **kw): + self.constraint_name = constraint_name + self.table_name = table_name + self.columns = columns + self.schema = schema + self.kw = kw + + @classmethod + def from_constraint(cls, constraint): + constraint_table = sqla_compat._table_for_constraint(constraint) + + return cls( + constraint.name, + constraint_table.name, + schema=constraint_table.schema, + *constraint.columns + ) + + def to_constraint(self, migration_context=None): + schema_obj = schemaobj.SchemaObjects(migration_context) + return schema_obj.primary_key_constraint( + self.constraint_name, self.table_name, + self.columns, schema=self.schema) + + @classmethod + @util._with_legacy_names([('name', 'constraint_name')]) + def create_primary_key( + cls, operations, + constraint_name, table_name, columns, schema=None): + """Issue a "create primary key" instruction using the current + migration context. + + e.g.:: + + from alembic import op + op.create_primary_key( + "pk_my_table", "my_table", + ["id", "version"] + ) + + This internally generates a :class:`~sqlalchemy.schema.Table` object + containing the necessary columns, then generates a new + :class:`~sqlalchemy.schema.PrimaryKeyConstraint` + object which it then associates with the + :class:`~sqlalchemy.schema.Table`. + Any event listeners associated with this action will be fired + off normally. The :class:`~sqlalchemy.schema.AddConstraint` + construct is ultimately used to generate the ALTER statement. + + :param name: Name of the primary key constraint. The name is necessary + so that an ALTER statement can be emitted. For setups that + use an automated naming scheme such as that described at + :ref:`sqla:constraint_naming_conventions` + ``name`` here can be ``None``, as the event listener will + apply the name to the constraint object when it is associated + with the table. + :param table_name: String name of the target table. + :param columns: a list of string column names to be applied to the + primary key constraint. + :param schema: Optional schema name to operate within. To control + quoting of the schema outside of the default behavior, use + the SQLAlchemy construct + :class:`~sqlalchemy.sql.elements.quoted_name`. + + .. versionadded:: 0.7.0 'schema' can now accept a + :class:`~sqlalchemy.sql.elements.quoted_name` construct. + + """ + op = cls(constraint_name, table_name, columns, schema) + return operations.invoke(op) + + @classmethod + def batch_create_primary_key(cls, operations, constraint_name, columns): + """Issue a "create primary key" instruction using the + current batch migration context. + + The batch form of this call omits the ``table_name`` and ``schema`` + arguments from the call. + + .. seealso:: + + :meth:`.Operations.create_primary_key` + + """ + raise NotImplementedError("not yet implemented") + + +@Operations.register_operation("create_unique_constraint") +@BatchOperations.register_operation( + "create_unique_constraint", "batch_create_unique_constraint") +class CreateUniqueConstraintOp(AddConstraintOp): + """Represent a create unique constraint operation.""" + + def __init__( + self, constraint_name, table_name, columns, schema=None, **kw): + self.constraint_name = constraint_name + self.table_name = table_name + self.columns = columns + self.schema = schema + self.kw = kw + + @classmethod + def from_constraint(cls, constraint): + constraint_table = sqla_compat._table_for_constraint(constraint) + + kw = {} + if constraint.deferrable: + kw['deferrable'] = constraint.deferrable + if constraint.initially: + kw['initially'] = constraint.initially + + return cls( + constraint.name, + constraint_table.name, + [c.name for c in constraint.columns], + schema=constraint_table.schema, + **kw + ) + + def to_constraint(self, migration_context=None): + schema_obj = schemaobj.SchemaObjects(migration_context) + return schema_obj.unique_constraint( + self.constraint_name, self.table_name, self.columns, + schema=self.schema, **self.kw) + + @classmethod + @util._with_legacy_names([ + ('name', 'constraint_name'), + ('source', 'table_name') + ]) + def create_unique_constraint( + cls, operations, constraint_name, table_name, columns, + schema=None, **kw): + """Issue a "create unique constraint" instruction using the + current migration context. + + e.g.:: + + from alembic import op + op.create_unique_constraint("uq_user_name", "user", ["name"]) + + This internally generates a :class:`~sqlalchemy.schema.Table` object + containing the necessary columns, then generates a new + :class:`~sqlalchemy.schema.UniqueConstraint` + object which it then associates with the + :class:`~sqlalchemy.schema.Table`. + Any event listeners associated with this action will be fired + off normally. The :class:`~sqlalchemy.schema.AddConstraint` + construct is ultimately used to generate the ALTER statement. + + :param name: Name of the unique constraint. The name is necessary + so that an ALTER statement can be emitted. For setups that + use an automated naming scheme such as that described at + :ref:`sqla:constraint_naming_conventions`, + ``name`` here can be ``None``, as the event listener will + apply the name to the constraint object when it is associated + with the table. + :param table_name: String name of the source table. + :param columns: a list of string column names in the + source table. + :param deferrable: optional bool. If set, emit DEFERRABLE or + NOT DEFERRABLE when issuing DDL for this constraint. + :param initially: optional string. If set, emit INITIALLY + when issuing DDL for this constraint. + :param schema: Optional schema name to operate within. To control + quoting of the schema outside of the default behavior, use + the SQLAlchemy construct + :class:`~sqlalchemy.sql.elements.quoted_name`. + + .. versionadded:: 0.7.0 'schema' can now accept a + :class:`~sqlalchemy.sql.elements.quoted_name` construct. """ - return self.migration_context + op = cls( + constraint_name, table_name, columns, + schema=schema, **kw + ) + return operations.invoke(op) - def rename_table(self, old_table_name, new_table_name, schema=None): + @classmethod + @util._with_legacy_names([('name', 'constraint_name')]) + def batch_create_unique_constraint( + cls, operations, constraint_name, columns, **kw): + """Issue a "create unique constraint" instruction using the + current batch migration context. + + The batch form of this call omits the ``source`` and ``schema`` + arguments from the call. + + .. seealso:: + + :meth:`.Operations.create_unique_constraint` + + """ + kw['schema'] = operations.impl.schema + op = cls( + constraint_name, operations.impl.table_name, columns, + **kw + ) + return operations.invoke(op) + + +@Operations.register_operation("create_foreign_key") +@BatchOperations.register_operation( + "create_foreign_key", "batch_create_foreign_key") +class CreateForeignKeyOp(AddConstraintOp): + """Represent a create foreign key constraint operation.""" + + def __init__( + self, constraint_name, source_table, referent_table, local_cols, + remote_cols, **kw): + self.constraint_name = constraint_name + self.source_table = source_table + self.referent_table = referent_table + self.local_cols = local_cols + self.remote_cols = remote_cols + self.kw = kw + + @classmethod + def from_constraint(cls, constraint): + kw = {} + if constraint.onupdate: + kw['onupdate'] = constraint.onupdate + if constraint.ondelete: + kw['ondelete'] = constraint.ondelete + if constraint.initially: + kw['initially'] = constraint.initially + if constraint.deferrable: + kw['deferrable'] = constraint.deferrable + if constraint.use_alter: + kw['use_alter'] = constraint.use_alter + + source_schema, source_table, \ + source_columns, target_schema, \ + target_table, target_columns = sqla_compat._fk_spec(constraint) + + kw['source_schema'] = source_schema + kw['referent_schema'] = target_schema + + return cls( + constraint.name, + source_table, + target_table, + source_columns, + target_columns, + **kw + ) + + def to_constraint(self, migration_context=None): + schema_obj = schemaobj.SchemaObjects(migration_context) + return schema_obj.foreign_key_constraint( + self.constraint_name, + self.source_table, self.referent_table, + self.local_cols, self.remote_cols, + **self.kw) + + @classmethod + @util._with_legacy_names([('name', 'constraint_name')]) + def create_foreign_key(cls, operations, constraint_name, + source_table, referent_table, local_cols, + remote_cols, onupdate=None, ondelete=None, + deferrable=None, initially=None, match=None, + source_schema=None, referent_schema=None, + **dialect_kw): + """Issue a "create foreign key" instruction using the + current migration context. + + e.g.:: + + from alembic import op + op.create_foreign_key( + "fk_user_address", "address", + "user", ["user_id"], ["id"]) + + This internally generates a :class:`~sqlalchemy.schema.Table` object + containing the necessary columns, then generates a new + :class:`~sqlalchemy.schema.ForeignKeyConstraint` + object which it then associates with the + :class:`~sqlalchemy.schema.Table`. + Any event listeners associated with this action will be fired + off normally. The :class:`~sqlalchemy.schema.AddConstraint` + construct is ultimately used to generate the ALTER statement. + + :param name: Name of the foreign key constraint. The name is necessary + so that an ALTER statement can be emitted. For setups that + use an automated naming scheme such as that described at + :ref:`sqla:constraint_naming_conventions`, + ``name`` here can be ``None``, as the event listener will + apply the name to the constraint object when it is associated + with the table. + :param source_table: String name of the source table. + :param referent_table: String name of the destination table. + :param local_cols: a list of string column names in the + source table. + :param remote_cols: a list of string column names in the + remote table. + :param onupdate: Optional string. If set, emit ON UPDATE when + issuing DDL for this constraint. Typical values include CASCADE, + DELETE and RESTRICT. + :param ondelete: Optional string. If set, emit ON DELETE when + issuing DDL for this constraint. Typical values include CASCADE, + DELETE and RESTRICT. + :param deferrable: optional bool. If set, emit DEFERRABLE or NOT + DEFERRABLE when issuing DDL for this constraint. + :param source_schema: Optional schema name of the source table. + :param referent_schema: Optional schema name of the destination table. + + """ + + op = cls( + constraint_name, + source_table, referent_table, + local_cols, remote_cols, + onupdate=onupdate, ondelete=ondelete, + deferrable=deferrable, + source_schema=source_schema, + referent_schema=referent_schema, + initially=initially, match=match, + **dialect_kw + ) + return operations.invoke(op) + + @classmethod + @util._with_legacy_names([('name', 'constraint_name')]) + def batch_create_foreign_key( + cls, operations, constraint_name, referent_table, + local_cols, remote_cols, + referent_schema=None, + onupdate=None, ondelete=None, + deferrable=None, initially=None, match=None, + **dialect_kw): + """Issue a "create foreign key" instruction using the + current batch migration context. + + The batch form of this call omits the ``source`` and ``source_schema`` + arguments from the call. + + e.g.:: + + with batch_alter_table("address") as batch_op: + batch_op.create_foreign_key( + "fk_user_address", + "user", ["user_id"], ["id"]) + + .. seealso:: + + :meth:`.Operations.create_foreign_key` + + """ + op = cls( + constraint_name, + operations.impl.table_name, referent_table, + local_cols, remote_cols, + onupdate=onupdate, ondelete=ondelete, + deferrable=deferrable, + source_schema=operations.impl.schema, + referent_schema=referent_schema, + initially=initially, match=match, + **dialect_kw + ) + return operations.invoke(op) + + +@Operations.register_operation("create_check_constraint") +@BatchOperations.register_operation( + "create_check_constraint", "batch_create_check_constraint") +class CreateCheckConstraintOp(AddConstraintOp): + """Represent a create check constraint operation.""" + + def __init__( + self, constraint_name, table_name, condition, schema=None, **kw): + self.constraint_name = constraint_name + self.table_name = table_name + self.condition = condition + self.schema = schema + self.kw = kw + + @classmethod + def from_constraint(cls, constraint): + constraint_table = sqla_compat._table_for_constraint(constraint) + + return cls( + constraint.name, + constraint_table.name, + constraint.condition, + schema=constraint_table.schema + ) + + def to_constraint(self, migration_context=None): + schema_obj = schemaobj.SchemaObjects(migration_context) + return schema_obj.check_constraint( + self.constraint_name, self.table_name, + self.condition, schema=self.schema, **self.kw) + + @classmethod + @util._with_legacy_names([ + ('name', 'constraint_name'), + ('source', 'table_name') + ]) + def create_check_constraint( + cls, operations, + constraint_name, table_name, condition, + schema=None, **kw): + """Issue a "create check constraint" instruction using the + current migration context. + + e.g.:: + + from alembic import op + from sqlalchemy.sql import column, func + + op.create_check_constraint( + "ck_user_name_len", + "user", + func.len(column('name')) > 5 + ) + + CHECK constraints are usually against a SQL expression, so ad-hoc + table metadata is usually needed. The function will convert the given + arguments into a :class:`sqlalchemy.schema.CheckConstraint` bound + to an anonymous table in order to emit the CREATE statement. + + :param name: Name of the check constraint. The name is necessary + so that an ALTER statement can be emitted. For setups that + use an automated naming scheme such as that described at + :ref:`sqla:constraint_naming_conventions`, + ``name`` here can be ``None``, as the event listener will + apply the name to the constraint object when it is associated + with the table. + :param table_name: String name of the source table. + :param condition: SQL expression that's the condition of the + constraint. Can be a string or SQLAlchemy expression language + structure. + :param deferrable: optional bool. If set, emit DEFERRABLE or + NOT DEFERRABLE when issuing DDL for this constraint. + :param initially: optional string. If set, emit INITIALLY + when issuing DDL for this constraint. + :param schema: Optional schema name to operate within. To control + quoting of the schema outside of the default behavior, use + the SQLAlchemy construct + :class:`~sqlalchemy.sql.elements.quoted_name`. + + .. versionadded:: 0.7.0 'schema' can now accept a + :class:`~sqlalchemy.sql.elements.quoted_name` construct. + + """ + op = cls(constraint_name, table_name, condition, schema=schema, **kw) + return operations.invoke(op) + + @classmethod + @util._with_legacy_names([('name', 'constraint_name')]) + def batch_create_check_constraint( + cls, operations, constraint_name, condition, **kw): + """Issue a "create check constraint" instruction using the + current batch migration context. + + The batch form of this call omits the ``source`` and ``schema`` + arguments from the call. + + .. seealso:: + + :meth:`.Operations.create_check_constraint` + + """ + raise NotImplementedError("not yet implemented") + + +@Operations.register_operation("create_index") +@BatchOperations.register_operation("create_index", "batch_create_index") +class CreateIndexOp(MigrateOperation): + """Represent a create index operation.""" + + def __init__( + self, index_name, table_name, columns, schema=None, + unique=False, quote=None, _orig_index=None, **kw): + self.index_name = index_name + self.table_name = table_name + self.columns = columns + self.schema = schema + self.unique = unique + self.quote = quote + self.kw = kw + self._orig_index = _orig_index + + @classmethod + def from_index(cls, index): + return cls( + index.name, + index.table.name, + sqla_compat._get_index_expressions(index), + schema=index.table.schema, + unique=index.unique, + quote=index.name.quote, + _orig_index=index, + **index.dialect_kwargs + ) + + def to_index(self, migration_context=None): + if self._orig_index: + return self._orig_index + schema_obj = schemaobj.SchemaObjects(migration_context) + return schema_obj.index( + self.index_name, self.table_name, self.columns, schema=self.schema, + unique=self.unique, quote=self.quote, **self.kw) + + @classmethod + @util._with_legacy_names([('name', 'index_name')]) + def create_index( + cls, operations, + index_name, table_name, columns, schema=None, + unique=False, quote=None, **kw): + """Issue a "create index" instruction using the current + migration context. + + e.g.:: + + from alembic import op + op.create_index('ik_test', 't1', ['foo', 'bar']) + + Functional indexes can be produced by using the + :func:`sqlalchemy.sql.expression.text` construct:: + + from alembic import op + from sqlalchemy import text + op.create_index('ik_test', 't1', [text('lower(foo)')]) + + .. versionadded:: 0.6.7 support for making use of the + :func:`~sqlalchemy.sql.expression.text` construct in + conjunction with + :meth:`.Operations.create_index` in + order to produce functional expressions within CREATE INDEX. + + :param index_name: name of the index. + :param table_name: name of the owning table. + :param columns: a list consisting of string column names and/or + :func:`~sqlalchemy.sql.expression.text` constructs. + :param schema: Optional schema name to operate within. To control + quoting of the schema outside of the default behavior, use + the SQLAlchemy construct + :class:`~sqlalchemy.sql.elements.quoted_name`. + + .. versionadded:: 0.7.0 'schema' can now accept a + :class:`~sqlalchemy.sql.elements.quoted_name` construct. + + :param unique: If True, create a unique index. + + :param quote: + Force quoting of this column's name on or off, corresponding + to ``True`` or ``False``. When left at its default + of ``None``, the column identifier will be quoted according to + whether the name is case sensitive (identifiers with at least one + upper case character are treated as case sensitive), or if it's a + reserved word. This flag is only needed to force quoting of a + reserved word which is not known by the SQLAlchemy dialect. + + :param \**kw: Additional keyword arguments not mentioned above are + dialect specific, and passed in the form + ``_``. + See the documentation regarding an individual dialect at + :ref:`dialect_toplevel` for detail on documented arguments. + """ + op = cls( + index_name, table_name, columns, schema=schema, + unique=unique, quote=quote, **kw + ) + return operations.invoke(op) + + @classmethod + def batch_create_index(cls, operations, index_name, columns, **kw): + """Issue a "create index" instruction using the + current batch migration context. + + .. seealso:: + + :meth:`.Operations.create_index` + + """ + + op = cls( + index_name, operations.impl.table_name, columns, + schema=operations.impl.schema, **kw + ) + return operations.invoke(op) + + +@Operations.register_operation("drop_index") +@BatchOperations.register_operation("drop_index", "batch_drop_index") +class DropIndexOp(MigrateOperation): + """Represent a drop index operation.""" + + def __init__(self, index_name, table_name=None, schema=None): + self.index_name = index_name + self.table_name = table_name + self.schema = schema + + @classmethod + def from_index(cls, index): + return cls( + index.name, + index.table.name, + schema=index.table.schema, + ) + + def to_index(self, migration_context=None): + schema_obj = schemaobj.SchemaObjects(migration_context) + + # need a dummy column name here since SQLAlchemy + # 0.7.6 and further raises on Index with no columns + return schema_obj.index( + self.index_name, self.table_name, ['x'], schema=self.schema) + + @classmethod + @util._with_legacy_names([ + ('name', 'index_name'), ('tablename', 'table_name')]) + def drop_index(cls, operations, index_name, table_name=None, schema=None): + """Issue a "drop index" instruction using the current + migration context. + + e.g.:: + + drop_index("accounts") + + :param index_name: name of the index. + :param table_name: name of the owning table. Some + backends such as Microsoft SQL Server require this. + :param schema: Optional schema name to operate within. To control + quoting of the schema outside of the default behavior, use + the SQLAlchemy construct + :class:`~sqlalchemy.sql.elements.quoted_name`. + + .. versionadded:: 0.7.0 'schema' can now accept a + :class:`~sqlalchemy.sql.elements.quoted_name` construct. + + """ + op = cls(index_name, table_name=table_name, schema=schema) + return operations.invoke(op) + + @classmethod + @util._with_legacy_names([('name', 'index_name')]) + def batch_drop_index(cls, operations, index_name, **kw): + """Issue a "drop index" instruction using the + current batch migration context. + + .. seealso:: + + :meth:`.Operations.drop_index` + + """ + + op = cls( + index_name, table_name=operations.impl.table_name, + schema=operations.impl.schema + ) + return operations.invoke(op) + + +@Operations.register_operation("create_table") +class CreateTableOp(MigrateOperation): + """Represent a create table operation.""" + + def __init__( + self, table_name, columns, schema=None, _orig_table=None, **kw): + self.table_name = table_name + self.columns = columns + self.schema = schema + self.kw = kw + self._orig_table = _orig_table + + @classmethod + def from_table(cls, table): + return cls( + table.name, + list(table.c) + list(table.constraints), + schema=table.schema, + _orig_table=table, + **table.kwargs + ) + + def to_table(self, migration_context=None): + if self._orig_table is not None: + return self._orig_table + schema_obj = schemaobj.SchemaObjects(migration_context) + + return schema_obj.table( + self.table_name, *self.columns, schema=self.schema, **self.kw + ) + + @classmethod + @util._with_legacy_names([('name', 'table_name')]) + def create_table(cls, operations, table_name, *columns, **kw): + """Issue a "create table" instruction using the current migration + context. + + This directive receives an argument list similar to that of the + traditional :class:`sqlalchemy.schema.Table` construct, but without the + metadata:: + + from sqlalchemy import INTEGER, VARCHAR, NVARCHAR, Column + from alembic import op + + op.create_table( + 'account', + Column('id', INTEGER, primary_key=True), + Column('name', VARCHAR(50), nullable=False), + Column('description', NVARCHAR(200)), + Column('timestamp', TIMESTAMP, server_default=func.now()) + ) + + Note that :meth:`.create_table` accepts + :class:`~sqlalchemy.schema.Column` + constructs directly from the SQLAlchemy library. In particular, + default values to be created on the database side are + specified using the ``server_default`` parameter, and not + ``default`` which only specifies Python-side defaults:: + + from alembic import op + from sqlalchemy import Column, TIMESTAMP, func + + # specify "DEFAULT NOW" along with the "timestamp" column + op.create_table('account', + Column('id', INTEGER, primary_key=True), + Column('timestamp', TIMESTAMP, server_default=func.now()) + ) + + The function also returns a newly created + :class:`~sqlalchemy.schema.Table` object, corresponding to the table + specification given, which is suitable for + immediate SQL operations, in particular + :meth:`.Operations.bulk_insert`:: + + from sqlalchemy import INTEGER, VARCHAR, NVARCHAR, Column + from alembic import op + + account_table = op.create_table( + 'account', + Column('id', INTEGER, primary_key=True), + Column('name', VARCHAR(50), nullable=False), + Column('description', NVARCHAR(200)), + Column('timestamp', TIMESTAMP, server_default=func.now()) + ) + + op.bulk_insert( + account_table, + [ + {"name": "A1", "description": "account 1"}, + {"name": "A2", "description": "account 2"}, + ] + ) + + .. versionadded:: 0.7.0 + + :param table_name: Name of the table + :param \*columns: collection of :class:`~sqlalchemy.schema.Column` + objects within + the table, as well as optional :class:`~sqlalchemy.schema.Constraint` + objects + and :class:`~.sqlalchemy.schema.Index` objects. + :param schema: Optional schema name to operate within. To control + quoting of the schema outside of the default behavior, use + the SQLAlchemy construct + :class:`~sqlalchemy.sql.elements.quoted_name`. + + .. versionadded:: 0.7.0 'schema' can now accept a + :class:`~sqlalchemy.sql.elements.quoted_name` construct. + :param \**kw: Other keyword arguments are passed to the underlying + :class:`sqlalchemy.schema.Table` object created for the command. + + :return: the :class:`~sqlalchemy.schema.Table` object corresponding + to the parameters given. + + .. versionadded:: 0.7.0 - the :class:`~sqlalchemy.schema.Table` + object is returned. + + """ + op = cls(table_name, columns, **kw) + return operations.invoke(op) + + +@Operations.register_operation("drop_table") +class DropTableOp(MigrateOperation): + """Represent a drop table operation.""" + + def __init__(self, table_name, schema=None, table_kw=None): + self.table_name = table_name + self.schema = schema + self.table_kw = table_kw or {} + + @classmethod + def from_table(cls, table): + return cls(table.name, schema=table.schema) + + def to_table(self, migration_context): + schema_obj = schemaobj.SchemaObjects(migration_context) + return schema_obj.table( + self.table_name, + schema=self.schema, + **self.table_kw) + + @classmethod + @util._with_legacy_names([('name', 'table_name')]) + def drop_table(cls, operations, table_name, schema=None, **kw): + """Issue a "drop table" instruction using the current + migration context. + + + e.g.:: + + drop_table("accounts") + + :param table_name: Name of the table + :param schema: Optional schema name to operate within. To control + quoting of the schema outside of the default behavior, use + the SQLAlchemy construct + :class:`~sqlalchemy.sql.elements.quoted_name`. + + .. versionadded:: 0.7.0 'schema' can now accept a + :class:`~sqlalchemy.sql.elements.quoted_name` construct. + + :param \**kw: Other keyword arguments are passed to the underlying + :class:`sqlalchemy.schema.Table` object created for the command. + + """ + op = cls(table_name, schema=schema, table_kw=kw) + operations.invoke(op) + + +class AlterTableOp(MigrateOperation): + """Represent an alter table operation.""" + + def __init__(self, table_name, schema=None): + self.table_name = table_name + self.schema = schema + + +@Operations.register_operation("rename_table") +class RenameTableOp(AlterTableOp): + """Represent a rename table operation.""" + + def __init__(self, old_table_name, new_table_name, schema=None): + super(RenameTableOp, self).__init__(old_table_name, schema=schema) + self.new_table_name = new_table_name + + @classmethod + def rename_table( + cls, operations, old_table_name, new_table_name, schema=None): """Emit an ALTER TABLE to rename a table. :param old_table_name: old name. @@ -349,25 +976,51 @@ class Operations(object): :class:`~sqlalchemy.sql.elements.quoted_name` construct. """ - self.impl.rename_table( - old_table_name, - new_table_name, - schema=schema - ) + op = cls(old_table_name, new_table_name, schema=schema) + return operations.invoke(op) + +@Operations.register_operation("alter_column") +@BatchOperations.register_operation("alter_column", "batch_alter_column") +class AlterColumnOp(AlterTableOp): + """Represent an alter column operation.""" + + def __init__( + self, table_name, column_name, schema=None, + existing_type=None, + existing_server_default=False, + existing_nullable=None, + modify_nullable=None, + modify_server_default=False, + modify_name=None, + modify_type=None, + **kw + + ): + super(AlterColumnOp, self).__init__(table_name, schema=schema) + self.column_name = column_name + self.existing_type = existing_type + self.existing_server_default = existing_server_default + self.existing_nullable = existing_nullable + self.modify_nullable = modify_nullable + self.modify_server_default = modify_server_default + self.modify_name = modify_name + self.modify_type = modify_type + self.kw = kw + + @classmethod @util._with_legacy_names([('name', 'new_column_name')]) - def alter_column(self, table_name, column_name, - nullable=None, - server_default=False, - new_column_name=None, - type_=None, - autoincrement=None, - existing_type=None, - existing_server_default=False, - existing_nullable=None, - existing_autoincrement=None, - schema=None - ): + def alter_column( + cls, operations, table_name, column_name, + nullable=None, + server_default=False, + new_column_name=None, + type_=None, + existing_type=None, + existing_server_default=False, + existing_nullable=None, + schema=None, **kw + ): """Issue an "alter column" instruction using the current migration context. @@ -444,97 +1097,75 @@ class Operations(object): """ - compiler = self.impl.dialect.statement_compiler( - self.impl.dialect, - None + alt = cls( + table_name, column_name, schema=schema, + existing_type=existing_type, + existing_server_default=existing_server_default, + existing_nullable=existing_nullable, + modify_name=new_column_name, + modify_type=type_, + modify_server_default=server_default, + modify_nullable=nullable, + **kw ) - def _count_constraint(constraint): - return not isinstance( - constraint, - sa_schema.PrimaryKeyConstraint) and \ - (not constraint._create_rule or - constraint._create_rule(compiler)) + return operations.invoke(alt) - if existing_type and type_: - t = self._table(table_name, - sa_schema.Column(column_name, existing_type), - schema=schema - ) - for constraint in t.constraints: - if _count_constraint(constraint): - self.impl.drop_constraint(constraint) + @classmethod + def batch_alter_column( + cls, operations, column_name, + nullable=None, + server_default=False, + new_column_name=None, + type_=None, + existing_type=None, + existing_server_default=False, + existing_nullable=None, + **kw + ): + """Issue an "alter column" instruction using the current + batch migration context. - self.impl.alter_column(table_name, column_name, - nullable=nullable, - server_default=server_default, - name=new_column_name, - type_=type_, - schema=schema, - autoincrement=autoincrement, - existing_type=existing_type, - existing_server_default=existing_server_default, - existing_nullable=existing_nullable, - existing_autoincrement=existing_autoincrement - ) + .. seealso:: - if type_: - t = self._table(table_name, - sa_schema.Column(column_name, type_), - schema=schema - ) - for constraint in t.constraints: - if _count_constraint(constraint): - self.impl.add_constraint(constraint) - - def f(self, name): - """Indicate a string name that has already had a naming convention - applied to it. - - This feature combines with the SQLAlchemy ``naming_convention`` feature - to disambiguate constraint names that have already had naming - conventions applied to them, versus those that have not. This is - necessary in the case that the ``"%(constraint_name)s"`` token - is used within a naming convention, so that it can be identified - that this particular name should remain fixed. - - If the :meth:`.Operations.f` is used on a constraint, the naming - convention will not take effect:: - - op.add_column('t', 'x', Boolean(name=op.f('ck_bool_t_x'))) - - Above, the CHECK constraint generated will have the name - ``ck_bool_t_x`` regardless of whether or not a naming convention is - in use. - - Alternatively, if a naming convention is in use, and 'f' is not used, - names will be converted along conventions. If the ``target_metadata`` - contains the naming convention - ``{"ck": "ck_bool_%(table_name)s_%(constraint_name)s"}``, then the - output of the following: - - op.add_column('t', 'x', Boolean(name='x')) - - will be:: - - CONSTRAINT ck_bool_t_x CHECK (x in (1, 0))) - - The function is rendered in the output of autogenerate when - a particular constraint name is already converted, for SQLAlchemy - version **0.9.4 and greater only**. Even though ``naming_convention`` - was introduced in 0.9.2, the string disambiguation service is new - as of 0.9.4. - - .. versionadded:: 0.6.4 + :meth:`.Operations.add_column` """ - if conv: - return conv(name) - else: - raise NotImplementedError( - "op.f() feature requires SQLAlchemy 0.9.4 or greater.") + alt = cls( + operations.impl.table_name, column_name, + schema=operations.impl.schema, + existing_type=existing_type, + existing_server_default=existing_server_default, + existing_nullable=existing_nullable, + modify_name=new_column_name, + modify_type=type_, + modify_server_default=server_default, + modify_nullable=nullable, + **kw + ) - def add_column(self, table_name, column, schema=None): + return operations.invoke(alt) + + +@Operations.register_operation("add_column") +@BatchOperations.register_operation("add_column", "batch_add_column") +class AddColumnOp(AlterTableOp): + """Represent an add column operation.""" + + def __init__(self, table_name, column, schema=None): + super(AddColumnOp, self).__init__(table_name, schema=schema) + self.column = column + + @classmethod + def from_column(cls, col): + return cls(col.table.name, col, schema=col.table.schema) + + @classmethod + def from_column_and_tablename(cls, schema, tname, col): + return cls(tname, col, schema=schema) + + @classmethod + def add_column(cls, operations, table_name, column, schema=None): """Issue an "add column" instruction using the current migration context. @@ -588,19 +1219,47 @@ class Operations(object): """ - t = self._table(table_name, column, schema=schema) - self.impl.add_column( - table_name, - column, - schema=schema - ) - for constraint in t.constraints: - if not isinstance(constraint, sa_schema.PrimaryKeyConstraint): - self.impl.add_constraint(constraint) - for index in t.indexes: - self.impl._exec(sa_schema.CreateIndex(index)) + op = cls(table_name, column, schema=schema) + return operations.invoke(op) - def drop_column(self, table_name, column_name, **kw): + @classmethod + def batch_add_column(cls, operations, column): + """Issue an "add column" instruction using the current + batch migration context. + + .. seealso:: + + :meth:`.Operations.add_column` + + """ + op = cls( + operations.impl.table_name, column, + schema=operations.impl.schema + ) + return operations.invoke(op) + + +@Operations.register_operation("drop_column") +@BatchOperations.register_operation("drop_column", "batch_drop_column") +class DropColumnOp(AlterTableOp): + """Represent a drop column operation.""" + + def __init__(self, table_name, column_name, schema=None, **kw): + super(DropColumnOp, self).__init__(table_name, schema=schema) + self.column_name = column_name + self.kw = kw + + @classmethod + def from_column_and_tablename(cls, schema, tname, col): + return cls(tname, col.name, schema=schema) + + def to_column(self, migration_context=None): + schema_obj = schemaobj.SchemaObjects(migration_context) + return schema_obj.column(self.column_name, NULLTYPE) + + @classmethod + def drop_column( + cls, operations, table_name, column_name, schema=None, **kw): """Issue a "drop column" instruction using the current migration context. @@ -644,454 +1303,36 @@ class Operations(object): """ - self.impl.drop_column( - table_name, - self._column(column_name, NULLTYPE), - **kw - ) + op = cls(table_name, column_name, schema=schema, **kw) + return operations.invoke(op) - def create_primary_key(self, name, table_name, cols, schema=None): - """Issue a "create primary key" instruction using the current - migration context. + @classmethod + def batch_drop_column(cls, operations, column_name): + """Issue a "drop column" instruction using the current + batch migration context. - e.g.:: + .. seealso:: - from alembic import op - op.create_primary_key( - "pk_my_table", "my_table", - ["id", "version"] - ) - - This internally generates a :class:`~sqlalchemy.schema.Table` object - containing the necessary columns, then generates a new - :class:`~sqlalchemy.schema.PrimaryKeyConstraint` - object which it then associates with the - :class:`~sqlalchemy.schema.Table`. - Any event listeners associated with this action will be fired - off normally. The :class:`~sqlalchemy.schema.AddConstraint` - construct is ultimately used to generate the ALTER statement. - - :param name: Name of the primary key constraint. The name is necessary - so that an ALTER statement can be emitted. For setups that - use an automated naming scheme such as that described at - :ref:`sqla:constraint_naming_conventions` - ``name`` here can be ``None``, as the event listener will - apply the name to the constraint object when it is associated - with the table. - :param table_name: String name of the target table. - :param cols: a list of string column names to be applied to the - primary key constraint. - :param schema: Optional schema name to operate within. To control - quoting of the schema outside of the default behavior, use - the SQLAlchemy construct - :class:`~sqlalchemy.sql.elements.quoted_name`. - - .. versionadded:: 0.7.0 'schema' can now accept a - :class:`~sqlalchemy.sql.elements.quoted_name` construct. + :meth:`.Operations.drop_column` """ - self.impl.add_constraint( - self._primary_key_constraint(name, table_name, cols, - schema) - ) + op = cls( + operations.impl.table_name, column_name, + schema=operations.impl.schema) + return operations.invoke(op) - def create_foreign_key(self, name, source, referent, local_cols, - remote_cols, onupdate=None, ondelete=None, - deferrable=None, initially=None, match=None, - source_schema=None, referent_schema=None, - **dialect_kw): - """Issue a "create foreign key" instruction using the - current migration context. - e.g.:: +@Operations.register_operation("bulk_insert") +class BulkInsertOp(MigrateOperation): + """Represent a bulk insert operation.""" - from alembic import op - op.create_foreign_key( - "fk_user_address", "address", - "user", ["user_id"], ["id"]) + def __init__(self, table, rows, multiinsert=True): + self.table = table + self.rows = rows + self.multiinsert = multiinsert - This internally generates a :class:`~sqlalchemy.schema.Table` object - containing the necessary columns, then generates a new - :class:`~sqlalchemy.schema.ForeignKeyConstraint` - object which it then associates with the - :class:`~sqlalchemy.schema.Table`. - Any event listeners associated with this action will be fired - off normally. The :class:`~sqlalchemy.schema.AddConstraint` - construct is ultimately used to generate the ALTER statement. - - :param name: Name of the foreign key constraint. The name is necessary - so that an ALTER statement can be emitted. For setups that - use an automated naming scheme such as that described at - :ref:`sqla:constraint_naming_conventions`, - ``name`` here can be ``None``, as the event listener will - apply the name to the constraint object when it is associated - with the table. - :param source: String name of the source table. - :param referent: String name of the destination table. - :param local_cols: a list of string column names in the - source table. - :param remote_cols: a list of string column names in the - remote table. - :param onupdate: Optional string. If set, emit ON UPDATE when - issuing DDL for this constraint. Typical values include CASCADE, - DELETE and RESTRICT. - :param ondelete: Optional string. If set, emit ON DELETE when - issuing DDL for this constraint. Typical values include CASCADE, - DELETE and RESTRICT. - :param deferrable: optional bool. If set, emit DEFERRABLE or NOT - DEFERRABLE when issuing DDL for this constraint. - :param source_schema: Optional schema name of the source table. - :param referent_schema: Optional schema name of the destination table. - - """ - - self.impl.add_constraint( - self._foreign_key_constraint(name, source, referent, - local_cols, remote_cols, - onupdate=onupdate, ondelete=ondelete, - deferrable=deferrable, - source_schema=source_schema, - referent_schema=referent_schema, - initially=initially, match=match, - **dialect_kw) - ) - - def create_unique_constraint(self, name, source, local_cols, - schema=None, **kw): - """Issue a "create unique constraint" instruction using the - current migration context. - - e.g.:: - - from alembic import op - op.create_unique_constraint("uq_user_name", "user", ["name"]) - - This internally generates a :class:`~sqlalchemy.schema.Table` object - containing the necessary columns, then generates a new - :class:`~sqlalchemy.schema.UniqueConstraint` - object which it then associates with the - :class:`~sqlalchemy.schema.Table`. - Any event listeners associated with this action will be fired - off normally. The :class:`~sqlalchemy.schema.AddConstraint` - construct is ultimately used to generate the ALTER statement. - - :param name: Name of the unique constraint. The name is necessary - so that an ALTER statement can be emitted. For setups that - use an automated naming scheme such as that described at - :ref:`sqla:constraint_naming_conventions`, - ``name`` here can be ``None``, as the event listener will - apply the name to the constraint object when it is associated - with the table. - :param source: String name of the source table. Dotted schema names are - supported. - :param local_cols: a list of string column names in the - source table. - :param deferrable: optional bool. If set, emit DEFERRABLE or - NOT DEFERRABLE when issuing DDL for this constraint. - :param initially: optional string. If set, emit INITIALLY - when issuing DDL for this constraint. - :param schema: Optional schema name to operate within. To control - quoting of the schema outside of the default behavior, use - the SQLAlchemy construct - :class:`~sqlalchemy.sql.elements.quoted_name`. - - .. versionadded:: 0.7.0 'schema' can now accept a - :class:`~sqlalchemy.sql.elements.quoted_name` construct. - - """ - - self.impl.add_constraint( - self._unique_constraint(name, source, local_cols, - schema=schema, **kw) - ) - - def create_check_constraint(self, name, source, condition, - schema=None, **kw): - """Issue a "create check constraint" instruction using the - current migration context. - - e.g.:: - - from alembic import op - from sqlalchemy.sql import column, func - - op.create_check_constraint( - "ck_user_name_len", - "user", - func.len(column('name')) > 5 - ) - - CHECK constraints are usually against a SQL expression, so ad-hoc - table metadata is usually needed. The function will convert the given - arguments into a :class:`sqlalchemy.schema.CheckConstraint` bound - to an anonymous table in order to emit the CREATE statement. - - :param name: Name of the check constraint. The name is necessary - so that an ALTER statement can be emitted. For setups that - use an automated naming scheme such as that described at - :ref:`sqla:constraint_naming_conventions`, - ``name`` here can be ``None``, as the event listener will - apply the name to the constraint object when it is associated - with the table. - :param source: String name of the source table. - :param condition: SQL expression that's the condition of the - constraint. Can be a string or SQLAlchemy expression language - structure. - :param deferrable: optional bool. If set, emit DEFERRABLE or - NOT DEFERRABLE when issuing DDL for this constraint. - :param initially: optional string. If set, emit INITIALLY - when issuing DDL for this constraint. - :param schema: Optional schema name to operate within. To control - quoting of the schema outside of the default behavior, use - the SQLAlchemy construct - :class:`~sqlalchemy.sql.elements.quoted_name`. - - .. versionadded:: 0.7.0 'schema' can now accept a - :class:`~sqlalchemy.sql.elements.quoted_name` construct. - - """ - self.impl.add_constraint( - self._check_constraint( - name, source, condition, schema=schema, **kw) - ) - - def create_table(self, name, *columns, **kw): - """Issue a "create table" instruction using the current migration - context. - - This directive receives an argument list similar to that of the - traditional :class:`sqlalchemy.schema.Table` construct, but without the - metadata:: - - from sqlalchemy import INTEGER, VARCHAR, NVARCHAR, Column - from alembic import op - - op.create_table( - 'account', - Column('id', INTEGER, primary_key=True), - Column('name', VARCHAR(50), nullable=False), - Column('description', NVARCHAR(200)), - Column('timestamp', TIMESTAMP, server_default=func.now()) - ) - - Note that :meth:`.create_table` accepts - :class:`~sqlalchemy.schema.Column` - constructs directly from the SQLAlchemy library. In particular, - default values to be created on the database side are - specified using the ``server_default`` parameter, and not - ``default`` which only specifies Python-side defaults:: - - from alembic import op - from sqlalchemy import Column, TIMESTAMP, func - - # specify "DEFAULT NOW" along with the "timestamp" column - op.create_table('account', - Column('id', INTEGER, primary_key=True), - Column('timestamp', TIMESTAMP, server_default=func.now()) - ) - - The function also returns a newly created - :class:`~sqlalchemy.schema.Table` object, corresponding to the table - specification given, which is suitable for - immediate SQL operations, in particular - :meth:`.Operations.bulk_insert`:: - - from sqlalchemy import INTEGER, VARCHAR, NVARCHAR, Column - from alembic import op - - account_table = op.create_table( - 'account', - Column('id', INTEGER, primary_key=True), - Column('name', VARCHAR(50), nullable=False), - Column('description', NVARCHAR(200)), - Column('timestamp', TIMESTAMP, server_default=func.now()) - ) - - op.bulk_insert( - account_table, - [ - {"name": "A1", "description": "account 1"}, - {"name": "A2", "description": "account 2"}, - ] - ) - - .. versionadded:: 0.7.0 - - :param name: Name of the table - :param \*columns: collection of :class:`~sqlalchemy.schema.Column` - objects within - the table, as well as optional :class:`~sqlalchemy.schema.Constraint` - objects - and :class:`~.sqlalchemy.schema.Index` objects. - :param schema: Optional schema name to operate within. To control - quoting of the schema outside of the default behavior, use - the SQLAlchemy construct - :class:`~sqlalchemy.sql.elements.quoted_name`. - - .. versionadded:: 0.7.0 'schema' can now accept a - :class:`~sqlalchemy.sql.elements.quoted_name` construct. - :param \**kw: Other keyword arguments are passed to the underlying - :class:`sqlalchemy.schema.Table` object created for the command. - - :return: the :class:`~sqlalchemy.schema.Table` object corresponding - to the parameters given. - - .. versionadded:: 0.7.0 - the :class:`~sqlalchemy.schema.Table` - object is returned. - - """ - table = self._table(name, *columns, **kw) - self.impl.create_table(table) - return table - - def drop_table(self, name, **kw): - """Issue a "drop table" instruction using the current - migration context. - - - e.g.:: - - drop_table("accounts") - - :param name: Name of the table - :param schema: Optional schema name to operate within. To control - quoting of the schema outside of the default behavior, use - the SQLAlchemy construct - :class:`~sqlalchemy.sql.elements.quoted_name`. - - .. versionadded:: 0.7.0 'schema' can now accept a - :class:`~sqlalchemy.sql.elements.quoted_name` construct. - - :param \**kw: Other keyword arguments are passed to the underlying - :class:`sqlalchemy.schema.Table` object created for the command. - - """ - self.impl.drop_table( - self._table(name, **kw) - ) - - def create_index(self, name, table_name, columns, schema=None, - unique=False, quote=None, **kw): - """Issue a "create index" instruction using the current - migration context. - - e.g.:: - - from alembic import op - op.create_index('ik_test', 't1', ['foo', 'bar']) - - Functional indexes can be produced by using the - :func:`sqlalchemy.sql.expression.text` construct:: - - from alembic import op - from sqlalchemy import text - op.create_index('ik_test', 't1', [text('lower(foo)')]) - - .. versionadded:: 0.6.7 support for making use of the - :func:`~sqlalchemy.sql.expression.text` construct in - conjunction with - :meth:`.Operations.create_index` in - order to produce functional expressions within CREATE INDEX. - - :param name: name of the index. - :param table_name: name of the owning table. - :param columns: a list consisting of string column names and/or - :func:`~sqlalchemy.sql.expression.text` constructs. - :param schema: Optional schema name to operate within. To control - quoting of the schema outside of the default behavior, use - the SQLAlchemy construct - :class:`~sqlalchemy.sql.elements.quoted_name`. - - .. versionadded:: 0.7.0 'schema' can now accept a - :class:`~sqlalchemy.sql.elements.quoted_name` construct. - - :param unique: If True, create a unique index. - - :param quote: - Force quoting of this column's name on or off, corresponding - to ``True`` or ``False``. When left at its default - of ``None``, the column identifier will be quoted according to - whether the name is case sensitive (identifiers with at least one - upper case character are treated as case sensitive), or if it's a - reserved word. This flag is only needed to force quoting of a - reserved word which is not known by the SQLAlchemy dialect. - - :param \**kw: Additional keyword arguments not mentioned above are - dialect specific, and passed in the form ``_``. - See the documentation regarding an individual dialect at - :ref:`dialect_toplevel` for detail on documented arguments. - """ - - self.impl.create_index( - self._index(name, table_name, columns, schema=schema, - unique=unique, quote=quote, **kw) - ) - - @util._with_legacy_names([('tablename', 'table_name')]) - def drop_index(self, name, table_name=None, schema=None): - """Issue a "drop index" instruction using the current - migration context. - - e.g.:: - - drop_index("accounts") - - :param name: name of the index. - :param table_name: name of the owning table. Some - backends such as Microsoft SQL Server require this. - :param schema: Optional schema name to operate within. To control - quoting of the schema outside of the default behavior, use - the SQLAlchemy construct - :class:`~sqlalchemy.sql.elements.quoted_name`. - - .. versionadded:: 0.7.0 'schema' can now accept a - :class:`~sqlalchemy.sql.elements.quoted_name` construct. - - """ - # need a dummy column name here since SQLAlchemy - # 0.7.6 and further raises on Index with no columns - self.impl.drop_index( - self._index(name, table_name, ['x'], schema=schema) - ) - - @util._with_legacy_names([("type", "type_")]) - def drop_constraint(self, name, table_name, type_=None, schema=None): - """Drop a constraint of the given name, typically via DROP CONSTRAINT. - - :param name: name of the constraint. - :param table_name: table name. - :param ``type_``: optional, required on MySQL. can be - 'foreignkey', 'primary', 'unique', or 'check'. - :param schema: Optional schema name to operate within. To control - quoting of the schema outside of the default behavior, use - the SQLAlchemy construct - :class:`~sqlalchemy.sql.elements.quoted_name`. - - .. versionadded:: 0.7.0 'schema' can now accept a - :class:`~sqlalchemy.sql.elements.quoted_name` construct. - - """ - - t = self._table(table_name, schema=schema) - types = { - 'foreignkey': lambda name: sa_schema.ForeignKeyConstraint( - [], [], name=name), - 'primary': sa_schema.PrimaryKeyConstraint, - 'unique': sa_schema.UniqueConstraint, - 'check': lambda name: sa_schema.CheckConstraint("", name=name), - None: sa_schema.Constraint - } - try: - const = types[type_] - except KeyError: - raise TypeError("'type' can be one of %s" % - ", ".join(sorted(repr(x) for x in types))) - - const = const(name=name) - t.append_constraint(const) - self.impl.drop_constraint(const) - - def bulk_insert(self, table, rows, multiinsert=True): + @classmethod + def bulk_insert(cls, operations, table, rows, multiinsert=True): """Issue a "bulk insert" operation using the current migration context. @@ -1174,53 +1415,21 @@ class Operations(object): .. versionadded:: 0.6.4 """ - self.impl.bulk_insert(table, rows, multiinsert=multiinsert) - def inline_literal(self, value, type_=None): - """Produce an 'inline literal' expression, suitable for - using in an INSERT, UPDATE, or DELETE statement. + op = cls(table, rows, multiinsert=multiinsert) + operations.invoke(op) - When using Alembic in "offline" mode, CRUD operations - aren't compatible with SQLAlchemy's default behavior surrounding - literal values, - which is that they are converted into bound values and passed - separately into the ``execute()`` method of the DBAPI cursor. - An offline SQL - script needs to have these rendered inline. While it should - always be noted that inline literal values are an **enormous** - security hole in an application that handles untrusted input, - a schema migration is not run in this context, so - literals are safe to render inline, with the caveat that - advanced types like dates may not be supported directly - by SQLAlchemy. - See :meth:`.execute` for an example usage of - :meth:`.inline_literal`. +@Operations.register_operation("execute") +class ExecuteSQLOp(MigrateOperation): + """Represent an execute SQL operation.""" - The environment can also be configured to attempt to render - "literal" values inline automatically, for those simple types - that are supported by the dialect; see - :paramref:`.EnvironmentContext.configure.literal_binds` for this - more recently added feature. + def __init__(self, sqltext, execution_options=None): + self.sqltext = sqltext + self.execution_options = execution_options - :param value: The value to render. Strings, integers, and simple - numerics should be supported. Other types like boolean, - dates, etc. may or may not be supported yet by various - backends. - :param ``type_``: optional - a :class:`sqlalchemy.types.TypeEngine` - subclass stating the type of this value. In SQLAlchemy - expressions, this is usually derived automatically - from the Python type of the value itself, as well as - based on the context in which the value is used. - - .. seealso:: - - :paramref:`.EnvironmentContext.configure.literal_binds` - - """ - return impl._literal_bindparam(None, value, type_=type_) - - def execute(self, sql, execution_options=None): + @classmethod + def execute(cls, operations, sqltext, execution_options=None): """Execute the given SQL using the current migration context. In a SQL script context, the statement is emitted directly to the @@ -1283,177 +1492,74 @@ class Operations(object): execution options, will be passed to :meth:`sqlalchemy.engine.Connection.execution_options`. """ - self.migration_context.impl.execute( - sql, - execution_options=execution_options) - - def get_bind(self): - """Return the current 'bind'. - - Under normal circumstances, this is the - :class:`~sqlalchemy.engine.Connection` currently being used - to emit SQL to the database. - - In a SQL script context, this value is ``None``. [TODO: verify this] - - """ - return self.migration_context.impl.bind + op = cls(sqltext, execution_options=execution_options) + return operations.invoke(op) -class BatchOperations(Operations): - """Modifies the interface :class:`.Operations` for batch mode. +class OpContainer(MigrateOperation): + """Represent a sequence of operations operation.""" + def __init__(self, ops=()): + self.ops = ops - This basically omits the ``table_name`` and ``schema`` parameters - from associated methods, as these are a given when running under batch - mode. + +class ModifyTableOps(OpContainer): + """Contains a sequence of operations that all apply to a single Table.""" + + def __init__(self, table_name, ops, schema=None): + super(ModifyTableOps, self).__init__(ops) + self.table_name = table_name + self.schema = schema + + +class UpgradeOps(OpContainer): + """contains a sequence of operations that would apply to the + 'upgrade' stream of a script. .. seealso:: - :meth:`.Operations.batch_alter_table` + :ref:`customizing_revision` """ - def _noop(self, operation): - raise NotImplementedError( - "The %s method does not apply to a batch table alter operation." - % operation) - def add_column(self, column): - """Issue an "add column" instruction using the current - batch migration context. +class DowngradeOps(OpContainer): + """contains a sequence of operations that would apply to the + 'downgrade' stream of a script. - .. seealso:: + .. seealso:: - :meth:`.Operations.add_column` + :ref:`customizing_revision` - """ + """ - return super(BatchOperations, self).add_column( - self.impl.table_name, column, schema=self.impl.schema) - def alter_column(self, column_name, **kw): - """Issue an "alter column" instruction using the current - batch migration context. +class MigrationScript(MigrateOperation): + """represents a migration script. - .. seealso:: + E.g. when autogenerate encounters this object, this corresponds to the + production of an actual script file. - :meth:`.Operations.add_column` + A normal :class:`.MigrationScript` object would contain a single + :class:`.UpgradeOps` and a single :class:`.DowngradeOps` directive. - """ - kw['schema'] = self.impl.schema - return super(BatchOperations, self).alter_column( - self.impl.table_name, column_name, **kw) + .. seealso:: - def drop_column(self, column_name): - """Issue a "drop column" instruction using the current - batch migration context. + :ref:`customizing_revision` - .. seealso:: + """ - :meth:`.Operations.drop_column` + def __init__( + self, rev_id, upgrade_ops, downgrade_ops, + message=None, + imports=None, head=None, splice=None, + branch_label=None, version_path=None): + self.rev_id = rev_id + self.message = message + self.imports = imports + self.head = head + self.splice = splice + self.branch_label = branch_label + self.version_path = version_path + self.upgrade_ops = upgrade_ops + self.downgrade_ops = downgrade_ops - """ - return super(BatchOperations, self).drop_column( - self.impl.table_name, column_name, schema=self.impl.schema) - - def create_primary_key(self, name, cols): - """Issue a "create primary key" instruction using the - current batch migration context. - - The batch form of this call omits the ``table_name`` and ``schema`` - arguments from the call. - - .. seealso:: - - :meth:`.Operations.create_primary_key` - - """ - raise NotImplementedError("not yet implemented") - - def create_foreign_key( - self, name, referent, local_cols, remote_cols, **kw): - """Issue a "create foreign key" instruction using the - current batch migration context. - - The batch form of this call omits the ``source`` and ``source_schema`` - arguments from the call. - - e.g.:: - - with batch_alter_table("address") as batch_op: - batch_op.create_foreign_key( - "fk_user_address", - "user", ["user_id"], ["id"]) - - .. seealso:: - - :meth:`.Operations.create_foreign_key` - - """ - return super(BatchOperations, self).create_foreign_key( - name, self.impl.table_name, referent, local_cols, remote_cols, - source_schema=self.impl.schema, **kw) - - def create_unique_constraint(self, name, local_cols, **kw): - """Issue a "create unique constraint" instruction using the - current batch migration context. - - The batch form of this call omits the ``source`` and ``schema`` - arguments from the call. - - .. seealso:: - - :meth:`.Operations.create_unique_constraint` - - """ - kw['schema'] = self.impl.schema - return super(BatchOperations, self).create_unique_constraint( - name, self.impl.table_name, local_cols, **kw) - - def create_check_constraint(self, name, condition, **kw): - """Issue a "create check constraint" instruction using the - current batch migration context. - - The batch form of this call omits the ``source`` and ``schema`` - arguments from the call. - - .. seealso:: - - :meth:`.Operations.create_check_constraint` - - """ - raise NotImplementedError("not yet implemented") - - def drop_constraint(self, name, type_=None): - """Issue a "drop constraint" instruction using the - current batch migration context. - - The batch form of this call omits the ``table_name`` and ``schema`` - arguments from the call. - - .. seealso:: - - :meth:`.Operations.drop_constraint` - - """ - return super(BatchOperations, self).drop_constraint( - name, self.impl.table_name, type_=type_, - schema=self.impl.schema) - - def create_index(self, name, columns, **kw): - """Issue a "create index" instruction using the - current batch migration context.""" - - kw['schema'] = self.impl.schema - - return super(BatchOperations, self).create_index( - name, self.impl.table_name, columns, **kw) - - def drop_index(self, name, **kw): - """Issue a "drop index" instruction using the - current batch migration context.""" - - kw['schema'] = self.impl.schema - - return super(BatchOperations, self).drop_index( - name, self.impl.table_name, **kw) diff --git a/alembic/operations/schemaobj.py b/alembic/operations/schemaobj.py new file mode 100644 index 0000000..b590aca --- /dev/null +++ b/alembic/operations/schemaobj.py @@ -0,0 +1,157 @@ +from sqlalchemy import schema as sa_schema +from sqlalchemy.types import NULLTYPE, Integer +from ..util.compat import string_types +from .. import util + + +class SchemaObjects(object): + + def __init__(self, migration_context=None): + self.migration_context = migration_context + + def primary_key_constraint(self, name, table_name, cols, schema=None): + m = self.metadata() + columns = [sa_schema.Column(n, NULLTYPE) for n in cols] + t1 = sa_schema.Table(table_name, m, + *columns, + schema=schema) + p = sa_schema.PrimaryKeyConstraint(*columns, name=name) + t1.append_constraint(p) + return p + + def foreign_key_constraint( + self, name, source, referent, + local_cols, remote_cols, + onupdate=None, ondelete=None, + deferrable=None, source_schema=None, + referent_schema=None, initially=None, + match=None, **dialect_kw): + m = self.metadata() + if source == referent: + t1_cols = local_cols + remote_cols + else: + t1_cols = local_cols + sa_schema.Table( + referent, m, + *[sa_schema.Column(n, NULLTYPE) for n in remote_cols], + schema=referent_schema) + + t1 = sa_schema.Table( + source, m, + *[sa_schema.Column(n, NULLTYPE) for n in t1_cols], + schema=source_schema) + + tname = "%s.%s" % (referent_schema, referent) if referent_schema \ + else referent + + if util.sqla_08: + # "match" kw unsupported in 0.7 + dialect_kw['match'] = match + + f = sa_schema.ForeignKeyConstraint(local_cols, + ["%s.%s" % (tname, n) + for n in remote_cols], + name=name, + onupdate=onupdate, + ondelete=ondelete, + deferrable=deferrable, + initially=initially, + **dialect_kw + ) + t1.append_constraint(f) + + return f + + def unique_constraint(self, name, source, local_cols, schema=None, **kw): + t = sa_schema.Table( + source, self.metadata(), + *[sa_schema.Column(n, NULLTYPE) for n in local_cols], + schema=schema) + kw['name'] = name + uq = sa_schema.UniqueConstraint(*[t.c[n] for n in local_cols], **kw) + # TODO: need event tests to ensure the event + # is fired off here + t.append_constraint(uq) + return uq + + def check_constraint(self, name, source, condition, schema=None, **kw): + t = sa_schema.Table(source, self.metadata(), + sa_schema.Column('x', Integer), schema=schema) + ck = sa_schema.CheckConstraint(condition, name=name, **kw) + t.append_constraint(ck) + return ck + + def generic_constraint(self, name, table_name, type_, schema=None, **kw): + t = self.table(table_name, schema=schema) + types = { + 'foreignkey': lambda name: sa_schema.ForeignKeyConstraint( + [], [], name=name), + 'primary': sa_schema.PrimaryKeyConstraint, + 'unique': sa_schema.UniqueConstraint, + 'check': lambda name: sa_schema.CheckConstraint("", name=name), + None: sa_schema.Constraint + } + try: + const = types[type_] + except KeyError: + raise TypeError("'type' can be one of %s" % + ", ".join(sorted(repr(x) for x in types))) + else: + const = const(name=name) + t.append_constraint(const) + return const + + def metadata(self): + kw = {} + if self.migration_context is not None and \ + 'target_metadata' in self.migration_context.opts: + mt = self.migration_context.opts['target_metadata'] + if hasattr(mt, 'naming_convention'): + kw['naming_convention'] = mt.naming_convention + return sa_schema.MetaData(**kw) + + def table(self, name, *columns, **kw): + m = self.metadata() + t = sa_schema.Table(name, m, *columns, **kw) + for f in t.foreign_keys: + self._ensure_table_for_fk(m, f) + return t + + def column(self, name, type_, **kw): + return sa_schema.Column(name, type_, **kw) + + def index(self, name, tablename, columns, schema=None, **kw): + t = sa_schema.Table( + tablename or 'no_table', self.metadata(), + schema=schema + ) + idx = sa_schema.Index( + name, + *[util.sqla_compat._textual_index_column(t, n) for n in columns], + **kw) + return idx + + def _parse_table_key(self, table_key): + if '.' in table_key: + tokens = table_key.split('.') + sname = ".".join(tokens[0:-1]) + tname = tokens[-1] + else: + tname = table_key + sname = None + return (sname, tname) + + def _ensure_table_for_fk(self, metadata, fk): + """create a placeholder Table object for the referent of a + ForeignKey. + + """ + if isinstance(fk._colspec, string_types): + table_key, cname = fk._colspec.rsplit('.', 1) + sname, tname = self._parse_table_key(table_key) + if table_key not in metadata.tables: + rel_t = sa_schema.Table(tname, metadata, schema=sname) + else: + rel_t = metadata.tables[table_key] + if cname not in rel_t.c: + rel_t.append_column(sa_schema.Column(cname, NULLTYPE)) diff --git a/alembic/operations/toimpl.py b/alembic/operations/toimpl.py new file mode 100644 index 0000000..1327367 --- /dev/null +++ b/alembic/operations/toimpl.py @@ -0,0 +1,162 @@ +from . import ops + +from . import Operations +from sqlalchemy import schema as sa_schema + + +@Operations.implementation_for(ops.AlterColumnOp) +def alter_column(operations, operation): + + compiler = operations.impl.dialect.statement_compiler( + operations.impl.dialect, + None + ) + + existing_type = operation.existing_type + existing_nullable = operation.existing_nullable + existing_server_default = operation.existing_server_default + type_ = operation.modify_type + column_name = operation.column_name + table_name = operation.table_name + schema = operation.schema + server_default = operation.modify_server_default + new_column_name = operation.modify_name + nullable = operation.modify_nullable + + def _count_constraint(constraint): + return not isinstance( + constraint, + sa_schema.PrimaryKeyConstraint) and \ + (not constraint._create_rule or + constraint._create_rule(compiler)) + + if existing_type and type_: + t = operations.schema_obj.table( + table_name, + sa_schema.Column(column_name, existing_type), + schema=schema + ) + for constraint in t.constraints: + if _count_constraint(constraint): + operations.impl.drop_constraint(constraint) + + operations.impl.alter_column( + table_name, column_name, + nullable=nullable, + server_default=server_default, + name=new_column_name, + type_=type_, + schema=schema, + existing_type=existing_type, + existing_server_default=existing_server_default, + existing_nullable=existing_nullable, + **operation.kw + ) + + if type_: + t = operations.schema_obj.table( + table_name, + operations.schema_obj.column(column_name, type_), + schema=schema + ) + for constraint in t.constraints: + if _count_constraint(constraint): + operations.impl.add_constraint(constraint) + + +@Operations.implementation_for(ops.DropTableOp) +def drop_table(operations, operation): + operations.impl.drop_table( + operation.to_table(operations.migration_context) + ) + + +@Operations.implementation_for(ops.DropColumnOp) +def drop_column(operations, operation): + column = operation.to_column(operations.migration_context) + operations.impl.drop_column( + operation.table_name, + column, + schema=operation.schema, + **operation.kw + ) + + +@Operations.implementation_for(ops.CreateIndexOp) +def create_index(operations, operation): + idx = operation.to_index(operations.migration_context) + operations.impl.create_index(idx) + + +@Operations.implementation_for(ops.DropIndexOp) +def drop_index(operations, operation): + operations.impl.drop_index( + operation.to_index(operations.migration_context) + ) + + +@Operations.implementation_for(ops.CreateTableOp) +def create_table(operations, operation): + table = operation.to_table(operations.migration_context) + operations.impl.create_table(table) + return table + + +@Operations.implementation_for(ops.RenameTableOp) +def rename_table(operations, operation): + operations.impl.rename_table( + operation.table_name, + operation.new_table_name, + schema=operation.schema) + + +@Operations.implementation_for(ops.AddColumnOp) +def add_column(operations, operation): + table_name = operation.table_name + column = operation.column + schema = operation.schema + + t = operations.schema_obj.table(table_name, column, schema=schema) + operations.impl.add_column( + table_name, + column, + schema=schema + ) + for constraint in t.constraints: + if not isinstance(constraint, sa_schema.PrimaryKeyConstraint): + operations.impl.add_constraint(constraint) + for index in t.indexes: + operations.impl.create_index(index) + + +@Operations.implementation_for(ops.AddConstraintOp) +def create_constraint(operations, operation): + operations.impl.add_constraint( + operation.to_constraint(operations.migration_context) + ) + + +@Operations.implementation_for(ops.DropConstraintOp) +def drop_constraint(operations, operation): + operations.impl.drop_constraint( + operations.schema_obj.generic_constraint( + operation.constraint_name, + operation.table_name, + operation.constraint_type, + schema=operation.schema, + ) + ) + + +@Operations.implementation_for(ops.BulkInsertOp) +def bulk_insert(operations, operation): + operations.impl.bulk_insert( + operation.table, operation.rows, multiinsert=operation.multiinsert) + + +@Operations.implementation_for(ops.ExecuteSQLOp) +def execute_sql(operations, operation): + operations.migration_context.impl.execute( + operation.sqltext, + execution_options=operation.execution_options + ) diff --git a/alembic/runtime/__init__.py b/alembic/runtime/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/alembic/environment.py b/alembic/runtime/environment.py similarity index 94% rename from alembic/environment.py rename to alembic/runtime/environment.py index 860315b..3b04fea 100644 --- a/alembic/environment.py +++ b/alembic/runtime/environment.py @@ -1,9 +1,9 @@ -from .operations import Operations +from ..operations import Operations from .migration import MigrationContext -from . import util +from .. import util -class EnvironmentContext(object): +class EnvironmentContext(util.ModuleClsProxy): """Represent the state made available to an ``env.py`` script. @@ -96,14 +96,11 @@ class EnvironmentContext(object): be made available as ``from alembic import context``. """ - from .context import _install_proxy - _install_proxy(self) + self._install_proxy() return self def __exit__(self, *arg, **kw): - from . import context, op - context._remove_proxy() - op._remove_proxy() + self._remove_proxy() def is_offline_mode(self): """Return True if the current migrations environment @@ -293,6 +290,7 @@ class EnvironmentContext(object): include_symbol=None, include_object=None, include_schemas=False, + process_revision_directives=None, compare_type=False, compare_server_default=False, render_item=None, @@ -656,6 +654,43 @@ class EnvironmentContext(object): :ref:`autogen_module_prefix` + :param process_revision_directives: a callable function that will + be passed a structure representing the end result of an autogenerate + or plain "revision" operation, which can be manipulated to affect + how the ``alembic revision`` command ultimately outputs new + revision scripts. The structure of the callable is:: + + def process_revision_directives(context, revision, directives): + pass + + The ``directives`` parameter is a Python list containing + a single :class:`.MigrationScript` directive, which represents + the revision file to be generated. This list as well as its + contents may be freely modified to produce any set of commands. + The section :ref:`customizing_revision` shows an example of + doing this. The ``context`` parameter is the + :class:`.MigrationContext` in use, + and ``revision`` is a tuple of revision identifiers representing the + current revision of the database. + + The callable is invoked at all times when the ``--autogenerate`` + option is passed to ``alembic revision``. If ``--autogenerate`` + is not passed, the callable is invoked only if the + ``revision_environment`` variable is set to True in the Alembic + configuration, in which case the given ``directives`` collection + will contain empty :class:`.UpgradeOps` and :class:`.DowngradeOps` + collections for ``.upgrade_ops`` and ``.downgrade_ops``. The + ``--autogenerate`` option itself can be inferred by inspecting + ``context.config.cmd_opts.autogenerate``. + + + .. versionadded:: 0.8.0 + + .. seealso:: + + :ref:`customizing_revision` + + Parameters specific to individual backends: :param mssql_batch_separator: The "batch separator" which will @@ -696,6 +731,8 @@ class EnvironmentContext(object): opts['alembic_module_prefix'] = alembic_module_prefix opts['user_module_prefix'] = user_module_prefix opts['literal_binds'] = literal_binds + opts['process_revision_directives'] = process_revision_directives + if render_item is not None: opts['render_item'] = render_item if compare_type is not None: diff --git a/alembic/migration.py b/alembic/runtime/migration.py similarity index 99% rename from alembic/migration.py rename to alembic/runtime/migration.py index 9b46052..84a3c7f 100644 --- a/alembic/migration.py +++ b/alembic/runtime/migration.py @@ -6,8 +6,8 @@ from sqlalchemy import MetaData, Table, Column, String, literal_column from sqlalchemy.engine.strategies import MockEngineStrategy from sqlalchemy.engine import url as sqla_url -from .compat import callable, EncodedIO -from . import ddl, util +from ..util.compat import callable, EncodedIO +from .. import ddl, util log = logging.getLogger(__name__) diff --git a/alembic/script/__init__.py b/alembic/script/__init__.py new file mode 100644 index 0000000..cae294f --- /dev/null +++ b/alembic/script/__init__.py @@ -0,0 +1,3 @@ +from .base import ScriptDirectory, Script # noqa + +__all__ = ['ScriptDirectory', 'Script'] diff --git a/alembic/script.py b/alembic/script/base.py similarity index 99% rename from alembic/script.py rename to alembic/script/base.py index 095a04b..e30c8b2 100644 --- a/alembic/script.py +++ b/alembic/script/base.py @@ -2,10 +2,10 @@ import datetime import os import re import shutil -from . import util -from . import compat +from .. import util +from ..util import compat from . import revision -from . import migration +from ..runtime import migration from contextlib import contextmanager diff --git a/alembic/revision.py b/alembic/script/revision.py similarity index 99% rename from alembic/revision.py rename to alembic/script/revision.py index 4eea514..e9958b1 100644 --- a/alembic/revision.py +++ b/alembic/script/revision.py @@ -1,10 +1,9 @@ import re import collections -import itertools -from . import util +from .. import util from sqlalchemy import util as sqlautil -from . import compat +from ..util import compat _relative_destination = re.compile(r'(?:(.+?)@)?(\w+)?((?:\+|-)\d+)') diff --git a/alembic/testing/assertions.py b/alembic/testing/assertions.py index b3a5acd..6acca21 100644 --- a/alembic/testing/assertions.py +++ b/alembic/testing/assertions.py @@ -2,9 +2,9 @@ from __future__ import absolute_import import re -from alembic import util +from .. import util from sqlalchemy.engine import default -from alembic.compat import text_type, py3k +from ..util.compat import text_type, py3k import contextlib from sqlalchemy.util import decorator from sqlalchemy import exc as sa_exc diff --git a/alembic/testing/env.py b/alembic/testing/env.py index 9c53d5d..f8ad447 100644 --- a/alembic/testing/env.py +++ b/alembic/testing/env.py @@ -4,9 +4,9 @@ import os import shutil import textwrap -from alembic.compat import u -from alembic.script import Script, ScriptDirectory -from alembic import util +from ..util.compat import u +from ..script import Script, ScriptDirectory +from .. import util from . import engines from . import provision diff --git a/alembic/testing/exclusions.py b/alembic/testing/exclusions.py index 88df9fc..90f8bc6 100644 --- a/alembic/testing/exclusions.py +++ b/alembic/testing/exclusions.py @@ -14,11 +14,12 @@ from .plugin.plugin_base import SkipTest from sqlalchemy.util import decorator from . import config from sqlalchemy import util -from alembic import compat +from ..util import compat import inspect import contextlib from .compat import get_url_driver_name, get_url_backend_name + def skip_if(predicate, reason=None): rule = compound() pred = _as_predicate(predicate, reason) diff --git a/alembic/testing/fixtures.py b/alembic/testing/fixtures.py index ae25fd2..7e05525 100644 --- a/alembic/testing/fixtures.py +++ b/alembic/testing/fixtures.py @@ -5,13 +5,12 @@ import re from sqlalchemy import create_engine, text, MetaData import alembic -from alembic.compat import configparser -from alembic import util -from alembic.compat import string_types, text_type -from alembic.migration import MigrationContext -from alembic.environment import EnvironmentContext -from alembic.operations import Operations -from alembic.ddl.impl import _impls +from ..util.compat import configparser +from .. import util +from ..util.compat import string_types, text_type +from ..migration import MigrationContext +from ..environment import EnvironmentContext +from ..operations import Operations from contextlib import contextmanager from .plugin.plugin_base import SkipTest from .assertions import _get_dialect, eq_ diff --git a/alembic/testing/mock.py b/alembic/testing/mock.py index cdfcb88..b82a404 100644 --- a/alembic/testing/mock.py +++ b/alembic/testing/mock.py @@ -12,7 +12,7 @@ """ from __future__ import absolute_import -from alembic.compat import py33 +from ..util.compat import py33 if py33: from unittest.mock import MagicMock, Mock, call, patch diff --git a/alembic/testing/provision.py b/alembic/testing/provision.py index 801d36b..37ae141 100644 --- a/alembic/testing/provision.py +++ b/alembic/testing/provision.py @@ -3,9 +3,9 @@ """ from sqlalchemy.engine import url as sa_url from sqlalchemy import text -from alembic import compat -from alembic.testing import config, engines -from alembic.testing.compat import get_url_backend_name +from ..util import compat +from . import config, engines +from .compat import get_url_backend_name FOLLOWER_IDENT = None diff --git a/alembic/util.py b/alembic/util.py deleted file mode 100644 index 2e0f731..0000000 --- a/alembic/util.py +++ /dev/null @@ -1,405 +0,0 @@ -import sys -import os -import textwrap -import warnings -import re -import inspect -import uuid -import collections - -from mako.template import Template -from sqlalchemy.engine import url -from sqlalchemy import __version__ - -from .compat import callable, exec_, load_module_py, load_module_pyc, \ - binary_type, string_types, py27 - - -class CommandError(Exception): - pass - - -def _safe_int(value): - try: - return int(value) - except: - return value -_vers = tuple( - [_safe_int(x) for x in re.findall(r'(\d+|[abc]\d)', __version__)]) -sqla_07 = _vers > (0, 7, 2) -sqla_079 = _vers >= (0, 7, 9) -sqla_08 = _vers >= (0, 8, 0) -sqla_083 = _vers >= (0, 8, 3) -sqla_084 = _vers >= (0, 8, 4) -sqla_09 = _vers >= (0, 9, 0) -sqla_092 = _vers >= (0, 9, 2) -sqla_094 = _vers >= (0, 9, 4) -sqla_094 = _vers >= (0, 9, 4) -sqla_099 = _vers >= (0, 9, 9) -sqla_100 = _vers >= (1, 0, 0) -sqla_105 = _vers >= (1, 0, 5) -if not sqla_07: - raise CommandError( - "SQLAlchemy 0.7.3 or greater is required. ") - -from sqlalchemy.util import format_argspec_plus, update_wrapper -from sqlalchemy.util.compat import inspect_getfullargspec - -import logging -log = logging.getLogger(__name__) - -if py27: - # disable "no handler found" errors - logging.getLogger('alembic').addHandler(logging.NullHandler()) - - -try: - import fcntl - import termios - import struct - ioctl = fcntl.ioctl(0, termios.TIOCGWINSZ, - struct.pack('HHHH', 0, 0, 0, 0)) - _h, TERMWIDTH, _hp, _wp = struct.unpack('HHHH', ioctl) - if TERMWIDTH <= 0: # can occur if running in emacs pseudo-tty - TERMWIDTH = None -except (ImportError, IOError): - TERMWIDTH = None - - -def template_to_file(template_file, dest, output_encoding, **kw): - with open(dest, 'wb') as f: - template = Template(filename=template_file) - f.write( - template.render_unicode(**kw).encode(output_encoding) - ) - - -def create_module_class_proxy(cls, globals_, locals_): - """Create module level proxy functions for the - methods on a given class. - - The functions will have a compatible signature - as the methods. A proxy is established - using the ``_install_proxy(obj)`` function, - and removed using ``_remove_proxy()``, both - installed by calling this function. - - """ - attr_names = set() - - def _install_proxy(obj): - globals_['_proxy'] = obj - for name in attr_names: - globals_[name] = getattr(obj, name) - - def _remove_proxy(): - globals_['_proxy'] = None - for name in attr_names: - del globals_[name] - - globals_['_install_proxy'] = _install_proxy - globals_['_remove_proxy'] = _remove_proxy - - def _create_op_proxy(name): - fn = getattr(cls, name) - spec = inspect.getargspec(fn) - if spec[0] and spec[0][0] == 'self': - spec[0].pop(0) - args = inspect.formatargspec(*spec) - num_defaults = 0 - if spec[3]: - num_defaults += len(spec[3]) - name_args = spec[0] - if num_defaults: - defaulted_vals = name_args[0 - num_defaults:] - else: - defaulted_vals = () - - apply_kw = inspect.formatargspec( - name_args, spec[1], spec[2], - defaulted_vals, - formatvalue=lambda x: '=' + x) - - def _name_error(name): - raise NameError( - "Can't invoke function '%s', as the proxy object has " - "not yet been " - "established for the Alembic '%s' class. " - "Try placing this code inside a callable." % ( - name, cls.__name__ - )) - globals_['_name_error'] = _name_error - - func_text = textwrap.dedent("""\ - def %(name)s(%(args)s): - %(doc)r - try: - p = _proxy - except NameError: - _name_error('%(name)s') - return _proxy.%(name)s(%(apply_kw)s) - e - """ % { - 'name': name, - 'args': args[1:-1], - 'apply_kw': apply_kw[1:-1], - 'doc': fn.__doc__, - }) - lcl = {} - exec_(func_text, globals_, lcl) - return lcl[name] - - for methname in dir(cls): - if not methname.startswith('_'): - if callable(getattr(cls, methname)): - locals_[methname] = _create_op_proxy(methname) - else: - attr_names.add(methname) - - -def write_outstream(stream, *text): - encoding = getattr(stream, 'encoding', 'ascii') or 'ascii' - for t in text: - if not isinstance(t, binary_type): - t = t.encode(encoding, 'replace') - t = t.decode(encoding) - try: - stream.write(t) - except IOError: - # suppress "broken pipe" errors. - # no known way to handle this on Python 3 however - # as the exception is "ignored" (noisily) in TextIOWrapper. - break - - -def coerce_resource_to_filename(fname): - """Interpret a filename as either a filesystem location or as a package - resource. - - Names that are non absolute paths and contain a colon - are interpreted as resources and coerced to a file location. - - """ - if not os.path.isabs(fname) and ":" in fname: - import pkg_resources - fname = pkg_resources.resource_filename(*fname.split(':')) - return fname - - -def status(_statmsg, fn, *arg, **kw): - msg(_statmsg + " ...", False) - try: - ret = fn(*arg, **kw) - write_outstream(sys.stdout, " done\n") - return ret - except: - write_outstream(sys.stdout, " FAILED\n") - raise - - -def err(message): - log.error(message) - msg("FAILED: %s" % message) - sys.exit(-1) - - -def obfuscate_url_pw(u): - u = url.make_url(u) - if u.password: - u.password = 'XXXXX' - return str(u) - - -def asbool(value): - return value is not None and \ - value.lower() == 'true' - - -def warn(msg): - warnings.warn(msg) - - -def msg(msg, newline=True): - if TERMWIDTH is None: - write_outstream(sys.stdout, msg) - if newline: - write_outstream(sys.stdout, "\n") - else: - # left indent output lines - lines = textwrap.wrap(msg, TERMWIDTH) - if len(lines) > 1: - for line in lines[0:-1]: - write_outstream(sys.stdout, " ", line, "\n") - write_outstream(sys.stdout, " ", lines[-1], ("\n" if newline else "")) - - -def load_python_file(dir_, filename): - """Load a file from the given path as a Python module.""" - - module_id = re.sub(r'\W', "_", filename) - path = os.path.join(dir_, filename) - _, ext = os.path.splitext(filename) - if ext == ".py": - if os.path.exists(path): - module = load_module_py(module_id, path) - elif os.path.exists(simple_pyc_file_from_path(path)): - # look for sourceless load - module = load_module_pyc( - module_id, simple_pyc_file_from_path(path)) - else: - raise ImportError("Can't find Python file %s" % path) - elif ext in (".pyc", ".pyo"): - module = load_module_pyc(module_id, path) - del sys.modules[module_id] - return module - - -def simple_pyc_file_from_path(path): - """Given a python source path, return the so-called - "sourceless" .pyc or .pyo path. - - This just a .pyc or .pyo file where the .py file would be. - - Even with PEP-3147, which normally puts .pyc/.pyo files in __pycache__, - this use case remains supported as a so-called "sourceless module import". - - """ - if sys.flags.optimize: - return path + "o" # e.g. .pyo - else: - return path + "c" # e.g. .pyc - - -def pyc_file_from_path(path): - """Given a python source path, locate the .pyc. - - See http://www.python.org/dev/peps/pep-3147/ - #detecting-pep-3147-availability - http://www.python.org/dev/peps/pep-3147/#file-extension-checks - - """ - import imp - has3147 = hasattr(imp, 'get_tag') - if has3147: - return imp.cache_from_source(path) - else: - return simple_pyc_file_from_path(path) - - -def rev_id(): - val = int(uuid.uuid4()) % 100000000000000 - return hex(val)[2:-1] - - -def to_tuple(x, default=None): - if x is None: - return default - elif isinstance(x, string_types): - return (x, ) - elif isinstance(x, collections.Iterable): - return tuple(x) - else: - raise ValueError("Don't know how to turn %r into a tuple" % x) - - -def format_as_comma(value): - if value is None: - return "" - elif isinstance(value, string_types): - return value - elif isinstance(value, collections.Iterable): - return ", ".join(value) - else: - raise ValueError("Don't know how to comma-format %r" % value) - - -class memoized_property(object): - - """A read-only @property that is only evaluated once.""" - - def __init__(self, fget, doc=None): - self.fget = fget - self.__doc__ = doc or fget.__doc__ - self.__name__ = fget.__name__ - - def __get__(self, obj, cls): - if obj is None: - return self - obj.__dict__[self.__name__] = result = self.fget(obj) - return result - - -class immutabledict(dict): - - def _immutable(self, *arg, **kw): - raise TypeError("%s object is immutable" % self.__class__.__name__) - - __delitem__ = __setitem__ = __setattr__ = \ - clear = pop = popitem = setdefault = \ - update = _immutable - - def __new__(cls, *args): - new = dict.__new__(cls) - dict.__init__(new, *args) - return new - - def __init__(self, *args): - pass - - def __reduce__(self): - return immutabledict, (dict(self), ) - - def union(self, d): - if not self: - return immutabledict(d) - else: - d2 = immutabledict(self) - dict.update(d2, d) - return d2 - - def __repr__(self): - return "immutabledict(%s)" % dict.__repr__(self) - - -def _with_legacy_names(translations): - def decorate(fn): - - spec = inspect_getfullargspec(fn) - metadata = dict(target='target', fn='fn') - metadata.update(format_argspec_plus(spec, grouped=False)) - - has_keywords = bool(spec[2]) - - if not has_keywords: - metadata['args'] += ", **kw" - metadata['apply_kw'] += ", **kw" - - def go(*arg, **kw): - names = set(kw).difference(spec[0]) - for oldname, newname in translations: - if oldname in kw: - kw[newname] = kw.pop(oldname) - names.discard(oldname) - - warnings.warn( - "Argument '%s' is now named '%s' for function '%s'" % - (oldname, newname, fn.__name__)) - if not has_keywords and names: - raise TypeError("Unknown arguments: %s" % ", ".join(names)) - return fn(*arg, **kw) - - code = 'lambda %(args)s: %(target)s(%(apply_kw)s)' % ( - metadata) - decorated = eval(code, {"target": go}) - decorated.__defaults__ = getattr(fn, '__func__', fn).__defaults__ - update_wrapper(decorated, fn) - if hasattr(decorated, '__wrapped__'): - # update_wrapper in py3k applies __wrapped__, which causes - # inspect.getargspec() to ignore the extra arguments on our - # wrapper as of Python 3.4. We need this for the - # "module class proxy" thing though, so just del the __wrapped__ - # for now. See #175 as well as bugs.python.org/issue17482 - del decorated.__wrapped__ - return decorated - - return decorate diff --git a/alembic/util/__init__.py b/alembic/util/__init__.py new file mode 100644 index 0000000..bd7196c --- /dev/null +++ b/alembic/util/__init__.py @@ -0,0 +1,20 @@ +from .langhelpers import ( # noqa + asbool, rev_id, to_tuple, to_list, memoized_property, + immutabledict, _with_legacy_names, Dispatcher, ModuleClsProxy) +from .messaging import ( # noqa + write_outstream, status, err, obfuscate_url_pw, warn, msg, format_as_comma) +from .pyfiles import ( # noqa + template_to_file, coerce_resource_to_filename, simple_pyc_file_from_path, + pyc_file_from_path, load_python_file) +from .sqla_compat import ( # noqa + sqla_07, sqla_079, sqla_08, sqla_083, sqla_084, sqla_09, sqla_092, + sqla_094, sqla_094, sqla_099, sqla_100, sqla_105) + + +class CommandError(Exception): + pass + + +if not sqla_07: + raise CommandError( + "SQLAlchemy 0.7.3 or greater is required. ") diff --git a/alembic/compat.py b/alembic/util/compat.py similarity index 100% rename from alembic/compat.py rename to alembic/util/compat.py diff --git a/alembic/util/langhelpers.py b/alembic/util/langhelpers.py new file mode 100644 index 0000000..904848c --- /dev/null +++ b/alembic/util/langhelpers.py @@ -0,0 +1,275 @@ +import textwrap +import warnings +import inspect +import uuid +import collections + +from .compat import callable, exec_, string_types, with_metaclass + +from sqlalchemy.util import format_argspec_plus, update_wrapper +from sqlalchemy.util.compat import inspect_getfullargspec + + +class _ModuleClsMeta(type): + def __setattr__(cls, key, value): + super(_ModuleClsMeta, cls).__setattr__(key, value) + cls._update_module_proxies(key) + + +class ModuleClsProxy(with_metaclass(_ModuleClsMeta)): + """Create module level proxy functions for the + methods on a given class. + + The functions will have a compatible signature + as the methods. + + """ + + _setups = collections.defaultdict(lambda: (set(), [])) + + @classmethod + def _update_module_proxies(cls, name): + attr_names, modules = cls._setups[cls] + for globals_, locals_ in modules: + cls._add_proxied_attribute(name, globals_, locals_, attr_names) + + def _install_proxy(self): + attr_names, modules = self._setups[self.__class__] + for globals_, locals_ in modules: + globals_['_proxy'] = self + for attr_name in attr_names: + globals_[attr_name] = getattr(self, attr_name) + + def _remove_proxy(self): + attr_names, modules = self._setups[self.__class__] + for globals_, locals_ in modules: + globals_['_proxy'] = None + for attr_name in attr_names: + del globals_[attr_name] + + @classmethod + def create_module_class_proxy(cls, globals_, locals_): + attr_names, modules = cls._setups[cls] + modules.append( + (globals_, locals_) + ) + cls._setup_proxy(globals_, locals_, attr_names) + + @classmethod + def _setup_proxy(cls, globals_, locals_, attr_names): + for methname in dir(cls): + cls._add_proxied_attribute(methname, globals_, locals_, attr_names) + + @classmethod + def _add_proxied_attribute(cls, methname, globals_, locals_, attr_names): + if not methname.startswith('_'): + meth = getattr(cls, methname) + if callable(meth): + locals_[methname] = cls._create_method_proxy( + methname, globals_, locals_) + else: + attr_names.add(methname) + + @classmethod + def _create_method_proxy(cls, name, globals_, locals_): + fn = getattr(cls, name) + spec = inspect.getargspec(fn) + if spec[0] and spec[0][0] == 'self': + spec[0].pop(0) + args = inspect.formatargspec(*spec) + num_defaults = 0 + if spec[3]: + num_defaults += len(spec[3]) + name_args = spec[0] + if num_defaults: + defaulted_vals = name_args[0 - num_defaults:] + else: + defaulted_vals = () + + apply_kw = inspect.formatargspec( + name_args, spec[1], spec[2], + defaulted_vals, + formatvalue=lambda x: '=' + x) + + def _name_error(name): + raise NameError( + "Can't invoke function '%s', as the proxy object has " + "not yet been " + "established for the Alembic '%s' class. " + "Try placing this code inside a callable." % ( + name, cls.__name__ + )) + globals_['_name_error'] = _name_error + + func_text = textwrap.dedent("""\ + def %(name)s(%(args)s): + %(doc)r + try: + p = _proxy + except NameError: + _name_error('%(name)s') + return _proxy.%(name)s(%(apply_kw)s) + e + """ % { + 'name': name, + 'args': args[1:-1], + 'apply_kw': apply_kw[1:-1], + 'doc': fn.__doc__, + }) + lcl = {} + exec_(func_text, globals_, lcl) + return lcl[name] + + +def asbool(value): + return value is not None and \ + value.lower() == 'true' + + +def rev_id(): + val = int(uuid.uuid4()) % 100000000000000 + return hex(val)[2:-1] + + +def to_list(x, default=None): + if x is None: + return default + elif isinstance(x, string_types): + return [x] + elif isinstance(x, collections.Iterable): + return list(x) + else: + raise ValueError("Don't know how to turn %r into a list" % x) + + +def to_tuple(x, default=None): + if x is None: + return default + elif isinstance(x, string_types): + return (x, ) + elif isinstance(x, collections.Iterable): + return tuple(x) + else: + raise ValueError("Don't know how to turn %r into a tuple" % x) + + +class memoized_property(object): + + """A read-only @property that is only evaluated once.""" + + def __init__(self, fget, doc=None): + self.fget = fget + self.__doc__ = doc or fget.__doc__ + self.__name__ = fget.__name__ + + def __get__(self, obj, cls): + if obj is None: + return self + obj.__dict__[self.__name__] = result = self.fget(obj) + return result + + +class immutabledict(dict): + + def _immutable(self, *arg, **kw): + raise TypeError("%s object is immutable" % self.__class__.__name__) + + __delitem__ = __setitem__ = __setattr__ = \ + clear = pop = popitem = setdefault = \ + update = _immutable + + def __new__(cls, *args): + new = dict.__new__(cls) + dict.__init__(new, *args) + return new + + def __init__(self, *args): + pass + + def __reduce__(self): + return immutabledict, (dict(self), ) + + def union(self, d): + if not self: + return immutabledict(d) + else: + d2 = immutabledict(self) + dict.update(d2, d) + return d2 + + def __repr__(self): + return "immutabledict(%s)" % dict.__repr__(self) + + +def _with_legacy_names(translations): + def decorate(fn): + + spec = inspect_getfullargspec(fn) + metadata = dict(target='target', fn='fn') + metadata.update(format_argspec_plus(spec, grouped=False)) + + has_keywords = bool(spec[2]) + + if not has_keywords: + metadata['args'] += ", **kw" + metadata['apply_kw'] += ", **kw" + + def go(*arg, **kw): + names = set(kw).difference(spec[0]) + for oldname, newname in translations: + if oldname in kw: + kw[newname] = kw.pop(oldname) + names.discard(oldname) + + warnings.warn( + "Argument '%s' is now named '%s' for function '%s'" % + (oldname, newname, fn.__name__)) + if not has_keywords and names: + raise TypeError("Unknown arguments: %s" % ", ".join(names)) + return fn(*arg, **kw) + + code = 'lambda %(args)s: %(target)s(%(apply_kw)s)' % ( + metadata) + decorated = eval(code, {"target": go}) + decorated.__defaults__ = getattr(fn, '__func__', fn).__defaults__ + update_wrapper(decorated, fn) + if hasattr(decorated, '__wrapped__'): + # update_wrapper in py3k applies __wrapped__, which causes + # inspect.getargspec() to ignore the extra arguments on our + # wrapper as of Python 3.4. We need this for the + # "module class proxy" thing though, so just del the __wrapped__ + # for now. See #175 as well as bugs.python.org/issue17482 + del decorated.__wrapped__ + return decorated + + return decorate + + +class Dispatcher(object): + def __init__(self): + self._registry = {} + + def dispatch_for(self, target, qualifier='default'): + def decorate(fn): + assert isinstance(target, type) + assert target not in self._registry + self._registry[(target, qualifier)] = fn + return fn + return decorate + + def dispatch(self, obj, qualifier='default'): + for spcls in type(obj).__mro__: + if qualifier != 'default' and (spcls, qualifier) in self._registry: + return self._registry[(spcls, qualifier)] + elif (spcls, 'default') in self._registry: + return self._registry[(spcls, 'default')] + else: + raise ValueError("no dispatch function for object: %s" % obj) + + def branch(self): + """Return a copy of this dispatcher that is independently + writable.""" + + d = Dispatcher() + d._registry.update(self._registry) + return d diff --git a/alembic/util/messaging.py b/alembic/util/messaging.py new file mode 100644 index 0000000..c202e96 --- /dev/null +++ b/alembic/util/messaging.py @@ -0,0 +1,94 @@ +from .compat import py27, binary_type, string_types +import sys +from sqlalchemy.engine import url +import warnings +import textwrap +import collections +import logging + +log = logging.getLogger(__name__) + +if py27: + # disable "no handler found" errors + logging.getLogger('alembic').addHandler(logging.NullHandler()) + + +try: + import fcntl + import termios + import struct + ioctl = fcntl.ioctl(0, termios.TIOCGWINSZ, + struct.pack('HHHH', 0, 0, 0, 0)) + _h, TERMWIDTH, _hp, _wp = struct.unpack('HHHH', ioctl) + if TERMWIDTH <= 0: # can occur if running in emacs pseudo-tty + TERMWIDTH = None +except (ImportError, IOError): + TERMWIDTH = None + + +def write_outstream(stream, *text): + encoding = getattr(stream, 'encoding', 'ascii') or 'ascii' + for t in text: + if not isinstance(t, binary_type): + t = t.encode(encoding, 'replace') + t = t.decode(encoding) + try: + stream.write(t) + except IOError: + # suppress "broken pipe" errors. + # no known way to handle this on Python 3 however + # as the exception is "ignored" (noisily) in TextIOWrapper. + break + + +def status(_statmsg, fn, *arg, **kw): + msg(_statmsg + " ...", False) + try: + ret = fn(*arg, **kw) + write_outstream(sys.stdout, " done\n") + return ret + except: + write_outstream(sys.stdout, " FAILED\n") + raise + + +def err(message): + log.error(message) + msg("FAILED: %s" % message) + sys.exit(-1) + + +def obfuscate_url_pw(u): + u = url.make_url(u) + if u.password: + u.password = 'XXXXX' + return str(u) + + +def warn(msg): + warnings.warn(msg) + + +def msg(msg, newline=True): + if TERMWIDTH is None: + write_outstream(sys.stdout, msg) + if newline: + write_outstream(sys.stdout, "\n") + else: + # left indent output lines + lines = textwrap.wrap(msg, TERMWIDTH) + if len(lines) > 1: + for line in lines[0:-1]: + write_outstream(sys.stdout, " ", line, "\n") + write_outstream(sys.stdout, " ", lines[-1], ("\n" if newline else "")) + + +def format_as_comma(value): + if value is None: + return "" + elif isinstance(value, string_types): + return value + elif isinstance(value, collections.Iterable): + return ", ".join(value) + else: + raise ValueError("Don't know how to comma-format %r" % value) diff --git a/alembic/util/pyfiles.py b/alembic/util/pyfiles.py new file mode 100644 index 0000000..c51e187 --- /dev/null +++ b/alembic/util/pyfiles.py @@ -0,0 +1,80 @@ +import sys +import os +import re +from .compat import load_module_py, load_module_pyc +from mako.template import Template + + +def template_to_file(template_file, dest, output_encoding, **kw): + with open(dest, 'wb') as f: + template = Template(filename=template_file) + f.write( + template.render_unicode(**kw).encode(output_encoding) + ) + + +def coerce_resource_to_filename(fname): + """Interpret a filename as either a filesystem location or as a package + resource. + + Names that are non absolute paths and contain a colon + are interpreted as resources and coerced to a file location. + + """ + if not os.path.isabs(fname) and ":" in fname: + import pkg_resources + fname = pkg_resources.resource_filename(*fname.split(':')) + return fname + + +def simple_pyc_file_from_path(path): + """Given a python source path, return the so-called + "sourceless" .pyc or .pyo path. + + This just a .pyc or .pyo file where the .py file would be. + + Even with PEP-3147, which normally puts .pyc/.pyo files in __pycache__, + this use case remains supported as a so-called "sourceless module import". + + """ + if sys.flags.optimize: + return path + "o" # e.g. .pyo + else: + return path + "c" # e.g. .pyc + + +def pyc_file_from_path(path): + """Given a python source path, locate the .pyc. + + See http://www.python.org/dev/peps/pep-3147/ + #detecting-pep-3147-availability + http://www.python.org/dev/peps/pep-3147/#file-extension-checks + + """ + import imp + has3147 = hasattr(imp, 'get_tag') + if has3147: + return imp.cache_from_source(path) + else: + return simple_pyc_file_from_path(path) + + +def load_python_file(dir_, filename): + """Load a file from the given path as a Python module.""" + + module_id = re.sub(r'\W', "_", filename) + path = os.path.join(dir_, filename) + _, ext = os.path.splitext(filename) + if ext == ".py": + if os.path.exists(path): + module = load_module_py(module_id, path) + elif os.path.exists(simple_pyc_file_from_path(path)): + # look for sourceless load + module = load_module_pyc( + module_id, simple_pyc_file_from_path(path)) + else: + raise ImportError("Can't find Python file %s" % path) + elif ext in (".pyc", ".pyo"): + module = load_module_pyc(module_id, path) + del sys.modules[module_id] + return module diff --git a/alembic/util/sqla_compat.py b/alembic/util/sqla_compat.py new file mode 100644 index 0000000..871dcb8 --- /dev/null +++ b/alembic/util/sqla_compat.py @@ -0,0 +1,160 @@ +import re +from sqlalchemy import __version__ +from sqlalchemy.schema import ForeignKeyConstraint, CheckConstraint, Column +from sqlalchemy import types as sqltypes +from sqlalchemy import schema, sql +from sqlalchemy.sql.visitors import traverse +from sqlalchemy.ext.compiler import compiles +from sqlalchemy.sql.expression import _BindParamClause +from . import compat + + +def _safe_int(value): + try: + return int(value) + except: + return value +_vers = tuple( + [_safe_int(x) for x in re.findall(r'(\d+|[abc]\d)', __version__)]) +sqla_07 = _vers > (0, 7, 2) +sqla_079 = _vers >= (0, 7, 9) +sqla_08 = _vers >= (0, 8, 0) +sqla_083 = _vers >= (0, 8, 3) +sqla_084 = _vers >= (0, 8, 4) +sqla_09 = _vers >= (0, 9, 0) +sqla_092 = _vers >= (0, 9, 2) +sqla_094 = _vers >= (0, 9, 4) +sqla_094 = _vers >= (0, 9, 4) +sqla_099 = _vers >= (0, 9, 9) +sqla_100 = _vers >= (1, 0, 0) +sqla_105 = _vers >= (1, 0, 5) + +if sqla_08: + from sqlalchemy.sql.expression import TextClause +else: + from sqlalchemy.sql.expression import _TextClause as TextClause + + +def _table_for_constraint(constraint): + if isinstance(constraint, ForeignKeyConstraint): + return constraint.parent + else: + return constraint.table + + +def _columns_for_constraint(constraint): + if isinstance(constraint, ForeignKeyConstraint): + return [fk.parent for fk in constraint.elements] + elif isinstance(constraint, CheckConstraint): + return _find_columns(constraint.sqltext) + else: + return list(constraint.columns) + + +def _fk_spec(constraint): + if sqla_100: + source_columns = [ + constraint.columns[key].name for key in constraint.column_keys] + else: + source_columns = [ + element.parent.name for element in constraint.elements] + + source_table = constraint.parent.name + source_schema = constraint.parent.schema + target_schema = constraint.elements[0].column.table.schema + target_table = constraint.elements[0].column.table.name + target_columns = [element.column.name for element in constraint.elements] + + return ( + source_schema, source_table, + source_columns, target_schema, target_table, target_columns) + + +def _is_type_bound(constraint): + # this deals with SQLAlchemy #3260, don't copy CHECK constraints + # that will be generated by the type. + if sqla_100: + # new feature added for #3260 + return constraint._type_bound + else: + # old way, look at what we know Boolean/Enum to use + return ( + constraint._create_rule is not None and + isinstance( + getattr(constraint._create_rule, "target", None), + sqltypes.SchemaType) + ) + + +def _find_columns(clause): + """locate Column objects within the given expression.""" + + cols = set() + traverse(clause, {}, {'column': cols.add}) + return cols + + +def _textual_index_column(table, text_): + """a workaround for the Index construct's severe lack of flexibility""" + if isinstance(text_, compat.string_types): + c = Column(text_, sqltypes.NULLTYPE) + table.append_column(c) + return c + elif isinstance(text_, TextClause): + return _textual_index_element(table, text_) + else: + raise ValueError("String or text() construct expected") + + +class _textual_index_element(sql.ColumnElement): + """Wrap around a sqlalchemy text() construct in such a way that + we appear like a column-oriented SQL expression to an Index + construct. + + The issue here is that currently the Postgresql dialect, the biggest + recipient of functional indexes, keys all the index expressions to + the corresponding column expressions when rendering CREATE INDEX, + so the Index we create here needs to have a .columns collection that + is the same length as the .expressions collection. Ultimately + SQLAlchemy should support text() expressions in indexes. + + See https://bitbucket.org/zzzeek/sqlalchemy/issue/3174/\ + support-text-sent-to-indexes + + """ + __visit_name__ = '_textual_idx_element' + + def __init__(self, table, text): + self.table = table + self.text = text + self.key = text.text + self.fake_column = schema.Column(self.text.text, sqltypes.NULLTYPE) + table.append_column(self.fake_column) + + def get_children(self): + return [self.fake_column] + + +@compiles(_textual_index_element) +def _render_textual_index_column(element, compiler, **kw): + return compiler.process(element.text, **kw) + + +class _literal_bindparam(_BindParamClause): + pass + + +@compiles(_literal_bindparam) +def _render_literal_bindparam(element, compiler, **kw): + return compiler.render_literal_bindparam(element, **kw) + + +def _get_index_expressions(idx): + if sqla_08: + return list(idx.expressions) + else: + return list(idx.columns) + + +def _get_index_column_names(idx): + return [getattr(exp, "name", None) for exp in _get_index_expressions(idx)] diff --git a/docs/build/api.rst b/docs/build/api.rst deleted file mode 100644 index fea4e14..0000000 --- a/docs/build/api.rst +++ /dev/null @@ -1,217 +0,0 @@ -.. _api: - -=========== -API Details -=========== - -This section describes some key functions used within the migration process, particularly those referenced within -a migration environment's ``env.py`` file. - -Overview -======== - -The three main objects in use are the :class:`.EnvironmentContext`, :class:`.MigrationContext`, -and :class:`.Operations` classes, pictured below. - -.. image:: api_overview.png - -An Alembic command begins by instantiating an :class:`.EnvironmentContext` object, then -making it available via the ``alembic.context`` proxy module. The ``env.py`` -script, representing a user-configurable migration environment, is then -invoked. The ``env.py`` script is then responsible for calling upon the -:meth:`.EnvironmentContext.configure`, whose job it is to create -a :class:`.MigrationContext` object. - -Before this method is called, there's not -yet any database connection or dialect-specific state set up. While -many methods on :class:`.EnvironmentContext` are usable at this stage, -those which require database access, or at least access to the kind -of database dialect in use, are not. Once the -:meth:`.EnvironmentContext.configure` method is called, the :class:`.EnvironmentContext` -is said to be *configured* with database connectivity, available via -a new :class:`.MigrationContext` object. The :class:`.MigrationContext` -is associated with the :class:`.EnvironmentContext` object -via the :meth:`.EnvironmentContext.get_context` method. - -Finally, ``env.py`` calls upon the :meth:`.EnvironmentContext.run_migrations` -method. Within this method, a new :class:`.Operations` object, which -provides an API for individual database migration operations, is established -within the ``alembic.op`` proxy module. The :class:`.Operations` object -uses the :class:`.MigrationContext` object ultimately as a source of -database connectivity, though in such a way that it does not care if the -:class:`.MigrationContext` is talking to a real database or just writing -out SQL to a file. - -The Environment Context -======================= - -The :class:`.EnvironmentContext` class provides most of the -API used within an ``env.py`` script. Within ``env.py``, -the instantated :class:`.EnvironmentContext` is made available -via a special *proxy module* called ``alembic.context``. That is, -you can import ``alembic.context`` like a regular Python module, -and each name you call upon it is ultimately routed towards the -current :class:`.EnvironmentContext` in use. - -In particular, the key method used within ``env.py`` is :meth:`.EnvironmentContext.configure`, -which establishes all the details about how the database will be accessed. - -.. automodule:: alembic.environment - :members: - -The Migration Context -===================== - -.. automodule:: alembic.migration - :members: - -The Operations Object -===================== - -Within migration scripts, actual database migration operations are handled -via an instance of :class:`.Operations`. See :ref:`ops` for an overview -of this object. - -Commands -========= - -Alembic commands are all represented by functions in the :mod:`alembic.command` -package. They all accept the same style of usage, being sent -the :class:`~.alembic.config.Config` object as the first argument. - -Commands can be run programmatically, by first constructing a :class:`.Config` -object, as in:: - - from alembic.config import Config - from alembic import command - alembic_cfg = Config("/path/to/yourapp/alembic.ini") - command.upgrade(alembic_cfg, "head") - -In many cases, and perhaps more often than not, an application will wish -to call upon a series of Alembic commands and/or other features. It is -usually a good idea to link multiple commands along a single connection -and transaction, if feasible. This can be achieved using the -:attr:`.Config.attributes` dictionary in order to share a connection:: - - with engine.begin() as connection: - alembic_cfg.attributes['connection'] = connection - command.upgrade(alembic_cfg, "head") - -This recipe requires that ``env.py`` consumes this connection argument; -see the example in :ref:`connection_sharing` for details. - -To write small API functions that make direct use of database and script directory -information, rather than just running one of the built-in commands, -use the :class:`.ScriptDirectory` and :class:`.MigrationContext` -classes directly. - -.. currentmodule:: alembic.command - -.. automodule:: alembic.command - :members: - -Configuration -============== - -The :class:`.Config` object represents the configuration -passed to the Alembic environment. From an API usage perspective, -it is needed for the following use cases: - -* to create a :class:`.ScriptDirectory`, which allows you to work - with the actual script files in a migration environment -* to create an :class:`.EnvironmentContext`, which allows you to - actually run the ``env.py`` module within the migration environment -* to programatically run any of the commands in the :mod:`alembic.command` - module. - -The :class:`.Config` is *not* needed for these cases: - -* to instantiate a :class:`.MigrationContext` directly - this object - only needs a SQLAlchemy connection or dialect name. -* to instantiate a :class:`.Operations` object - this object only - needs a :class:`.MigrationContext`. - -.. currentmodule:: alembic.config - -.. automodule:: alembic.config - :members: - -Script Directory -================ - -The :class:`.ScriptDirectory` object provides programmatic access -to the Alembic version files present in the filesystem. - -.. automodule:: alembic.script - :members: - -Revision -======== - -The :class:`.RevisionMap` object serves as the basis for revision -management, used exclusively by :class:`.ScriptDirectory`. - -.. automodule:: alembic.revision - :members: - -Autogeneration -============== - -Alembic 0.3 introduces a small portion of the autogeneration system -as a public API. - -.. autofunction:: alembic.autogenerate.compare_metadata - -DDL Internals -============= - -These are some of the constructs used to generate migration -instructions. The APIs here build off of the :class:`sqlalchemy.schema.DDLElement` -and :mod:`sqlalchemy.ext.compiler` systems. - -For programmatic usage of Alembic's migration directives, the easiest -route is to use the higher level functions given by :mod:`alembic.operations`. - -.. automodule:: alembic.ddl - :members: - :undoc-members: - -.. automodule:: alembic.ddl.base - :members: - :undoc-members: - -.. automodule:: alembic.ddl.impl - :members: - :undoc-members: - -MySQL ------ - -.. automodule:: alembic.ddl.mysql - :members: - :undoc-members: - :show-inheritance: - -MS-SQL ------- - -.. automodule:: alembic.ddl.mssql - :members: - :undoc-members: - :show-inheritance: - -Postgresql ----------- - -.. automodule:: alembic.ddl.postgresql - :members: - :undoc-members: - :show-inheritance: - -SQLite ------- - -.. automodule:: alembic.ddl.sqlite - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/build/api/api_overview.png b/docs/build/api/api_overview.png new file mode 100644 index 0000000000000000000000000000000000000000..37e7312e0e15851146eab91aaa60e683664d445f GIT binary patch literal 123965 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 BLoNUS literal 0 HcmV?d00001 diff --git a/docs/build/api/autogenerate.rst b/docs/build/api/autogenerate.rst new file mode 100644 index 0000000..b024ab1 --- /dev/null +++ b/docs/build/api/autogenerate.rst @@ -0,0 +1,235 @@ +.. _alembic.autogenerate.toplevel: + +============== +Autogeneration +============== + +The autogenerate system has two areas of API that are public: + +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 + is available either as a rudimentary list of changes, or as + a :class:`.MigrateOperation` structure. + +2. The ability to alter how the ``alembic revision`` command generates + revision scripts, including support for multiple revision scripts + generated in one pass. + +Getting Diffs +============== + +.. autofunction:: alembic.autogenerate.compare_metadata + +.. autofunction:: alembic.autogenerate.produce_migrations + +.. _customizing_revision: + +Customizing Revision Generation +========================================== + +.. versionadded:: 0.8.0 - the ``alembic revision`` system is now customizable. + +The ``alembic revision`` command, also available programmatically +via :func:`.command.revision`, essentially produces a single migration +script after being run. Whether or not the ``--autogenerate`` option +was specified basically determines if this script is a blank revision +script with empty ``upgrade()`` and ``downgrade()`` functions, or was +produced with alembic operation directives as the result of autogenerate. + +In either case, the system creates a full plan of what is to be done +in the form of a :class:`.MigrateOperation` structure, which is then +used to produce the script. + +For example, suppose we ran ``alembic revision --autogenerate``, and the +end result was that it produced a new revision ``'eced083f5df'`` +with the following contents:: + + """create the organization table.""" + + # revision identifiers, used by Alembic. + revision = 'eced083f5df' + down_revision = 'beafc7d709f' + + from alembic import op + import sqlalchemy as sa + + + def upgrade(): + op.create_table( + 'organization', + sa.Column('id', sa.Integer(), primary_key=True), + sa.Column('name', sa.String(50), nullable=False) + ) + op.add_column( + 'user', + sa.Column('organization_id', sa.Integer()) + ) + op.create_foreign_key( + 'org_fk', 'user', 'organization', ['organization_id'], ['id'] + ) + + def downgrade(): + op.drop_constraint('org_fk', 'user') + op.drop_column('user', 'organization_id') + op.drop_table('organization') + +The above script is generated by a :class:`.MigrateOperation` structure +that looks like this:: + + from alembic.operations import ops + import sqlalchemy as sa + + migration_script = ops.MigrationScript( + 'eced083f5df', + ops.UpgradeOps( + ops=[ + ops.CreateTableOp( + 'organization', + [ + sa.Column('id', sa.Integer(), primary_key=True), + sa.Column('name', sa.String(50), nullable=False) + ] + ), + ops.ModifyTableOps( + 'user', + ops=[ + ops.AddColumnOp( + 'user', + sa.Column('organization_id', sa.Integer()) + ), + ops.CreateForeignKeyOp( + 'org_fk', 'user', 'organization', + ['organization_id'], ['id'] + ) + ] + ) + ] + ), + ops.DowngradeOps( + ops=[ + ops.ModifyTableOps( + 'user', + ops=[ + ops.DropConstraintOp('org_fk', 'user'), + ops.DropColumnOp('user', 'organization_id') + ] + ), + ops.DropTableOp('organization') + ] + ), + message='create the organization table.' + ) + +When we deal with a :class:`.MigrationScript` structure, we can render +the upgrade/downgrade sections into strings for debugging purposes +using the :func:`.render_python_code` helper function:: + + from alembic.autogenerate import render_python_code + print(render_python_code(migration_script.upgrade_ops)) + +Renders:: + + ### commands auto generated by Alembic - please adjust! ### + op.create_table('organization', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=50), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.add_column('user', sa.Column('organization_id', sa.Integer(), nullable=True)) + op.create_foreign_key('org_fk', 'user', 'organization', ['organization_id'], ['id']) + ### end Alembic commands ### + +Given that structures like the above are used to generate new revision +files, and that we'd like to be able to alter these as they are created, +we then need a system to access this structure when the +:func:`.command.revision` command is used. The +:paramref:`.EnvironmentContext.configure.process_revision_directives` +parameter gives us a way to alter this. This is a function that +is passed the above structure as generated by Alembic, giving us a chance +to alter it. +For example, if we wanted to put all the "upgrade" operations into +a certain branch, and we wanted our script to not have any "downgrade" +operations at all, we could build an extension as follows, illustrated +within an ``env.py`` script:: + + def process_revision_directives(context, revision, directives): + script = directives[0] + + # set specific branch + script.head = "mybranch@head" + + # erase downgrade operations + script.downgrade_ops.ops[:] = [] + + # ... + + def run_migrations_online(): + + # ... + with engine.connect() as connection: + + context.configure( + connection=connection, + target_metadata=target_metadata, + process_revision_directives=process_revision_directives) + + with context.begin_transaction(): + context.run_migrations() + +Above, the ``directives`` argument is a Python list. We may alter the +given structure within this list in-place, or replace it with a new +structure consisting of zero or more :class:`.MigrationScript` directives. +The :func:`.command.revision` command will then produce scripts corresponding +to whatever is in this list. + +.. autofunction:: alembic.autogenerate.render_python_code + +Autogenerating Custom Operation Directives +========================================== + +In the section :ref:`operation_plugins`, we talked about adding new +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:: + + # note: this is a continuation of the example from the + # "Operation Plugins" section + + from alembic.autogenerate import renderers + + @renderers.dispatch_for(CreateSequenceOp) + def render_create_sequence(autogen_context, op): + return "op.create_sequence(%r, **%r)" % ( + op.sequence_name, + op.kw + ) + +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 + + upgrade_ops = ops.UpgradeOps( + ops=[ + CreateSequenceOp("my_seq") + ] + ) + + print(render_python_code(upgrade_ops)) + +Which produces:: + + ### commands auto generated by Alembic - please adjust! ### + op.create_sequence('my_seq', **{}) + ### end Alembic commands ### + diff --git a/docs/build/api/commands.rst b/docs/build/api/commands.rst new file mode 100644 index 0000000..65dcc09 --- /dev/null +++ b/docs/build/api/commands.rst @@ -0,0 +1,38 @@ +.. _alembic.command.toplevel: + +========= +Commands +========= + +Alembic commands are all represented by functions in the :ref:`alembic.command.toplevel` +package. They all accept the same style of usage, being sent +the :class:`.Config` object as the first argument. + +Commands can be run programmatically, by first constructing a :class:`.Config` +object, as in:: + + from alembic.config import Config + from alembic import command + alembic_cfg = Config("/path/to/yourapp/alembic.ini") + command.upgrade(alembic_cfg, "head") + +In many cases, and perhaps more often than not, an application will wish +to call upon a series of Alembic commands and/or other features. It is +usually a good idea to link multiple commands along a single connection +and transaction, if feasible. This can be achieved using the +:attr:`.Config.attributes` dictionary in order to share a connection:: + + with engine.begin() as connection: + alembic_cfg.attributes['connection'] = connection + command.upgrade(alembic_cfg, "head") + +This recipe requires that ``env.py`` consumes this connection argument; +see the example in :ref:`connection_sharing` for details. + +To write small API functions that make direct use of database and script directory +information, rather than just running one of the built-in commands, +use the :class:`.ScriptDirectory` and :class:`.MigrationContext` +classes directly. + +.. automodule:: alembic.command + :members: diff --git a/docs/build/api/config.rst b/docs/build/api/config.rst new file mode 100644 index 0000000..25d934f --- /dev/null +++ b/docs/build/api/config.rst @@ -0,0 +1,26 @@ +.. _alembic.config.toplevel: + +============== +Configuration +============== + +The :class:`.Config` object represents the configuration +passed to the Alembic environment. From an API usage perspective, +it is needed for the following use cases: + +* to create a :class:`.ScriptDirectory`, which allows you to work + with the actual script files in a migration environment +* to create an :class:`.EnvironmentContext`, which allows you to + actually run the ``env.py`` module within the migration environment +* to programatically run any of the commands in the :ref:`alembic.command.toplevel` + module. + +The :class:`.Config` is *not* needed for these cases: + +* to instantiate a :class:`.MigrationContext` directly - this object + only needs a SQLAlchemy connection or dialect name. +* to instantiate a :class:`.Operations` object - this object only + needs a :class:`.MigrationContext`. + +.. automodule:: alembic.config + :members: diff --git a/docs/build/api/ddl.rst b/docs/build/api/ddl.rst new file mode 100644 index 0000000..2d114c8 --- /dev/null +++ b/docs/build/api/ddl.rst @@ -0,0 +1,56 @@ +.. _alembic.ddl.toplevel: + +============= +DDL Internals +============= + +These are some of the constructs used to generate migration +instructions. The APIs here build off of the :class:`sqlalchemy.schema.DDLElement` +and :ref:`sqlalchemy.ext.compiler_toplevel` systems. + +For programmatic usage of Alembic's migration directives, the easiest +route is to use the higher level functions given by :ref:`alembic.operations.toplevel`. + +.. automodule:: alembic.ddl + :members: + :undoc-members: + +.. automodule:: alembic.ddl.base + :members: + :undoc-members: + +.. automodule:: alembic.ddl.impl + :members: + :undoc-members: + +MySQL +============= + +.. automodule:: alembic.ddl.mysql + :members: + :undoc-members: + :show-inheritance: + +MS-SQL +============= + +.. automodule:: alembic.ddl.mssql + :members: + :undoc-members: + :show-inheritance: + +Postgresql +============= + +.. automodule:: alembic.ddl.postgresql + :members: + :undoc-members: + :show-inheritance: + +SQLite +============= + +.. automodule:: alembic.ddl.sqlite + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/build/api/environment.rst b/docs/build/api/environment.rst new file mode 100644 index 0000000..5a22773 --- /dev/null +++ b/docs/build/api/environment.rst @@ -0,0 +1,19 @@ +.. _alembic.runtime.environment.toplevel: + +======================= +The Environment Context +======================= + +The :class:`.EnvironmentContext` class provides most of the +API used within an ``env.py`` script. Within ``env.py``, +the instantated :class:`.EnvironmentContext` is made available +via a special *proxy module* called ``alembic.context``. That is, +you can import ``alembic.context`` like a regular Python module, +and each name you call upon it is ultimately routed towards the +current :class:`.EnvironmentContext` in use. + +In particular, the key method used within ``env.py`` is :meth:`.EnvironmentContext.configure`, +which establishes all the details about how the database will be accessed. + +.. automodule:: alembic.runtime.environment + :members: EnvironmentContext diff --git a/docs/build/api/index.rst b/docs/build/api/index.rst new file mode 100644 index 0000000..aa7c1a9 --- /dev/null +++ b/docs/build/api/index.rst @@ -0,0 +1,33 @@ +.. _api: + +=========== +API Details +=========== + +Alembic's internal API has many public integration points that can be used +to extend Alembic's functionality as well as to re-use its functionality +in new ways. As the project has grown, more APIs are created and exposed +for this purpose. + +Direct use of the vast majority of API details discussed here is not needed +for rudimentary use of Alembic; the only API that is used normally by end users is +the methods provided by the :class:`.Operations` class, which is discussed +outside of this subsection, and the parameters that can be passed to +the :meth:`.EnvironmentContext.configure` method, used when configuring +one's ``env.py`` environment. However, real-world applications will +usually end up using more of the internal API, in particular being able +to run commands programmatically, as discussed in the section :doc:`/api/commands`. + +.. toctree:: + :maxdepth: 2 + + overview + environment + migration + config + commands + operations + autogenerate + script + ddl + diff --git a/docs/build/api/migration.rst b/docs/build/api/migration.rst new file mode 100644 index 0000000..ae74818 --- /dev/null +++ b/docs/build/api/migration.rst @@ -0,0 +1,8 @@ +.. _alembic.runtime.migration.toplevel: + +===================== +The Migration Context +===================== + +.. automodule:: alembic.runtime.migration + :members: MigrationContext diff --git a/docs/build/api/operations.rst b/docs/build/api/operations.rst new file mode 100644 index 0000000..d9ff238 --- /dev/null +++ b/docs/build/api/operations.rst @@ -0,0 +1,123 @@ +.. _alembic.operations.toplevel: + +===================== +The Operations Object +===================== + +Within migration scripts, actual database migration operations are handled +via an instance of :class:`.Operations`. The :class:`.Operations` class +lists out available migration operations that are linked to a +:class:`.MigrationContext`, which communicates instructions originated +by the :class:`.Operations` object into SQL that is sent to a database or SQL +output stream. + +Most methods on the :class:`.Operations` class are generated dynamically +using a "plugin" system, described in the next section +:ref:`operation_plugins`. Additionally, when Alembic migration scripts +actually run, the methods on the current :class:`.Operations` object are +proxied out to the ``alembic.op`` module, so that they are available +using module-style access. + +For an overview of how to use an :class:`.Operations` object directly +in programs, as well as for reference to the standard operation methods +as well as "batch" methods, see :ref:`ops`. + +.. _operation_plugins: + +Operation Plugins +===================== + +The Operations object is extensible using a plugin system. This system +allows one to add new ``op.`` methods at runtime. The +steps to use this system are to first create a subclass of +:class:`.MigrateOperation`, register it using the :meth:`.Operations.register_operation` +class decorator, then build a default "implementation" function which is +established using the :meth:`.Operations.implementation_for` decorator. + +.. versionadded:: 0.8.0 - the :class:`.Operations` class is now an + open namespace that is extensible via the creation of new + :class:`.MigrateOperation` subclasses. + +Below we illustrate a very simple operation ``CreateSequenceOp`` which +will implement a new method ``op.create_sequence()`` for use in +migration scripts:: + + from alembic.operations import Operations, MigrateOperation + + @Operations.register_operation("create_sequence") + class CreateSequenceOp(MigrateOperation): + """Create a SEQUENCE.""" + + def __init__(self, sequence_name, **kw): + self.sequence_name = sequence_name + self.kw = kw + + @classmethod + def create_sequence(cls, operations, sequence_name, **kw): + """Issue a "CREATE SEQUENCE" instruction.""" + + 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 +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, +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) + +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 +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:: + + def upgrade(): + op.create_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. + + +.. versionadded:: 0.8 - the migration operations available via the + :class:`.Operations` class as well as the ``alembic.op`` namespace + is now extensible using a plugin system. + + +.. _operation_objects: + +Built-in Operation Objects +============================== + +The migration operations present on :class:`.Operations` are themselves +delivered via operation objects that represent an operation and its +arguments. All operations descend from the :class:`.MigrateOperation` +class, and are registered with the :class:`.Operations` class using +the :meth:`.Operations.register_operation` class decorator. The +:class:`.MigrateOperation` objects also serve as the basis for how the +autogenerate system renders new migration scripts. + +.. seealso:: + + :ref:`operation_plugins` + + :ref:`customizing_revision` + +The built-in operation objects are listed below. + +.. _alembic.operations.ops.toplevel: + +.. automodule:: alembic.operations.ops + :members: diff --git a/docs/build/api/overview.rst b/docs/build/api/overview.rst new file mode 100644 index 0000000..048d1e6 --- /dev/null +++ b/docs/build/api/overview.rst @@ -0,0 +1,47 @@ +======== +Overview +======== + +A visualization of the primary features of Alembic's internals is presented +in the following figure. The module and class boxes do not list out +all the operations provided by each unit; only a small set of representative +elements intended to convey the primary purpose of each system. + +.. image:: api_overview.png + +The script runner for Alembic is present in the :ref:`alembic.config.toplevel` module. +This module produces a :class:`.Config` object and passes it to the +appropriate function in :ref:`alembic.command.toplevel`. Functions within +:ref:`alembic.command.toplevel` will typically instantiate an +:class:`.ScriptDirectory` instance, which represents the collection of +version files, and an :class:`.EnvironmentContext`, which represents a +configurational object passed to the environment's ``env.py`` script. + +Within the execution of ``env.py``, a :class:`.MigrationContext` +object is produced when the :meth:`.EnvironmentContext.configure` +method is called. :class:`.MigrationContext` is the gateway to the database +for other parts of the application, and produces a :class:`.DefaultImpl` +object which does the actual database communication, and knows how to +create the specific SQL text of the various DDL directives such as +ALTER TABLE; :class:`.DefaultImpl` has subclasses that are per-database-backend. +In "offline" mode (e.g. ``--sql``), the :class:`.MigrationContext` will +produce SQL to a file output stream instead of a database. + +During an upgrade or downgrade operation, a specific series of migration +scripts are invoked starting with the :class:`.MigrationContext` in conjunction +with the :class:`.ScriptDirectory`; the actual scripts themselves make use +of the :class:`.Operations` object, which provide the end-user interface to +specific database operations. The :class:`.Operations` object is generated +based on a series of "operation directive" objects that are user-extensible, +and start out in the :ref:`alembic.operations.ops.toplevel` module. + +Another prominent feature of Alembic is the "autogenerate" feature, which +produces new migration scripts that contain Python code. The autogenerate +feature starts in :ref:`alembic.autogenerate.toplevel`, and is used exclusively +by the :func:`.alembic.command.revision` command when the ``--autogenerate`` +flag is passed. Autogenerate refers to the :class:`.MigrationContext` +and :class:`.DefaultImpl` in order to access database connectivity and +access per-backend rules for autogenerate comparisons. It also makes use +of :ref:`alembic.operations.ops.toplevel` in order to represent the operations that +it will render into scripts. + diff --git a/docs/build/api/script.rst b/docs/build/api/script.rst new file mode 100644 index 0000000..8dc594b --- /dev/null +++ b/docs/build/api/script.rst @@ -0,0 +1,20 @@ +.. _alembic.script.toplevel: + +================ +Script Directory +================ + +The :class:`.ScriptDirectory` object provides programmatic access +to the Alembic version files present in the filesystem. + +.. automodule:: alembic.script + :members: + +Revision +======== + +The :class:`.RevisionMap` object serves as the basis for revision +management, used exclusively by :class:`.ScriptDirectory`. + +.. automodule:: alembic.script.revision + :members: diff --git a/docs/build/api_overview.png b/docs/build/api_overview.png deleted file mode 100644 index dab204b6e30ebda4613bad87cde302d68a74f539..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 64697 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

;C^PL)eL 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 >1OnbQjdPV<4L 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 S@?tR1M_g1!q*_QX;Qs)hY`ua2 diff --git a/docs/build/assets/api_overview.graffle b/docs/build/assets/api_overview.graffle index 7c083e5..1e58ea5 100644 --- a/docs/build/assets/api_overview.graffle +++ b/docs/build/assets/api_overview.graffle @@ -4,34 +4,56 @@ ActiveLayerIndex 0 + ApplicationVersion + + com.omnigroup.OmniGrafflePro + 139.18.0.187838 + AutoAdjust - CanvasColor + BackgroundGraphic - w - 1 + Bounds + {{0, 0}, {1176, 768}} + Class + SolidGraphic + ID + 2 + Style + + shadow + + Draws + NO + + stroke + + Draws + NO + + + BaseZoom + 0 CanvasOrigin {0, 0} - CanvasScale - 1 ColumnAlign 1 ColumnSpacing 36 CreationDate - 2012-01-24 16:51:07 -0500 + 2012-01-24 21:51:07 +0000 Creator classic DisplayScale - 1 in = 1 in + 1 0/72 in = 1.0000 in GraphDocumentVersion - 5 + 8 GraphicsList Bounds - {{319.25, 165}, {66, 12}} + {{601.74580087231288, 420}, {84, 12}} Class ShapedGraphic FitText @@ -39,7 +61,7 @@ Flow Resize ID - 2054 + 2140 Shape Rectangle Style @@ -60,19 +82,261 @@ Align 0 Text - {\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf360 -{\fonttbl\f0\fswiss\fcharset0 Helvetica;} + {\rtf1\ansi\ansicpg1252\cocoartf1347\cocoasubrtf570 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 -\f0\fs20 \cf0 <<proxies>>} +\f0\fs20 \cf0 <<instantiates>>} + VerticalPad + 0 Wrap NO + + Class + TableGroup + Graphics + + + Bounds + {{191, 107.40116119384766}, {102.9071044921875, 14}} + Class + ShapedGraphic + FitText + Vertical + Flow + Resize + ID + 2132 + Shape + Rectangle + Style + + fill + + GradientCenter + {-0.29411799999999999, -0.264706} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1347\cocoasubrtf570 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\b\fs24 \cf0 PostgresqlImpl} + VerticalPad + 0 + + TextPlacement + 0 + + + GroupConnect + YES + ID + 2131 + + + Class + TableGroup + Graphics + + + Bounds + {{230.9169921875, 132.80233001708984}, {102.9071044921875, 14}} + Class + ShapedGraphic + FitText + Vertical + Flow + Resize + ID + 2130 + Shape + Rectangle + Style + + fill + + GradientCenter + {-0.29411799999999999, -0.264706} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1347\cocoasubrtf570 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\b\fs24 \cf0 MSSQLImpl} + VerticalPad + 0 + + TextPlacement + 0 + + + GroupConnect + YES + ID + 2129 + + + Class + TableGroup + Graphics + + + Bounds + {{226, 82}, {102.9071044921875, 14}} + Class + ShapedGraphic + FitText + Vertical + Flow + Resize + ID + 2127 + Shape + Rectangle + Style + + fill + + GradientCenter + {-0.29411799999999999, -0.264706} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1347\cocoasubrtf570 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\b\fs24 \cf0 MySQLImpl} + VerticalPad + 0 + + TextPlacement + 0 + + + GroupConnect + YES + ID + 2126 + + + Class + LineGraphic + Head + + ID + 2055 + + ID + 2135 + Points + + {280.22809604806071, 146.80233001708984} + {272.46503226582109, 172.16651000976572} + + Style + + stroke + + HeadArrow + UMLInheritance + Legacy + + TailArrow + 0 + + + Tail + + ID + 2129 + + + + Class + LineGraphic + Head + + ID + 2055 + + ID + 2134 + Points + + {243.64926792598939, 121.40116119384763} + {252.32082843664148, 172.16651000976572} + + Style + + stroke + + HeadArrow + UMLInheritance + Legacy + + TailArrow + 0 + + + Tail + + ID + 2131 + + + + Class + LineGraphic + Head + + ID + 2055 + + ID + 2133 + Points + + {276.4518773872507, 95.999999999999986} + {265.55272336402226, 172.16651000976572} + + Style + + stroke + + HeadArrow + UMLInheritance + Legacy + + TailArrow + 0 + + + Tail + + ID + 2126 + + Bounds - {{444, 216.633}, {66, 12}} + {{504, 310}, {84, 12}} Class ShapedGraphic FitText @@ -80,7 +344,7 @@ Flow Resize ID - 2053 + 2125 Shape Rectangle Style @@ -101,12 +365,14 @@ Align 0 Text - {\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf360 -{\fonttbl\f0\fswiss\fcharset0 Helvetica;} + {\rtf1\ansi\ansicpg1252\cocoartf1347\cocoasubrtf570 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 -\f0\fs20 \cf0 <<proxies>>} +\f0\fs20 \cf0 <<instantiates>>} + VerticalPad + 0 Wrap NO @@ -117,14 +383,20 @@ Head ID - 2048 + 33 ID - 2051 + 2124 + OrthogonalBarAutomatic + + OrthogonalBarPoint + {0, 0} + OrthogonalBarPosition + 16 Points - {165, 221.6} - {109, 221.6} + {563, 340.34042553191489} + {497.13201904296875, 327.88251038766401} Style @@ -132,6 +404,846 @@ HeadArrow StickArrow + Legacy + + LineType + 2 + Pattern + 1 + TailArrow + 0 + + + Tail + + ID + 2072 + + + + Bounds + {{494.00001409542369, 415.9000186920166}, {55, 12}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 2123 + Line + + ID + 2139 + Position + 0.37128287553787231 + RotationType + 0 + + Shape + Rectangle + Style + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1347\cocoasubrtf570 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 + +\f0\fs20 \cf0 <<uses>>} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{713.35945466160774, 356.11699358749399}, {55, 12}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 2122 + Line + + ID + 2121 + Position + 0.49189183115959167 + RotationType + 0 + + Shape + Rectangle + Style + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1347\cocoasubrtf570 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 + +\f0\fs20 \cf0 <<uses>>} + VerticalPad + 0 + + Wrap + NO + + + Class + LineGraphic + Head + + ID + 2081 + Info + 5 + + ID + 2121 + Points + + {702, 363.10150901307452} + {781, 361.10002136230463} + + Style + + stroke + + HeadArrow + StickArrow + HopLines + + HopType + 102 + Legacy + + Pattern + 1 + TailArrow + 0 + + + Tail + + ID + 2072 + + + + Class + LineGraphic + Head + + ID + 2059 + + ID + 2120 + OrthogonalBarAutomatic + + OrthogonalBarPoint + {0, 0} + OrthogonalBarPosition + -1 + Points + + {637, 406} + {565.78369522094727, 454.05202861384231} + + Style + + stroke + + HeadArrow + StickArrow + Legacy + + LineType + 2 + Pattern + 1 + TailArrow + 0 + + + Tail + + ID + 2072 + + + + Bounds + {{717, 400}, {68, 12}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 2119 + Shape + Rectangle + Style + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1347\cocoasubrtf570 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 + +\f0\fs20 \cf0 <<invokes>>} + VerticalPad + 0 + + Wrap + NO + + + Class + LineGraphic + Head + + ID + 2072 + Info + 5 + + ID + 2118 + OrthogonalBarAutomatic + + OrthogonalBarPoint + {0, 0} + OrthogonalBarPosition + -1 + Points + + {759.34192925872742, 429.89997863769531} + {702, 384.99999999999994} + + Style + + stroke + + HeadArrow + StickArrow + Legacy + + LineType + 2 + Pattern + 1 + TailArrow + 0 + + + Tail + + ID + 2048 + Info + 3 + + + + Bounds + {{603.74580087231288, 470.3107529903566}, {80, 12}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 2117 + Line + + ID + 2116 + Position + 0.47171458601951599 + RotationType + 0 + + Shape + Rectangle + Style + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1347\cocoasubrtf570 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 + +\f0\fs20 \cf0 <<configures>>} + VerticalPad + 0 + + Wrap + NO + + + Class + LineGraphic + Head + + ID + 2059 + + ID + 2116 + Points + + {713.35941696166992, 476.88540101271974} + {565.78369522094727, 475.66718967115884} + + Style + + stroke + + HeadArrow + StickArrow + HopLines + + HopType + 102 + Legacy + + Pattern + 1 + TailArrow + 0 + + + Tail + + ID + 2048 + + + + Bounds + {{816, 258.37493918977634}, {69, 24}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 2113 + Line + + ID + 2109 + Position + 0.46421170234680176 + RotationType + 0 + + Shape + Rectangle + Style + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1347\cocoasubrtf570 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 + +\f0\fs20 \cf0 <<generates,\ +renders>>} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{705.05227716905051, 191.22492316822797}, {69, 24}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 2112 + Line + + ID + 2108 + Position + 0.46593526005744934 + RotationType + 0 + + Shape + Rectangle + Style + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1347\cocoasubrtf570 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 + +\f0\fs20 \cf0 <<provides\ +operations>>} + VerticalPad + 0 + + Wrap + NO + + + Class + LineGraphic + Head + + ID + 2098 + + ID + 2109 + Points + + {850.5, 298.10002136230469} + {850.50001322861976, 238.37493896484375} + + Style + + stroke + + HeadArrow + StickArrow + HopLines + + HopType + 102 + Legacy + + Pattern + 1 + TailArrow + 0 + + + Tail + + ID + 2081 + + + + Class + LineGraphic + Head + + ID + 38 + + ID + 2108 + Points + + {781.00002098083496, 203.28096591495026} + {692.04400634765625, 203.16068579982147} + + Style + + stroke + + HeadArrow + StickArrow + Legacy + + Pattern + 1 + TailArrow + 0 + + + Tail + + ID + 2098 + + + + Bounds + {{623.48996514081955, 291.09998092651369}, {55, 12}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 2107 + Line + + ID + 2105 + Position + 0.43473681807518005 + RotationType + 0 + + Shape + Rectangle + Style + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1347\cocoasubrtf570 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 + +\f0\fs20 \cf0 <<uses>>} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{513.14304282962803, 197.37493856351756}, {55, 12}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 2106 + Line + + ID + 2104 + Position + 0.3995765745639801 + RotationType + 0 + + Shape + Rectangle + Style + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1347\cocoasubrtf570 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 + +\f0\fs20 \cf0 <<uses>>} + VerticalPad + 0 + + Wrap + NO + + + Class + LineGraphic + Head + + ID + 41 + Info + 4 + + ID + 2105 + OrthogonalBarAutomatic + + OrthogonalBarPoint + {0, 0} + OrthogonalBarPosition + 5.1000003814697266 + Points + + {781, 339.20153037537921} + {747, 331} + {744, 297.09998092651369} + {533, 272.33299255371094} + {526, 233} + {491.30664526513783, 232.60000610351562} + + Style + + stroke + + HeadArrow + StickArrow + Legacy + + LineType + 2 + Pattern + 1 + TailArrow + 0 + + + Tail + + ID + 2081 + Info + 2 + + + + Class + LineGraphic + Head + + ID + 41 + + ID + 2104 + Points + + {572.95599365234375, 203} + {492.0880126953125, 203.93833970103648} + + Style + + stroke + + HeadArrow + StickArrow + HopLines + + HopType + 102 + Legacy + + Pattern + 1 + TailArrow + 0 + + + Tail + + ID + 38 + + + + Bounds + {{392.47411627278478, 268.53371033283503}, {84, 12}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 2103 + Line + + ID + 2102 + Offset + 1 + Position + 0.46998947858810425 + RotationType + 0 + + Shape + Rectangle + Style + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1347\cocoasubrtf570 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 + +\f0\fs20 \cf0 <<instantiates>>} + VerticalPad + 0 + + Wrap + NO + + + Class + LineGraphic + Head + + ID + 41 + + ID + 2102 + Points + + {435.00741612193735, 298.09998092651369} + {436.00000000000011, 248} + + Style + + stroke + + HeadArrow + StickArrow + HopLines + + HopType + 102 + Legacy + Pattern 1 TailArrow @@ -144,6 +1256,58 @@ 33 + + Bounds + {{320.83625227212906, 209.28763384458864}, {55, 12}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 2101 + Line + + ID + 2040 + Position + 0.39780238270759583 + RotationType + 0 + + Shape + Rectangle + Style + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1347\cocoasubrtf570 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 + +\f0\fs20 \cf0 <<uses>>} + VerticalPad + 0 + + Wrap + NO + Class TableGroup @@ -151,7 +1315,709 @@ Bounds - {{19, 207.6}, {90, 14}} + {{781.00002098083496, 168.37493896484375}, {139, 14}} + Class + ShapedGraphic + FitText + Vertical + Flow + Resize + ID + 2099 + Shape + Rectangle + Style + + fill + + GradientCenter + {-0.29411799999999999, -0.264706} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1347\cocoasubrtf570 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\b\fs24 \cf0 alembic.operations.op} + VerticalPad + 0 + + TextPlacement + 0 + + + Bounds + {{781.00002098083496, 182.37493896484375}, {139, 56}} + Class + ShapedGraphic + FitText + Vertical + Flow + Resize + ID + 2100 + Shape + Rectangle + Style + + fill + + GradientCenter + {-0.29411799999999999, -0.264706} + + + Text + + Align + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1347\cocoasubrtf570 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 + +\f0\fs24 \cf0 CreateTableOp\ +AlterColumnOp\ +AddColumnOp\ +DropColumnOp} + VerticalPad + 0 + + TextPlacement + 0 + + + GridH + + 2099 + 2100 + + + GroupConnect + YES + ID + 2098 + + + Bounds + {{333.24926419826539, 462.28131709379346}, {78, 12}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 2090 + Line + + ID + 2068 + Position + 0.44118145108222961 + RotationType + 0 + + Shape + Rectangle + Style + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1347\cocoasubrtf570 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 + +\f0\fs20 \cf0 <<read/write>>} + VerticalPad + 0 + + Wrap + NO + + + Class + TableGroup + Graphics + + + Bounds + {{781, 298.10002136230469}, {139, 14}} + Class + ShapedGraphic + FitText + Vertical + Flow + Resize + ID + 2082 + Shape + Rectangle + Style + + fill + + GradientCenter + {-0.29411799999999999, -0.264706} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1347\cocoasubrtf570 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\b\fs24 \cf0 alembic.autogenerate} + VerticalPad + 0 + + TextPlacement + 0 + + + Bounds + {{781, 312.10002136230469}, {139, 70}} + Class + ShapedGraphic + FitText + Vertical + Flow + Resize + ID + 2083 + Shape + Rectangle + Style + + fill + + GradientCenter + {-0.29411799999999999, -0.264706} + + + Text + + Align + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1347\cocoasubrtf570 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 + +\f0\fs24 \cf0 compare_metadata()\ +produce_migrations()\ +compare\ +render\ +generate} + VerticalPad + 0 + + TextPlacement + 0 + + + GridH + + 2082 + 2083 + + + GroupConnect + YES + ID + 2081 + Magnets + + {0.032374100719424703, 0.5} + {-0.5071942446043165, -0.010850225176129769} + {0.52163523392711664, 0} + {0, -0.5} + {-0.5, 0.24999999999999911} + + + + Class + TableGroup + Graphics + + + Bounds + {{563, 322}, {139, 14}} + Class + ShapedGraphic + FitText + Vertical + Flow + Resize + ID + 2073 + Shape + Rectangle + Style + + fill + + GradientCenter + {-0.29411799999999999, -0.264706} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1347\cocoasubrtf570 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\b\fs24 \cf0 alembic.command} + VerticalPad + 0 + + TextPlacement + 0 + + + Bounds + {{563, 336}, {139, 70}} + Class + ShapedGraphic + FitText + Vertical + Flow + Resize + ID + 2074 + Shape + Rectangle + Style + + fill + + GradientCenter + {-0.29411799999999999, -0.264706} + + + Text + + Align + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1347\cocoasubrtf570 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 + +\f0\fs24 \cf0 init()\ +revision()\ +upgrade()\ +downgrade()\ +history()} + VerticalPad + 0 + + TextPlacement + 0 + + + GridH + + 2073 + 2074 + + + GroupConnect + YES + ID + 2072 + Magnets + + {0.032374100719424703, 0.5} + {-0.5071942446043165, -0.010850225176129769} + {0.26978417266187105, 0.50105453672863209} + {0.16675024238421798, -0.51583989461263036} + {0.5, 0.24999999999999911} + {0.50000000000000089, -0.010696321272922305} + {-0.50719424460431561, -0.28571428571428559} + + + + Class + LineGraphic + Head + + ID + 2067 + + ID + 2068 + Points + + {426.78369522094727, 467.79283450278251} + {303.17371368408192, 468.90004920959467} + + Style + + stroke + + HeadArrow + StickArrow + HopLines + + HopType + 102 + Legacy + + Pattern + 1 + TailArrow + 0 + + + Tail + + ID + 2059 + + + + Class + Group + Graphics + + + Bounds + {{218.92971038818359, 448.71651649475098}, {74.487998962402344, 46}} + Class + ShapedGraphic + FitText + Vertical + Flow + Resize + FontInfo + + Font + Helvetica + Size + 10 + + ID + 2066 + Shape + Rectangle + Style + + Text + + Align + 0 + Pad + 1 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1347\cocoasubrtf570 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural + +\f0\fs20 \cf0 \expnd0\expndtw0\kerning0 +/versions/a.py\ +/versions/b.py\ +/versions/...} + + + + Bounds + {{209.17371368408203, 424.9000186920166}, {94, 84}} + Class + ShapedGraphic + ID + 2067 + Magnets + + {0.49999999999999911, -0.30952344621930905} + {0.49999999999999911, 0.023809887114024875} + + Shape + Rectangle + Style + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1347\cocoasubrtf570 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 filesystem} + + TextPlacement + 0 + + + ID + 2065 + + + Class + TableGroup + Graphics + + + Bounds + {{426.78369522094727, 442.76912879943848}, {139, 14}} + Class + ShapedGraphic + FitText + Vertical + Flow + Resize + ID + 2060 + Shape + Rectangle + Style + + fill + + GradientCenter + {-0.29411799999999999, -0.264706} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1347\cocoasubrtf570 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\b\fs24 \cf0 ScriptDirectory} + VerticalPad + 0 + + TextPlacement + 0 + + + Bounds + {{426.78369522094727, 456.76912879943848}, {139, 42}} + Class + ShapedGraphic + FitText + Vertical + Flow + Resize + ID + 2061 + Shape + Rectangle + Style + + fill + + GradientCenter + {-0.29411799999999999, -0.264706} + + + Text + + Align + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1347\cocoasubrtf570 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 + +\f0\fs24 \cf0 walk_revisions()\ +get_revision()\ +generate_revision()} + VerticalPad + 0 + + TextPlacement + 0 + + + GridH + + 2060 + 2061 + + + GroupConnect + YES + ID + 2059 + Magnets + + {0.51040606996823534, 0.089285714285713524} + {0.25000000000000044, -0.50000000000000089} + {-0.50398241924039766, -0.053571428571430602} + {-0.00038529693823985411, 0.5357142857142847} + {0.5015561494895886, -0.29944872856140314} + + + + Class + LineGraphic + Head + + ID + 2038 + + ID + 2058 + Points + + {259.5464429157899, 256.16651000976572} + {259.5464429157899, 299.49998778426624} + + Style + + stroke + + HeadArrow + StickArrow + Legacy + + Pattern + 1 + TailArrow + 0 + + + Tail + + ID + 2055 + + + + Class + TableGroup + Graphics + + + Bounds + {{208.09290313720703, 172.16651000976572}, {102.90709686279297, 14}} + Class + ShapedGraphic + FitText + Vertical + Flow + Resize + ID + 2056 + Shape + Rectangle + Style + + fill + + GradientCenter + {-0.29411799999999999, -0.264706} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1347\cocoasubrtf570 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\b\fs24 \cf0 DefaultImpl} + VerticalPad + 0 + + TextPlacement + 0 + + + Bounds + {{208.09290313720703, 186.16651000976572}, {102.90709686279297, 70}} + Class + ShapedGraphic + FitText + Vertical + Flow + Resize + ID + 2057 + Shape + Rectangle + Style + + fill + + GradientCenter + {-0.29411799999999999, -0.264706} + + + Text + + Align + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1347\cocoasubrtf570 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 + +\f0\fs24 \cf0 execute()\ +create_table()\ +alter_column()\ +add_column()\ +drop_column()} + VerticalPad + 0 + + TextPlacement + 0 + + + GridH + + 2056 + 2057 + + + GroupConnect + YES + ID + 2055 + + + Class + TableGroup + Graphics + + + Bounds + {{713.35941696166992, 429.89997863769531}, {119.0880126953125, 14}} Class ShapedGraphic FitText @@ -166,28 +2032,28 @@ fill - GradientAngle - 304 GradientCenter - {-0.294118, -0.264706} + {-0.29411799999999999, -0.264706} Text Text - {\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf360 -{\fonttbl\f0\fswiss\fcharset0 Helvetica;} + {\rtf1\ansi\ansicpg1252\cocoartf1347\cocoasubrtf570 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc -\f0\b\fs24 \cf0 Config} +\f0\b\fs24 \cf0 alembic.config} + VerticalPad + 0 TextPlacement 0 Bounds - {{19, 221.6}, {90, 14}} + {{713.35941696166992, 443.89997863769531}, {119.0880126953125, 42}} Class ShapedGraphic FitText @@ -202,10 +2068,8 @@ fill - GradientAngle - 304 GradientCenter - {-0.294118, -0.264706} + {-0.29411799999999999, -0.264706} Text @@ -213,12 +2077,16 @@ Align 0 Text - {\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf360 -{\fonttbl\f0\fswiss\fcharset0 Helvetica;} + {\rtf1\ansi\ansicpg1252\cocoartf1347\cocoasubrtf570 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 -\f0\fs24 \cf0 ConfigParser} +\f0\fs24 \cf0 Config\ +Command\ +main()} + VerticalPad + 0 TextPlacement 0 @@ -234,180 +2102,13 @@ YES ID 2048 - - - Class - LineGraphic - Head - - ID - 33 - - ID - 2046 - OrthogonalBarAutomatic - - OrthogonalBarPosition - 28.725006103515625 - Points - - {385.25, 157} - {304, 191.818} - - Style - - stroke - - HeadArrow - StickArrow - LineType - 2 - Pattern - 1 - TailArrow - 0 - - - Tail - - ID - 2042 - - - - Class - LineGraphic - Head - - ID - 38 - - ID - 2044 - OrthogonalBarAutomatic - - OrthogonalBarPosition - 52.850021362304688 - Points - - {454.25, 177} - {442.638, 294.6} - - Style - - stroke - - HeadArrow - StickArrow - LineType - 2 - Pattern - 1 - TailArrow - 0 - - - Tail - - ID - 2043 - - - - Bounds - {{385.25, 172}, {69, 14}} - Class - ShapedGraphic - FitText - YES - Flow - Resize - ID - 2043 Magnets - {0.5, -0.142857} + {0.5, -4.4408920985006262e-16} + {-0.5, -0.25000000000000178} + {-0.1138779104937786, -0.5} + {-0.49999999999999911, 0.33902539955400712} - Shape - Rectangle - Style - - fill - - Draws - NO - - shadow - - Draws - NO - - stroke - - Draws - NO - - - Text - - Text - {\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf360 -{\fonttbl\f0\fswiss\fcharset0 Helvetica;} -{\colortbl;\red255\green255\blue255;} -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural - -\f0\fs24 \cf0 alembic.op} - - Wrap - NO - - - Bounds - {{385.25, 149.6}, {94, 14}} - Class - ShapedGraphic - FitText - YES - Flow - Resize - ID - 2042 - Magnets - - {0.49734, 0.0285711} - - Shape - Rectangle - Style - - fill - - Draws - NO - - shadow - - Draws - NO - - stroke - - Draws - NO - - - Text - - Text - {\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf360 -{\fonttbl\f0\fswiss\fcharset0 Helvetica;} -{\colortbl;\red255\green255\blue255;} -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural - -\f0\fs24 \cf0 alembic.context} - - Wrap - NO Class @@ -415,14 +2116,14 @@ Head ID - 2038 + 2055 ID 2040 Points - {166.088, 336.6} - {105.686, 336.6} + {373, 215.59905413254651} + {311, 214.81620239134219} Style @@ -430,6 +2131,8 @@ HeadArrow StickArrow + Legacy + Pattern 1 TailArrow @@ -444,185 +2147,27 @@ Bounds - {{19, 294.6}, {86.1858, 84}} + {{216.45355606079102, 299.9999877929688}, {86.1858, 84}} Class ShapedGraphic ID 2038 Shape Cylinder + Style + Text Text - {\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf360 -{\fonttbl\f0\fswiss\fcharset0 Helvetica;} + {\rtf1\ansi\ansicpg1252\cocoartf1347\cocoasubrtf570 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc \f0\fs24 \cf0 database} - - - - Bounds - {{227.597, 278.569}, {55, 12}} - Class - ShapedGraphic - FitText - YES - ID - 51 - Line - - ID - 50 - Offset - -20 - Position - 0.40689659118652344 - RotationType + VerticalPad 0 - Shape - Rectangle - Style - - shadow - - Draws - NO - - stroke - - Draws - NO - - - Text - - Align - 0 - Text - {\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf360 -{\fonttbl\f0\fswiss\fcharset0 Helvetica;} -{\colortbl;\red255\green255\blue255;} -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural - -\f0\fs20 \cf0 <<uses>>} - - - - Class - LineGraphic - Head - - ID - 41 - - ID - 50 - Points - - {234.897, 263.6} - {235.389, 315.6} - - Style - - stroke - - HeadArrow - StickArrow - Pattern - 1 - TailArrow - 0 - - - Tail - - ID - 33 - - - - Bounds - {{308.265, 310.6}, {55, 12}} - Class - ShapedGraphic - FitText - YES - ID - 49 - Line - - ID - 9 - Offset - -20 - Position - 0.5199354887008667 - RotationType - 0 - - Shape - Rectangle - Style - - shadow - - Draws - NO - - stroke - - Draws - NO - - - Text - - Align - 0 - Text - {\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf360 -{\fonttbl\f0\fswiss\fcharset0 Helvetica;} -{\colortbl;\red255\green255\blue255;} -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural - -\f0\fs20 \cf0 <<uses>>} - - - - Class - LineGraphic - Head - - ID - 41 - - ID - 9 - Points - - {368.99, 336.6} - {305.088, 336.6} - - Style - - stroke - - HeadArrow - StickArrow - Pattern - 1 - TailArrow - 0 - - - Tail - - ID - 38 - Class @@ -631,7 +2176,7 @@ Bounds - {{166.088, 315.6}, {139, 14}} + {{373, 180.20000610351565}, {119.0880126953125, 14}} Class ShapedGraphic FitText @@ -646,28 +2191,28 @@ fill - GradientAngle - 304 GradientCenter - {-0.294118, -0.264706} + {-0.29411799999999999, -0.264706} Text Text - {\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf360 -{\fonttbl\f0\fswiss\fcharset0 Helvetica;} + {\rtf1\ansi\ansicpg1252\cocoartf1347\cocoasubrtf570 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc \f0\b\fs24 \cf0 MigrationContext} + VerticalPad + 0 TextPlacement 0 Bounds - {{166.088, 329.6}, {139, 28}} + {{373, 194.20000610351565}, {119.0880126953125, 56}} Class ShapedGraphic FitText @@ -682,10 +2227,8 @@ fill - GradientAngle - 304 GradientCenter - {-0.294118, -0.264706} + {-0.29411799999999999, -0.264706} Text @@ -693,13 +2236,17 @@ Align 0 Text - {\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf360 -{\fonttbl\f0\fswiss\fcharset0 Helvetica;} + {\rtf1\ansi\ansicpg1252\cocoartf1347\cocoasubrtf570 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural\pardirnatural +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural \f0\fs24 \cf0 connection\ -run_migrations()} +run_migrations()\ +execute()\ +stamp()} + VerticalPad + 0 TextPlacement 0 @@ -715,6 +2262,14 @@ run_migrations()} YES ID 41 + Magnets + + {0.5, -0.16088094860684521} + {0.0042301604752394972, -0.5514285714285716} + {-0.49936690654431892, 0.0057142857142853387} + {0.49343873986566722, 0.24857142857142822} + {0.029020499831381219, 0.46857134137834766} + Class @@ -723,7 +2278,7 @@ run_migrations()} Bounds - {{368.99, 294.6}, {139, 14}} + {{572.95599365234375, 175.59130477905273}, {119.0880126953125, 14}} Class ShapedGraphic FitText @@ -738,28 +2293,28 @@ run_migrations()} fill - GradientAngle - 304 GradientCenter - {-0.294118, -0.264706} + {-0.29411799999999999, -0.264706} Text Text - {\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf360 -{\fonttbl\f0\fswiss\fcharset0 Helvetica;} + {\rtf1\ansi\ansicpg1252\cocoartf1347\cocoasubrtf570 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc \f0\b\fs24 \cf0 Operations} + VerticalPad + 0 TextPlacement 0 Bounds - {{368.99, 308.6}, {139, 70}} + {{572.95599365234375, 189.59130477905273}, {119.0880126953125, 70}} Class ShapedGraphic FitText @@ -774,10 +2329,8 @@ run_migrations()} fill - GradientAngle - 304 GradientCenter - {-0.294118, -0.264706} + {-0.29411799999999999, -0.264706} Text @@ -785,16 +2338,18 @@ run_migrations()} Align 0 Text - {\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf360 -{\fonttbl\f0\fswiss\fcharset0 Helvetica;} + {\rtf1\ansi\ansicpg1252\cocoartf1347\cocoasubrtf570 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 \f0\fs24 \cf0 migration_context\ create_table()\ alter_column()\ add_column()\ drop_column()} + VerticalPad + 0 TextPlacement 0 @@ -810,6 +2365,10 @@ drop_column()} YES ID 38 + Magnets + + {-0.49999999999999911, -0.17370600927443736} + Class @@ -818,7 +2377,7 @@ drop_column()} Bounds - {{165, 179.6}, {139, 14}} + {{367.95599365234375, 298.09998092651369}, {129.176025390625, 14.000003814697266}} Class ShapedGraphic FitText @@ -833,28 +2392,28 @@ drop_column()} fill - GradientAngle - 304 GradientCenter - {-0.294118, -0.264706} + {-0.29411799999999999, -0.264706} Text Text - {\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf360 -{\fonttbl\f0\fswiss\fcharset0 Helvetica;} + {\rtf1\ansi\ansicpg1252\cocoartf1347\cocoasubrtf570 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc \f0\b\fs24 \cf0 EnvironmentContext} + VerticalPad + 0 TextPlacement 0 Bounds - {{165, 193.6}, {139, 70}} + {{367.95599365234375, 312.09998855590823}, {129.176025390625, 70.000015258789062}} Class ShapedGraphic FitText @@ -869,10 +2428,8 @@ drop_column()} fill - GradientAngle - 304 GradientCenter - {-0.294118, -0.264706} + {-0.29411799999999999, -0.264706} Text @@ -880,16 +2437,18 @@ drop_column()} Align 0 Text - {\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf360 -{\fonttbl\f0\fswiss\fcharset0 Helvetica;} + {\rtf1\ansi\ansicpg1252\cocoartf1347\cocoasubrtf570 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 \f0\fs24 \cf0 migration_context\ configure()\ run_migrations()\ begin_transaction()\ is_offline_mode()} + VerticalPad + 0 TextPlacement 0 @@ -905,10 +2464,16 @@ is_offline_mode()} YES ID 33 + Magnets + + {0.5, -0.14544617445169949} + {0.019251798561151112, 0.50476190476190474} + {0.019070177820008194, -0.49999999999999956} + Bounds - {{153.176, 149.6}, {164.824, 255}} + {{350, 148.9999938964844}, {164.82400000000001, 255.60000610351562}} Class ShapedGraphic ID @@ -951,12 +2516,14 @@ is_offline_mode()} Align 0 Text - {\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf360 -{\fonttbl\f0\fswiss\fcharset0 Helvetica;} + {\rtf1\ansi\ansicpg1252\cocoartf1347\cocoasubrtf570 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural\pardirnatural +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural \f0\fs24 \cf0 env.py script} + VerticalPad + 0 TextPlacement 0 @@ -965,11 +2532,16 @@ is_offline_mode()} Bounds - {{343.99, 259.266}, {189, 145.334}} + {{552, 149}, {169, 130.33299255371094}} Class ShapedGraphic ID 2032 + Magnets + + {-0.43313956596913394, 0.50000000000000044} + {0.014211640211639676, 0.49587157857074082} + Shape Rectangle Style @@ -1008,12 +2580,14 @@ is_offline_mode()} Align 0 Text - {\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf360 -{\fonttbl\f0\fswiss\fcharset0 Helvetica;} + {\rtf1\ansi\ansicpg1252\cocoartf1347\cocoasubrtf570 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural\pardirnatural +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural \f0\fs24 \cf0 migration script} + VerticalPad + 0 TextPlacement 0 @@ -1021,61 +2595,51 @@ is_offline_mode()} NO - Bounds - {{138.176, 127.6}, {420.824, 293.4}} Class - ShapedGraphic + LineGraphic + Head + + ID + 2048 + ID - 2037 - Shape - Rectangle + 2139 + OrthogonalBarAutomatic + + OrthogonalBarPoint + {0, 0} + OrthogonalBarPosition + -1 + Points + + {435.00741612193735, 382.10000381469729} + {548, 421.9000186920166} + {601.38076234099412, 436} + {713.35941696166992, 443.8999786376952} + Style - fill - - Draws - NO - - shadow - - Draws - NO - Fuzziness - 0.0 - stroke - Color - - b - 0.191506 - g - 0.389204 - r - 0.744565 - - CornerRadius - 5 + HeadArrow + StickArrow + Legacy + + LineType + 2 Pattern 1 + TailArrow + 0 - Text + Tail - Align - 0 - Text - {\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf360 -{\fonttbl\f0\fswiss\fcharset0 Helvetica;} -{\colortbl;\red255\green255\blue255;} -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural\pardirnatural - -\f0\fs24 \cf0 alembic command} + ID + 33 + Info + 2 - TextPlacement - 0 - Wrap - NO GridInfo @@ -1085,11 +2649,9 @@ is_offline_mode()} GuidesVisible YES HPages - 1 + 2 ImageCounter 1 - IsPalette - NO KeepToScale Layers @@ -1106,78 +2668,28 @@ is_offline_mode()} LayoutInfo - + + Animate + NO + circoMinDist + 18 + circoSeparation + 0.0 + layoutEngine + dot + neatoSeparation + 0.0 + twopiSeparation + 0.0 + LinksVisible NO MagnetsVisible NO - MasterSheet - Master 1 MasterSheets - - - ActiveLayerIndex - 0 - AutoAdjust - - CanvasColor - - w - 1 - - CanvasOrigin - {0, 0} - CanvasScale - 1 - ColumnAlign - 1 - ColumnSpacing - 36 - DisplayScale - 1 in = 1 in - GraphicsList - - GridInfo - - HPages - 1 - IsPalette - NO - KeepToScale - - Layers - - - Lock - NO - Name - Layer 1 - Print - YES - View - YES - - - LayoutInfo - - Orientation - 2 - OutlineStyle - Basic - RowAlign - 1 - RowSpacing - 36 - SheetTitle - Master 1 - UniqueID - 1 - VPages - 1 - - + ModificationDate - 2012-01-24 17:59:01 -0500 + 2015-07-02 23:12:07 +0000 Modifier classic NotesVisible @@ -1189,35 +2701,47 @@ is_offline_mode()} OutlineStyle Basic PageBreaks - YES + NO PrintInfo NSBottomMargin + + float + 12 + + NSHorizonalPagination coded - BAtzdHJlYW10eXBlZIHoA4QBQISEhAhOU051bWJlcgCEhAdOU1ZhbHVlAISECE5TT2JqZWN0AIWEASqEhAFklwCG + BAtzdHJlYW10eXBlZIHoA4QBQISEhAhOU051bWJlcgCEhAdOU1ZhbHVlAISECE5TT2JqZWN0AIWEASqEhAFxlwCG NSLeftMargin - coded - BAtzdHJlYW10eXBlZIHoA4QBQISEhAhOU051bWJlcgCEhAdOU1ZhbHVlAISECE5TT2JqZWN0AIWEASqEhAFklwCG + float + 12 NSPaperSize size {612, 792} + NSPrintReverseOrientation + + int + 0 + NSRightMargin - coded - BAtzdHJlYW10eXBlZIHoA4QBQISEhAhOU051bWJlcgCEhAdOU1ZhbHVlAISECE5TT2JqZWN0AIWEASqEhAFklwCG + float + 12 NSTopMargin - coded - BAtzdHJlYW10eXBlZIHoA4QBQISEhAhOU051bWJlcgCEhAdOU1ZhbHVlAISECE5TT2JqZWN0AIWEASqEhAFklwCG + float + 12 + PrintOnePage + ReadOnly NO RowAlign @@ -1239,25 +2763,33 @@ is_offline_mode()} WindowInfo CurrentSheet - 0 - DrawerOpen - - DrawerTab - Outline - DrawerWidth - 209 - FitInWindow - + 0 + ExpandedCanvases + Frame - {{335, 211}, {760, 817}} - ShowRuler + {{130, 128}, {1193, 852}} + ListView - ShowStatusBar - + OutlineWidth + 142 + RightSidebar + + Sidebar + + SidebarWidth + 138 VisibleRegion - {{-84, 0}, {745, 703}} + {{-8, 1}, {1193, 755}} Zoom - 1 + 1 + ZoomValues + + + Canvas 1 + 1 + 1 + + diff --git a/docs/build/changelog.rst b/docs/build/changelog.rst index 193c87f..8fd6293 100644 --- a/docs/build/changelog.rst +++ b/docs/build/changelog.rst @@ -3,6 +3,44 @@ Changelog ========== +.. changelog:: + :version: 0.8.0 + + .. change:: + :tags: feature, operations + :tickets: 302 + + The internal system for Alembic operations has been reworked to now + build upon an extensible system of operation objects. New operations + can be added to the ``op.`` namespace, including that they are + available in custom autogenerate schemes. + + .. seealso:: + + :ref:`operation_plugins` + + .. change:: + :tags: feature, autogenerate + :tickets: 301 + + The internal system for autogenerate been reworked to build upon + the extensible system of operation objects present in + :ticket:`302`. As part of this change, autogenerate now produces + a full object graph representing a list of migration scripts to + be written as well as operation objects that will render all the + Python code within them; a new hook + :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. + + .. seealso:: + + :ref:`alembic.autogenerate.toplevel` + + .. changelog:: :version: 0.7.7 diff --git a/docs/build/cookbook.rst b/docs/build/cookbook.rst index 8c1e0d7..541f595 100644 --- a/docs/build/cookbook.rst +++ b/docs/build/cookbook.rst @@ -193,7 +193,7 @@ Sharing a Connection with a Series of Migration Commands and Environments ========================================================================= It is often the case that an application will need to call upon a series -of commands within :mod:`alembic.command`, where it would be advantageous +of commands within :ref:`alembic.command.toplevel`, where it would be advantageous for all operations to proceed along a single transaction. The connectivity for a migration is typically solely determined within the ``env.py`` script of a migration environment, which is called within the scope of a command. diff --git a/docs/build/front.rst b/docs/build/front.rst index 3270f5c..6e28419 100644 --- a/docs/build/front.rst +++ b/docs/build/front.rst @@ -49,25 +49,19 @@ then proceed through the usage of this command. Dependencies ------------ -Alembic's install process will ensure that `SQLAlchemy `_ +Alembic's install process will ensure that SQLAlchemy_ is installed, in addition to other dependencies. Alembic will work with -SQLAlchemy as of version **0.7.3**. The latest version of SQLAlchemy within -the **0.7**, **0.8**, or more recent series is strongly recommended. +SQLAlchemy as of version **0.7.3**, however more features are available with +newer versions such as the 0.9 or 1.0 series. Alembic supports Python versions 2.6 and above. -.. versionchanged:: 0.5.0 - Support for SQLAlchemy 0.6 has been dropped. - -.. versionchanged:: 0.6.0 - Now supporting Python 2.6 and above. - Community ========= Alembic is developed by `Mike Bayer `_, and is -loosely associated with the `SQLAlchemy `_ and `Pylons `_ -projects. +loosely associated with the SQLAlchemy_, `Pylons `_, +and `Openstack `_ projects. User issues, discussion of potential bugs and features should be posted to the Alembic Google Group at `sqlalchemy-alembic `_. @@ -78,3 +72,6 @@ Bugs ==== Bugs and feature enhancements to Alembic should be reported on the `Bitbucket issue tracker `_. + + +.. _SQLAlchemy: http://www.sqlalchemy.org \ No newline at end of file diff --git a/docs/build/index.rst b/docs/build/index.rst index de18f9e..17ffc06 100644 --- a/docs/build/index.rst +++ b/docs/build/index.rst @@ -6,7 +6,7 @@ Welcome to Alembic's documentation! with the `SQLAlchemy `_ Database Toolkit for Python. .. toctree:: - :maxdepth: 2 + :maxdepth: 3 front tutorial @@ -17,7 +17,7 @@ with the `SQLAlchemy `_ Database Toolkit for Python. branches ops cookbook - api + api/index changelog Indices and tables diff --git a/docs/build/ops.rst b/docs/build/ops.rst index 1df9d27..49aaef5 100644 --- a/docs/build/ops.rst +++ b/docs/build/ops.rst @@ -7,8 +7,8 @@ Operation Reference This file provides documentation on Alembic migration directives. The directives here are used within user-defined migration files, -within the ``upgrade()`` and ``downgrade()`` functions, as well as -any functions further invoked by those. +within the ``upgrade()`` and ``downgrade()`` functions, as well as +any functions further invoked by those. All directives exist as methods on a class called :class:`.Operations`. When migration scripts are run, this object is made available @@ -18,12 +18,15 @@ Currently, ``alembic.op`` is a real Python module, populated with individual proxies for each method on :class:`.Operations`, so symbols can be imported safely from the ``alembic.op`` namespace. -A key design philosophy to the :mod:`alembic.operations` methods is that -to the greatest degree possible, they internally generate the +The :class:`.Operations` system is also fully extensible. See +:ref:`operation_plugins` for details on this. + +A key design philosophy to the :ref:`alembic.operations.toplevel` methods is that +to the greatest degree possible, they internally generate the appropriate SQLAlchemy metadata, typically involving :class:`~sqlalchemy.schema.Table` and :class:`~sqlalchemy.schema.Constraint` -objects. This so that migration instructions can be -given in terms of just the string names and/or flags involved. +objects. This so that migration instructions can be +given in terms of just the string names and/or flags involved. The exceptions to this rule include the :meth:`~.Operations.add_column` and :meth:`~.Operations.create_table` directives, which require full :class:`~sqlalchemy.schema.Column` @@ -36,6 +39,5 @@ circumstances they are called from an actual migration script, which itself would be invoked by the :meth:`.EnvironmentContext.run_migrations` method. - .. automodule:: alembic.operations - :members: + :members: Operations, BatchOperations diff --git a/tests/_autogen_fixtures.py b/tests/_autogen_fixtures.py new file mode 100644 index 0000000..7ef6cbf --- /dev/null +++ b/tests/_autogen_fixtures.py @@ -0,0 +1,251 @@ +from sqlalchemy import MetaData, Column, Table, Integer, String, Text, \ + Numeric, CHAR, ForeignKey, Index, UniqueConstraint, CheckConstraint, text +from sqlalchemy.engine.reflection import Inspector + +from alembic import autogenerate +from alembic.migration import MigrationContext +from alembic.testing import config +from alembic.testing.env import staging_env, clear_staging_env +from alembic.testing import eq_ +from alembic.ddl.base import _fk_spec + +names_in_this_test = set() + +from sqlalchemy import event + + +@event.listens_for(Table, "after_parent_attach") +def new_table(table, parent): + names_in_this_test.add(table.name) + + +def _default_include_object(obj, name, type_, reflected, compare_to): + if type_ == "table": + return name in names_in_this_test + else: + return True + +_default_object_filters = [ + _default_include_object +] + + +class ModelOne(object): + __requires__ = ('unique_constraint_reflection', ) + + schema = None + + @classmethod + def _get_db_schema(cls): + schema = cls.schema + + m = MetaData(schema=schema) + + Table('user', m, + Column('id', Integer, primary_key=True), + Column('name', String(50)), + Column('a1', Text), + Column("pw", String(50)), + Index('pw_idx', 'pw') + ) + + Table('address', m, + Column('id', Integer, primary_key=True), + Column('email_address', String(100), nullable=False), + ) + + Table('order', m, + Column('order_id', Integer, primary_key=True), + Column("amount", Numeric(8, 2), nullable=False, + server_default=text("0")), + CheckConstraint('amount >= 0', name='ck_order_amount') + ) + + Table('extra', m, + Column("x", CHAR), + Column('uid', Integer, ForeignKey('user.id')) + ) + + return m + + @classmethod + def _get_model_schema(cls): + schema = cls.schema + + m = MetaData(schema=schema) + + Table('user', m, + Column('id', Integer, primary_key=True), + Column('name', String(50), nullable=False), + Column('a1', Text, server_default="x") + ) + + Table('address', m, + Column('id', Integer, primary_key=True), + Column('email_address', String(100), nullable=False), + Column('street', String(50)), + UniqueConstraint('email_address', name="uq_email") + ) + + Table('order', m, + Column('order_id', Integer, primary_key=True), + Column('amount', Numeric(10, 2), nullable=True, + server_default=text("0")), + Column('user_id', Integer, ForeignKey('user.id')), + CheckConstraint('amount > -1', name='ck_order_amount'), + ) + + Table('item', m, + Column('id', Integer, primary_key=True), + Column('description', String(100)), + Column('order_id', Integer, ForeignKey('order.order_id')), + CheckConstraint('len(description) > 5') + ) + return m + + +class _ComparesFKs(object): + def _assert_fk_diff( + self, diff, type_, source_table, source_columns, + target_table, target_columns, name=None, conditional_name=None, + source_schema=None): + # the public API for ForeignKeyConstraint was not very rich + # in 0.7, 0.8, so here we use the well-known but slightly + # private API to get at its elements + (fk_source_schema, fk_source_table, + fk_source_columns, fk_target_schema, fk_target_table, + fk_target_columns) = _fk_spec(diff[1]) + + eq_(diff[0], type_) + eq_(fk_source_table, source_table) + eq_(fk_source_columns, source_columns) + eq_(fk_target_table, target_table) + eq_(fk_source_schema, source_schema) + + eq_([elem.column.name for elem in diff[1].elements], + target_columns) + if conditional_name is not None: + if config.requirements.no_fk_names.enabled: + eq_(diff[1].name, None) + elif conditional_name == 'servergenerated': + fks = Inspector.from_engine(self.bind).\ + get_foreign_keys(source_table) + server_fk_name = fks[0]['name'] + eq_(diff[1].name, server_fk_name) + else: + eq_(diff[1].name, conditional_name) + else: + eq_(diff[1].name, name) + + +class AutogenTest(_ComparesFKs): + + def _flatten_diffs(self, diffs): + for d in diffs: + if isinstance(d, list): + for fd in self._flatten_diffs(d): + yield fd + else: + yield d + + @classmethod + def _get_bind(cls): + return config.db + + configure_opts = {} + + @classmethod + def setup_class(cls): + staging_env() + cls.bind = cls._get_bind() + cls.m1 = cls._get_db_schema() + cls.m1.create_all(cls.bind) + cls.m2 = cls._get_model_schema() + + @classmethod + def teardown_class(cls): + cls.m1.drop_all(cls.bind) + clear_staging_env() + + def setUp(self): + self.conn = conn = self.bind.connect() + ctx_opts = { + 'compare_type': True, + 'compare_server_default': True, + 'target_metadata': self.m2, + 'upgrade_token': "upgrades", + 'downgrade_token': "downgrades", + 'alembic_module_prefix': 'op.', + 'sqlalchemy_module_prefix': 'sa.', + } + if self.configure_opts: + ctx_opts.update(self.configure_opts) + self.context = context = MigrationContext.configure( + connection=conn, + opts=ctx_opts + ) + + connection = context.bind + self.autogen_context = { + 'imports': set(), + 'connection': connection, + 'dialect': connection.dialect, + 'context': context + } + + def tearDown(self): + self.conn.close() + + +class AutogenFixtureTest(_ComparesFKs): + + def _fixture( + self, m1, m2, include_schemas=False, + opts=None, object_filters=_default_object_filters): + self.metadata, model_metadata = m1, m2 + self.metadata.create_all(self.bind) + + with self.bind.connect() as conn: + ctx_opts = { + 'compare_type': True, + 'compare_server_default': True, + 'target_metadata': model_metadata, + 'upgrade_token': "upgrades", + 'downgrade_token': "downgrades", + 'alembic_module_prefix': 'op.', + 'sqlalchemy_module_prefix': 'sa.', + } + if opts: + ctx_opts.update(opts) + self.context = context = MigrationContext.configure( + connection=conn, + opts=ctx_opts + ) + + connection = context.bind + autogen_context = { + 'imports': set(), + 'connection': connection, + 'dialect': connection.dialect, + 'context': context, + 'metadata': model_metadata, + 'object_filters': object_filters, + 'include_schemas': include_schemas + } + diffs = [] + autogenerate._produce_net_changes( + autogen_context, diffs + ) + return diffs + + reports_unnamed_constraints = False + + def setUp(self): + staging_env() + self.bind = config.db + + def tearDown(self): + if hasattr(self, 'metadata'): + self.metadata.drop_all(self.bind) + clear_staging_env() + diff --git a/tests/test_autogen_composition.py b/tests/test_autogen_composition.py new file mode 100644 index 0000000..b1717ab --- /dev/null +++ b/tests/test_autogen_composition.py @@ -0,0 +1,328 @@ +import re + +from alembic import autogenerate +from alembic.migration import MigrationContext +from alembic.testing import TestBase +from alembic.testing import eq_ + +from ._autogen_fixtures import AutogenTest, ModelOne, _default_include_object + + +class AutogenerateDiffTest(ModelOne, AutogenTest, TestBase): + __only_on__ = 'sqlite' + + def test_render_nothing(self): + context = MigrationContext.configure( + connection=self.bind.connect(), + opts={ + 'compare_type': True, + 'compare_server_default': True, + 'target_metadata': self.m1, + 'upgrade_token': "upgrades", + 'downgrade_token': "downgrades", + } + ) + template_args = {} + autogenerate._render_migration_diffs(context, template_args, set()) + + eq_(re.sub(r"u'", "'", template_args['upgrades']), + """### commands auto generated by Alembic - please adjust! ### + pass + ### end Alembic commands ###""") + eq_(re.sub(r"u'", "'", template_args['downgrades']), + """### commands auto generated by Alembic - please adjust! ### + pass + ### end Alembic commands ###""") + + def test_render_nothing_batch(self): + context = MigrationContext.configure( + connection=self.bind.connect(), + opts={ + 'compare_type': True, + 'compare_server_default': True, + 'target_metadata': self.m1, + 'upgrade_token': "upgrades", + 'downgrade_token': "downgrades", + 'alembic_module_prefix': 'op.', + 'sqlalchemy_module_prefix': 'sa.', + 'render_as_batch': True, + 'include_symbol': lambda name, schema: False + } + ) + template_args = {} + autogenerate._render_migration_diffs( + context, template_args, set(), + + ) + eq_(re.sub(r"u'", "'", template_args['upgrades']), + """### commands auto generated by Alembic - please adjust! ### + pass + ### end Alembic commands ###""") + eq_(re.sub(r"u'", "'", template_args['downgrades']), + """### commands auto generated by Alembic - please adjust! ### + pass + ### end Alembic commands ###""") + + def test_render_diffs_standard(self): + """test a full render including indentation""" + + template_args = {} + autogenerate._render_migration_diffs( + self.context, template_args, set()) + eq_(re.sub(r"u'", "'", template_args['upgrades']), + """### commands auto generated by Alembic - please adjust! ### + op.create_table('item', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('description', sa.String(length=100), nullable=True), + sa.Column('order_id', sa.Integer(), nullable=True), + sa.CheckConstraint('len(description) > 5'), + sa.ForeignKeyConstraint(['order_id'], ['order.order_id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.drop_table('extra') + op.add_column('address', sa.Column('street', sa.String(length=50), \ +nullable=True)) + op.create_unique_constraint('uq_email', 'address', ['email_address']) + op.add_column('order', sa.Column('user_id', sa.Integer(), nullable=True)) + op.alter_column('order', 'amount', + existing_type=sa.NUMERIC(precision=8, scale=2), + type_=sa.Numeric(precision=10, scale=2), + nullable=True, + existing_server_default=sa.text('0')) + op.create_foreign_key(None, 'order', 'user', ['user_id'], ['id']) + op.alter_column('user', 'a1', + existing_type=sa.TEXT(), + server_default='x', + existing_nullable=True) + op.alter_column('user', 'name', + existing_type=sa.VARCHAR(length=50), + nullable=False) + op.drop_index('pw_idx', table_name='user') + op.drop_column('user', 'pw') + ### end Alembic commands ###""") + + eq_(re.sub(r"u'", "'", template_args['downgrades']), + """### commands auto generated by Alembic - please adjust! ### + op.add_column('user', sa.Column('pw', sa.VARCHAR(length=50), \ +nullable=True)) + op.create_index('pw_idx', 'user', ['pw'], unique=False) + op.alter_column('user', 'name', + existing_type=sa.VARCHAR(length=50), + nullable=True) + op.alter_column('user', 'a1', + existing_type=sa.TEXT(), + server_default=None, + existing_nullable=True) + op.drop_constraint(None, 'order', type_='foreignkey') + op.alter_column('order', 'amount', + existing_type=sa.Numeric(precision=10, scale=2), + type_=sa.NUMERIC(precision=8, scale=2), + nullable=False, + existing_server_default=sa.text('0')) + op.drop_column('order', 'user_id') + op.drop_constraint('uq_email', 'address', type_='unique') + op.drop_column('address', 'street') + op.create_table('extra', + sa.Column('x', sa.CHAR(), nullable=True), + sa.Column('uid', sa.INTEGER(), nullable=True), + sa.ForeignKeyConstraint(['uid'], ['user.id'], ) + ) + op.drop_table('item') + ### end Alembic commands ###""") + + def test_render_diffs_batch(self): + """test a full render in batch mode including indentation""" + + template_args = {} + self.context.opts['render_as_batch'] = True + autogenerate._render_migration_diffs( + self.context, template_args, set()) + + eq_(re.sub(r"u'", "'", template_args['upgrades']), + """### commands auto generated by Alembic - please adjust! ### + op.create_table('item', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('description', sa.String(length=100), nullable=True), + sa.Column('order_id', sa.Integer(), nullable=True), + sa.CheckConstraint('len(description) > 5'), + sa.ForeignKeyConstraint(['order_id'], ['order.order_id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.drop_table('extra') + with op.batch_alter_table('address', schema=None) as batch_op: + batch_op.add_column(sa.Column('street', sa.String(length=50), nullable=True)) + batch_op.create_unique_constraint('uq_email', ['email_address']) + + with op.batch_alter_table('order', schema=None) as batch_op: + batch_op.add_column(sa.Column('user_id', sa.Integer(), nullable=True)) + batch_op.alter_column('amount', + existing_type=sa.NUMERIC(precision=8, scale=2), + type_=sa.Numeric(precision=10, scale=2), + nullable=True, + existing_server_default=sa.text('0')) + batch_op.create_foreign_key(None, 'order', 'user', ['user_id'], ['id']) + + with op.batch_alter_table('user', schema=None) as batch_op: + batch_op.alter_column('a1', + existing_type=sa.TEXT(), + server_default='x', + existing_nullable=True) + batch_op.alter_column('name', + existing_type=sa.VARCHAR(length=50), + nullable=False) + batch_op.drop_index('pw_idx') + batch_op.drop_column('pw') + + ### end Alembic commands ###""") + + eq_(re.sub(r"u'", "'", template_args['downgrades']), + """### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('user', schema=None) as batch_op: + batch_op.add_column(sa.Column('pw', sa.VARCHAR(length=50), nullable=True)) + batch_op.create_index('pw_idx', ['pw'], unique=False) + batch_op.alter_column('name', + existing_type=sa.VARCHAR(length=50), + nullable=True) + batch_op.alter_column('a1', + existing_type=sa.TEXT(), + server_default=None, + existing_nullable=True) + + with op.batch_alter_table('order', schema=None) as batch_op: + batch_op.drop_constraint(None, type_='foreignkey') + batch_op.alter_column('amount', + existing_type=sa.Numeric(precision=10, scale=2), + type_=sa.NUMERIC(precision=8, scale=2), + nullable=False, + existing_server_default=sa.text('0')) + batch_op.drop_column('user_id') + + with op.batch_alter_table('address', schema=None) as batch_op: + batch_op.drop_constraint('uq_email', type_='unique') + batch_op.drop_column('street') + + op.create_table('extra', + sa.Column('x', sa.CHAR(), nullable=True), + sa.Column('uid', sa.INTEGER(), nullable=True), + sa.ForeignKeyConstraint(['uid'], ['user.id'], ) + ) + op.drop_table('item') + ### end Alembic commands ###""") + + +class AutogenerateDiffTestWSchema(ModelOne, AutogenTest, TestBase): + __only_on__ = 'postgresql' + schema = "test_schema" + + def test_render_nothing(self): + context = MigrationContext.configure( + connection=self.bind.connect(), + opts={ + 'compare_type': True, + 'compare_server_default': True, + 'target_metadata': self.m1, + 'upgrade_token': "upgrades", + 'downgrade_token': "downgrades", + 'alembic_module_prefix': 'op.', + 'sqlalchemy_module_prefix': 'sa.', + 'include_symbol': lambda name, schema: False + } + ) + template_args = {} + autogenerate._render_migration_diffs( + context, template_args, set(), + + ) + eq_(re.sub(r"u'", "'", template_args['upgrades']), + """### commands auto generated by Alembic - please adjust! ### + pass + ### end Alembic commands ###""") + eq_(re.sub(r"u'", "'", template_args['downgrades']), + """### commands auto generated by Alembic - please adjust! ### + pass + ### end Alembic commands ###""") + + def test_render_diffs_extras(self): + """test a full render including indentation (include and schema)""" + + template_args = {} + self.context.opts.update({ + 'include_object': _default_include_object, + 'include_schemas': True + }) + autogenerate._render_migration_diffs( + self.context, template_args, set() + ) + + eq_(re.sub(r"u'", "'", template_args['upgrades']), + """### commands auto generated by Alembic - please adjust! ### + op.create_table('item', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('description', sa.String(length=100), nullable=True), + sa.Column('order_id', sa.Integer(), nullable=True), + sa.CheckConstraint('len(description) > 5'), + sa.ForeignKeyConstraint(['order_id'], ['%(schema)s.order.order_id'], ), + sa.PrimaryKeyConstraint('id'), + schema='%(schema)s' + ) + op.drop_table('extra', schema='%(schema)s') + op.add_column('address', sa.Column('street', sa.String(length=50), \ +nullable=True), schema='%(schema)s') + op.create_unique_constraint('uq_email', 'address', ['email_address'], \ +schema='test_schema') + op.add_column('order', sa.Column('user_id', sa.Integer(), nullable=True), \ +schema='%(schema)s') + op.alter_column('order', 'amount', + existing_type=sa.NUMERIC(precision=8, scale=2), + type_=sa.Numeric(precision=10, scale=2), + nullable=True, + existing_server_default=sa.text('0'), + schema='%(schema)s') + op.create_foreign_key(None, 'order', 'user', ['user_id'], ['id'], \ +source_schema='%(schema)s', referent_schema='%(schema)s') + op.alter_column('user', 'a1', + existing_type=sa.TEXT(), + server_default='x', + existing_nullable=True, + schema='%(schema)s') + op.alter_column('user', 'name', + existing_type=sa.VARCHAR(length=50), + nullable=False, + schema='%(schema)s') + op.drop_index('pw_idx', table_name='user', schema='test_schema') + op.drop_column('user', 'pw', schema='%(schema)s') + ### end Alembic commands ###""" % {"schema": self.schema}) + + eq_(re.sub(r"u'", "'", template_args['downgrades']), + """### commands auto generated by Alembic - please adjust! ### + op.add_column('user', sa.Column('pw', sa.VARCHAR(length=50), \ +autoincrement=False, nullable=True), schema='%(schema)s') + op.create_index('pw_idx', 'user', ['pw'], unique=False, schema='%(schema)s') + op.alter_column('user', 'name', + existing_type=sa.VARCHAR(length=50), + nullable=True, + schema='%(schema)s') + op.alter_column('user', 'a1', + existing_type=sa.TEXT(), + server_default=None, + existing_nullable=True, + schema='%(schema)s') + op.drop_constraint(None, 'order', schema='%(schema)s', type_='foreignkey') + op.alter_column('order', 'amount', + existing_type=sa.Numeric(precision=10, scale=2), + type_=sa.NUMERIC(precision=8, scale=2), + nullable=False, + existing_server_default=sa.text('0'), + schema='%(schema)s') + op.drop_column('order', 'user_id', schema='%(schema)s') + op.drop_constraint('uq_email', 'address', schema='test_schema', type_='unique') + op.drop_column('address', 'street', schema='%(schema)s') + op.create_table('extra', + sa.Column('x', sa.CHAR(length=1), autoincrement=False, nullable=True), + sa.Column('uid', sa.INTEGER(), autoincrement=False, nullable=True), + sa.ForeignKeyConstraint(['uid'], ['%(schema)s.user.id'], \ +name='extra_uid_fkey'), + schema='%(schema)s' + ) + op.drop_table('item', schema='%(schema)s') + ### end Alembic commands ###""" % {"schema": self.schema}) diff --git a/tests/test_autogenerate.py b/tests/test_autogen_diffs.py similarity index 52% rename from tests/test_autogenerate.py rename to tests/test_autogen_diffs.py index a089b42..f32fd84 100644 --- a/tests/test_autogenerate.py +++ b/tests/test_autogen_diffs.py @@ -1,4 +1,3 @@ -import re import sys from sqlalchemy import MetaData, Column, Table, Integer, String, Text, \ @@ -13,170 +12,13 @@ from alembic.testing import TestBase from alembic.testing import config from alembic.testing import assert_raises_message from alembic.testing.mock import Mock -from alembic.testing.env import staging_env, clear_staging_env from alembic.testing import eq_ -from alembic.ddl.base import _fk_spec from alembic.util import CommandError +from ._autogen_fixtures import \ + AutogenTest, AutogenFixtureTest, _default_object_filters py3k = sys.version_info >= (3, ) -names_in_this_test = set() - - -def _default_include_object(obj, name, type_, reflected, compare_to): - if type_ == "table": - return name in names_in_this_test - else: - return True - -_default_object_filters = [ - _default_include_object -] -from sqlalchemy import event - - -@event.listens_for(Table, "after_parent_attach") -def new_table(table, parent): - names_in_this_test.add(table.name) - - -class _ComparesFKs(object): - def _assert_fk_diff( - self, diff, type_, source_table, source_columns, - target_table, target_columns, name=None, conditional_name=None, - source_schema=None): - # the public API for ForeignKeyConstraint was not very rich - # in 0.7, 0.8, so here we use the well-known but slightly - # private API to get at its elements - (fk_source_schema, fk_source_table, - fk_source_columns, fk_target_schema, fk_target_table, - fk_target_columns) = _fk_spec(diff[1]) - - eq_(diff[0], type_) - eq_(fk_source_table, source_table) - eq_(fk_source_columns, source_columns) - eq_(fk_target_table, target_table) - eq_(fk_source_schema, source_schema) - - eq_([elem.column.name for elem in diff[1].elements], - target_columns) - if conditional_name is not None: - if config.requirements.no_fk_names.enabled: - eq_(diff[1].name, None) - elif conditional_name == 'servergenerated': - fks = Inspector.from_engine(self.bind).\ - get_foreign_keys(source_table) - server_fk_name = fks[0]['name'] - eq_(diff[1].name, server_fk_name) - else: - eq_(diff[1].name, conditional_name) - else: - eq_(diff[1].name, name) - - -class AutogenTest(_ComparesFKs): - - @classmethod - def _get_bind(cls): - return config.db - - configure_opts = {} - - @classmethod - def setup_class(cls): - staging_env() - cls.bind = cls._get_bind() - cls.m1 = cls._get_db_schema() - cls.m1.create_all(cls.bind) - cls.m2 = cls._get_model_schema() - - @classmethod - def teardown_class(cls): - cls.m1.drop_all(cls.bind) - clear_staging_env() - - def setUp(self): - self.conn = conn = self.bind.connect() - ctx_opts = { - 'compare_type': True, - 'compare_server_default': True, - 'target_metadata': self.m2, - 'upgrade_token': "upgrades", - 'downgrade_token': "downgrades", - 'alembic_module_prefix': 'op.', - 'sqlalchemy_module_prefix': 'sa.', - } - if self.configure_opts: - ctx_opts.update(self.configure_opts) - self.context = context = MigrationContext.configure( - connection=conn, - opts=ctx_opts - ) - - connection = context.bind - self.autogen_context = { - 'imports': set(), - 'connection': connection, - 'dialect': connection.dialect, - 'context': context - } - - def tearDown(self): - self.conn.close() - - -class AutogenFixtureTest(_ComparesFKs): - - def _fixture( - self, m1, m2, include_schemas=False, - opts=None, object_filters=_default_object_filters): - self.metadata, model_metadata = m1, m2 - self.metadata.create_all(self.bind) - - with self.bind.connect() as conn: - ctx_opts = { - 'compare_type': True, - 'compare_server_default': True, - 'target_metadata': model_metadata, - 'upgrade_token': "upgrades", - 'downgrade_token': "downgrades", - 'alembic_module_prefix': 'op.', - 'sqlalchemy_module_prefix': 'sa.', - } - if opts: - ctx_opts.update(opts) - self.context = context = MigrationContext.configure( - connection=conn, - opts=ctx_opts - ) - - connection = context.bind - autogen_context = { - 'imports': set(), - 'connection': connection, - 'dialect': connection.dialect, - 'context': context - } - diffs = [] - autogenerate._produce_net_changes( - connection, model_metadata, diffs, - autogen_context, - object_filters=object_filters, - include_schemas=include_schemas - ) - return diffs - - reports_unnamed_constraints = False - - def setUp(self): - staging_env() - self.bind = config.db - - def tearDown(self): - if hasattr(self, 'metadata'): - self.metadata.drop_all(self.bind) - clear_staging_env() - class AutogenCrossSchemaTest(AutogenTest, TestBase): __only_on__ = 'postgresql' @@ -221,8 +63,6 @@ class AutogenCrossSchemaTest(AutogenTest, TestBase): return m def test_default_schema_omitted_upgrade(self): - metadata = self.m2 - connection = self.context.bind diffs = [] def include_object(obj, name, type_, reflected, compare_to): @@ -230,17 +70,17 @@ class AutogenCrossSchemaTest(AutogenTest, TestBase): return name == "t3" else: return True - autogenerate._produce_net_changes(connection, metadata, diffs, - self.autogen_context, - object_filters=[include_object], - include_schemas=True - ) + self.autogen_context.update({ + 'object_filters': [include_object], + 'include_schemas': True, + 'metadata': self.m2 + }) + autogenerate._produce_net_changes(self.autogen_context, diffs) + eq_(diffs[0][0], "add_table") eq_(diffs[0][1].schema, None) def test_alt_schema_included_upgrade(self): - metadata = self.m2 - connection = self.context.bind diffs = [] def include_object(obj, name, type_, reflected, compare_to): @@ -248,17 +88,18 @@ class AutogenCrossSchemaTest(AutogenTest, TestBase): return name == "t4" else: return True - autogenerate._produce_net_changes(connection, metadata, diffs, - self.autogen_context, - object_filters=[include_object], - include_schemas=True - ) + + self.autogen_context.update({ + 'object_filters': [include_object], + 'include_schemas': True, + 'metadata': self.m2 + }) + autogenerate._produce_net_changes(self.autogen_context, diffs) + eq_(diffs[0][0], "add_table") eq_(diffs[0][1].schema, config.test_schema) def test_default_schema_omitted_downgrade(self): - metadata = self.m2 - connection = self.context.bind diffs = [] def include_object(obj, name, type_, reflected, compare_to): @@ -266,17 +107,17 @@ class AutogenCrossSchemaTest(AutogenTest, TestBase): return name == "t1" else: return True - autogenerate._produce_net_changes(connection, metadata, diffs, - self.autogen_context, - object_filters=[include_object], - include_schemas=True - ) + self.autogen_context.update({ + 'object_filters': [include_object], + 'include_schemas': True, + 'metadata': self.m2 + }) + autogenerate._produce_net_changes(self.autogen_context, diffs) + eq_(diffs[0][0], "remove_table") eq_(diffs[0][1].schema, None) def test_alt_schema_included_downgrade(self): - metadata = self.m2 - connection = self.context.bind diffs = [] def include_object(obj, name, type_, reflected, compare_to): @@ -284,11 +125,12 @@ class AutogenCrossSchemaTest(AutogenTest, TestBase): return name == "t2" else: return True - autogenerate._produce_net_changes(connection, metadata, diffs, - self.autogen_context, - object_filters=[include_object], - include_schemas=True - ) + self.autogen_context.update({ + 'object_filters': [include_object], + 'include_schemas': True, + 'metadata': self.m2 + }) + autogenerate._produce_net_changes(self.autogen_context, diffs) eq_(diffs[0][0], "remove_table") eq_(diffs[0][1].schema, config.test_schema) @@ -426,12 +268,12 @@ class AutogenerateDiffTest(ModelOne, AutogenTest, TestBase): """test generation of diff rules""" metadata = self.m2 - connection = self.context.bind diffs = [] + ctx = self.autogen_context.copy() + ctx['metadata'] = self.m2 + ctx['object_filters'] = _default_object_filters autogenerate._produce_net_changes( - connection, metadata, diffs, - self.autogen_context, - object_filters=_default_object_filters, + ctx, diffs ) eq_( @@ -484,228 +326,31 @@ class AutogenerateDiffTest(ModelOne, AutogenTest, TestBase): eq_(diffs[10][0], 'remove_column') eq_(diffs[10][3].name, 'pw') - def test_render_nothing(self): - context = MigrationContext.configure( - connection=self.bind.connect(), - opts={ - 'compare_type': True, - 'compare_server_default': True, - 'target_metadata': self.m1, - 'upgrade_token': "upgrades", - 'downgrade_token': "downgrades", - } - ) - template_args = {} - autogenerate._produce_migration_diffs(context, template_args, set()) - - eq_(re.sub(r"u'", "'", template_args['upgrades']), - """### commands auto generated by Alembic - please adjust! ### - pass - ### end Alembic commands ###""") - eq_(re.sub(r"u'", "'", template_args['downgrades']), - """### commands auto generated by Alembic - please adjust! ### - pass - ### end Alembic commands ###""") - - def test_render_nothing_batch(self): - context = MigrationContext.configure( - connection=self.bind.connect(), - opts={ - 'compare_type': True, - 'compare_server_default': True, - 'target_metadata': self.m1, - 'upgrade_token': "upgrades", - 'downgrade_token': "downgrades", - 'alembic_module_prefix': 'op.', - 'sqlalchemy_module_prefix': 'sa.', - 'render_as_batch': True - } - ) - template_args = {} - autogenerate._produce_migration_diffs( - context, template_args, set(), - include_symbol=lambda name, schema: False - ) - eq_(re.sub(r"u'", "'", template_args['upgrades']), - """### commands auto generated by Alembic - please adjust! ### - pass - ### end Alembic commands ###""") - eq_(re.sub(r"u'", "'", template_args['downgrades']), - """### commands auto generated by Alembic - please adjust! ### - pass - ### end Alembic commands ###""") - - def test_render_diffs_standard(self): - """test a full render including indentation""" - - template_args = {} - autogenerate._produce_migration_diffs( - self.context, template_args, set()) - eq_(re.sub(r"u'", "'", template_args['upgrades']), - """### commands auto generated by Alembic - please adjust! ### - op.create_table('item', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('description', sa.String(length=100), nullable=True), - sa.Column('order_id', sa.Integer(), nullable=True), - sa.CheckConstraint('len(description) > 5'), - sa.ForeignKeyConstraint(['order_id'], ['order.order_id'], ), - sa.PrimaryKeyConstraint('id') - ) - op.drop_table('extra') - op.add_column('address', sa.Column('street', sa.String(length=50), \ -nullable=True)) - op.create_unique_constraint('uq_email', 'address', ['email_address']) - op.add_column('order', sa.Column('user_id', sa.Integer(), nullable=True)) - op.alter_column('order', 'amount', - existing_type=sa.NUMERIC(precision=8, scale=2), - type_=sa.Numeric(precision=10, scale=2), - nullable=True, - existing_server_default=sa.text('0')) - op.create_foreign_key(None, 'order', 'user', ['user_id'], ['id']) - op.alter_column('user', 'a1', - existing_type=sa.TEXT(), - server_default='x', - existing_nullable=True) - op.alter_column('user', 'name', - existing_type=sa.VARCHAR(length=50), - nullable=False) - op.drop_index('pw_idx', table_name='user') - op.drop_column('user', 'pw') - ### end Alembic commands ###""") - - eq_(re.sub(r"u'", "'", template_args['downgrades']), - """### commands auto generated by Alembic - please adjust! ### - op.add_column('user', sa.Column('pw', sa.VARCHAR(length=50), \ -nullable=True)) - op.create_index('pw_idx', 'user', ['pw'], unique=False) - op.alter_column('user', 'name', - existing_type=sa.VARCHAR(length=50), - nullable=True) - op.alter_column('user', 'a1', - existing_type=sa.TEXT(), - server_default=None, - existing_nullable=True) - op.drop_constraint(None, 'order', type_='foreignkey') - op.alter_column('order', 'amount', - existing_type=sa.Numeric(precision=10, scale=2), - type_=sa.NUMERIC(precision=8, scale=2), - nullable=False, - existing_server_default=sa.text('0')) - op.drop_column('order', 'user_id') - op.drop_constraint('uq_email', 'address', type_='unique') - op.drop_column('address', 'street') - op.create_table('extra', - sa.Column('x', sa.CHAR(), nullable=True), - sa.Column('uid', sa.INTEGER(), nullable=True), - sa.ForeignKeyConstraint(['uid'], ['user.id'], ) - ) - op.drop_table('item') - ### end Alembic commands ###""") - - def test_render_diffs_batch(self): - """test a full render in batch mode including indentation""" - - template_args = {} - self.context.opts['render_as_batch'] = True - autogenerate._produce_migration_diffs( - self.context, template_args, set()) - - eq_(re.sub(r"u'", "'", template_args['upgrades']), - """### commands auto generated by Alembic - please adjust! ### - op.create_table('item', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('description', sa.String(length=100), nullable=True), - sa.Column('order_id', sa.Integer(), nullable=True), - sa.CheckConstraint('len(description) > 5'), - sa.ForeignKeyConstraint(['order_id'], ['order.order_id'], ), - sa.PrimaryKeyConstraint('id') - ) - op.drop_table('extra') - with op.batch_alter_table('address', schema=None) as batch_op: - batch_op.add_column(sa.Column('street', sa.String(length=50), nullable=True)) - batch_op.create_unique_constraint('uq_email', ['email_address']) - - with op.batch_alter_table('order', schema=None) as batch_op: - batch_op.add_column(sa.Column('user_id', sa.Integer(), nullable=True)) - batch_op.alter_column('amount', - existing_type=sa.NUMERIC(precision=8, scale=2), - type_=sa.Numeric(precision=10, scale=2), - nullable=True, - existing_server_default=sa.text('0')) - batch_op.create_foreign_key(None, 'order', 'user', ['user_id'], ['id']) - - with op.batch_alter_table('user', schema=None) as batch_op: - batch_op.alter_column('a1', - existing_type=sa.TEXT(), - server_default='x', - existing_nullable=True) - batch_op.alter_column('name', - existing_type=sa.VARCHAR(length=50), - nullable=False) - batch_op.drop_index('pw_idx') - batch_op.drop_column('pw') - - ### end Alembic commands ###""") - - eq_(re.sub(r"u'", "'", template_args['downgrades']), - """### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('user', schema=None) as batch_op: - batch_op.add_column(sa.Column('pw', sa.VARCHAR(length=50), nullable=True)) - batch_op.create_index('pw_idx', ['pw'], unique=False) - batch_op.alter_column('name', - existing_type=sa.VARCHAR(length=50), - nullable=True) - batch_op.alter_column('a1', - existing_type=sa.TEXT(), - server_default=None, - existing_nullable=True) - - with op.batch_alter_table('order', schema=None) as batch_op: - batch_op.drop_constraint(None, type_='foreignkey') - batch_op.alter_column('amount', - existing_type=sa.Numeric(precision=10, scale=2), - type_=sa.NUMERIC(precision=8, scale=2), - nullable=False, - existing_server_default=sa.text('0')) - batch_op.drop_column('user_id') - - with op.batch_alter_table('address', schema=None) as batch_op: - batch_op.drop_constraint('uq_email', type_='unique') - batch_op.drop_column('street') - - op.create_table('extra', - sa.Column('x', sa.CHAR(), nullable=True), - sa.Column('uid', sa.INTEGER(), nullable=True), - sa.ForeignKeyConstraint(['uid'], ['user.id'], ) - ) - op.drop_table('item') - ### end Alembic commands ###""") - def test_include_symbol(self): + + diffs = [] + + def include_symbol(name, schema=None): + return name in ('address', 'order') + context = MigrationContext.configure( connection=self.bind.connect(), opts={ 'compare_type': True, 'compare_server_default': True, 'target_metadata': self.m2, - 'include_symbol': lambda name, schema=None: - name in ('address', 'order'), - 'upgrade_token': "upgrades", - 'downgrade_token': "downgrades", - 'alembic_module_prefix': 'op.', - 'sqlalchemy_module_prefix': 'sa.', + 'include_symbol': include_symbol, } ) - template_args = {} - autogenerate._produce_migration_diffs(context, template_args, set()) - template_args['upgrades'] = \ - template_args['upgrades'].replace("u'", "'") - template_args['downgrades'] = template_args['downgrades'].\ - replace("u'", "'") - assert "alter_column('user'" not in template_args['upgrades'] - assert "alter_column('user'" not in template_args['downgrades'] - assert "alter_column('order'" in template_args['upgrades'] - assert "alter_column('order'" in template_args['downgrades'] + + diffs = autogenerate.compare_metadata( + context, context.opts['target_metadata']) + + alter_cols = set([ + d[2] for d in self._flatten_diffs(diffs) + if d[0].startswith('modify') + ]) + eq_(alter_cols, set(['order'])) def test_include_object(self): def include_object(obj, name, type_, reflected, compare_to): @@ -732,28 +377,23 @@ nullable=True)) 'compare_server_default': True, 'target_metadata': self.m2, 'include_object': include_object, - 'upgrade_token': "upgrades", - 'downgrade_token': "downgrades", - 'alembic_module_prefix': 'op.', - 'sqlalchemy_module_prefix': 'sa.', } ) - template_args = {} - autogenerate._produce_migration_diffs(context, template_args, set()) - template_args['upgrades'] = \ - template_args['upgrades'].replace("u'", "'") - template_args['downgrades'] = template_args['downgrades'].\ - replace("u'", "'") - assert "op.create_table('item'" not in template_args['upgrades'] - assert "op.create_table('item'" not in template_args['downgrades'] + diffs = autogenerate.compare_metadata( + context, context.opts['target_metadata']) - assert "alter_column('user'" in template_args['upgrades'] - assert "alter_column('user'" in template_args['downgrades'] - assert "'street'" not in template_args['upgrades'] - assert "'street'" not in template_args['downgrades'] - assert "alter_column('order'" in template_args['upgrades'] - assert "alter_column('order'" in template_args['downgrades'] + alter_cols = set([ + d[2] for d in self._flatten_diffs(diffs) + if d[0].startswith('modify') + ]).union( + d[3].name for d in self._flatten_diffs(diffs) + if d[0] == 'add_column' + ).union( + d[1].name for d in self._flatten_diffs(diffs) + if d[0] == 'add_table' + ) + eq_(alter_cols, set(['user_id', 'order', 'user'])) def test_skip_null_type_comparison_reflected(self): diff = [] @@ -841,14 +481,14 @@ class AutogenerateDiffTestWSchema(ModelOne, AutogenTest, TestBase): """test generation of diff rules""" metadata = self.m2 - connection = self.context.bind diffs = [] - autogenerate._produce_net_changes( - connection, metadata, diffs, - self.autogen_context, - object_filters=_default_object_filters, - include_schemas=True - ) + + self.autogen_context.update({ + 'object_filters': _default_object_filters, + 'include_schemas': True, + 'metadata': self.m2 + }) + autogenerate._produce_net_changes(self.autogen_context, diffs) eq_( diffs[0], @@ -901,116 +541,6 @@ class AutogenerateDiffTestWSchema(ModelOne, AutogenTest, TestBase): eq_(diffs[10][0], 'remove_column') eq_(diffs[10][3].name, 'pw') - def test_render_nothing(self): - context = MigrationContext.configure( - connection=self.bind.connect(), - opts={ - 'compare_type': True, - 'compare_server_default': True, - 'target_metadata': self.m1, - 'upgrade_token': "upgrades", - 'downgrade_token': "downgrades", - 'alembic_module_prefix': 'op.', - 'sqlalchemy_module_prefix': 'sa.', - } - ) - template_args = {} - autogenerate._produce_migration_diffs( - context, template_args, set(), - include_symbol=lambda name, schema: False - ) - eq_(re.sub(r"u'", "'", template_args['upgrades']), - """### commands auto generated by Alembic - please adjust! ### - pass - ### end Alembic commands ###""") - eq_(re.sub(r"u'", "'", template_args['downgrades']), - """### commands auto generated by Alembic - please adjust! ### - pass - ### end Alembic commands ###""") - - def test_render_diffs_extras(self): - """test a full render including indentation (include and schema)""" - - template_args = {} - autogenerate._produce_migration_diffs( - self.context, template_args, set(), - include_object=_default_include_object, - include_schemas=True - ) - - eq_(re.sub(r"u'", "'", template_args['upgrades']), - """### commands auto generated by Alembic - please adjust! ### - op.create_table('item', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('description', sa.String(length=100), nullable=True), - sa.Column('order_id', sa.Integer(), nullable=True), - sa.CheckConstraint('len(description) > 5'), - sa.ForeignKeyConstraint(['order_id'], ['%(schema)s.order.order_id'], ), - sa.PrimaryKeyConstraint('id'), - schema='%(schema)s' - ) - op.drop_table('extra', schema='%(schema)s') - op.add_column('address', sa.Column('street', sa.String(length=50), \ -nullable=True), schema='%(schema)s') - op.create_unique_constraint('uq_email', 'address', ['email_address'], \ -schema='test_schema') - op.add_column('order', sa.Column('user_id', sa.Integer(), nullable=True), \ -schema='%(schema)s') - op.alter_column('order', 'amount', - existing_type=sa.NUMERIC(precision=8, scale=2), - type_=sa.Numeric(precision=10, scale=2), - nullable=True, - existing_server_default=sa.text('0'), - schema='%(schema)s') - op.create_foreign_key(None, 'order', 'user', ['user_id'], ['id'], \ -source_schema='%(schema)s', referent_schema='%(schema)s') - op.alter_column('user', 'a1', - existing_type=sa.TEXT(), - server_default='x', - existing_nullable=True, - schema='%(schema)s') - op.alter_column('user', 'name', - existing_type=sa.VARCHAR(length=50), - nullable=False, - schema='%(schema)s') - op.drop_index('pw_idx', table_name='user', schema='test_schema') - op.drop_column('user', 'pw', schema='%(schema)s') - ### end Alembic commands ###""" % {"schema": self.schema}) - - eq_(re.sub(r"u'", "'", template_args['downgrades']), - """### commands auto generated by Alembic - please adjust! ### - op.add_column('user', sa.Column('pw', sa.VARCHAR(length=50), \ -autoincrement=False, nullable=True), schema='%(schema)s') - op.create_index('pw_idx', 'user', ['pw'], unique=False, schema='%(schema)s') - op.alter_column('user', 'name', - existing_type=sa.VARCHAR(length=50), - nullable=True, - schema='%(schema)s') - op.alter_column('user', 'a1', - existing_type=sa.TEXT(), - server_default=None, - existing_nullable=True, - schema='%(schema)s') - op.drop_constraint(None, 'order', schema='%(schema)s', type_='foreignkey') - op.alter_column('order', 'amount', - existing_type=sa.Numeric(precision=10, scale=2), - type_=sa.NUMERIC(precision=8, scale=2), - nullable=False, - existing_server_default=sa.text('0'), - schema='%(schema)s') - op.drop_column('order', 'user_id', schema='%(schema)s') - op.drop_constraint('uq_email', 'address', schema='test_schema', type_='unique') - op.drop_column('address', 'street', schema='%(schema)s') - op.create_table('extra', - sa.Column('x', sa.CHAR(length=1), autoincrement=False, nullable=True), - sa.Column('uid', sa.INTEGER(), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['uid'], ['%(schema)s.user.id'], \ -name='extra_uid_fkey'), - schema='%(schema)s' - ) - op.drop_table('item', schema='%(schema)s') - ### end Alembic commands ###""" % {"schema": self.schema}) - class AutogenerateCustomCompareTypeTest(AutogenTest, TestBase): __only_on__ = 'sqlite' @@ -1038,8 +568,9 @@ class AutogenerateCustomCompareTypeTest(AutogenTest, TestBase): self.context._user_compare_type = my_compare_type diffs = [] - autogenerate._produce_net_changes(self.context.bind, self.m2, - diffs, self.autogen_context) + ctx = self.autogen_context.copy() + ctx['metadata'] = self.m2 + autogenerate._produce_net_changes(ctx, diffs) first_table = self.m2.tables['sometable'] first_column = first_table.columns['id'] @@ -1062,8 +593,10 @@ class AutogenerateCustomCompareTypeTest(AutogenTest, TestBase): self.context._user_compare_type = my_compare_type diffs = [] - autogenerate._produce_net_changes(self.context.bind, self.m2, - diffs, self.autogen_context) + ctx = self.autogen_context.copy() + ctx['metadata'] = self.m2 + diffs = [] + autogenerate._produce_net_changes(ctx, diffs) eq_(diffs, []) @@ -1072,9 +605,10 @@ class AutogenerateCustomCompareTypeTest(AutogenTest, TestBase): my_compare_type.return_value = True self.context._user_compare_type = my_compare_type + ctx = self.autogen_context.copy() + ctx['metadata'] = self.m2 diffs = [] - autogenerate._produce_net_changes(self.context.bind, self.m2, - diffs, self.autogen_context) + autogenerate._produce_net_changes(ctx, diffs) eq_(diffs[0][0][0], 'modify_type') eq_(diffs[1][0][0], 'modify_type') @@ -1101,14 +635,10 @@ class PKConstraintUpgradesIgnoresNullableTest(AutogenTest, TestBase): return cls._get_db_schema() def test_no_change(self): - metadata = self.m2 - connection = self.context.bind - diffs = [] - - autogenerate._produce_net_changes(connection, metadata, diffs, - self.autogen_context - ) + ctx = self.autogen_context.copy() + ctx['metadata'] = self.m2 + autogenerate._produce_net_changes(ctx, diffs) eq_(diffs, []) @@ -1143,15 +673,12 @@ class AutogenKeyTest(AutogenTest, TestBase): symbols = ['someothertable', 'sometable'] def test_autogen(self): - metadata = self.m2 - connection = self.context.bind diffs = [] - autogenerate._produce_net_changes(connection, metadata, diffs, - self.autogen_context, - include_schemas=False - ) + ctx = self.autogen_context.copy() + ctx['metadata'] = self.m2 + autogenerate._produce_net_changes(ctx, diffs) eq_(diffs[0][0], "add_table") eq_(diffs[0][1].name, "sometable") eq_(diffs[1][0], "add_column") @@ -1178,8 +705,10 @@ class AutogenVersionTableTest(AutogenTest, TestBase): def test_no_version_table(self): diffs = [] - autogenerate._produce_net_changes(self.context.bind, self.m2, - diffs, self.autogen_context) + ctx = self.autogen_context.copy() + ctx['metadata'] = self.m2 + + autogenerate._produce_net_changes(ctx, diffs) eq_(diffs, []) def test_version_table_in_target(self): @@ -1188,8 +717,9 @@ class AutogenVersionTableTest(AutogenTest, TestBase): self.version_table_name, self.m2, Column('x', Integer), schema=self.version_table_schema) - autogenerate._produce_net_changes(self.context.bind, self.m2, - diffs, self.autogen_context) + ctx = self.autogen_context.copy() + ctx['metadata'] = self.m2 + autogenerate._produce_net_changes(ctx, diffs) eq_(diffs, []) @@ -1239,13 +769,10 @@ class AutogenerateDiffOrderTest(AutogenTest, TestBase): before their parent tables """ - metadata = self.m2 - connection = self.context.bind + ctx = self.autogen_context.copy() + ctx['metadata'] = self.m2 diffs = [] - - autogenerate._produce_net_changes(connection, metadata, diffs, - self.autogen_context - ) + autogenerate._produce_net_changes(ctx, diffs) eq_(diffs[0][0], 'add_table') eq_(diffs[0][1].name, "parent") diff --git a/tests/test_autogen_fks.py b/tests/test_autogen_fks.py index 90d25c4..525bed5 100644 --- a/tests/test_autogen_fks.py +++ b/tests/test_autogen_fks.py @@ -1,5 +1,5 @@ import sys -from alembic.testing import TestBase, config +from alembic.testing import TestBase from sqlalchemy import MetaData, Column, Table, Integer, String, \ ForeignKeyConstraint @@ -7,7 +7,7 @@ from alembic.testing import eq_ py3k = sys.version_info >= (3, ) -from .test_autogenerate import AutogenFixtureTest +from ._autogen_fixtures import AutogenFixtureTest class AutogenerateForeignKeysTest(AutogenFixtureTest, TestBase): diff --git a/tests/test_autogen_indexes.py b/tests/test_autogen_indexes.py index 1f92649..8ee33bc 100644 --- a/tests/test_autogen_indexes.py +++ b/tests/test_autogen_indexes.py @@ -12,7 +12,7 @@ from alembic.testing.env import staging_env py3k = sys.version_info >= (3, ) -from .test_autogenerate import AutogenFixtureTest +from ._autogen_fixtures import AutogenFixtureTest class NoUqReflection(object): diff --git a/tests/test_autogen_render.py b/tests/test_autogen_render.py index 52f3601..4a49d5c 100644 --- a/tests/test_autogen_render.py +++ b/tests/test_autogen_render.py @@ -2,6 +2,7 @@ import re import sys from alembic.testing import TestBase, exclusions +from alembic.operations import ops from sqlalchemy import MetaData, Column, Table, String, \ Numeric, CHAR, ForeignKey, DATETIME, Integer, \ CheckConstraint, Unicode, Enum, cast,\ @@ -16,7 +17,8 @@ from sqlalchemy.sql import and_, column, literal_column, false from alembic.testing.mock import patch -from alembic import autogenerate, util, compat +from alembic import autogenerate, util +from alembic.util import compat from alembic.testing import eq_, eq_ignore_whitespace, config from alembic.testing.fixtures import op_fixture @@ -58,8 +60,9 @@ class AutogenRenderTest(TestBase): Column('code', String(255)), ) idx = Index('test_active_code_idx', t.c.active, t.c.code) + op_obj = ops.CreateIndexOp.from_index(idx) eq_ignore_whitespace( - autogenerate.render._add_index(idx, self.autogen_context), + autogenerate.render_op_text(self.autogen_context, op_obj), "op.create_index('test_active_code_idx', 'test', " "['active', 'code'], unique=False)" ) @@ -76,8 +79,9 @@ class AutogenRenderTest(TestBase): schema='CamelSchema' ) idx = Index('test_active_code_idx', t.c.active, t.c.code) + op_obj = ops.CreateIndexOp.from_index(idx) eq_ignore_whitespace( - autogenerate.render._add_index(idx, self.autogen_context), + autogenerate.render_op_text(self.autogen_context, op_obj), "op.create_index('test_active_code_idx', 'test', " "['active', 'code'], unique=False, schema='CamelSchema')" ) @@ -94,16 +98,18 @@ class AutogenRenderTest(TestBase): idx = Index('foo_idx', t.c.x, t.c.y, postgresql_where=(t.c.y == 'something')) + op_obj = ops.CreateIndexOp.from_index(idx) + if compat.sqla_08: eq_ignore_whitespace( - autogenerate.render._add_index(idx, autogen_context), + autogenerate.render_op_text(autogen_context, op_obj), """op.create_index('foo_idx', 't', \ ['x', 'y'], unique=False, """ """postgresql_where=sa.text(!U"t.y = 'something'"))""" ) else: eq_ignore_whitespace( - autogenerate.render._add_index(idx, autogen_context), + autogenerate.render_op_text(autogen_context, op_obj), """op.create_index('foo_idx', 't', ['x', 'y'], \ unique=False, """ """postgresql_where=sa.text(!U't.y = %(y_1)s'))""" @@ -118,8 +124,10 @@ unique=False, """ Column('code', String(255)) ) idx = Index('test_lower_code_idx', func.lower(t.c.code)) + op_obj = ops.CreateIndexOp.from_index(idx) + eq_ignore_whitespace( - autogenerate.render._add_index(idx, self.autogen_context), + autogenerate.render_op_text(self.autogen_context, op_obj), "op.create_index('test_lower_code_idx', 'test', " "[sa.text(!U'lower(test.code)')], unique=False)" ) @@ -133,8 +141,9 @@ unique=False, """ Column('code', String(255)) ) idx = Index('test_lower_code_idx', cast(t.c.code, String)) + op_obj = ops.CreateIndexOp.from_index(idx) eq_ignore_whitespace( - autogenerate.render._add_index(idx, self.autogen_context), + autogenerate.render_op_text(self.autogen_context, op_obj), "op.create_index('test_lower_code_idx', 'test', " "[sa.text(!U'CAST(test.code AS CHAR)')], unique=False)" ) @@ -148,8 +157,9 @@ unique=False, """ Column('code', String(255)) ) idx = Index('test_desc_code_idx', t.c.code.desc()) + op_obj = ops.CreateIndexOp.from_index(idx) eq_ignore_whitespace( - autogenerate.render._add_index(idx, self.autogen_context), + autogenerate.render_op_text(self.autogen_context, op_obj), "op.create_index('test_desc_code_idx', 'test', " "[sa.text(!U'test.code DESC')], unique=False)" ) @@ -165,8 +175,9 @@ unique=False, """ Column('code', String(255)), ) idx = Index('test_active_code_idx', t.c.active, t.c.code) + op_obj = ops.DropIndexOp.from_index(idx) eq_ignore_whitespace( - autogenerate.render._drop_index(idx, self.autogen_context), + autogenerate.render_op_text(self.autogen_context, op_obj), "op.drop_index('test_active_code_idx', table_name='test')" ) @@ -182,8 +193,9 @@ unique=False, """ schema='CamelSchema' ) idx = Index('test_active_code_idx', t.c.active, t.c.code) + op_obj = ops.DropIndexOp.from_index(idx) eq_ignore_whitespace( - autogenerate.render._drop_index(idx, self.autogen_context), + autogenerate.render_op_text(self.autogen_context, op_obj), "op.drop_index('test_active_code_idx', " + "table_name='test', schema='CamelSchema')" ) @@ -199,9 +211,9 @@ unique=False, """ Column('code', String(255)), ) uq = UniqueConstraint(t.c.code, name='uq_test_code') + op_obj = ops.AddConstraintOp.from_constraint(uq) eq_ignore_whitespace( - autogenerate.render._add_unique_constraint( - uq, self.autogen_context), + autogenerate.render_op_text(self.autogen_context, op_obj), "op.create_unique_constraint('uq_test_code', 'test', ['code'])" ) @@ -217,9 +229,9 @@ unique=False, """ schema='CamelSchema' ) uq = UniqueConstraint(t.c.code, name='uq_test_code') + op_obj = ops.AddConstraintOp.from_constraint(uq) eq_ignore_whitespace( - autogenerate.render._add_unique_constraint( - uq, self.autogen_context), + autogenerate.render_op_text(self.autogen_context, op_obj), "op.create_unique_constraint('uq_test_code', 'test', " "['code'], schema='CamelSchema')" ) @@ -235,8 +247,9 @@ unique=False, """ Column('code', String(255)), ) uq = UniqueConstraint(t.c.code, name='uq_test_code') + op_obj = ops.DropConstraintOp.from_constraint(uq) eq_ignore_whitespace( - autogenerate.render._drop_constraint(uq, self.autogen_context), + autogenerate.render_op_text(self.autogen_context, op_obj), "op.drop_constraint('uq_test_code', 'test', type_='unique')" ) @@ -252,8 +265,9 @@ unique=False, """ schema='CamelSchema' ) uq = UniqueConstraint(t.c.code, name='uq_test_code') + op_obj = ops.DropConstraintOp.from_constraint(uq) eq_ignore_whitespace( - autogenerate.render._drop_constraint(uq, self.autogen_context), + autogenerate.render_op_text(self.autogen_context, op_obj), "op.drop_constraint('uq_test_code', 'test', " "schema='CamelSchema', type_='unique')" ) @@ -264,8 +278,9 @@ unique=False, """ b = Table('b', m, Column('a_id', Integer, ForeignKey('a.id'))) fk = ForeignKeyConstraint(['a_id'], ['a.id'], name='fk_a_id') b.append_constraint(fk) + op_obj = ops.AddConstraintOp.from_constraint(fk) eq_ignore_whitespace( - autogenerate.render._add_fk_constraint(fk, self.autogen_context), + autogenerate.render_op_text(self.autogen_context, op_obj), "op.create_foreign_key('fk_a_id', 'b', 'a', ['a_id'], ['id'])" ) @@ -281,11 +296,12 @@ unique=False, """ # SQLA 0.9 generates a u'' here for remote cols while 0.8 does not, # so just whack out "'u" here from the generated + op_obj = ops.AddConstraintOp.from_constraint(fk) eq_ignore_whitespace( re.sub( r"u'", "'", - autogenerate.render._add_fk_constraint( - fk, self.autogen_context)), + autogenerate.render_op_text(self.autogen_context, op_obj), + ), "op.create_foreign_key(None, 't', 't2', ['c'], ['c_rem'], " "onupdate='CASCADE')" ) @@ -294,11 +310,12 @@ unique=False, """ if not util.sqla_08: t1.append_constraint(fk) + op_obj = ops.AddConstraintOp.from_constraint(fk) eq_ignore_whitespace( re.sub( r"u'", "'", - autogenerate.render._add_fk_constraint( - fk, self.autogen_context)), + autogenerate.render_op_text(self.autogen_context, op_obj) + ), "op.create_foreign_key(None, 't', 't2', ['c'], ['c_rem'], " "ondelete='CASCADE')" ) @@ -306,11 +323,11 @@ unique=False, """ fk = ForeignKeyConstraint([t1.c.c], [t2.c.c_rem], deferrable=True) if not util.sqla_08: t1.append_constraint(fk) + op_obj = ops.AddConstraintOp.from_constraint(fk) eq_ignore_whitespace( re.sub( r"u'", "'", - autogenerate.render._add_fk_constraint( - fk, self.autogen_context), + autogenerate.render_op_text(self.autogen_context, op_obj) ), "op.create_foreign_key(None, 't', 't2', ['c'], ['c_rem'], " "deferrable=True)" @@ -319,11 +336,11 @@ unique=False, """ fk = ForeignKeyConstraint([t1.c.c], [t2.c.c_rem], initially="XYZ") if not util.sqla_08: t1.append_constraint(fk) + op_obj = ops.AddConstraintOp.from_constraint(fk) eq_ignore_whitespace( re.sub( r"u'", "'", - autogenerate.render._add_fk_constraint( - fk, self.autogen_context) + autogenerate.render_op_text(self.autogen_context, op_obj), ), "op.create_foreign_key(None, 't', 't2', ['c'], ['c_rem'], " "initially='XYZ')" @@ -334,11 +351,11 @@ unique=False, """ initially="XYZ", ondelete="CASCADE", deferrable=True) if not util.sqla_08: t1.append_constraint(fk) + op_obj = ops.AddConstraintOp.from_constraint(fk) eq_ignore_whitespace( re.sub( r"u'", "'", - autogenerate.render._add_fk_constraint( - fk, self.autogen_context) + autogenerate.render_op_text(self.autogen_context, op_obj) ), "op.create_foreign_key(None, 't', 't2', ['c'], ['c_rem'], " "ondelete='CASCADE', initially='XYZ', deferrable=True)" @@ -351,7 +368,8 @@ unique=False, """ 'b', m, Column('a_id', Integer, ForeignKey('a.aid'), key='baid')) - py_code = autogenerate.render._add_table(b, self.autogen_context) + op_obj = ops.CreateTableOp.from_table(b) + py_code = autogenerate.render_op_text(self.autogen_context, op_obj) eq_ignore_whitespace( py_code, @@ -373,7 +391,8 @@ unique=False, """ fk = ForeignKeyConstraint(['baid'], ['a.aid'], name='fk_a_id') b.append_constraint(fk) - py_code = autogenerate.render._add_table(b, self.autogen_context) + op_obj = ops.CreateTableOp.from_table(b) + py_code = autogenerate.render_op_text(self.autogen_context, op_obj) eq_ignore_whitespace( py_code, @@ -389,14 +408,16 @@ unique=False, """ "fk_a_id FOREIGN KEY(a_id) REFERENCES a (id))") context = op_fixture() - py_code = autogenerate.render._add_fk_constraint( - fk, self.autogen_context) + + op_obj = ops.AddConstraintOp.from_constraint(fk) eq_ignore_whitespace( - autogenerate.render._add_fk_constraint(fk, self.autogen_context), + autogenerate.render_op_text(self.autogen_context, op_obj), "op.create_foreign_key('fk_a_id', 'b', 'a', ['a_id'], ['id'])" ) + py_code = autogenerate.render_op_text(self.autogen_context, op_obj) + eval(py_code) context.assert_( "ALTER TABLE b ADD CONSTRAINT fk_a_id " @@ -414,8 +435,9 @@ unique=False, """ ["a_id"], ["CamelSchemaTwo.a.id"], name='fk_a_id') b.append_constraint(fk) + op_obj = ops.AddConstraintOp.from_constraint(fk) eq_ignore_whitespace( - autogenerate.render._add_fk_constraint(fk, self.autogen_context), + autogenerate.render_op_text(self.autogen_context, op_obj), "op.create_foreign_key('fk_a_id', 'b', 'a', ['a_id'], ['id']," " source_schema='CamelSchemaOne', " "referent_schema='CamelSchemaTwo')" @@ -427,8 +449,9 @@ unique=False, """ b = Table('b', m, Column('a_id', Integer, ForeignKey('a.id'))) fk = ForeignKeyConstraint(['a_id'], ['a.id'], name='fk_a_id') b.append_constraint(fk) + op_obj = ops.DropConstraintOp.from_constraint(fk) eq_ignore_whitespace( - autogenerate.render._drop_constraint(fk, self.autogen_context), + autogenerate.render_op_text(self.autogen_context, op_obj), "op.drop_constraint('fk_a_id', 'b', type_='foreignkey')" ) @@ -444,9 +467,10 @@ unique=False, """ ["a_id"], ["CamelSchemaTwo.a.id"], name='fk_a_id') b.append_constraint(fk) + op_obj = ops.DropConstraintOp.from_constraint(fk) eq_ignore_whitespace( - autogenerate.render._drop_constraint(fk, self.autogen_context), + autogenerate.render_op_text(self.autogen_context, op_obj), "op.drop_constraint('fk_a_id', 'b', schema='CamelSchemaOne', " "type_='foreignkey')" ) @@ -462,8 +486,10 @@ unique=False, """ UniqueConstraint("name", name="uq_name"), UniqueConstraint("timestamp"), ) + + op_obj = ops.CreateTableOp.from_table(t) eq_ignore_whitespace( - autogenerate.render._add_table(t, self.autogen_context), + autogenerate.render_op_text(self.autogen_context, op_obj), "op.create_table('test'," "sa.Column('id', sa.Integer(), nullable=False)," "sa.Column('name', sa.Unicode(length=255), nullable=True)," @@ -487,8 +513,9 @@ unique=False, """ Column('q', Integer, ForeignKey('address.id')), schema='foo' ) + op_obj = ops.CreateTableOp.from_table(t) eq_ignore_whitespace( - autogenerate.render._add_table(t, self.autogen_context), + autogenerate.render_op_text(self.autogen_context, op_obj), "op.create_table('test'," "sa.Column('id', sa.Integer(), nullable=False)," "sa.Column('q', sa.Integer(), nullable=True)," @@ -503,8 +530,9 @@ unique=False, """ t = Table(compat.ue('\u0411\u0435\u0437'), m, Column('id', Integer, primary_key=True), ) + op_obj = ops.CreateTableOp.from_table(t) eq_ignore_whitespace( - autogenerate.render._add_table(t, self.autogen_context), + autogenerate.render_op_text(self.autogen_context, op_obj), "op.create_table(%r," "sa.Column('id', sa.Integer(), nullable=False)," "sa.PrimaryKeyConstraint('id'))" % compat.ue('\u0411\u0435\u0437') @@ -516,8 +544,9 @@ unique=False, """ Column('id', Integer, primary_key=True), schema=compat.ue('\u0411\u0435\u0437') ) + op_obj = ops.CreateTableOp.from_table(t) eq_ignore_whitespace( - autogenerate.render._add_table(t, self.autogen_context), + autogenerate.render_op_text(self.autogen_context, op_obj), "op.create_table('test'," "sa.Column('id', sa.Integer(), nullable=False)," "sa.PrimaryKeyConstraint('id')," @@ -534,8 +563,9 @@ unique=False, """ Column('c', Integer), Column('d', Integer), ) + op_obj = ops.CreateTableOp.from_table(t) eq_ignore_whitespace( - autogenerate.render._add_table(t, self.autogen_context), + autogenerate.render_op_text(self.autogen_context, op_obj), "op.create_table('test'," "*[sa.Column('a', sa.Integer(), nullable=True)," "sa.Column('b', sa.Integer(), nullable=True)," @@ -549,9 +579,10 @@ unique=False, """ Column('b', Integer), Column('c', Integer), ) + op_obj = ops.CreateTableOp.from_table(t2) eq_ignore_whitespace( - autogenerate.render._add_table(t2, self.autogen_context), + autogenerate.render_op_text(self.autogen_context, op_obj), "op.create_table('test2'," "sa.Column('a', sa.Integer(), nullable=True)," "sa.Column('b', sa.Integer(), nullable=True)," @@ -564,8 +595,9 @@ unique=False, """ Column('id', Integer, primary_key=True), Column('q', Integer, ForeignKey('foo.address.id')), ) + op_obj = ops.CreateTableOp.from_table(t) eq_ignore_whitespace( - autogenerate.render._add_table(t, self.autogen_context), + autogenerate.render_op_text(self.autogen_context, op_obj), "op.create_table('test'," "sa.Column('id', sa.Integer(), nullable=False)," "sa.Column('q', sa.Integer(), nullable=True)," @@ -580,10 +612,11 @@ unique=False, """ Column('id', Integer, primary_key=True), Column('q', Integer, ForeignKey('address.id')), ) + op_obj = ops.CreateTableOp.from_table(t) eq_ignore_whitespace( re.sub( r"u'", "'", - autogenerate.render._add_table(t, self.autogen_context) + autogenerate.render_op_text(self.autogen_context, op_obj) ), "op.create_table('test'," "sa.Column('id', sa.Integer(), nullable=False)," @@ -600,8 +633,9 @@ unique=False, """ Column('id', Integer, primary_key=True), Column('q', Integer, ForeignKey('bar.address.id')), ) + op_obj = ops.CreateTableOp.from_table(t) eq_ignore_whitespace( - autogenerate.render._add_table(t, self.autogen_context), + autogenerate.render_op_text(self.autogen_context, op_obj), "op.create_table('test'," "sa.Column('id', sa.Integer(), nullable=False)," "sa.Column('q', sa.Integer(), nullable=True)," @@ -618,8 +652,9 @@ unique=False, """ Column('q', Integer, ForeignKey('bar.address.id')), sqlite_autoincrement=True, mysql_engine="InnoDB" ) + op_obj = ops.CreateTableOp.from_table(t) eq_ignore_whitespace( - autogenerate.render._add_table(t, self.autogen_context), + autogenerate.render_op_text(self.autogen_context, op_obj), "op.create_table('test'," "sa.Column('id', sa.Integer(), nullable=False)," "sa.Column('q', sa.Integer(), nullable=True)," @@ -629,17 +664,20 @@ unique=False, """ ) def test_render_drop_table(self): + op_obj = ops.DropTableOp.from_table( + Table("sometable", MetaData()) + ) eq_ignore_whitespace( - autogenerate.render._drop_table(Table("sometable", MetaData()), - self.autogen_context), + autogenerate.render_op_text(self.autogen_context, op_obj), "op.drop_table('sometable')" ) def test_render_drop_table_w_schema(self): + op_obj = ops.DropTableOp.from_table( + Table("sometable", MetaData(), schema='foo') + ) eq_ignore_whitespace( - autogenerate.render._drop_table( - Table("sometable", MetaData(), schema='foo'), - self.autogen_context), + autogenerate.render_op_text(self.autogen_context, op_obj), "op.drop_table('sometable', schema='foo')" ) @@ -647,8 +685,9 @@ unique=False, """ m = MetaData() t = Table('test', m, Column('x', Boolean())) + op_obj = ops.CreateTableOp.from_table(t) eq_ignore_whitespace( - autogenerate.render._add_table(t, self.autogen_context), + autogenerate.render_op_text(self.autogen_context, op_obj), "op.create_table('test'," "sa.Column('x', sa.Boolean(), nullable=True))" ) @@ -658,52 +697,53 @@ unique=False, """ t1 = Table('t1', m, Column('x', Integer)) t2 = Table('t2', m, Column('x', Integer, primary_key=True)) + op_obj = ops.CreateTableOp.from_table(t1) eq_ignore_whitespace( - autogenerate.render._add_table(t1, self.autogen_context), + autogenerate.render_op_text(self.autogen_context, op_obj), "op.create_table('t1'," "sa.Column('x', sa.Integer(), nullable=True))" ) + op_obj = ops.CreateTableOp.from_table(t2) eq_ignore_whitespace( - autogenerate.render._add_table(t2, self.autogen_context), + autogenerate.render_op_text(self.autogen_context, op_obj), "op.create_table('t2'," "sa.Column('x', sa.Integer(), nullable=False)," "sa.PrimaryKeyConstraint('x'))" ) def test_render_add_column(self): + op_obj = ops.AddColumnOp( + "foo", Column("x", Integer, server_default="5")) eq_ignore_whitespace( - autogenerate.render._add_column( - None, "foo", Column("x", Integer, server_default="5"), - self.autogen_context), + autogenerate.render_op_text(self.autogen_context, op_obj), "op.add_column('foo', sa.Column('x', sa.Integer(), " "server_default='5', nullable=True))" ) def test_render_add_column_w_schema(self): + op_obj = ops.AddColumnOp( + "bar", Column("x", Integer, server_default="5"), + schema="foo") eq_ignore_whitespace( - autogenerate.render._add_column( - "foo", "bar", Column("x", Integer, server_default="5"), - self.autogen_context), + autogenerate.render_op_text(self.autogen_context, op_obj), "op.add_column('bar', sa.Column('x', sa.Integer(), " "server_default='5', nullable=True), schema='foo')" ) def test_render_drop_column(self): + op_obj = ops.DropColumnOp.from_column_and_tablename( + None, "foo", Column("x", Integer, server_default="5")) eq_ignore_whitespace( - autogenerate.render._drop_column( - None, "foo", Column("x", Integer, server_default="5"), - self.autogen_context), - + autogenerate.render_op_text(self.autogen_context, op_obj), "op.drop_column('foo', 'x')" ) def test_render_drop_column_w_schema(self): + op_obj = ops.DropColumnOp.from_column_and_tablename( + "foo", "bar", Column("x", Integer, server_default="5")) eq_ignore_whitespace( - autogenerate.render._drop_column( - "foo", "bar", Column("x", Integer, server_default="5"), - self.autogen_context), - + autogenerate.render_op_text(self.autogen_context, op_obj), "op.drop_column('bar', 'x', schema='foo')" ) @@ -783,9 +823,8 @@ unique=False, """ PrimaryKeyConstraint('x'), ForeignKeyConstraint(['x'], ['y']) ) - result = autogenerate.render._add_table( - t, autogen_context - ) + op_obj = ops.CreateTableOp.from_table(t) + result = autogenerate.render_op_text(autogen_context, op_obj) eq_ignore_whitespace( result, "sa.create_table('t'," @@ -794,45 +833,50 @@ unique=False, """ ) def test_render_modify_type(self): + op_obj = ops.AlterColumnOp( + "sometable", "somecolumn", + modify_type=CHAR(10), existing_type=CHAR(20) + ) eq_ignore_whitespace( - autogenerate.render._modify_col( - "sometable", "somecolumn", - self.autogen_context, - type_=CHAR(10), existing_type=CHAR(20)), + autogenerate.render_op_text(self.autogen_context, op_obj), "op.alter_column('sometable', 'somecolumn', " "existing_type=sa.CHAR(length=20), type_=sa.CHAR(length=10))" ) def test_render_modify_type_w_schema(self): + op_obj = ops.AlterColumnOp( + "sometable", "somecolumn", + modify_type=CHAR(10), existing_type=CHAR(20), + schema='foo' + ) eq_ignore_whitespace( - autogenerate.render._modify_col( - "sometable", "somecolumn", - self.autogen_context, - type_=CHAR(10), existing_type=CHAR(20), - schema='foo'), + autogenerate.render_op_text(self.autogen_context, op_obj), "op.alter_column('sometable', 'somecolumn', " "existing_type=sa.CHAR(length=20), type_=sa.CHAR(length=10), " "schema='foo')" ) def test_render_modify_nullable(self): + op_obj = ops.AlterColumnOp( + "sometable", "somecolumn", + existing_type=Integer(), + modify_nullable=True + ) eq_ignore_whitespace( - autogenerate.render._modify_col( - "sometable", "somecolumn", - self.autogen_context, - existing_type=Integer(), - nullable=True), + autogenerate.render_op_text(self.autogen_context, op_obj), "op.alter_column('sometable', 'somecolumn', " "existing_type=sa.Integer(), nullable=True)" ) def test_render_modify_nullable_w_schema(self): + op_obj = ops.AlterColumnOp( + "sometable", "somecolumn", + existing_type=Integer(), + modify_nullable=True, schema='foo' + ) + eq_ignore_whitespace( - autogenerate.render._modify_col( - "sometable", "somecolumn", - self.autogen_context, - existing_type=Integer(), - nullable=True, schema='foo'), + autogenerate.render_op_text(self.autogen_context, op_obj), "op.alter_column('sometable', 'somecolumn', " "existing_type=sa.Integer(), nullable=True, schema='foo')" ) @@ -993,23 +1037,22 @@ unique=False, """ 't', m, Column('c', Integer), schema=compat.ue('\u0411\u0435\u0437') ) + op_obj = ops.AddConstraintOp.from_constraint(UniqueConstraint(t.c.c)) eq_ignore_whitespace( - autogenerate.render._add_unique_constraint( - UniqueConstraint(t.c.c), - self.autogen_context - ), + autogenerate.render_op_text(self.autogen_context, op_obj), "op.create_unique_constraint(None, 't', ['c'], " "schema=%r)" % compat.ue('\u0411\u0435\u0437') ) def test_render_modify_nullable_w_default(self): + op_obj = ops.AlterColumnOp( + "sometable", "somecolumn", + existing_type=Integer(), + existing_server_default="5", + modify_nullable=True + ) eq_ignore_whitespace( - autogenerate.render._modify_col( - "sometable", "somecolumn", - self.autogen_context, - existing_type=Integer(), - existing_server_default="5", - nullable=True), + autogenerate.render_op_text(self.autogen_context, op_obj), "op.alter_column('sometable', 'somecolumn', " "existing_type=sa.Integer(), nullable=True, " "existing_server_default='5')" @@ -1236,13 +1279,14 @@ unique=False, """ ) def test_render_modify_reflected_int_server_default(self): + op_obj = ops.AlterColumnOp( + "sometable", "somecolumn", + existing_type=Integer(), + existing_server_default=DefaultClause(text("5")), + modify_nullable=True + ) eq_ignore_whitespace( - autogenerate.render._modify_col( - "sometable", "somecolumn", - self.autogen_context, - existing_type=Integer(), - existing_server_default=DefaultClause(text("5")), - nullable=True), + autogenerate.render_op_text(self.autogen_context, op_obj), "op.alter_column('sometable', 'somecolumn', " "existing_type=sa.Integer(), nullable=True, " "existing_server_default=sa.text(!U'5'))" @@ -1280,10 +1324,9 @@ class RenderNamingConventionTest(TestBase): def test_schema_type_boolean(self): t = Table('t', self.metadata, Column('c', Boolean(name='xyz'))) + op_obj = ops.AddColumnOp.from_column(t.c.c) eq_ignore_whitespace( - autogenerate.render._add_column( - None, "t", t.c.c, - self.autogen_context), + autogenerate.render_op_text(self.autogen_context, op_obj), "op.add_column('t', " "sa.Column('c', sa.Boolean(name='xyz'), nullable=True))" ) @@ -1316,8 +1359,9 @@ class RenderNamingConventionTest(TestBase): Column('code', String(255)), ) idx = Index(None, t.c.active, t.c.code) + op_obj = ops.CreateIndexOp.from_index(idx) eq_ignore_whitespace( - autogenerate.render._add_index(idx, self.autogen_context), + autogenerate.render_op_text(self.autogen_context, op_obj), "op.create_index(op.f('ix_ct_test_active'), 'test', " "['active', 'code'], unique=False)" ) @@ -1329,8 +1373,9 @@ class RenderNamingConventionTest(TestBase): Column('code', String(255)), ) idx = Index(None, t.c.active, t.c.code) + op_obj = ops.DropIndexOp.from_index(idx) eq_ignore_whitespace( - autogenerate.render._drop_index(idx, self.autogen_context), + autogenerate.render_op_text(self.autogen_context, op_obj), "op.drop_index(op.f('ix_ct_test_active'), table_name='test')" ) @@ -1342,8 +1387,9 @@ class RenderNamingConventionTest(TestBase): schema='CamelSchema' ) idx = Index(None, t.c.active, t.c.code) + op_obj = ops.CreateIndexOp.from_index(idx) eq_ignore_whitespace( - autogenerate.render._add_index(idx, self.autogen_context), + autogenerate.render_op_text(self.autogen_context, op_obj), "op.create_index(op.f('ix_ct_CamelSchema_test_active'), 'test', " "['active', 'code'], unique=False, schema='CamelSchema')" ) @@ -1360,8 +1406,9 @@ class RenderNamingConventionTest(TestBase): def test_inline_pk_constraint(self): t = Table('t', self.metadata, Column('c', Integer, primary_key=True)) + op_obj = ops.CreateTableOp.from_table(t) eq_ignore_whitespace( - autogenerate.render._add_table(t, self.autogen_context), + autogenerate.render_op_text(self.autogen_context, op_obj), "op.create_table('t',sa.Column('c', sa.Integer(), nullable=False)," "sa.PrimaryKeyConstraint('c', name=op.f('pk_ct_t')))" ) @@ -1369,16 +1416,18 @@ class RenderNamingConventionTest(TestBase): def test_inline_ck_constraint(self): t = Table( 't', self.metadata, Column('c', Integer), CheckConstraint("c > 5")) + op_obj = ops.CreateTableOp.from_table(t) eq_ignore_whitespace( - autogenerate.render._add_table(t, self.autogen_context), + autogenerate.render_op_text(self.autogen_context, op_obj), "op.create_table('t',sa.Column('c', sa.Integer(), nullable=True)," "sa.CheckConstraint(!U'c > 5', name=op.f('ck_ct_t')))" ) def test_inline_fk(self): t = Table('t', self.metadata, Column('c', Integer, ForeignKey('q.id'))) + op_obj = ops.CreateTableOp.from_table(t) eq_ignore_whitespace( - autogenerate.render._add_table(t, self.autogen_context), + autogenerate.render_op_text(self.autogen_context, op_obj), "op.create_table('t',sa.Column('c', sa.Integer(), nullable=True)," "sa.ForeignKeyConstraint(['c'], ['q.id'], " "name=op.f('fk_ct_t_c_q')))" diff --git a/tests/test_batch.py b/tests/test_batch.py index a498c36..41d1957 100644 --- a/tests/test_batch.py +++ b/tests/test_batch.py @@ -1,15 +1,13 @@ from contextlib import contextmanager import re -import io - from alembic.testing import exclusions from alembic.testing import TestBase, eq_, config from alembic.testing.fixtures import op_fixture from alembic.testing import mock from alembic.operations import Operations -from alembic.batch import ApplyBatchImpl -from alembic.migration import MigrationContext +from alembic.operations.batch import ApplyBatchImpl +from alembic.runtime.migration import MigrationContext from sqlalchemy import Integer, Table, Column, String, MetaData, ForeignKey, \ @@ -330,7 +328,7 @@ class BatchApplyTest(TestBase): impl = self._simple_fixture() col = Column('g', Integer) # operations.add_column produces a table - t = self.op._table('tname', col) # noqa + t = self.op.schema_obj.table('tname', col) # noqa impl.add_column('tname', col) new_table = self._assert_impl(impl, colnames=['id', 'x', 'y', 'g']) eq_(new_table.c.g.name, 'g') @@ -420,7 +418,7 @@ class BatchApplyTest(TestBase): def test_add_fk(self): impl = self._simple_fixture() impl.add_column('tname', Column('user_id', Integer)) - fk = self.op._foreign_key_constraint( + fk = self.op.schema_obj.foreign_key_constraint( 'fk1', 'tname', 'user', ['user_id'], ['id']) impl.add_constraint(fk) @@ -447,7 +445,7 @@ class BatchApplyTest(TestBase): def test_add_uq(self): impl = self._simple_fixture() - uq = self.op._unique_constraint( + uq = self.op.schema_obj.unique_constraint( 'uq1', 'tname', ['y'] ) @@ -459,7 +457,7 @@ class BatchApplyTest(TestBase): def test_drop_uq(self): impl = self._uq_fixture() - uq = self.op._unique_constraint( + uq = self.op.schema_obj.unique_constraint( 'uq1', 'tname', ['y'] ) impl.drop_constraint(uq) @@ -469,7 +467,7 @@ class BatchApplyTest(TestBase): def test_create_index(self): impl = self._simple_fixture() - ix = self.op._index('ix1', 'tname', ['y']) + ix = self.op.schema_obj.index('ix1', 'tname', ['y']) impl.create_index(ix) self._assert_impl( @@ -479,7 +477,7 @@ class BatchApplyTest(TestBase): def test_drop_index(self): impl = self._ix_fixture() - ix = self.op._index('ix1', 'tname', ['y']) + ix = self.op.schema_obj.index('ix1', 'tname', ['y']) impl.drop_index(ix) self._assert_impl( impl, colnames=['id', 'x', 'y'], @@ -498,12 +496,14 @@ class BatchAPITest(TestBase): @contextmanager def _fixture(self, schema=None): - migration_context = mock.Mock(opts={}) + migration_context = mock.Mock( + opts={}, impl=mock.MagicMock(__dialect__='sqlite')) op = Operations(migration_context) batch = op.batch_alter_table( 'tname', recreate='never', schema=schema).__enter__() - with mock.patch("alembic.operations.sa_schema") as mock_schema: + mock_schema = mock.MagicMock() + with mock.patch("alembic.operations.schemaobj.sa_schema", mock_schema): yield batch batch.impl.flush() self.mock_schema = mock_schema diff --git a/tests/test_config.py b/tests/test_config.py index db37456..da0b413 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,9 +1,8 @@ #!coding: utf-8 -import os -import tempfile -from alembic import config, util, compat +from alembic import config, util +from alembic.util import compat from alembic.migration import MigrationContext from alembic.operations import Operations from alembic.script import ScriptDirectory diff --git a/tests/test_op.py b/tests/test_op.py index 7d5f83e..9c14e49 100644 --- a/tests/test_op.py +++ b/tests/test_op.py @@ -524,7 +524,8 @@ class OpTest(TestBase): def test_add_foreign_key_dialect_kw(self): op_fixture() with mock.patch( - "alembic.operations.sa_schema.ForeignKeyConstraint") as fkc: + "sqlalchemy.schema.ForeignKeyConstraint" + ) as fkc: op.create_foreign_key('fk_test', 't1', 't2', ['foo', 'bar'], ['bat', 'hoho'], foobar_arg='xyz') @@ -808,12 +809,6 @@ class OpTest(TestBase): op.drop_constraint("f1", "t1", type_="foreignkey") context.assert_("ALTER TABLE t1 DROP FOREIGN KEY f1") - assert_raises_message( - TypeError, - r"Unknown arguments: badarg\d, badarg\d", - op.alter_column, "t", "c", badarg1="x", badarg2="y" - ) - @config.requirements.fail_before_sqla_084 def test_naming_changes_drop_idx(self): context = op_fixture('mssql') @@ -856,4 +851,32 @@ class SQLModeOpTest(TestBase): context.assert_( "CREATE TABLE some_table (id INTEGER NOT NULL, st_id INTEGER, " "PRIMARY KEY (id), FOREIGN KEY(st_id) REFERENCES some_table (id))" - ) \ No newline at end of file + ) + + +class CustomOpTest(TestBase): + def test_custom_op(self): + from alembic.operations import Operations, MigrateOperation + + @Operations.register_operation("create_sequence") + class CreateSequenceOp(MigrateOperation): + """Create a SEQUENCE.""" + + def __init__(self, sequence_name, **kw): + self.sequence_name = sequence_name + self.kw = kw + + @classmethod + def create_sequence(cls, operations, sequence_name, **kw): + """Issue a "CREATE SEQUENCE" instruction.""" + + op = CreateSequenceOp(sequence_name, **kw) + return operations.invoke(op) + + @Operations.implementation_for(CreateSequenceOp) + def create_sequence(operations, operation): + operations.execute("CREATE SEQUENCE %s" % operation.sequence_name) + + context = op_fixture() + op.create_sequence('foob') + context.assert_("CREATE SEQUENCE foob") diff --git a/tests/test_revision.py b/tests/test_revision.py index d73316d..0a515de 100644 --- a/tests/test_revision.py +++ b/tests/test_revision.py @@ -1,6 +1,6 @@ from alembic.testing.fixtures import TestBase from alembic.testing import eq_, assert_raises_message -from alembic.revision import RevisionMap, Revision, MultipleHeads, \ +from alembic.script.revision import RevisionMap, Revision, MultipleHeads, \ RevisionError diff --git a/tests/test_script_consumption.py b/tests/test_script_consumption.py index 11b8080..c2eef0a 100644 --- a/tests/test_script_consumption.py +++ b/tests/test_script_consumption.py @@ -3,7 +3,8 @@ import os import re -from alembic import command, util, compat +from alembic import command, util +from alembic.util import compat from alembic.script import ScriptDirectory, Script from alembic.testing.env import clear_staging_env, staging_env, \ _sqlite_testing_config, write_script, _sqlite_file_db, \ diff --git a/tests/test_script_production.py b/tests/test_script_production.py index 1f380ab..3ce6200 100644 --- a/tests/test_script_production.py +++ b/tests/test_script_production.py @@ -1,15 +1,20 @@ from alembic.testing.fixtures import TestBase -from alembic.testing import eq_, ne_, is_, assert_raises_message +from alembic.testing import eq_, ne_, assert_raises_message from alembic.testing.env import clear_staging_env, staging_env, \ _get_staging_directory, _no_sql_testing_config, env_file_fixture, \ script_file_fixture, _testing_config, _sqlite_testing_config, \ - three_rev_fixture, _multi_dir_testing_config + three_rev_fixture, _multi_dir_testing_config, write_script,\ + _sqlite_file_db from alembic import command from alembic.script import ScriptDirectory from alembic.environment import EnvironmentContext +from alembic.testing import mock from alembic import util +from alembic.operations import ops import os import datetime +import sqlalchemy as sa +from sqlalchemy.engine.reflection import Inspector env, abc, def_ = None, None, None @@ -214,6 +219,174 @@ class RevisionCommandTest(TestBase): ) +class CustomizeRevisionTest(TestBase): + def setUp(self): + self.env = staging_env() + self.cfg = _multi_dir_testing_config() + self.cfg.set_main_option("revision_environment", "true") + + script = ScriptDirectory.from_config(self.cfg) + # MARKMARK + self.model1 = util.rev_id() + self.model2 = util.rev_id() + self.model3 = util.rev_id() + for model, name in [ + (self.model1, "model1"), + (self.model2, "model2"), + (self.model3, "model3"), + ]: + script.generate_revision( + model, name, refresh=True, + version_path=os.path.join(_get_staging_directory(), name), + head="base") + + write_script(script, model, """\ +"%s" +revision = '%s' +down_revision = None +branch_labels = ['%s'] + +from alembic import op + +def upgrade(): + pass + +def downgrade(): + pass + +""" % (name, model, name)) + + def tearDown(self): + clear_staging_env() + + def _env_fixture(self, fn, target_metadata): + self.engine = engine = _sqlite_file_db() + + def run_env(self): + from alembic import context + + with engine.connect() as connection: + context.configure( + connection=connection, + target_metadata=target_metadata, + process_revision_directives=fn) + with context.begin_transaction(): + context.run_migrations() + + return mock.patch( + "alembic.script.base.ScriptDirectory.run_env", + run_env + ) + + def test_new_locations_no_autogen(self): + m = sa.MetaData() + + def process_revision_directives(context, rev, generate_revisions): + generate_revisions[:] = [ + ops.MigrationScript( + util.rev_id(), + ops.UpgradeOps(), + ops.DowngradeOps(), + version_path=os.path.join( + _get_staging_directory(), "model1"), + head="model1@head" + ), + ops.MigrationScript( + util.rev_id(), + ops.UpgradeOps(), + ops.DowngradeOps(), + version_path=os.path.join( + _get_staging_directory(), "model2"), + head="model2@head" + ), + ops.MigrationScript( + util.rev_id(), + ops.UpgradeOps(), + ops.DowngradeOps(), + version_path=os.path.join( + _get_staging_directory(), "model3"), + head="model3@head" + ), + ] + + with self._env_fixture(process_revision_directives, m): + revs = command.revision(self.cfg, message="some message") + + script = ScriptDirectory.from_config(self.cfg) + + for rev, model in [ + (revs[0], "model1"), + (revs[1], "model2"), + (revs[2], "model3"), + ]: + rev_script = script.get_revision(rev.revision) + eq_( + rev_script.path, + os.path.abspath(os.path.join( + _get_staging_directory(), model, + "%s_.py" % (rev_script.revision, ) + )) + ) + assert os.path.exists(rev_script.path) + + def test_autogen(self): + m = sa.MetaData() + sa.Table('t', m, sa.Column('x', sa.Integer)) + + def process_revision_directives(context, rev, generate_revisions): + existing_upgrades = generate_revisions[0].upgrade_ops + existing_downgrades = generate_revisions[0].downgrade_ops + + # model1 will run the upgrades, e.g. create the table, + # model2 will run the downgrades as upgrades, e.g. drop + # the table again + + generate_revisions[:] = [ + ops.MigrationScript( + util.rev_id(), + existing_upgrades, + ops.DowngradeOps(), + version_path=os.path.join( + _get_staging_directory(), "model1"), + head="model1@head" + ), + ops.MigrationScript( + util.rev_id(), + existing_downgrades, + ops.DowngradeOps(), + version_path=os.path.join( + _get_staging_directory(), "model2"), + head="model2@head" + ) + ] + + with self._env_fixture(process_revision_directives, m): + command.upgrade(self.cfg, "heads") + + eq_( + Inspector.from_engine(self.engine).get_table_names(), + ["alembic_version"] + ) + + command.revision( + self.cfg, message="some message", + autogenerate=True) + + command.upgrade(self.cfg, "model1@head") + + eq_( + Inspector.from_engine(self.engine).get_table_names(), + ["alembic_version", "t"] + ) + + command.upgrade(self.cfg, "model2@head") + + eq_( + Inspector.from_engine(self.engine).get_table_names(), + ["alembic_version"] + ) + + class MultiDirRevisionCommandTest(TestBase): def setUp(self): self.env = staging_env()