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:
# 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
# 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
# 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
# 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 = [
# 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 = [
# 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.
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
# 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 = [
--- /dev/null
+.. 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.
)
+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()