- The "multiple heads / branches" feature has now landed. This is

by far the most significant change Alembic has seen since its inception;
while the workflow of most commands hasn't changed, and the format
of version files and the ``alembic_version`` table are unchanged as well,
a new suite of features opens up in the case where multiple version
files refer to the same parent, or to the "base".  Merging of
branches, operating across distinct named heads, and multiple
independent bases are now all supported.   The feature incurs radical
changes to the internals of versioning and traversal, and should be
treated as "beta mode" for the next several subsequent releases
within 0.7.
fixes #167
This commit is contained in:
Mike Bayer 2014-11-20 18:08:02 -05:00
parent 0b63884a2c
commit 5c747a068b
24 changed files with 4090 additions and 700 deletions

View File

@ -18,7 +18,7 @@ def list_templates(config):
config.print_stdout("%s - %s", tempname, synopsis)
config.print_stdout("\nTemplates are used via the 'init' command, e.g.:")
config.print_stdout("\n alembic init --template pylons ./scripts")
config.print_stdout("\n alembic init --template generic ./scripts")
def init(config, directory, template='generic'):
@ -64,7 +64,9 @@ def init(config, directory, template='generic'):
"settings in %r before proceeding." % config_file)
def revision(config, message=None, autogenerate=False, sql=False):
def revision(
config, message=None, autogenerate=False, sql=False,
head="head", splice=False, branch_label=None):
"""Create a new revision file."""
script = ScriptDirectory.from_config(config)
@ -82,7 +84,8 @@ def revision(config, message=None, autogenerate=False, sql=False):
environment = True
def retrieve_migrations(rev, context):
if script.get_revision(rev) is not script.get_revision("head"):
if set(script.get_revisions(rev)) != \
set(script.get_revisions("heads")):
raise util.CommandError("Target database is not up to date.")
autogen._produce_migration_diffs(context, template_args, imports)
return []
@ -99,7 +102,31 @@ def revision(config, message=None, autogenerate=False, sql=False):
template_args=template_args,
):
script.run_env()
return script.generate_revision(util.rev_id(), message, refresh=True,
return script.generate_revision(
util.rev_id(), message, refresh=True,
head=head, splice=splice, branch_labels=branch_label,
**template_args)
def merge(config, revisions, message=None, branch_label=None):
"""Merge two revisions together. Creates a new migration file.
.. versionadded:: 0.7.0
.. seealso::
:ref:`branches`
"""
script = ScriptDirectory.from_config(config)
template_args = {
'config': config # Let templates use config for
# e.g. multiple databases
}
return script.generate_revision(
util.rev_id(), message, refresh=True,
head=revisions, branch_labels=branch_label,
**template_args)
@ -157,7 +184,28 @@ def downgrade(config, revision, sql=False, tag=None):
script.run_env()
def history(config, rev_range=None):
def show(config, rev):
"""Show the revision(s) denoted by the given symbol."""
script = ScriptDirectory.from_config(config)
if rev == "current":
def show_current(rev, context):
for sc in script.get_revisions(rev):
config.print_stdout(sc.log_entry)
return []
with EnvironmentContext(
config,
script,
fn=show_current
):
script.run_env()
else:
for sc in script.get_revisions(rev):
config.print_stdout(sc.log_entry)
def history(config, rev_range=None, verbose=False):
"""List changeset scripts in chronological order."""
script = ScriptDirectory.from_config(config)
@ -173,10 +221,11 @@ def history(config, rev_range=None):
def _display_history(config, script, base, head):
for sc in script.walk_revisions(
base=base or "base",
head=head or "head"):
if sc.is_head:
config.print_stdout("")
config.print_stdout(sc.log_entry)
head=head or "heads"):
config.print_stdout(
sc.cmd_format(
verbose=verbose, include_branches=True,
include_doc=True, include_parents=True))
def _display_history_w_current(config, script, base=None, head=None):
def _display_current_history(rev, context):
@ -201,37 +250,51 @@ def history(config, rev_range=None):
_display_history(config, script, base, head)
def branches(config):
"""Show current un-spliced branch points"""
def heads(config, verbose=False):
"""Show current available heads in the script directory"""
script = ScriptDirectory.from_config(config)
for rev in script.get_revisions("heads"):
config.print_stdout(
rev.cmd_format(
verbose, include_branches=True, tree_indicators=False))
def branches(config, verbose=False):
"""Show current branch points"""
script = ScriptDirectory.from_config(config)
for sc in script.walk_revisions():
if sc.is_branch_point:
config.print_stdout(sc)
for rev in sc.nextrev:
config.print_stdout("%s -> %s",
" " * len(str(sc.down_revision)),
script.get_revision(rev)
config.print_stdout(
"%s\n%s\n",
sc.cmd_format(verbose, include_branches=True),
"\n".join(
"%s -> %s" % (
" " * len(str(sc.revision)),
rev_obj.cmd_format(
False, include_branches=True, include_doc=verbose)
) for rev_obj in
(script.get_revision(rev) for rev in sc.nextrev)
)
)
def current(config, head_only=False):
"""Display the current revision for each database."""
def current(config, verbose=False, head_only=False):
"""Display the current revision for a database."""
script = ScriptDirectory.from_config(config)
def display_version(rev, context):
rev = script.get_revision(rev)
if head_only:
config.print_stdout("%s%s" % (
rev.revision if rev else None,
" (head)" if rev and rev.is_head else ""))
util.warn("--head-only is deprecated")
else:
config.print_stdout("Current revision for %s: %s",
util.obfuscate_url_pw(
context.connection.engine.url),
rev)
def display_version(rev, context):
if verbose:
config.print_stdout(
"Current revision(s) for %s:",
util.obfuscate_url_pw(context.connection.engine.url)
)
for rev in script.get_revisions(rev):
config.print_stdout(rev.cmd_format(verbose))
return []
with EnvironmentContext(
@ -248,31 +311,27 @@ def stamp(config, revision, sql=False, tag=None):
script = ScriptDirectory.from_config(config)
starting_rev = None
if ":" in revision:
if not sql:
raise util.CommandError("Range revision not allowed")
starting_rev, revision = revision.split(':', 2)
starting_rev = script.get_revision(starting_rev)
if starting_rev is not None:
starting_rev = starting_rev.revision
def do_stamp(rev, context):
if sql:
current = False
else:
current = context._current_rev()
dest = script.get_revision(revision)
if dest is not None:
dest = dest.revision
context._update_current_rev(current, dest)
return []
return script._stamp_revs(revision, rev)
with EnvironmentContext(
config,
script,
fn=do_stamp,
as_sql=sql,
destination_rev=revision,
starting_rev=starting_rev,
tag=tag
):
script.run_env()
def splice(config, parent, child):
"""'splice' two branches, creating a new revision file.
this command isn't implemented right now.
"""
raise NotImplementedError()

View File

@ -8,6 +8,7 @@ if sys.version_info < (2, 6):
sqla_08 = sa_version >= '0.8.0'
sqla_09 = sa_version >= '0.9.0'
py27 = sys.version_info >= (2, 7)
py2k = sys.version_info < (3, 0)
py3k = sys.version_info >= (3, 0)
py33 = sys.version_info >= (3, 3)
@ -128,6 +129,31 @@ else:
reraise(type(exception), exception, tb=exc_tb)
if py3k:
def reraise(tp, value, tb=None, cause=None):
if cause is not None:
value.__cause__ = cause
if value.__traceback__ is not tb:
raise value.with_traceback(tb)
raise value
def raise_from_cause(exception, exc_info=None):
if exc_info is None:
exc_info = sys.exc_info()
exc_type, exc_value, exc_tb = exc_info
reraise(type(exception), exception, tb=exc_tb, cause=exc_value)
else:
exec("def reraise(tp, value, tb=None, cause=None):\n"
" raise tp, value, tb\n")
def raise_from_cause(exception, exc_info=None):
# not as nice as that of Py3K, but at least preserves
# the code line where the issue occurred
if exc_info is None:
exc_info = sys.exc_info()
exc_type, exc_value, exc_tb = exc_info
reraise(type(exception), exception, tb=exc_tb)
# produce a wrapper that allows encoded text to stream
# into a given buffer, but doesn't close it.
# not sure of a more idiomatic approach to this.

View File

@ -203,54 +203,107 @@ class CommandLine(object):
def _generate_args(self, prog):
def add_options(parser, positional, kwargs):
if 'template' in kwargs:
parser.add_argument("-t", "--template",
kwargs_opts = {
'template': (
"-t", "--template",
dict(
default='generic',
type=str,
help="Setup template for use with 'init'")
if 'message' in kwargs:
parser.add_argument(
help="Setup template for use with 'init'"
)
),
'message': (
"-m", "--message",
dict(
type=str,
help="Message string to use with 'revision'")
if 'sql' in kwargs:
parser.add_argument(
),
'sql': (
"--sql",
dict(
action="store_true",
help="Don't emit SQL to database - dump to "
"standard output/file instead")
if 'tag' in kwargs:
parser.add_argument(
"standard output/file instead"
)
),
'tag': (
"--tag",
dict(
type=str,
help="Arbitrary 'tag' name - can be used by "
"custom env.py scripts.")
if 'autogenerate' in kwargs:
parser.add_argument(
),
'head': (
"--head",
dict(
type=str,
help="Specify head revision or <branchname>@head "
"to base new revision on."
)
),
'splice': (
"--splice",
dict(
action="store_true",
help="Allow a non-head revision as the "
"'head' to splice onto"
)
),
'branch_label': (
"--branch-label",
dict(
type=str,
help="Specify a branch label to apply to the "
"new revision"
)
),
'verbose': (
"-v", "--verbose",
dict(
action="store_true",
help="Use more verbose output"
)
),
'autogenerate': (
"--autogenerate",
dict(
action="store_true",
help="Populate revision script with candidate "
"migration operations, based on comparison "
"of database to model.")
# "current" command
if 'head_only' in kwargs:
parser.add_argument(
),
'head_only': (
"--head-only",
dict(
action="store_true",
help="Only show current version and "
"whether or not this is the head revision.")
if 'rev_range' in kwargs:
parser.add_argument("-r", "--rev-range",
help="Deprecated. Use --verbose for "
"additional output")
),
'rev_range': (
"-r", "--rev-range",
dict(
action="store",
help="Specify a revision range; "
"format is [start]:[end]")
)
}
positional_help = {
'directory': "location of scripts directory",
'revision': "revision identifier"
'revision': "revision identifier",
'revisions': "one or more revisions, or 'heads' for all heads"
}
for arg in kwargs:
if arg in kwargs_opts:
args = kwargs_opts[arg]
args, kw = args[0:-1], args[-1]
parser.add_argument(*args, **kw)
for arg in positional:
if arg == "revisions":
subparser.add_argument(
arg, nargs='+', help=positional_help.get(arg))
else:
subparser.add_argument(arg, help=positional_help.get(arg))
parser = ArgumentParser(prog=prog)
@ -267,7 +320,8 @@ class CommandLine(object):
help="Additional arguments consumed by "
"custom env.py scripts, e.g. -x "
"setting1=somesetting -x setting2=somesetting")
parser.add_argument("--raiseerr", action="store_true",
help="Raise a full stack trace on error")
subparsers = parser.add_subparsers()
for fn in [getattr(command, n) for n in dir(command)]:
@ -299,6 +353,9 @@ class CommandLine(object):
**dict((k, getattr(options, k)) for k in kwarg)
)
except util.CommandError as e:
if options.raiseerr:
raise
else:
util.err(str(e))
def main(self, argv=None):

View File

@ -104,7 +104,7 @@ class DefaultImpl(with_metaclass(ImplMeta)):
conn = self.connection
if execution_options:
conn = conn.execution_options(**execution_options)
conn.execute(construct, *multiparams, **params)
return conn.execute(construct, *multiparams, **params)
def execute(self, sql, execution_options=None):
self._exec(sql, execution_options)

View File

@ -136,13 +136,33 @@ class EnvironmentContext(object):
return not self.is_offline_mode()
def get_head_revision(self):
"""Return the hex identifier of the 'head' revision.
"""Return the hex identifier of the 'head' script revision.
If the script directory has multiple heads, this
method raises a :class:`.CommandError`;
:meth:`.EnvironmentContext.get_head_revisions` should be preferred.
This function does not require that the :class:`.MigrationContext`
has been configured.
.. seealso:: :meth:`.EnvironmentContext.get_head_revisions`
"""
return self.script._as_rev_number("head")
return self.script.as_revision_number("head")
def get_head_revisions(self):
"""Return the hex identifier of the 'heads' script revision(s).
This returns a tuple containing the version number of all
heads in the script directory.
This function does not require that the :class:`.MigrationContext`
has been configured.
.. versionadded:: 0.7.0
"""
return self.script.as_revision_number("heads")
def get_starting_revision_argument(self):
"""Return the 'starting revision' argument,
@ -157,12 +177,16 @@ class EnvironmentContext(object):
"""
if self._migration_context is not None:
return self.script._as_rev_number(
return self.script.as_revision_number(
self.get_context()._start_from_rev)
elif 'starting_rev' in self.context_opts:
return self.script._as_rev_number(
return self.script.as_revision_number(
self.context_opts['starting_rev'])
else:
# this should raise only in the case that a command
# is being run where the "starting rev" is never applicable;
# this is to catch scripts which rely upon this in
# non-sql mode or similar
raise util.CommandError(
"No starting revision argument is available.")
@ -180,7 +204,7 @@ class EnvironmentContext(object):
has been configured.
"""
return self.script._as_rev_number(
return self.script.as_revision_number(
self.context_opts['destination_rev'])
def get_tag_argument(self):
@ -342,8 +366,6 @@ class EnvironmentContext(object):
:param output_encoding: when using ``--sql`` to generate SQL
scripts, apply this encoding to the string output.
.. versionadded:: 0.5.0
:param starting_rev: Override the "starting revision" argument
when using ``--sql`` mode.
:param tag: a string tag for usage by custom ``env.py`` scripts.
@ -355,15 +377,11 @@ class EnvironmentContext(object):
option is used, or if the option "revision_environment=true"
is present in the alembic.ini file.
.. versionadded:: 0.3.3
:param version_table: The name of the Alembic version table.
The default is ``'alembic_version'``.
:param version_table_schema: Optional schema to place version
table within.
.. versionadded:: 0.5.0
Parameters specific to the autogenerate feature, when
``alembic revision`` is run with the ``--autogenerate`` feature:
@ -558,8 +576,6 @@ class EnvironmentContext(object):
option to specify a callable which
can filter the tables/schemas that get included.
.. versionadded :: 0.4.0
.. seealso::
:paramref:`.EnvironmentContext.configure.include_object`
@ -589,8 +605,6 @@ class EnvironmentContext(object):
``"primary_key"``, ``"foreign_key"``, ``"unique"``, ``"check"``,
``"type"``, ``"server_default"``.
.. versionadded:: 0.5.0
.. seealso::
:ref:`autogen_render_types`

View File

@ -2,7 +2,6 @@ import logging
import sys
from contextlib import contextmanager
from sqlalchemy import MetaData, Table, Column, String, literal_column
from sqlalchemy import create_engine
from sqlalchemy.engine import url as sqla_url
@ -187,37 +186,83 @@ class MigrationContext(object):
"""Return the current revision, usually that which is present
in the ``alembic_version`` table in the database.
This method intends to be used only for a migration stream that
does not contain unmerged branches in the target database;
if there are multiple branches present, an exception is raised.
The :meth:`.MigrationContext.get_current_heads` should be preferred
over this method going forward in order to be compatible with
branch migration support.
If this :class:`.MigrationContext` was configured in "offline"
mode, that is with ``as_sql=True``, the ``starting_rev``
parameter is returned instead, if any.
"""
heads = self.get_current_heads()
if len(heads) == 0:
return None
elif len(heads) > 1:
raise util.CommandError(
"Version table '%s' has more than one head present; "
"please use get_current_heads()" % self.version_table)
else:
return heads[0]
def get_current_heads(self):
"""Return a tuple of the current 'head versions' that are represented
in the target database.
For a migration stream without branches, this will be a single
value, synonymous with that of
:meth:`.MigrationContext.get_current_revision`. However when multiple
unmerged branches exist within the target database, the returned tuple
will contain a value for each head.
If this :class:`.MigrationContext` was configured in "offline"
mode, that is with ``as_sql=True``, the ``starting_rev``
parameter is returned in a one-length tuple.
If no version table is present, or if there are no revisions
present, an empty tuple is returned.
.. versionadded:: 0.7.0
"""
if self.as_sql:
return self._start_from_rev
return util.to_tuple(self._start_from_rev, default=())
else:
if self._start_from_rev:
raise util.CommandError(
"Can't specify current_rev to context "
"when using a database connection")
if not self._has_version_table():
return ()
return tuple(
row[0] for row in self.connection.execute(self._version.select())
)
def _ensure_version_table(self):
self._version.create(self.connection, checkfirst=True)
return self.connection.scalar(self._version.select())
_current_rev = get_current_revision
"""The 0.2 method name, for backwards compat."""
def _has_version_table(self):
return self.connection.dialect.has_table(
self.connection, self.version_table, self.version_table_schema)
def _update_current_rev(self, old, new):
if old == new:
return
if new is None:
self.impl._exec(self._version.delete())
elif old is None:
self.impl._exec(self._version.insert().
values(version_num=literal_column("'%s'" % new))
)
else:
self.impl._exec(self._version.update().
values(version_num=literal_column("'%s'" % new))
)
def stamp(self, script_directory, revision):
"""Stamp the version table with a specific revision.
This method calculates those branches to which the given revision
can apply, and updates those branches as though they were migrated
towards that revision (either up or down). If no current branches
include the revision, it is added as a new branch head.
.. versionadded:: 0.7.0
"""
heads = self.get_current_heads()
head_maintainer = HeadMaintainer(self, heads)
for step in script_directory._steps_revs(revision, heads):
head_maintainer.update_to_step(step)
def run_migrations(self, **kw):
"""Run the migration scripts established for this
@ -240,41 +285,33 @@ class MigrationContext(object):
method within revision scripts.
"""
current_rev = rev = False
stamp_per_migration = not self.impl.transactional_ddl or \
self._transaction_per_migration
self.impl.start_migrations()
for change, prev_rev, rev, doc in self._migrations_fn(
self.get_current_revision(),
self):
heads = self.get_current_heads()
if not self.as_sql and not heads:
self._ensure_version_table()
head_maintainer = HeadMaintainer(self, heads)
for step in self._migrations_fn(heads, self):
with self.begin_transaction(_per_migration=True):
if current_rev is False:
current_rev = prev_rev
if self.as_sql and not current_rev:
if self.as_sql and not head_maintainer.heads:
# for offline mode, include a CREATE TABLE from
# the base
self._version.create(self.connection)
if doc:
log.info(
"Running %s %s -> %s, %s", change.__name__, prev_rev,
rev, doc)
else:
log.info(
"Running %s %s -> %s", change.__name__, prev_rev, rev)
log.info("Running %s", step)
if self.as_sql:
self.impl.static_output(
"-- Running %s %s -> %s" %
(change.__name__, prev_rev, rev)
)
change(**kw)
if stamp_per_migration:
self._update_current_rev(prev_rev, rev)
prev_rev = rev
self.impl.static_output("-- Running %s" % (step.short_log,))
step.migration_fn(**kw)
if rev is not False:
if not stamp_per_migration:
self._update_current_rev(current_rev, rev)
# previously, we wouldn't stamp per migration
# if we were in a transaction, however given the more
# complex model that involves any number of inserts
# and row-targeted updates and deletes, it's simpler for now
# just to run the operations on every version
head_maintainer.update_to_step(step)
if self.as_sql and not rev:
if self.as_sql and not head_maintainer.heads:
self._version.drop(self.connection)
def execute(self, sql, execution_options=None):
@ -372,3 +409,339 @@ class MigrationContext(object):
metadata_column,
rendered_metadata_default,
rendered_column_default)
class HeadMaintainer(object):
def __init__(self, context, heads):
self.context = context
self.heads = set(heads)
def _insert_version(self, version):
assert version not in self.heads
self.heads.add(version)
self.context.impl._exec(
self.context._version.insert().
values(
version_num=literal_column("'%s'" % version)
)
)
def _delete_version(self, version):
self.heads.remove(version)
ret = self.context.impl._exec(
self.context._version.delete().where(
self.context._version.c.version_num ==
literal_column("'%s'" % version)))
if not self.context.as_sql and ret.rowcount != 1:
raise util.CommandError(
"Online migration expected to match one "
"row when deleting '%s' in '%s'; "
"%d found"
% (version,
self.context.version_table, ret.rowcount))
def _update_version(self, from_, to_):
assert to_ not in self.heads
self.heads.remove(from_)
self.heads.add(to_)
ret = self.context.impl._exec(
self.context._version.update().
values(version_num=literal_column("'%s'" % to_)).where(
self.context._version.c.version_num
== literal_column("'%s'" % from_))
)
if not self.context.as_sql and ret.rowcount != 1:
raise util.CommandError(
"Online migration expected to match one "
"row when updating '%s' to '%s' in '%s'; "
"%d found"
% (from_, to_, self.context.version_table, ret.rowcount))
def update_to_step(self, step):
if step.should_delete_branch(self.heads):
vers = step.delete_version_num
log.debug("branch delete %s", vers)
self._delete_version(vers)
elif step.should_create_branch(self.heads):
vers = step.insert_version_num
log.debug("new branch insert %s", vers)
self._insert_version(vers)
elif step.should_merge_branches(self.heads):
# delete revs, update from rev, update to rev
(delete_revs, update_from_rev,
update_to_rev) = step.merge_branch_idents
log.debug(
"merge, delete %s, update %s to %s",
delete_revs, update_from_rev, update_to_rev)
for delrev in delete_revs:
self._delete_version(delrev)
self._update_version(update_from_rev, update_to_rev)
elif step.should_unmerge_branches(self.heads):
(update_from_rev, update_to_rev,
insert_revs) = step.unmerge_branch_idents
log.debug(
"unmerge, insert %s, update %s to %s",
insert_revs, update_from_rev, update_to_rev)
for insrev in insert_revs:
self._insert_version(insrev)
self._update_version(update_from_rev, update_to_rev)
else:
from_, to_ = step.update_version_num
log.debug("update %s to %s", from_, to_)
self._update_version(from_, to_)
class MigrationStep(object):
@property
def name(self):
return self.migration_fn.__name__
@classmethod
def upgrade_from_script(cls, revision_map, script):
return RevisionStep(revision_map, script, True)
@classmethod
def downgrade_from_script(cls, revision_map, script):
return RevisionStep(revision_map, script, False)
@property
def is_downgrade(self):
return not self.is_upgrade
@property
def merge_branch_idents(self):
return (
# delete revs, update from rev, update to rev
list(self.from_revisions[0:-1]), self.from_revisions[-1],
self.to_revisions[0]
)
@property
def unmerge_branch_idents(self):
return (
# update from rev, update to rev, insert revs
self.from_revisions[0], self.to_revisions[-1],
list(self.to_revisions[0:-1])
)
@property
def short_log(self):
return "%s %s -> %s" % (
self.name,
util.format_as_comma(self.from_revisions),
util.format_as_comma(self.to_revisions)
)
def __str__(self):
if self.doc:
return "%s %s -> %s, %s" % (
self.name,
util.format_as_comma(self.from_revisions),
util.format_as_comma(self.to_revisions),
self.doc
)
else:
return self.short_log
class RevisionStep(MigrationStep):
def __init__(self, revision_map, revision, is_upgrade):
self.revision_map = revision_map
self.revision = revision
self.is_upgrade = is_upgrade
if is_upgrade:
self.migration_fn = revision.module.upgrade
else:
self.migration_fn = revision.module.downgrade
def __eq__(self, other):
return isinstance(other, RevisionStep) and \
other.revision == self.revision and \
self.is_upgrade == other.is_upgrade
@property
def doc(self):
return self.revision.doc
@property
def from_revisions(self):
if self.is_upgrade:
return self.revision._down_revision_tuple
else:
return (self.revision.revision, )
@property
def to_revisions(self):
if self.is_upgrade:
return (self.revision.revision, )
else:
return self.revision._down_revision_tuple
@property
def _has_scalar_down_revision(self):
return len(self.revision._down_revision_tuple) == 1
def should_delete_branch(self, heads):
if not self.is_downgrade:
return False
if self.revision.revision not in heads:
return False
downrevs = self.revision._down_revision_tuple
if not downrevs:
# is a base
return True
elif len(downrevs) == 1:
downrev = self.revision_map.get_revision(downrevs[0])
if not downrev.is_branch_point:
return False
descendants = set(
r.revision for r in self.revision_map._get_descendant_nodes(
self.revision_map.get_revisions(downrev.nextrev),
check=False
)
)
# the downrev is a branchpoint, and other members or descendants
# of the branch are still in heads; so delete this branch.
# the reason this occurs is because traversal tries to stay
# fully on one branch down to the branchpoint before starting
# the other; so if we have a->b->(c1->d1->e1, c2->d2->e2),
# on a downgrade from the top we may go e1, d1, c1, now heads
# are at c1 and e2, with the current method, we don't know that
# "e2" is important unless we get all descendants of c1/c2
if len(descendants.intersection(heads).difference(
[self.revision.revision])):
# TODO: this doesn't work; make sure tests are here to ensure
# this fails
#if len(downrev.nextrev.intersection(heads).difference(
# [self.revision.revision])):
return True
else:
return False
else:
# is a merge point
return False
def should_create_branch(self, heads):
if not self.is_upgrade:
return False
downrevs = self.revision._down_revision_tuple
if not downrevs:
# is a base
return True
elif len(downrevs) == 1:
if downrevs[0] in heads:
return False
else:
return True
else:
# is a merge point
return False
def should_merge_branches(self, heads):
if not self.is_upgrade:
return False
downrevs = self.revision._down_revision_tuple
if len(downrevs) > 1 and \
len(heads.intersection(downrevs)) > 1:
return True
return False
def should_unmerge_branches(self, heads):
if not self.is_downgrade:
return False
downrevs = self.revision._down_revision_tuple
if self.revision.revision in heads and len(downrevs) > 1:
return True
return False
@property
def update_version_num(self):
assert self._has_scalar_down_revision
if self.is_upgrade:
return self.revision.down_revision, self.revision.revision
else:
return self.revision.revision, self.revision.down_revision
@property
def delete_version_num(self):
return self.revision.revision
@property
def insert_version_num(self):
return self.revision.revision
class StampStep(MigrationStep):
def __init__(self, from_, to_, is_upgrade, branch_move):
self.from_ = util.to_tuple(from_, default=())
self.to_ = util.to_tuple(to_, default=())
self.is_upgrade = is_upgrade
self.branch_move = branch_move
self.migration_fn = self.stamp_revision
doc = None
def stamp_revision(self, **kw):
return None
def __eq__(self, other):
return isinstance(other, StampStep) and \
other.from_revisions == self.revisions and \
other.to_revisions == self.to_revisions and \
other.branch_move == self.branch_move and \
self.is_upgrade == other.is_upgrade
@property
def from_revisions(self):
return self.from_
@property
def to_revisions(self):
return self.to_
@property
def delete_version_num(self):
assert len(self.from_) == 1
return self.from_[0]
@property
def insert_version_num(self):
assert len(self.to_) == 1
return self.to_[0]
@property
def update_version_num(self):
assert len(self.from_) == 1
assert len(self.to_) == 1
return self.from_[0], self.to_[0]
def should_delete_branch(self, heads):
return self.is_downgrade and self.branch_move
def should_create_branch(self, heads):
return self.is_upgrade and self.branch_move
def should_merge_branches(self, heads):
return len(self.from_) > 1
def should_unmerge_branches(self, heads):
return len(self.to_) > 1

View File

@ -296,8 +296,6 @@ class Operations(object):
the SQLAlchemy construct
:class:`~sqlalchemy.sql.elements.quoted_name`.
.. versionadded:: 0.4.0 support for 'schema'
.. versionadded:: 0.7.0 'schema' can now accept a
:class:`~sqlalchemy.sql.elements.quoted_name` construct.
@ -359,12 +357,6 @@ class Operations(object):
Set to ``None`` to have the default removed.
:param new_column_name: Optional; specify a string name here to
indicate the new name within a column rename operation.
.. versionchanged:: 0.5.0
The ``name`` parameter is now named ``new_column_name``.
The old name will continue to function for backwards
compatibility.
:param ``type_``: Optional; a :class:`~sqlalchemy.types.TypeEngine`
type object to specify a change to the column's type.
For SQLAlchemy types that also indicate a constraint (i.e.
@ -398,8 +390,6 @@ class Operations(object):
the SQLAlchemy construct
:class:`~sqlalchemy.sql.elements.quoted_name`.
.. versionadded:: 0.4.0 support for 'schema'
.. versionadded:: 0.7.0 'schema' can now accept a
:class:`~sqlalchemy.sql.elements.quoted_name` construct.
@ -543,8 +533,6 @@ class Operations(object):
the SQLAlchemy construct
:class:`~sqlalchemy.sql.elements.quoted_name`.
.. versionadded:: 0.4.0 support for 'schema'
.. versionadded:: 0.7.0 'schema' can now accept a
:class:`~sqlalchemy.sql.elements.quoted_name` construct.
@ -578,8 +566,6 @@ class Operations(object):
the SQLAlchemy construct
:class:`~sqlalchemy.sql.elements.quoted_name`.
.. versionadded:: 0.4.0 support for 'schema'
.. versionadded:: 0.7.0 'schema' can now accept a
:class:`~sqlalchemy.sql.elements.quoted_name` construct.
@ -636,8 +622,6 @@ class Operations(object):
off normally. The :class:`~sqlalchemy.schema.AddConstraint`
construct is ultimately used to generate the ALTER statement.
.. versionadded:: 0.5.0
:param name: Name of the primary key constraint. The name is necessary
so that an ALTER statement can be emitted. For setups that
use an automated naming scheme such as that described at
@ -653,8 +637,6 @@ class Operations(object):
the SQLAlchemy construct
:class:`~sqlalchemy.sql.elements.quoted_name`.
.. versionadded:: 0.4.0 support for 'schema'
.. versionadded:: 0.7.0 'schema' can now accept a
:class:`~sqlalchemy.sql.elements.quoted_name` construct.
@ -764,8 +746,6 @@ class Operations(object):
the SQLAlchemy construct
:class:`~sqlalchemy.sql.elements.quoted_name`.
.. versionadded:: 0.4.0 support for 'schema'
.. versionadded:: 0.7.0 'schema' can now accept a
:class:`~sqlalchemy.sql.elements.quoted_name` construct.
@ -817,8 +797,6 @@ class Operations(object):
the SQLAlchemy construct
:class:`~sqlalchemy.sql.elements.quoted_name`.
.. versionadded:: 0.4.0 support for 'schema'
.. versionadded:: 0.7.0 'schema' can now accept a
:class:`~sqlalchemy.sql.elements.quoted_name` construct.
@ -901,8 +879,6 @@ class Operations(object):
the SQLAlchemy construct
:class:`~sqlalchemy.sql.elements.quoted_name`.
.. versionadded:: 0.4.0 support for 'schema'
.. versionadded:: 0.7.0 'schema' can now accept a
:class:`~sqlalchemy.sql.elements.quoted_name` construct.
:param \**kw: Other keyword arguments are passed to the underlying
@ -934,13 +910,9 @@ class Operations(object):
the SQLAlchemy construct
:class:`~sqlalchemy.sql.elements.quoted_name`.
.. versionadded:: 0.4.0 support for 'schema'
.. versionadded:: 0.7.0 'schema' can now accept a
:class:`~sqlalchemy.sql.elements.quoted_name` construct.
.. versionadded:: 0.4.0
:param \**kw: Other keyword arguments are passed to the underlying
:class:`sqlalchemy.schema.Table` object created for the command.
@ -974,12 +946,6 @@ class Operations(object):
:param name: name of the index.
:param table_name: name of the owning table.
.. versionchanged:: 0.5.0
The ``tablename`` parameter is now named ``table_name``.
As this is a positional argument, the old name is no
longer present.
:param columns: a list consisting of string column names and/or
:func:`~sqlalchemy.sql.expression.text` constructs.
:param schema: Optional schema name to operate within. To control
@ -987,8 +953,6 @@ class Operations(object):
the SQLAlchemy construct
:class:`~sqlalchemy.sql.elements.quoted_name`.
.. versionadded:: 0.4.0 support for 'schema'
.. versionadded:: 0.7.0 'schema' can now accept a
:class:`~sqlalchemy.sql.elements.quoted_name` construct.
@ -1026,19 +990,11 @@ class Operations(object):
:param name: name of the index.
:param table_name: name of the owning table. Some
backends such as Microsoft SQL Server require this.
.. versionchanged:: 0.5.0
The ``tablename`` parameter is now named ``table_name``.
The old name will continue to function for backwards
compatibility.
:param schema: Optional schema name to operate within. To control
quoting of the schema outside of the default behavior, use
the SQLAlchemy construct
:class:`~sqlalchemy.sql.elements.quoted_name`.
.. versionadded:: 0.4.0 support for 'schema'
.. versionadded:: 0.7.0 'schema' can now accept a
:class:`~sqlalchemy.sql.elements.quoted_name` construct.
@ -1055,29 +1011,13 @@ class Operations(object):
:param name: name of the constraint.
:param table_name: table name.
.. versionchanged:: 0.5.0
The ``tablename`` parameter is now named ``table_name``.
As this is a positional argument, the old name is no
longer present.
:param ``type_``: optional, required on MySQL. can be
'foreignkey', 'primary', 'unique', or 'check'.
.. versionchanged:: 0.5.0
The ``type`` parameter is now named ``type_``. The old name
``type`` will remain for backwards compatibility.
.. versionadded:: 0.3.6 'primary' qualfier to enable
dropping of MySQL primary key constraints.
:param schema: Optional schema name to operate within. To control
quoting of the schema outside of the default behavior, use
the SQLAlchemy construct
:class:`~sqlalchemy.sql.elements.quoted_name`.
.. versionadded:: 0.4.0 support for 'schema'
.. versionadded:: 0.7.0 'schema' can now accept a
:class:`~sqlalchemy.sql.elements.quoted_name` construct.

646
alembic/revision.py Normal file
View File

@ -0,0 +1,646 @@
import re
import collections
from . import util
from sqlalchemy import util as sqlautil
from . import compat
_relative_destination = re.compile(r'(?:(.+?)@)?((?:\+|-)\d+)')
class RevisionError(Exception):
pass
class RangeNotAncestorError(RevisionError):
def __init__(self, lower, upper):
self.lower = lower
self.upper = upper
super(RangeNotAncestorError, self).__init__(
"Revision %s is not an ancestor of revision %s" %
(lower or "base", upper or "base")
)
class MultipleHeads(RevisionError):
def __init__(self, heads, argument):
self.heads = heads
self.argument = argument
super(MultipleHeads, self).__init__(
"Multiple heads are present for given argument '%s'; "
"%s" % (argument, ", ".join(heads))
)
class ResolutionError(RevisionError):
pass
class RevisionMap(object):
"""Maintains a map of :class:`.Revision` objects.
:class:`.RevisionMap` is used by :class:`.ScriptDirectory` to maintain
and traverse the collection of :class:`.Script` objects, which are
themselves instances of :class:`.Revision`.
"""
def __init__(self, generator):
"""Construct a new :class:`.RevisionMap`.
:param generator: a zero-arg callable that will generate an iterable
of :class:`.Revision` instances to be used. These are typically
:class:`.Script` subclasses within regular Alembic use.
"""
self._generator = generator
@util.memoized_property
def heads(self):
"""All "head" revisions as strings.
This is normally a tuple of length one,
unless unmerged branches are present.
:return: a tuple of string revision numbers.
"""
self._revision_map
return self.heads
@util.memoized_property
def bases(self):
"""All "base" revisions as strings.
These are revisions that have a ``down_revision`` of None,
or empty tuple.
:return: a tuple of string revision numbers.
"""
self._revision_map
return self.bases
@util.memoized_property
def _revision_map(self):
"""memoized attribute, initializes the revision map from the
initial collection.
"""
map_ = {}
heads = sqlautil.OrderedSet()
self.bases = ()
has_branch_labels = set()
for revision in self._generator():
if revision.revision in map_:
util.warn("Revision %s is present more than once" %
revision.revision)
map_[revision.revision] = revision
if revision.branch_labels:
has_branch_labels.add(revision)
heads.add(revision.revision)
if revision.is_base:
self.bases += (revision.revision, )
for rev in map_.values():
for downrev in rev._down_revision_tuple:
if downrev not in map_:
util.warn("Revision %s referenced from %s is not present"
% (rev.down_revision, rev))
down_revision = map_[downrev]
down_revision.add_nextrev(rev.revision)
heads.discard(downrev)
map_[None] = map_[()] = None
self.heads = tuple(heads)
for revision in has_branch_labels:
self._add_branches(revision, map_)
return map_
def _add_branches(self, revision, map_):
if revision.branch_labels:
for branch_label in revision._orig_branch_labels:
if branch_label in map_:
raise RevisionError(
"Branch name '%s' in revision %s already "
"used by revision %s" %
(branch_label, revision.revision,
map_[branch_label].revision)
)
map_[branch_label] = revision
revision.branch_labels.update(revision.branch_labels)
for node in self._get_descendant_nodes([revision], map_):
node.branch_labels.update(revision.branch_labels)
parent = node
while parent and \
not parent.is_branch_point and not parent.is_merge_point:
parent.branch_labels.update(revision.branch_labels)
if parent.down_revision:
parent = map_[parent.down_revision]
else:
break
def add_revision(self, revision, _replace=False):
"""add a single revision to an existing map.
This method is for single-revision use cases, it's not
appropriate for fully populating an entire revision map.
"""
map_ = self._revision_map
if not _replace and revision.revision in map_:
util.warn("Revision %s is present more than once" %
revision.revision)
elif _replace and revision.revision not in map_:
raise Exception("revision %s not in map" % revision.revision)
map_[revision.revision] = revision
self._add_branches(revision, map_)
if revision.is_base:
self.bases += (revision.revision, )
for downrev in revision._down_revision_tuple:
if downrev not in map_:
util.warn(
"Revision %s referenced from %s is not present"
% (revision.down_revision, revision)
)
map_[downrev].add_nextrev(revision.revision)
if revision.is_head:
self.heads = tuple(
head for head in self.heads
if head not in
set(revision._down_revision_tuple).union([revision.revision])
) + (revision.revision,)
def get_current_head(self, branch_label=None):
"""Return the current head revision.
If the script directory has multiple heads
due to branching, an error is raised;
:meth:`.ScriptDirectory.get_heads` should be
preferred.
:param branch_label: optional branch name which will limit the
heads considered to those which include that branch_label.
:return: a string revision number.
.. seealso::
:meth:`.ScriptDirectory.get_heads`
"""
current_heads = self.heads
if branch_label:
current_heads = self.filter_for_lineage(current_heads, branch_label)
if len(current_heads) > 1:
raise MultipleHeads(
current_heads,
"%s@head" % branch_label if branch_label else "head")
if current_heads:
return current_heads[0]
else:
return None
def _get_base_revisions(self, identifier):
return self.filter_for_lineage(self.bases, identifier)
def get_revisions(self, id_):
"""Return the :class:`.Revision` instances with the given rev id
or identifiers.
May be given a single identifier, a sequence of identifiers, or the
special symbols "head" or "base". The result is a tuple of one
or more identifiers.
Supports partial identifiers, where the given identifier
is matched against all identifiers that start with the given
characters; if there is exactly one match, that determines the
full revision.
"""
if isinstance(id_, (list, tuple, set, frozenset)):
return sum([self.get_revisions(id_elem) for id_elem in id_], ())
else:
resolved_id, branch_label = self._resolve_revision_number(id_)
return tuple(
self._revision_for_ident(rev_id, branch_label)
for rev_id in resolved_id)
def get_revision(self, id_):
"""Return the :class:`.Revision` instance with the given rev id.
If a symbolic name such as "head" or "base" is given, resolves
the identifier into the current head or base revision. If the symbolic
name refers to multiples, :class:`.MultipleHeads` is raised.
Supports partial identifiers, where the given identifier
is matched against all identifiers that start with the given
characters; if there is exactly one match, that determines the
full revision.
"""
resolved_id, branch_label = self._resolve_revision_number(id_)
if len(resolved_id) > 1:
raise MultipleHeads(resolved_id, id_)
elif resolved_id:
resolved_id = resolved_id[0]
return self._revision_for_ident(resolved_id, branch_label)
def _resolve_branch(self, branch_label):
try:
branch_rev = self._revision_map[branch_label]
except KeyError:
try:
nonbranch_rev = self._revision_for_ident(branch_label)
except ResolutionError:
raise ResolutionError("No such branch: '%s'" % branch_label)
else:
return nonbranch_rev
else:
return branch_rev
def _revision_for_ident(self, resolved_id, check_branch=None):
if check_branch:
branch_rev = self._resolve_branch(check_branch)
else:
branch_rev = None
try:
revision = self._revision_map[resolved_id]
except KeyError:
# do a partial lookup
revs = [x for x in self._revision_map
if x and x.startswith(resolved_id)]
if branch_rev:
revs = self.filter_for_lineage(revs, check_branch)
if not revs:
raise ResolutionError(
"No such revision or branch '%s'" % resolved_id)
elif len(revs) > 1:
raise ResolutionError(
"Multiple revisions start "
"with '%s': %s..." % (
resolved_id,
", ".join("'%s'" % r for r in revs[0:3])
))
else:
revision = self._revision_map[revs[0]]
if check_branch and revision is not None:
if not self._shares_lineage(
revision.revision, branch_rev.revision):
raise ResolutionError(
"Revision %s is not a member of branch '%s'" %
(revision.revision, check_branch))
return revision
def filter_for_lineage(self, targets, check_against):
id_, branch_label = self._resolve_revision_number(check_against)
shares = []
if branch_label:
shares.append(branch_label)
if id_:
shares.append(id_[0])
#shares = branch_label or (id_[0] if id_ else None)
return [
tg for tg in targets
if self._shares_lineage(tg, shares)]
def _shares_lineage(self, target, test_against_revs):
if not test_against_revs:
return True
if not isinstance(target, Revision):
target = self._revision_for_ident(target)
test_against_revs = [
self._revision_for_ident(test_against_rev)
if not isinstance(test_against_rev, Revision)
else test_against_rev
for test_against_rev
in util.to_tuple(test_against_revs, default=())
]
return bool(
set(self._get_descendant_nodes([target]))
.union(self._get_ancestor_nodes([target]))
.intersection(test_against_revs)
)
def _resolve_revision_number(self, id_):
if isinstance(id_, compat.string_types) and "@" in id_:
branch_label, id_ = id_.split('@', 1)
else:
branch_label = None
# ensure map is loaded
self._revision_map
if id_ == 'heads':
if branch_label:
return self.filter_for_lineage(
self.heads, branch_label), branch_label
else:
return self.heads, branch_label
elif id_ == 'head':
return (self.get_current_head(branch_label), ), branch_label
elif id_ == 'base' or id_ is None:
return (), branch_label
else:
return util.to_tuple(id_, default=None), branch_label
def iterate_revisions(
self, upper, lower, implicit_base=False, inclusive=False,
assert_relative_length=True):
"""Iterate through script revisions, starting at the given
upper revision identifier and ending at the lower.
The traversal uses strictly the `down_revision`
marker inside each migration script, so
it is a requirement that upper >= lower,
else you'll get nothing back.
The iterator yields :class:`.Revision` objects.
"""
if isinstance(upper, compat.string_types) and \
_relative_destination.match(upper):
reldelta = 1 if inclusive else 0
match = _relative_destination.match(upper)
relative = int(match.group(2))
branch_label = match.group(1)
if branch_label:
from_ = "%s@head" % branch_label
else:
from_ = "head"
revs = list(
self._iterate_revisions(
from_, lower,
inclusive=inclusive, implicit_base=implicit_base))
revs = revs[-relative - reldelta:]
if assert_relative_length and \
len(revs) != abs(relative) + reldelta:
raise RevisionError(
"Relative revision %s didn't "
"produce %d migrations" % (upper, abs(relative)))
return iter(revs)
elif isinstance(lower, compat.string_types) and \
_relative_destination.match(lower):
reldelta = 1 if inclusive else 0
match = _relative_destination.match(lower)
relative = int(match.group(2))
branch_label = match.group(1)
if branch_label:
to_ = "%s@base" % branch_label
else:
to_ = "base"
revs = list(
self._iterate_revisions(
upper, to_,
inclusive=inclusive, implicit_base=implicit_base))
revs = revs[0:-relative + reldelta]
if assert_relative_length and \
len(revs) != abs(relative) + reldelta:
raise RevisionError(
"Relative revision %s didn't "
"produce %d migrations" % (lower, abs(relative)))
return iter(revs)
else:
return self._iterate_revisions(
upper, lower, inclusive=inclusive, implicit_base=implicit_base)
def _get_descendant_nodes(self, targets, map_=None, check=False):
return self._iterate_related_revisions(
lambda rev: rev.nextrev,
targets, map_=map_, check=check
)
def _get_ancestor_nodes(self, targets, map_=None, check=False):
return self._iterate_related_revisions(
lambda rev: rev._down_revision_tuple,
targets, map_=map_, check=check
)
def _iterate_related_revisions(self, fn, targets, map_, check=False):
if map_ is None:
map_ = self._revision_map
todo = collections.deque()
for target in targets:
todo.append(target)
if check:
per_target = set()
while todo:
rev = todo.pop()
todo.extend(
map_[rev_id] for rev_id in fn(rev))
if check:
per_target.add(rev)
yield rev
if check and per_target.intersection(targets).difference([target]):
raise RevisionError(
"Requested revision %s overlaps with "
"other requested revisions" % target.revision)
def _iterate_revisions(
self, upper, lower, inclusive=True, implicit_base=False):
"""iterate revisions from upper to lower.
The traversal is depth-first within branches, and breadth-first
across branches as a whole.
"""
requested_lowers = self.get_revisions(lower)
# some complexity to accommodate an iteration where some
# branches are starting from nothing, and others are starting
# from a given point. Additionally, if the bottom branch
# is specified using a branch identifier, then we limit operations
# to just that branch.
limit_to_lower_branch = \
isinstance(lower, compat.string_types) and lower.endswith('@base')
uppers = self.get_revisions(upper)
upper_ancestors = set(self._get_ancestor_nodes(uppers, check=True))
if limit_to_lower_branch:
lowers = self.get_revisions(self._get_base_revisions(lower))
elif implicit_base and requested_lowers:
lower_ancestors = set(
self._get_ancestor_nodes(requested_lowers)
)
lower_descendants = set(
self._get_descendant_nodes(requested_lowers)
)
base_lowers = set()
candidate_lowers = upper_ancestors.\
difference(lower_ancestors).\
difference(lower_descendants)
for rev in candidate_lowers:
for downrev in rev._down_revision_tuple:
if self._revision_map[downrev] in candidate_lowers:
break
else:
base_lowers.add(rev)
lowers = base_lowers.union(requested_lowers)
elif implicit_base:
base_lowers = set(self.get_revisions(self.bases))
lowers = base_lowers.union(requested_lowers)
elif not requested_lowers:
lowers = set(self.get_revisions(self.bases))
else:
lowers = requested_lowers
# represents all nodes we will produce
total_space = set(
rev.revision for rev in upper_ancestors).intersection(
rev.revision for rev
in self._get_descendant_nodes(lowers, check=True)
)
if not total_space:
raise RangeNotAncestorError(lower, upper)
# organize branch points to be consumed separately from
# member nodes
branch_todo = set(
rev for rev in
(self._revision_map[rev] for rev in total_space)
if rev.is_branch_point and
len(total_space.intersection(rev.nextrev)) > 1
)
# it's not possible for any "uppers" to be in branch_todo,
# because the .nextrev of those nodes is not in total_space
#assert not branch_todo.intersection(uppers)
todo = collections.deque(
r for r in uppers if r.revision in total_space)
# iterate for total_space being emptied out
while total_space:
# when everything non-branch pending is consumed,
# add to the todo any branch nodes that have no
# descendants left in the queue
if not todo:
todo.extendleft(
rev for rev in branch_todo
if not rev.nextrev.intersection(total_space)
)
branch_todo.difference_update(todo)
# iterate nodes that are in the immediate todo
while todo:
rev = todo.popleft()
total_space.remove(rev.revision)
# do depth first for elements within branches,
# don't consume any actual branch nodes
todo.extendleft([
self._revision_map[downrev]
for downrev in reversed(rev._down_revision_tuple)
if self._revision_map[downrev] not in branch_todo
and downrev in total_space])
if not inclusive and rev in requested_lowers:
continue
yield rev
assert not branch_todo
class Revision(object):
"""Base class for revisioned objects.
The :class:`.Revision` class is the base of the more public-facing
:class:`.Script` object, which represents a migration script.
The mechanics of revision management and traversal are encapsulated
within :class:`.Revision`, while :class:`.Script` applies this logic
to Python files in a version directory.
"""
nextrev = frozenset()
revision = None
"""The string revision number."""
down_revision = None
"""The ``down_revision`` identifier(s) within the migration script."""
branch_labels = None
"""Optional string/tuple of symbolic names to apply to this
revision's branch"""
def __init__(self, revision, down_revision, branch_labels=None):
self.revision = revision
self.down_revision = tuple_rev_as_scalar(down_revision)
self._orig_branch_labels = util.to_tuple(branch_labels, default=())
self.branch_labels = set(self._orig_branch_labels)
def add_nextrev(self, rev):
self.nextrev = self.nextrev.union([rev])
@property
def _down_revision_tuple(self):
return util.to_tuple(self.down_revision, default=())
@property
def is_head(self):
"""Return True if this :class:`.Revision` is a 'head' revision.
This is determined based on whether any other :class:`.Script`
within the :class:`.ScriptDirectory` refers to this
:class:`.Script`. Multiple heads can be present.
"""
return not bool(self.nextrev)
@property
def is_base(self):
"""Return True if this :class:`.Revision` is a 'base' revision."""
return self.down_revision is None
@property
def is_branch_point(self):
"""Return True if this :class:`.Script` is a branch point.
A branchpoint is defined as a :class:`.Script` which is referred
to by more than one succeeding :class:`.Script`, that is more
than one :class:`.Script` has a `down_revision` identifier pointing
here.
"""
return len(self.nextrev) > 1
@property
def is_merge_point(self):
"""Return True if this :class:`.Script` is a merge point."""
return len(self._down_revision_tuple) > 1
def tuple_rev_as_scalar(rev):
if not rev:
return None
elif len(rev) == 1:
return rev[0]
else:
return rev

View File

@ -3,6 +3,11 @@ import os
import re
import shutil
from . import util
from . import compat
from . import revision
from . import migration
from contextlib import contextmanager
_sourceless_rev_file = re.compile(r'(?!__init__)(.*\.py)(c|o)?$')
_only_source_rev_file = re.compile(r'(?!__init__)(.*\.py)$')
@ -10,7 +15,6 @@ _legacy_rev = re.compile(r'([a-f0-9]+)\.py$')
_mod_def_re = re.compile(r'(upgrade|downgrade)_([a-z0-9]+)')
_slug_re = re.compile(r'\w+')
_default_file_template = "%(rev)s_%(slug)s"
_relative_destination = re.compile(r'(?:\+|-)\d+')
class ScriptDirectory(object):
@ -43,12 +47,20 @@ class ScriptDirectory(object):
self.truncate_slug_length = truncate_slug_length or 40
self.sourceless = sourceless
self.output_encoding = output_encoding
self.revision_map = revision.RevisionMap(self._load_revisions)
if not os.access(dir, os.F_OK):
raise util.CommandError("Path doesn't exist: %r. Please use "
"the 'init' command to create a new "
"scripts folder." % dir)
def _load_revisions(self):
for file_ in os.listdir(self.versions):
script = Script._from_filename(self, self.versions, file_)
if script is None:
continue
yield script
@classmethod
def from_config(cls, config):
"""Produce a new :class:`.ScriptDirectory` given a :class:`.Config`
@ -75,65 +87,94 @@ class ScriptDirectory(object):
output_encoding=config.get_main_option("output_encoding", "utf-8")
)
def walk_revisions(self, base="base", head="head"):
@contextmanager
def _catch_revision_errors(
self,
ancestor=None, multiple_heads=None, start=None, end=None):
try:
yield
except revision.RangeNotAncestorError as rna:
if start is None:
start = rna.lower
if end is None:
end = rna.upper
if not ancestor:
ancestor = (
"Requested range %(start)s:%(end)s does not refer to "
"ancestor/descendant revisions along the same branch"
)
ancestor = ancestor % {"start": start, "end": end}
compat.raise_from_cause(util.CommandError(ancestor))
except revision.MultipleHeads as mh:
if not multiple_heads:
multiple_heads = (
"Multiple head revisions are present for given "
"argument '%(head_arg)s'; please "
"specify a specific target revision, "
"'<branchname>@%(head_arg)s' to "
"narrow to a specific head, or 'heads' for all heads")
multiple_heads = multiple_heads % {
"head_arg": end or mh.argument,
"heads": util.format_as_comma(mh.heads)
}
compat.raise_from_cause(util.CommandError(multiple_heads))
except revision.RevisionError as err:
compat.raise_from_cause(util.CommandError(err.args[0]))
def walk_revisions(self, base="base", head="heads"):
"""Iterate through all revisions.
This is actually a breadth-first tree traversal,
with leaf nodes being heads.
:param base: the base revision, or "base" to start from the
empty revision.
:param head: the head revision; defaults to "heads" to indicate
all head revisions. May also be "head" to indicate a single
head revision.
.. versionchanged:: 0.7.0 the "head" identifier now refers to
the head of a non-branched repository only; use "heads" to
refer to the set of all head branches simultaneously.
"""
if head == "head":
heads = set(self.get_heads())
else:
heads = set([head])
while heads:
todo = set(heads)
heads = set()
for head in todo:
if head in heads:
break
for sc in self.iterate_revisions(head, base):
if sc.is_branch_point and sc.revision not in todo:
heads.add(sc.revision)
break
else:
yield sc
with self._catch_revision_errors(start=base, end=head):
for rev in self.revision_map.iterate_revisions(
head, base, inclusive=True, assert_relative_length=False):
yield rev
def get_revisions(self, id_):
"""Return the :class:`.Script` instance with the given rev identifier,
symbolic name, or sequence of identifiers.
.. versionadded:: 0.7.0
"""
with self._catch_revision_errors():
return self.revision_map.get_revisions(id_)
def get_revision(self, id_):
"""Return the :class:`.Script` instance with the given rev id."""
"""Return the :class:`.Script` instance with the given rev id.
id_ = self.as_revision_number(id_)
try:
return self._revision_map[id_]
except KeyError:
# do a partial lookup
revs = [x for x in self._revision_map
if x is not None and x.startswith(id_)]
if not revs:
raise util.CommandError("No such revision '%s'" % id_)
elif len(revs) > 1:
raise util.CommandError(
"Multiple revisions start "
"with '%s', %s..." % (
id_,
", ".join("'%s'" % r for r in revs[0:3])
))
else:
return self._revision_map[revs[0]]
.. seealso::
_get_rev = get_revision
:meth:`.ScriptDirectory.get_revisions`
"""
with self._catch_revision_errors():
return self.revision_map.get_revision(id_)
def as_revision_number(self, id_):
"""Convert a symbolic revision, i.e. 'head' or 'base', into
an actual revision number."""
if id_ == 'head':
id_ = self.get_current_head()
elif id_ == 'base':
id_ = None
return id_
with self._catch_revision_errors():
rev, branch_name = self.revision_map._resolve_revision_number(id_)
_as_rev_number = as_revision_number
if not rev:
# convert () to None
return None
else:
return rev[0]
def iterate_revisions(self, upper, lower):
"""Iterate through script revisions, starting at the given
@ -146,58 +187,156 @@ class ScriptDirectory(object):
The iterator yields :class:`.Script` objects.
"""
if upper is not None and _relative_destination.match(upper):
relative = int(upper)
revs = list(self._iterate_revisions("head", lower))
revs = revs[-relative:]
if len(revs) != abs(relative):
raise util.CommandError(
"Relative revision %s didn't "
"produce %d migrations" % (upper, abs(relative)))
return iter(revs)
elif lower is not None and _relative_destination.match(lower):
relative = int(lower)
revs = list(self._iterate_revisions(upper, "base"))
revs = revs[0:-relative]
if len(revs) != abs(relative):
raise util.CommandError(
"Relative revision %s didn't "
"produce %d migrations" % (lower, abs(relative)))
return iter(revs)
else:
return self._iterate_revisions(upper, lower)
.. seealso::
def _iterate_revisions(self, upper, lower):
lower = self.get_revision(lower)
upper = self.get_revision(upper)
orig = lower.revision if lower else 'base', \
upper.revision if upper else 'base'
script = upper
while script != lower:
if script is None and lower is not None:
:meth:`.RevisionMap.iterate_revisions`
"""
return self.revision_map.iterate_revisions(upper, lower)
def get_current_head(self):
"""Return the current head revision.
If the script directory has multiple heads
due to branching, an error is raised;
:meth:`.ScriptDirectory.get_heads` should be
preferred.
:return: a string revision number.
.. seealso::
:meth:`.ScriptDirectory.get_heads`
"""
with self._catch_revision_errors(multiple_heads=(
'The script directory has multiple heads (due to branching).'
'Please use get_heads(), or merge the branches using '
'alembic merge.'
)):
return self.revision_map.get_current_head()
def get_heads(self):
"""Return all "head" revisions as strings.
This is normally a list of length one,
unless branches are present. The
:meth:`.ScriptDirectory.get_current_head()` method
can be used normally when a script directory
has only one head.
:return: a tuple of string revision numbers.
"""
return list(self.revision_map.heads)
def get_base(self):
"""Return the "base" revision as a string.
This is the revision number of the script that
has a ``down_revision`` of None.
If the script directory has multiple bases, an error is raised;
:meth:`.ScriptDirectory.get_bases` should be
preferred.
"""
bases = self.get_bases()
if len(bases) > 1:
raise util.CommandError(
"Revision %s is not an ancestor of %s" % orig)
yield script
downrev = script.down_revision
script = self._revision_map[downrev]
"The script directory has multiple bases. "
"Please use get_bases().")
elif bases:
return bases[0]
else:
return None
def get_bases(self):
"""return all "base" revisions as strings.
This is the revision number of all scripts that
have a ``down_revision`` of None.
.. versionadded:: 0.7.0
"""
return list(self.revision_map.bases)
def _upgrade_revs(self, destination, current_rev):
revs = self.iterate_revisions(destination, current_rev)
with self._catch_revision_errors(
ancestor="Destination %(end)s is not a valid upgrade "
"target from current head(s)", end=destination):
revs = self.revision_map.iterate_revisions(
destination, current_rev, implicit_base=True)
revs = list(revs)
return [
(script.module.upgrade, script.down_revision, script.revision,
script.doc)
migration.MigrationStep.upgrade_from_script(
self.revision_map, script)
for script in reversed(list(revs))
]
def _downgrade_revs(self, destination, current_rev):
revs = self.iterate_revisions(current_rev, destination)
with self._catch_revision_errors(
ancestor="Destination %(end)s is not a valid downgrade "
"target from current head(s)", end=destination):
revs = self.revision_map.iterate_revisions(
current_rev, destination)
return [
(script.module.downgrade, script.revision, script.down_revision,
script.doc)
migration.MigrationStep.downgrade_from_script(
self.revision_map, script)
for script in revs
]
def _stamp_revs(self, revision, heads):
with self._catch_revision_errors(
multiple_heads="Multiple heads are present; please specify a "
"single target revision"):
heads = self.get_revisions(heads)
# filter for lineage will resolve things like
# branchname@base, version@base, etc.
filtered_heads = self.revision_map.filter_for_lineage(
heads, revision)
dest = self.get_revision(revision)
if dest is None:
# dest is 'base'. Return a "delete branch" migration
# for all applicable heads.
return [
migration.StampStep(head.revision, None, False, True)
for head in filtered_heads
]
elif dest in filtered_heads:
# the dest is already in the version table, do nothing.
return []
# figure out if the dest is a descendant or an
# ancestor of the selected nodes
descendants = set(self.revision_map._get_descendant_nodes([dest]))
ancestors = set(self.revision_map._get_ancestor_nodes([dest]))
if descendants.intersection(filtered_heads):
# heads are above the target, so this is a downgrade.
# we can treat them as a "merge", single step.
assert not ancestors.intersection(filtered_heads)
todo_heads = [head.revision for head in filtered_heads]
step = migration.StampStep(
todo_heads, dest.revision, False, False)
return [step]
elif ancestors.intersection(filtered_heads):
# heads are below the target, so this is an upgrade.
# we can treat them as a "merge", single step.
todo_heads = [head.revision for head in filtered_heads]
step = migration.StampStep(
todo_heads, dest.revision, True, False)
return [step]
else:
# destination is in a branch not represented,
# treat it as new branch
step = migration.StampStep((), dest.revision, True, True)
return [step]
def run_env(self):
"""Run the script environment.
@ -213,29 +352,101 @@ class ScriptDirectory(object):
def env_py_location(self):
return os.path.abspath(os.path.join(self.dir, "env.py"))
@util.memoized_property
def _revision_map(self):
map_ = {}
def _generate_template(self, src, dest, **kw):
util.status("Generating %s" % os.path.abspath(dest),
util.template_to_file,
src,
dest,
self.output_encoding,
**kw
)
for file_ in os.listdir(self.versions):
script = Script._from_filename(self, self.versions, file_)
if script is None:
continue
if script.revision in map_:
util.warn("Revision %s is present more than once" %
script.revision)
map_[script.revision] = script
for rev in map_.values():
if rev.down_revision is None:
continue
if rev.down_revision not in map_:
util.warn("Revision %s referenced from %s is not present"
% (rev.down_revision, rev))
rev.down_revision = None
def _copy_file(self, src, dest):
util.status("Generating %s" % os.path.abspath(dest),
shutil.copy,
src, dest)
def generate_revision(
self, revid, message, head=None,
refresh=False, splice=False, branch_labels=None, **kw):
"""Generate a new revision file.
This runs the ``script.py.mako`` template, given
template arguments, and creates a new file.
:param revid: String revision id. Typically this
comes from ``alembic.util.rev_id()``.
:param message: the revision message, the one passed
by the -m argument to the ``revision`` command.
:param head: the head revision to generate against. Defaults
to the current "head" if no branches are present, else raises
an exception.
.. versionadded:: 0.7.0
:param refresh: when True, the in-memory state of this
:class:`.ScriptDirectory` will be updated with a new
:class:`.Script` instance representing the new revision;
the :class:`.Script` instance is returned.
If False, the file is created but the state of the
:class:`.ScriptDirectory` is unmodified; ``None``
is returned.
:param splice: if True, allow the "head" version to not be an
actual head; otherwise, the selected head must be a head
(e.g. endpoint) revision.
"""
if head is None:
head = "head"
with self._catch_revision_errors(multiple_heads=(
"Multiple heads are present; please specify the head "
"revision on which the new revision should be based, "
"or perform a merge."
)):
heads = self.revision_map.get_revisions(head)
if len(set(heads)) != len(heads):
raise util.CommandError("Duplicate head revisions specified")
create_date = datetime.datetime.now()
path = self._rev_path(revid, message, create_date)
if not splice:
for head in heads:
if head is not None and not head.is_head:
raise util.CommandError(
"Revision %s is not a head revision; please specify "
"--splice to create a new branch from this revision"
% head.revision)
self._generate_template(
os.path.join(self.dir, "script.py.mako"),
path,
up_revision=str(revid),
down_revision=revision.tuple_rev_as_scalar(
tuple(h.revision if h is not None else None for h in heads)),
branch_labels=util.to_tuple(branch_labels),
create_date=create_date,
comma=util.format_as_comma,
message=message if message is not None else ("empty message"),
**kw
)
if refresh:
script = Script._from_path(self, path)
if branch_labels and not script.branch_labels:
raise util.CommandError(
"Version %s specified branch_labels %s, however the "
"migration file %s does not have them; have you upgraded "
"your script.py.mako to include the "
"'branch_labels' section?" % (
script.revision, branch_labels, script.path
))
self.revision_map.add_revision(script)
return script
else:
map_[rev.down_revision].add_nextrev(rev.revision)
map_[None] = None
return map_
return None
def _rev_path(self, rev_id, message, create_date):
slug = "_".join(_slug_re.findall(message or "")).lower()
@ -255,124 +466,8 @@ class ScriptDirectory(object):
)
return os.path.join(self.versions, filename)
def get_current_head(self):
"""Return the current head revision.
If the script directory has multiple heads
due to branching, an error is raised.
Returns a string revision number.
"""
current_heads = self.get_heads()
if len(current_heads) > 1:
raise util.CommandError(
'Only a single head is supported. The '
'script directory has multiple heads (due to branching), '
'which must be resolved by manually editing the revision '
'files to form a linear sequence. Run `alembic branches` to '
'see the divergence(s).')
if current_heads:
return current_heads[0]
else:
return None
_current_head = get_current_head
"""the 0.2 name, for backwards compat."""
def get_heads(self):
"""Return all "head" revisions as strings.
Returns a list of string revision numbers.
This is normally a list of length one,
unless branches are present. The
:meth:`.ScriptDirectory.get_current_head()` method
can be used normally when a script directory
has only one head.
"""
heads = []
for script in self._revision_map.values():
if script and script.is_head:
heads.append(script.revision)
return heads
def get_base(self):
"""Return the "base" revision as a string.
This is the revision number of the script that
has a ``down_revision`` of None.
Behavior is not defined if more than one script
has a ``down_revision`` of None.
"""
for script in self._revision_map.values():
if script and script.down_revision is None \
and script.revision in self._revision_map:
return script.revision
else:
return None
def _generate_template(self, src, dest, **kw):
util.status("Generating %s" % os.path.abspath(dest),
util.template_to_file,
src,
dest,
self.output_encoding,
**kw
)
def _copy_file(self, src, dest):
util.status("Generating %s" % os.path.abspath(dest),
shutil.copy,
src, dest)
def generate_revision(self, revid, message, refresh=False, **kw):
"""Generate a new revision file.
This runs the ``script.py.mako`` template, given
template arguments, and creates a new file.
:param revid: String revision id. Typically this
comes from ``alembic.util.rev_id()``.
:param message: the revision message, the one passed
by the -m argument to the ``revision`` command.
:param refresh: when True, the in-memory state of this
:class:`.ScriptDirectory` will be updated with a new
:class:`.Script` instance representing the new revision;
the :class:`.Script` instance is returned.
If False, the file is created but the state of the
:class:`.ScriptDirectory` is unmodified; ``None``
is returned.
"""
current_head = self.get_current_head()
create_date = datetime.datetime.now()
path = self._rev_path(revid, message, create_date)
self._generate_template(
os.path.join(self.dir, "script.py.mako"),
path,
up_revision=str(revid),
down_revision=current_head,
create_date=create_date,
message=message if message is not None else ("empty message"),
**kw
)
if refresh:
script = Script._from_path(self, path)
self._revision_map[script.revision] = script
if script.down_revision:
self._revision_map[script.down_revision].\
add_nextrev(script.revision)
return script
else:
return None
class Script(object):
class Script(revision.Revision):
"""Represent a single revision file in a ``versions/`` directory.
@ -381,16 +476,14 @@ class Script(object):
"""
nextrev = frozenset()
def __init__(self, module, rev_id, path):
self.module = module
self.revision = rev_id
self.path = path
self.down_revision = getattr(module, 'down_revision', None)
revision = None
"""The string revision number for this :class:`.Script` instance."""
super(Script, self).__init__(
rev_id,
module.down_revision,
branch_labels=util.to_tuple(
getattr(module, 'branch_labels', None), default=()))
module = None
"""The Python module representing the actual script itself."""
@ -398,9 +491,6 @@ class Script(object):
path = None
"""Filesystem path of the script."""
down_revision = None
"""The ``down_revision`` identifier within the migration script."""
@property
def doc(self):
"""Return the docstring given in the script."""
@ -419,58 +509,83 @@ class Script(object):
else:
return ""
def add_nextrev(self, rev):
self.nextrev = self.nextrev.union([rev])
@property
def is_head(self):
"""Return True if this :class:`.Script` is a 'head' revision.
This is determined based on whether any other :class:`.Script`
within the :class:`.ScriptDirectory` refers to this
:class:`.Script`. Multiple heads can be present.
"""
return not bool(self.nextrev)
@property
def is_branch_point(self):
"""Return True if this :class:`.Script` is a branch point.
A branchpoint is defined as a :class:`.Script` which is referred
to by more than one succeeding :class:`.Script`, that is more
than one :class:`.Script` has a `down_revision` identifier pointing
here.
"""
return len(self.nextrev) > 1
@property
def log_entry(self):
return \
"Rev: %s%s%s\n" \
"Parent: %s\n" \
"Path: %s\n" \
"\n%s\n" % (
entry = "Rev: %s%s%s%s\n" % (
self.revision,
" (head)" if self.is_head else "",
" (branchpoint)" if self.is_branch_point else "",
self.down_revision,
self.path,
" (mergepoint)" if self.is_merge_point else "",
)
if self.is_merge_point:
entry += "Merges: %s\n" % (self._format_down_revision(), )
else:
entry += "Parent: %s\n" % (self._format_down_revision(), )
if self.is_branch_point:
entry += "Branches into: %s\n" % (
util.format_as_comma(self.nextrev))
if self.branch_labels:
entry += "Branch names: %s\n" % (
util.format_as_comma(self.branch_labels), )
entry += "Path: %s\n" % (self.path,)
entry += "\n%s\n" % (
"\n".join(
" %s" % para
for para in self.longdoc.splitlines()
)
)
return entry
def __str__(self):
return "%s -> %s%s%s, %s" % (
self.down_revision,
return "%s -> %s%s%s%s, %s" % (
self._format_down_revision(),
self.revision,
" (head)" if self.is_head else "",
" (branchpoint)" if self.is_branch_point else "",
" (mergepoint)" if self.is_merge_point else "",
self.doc)
def _head_only(
self, include_branches=False, include_doc=False,
include_parents=False, tree_indicators=True):
text = self.revision
if include_parents:
text = "%s -> %s" % (
self._format_down_revision(), text)
if include_branches and self.branch_labels:
text += " (%s)" % util.format_as_comma(self.branch_labels)
if tree_indicators:
text += "%s%s%s" % (
" (head)" if self.is_head else "",
" (branchpoint)" if self.is_branch_point else "",
" (mergepoint)" if self.is_merge_point else "",
)
if include_doc:
text += ", %s" % self.doc
return text
def cmd_format(
self,
verbose,
include_branches=False, include_doc=False,
include_parents=False, tree_indicators=True):
if verbose:
return self.log_entry
else:
return self._head_only(
include_branches, include_doc,
include_parents, tree_indicators)
def _format_down_revision(self):
if not self.down_revision:
return "<base>"
else:
return util.format_as_comma(self._down_revision_tuple)
@classmethod
def _from_path(cls, scriptdir, path):
dir_, filename = os.path.split(path)

View File

@ -1,7 +1,7 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
@ -9,6 +9,7 @@ Create Date: ${create_date}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
from alembic import op
import sqlalchemy as sa

View File

@ -4,7 +4,7 @@ import re
%>"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
@ -12,6 +12,7 @@ Create Date: ${create_date}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
from alembic import op
import sqlalchemy as sa

View File

@ -1,7 +1,7 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
@ -9,6 +9,7 @@ Create Date: ${create_date}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
from alembic import op
import sqlalchemy as sa

View File

@ -166,7 +166,7 @@ def _testing_config():
def write_script(
scriptdir, rev_id, content, encoding='ascii', sourceless=False):
old = scriptdir._revision_map[rev_id]
old = scriptdir.revision_map.get_revision(rev_id)
path = old.path
content = textwrap.dedent(content)
@ -178,12 +178,11 @@ def write_script(
if os.access(pyc_path, os.F_OK):
os.unlink(pyc_path)
script = Script._from_path(scriptdir, path)
old = scriptdir._revision_map[script.revision]
old = scriptdir.revision_map.get_revision(script.revision)
if old.down_revision != script.down_revision:
raise Exception("Can't change down_revision "
"on a refresh operation.")
scriptdir._revision_map[script.revision] = script
script.nextrev = old.nextrev
scriptdir.revision_map.add_revision(script, _replace=True)
if sourceless:
make_sourceless(path)

View File

@ -5,13 +5,14 @@ import warnings
import re
import inspect
import uuid
import collections
from mako.template import Template
from sqlalchemy.engine import url
from sqlalchemy import __version__
from .compat import callable, exec_, load_module_py, load_module_pyc, \
binary_type
binary_type, string_types, py27
class CommandError(Exception):
@ -44,6 +45,11 @@ from sqlalchemy.util.compat import inspect_getfullargspec
import logging
log = logging.getLogger(__name__)
if py27:
# disable "no handler found" errors
logging.getLogger('alembic').addHandler(logging.NullHandler())
try:
import fcntl
import termios
@ -282,6 +288,28 @@ def rev_id():
return hex(val)[2:-1]
def to_tuple(x, default=None):
if x is None:
return default
elif isinstance(x, string_types):
return (x, )
elif isinstance(x, collections.Iterable):
return tuple(x)
else:
raise ValueError("Don't know how to turn %r into a tuple" % x)
def format_as_comma(value):
if value is None:
return ""
elif isinstance(value, string_types):
return value
elif isinstance(value, collections.Iterable):
return ", ".join(value)
else:
raise ValueError("Don't know how to comma-format %r" % value)
class memoized_property(object):
"""A read-only @property that is only evaluated once."""

9
docs/build/api.rst vendored
View File

@ -132,6 +132,15 @@ to the Alembic version files present in the filesystem.
.. automodule:: alembic.script
:members:
Revision
========
The :class:`.RevisionMap` object serves as the basis for revision
management, used exclusively by :class:`.ScriptDirectory`.
.. automodule:: alembic.revision
:members:
Autogeneration
==============

View File

@ -1,10 +1,52 @@
==========
Changelog
==========
.. changelog::
:version: 0.7.0
.. change::
:tags: feature, versioning
:tickets: 167
The "multiple heads / branches" feature has now landed. This is
by far the most significant change Alembic has seen since its inception;
while the workflow of most commands hasn't changed, and the format
of version files and the ``alembic_version`` table are unchanged as well,
a new suite of features opens up in the case where multiple version
files refer to the same parent, or to the "base". Merging of
branches, operating across distinct named heads, and multiple
independent bases are now all supported. The feature incurs radical
changes to the internals of versioning and traversal, and should be
treated as "beta mode" for the next several subsequent releases
within 0.7.
.. seealso::
:ref:`branches`
.. change::
:tags: feature, commands
New commands added: ``alembic show``, ``alembic heads`` and
``alembic merge``. Also, a new option ``--verbose`` has been
added to several informational commands, such as ``alembic history``,
``alembic current``, ``alembic branches``, and ``alembic heads``.
``alembic revision`` also contains several new options used
within the new branch management system. The output of commands has
been altered in many cases to support new fields and attributes;
the ``history`` command in particular now returns it's "verbose" output
only if ``--verbose`` is sent; without this flag it reverts to it's
older behavior of short line items (which was never changed in the docs).
.. change::
:tags: changed, commands
The ``--head_only`` option to the ``alembic current`` command is
deprecated; the ``current`` command now lists just the version numbers
alone by default; use ``--verbose`` to get at additional output.
.. change::
:tags: feature, config
:pullreq: bitbucket:33

View File

@ -207,8 +207,6 @@ This file contains the following features:
``%%(minute).2d``, ``%%(second).2d`` - components of the create date
as returned by ``datetime.datetime.now()``
.. versionadded:: 0.3.6 - added date parameters to ``file_template``.
* ``truncate_slug_length`` - defaults to 40, the max number of characters
to include in the "slug" field.
@ -223,8 +221,6 @@ This file contains the following features:
that the migration environment script ``env.py`` should be run unconditionally when
generating new revision files
.. versionadded:: 0.3.3
* ``sourceless`` - when set to 'true', revision files that only exist as .pyc
or .pyo files in the versions directory will be used as versions, allowing
"sourceless" versioning folders. When left at the default of 'false',
@ -249,6 +245,9 @@ the SQLAlchemy URL is all that's needed::
sqlalchemy.url = postgresql://scott:tiger@localhost/test
.. _create_migration:
Create a Migration Script
=========================
@ -263,7 +262,7 @@ A new file ``1975ea83b712_create_account_table.py`` is generated. Looking insid
"""create account table
Revision ID: 1975ea83b712
Revises: None
Revises:
Create Date: 2011-11-08 11:40:27.089406
"""
@ -271,6 +270,7 @@ A new file ``1975ea83b712_create_account_table.py`` is generated. Looking insid
# revision identifiers, used by Alembic.
revision = '1975ea83b712'
down_revision = None
branch_labels = None
from alembic import op
import sqlalchemy as sa
@ -359,7 +359,7 @@ Let's do another one so we have some things to play with. We again create a r
file::
$ alembic revision -m "Add a column"
Generating /path/to/yourapp/alembic/versions/ae1027a6acf.py_add_a_column.py...
Generating /path/to/yourapp/alembic/versions/ae1027a6acf_add_a_column.py...
done
Let's edit this file and add a new column to the ``account`` table::
@ -397,7 +397,8 @@ We've now added the ``last_transaction_date`` column to the database.
Relative Migration Identifiers
==============================
As of 0.3.3, relative upgrades/downgrades are also supported. To move two versions from the current, a decimal value "+N" can be supplied::
Relative upgrades/downgrades are also supported. To move two versions from
the current, a decimal value "+N" can be supplied::
$ alembic upgrade +2
@ -405,6 +406,20 @@ Negative values are accepted for downgrades::
$ alembic downgrade -1
Partial Revision Identifiers
=============================
Any time we need to refer to a revision number explicitly, we have the option
to use a partial number. As long as this number uniquely identifies the
version, it may be used in any command in any place that version numbers
are accepted::
$ alembic upgrade ae1
Above, we use ``ae1`` to refer to revision ``ae1027a6acf``.
Alembic will stop and let you know if more than one version starts with
that prefix.
Getting Information
===================
@ -419,28 +434,40 @@ First we can view the current revision::
``head`` is displayed only if the revision identifier for this database matches the head revision.
We can also view history::
We can also view history with ``alembic history``; the ``--verbose`` option
(accepted by several commands, including ``history``, ``current``, ``heads``
and ``branches``) will show us full information about each revision::
$ alembic history
$ alembic history --verbose
1975ea83b712 -> ae1027a6acf (head), Add a column
None -> 1975ea83b712, empty message
Rev: ae1027a6acf (head)
Parent: 1975ea83b712
Path: /path/to/yourproject/alembic/versions/ae1027a6acf_add_a_column.py
We can also identify specific migrations using just enough characters to uniquely identify them.
If we wanted to upgrade directly to ``ae1027a6acf`` we could say::
add a column
$ alembic upgrade ae1
Revision ID: ae1027a6acf
Revises: 1975ea83b712
Create Date: 2014-11-20 13:02:54.849677
Alembic will stop and let you know if more than one version starts with that prefix.
Rev: 1975ea83b712
Parent: <base>
Path: /path/to/yourproject/alembic/versions/1975ea83b712_add_account_table.py
add account table
Revision ID: 1975ea83b712
Revises:
Create Date: 2014-11-20 13:02:46.257104
Viewing History Ranges
----------------------
Using the ``-r`` option to ``alembic history``, we can also view various slices
of history. The ``-r`` argument accepts an argument ``[start]:[end]``, where
either may be a revision number, or various combinations of ``base``, ``head``,
``currrent`` to specify the current revision, as well as negative relative
ranges for ``[start]`` and positive relative ranges for ``[end]``::
either may be a revision number, symbols like ``head``, ``heads`` or
``base``, ``current`` to specify the current revision(s), as well as negative
relative ranges for ``[start]`` and positive relative ranges for ``[end]``::
$ alembic history -r1975ea:ae1027
@ -1333,20 +1360,53 @@ tokenized::
For more detail on the naming convention feature, see :ref:`sqla:constraint_naming_conventions`.
.. _branches:
Working with Branches
=====================
A *branch* describes when a source tree is broken up into two versions representing
two independent sets of changes. The challenge of a branch is to *merge* the
branches into a single series of changes. Alembic's GUID-based version number scheme
allows branches to be reconciled.
.. note:: Alembic 0.7.0 features an all-new versioning model that fully
supports branch points, merge points, and long-lived, labeled branches,
including independent branches originating from multiple bases.
A great emphasis has been placed on there being almost no impact on the
existing Alembic workflow, including that all commands work pretty much
the same as they did before, the format of migration files doesn't require
any change (though there are some changes that are recommended),
and even the structure of the ``alembic_version``
table does not change at all. However, most alembic commands now offer
new features which will break out an Alembic environment into
"branch mode", where things become a lot more intricate. Working in
"branch mode" should be considered as a "beta" feature, with many new
paradigms and use cases still to be stress tested in the wild.
Please tread lightly!
Consider if we merged into our source repository another branch which contained
.. versionadded:: 0.7.0
A **branch** describes a point in a migration stream when two or more
versions refer to the same parent migration as their anscestor. Branches
occur naturally when two divergent source trees, both containing Alembic
revision files created independently within those source trees, are merged
together into one. When this occurs, the challenge of a branch is to **merge** the
branches into a single series of changes, so that databases established
from either source tree individually can be upgraded to reference the merged
result equally. Another scenario where branches are present are when we create them
directly; either at some point in the migration stream we'd like different
series of migrations to be managed independently (e.g. we create a tree),
or we'd like separate migration streams for different features starting
at the root (e.g. a *forest*). We'll illustrate all of these cases, starting
with the most common which is a source-merge-originated branch that we'll
merge.
Starting with the "account table" example we began in :ref:`create_migration`,
assume we have our basemost version ``1975ea83b712``, which leads into
the second revision ``ae1027a6acf``, and the migration files for these
two revisions are checked into our source repository.
Consider if we merged into our source repository another code branch which contained
a revision for another table called ``shopping_cart``. This revision was made
against our first Alembic revision, the one that generated ``account``. After
loading the second source tree in, a new file ``27c6a30d7c24.py`` exists within
our ``versions`` directory. Both it, as well as ``ae1027a6acf.py``, reference
loading the second source tree in, a new file
``27c6a30d7c24_add_accont_table.py`` exists within our ``versions`` directory.
Both it, as well as ``ae1027a6acf_add_a_column.py``, reference
``1975ea83b712`` as the "downgrade" revision. To illustrate::
# main source tree:
@ -1355,67 +1415,618 @@ our ``versions`` directory. Both it, as well as ``ae1027a6acf.py``, reference
# branched source tree
1975ea83b712 (add account table) -> 27c6a30d7c24 (add shopping cart table)
So above we can see 1975ea83b712 is our *branch point*. The Alembic command ``branches``
illustrates this fact::
Above, we can see ``1975ea83b712`` is our **branch point**; two distinct versions
both refer to it as its parent. The Alembic command ``branches`` illustrates
this fact::
$ alembic branches
None -> 1975ea83b712 (branchpoint), add account table
-> 1975ea83b712 -> 27c6a30d7c24 (head), add shopping cart table
-> 1975ea83b712 -> ae1027a6acf (head), add a column
$ alembic branches --verbose
Rev: 1975ea83b712 (branchpoint)
Parent: <base>
Branches into: 27c6a30d7c24, ae1027a6acf
Path: foo/versions/1975ea83b712_add_account_table.py
add account table
Revision ID: 1975ea83b712
Revises:
Create Date: 2014-11-20 13:02:46.257104
-> 27c6a30d7c24 (head), add shopping cart table
-> ae1027a6acf (head), add a column
History shows it too, illustrating two ``head`` entries as well
as a ``branchpoint``::
$ alembic history
1975ea83b712 -> 27c6a30d7c24 (head), add shopping cart table
1975ea83b712 -> ae1027a6acf (head), add a column
None -> 1975ea83b712 (branchpoint), add account table
<base> -> 1975ea83b712 (branchpoint), add account table
Alembic will also refuse to run any migrations until this is resolved::
We can get a view of just the current heads using ``alembic heads``::
$ alembic upgrade head
INFO [alembic.context] Context class PostgresqlContext.
INFO [alembic.context] Will assume transactional DDL.
Exception: Only a single head supported so far...
$ alembic heads --verbose
Rev: 27c6a30d7c24 (head)
Parent: 1975ea83b712
Path: foo/versions/27c6a30d7c24_add_shopping_cart_table.py
We resolve this branch by editing the files to be in a straight line. In this case we edit
``27c6a30d7c24.py`` to point to ``ae1027a6acf.py``::
"""add shopping cart table
add shopping cart table
Revision ID: 27c6a30d7c24
Revises: ae1027a6acf # changed from 1975ea83b712
Create Date: 2011-11-08 13:02:14.212810
Revises: 1975ea83b712
Create Date: 2014-11-20 13:03:11.436407
Rev: ae1027a6acf (head)
Parent: 1975ea83b712
Path: foo/versions/ae1027a6acf_add_a_column.py
add a column
Revision ID: ae1027a6acf
Revises: 1975ea83b712
Create Date: 2014-11-20 13:02:54.849677
If we try to run an ``upgrade`` to the usual end target of ``head``, Alembic no
longer considers this to be an unambiguous command. As we have more than
one ``head``, the ``upgrade`` command wants us to provide more information::
$ alembic upgrade head
FAILED: Multiple head revisions are present for given argument 'head'; please specify a specific
target revision, '<branchname>@head' to narrow to a specific head, or 'heads' for all heads
The ``upgrade`` command gives us quite a few options in which we can proceed
with our upgrade, either giving it information on *which* head we'd like to upgrade
towards, or alternatively stating that we'd like *all* heads to be upgraded
towards at once. However, in the typical case of two source trees being
merged, we will want to pursue a third option, which is that we can **merge** these
branches.
Merging Branches
----------------
An Alembic merge is a migration file that joins two or
more "head" files together. If the two branches we have right now can
be said to be a "tree" structure, introducing this merge file will
turn it into a "diamond" structure::
-- ae1027a6acf -->
/ \
<base> --> 1975ea83b712 --> --> mergepoint
\ /
-- 27c6a30d7c24 -->
We create the merge file using ``alembic merge``; with this command, we can
pass to it an argument such as ``heads``, meaning we'd like to merge all
heads. Or, we can pass it individual revision numbers sequentally::
$ alembic merge -m "merge ae1 and 27c" ae1027 27c6a
Generating /path/to/foo/versions/53fffde5ad5_merge_ae1_and_27c.py ... done
Looking inside the new file, we see it as a regular migration file, with
the only new twist is that ``down_revision`` points to both revisions::
"""merge ae1 and 27c
Revision ID: 53fffde5ad5
Revises: ae1027a6acf, 27c6a30d7c24
Create Date: 2014-11-20 13:31:50.811663
"""
# revision identifiers, used by Alembic.
revision = '53fffde5ad5'
down_revision = ('ae1027a6acf', '27c6a30d7c24')
branch_labels = None
from alembic import op
import sqlalchemy as sa
def upgrade():
pass
def downgrade():
pass
This file is a regular migration file, and if we wish to, we may place
:class:`.Operations` directives into the ``upgrade()`` and ``downgrade()``
functions like any other migration file. Though it is probably best to limit
the instructions placed here only to those that deal with any kind of
reconciliation that is needed between the two merged branches, if any.
The ``heads`` command now illustrates that the multiple heads in our
``versions/`` directory have been resolved into our new head::
$ alembic heads --verbose
Rev: 53fffde5ad5 (head) (mergepoint)
Merges: ae1027a6acf, 27c6a30d7c24
Path: foo/versions/53fffde5ad5_merge_ae1_and_27c.py
merge ae1 and 27c
Revision ID: 53fffde5ad5
Revises: ae1027a6acf, 27c6a30d7c24
Create Date: 2014-11-20 13:31:50.811663
History shows a similar result, as the mergepoint becomes our head::
$ alembic history
ae1027a6acf, 27c6a30d7c24 -> 53fffde5ad5 (head) (mergepoint), merge ae1 and 27c
1975ea83b712 -> ae1027a6acf, add a column
1975ea83b712 -> 27c6a30d7c24, add shopping cart table
<base> -> 1975ea83b712 (branchpoint), add account table
With a single ``head`` target, a generic ``upgrade`` can proceed::
$ alembic upgrade head
INFO [alembic.migration] Context impl PostgresqlImpl.
INFO [alembic.migration] Will assume transactional DDL.
INFO [alembic.migration] Running upgrade -> 1975ea83b712, add account table
INFO [alembic.migration] Running upgrade 1975ea83b712 -> 27c6a30d7c24, add shopping cart table
INFO [alembic.migration] Running upgrade 1975ea83b712 -> ae1027a6acf, add a column
INFO [alembic.migration] Running upgrade ae1027a6acf, 27c6a30d7c24 -> 53fffde5ad5, merge ae1 and 27c
.. topic:: merge mechanics
The upgrade process traverses through all of our migration files using
a **topological sorting** algorithm, treating the list of migration
files not as a linked list, but as a **directed acyclic graph**. The starting
points of this traversal are the **current heads** within our database,
and the end point is the "head" revision or revisions specified.
When a migration proceeds across a point at which there are multiple heads,
the ``alembic_version`` table will at that point store *multiple* rows,
one for each head. Our migration process above will emit SQL against
``alembic_version`` along these lines:
.. sourcecode:: sql
-- Running upgrade -> 1975ea83b712, add account table
INSERT INTO alembic_version (version_num) VALUES ('1975ea83b712')
-- Running upgrade 1975ea83b712 -> 27c6a30d7c24, add shopping cart table
UPDATE alembic_version SET version_num='27c6a30d7c24' WHERE alembic_version.version_num = '1975ea83b712'
-- Running upgrade 1975ea83b712 -> ae1027a6acf, add a column
INSERT INTO alembic_version (version_num) VALUES ('ae1027a6acf')
-- Running upgrade ae1027a6acf, 27c6a30d7c24 -> 53fffde5ad5, merge ae1 and 27c
DELETE FROM alembic_version WHERE alembic_version.version_num = 'ae1027a6acf'
UPDATE alembic_version SET version_num='53fffde5ad5' WHERE alembic_version.version_num = '27c6a30d7c24'
At the point at which both ``27c6a30d7c24`` and ``ae1027a6acf`` exist within our
database, both values are present in ``alembic_version``, which now has
two rows. If we upgrade to these two versions alone, then stop and
run ``alembic current``, we will see this::
$ alembic current --verbose
Current revision(s) for postgresql://scott:XXXXX@localhost/test:
Rev: ae1027a6acf
Parent: 1975ea83b712
Path: foo/versions/ae1027a6acf_add_a_column.py
add a column
Revision ID: ae1027a6acf
Revises: 1975ea83b712
Create Date: 2014-11-20 13:02:54.849677
Rev: 27c6a30d7c24
Parent: 1975ea83b712
Path: foo/versions/27c6a30d7c24_add_shopping_cart_table.py
add shopping cart table
Revision ID: 27c6a30d7c24
Revises: 1975ea83b712
Create Date: 2014-11-20 13:03:11.436407
A key advantage to the ``merge`` process is that it will
run equally well on databases that were present on version ``ae1027a6acf``
alone, versus databases that were present on version ``27c6a30d7c24`` alone;
whichever version was not yet applied, will be applied before the merge point
can be crossed. This brings forth a way of thinking about a merge file,
as well as about any Alembic revision file. As they are considered to
be "nodes" within a set that is subject to topological sorting, each
"node" is a point that cannot be crossed until all of its dependencies
are satisfied.
Prior to Alembic's support of merge points, the use case of databases
sitting on different heads was basically impossible to reconcile; having
to manually splice the head files together invariably meant that one migration
would occur before the other, thus being incompatible with databases that
were present on the other migration.
Working with Explicit Branches
------------------------------
The ``alembic upgrade`` command hinted at other options besides merging when
dealing with multiple heads. Let's back up and assume we're back where
we have as our heads just ``ae1027a6acf`` and ``27c6a30d7c24``::
$ alembic heads
27c6a30d7c24
ae1027a6acf
Earlier, when we did ``alembic upgrade head``, it gave us an error which
suggested ``please specify a specific target revision, '<branchname>@head' to
narrow to a specific head, or 'heads' for all heads`` in order to proceed
without merging. Let's cover those cases.
Referring to all heads at once
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The ``heads`` identifier is a lot like ``head``, except it explicitly refers
to *all* heads at once. That is, it's like telling Alembic to do the operation
for both ``ae1027a6acf`` and ``27c6a30d7c24`` simultaneously. If we started
from a fresh database and ran ``upgrade heads`` we'd see::
$ alembic upgrade heads
INFO [alembic.migration] Context impl PostgresqlImpl.
INFO [alembic.migration] Will assume transactional DDL.
INFO [alembic.migration] Running upgrade -> 1975ea83b712, add account table
INFO [alembic.migration] Running upgrade 1975ea83b712 -> ae1027a6acf, add a column
INFO [alembic.migration] Running upgrade 1975ea83b712 -> 27c6a30d7c24, add shopping cart table
Since we've upgraded to ``heads``, and we do in fact have more than one head,
that means these two distinct heads are now in our ``alembic_version`` table.
We can see this if we run ``alembic current``::
$ alembic current
ae1027a6acf (head)
27c6a30d7c24 (head)
That means there's two rows in ``alembic_version`` right now. If we downgrade
one step at a time, Alembic will **delete** from the ``alembic_version`` table
each branch that's closed out, until only one branch remains; then it will
continue updating the single value down to the previous versions::
$ alembic downgrade -1
INFO [alembic.migration] Running downgrade ae1027a6acf -> 1975ea83b712, add a column
$ alembic current
27c6a30d7c24 (head)
$ alembic downgrade -1
INFO [alembic.migration] Running downgrade 27c6a30d7c24 -> 1975ea83b712, add shopping cart table
$ alembic current
1975ea83b712 (branchpoint)
$ alembic downgrade -1
INFO [alembic.migration] Running downgrade 1975ea83b712 -> , add account table
$ alembic current
Referring to a Specific Version
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
We can pass a specific version number to ``upgrade``. Alembic will ensure that
all revisions upon which this version depends are invoked, and nothing more.
So if we ``upgrade`` either to ``27c6a30d7c24`` or ``ae1027a6acf`` specifically,
it guarantees that ``1975ea83b712`` will have been applied, but not that
any "sibling" versions are applied::
$ alembic upgrade 27c6a
INFO [alembic.migration] Running upgrade -> 1975ea83b712, add account table
INFO [alembic.migration] Running upgrade 1975ea83b712 -> 27c6a30d7c24, add shopping cart table
With ``1975ea83b712`` and ``27c6a30d7c24`` applied, ``ae1027a6acf`` is just
a single additional step::
$ alembic upgrade ae102
INFO [alembic.migration] Running upgrade 1975ea83b712 -> ae1027a6acf, add a column
Working with Branch Labels
^^^^^^^^^^^^^^^^^^^^^^^^^^
To satisfy the use case where an environment has long-lived branches, especially
independent branches as will be discussed in the next section, Alembic supports
the concept of **branch labels**. These are string values that are present
within the migration file, using the new identifier ``branch_labels``.
For example, if we want to refer to the "shopping cart" branch using the name
"shoppingcart", we can add that name to our file
``27c6a30d7c24_add_shopping_cart_table.py``::
"""add shopping cart table
"""
# revision identifiers, used by Alembic.
revision = '27c6a30d7c24'
# changed from 1975ea83b712
down_revision = 'ae1027a6acf'
down_revision = '1975ea83b712'
branch_labels = ('shoppingcart',)
.. sidebar:: The future of Branches
# ...
As of this writing, a new approach to branching has been planned. When
implemented, the task of manually splicing files into a line will no longer
be needed; instead, a simple command along the lines of ``alembic merge``
will be able to produce merges of migration files. Keep a lookout
for future Alembic versions!
The ``branches`` command then shows no branches::
$ alembic branches
$
And the history is similarly linear::
The ``branch_labels`` attribute refers to a string name, or a tuple
of names, which will now apply to this revision, all descendants of this
revision, as well as all ancestors of this revision up until the preceding
branch point, in this case ``1975ea83b712``. We can see the ``shoppingcart``
label applied to this revision::
$ alembic history
1975ea83b712 -> 27c6a30d7c24 (shoppingcart) (head), add shopping cart table
1975ea83b712 -> ae1027a6acf (head), add a column
<base> -> 1975ea83b712 (branchpoint), add account table
ae1027a6acf -> 27c6a30d7c24 (head), add shopping cart table
With the label applied, the name ``shoppingcart`` now serves as an alias
for the ``27c6a30d7c24`` revision specifically. We can illustrate this
by showing it with ``alembic show``::
$ alembic show shoppingcart
Rev: 27c6a30d7c24 (head)
Parent: 1975ea83b712
Branch names: shoppingcart
Path: foo/versions/27c6a30d7c24_add_shopping_cart_table.py
add shopping cart table
Revision ID: 27c6a30d7c24
Revises: 1975ea83b712
Create Date: 2014-11-20 13:03:11.436407
However, when using branch labels, we usually want to use them using a syntax
known as "branch at" syntax; this syntax allows us to state that we want to
use a specific revision, let's say a "head" revision, in terms of a *specific*
branch. While normally, we can't refer to ``alembic upgrade head`` when
there's multiple heads, we *can* refer to this head specifcally using
``shoppingcart@head`` syntax::
$ alembic upgrade shoppingcart@head
INFO [alembic.migration] Running upgrade 1975ea83b712 -> 27c6a30d7c24, add shopping cart table
The ``shoppingcart@head`` syntax becomes important to us if we wish to
add new migration files to our versions directory while maintaining multiple
branches. Just like the ``upgrade`` command, if we attempted to add a new
revision file to our multiple-heads layout without a specific parent revision,
we'd get a familiar error::
$ alembic revision -m "add a shopping cart column"
FAILED: Multiple heads are present; please specify the head revision on
which the new revision should be based, or perform a merge.
The ``alembic revision`` command is pretty clear in what we need to do;
to add our new revision specifically to the ``shoppingcart`` branch,
we use the ``--head`` argument, either with the specific revision identifier
``27c6a30d7c24``, or more generically using our branchname ``shoppingcart@head``::
$ alembic revision -m "add a shopping cart column" --head shoppingcart@head
Generating /path/to/foo/versions/d747a8a8879_add_a_shopping_cart_column.py ... done
``alembic history`` shows both files now part of the ``shoppingcart`` branch::
$ alembic history
1975ea83b712 -> ae1027a6acf (head), add a column
27c6a30d7c24 -> d747a8a8879 (shoppingcart) (head), add a shopping cart column
1975ea83b712 -> 27c6a30d7c24 (shoppingcart), add shopping cart table
<base> -> 1975ea83b712 (branchpoint), add account table
We can limit our history operation just to this branch as well::
$ alembic history -r shoppingcart:
27c6a30d7c24 -> d747a8a8879 (shoppingcart) (head), add a shopping cart column
1975ea83b712 -> 27c6a30d7c24 (shoppingcart), add shopping cart table
If we want to illustrate the path of ``shoppingcart`` all the way from the
base, we can do that as follows::
$ alembic history -r :shoppingcart@head
27c6a30d7c24 -> d747a8a8879 (shoppingcart) (head), add a shopping cart column
1975ea83b712 -> 27c6a30d7c24 (shoppingcart), add shopping cart table
<base> -> 1975ea83b712 (branchpoint), add account table
We can run this operation from the "base" side as well, but we get a different
result::
$ alembic history -r shoppingcart@base:
1975ea83b712 -> ae1027a6acf (head), add a column
27c6a30d7c24 -> d747a8a8879 (shoppingcart) (head), add a shopping cart column
1975ea83b712 -> 27c6a30d7c24 (shoppingcart), add shopping cart table
<base> -> 1975ea83b712 (branchpoint), add account table
When we list from ``shoppingcart@base`` without an endpoint, it's really shorthand
for ``-r shoppingcart@base:heads``, e.g. all heads, and since ``shoppingcart@base``
is the same "base" shared by the ``ae1027a6acf`` revision, we get that
revision in our listing as well. The ``<branchname>@base`` syntax can be
useful when we are dealing with individual bases, as we'll see in the next
section.
The ``<branchname>@head`` format can also be used with revision numbers
instead of branch names, though this is less convenient. If we wanted to
add a new revision to our branch that includes the un-labeled ``ae1027a6acf``,
if this weren't a head already, we could ask for the "head of the branch
that includes ``ae1027a6acf``" as follows::
$ alembic revision -m "add another account column" --head ae10@head
Generating /Users/classic/dev/alembic/foo/versions/55af2cb1c267_add_another_account_column.py ... done
Working with Multiple Bases
---------------------------
We've seen in the previous section that ``alembic upgrade`` is fine
if we have multiple heads, ``alembic revision`` allows us to tell it which
"head" we'd like to associate our new revision file with, and branch labels
allow us to assign names to branches that we can use in subsequent commands.
Let's put all these together and refer to a new "base", that is, a whole
new tree of revision files that will be semi-independent of the account/shopping
cart revisions we've been working with.
Creating a Labeled Base Revision
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
We want to create a new, labeled branch in one step. To ensure the branch can
accommodate this label, we need to ensure our ``script.py.mako`` file, used
for generating new revision files, has the appropriate substitutions present.
If Alembic version 0.7.0 or greater was used to generate the original
migration environment, this is already done. However when working with an older
environment, ``script.py.mako`` needs to have this directive added, typically
underneath the ``down_revision`` directive::
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
# add this here in order to use revision with branch_label
branch_labels = ${repr(branch_labels)}
With this in place, we can create a new revision file, starting up a branch
that will deal with database tables involving networking; we specify the
"head" version of ``base`` as well as a ``branch_label``::
$ alembic revision -m "create networking branch" --head=base --branch-label=networking
Generating /Users/classic/dev/alembic/foo/versions/3782d9986ced_create_networking_branch.py ... done
If we ran the above command and we didn't have the newer ``script.py.mako``
directive, we'd get this error::
FAILED: Version 3cac04ae8714 specified branch_labels networking, however
the migration file foo/versions/3cac04ae8714_create_networking_branch.py
does not have them; have you upgraded your script.py.mako to include the 'branch_labels'
section?
When we receive the above error, and we would like to try again, we need to
either **delete** the incorrectly generated file in order to run ``revision``
again, *or* we can edit the ``3cac04ae8714_create_networking_branch.py``
directly to add the ``branch_labels`` in of our choosing.
Running with Multiple Bases
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Once we have a new, permanent (for as long as we desire it to be)
base in our system, we'll always have multiple heads present::
$ alembic heads
3782d9986ced (networking)
ae1027a6acf
d747a8a8879 (shoppingcart)
When we want to add a new revision file to ``networking``, we specify
``networking@head`` as the ``--head``::
$ alembic revision -m "add ip number table" --head=networking@head
Generating /Users/classic/dev/alembic/foo/versions/109ec7d132bf_add_ip_number_table.py ... done
It's important that we refer to the head using ``networking@head``; if we
only refer to ``networking``, that refers to only ``3782d9986ced`` specifically;
if we specify this and it's not a head, ``alembic revision`` will make sure
we didn't mean to specify the head::
$ alembic revision -m "add DNS table" --head=networking
FAILED: Revision 3782d9986ced is not a head revision; please
specify --splice to create a new branch from this revision
As mentioned earlier, as this base is independent, we can view its history
from the base using ``history -r networking@base:``::
$ alembic history -r networking@base:
109ec7d132bf -> 29f859a13ea (networking) (head), add DNS table
3782d9986ced -> 109ec7d132bf (networking), add ip number table
<base> -> 3782d9986ced (networking), create networking branch
Note this is the same output we'd get at this point if we used
``-r :networking@head``.
We have quite a lot of versioning going on, history overall now shows::
$ alembic history
109ec7d132bf -> 29f859a13ea (networking) (head), add DNS table
3782d9986ced -> 109ec7d132bf (networking), add ip number table
<base> -> 3782d9986ced (networking), create networking branch
ae1027a6acf -> 55af2cb1c267 (head), add another account column
1975ea83b712 -> ae1027a6acf, add a column
None -> 1975ea83b712, add account table
27c6a30d7c24 -> d747a8a8879 (shoppingcart) (head), add a shopping cart column
1975ea83b712 -> 27c6a30d7c24 (shoppingcart), add shopping cart table
<base> -> 1975ea83b712 (branchpoint), add account table
We may now run upgrades or downgrades freely, among individual branches
(let's assume a clean database again)::
$ alembic upgrade networking@head
INFO [alembic.migration] Running upgrade -> 3782d9986ced, create networking branch
INFO [alembic.migration] Running upgrade 3782d9986ced -> 109ec7d132bf, add ip number table
INFO [alembic.migration] Running upgrade 109ec7d132bf -> 29f859a13ea, add DNS table
or against the whole thing using ``heads``::
$ alembic upgrade heads
INFO [alembic.migration] Running upgrade -> 1975ea83b712, add account table
INFO [alembic.migration] Running upgrade 1975ea83b712 -> 27c6a30d7c24, add shopping cart table
INFO [alembic.migration] Running upgrade 27c6a30d7c24 -> d747a8a8879, add a shopping cart column
INFO [alembic.migration] Running upgrade 1975ea83b712 -> ae1027a6acf, add a column
INFO [alembic.migration] Running upgrade ae1027a6acf -> 55af2cb1c267, add another account column
If you actually wanted, all three branches can be merged::
$ alembic merge -m "merge all three branches" heads
Generating /Users/classic/dev/alembic/foo/versions/3180f4d6e81d_merge_all_three_branches.py ... done
$ alembic upgrade head
INFO [alembic.migration] Running upgrade 29f859a13ea, 55af2cb1c267, d747a8a8879 -> 3180f4d6e81d, merge all three branches
at which point, we're back to one head, but note! This head has **two** labels
now::
$ alembic heads
3180f4d6e81d (shoppingcart, networking)
$ alembic current --verbose
Current revision(s) for postgresql://scott:XXXXX@localhost/test:
Rev: 3180f4d6e81d (head) (mergepoint)
Merges: 29f859a13ea, 55af2cb1c267, d747a8a8879
Branch names: shoppingcart, networking
Path: foo/versions/3180f4d6e81d_merge_all_three_branches.py
merge all three branches
Revision ID: 3180f4d6e81d
Revises: 29f859a13ea, 55af2cb1c267, d747a8a8879
Create Date: 2014-11-20 16:27:56.395477
When labels are combined like this, it means that ``networking@head`` and
``shoppingcart@head`` are ultimately along the same branch, as is the
unnamed ``ae1027a6acf`` branch since we've merged everything together.
``alembic history`` when leading from ``networking@base:``,
``:shoppingcart@head`` or similar will show the whole tree at this point::
$ alembic history -r :shoppingcart@head
29f859a13ea, 55af2cb1c267, d747a8a8879 -> 3180f4d6e81d (networking, shoppingcart) (head) (mergepoint), merge all three branches
109ec7d132bf -> 29f859a13ea (networking), add DNS table
3782d9986ced -> 109ec7d132bf (networking), add ip number table
<base> -> 3782d9986ced (networking), create networking branch
ae1027a6acf -> 55af2cb1c267, add another account column
1975ea83b712 -> ae1027a6acf, add a column
27c6a30d7c24 -> d747a8a8879 (shoppingcart), add a shopping cart column
1975ea83b712 -> 27c6a30d7c24 (shoppingcart), add shopping cart table
<base> -> 1975ea83b712 (branchpoint), add account table
It follows then that the "branch labels" feature is useful for branches
that are **unmerged**. Once branches are merged into a single stream, labels
are not particularly useful as they tend to refer to the whole revision
stream in any case. They can of course be removed from revision files
at the point at which they are no longer useful, or moved to other files.
For posterity, here's the graph of the whole thing::
--- ae10 --> 55af --->--
/ \
<base> --> 1975 --> |
\ |
--- 27c6 --> d747 --> |
(shoppingcart) \ |
+--+-----> 3180
| (networking,
/ shoppingcart)
<base> --> 3782 -----> 109e ----> 29f8 --->
(networking)
If there's any point to be made here, it's if you are too freely branching, merging
and labeling, things can get pretty crazy! Hence the branching system should
be used carefully and thoughtfully for best results.
.. _building_uptodate:

View File

@ -3,8 +3,10 @@ from io import TextIOWrapper, BytesIO
from alembic.script import ScriptDirectory
from alembic.testing.fixtures import TestBase, capture_context_buffer
from alembic.testing.env import staging_env, _sqlite_testing_config, \
three_rev_fixture, clear_staging_env, _no_sql_testing_config
from alembic.testing import eq_
three_rev_fixture, clear_staging_env, _no_sql_testing_config, \
_sqlite_file_db, write_script, env_file_fixture
from alembic.testing import eq_, assert_raises_message
from alembic import util
class HistoryTest(TestBase):
@ -42,47 +44,195 @@ class HistoryTest(TestBase):
def test_history_full(self):
self.cfg.stdout = buf = self._buf_fixture()
command.history(self.cfg)
command.history(self.cfg, verbose=True)
self._eq_cmd_output(buf, [self.c, self.b, self.a])
def test_history_num_range(self):
self.cfg.stdout = buf = self._buf_fixture()
command.history(self.cfg, "%s:%s" % (self.a, self.b))
self._eq_cmd_output(buf, [self.b])
command.history(self.cfg, "%s:%s" % (self.a, self.b), verbose=True)
self._eq_cmd_output(buf, [self.b, self.a])
def test_history_base_to_num(self):
self.cfg.stdout = buf = self._buf_fixture()
command.history(self.cfg, ":%s" % (self.b))
command.history(self.cfg, ":%s" % (self.b), verbose=True)
self._eq_cmd_output(buf, [self.b, self.a])
def test_history_num_to_head(self):
self.cfg.stdout = buf = self._buf_fixture()
command.history(self.cfg, "%s:" % (self.a))
self._eq_cmd_output(buf, [self.c, self.b])
command.history(self.cfg, "%s:" % (self.a), verbose=True)
self._eq_cmd_output(buf, [self.c, self.b, self.a])
def test_history_num_plus_relative(self):
self.cfg.stdout = buf = self._buf_fixture()
command.history(self.cfg, "%s:+2" % (self.a))
self._eq_cmd_output(buf, [self.c, self.b])
command.history(self.cfg, "%s:+2" % (self.a), verbose=True)
self._eq_cmd_output(buf, [self.c, self.b, self.a])
def test_history_relative_to_num(self):
self.cfg.stdout = buf = self._buf_fixture()
command.history(self.cfg, "-2:%s" % (self.c))
self._eq_cmd_output(buf, [self.c, self.b])
command.history(self.cfg, "-2:%s" % (self.c), verbose=True)
self._eq_cmd_output(buf, [self.c, self.b, self.a])
def test_history_too_large_relative_to_num(self):
self.cfg.stdout = buf = self._buf_fixture()
command.history(self.cfg, "-5:%s" % (self.c), verbose=True)
self._eq_cmd_output(buf, [self.c, self.b, self.a])
def test_history_current_to_head_as_b(self):
command.stamp(self.cfg, self.b)
self.cfg.stdout = buf = self._buf_fixture()
command.history(self.cfg, "current:")
self._eq_cmd_output(buf, [self.c])
command.history(self.cfg, "current:", verbose=True)
self._eq_cmd_output(buf, [self.c, self.b])
def test_history_current_to_head_as_base(self):
command.stamp(self.cfg, "base")
self.cfg.stdout = buf = self._buf_fixture()
command.history(self.cfg, "current:")
command.history(self.cfg, "current:", verbose=True)
self._eq_cmd_output(buf, [self.c, self.b, self.a])
class RevisionTest(TestBase):
def setUp(self):
self.env = staging_env()
self.cfg = _sqlite_testing_config()
def tearDown(self):
clear_staging_env()
def _env_fixture(self):
env_file_fixture("""
from sqlalchemy import MetaData, engine_from_config
target_metadata = MetaData()
engine = engine_from_config(
config.get_section(config.config_ini_section),
prefix='sqlalchemy.')
connection = engine.connect()
context.configure(connection=connection, target_metadata=target_metadata)
try:
with context.begin_transaction():
context.run_migrations()
finally:
connection.close()
""")
def test_create_rev_plain_db_not_up_to_date(self):
self._env_fixture()
command.revision(self.cfg)
command.revision(self.cfg) # no problem
def test_create_rev_autogen(self):
self._env_fixture()
command.revision(self.cfg, autogenerate=True)
def test_create_rev_autogen_db_not_up_to_date(self):
self._env_fixture()
command.revision(self.cfg)
assert_raises_message(
util.CommandError,
"Target database is not up to date.",
command.revision, self.cfg, autogenerate=True
)
def test_create_rev_autogen_db_not_up_to_date_multi_heads(self):
self._env_fixture()
command.revision(self.cfg)
rev2 = command.revision(self.cfg)
rev3a = command.revision(self.cfg)
command.revision(self.cfg, head=rev2.revision, splice=True)
command.upgrade(self.cfg, "heads")
command.revision(self.cfg, head=rev3a.revision)
assert_raises_message(
util.CommandError,
"Target database is not up to date.",
command.revision, self.cfg, autogenerate=True
)
def test_create_rev_plain_db_not_up_to_date_multi_heads(self):
self._env_fixture()
command.revision(self.cfg)
rev2 = command.revision(self.cfg)
rev3a = command.revision(self.cfg)
command.revision(self.cfg, head=rev2.revision, splice=True)
command.upgrade(self.cfg, "heads")
command.revision(self.cfg, head=rev3a.revision)
assert_raises_message(
util.CommandError,
"Multiple heads are present; please specify the head revision "
"on which the new revision should be based, or perform a merge.",
command.revision, self.cfg
)
def test_create_rev_autogen_need_to_select_head(self):
self._env_fixture()
command.revision(self.cfg)
rev2 = command.revision(self.cfg)
command.revision(self.cfg)
command.revision(self.cfg, head=rev2.revision, splice=True)
command.upgrade(self.cfg, "heads")
# there's multiple heads present
assert_raises_message(
util.CommandError,
"Multiple heads are present; please specify the head revision "
"on which the new revision should be based, or perform a merge.",
command.revision, self.cfg, autogenerate=True
)
def test_create_rev_plain_need_to_select_head(self):
self._env_fixture()
command.revision(self.cfg)
rev2 = command.revision(self.cfg)
command.revision(self.cfg)
command.revision(self.cfg, head=rev2.revision, splice=True)
command.upgrade(self.cfg, "heads")
# there's multiple heads present
assert_raises_message(
util.CommandError,
"Multiple heads are present; please specify the head revision "
"on which the new revision should be based, or perform a merge.",
command.revision, self.cfg
)
def test_create_rev_plain_post_merge(self):
self._env_fixture()
command.revision(self.cfg)
rev2 = command.revision(self.cfg)
command.revision(self.cfg)
command.revision(self.cfg, head=rev2.revision, splice=True)
command.merge(self.cfg, "heads")
command.revision(self.cfg)
def test_create_rev_autogenerate_post_merge(self):
self._env_fixture()
command.revision(self.cfg)
rev2 = command.revision(self.cfg)
command.revision(self.cfg)
command.revision(self.cfg, head=rev2.revision, splice=True)
command.merge(self.cfg, "heads")
command.upgrade(self.cfg, "heads")
command.revision(self.cfg, autogenerate=True)
def test_create_rev_autogenerate_db_not_up_to_date_post_merge(self):
self._env_fixture()
command.revision(self.cfg)
rev2 = command.revision(self.cfg)
command.revision(self.cfg)
command.revision(self.cfg, head=rev2.revision, splice=True)
command.upgrade(self.cfg, "heads")
command.merge(self.cfg, "heads")
assert_raises_message(
util.CommandError,
"Target database is not up to date.",
command.revision, self.cfg, autogenerate=True
)
class UpgradeDowngradeStampTest(TestBase):
def setUp(self):
@ -134,9 +284,68 @@ class UpgradeDowngradeStampTest(TestBase):
assert "DROP STEP 2" in buf.getvalue()
assert "DROP STEP 1" not in buf.getvalue()
def test_stamp(self):
def test_sql_stamp_from_rev(self):
with capture_context_buffer() as buf:
command.stamp(self.cfg, "head", sql=True)
assert "UPDATE alembic_version "\
"SET version_num='%s';" % self.c in buf.getvalue()
command.stamp(self.cfg, "%s:head" % self.a, sql=True)
assert (
"UPDATE alembic_version "
"SET version_num='%s' "
"WHERE alembic_version.version_num = '%s';" % (self.c, self.a)
) in buf.getvalue()
def test_sql_stamp_from_partial_rev(self):
with capture_context_buffer() as buf:
command.stamp(self.cfg, "%s:head" % self.a[0:3], sql=True)
assert (
"UPDATE alembic_version "
"SET version_num='%s' "
"WHERE alembic_version.version_num = '%s';" % (self.c, self.a)
) in buf.getvalue()
class LiveStampTest(TestBase):
__only_on__ = 'sqlite'
def setUp(self):
self.bind = _sqlite_file_db()
self.env = staging_env()
self.cfg = _sqlite_testing_config()
self.a = a = util.rev_id()
self.b = b = util.rev_id()
script = ScriptDirectory.from_config(self.cfg)
script.generate_revision(a, None, refresh=True)
write_script(script, a, """
revision = '%s'
down_revision = None
""" % a)
script.generate_revision(b, None, refresh=True)
write_script(script, b, """
revision = '%s'
down_revision = '%s'
""" % (b, a))
def tearDown(self):
clear_staging_env()
def test_stamp_creates_table(self):
command.stamp(self.cfg, "head")
eq_(
self.bind.scalar("select version_num from alembic_version"),
self.b
)
def test_stamp_existing_upgrade(self):
command.stamp(self.cfg, self.a)
command.stamp(self.cfg, self.b)
eq_(
self.bind.scalar("select version_num from alembic_version"),
self.b
)
def test_stamp_existing_downgrade(self):
command.stamp(self.cfg, self.b)
command.stamp(self.cfg, self.a)
eq_(
self.bind.scalar("select version_num from alembic_version"),
self.a
)

View File

@ -1,11 +1,11 @@
from alembic.testing.fixtures import TestBase
from alembic.testing.fixtures import TestBase, capture_context_buffer
from alembic import command, util
from alembic.testing import assert_raises_message
from alembic.testing.env import staging_env, _no_sql_testing_config, \
three_rev_fixture, clear_staging_env, env_file_fixture
import re
a = b = c = None
@ -53,14 +53,14 @@ assert context.get_starting_revision_argument() == 'x'
command.upgrade(self.cfg, "x:y", sql=True)
command.downgrade(self.cfg, "x:y", sql=True)
def test_starting_rev_pre_context_stamp(self):
def test_starting_rev_pre_context_cmd_w_no_startrev(self):
env_file_fixture("""
assert context.get_starting_revision_argument() == 'x'
""")
assert_raises_message(
util.CommandError,
"No starting revision argument is available.",
command.stamp, self.cfg, a)
command.current, self.cfg)
def test_starting_rev_current_pre_context(self):
env_file_fixture("""
@ -165,3 +165,13 @@ assert not context.requires_connection()
""")
command.upgrade(self.cfg, a, sql=True)
command.downgrade(self.cfg, "%s:%s" % (b, a), sql=True)
def test_running_comments_not_in_sql(self):
message = "this is a very long \nand multiline\nmessage"
d = command.revision(self.cfg, message=message)
with capture_context_buffer(transactional_ddl=True) as buf:
command.upgrade(self.cfg, "%s:%s" % (a, d.revision), sql=True)
assert not re.match(r".*-- .*and multiline", buf.getvalue(), re.S | re.M)

717
tests/test_revision.py Normal file
View File

@ -0,0 +1,717 @@
from alembic.testing.fixtures import TestBase
from alembic.testing import eq_, assert_raises_message
from alembic.revision import RevisionMap, Revision, MultipleHeads, \
RevisionError
class APITest(TestBase):
def test_add_revision_one_head(self):
map_ = RevisionMap(
lambda: [
Revision('a', ()),
Revision('b', ('a',)),
Revision('c', ('b',)),
]
)
eq_(map_.heads, ('c', ))
map_.add_revision(Revision('d', ('c', )))
eq_(map_.heads, ('d', ))
def test_add_revision_two_head(self):
map_ = RevisionMap(
lambda: [
Revision('a', ()),
Revision('b', ('a',)),
Revision('c1', ('b',)),
Revision('c2', ('b',)),
]
)
eq_(map_.heads, ('c1', 'c2'))
map_.add_revision(Revision('d1', ('c1', )))
eq_(map_.heads, ('c2', 'd1'))
def test_get_revision_head_single(self):
map_ = RevisionMap(
lambda: [
Revision('a', ()),
Revision('b', ('a',)),
Revision('c', ('b',)),
]
)
eq_(map_.get_revision('head'), map_._revision_map['c'])
def test_get_revision_base_single(self):
map_ = RevisionMap(
lambda: [
Revision('a', ()),
Revision('b', ('a',)),
Revision('c', ('b',)),
]
)
eq_(map_.get_revision('base'), None)
def test_get_revision_head_multiple(self):
map_ = RevisionMap(
lambda: [
Revision('a', ()),
Revision('b', ('a',)),
Revision('c1', ('b',)),
Revision('c2', ('b',)),
]
)
assert_raises_message(
MultipleHeads,
"Multiple heads are present",
map_.get_revision, 'head'
)
def test_get_revision_heads_multiple(self):
map_ = RevisionMap(
lambda: [
Revision('a', ()),
Revision('b', ('a',)),
Revision('c1', ('b',)),
Revision('c2', ('b',)),
]
)
assert_raises_message(
MultipleHeads,
"Multiple heads are present",
map_.get_revision, "heads"
)
def test_get_revision_base_multiple(self):
map_ = RevisionMap(
lambda: [
Revision('a', ()),
Revision('b', ('a',)),
Revision('c', ()),
Revision('d', ('c',)),
]
)
eq_(map_.get_revision('base'), None)
class DownIterateTest(TestBase):
def _assert_iteration(
self, upper, lower, assertion, inclusive=True, map_=None,
implicit_base=False):
if map_ is None:
map_ = self.map
eq_(
[
rev.revision for rev in
map_.iterate_revisions(
upper, lower,
inclusive=inclusive, implicit_base=implicit_base
)
],
assertion
)
class DiamondTest(DownIterateTest):
def setUp(self):
self.map = RevisionMap(
lambda: [
Revision('a', ()),
Revision('b1', ('a',)),
Revision('b2', ('a',)),
Revision('c', ('b1', 'b2')),
Revision('d', ('c',)),
]
)
def test_iterate_simple_diamond(self):
self._assert_iteration(
"d", "a",
["d", "c", "b1", "b2", "a"]
)
class LabeledBranchTest(DownIterateTest):
def test_dupe_branch_collection(self):
fn = lambda: [
Revision('a', ()),
Revision('b', ('a',)),
Revision('c', ('b',), branch_labels=['xy1']),
Revision('d', ()),
Revision('e', ('d',), branch_labels=['xy1']),
Revision('f', ('e',))
]
assert_raises_message(
RevisionError,
r"Branch name 'xy1' in revision (?:e|c) already "
"used by revision (?:e|c)",
getattr, RevisionMap(fn), "_revision_map"
)
def test_filter_for_lineage_labeled_head_across_merge(self):
fn = lambda: [
Revision('a', ()),
Revision('b', ('a', )),
Revision('c1', ('b', ), branch_labels='c1branch'),
Revision('c2', ('b', )),
Revision('d', ('c1', 'c2')),
]
map_ = RevisionMap(fn)
c1 = map_.get_revision('c1')
c2 = map_.get_revision('c2')
d = map_.get_revision('d')
eq_(
map_.filter_for_lineage([c1, c2, d], "c1branch@head"),
[c1, c2, d]
)
def setUp(self):
self.map = RevisionMap(lambda: [
Revision('a', (), branch_labels='abranch'),
Revision('b', ('a',)),
Revision('somelongername', ('b',)),
Revision('c', ('somelongername',)),
Revision('d', ()),
Revision('e', ('d',), branch_labels=['ebranch']),
Revision('someothername', ('e',)),
Revision('f', ('someothername',)),
])
def test_get_base_revisions_labeled(self):
eq_(
self.map._get_base_revisions("somelongername@base"),
['a']
)
def test_get_current_named_rev(self):
eq_(
self.map.get_revision("ebranch@head"),
self.map.get_revision("f")
)
def test_get_base_revisions(self):
eq_(
self.map._get_base_revisions("base"),
['a', 'd']
)
def test_iterate_head_to_named_base(self):
self._assert_iteration(
"heads", "ebranch@base",
['f', 'someothername', 'e', 'd']
)
self._assert_iteration(
"heads", "abranch@base",
['c', 'somelongername', 'b', 'a']
)
def test_iterate_named_head_to_base(self):
self._assert_iteration(
"ebranch@head", "base",
['f', 'someothername', 'e', 'd']
)
self._assert_iteration(
"abranch@head", "base",
['c', 'somelongername', 'b', 'a']
)
def test_iterate_named_head_to_heads(self):
self._assert_iteration(
"heads", "ebranch@head",
['f'],
inclusive=True
)
def test_iterate_named_rev_to_heads(self):
self._assert_iteration(
"heads", "ebranch@d",
['f', 'someothername', 'e', 'd'],
inclusive=True
)
def test_iterate_head_to_version_specific_base(self):
self._assert_iteration(
"heads", "e@base",
['f', 'someothername', 'e', 'd']
)
self._assert_iteration(
"heads", "c@base",
['c', 'somelongername', 'b', 'a']
)
def test_iterate_to_branch_at_rev(self):
self._assert_iteration(
"heads", "ebranch@d",
['f', 'someothername', 'e', 'd']
)
def test_branch_w_down_relative(self):
self._assert_iteration(
"heads", "ebranch@-2",
['f', 'someothername', 'e']
)
def test_branch_w_up_relative(self):
self._assert_iteration(
"ebranch@+2", "base",
['someothername', 'e', 'd']
)
def test_partial_id_resolve(self):
eq_(self.map.get_revision("ebranch@some").revision, "someothername")
eq_(self.map.get_revision("abranch@some").revision, "somelongername")
def test_branch_at_heads(self):
eq_(
self.map.get_revision("abranch@heads").revision,
"c"
)
def test_branch_at_syntax(self):
eq_(self.map.get_revision("abranch@head").revision, 'c')
eq_(self.map.get_revision("abranch@base"), None)
eq_(self.map.get_revision("ebranch@head").revision, 'f')
eq_(self.map.get_revision("abranch@base"), None)
eq_(self.map.get_revision("ebranch@d").revision, 'd')
def test_branch_at_self(self):
eq_(self.map.get_revision("ebranch@ebranch").revision, 'e')
def test_retrieve_branch_revision(self):
eq_(self.map.get_revision("abranch").revision, 'a')
eq_(self.map.get_revision("ebranch").revision, 'e')
def test_rev_not_in_branch(self):
assert_raises_message(
RevisionError,
"Revision b is not a member of branch 'ebranch'",
self.map.get_revision, "ebranch@b"
)
assert_raises_message(
RevisionError,
"Revision d is not a member of branch 'abranch'",
self.map.get_revision, "abranch@d"
)
def test_no_revision_exists(self):
assert_raises_message(
RevisionError,
"No such revision or branch 'q'",
self.map.get_revision, "abranch@q"
)
def test_not_actually_a_branch(self):
eq_(self.map.get_revision("e@d").revision, "d")
def test_not_actually_a_branch_partial_resolution(self):
eq_(self.map.get_revision("someoth@d").revision, "d")
def test_no_such_branch(self):
assert_raises_message(
RevisionError,
"No such branch: 'x'",
self.map.get_revision, "x@d"
)
class LongShortBranchTest(DownIterateTest):
def setUp(self):
self.map = RevisionMap(
lambda: [
Revision('a', ()),
Revision('b1', ('a',)),
Revision('b2', ('a',)),
Revision('c1', ('b1',)),
Revision('d11', ('c1',)),
Revision('d12', ('c1',)),
]
)
def test_iterate_full(self):
self._assert_iteration(
"heads", "base",
['b2', 'd11', 'd12', 'c1', 'b1', 'a']
)
class MultipleBranchTest(DownIterateTest):
def setUp(self):
self.map = RevisionMap(
lambda: [
Revision('a', ()),
Revision('b1', ('a',)),
Revision('b2', ('a',)),
Revision('cb1', ('b1',)),
Revision('cb2', ('b2',)),
Revision('d1cb1', ('cb1',)), # head
Revision('d2cb1', ('cb1',)), # head
Revision('d1cb2', ('cb2',)),
Revision('d2cb2', ('cb2',)),
Revision('d3cb2', ('cb2',)), # head
Revision('d1d2cb2', ('d1cb2', 'd2cb2')) # head + merge point
]
)
def test_iterate_from_merge_point(self):
self._assert_iteration(
"d1d2cb2", "a",
['d1d2cb2', 'd1cb2', 'd2cb2', 'cb2', 'b2', 'a']
)
def test_iterate_multiple_heads(self):
self._assert_iteration(
["d2cb2", "d3cb2"], "a",
['d2cb2', 'd3cb2', 'cb2', 'b2', 'a']
)
def test_iterate_single_branch(self):
self._assert_iteration(
"d3cb2", "a",
['d3cb2', 'cb2', 'b2', 'a']
)
def test_iterate_single_branch_to_base(self):
self._assert_iteration(
"d3cb2", "base",
['d3cb2', 'cb2', 'b2', 'a']
)
def test_iterate_multiple_branch_to_base(self):
self._assert_iteration(
["d3cb2", "cb1"], "base",
['d3cb2', 'cb2', 'b2', 'cb1', 'b1', 'a']
)
def test_iterate_multiple_heads_single_base(self):
# head d1cb1 is omitted as it is not
# a descendant of b2
self._assert_iteration(
["d1cb1", "d2cb2", "d3cb2"], "b2",
["d2cb2", 'd3cb2', 'cb2', 'b2']
)
def test_same_branch_wrong_direction(self):
# nodes b1 and d1cb1 are connected, but
# db1cb1 is the descendant of b1
assert_raises_message(
RevisionError,
r"Revision d1cb1 is not an ancestor of revision b1",
list,
self.map._iterate_revisions('b1', 'd1cb1')
)
def test_distinct_branches(self):
# nodes db2cb2 and b1 have no path to each other
assert_raises_message(
RevisionError,
r"Revision b1 is not an ancestor of revision d2cb2",
list,
self.map._iterate_revisions('d2cb2', 'b1')
)
def test_wrong_direction_to_base(self):
assert_raises_message(
RevisionError,
r"Revision d1cb1 is not an ancestor of revision base",
list,
self.map._iterate_revisions(None, 'd1cb1')
)
assert_raises_message(
RevisionError,
r"Revision d1cb1 is not an ancestor of revision base",
list,
self.map._iterate_revisions((), 'd1cb1')
)
class BranchTravellingTest(DownIterateTest):
"""test the order of revs when going along multiple branches.
We want depth-first along branches, but then we want to
terminate all branches at their branch point before continuing
to the nodes preceding that branch.
"""
def setUp(self):
self.map = RevisionMap(
lambda: [
Revision('a1', ()),
Revision('a2', ('a1',)),
Revision('a3', ('a2',)),
Revision('b1', ('a3',)),
Revision('b2', ('a3',)),
Revision('cb1', ('b1',)),
Revision('cb2', ('b2',)),
Revision('db1', ('cb1',)),
Revision('db2', ('cb2',)),
Revision('e1b1', ('db1',)),
Revision('fe1b1', ('e1b1',)),
Revision('e2b1', ('db1',)),
Revision('e2b2', ('db2',)),
Revision("merge", ('e2b1', 'e2b2'))
]
)
def test_iterate_one_branch_both_to_merge(self):
# test that when we hit a merge point, implicit base will
# ensure all branches that supply the merge point are filled in
self._assert_iteration(
"merge", "db1",
['merge',
'e2b1', 'db1',
'e2b2', 'db2', 'cb2', 'b2'],
implicit_base=True
)
def test_three_branches_end_in_single_branch(self):
self._assert_iteration(
["merge", "fe1b1"], "a3",
['merge', 'e2b1', 'e2b2', 'db2', 'cb2', 'b2',
'fe1b1', 'e1b1', 'db1', 'cb1', 'b1', 'a3']
)
def test_two_branches_to_root(self):
# here we want 'a3' as a "stop" branch point, but *not*
# 'db1', as we don't have multiple traversals on db1
self._assert_iteration(
"merge", "a1",
['merge',
'e2b1', 'db1', 'cb1', 'b1', # e2b1 branch
'e2b2', 'db2', 'cb2', 'b2', # e2b2 branch
'a3', # both terminate at a3
'a2', 'a1' # finish out
] # noqa
)
def test_two_branches_end_in_branch(self):
self._assert_iteration(
"merge", "b1",
# 'b1' is local to 'e2b1'
# branch so that is all we get
['merge', 'e2b1', 'db1', 'cb1', 'b1',
] # noqa
)
def test_two_branches_end_behind_branch(self):
self._assert_iteration(
"merge", "a2",
['merge',
'e2b1', 'db1', 'cb1', 'b1', # e2b1 branch
'e2b2', 'db2', 'cb2', 'b2', # e2b2 branch
'a3', # both terminate at a3
'a2'
] # noqa
)
def test_three_branches_to_root(self):
# in this case, both "a3" and "db1" are stop points
self._assert_iteration(
["merge", "fe1b1"], "a1",
['merge',
'e2b1', # e2b1 branch
'e2b2', 'db2', 'cb2', 'b2', # e2b2 branch
'fe1b1', 'e1b1', # fe1b1 branch
'db1', # fe1b1 and e2b1 branches terminate at db1
'cb1', 'b1', # e2b1 branch continued....might be nicer
# if this was before the e2b2 branch...
'a3', # e2b1 and e2b2 branches terminate at a3
'a2', 'a1' # finish out
] # noqa
)
def test_three_branches_end_multiple_bases(self):
# in this case, both "a3" and "db1" are stop points
self._assert_iteration(
["merge", "fe1b1"], ["cb1", "cb2"],
[
'merge',
'e2b1',
'e2b2', 'db2', 'cb2',
'fe1b1', 'e1b1',
'db1',
'cb1'
]
)
def test_three_branches_end_multiple_bases_exclusive(self):
self._assert_iteration(
["merge", "fe1b1"], ["cb1", "cb2"],
[
'merge',
'e2b1',
'e2b2', 'db2',
'fe1b1', 'e1b1',
'db1',
],
inclusive=False
)
def test_detect_invalid_head_selection(self):
# db1 is an ancestor of fe1b1
assert_raises_message(
RevisionError,
"Requested revision fe1b1 overlaps "
"with other requested revisions",
list,
self.map._iterate_revisions(["db1", "b2", "fe1b1"], ())
)
def test_three_branches_end_multiple_bases_exclusive_blank(self):
self._assert_iteration(
["e2b1", "b2", "fe1b1"], (),
[
'e2b1',
'b2',
'fe1b1', 'e1b1',
'db1', 'cb1', 'b1', 'a3', 'a2', 'a1'
],
inclusive=False
)
def test_iterate_to_symbolic_base(self):
self._assert_iteration(
["fe1b1"], "base",
['fe1b1', 'e1b1', 'db1', 'cb1', 'b1', 'a3', 'a2', 'a1'],
inclusive=False
)
class MultipleBaseTest(DownIterateTest):
def setUp(self):
self.map = RevisionMap(
lambda: [
Revision('base1', ()),
Revision('base2', ()),
Revision('base3', ()),
Revision('a1a', ('base1',)),
Revision('a1b', ('base1',)),
Revision('a2', ('base2',)),
Revision('a3', ('base3',)),
Revision('b1a', ('a1a',)),
Revision('b1b', ('a1b',)),
Revision('b2', ('a2',)),
Revision('b3', ('a3',)),
Revision('c2', ('b2',)),
Revision('d2', ('c2',)),
Revision('mergeb3d2', ('b3', 'd2'))
]
)
def test_heads_to_base(self):
self._assert_iteration(
"heads", "base",
[
'b1a', 'a1a',
'b1b', 'a1b',
'mergeb3d2',
'b3', 'a3', 'base3',
'd2', 'c2', 'b2', 'a2', 'base2',
'base1'
]
)
def test_heads_to_base_exclusive(self):
self._assert_iteration(
"heads", "base",
[
'b1a', 'a1a',
'b1b', 'a1b',
'mergeb3d2',
'b3', 'a3', 'base3',
'd2', 'c2', 'b2', 'a2', 'base2',
'base1',
],
inclusive=False
)
def test_heads_to_blank(self):
self._assert_iteration(
"heads", None,
[
'b1a', 'a1a',
'b1b', 'a1b',
'mergeb3d2',
'b3', 'a3', 'base3',
'd2', 'c2', 'b2', 'a2', 'base2',
'base1'
]
)
def test_detect_invalid_base_selection(self):
assert_raises_message(
RevisionError,
"Requested revision a2 overlaps with "
"other requested revisions",
list,
self.map._iterate_revisions(["c2"], ["a2", "b2"])
)
def test_heads_to_revs_plus_implicit_base_exclusive(self):
self._assert_iteration(
"heads", ["c2"],
[
'b1a', 'a1a',
'b1b', 'a1b',
'mergeb3d2',
'b3', 'a3', 'base3',
'd2',
'base1'
],
inclusive=False,
implicit_base=True
)
def test_heads_to_revs_base_exclusive(self):
self._assert_iteration(
"heads", ["c2"],
[
'mergeb3d2', 'd2'
],
inclusive=False
)
def test_heads_to_revs_plus_implicit_base_inclusive(self):
self._assert_iteration(
"heads", ["c2"],
[
'b1a', 'a1a',
'b1b', 'a1b',
'mergeb3d2',
'b3', 'a3', 'base3',
'd2', 'c2',
'base1'
],
implicit_base=True
)
def test_specific_path_one(self):
self._assert_iteration(
"b3", "base3",
['b3', 'a3', 'base3']
)
def test_specific_path_two_implicit_base(self):
self._assert_iteration(
["b3", "b2"], "base3",
['b3', 'a3', 'b2', 'a2', 'base2'],
inclusive=False, implicit_base=True
)

View File

@ -229,7 +229,7 @@ class VersionNameTemplateTest(TestBase):
""" % a)
script = ScriptDirectory.from_config(self.cfg)
rev = script._get_rev(a)
rev = script.get_revision(a)
eq_(rev.revision, a)
eq_(os.path.basename(rev.path), "myfile_some_message.py")
@ -252,7 +252,7 @@ class VersionNameTemplateTest(TestBase):
""")
script = ScriptDirectory.from_config(self.cfg)
rev = script._get_rev(a)
rev = script.get_revision(a)
eq_(rev.revision, a)
eq_(os.path.basename(rev.path), "%s.py" % a)
@ -262,7 +262,7 @@ class VersionNameTemplateTest(TestBase):
a = util.rev_id()
script.generate_revision(a, "foobar", refresh=True)
path = script._revision_map[a].path
path = script.get_revision(a).path
with open(path, 'w') as fp:
fp.write("""
down_revision = None

View File

@ -1,8 +1,9 @@
from alembic.testing.fixtures import TestBase
from alembic.testing import eq_, ne_, is_
from alembic.testing import eq_, ne_, is_, assert_raises_message
from alembic.testing.env import clear_staging_env, staging_env, \
_get_staging_directory, _no_sql_testing_config, env_file_fixture, \
script_file_fixture, _testing_config
script_file_fixture, _testing_config, _sqlite_testing_config, \
three_rev_fixture
from alembic import command
from alembic.script import ScriptDirectory
from alembic.environment import EnvironmentContext
@ -72,7 +73,7 @@ class GeneralOrderedTests(TestBase):
'%s_this_is_the_next_rev.py' % def_), os.F_OK)
eq_(script.revision, def_)
eq_(script.down_revision, abc)
eq_(env._revision_map[abc].nextrev, set([def_]))
eq_(env.get_revision(abc).nextrev, set([def_]))
assert script.module.down_revision == abc
assert callable(script.module.upgrade)
assert callable(script.module.downgrade)
@ -84,8 +85,8 @@ class GeneralOrderedTests(TestBase):
# new ScriptDirectory instance.
env = staging_env(create=False)
abc_rev = env._revision_map[abc]
def_rev = env._revision_map[def_]
abc_rev = env.get_revision(abc)
def_rev = env.get_revision(def_)
eq_(abc_rev.nextrev, set([def_]))
eq_(abc_rev.revision, abc)
eq_(def_rev.down_revision, abc)
@ -97,7 +98,7 @@ class GeneralOrderedTests(TestBase):
script = env.generate_revision(rid, "dont' refresh")
is_(script, None)
env2 = staging_env(create=False)
eq_(env2._as_rev_number("head"), rid)
eq_(env2.get_current_head(), rid)
def _test_008_long_name(self):
rid = util.rev_id()
@ -151,6 +152,74 @@ class ScriptNamingTest(TestBase):
)
class RevisionCommandTest(TestBase):
def setUp(self):
self.env = staging_env()
self.cfg = _sqlite_testing_config()
self.a, self.b, self.c = three_rev_fixture(self.cfg)
def tearDown(self):
clear_staging_env()
def test_create_script_basic(self):
rev = command.revision(self.cfg, message="some message")
script = ScriptDirectory.from_config(self.cfg)
rev = script.get_revision(rev.revision)
eq_(rev.down_revision, self.c)
assert "some message" in rev.doc
def test_create_script_splice(self):
rev = command.revision(
self.cfg, message="some message", head=self.b, splice=True)
script = ScriptDirectory.from_config(self.cfg)
rev = script.get_revision(rev.revision)
eq_(rev.down_revision, self.b)
assert "some message" in rev.doc
eq_(set(script.get_heads()), set([rev.revision, self.c]))
def test_create_script_missing_splice(self):
assert_raises_message(
util.CommandError,
"Revision %s is not a head revision; please specify --splice "
"to create a new branch from this revision" % self.b,
command.revision,
self.cfg, message="some message", head=self.b
)
def test_create_script_branches(self):
rev = command.revision(
self.cfg, message="some message", branch_label="foobar")
script = ScriptDirectory.from_config(self.cfg)
rev = script.get_revision(rev.revision)
eq_(script.get_revision("foobar"), rev)
def test_create_script_branches_old_template(self):
script = ScriptDirectory.from_config(self.cfg)
with open(os.path.join(script.dir, "script.py.mako"), "w") as file_:
file_.write(
"<%text>#</%text> ${message}\n"
"revision = ${repr(up_revision)}\n"
"down_revision = ${repr(down_revision)}\n"
"def upgrade():\n"
" ${upgrades if upgrades else 'pass'}\n\n"
"def downgrade():\n"
" ${downgrade if downgrades else 'pass'}\n\n"
)
# works OK if no branch names
command.revision(self.cfg, message="some message")
assert_raises_message(
util.CommandError,
r"Version \w+ specified branch_labels foobar, "
r"however the migration file .+?\b does not have them; have you "
"upgraded your script.py.mako to include the 'branch_labels' "
r"section\?",
command.revision,
self.cfg, message="some message", branch_label="foobar"
)
class TemplateArgsTest(TestBase):
def setUp(self):

View File

@ -1,9 +1,10 @@
from alembic.testing.fixtures import TestBase
from alembic.testing import config, eq_, assert_raises
from alembic.testing import config, eq_, assert_raises, assert_raises_message
from sqlalchemy import Table, MetaData, Column, String
from sqlalchemy.engine.reflection import Inspector
from alembic import migration
from alembic.util import CommandError
@ -11,6 +12,18 @@ version_table = Table('version_table', MetaData(),
Column('version_num', String(32), nullable=False))
def _up(from_, to_, branch_presence_changed=False):
return migration.StampStep(
from_, to_, True, branch_presence_changed
)
def _down(from_, to_, branch_presence_changed=False):
return migration.StampStep(
from_, to_, False, branch_presence_changed
)
class TestMigrationContext(TestBase):
@classmethod
@ -27,8 +40,7 @@ class TestMigrationContext(TestBase):
self.connection.close()
def make_one(self, **kwargs):
from alembic.migration import MigrationContext
return MigrationContext.configure(**kwargs)
return migration.MigrationContext.configure(**kwargs)
def get_revision(self):
result = self.connection.execute(version_table.select())
@ -52,12 +64,12 @@ class TestMigrationContext(TestBase):
opts={'version_table_schema': 'explicit'})
eq_(context._version.schema, 'explicit')
def test_get_current_revision_creates_version_table(self):
def test_get_current_revision_doesnt_create_version_table(self):
context = self.make_one(connection=self.connection,
opts={'version_table': 'version_table'})
eq_(context.get_current_revision(), None)
insp = Inspector(self.connection)
assert ('version_table' in insp.get_table_names())
assert ('version_table' not in insp.get_table_names())
def test_get_current_revision(self):
context = self.make_one(connection=self.connection,
@ -82,14 +94,161 @@ class TestMigrationContext(TestBase):
'as_sql': True})
eq_(context.get_current_revision(), 'startrev')
def test__update_current_rev(self):
def test_get_current_revision_multiple_heads(self):
version_table.create(self.connection)
context = self.make_one(connection=self.connection,
opts={'version_table': 'version_table'})
updater = migration.HeadMaintainer(context, ())
updater.update_to_step(_up(None, 'a', True))
updater.update_to_step(_up(None, 'b', True))
assert_raises_message(
CommandError,
"Version table 'version_table' has more than one head present; "
"please use get_current_heads()",
context.get_current_revision
)
def test_get_heads(self):
version_table.create(self.connection)
context = self.make_one(connection=self.connection,
opts={'version_table': 'version_table'})
updater = migration.HeadMaintainer(context, ())
updater.update_to_step(_up(None, 'a', True))
updater.update_to_step(_up(None, 'b', True))
eq_(context.get_current_heads(), ('a', 'b'))
def test_get_heads_offline(self):
version_table.create(self.connection)
context = self.make_one(connection=self.connection,
opts={
'starting_rev': 'q',
'version_table': 'version_table',
'as_sql': True})
eq_(context.get_current_heads(), ('q', ))
class UpdateRevTest(TestBase):
@classmethod
def setup_class(cls):
cls.bind = config.db
def setUp(self):
self.connection = self.bind.connect()
self.context = migration.MigrationContext.configure(
connection=self.connection,
opts={"version_table": "version_table"})
version_table.create(self.connection)
self.updater = migration.HeadMaintainer(self.context, ())
def tearDown(self):
version_table.drop(self.connection, checkfirst=True)
self.connection.close()
def _assert_heads(self, heads):
eq_(self.context.get_current_heads(), heads)
eq_(self.updater.heads, set(heads))
def test_update_none_to_single(self):
self.updater.update_to_step(_up(None, 'a', True))
self._assert_heads(('a',))
def test_update_single_to_single(self):
self.updater.update_to_step(_up(None, 'a', True))
self.updater.update_to_step(_up('a', 'b'))
self._assert_heads(('b',))
def test_update_single_to_none(self):
self.updater.update_to_step(_up(None, 'a', True))
self.updater.update_to_step(_down('a', None, True))
self._assert_heads(())
def test_add_branches(self):
self.updater.update_to_step(_up(None, 'a', True))
self.updater.update_to_step(_up('a', 'b'))
self.updater.update_to_step(_up(None, 'c', True))
self._assert_heads(('b', 'c'))
self.updater.update_to_step(_up('c', 'd'))
self.updater.update_to_step(_up('d', 'e1'))
self.updater.update_to_step(_up('d', 'e2', True))
self._assert_heads(('b', 'e1', 'e2'))
def test_teardown_branches(self):
self.updater.update_to_step(_up(None, 'd1', True))
self.updater.update_to_step(_up(None, 'd2', True))
self._assert_heads(('d1', 'd2'))
self.updater.update_to_step(_down('d1', 'c'))
self._assert_heads(('c', 'd2'))
self.updater.update_to_step(_down('d2', 'c', True))
self._assert_heads(('c',))
self.updater.update_to_step(_down('c', 'b'))
self._assert_heads(('b',))
def test_resolve_merges(self):
self.updater.update_to_step(_up(None, 'a', True))
self.updater.update_to_step(_up('a', 'b'))
self.updater.update_to_step(_up('b', 'c1'))
self.updater.update_to_step(_up('b', 'c2', True))
self.updater.update_to_step(_up('c1', 'd1'))
self.updater.update_to_step(_up('c2', 'd2'))
self._assert_heads(('d1', 'd2'))
self.updater.update_to_step(_up(('d1', 'd2'), 'e'))
self._assert_heads(('e',))
def test_unresolve_merges(self):
self.updater.update_to_step(_up(None, 'e', True))
self.updater.update_to_step(_down('e', ('d1', 'd2')))
self._assert_heads(('d2', 'd1'))
self.updater.update_to_step(_down('d2', 'c2'))
self._assert_heads(('c2', 'd1'))
def test_update_no_match(self):
self.updater.update_to_step(_up(None, 'a', True))
self.updater.heads.add('x')
assert_raises_message(
CommandError,
"Online migration expected to match one row when updating "
"'x' to 'b' in 'version_table'; 0 found",
self.updater.update_to_step, _up('x', 'b')
)
def test_update_multi_match(self):
self.connection.execute(version_table.insert(), version_num='a')
self.connection.execute(version_table.insert(), version_num='a')
self.updater.heads.add('a')
assert_raises_message(
CommandError,
"Online migration expected to match one row when updating "
"'a' to 'b' in 'version_table'; 2 found",
self.updater.update_to_step, _up('a', 'b')
)
def test_delete_no_match(self):
self.updater.update_to_step(_up(None, 'a', True))
self.updater.heads.add('x')
assert_raises_message(
CommandError,
"Online migration expected to match one row when "
"deleting 'x' in 'version_table'; 0 found",
self.updater.update_to_step, _down('x', None, True)
)
def test_delete_multi_match(self):
self.connection.execute(version_table.insert(), version_num='a')
self.connection.execute(version_table.insert(), version_num='a')
self.updater.heads.add('a')
assert_raises_message(
CommandError,
"Online migration expected to match one row when "
"deleting 'a' in 'version_table'; 2 found",
self.updater.update_to_step, _down('a', None, True)
)
context._update_current_rev(None, 'a')
eq_(self.get_revision(), 'a')
context._update_current_rev('a', 'b')
eq_(self.get_revision(), 'b')
context._update_current_rev('b', None)
eq_(self.get_revision(), None)

View File

@ -2,146 +2,450 @@ from alembic.testing.env import clear_staging_env, staging_env
from alembic.testing import assert_raises_message, eq_
from alembic import util
from alembic.testing.fixtures import TestBase
from alembic.testing import mock
from alembic.migration import MigrationStep, HeadMaintainer
env = None
a, b, c, d, e = None, None, None, None, None
cfg = None
class MigrationTest(TestBase):
def up_(self, rev):
return MigrationStep.upgrade_from_script(
self.env.revision_map, rev)
def down_(self, rev):
return MigrationStep.downgrade_from_script(
self.env.revision_map, rev)
def _assert_downgrade(self, destination, source, expected, expected_heads):
revs = self.env._downgrade_revs(destination, source)
eq_(
revs, expected
)
heads = set(util.to_tuple(source, default=()))
head = HeadMaintainer(mock.Mock(), heads)
for rev in revs:
head.update_to_step(rev)
eq_(head.heads, expected_heads)
def _assert_upgrade(self, destination, source, expected, expected_heads):
revs = self.env._upgrade_revs(destination, source)
eq_(
revs, expected
)
heads = set(util.to_tuple(source, default=()))
head = HeadMaintainer(mock.Mock(), heads)
for rev in revs:
head.update_to_step(rev)
eq_(head.heads, expected_heads)
class RevisionPathTest(TestBase):
class RevisionPathTest(MigrationTest):
@classmethod
def setup_class(cls):
global env
env = staging_env()
global a, b, c, d, e
a = env.generate_revision(util.rev_id(), '->a', refresh=True)
b = env.generate_revision(util.rev_id(), 'a->b', refresh=True)
c = env.generate_revision(util.rev_id(), 'b->c', refresh=True)
d = env.generate_revision(util.rev_id(), 'c->d', refresh=True)
e = env.generate_revision(util.rev_id(), 'd->e', refresh=True)
cls.env = env = staging_env()
cls.a = env.generate_revision(util.rev_id(), '->a', refresh=True)
cls.b = env.generate_revision(util.rev_id(), 'a->b', refresh=True)
cls.c = env.generate_revision(util.rev_id(), 'b->c', refresh=True)
cls.d = env.generate_revision(util.rev_id(), 'c->d', refresh=True)
cls.e = env.generate_revision(util.rev_id(), 'd->e', refresh=True)
@classmethod
def teardown_class(cls):
clear_staging_env()
def test_upgrade_path(self):
eq_(
env._upgrade_revs(e.revision, c.revision),
a, b, c, d, e = self.a, self.b, self.c, self.d, self.e
self._assert_upgrade(
e.revision, c.revision,
[
(d.module.upgrade, c.revision, d.revision, d.doc),
(e.module.upgrade, d.revision, e.revision, e.doc),
]
self.up_(d),
self.up_(e)
],
set([e.revision])
)
eq_(
env._upgrade_revs(c.revision, None),
self._assert_upgrade(
c.revision, None,
[
(a.module.upgrade, None, a.revision, a.doc),
(b.module.upgrade, a.revision, b.revision, b.doc),
(c.module.upgrade, b.revision, c.revision, c.doc),
]
self.up_(a),
self.up_(b),
self.up_(c),
],
set([c.revision])
)
def test_relative_upgrade_path(self):
eq_(
env._upgrade_revs("+2", a.revision),
a, b, c, d, e = self.a, self.b, self.c, self.d, self.e
self._assert_upgrade(
"+2", a.revision,
[
(b.module.upgrade, a.revision, b.revision, b.doc),
(c.module.upgrade, b.revision, c.revision, c.doc),
]
self.up_(b),
self.up_(c),
],
set([c.revision])
)
eq_(
env._upgrade_revs("+1", a.revision),
self._assert_upgrade(
"+1", a.revision,
[
(b.module.upgrade, a.revision, b.revision, b.doc),
]
self.up_(b)
],
set([b.revision])
)
eq_(
env._upgrade_revs("+3", b.revision),
[
(c.module.upgrade, b.revision, c.revision, c.doc),
(d.module.upgrade, c.revision, d.revision, d.doc),
(e.module.upgrade, d.revision, e.revision, e.doc),
]
self._assert_upgrade(
"+3", b.revision,
[self.up_(c), self.up_(d), self.up_(e)],
set([e.revision])
)
def test_invalid_relative_upgrade_path(self):
a, b, c, d, e = self.a, self.b, self.c, self.d, self.e
assert_raises_message(
util.CommandError,
"Relative revision -2 didn't produce 2 migrations",
env._upgrade_revs, "-2", b.revision
self.env._upgrade_revs, "-2", b.revision
)
assert_raises_message(
util.CommandError,
r"Relative revision \+5 didn't produce 5 migrations",
env._upgrade_revs, "+5", b.revision
self.env._upgrade_revs, "+5", b.revision
)
def test_downgrade_path(self):
a, b, c, d, e = self.a, self.b, self.c, self.d, self.e
eq_(
env._downgrade_revs(c.revision, e.revision),
[
(e.module.downgrade, e.revision, e.down_revision, e.doc),
(d.module.downgrade, d.revision, d.down_revision, d.doc),
]
self._assert_downgrade(
c.revision, e.revision,
[self.down_(e), self.down_(d)],
set([c.revision])
)
eq_(
env._downgrade_revs(None, c.revision),
[
(c.module.downgrade, c.revision, c.down_revision, c.doc),
(b.module.downgrade, b.revision, b.down_revision, b.doc),
(a.module.downgrade, a.revision, a.down_revision, a.doc),
]
self._assert_downgrade(
None, c.revision,
[self.down_(c), self.down_(b), self.down_(a)],
set()
)
def test_relative_downgrade_path(self):
eq_(
env._downgrade_revs("-1", c.revision),
[
(c.module.downgrade, c.revision, c.down_revision, c.doc),
]
a, b, c, d, e = self.a, self.b, self.c, self.d, self.e
self._assert_downgrade(
"-1", c.revision,
[self.down_(c)],
set([b.revision])
)
eq_(
env._downgrade_revs("-3", e.revision),
[
(e.module.downgrade, e.revision, e.down_revision, e.doc),
(d.module.downgrade, d.revision, d.down_revision, d.doc),
(c.module.downgrade, c.revision, c.down_revision, c.doc),
]
self._assert_downgrade(
"-3", e.revision,
[self.down_(e), self.down_(d), self.down_(c)],
set([b.revision])
)
def test_invalid_relative_downgrade_path(self):
a, b, c, d, e = self.a, self.b, self.c, self.d, self.e
assert_raises_message(
util.CommandError,
"Relative revision -5 didn't produce 5 migrations",
env._downgrade_revs, "-5", b.revision
self.env._downgrade_revs, "-5", b.revision
)
assert_raises_message(
util.CommandError,
r"Relative revision \+2 didn't produce 2 migrations",
env._downgrade_revs, "+2", b.revision
self.env._downgrade_revs, "+2", b.revision
)
def test_invalid_move_rev_to_none(self):
a, b, c, d, e = self.a, self.b, self.c, self.d, self.e
assert_raises_message(
util.CommandError,
"Revision %s is not an ancestor of base" % b.revision,
env._downgrade_revs, b.revision[0:3], None
r"Destination %s is not a valid downgrade "
"target from current head\(s\)" % b.revision[0:3],
self.env._downgrade_revs, b.revision[0:3], None
)
def test_invalid_move_higher_to_lower(self):
a, b, c, d, e = self.a, self.b, self.c, self.d, self.e
assert_raises_message(
util.CommandError,
"Revision %s is not an ancestor of %s" % (c.revision, b.revision),
env._downgrade_revs, c.revision[0:4], b.revision
r"Destination %s is not a valid downgrade "
"target from current head\(s\)" % c.revision[0:4],
self.env._downgrade_revs, c.revision[0:4], b.revision
)
class BranchedPathTest(MigrationTest):
@classmethod
def setup_class(cls):
cls.env = env = staging_env()
cls.a = env.generate_revision(util.rev_id(), '->a', refresh=True)
cls.b = env.generate_revision(util.rev_id(), 'a->b', refresh=True)
cls.c1 = env.generate_revision(
util.rev_id(), 'b->c1',
branch_labels='c1branch',
refresh=True)
cls.d1 = env.generate_revision(util.rev_id(), 'c1->d1', refresh=True)
cls.c2 = env.generate_revision(
util.rev_id(), 'b->c2',
branch_labels='c2branch',
head=cls.b.revision, refresh=True, splice=True)
cls.d2 = env.generate_revision(
util.rev_id(), 'c2->d2',
head=cls.c2.revision, refresh=True)
@classmethod
def teardown_class(cls):
clear_staging_env()
def test_stamp_down_across_multiple_branch_to_branchpoint(self):
a, b, c1, d1, c2, d2 = (
self.a, self.b, self.c1, self.d1, self.c2, self.d2
)
revs = self.env._stamp_revs(
self.b.revision, [self.d1.revision, self.c2.revision])
eq_(len(revs), 1)
eq_(
revs[0].merge_branch_idents,
# DELETE d1 revision, UPDATE c2 to b
([self.d1.revision], self.c2.revision, self.b.revision)
)
def test_stamp_to_labeled_base_multiple_heads(self):
a, b, c1, d1, c2, d2 = (
self.a, self.b, self.c1, self.d1, self.c2, self.d2
)
revs = self.env._stamp_revs(
"c1branch@base", [self.d1.revision, self.c2.revision])
eq_(len(revs), 1)
assert revs[0].should_delete_branch
eq_(revs[0].delete_version_num, self.d1.revision)
def test_stamp_to_labeled_head_multiple_heads(self):
a, b, c1, d1, c2, d2 = (
self.a, self.b, self.c1, self.d1, self.c2, self.d2
)
revs = self.env._stamp_revs(
"c2branch@head", [self.d1.revision, self.c2.revision])
eq_(len(revs), 1)
eq_(
revs[0].merge_branch_idents,
# the c1branch remains unchanged
([], self.c2.revision, self.d2.revision)
)
def test_upgrade_single_branch(self):
a, b, c1, d1, c2, d2 = (
self.a, self.b, self.c1, self.d1, self.c2, self.d2
)
self._assert_upgrade(
d1.revision, b.revision,
[self.up_(c1), self.up_(d1)],
set([d1.revision])
)
def test_upgrade_multiple_branch(self):
# move from a single head to multiple heads
a, b, c1, d1, c2, d2 = (
self.a, self.b, self.c1, self.d1, self.c2, self.d2
)
self._assert_upgrade(
(d1.revision, d2.revision), a.revision,
[self.up_(b), self.up_(c2), self.up_(d2),
self.up_(c1), self.up_(d1)],
set([d1.revision, d2.revision])
)
def test_downgrade_multiple_branch(self):
a, b, c1, d1, c2, d2 = (
self.a, self.b, self.c1, self.d1, self.c2, self.d2
)
self._assert_downgrade(
a.revision, (d1.revision, d2.revision),
[self.down_(d1), self.down_(c1), self.down_(d2),
self.down_(c2), self.down_(b)],
set([a.revision])
)
class ForestTest(MigrationTest):
@classmethod
def setup_class(cls):
cls.env = env = staging_env()
cls.a1 = env.generate_revision(util.rev_id(), '->a1', refresh=True)
cls.b1 = env.generate_revision(util.rev_id(), 'a1->b1', refresh=True)
cls.a2 = env.generate_revision(
util.rev_id(), '->a2', head=(),
refresh=True)
cls.b2 = env.generate_revision(
util.rev_id(), 'a2->b2', head=cls.a2.revision, refresh=True)
@classmethod
def teardown_class(cls):
clear_staging_env()
def test_base_to_heads(self):
a1, b1, a2, b2 = self.a1, self.b1, self.a2, self.b2
eq_(
self.env._upgrade_revs("heads", "base"),
[self.up_(a2), self.up_(b2), self.up_(a1), self.up_(b1), ]
)
class MergedPathTest(MigrationTest):
@classmethod
def setup_class(cls):
cls.env = env = staging_env()
cls.a = env.generate_revision(util.rev_id(), '->a', refresh=True)
cls.b = env.generate_revision(util.rev_id(), 'a->b', refresh=True)
cls.c1 = env.generate_revision(util.rev_id(), 'b->c1', refresh=True)
cls.d1 = env.generate_revision(util.rev_id(), 'c1->d1', refresh=True)
cls.c2 = env.generate_revision(
util.rev_id(), 'b->c2',
branch_labels='c2branch',
head=cls.b.revision, refresh=True, splice=True)
cls.d2 = env.generate_revision(
util.rev_id(), 'c2->d2',
head=cls.c2.revision, refresh=True)
cls.e = env.generate_revision(
util.rev_id(), 'merge d1 and d2',
head=(cls.d1.revision, cls.d2.revision), refresh=True
)
cls.f = env.generate_revision(util.rev_id(), 'e->f', refresh=True)
@classmethod
def teardown_class(cls):
clear_staging_env()
def test_stamp_down_across_merge_point_branch(self):
a, b, c1, d1, c2, d2, e, f = (
self.a, self.b, self.c1, self.d1, self.c2, self.d2,
self.e, self.f
)
revs = self.env._stamp_revs(self.c2.revision, [self.e.revision])
eq_(len(revs), 1)
eq_(
revs[0].merge_branch_idents,
# no deletes, UPDATE e to c2
([], self.e.revision, self.c2.revision)
)
def test_stamp_down_across_merge_prior_branching(self):
a, b, c1, d1, c2, d2, e, f = (
self.a, self.b, self.c1, self.d1, self.c2, self.d2,
self.e, self.f
)
revs = self.env._stamp_revs(self.a.revision, [self.e.revision])
eq_(len(revs), 1)
eq_(
revs[0].merge_branch_idents,
# no deletes, UPDATE e to c2
([], self.e.revision, self.a.revision)
)
def test_stamp_up_across_merge_from_single_branch(self):
a, b, c1, d1, c2, d2, e, f = (
self.a, self.b, self.c1, self.d1, self.c2, self.d2,
self.e, self.f
)
revs = self.env._stamp_revs(self.e.revision, [self.c2.revision])
eq_(len(revs), 1)
eq_(
revs[0].merge_branch_idents,
# no deletes, UPDATE e to c2
([], self.c2.revision, self.e.revision)
)
def test_stamp_labled_head_across_merge_from_multiple_branch(self):
a, b, c1, d1, c2, d2, e, f = (
self.a, self.b, self.c1, self.d1, self.c2, self.d2,
self.e, self.f
)
# this is testing that filter_for_lineage() checks for
# d1 both in terms of "c2branch" as well as that the "head"
# revision "f" is the head of both d1 and d2
revs = self.env._stamp_revs(
"c2branch@head", [self.d1.revision, self.c2.revision])
eq_(len(revs), 1)
eq_(
revs[0].merge_branch_idents,
# DELETE d1 revision, UPDATE c2 to e
([self.d1.revision], self.c2.revision, self.f.revision)
)
def test_stamp_up_across_merge_from_multiple_branch(self):
a, b, c1, d1, c2, d2, e, f = (
self.a, self.b, self.c1, self.d1, self.c2, self.d2,
self.e, self.f
)
revs = self.env._stamp_revs(
self.e.revision, [self.d1.revision, self.c2.revision])
eq_(len(revs), 1)
eq_(
revs[0].merge_branch_idents,
# DELETE d1 revision, UPDATE c2 to e
([self.d1.revision], self.c2.revision, self.e.revision)
)
def test_stamp_up_across_merge_prior_branching(self):
a, b, c1, d1, c2, d2, e, f = (
self.a, self.b, self.c1, self.d1, self.c2, self.d2,
self.e, self.f
)
revs = self.env._stamp_revs(self.e.revision, [self.b.revision])
eq_(len(revs), 1)
eq_(
revs[0].merge_branch_idents,
# no deletes, UPDATE e to c2
([], self.b.revision, self.e.revision)
)
def test_upgrade_across_merge_point(self):
a, b, c1, d1, c2, d2, e, f = (
self.a, self.b, self.c1, self.d1, self.c2, self.d2,
self.e, self.f
)
eq_(
self.env._upgrade_revs(f.revision, b.revision),
[
self.up_(c2),
self.up_(d2),
self.up_(c1), # b->c1, create new branch
self.up_(d1),
self.up_(e), # d1/d2 -> e, merge branches
# (DELETE d2, UPDATE d1->e)
self.up_(f)
]
)
def test_downgrade_across_merge_point(self):
a, b, c1, d1, c2, d2, e, f = (
self.a, self.b, self.c1, self.d1, self.c2, self.d2,
self.e, self.f
)
eq_(
self.env._downgrade_revs(b.revision, f.revision),
[
self.down_(f),
self.down_(e), # e -> d1 and d2, unmerge branches
# (UPDATE e->d1, INSERT d2)
self.down_(d1),
self.down_(c1),
self.down_(d2),
self.down_(c2), # c2->b, delete branch
]
)