Disclaimer: Co-Authored with Claude Code
Environment
- django-postgres-extra: 2.0.9 (same code on
master)
- Django: 5.2.16 (reproduces on any 5.1+)
- Python: 3.12 · PostgreSQL: 18
Summary
On Django 5.1+, applying/rendering a historical migration that contains index_together
(via migrations.AlterIndexTogether(...) or CreateModel(options={'index_together': ...}))
for a PostgresPartitionedModel crashes:
TypeError: 'class Meta' got invalid attribute(s): index_together
… psqlextra/backend/migrations/state/model.py:98 render
return type(*self._pre_render(self.name, bases, attributes))
… psqlextra/models/partitioned.py:23 __new__
… django/db/models/base.py:145 add_to_class → Options(meta, app_label)
… django/db/models/options.py:227 contribute_to_class → raise TypeError
Stock (non-partitioned) models are NOT affected — only partitioned models rendered by psqlextra.
Root cause
Django 5.1 removed index_together from django.db.models.options.DEFAULT_NAMES. Django fixed
its own state rendering to tolerate index_together in historical ModelState
(ticket #34856, commit b44efdfe, backported to 5.1.x as 2ee6ca6d); AlterIndexTogether stays
supported for historical migrations.
But PostgresPartitionedModelState.render() builds Meta straight from self.options:
meta = type("Meta", (), {"app_label": self.app_label, "apps": apps, **self.options})
so index_together (still in historical state) is passed to Options.contribute_to_class,
which now rejects it. _pre_render() doesn't filter it either. This bypasses Django's #34856
handling. master has identical code.
Steps to reproduce
- A
PostgresPartitionedModel that once used index_together, with the historical migration
still present (AlterIndexTogether, or index_together in a CreateModel options dict).
- On Django ≥ 5.1, run
manage.py migrate (or the test runner — migrates from scratch).
- → TypeError: 'class Meta' got invalid attribute(s): index_together
Expected
Historical partitioned-model state containing index_together should render without crashing on
Django 5.1+, consistent with stock Django after #34856 — no forced hand-editing of migrations.
Suggested fix
Mirror Django's #34856 handling in the partitioned state render(): before building the Meta
class, drop options no longer accepted by the current Options (notably index_together) from
self.options — e.g. filter to keys in Options.DEFAULT_NAMES, or explicitly pop
index_together.
Workaround
Hand-edit the historical migrations to use AddIndex with explicit index names; for existing
DBs, reconcile physical index names with a conditional RenameIndex(old_fields=...) / RunSQL
migration that renames only when the old-named index exists (validate against a prod-like DB).
Disclaimer: Co-Authored with Claude Code
Environment
master)Summary
On Django 5.1+, applying/rendering a historical migration that contains
index_together(via
migrations.AlterIndexTogether(...)orCreateModel(options={'index_together': ...}))for a
PostgresPartitionedModelcrashes:Stock (non-partitioned) models are NOT affected — only partitioned models rendered by psqlextra.
Root cause
Django 5.1 removed
index_togetherfromdjango.db.models.options.DEFAULT_NAMES. Django fixedits own state rendering to tolerate
index_togetherin historicalModelState(ticket #34856, commit b44efdfe, backported to 5.1.x as 2ee6ca6d);
AlterIndexTogetherstayssupported for historical migrations.
But
PostgresPartitionedModelState.render()builds Meta straight fromself.options:so
index_together(still in historical state) is passed toOptions.contribute_to_class,which now rejects it.
_pre_render()doesn't filter it either. This bypasses Django's #34856handling.
masterhas identical code.Steps to reproduce
PostgresPartitionedModelthat once usedindex_together, with the historical migrationstill present (
AlterIndexTogether, orindex_togetherin aCreateModeloptions dict).manage.py migrate(or the test runner — migrates from scratch).Expected
Historical partitioned-model state containing
index_togethershould render without crashing onDjango 5.1+, consistent with stock Django after #34856 — no forced hand-editing of migrations.
Suggested fix
Mirror Django's #34856 handling in the partitioned state
render(): before building the Metaclass, drop options no longer accepted by the current
Options(notablyindex_together) fromself.options— e.g. filter to keys inOptions.DEFAULT_NAMES, or explicitly popindex_together.Workaround
Hand-edit the historical migrations to use
AddIndexwith explicit index names; for existingDBs, reconcile physical index names with a conditional
RenameIndex(old_fields=...)/RunSQLmigration that renames only when the old-named index exists (validate against a prod-like DB).