Skip to content

Historical migrations with index_together crash on Django 5.1+ for partitioned models (TypeError: 'class Meta' got invalid attribute(s): index_together) #286

Description

@lorthirk

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

  1. A PostgresPartitionedModel that once used index_together, with the historical migration
    still present (AlterIndexTogether, or index_together in a CreateModel options dict).
  2. On Django ≥ 5.1, run manage.py migrate (or the test runner — migrates from scratch).
  3. → 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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions