Update gitlab_subscriptions to use new plan associations and enum identifiers
**Summary**
This task migrates `gitlab_subscriptions` plan filtering from `hosted_plan_id` (database foreign key to `plans.id`) to `hosted_plan_name_uid` (in-memory enum identifier), removing the dependency on the `plans` table.
#### Phase 1: Make `hosted_plan_name_uid` the Source of Truth for Writes
The `hosted_plan_name_uid` column already exists on `gitlab_subscriptions` (added in migration `20251119203450`), is indexed, and is being backfilled (`BackfillGitlabSubscriptionsHostedPlanNameUid`, queued in `20260115114900`). The `set_hosted_plan_name_uid` callback in `ee/app/models/gitlab_subscription.rb` already dual-writes both `hosted_plan_id` and `hosted_plan_name_uid`.
1. **Confirm the backfill migration** `BackfillGitlabSubscriptionsHostedPlanNameUid` has completed on production
2. **Update `set_hosted_plan_name_uid`** in `ee/app/models/gitlab_subscription.rb`: Make `hosted_plan_name_uid` the primary write. Continue writing `hosted_plan_id` for backward compatibility, but derive the uid from the in-memory `Plan::PLAN_NAME_UID_LIST` instead of calling `hosted_plan&.plan_name_uid_before_type_cast` (which loads the Plan record via the FK)
3. **Update `plan_code=` setter** in `ee/app/models/gitlab_subscription.rb`: Currently does `self.hosted_plan = Plan.find_by(name: code)`. Update to also set `hosted_plan_name_uid` directly from `Plan::PLAN_NAME_UID_LIST` as the primary value, keeping `hosted_plan_id` populated for backward compatibility
4. **Update test factories and specs** to set `hosted_plan_name_uid` as the primary attribute when creating `gitlab_subscription` records
`hosted_plan_name_uid` is the authoritative value on write. `hosted_plan_id` is still populated for backward compatibility with existing readers.
#### Phase 2: Switch All Reads to `hosted_plan_name_uid`
This phase eliminates every read of `hosted_plan_id` in the `gitlab_subscriptions` context, replacing it with `hosted_plan_name_uid`.
1. **`GitlabSubscription`** (`ee/app/models/gitlab_subscription.rb`):
- Replace `scope :by_hosted_plan_ids` with a new scope `by_hosted_plan_name_uids` that filters on `hosted_plan_name_uid` instead of `hosted_plan_id`:
```ruby
scope :by_hosted_plan_name_uids, ->(uids) { where(hosted_plan_name_uid: uids) }
```
- Replace `scope :with_hosted_plan` to filter on `hosted_plan_name_uid` using `Plan::PLAN_NAME_UID_LIST` instead of `joins(:hosted_plan).where('plans.name' => ...)`:
```ruby
scope :with_hosted_plan, ->(plan_name) do
uids = Array(plan_name).map { |n| Plan.plan_name_uids[n] }
where(trial: [false, nil], hosted_plan_name_uid: uids)
end
```
- Replace `scope :with_a_paid_hosted_plan` and `scope :with_a_paid_or_trial_hosted_plan` similarly, eliminating the `joins(:hosted_plan)` and `allow_cross_joins_across_databases` calls
- Replace `delegate :name, :title, to: :hosted_plan, prefix: :plan` with methods that derive `plan_name` and `plan_title` from `hosted_plan_name_uid` using the in-memory enum (e.g. `Plan.plan_name_uids.key(hosted_plan_name_uid)`)
- Replace `ultimate_trial_paid_customer_plan_id` and `premium_plan_id` private methods (which call `Plan.find_by`) with uid-based equivalents using `Plan::PLAN_NAME_UID_LIST`
- Update `involves_ultimate_trial_paid_customer_plan?` and `reset_involves_ultimate_trial_paid_customer_plan?` to compare `hosted_plan_name_uid` / `hosted_plan_name_uid_was` instead of `hosted_plan_id` / `hosted_plan_id_was`
2. **`EE::Namespace`** (`ee/app/models/ee/namespace.rb`):
- Update `scope :in_specific_plans` to filter on `gitlab_subscriptions.hosted_plan_name_uid` instead of `left_joins(gitlab_subscription: :hosted_plan).where(plans: { name: ... })`
- Update `scope :not_free` similarly
- Update `scope :with_feature_available_in_plan` to use `hosted_plan_name_uid` instead of joining through `plans`
- Update `scope :with_plan_for_feature` and `scope :with_ai_supported_plan_and_credits` to eliminate the raw SQL `LEFT OUTER JOIN "plans" ON "plans"."id" = "gitlab_subscriptions"."hosted_plan_id"` and use `hosted_plan_name_uid` instead
- Update `hosted_plan_for` to resolve the plan from `hosted_plan_name_uid` via the in-memory enum rather than loading via the FK association
3. **`EE::User`** (`ee/app/models/ee/user.rb`):
- Update `owns_paid_namespace?` to filter on `gitlab_subscriptions.hosted_plan_name_uid` instead of `gitlab_subscriptions: { hosted_plan: Plan.where(name: ...) }`
- Update `paid_namespaces` similarly
4. **`EE::Project`** (`ee/app/models/ee/project.rb`):
- Update `scope :for_plan_name` to filter on `gitlab_subscriptions.hosted_plan_name_uid` instead of `joins(namespace: { gitlab_subscription: :hosted_plan }).where(plans: { name: ... })`
5. **`EE::Plan`** (`ee/app/models/ee/plan.rb`):
- Update `scope :with_subscriptions` and `scope :by_namespace` if they still rely on the `hosted_plan_id` FK join
6. **Admin view** (`ee/app/views/admin/_namespace_plan.html.haml`):
- Update the plan dropdown from `f.select :hosted_plan_id, Plan.pluck(:title, :id)` to use `hosted_plan_name_uid` with `Plan::PLAN_NAME_UID_LIST` values
7. **All corresponding specs** for the above files, including:
- `ee/spec/models/gitlab_subscription_spec.rb`
- `ee/spec/models/ee/namespace_spec.rb`
- `ee/spec/models/ee/user_spec.rb`
No application code reads `hosted_plan_id` from `gitlab_subscriptions`. The column is still written but never consumed.
---
#### Phase 3: Cleanup
Remove all vestiges of `hosted_plan_id` from the `gitlab_subscriptions` context.
1. **Stop writing `hosted_plan_id`**: Remove the `hosted_plan_id` assignment from `set_hosted_plan_name_uid`, `plan_code=`, and any other write paths in `ee/app/models/gitlab_subscription.rb`
2. **Remove the `hosted_plan_id` column**: Add a database migration to drop `hosted_plan_id` from `gitlab_subscriptions`
3. **Remove dead code**:
- `belongs_to :hosted_plan` from `GitlabSubscription`
- `has_many :hosted_subscriptions` from `EE::Plan` (only after the separate `gitlab_subscription_histories` migration is also complete, since this association serves both tables)
- `scope :by_hosted_plan_ids` from `GitlabSubscription`
- `ultimate_trial_paid_customer_plan_id` and `premium_plan_id` methods (if not already removed in Phase 2)
4. **Clean up database metadata**:
- Remove `gitlab_subscriptions.hosted_plan_id` from `db/integer_ids_not_yet_initialized_to_bigint.yml` (if present)
- Mark the `BackfillGitlabSubscriptionsHostedPlanNameUid` migration as finalized
5. **Update all remaining specs** that reference `hosted_plan_id` in the subscriptions context
6. **Coordinate with the broader effort** in [#571422](https://gitlab.com/gitlab-org/gitlab/-/work_items/571422) to ensure alignment with the overall plan to drop `plans.id`
**Note:** The `has_many :hosted_subscriptions` association on `EE::Plan` can only be fully removed once the parallel `gitlab_subscription_histories` migration (tracked separately) has also completed its cleanup phase, since `EE::Plan` also declares `has_many :gitlab_subscription_histories, foreign_key: 'hosted_plan_id'`.
The `gitlab_subscriptions` table no longer has a `hosted_plan_id` column or `belongs_to :hosted_plan` association. All plan-based subscription filtering operates entirely through `hosted_plan_name_uid`, with no dependency on the `plans` table's `id` column.
task