From: Mike Bayer Date: Mon, 3 Aug 2015 23:18:30 +0000 (-0400) Subject: - Fixed bug where in the erroneous case that alembic_version contains X-Git-Tag: rel_0_8_0~5 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=e155fa69a628a89215f2ef843393d4f9f4dde758;p=thirdparty%2Fsqlalchemy%2Falembic.git - Fixed bug where in the erroneous case that alembic_version contains duplicate revisions, some commands would fail to process the version history correctly and end up with a KeyError. The fix allows the versioning logic to proceed, however a clear error is emitted later when attempting to update the alembic_version table. fixes #314 --- diff --git a/alembic/script/revision.py b/alembic/script/revision.py index e618c4d0..c1750a06 100644 --- a/alembic/script/revision.py +++ b/alembic/script/revision.py @@ -618,7 +618,8 @@ class RevisionMap(object): limit_to_lower_branch = \ isinstance(lower, compat.string_types) and lower.endswith('@base') - uppers = self.get_revisions(upper) + uppers = util.dedupe_tuple(self.get_revisions(upper)) + if not uppers and not requested_lowers: raise StopIteration() diff --git a/alembic/util/__init__.py b/alembic/util/__init__.py index ff08ad9c..a1110008 100644 --- a/alembic/util/__init__.py +++ b/alembic/util/__init__.py @@ -1,5 +1,5 @@ from .langhelpers import ( # noqa - asbool, rev_id, to_tuple, to_list, memoized_property, + asbool, rev_id, to_tuple, to_list, memoized_property, dedupe_tuple, immutabledict, _with_legacy_names, Dispatcher, ModuleClsProxy) from .messaging import ( # noqa write_outstream, status, err, obfuscate_url_pw, warn, msg, format_as_comma) diff --git a/alembic/util/langhelpers.py b/alembic/util/langhelpers.py index 6c92e3c6..9445949e 100644 --- a/alembic/util/langhelpers.py +++ b/alembic/util/langhelpers.py @@ -208,6 +208,24 @@ def to_tuple(x, default=None): raise ValueError("Don't know how to turn %r into a tuple" % x) +def unique_list(seq, hashfunc=None): + seen = set() + seen_add = seen.add + if not hashfunc: + return [x for x in seq + if x not in seen + and not seen_add(x)] + else: + return [x for x in seq + if hashfunc(x) not in seen + and not seen_add(hashfunc(x))] + + +def dedupe_tuple(tup): + return tuple(unique_list(tup)) + + + class memoized_property(object): """A read-only @property that is only evaluated once.""" diff --git a/docs/build/changelog.rst b/docs/build/changelog.rst index e69732e7..e1a5a760 100644 --- a/docs/build/changelog.rst +++ b/docs/build/changelog.rst @@ -124,6 +124,15 @@ Changelog :ref:`alembic.autogenerate.toplevel` + .. change:: + :tags: bug, versioning + :tickets: 314 + + Fixed bug where in the erroneous case that alembic_version contains + duplicate revisions, some commands would fail to process the + version history correctly and end up with a KeyError. The fix + allows the versioning logic to proceed, however a clear error is + emitted later when attempting to update the alembic_version table. .. changelog:: :version: 0.7.7 diff --git a/tests/test_command.py b/tests/test_command.py index aa8efa49..b57e3b34 100644 --- a/tests/test_command.py +++ b/tests/test_command.py @@ -184,6 +184,20 @@ finally: command.revision, self.cfg, autogenerate=True ) + def test_err_correctly_raised_on_dupe_rows(self): + self._env_fixture() + command.revision(self.cfg) + r2 = command.revision(self.cfg) + db = _sqlite_file_db() + command.upgrade(self.cfg, "head") + db.execute("insert into alembic_version values ('%s')" % r2.revision) + assert_raises_message( + util.CommandError, + "Online migration expected to match one row when " + "updating .* in 'alembic_version'; 2 found", + command.downgrade, self.cfg, "-1" + ) + def test_create_rev_plain_need_to_select_head(self): self._env_fixture() command.revision(self.cfg) diff --git a/tests/test_revision.py b/tests/test_revision.py index a96aa5b2..45687eab 100644 --- a/tests/test_revision.py +++ b/tests/test_revision.py @@ -94,6 +94,23 @@ class APITest(TestBase): ) eq_(map_.get_revision('base'), None) + def test_iterate_tolerates_dupe_targets(self): + map_ = RevisionMap( + lambda: [ + Revision('a', ()), + Revision('b', ('a',)), + Revision('c', ('b',)), + ] + ) + + eq_( + [ + r.revision for r in + map_._iterate_revisions(('c', 'c'), 'a') + ], + ['c', 'b', 'a'] + ) + class DownIterateTest(TestBase): def _assert_iteration(