Drop FK fk_2902beee34 from packages_helm_metadata_cache_states.project_id
What does this MR do and why?
Drops the hard foreign key fk_2902beee34 (packages_helm_metadata_cache_states.project_id -> projects.id).
packages_helm_metadata_cache_states.project_id is a denormalized sharding key copied from its parent packages_helm_metadata_caches.project_id by the permanent trigger trigger_489fffe04425 (AssignDesiredShardingKey). The parent column has no hard FK to projects and is cleaned up only via a loose foreign key. This means the parent can legitimately hold the project_id of a deleted project during the cleanup backlog window. When the trigger copies that orphaned value into the child table, the hard FK on the child rejects it with PG::ForeignKeyViolation, aborting the write (incident: https://gitlab.com/gitlab-org/gitlab/-/issues/605940).
The fix is to drop the child FK. Rows are still cleaned up transitively: the loose foreign key on the parent sets its status to pending_destruction, Packages::Helm::CleanupStaleMetadataCacheWorker then destroys the parent, and the child row is removed by the ON DELETE CASCADE on fk_rails_281379b415 (packages_helm_metadata_cache_id). This follows the exact precedent established in #606941 (closed) for packages_nuget_symbol_states and packages_package_file_states (landed in commit cb0b9415), and addresses the write-path exposure left after !246021 (merged).
Scope of the remaining exposure
The backfill is already finalized (20260420233038, recorded as finalized_by in the batched background migration YAML), so what remains is the runtime write path only, and specifically on Geo primaries. Packages::Helm::MetadataCache includes ::Geo::VerifiableModel (ee/app/models/ee/packages/helm/metadata_cache.rb), and the states row is written by after_save :save_verification_details, which returns early unless Gitlab::Geo.primary?. The linked incident was an upgrade-time Sev1; that specific urgency no longer applies, so this is a correctness fix rather than an incident mitigation.
The migration stays in db/migrate because the operation is cheap, not for ordering reasons: table_size: small, and the database testing pipeline showed only 0.2-0.3 ms holding ACCESS EXCLUSIVE on projects under with_lock_retries. It cannot be ordered ahead of the 19.0 post-deploy finalize migration in any case, since Gitlab::Database::Migrations::Version#<=> compares milestone before migration type.
References
- Closes #613746 (closed)
- Incident: https://gitlab.com/gitlab-org/gitlab/-/issues/605940
- Pattern precedent: #606941 (closed)
- Write-path MR: !246021 (merged)
- Follow-up (allowlist cleanup): #621884
- Overlapping MR on the same spec line: !250288 (merged)
Screenshots or screen recordings
N/A - migration-only change.
How to set up and validate locally
Run bundle exec rails db:migrate and confirm fk_2902beee34 no longer appears in \d packages_helm_metadata_cache_states in psql.
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.