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