Migrations: jobs 0014 aborts on the very case collisions (Q95 beside q95) it was written to fold #570
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#570
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 data migrations and running jobs 0014 forward withMigrationExecutoron SQLite over seeded rows.Checked again on 2026-10-01 against
mainat0a5cad587: still present.jobs/migrations/0014_remove_companyidentifier_one_company_per_identifier_per_owner_and_more.pyexists to fold stored identifiers to their scheme's case and to report, rather than refuse, any pair that collides once folded (#211). It cannot do either for the rows it was written for. It saves each folded value before it checks for a duplicate, while the old case-sensitive constraint is still in place, so the first collision raisesIntegrityErrorand the migration rolls back. Other identifiers are not folded or de-duplicated at all, yet the new constraint compares them case-insensitively, so two of them that differ only in case makeAddConstraintfail. Either way an instance holding such rows cannot migrate past jobs 0014.Where:
jobs/migrations/0014_remove_companyidentifier_one_company_per_identifier_per_owner_and_more.py:fold_and_reportsaves at line 43 (row.save(update_fields=["value"])) and only builds the duplicate key at line 47; lines 45-46 skip everyotherrow; the operations list runsRunPython(fold_and_report, ...)at line 70, beforeRemoveConstraint("one_company_per_identifier_per_owner")at lines 71-74, and adds the newunique_other_identifier_per_companyonLower("label"),Lower("value")at lines 89-99.What happens:
Q95on one company andq95on another, which the case-sensitive constraint from0005_company_identifiers.py(owner,scheme,value, where scheme is notother) allowed. Whichever row comes second bypk, folding makes it equal to the first while that constraint still exists, and line 43 raisesIntegrityError: UNIQUE constraint failed: jobs_companyidentifier.owner_id, jobs_companyidentifier.scheme, jobs_companyidentifier.value. The report-and-remove branch at lines 48-56 is never reached.VAT/pt123andvat/PT123, which the oldunique_other_identifier_per_company(company,scheme,label,value) allowed. The data step skips both, and theAddConstraintat lines 89-99 fails withIntegrityError: UNIQUE constraint failed: index 'unique_other_identifier_per_company'.Both were reproduced by migrating
jobsto 0013, inserting the rows through the historical models and migrating to 0014. A single lowercaseq95with no partner folds toQ95correctly.Why it matters: the upgrade stops, and stays stopped, for exactly the accounts #211 was about: rows saved before a scheme folded its case, and Other values typed in two cases. The transaction rolls back, so no data is lost, but the operator is left on the older release until somebody edits the table by hand. #211 is closed as if this had been handled. Instances already past 0014 are unaffected.
Fix: move both
RemoveConstraintoperations ahead of theRunPython, so the data step runs with no case-sensitive unique constraint in place, and compute the duplicate key before writing: fold, look the key up inseen, delete and report a duplicate, and save only a row that survives. Give Other rows the same treatment keyed on(row.company_id, row.label.casefold(), row.value.casefold()), keeping the value as typed and deleting (and naming on the console) only a second identical-but-for-case row on the same company, so the expression constraint can always be created. Editing the migration is safe: an instance past 0014 never runs it again.Done when
tests/test_migration_identifier_case.pymigratesjobsto0013_company_kindwithMigrationExecutor, insertsQ95andq95for one owner on two companies and two Other identifiers differing only in case on one company, migrates to 0014, and asserts that the migration completes, one Wikidata rowQ95remains, and one of the two Other rows remains. It fails today withIntegrityError.Related to #211 (closed), whose data migration this is.