Fix keys.organization_id NOT NULL constraint failure on Self-Managed/Dedicated upgrades
What does this MR do and why?
Fixes a PG::CheckViolation crash that prevents Self-Managed and Dedicated instances from upgrading when the 20260422180000 AddNotNullConstraintToKeysOrganizationId post-deployment migration runs.
Root cause
The BackfillOrganizationIdKeys batched background migration (BBM) backfills SSH keys via:
UPDATE keys
SET organization_id = users.organization_id
FROM users
WHERE keys.user_id = users.id
AND keys.organization_id IS NULLSSH keys with NULL user_id (e.g. keys created before the user association was enforced) or a user_id pointing to a deleted user are silently skipped by this join. When the subsequent AddNotNullConstraintToKeysOrganizationId migration tries to validate the CHECK (organization_id IS NOT NULL) constraint, it fails:
PG::CheckViolation: check constraint "check_8933ae4f38" of relation "keys"
is violated by some rowDeploy keys already had an orphan fallback (backfill_orphaned_deploy_keys assigns the default organization), but regular SSH keys did not.
Fix
This MR mirrors the approach used in !242492 (merged) for oauth_applications:
- No-op
20260422180000_add_not_null_constraint_to_keys_organization_id.rb— the migration is replaced by two later migrations so it cannot be fixed in place without backdating. - New backfill
20260714000001_backfill_null_organization_id_on_keys.rb— iterateskeysrows withorganization_id IS NULLin batches of 1 000 and sets them to the default organization (id = 1). On GitLab.com all rows were already backfilled by the BBM, so this is a no-op there. - New constraint
20260714000002_add_not_null_constraint_to_keys_org_id.rb— re-adds theNOT NULLconstraint.add_not_null_constraintis idempotent, so on GitLab.com (where the constraint already exists from the original migration) this is a no-op.
Customer workaround automated by this MR
Customers who hit this issue were advised to run:
UPDATE keys SET organization_id = 1 WHERE organization_id IS NULL;This MR automates that fix safely, in batches, as part of the upgrade path.
References
- Closes #605197 (closed)
- Pattern reference: !242492 (merged)
Screenshots or screen recordings
N/A — database migration only.
| Before | After |
|---|---|
How to set up and validate locally
-
Create a
keysrow withorganization_id = NULL(requires dropping the check constraint first):ALTER TABLE keys DROP CONSTRAINT IF EXISTS check_8933ae4f38; INSERT INTO keys (user_id, title, key, fingerprint, organization_id) VALUES (NULL, 'orphan', 'ssh-rsa AAAA...', 'aa:bb:cc', NULL); -
Run the backfill migration:
bundle exec rails db:migrate:up VERSION=20260714000001 -
Verify the row now has
organization_id = 1. -
Run the constraint migration:
bundle exec rails db:migrate:up VERSION=20260714000002 -
Confirm no
PG::CheckViolationis raised.
Database review: queries and query plans
Documenting the SQL and query plans for the batched data backfill in 20260714000001 (per the database review guidelines for bulk update_all operations).
SQL executed
The migration iterates keys rows with organization_id IS NULL in batches of 1,000 (each_batch) and updates each batch to the default organization:
-- batch boundary lookup (each_batch)
SELECT keys.id
FROM keys
WHERE keys.organization_id IS NULL
ORDER BY keys.id ASC
LIMIT 1 OFFSET 1000;
-- per-batch update
UPDATE keys
SET organization_id = 1
WHERE keys.organization_id IS NULL
AND keys.id >= :batch_start
AND keys.id < :batch_end;20260714000002 runs DDL only (add_not_null_constraint :keys, :organization_id, i.e. ADD CONSTRAINT ... NOT VALID + VALIDATE CONSTRAINT); it is idempotent and a no-op where the constraint already exists.
Index usage
The organization_id IS NULL predicate is served by the existing btree index index_keys_on_organization_id — PostgreSQL btree indexes include NULLs, so this is not a sequential scan of keys:
EXPLAIN SELECT COUNT(*) FROM keys WHERE organization_id IS NULL;
Aggregate (cost=3.19..3.20 rows=1 width=8)
-> Index Only Scan using index_keys_on_organization_id on keys (cost=0.15..3.18 rows=2 width=0)
Index Cond: (organization_id IS NULL)(Plan captured on a local development database; representative GitLab.com-scale numbers will come from the db:gitlabcom-database-testing pipeline. The relevant fact — index usage for organization_id IS NULL rather than a seq scan — is shown by the plan above.)
Expected cost
- GitLab.com: the
NOT NULLcheck constraint is already present and validated, sokeyscannot containorganization_id IS NULLrows — the backfill matches nothing (effective no-op). - Self-Managed / Dedicated: cost is proportional to the small residual NULL set, processed in batches of 1,000 with
disable_ddl_transaction!so each batch commits independently and holds no long locks.
Remaining pre-merge steps: request a database review and trigger the
db:gitlabcom-database-testingpipeline.
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.