Add exclude_ids argument to BaseGroupsResolver
What does this MR do and why?
Expose an exclude_ids argument on BaseGroupsResolver so that consumers of the Groups GraphQL API can exclude specific groups by GitLab Global ID directly in their query.
This is needed for the Organizations Universal Onboarding Workflow to query top-level groups (TLGs) while excluding the group currently being onboarded. Without this argument, callers must filter client-side or make additional round-trips, which is inefficient and error-prone.
The argument uses GlobalIDType[Group] for type-safe Global ID coercion (e.g. gid://gitlab/Group/123) and passes the coerced integer IDs to GroupsFinder as exclude_group_ids, which already supports this parameter via groups.id_not_in(params[:exclude_group_ids]).
Closes #607395 (closed)
References
- Issue: #607395 (closed)
GroupsFinderalready supportsexclude_group_ids:app/finders/groups_finder.rb
Database review
Scoped to owned TLGs
SQL
SELECT
"namespaces"."id",
"namespaces"."name",
"namespaces"."path",
"namespaces"."owner_id",
"namespaces"."created_at",
"namespaces"."updated_at",
"namespaces"."type",
"namespaces"."avatar",
"namespaces"."membership_lock",
"namespaces"."share_with_group_lock",
"namespaces"."visibility_level",
"namespaces"."request_access_enabled",
"namespaces"."ldap_sync_status",
"namespaces"."ldap_sync_error",
"namespaces"."ldap_sync_last_update_at",
"namespaces"."ldap_sync_last_successful_update_at",
"namespaces"."ldap_sync_last_sync_at",
"namespaces"."lfs_enabled",
"namespaces"."parent_id",
"namespaces"."shared_runners_minutes_limit",
"namespaces"."repository_size_limit",
"namespaces"."require_two_factor_authentication",
"namespaces"."two_factor_grace_period",
"namespaces"."project_creation_level",
"namespaces"."runners_token",
"namespaces"."saml_discovery_token",
"namespaces"."runners_token_encrypted",
"namespaces"."auto_devops_enabled",
"namespaces"."extra_shared_runners_minutes_limit",
"namespaces"."last_ci_minutes_notification_at",
"namespaces"."last_ci_minutes_usage_notification_level",
"namespaces"."subgroup_creation_level",
"namespaces"."max_pages_size",
"namespaces"."max_artifacts_size",
"namespaces"."mentions_disabled",
"namespaces"."default_branch_protection",
"namespaces"."max_personal_access_token_lifetime",
"namespaces"."shared_runners_enabled",
"namespaces"."allow_descendants_override_disabled_shared_runners",
"namespaces"."traversal_ids",
"namespaces"."organization_id",
"namespaces"."state"
FROM
"namespaces"
INNER JOIN "members" ON "namespaces"."id" = "members"."source_id"
WHERE
"members"."type" = 'GroupMember'
AND "members"."source_type" = 'Namespace'
AND "namespaces"."type" = 'Group'
AND "members"."user_id" = 5413811
AND "members"."requested_at" IS NULL
AND (access_level >= 10)
AND "members"."access_level" = 50
AND "namespaces"."id" NOT IN (92221560, 95280580)
AND "namespaces"."parent_id" IS NULL
ORDER BY
LOWER("namespaces"."name") ASC,
"namespaces"."id" DESC
LIMIT 101Query plan
Limit (cost=35.12..35.12 rows=1 width=390) (actual time=122.640..122.643 rows=5 loops=1)
Buffers: shared hit=43 read=76 dirtied=1
WAL: records=1 fpi=1 bytes=8097
I/O Timings: read=121.537 write=0.000
-> Sort (cost=35.12..35.12 rows=1 width=390) (actual time=122.638..122.640 rows=5 loops=1)
Sort Key: (lower((namespaces.name)::text)), namespaces.id DESC
Sort Method: quicksort Memory: 26kB
Buffers: shared hit=43 read=76 dirtied=1
WAL: records=1 fpi=1 bytes=8097
I/O Timings: read=121.537 write=0.000
-> Nested Loop (cost=1.13..35.11 rows=1 width=390) (actual time=19.143..122.573 rows=5 loops=1)
Buffers: shared hit=37 read=76 dirtied=1
WAL: records=1 fpi=1 bytes=8097
I/O Timings: read=121.537 write=0.000
-> Index Scan Backward using idx_members_on_user_and_source_and_source_type_and_member_role on public.members (cost=0.57..27.94 rows=2 width=4) (actual time=7.985..63.167 rows=18 loops=1)
Index Cond: ((members.user_id = 5413811) AND ((members.source_type)::text = 'Namespace'::text))
Filter: ((members.requested_at IS NULL) AND (members.access_level >= 10) AND ((members.type)::text = 'GroupMember'::text) AND (members.access_level = 50))
Rows Removed by Filter: 9
Buffers: shared hit=2 read=32 dirtied=1
WAL: records=1 fpi=1 bytes=8097
I/O Timings: read=62.778 write=0.000
-> Index Scan using index_groups_on_parent_id_id on public.namespaces (cost=0.56..3.58 rows=1 width=358) (actual time=3.291..3.291 rows=0 loops=18)
Index Cond: ((namespaces.parent_id IS NULL) AND (namespaces.id = members.source_id))
Filter: (namespaces.id <> ALL ('{92221560,95280580}'::integer[]))
Buffers: shared hit=35 read=44
I/O Timings: read=58.759 write=0.000
Settings: jit = 'off', random_page_cost = '1.5', work_mem = '230MB', seq_page_cost = '4', effective_cache_size = '472585MB'
Query ID: -4011964592960925909Time: 127.156 ms
- planning: 4.419 ms
- execution: 122.737 ms
- I/O read: 121.537 ms
- I/O write: 0.000 ms
Shared buffers:
- hits: 43 (~344.00 KiB) from the buffer pool
- reads: 76 (~608.00 KiB) from the OS file cache, including disk I/O
- dirtied: 1 (~8.00 KiB)
- writes: 0 https://postgres.ai/console/gitlab/gitlab-production-main/sessions/54154/commands/156652
Unscoped
SQL
SELECT
"namespaces"."id",
"namespaces"."name",
"namespaces"."path",
"namespaces"."owner_id",
"namespaces"."created_at",
"namespaces"."updated_at",
"namespaces"."type",
"namespaces"."avatar",
"namespaces"."membership_lock",
"namespaces"."share_with_group_lock",
"namespaces"."visibility_level",
"namespaces"."request_access_enabled",
"namespaces"."ldap_sync_status",
"namespaces"."ldap_sync_error",
"namespaces"."ldap_sync_last_update_at",
"namespaces"."ldap_sync_last_successful_update_at",
"namespaces"."ldap_sync_last_sync_at",
"namespaces"."lfs_enabled",
"namespaces"."parent_id",
"namespaces"."shared_runners_minutes_limit",
"namespaces"."repository_size_limit",
"namespaces"."require_two_factor_authentication",
"namespaces"."two_factor_grace_period",
"namespaces"."project_creation_level",
"namespaces"."runners_token",
"namespaces"."saml_discovery_token",
"namespaces"."runners_token_encrypted",
"namespaces"."auto_devops_enabled",
"namespaces"."extra_shared_runners_minutes_limit",
"namespaces"."last_ci_minutes_notification_at",
"namespaces"."last_ci_minutes_usage_notification_level",
"namespaces"."subgroup_creation_level",
"namespaces"."max_pages_size",
"namespaces"."max_artifacts_size",
"namespaces"."mentions_disabled",
"namespaces"."default_branch_protection",
"namespaces"."max_personal_access_token_lifetime",
"namespaces"."shared_runners_enabled",
"namespaces"."allow_descendants_override_disabled_shared_runners",
"namespaces"."traversal_ids",
"namespaces"."organization_id",
"namespaces"."state"
FROM
"namespaces"
WHERE
"namespaces"."type" = 'Group'
AND "namespaces"."id" NOT IN (92221560, 95280580)
ORDER BY
LOWER("namespaces"."name") ASC,
"namespaces"."id" DESC
LIMIT 101Query plan
Limit (cost=4.81..58.40 rows=101 width=391) (actual time=378.396..820.796 rows=101 loops=1)
Buffers: shared hit=6 read=579 dirtied=2
WAL: records=2 fpi=2 bytes=15950
I/O Timings: read=810.720 write=0.000
-> Incremental Sort (cost=4.81..5627128.21 rows=10604968 width=391) (actual time=378.393..820.780 rows=101 loops=1)
Sort Key: (lower((namespaces.name)::text)), namespaces.id DESC
Presorted Key: (lower((namespaces.name)::text))
Full-sort Groups: 4 Sort Method: quicksort Average Memory: 34kB Peak Memory: 34kB
Buffers: shared hit=6 read=579 dirtied=2
WAL: records=2 fpi=2 bytes=15950
I/O Timings: read=810.720 write=0.000
-> Index Scan using index_on_namespaces_lower_name on public.namespaces (cost=0.57..5307626.48 rows=10604968 width=391) (actual time=24.157..820.065 rows=102 loops=1)
Filter: ((namespaces.id <> ALL ('{92221560,95280580}'::integer[])) AND ((namespaces.type)::text = 'Group'::text))
Rows Removed by Filter: 471
Buffers: shared read=579 dirtied=2
WAL: records=2 fpi=2 bytes=15950
I/O Timings: read=810.720 write=0.000
Settings: random_page_cost = '1.5', work_mem = '230MB', seq_page_cost = '4', effective_cache_size = '472585MB', jit = 'off'
Query ID: -830973834980066604Time: 824.576 ms
- planning: 3.652 ms
- execution: 820.924 ms
- I/O read: 810.720 ms
- I/O write: 0.000 ms
Shared buffers:
- hits: 6 (~48.00 KiB) from the buffer pool
- reads: 579 (~4.50 MiB) from the OS file cache, including disk I/O
- dirtied: 2 (~16.00 KiB)
- writes: 0 https://postgres.ai/console/gitlab/gitlab-production-main/sessions/54231/commands/156799
Screenshots or screen recordings
Without excludeIds |
With excludeIds |
|---|---|
![]() |
![]() |
How to set up and validate locally
- In a Rails console or via GraphQL explorer, run:
query { groups(excludeIds: ["gid://gitlab/Group/123"]) { nodes { id name } } } - Verify the group with ID 123 is excluded from results.
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

