From 2639f8f5f85294a84414a5adeb8bcce601e8977a Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sun, 23 Nov 2014 15:23:52 -0500 Subject: - 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. --- alembic/compat.py | 3 + alembic/revision.py | 140 ++++++++++++++++++++++++++-------------- docs/build/branches.rst | 5 ++ docs/build/changelog.rst | 8 +++ docs/build/tutorial.rst | 30 +++++---- tests/test_version_traversal.py | 46 +++++++++++++ 6 files changed, 174 insertions(+), 58 deletions(-) diff --git a/alembic/compat.py b/alembic/compat.py index d7a1303..a9e35f0 100644 --- a/alembic/compat.py +++ b/alembic/compat.py @@ -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 diff --git a/alembic/revision.py b/alembic/revision.py index 1afb203..7a09e1a 100644 --- a/alembic/revision.py +++ b/alembic/revision.py @@ -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): - - 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_upper = self._relative_iterate( + upper, lower, True, implicit_base, + inclusive, assert_relative_length + ) + if relative_upper: + return relative_upper - if branch_label: - to_ = "%s@base" % branch_label - else: - to_ = "base" + relative_lower = self._relative_iterate( + lower, upper, False, implicit_base, + inclusive, assert_relative_length + ) + if relative_lower: + return relative_lower - 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): diff --git a/docs/build/branches.rst b/docs/build/branches.rst index 796cfb9..d0e3a6f 100644 --- a/docs/build/branches.rst +++ b/docs/build/branches.rst @@ -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: diff --git a/docs/build/changelog.rst b/docs/build/changelog.rst index 95a690d..4e9a97c 100644 --- a/docs/build/changelog.rst +++ b/docs/build/changelog.rst @@ -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 diff --git a/docs/build/tutorial.rst b/docs/build/tutorial.rst index 5eda91d..de2a93e 100644 --- a/docs/build/tutorial.rst +++ b/docs/build/tutorial.rst @@ -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 -============================= - -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:: +Relative identifiers may also be in terms of a specific revision. For example, +to upgrade to revision ``ae1027a6acf`` plus two additional steps:: - $ alembic upgrade ae1 + $ alembic upgrade ae10+2 -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 =================== diff --git a/tests/test_version_traversal.py b/tests/test_version_traversal.py index 1fc99e2..72ae03a 100644 --- a/tests/test_version_traversal.py +++ b/tests/test_version_traversal.py @@ -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 -- cgit v1.2.1