Add issues bigint foreign keys batch one
What does this MR do and why?
Recreates foreign keys on the bigint shadow columns for the first batch of the issues integer-to-bigint ID conversion, tracked in #629827 (closed).
The batch one indexes are already in place (prepared asynchronously in !251347 (merged) and created synchronously in !253447 (merged)). The remaining batch one work for this release is the foreign keys: they must exist and be validated on the *_convert_to_bigint columns before the columns are swapped next release.
Foreign keys added
Five of the batch one columns carry a foreign key:
| Migration | Column | Target | ON DELETE | Temp FK name |
|---|---|---|---|---|
20260904090001 |
closed_by_id_convert_to_bigint |
users(id) |
SET NULL |
fk_c63cbf6c25_tmp |
20260904090002 |
updated_by_id_convert_to_bigint |
users(id) |
SET NULL |
fk_ffed080f01_tmp |
20260904090003 |
promoted_to_epic_id_convert_to_bigint |
epics(id) |
SET NULL |
fk_df75a7c8b8_tmp |
20260904090004 |
duplicated_to_id_convert_to_bigint |
issues(id) (self-referential) |
SET NULL |
fk_9c4516d665_tmp |
20260904090005 |
moved_to_id_convert_to_bigint |
issues(id) (self-referential) |
SET NULL |
fk_a194299be1_tmp |
Each FK is created in its own migration (foreign key constraints should not be added more than once per migration file unless source and target are identical), with the _tmp naming convention (tmp_foreign_key_name) so the swap migration can rename them to the canonical names.
Synchronous validation
The FKs are validated synchronously (the add_concurrent_foreign_key default), rather than validate: false + async validation. Validation skips NULL values, so the row set to validate is a subset of the table.
Timings were measured on a production clone and are within the post-deployment concurrent-operation budget (20 minutes), so synchronous validation is kept:
| Migration | Column | Total runtime |
|---|---|---|
20260904090001 |
closed_by_id_convert_to_bigint |
370.2 s |
20260904090002 |
updated_by_id_convert_to_bigint |
68.9 s |
20260904090003 |
promoted_to_epic_id_convert_to_bigint |
8.2 s |
20260904090004 |
duplicated_to_id_convert_to_bigint |
31.5 s |
20260904090005 |
moved_to_id_convert_to_bigint |
122.3 s |
The closed_by_id FK is the slowest (~6 minutes) because its column is non-partial and has the most non-NULL values to validate; the others are sparser (partial indexes). All five stay comfortably under the concurrent-operation limit.
Why last_edited_by_id is not here
last_edited_by_id is one of the batch one columns and its index was created, but the column has no foreign key, so there is nothing to add for it.
Self-referential foreign keys (duplicated_to_id, moved_to_id)
These two batch one columns have self-referential foreign keys — they point at issues(id), and id itself is not converted until batch two. We create them now, in batch one, pointing the bigint shadow columns at the still-integer id.
A bigint column referencing an integer primary key is not a problem: PostgreSQL accepts the foreign key, and the cross-type equality operator (bigint = integer) belongs to the integer_ops B-tree family, so issues_pkey is still used to enforce the constraint — there is no sequential-scan penalty. The values involved are also well within integer range, so no value can fail to be represented.
The trade-off is that these two FKs will have to be recreated in batch two once id becomes bigint (pointing at the final bigint id). We accept that recreation cost because the priority for the production instance is to keep the foreign keys present throughout the conversion and to minimize the number of duplicate indexes/constraints held at any one time, rather than to avoid rebuilding these two FKs.
This differs from the merge_requests conversion precedent (#507695 (closed)), which deferred FKs targeting the converting id to the batch that swaps id. For issues we prefer to keep them in place now and recreate them in batch two.
Migration output
db/structure.sql is intentionally unchanged: the temporary _tmp foreign keys live on *_convert_to_bigint columns that are not part of the final schema (which already reflects the all-bigint state for new instances). Each migration is guarded to run only where the conversion column exists, so it is a no-op on instances that already have native bigint IDs.
MR acceptance checklist
- Database review requested — foreign key creation with synchronous validation; timings confirmed on a production clone.