Skip to content

Get ancestors of an Epic using GraphQL

What does this MR do?

Related to #327576 (closed)

These changes add the ancestors field to EpicType allowing us to fetch all the parent epics of an epic using GraphQL.

GraphQL Query
query {
  group(fullPath: "group1") {
    epics {
      edges {
        node {
          title
          ancestors {
            edges {
              node {
                title
              }
            }
          }
        }
      }
    }
  }
}
RESPONSE
{
  "data": {
    "group": {
      "epics": {
        "edges": [
          {
            "node": {
              "title": "Epic 5",
              "ancestors": {
                "edges": [
                  {
                    "node": {
                      "title": "Parent Epic C"
                    }
                  },
                  {
                    "node": {
                      "title": "Parent Epic D"
                    }
                  },
                ]
              }
            }
          },
          {
            "node": {
              "title": "Epic 4",
              "ancestors": {
                "edges": [
                  {
                    "node": {
                      "title": "Parent Epic E"
                    }
                  },
                  {
                    "node": {
                      "title": "Parent Epic F"
                    }
                  }
                ]
              }
            }
          }
        }
      }
    }
  }
}

Additionally, we include the new param child_id in EpicsFinder that allows us to filter the collection of epics by the ancestors of a child epic. This param works similarly to parent_id, although it returns the ancestors of the given epic instead of its descendants. Similarly, the presence of the include_ancestor_groups param will return results from ancestor groups as well.

Example

Given the following epics:

  • parent-group/ancestor-epic
  • parent-group/subgroup/parent-epic
  • parent-group/subgroup/epic

With the following hierarchy:

Epic -> Parent Epic -> Ancestor Epic

When using the finder with include_ancestor_groups set as true the result will include epics from "Parent Group"

EpicsFinder.new(current_user, group_id: subgroup.id, child_id: epic.id, include_ancestor_groups: true).execute
=> "Parent Epic", "Ancestor Epic"

If include_ancestor_groups is omitted its default values is false

EpicsFinder.new(current_user, group_id: subgroup.id, child_id: epic.id).execute
=> "Parent Epic"

Query Plans

1. Finder query

SQL query
SELECT epics.* FROM epics WHERE epics.group_id IN (WITH RECURSIVE base_and_descendants AS ((SELECT namespaces.* FROM namespaces WHERE namespaces.type = 'Group' AND namespaces.id = 2593421)
UNION
	(SELECT namespaces.* FROM namespaces, base_and_descendants WHERE namespaces.type = 'Group' AND namespaces.parent_id = base_and_descendants.id)) SELECT namespaces.id FROM base_and_descendants AS namespaces) AND epics.id IN (WITH RECURSIVE base_and_ancestors AS ((SELECT 1 as depth, ARRAY[epics.id] AS tree_path, false AS tree_cycle, epics.* FROM epics WHERE epics.id = 277866)
UNION
	(SELECT (base_and_ancestors.depth + 1), tree_path || epics.id, epics.id = ANY(tree_path), epics.* FROM epics, base_and_ancestors WHERE epics.id = base_and_ancestors.parent_id AND base_and_ancestors.tree_cycle = FALSE)) SELECT id FROM base_and_ancestors AS epics WHERE epics.id NOT IN (SELECT epics.id FROM epics WHERE epics.id = 277866) ORDER BY depth ASC) ORDER BY epics.id DESC

Link to postgres.ai

