Migrations: reversing documents 0005 or accounts 0021 fails as soon as the table holds rows #571
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
performance
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo#571
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found on 2026-09-29 in a read-only review of postulo at
95348908d, reading the reverse paths of the data migrations and running two of them backwards withMigrationExecutoron SQLite over seeded rows.Related to #130.
Checked again on 2026-10-01 against
mainat0a5cad587: still present.Two migrations declare a reverse that does not work on a live instance. documents 0005 promises "a reverse that carries back, so this is not a one-way door on somebody's live instance", but its reverse re-creates the old check constraint before it has carried anything back, so it fails on the first document copy. accounts 0021 declares its data step reversible with
RunPython.noop, but undoing itsRemoveFieldre-adds a unique token column filled with one default for every row, so it fails with two invitations. In both cases the rollback aborts (in a transaction, so nothing is lost) with anIntegrityErrorrather than a clear refusal.documents/migrations/0005_polymorphic_links.py: the docstring at lines 8-10;RunPython(carry_across, carry_back)at line 126 comes beforeRemoveConstraint(model_name="documentcopy", name="documents_copy_of_one_document")at lines 129-131.accounts/migrations/0021_invite_token_fingerprint.py:RunPython(fingerprint_the_tokens, migrations.RunPython.noop)at line 33 andRemoveField(model_name="invite", name="token")at lines 45-48.renderedanduploadcolumns empty, then re-createsdocuments_copy_of_one_document(exactly one ofrenderedanduploadset, from0002_document_copies.py), and only then runscarry_back. Every existingDocumentCopyhas both columns empty at that moment, somigrate documents 0004on an instance where any document was ever copied to a store fails withIntegrityError: CHECK constraint failed: documents_copy_of_one_document. With no copies it succeeds.RemoveField, which re-addstokenas it was in0002_invite_profile.py: unique, with the callable defaultgenerate_invite_token. Django evaluates that default once for the whole table, so with two or more invitationsmigrate accounts 0020fails withIntegrityError: UNIQUE constraint failed: new__accounts_invite.token. With one invitation it succeeds, but that invitation's link no longer opens, since only the fingerprint was kept.RemoveConstraintoperations (lines 129-137) ahead of theRunPython. Forward behaviour is unchanged, and in reverse the old constraints are then re-created only aftercarry_backhas filled the columns. The comment at lines 50-51 also says a copy with neither link is "left for the removal below to take with it"; removing columns removes no rows, so it should say theAlterFieldto not-null at lines 143-157 would refuse such a row.reverse_code=None(the default forRunPython) so Django refuses the rollback withIrreversibleErrorbefore touching the table, and add a line to the docstring that the tokens cannot be recovered from their fingerprints.Done when
tests/test_migration_reversal.pymigrates todocuments 0005_polymorphic_links, creates oneUploadedDocumentand oneDocumentCopythrough the historical models, migrates back to0004_letter_language, and asserts the copy'supload_idpoints at the upload; it fails today withIntegrityError.accountsfrom0021_invite_token_fingerprintback to0020_profile_keyboard_shortcutswith two invitations raisesIrreversibleError; today it raisesIntegrityError.