[Phase 3] Persist and expose MR ↔️ work item relations over GraphQL
What does this MR do and why?
Phase 3 of persisting MR
Exposes the persisted relations (the link_type column added in Phase 1, written by the services in Phase 2) over GraphQL, and deprecates the experimental derived linkedWorkItems field in favour of the new persisted one. This MR is the GraphQL API surface only.
Stack status. Phases 1 and 2 are merged to master, so this now targets
master. The mentioned-from-description backend logic (cache_merge_request_issues!rename + mentioned-persistence, the model validation relaxation, and the MENTIONED-creation restriction) has moved to Phase 4 (!241573), which targets this branch. The model rename is Phase 5 (!239615). Both stack on top of this MR.
Feature flag
explicit_mr_work_item_relations(wip, default off,group::project management). The mutations, the relations resolver, and the work item creation widget are gated on this flag — the same one the frontend uses (!238648 (merged)), consolidated per gitlab-org/plan-stage#456.- While disabled: the resolver returns
null, both relation mutations raise resource-not-available, and theworkItemCreatedevelopment widget is a no-op.
Read surface
- New
MergeRequestWorkItemRelationtype (connection), backed directly bymerge_requests_closing_issuesrows — no PORO, mirroring the existingWorkItemClosingMergeRequesttype on the work item side. Authorized per row by the existingread_merge_request_closing_issuepolicy, so confidential/unreadable work items are filtered out of the connection.- Fields:
id(global ID),linkType,fromMrDescription,workItem(batch-loaded).
- Fields:
- New
workItemRelationsfield onMergeRequestType, capped atmax_page_size: 500, with an optionaltypes:filter. linkedWorkItemsis deprecated (Use 'workItemRelations' instead, 19.2); its runtime is unchanged during the deprecation window.
Mutations introduced by this MR
| Mutation | Arguments | Behaviour |
|---|---|---|
mergeRequestCreateWorkItemRelations |
projectPath, iid, workItemIds: [WorkItemID!]!, linkType (default MENTIONED) |
Links existing work items to an MR. Delegates to MergeRequests::WorkItemRelations::CreateService; returns the created workItemRelations and per-item errors. |
mergeRequestDestroyWorkItemRelations |
projectPath, iid, ids: [MergeRequestsClosingIssuesID!]! (relation global IDs) |
Removes persisted relations. Delegates to MergeRequests::WorkItemRelations::DestroyService; returns removedRelationIds. |
workItemCreate (extended) |
new optional developmentWidget: { mergeRequestIds: [MergeRequestID!]!, linkType } |
Links a newly created work item to one or more MRs in the same call — see below. |
Both dedicated mutations are Mutations::MergeRequests::WorkItemRelations::{Create,Destroy} and authorize the granular update_merge_request token scope.
Link relations at work item creation time
Requested in review on the frontend MR (!240762#note_3462809191): when a user creates a work item from an MR context, establish the link in the same workItemCreate call instead of forcing a follow-up mergeRequestCreateWorkItemRelations.
- New
Types::WorkItems::Widgets::DevelopmentCreateInputType(WorkItemWidgetDevelopmentCreateInput):mergeRequestIds: [MergeRequestID!]!(capped atMergeRequests::WorkItemRelations::BaseService::MAX_RELATIONS, referenced directly so the limit is not duplicated) andlinkType(defaultsRELATED). - New
WorkItems::Callbacks::Development, auto-dispatched by the widget framework (viaWidget.callback_class, no registration file needed). It reusesMergeRequests::WorkItemRelations::CreateServiceonce per MR, so idempotency, the relation limit, and the granularcreate_merge_request_work_item_relationauthorization are unchanged — creating a work item cannot link it to an MR you can't manage. - Gated per MR on
explicit_mr_work_item_relations(actor:merge_request.project), consistent with the rest of the backend.
Other
by_link_typesscope on the relations model (used by the resolver'stypes:filter) — see Database review below.- Regenerated GraphQL reference docs and introspection schemas.
Database review
The new query is a single read keyed on merge_request_id and bounded by the number of issues referenced in one MR description. Verified on a Database Lab production clone (snapshot 2026-06-18), worst-case MR (~2,960 rows), EXPLAIN (ANALYZE, BUFFERS), warm cache. Counts only, no row data; literal id shown as <mr>.
Read — by_link_types (resolver types: filter):
SELECT * FROM merge_requests_closing_issues WHERE merge_request_id = ? AND link_type IN (?);Index Scan using index_mr_closing_issues_on_merge_request_id_and_link_type
(cost=0.43..3036.00 rows=3111) (actual rows=2962 loops=1)
Index Cond: ((merge_request_id = <mr>) AND (link_type = ANY ('{0,1,2}'::integer[])))
Buffers: shared hit=90
Execution Time: 0.817 msAn index seek on the existing index_mr_closing_issues_on_merge_request_id_and_link_type. No new index or migration. ~database review requested.
How to set up and validate locally
bundle exec rspec \
spec/graphql/types/merge_requests/work_item_relation_type_spec.rb \
spec/requests/api/graphql/merge_request/work_item_relations_spec.rb \
spec/requests/api/graphql/merge_request/linked_work_items_spec.rb \
spec/requests/api/graphql/mutations/merge_requests/work_item_relations/create_spec.rb \
spec/requests/api/graphql/mutations/merge_requests/work_item_relations/destroy_spec.rb \
spec/graphql/types/work_items/widgets/development_create_input_type_spec.rb \
spec/services/work_items/callbacks/development_spec.rb \
spec/requests/api/graphql/mutations/work_items/create_spec.rb \
spec/models/merge_requests_closing_issues_spec.rbSpecs cover: FF-on read (with link-type filtering and per-row authorization), FF-off returning null, create/destroy authorization + FF gating, the linkedWorkItems deprecation annotation, the workItemCreate development widget (links on create, FF-off no-op, over-limit error, unauthorized-MR error surfaced), and the by_link_types scope.
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist.