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 | 327 +-- alembic/autogenerate/compare.py | 54 +- alembic/autogenerate/compose.py | 144 + alembic/autogenerate/generate.py | 92 + alembic/autogenerate/render.py | 493 ++-- 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} | 2034 +++++++------- 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 | 2474 +++++++++++++---- 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} | 669 +---- 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, 7012 insertions(+), 3447 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 08756184..5a97f5e0 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 f4294417..345bf262 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 2d759120..4272a7ed 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 6281a6ce..cff977bb 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) + + migration_script = ops.MigrationScript( + rev_id=None, + upgrade_ops=ops.UpgradeOps([]), + downgrade_ops=ops.DowngradeOps([]), + ) + + compose._to_migration_script(autogen_context, migration_script, diffs) + + return migration_script + + +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 _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): +def _render_migration_diffs(context, template_args, imports): + """legacy, used by test_autogen_composition at the moment""" - buf = StringIO() - printer = PythonPrinter(buf) + migration_script = produce_migrations(context, None) + + autogen_context = _autogen_context(context, imports=imports) + diffs = [] - printer.writeline( - "### commands auto generated by Alembic - " - "please adjust! ###" + compare._produce_net_changes(autogen_context, diffs) + + migration_script = ops.MigrationScript( + rev_id=None, + imports=imports, + 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 ###") + render._render_migration_script( + autogen_context, migration_script, template_args + ) - return buf.getvalue() +def _autogen_context( + context, imports=None, metadata=None, include_symbol=None, + include_object=None, include_schemas=False): -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) + 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) - } - - 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) + 'opts': opts, + 'metadata': metadata, + 'object_filters': object_filters, + 'include_schemas': include_schemas } - 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 2aae9621..cd6b6965 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 00000000..b42b5056 --- /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 00000000..c6861566 --- /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 50076529..c3f3df1e 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,69 +23,91 @@ except ImportError: return name -class _f_name(object): +def _indent(text): + text = re.compile(r'^', re.M).sub(" ", text).strip() + text = re.compile(r' +$', re.M).sub("", text) + return text - def __init__(self, prefix, name): - self.prefix = prefix - self.name = name - def __repr__(self): - return "%sf(%r)" % (self.prefix, _ident(self.name)) +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)) -def _ident(name): - """produce a __repr__() object for a string identifier that may - use quoted_name() in SQLAlchemy 0.9 and greater. +default_renderers = renderers = util.Dispatcher() - The issue worked around here is that quoted_name() doesn't have - very good repr() behavior by itself when unicode is involved. - """ - if name is None: - return name - elif compat.sqla_09 and isinstance(name, sql.elements.quoted_name): - if compat.py2k: - # the attempt to encode to ascii here isn't super ideal, - # however we are trying to cut down on an explosion of - # u'' literals only when py2k + SQLA 0.9, in particular - # makes unit tests testing code generation very difficult - try: - return name.encode('ascii') - except UnicodeError: - return compat.text_type(name) - else: - return compat.text_type(name) - elif isinstance(name, compat.string_types): - return name +def _render_cmd_body(op_container, autogen_context): + buf = StringIO() + printer = PythonPrinter(buf) -def _render_potential_expr(value, autogen_context, wrap_in_text=True): - if isinstance(value, sql.ClauseElement): - if compat.sqla_08: - compile_kw = dict(compile_kwargs={'literal_binds': True}) - else: - compile_kw = {} + printer.writeline( + "### commands auto generated by Alembic - " + "please adjust! ###" + ) - if wrap_in_text: - template = "%(prefix)stext(%(sql)r)" - else: - template = "%(sql)r" + if not op_container.ops: + printer.writeline("pass") + else: + for op in op_container.ops: + lines = render_op(autogen_context, op) - return template % { - "prefix": _sqlalchemy_autogenerate_prefix(autogen_context), - "sql": compat.text_type( - value.compile(dialect=autogen_context['dialect'], - **compile_kw) - ) - } + 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 repr(value) + return [ + "pass" + ] + +@renderers.dispatch_for(ops.CreateTableOp) +def _add_table(autogen_context, op): + table = op.to_table() -def _add_table(table, autogen_context): args = [col for col in - [_render_column(col, autogen_context) for col in table.c] + [_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 @@ -98,45 +121,33 @@ def _add_table(table, autogen_context): args = ',\n'.join(args) text = "%(prefix)screate_table(%(tablename)r,\n%(args)s" % { - 'tablename': _ident(table.name), + 'tablename': _ident(op.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]) + 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 -def _drop_table(table, autogen_context): +@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(table.name) + "tname": _ident(op.table_name) } - if table.schema: - text += ", schema=%r" % _ident(table.schema) + if op.schema: + text += ", schema=%r" % _ident(op.schema) text += ")" return text -def _get_index_rendered_expressions(idx, autogen_context): - if compat.sqla_08: - return [repr(_ident(getattr(exp, "name", None))) - if isinstance(exp, sa_schema.Column) - else _render_potential_expr(exp, autogen_context) - for exp in idx.expressions] - else: - return [ - 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. - """ +@renderers.dispatch_for(ops.CreateIndexOp) +def _add_index(autogen_context, op): + index = op.to_index() has_batch = 'batch_prefix' in autogen_context @@ -167,11 +178,8 @@ def _add_index(index, autogen_context): return text -def _drop_index(index, autogen_context): - """ - Generate Alembic operations for the DROP INDEX of an - :class:`~sqlalchemy.schema.Index` instance. - """ +@renderers.dispatch_for(ops.DropIndexOp) +def _drop_index(autogen_context, op): has_batch = 'batch_prefix' in autogen_context if has_batch: @@ -182,90 +190,39 @@ def _drop_index(index, autogen_context): 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 '') + '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 -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 = [] - - has_batch = 'batch_prefix' in autogen_context - - if constraint.deferrable: - opts.append(("deferrable", str(constraint.deferrable))) - if constraint.initially: - opts.append(("initially", str(constraint.initially))) - if not has_batch and alter and constraint.table.schema: - opts.append(("schema", _ident(constraint.table.schema))) - if not alter and constraint.name: - opts.append( - ("name", - _render_gen_name(autogen_context, constraint.name))) - - if alter: - args = [ - 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])) - args.extend(["%s=%r" % (k, v) for k, v in opts]) - return "%(prefix)screate_unique_constraint(%(args)s)" % { - 'prefix': _alembic_autogenerate_prefix(autogen_context), - 'args': ", ".join(args) - } - else: - args = [repr(_ident(col.name)) for col in constraint.columns] - args.extend(["%s=%r" % (k, v) for k, v in opts]) - return "%(prefix)sUniqueConstraint(%(args)s)" % { - "prefix": _sqlalchemy_autogenerate_prefix(autogen_context), - "args": ", ".join(args) - } +@renderers.dispatch_for(ops.CreateUniqueConstraintOp) +def _add_unique_constraint(autogen_context, op): + return [_uq_constraint(op.to_constraint(), autogen_context, True)] -def _add_fk_constraint(constraint, autogen_context): - source_schema, source_table, \ - source_columns, target_schema, \ - target_table, target_columns = _fk_spec(constraint) +@renderers.dispatch_for(ops.CreateForeignKeyOp) +def _add_fk_constraint(autogen_context, op): 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]) + 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]) ] - 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)) + 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), @@ -273,41 +230,18 @@ def _add_fk_constraint(constraint, autogen_context): } +@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() -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", - } +@renderers.dispatch_for(ops.DropConstraintOp) +def _drop_constraint(autogen_context, op): if 'batch_prefix' in autogen_context: template = "%(prefix)sdrop_constraint"\ @@ -316,19 +250,22 @@ def _drop_constraint(constraint, autogen_context): 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 '', + '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 -def _add_column(schema, tname, column, autogen_context): +@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: @@ -345,7 +282,11 @@ def _add_column(schema, tname, column, autogen_context): return text -def _drop_column(schema, tname, column, autogen_context): +@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: @@ -357,21 +298,25 @@ def _drop_column(schema, tname, column, autogen_context): text = template % { "prefix": _alembic_autogenerate_prefix(autogen_context), "tname": _ident(tname), - "cname": _ident(column.name), + "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): +@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: @@ -413,6 +358,114 @@ def _modify_col(tname, cname, return text +class _f_name(object): + + def __init__(self, prefix, name): + self.prefix = prefix + self.name = name + + def __repr__(self): + return "%sf(%r)" % (self.prefix, _ident(self.name)) + + +def _ident(name): + """produce a __repr__() object for a string identifier that may + use quoted_name() in SQLAlchemy 0.9 and greater. + + The issue worked around here is that quoted_name() doesn't have + very good repr() behavior by itself when unicode is involved. + + """ + if name is None: + return name + elif compat.sqla_09 and isinstance(name, sql.elements.quoted_name): + if compat.py2k: + # the attempt to encode to ascii here isn't super ideal, + # however we are trying to cut down on an explosion of + # u'' literals only when py2k + SQLA 0.9, in particular + # makes unit tests testing code generation very difficult + try: + return name.encode('ascii') + except UnicodeError: + return compat.text_type(name) + else: + return compat.text_type(name) + elif isinstance(name, compat.string_types): + return name + + +def _render_potential_expr(value, autogen_context, wrap_in_text=True): + if isinstance(value, sql.ClauseElement): + if compat.sqla_08: + compile_kw = dict(compile_kwargs={'literal_binds': True}) + else: + compile_kw = {} + + if wrap_in_text: + template = "%(prefix)stext(%(sql)r)" + else: + template = "%(sql)r" + + return template % { + "prefix": _sqlalchemy_autogenerate_prefix(autogen_context), + "sql": compat.text_type( + value.compile(dialect=autogen_context['dialect'], + **compile_kw) + ) + } + + else: + return repr(value) + + +def _get_index_rendered_expressions(idx, autogen_context): + if compat.sqla_08: + return [repr(_ident(getattr(exp, "name", None))) + if isinstance(exp, sa_schema.Column) + else _render_potential_expr(exp, autogen_context) + for exp in idx.expressions] + else: + return [ + repr(_ident(getattr(col, "name", None))) for col in idx.columns] + + +def _uq_constraint(constraint, autogen_context, alter): + opts = [] + + has_batch = 'batch_prefix' in autogen_context + + if constraint.deferrable: + opts.append(("deferrable", str(constraint.deferrable))) + if constraint.initially: + opts.append(("initially", str(constraint.initially))) + if not has_batch and alter and constraint.table.schema: + opts.append(("schema", _ident(constraint.table.schema))) + if not alter and constraint.name: + opts.append( + ("name", + _render_gen_name(autogen_context, constraint.name))) + + if alter: + args = [ + 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])) + args.extend(["%s=%r" % (k, v) for k, v in opts]) + return "%(prefix)screate_unique_constraint(%(args)s)" % { + 'prefix': _alembic_autogenerate_prefix(autogen_context), + 'args': ", ".join(args) + } + else: + args = [repr(_ident(col.name)) for col in constraint.columns] + args.extend(["%s=%r" % (k, v) for k, v in opts]) + return "%(prefix)sUniqueConstraint(%(args)s)" % { + "prefix": _sqlalchemy_autogenerate_prefix(autogen_context), + "args": ", ".join(args) + } + + 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 5ba6d6a8..3ce51311 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 7f813d27..b3fc36fc 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 9c0f6760..758fca87 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 dbdc991a..f4a525f2 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 3cca1ef1..debef26f 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 f516e9bc..f51de33b 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 79561858..b1cb324e 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 9f97b345..ea423d76 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 8e5f7771..1f367a10 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 00000000..1f6ee5da --- /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 00000000..18710fc7 --- /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 10067396..726df78f 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 2bf80605..1a38d074 100644 --- a/alembic/operations.py +++ b/alembic/operations/ops.py @@ -1,345 +1,82 @@ -from contextlib import contextmanager +from .. import util +from ..util import sqla_compat +from . import schemaobj +from sqlalchemy.types import NULLTYPE +from .base import Operations, BatchOperations -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 +class MigrateOperation(object): + """base class for migration command and organization objects. -__all__ = ('Operations', 'BatchOperations') + This system is part of the operation extensibility API. -try: - from sqlalchemy.sql.naming import conv -except: - conv = None + .. versionadded:: 0.8.0 + .. seealso:: -class Operations(object): - - """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 + :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 _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 _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 - ) - 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) - - 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)) - - @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. + 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) - .. seealso:: - :ref:`batch_migrations` +@Operations.register_operation("drop_constraint") +@BatchOperations.register_operation("drop_constraint", "batch_drop_constraint") +class DropConstraintOp(MigrateOperation): + """Represent a drop constraint operation.""" - """ - 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. + 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 - """ + @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", + } - return self.migration_context + constraint_table = sqla_compat._table_for_constraint(constraint) + return cls( + constraint.name, + constraint_table.name, + schema=constraint_table.schema, + type_=types[constraint.__visit_name__] + ) - def rename_table(self, old_table_name, new_table_name, schema=None): - """Emit an ALTER TABLE to rename a table. + @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. - :param old_table_name: old name. - :param new_table_name: new name. + :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 @@ -349,234 +86,96 @@ class Operations(object): :class:`~sqlalchemy.sql.elements.quoted_name` construct. """ - self.impl.rename_table( - old_table_name, - new_table_name, - schema=schema - ) - @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 - ): - """Issue an "alter column" instruction using the - current migration context. + op = cls(name, table_name, type_=type_, schema=schema) + return operations.invoke(op) - Generally, only that aspect of the column which - is being changed, i.e. name, type, nullability, - default, needs to be specified. Multiple changes - can also be specified at once and the backend should - "do the right thing", emitting each change either - separately or together as the backend allows. - - MySQL has special requirements here, since MySQL - cannot ALTER a column without a full specification. - When producing MySQL-compatible migration files, - it is recommended that the ``existing_type``, - ``existing_server_default``, and ``existing_nullable`` - parameters be present, if not being altered. + @classmethod + def batch_drop_constraint(cls, operations, name, type_=None): + """Issue a "drop constraint" instruction using the + current batch migration context. - Type changes which are against the SQLAlchemy - "schema" types :class:`~sqlalchemy.types.Boolean` - and :class:`~sqlalchemy.types.Enum` may also - add or drop constraints which accompany those - types on backends that don't support them natively. - The ``existing_server_default`` argument is - used in this case as well to remove a previous - constraint. + The batch form of this call omits the ``table_name`` and ``schema`` + arguments from the call. - :param table_name: string name of the target table. - :param column_name: string name of the target column, - as it exists before the operation begins. - :param nullable: Optional; specify ``True`` or ``False`` - to alter the column's nullability. - :param server_default: Optional; specify a string - SQL expression, :func:`~sqlalchemy.sql.expression.text`, - or :class:`~sqlalchemy.schema.DefaultClause` to indicate - an alteration to the column's default value. - Set to ``None`` to have the default removed. - :param new_column_name: Optional; specify a string name here to - indicate the new name within a column rename operation. - :param ``type_``: Optional; a :class:`~sqlalchemy.types.TypeEngine` - type object to specify a change to the column's type. - For SQLAlchemy types that also indicate a constraint (i.e. - :class:`~sqlalchemy.types.Boolean`, :class:`~sqlalchemy.types.Enum`), - the constraint is also generated. - :param autoincrement: set the ``AUTO_INCREMENT`` flag of the column; - currently understood by the MySQL dialect. - :param existing_type: Optional; a - :class:`~sqlalchemy.types.TypeEngine` - type object to specify the previous type. This - is required for all MySQL column alter operations that - don't otherwise specify a new type, as well as for - when nullability is being changed on a SQL Server - column. It is also used if the type is a so-called - SQLlchemy "schema" type which may define a constraint (i.e. - :class:`~sqlalchemy.types.Boolean`, - :class:`~sqlalchemy.types.Enum`), - so that the constraint can be dropped. - :param existing_server_default: Optional; The existing - default value of the column. Required on MySQL if - an existing default is not being changed; else MySQL - removes the default. - :param existing_nullable: Optional; the existing nullability - of the column. Required on MySQL if the existing nullability - is not being changed; else MySQL sets this to NULL. - :param existing_autoincrement: Optional; the existing autoincrement - of the column. Used for MySQL's system of altering a column - that specifies ``AUTO_INCREMENT``. - :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`. + .. seealso:: - .. versionadded:: 0.7.0 'schema' can now accept a - :class:`~sqlalchemy.sql.elements.quoted_name` construct. + :meth:`.Operations.drop_constraint` """ - - compiler = self.impl.dialect.statement_compiler( - self.impl.dialect, - None + op = cls( + name, operations.impl.table_name, + type_=type_, schema=operations.impl.schema ) + return operations.invoke(op) - 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 = 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) - - 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 - ) - - 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 - - """ - if conv: - return conv(name) - else: - raise NotImplementedError( - "op.f() feature requires SQLAlchemy 0.9.4 or greater.") - - def add_column(self, table_name, column, schema=None): - """Issue an "add column" instruction using the current - migration context. - - e.g.:: - from alembic import op - from sqlalchemy import Column, String +@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.""" - op.add_column('organization', - Column('name', String()) - ) + 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 - The provided :class:`~sqlalchemy.schema.Column` object can also - specify a :class:`~sqlalchemy.schema.ForeignKey`, referencing - a remote table name. Alembic will automatically generate a stub - "referenced" table and emit a second ALTER statement in order - to add the constraint separately:: + @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 + ) - from alembic import op - from sqlalchemy import Column, INTEGER, ForeignKey + 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) - op.add_column('organization', - Column('account_id', INTEGER, ForeignKey('accounts.id')) - ) + @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. - Note that this statement uses the :class:`~sqlalchemy.schema.Column` - construct as is 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:: + e.g.:: from alembic import op - from sqlalchemy import Column, TIMESTAMP, func + op.create_primary_key( + "pk_my_table", "my_table", + ["id", "version"] + ) - # specify "DEFAULT NOW" along with the column add - op.add_column('account', - Column('timestamp', TIMESTAMP, server_default=func.now()) - ) + 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 table_name: String name of the parent table. - :param column: a :class:`sqlalchemy.schema.Column` object - representing the new column. + :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 @@ -585,102 +184,103 @@ class Operations(object): .. 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) - 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)) + @classmethod + def batch_create_primary_key(cls, operations, constraint_name, columns): + """Issue a "create primary key" instruction using the + current batch migration context. - def drop_column(self, table_name, column_name, **kw): - """Issue a "drop column" instruction using the current - migration context. + The batch form of this call omits the ``table_name`` and ``schema`` + arguments from the call. - e.g.:: + .. seealso:: - drop_column('organization', 'account_id') + :meth:`.Operations.create_primary_key` - :param table_name: name of table - :param column_name: name of column - :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`. + """ + raise NotImplementedError("not yet implemented") - .. versionadded:: 0.7.0 'schema' can now accept a - :class:`~sqlalchemy.sql.elements.quoted_name` construct. - :param mssql_drop_check: Optional boolean. When ``True``, on - Microsoft SQL Server only, first - drop the CHECK constraint on the column using a - SQL-script-compatible - block that selects into a @variable from sys.check_constraints, - then exec's a separate DROP CONSTRAINT for that constraint. - :param mssql_drop_default: Optional boolean. When ``True``, on - Microsoft SQL Server only, first - drop the DEFAULT constraint on the column using a - SQL-script-compatible - block that selects into a @variable from sys.default_constraints, - then exec's a separate DROP CONSTRAINT for that default. - :param mssql_drop_foreign_key: Optional boolean. When ``True``, on - Microsoft SQL Server only, first - drop a single FOREIGN KEY constraint on the column using a - SQL-script-compatible - block that selects into a @variable from - sys.foreign_keys/sys.foreign_key_columns, - then exec's a separate DROP CONSTRAINT for that default. Only - works if the column has exactly one FK constraint which refers to - it, at the moment. +@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.""" - .. versionadded:: 0.6.2 + 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) - self.impl.drop_column( - table_name, - self._column(column_name, NULLTYPE), + 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 create_primary_key(self, name, table_name, cols, schema=None): - """Issue a "create primary key" instruction using the current - migration context. + 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_primary_key( - "pk_my_table", "my_table", - ["id", "version"] - ) + 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.PrimaryKeyConstraint` + :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 primary key constraint. The name is necessary + :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` + :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 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 @@ -690,12 +290,94 @@ class Operations(object): :class:`~sqlalchemy.sql.elements.quoted_name` construct. """ - self.impl.add_constraint( - self._primary_key_constraint(name, table_name, cols, - schema) + + op = cls( + constraint_name, table_name, columns, + schema=schema, **kw ) + return operations.invoke(op) + + @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. - def create_foreign_key(self, name, source, referent, local_cols, + 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, @@ -726,8 +408,8 @@ class Operations(object): ``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 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 @@ -745,51 +427,233 @@ class Operations(object): """ - 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) + 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) - def create_unique_constraint(self, name, source, local_cols, - schema=None, **kw): - """Issue a "create unique constraint" instruction using the + @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 - op.create_unique_constraint("uq_user_name", "user", ["name"]) + 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)')]) - 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. + .. 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 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 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 @@ -798,49 +662,87 @@ class Operations(object): .. 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` + """ - self.impl.add_constraint( - self._unique_constraint(name, source, local_cols, - schema=schema, **kw) + op = cls( + index_name, operations.impl.table_name, columns, + schema=operations.impl.schema, **kw ) + return operations.invoke(op) - def create_check_constraint(self, name, source, condition, - schema=None, **kw): - """Issue a "create check constraint" instruction using the - current migration context. - e.g.:: +@Operations.register_operation("drop_index") +@BatchOperations.register_operation("drop_index", "batch_drop_index") +class DropIndexOp(MigrateOperation): + """Represent a drop index operation.""" - from alembic import op - from sqlalchemy.sql import column, func + def __init__(self, index_name, table_name=None, schema=None): + self.index_name = index_name + self.table_name = table_name + self.schema = schema - op.create_check_constraint( - "ck_user_name_len", - "user", - func.len(column('name')) > 5 - ) + @classmethod + def from_index(cls, index): + return cls( + index.name, + index.table.name, + schema=index.table.schema, + ) - 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. + def to_index(self, migration_context=None): + schema_obj = schemaobj.SchemaObjects(migration_context) - :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. + # 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 @@ -850,12 +752,62 @@ class Operations(object): :class:`~sqlalchemy.sql.elements.quoted_name` construct. """ - self.impl.add_constraint( - self._check_constraint( - name, source, condition, schema=schema, **kw) + 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 ) - def create_table(self, name, *columns, **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. @@ -917,7 +869,7 @@ class Operations(object): .. versionadded:: 0.7.0 - :param name: Name of the table + :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` @@ -940,63 +892,322 @@ class Operations(object): object is returned. """ - table = self._table(name, *columns, **kw) - self.impl.create_table(table) - return table + 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. + :param new_table_name: new name. + :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(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( + 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. + + Generally, only that aspect of the column which + is being changed, i.e. name, type, nullability, + default, needs to be specified. Multiple changes + can also be specified at once and the backend should + "do the right thing", emitting each change either + separately or together as the backend allows. + + MySQL has special requirements here, since MySQL + cannot ALTER a column without a full specification. + When producing MySQL-compatible migration files, + it is recommended that the ``existing_type``, + ``existing_server_default``, and ``existing_nullable`` + parameters be present, if not being altered. + + Type changes which are against the SQLAlchemy + "schema" types :class:`~sqlalchemy.types.Boolean` + and :class:`~sqlalchemy.types.Enum` may also + add or drop constraints which accompany those + types on backends that don't support them natively. + The ``existing_server_default`` argument is + used in this case as well to remove a previous + constraint. + + :param table_name: string name of the target table. + :param column_name: string name of the target column, + as it exists before the operation begins. + :param nullable: Optional; specify ``True`` or ``False`` + to alter the column's nullability. + :param server_default: Optional; specify a string + SQL expression, :func:`~sqlalchemy.sql.expression.text`, + or :class:`~sqlalchemy.schema.DefaultClause` to indicate + an alteration to the column's default value. + Set to ``None`` to have the default removed. + :param new_column_name: Optional; specify a string name here to + indicate the new name within a column rename operation. + :param ``type_``: Optional; a :class:`~sqlalchemy.types.TypeEngine` + type object to specify a change to the column's type. + For SQLAlchemy types that also indicate a constraint (i.e. + :class:`~sqlalchemy.types.Boolean`, :class:`~sqlalchemy.types.Enum`), + the constraint is also generated. + :param autoincrement: set the ``AUTO_INCREMENT`` flag of the column; + currently understood by the MySQL dialect. + :param existing_type: Optional; a + :class:`~sqlalchemy.types.TypeEngine` + type object to specify the previous type. This + is required for all MySQL column alter operations that + don't otherwise specify a new type, as well as for + when nullability is being changed on a SQL Server + column. It is also used if the type is a so-called + SQLlchemy "schema" type which may define a constraint (i.e. + :class:`~sqlalchemy.types.Boolean`, + :class:`~sqlalchemy.types.Enum`), + so that the constraint can be dropped. + :param existing_server_default: Optional; The existing + default value of the column. Required on MySQL if + an existing default is not being changed; else MySQL + removes the default. + :param existing_nullable: Optional; the existing nullability + of the column. Required on MySQL if the existing nullability + is not being changed; else MySQL sets this to NULL. + :param existing_autoincrement: Optional; the existing autoincrement + of the column. Used for MySQL's system of altering a column + that specifies ``AUTO_INCREMENT``. + :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. + + """ + + 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 + ) + + return operations.invoke(alt) + + @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. + + .. seealso:: - def drop_table(self, name, **kw): - """Issue a "drop table" instruction using the current - migration context. + :meth:`.Operations.add_column` + """ + 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 + ) - e.g.:: + return operations.invoke(alt) - 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`. +@Operations.register_operation("add_column") +@BatchOperations.register_operation("add_column", "batch_add_column") +class AddColumnOp(AlterTableOp): + """Represent an add column operation.""" - .. versionadded:: 0.7.0 'schema' can now accept a - :class:`~sqlalchemy.sql.elements.quoted_name` construct. + def __init__(self, table_name, column, schema=None): + super(AddColumnOp, self).__init__(table_name, schema=schema) + self.column = column - :param \**kw: Other keyword arguments are passed to the underlying - :class:`sqlalchemy.schema.Table` object created for the command. + @classmethod + def from_column(cls, col): + return cls(col.table.name, col, schema=col.table.schema) - """ - self.impl.drop_table( - self._table(name, **kw) - ) + @classmethod + def from_column_and_tablename(cls, schema, tname, col): + return cls(tname, col, schema=schema) - def create_index(self, name, table_name, columns, schema=None, - unique=False, quote=None, **kw): - """Issue a "create index" instruction using the current + @classmethod + def add_column(cls, operations, table_name, column, schema=None): + """Issue an "add column" instruction using the current migration context. e.g.:: from alembic import op - op.create_index('ik_test', 't1', ['foo', 'bar']) + from sqlalchemy import Column, String - Functional indexes can be produced by using the - :func:`sqlalchemy.sql.expression.text` construct:: + op.add_column('organization', + Column('name', String()) + ) + + The provided :class:`~sqlalchemy.schema.Column` object can also + specify a :class:`~sqlalchemy.schema.ForeignKey`, referencing + a remote table name. Alembic will automatically generate a stub + "referenced" table and emit a second ALTER statement in order + to add the constraint separately:: from alembic import op - from sqlalchemy import text - op.create_index('ik_test', 't1', [text('lower(foo)')]) + from sqlalchemy import Column, INTEGER, ForeignKey - .. 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. + op.add_column('organization', + Column('account_id', INTEGER, ForeignKey('accounts.id')) + ) - :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. + Note that this statement uses the :class:`~sqlalchemy.schema.Column` + construct as is 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 column add + op.add_column('account', + Column('timestamp', TIMESTAMP, server_default=func.now()) + ) + + :param table_name: String name of the parent table. + :param column: a :class:`sqlalchemy.schema.Column` object + representing the new column. :param schema: Optional schema name to operate within. To control quoting of the schema outside of the default behavior, use the SQLAlchemy construct @@ -1005,40 +1216,59 @@ class Operations(object): .. 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) + op = cls(table_name, column, schema=schema) + return operations.invoke(op) + + @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) - @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 + +@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. e.g.:: - drop_index("accounts") + drop_column('organization', 'account_id') - :param name: name of the index. - :param table_name: name of the owning table. Some - backends such as Microsoft SQL Server require this. + :param table_name: name of table + :param column_name: name of column :param schema: Optional schema name to operate within. To control quoting of the schema outside of the default behavior, use the SQLAlchemy construct @@ -1047,51 +1277,62 @@ class Operations(object): .. versionadded:: 0.7.0 'schema' can now accept a :class:`~sqlalchemy.sql.elements.quoted_name` construct. + :param mssql_drop_check: Optional boolean. When ``True``, on + Microsoft SQL Server only, first + drop the CHECK constraint on the column using a + SQL-script-compatible + block that selects into a @variable from sys.check_constraints, + then exec's a separate DROP CONSTRAINT for that constraint. + :param mssql_drop_default: Optional boolean. When ``True``, on + Microsoft SQL Server only, first + drop the DEFAULT constraint on the column using a + SQL-script-compatible + block that selects into a @variable from sys.default_constraints, + then exec's a separate DROP CONSTRAINT for that default. + :param mssql_drop_foreign_key: Optional boolean. When ``True``, on + Microsoft SQL Server only, first + drop a single FOREIGN KEY constraint on the column using a + SQL-script-compatible + block that selects into a @variable from + sys.foreign_keys/sys.foreign_key_columns, + then exec's a separate DROP CONSTRAINT for that default. Only + works if the column has exactly one FK constraint which refers to + it, at the moment. + + .. versionadded:: 0.6.2 + """ - # 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. + op = cls(table_name, column_name, schema=schema, **kw) + return operations.invoke(op) - :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`. + @classmethod + def batch_drop_column(cls, operations, column_name): + """Issue a "drop column" instruction using the current + batch migration context. - .. versionadded:: 0.7.0 'schema' can now accept a - :class:`~sqlalchemy.sql.elements.quoted_name` construct. + .. seealso:: + + :meth:`.Operations.drop_column` """ + op = cls( + operations.impl.table_name, column_name, + schema=operations.impl.schema) + return operations.invoke(op) - 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) +@Operations.register_operation("bulk_insert") +class BulkInsertOp(MigrateOperation): + """Represent a bulk insert operation.""" + + def __init__(self, table, rows, multiinsert=True): + self.table = table + self.rows = rows + self.multiinsert = multiinsert - 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. - - 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:: + op = cls(table, rows, multiinsert=multiinsert) + operations.invoke(op) - :paramref:`.EnvironmentContext.configure.literal_binds` - """ - return impl._literal_bindparam(None, value, type_=type_) +@Operations.register_operation("execute") +class ExecuteSQLOp(MigrateOperation): + """Represent an execute SQL operation.""" + + def __init__(self, sqltext, execution_options=None): + self.sqltext = sqltext + self.execution_options = execution_options - 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) + op = cls(sqltext, execution_options=execution_options) + return operations.invoke(op) - 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. +class OpContainer(MigrateOperation): + """Represent a sequence of operations operation.""" + def __init__(self, ops=()): + self.ops = ops - In a SQL script context, this value is ``None``. [TODO: verify this] - """ - return self.migration_context.impl.bind +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 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. +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. - - .. seealso:: - - :meth:`.Operations.add_column` - - """ - - 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. - - .. seealso:: - - :meth:`.Operations.add_column` - - """ - kw['schema'] = self.impl.schema - return super(BatchOperations, self).alter_column( - self.impl.table_name, column_name, **kw) - - def drop_column(self, column_name): - """Issue a "drop column" instruction using the current - batch migration context. - - .. seealso:: - - :meth:`.Operations.drop_column` - - """ - 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` +class DowngradeOps(OpContainer): + """contains a sequence of operations that would apply to the + 'downgrade' stream of a script. - """ - raise NotImplementedError("not yet implemented") + .. seealso:: - def drop_constraint(self, name, type_=None): - """Issue a "drop constraint" instruction using the - current batch migration context. + :ref:`customizing_revision` - The batch form of this call omits the ``table_name`` and ``schema`` - arguments from the call. + """ - .. seealso:: - :meth:`.Operations.drop_constraint` +class MigrationScript(MigrateOperation): + """represents a migration script. - """ - return super(BatchOperations, self).drop_constraint( - name, self.impl.table_name, type_=type_, - schema=self.impl.schema) + E.g. when autogenerate encounters this object, this corresponds to the + production of an actual script file. - def create_index(self, name, columns, **kw): - """Issue a "create index" instruction using the - current batch migration context.""" + A normal :class:`.MigrationScript` object would contain a single + :class:`.UpgradeOps` and a single :class:`.DowngradeOps` directive. - kw['schema'] = self.impl.schema + .. seealso:: - return super(BatchOperations, self).create_index( - name, self.impl.table_name, columns, **kw) + :ref:`customizing_revision` - def drop_index(self, name, **kw): - """Issue a "drop index" instruction using the - current batch migration context.""" + """ - kw['schema'] = self.impl.schema + 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_index( - name, self.impl.table_name, **kw) diff --git a/alembic/operations/schemaobj.py b/alembic/operations/schemaobj.py new file mode 100644 index 00000000..b590acab --- /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 00000000..13273673 --- /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 00000000..e69de29b 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 860315b5..3b04fea4 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 9b460520..84a3c7fd 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 00000000..cae294f8 --- /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 095a04ba..e30c8b27 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 4eea5145..e9958b1f 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 b3a5acda..6acca216 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 9c53d5db..f8ad447f 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 88df9fc3..90f8bc6d 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 ae25fd27..7e05525d 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 cdfcb88a..b82a404e 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 801d36be..37ae1410 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 2e0f731f..00000000 --- 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 00000000..bd7196ce --- /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 00000000..904848c0 --- /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 00000000..c202e96c --- /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 00000000..c51e1878 --- /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 00000000..871dcb88 --- /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 fea4e147..00000000 --- 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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

pH#+#M4{jgmy4zt$ZwZa4v9i`X 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 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 00000000..048d1e62 --- /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 00000000..8dc594bb --- /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 Hc-jL100001 literal 64697 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 z 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 By@CJ$ diff --git a/docs/build/assets/api_overview.graffle b/docs/build/assets/api_overview.graffle index 7c083e51..1e58ea5f 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,89 +82,64 @@ 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>>} - - Wrap - NO - - - Bounds - {{444, 216.633}, {66, 12}} - Class - ShapedGraphic - FitText - YES - Flow - Resize - ID - 2053 - Shape - Rectangle - Style - - shadow - - Draws - NO - - stroke - - Draws - NO - - - Text - - Align +\f0\fs20 \cf0 <<instantiates>>} + VerticalPad 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 <<proxies>>} Wrap NO Class - LineGraphic - Head - - ID - 2048 - - ID - 2051 - Points + TableGroup + Graphics - {165, 221.6} - {109, 221.6} - - Style - - stroke - HeadArrow - StickArrow - Pattern - 1 - TailArrow - 0 + 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 - - Tail - - ID - 33 - + + GroupConnect + YES + ID + 2131 Class @@ -151,7 +148,7 @@ Bounds - {{19, 207.6}, {90, 14}} + {{230.9169921875, 132.80233001708984}, {102.9071044921875, 14}} Class ShapedGraphic FitText @@ -159,35 +156,46 @@ Flow Resize ID - 2049 + 2130 Shape Rectangle Style 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 MSSQLImpl} + VerticalPad + 0 TextPlacement 0 + + GroupConnect + YES + ID + 2129 + + + Class + TableGroup + Graphics + Bounds - {{19, 221.6}, {90, 14}} + {{226, 82}, {102.9071044921875, 14}} Class ShapedGraphic FitText @@ -195,45 +203,37 @@ Flow Resize ID - 2050 + 2127 Shape Rectangle Style fill - GradientAngle - 304 GradientCenter - {-0.294118, -0.264706} + {-0.29411799999999999, -0.264706} Text - 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\qc -\f0\fs24 \cf0 ConfigParser} +\f0\b\fs24 \cf0 MySQLImpl} + VerticalPad + 0 TextPlacement 0 - GridH - - 2049 - 2050 - - GroupConnect YES ID - 2048 + 2126 Class @@ -241,29 +241,23 @@ Head ID - 33 + 2055 ID - 2046 - OrthogonalBarAutomatic - - OrthogonalBarPosition - 28.725006103515625 + 2135 Points - {385.25, 157} - {304, 191.818} + {280.22809604806071, 146.80233001708984} + {272.46503226582109, 172.16651000976572} Style stroke HeadArrow - StickArrow - LineType - 2 - Pattern - 1 + UMLInheritance + Legacy + TailArrow 0 @@ -271,7 +265,7 @@ Tail ID - 2042 + 2129 @@ -280,29 +274,23 @@ Head ID - 38 + 2055 ID - 2044 - OrthogonalBarAutomatic - - OrthogonalBarPosition - 52.850021362304688 + 2134 Points - {454.25, 177} - {442.638, 294.6} + {243.64926792598939, 121.40116119384763} + {252.32082843664148, 172.16651000976572} Style stroke HeadArrow - StickArrow - LineType - 2 - Pattern - 1 + UMLInheritance + Legacy + TailArrow 0 @@ -310,60 +298,45 @@ Tail ID - 2043 + 2131 - Bounds - {{385.25, 172}, {69, 14}} Class - ShapedGraphic - FitText - YES - Flow - Resize + LineGraphic + Head + + ID + 2055 + ID - 2043 - Magnets + 2133 + Points - {0.5, -0.142857} + {276.4518773872507, 95.999999999999986} + {265.55272336402226, 172.16651000976572} - Shape - Rectangle Style - fill - - Draws - NO - - shadow - - Draws - NO - stroke - Draws - NO + HeadArrow + UMLInheritance + Legacy + + TailArrow + 0 - Text + Tail - 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} + ID + 2126 - Wrap - NO Bounds - {{385.25, 149.6}, {94, 14}} + {{504, 310}, {84, 12}} Class ShapedGraphic FitText @@ -371,20 +344,11 @@ Flow Resize ID - 2042 - Magnets - - {0.49734, 0.0285711} - + 2125 Shape Rectangle Style - fill - - Draws - NO - shadow Draws @@ -398,13 +362,17 @@ Text + 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\qc\pardirnatural +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 -\f0\fs24 \cf0 alembic.context} +\f0\fs20 \cf0 <<instantiates>>} + VerticalPad + 0 Wrap NO @@ -415,14 +383,20 @@ Head ID - 2038 + 33 ID - 2040 + 2124 + OrthogonalBarAutomatic + + OrthogonalBarPoint + {0, 0} + OrthogonalBarPosition + 16 Points - {166.088, 336.6} - {105.686, 336.6} + {563, 340.34042553191489} + {497.13201904296875, 327.88251038766401} Style @@ -430,6 +404,10 @@ HeadArrow StickArrow + Legacy + + LineType + 2 Pattern 1 TailArrow @@ -439,46 +417,78 @@ Tail ID - 41 + 2072 Bounds - {{19, 294.6}, {86.1858, 84}} + {{494.00001409542369, 415.9000186920166}, {55, 12}} Class ShapedGraphic + FitText + YES + Flow + Resize ID - 2038 + 2123 + Line + + ID + 2139 + Position + 0.37128287553787231 + RotationType + 0 + Shape - Cylinder + Rectangle + Style + + shadow + + Draws + NO + + stroke + + Draws + NO + + Text + 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\qc\pardirnatural +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 -\f0\fs24 \cf0 database} +\f0\fs20 \cf0 <<uses>>} + VerticalPad + 0 + Wrap + NO Bounds - {{227.597, 278.569}, {55, 12}} + {{713.35945466160774, 356.11699358749399}, {55, 12}} Class ShapedGraphic FitText YES + Flow + Resize ID - 51 + 2122 Line ID - 50 - Offset - -20 + 2121 Position - 0.40689659118652344 + 0.49189183115959167 RotationType 0 @@ -491,24 +501,1614 @@ Draws NO - stroke + 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 + 0 + + + Tail + + ID + 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 + Graphics + + + Bounds + {{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 + Vertical + Flow + Resize + ID + 2049 + 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.config} + VerticalPad + 0 + + TextPlacement + 0 + - Draws - NO - - - Text - - Align - 0 - Text - {\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf360 -{\fonttbl\f0\fswiss\fcharset0 Helvetica;} + Bounds + {{713.35941696166992, 443.89997863769531}, {119.0880126953125, 42}} + Class + ShapedGraphic + FitText + Vertical + Flow + Resize + ID + 2050 + 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\ql\qnatural +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 -\f0\fs20 \cf0 <<uses>>} - +\f0\fs24 \cf0 Config\ +Command\ +main()} + VerticalPad + 0 + + TextPlacement + 0 + + + GridH + + 2049 + 2050 + + + GroupConnect + YES + ID + 2048 + Magnets + + {0.5, -4.4408920985006262e-16} + {-0.5, -0.25000000000000178} + {-0.1138779104937786, -0.5} + {-0.49999999999999911, 0.33902539955400712} + Class @@ -516,14 +2116,14 @@ Head ID - 41 + 2055 ID - 50 + 2040 Points - {234.897, 263.6} - {235.389, 315.6} + {373, 215.59905413254651} + {311, 214.81620239134219} Style @@ -531,6 +2131,8 @@ HeadArrow StickArrow + Legacy + Pattern 1 TailArrow @@ -540,88 +2142,31 @@ Tail ID - 33 + 41 Bounds - {{308.265, 310.6}, {55, 12}} + {{216.45355606079102, 299.9999877929688}, {86.1858, 84}} Class ShapedGraphic - FitText - YES ID - 49 - Line - - ID - 9 - Offset - -20 - Position - 0.5199354887008667 - RotationType - 0 - + 2038 Shape - Rectangle + Cylinder Style - - shadow - - Draws - NO - - stroke - - Draws - NO - - + Text - 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\pardirnatural\qc -\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 +\f0\fs24 \cf0 database} + VerticalPad + 0 @@ -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 + 0 + ExpandedCanvases + + Frame + {{130, 128}, {1193, 852}} + ListView - DrawerTab - Outline - DrawerWidth - 209 - FitInWindow + OutlineWidth + 142 + RightSidebar - Frame - {{335, 211}, {760, 817}} - ShowRuler + Sidebar - ShowStatusBar - + 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 193c87fc..8fd6293d 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 8c1e0d71..541f595f 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 3270f5ce..6e284193 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 de18f9e0..17ffc066 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 1df9d276..49aaef5c 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 00000000..7ef6cbf7 --- /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 00000000..b1717ab9 --- /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 a089b428..f32fd849 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'] - - 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'] + + 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') + ]).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 90d25c42..525bed58 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 1f92649c..8ee33bcc 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 52f36018..4a49d5c5 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 a498c363..41d1957e 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 db37456d..da0b4131 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 7d5f83e6..9c14e491 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 d73316d8..0a515deb 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 11b80803..c2eef0a2 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 1f380ab4..3ce6200c 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() -- 2.47.2