Fix LeadingJoin Error in Work Item Hierarchy Queries
Problem
Premium customers encounter an ActiveRecord::ConfigurationError when querying Epics with child Issues through GraphQL:
ActiveRecord::ConfigurationError: Arel::Nodes::LeadingJoin is not supported for visitor_forUltimate customers querying identical Epic→Issue structures do not experience this error.
Affected versions: GitLab 18.10+, Premium-tier groups only
Root Cause
The bug is in the EE override of work_item_children_keyset_order (ee/app/models/ee/work_item.rb), which fires only for Premium groups (those without the :subepics license).
The original code builds the query through an association proxy:
non_epic_children = work_item.work_item_children.where.not(work_item_type_id: epic_type_id)
keyset_order.apply_cursor_conditions(non_epic_children.includes(:parent_link)).reorder(keyset_order)work_item.work_item_children is a has_many :through :child_links association. When ActiveRecord constructs its scope via AssociationScope, it internally injects Arel::Nodes::LeadingJoin nodes into joins_values to join the through-table to the source table. These Arel nodes then bleed into the joins_values of the returned relation.
When the GraphQL preloader later attempts to preload work_item_children_by_relative_position (itself a has_many :through with a lambda scope), ThroughAssociation#through_scope reads those joins_values and passes them into JoinDependency#walk_tree. walk_tree only handles Symbol, String, Array, and Hash — it raises ConfigurationError when it encounters the LeadingJoin Arel node.
The CE path avoids this entirely because it calls self.joins(:parent_link) at the class level (WorkItem.joins(...)) — no association proxy, no LeadingJoin contamination.
The Ultimate path avoids this because the EE override returns super when :subepics is licensed, falling through to the safe CE code.
Solution
Rewrite the EE override to build the query at the class level, matching the CE pattern:
epic_type_id = ::WorkItems::TypesFramework::Provider.new(work_item.namespace).find_by_base_type(:epic).id
keyset_order = ::WorkItem.work_item_children_keyset_order_config
keyset_order.apply_cursor_conditions(
joins(:parent_link).where.not(work_item_type_id: epic_type_id)
).reorder(keyset_order)This uses joins(:parent_link) at the class scope (same as CE) and adds the WHERE NOT filter as a simple condition. The has_many :through :child_links association already constrains results to children of the given work item, so the epic-type exclusion is all that's needed.
Why this works
self.joins(:parent_link)operates on theWorkItemclass, not an association proxy — noLeadingJoinArel nodes are introduced- The query is compatible with ActiveRecord's preloader for
has_many :throughassociations - Works identically for both Premium and Ultimate tiers
- Maintains correct keyset ordering and epic-type filtering
Verification
Test query:
query {
workItemsByReference(contextNamespacePath: "premium-group", refs: ["premium-group&1"]) {
nodes {
widgets {
... on WorkItemWidgetHierarchy {
children {
count
nodes { id title }
}
}
}
}
}
}How to Test Locally
Prerequisites: Premium-licensed group (NOT Ultimate) Setup:
# Rails console - create Epic with child Issues
group = Group.find_by(path: 'your-premium-group')
epic = group.work_items.create!(work_item_type: WorkItems::Type.default_by_type(:epic), title: 'Test Epic')
issue = group.projects.first.issues.create!(title: 'Child Issue')
WorkItems::ParentLink.create!(work_item_parent: epic, work_item: issue)Test on master (should fail):
query {
workItemsByReference(contextNamespacePath: "your-premium-group", refs: ["your-premium-group&EPIC_IID"]) {
nodes {
widgets {
... on WorkItemWidgetHierarchy {
children { count }
}
}
}
}
}Expected: ActiveRecord::ConfigurationError
Test on this branch (should succeed):
Same query - returns children count without error.
Related
Relates to https://gitlab.com/gitlab-com/request-for-help/-/work_items/4588