[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) 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), not master.
- Phase 3 (!238500) — 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.
Please review and merge the stack bottom-up.
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. 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 four statements below all run in the existing transaction, keyed on merge_request_id. Plans were verified on a Database Lab production clone (snapshot 2026-06-18), warm cache, against a worst-case MR with ~2,960 from-description rows. Data shown is counts only; the id is shown as <mr>.
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;Index Scan using index_mr_closing_issues_on_merge_request_id_and_link_type
Filter: from_mr_description
actual rows=2962
Buffers: shared hit=90
Execution Time: 3.15 ms2. 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 (?);Index Scan using index_mr_closing_issues_on_mr_id_issue_id_link_type
Buffers: shared hit=2
Execution Time: 0.034 ms3. 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 (?);Update
-> Index Scan using index_mr_closing_issues_on_mr_id_issue_id_link_type
Buffers: shared hit=2
Execution Time: 0.047 ms4. INSERT (bulk_insert!, batches of 100)
INSERT INTO merge_requests_closing_issues
(issue_id, merge_request_id, from_mr_description, link_type, created_at, updated_at)
VALUES (...), ...;Plain row insert; no plan of interest.
database review requested.
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.