from . import context
from . import op
-__version__ = "1.8.2"
+__version__ = "1.9.0"
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,
from .editor import open_in_editor
+from .exc import AutogenerateDiffsDetected
from .exc import CommandError
from .langhelpers import _with_legacy_names
from .langhelpers import asbool
class CommandError(Exception):
pass
+
+
+class AutogenerateDiffsDetected(CommandError):
+ pass
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
==========
.. changelog::
- :version: 1.8.2
+ :version: 1.9.0
:include_notes_from: unreleased
.. changelog::
--- /dev/null
+.. 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`
+
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
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 = (