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):
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):
"""
- 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):
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
$ 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
===================
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(
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(
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