summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2015-06-25 18:01:25 -0400
committerMike Bayer <mike_mp@zzzcomputing.com>2015-06-25 18:01:25 -0400
commit492e4b89d32823a9c156f11eebef23e70440374a (patch)
treee016cc869bea8819ba8c0f56c2d85bbca6be8baa
parente22f116d71a2fb0d47ba81c1e01a9b801ce3d877 (diff)
downloadalembic-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__.py3
-rw-r--r--alembic/autogenerate/api.py9
-rw-r--r--alembic/autogenerate/compare.py10
-rw-r--r--alembic/autogenerate/compose.py137
-rw-r--r--alembic/autogenerate/generate.py21
-rw-r--r--alembic/autogenerate/render.py446
-rw-r--r--alembic/operations/ops.py236
-rw-r--r--alembic/operations/schemaobj.py5
-rw-r--r--alembic/util/sqla_compat.py13
-rw-r--r--tests/test_autogen_render.py4
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')"
)