#37061 new Cleanup/optimization

Add migration_recorder_class and migration_executor_class hooks to BaseDatabaseWrapper so third-party backends can customise migration infrastructure without monkey-patching Django internals.

Reported by: Laurent Tramoy Owned by:
Component: Migrations Version: 6.0
Severity: Normal Keywords:
Cc: Triage Stage: Unreviewed
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

Context

I'd like to use clickhouse inside my django project. The main project to do that is django-clickhouse-backend,
but it currently patches too many things related to migrations, that's an issue in my project.

Third-party database backends that need to customise Django's migration behaviour currently
have no way to extend relevant classes. The only available mechanism is to monkey-patch
MigrationRecorder (to customise migration tracking) and Migration.apply (to inject
per-operation logic before execution), affecting the entire Django process globally.

With migration_executor_class, a backend can instead subclass MigrationExecutor and
override apply_migration — the method that calls migration.apply(). Per-operation
logic moves up to the executor level, and Migration.apply itself never needs to be
touched.

Proposed change

Add two None defaulting class attributes to BaseDatabaseWrapper,
following the exact same pattern already used for schema_editor_class,
creation_class, introspection_class, ops_class, and
validation_class:

# django/db/backends/base/base.py
class BaseDatabaseWrapper:
    ...
    migration_recorder_class = None   # defaults to MigrationRecorder
    migration_executor_class = None   # defaults to MigrationExecutor

And update the four files that instantiate these classes to respect
the hook:

  • django/db/migrations/executor.pyMigrationExecutor.__init__
  • django/db/migrations/loader.pyMigrationLoader.build_graph
  • django/core/management/commands/migrate.pyCommand.handle
  • django/core/management/commands/showmigrations.pyshow_list

With these two hooks, django-clickhouse-backend (and any future backend
with similar needs) can:

  1. Set migration_recorder_class = ClickHouseMigrationRecorder on its DatabaseWrapper — a proper subclass scoped to ClickHouse connections.
  2. Set migration_executor_class = ClickHouseMigrationExecutor on its DatabaseWrapper — a proper subclass that adds cluster logic without touching Migration.apply.

Why both hooks?

migration_recorder_class allows a backend to replace the
django_migrations tracking table with a backend-appropriate equivalent
(e.g. a ClickHouse MergeTree table instead of a standard Django model,
with backend-specific semantics for record_applied, flush, etc.).

migration_executor_class allows a backend to inject cluster-aware
logic *above* Migration.apply — for example, skipping a migration
operation that was already executed on a remote replica — without needing
to touch Migration.apply itself. This is exactly the scenario that
currently forces django-clickhouse-backend to patch Migration.apply
globally.

Why None as default instead of pointing at the built-in classes?

Using None (resolved at call sites via or MigrationRecorder) avoids
a circular import: base.py is imported very early and importing
MigrationRecorder or MigrationExecutor there would pull in the
migrations module graph before it is needed. The None sentinel makes the
intent explicit and keeps the default behaviour identical to today.

Change History (0)

Note: See TracTickets for help on using tickets.
Back to Top