]> git.ipfire.org Git - thirdparty/sqlalchemy/alembic.git/commitdiff
- Relative revision identifiers as used with ``alembic upgrade``,
authorMike Bayer <mike_mp@zzzcomputing.com>
Sun, 23 Nov 2014 20:23:52 +0000 (15:23 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sun, 23 Nov 2014 20:23:52 +0000 (15:23 -0500)
``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
alembic/revision.py
docs/build/branches.rst
docs/build/changelog.rst
docs/build/tutorial.rst
tests/test_version_traversal.py

index d7a13031694986208126c4982e560f175d729318..a9e35f0ba858545336dee85053dc68efd261fb27 100644 (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
index 1afb203dcb191fa6de97d652f9ffcd08cabb00b1..7a09e1a729c5ca9b8c893aeb7a3b26a72a5eeb5b 100644 (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):
-
-            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):
index 796cfb955e1c086fd178b364ae7086b25eaca832..d0e3a6f97ba290862c8fde240bdc7c3a4f850b72 100644 (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:
 
index 95a690d2a55bdf867790601c9021eb2fe044f801..4e9a97c9d773923829bacae60ff25e7e031cb1f2 100644 (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
index 5eda91dbf780ce96431275e5402697fdfe96d93d..de2a93e1082297f396df65292da71a1d1e3e59f7 100644 (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
-=============================
-
-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
 ===================
index 1fc99e21692b3d9fc5a85dfddd4655ac846e9d41..72ae03a405df5a4f8c2f13cb7c434baf6a54b678 100644 (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