summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2014-11-23 15:23:52 -0500
committerMike Bayer <mike_mp@zzzcomputing.com>2014-11-23 15:23:52 -0500
commit2639f8f5f85294a84414a5adeb8bcce601e8977a (patch)
treee1a37dbede088ce44759d7adf37abb4e9280d6c9
parent6b0b54b35bb4039a59d3ac53c58b998800072726 (diff)
downloadalembic-2639f8f5f85294a84414a5adeb8bcce601e8977a.tar.gz
- 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.
-rw-r--r--alembic/compat.py3
-rw-r--r--alembic/revision.py140
-rw-r--r--docs/build/branches.rst5
-rw-r--r--docs/build/changelog.rst8
-rw-r--r--docs/build/tutorial.rst30
-rw-r--r--tests/test_version_traversal.py46
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
@@ -73,6 +73,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