]> git.ipfire.org Git - thirdparty/sqlalchemy/alembic.git/commitdiff
support paths in file_template
authorMike Bayer <mike_mp@zzzcomputing.com>
Fri, 26 Dec 2025 17:14:49 +0000 (12:14 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Fri, 26 Dec 2025 17:17:14 +0000 (12:17 -0500)
The ``file_template`` configuration option now supports directory paths,
allowing migration files to be organized into subdirectories. When using
directory separators in ``file_template`` (e.g.,
``%(year)d/%(month).2d/%(day).2d_%(rev)s_%(slug)s``), Alembic will
automatically create the necessary directory structure. The
``recursive_version_locations`` setting must be set to ``true`` when using
this feature in order for the revision files to be located for subsequent
commands.

Fixes: #1774
Change-Id: Id68a3b0483c6519d724bbb79bbf8b471ea697c36

alembic/script/base.py
alembic/templates/async/alembic.ini.mako
alembic/templates/generic/alembic.ini.mako
alembic/templates/multidb/alembic.ini.mako
alembic/templates/pyproject/pyproject.toml.mako
alembic/templates/pyproject_async/pyproject.toml.mako
docs/build/tutorial.rst
docs/build/unreleased/1774.rst [new file with mode: 0644]
tests/test_script_production.py

index 6b9692c63cb3705afaeb589934756b20f70b13b8..f841708598d05cb87396a73e6525e2a3e3a81520 100644 (file)
@@ -696,6 +696,7 @@ class ScriptDirectory:
             self._ensure_directory(version_path)
 
         path = self._rev_path(version_path, revid, message, create_date)
+        self._ensure_directory(path.parent)
 
         if not splice:
             for head_ in heads:
index 62617c4c237a34f5e28ba792188c8ada684c8d67..02ccb0f6de8d6654b151721a0fe4f9441d194318 100644 (file)
@@ -12,6 +12,8 @@ script_location = ${script_location}
 # see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
 # for all available tokens
 # file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
+# Or organize into date-based subdirectories (requires recursive_version_locations = true)
+# file_template = %%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s
 
 # sys.path path, will be prepended to sys.path if present.
 # defaults to the current working directory.  for multiple paths, the path separator
index eb8c063fcdda95f68d17a743c93166d7308ca244..0127b2af8da1d11127062dc22de75206211c081f 100644 (file)
@@ -12,6 +12,8 @@ script_location = ${script_location}
 # see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
 # for all available tokens
 # file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
+# Or organize into date-based subdirectories (requires recursive_version_locations = true)
+# file_template = %%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s
 
 # sys.path path, will be prepended to sys.path if present.
 # defaults to the current working directory.  for multiple paths, the path separator
index f4b65e6201c0f468308d35b504e084557dfbbe3c..76846465bead038ac27e7460c8dd9079f4850a1f 100644 (file)
@@ -12,6 +12,8 @@ script_location = ${script_location}
 # see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
 # for all available tokens
 # file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
+# Or organize into date-based subdirectories (requires recursive_version_locations = true)
+# file_template = %%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s
 
 # sys.path path, will be prepended to sys.path if present.
 # defaults to the current working directory.  for multiple paths, the path separator
index 224faec1e36e98e58dbb5aa88aee390298660403..7edd43b0c9aad1e751220c1bd2beacd31873d5d6 100644 (file)
@@ -11,6 +11,8 @@ script_location = "${script_location}"
 # see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
 # for all available tokens
 # file_template = "%%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s"
+# Or organize into date-based subdirectories (requires recursive_version_locations = true)
+# file_template = "%%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s"
 
 # additional paths to be prepended to sys.path. defaults to the current working directory.
 prepend_sys_path = [
index 224faec1e36e98e58dbb5aa88aee390298660403..7edd43b0c9aad1e751220c1bd2beacd31873d5d6 100644 (file)
@@ -11,6 +11,8 @@ script_location = "${script_location}"
 # see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
 # for all available tokens
 # file_template = "%%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s"
+# Or organize into date-based subdirectories (requires recursive_version_locations = true)
+# file_template = "%%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s"
 
 # additional paths to be prepended to sys.path. defaults to the current working directory.
 prepend_sys_path = [
index 365842b04353d1079abe4c0fe9b53c8c8a00ff5d..60242bd640f876eaa24bc0dfb41de3cb96406a6f 100644 (file)
@@ -157,6 +157,8 @@ The all-in-one .ini file created by ``generic`` is illustrated below::
     # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
     # Uncomment the line below if you want the files to be prepended with date and time
     # file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
+    # Or organize into date-based subdirectories (requires recursive_version_locations = true)
+    # file_template = %%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s
 
     # sys.path path, will be prepended to sys.path if present.
     # defaults to the current working directory.
@@ -361,6 +363,20 @@ This file contains the following features:
       by default ``datetime.datetime.now()`` unless the ``timezone``
       configuration option is also used.
 
+  The ``file_template`` may also include directory separators to organize
+  migration files into subdirectories. When using directory paths in
+  ``file_template``, ``recursive_version_locations`` must be set to ``true``.
+  For example::
+
+      file_template = %%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s
+      recursive_version_locations = true
+
+  This would create migration files organized by date in a structure like
+  ``versions/2024/12/26_143022_abc123_add_user_table.py``.
+
+  .. versionadded:: 1.18.0
+     Support for directory paths in ``file_template``
+
 * ``timezone`` - an optional timezone name (e.g. ``UTC``, ``EST5EDT``, etc.)
   that will be applied to the timestamp which renders inside the migration
   file's comment as well as within the filename. This option requires Python>=3.9
@@ -625,6 +641,8 @@ remains available as the absolute path to the ``pyproject.toml`` file::
     # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
     # Uncomment the line below if you want the files to be prepended with date and time
     # file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
+    # Or organize into date-based subdirectories (requires recursive_version_locations = true)
+    # file_template = %%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s
 
     # additional paths to be prepended to sys.path. defaults to the current working directory.
     prepend_sys_path = [
diff --git a/docs/build/unreleased/1774.rst b/docs/build/unreleased/1774.rst
new file mode 100644 (file)
index 0000000..acf89af
--- /dev/null
@@ -0,0 +1,12 @@
+.. change::
+    :tags: usecase, environment
+    :tickets: 1774
+
+    The ``file_template`` configuration option now supports directory paths,
+    allowing migration files to be organized into subdirectories. When using
+    directory separators in ``file_template`` (e.g.,
+    ``%(year)d/%(month).2d/%(day).2d_%(rev)s_%(slug)s``), Alembic will
+    automatically create the necessary directory structure. The
+    ``recursive_version_locations`` setting must be set to ``true`` when using
+    this feature in order for the revision files to be located for subsequent
+    commands.
index b8a3c8c6c2f5f55a17880ba8138419bcf141972c..a9145773d04b58cc0c4059eebe0307e8818a8a5e 100644 (file)
@@ -331,6 +331,64 @@ class ScriptNamingTest(TestBase):
                 )
 
 
+class FileTemplateDirectoryTest(TestBase):
+    """Test file_template with directory paths."""
+
+    def setUp(self):
+        self.env = staging_env()
+
+    def tearDown(self):
+        clear_staging_env()
+
+    @testing.variation("use_recursive_version_locations", [True, False])
+    def test_file_template_with_directory_path(
+        self, use_recursive_version_locations
+    ):
+        """Test that file_template supports directory paths."""
+        script = ScriptDirectory(
+            self.env.dir,
+            file_template="%(year)d/%(month).2d/" "%(day).2d_%(rev)s_%(slug)s",
+            recursive_version_locations=bool(use_recursive_version_locations),
+        )
+
+        create_date = datetime.datetime(2024, 12, 26, 14, 30, 22)
+        with mock.patch.object(
+            script, "_generate_create_date", return_value=create_date
+        ):
+            generated_script = script.generate_revision(
+                util.rev_id(), "test message"
+            )
+
+        # Verify file was created in subdirectory structure
+        # regardless of recursive_version_locations setting
+        assert generated_script is not None
+        expected_path = (
+            Path(self.env.dir)
+            / "versions"
+            / "2024"
+            / "12"
+            / f"26_{generated_script.revision}_test_message.py"
+        )
+        eq_(Path(generated_script.path), expected_path)
+        assert expected_path.exists()
+
+        # Verify the script is loadable with recursive_version_locations,
+        # but not if it's not set
+        script2 = ScriptDirectory(
+            self.env.dir,
+            file_template="%(year)d/%(month).2d/" "%(day).2d_%(rev)s_%(slug)s",
+            recursive_version_locations=bool(use_recursive_version_locations),
+        )
+        if use_recursive_version_locations:
+            assert generated_script.revision in [
+                rev.revision for rev in script2.walk_revisions()
+            ]
+        else:
+            assert generated_script.revision not in [
+                rev.revision for rev in script2.walk_revisions()
+            ]
+
+
 class RevisionCommandTest(TestBase):
     def setUp(self):
         self.env = staging_env()