Opened 68 minutes ago
#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.py—MigrationExecutor.__init__django/db/migrations/loader.py—MigrationLoader.build_graphdjango/core/management/commands/migrate.py—Command.handledjango/core/management/commands/showmigrations.py—show_list
With these two hooks, django-clickhouse-backend (and any future backend
with similar needs) can:
- Set
migration_recorder_class = ClickHouseMigrationRecorderon itsDatabaseWrapper— a proper subclass scoped to ClickHouse connections. - Set
migration_executor_class = ClickHouseMigrationExecutoron itsDatabaseWrapper— a proper subclass that adds cluster logic without touchingMigration.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.