Plan with execution
Sort  (cost=1866.46..1866.46 rows=1 width=1198) (actual time=5.415..5.423 rows=0 loops=1)
   Sort Key: epics.id DESC
   Sort Method: quicksort  Memory: 25kB
   Buffers: shared hit=3 read=3
   I/O Timings: read=5.321 write=0.000
   ->  Hash Semi Join  (cost=1777.21..1866.45 rows=1 width=1198) (actual time=5.390..5.397 rows=0 loops=1)
         Hash Cond: (epics.group_id = namespaces.id)
         Buffers: shared read=3
         I/O Timings: read=5.321 write=0.000
         ->  Nested Loop  (cost=185.08..274.24 rows=26 width=1198) (actual time=5.389..5.393 rows=0 loops=1)
               Buffers: shared read=3
               I/O Timings: read=5.321 write=0.000
               ->  HashAggregate  (cost=184.67..184.93 rows=26 width=4) (actual time=5.388..5.392 rows=0 loops=1)
                     Group Key: epics_1.id
                     Buffers: shared read=3
                     I/O Timings: read=5.321 write=0.000
                     ->  Sort  (cost=184.28..184.34 rows=26 width=8) (actual time=5.384..5.388 rows=0 loops=1)
                           Sort Key: epics_1.depth
                           Sort Method: quicksort  Memory: 25kB
                           Buffers: shared read=3
                           I/O Timings: read=5.321 write=0.000
                           CTE base_and_ancestors
                             ->  Recursive Union  (cost=0.42..179.08 rows=51 width=1235) (actual time=5.373..5.375 rows=0 loops=1)
                                   Buffers: shared read=3
                                   I/O Timings: read=5.321 write=0.000
                                   ->  Index Scan using epics_pkey on public.epics epics_2  (cost=0.42..3.44 rows=1 width=1235) (actual time=5.361..5.362 rows=0 loops=1)
                                         Index Cond: (epics_2.id = 277866)
                                         Buffers: shared read=3
                                         I/O Timings: read=5.321 write=0.000
                                   ->  Nested Loop  (cost=0.42..17.46 rows=5 width=1235) (actual time=0.010..0.011 rows=0 loops=1)
                                         I/O Timings: read=0.000 write=0.000
                                         ->  WorkTable Scan on base_and_ancestors  (cost=0.00..0.20 rows=5 width=40) (actual time=0.009..0.009 rows=0 loops=1)
                                               Filter: (NOT base_and_ancestors.tree_cycle)
                                               Rows Removed by Filter: 0
                                               I/O Timings: read=0.000 write=0.000
                                         ->  Index Scan using epics_pkey on public.epics epics_3  (cost=0.42..3.44 rows=1 width=1198) (actual time=0.000..0.000 rows=0 loops=0)
                                               Index Cond: (epics_3.id = base_and_ancestors.parent_id)
                                               I/O Timings: read=0.000 write=0.000
                           ->  CTE Scan on base_and_ancestors epics_1  (cost=3.44..4.58 rows=26 width=8) (actual time=5.375..5.376 rows=0 loops=1)
                                 Filter: (NOT (hashed SubPlan 3))
                                 Rows Removed by Filter: 0
                                 Buffers: shared read=3
                                 I/O Timings: read=5.321 write=0.000
                                 SubPlan 3
                                   ->  Index Only Scan using epics_pkey on public.epics epics_4  (cost=0.42..3.44 rows=1 width=4) (actual time=0.000..0.000 rows=0 loops=0)
                                         Index Cond: (epics_4.id = 277866)
                                         Heap Fetches: 0
                                         I/O Timings: read=0.000 write=0.000
               ->  Index Scan using epics_pkey on public.epics  (cost=0.42..3.44 rows=1 width=1198) (actual time=0.000..0.000 rows=0 loops=0)
                     Index Cond: (epics.id = epics_1.id)
                     I/O Timings: read=0.000 write=0.000
         ->  Hash  (cost=1589.87..1589.87 rows=181 width=4) (actual time=0.000..0.000 rows=0 loops=0)
               I/O Timings: read=0.000 write=0.000
               ->  CTE Scan on base_and_descendants namespaces  (cost=1584.44..1588.06 rows=181 width=4) (actual time=0.000..0.000 rows=0 loops=0)
                     I/O Timings: read=0.000 write=0.000
                     CTE base_and_descendants
                       ->  Recursive Union  (cost=0.43..1584.44 rows=181 width=350) (actual time=0.000..0.000 rows=0 loops=0)
                             I/O Timings: read=0.000 write=0.000
                             ->  Index Scan using index_namespaces_on_type_and_id_partial on public.namespaces namespaces_1  (cost=0.43..3.45 rows=1 width=350) (actual time=0.000..0.000 rows=0 loops=0)
                                   Index Cond: (((namespaces_1.type)::text = 'Group'::text) AND (namespaces_1.id = 2593421))
                                   I/O Timings: read=0.000 write=0.000
                             ->  Nested Loop  (cost=0.56..157.74 rows=18 width=350) (actual time=0.000..0.000 rows=0 loops=0)
                                   I/O Timings: read=0.000 write=0.000
                                   ->  WorkTable Scan on base_and_descendants  (cost=0.00..0.20 rows=10 width=4) (actual time=0.000..0.000 rows=0 loops=0)
                                         I/O Timings: read=0.000 write=0.000
                                   ->  Index Scan using index_namespaces_on_parent_id_and_id on public.namespaces namespaces_2  (cost=0.56..15.73 rows=2 width=350) (actual time=0.000..0.000 rows=0 loops=0)
                                         Index Cond: (namespaces_2.parent_id = base_and_descendants.id)
                                         Filter: ((namespaces_2.type)::text = 'Group'::text)
                                         Rows Removed by Filter: 0
                                         I/O Timings: read=0.000 write=0.000

