Skip to content

NOT NULL constraint to target_project_id column

What does this MR do?

This change adds and validates NOT NULL CHECK constraint on merge_request_metrics.target_project_id column.

The data was already migrated in this MR: !37713 (merged)

The background migration that fills up the column has been already executed in 13.3 milestone. We have 0 records to migrate on PRD (0 missed records by failed sidekiq job).

This is what happens:

  1. Add a NOT NULL NOT VALID constraint to ensure new records will not have NULL target project id.
    • This is already ensured on the model level since (13.3)
  2. Add a temporary index to support the data migration of missed records.
  3. Steal the queued BG jobs and check and update missed records with EachBatch.
  4. Validate the NOT NULL constraint and remove the temporary index.

Migration

Index creation takes <2 minutes on DB lab.

Up

== 20200831065320 AddNotValidNotNullConstraintToMrMetrics: migrating ==========
-- transaction_open?()
   -> 0.0000s
-- execute("ALTER TABLE merge_request_metrics\nADD CONSTRAINT check_e03d0900bf\nCHECK ( target_project_id IS NOT NULL )\nNOT VALID;\n")
   -> 0.0014s
== 20200831065320 AddNotValidNotNullConstraintToMrMetrics: migrated (0.0116s) =

== 20200831065322 AddTmpIndexToTargetProjectId: migrating =====================
-- transaction_open?()
   -> 0.0000s
-- index_exists?(:merge_request_metrics, :id, {:where=>"target_project_id IS NULL", :name=>"tmp_index_on_mr_metrics_target_project_id_null", :algorithm=>:concurrently})
   -> 0.0035s
-- add_index(:merge_request_metrics, :id, {:where=>"target_project_id IS NULL", :name=>"tmp_index_on_mr_metrics_target_project_id_null", :algorithm=>:concurrently})
   -> 0.0185s
== 20200831065322 AddTmpIndexToTargetProjectId: migrated (0.0223s) ============

== 20200831065705 EnsureTargetProjectIdIsFilled: migrating ====================
== 20200831065705 EnsureTargetProjectIdIsFilled: migrated (0.0112s) ===========

== 20200831074356 ValidateNotNullConstraintOnMrMetrics: migrating =============
-- execute("ALTER TABLE merge_request_metrics VALIDATE CONSTRAINT check_e03d0900bf;")
   -> 0.0012s
-- transaction_open?()
   -> 0.0000s
-- indexes(:merge_request_metrics)
   -> 0.0137s
-- remove_index(:merge_request_metrics, {:algorithm=>:concurrently, :name=>"tmp_index_on_mr_metrics_target_project_id_null"})
   -> 0.0034s
== 20200831074356 ValidateNotNullConstraintOnMrMetrics: migrated (0.0201s) ====

Down

== 20200831074356 ValidateNotNullConstraintOnMrMetrics: reverting =============
-- transaction_open?()
   -> 0.0000s
-- index_exists?(:merge_request_metrics, :id, {:where=>"target_project_id IS NULL", :name=>"tmp_index_on_mr_metrics_target_project_id_null", :algorithm=>:concurrently})
   -> 0.0037s
-- add_index(:merge_request_metrics, :id, {:where=>"target_project_id IS NULL", :name=>"tmp_index_on_mr_metrics_target_project_id_null", :algorithm=>:concurrently})
   -> 0.0222s
== 20200831074356 ValidateNotNullConstraintOnMrMetrics: reverted (0.0263s) ====


== 20200831065705 EnsureTargetProjectIdIsFilled: reverting ====================
== 20200831065705 EnsureTargetProjectIdIsFilled: reverted (0.0000s) ===========


== 20200831065322 AddTmpIndexToTargetProjectId: reverting =====================
-- transaction_open?()
   -> 0.0000s
-- indexes(:merge_request_metrics)
   -> 0.0047s
-- remove_index(:merge_request_metrics, {:algorithm=>:concurrently, :name=>"tmp_index_on_mr_metrics_target_project_id_null"})
   -> 0.0017s
== 20200831065322 AddTmpIndexToTargetProjectId: reverted (0.0069s) ============


== 20200831065320 AddNotValidNotNullConstraintToMrMetrics: reverting ==========
-- execute("ALTER TABLE merge_request_metrics\nDROP CONSTRAINT IF EXISTS check_e03d0900bf\n")
   -> 0.0006s
== 20200831065320 AddNotValidNotNullConstraintToMrMetrics: reverted (0.0042s) =

Queries

Each Batch query:

SELECT "merge_request_metrics"."id" FROM "merge_request_metrics" WHERE "merge_request_metrics"."target_project_id" IS NULL AND "merge_request_metrics"."id" >= 1 ORDER BY "merge_request_metrics"."id" ASC LIMIT 1 OFFSET 1

Plan

Update query (cannot get a real plan since we migrated everything on PRD). The structure is similar to the query above (it has an extra join which shouldn't add too much overhead on the query).

 WITH target_project_id_and_metrics_id as (
          SELECT "merge_request_metrics"."id" AS id, "merge_requests"."target_project_id" AS target_project_id FROM "merge_request_metrics" INNER JOIN "merge_requests" ON "merge_requests"."id" = "merge_request_metrics"."merge_request_id" WHERE "merge_request_metrics"."target_project_id" IS NULL AND "merge_request_metrics"."id" >= 1
        )
        UPDATE "merge_request_metrics"
        SET target_project_id = target_project_id_and_metrics_id.target_project_id
        FROM target_project_id_and_metrics_id
        WHERE merge_request_metrics.id = target_project_id_and_metrics_id.id

Does this MR meet the acceptance criteria?

Conformity

Availability and Testing

Security

If this MR contains changes to processing or storing of credentials or tokens, authorization and authentication methods and other items described in the security review guidelines:

  • Label as security and @ mention @gitlab-com/gl-security/appsec
  • The MR includes necessary changes to maintain consistency between UI, API, email, or other methods
  • Security reports checked/validated by a reviewer from the AppSec team

Closes #233507 (closed)

Edited by Adam Hegyi

Merge request reports