Expand User#belongs_to_paid_namespace? namespace search
What does this MR do and why?
Resolves https://gitlab.com/gitlab-org/gitlab/-/issues/416651+.
The method User#belongs_to_paid_namespace?
is intended to fetch all paid namespaces a user is a non-guest member of. However, it currently only accounts for direct memberships with root namespaces. It fails to include:
- Membership via a sub-group
- Membership via a project
- Membership via an invite to a sub-group via another Group
- Membership via an invite to a project via another group
This MR changes the method's behaviour to account for all the cases above.
A list of methods impacted by this change can be found here.
Screenshots or screen recordings
No visual changes.
How to set up and validate locally
- Start GDK.
- Pick a user (user A) with a paid root namespace containing some sub-groups and projects.
- Register a new user (user B).
- Log out as user B and log back in as user A.
- Using the Rails console, validate that user B returns
false
forbelongs_to_paid_namespace?
:user_b = User.last user_b.belongs_to_paid_namespace?
- For the targets listed below:
- Add user B to the target as a non-guest. (Guest membership should always return
false
). - Validate using the Rails console that
belongs_to_paid_namespace?
returns the expected result. - Remove user B from the target, or leave them in the target to validate behaviour for multiple memberships.
- Add user B to the target as a non-guest. (Guest membership should always return
Targets for belongs_to_paid_namespace? == true
:
- Paid root group.
- A sub-group under a paid root group.
- A project under a paid root group.
- A group linked to another group under a paid group.
- A project linked to another group under a paid group.
Targets for belongs_to_paid_namespace? == false
:
- Root groups without a paid subscription.
- Any namespaces or projects under or linked to a root group without a paid subscription.
SQL Queries
Before: find top-level paid namespaces user is a non-guest member of
There was one SQL query to find the top-level paid namespaces the user is a non-guest member of. It also has a variation where namespaces with trial subscriptions are excluded.
Default mode
SQL Query
SELECT
1 AS one
FROM ((
SELECT
"namespaces"."id"
FROM
"namespaces"
WHERE
"namespaces"."type" = 'User'
AND "namespaces"."owner_id" = 106)
UNION (
SELECT
"namespaces"."id"
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" = 106
AND "members"."requested_at" IS NULL
AND (access_level >= 10)
AND "members"."access_level" IN (20, 30, 40, 50)
AND "namespaces"."parent_id" IS NULL)) namespaces
LEFT OUTER JOIN "gitlab_subscriptions" ON "gitlab_subscriptions"."namespace_id" = "namespaces"."id"
WHERE
"gitlab_subscriptions"."hosted_plan_id" IN (
SELECT
"plans"."id"
FROM
"plans"
WHERE
"plans"."name" IN ('bronze', 'silver', 'premium', 'gold', 'ultimate', 'ultimate_trial', 'premium_trial', 'opensource'))
LIMIT 1
Query plan on GitLab using the gitlab-qa
user.
Excluding trials
SQL Query
SELECT
1 AS one
FROM ((
SELECT
"namespaces"."id"
FROM
"namespaces"
WHERE
"namespaces"."type" = 'User'
AND "namespaces"."owner_id" = 106)
UNION (
SELECT
"namespaces"."id"
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" = 106
AND "members"."requested_at" IS NULL
AND (access_level >= 10)
AND "members"."access_level" IN (20, 30, 40, 50)
AND "namespaces"."parent_id" IS NULL)) namespaces
LEFT OUTER JOIN "gitlab_subscriptions" ON "gitlab_subscriptions"."namespace_id" = "namespaces"."id"
WHERE
"gitlab_subscriptions"."hosted_plan_id" IN (
SELECT
"plans"."id"
FROM
"plans"
WHERE
"plans"."name" IN ('bronze', 'silver', 'premium', 'gold', 'ultimate', 'ultimate_trial', 'premium_trial', 'opensource'))
AND ("gitlab_subscriptions"."trial" = FALSE
OR "gitlab_subscriptions"."trial" IS NULL
OR "gitlab_subscriptions"."trial_ends_on" <= '2023-09-13')
LIMIT 1
Query plan on GitLab using the gitlab-qa
user.
After: find top-level paid namespaces of all namespaces and projects user is a non-guest member of
The old query is preceded by three pre-existing queries, which fetch all namespace IDs the user has a non-guest membership to. The old query has also been altered to search for the top-level paid namespaces using just the fetched namespace IDs, instead of relying on nested queries.
The three pre-existing queries are leveraged from the billable_code_suggestions_root_group_ids
method in the same User
model. This MR pulls them out into a new shared method and does not alter the queries.
Query 1: fetch namespace IDs from project and project-group link memberships
SQL Query
SELECT
"projects"."namespace_id"
FROM
"projects"
WHERE
"projects"."id" IN (
SELECT
"project_authorizations"."project_id"
FROM
"project_authorizations"
WHERE
"project_authorizations"."user_id" = 1614863
AND (access_level > 10))
Query plan on GitLab using the gitlab-qa
user.
Query 2: fetch namespace IDs from group and sub-group memberships
SQL Query
SELECT
"members"."source_id"
FROM
"members"
LEFT OUTER JOIN "users" ON "users"."id" = "members"."user_id"
WHERE
"members"."type" = 'GroupMember'
AND "members"."source_type" = 'Namespace'
AND "members"."user_id" = 1614863
AND (("members"."user_id" IS NULL
AND "members"."invite_token" IS NOT NULL)
OR "users"."state" = 'active')
AND "members"."requested_at" IS NULL
AND (members.access_level > 5)
AND (members.access_level > 10)
Query plan on GitLab using the gitlab-qa
user.
Query 3: fetch namespace IDs from group-group link memberships
SQL Query
SELECT
"group_group_links"."shared_group_id"
FROM
"group_group_links"
WHERE (group_access > 10)
AND "group_group_links"."shared_with_group_id" = 9970
Query plan on GitLab using the gitlab-org
namespace.
Query 4: fetch top-level paid namespaces a user is a member of
Default mode
SQL Query
SELECT
1 AS one
FROM
"namespaces"
LEFT OUTER JOIN "gitlab_subscriptions" ON "gitlab_subscriptions"."namespace_id" = "namespaces"."id"
LEFT OUTER JOIN "plans" ON "plans"."id" = "gitlab_subscriptions"."hosted_plan_id"
WHERE ("namespaces"."id" = 9970
OR "namespaces"."type" = 'User'
AND "namespaces"."owner_id" = 1614863)
AND "gitlab_subscriptions"."hosted_plan_id" IN (
SELECT
"plans"."id"
FROM
"plans"
WHERE
"plans"."name" IN ('bronze', 'silver', 'premium', 'gold', 'ultimate', 'ultimate_trial', 'premium_trial', 'opensource'))
LIMIT 1
Query plan on GitLab using the gitlab-qa
user and the gitlab-org
namespace.
Excluding trials
SQL Query
SELECT
1 AS one
FROM
"namespaces"
LEFT OUTER JOIN "gitlab_subscriptions" ON "gitlab_subscriptions"."namespace_id" = "namespaces"."id"
LEFT OUTER JOIN "plans" ON "plans"."id" = "gitlab_subscriptions"."hosted_plan_id"
WHERE ("namespaces"."id" = 9970
OR "namespaces"."type" = 'User'
AND "namespaces"."owner_id" = 1614863)
AND "gitlab_subscriptions"."hosted_plan_id" IN (
SELECT
"plans"."id"
FROM
"plans"
WHERE
"plans"."name" IN ('bronze', 'silver', 'premium', 'gold', 'ultimate', 'ultimate_trial', 'premium_trial', 'opensource'))
AND ("gitlab_subscriptions"."trial" = FALSE
OR "gitlab_subscriptions"."trial" IS NULL
OR "gitlab_subscriptions"."trial_ends_on" <= '2023-09-14')
LIMIT 1
Query plan on GitLab using the gitlab-qa
user and the gitlab-org
namespace.
MR acceptance checklist
This checklist encourages us to confirm any changes have been analyzed to reduce risks in quality, performance, reliability, security, and maintainability.
-
I have evaluated the MR acceptance checklist for this MR.