Fix award_emoji namespace_id for issue related records

What does this MR do and why?

award_emoji rows require a sharding key: either namespace_id or organization_id must be set. When a work item was moved to a different namespace, WorkItems::DataSync::Widgets::AwardEmoji copied the source row attributes and replaced only awardable_id and awardable_type. namespace_id was carried over from the source, so the copied rows kept pointing at the namespace the issue came from.

The copy path itself is fixed in !254243 (merged). This MR corrects the rows that already drifted.

Only rows with awardable_type = 'Issue' are affected. Work item moves always write the target's base class name, which is Issue. Notes, merge requests, epics and snippets are out of scope.

This MR originally did the fix inline in a post-deployment migration. GitLab.com turned out to have around 1.5 million rows with awardable_type = 'Issue', about 50% more than estimated, and the measured timings were not good enough. The fix is now a batched background migration.

What the MR contains:

  1. db/post_migrate/20260910150001_add_tmp_index_on_award_emoji_for_issues.rb — temporary partial index tmp_idx_award_emoji_on_id_where_awardable_type_issue on award_emoji (id) WHERE awardable_type = 'Issue'.

  2. lib/gitlab/background_migration/fix_award_emoji_namespace_id_for_issues.rb — the batched background migration FixAwardEmojiNamespaceIdForIssues. It uses scope_to ->(relation) { relation.where(awardable_type: 'Issue') } so batching is served by the temporary index. Each sub batch runs:

    WITH relation AS MATERIALIZED (
      SELECT "award_emoji"."id", "award_emoji"."awardable_id" FROM ...
    )
    UPDATE "award_emoji"
    SET
      "namespace_id" = "issues"."namespace_id"
    FROM
      "relation"
      INNER JOIN "issues" ON "issues"."id" = "relation"."awardable_id"
    WHERE
      "award_emoji"."id" = "relation"."id"
  3. db/post_migrate/20260911093000_queue_fix_award_emoji_namespace_id_for_issues.rb — queues the migration with BATCH_SIZE 10000 and SUB_BATCH_SIZE 100, the same values used for this table in !211844 (merged).

  4. db/docs/batched_background_migrations/fix_award_emoji_namespace_id_for_issues.yml.

  5. Specs: spec/lib/gitlab/background_migration/fix_award_emoji_namespace_id_for_issues_spec.rb and spec/migrations/20260911093000_queue_fix_award_emoji_namespace_id_for_issues_spec.rb.

Design notes:

  • scope_to normally triggers the Database/AvoidScopeTo RuboCop rule. It is disabled with an inline justification naming the supporting index, the same way FixPSentNotificationsRecordsRelatedToDesignManagement does it with tmp_idx_p_sent_notifications_on_id_for_designs. Without the scope the migration would iterate over the entire table.
  • The MATERIALIZED CTE keeps the sub batch as a fixed, already-materialized set of ids, so the update produces stable query plans. The alternative hands the planner a large IN list that it can re-plan differently as statistics change.
  • The update is unconditional rather than filtered on a mismatch. Rows whose namespace_id is already correct are rewritten too. The batching and pause interval of the background migration handle the write volume.
  • The temporary index must outlive the background migration. The earlier migration that dropped it has been removed, and the index is now part of db/structure.sql. It is dropped in a follow-up, together with the finalize migration, in 19.6: #628743
  • Rows pointing at a deleted issue are left alone, because the join to issues is an inner join.

Query plans

https://console.postgres.ai/gitlab/projects/gitlab-production-main/sessions/56860/commands/161407

Sub batch update
WITH relation AS MATERIALIZED (
  SELECT "award_emoji"."id", "award_emoji"."awardable_id" FROM "award_emoji" WHERE "award_emoji"."awardable_type" = 'Issue' AND "award_emoji"."id" >= 75291 AND "award_emoji"."id" < 75588
)
UPDATE "award_emoji"
SET "namespace_id" = "issues"."namespace_id"
FROM relation INNER JOIN "issues" ON "issues"."id" = relation."awardable_id"
WHERE "award_emoji"."id" = relation."id"

References

Screenshots or screen recordings

No UI change.

Before After
N/A N/A

How to set up and validate locally

The background migration spec covers four cases: a row whose namespace_id points at the source namespace (realigned), a row already consistent with the issue (stays correct), a row pointing at a non-existent issue (untouched), and a MergeRequest row with a stale namespace (untouched, filtered out by scope_to). It also asserts the number of UPDATE statements, which confirms scope_to filters before the update runs.

Validation already done:

bundle exec rails db:migrate

Applied cleanly.

RAILS_ENV=test bundle exec rspec spec/lib/gitlab/background_migration/fix_award_emoji_namespace_id_for_issues_spec.rb spec/migrations/20260911093000_queue_fix_award_emoji_namespace_id_for_issues_spec.rb

4 examples, 0 failures.

RAILS_ENV=test bundle exec rspec spec/db/docs_spec.rb spec/lib/gitlab/database/dictionary_spec.rb spec/lib/gitlab/utils/batched_background_migrations_dictionary_spec.rb

64 examples, 0 failures.

bundle exec rubocop on both migrations, the background migration and both specs reported no offenses.

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.

  • This MR needs database review.
  • No changelog entry is needed. This is a data fix with no user-facing behaviour change.
  • No documentation update is needed.
Edited by Mario Celi

Merge request reports

Loading
Loading