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:
-
db/post_migrate/20260910150001_add_tmp_index_on_award_emoji_for_issues.rb— temporary partial indextmp_idx_award_emoji_on_id_where_awardable_type_issueonaward_emoji (id) WHERE awardable_type = 'Issue'. -
lib/gitlab/background_migration/fix_award_emoji_namespace_id_for_issues.rb— the batched background migrationFixAwardEmojiNamespaceIdForIssues. It usesscope_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" -
db/post_migrate/20260911093000_queue_fix_award_emoji_namespace_id_for_issues.rb— queues the migration withBATCH_SIZE10000 andSUB_BATCH_SIZE100, the same values used for this table in !211844 (merged). -
db/docs/batched_background_migrations/fix_award_emoji_namespace_id_for_issues.yml. -
Specs:
spec/lib/gitlab/background_migration/fix_award_emoji_namespace_id_for_issues_spec.rbandspec/migrations/20260911093000_queue_fix_award_emoji_namespace_id_for_issues_spec.rb.
Design notes:
scope_tonormally triggers theDatabase/AvoidScopeToRuboCop rule. It is disabled with an inline justification naming the supporting index, the same wayFixPSentNotificationsRecordsRelatedToDesignManagementdoes it withtmp_idx_p_sent_notifications_on_id_for_designs. Without the scope the migration would iterate over the entire table.- The
MATERIALIZEDCTE 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 largeINlist that it can re-plan differently as statistics change. - The update is unconditional rather than filtered on a mismatch. Rows whose
namespace_idis 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
issuesis 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
- Related to #628466 (closed)
- Depends on !254243 (merged). It must be merged first, otherwise new drift can appear while the background migration runs.
- Prior backfill of this table for all awardable types: !211844 (merged)
- Original bug report: #628128 (closed)
- Follow-up to finalize the background migration and drop the temporary index: #628743
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:migrateApplied 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.rb4 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.rb64 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.