2. Finder query with include_ancestor_groups

SQL query
SELECT epics.* FROM epics WHERE epics.group_id IN (WITH RECURSIVE base_and_ancestors AS ((SELECT namespaces.* FROM namespaces WHERE namespaces.type = 'Group' AND namespaces.id = 2593421)
UNION
	(SELECT namespaces.* FROM namespaces, base_and_ancestors WHERE namespaces.type = 'Group' AND namespaces.id = base_and_ancestors.parent_id)), base_and_descendants AS ((SELECT namespaces.* FROM namespaces WHERE namespaces.type = 'Group' AND namespaces.id = 2593421)
UNION
	(SELECT namespaces.* FROM namespaces, base_and_descendants WHERE namespaces.type = 'Group' AND namespaces.parent_id = base_and_descendants.id)) SELECT namespaces.id FROM ((SELECT namespaces.* FROM base_and_ancestors AS namespaces WHERE namespaces.type = 'Group')
UNION
	(SELECT namespaces.* FROM base_and_descendants AS namespaces WHERE namespaces.type = 'Group')) namespaces WHERE namespaces.type = 'Group') AND epics.id IN (WITH RECURSIVE base_and_ancestors AS ((SELECT 1 as depth, ARRAY[epics.id] AS tree_path, false AS tree_cycle, epics.* FROM epics WHERE epics.id = 277866)
UNION
	(SELECT (base_and_ancestors.depth + 1), tree_path || epics.id, epics.id = ANY(tree_path), epics.* FROM epics, base_and_ancestors WHERE epics.id = base_and_ancestors.parent_id AND base_and_ancestors.tree_cycle = FALSE)) SELECT id FROM base_and_ancestors AS epics WHERE epics.id NOT IN (SELECT epics.id FROM epics WHERE epics.id = 277866) ORDER BY depth ASC) ORDER BY epics.id DESC

Link to postrgres.ai

