diff options
author | Mike Bayer <mike_mp@zzzcomputing.com> | 2015-06-25 18:01:25 -0400 |
---|---|---|
committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2015-06-25 18:01:25 -0400 |
commit | 492e4b89d32823a9c156f11eebef23e70440374a (patch) | |
tree | e016cc869bea8819ba8c0f56c2d85bbca6be8baa | |
parent | e22f116d71a2fb0d47ba81c1e01a9b801ce3d877 (diff) | |
download | alembic-492e4b89d32823a9c156f11eebef23e70440374a.tar.gz |
- the new compose /render model. tests need to be updated
completely. One test works, test_render_add_index_schema.
-rw-r--r-- | alembic/autogenerate/__init__.py | 3 | ||||
-rw-r--r-- | alembic/autogenerate/api.py | 9 | ||||
-rw-r--r-- | alembic/autogenerate/compare.py | 10 | ||||
-rw-r--r-- | alembic/autogenerate/compose.py | 137 | ||||
-rw-r--r-- | alembic/autogenerate/generate.py | 21 | ||||
-rw-r--r-- | alembic/autogenerate/render.py | 446 | ||||
-rw-r--r-- | alembic/operations/ops.py | 236 | ||||
-rw-r--r-- | alembic/operations/schemaobj.py | 5 | ||||
-rw-r--r-- | alembic/util/sqla_compat.py | 13 | ||||
-rw-r--r-- | tests/test_autogen_render.py | 4 |
10 files changed, 551 insertions, 333 deletions
diff --git a/alembic/autogenerate/__init__.py b/alembic/autogenerate/__init__.py index 2acd330..c33d2d8 100644 --- a/alembic/autogenerate/__init__.py +++ b/alembic/autogenerate/__init__.py @@ -1,4 +1,5 @@ from .api import ( # noqa - compare_metadata, _produce_net_changes, _render_migration_diffs + compare_metadata, _produce_net_changes ) from .generate import RevisionContext # noqa +from .render import render_op_text # noqa
\ No newline at end of file diff --git a/alembic/autogenerate/api.py b/alembic/autogenerate/api.py index 7f0b089..6c30b13 100644 --- a/alembic/autogenerate/api.py +++ b/alembic/autogenerate/api.py @@ -116,15 +116,6 @@ def compare_metadata(context, metadata): return diffs -def _render_migration_diffs(context, template_args, imports): - - autogen_context = _autogen_context(context, imports) - - diffs = [] - _produce_net_changes(autogen_context, diffs) - compose._render_diffs(diffs, autogen_context, template_args) - - def _autogen_context( context, imports=None, metadata=None, include_symbol=None, include_object=None, include_schemas=False): diff --git a/alembic/autogenerate/compare.py b/alembic/autogenerate/compare.py index 9c04f80..2e3d6e8 100644 --- a/alembic/autogenerate/compare.py +++ b/alembic/autogenerate/compare.py @@ -2,6 +2,7 @@ from sqlalchemy import schema as sa_schema, types as sqltypes from sqlalchemy import event import logging from ..util import compat +from ..util import sqla_compat from sqlalchemy.util import OrderedSet import re from .render import _user_defined_render @@ -250,7 +251,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 +268,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 index 4819390..f0ec4e5 100644 --- a/alembic/autogenerate/compose.py +++ b/alembic/autogenerate/compose.py @@ -1,96 +1,74 @@ import itertools -import re -from mako.pygen import PythonPrinter +from ..operations import ops -from ..util.compat import StringIO -from .render import _drop_table, _drop_column, _drop_index, _drop_constraint, \ - _add_table, _add_column, _add_index, _add_constraint, _modify_col, \ - _add_fk_constraint - - -def _render_diffs(diffs, autogen_context, template_args): - opts = autogen_context['opts'] - imports = autogen_context['imports'] - 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)) - - -def _indent(text): - text = re.compile(r'^', re.M).sub(" ", text).strip() - text = re.compile(r' +$', re.M).sub("", text) - return text - - -def _render_cmd_body(fn, diffs, autogen_context): - - buf = StringIO() - printer = PythonPrinter(buf) - - printer.writeline( - "### commands auto generated by Alembic - " - "please adjust! ###" +def _to_migration_script(autogen_context, migration_script, diffs): + _to_upgrade_op( + autogen_context, + diffs, + migration_script.upgrade_ops, ) - for line in fn(diffs, autogen_context): - printer.writeline(line) + _to_downgrade_op( + autogen_context, + diffs, + migration_script.downgrade_ops, + ) - printer.writeline("### end Alembic commands ###") - return buf.getvalue() +def _to_upgrade_op(autogen_context, diffs, upgrade_ops): + return _to_updown_op(autogen_context, diffs, upgrade_ops, "upgrade") -def _produce_upgrade_commands(diffs, autogen_context): - return _produce_commands("upgrade", diffs, autogen_context) +def _to_downgrade_op(autogen_context, diffs, downgrade_ops): + return _to_updown_op(autogen_context, diffs, downgrade_ops, "downgrade") -def _produce_downgrade_commands(diffs, autogen_context): - return _produce_commands("downgrade", diffs, autogen_context) +def _to_updown_op(autogen_context, diffs, op_container, type_): + if not diffs: + return + if type_ == 'downgrade': + diffs = reversed(diffs) -def _produce_commands(type_, diffs, autogen_context): - opts = autogen_context['opts'] - render_as_batch = opts.get('render_as_batch', False) + dest = [op_container.ops] - 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" + for (schema, table), subdiffs in _group_diffs_by_table(diffs): + if table is not None: + table_ops = [] + op = ops.ModifyTableOps(table.name, table_ops, schema=table.schema) + dest[-1].append(op) + dest.append(ops) + for diff in subdiffs: + _produce_command(autogen_context, diff, dest[-1], type_) + dest.pop(-1) -def _invoke_command(updown, args, autogen_context): - if isinstance(args, tuple): - return _invoke_adddrop_command(updown, args, autogen_context) +def _produce_command(autogen_context, diff, ops, updown): + if isinstance(diff, tuple): + _produce_adddrop_command(updown, diff, autogen_context) else: - return _invoke_modify_command(updown, args, autogen_context) + _produce_modify_command(updown, diff, autogen_context) -def _invoke_adddrop_command(updown, args, autogen_context): - cmd_type = args[0] +def _produce_adddrop_command(updown, diff, autogen_context): + cmd_type = diff[0] adddrop, cmd_type = cmd_type.split("_") - cmd_args = args[1:] + (autogen_context,) + cmd_args = diff[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) + "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] @@ -105,16 +83,16 @@ def _invoke_adddrop_command(updown, args, autogen_context): return cmd_callables[0](*cmd_args) -def _invoke_modify_command(updown, args, autogen_context): - sname, tname, cname = args[0][1:4] +def _produce_modify_command(updown, diffs, autogen_context): + sname, tname, cname = diffs[0][1:4] kw = {} _arg_struct = { - "modify_type": ("existing_type", "type_"), - "modify_nullable": ("existing_nullable", "nullable"), - "modify_default": ("existing_server_default", "server_default"), + "modify_type": ("existing_type", "modify_type"), + "modify_nullable": ("existing_nullable", "modify_nullable"), + "modify_default": ("existing_server_default", "modify_server_default"), } - for diff in args: + for diff in diffs: diff_kw = diff[4] for arg in ("existing_type", "existing_nullable", @@ -133,7 +111,11 @@ def _invoke_modify_command(updown, args, autogen_context): 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) + + return ops.AlterColumnOp( + tname, cname, schema=sname, + **kw + ) def _group_diffs_by_table(diffs): @@ -155,3 +137,4 @@ def _group_diffs_by_table(diffs): return sname, tname return itertools.groupby(diffs, _derive_table) + diff --git a/alembic/autogenerate/generate.py b/alembic/autogenerate/generate.py index a2f1501..095af66 100644 --- a/alembic/autogenerate/generate.py +++ b/alembic/autogenerate/generate.py @@ -1,6 +1,7 @@ from .. import util from . import api from . import compose +from . import render from ..operations import ops @@ -15,9 +16,14 @@ class RevisionContext(object): } def _to_script(self, migration_script): + template_args = {} for k, v in self.template_args.items(): migration_script.template_args.setdefault(k, v) + 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, @@ -26,7 +32,7 @@ class RevisionContext(object): splice=migration_script.splice, branch_labels=migration_script.branch_label, version_path=migration_script.version_path, - **migration_script.template_args) + **template_args) def run_autogenerate(self, rev, context): if self.command_args['sql']: @@ -62,20 +68,19 @@ class RevisionContext(object): diffs = [] api._produce_net_changes(autogen_context, diffs) - self.generated_revisions = [ - self._default_revision() - ] + migration_script = self._default_revision() + migration_script.autogen_context = autogen_context - template_args = {} - compose._render_diffs(diffs, autogen_context, template_args) + compose._to_migration_script(autogen_context, migration_script, diffs) + + self.generated_revisions = [migration_script] - self.generated_revisions[0].template_args = template_args + # DO THE HOOK HERE!! def run_no_autogenerate(self, rev, context): self.generated_revisions = [ self._default_revision() ] - self.generated_revisions[0].template_args = {} def _default_revision(self): return ops.MigrationScript( diff --git a/alembic/autogenerate/render.py b/alembic/autogenerate/render.py index 82f348b..c51a348 100644 --- a/alembic/autogenerate/render.py +++ b/alembic/autogenerate/render.py @@ -1,14 +1,17 @@ from sqlalchemy import schema as sa_schema, types as sqltypes, sql -import logging +from ..operations import ops from ..util import compat from ..ddl.base import _table_for_constraint, _fk_spec import re 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 +# TODO: put this in sqla_compat try: from sqlalchemy.sql.naming import conv @@ -22,73 +25,89 @@ 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. +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" + for op in op_container: + lines = render_op(autogen_context, op) + + for line in lines: + printer.writeline(line) + + printer.writeline("### end Alembic commands ###") + + return buf.getvalue() - return template % { - "prefix": _sqlalchemy_autogenerate_prefix(autogen_context), - "sql": compat.text_type( - value.compile(dialect=autogen_context['dialect'], - **compile_kw) - ) - } +def render_op(autogen_context, op): + renderer = renderers.dispatch(op) + lines = 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("") else: - return repr(value) + return [ + "pass" + ] -def _add_table(table, autogen_context): +@renderers.dispatch_for(ops.CreateTableOp) +def _add_table(autogen_context, op): args = [col for col in - [_render_column(col, autogen_context) for col in table.c] + [_render_column(col, autogen_context) for col in op.columns] if col] + \ sorted([rcons for rcons in [_render_constraint(cons, autogen_context) for cons in - table.constraints] + op.constraints] if rcons is not None ]) @@ -98,45 +117,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 + 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] + return [text] -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 @@ -164,14 +171,11 @@ def _add_index(index, autogen_context): for key, val in index.kwargs.items()])) if len(index.kwargs) else '' } - return text + 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 +186,36 @@ 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 + return [text] -def _render_unique_constraint(constraint, autogen_context): - rendered = _user_defined_render("unique", constraint, autogen_context) - if rendered is not False: - return rendered +@renderers.dispatch_for(ops.CreateUniqueConstraintOp) +def _add_unique_constraint(autogen_context, op): + return _uq_constraint(op.to_constraint(), autogen_context, True) - 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) - } - - -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: + args.append("%s=%r" % (k, op.kw[k])) return "%(prefix)screate_foreign_key(%(args)s)" % { 'prefix': _alembic_autogenerate_prefix(autogen_context), @@ -273,41 +223,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 +243,21 @@ 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 + 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: @@ -342,10 +271,14 @@ def _add_column(schema, tname, column, autogen_context): "column": _render_column(column, autogen_context), "schema": schema } - return text + 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,7 +290,7 @@ 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 @@ -412,6 +345,125 @@ def _modify_col(tname, cname, text += ")" 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 _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 _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'] diff --git a/alembic/operations/ops.py b/alembic/operations/ops.py index 0c3a698..f2e84f1 100644 --- a/alembic/operations/ops.py +++ b/alembic/operations/ops.py @@ -1,5 +1,6 @@ from .. import util - +from ..util import sqla_compat +from . import schemaobj to_impl = util.Dispatcher() @@ -9,39 +10,167 @@ class MigrateOperation(object): class AddConstraintOp(MigrateOperation): - pass + @classmethod + 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) class DropConstraintOp(MigrateOperation): - def __init__(self, name, table_name, type_=None, schema=None): - self.name = name + 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", + } + + constraint_table = sqla_compat._table_for_constraint(constraint) + return DropConstraintOp( + constraint.name, + constraint_table.name, + schema=constraint_table.schema, + type_=types[constraint.__visit_name__] + ) + + +class CreatePrimaryKeyOp(AddConstraintOp): + def __init__( + self, constraint_name, table_name, columns, schema=None, **kw): + self.constraint_name = constraint_name self.table_name = table_name - self.type_ = type_ + self.columns = columns self.schema = schema + self.kw = kw + + @classmethod + def from_constraint(cls, constraint): + constraint_table = sqla_compat._table_for_constraint(constraint) + + return CreatePrimaryKeyOp( + constraint.name, + constraint_table.name, + schema=constraint_table.schema, + *constraint.columns + ) class CreateUniqueConstraintOp(AddConstraintOp): - def __init__(self, name, local_cols, **kw): - self.name = name + def __init__( + self, constraint_name, table_name, columns, schema=None, **kw): + self.constraint_name = constraint_name + self.table_name = table_name + self.columns = columns + self.schema = schema + self.kw = kw + + @classmethod + def from_constraint(cls, constraint): + constraint_table = sqla_compat._table_for_constraint(constraint) + + kw = {} + if constraint.deferrable: + kw['deferrable'] = constraint.deferrable + if constraint.initially: + kw['initially'] = constraint.initially + + return CreateUniqueConstraintOp( + constraint.name, + constraint_table.name, + schema=constraint_table.schema, + *constraint.columns, + **kw + ) + + def to_constraint(self): + schema_obj = schemaobj.SchemaObjects() + return schema_obj.unique_constraint( + self.constraint_name, self.table_name, self.columns, + schema=self.schema, **self.kw) + + +class CreateForeignKeyOp(AddConstraintOp): + 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 CreateForeignKeyOp( + constraint.name, + source_table, + target_table, + source_columns, + target_columns, + **kw + ) + class CreateCheckConstraintOp(AddConstraintOp): def __init__( - self, name, source, condition, schema=None, **kw): - self.name = name - self.source = source + 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 CreateCheckConstraintOp( + constraint.name, + constraint_table.name, + constraint.condition, + schema=constraint_table.schema + ) + class CreateIndexOp(MigrateOperation): def __init__( - self, name, table_name, columns, schema=None, + self, index_name, table_name, columns, schema=None, unique=False, quote=None, **kw): - self.name = name + self.index_name = index_name self.table_name = table_name self.columns = columns self.schema = schema @@ -49,27 +178,61 @@ class CreateIndexOp(MigrateOperation): self.quote = quote self.kw = kw + @classmethod + def from_index(cls, index): + return CreateIndexOp( + index.name, + index.table.name, + sqla_compat._get_index_expressions(index), + schema=index.table.schema, + unique=index.unique, + quote=index.name.quote, + **index.dialect_kwargs + ) + + def to_index(self): + schema_obj = schemaobj.SchemaObjects() + return schema_obj.index( + self.index_name, self.table_name, self.columns, schema=self.schema, + unique=self.unique, quote=self.quote, **self.kw) + class DropIndexOp(MigrateOperation): - def __init__(self, name, table_name=None, schema=None): - self.name = name + def __init__(self, index_name, table_name=None, schema=None): + self.index_name = index_name self.table_name = table_name self.schema = schema class CreateTableOp(MigrateOperation): - def __init__(self, name, *columns, **kw): - self.name = name + def __init__(self, table_name, columns, constraints, schema=None, **kw): + self.table_name = table_name self.columns = columns + self.constraints = constraints + self.schema = schema self.kw = kw + @classmethod + def from_table(cls, table): + return CreateTableOp( + table.name, + list(table.c), + list(table.constraints), + schema=table.schema, + **table.kwargs + ) + class DropTableOp(MigrateOperation): - def __init__(self, name, schema=None, table_kw=None): - self.name = name + 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 DropTableOp(table.name, schema=table.schema) + class AlterTableOp(MigrateOperation): @@ -91,33 +254,46 @@ class AlterColumnOp(AlterTableOp): self, table_name, column_name, schema=None, existing_type=None, existing_server_default=False, - existing_nullable=None + 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 - - modify_nullable = None - modify_server_default = False - modify_name = None - modify_type = None - kw = None + self.modify_nullable = modify_nullable + self.modify_server_default = modify_server_default + self.modify_name = modify_name + self.modify_nullable_type = modify_type + self.kw = kw class AddColumnOp(AlterTableOp): - def __init__(self, name, column, schema=None): - super(AddColumnOp, self).__init__(name, schema=schema) + def __init__(self, table_name, column, schema=None): + super(AddColumnOp, self).__init__(table_name, schema=schema) self.column = column + @classmethod + def from_column_and_tablename(cls, schema, tname, col): + return AddColumnOp(tname, col, schema=schema) + class DropColumnOp(AlterTableOp): - def __init__(self, name, column, schema=None): - super(DropColumnOp, self).__init__(name, schema=schema) - self.column = column + def __init__(self, table_name, column_name, schema=None): + super(DropColumnOp, self).__init__(table_name, schema=schema) + self.column_name = column_name + + @classmethod + def from_column_and_tablename(cls, schema, tname, col): + return DropColumnOp(tname, col.name, schema=schema) class BulkInsertOp(MigrateOperation): diff --git a/alembic/operations/schemaobj.py b/alembic/operations/schemaobj.py index 4e0474e..c99205a 100644 --- a/alembic/operations/schemaobj.py +++ b/alembic/operations/schemaobj.py @@ -6,7 +6,7 @@ from .. import util class SchemaObjects(object): - def __init__(self, migration_context): + def __init__(self, migration_context=None): self.migration_context = migration_context def primary_key_constraint(self, name, table_name, cols, schema=None): @@ -83,7 +83,8 @@ class SchemaObjects(object): def metadata(self): kw = {} - if 'target_metadata' in self.migration_context.opts: + 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 diff --git a/alembic/util/sqla_compat.py b/alembic/util/sqla_compat.py index b6fd0ab..b8cbcc5 100644 --- a/alembic/util/sqla_compat.py +++ b/alembic/util/sqla_compat.py @@ -102,6 +102,8 @@ def _textual_index_column(table, text_): return c elif isinstance(text_, TextClause): return _textual_index_element(table, text_) + elif isinstance(text_, sql.ColumnElement): + return text_ else: raise ValueError("String or text() construct expected") @@ -147,3 +149,14 @@ class _literal_bindparam(_BindParamClause): @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/tests/test_autogen_render.py b/tests/test_autogen_render.py index 32975f0..5bc5788 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,\ @@ -77,8 +78,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')" ) |