#37061: Add migration_recorder_class and migration_executor_class hooks to
BaseDatabaseWrapper so third-party backends can customise migration
infrastructure without monkey-patching Django internals.
-------------------------------------+-------------------------------------
Reporter: Laurent Tramoy | Type:
| Cleanup/optimization
Status: new | Component:
| Migrations
Version: 6.0 | Severity: Normal
Keywords: | Triage Stage:
| Unreviewed
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
== Context
I'd like to use clickhouse inside my django project. The main project to
do that is [
https://github.com/jayvynl/django-clickhouse-backend 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_graph`
- `django/core/management/commands/migrate.py` — `Command.handle`
- `django/core/management/commands/showmigrations.py` — `show_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.
--
Ticket URL: <
https://code.djangoproject.com/ticket/37061>
Django <
https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.