Plan with execution
Sort  (cost=2162.50..2162.51 rows=1 width=1198) (actual time=0.187..0.194 rows=0 loops=1)
   Sort Key: epics.id DESC
   Sort Method: quicksort  Memory: 25kB
   Buffers: shared hit=6
   I/O Timings: read=0.000 write=0.000
   ->  Hash Semi Join  (cost=2125.53..2162.49 rows=1 width=1198) (actual time=0.076..0.083 rows=0 loops=1)
         Hash Cond: (epics.id = "ANY_subquery".id)
         Buffers: shared hit=3
         I/O Timings: read=0.000 write=0.000
         ->  Nested Loop  (cost=1940.61..1977.51 rows=21 width=1198) (actual time=0.000..0.000 rows=0 loops=0)
               I/O Timings: read=0.000 write=0.000
               ->  Unique  (cost=1940.19..1940.20 rows=2 width=4) (actual time=0.000..0.000 rows=0 loops=0)
                     I/O Timings: read=0.000 write=0.000
                     ->  Sort  (cost=1940.19..1940.20 rows=2 width=4) (actual time=0.000..0.000 rows=0 loops=0)
                           Sort Key: namespaces.id
                           I/O Timings: read=0.000 write=0.000
                           ->  Subquery Scan on namespaces  (cost=1939.89..1940.16 rows=2 width=4) (actual time=0.000..0.000 rows=0 loops=0)
                                 I/O Timings: read=0.000 write=0.000
                                 CTE base_and_ancestors
                                   ->  Recursive Union  (cost=0.43..350.87 rows=21 width=350) (actual time=0.000..0.000 rows=0 loops=0)
                                         I/O Timings: read=0.000 write=0.000
                                         ->  Index Scan using index_namespaces_on_type_and_id_partial on public.namespaces namespaces_3  (cost=0.43..3.45 rows=1 width=350) (actual time=0.000..0.000 rows=0 loops=0)
                                               Index Cond: (((namespaces_3.type)::text = 'Group'::text) AND (namespaces_3.id = 2593421))
                                               I/O Timings: read=0.000 write=0.000
                                         ->  Nested Loop  (cost=0.43..34.70 rows=2 width=350) (actual time=0.000..0.000 rows=0 loops=0)
                                               I/O Timings: read=0.000 write=0.000
                                               ->  WorkTable Scan on base_and_ancestors  (cost=0.00..0.20 rows=10 width=4) (actual time=0.000..0.000 rows=0 loops=0)
                                                     I/O Timings: read=0.000 write=0.000
                                               ->  Index Scan using index_namespaces_on_type_and_id_partial on public.namespaces namespaces_4  (cost=0.43..3.45 rows=1 width=350) (actual time=0.000..0.000 rows=0 loops=0)
                                                     Index Cond: (((namespaces_4.type)::text = 'Group'::text) AND (namespaces_4.id = base_and_ancestors.parent_id))
                                                     I/O Timings: read=0.000 write=0.000
                                 CTE base_and_descendants
                                   ->  Recursive Union  (cost=0.43..1584.44 rows=181 width=350) (actual time=0.000..0.000 rows=0 loops=0)
                                         I/O Timings: read=0.000 write=0.000
                                         ->  Index Scan using index_namespaces_on_type_and_id_partial on public.namespaces namespaces_5  (cost=0.43..3.45 rows=1 width=350) (actual time=0.000..0.000 rows=0 loops=0)
                                               Index Cond: (((namespaces_5.type)::text = 'Group'::text) AND (namespaces_5.id = 2593421))
                                               I/O Timings: read=0.000 write=0.000
                                         ->  Nested Loop  (cost=0.56..157.74 rows=18 width=350) (actual time=0.000..0.000 rows=0 loops=0)
                                               I/O Timings: read=0.000 write=0.000
                                               ->  WorkTable Scan on base_and_descendants  (cost=0.00..0.20 rows=10 width=4) (actual time=0.000..0.000 rows=0 loops=0)
                                                     I/O Timings: read=0.000 write=0.000
                                               ->  Index Scan using index_namespaces_on_parent_id_and_id on public.namespaces namespaces_6  (cost=0.56..15.73 rows=2 width=350) (actual time=0.000..0.000 rows=0 loops=0)
                                                     Index Cond: (namespaces_6.parent_id = base_and_descendants.id)
                                                     Filter: ((namespaces_6.type)::text = 'Group'::text)
                                                     Rows Removed by Filter: 0
                                                     I/O Timings: read=0.000 write=0.000
                                 ->  Unique  (cost=4.58..4.83 rows=2 width=2946) (actual time=0.000..0.000 rows=0 loops=0)
                                       I/O Timings: read=0.000 write=0.000
                                       ->  Sort  (cost=4.58..4.59 rows=2 width=2946) (actual time=0.000..0.000 rows=0 loops=0)
                                             Sort Key: namespaces_1.id, namespaces_1.name, namespaces_1.path, namespaces_1.owner_id, namespaces_1.created_at, namespaces_1.updated_at, namespaces_1.type, namespaces_1.description, namespaces_1.avatar, namespaces_1.membership_lock, namespaces_1.share_with_group_lock, namespaces_1.visibility_level, namespaces_1.request_access_enabled, namespaces_1.ldap_sync_status, namespaces_1.ldap_sync_error, namespaces_1.ldap_sync_last_update_at, namespaces_1.ldap_sync_last_successful_update_at, namespaces_1.ldap_sync_last_sync_at, namespaces_1.lfs_enabled, namespaces_1.description_html, namespaces_1.parent_id, namespaces_1.shared_runners_minutes_limit, namespaces_1.repository_size_limit, namespaces_1.require_two_factor_authentication, namespaces_1.two_factor_grace_period, namespaces_1.cached_markdown_version, namespaces_1.project_creation_level, namespaces_1.runners_token, namespaces_1.file_template_project_id, namespaces_1.saml_discovery_token, namespaces_1.runners_token_encrypted, namespaces_1.custom_project_templates_group_id, namespaces_1.auto_devops_enabled, namespaces_1.extra_shared_runners_minutes_limit, namespaces_1.last_ci_minutes_notification_at, namespaces_1.last_ci_minutes_usage_notification_level, namespaces_1.subgroup_creation_level, namespaces_1.emails_disabled, namespaces_1.max_pages_size, namespaces_1.max_artifacts_size, namespaces_1.mentions_disabled, namespaces_1.default_branch_protection, namespaces_1.unlock_membership_to_ldap, namespaces_1.max_personal_access_token_lifetime, namespaces_1.push_rule_id, namespaces_1.shared_runners_enabled, namespaces_1.allow_descendants_override_disabled_shared_runners, namespaces_1.traversal_ids, namespaces_1.delayed_project_removal
                                             I/O Timings: read=0.000 write=0.000
                                             ->  Append  (cost=0.00..4.57 rows=2 width=2946) (actual time=0.000..0.000 rows=0 loops=0)
                                                   I/O Timings: read=0.000 write=0.000
                                                   ->  CTE Scan on base_and_ancestors namespaces_1  (cost=0.00..0.47 rows=1 width=2946) (actual time=0.000..0.000 rows=0 loops=0)
                                                         Filter: ((namespaces_1.type)::text = 'Group'::text)
                                                         Rows Removed by Filter: 0
                                                         I/O Timings: read=0.000 write=0.000
                                                   ->  CTE Scan on base_and_descendants namespaces_2  (cost=0.00..4.07 rows=1 width=2946) (actual time=0.000..0.000 rows=0 loops=0)
                                                         Filter: ((namespaces_2.type)::text = 'Group'::text)
                                                         Rows Removed by Filter: 0
                                                         I/O Timings: read=0.000 write=0.000
               ->  Index Scan using index_epics_on_group_id_and_iid on public.epics  (cost=0.42..18.54 rows=11 width=1198) (actual time=0.000..0.000 rows=0 loops=0)
                     Index Cond: (epics.group_id = namespaces.id)
                     I/O Timings: read=0.000 write=0.000
         ->  Hash  (cost=184.60..184.60 rows=26 width=4) (actual time=0.053..0.055 rows=0 loops=1)
               Buckets: 1024  Batches: 1  Memory Usage: 8kB
               Buffers: shared hit=3
               I/O Timings: read=0.000 write=0.000
               ->  Subquery Scan on ANY_subquery  (cost=184.28..184.60 rows=26 width=4) (actual time=0.052..0.054 rows=0 loops=1)
                     Buffers: shared hit=3
                     I/O Timings: read=0.000 write=0.000
                     ->  Sort  (cost=184.28..184.34 rows=26 width=8) (actual time=0.051..0.053 rows=0 loops=1)
                           Sort Key: epics_1.depth
                           Sort Method: quicksort  Memory: 25kB
                           Buffers: shared hit=3
                           I/O Timings: read=0.000 write=0.000
                           CTE base_and_ancestors
                             ->  Recursive Union  (cost=0.42..179.08 rows=51 width=1235) (actual time=0.032..0.033 rows=0 loops=1)
                                   Buffers: shared hit=3
                                   I/O Timings: read=0.000 write=0.000
                                   ->  Index Scan using epics_pkey on public.epics epics_2  (cost=0.42..3.44 rows=1 width=1235) (actual time=0.028..0.028 rows=0 loops=1)
                                         Index Cond: (epics_2.id = 277866)
                                         Buffers: shared hit=3
                                         I/O Timings: read=0.000 write=0.000
                                   ->  Nested Loop  (cost=0.42..17.46 rows=5 width=1235) (actual time=0.003..0.004 rows=0 loops=1)
                                         I/O Timings: read=0.000 write=0.000
                                         ->  WorkTable Scan on base_and_ancestors base_and_ancestors_1  (cost=0.00..0.20 rows=5 width=40) (actual time=0.002..0.002 rows=0 loops=1)
                                               Filter: (NOT base_and_ancestors_1.tree_cycle)
                                               Rows Removed by Filter: 0
                                               I/O Timings: read=0.000 write=0.000
                                         ->  Index Scan using epics_pkey on public.epics epics_3  (cost=0.42..3.44 rows=1 width=1198) (actual time=0.000..0.000 rows=0 loops=0)
                                               Index Cond: (epics_3.id = base_and_ancestors_1.parent_id)
                                               I/O Timings: read=0.000 write=0.000
                           ->  CTE Scan on base_and_ancestors epics_1  (cost=3.44..4.58 rows=26 width=8) (actual time=0.034..0.034 rows=0 loops=1)
                                 Filter: (NOT (hashed SubPlan 4))
                                 Rows Removed by Filter: 0
                                 Buffers: shared hit=3
                                 I/O Timings: read=0.000 write=0.000
                                 SubPlan 4
                                   ->  Index Only Scan using epics_pkey on public.epics epics_4  (cost=0.42..3.44 rows=1 width=4) (actual time=0.000..0.000 rows=0 loops=0)
                                         Index Cond: (epics_4.id = 277866)
                                         Heap Fetches: 0
                                         I/O Timings: read=0.000 write=0.000

Does this MR meet the acceptance criteria?

Conformity

Edited by Eugenia Grieff

Merge request reports