summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNathan Louie <nxlouie@umich.edu>2022-12-13 12:58:09 -0500
committerMike Bayer <mike_mp@zzzcomputing.com>2022-12-15 11:44:48 -0500
commit4678d7f1da009689449a6550768139c32b50c646 (patch)
tree910b93354c60c1424fb00306f58474c4fcbc1674
parent3a5a7f33320d88363be18776a2076a19e600a593 (diff)
downloadalembic-4678d7f1da009689449a6550768139c32b50c646.tar.gz
add check command for upgrade diffs
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: https://github.com/sqlalchemy/alembic/pull/1101 Pull-request-sha: 807ed545df70e7a10b913e2951a1b636f138a4ff Change-Id: I03b146eaf762be464a0ff0858ff5730cc9366c84
-rw-r--r--alembic/__init__.py2
-rw-r--r--alembic/command.py56
-rw-r--r--alembic/util/__init__.py1
-rw-r--r--alembic/util/exc.py4
-rw-r--r--docs/build/autogenerate.rst42
-rw-r--r--docs/build/changelog.rst2
-rw-r--r--docs/build/unreleased/724.rst14
-rw-r--r--tests/test_command.py59
8 files changed, 178 insertions, 2 deletions
diff --git a/alembic/__init__.py b/alembic/__init__.py
index 25833e5..26180b1 100644
--- a/alembic/__init__.py
+++ b/alembic/__init__.py
@@ -3,4 +3,4 @@ import sys
from . import context
from . import op
-__version__ = "1.8.2"
+__version__ = "1.9.0"
diff --git a/alembic/command.py b/alembic/command.py
index 5c33a95..d2c5c85 100644
--- a/alembic/command.py
+++ b/alembic/command.py
@@ -240,6 +240,62 @@ def revision(
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,
diff --git a/alembic/util/__init__.py b/alembic/util/__init__.py
index d5fa4d3..4374f46 100644
--- a/alembic/util/__init__.py
+++ b/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
diff --git a/alembic/util/exc.py b/alembic/util/exc.py
index f7ad021..0d0496b 100644
--- a/alembic/util/exc.py
+++ b/alembic/util/exc.py
@@ -1,2 +1,6 @@
class CommandError(Exception):
pass
+
+
+class AutogenerateDiffsDetected(CommandError):
+ pass
diff --git a/docs/build/autogenerate.rst b/docs/build/autogenerate.rst
index a623372..4cb8006 100644
--- a/docs/build/autogenerate.rst
+++ b/docs/build/autogenerate.rst
@@ -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``. \ No newline at end of file
diff --git a/docs/build/changelog.rst b/docs/build/changelog.rst
index f88978a..178a2a2 100644
--- a/docs/build/changelog.rst
+++ b/docs/build/changelog.rst
@@ -4,7 +4,7 @@ Changelog
==========
.. changelog::
- :version: 1.8.2
+ :version: 1.9.0
:include_notes_from: unreleased
.. changelog::
diff --git a/docs/build/unreleased/724.rst b/docs/build/unreleased/724.rst
new file mode 100644
index 0000000..5bd1987
--- /dev/null
+++ b/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`
+
diff --git a/tests/test_command.py b/tests/test_command.py
index e136c4e..5ec3567 100644
--- a/tests/test_command.py
+++ b/tests/test_command.py
@@ -9,7 +9,9 @@ from typing import cast
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
@@ -537,6 +539,63 @@ finally:
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 = (