splice=migration_script.splice,
branch_labels=migration_script.branch_label,
version_path=migration_script.version_path,
+ depends_on=migration_script.depends_on,
**template_args)
def run_autogenerate(self, rev, context):
head=self.command_args['head'],
splice=self.command_args['splice'],
branch_label=self.command_args['branch_label'],
- version_path=self.command_args['version_path']
+ version_path=self.command_args['version_path'],
+ depends_on=self.command_args['depends_on']
)
op._autogen_context = None
return op
def revision(
config, message=None, autogenerate=False, sql=False,
head="head", splice=False, branch_label=None,
- version_path=None, rev_id=None):
+ version_path=None, rev_id=None, depends_on=None):
"""Create a new revision file."""
script_directory = ScriptDirectory.from_config(config)
message=message,
autogenerate=autogenerate,
sql=sql, head=head, splice=splice, branch_label=branch_label,
- version_path=version_path, rev_id=rev_id
+ version_path=version_path, rev_id=rev_id, depends_on=depends_on
)
revision_context = autogen.RevisionContext(
config, script_directory, command_args)
"'head' to splice onto"
)
),
+ 'depends_on': (
+ "--depends-on",
+ dict(
+ action="append",
+ help="Specify one or more revision identifiers "
+ "which this revision should depend on."
+ )
+ ),
'rev_id': (
"--rev-id",
dict(
self, rev_id, upgrade_ops, downgrade_ops,
message=None,
imports=None, head=None, splice=None,
- branch_label=None, version_path=None):
+ branch_label=None, version_path=None, depends_on=None):
self.rev_id = rev_id
self.message = message
self.imports = imports
self.splice = splice
self.branch_label = branch_label
self.version_path = version_path
+ self.depends_on = depends_on
self.upgrade_ops = upgrade_ops
self.downgrade_ops = downgrade_ops
@contextmanager
def _catch_revision_errors(
self,
- ancestor=None, multiple_heads=None, start=None, end=None):
+ ancestor=None, multiple_heads=None, start=None, end=None,
+ resolution=None):
try:
yield
except revision.RangeNotAncestorError as rna:
"heads": util.format_as_comma(mh.heads)
}
compat.raise_from_cause(util.CommandError(multiple_heads))
+ except revision.ResolutionError as re:
+ if resolution is None:
+ resolution = "Can't locate revision identified by '%s'" % (
+ re.argument
+ )
+ compat.raise_from_cause(util.CommandError(resolution))
except revision.RevisionError as err:
compat.raise_from_cause(util.CommandError(err.args[0]))
"--splice to create a new branch from this revision"
% head.revision)
+ if depends_on:
+ with self._catch_revision_errors():
+ depends_on = [
+ dep
+ if dep in rev.branch_labels # maintain branch labels
+ else rev.revision # resolve partial revision identifiers
+ for rev in [
+ self.revision_map.get_revision(dep)
+ for dep in util.to_list(depends_on)
+ ]
+
+ ]
+
self._generate_template(
os.path.join(self.dir, "script.py.mako"),
path,
class ResolutionError(RevisionError):
- pass
+ def __init__(self, message, argument):
+ super(ResolutionError, self).__init__(message)
+ self.argument = argument
class RevisionMap(object):
self._real_bases = ()
has_branch_labels = set()
+ has_depends_on = set()
for revision in self._generator():
if revision.revision in map_:
map_[revision.revision] = revision
if revision.branch_labels:
has_branch_labels.add(revision)
+ if revision.dependencies:
+ has_depends_on.add(revision)
heads.add(revision.revision)
_real_heads.add(revision.revision)
if revision.is_base:
if revision._is_real_base:
self._real_bases += (revision.revision, )
+ # add the branch_labels to the map_. We'll need these
+ # to resolve the dependencies.
+ for revision in has_branch_labels:
+ self._map_branch_labels(revision, map_)
+
+ for revision in has_depends_on:
+ self._add_depends_on(revision, map_)
+
for rev in map_.values():
for downrev in rev._all_down_revisions:
if downrev not in map_:
self._real_heads = tuple(_real_heads)
for revision in has_branch_labels:
- self._add_branches(revision, map_)
+ self._add_branches(revision, map_, map_branch_labels=False)
return map_
- def _add_branches(self, revision, map_):
+ def _map_branch_labels(self, revision, map_):
if revision.branch_labels:
for branch_label in revision._orig_branch_labels:
if branch_label in map_:
map_[branch_label].revision)
)
map_[branch_label] = revision
+
+ def _add_branches(self, revision, map_, map_branch_labels=True):
+ if map_branch_labels:
+ self._map_branch_labels(revision, map_)
+
+ if revision.branch_labels:
revision.branch_labels.update(revision.branch_labels)
for node in self._get_descendant_nodes(
[revision], map_, include_dependencies=False):
parent = node
while parent and \
- not parent._is_real_branch_point and not parent.is_merge_point:
+ not parent._is_real_branch_point and \
+ not parent.is_merge_point:
parent.branch_labels.update(revision.branch_labels)
if parent.down_revision:
else:
break
+ def _add_depends_on(self, revision, map_):
+ if revision.dependencies:
+ revision._resolved_dependencies = tuple(
+ map_[dep].revision for dep
+ in util.to_tuple(revision.dependencies)
+ )
+
def add_revision(self, revision, _replace=False):
"""add a single revision to an existing map.
map_[revision.revision] = revision
self._add_branches(revision, map_)
+ self._add_depends_on(revision, map_)
+
if revision.is_base:
self.bases += (revision.revision, )
if revision._is_real_base:
try:
nonbranch_rev = self._revision_for_ident(branch_label)
except ResolutionError:
- raise ResolutionError("No such branch: '%s'" % branch_label)
+ raise ResolutionError(
+ "No such branch: '%s'" % branch_label, branch_label)
else:
return nonbranch_rev
else:
revs = self.filter_for_lineage(revs, check_branch)
if not revs:
raise ResolutionError(
- "No such revision or branch '%s'" % resolved_id)
+ "No such revision or branch '%s'" % resolved_id,
+ 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])
- ))
+ ), resolved_id)
else:
revision = self._revision_map[revs[0]]
revision.revision, branch_rev.revision):
raise ResolutionError(
"Revision %s is not a member of branch '%s'" %
- (revision.revision, check_branch))
+ (revision.revision, check_branch), resolved_id)
return revision
def filter_for_lineage(
r for r in uppers if r.revision in total_space)
# iterate for total_space being emptied out
+ total_space_modified = True
while total_space:
+
+ if not total_space_modified:
+ raise RevisionError(
+ "Dependency resolution failed; iteration can't proceed")
+ total_space_modified = False
# when everything non-branch pending is consumed,
# add to the todo any branch nodes that have no
# descendants left in the queue
while todo:
rev = todo.popleft()
total_space.remove(rev.revision)
+ total_space_modified = True
# do depth first for elements within branches,
# don't consume any actual branch nodes
self.revision = revision
self.down_revision = tuple_rev_as_scalar(down_revision)
self.dependencies = tuple_rev_as_scalar(dependencies)
+ self._resolved_dependencies = ()
self._orig_branch_labels = util.to_tuple(branch_labels, default=())
self.branch_labels = set(self._orig_branch_labels)
@property
def _all_down_revisions(self):
return util.to_tuple(self.down_revision, default=()) + \
- util.to_tuple(self.dependencies, default=())
+ self._resolved_dependencies
@property
def _versioned_down_revisions(self):
"""Return True if this :class:`.Revision` is a "real" base revision,
e.g. that it has no dependencies either."""
+ # we use self.dependencies here because this is called up
+ # in initialization where _real_dependencies isn't set up
+ # yet
return self.down_revision is None and self.dependencies is None
@property
-.. _branches:
+f.. _branches:
Working with Branches
=====================
an entry in ``down_revision`` from a graph perspective, but different
from a semantic perspective.
-First we will build out our new revision on the ``networking`` branch
-in the usual way::
+To use ``depends_on``, we can specify it as part of our ``alembic revision``
+command::
- $ alembic revision -m "add ip account table" --head=networking@head
+ $ alembic revision -m "add ip account table" --head=networking@head --depends-on=55af2cb1c267
Generating /path/to/foo/model/networking/2a95102259be_add_ip_account_table.py ... done
-Next, we'll add an explicit dependency inside the file, by placing the
-directive ``depends_on='55af2cb1c267'`` underneath the other directives::
+Within our migration file, we'll see this new directive present::
# revision identifiers, used by Alembic.
revision = '2a95102259be'
branch_labels = None
depends_on='55af2cb1c267'
-Currently, ``depends_on`` needs to be a real revision number, not a partial
-number or branch name. It can of course refer to a tuple of any number
-of dependent revisions::
+``depends_on`` may be either a real revision number or a branch
+name. When specified at the command line, a resolution from a
+partial revision number will work as well. It can refer
+to any number of dependent revisions as well; for example, if we were
+to run the command::
+
+ $ alembic revision -m "add ip account table" \\
+ --head=networking@head \\
+ --depends-on=55af2cb1c267 --depends-on=d747a --depends-on=fa445
+ Generating /path/to/foo/model/networking/2a95102259be_add_ip_account_table.py ... done
+
+We'd see inside the file::
# revision identifiers, used by Alembic.
revision = '2a95102259be'
branch_labels = None
depends_on = ('55af2cb1c267', 'd747a8a8879', 'fa4456a9201')
+We also can of course add or alter this value within the file manually after
+it is generated, rather than using the ``--depends-on`` argument.
+
+.. versionadded:: 0.8 The ``depends_on`` attribute may be set directly
+ from the ``alembic revision`` command, rather than editing the file
+ directly. ``depends_on`` identifiers may also be specified as
+ branch names at the command line or directly within the migration file.
+ The values may be specified as partial revision numbers from the command
+ line which will be resolved to full revision numbers in the output file.
We can see the effect this directive has when we view the history
of the ``networking`` branch in terms of "heads", e.g., all the revisions that
.. changelog::
:version: 0.8.0
+ .. change::
+ :tags: feature, commands
+ :tickets: 311
+
+ Added new multiple-capable argument ``--depends-on`` to the
+ ``alembic revision`` command, allowing ``depends_on`` to be
+ established at the command line level rather than having to edit
+ the file after the fact. ``depends_on`` identifiers may also be
+ specified as branch names at the command line or directly within
+ the migration file. The values may be specified as partial
+ revision numbers from the command line which will be resolved to
+ full revision numbers in the output file.
+
.. change::
:tags: change, operations
command.upgrade(self.cfg, "heads")
command.revision(self.cfg, autogenerate=True)
+ def test_create_rev_depends_on(self):
+ self._env_fixture()
+ command.revision(self.cfg)
+ rev2 = command.revision(self.cfg)
+ rev3 = command.revision(self.cfg, depends_on=rev2.revision)
+ eq_(
+ rev3._resolved_dependencies, (rev2.revision, )
+ )
+
+ rev4 = command.revision(
+ self.cfg, depends_on=[rev2.revision, rev3.revision])
+ eq_(
+ rev4._resolved_dependencies, (rev2.revision, rev3.revision)
+ )
+
+ def test_create_rev_depends_on_branch_label(self):
+ self._env_fixture()
+ command.revision(self.cfg)
+ rev2 = command.revision(self.cfg, branch_label='foobar')
+ rev3 = command.revision(self.cfg, depends_on='foobar')
+ eq_(
+ rev3.dependencies, 'foobar'
+ )
+ eq_(
+ rev3._resolved_dependencies, (rev2.revision, )
+ )
+
+ def test_create_rev_depends_on_partial_revid(self):
+ self._env_fixture()
+ command.revision(self.cfg)
+ rev2 = command.revision(self.cfg)
+ assert len(rev2.revision) > 7
+ rev3 = command.revision(self.cfg, depends_on=rev2.revision[0:4])
+ eq_(
+ rev3.dependencies, rev2.revision
+ )
+ eq_(
+ rev3._resolved_dependencies, (rev2.revision, )
+ )
+
+ def test_create_rev_invalid_depends_on(self):
+ self._env_fixture()
+ command.revision(self.cfg)
+ assert_raises_message(
+ util.CommandError,
+ "Can't locate revision identified by 'invalid'",
+ command.revision, self.cfg, depends_on='invalid'
+ )
+
def test_create_rev_autogenerate_db_not_up_to_date_post_merge(self):
self._env_fixture()
command.revision(self.cfg)
Revision('b1', 'a1'),
Revision('c1', 'b1'),
- Revision('base2', (), dependencies='base1', branch_labels='b_2'),
+ Revision('base2', (), dependencies='b_1', branch_labels='b_2'),
Revision('a2', 'base2'),
Revision('b2', 'a2'),
Revision('c2', 'b2'),
remaining = set(revs[idx + 1:])
if remaining:
assert remaining.intersection(ancestors)
+
+
+class DepResolutionFailedTest(DownIterateTest):
+ def setUp(self):
+ self.map = RevisionMap(
+ lambda: [
+ Revision('base1', ()),
+ Revision('a1', 'base1'),
+ Revision('a2', 'base1'),
+ Revision('b1', 'a1'),
+ Revision('c1', 'b1'),
+ ]
+ )
+ # intentionally make a broken map
+ self.map._revision_map['fake'] = self.map._revision_map['a2']
+ self.map._revision_map['b1'].dependencies = 'fake'
+ self.map._revision_map['b1']._resolved_dependencies = ('fake', )
+
+ def test_failure_message(self):
+ iter_ = self.map.iterate_revisions("c1", "base1")
+ assert_raises_message(
+ RevisionError,
+ "Dependency resolution failed;",
+ list, iter_
+ )
)
+class DependsOnBranchLabelTest(MigrationTest):
+ @classmethod
+ def setup_class(cls):
+ cls.env = env = staging_env()
+ cls.a1 = env.generate_revision(
+ util.rev_id(), '->a1',
+ branch_labels=['lib1'])
+ cls.b1 = env.generate_revision(util.rev_id(), 'a1->b1')
+ cls.c1 = env.generate_revision(
+ util.rev_id(), 'b1->c1',
+ branch_labels=['c1lib'])
+
+ cls.a2 = env.generate_revision(util.rev_id(), '->a2', head=())
+ cls.b2 = env.generate_revision(
+ util.rev_id(), 'a2->b2', head=cls.a2.revision)
+ cls.c2 = env.generate_revision(
+ util.rev_id(), 'b2->c2', head=cls.b2.revision,
+ depends_on=['c1lib'])
+
+ cls.d1 = env.generate_revision(
+ util.rev_id(), 'c1->d1',
+ head=cls.c1.revision)
+ cls.e1 = env.generate_revision(
+ util.rev_id(), 'd1->e1',
+ head=cls.d1.revision)
+ cls.f1 = env.generate_revision(
+ util.rev_id(), 'e1->f1',
+ head=cls.e1.revision)
+
+ def test_upgrade_path(self):
+ self._assert_upgrade(
+ self.c2.revision, self.a2.revision,
+ [
+ self.up_(self.a1),
+ self.up_(self.b1),
+ self.up_(self.c1),
+ self.up_(self.b2),
+ self.up_(self.c2),
+ ],
+ set([self.c2.revision, ])
+ )
+
+
class ForestTest(MigrationTest):
@classmethod