Fix index introspection on PostgreSQL 18.

Review Request #14923 — Created March 17, 2026 and updated

Information

Django Evolution
master

Reviewers

PostgreSQL 18 stores NOT NULL constraints in pg_constraint (with
contype='n'). Django's get_constraints() includes these in its
results, but with all type flags (index, unique, primary_key, check,
foreign_key) set to False. get_constraints_for_table() was passing
through all constraint entries unconditionally, causing these NOT NULL
constraints to be tracked as regular non-unique indexes in
DatabaseState (e.g., "tests_testmodel_int_field1_not_null").

This caused two categories of failures:

  1. ChangeField operations that modified db_index or unique would
    find unexpected NOT NULL indexes in the database state, causing
    assertion failures.

  2. ChangeMeta(indexes) operations would see NOT NULL constraint
    entries instead of the actual Django-managed indexes, generating
    incorrect DROP/CREATE INDEX SQL.

The fix filters get_constraints_for_table() to only include entries
where index, unique, or primary_key is True. This excludes NOT NULL
constraints, check constraints, and foreign key constraint entries --
none of which should be tracked as indexes in DatabaseState. (Foreign
key fields still have their actual indexes captured via separate
index=True entries, and check constraints are handled through Django's
Meta.constraints system.)

Ran dbtests-minmax

Summary ID
Fix index introspection on PostgreSQL 18.
PostgreSQL 18 stores `NOT NULL` constraints in `pg_constraint` (with `contype='n'`). Django's `get_constraints()` includes these in its results, but with all type flags (index, unique, primary_key, check, foreign_key) set to False. `get_constraints_for_table()` was passing through all constraint entries unconditionally, causing these NOT NULL constraints to be tracked as regular non-unique indexes in `DatabaseState` (e.g., "tests_testmodel_int_field1_not_null"). This caused two categories of failures: 1. `ChangeField` operations that modified `db_index` or `unique` would find unexpected `NOT NULL` indexes in the database state, causing assertion failures. 2. `ChangeMeta(indexes)` operations would see `NOT NULL` constraint entries instead of the actual Django-managed indexes, generating incorrect `DROP/CREATE INDEX SQL`. The fix filters `get_constraints_for_table()` to only include entries where index, unique, or primary_key is True. This excludes NOT NULL constraints, check constraints, and foreign key constraint entries -- none of which should be tracked as indexes in DatabaseState. (Foreign key fields still have their actual indexes captured via separate `index=True` entries, and check constraints are handled through Django's `Meta.constraints` system.) Testing Done: Ran dbtests-minmax
wyzwxmuzpkxnwrpzokqqzpoosrtoznnm
Checks run (2 succeeded)
flake8 passed.
JSHint passed.