Skip to content

Commit

Permalink
add check command for upgrade diffs
Browse files Browse the repository at this point in the history
Added new Alembic command ``alembic check``. This performs the widely
requested feature of running an "autogenerate" comparison between the
current database and the :class:`.MetaData` that's currently set up for
autogenerate, returning an error code if the two do not match, based on
current autogenerate settings. Pull request courtesy Nathan Louie.

As this is a new feature we will call this 1.9.0

Fixes: #724
Closes: #1101
Pull-request: #1101
Pull-request-sha: 807ed54

Change-Id: I03b146eaf762be464a0ff0858ff5730cc9366c84
  • Loading branch information
nxlouie authored and zzzeek committed Dec 15, 2022
1 parent 3a5a7f3 commit 4678d7f
Show file tree
Hide file tree
Showing 8 changed files with 178 additions and 2 deletions.
2 changes: 1 addition & 1 deletion alembic/__init__.py
Expand Up @@ -3,4 +3,4 @@
from . import context
from . import op

__version__ = "1.8.2"
__version__ = "1.9.0"
56 changes: 56 additions & 0 deletions alembic/command.py
Expand Up @@ -240,6 +240,62 @@ def retrieve_migrations(rev, context):
return scripts


def check(
config: "Config",
) -> None:
"""Check if revision command with autogenerate has pending upgrade ops.
:param config: a :class:`.Config` object.
.. versionadded:: 1.9.0
"""

script_directory = ScriptDirectory.from_config(config)

command_args = dict(
message=None,
autogenerate=True,
sql=False,
head="head",
splice=False,
branch_label=None,
version_path=None,
rev_id=None,
depends_on=None,
)
revision_context = autogen.RevisionContext(
config,
script_directory,
command_args,
)

def retrieve_migrations(rev, context):
revision_context.run_autogenerate(rev, context)
return []

with EnvironmentContext(
config,
script_directory,
fn=retrieve_migrations,
as_sql=False,
template_args=revision_context.template_args,
revision_context=revision_context,
):
script_directory.run_env()

# the revision_context now has MigrationScript structure(s) present.

migration_script = revision_context.generated_revisions[-1]
diffs = migration_script.upgrade_ops.as_diffs()
if diffs:
raise util.AutogenerateDiffsDetected(
f"New upgrade operations detected: {diffs}"
)
else:
config.print_stdout("No new upgrade operations detected.")


def merge(
config: Config,
revisions: str,
Expand Down
1 change: 1 addition & 0 deletions alembic/util/__init__.py
@@ -1,4 +1,5 @@
from .editor import open_in_editor
from .exc import AutogenerateDiffsDetected
from .exc import CommandError
from .langhelpers import _with_legacy_names
from .langhelpers import asbool
Expand Down
4 changes: 4 additions & 0 deletions alembic/util/exc.py
@@ -1,2 +1,6 @@
class CommandError(Exception):
pass


class AutogenerateDiffsDetected(CommandError):
pass
42 changes: 42 additions & 0 deletions docs/build/autogenerate.rst
Expand Up @@ -886,3 +886,45 @@ be run against the newly generated file path::
Generating /path/to/project/versions/481b13bc369a_rev1.py ... done
Running post write hook "spaces_to_tabs" ...
done

.. _alembic_check:

Running Alembic Check to test for new upgrade operations
--------------------------------------------------------

When developing code it's useful to know if a set of code changes has made any
net change to the database model, such that new revisions would need to be
generated. To automate this, Alembic provides the ``alembic check`` command.
This command will run through the same process as
``alembic revision --autogenerate``, up until the point where revision files
would be generated, however does not generate any new files. Instead, it
returns an error code plus a message if it is detected that new operations
would be rendered into a new revision, or if not, returns a success code plus a
message. When ``alembic check`` returns a success code, this is an indication
that the ``alembic revision --autogenerate`` command would produce only empty
migrations, and does not need to be run.

``alembic check`` can be worked into CI systems and on-commit schemes to ensure
that incoming code does not warrant new revisions to be generated. In
the example below, a check that detects new operations is illustrated::


$ alembic check
FAILED: New upgrade operations detected: [
('add_column', None, 'my_table', Column('data', String(), table=<my_table>)),
('add_column', None, 'my_table', Column('newcol', Integer(), table=<my_table>))]

by contrast, when no new operations are detected::

$ alembic check
No new upgrade operations detected.


.. versionadded:: 1.9.0

.. note:: The ``alembic check`` command uses the same model comparison process
as the ``alembic revision --autogenerate`` process. This means parameters
such as :paramref:`.EnvironmentContext.configure.compare_type`
and :paramref:`.EnvironmentContext.configure.compare_server_default`
are in play as usual, as well as that limitations in autogenerate
detection are the same when running ``alembic check``.
2 changes: 1 addition & 1 deletion docs/build/changelog.rst
Expand Up @@ -4,7 +4,7 @@ Changelog
==========

.. changelog::
:version: 1.8.2
:version: 1.9.0
:include_notes_from: unreleased

.. changelog::
Expand Down
14 changes: 14 additions & 0 deletions docs/build/unreleased/724.rst
@@ -0,0 +1,14 @@
.. change::
:tags: feature, commands
:tickets: 724

Added new Alembic command ``alembic check``. This performs the widely
requested feature of running an "autogenerate" comparison between the
current database and the :class:`.MetaData` that's currently set up for
autogenerate, returning an error code if the two do not match, based on
current autogenerate settings. Pull request courtesy Nathan Louie.

.. seealso::

:ref:`alembic_check`

59 changes: 59 additions & 0 deletions tests/test_command.py
Expand Up @@ -9,7 +9,9 @@

from sqlalchemy import exc as sqla_exc
from sqlalchemy import text
from sqlalchemy import VARCHAR
from sqlalchemy.engine import Engine
from sqlalchemy.sql.schema import Column

from alembic import __version__
from alembic import command
Expand Down Expand Up @@ -537,6 +539,63 @@ def test_sensical_sql_w_env(self):
command.revision(self.cfg, sql=True)


class CheckTest(TestBase):
def setUp(self):
self.env = staging_env()
self.cfg = _sqlite_testing_config()

def tearDown(self):
clear_staging_env()

def _env_fixture(self, version_table_pk=True):
env_file_fixture(
"""
from sqlalchemy import MetaData, engine_from_config
target_metadata = MetaData()
engine = engine_from_config(
config.get_section(config.config_ini_section),
prefix='sqlalchemy.')
connection = engine.connect()
context.configure(
connection=connection, target_metadata=target_metadata,
version_table_pk=%r
)
try:
with context.begin_transaction():
context.run_migrations()
finally:
connection.close()
engine.dispose()
"""
% (version_table_pk,)
)

def test_check_no_changes(self):
self._env_fixture()
command.check(self.cfg) # no problem

def test_check_changes_detected(self):
self._env_fixture()
with mock.patch(
"alembic.operations.ops.UpgradeOps.as_diffs",
return_value=[
("remove_column", None, "foo", Column("old_data", VARCHAR()))
],
):
assert_raises_message(
util.AutogenerateDiffsDetected,
r"New upgrade operations detected: \[\('remove_column'",
command.check,
self.cfg,
)


class _StampTest:
def _assert_sql(self, emitted_sql, origin, destinations):
ins_expr = (
Expand Down

0 comments on commit 4678d7f

Please sign in to comment.