deb-alembic/alembic/autogenerate/render.py

705 lines
22 KiB
Python

from sqlalchemy import schema as sa_schema, types as sqltypes, sql
from ..operations import ops
from ..util import compat
import re
from ..util.compat import string_types
from .. import util
from mako.pygen import PythonPrinter
from ..util.compat import StringIO
MAX_PYTHON_ARGS = 255
try:
from sqlalchemy.sql.naming import conv
def _render_gen_name(autogen_context, name):
if isinstance(name, conv):
return _f_name(_alembic_autogenerate_prefix(autogen_context), name)
else:
return name
except ImportError:
def _render_gen_name(autogen_context, name):
return name
def _indent(text):
text = re.compile(r'^', re.M).sub(" ", text).strip()
text = re.compile(r' +$', re.M).sub("", text)
return text
def _render_migration_script(autogen_context, migration_script, template_args):
opts = autogen_context['opts']
imports = autogen_context['imports']
template_args[opts['upgrade_token']] = _indent(_render_cmd_body(
migration_script.upgrade_ops, autogen_context))
template_args[opts['downgrade_token']] = _indent(_render_cmd_body(
migration_script.downgrade_ops, autogen_context))
template_args['imports'] = "\n".join(sorted(imports))
default_renderers = renderers = util.Dispatcher()
def _render_cmd_body(op_container, autogen_context):
buf = StringIO()
printer = PythonPrinter(buf)
printer.writeline(
"### commands auto generated by Alembic - "
"please adjust! ###"
)
if not op_container.ops:
printer.writeline("pass")
else:
for op in op_container.ops:
lines = render_op(autogen_context, op)
for line in lines:
printer.writeline(line)
printer.writeline("### end Alembic commands ###")
return buf.getvalue()
def render_op(autogen_context, op):
renderer = renderers.dispatch(op)
lines = util.to_list(renderer(autogen_context, op))
return lines
def render_op_text(autogen_context, op):
return "\n".join(render_op(autogen_context, op))
@renderers.dispatch_for(ops.ModifyTableOps)
def _render_modify_table(autogen_context, op):
opts = autogen_context['opts']
render_as_batch = opts.get('render_as_batch', False)
if op.ops:
lines = []
if render_as_batch:
lines.append(
"with op.batch_alter_table(%r, schema=%r) as batch_op:"
% (op.table_name, op.schema)
)
autogen_context['batch_prefix'] = 'batch_op.'
for t_op in op.ops:
t_lines = render_op(autogen_context, t_op)
lines.extend(t_lines)
if render_as_batch:
del autogen_context['batch_prefix']
lines.append("")
return lines
else:
return [
"pass"
]
@renderers.dispatch_for(ops.CreateTableOp)
def _add_table(autogen_context, op):
table = op.to_table()
args = [col for col in
[_render_column(col, autogen_context) for col in table.columns]
if col] + \
sorted([rcons for rcons in
[_render_constraint(cons, autogen_context) for cons in
table.constraints]
if rcons is not None
])
if len(args) > MAX_PYTHON_ARGS:
args = '*[' + ',\n'.join(args) + ']'
else:
args = ',\n'.join(args)
text = "%(prefix)screate_table(%(tablename)r,\n%(args)s" % {
'tablename': _ident(op.table_name),
'prefix': _alembic_autogenerate_prefix(autogen_context),
'args': args,
}
if op.schema:
text += ",\nschema=%r" % _ident(op.schema)
for k in sorted(op.kw):
text += ",\n%s=%r" % (k.replace(" ", "_"), op.kw[k])
text += "\n)"
return text
@renderers.dispatch_for(ops.DropTableOp)
def _drop_table(autogen_context, op):
text = "%(prefix)sdrop_table(%(tname)r" % {
"prefix": _alembic_autogenerate_prefix(autogen_context),
"tname": _ident(op.table_name)
}
if op.schema:
text += ", schema=%r" % _ident(op.schema)
text += ")"
return text
@renderers.dispatch_for(ops.CreateIndexOp)
def _add_index(autogen_context, op):
index = op.to_index()
has_batch = 'batch_prefix' in autogen_context
if has_batch:
tmpl = "%(prefix)screate_index(%(name)r, [%(columns)s], "\
"unique=%(unique)r%(kwargs)s)"
else:
tmpl = "%(prefix)screate_index(%(name)r, %(table)r, [%(columns)s], "\
"unique=%(unique)r%(schema)s%(kwargs)s)"
text = tmpl % {
'prefix': _alembic_autogenerate_prefix(autogen_context),
'name': _render_gen_name(autogen_context, index.name),
'table': _ident(index.table.name),
'columns': ", ".join(
_get_index_rendered_expressions(index, autogen_context)),
'unique': index.unique or False,
'schema': (", schema=%r" % _ident(index.table.schema))
if index.table.schema else '',
'kwargs': (
', ' +
', '.join(
["%s=%s" %
(key, _render_potential_expr(val, autogen_context))
for key, val in index.kwargs.items()]))
if len(index.kwargs) else ''
}
return text
@renderers.dispatch_for(ops.DropIndexOp)
def _drop_index(autogen_context, op):
has_batch = 'batch_prefix' in autogen_context
if has_batch:
tmpl = "%(prefix)sdrop_index(%(name)r)"
else:
tmpl = "%(prefix)sdrop_index(%(name)r, "\
"table_name=%(table_name)r%(schema)s)"
text = tmpl % {
'prefix': _alembic_autogenerate_prefix(autogen_context),
'name': _render_gen_name(autogen_context, op.index_name),
'table_name': _ident(op.table_name),
'schema': ((", schema=%r" % _ident(op.schema))
if op.schema else '')
}
return text
@renderers.dispatch_for(ops.CreateUniqueConstraintOp)
def _add_unique_constraint(autogen_context, op):
return [_uq_constraint(op.to_constraint(), autogen_context, True)]
@renderers.dispatch_for(ops.CreateForeignKeyOp)
def _add_fk_constraint(autogen_context, op):
args = [
repr(
_render_gen_name(autogen_context, op.constraint_name)),
repr(_ident(op.source_table)),
repr(_ident(op.referent_table)),
repr([_ident(col) for col in op.local_cols]),
repr([_ident(col) for col in op.remote_cols])
]
for k in (
'source_schema', 'referent_schema',
'onupdate', 'ondelete', 'initially', 'deferrable', 'use_alter'
):
if k in op.kw:
value = op.kw[k]
if value is not None:
args.append("%s=%r" % (k, value))
return "%(prefix)screate_foreign_key(%(args)s)" % {
'prefix': _alembic_autogenerate_prefix(autogen_context),
'args': ", ".join(args)
}
@renderers.dispatch_for(ops.CreatePrimaryKeyOp)
def _add_pk_constraint(constraint, autogen_context):
raise NotImplementedError()
@renderers.dispatch_for(ops.CreateCheckConstraintOp)
def _add_check_constraint(constraint, autogen_context):
raise NotImplementedError()
@renderers.dispatch_for(ops.DropConstraintOp)
def _drop_constraint(autogen_context, op):
if 'batch_prefix' in autogen_context:
template = "%(prefix)sdrop_constraint"\
"(%(name)r, type_=%(type)r)"
else:
template = "%(prefix)sdrop_constraint"\
"(%(name)r, '%(table_name)s'%(schema)s, type_=%(type)r)"
text = template % {
'prefix': _alembic_autogenerate_prefix(autogen_context),
'name': _render_gen_name(
autogen_context, op.constraint_name),
'table_name': _ident(op.table_name),
'type': op.constraint_type,
'schema': (", schema='%s'" % _ident(op.schema))
if op.schema else '',
}
return text
@renderers.dispatch_for(ops.AddColumnOp)
def _add_column(autogen_context, op):
schema, tname, column = op.schema, op.table_name, op.column
if 'batch_prefix' in autogen_context:
template = "%(prefix)sadd_column(%(column)s)"
else:
template = "%(prefix)sadd_column(%(tname)r, %(column)s"
if schema:
template += ", schema=%(schema)r"
template += ")"
text = template % {
"prefix": _alembic_autogenerate_prefix(autogen_context),
"tname": tname,
"column": _render_column(column, autogen_context),
"schema": schema
}
return text
@renderers.dispatch_for(ops.DropColumnOp)
def _drop_column(autogen_context, op):
schema, tname, column_name = op.schema, op.table_name, op.column_name
if 'batch_prefix' in autogen_context:
template = "%(prefix)sdrop_column(%(cname)r)"
else:
template = "%(prefix)sdrop_column(%(tname)r, %(cname)r"
if schema:
template += ", schema=%(schema)r"
template += ")"
text = template % {
"prefix": _alembic_autogenerate_prefix(autogen_context),
"tname": _ident(tname),
"cname": _ident(column_name),
"schema": _ident(schema)
}
return text
@renderers.dispatch_for(ops.AlterColumnOp)
def _alter_column(autogen_context, op):
tname = op.table_name
cname = op.column_name
server_default = op.modify_server_default
type_ = op.modify_type
nullable = op.modify_nullable
existing_type = op.existing_type
existing_nullable = op.existing_nullable
existing_server_default = op.existing_server_default
schema = op.schema
indent = " " * 11
if 'batch_prefix' in autogen_context:
template = "%(prefix)salter_column(%(cname)r"
else:
template = "%(prefix)salter_column(%(tname)r, %(cname)r"
text = template % {
'prefix': _alembic_autogenerate_prefix(
autogen_context),
'tname': tname,
'cname': cname}
text += ",\n%sexisting_type=%s" % (
indent,
_repr_type(existing_type, autogen_context))
if server_default is not False:
rendered = _render_server_default(
server_default, autogen_context)
text += ",\n%sserver_default=%s" % (indent, rendered)
if type_ is not None:
text += ",\n%stype_=%s" % (indent,
_repr_type(type_, autogen_context))
if nullable is not None:
text += ",\n%snullable=%r" % (
indent, nullable,)
if existing_nullable is not None:
text += ",\n%sexisting_nullable=%r" % (
indent, existing_nullable)
if existing_server_default:
rendered = _render_server_default(
existing_server_default,
autogen_context)
text += ",\n%sexisting_server_default=%s" % (
indent, rendered)
if schema and "batch_prefix" not in autogen_context:
text += ",\n%sschema=%r" % (indent, schema)
text += ")"
return text
class _f_name(object):
def __init__(self, prefix, name):
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:
return "%s." % target.__module__
else:
return prefix
def _sqlalchemy_autogenerate_prefix(autogen_context):
return autogen_context['opts']['sqlalchemy_module_prefix'] or ''
def _alembic_autogenerate_prefix(autogen_context):
if 'batch_prefix' in autogen_context:
return autogen_context['batch_prefix']
else:
return autogen_context['opts']['alembic_module_prefix'] or ''
def _user_defined_render(type_, object_, autogen_context):
if 'opts' in autogen_context and \
'render_item' in autogen_context['opts']:
render = autogen_context['opts']['render_item']
if render:
rendered = render(type_, object_, autogen_context)
if rendered is not False:
return rendered
return False
def _render_column(column, autogen_context):
rendered = _user_defined_render("column", column, autogen_context)
if rendered is not False:
return rendered
opts = []
if column.server_default:
rendered = _render_server_default(
column.server_default, autogen_context
)
if rendered:
opts.append(("server_default", rendered))
if not column.autoincrement:
opts.append(("autoincrement", column.autoincrement))
if column.nullable is not None:
opts.append(("nullable", column.nullable))
# TODO: for non-ascii colname, assign a "key"
return "%(prefix)sColumn(%(name)r, %(type)s, %(kw)s)" % {
'prefix': _sqlalchemy_autogenerate_prefix(autogen_context),
'name': _ident(column.name),
'type': _repr_type(column.type, autogen_context),
'kw': ", ".join(["%s=%s" % (kwname, val) for kwname, val in opts])
}
def _render_server_default(default, autogen_context, repr_=True):
rendered = _user_defined_render("server_default", default, autogen_context)
if rendered is not False:
return rendered
if isinstance(default, sa_schema.DefaultClause):
if isinstance(default.arg, compat.string_types):
default = default.arg
else:
return _render_potential_expr(default.arg, autogen_context)
if isinstance(default, string_types) and repr_:
default = repr(re.sub(r"^'|'$", "", default))
return default
def _repr_type(type_, autogen_context):
rendered = _user_defined_render("type", type_, autogen_context)
if rendered is not False:
return rendered
mod = type(type_).__module__
imports = autogen_context.get('imports', None)
if mod.startswith("sqlalchemy.dialects"):
dname = re.match(r"sqlalchemy\.dialects\.(\w+)", mod).group(1)
if imports is not None:
imports.add("from sqlalchemy.dialects import %s" % dname)
return "%s.%r" % (dname, type_)
elif mod.startswith("sqlalchemy."):
prefix = _sqlalchemy_autogenerate_prefix(autogen_context)
return "%s%r" % (prefix, type_)
else:
prefix = _user_autogenerate_prefix(autogen_context, type_)
return "%s%r" % (prefix, type_)
_constraint_renderers = util.Dispatcher()
def _render_constraint(constraint, autogen_context):
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:
return rendered
if not constraint.columns:
return None
opts = []
if constraint.name:
opts.append(("name", repr(
_render_gen_name(autogen_context, constraint.name))))
return "%(prefix)sPrimaryKeyConstraint(%(args)s)" % {
"prefix": _sqlalchemy_autogenerate_prefix(autogen_context),
"args": ", ".join(
[repr(c.key) for c in constraint.columns] +
["%s=%s" % (kwname, val) for kwname, val in opts]
),
}
def _fk_colspec(fk, metadata_schema):
"""Implement a 'safe' version of ForeignKey._get_colspec() that
never tries to resolve the remote table.
"""
colspec = fk._get_colspec()
tokens = colspec.split(".")
tname, colname = tokens[-2:]
if metadata_schema is not None and len(tokens) == 2:
table_fullname = "%s.%s" % (metadata_schema, tname)
else:
table_fullname = ".".join(tokens[0:-1])
if fk.parent is not None and fk.parent.table is not None:
# 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)
colspec = "%s.%s" % (table_fullname, colname)
return colspec
def _populate_render_fk_opts(constraint, opts):
if constraint.onupdate:
opts.append(("onupdate", repr(constraint.onupdate)))
if constraint.ondelete:
opts.append(("ondelete", repr(constraint.ondelete)))
if constraint.initially:
opts.append(("initially", repr(constraint.initially)))
if constraint.deferrable:
opts.append(("deferrable", repr(constraint.deferrable)))
if constraint.use_alter:
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:
return rendered
opts = []
if constraint.name:
opts.append(("name", repr(
_render_gen_name(autogen_context, constraint.name))))
_populate_render_fk_opts(constraint, opts)
apply_metadata_schema = constraint.parent.metadata.schema
return "%(prefix)sForeignKeyConstraint([%(cols)s], "\
"[%(refcols)s], %(args)s)" % {
"prefix": _sqlalchemy_autogenerate_prefix(autogen_context),
"cols": ", ".join(
"%r" % _ident(f.parent.name) for f in constraint.elements),
"refcols": ", ".join(repr(_fk_colspec(f, apply_metadata_schema))
for f in constraint.elements),
"args": ", ".join(
["%s=%s" % (kwname, val) for kwname, val in opts]
),
}
@_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:
return rendered
# detect the constraint being part of
# a parent type which is probably in the Table already.
# ideally SQLAlchemy would give us more of a first class
# way to detect this.
if constraint._create_rule and \
hasattr(constraint._create_rule, 'target') and \
isinstance(constraint._create_rule.target,
sqltypes.TypeEngine):
return None
opts = []
if constraint.name:
opts.append(
(
"name",
repr(
_render_gen_name(
autogen_context, constraint.name))
)
)
return "%(prefix)sCheckConstraint(%(sqltext)s%(opts)s)" % {
"prefix": _sqlalchemy_autogenerate_prefix(autogen_context),
"opts": ", " + (", ".join("%s=%s" % (k, v)
for k, v in opts)) if opts else "",
"sqltext": _render_potential_expr(
constraint.sqltext, autogen_context, wrap_in_text=False)
}
renderers = default_renderers.branch()