Company page: the duplicate check strips the legal form off every other company's name on each view #553
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#553
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, by reading the duplicate check and timing it over 2,000 companies.Checked again on 2026-10-01 against
mainat0a5cad587: still present.duplicates.for_companyloads the name and website of every other company the person has and runsbare_name()on each: a Unicode fold, then a linear scan of the 116 trailing and 14 leading legal forms, repeated until none matches. It runs on every company page view, so the page's cost is linear in the number of companies, at about 80 µs per company. The module assumes a person's companies are counted "in dozens", but captures, CSV imports and applications each create companies, and a long search reaches hundreds.jobs/duplicates.py:222-257(for_company:for pk, other_name, other_website in others:withbare_name(other_name)per row,:241-243)jobs/duplicates.py:139-172(bare_name:strip()walks every form in_TRAILING, then_LEADING)jobs/duplicates.py:17(the "counts in dozens" assumption)jobs/views.py:317-320(CompanyDetailView.get_context_datacalls it on every view)for_companytakes about 224 ms per company page;bare_namealone is about 160 ms for 2,000 plain names and 380 ms for names carrying three forms (… GmbH & Co. KG). At 300 companies it adds about 30 ms.contacts_with_any, which is dict-based, takes 24 ms for 30 contacts over 2,000.strip()looks up only the forms that can match instead of scanning all of them; memoisingbare_namewithfunctools.lru_cachehelps a long-running process as well. The complete fix stores the bare name and the website domain in indexed columns set on save, with a data migration that recomputes them whenLEGAL_FORMSchanges, and asks the database:filter(Q(bare_name=…) | Q(domain=…)).exclude(pk=…).Done when
tests/test_duplicates.pytimesbare_nameover 2,000 names and asserts it stays under a generous budget (for example 20 ms), and the existing matching tests still pass; or, with stored columns, a test assertsfor_companymakes a fixed number of queries and no per-row Python comparison.Landed on main with a green Forgejo CI run. Its entry in the CHANGELOG's Unreleased section ends in (#553).