Use tag_list from job definition, if available

What does this MR do and why?

This MR implements the first step of migrating tag_list usage from legacy tables (ci_build_tags join table and tags table) to the new job definitions model, making job definitions the single source of truth (SSoT) for job tags, while falling back to legacy tables if the specified job's immutable details haven't yet been migrated to a job definition.

Background

As part of the CI/CD data sharding initiative, we are moving CI job metadata from legacy tables into the ci_job_definitions table. In Phase 2 (#565405), we started storing tag_list in job definitions. This MR switches the application to read tags from job definitions instead of the legacy ci_tags and ci_build_tags tables, whenever the job definition is available.

Changes

1. Override tag_list method in Ci::Build:

  • Reads tag_list from job_definition.config[:tag_list] when available
  • Falls back to the legacy tags association when job_definition is nil
  • Memoizes the result for performance
  • This ensures backward compatibility during the migration period

2. Update eager loading:

  • Modified eager_load_tags scope to include :job_definition association
  • Ensures efficient queries when accessing tags through job definitions

3. Adapt Ci::Build.build_matchers:

  • Updated to use job definition tags when available via new tag_names_array_query method
  • Uses COALESCE to try job definition first, then fall back to legacy tags
  • Maintains backward compatibility with feature flag control

4. Update Ci::PendingBuild.build_tags_ids:

  • New method that reads tags from job definition when available
  • Uses Ci::Tag.named(job_definition.config[:tag_list]).ids to get tag IDs
  • Falls back to build.tags_ids when job definition is nil or feature flag is disabled
  • NEW: Ensures pending builds use the correct tag source
  • NEW: Upserts tags from job definition when upsert_tags_from_ci_pending_build feature flag is enabled
  • NEW: Calls build.save_tags to ensure tags exist in the tags table before creating pending builds
  • NEW: This ensures ci_pending_builds.tag_ids can be populated even when tags don't yet exist in the database

5. Feature flags:

  • NEW: ci_build_uses_job_definition_tag_list - Controls reading tags from job definitions
  • NEW: upsert_tags_from_ci_pending_build - Controls upserting tags from job definitions to the tags table
  • Allows gradual migration and easy rollback if needed

6. Comprehensive test coverage:

  • Tests for reading tags from job definition
  • Tests for fallback to legacy tags when job definition is absent
  • Tests for empty tag lists in job definition
  • Tests for eager loading behavior
  • NEW: Tests for build_matchers with job definition tags
  • NEW: Tests for Ci::PendingBuild tag handling
  • NEW: Tests for upserting tags when they don't exist in the tags table
  • NEW: Tests for feature flag interactions between ci_build_uses_job_definition_tag_list and upsert_tags_from_ci_pending_build

Migration Path

This MR is part of a multi-phase migration:

  1. Phase 1: Add tag_list column to job definitions table
  2. Phase 2: Start storing tags in job definitions for new jobs
  3. 🔄 Phase 3 (this MR): Switch application to read from job definitions
  4. 📋 Phase 4: Backfill existing jobs with tags from legacy tables (!208674)
  5. 📋 Phase 5: Drop legacy ci_build_tags table

Benefits

  1. Single source of truth: Tags are now consistently read from job definitions
  2. Sharding ready: Job definitions are designed for horizontal sharding
  3. Performance: Reduces joins to legacy tables once migration is complete
  4. Data consistency: Eliminates potential discrepancies between legacy tables and job definitions
  5. NEW: Automatic tag creation: Tags are automatically created from job definitions when needed, ensuring ci_pending_builds can always reference valid tag IDs

References

Closes #569119 and #578669

Related to:

Screenshots or screen recordings

N/A - Backend code change

How to set up and validate locally

  1. Run the updated tests:

    bundle exec rspec spec/models/ci/build_spec.rb -e "tag_list"
    bundle exec rspec spec/models/ci/pending_build_spec.rb
  2. Test eager loading:

    # Verify no N+1 queries when accessing tags
    builds = Ci::Build.eager_load_tags.limit(10)
    
    # This should trigger a single query on p_ci_job_definition_instances/p_ci_job_definitions
    builds.each { |b| puts b.tag_list }
  3. Test with job_definitions:

    # Make sure we save the current development database state
    gdk sandbox disable; gdk sandbox enable
    # Get migrations from https://gitlab.com/gitlab-org/gitlab/-/merge_requests/208674
    curl https://gitlab.com/gitlab-org/gitlab/-/merge_requests/208674.diff | git apply
    # Clean migration MR code changes
    git reset && git clean -f -d . && git checkout . --force
    # Ensure there are some job descriptions now
    Ci::JobDefinition.count
    
    # Enable the feature flag
    Feature.enable(:ci_build_uses_job_definition_tag_list)
    
    # Verify tags are read from job definition
    build = Ci::BuildTag.first.build
    build.tag_list # Should load tags from job_definition.config[:tag_list] (`Ci::JobDefinition Load` and not hit `Ci::Tag Load`)
    
    pipeline = build.pipeline
    # Test return the builds from pipeline grouped by protected status and tags (coming from `p_ci_job_definitions`)
    pipeline.builds.latest.build_matchers(pipeline.project)
    
    # Verify tags are upserted when creating pending builds
    Feature.enable(:upsert_tags_from_ci_pending_build)
    Ci::PendingBuild.upsert_from_build!(build)
    # Rollback db state
    gdk sandbox reset

Database query plans

Ci::Build.build_matchers (FF disabled)

https://console.postgres.ai/gitlab/gitlab-production-ci/sessions/45265/commands/138815

SELECT array_agg(id), "p_ci_builds"."protected", (
     SELECT COALESCE(array_agg(tags.name ORDER BY tags.name), ARRAY[]::text[])
     FROM "p_ci_build_tags"
       INNER JOIN "tags" ON "tags"."id" = "p_ci_build_tags"."tag_id"
     WHERE "p_ci_build_tags"."build_id" = "p_ci_builds"."id"
       AND "p_ci_build_tags"."partition_id" = "p_ci_builds"."partition_id")
 FROM "p_ci_builds"
 WHERE "p_ci_builds"."type" = 'Ci::Build'
   AND "p_ci_builds"."commit_id" = 2143349430
   AND "p_ci_builds"."partition_id" = 107
   AND ("p_ci_builds"."retried" = FALSE
     OR "p_ci_builds"."retried" IS NULL)
 GROUP BY "p_ci_builds"."protected", (
     SELECT COALESCE(array_agg(tags.name ORDER BY tags.name), ARRAY[]::text[])
     FROM "p_ci_build_tags"
       INNER JOIN "tags" ON "tags"."id" = "p_ci_build_tags"."tag_id"
     WHERE "p_ci_build_tags"."build_id" = "p_ci_builds"."id"
       AND "p_ci_build_tags"."partition_id" = "p_ci_builds"."partition_id")
 Aggregate  (cost=27006.51..104781.49 rows=92 width=65) (actual time=7.916..8.378 rows=1 loops=1)
   Group Key: p_ci_builds.protected, ((SubPlan 1))
   Buffers: shared hit=55 read=12 dirtied=5
   WAL: records=9 fpi=5 bytes=40491
   ->  Incremental Sort  (cost=27006.51..52936.02 rows=92 width=41) (actual time=7.903..8.365 rows=10 loops=1)
         Sort Key: p_ci_builds.protected, ((SubPlan 1))
         Buffers: shared hit=55 read=12 dirtied=5
         WAL: records=9 fpi=5 bytes=40491
         ->  Gather Merge  (cost=1078.18..52932.29 rows=92 width=41) (actual time=7.820..8.339 rows=10 loops=1)
               Workers Planned: 1
               Workers Launched: 1
               Buffers: shared hit=50 read=12 dirtied=5
               WAL: records=9 fpi=5 bytes=40491
               ->  Sort  (cost=78.17..78.31 rows=54 width=17) (actual time=1.886..1.887 rows=5 loops=2)
                     Sort Key: p_ci_builds.protected
                     Sort Method: quicksort  Memory: 25kB
                     Buffers: shared hit=11 read=8 dirtied=5
                     WAL: records=9 fpi=5 bytes=40491
                     ->  Parallel Index Scan using ci_builds_107_commit_id_type_ref_idx on gitlab_partitions_dynamic.ci_builds_107 p_ci_builds  (cost=0.57..76.62 rows=54 width=17) (actual time=1.485..1.857 rows=5 loops=2)
                           Index Cond: ((p_ci_builds.commit_id = 2143349430) AND ((p_ci_builds.type)::text = 'Ci::Build'::text))
                           Filter: (((NOT p_ci_builds.retried) OR (p_ci_builds.retried IS NULL)) AND (p_ci_builds.partition_id = 107))
                           Rows Removed by Filter: 0
                           Buffers: shared hit=1 read=8 dirtied=5
                           WAL: records=9 fpi=5 bytes=40491
               SubPlan 1
                 ->  Aggregate  (cost=563.51..563.52 rows=1 width=32) (actual time=0.137..0.137 rows=1 loops=10)
                       Buffers: shared hit=39 read=4
                       ->  Sort  (cost=562.87..563.19 rows=128 width=34) (actual time=0.136..0.136 rows=0 loops=10)
                             Sort Key: tags.name
                             Sort Method: quicksort  Memory: 25kB
                             Buffers: shared hit=39 read=4
                             ->  Nested Loop  (cost=1.01..558.39 rows=128 width=34) (actual time=0.130..0.130 rows=0 loops=10)
                                   Buffers: shared hit=36 read=4
                                   ->  Append  (cost=0.58..117.11 rows=128 width=8) (actual time=0.128..0.129 rows=0 loops=10)
                                         Buffers: shared hit=36 read=4
                                         ->  Index Scan using ci_build_tags_100_build_id_partition_id_idx on gitlab_partitions_dynamic.ci_build_tags_100 p_ci_build_tags_1  (cost=0.58..39.10 rows=53 width=8) (actual time=0.000..0.000 rows=0 loops=0)
                                               Index Cond: ((p_ci_build_tags_1.build_id = p_ci_builds.id) AND (p_ci_build_tags_1.partition_id = p_ci_builds.partition_id))
                                         ->  Index Scan using ci_build_tags_101_build_id_partition_id_idx on gitlab_partitions_dynamic.ci_build_tags_101 p_ci_build_tags_2  (cost=0.57..14.84 rows=17 width=8) (actual time=0.000..0.000 rows=0 loops=0)
                                               Index Cond: ((p_ci_build_tags_2.build_id = p_ci_builds.id) AND (p_ci_build_tags_2.partition_id = p_ci_builds.partition_id))
                                         ->  Index Scan using ci_build_tags_102_build_id_partition_id_idx on gitlab_partitions_dynamic.ci_build_tags_102 p_ci_build_tags_3  (cost=0.58..28.56 rows=29 width=8) (actual time=0.000..0.000 rows=0 loops=0)
                                               Index Cond: ((p_ci_build_tags_3.build_id = p_ci_builds.id) AND (p_ci_build_tags_3.partition_id = p_ci_builds.partition_id))
                                         ->  Index Scan using ci_build_tags_103_build_id_partition_id_idx on gitlab_partitions_dynamic.ci_build_tags_103 p_ci_build_tags_4  (cost=0.57..6.36 rows=5 width=8) (actual time=0.000..0.000 rows=0 loops=0)
                                               Index Cond: ((p_ci_build_tags_4.build_id = p_ci_builds.id) AND (p_ci_build_tags_4.partition_id = p_ci_builds.partition_id))
                                         ->  Index Scan using ci_build_tags_104_build_id_partition_id_idx on gitlab_partitions_dynamic.ci_build_tags_104 p_ci_build_tags_5  (cost=0.57..7.07 rows=6 width=8) (actual time=0.000..0.000 rows=0 loops=0)
                                               Index Cond: ((p_ci_build_tags_5.build_id = p_ci_builds.id) AND (p_ci_build_tags_5.partition_id = p_ci_builds.partition_id))
                                         ->  Index Scan using ci_build_tags_105_build_id_partition_id_idx on gitlab_partitions_dynamic.ci_build_tags_105 p_ci_build_tags_6  (cost=0.57..7.07 rows=6 width=8) (actual time=0.000..0.000 rows=0 loops=0)
                                               Index Cond: ((p_ci_build_tags_6.build_id = p_ci_builds.id) AND (p_ci_build_tags_6.partition_id = p_ci_builds.partition_id))
                                         ->  Index Scan using ci_build_tags_106_build_id_partition_id_idx on gitlab_partitions_dynamic.ci_build_tags_106 p_ci_build_tags_7  (cost=0.57..7.15 rows=6 width=8) (actual time=0.000..0.000 rows=0 loops=0)
                                               Index Cond: ((p_ci_build_tags_7.build_id = p_ci_builds.id) AND (p_ci_build_tags_7.partition_id = p_ci_builds.partition_id))
                                         ->  Index Scan using ci_build_tags_107_build_id_partition_id_idx on gitlab_partitions_dynamic.ci_build_tags_107 p_ci_build_tags_8  (cost=0.57..6.33 rows=5 width=8) (actual time=0.127..0.127 rows=0 loops=10)
                                               Index Cond: ((p_ci_build_tags_8.build_id = p_ci_builds.id) AND (p_ci_build_tags_8.partition_id = p_ci_builds.partition_id))
                                               Buffers: shared hit=36 read=4
                                         ->  Seq Scan on gitlab_partitions_dynamic.ci_build_tags_108 p_ci_build_tags_9  (cost=0.00..0.00 rows=1 width=8) (actual time=0.000..0.000 rows=0 loops=0)
                                               Filter: ((p_ci_build_tags_9.build_id = p_ci_builds.id) AND (p_ci_build_tags_9.partition_id = p_ci_builds.partition_id))
                                               Rows Removed by Filter: 0
                                   ->  Index Scan using tags_pkey on public.tags  (cost=0.43..3.45 rows=1 width=38) (actual time=0.000..0.000 rows=0 loops=0)
                                         Index Cond: (tags.id = p_ci_build_tags.tag_id)
Settings: jit = 'off', seq_page_cost = '4', work_mem = '100MB', effective_cache_size = '338688MB', random_page_cost = '1.5'
Ci::Build.build_matchers (FF enabled)

https://console.postgres.ai/gitlab/gitlab-production-ci/sessions/45265/commands/138817

SELECT array_agg(id), "p_ci_builds"."protected", COALESCE((
     SELECT ARRAY (
     SELECT jsonb_array_elements_text(COALESCE("p_ci_job_definitions".config -> 'tag_list',
       '[]'::jsonb)) AS tag_list ORDER BY tag_list)
     FROM "p_ci_job_definition_instances"
     INNER JOIN "p_ci_job_definitions" ON "p_ci_job_definitions"."partition_id" IS NOT NULL
       AND "p_ci_job_definitions"."id" = "p_ci_job_definition_instances"."job_definition_id"
       AND "p_ci_job_definitions"."partition_id" = "p_ci_job_definition_instances"."partition_id"
     WHERE "p_ci_job_definition_instances"."job_id" = "p_ci_builds"."id"
       AND "p_ci_job_definition_instances"."partition_id" = "p_ci_builds"."partition_id" LIMIT 1), (
     SELECT COALESCE(array_agg(tags.name ORDER BY tags.name), ARRAY[]::text[])
     FROM "p_ci_build_tags"
     INNER JOIN "tags" ON "tags"."id" = "p_ci_build_tags"."tag_id"
     WHERE "p_ci_build_tags"."build_id" = "p_ci_builds"."id"
       AND "p_ci_build_tags"."partition_id" = "p_ci_builds"."partition_id"))
 FROM "p_ci_builds"
 WHERE "p_ci_builds"."type" = 'Ci::Build'
   AND "p_ci_builds"."commit_id" = 2143349430
   AND "p_ci_builds"."partition_id" = 107
   AND ("p_ci_builds"."retried" = FALSE
     OR "p_ci_builds"."retried" IS NULL)
 GROUP BY "p_ci_builds"."protected", COALESCE((
     SELECT ARRAY (
     SELECT jsonb_array_elements_text(COALESCE("p_ci_job_definitions".config -> 'tag_list',
       '[]'::jsonb)) AS tag_list ORDER BY tag_list)
     FROM "p_ci_job_definition_instances"
     INNER JOIN "p_ci_job_definitions" ON "p_ci_job_definitions"."partition_id" IS NOT NULL
       AND "p_ci_job_definitions"."id" = "p_ci_job_definition_instances"."job_definition_id"
       AND "p_ci_job_definitions"."partition_id" = "p_ci_job_definition_instances"."partition_id"
   WHERE "p_ci_job_definition_instances"."job_id" = "p_ci_builds"."id"
     AND "p_ci_job_definition_instances"."partition_id" = "p_ci_builds"."partition_id" LIMIT 1), (
   SELECT COALESCE(array_agg(tags.name ORDER BY tags.name), ARRAY[]::text[])
   FROM "p_ci_build_tags"
   INNER JOIN "tags" ON "tags"."id" = "p_ci_build_tags"."tag_id"
 WHERE "p_ci_build_tags"."build_id" = "p_ci_builds"."id"
   AND "p_ci_build_tags"."partition_id" = "p_ci_builds"."partition_id"))
 Aggregate  (cost=28529.94..110875.21 rows=92 width=65) (actual time=5.628..6.600 rows=1 loops=1)
   Group Key: p_ci_builds.protected, (COALESCE((SubPlan 2), ((SubPlan 3))::text[]))
   Buffers: shared hit=116
   ->  Incremental Sort  (cost=28529.94..55982.88 rows=92 width=41) (actual time=5.614..6.585 rows=10 loops=1)
         Sort Key: p_ci_builds.protected, (COALESCE((SubPlan 2), ((SubPlan 3))::text[]))
         Buffers: shared hit=116
         ->  Gather Merge  (cost=1078.18..55979.15 rows=92 width=41) (actual time=5.437..6.554 rows=10 loops=1)
               Workers Planned: 1
               Workers Launched: 1
               Buffers: shared hit=111
               ->  Sort  (cost=78.17..78.31 rows=54 width=17) (actual time=0.046..0.047 rows=5 loops=2)
                     Sort Key: p_ci_builds.protected
                     Sort Method: quicksort  Memory: 25kB
                     Buffers: shared hit=18
                     ->  Parallel Index Scan using ci_builds_107_commit_id_type_ref_idx on gitlab_partitions_dynamic.ci_builds_107 p_ci_builds  (cost=0.57..76.62 rows=54 width=17) (actual time=0.011..0.021 rows=5 loops=2)
                           Index Cond: ((p_ci_builds.commit_id = 2143349430) AND ((p_ci_builds.type)::text = 'Ci::Build'::text))
                           Filter: (((NOT p_ci_builds.retried) OR (p_ci_builds.retried IS NULL)) AND (p_ci_builds.partition_id = 107))
                           Rows Removed by Filter: 0
                           Buffers: shared hit=8
               SubPlan 2
                 ->  Limit  (cost=0.56..33.12 rows=1 width=32) (actual time=0.071..0.072 rows=1 loops=10)
                       Buffers: shared hit=93
                       ->  Nested Loop  (cost=0.56..293.60 rows=9 width=32) (actual time=0.071..0.071 rows=1 loops=10)
                             Buffers: shared hit=93
                             ->  Append  (cost=0.28..27.06 rows=9 width=16) (actual time=0.010..0.010 rows=1 loops=10)
                                   Buffers: shared hit=50
                                   ->  Index Scan using ci_job_definition_instances_100_pkey on gitlab_partitions_dynamic.ci_job_definition_instances_100 p_ci_job_definition_instances_1  (cost=0.28..3.30 rows=1 width=16) (actual time=0.000..0.000 rows=0 loops=0)
                                         Index Cond: ((p_ci_job_definition_instances_1.job_id = p_ci_builds.id) AND (p_ci_job_definition_instances_1.partition_id = p_ci_builds.partition_id))
                                   ->  Index Scan using ci_job_definition_instances_101_pkey on gitlab_partitions_dynamic.ci_job_definition_instances_101 p_ci_job_definition_instances_2  (cost=0.28..3.30 rows=1 width=16) (actual time=0.000..0.000 rows=0 loops=0)
                                         Index Cond: ((p_ci_job_definition_instances_2.job_id = p_ci_builds.id) AND (p_ci_job_definition_instances_2.partition_id = p_ci_builds.partition_id))
                                   ->  Index Scan using ci_job_definition_instances_102_pkey on gitlab_partitions_dynamic.ci_job_definition_instances_102 p_ci_job_definition_instances_3  (cost=0.29..3.31 rows=1 width=16) (actual time=0.000..0.000 rows=0 loops=0)
                                         Index Cond: ((p_ci_job_definition_instances_3.job_id = p_ci_builds.id) AND (p_ci_job_definition_instances_3.partition_id = p_ci_builds.partition_id))
                                   ->  Index Scan using ci_job_definition_instances_103_pkey on gitlab_partitions_dynamic.ci_job_definition_instances_103 p_ci_job_definition_instances_4  (cost=0.28..3.30 rows=1 width=16) (actual time=0.000..0.000 rows=0 loops=0)
                                         Index Cond: ((p_ci_job_definition_instances_4.job_id = p_ci_builds.id) AND (p_ci_job_definition_instances_4.partition_id = p_ci_builds.partition_id))
                                   ->  Index Scan using ci_job_definition_instances_104_pkey on gitlab_partitions_dynamic.ci_job_definition_instances_104 p_ci_job_definition_instances_5  (cost=0.29..3.31 rows=1 width=16) (actual time=0.000..0.000 rows=0 loops=0)
                                         Index Cond: ((p_ci_job_definition_instances_5.job_id = p_ci_builds.id) AND (p_ci_job_definition_instances_5.partition_id = p_ci_builds.partition_id))
                                   ->  Index Scan using ci_job_definition_instances_105_pkey on gitlab_partitions_dynamic.ci_job_definition_instances_105 p_ci_job_definition_instances_6  (cost=0.29..3.31 rows=1 width=16) (actual time=0.000..0.000 rows=0 loops=0)
                                         Index Cond: ((p_ci_job_definition_instances_6.job_id = p_ci_builds.id) AND (p_ci_job_definition_instances_6.partition_id = p_ci_builds.partition_id))
                                   ->  Index Scan using ci_job_definition_instances_106_pkey on gitlab_partitions_dynamic.ci_job_definition_instances_106 p_ci_job_definition_instances_7  (cost=0.57..3.59 rows=1 width=16) (actual time=0.000..0.000 rows=0 loops=0)
                                         Index Cond: ((p_ci_job_definition_instances_7.job_id = p_ci_builds.id) AND (p_ci_job_definition_instances_7.partition_id = p_ci_builds.partition_id))
                                   ->  Index Scan using ci_job_definition_instances_107_pkey on gitlab_partitions_dynamic.ci_job_definition_instances_107 p_ci_job_definition_instances_8  (cost=0.57..3.59 rows=1 width=16) (actual time=0.008..0.008 rows=1 loops=10)
                                         Index Cond: ((p_ci_job_definition_instances_8.job_id = p_ci_builds.id) AND (p_ci_job_definition_instances_8.partition_id = p_ci_builds.partition_id))
                                         Buffers: shared hit=50
                                   ->  Seq Scan on gitlab_partitions_dynamic.ci_job_definition_instances_108 p_ci_job_definition_instances_9  (cost=0.00..0.00 rows=1 width=16) (actual time=0.000..0.000 rows=0 loops=0)
                                         Filter: ((p_ci_job_definition_instances_9.partition_id = p_ci_builds.partition_id) AND (p_ci_job_definition_instances_9.job_id = p_ci_builds.id))
                                         Rows Removed by Filter: 0
                             ->  Append  (cost=0.28..25.43 rows=9 width=675) (actual time=0.007..0.007 rows=1 loops=10)
                                   Buffers: shared hit=40
                                   ->  Index Scan using ci_job_definitions_100_pkey on gitlab_partitions_dynamic.ci_job_definitions_100 p_ci_job_definitions_1  (cost=0.28..2.80 rows=1 width=886) (actual time=0.000..0.000 rows=0 loops=0)
                                         Index Cond: ((p_ci_job_definitions_1.id = p_ci_job_definition_instances.job_definition_id) AND (p_ci_job_definitions_1.partition_id = p_ci_builds.partition_id))
                                   ->  Index Scan using ci_job_definitions_101_pkey on gitlab_partitions_dynamic.ci_job_definitions_101 p_ci_job_definitions_2  (cost=0.28..2.79 rows=1 width=765) (actual time=0.000..0.000 rows=0 loops=0)
                                         Index Cond: ((p_ci_job_definitions_2.id = p_ci_job_definition_instances.job_definition_id) AND (p_ci_job_definitions_2.partition_id = p_ci_builds.partition_id))
                                   ->  Index Scan using ci_job_definitions_102_pkey on gitlab_partitions_dynamic.ci_job_definitions_102 p_ci_job_definitions_3  (cost=0.28..3.14 rows=1 width=820) (actual time=0.000..0.000 rows=0 loops=0)
                                         Index Cond: ((p_ci_job_definitions_3.id = p_ci_job_definition_instances.job_definition_id) AND (p_ci_job_definitions_3.partition_id = p_ci_builds.partition_id))
                                   ->  Index Scan using ci_job_definitions_103_pkey on gitlab_partitions_dynamic.ci_job_definitions_103 p_ci_job_definitions_4  (cost=0.28..3.14 rows=1 width=689) (actual time=0.000..0.000 rows=0 loops=0)
                                         Index Cond: ((p_ci_job_definitions_4.id = p_ci_job_definition_instances.job_definition_id) AND (p_ci_job_definitions_4.partition_id = p_ci_builds.partition_id))
                                   ->  Index Scan using ci_job_definitions_104_pkey on gitlab_partitions_dynamic.ci_job_definitions_104 p_ci_job_definitions_5  (cost=0.29..3.30 rows=1 width=741) (actual time=0.000..0.000 rows=0 loops=0)
                                         Index Cond: ((p_ci_job_definitions_5.id = p_ci_job_definition_instances.job_definition_id) AND (p_ci_job_definitions_5.partition_id = p_ci_builds.partition_id))
                                   ->  Index Scan using ci_job_definitions_105_pkey on gitlab_partitions_dynamic.ci_job_definitions_105 p_ci_job_definitions_6  (cost=0.29..3.31 rows=1 width=643) (actual time=0.000..0.000 rows=0 loops=0)
                                         Index Cond: ((p_ci_job_definitions_6.id = p_ci_job_definition_instances.job_definition_id) AND (p_ci_job_definitions_6.partition_id = p_ci_builds.partition_id))
                                   ->  Index Scan using ci_job_definitions_106_pkey on gitlab_partitions_dynamic.ci_job_definitions_106 p_ci_job_definitions_7  (cost=0.43..3.45 rows=1 width=671) (actual time=0.000..0.000 rows=0 loops=0)
                                         Index Cond: ((p_ci_job_definitions_7.id = p_ci_job_definition_instances.job_definition_id) AND (p_ci_job_definitions_7.partition_id = p_ci_builds.partition_id))
                                   ->  Index Scan using ci_job_definitions_107_pkey on gitlab_partitions_dynamic.ci_job_definitions_107 p_ci_job_definitions_8  (cost=0.44..3.46 rows=1 width=677) (actual time=0.006..0.006 rows=1 loops=10)
                                         Index Cond: ((p_ci_job_definitions_8.id = p_ci_job_definition_instances.job_definition_id) AND (p_ci_job_definitions_8.partition_id = p_ci_builds.partition_id))
                                         Buffers: shared hit=40
                                   ->  Seq Scan on gitlab_partitions_dynamic.ci_job_definitions_108 p_ci_job_definitions_9  (cost=0.00..0.00 rows=1 width=48) (actual time=0.000..0.000 rows=0 loops=0)
                                         Filter: ((p_ci_job_definitions_9.partition_id = p_ci_builds.partition_id) AND (p_ci_job_definitions_9.id = p_ci_job_definition_instances.job_definition_id))
                                         Rows Removed by Filter: 0
                             SubPlan 1
                               ->  Sort  (cost=3.84..4.09 rows=100 width=32) (actual time=0.051..0.051 rows=0 loops=10)
                                     Sort Key: (jsonb_array_elements_text(COALESCE((p_ci_job_definitions.config -> 'tag_list'::text), '[]'::jsonb)))
                                     Sort Method: quicksort  Memory: 25kB
                                     Buffers: shared hit=3
                                     ->  ProjectSet  (cost=0.00..0.52 rows=100 width=32) (actual time=0.046..0.046 rows=0 loops=10)
                                           ->  Result  (cost=0.00..0.01 rows=1 width=0) (actual time=0.000..0.000 rows=1 loops=10)
               SubPlan 3
                 ->  Aggregate  (cost=563.51..563.52 rows=1 width=32) (actual time=0.000..0.000 rows=0 loops=0)
                       ->  Sort  (cost=562.87..563.19 rows=128 width=34) (actual time=0.000..0.000 rows=0 loops=0)
                             Sort Key: tags.name
                             ->  Nested Loop  (cost=1.01..558.39 rows=128 width=34) (actual time=0.000..0.000 rows=0 loops=0)
                                   ->  Append  (cost=0.58..117.11 rows=128 width=8) (actual time=0.000..0.000 rows=0 loops=0)
                                         ->  Index Scan using ci_build_tags_100_build_id_partition_id_idx on gitlab_partitions_dynamic.ci_build_tags_100 p_ci_build_tags_1  (cost=0.58..39.10 rows=53 width=8) (actual time=0.000..0.000 rows=0 loops=0)
                                               Index Cond: ((p_ci_build_tags_1.build_id = p_ci_builds.id) AND (p_ci_build_tags_1.partition_id = p_ci_builds.partition_id))
                                         ->  Index Scan using ci_build_tags_101_build_id_partition_id_idx on gitlab_partitions_dynamic.ci_build_tags_101 p_ci_build_tags_2  (cost=0.57..14.84 rows=17 width=8) (actual time=0.000..0.000 rows=0 loops=0)
                                               Index Cond: ((p_ci_build_tags_2.build_id = p_ci_builds.id) AND (p_ci_build_tags_2.partition_id = p_ci_builds.partition_id))
                                         ->  Index Scan using ci_build_tags_102_build_id_partition_id_idx on gitlab_partitions_dynamic.ci_build_tags_102 p_ci_build_tags_3  (cost=0.58..28.56 rows=29 width=8) (actual time=0.000..0.000 rows=0 loops=0)
                                               Index Cond: ((p_ci_build_tags_3.build_id = p_ci_builds.id) AND (p_ci_build_tags_3.partition_id = p_ci_builds.partition_id))
                                         ->  Index Scan using ci_build_tags_103_build_id_partition_id_idx on gitlab_partitions_dynamic.ci_build_tags_103 p_ci_build_tags_4  (cost=0.57..6.36 rows=5 width=8) (actual time=0.000..0.000 rows=0 loops=0)
                                               Index Cond: ((p_ci_build_tags_4.build_id = p_ci_builds.id) AND (p_ci_build_tags_4.partition_id = p_ci_builds.partition_id))
                                         ->  Index Scan using ci_build_tags_104_build_id_partition_id_idx on gitlab_partitions_dynamic.ci_build_tags_104 p_ci_build_tags_5  (cost=0.57..7.07 rows=6 width=8) (actual time=0.000..0.000 rows=0 loops=0)
                                               Index Cond: ((p_ci_build_tags_5.build_id = p_ci_builds.id) AND (p_ci_build_tags_5.partition_id = p_ci_builds.partition_id))
                                         ->  Index Scan using ci_build_tags_105_build_id_partition_id_idx on gitlab_partitions_dynamic.ci_build_tags_105 p_ci_build_tags_6  (cost=0.57..7.07 rows=6 width=8) (actual time=0.000..0.000 rows=0 loops=0)
                                               Index Cond: ((p_ci_build_tags_6.build_id = p_ci_builds.id) AND (p_ci_build_tags_6.partition_id = p_ci_builds.partition_id))
                                         ->  Index Scan using ci_build_tags_106_build_id_partition_id_idx on gitlab_partitions_dynamic.ci_build_tags_106 p_ci_build_tags_7  (cost=0.57..7.15 rows=6 width=8) (actual time=0.000..0.000 rows=0 loops=0)
                                               Index Cond: ((p_ci_build_tags_7.build_id = p_ci_builds.id) AND (p_ci_build_tags_7.partition_id = p_ci_builds.partition_id))
                                         ->  Index Scan using ci_build_tags_107_build_id_partition_id_idx on gitlab_partitions_dynamic.ci_build_tags_107 p_ci_build_tags_8  (cost=0.57..6.33 rows=5 width=8) (actual time=0.000..0.000 rows=0 loops=0)
                                               Index Cond: ((p_ci_build_tags_8.build_id = p_ci_builds.id) AND (p_ci_build_tags_8.partition_id = p_ci_builds.partition_id))
                                         ->  Seq Scan on gitlab_partitions_dynamic.ci_build_tags_108 p_ci_build_tags_9  (cost=0.00..0.00 rows=1 width=8) (actual time=0.000..0.000 rows=0 loops=0)
                                               Filter: ((p_ci_build_tags_9.build_id = p_ci_builds.id) AND (p_ci_build_tags_9.partition_id = p_ci_builds.partition_id))
                                               Rows Removed by Filter: 0
                                   ->  Index Scan using tags_pkey on public.tags  (cost=0.43..3.45 rows=1 width=38) (actual time=0.000..0.000 rows=0 loops=0)
                                         Index Cond: (tags.id = p_ci_build_tags.tag_id)
Settings: effective_cache_size = '338688MB', random_page_cost = '1.5', jit = 'off', seq_page_cost = '4', work_mem = '100MB'

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.

  • Performance: Eager loading updated to include job_definition; memoization added for tag_list
  • Reliability: Maintains backward compatibility with fallback to legacy tags; feature flag allows safe rollout
  • Security: No security implications
  • Maintainability: Simplifies data model by establishing job definitions as SSoT
  • Testing: Comprehensive test coverage for all scenarios including edge cases
  • Database: Part of planned migration to drop legacy tables; no immediate database changes
Edited by Pedro Pombeiro

Merge request reports

Loading