[Phase 4] Persist mentioned-from-description relations and restrict MENTIONED creation
What does this MR do and why?
Part of the work for #601174.
Phase 3 (!238500 (merged)) added the persisted workItemRelations GraphQL surface. That surface exposed a parity gap, reported on !240762 (note 3467447255): the derived linkedWorkItems includes issues that are mentioned in the MR description but not closed by it, while the persisted workItemRelations only ever stored closes rows. The two lists therefore disagreed for any MR that references an issue without closing it.
This MR closes that gap and locks down who may create MENTIONED relations. Three changes:
- Persist mentioned-from-description relations.
MergeRequest#cache_merge_request_issues!(renamed fromcache_merge_request_closes_issues!, since it now caches more than just closes) additionally persists issues that are mentioned but not closed in the MR title/description asfrom_mr_description: true, link_type: :mentionedrows. The mentioned set is computed against the freshly-extracted closing set, so an issue is never persisted as bothclosesandmentioned. The result is that the persistedworkItemRelationsnow matches the derivedlinkedWorkItems. The promote/insert helpers and the from-description delete use the unscopedmerge_request_issuesassociation (themerge_request_closing_issuesassociation is scoped tocloses) and are scoped bylink_type. - Relax the model validation.
MergeRequestsClosingIssuesnow allowsfrom_mr_description: trueforclosesormentioned(both description-derived). Onlyrelated(user-only) rows are rejected, sincerelatedrelations are never derived from the description. - Restrict MENTIONED creation.
MergeRequests::WorkItemRelations::CreateServicenow rejectslink_type: :mentionedwith abad_requesterror ("Mentioned relations are managed automatically and cannot be created."). Both themergeRequestCreateWorkItemRelationsmutation and theworkItemCreatedevelopment widget go through this service, so both paths are covered. The mutation'slinkTypedefault is changed fromMENTIONEDtoRELATED.
Note: the linkedWorkItems deprecation is not part of this MR; it remains on Phase 3.
Stacked MR
This MR is part of a stack and targets 601174-phase3-graphql (Phase 3, !238500 (merged)), not master.
- Phase 3 (!238500 (merged)) — GraphQL API surface; targets
master. - Phase 4 (this MR) — backend mentioned-relation logic; targets Phase 3.
- Phase 5 (!239615) — model/table rename; stacks on top of this MR.
Database review
All write-path changes live in the existing transaction in MergeRequest#cache_merge_request_issues!. No new index and no migration are introduced by this MR. The only added per-call cost is one Gitlab::ReferenceExtractor#analyze over the MR title + description — this extraction already runs to derive linkedWorkItems; it simply was not previously run on the write path.
The 3 statements below all run in the existing transaction, keyed on merge_request_id. Re-verified on a Database Lab production clone (snapshot 2026-07-07), warm cache, against a worst-case MR with ~2,960 from-description rows. Data shown is counts only; the id is shown as <mr>.
Index note: these statements are served by
idx_mr_closing_issues_on_mr_link_issue_fromon(merge_request_id, link_type, issue_id, from_mr_description). That covering index replaced the olderindex_mr_closing_issues_on_merge_request_id_and_link_typevia migration20260616075216(milestone 19.2, from an earlier phase now onmaster). All statements below are index seeks (index-only for the promoteSELECT); no sequential scan.
1. DELETE the from-description rows (spans closes + mentioned)
DELETE FROM merge_requests_closing_issues
WHERE merge_request_id = <mr> AND from_mr_description = TRUE;https://postgres.ai/console/gitlab/gitlab-production-main/sessions/53592/commands/155732
2. Promote SELECT (pluck, per link type)
SELECT issue_id FROM merge_requests_closing_issues
WHERE merge_request_id = <mr> AND from_mr_description = FALSE
AND link_type = ? AND issue_id IN (?);https://postgres.ai/console/gitlab/gitlab-production-main/sessions/53592/commands/155734
3. Promote UPDATE (update_all)
UPDATE merge_requests_closing_issues SET from_mr_description = TRUE
WHERE merge_request_id = <mr> AND link_type = ? AND issue_id IN (?);https://postgres.ai/console/gitlab/gitlab-production-main/sessions/53592/commands/155736
How to set up and validate locally
bundle exec rspec spec/services/merge_requests/work_item_relations/create_service_spec.rb
bundle exec rspec spec/requests/api/graphql/mutations/merge_requests/work_item_relations/create_spec.rb
bundle exec rspec spec/requests/api/graphql/merge_request/work_item_relations_parity_spec.rb
bundle exec rspec spec/models/merge_requests_closing_issues_spec.rb
bundle exec rspec spec/models/merge_request_spec.rb -e cache_merge_request_issuesMR acceptance checklist
This checklist encourages us to confirm any changes have been analyzed to reduce risks in quality, performance, reliability, security, and maintainability. Review the acceptance checklist.