Resolve RepositoryLanguage#programming_language through stable language_id with transitional fallback

Related to #519895 (closed).

Problem

repository_languages references programming_languages by its id column. This value is not stable across GitLab instances, so when an organization moves to another cell, the copied repository_languages rows can point to a missing row (language name and color lookups crash) or to a different language (wrong data shown).

The language_id column is the stable identifier for this reference, but we can't switch to it outright: the backfill of repository_languages.language_id can only be finalized in 19.6, so self-managed instances running 19.3–19.5 can still have NULL values.

Proposal

Resolve the association through language_id when available and fall back to the legacy id otherwise:

belongs_to :programming_language # legacy, id-based
belongs_to :stable_programming_language,
  class_name: 'ProgrammingLanguage',
  foreign_key: :language_id,
  primary_key: :language_id,
  optional: true

def resolved_programming_language
  stable_programming_language || programming_language
end

delegate :name, :color, to: :resolved_programming_language

This behaves correctly everywhere without instance-type checks:

  • Self-managed (pre-19.6): rows without language_id use the legacy association, which is always correct on a single cell.
  • GitLab.com and new cells: language_id is fully backfilled, so the stable association wins, including for rows moved from another cell.

Preload both associations in the default_scope to avoid N+1 queries.

Follow-up in 19.6

See #614144 for removing the transitional legacy ID fallback after the backfill and NOT NULL validation are complete.

Edited by Vasilii Iakliushin