]> git.ipfire.org Git - thirdparty/sqlalchemy/alembic.git/commitdiff
add check command for upgrade diffs
authorNathan Louie <nxlouie@umich.edu>
Tue, 13 Dec 2022 17:58:09 +0000 (12:58 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Thu, 15 Dec 2022 16:44:48 +0000 (11:44 -0500)
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

alembic/__init__.py
alembic/command.py
alembic/util/__init__.py
alembic/util/exc.py
docs/build/autogenerate.rst
docs/build/changelog.rst
docs/build/unreleased/724.rst [new file with mode: 0644]
tests/test_command.py

index 25833e5b7c58922e19e4a6622821f8396f52ca5d..26180b10e8d0867269499edd406576325dd24935 100644 (file)
@@ -3,4 +3,4 @@ import sys
 from . import context
 from . import op
 
-__version__ = "1.8.2"
+__version__ = "1.9.0"
index 5c33a95eadf49de9823cc237ec004fcde27c991d..d2c5c85f7c9ca8a22543688c2c3eacdc17f94878 100644 (file)
@@ -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,
index d5fa4d32550ddecd0d978bdbf1f1578dee00ef57..4374f46a1f0db06ffcbb28218f7b3ce56496b383 100644 (file)
@@ -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
index f7ad0211527a1506fd4a9747d1fd6cf07ef0f499..0d0496b1e2967c5bdaca854531a9a2df339e425f 100644 (file)
@@ -1,2 +1,6 @@
 class CommandError(Exception):
     pass
+
+
+class AutogenerateDiffsDetected(CommandError):
+    pass
index a623372dc820c84cf5a0570304d25bb72ea8eed3..4cb800662cc2cc4502812372398a284d2de57f43 100644 (file)
@@ -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
index f88978a5a2ccc25445553f7fb01b47c1f83d33b1..178a2a20ef96f40fb44f2f153b28504b0318fa95 100644 (file)
@@ -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 (file)
index 0000000..5bd1987
--- /dev/null
@@ -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`
+
index e136c4e706b39dc45df8ad7e00cb2d71bafd8361..5ec3567927a482cf61098f23cc88743afaeeba5a 100644 (file)
@@ -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 = (