- Relative revision identifiers as used with ``alembic upgrade``,

``alembic downgrade`` and ``alembic history`` can be combined with
specific revisions as well, e.g. ``alembic upgrade ae10+3``, to produce
a migration target relative to the given exact version.
This commit is contained in:
Mike Bayer 2014-11-23 15:23:52 -05:00
parent 6b0b54b35b
commit 2639f8f5f8
6 changed files with 174 additions and 58 deletions

View File

@ -34,6 +34,7 @@ if py3k:
def ue(s):
return s
range = range
else:
import __builtin__ as compat_builtins
string_types = basestring,
@ -47,6 +48,8 @@ else:
def ue(s):
return unicode(s, "unicode_escape")
range = xrange
if py3k:
from configparser import ConfigParser as SafeConfigParser
import configparser

View File

@ -1,11 +1,12 @@
import re
import collections
import itertools
from . import util
from sqlalchemy import util as sqlautil
from . import compat
_relative_destination = re.compile(r'(?:(.+?)@)?((?:\+|-)\d+)')
_relative_destination = re.compile(r'(?:(.+?)@)?(\w+)?((?:\+|-)\d+)')
class RevisionError(Exception):
@ -402,6 +403,83 @@ class RevisionMap(object):
else:
return util.to_tuple(id_, default=None), branch_label
def _relative_iterate(
self, destination, source, is_upwards,
implicit_base, inclusive, assert_relative_length):
if isinstance(destination, compat.string_types):
match = _relative_destination.match(destination)
if not match:
return None
else:
return None
relative = int(match.group(3))
symbol = match.group(2)
branch_label = match.group(1)
reldelta = 1 if inclusive and not symbol else 0
if is_upwards:
if branch_label:
from_ = "%s@head" % branch_label
elif symbol:
if symbol.startswith("head"):
from_ = symbol
else:
from_ = "%s@head" % symbol
else:
from_ = "head"
to_ = source
else:
if branch_label:
to_ = "%s@base" % branch_label
elif symbol:
to_ = "%s@base" % symbol
else:
to_ = "base"
from_ = source
revs = list(
self._iterate_revisions(
from_, to_,
inclusive=inclusive, implicit_base=implicit_base))
if symbol:
if branch_label:
symbol_rev = self.get_revision(
"%s@%s" % (branch_label, symbol))
else:
symbol_rev = self.get_revision(symbol)
if symbol.startswith("head"):
index = 0
elif symbol == "base":
index = len(revs) - 1
else:
range_ = compat.range(len(revs) - 1, 0, -1)
for index in range_:
if symbol_rev.revision == revs[index].revision:
break
else:
index = 0
else:
index = 0
if is_upwards:
revs = revs[index - relative - reldelta:]
if not index and assert_relative_length and \
len(revs) < abs(relative - reldelta):
raise RevisionError(
"Relative revision %s didn't "
"produce %d migrations" % (destination, abs(relative)))
else:
revs = revs[0:index - relative + reldelta]
if not index and assert_relative_length and \
len(revs) != abs(relative) + reldelta:
raise RevisionError(
"Relative revision %s didn't "
"produce %d migrations" % (destination, abs(relative)))
return iter(revs)
def iterate_revisions(
self, upper, lower, implicit_base=False, inclusive=False,
assert_relative_length=True):
@ -417,54 +495,22 @@ class RevisionMap(object):
"""
if isinstance(upper, compat.string_types) and \
_relative_destination.match(upper):
relative_upper = self._relative_iterate(
upper, lower, True, implicit_base,
inclusive, assert_relative_length
)
if relative_upper:
return relative_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)
relative_lower = self._relative_iterate(
lower, upper, False, implicit_base,
inclusive, assert_relative_length
)
if relative_lower:
return relative_lower
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)
return self._iterate_revisions(
upper, lower, inclusive=inclusive, implicit_base=implicit_base)
def _get_descendant_nodes(
self, targets, map_=None, check=False, include_dependencies=True):

View File

@ -498,6 +498,11 @@ This kind of thing works from history as well::
$ alembic history -r current:shoppingcart@+2
The newer ``relnum+delta`` format can be combined as well, for example
if we wanted to list along ``shoppingcart`` up until two revisions
before the head::
$ alembic history -r :shoppingcart@head-2
.. _multiple_bases:

View File

@ -72,6 +72,14 @@ Changelog
:ref:`batch_migrations`
.. change::
:tags: feature, commands
Relative revision identifiers as used with ``alembic upgrade``,
``alembic downgrade`` and ``alembic history`` can be combined with
specific revisions as well, e.g. ``alembic upgrade ae10+3``, to produce
a migration target relative to the given exact version.
.. change::
:tags: bug, autogenerate, postgresql
:tickets: 247

View File

@ -405,6 +405,20 @@ Running again to ``head``::
We've now added the ``last_transaction_date`` column to the database.
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.
.. relative_migrations:
Relative Migration Identifiers
@ -419,19 +433,13 @@ Negative values are accepted for downgrades::
$ alembic downgrade -1
Partial Revision Identifiers
=============================
Relative identifiers may also be in terms of a specific revision. For example,
to upgrade to revision ``ae1027a6acf`` plus two additional steps::
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 ae10+2
$ 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.
.. versionadded:: 0.7.0 Support for relative migrations in terms of a specific
revision.
Getting Information
===================

View File

@ -99,6 +99,18 @@ class RevisionPathTest(MigrationTest):
set([e.revision])
)
self._assert_upgrade(
"%s+2" % b.revision, a.revision,
[self.up_(b), self.up_(c), self.up_(d)],
set([d.revision])
)
self._assert_upgrade(
"%s-2" % d.revision, a.revision,
[self.up_(b)],
set([b.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(
@ -142,6 +154,18 @@ class RevisionPathTest(MigrationTest):
set([b.revision])
)
self._assert_downgrade(
"%s+2" % a.revision, d.revision,
[self.down_(d)],
set([c.revision])
)
self._assert_downgrade(
"%s-2" % c.revision, d.revision,
[self.down_(d), self.down_(c), self.down_(b)],
set([a.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(
@ -275,6 +299,28 @@ class BranchedPathTest(MigrationTest):
set([a.revision])
)
def test_relative_upgrade(self):
a, b, c1, d1, c2, d2 = (
self.a, self.b, self.c1, self.d1, self.c2, self.d2
)
self._assert_upgrade(
"c2branch@head-1", b.revision,
[self.up_(c2)],
set([c2.revision])
)
def test_relative_downgrade(self):
a, b, c1, d1, c2, d2 = (
self.a, self.b, self.c1, self.d1, self.c2, self.d2
)
self._assert_downgrade(
"c2branch@base+2", [d2.revision, d1.revision],
[self.down_(d2), self.down_(c2), self.down_(d1)],
set([c1.revision])
)
class BranchFromMergepointTest(MigrationTest):
"""this is a form that will come up frequently in the