Summary
website_publication_keywords (the Publication↔Keyword M2M through table) carries a leftover sort_value column — a relic from when keywords was a SortedManyToManyField. It's now a plain ManyToManyField in the model, so the column is vestigial and should be dropped to make the schema match the model across all environments.
Surfaced while fixing the keyword-merge action (#1352 / PR #1362). The merge itself is already fixed (it now repoints existing rows instead of inserting), so this issue is the deeper cleanup, not urgent — but it also fixes a separate latent bug (see below).
Evidence it's a legacy artifact (from the prod dump, 2026-06-14)
- Of the six
website_*_keywords through tables, only website_publication_keywords has a sort_value column. The other five (grant/poster/project/projectumbrella/talk) have none — confirming a plain M2M never creates it.
- In that table's data: 986 of 1,215 rows have sequential
sort_values (1..N per publication) — the SortedManyToManyField signature — and 229 are NULL (newer plain .add()s).
- The genuinely-sorted tables (authors, project_umbrellas, award recipients) all show
sort_value integer NOT NULL, as expected.
Environment drift (important)
The two environments disagree on this one column's constraint (independent gitignored per-env migration histories):
- prod:
sort_value is nullable → a plain .add() inserting NULL succeeds.
- -test:
sort_value is NOT NULL → a plain .add() 500s with null value in column "sort_value" ... violates not-null constraint.
Latent bug this fixes
On -test, any INSERT into website_publication_keywords fails — that includes editing a Publication's keywords in the admin and saving, not just the merge action. Masked on prod only because prod's column is nullable. Dropping the column fixes this everywhere.
Proposed approach
Django won't auto-generate the drop (the model is already a plain M2M with no sort_value), so it needs an explicit, idempotent migration:
- A
migrations.RunSQL (or schema_editor) op that drops the column only if it exists, e.g. ALTER TABLE website_publication_keywords DROP COLUMN IF EXISTS sort_value; — safe on the five envs/tables that never had it and on prod (nullable) and -test (NOT NULL) alike.
- Wrap with a no-op reverse.
- Mind the deploy constraints: migrations run inside the container on each deploy via
docker-entrypoint.sh, and there's no shell on prod to fix a half-applied migration — so the op must be idempotent and not fail on any current env state. DROP COLUMN IF EXISTS satisfies this.
- Verify locally first (the model-built
settings_test DB won't have the column, so also test against a DB that does — add the column, run the migration, confirm clean).
Acceptance
Context: PR #1362, #1352. See repipoint fix rationale in PR #1362's discussion.
Summary
website_publication_keywords(the Publication↔Keyword M2M through table) carries a leftoversort_valuecolumn — a relic from whenkeywordswas aSortedManyToManyField. It's now a plainManyToManyFieldin the model, so the column is vestigial and should be dropped to make the schema match the model across all environments.Surfaced while fixing the keyword-merge action (#1352 / PR #1362). The merge itself is already fixed (it now repoints existing rows instead of inserting), so this issue is the deeper cleanup, not urgent — but it also fixes a separate latent bug (see below).
Evidence it's a legacy artifact (from the prod dump, 2026-06-14)
website_*_keywordsthrough tables, onlywebsite_publication_keywordshas asort_valuecolumn. The other five (grant/poster/project/projectumbrella/talk) have none — confirming a plain M2M never creates it.sort_values (1..N per publication) — theSortedManyToManyFieldsignature — and 229 areNULL(newer plain.add()s).sort_value integer NOT NULL, as expected.Environment drift (important)
The two environments disagree on this one column's constraint (independent gitignored per-env migration histories):
sort_valueis nullable → a plain.add()insertingNULLsucceeds.sort_valueis NOT NULL → a plain.add()500s withnull value in column "sort_value" ... violates not-null constraint.Latent bug this fixes
On
-test, any INSERT intowebsite_publication_keywordsfails — that includes editing a Publication's keywords in the admin and saving, not just the merge action. Masked on prod only because prod's column is nullable. Dropping the column fixes this everywhere.-test, change a keyword, save → expect a 500 today.Proposed approach
Django won't auto-generate the drop (the model is already a plain M2M with no
sort_value), so it needs an explicit, idempotent migration:migrations.RunSQL(orschema_editor) op that drops the column only if it exists, e.g.ALTER TABLE website_publication_keywords DROP COLUMN IF EXISTS sort_value;— safe on the five envs/tables that never had it and on prod (nullable) and -test (NOT NULL) alike.docker-entrypoint.sh, and there's no shell on prod to fix a half-applied migration — so the op must be idempotent and not fail on any current env state.DROP COLUMN IF EXISTSsatisfies this.settings_testDB won't have the column, so also test against a DB that does — add the column, run the migration, confirm clean).Acceptance
sort_valuefromwebsite_publication_keywordswhere present-testafter deployContext: PR #1362, #1352. See repipoint fix rationale in PR #1362's discussion.