Skip to content

Commit 51029b2

Browse files
zzzeekGerrit Code Review
authored andcommitted
Merge "Add "--check-heads" option to "current" command" into main
2 parents 5716f33 + 2199213 commit 51029b2

File tree

9 files changed

+117
-3
lines changed

9 files changed

+117
-3
lines changed

alembic/command.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -681,11 +681,18 @@ def branches(config: Config, verbose: bool = False) -> None:
681681
)
682682

683683

684-
def current(config: Config, verbose: bool = False) -> None:
684+
def current(
685+
config: Config, check_heads: bool = False, verbose: bool = False
686+
) -> None:
685687
"""Display the current revision for a database.
686688
687689
:param config: a :class:`.Config` instance.
688690
691+
:param check_heads: Check if all head revisions are applied to the
692+
database. Raises :class:`.DatabaseNotAtHead` if this is not the case.
693+
694+
.. versionadded:: 1.17.1
695+
689696
:param verbose: output in verbose mode.
690697
691698
"""
@@ -698,6 +705,12 @@ def display_version(rev, context):
698705
"Current revision(s) for %s:",
699706
util.obfuscate_url_pw(context.connection.engine.url),
700707
)
708+
if check_heads and (
709+
set(context.get_current_heads()) != set(script.get_heads())
710+
):
711+
raise util.DatabaseNotAtHead(
712+
"Database is not on all head revisions"
713+
)
701714
for rev in script.get_all_current(rev):
702715
config.print_stdout(rev.cmd_format(verbose))
703716

alembic/config.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -808,6 +808,17 @@ def __init__(self, prog: Optional[str] = None) -> None:
808808
"environment and version locations",
809809
),
810810
),
811+
"check_heads": (
812+
"-c",
813+
"--check-heads",
814+
dict(
815+
action="store_true",
816+
help=(
817+
"Check if all head revisions are applied to the database. "
818+
"Exit with an error code if this is not the case."
819+
),
820+
),
821+
),
811822
}
812823
_POSITIONAL_OPTS = {
813824
"directory": dict(help="location of scripts directory"),

alembic/util/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from .editor import open_in_editor as open_in_editor
22
from .exc import AutogenerateDiffsDetected as AutogenerateDiffsDetected
33
from .exc import CommandError as CommandError
4+
from .exc import DatabaseNotAtHead as DatabaseNotAtHead
45
from .langhelpers import _with_legacy_names as _with_legacy_names
56
from .langhelpers import asbool as asbool
67
from .langhelpers import dedupe_tuple as dedupe_tuple

alembic/util/exc.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,28 @@
1010

1111

1212
class CommandError(Exception):
13-
pass
13+
"""Base command error for all exceptions"""
14+
15+
16+
class DatabaseNotAtHead(CommandError):
17+
"""Indicates the database is not at current head revisions.
18+
19+
Raised by the :func:`.command.current` command when the
20+
:paramref:`.command.current.check_heads` parameter is used.
21+
22+
.. versionadded:: 1.17.1
23+
24+
"""
1425

1526

1627
class AutogenerateDiffsDetected(CommandError):
28+
"""Raised when diffs were detected by the :func:`.command.check`
29+
command.
30+
31+
.. versionadded:: 1.9.0
32+
33+
"""
34+
1735
def __init__(
1836
self,
1937
message: str,

docs/build/api/exceptions.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
.. _alembic.runtime.exceptions.toplevel:
2+
3+
=======================
4+
Exception Objects
5+
=======================
6+
7+
8+
.. automodule:: alembic.util.exc
9+
:members:

docs/build/api/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,5 @@ to run commands programmatically, as discussed in the section :doc:`/api/command
2929
autogenerate
3030
script
3131
ddl
32+
exceptions
3233

docs/build/cookbook.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1480,10 +1480,24 @@ at :func:`.autogenerate.compare_metadata`::
14801480

14811481

14821482

1483+
.. _cookbook_check_heads:
14831484

14841485
Test current database revision is at head(s)
14851486
============================================
14861487

1488+
.. versionchanged:: 1.17.1 This recipe is now part of the ``alembic current``
1489+
command using the :paramref:`.command.current.check_heads` parameter,
1490+
available from the command line as ``--check-heads``:
1491+
1492+
.. sourcecode::
1493+
1494+
alembic current --check-heads
1495+
1496+
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
1497+
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
1498+
ERROR [alembic.util.messaging] Database is not on all head revisions
1499+
FAILED: Database is not on all head revisions
1500+
14871501
A recipe to determine if a database schema is up to date in terms of applying
14881502
Alembic migrations. May be useful for test or installation suites to
14891503
determine if the target database is up to date. Makes use of the

docs/build/unreleased/1705.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
.. change::
2+
:tags: usecase, commands
3+
:tickets: 1705
4+
5+
Added :paramref:`.command.current.check_heads` parameter to
6+
:func:`.command.current` command, available from the command line via the
7+
``--check-heads`` option to ``alembic current``. This tests if all head
8+
revisions are applied to the database and raises :class:`.DatabaseNotAtHead`
9+
(or from the command line, exits with a non-zero exit code) if this is not
10+
the case. The parameter operates equvialently to the cookbook recipe
11+
:ref:`cookbook_check_heads`. Pull request courtesy Stefan Scherfke.

tests/test_command.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,8 @@ def _assert_lines(self, revs):
311311
eq_(lines, set(revs))
312312

313313
def test_doesnt_create_alembic_version(self):
314+
with self.bind.begin() as conn:
315+
conn.exec_driver_sql("drop table if exists alembic_version")
314316
command.current(self.cfg)
315317
engine = self.bind
316318
with engine.connect() as conn:
@@ -345,6 +347,40 @@ def test_heads_upg(self):
345347
with self._assert_lines(["a2", "b3"]):
346348
command.current(self.cfg)
347349

350+
def test_check_heads_success(self):
351+
"""
352+
"--check-heads" succeeds if all head revisions are applied.
353+
"""
354+
command.stamp(self.cfg, ())
355+
command.stamp(self.cfg, (self.a3.revision, self.b3.revision))
356+
with self._assert_lines(["a3", "b3"]):
357+
command.current(self.cfg, check_heads=True)
358+
359+
@testing.combinations(
360+
["a2"],
361+
["a3"],
362+
["b3"],
363+
["a2", "b3"],
364+
["a3", "b2"],
365+
argnames="revs",
366+
)
367+
def test_check_heads_fail(self, revs):
368+
"""
369+
"--check-heads" succeeds if all head revisions are applied.
370+
"""
371+
command.stamp(self.cfg, ())
372+
command.stamp(
373+
self.cfg, tuple(getattr(self, rev).revision for rev in revs)
374+
)
375+
assert_raises_message(
376+
util.DatabaseNotAtHead,
377+
"Database is not on all head revisions",
378+
command.current,
379+
self.cfg,
380+
check_heads=True,
381+
)
382+
command.stamp(self.cfg, ())
383+
348384

349385
class RevisionTest(TestBase):
350386
def setUp(self):
@@ -1562,7 +1598,7 @@ def existing_pyproject_fixture(self):
15621598
yield config.Config(
15631599
self.cfg.config_file_name, toml_file=root / "pyproject.toml"
15641600
)
1565-
shutil.rmtree(root)
1601+
os.unlink(root / "pyproject.toml")
15661602

15671603
@testing.variation("cmd", ["list_templates", "init"])
15681604
def test_init_custom_template_location(self, cmd, custom_template_fixture):

0 commit comments

Comments
 (0)