[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:

  1. Persist mentioned-from-description relations. MergeRequest#cache_merge_request_issues! (renamed from cache_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 as from_mr_description: true, link_type: :mentioned rows. The mentioned set is computed against the freshly-extracted closing set, so an issue is never persisted as both closes and mentioned. The result is that the persisted workItemRelations now matches the derived linkedWorkItems. The promote/insert helpers and the from-description delete use the unscoped merge_request_issues association (the merge_request_closing_issues association is scoped to closes) and are scoped by link_type.

  2. Relax the model validation. MergeRequestsClosingIssues now allows from_mr_description: true for closes or mentioned (both description-derived). Only related (user-only) rows are rejected, since related relations are never derived from the description.

  3. Restrict MENTIONED creation. MergeRequests::WorkItemRelations::CreateService now rejects link_type: :mentioned with a bad_request error ("Mentioned relations are managed automatically and cannot be created."). Both the mergeRequestCreateWorkItemRelations mutation and the workItemCreate development widget go through this service, so both paths are covered. The mutation's linkType default is changed from MENTIONED to RELATED.

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 ms
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 ms

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 (?);
Update
  -> Index Scan using index_mr_closing_issues_on_mr_id_issue_id_link_type
       Buffers: shared hit=2
Execution Time: 0.047 ms

4. 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_issues

MR 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.

Edited by Jorge Tomás

Merge request reports

Loading