Add RuboCop cop to prevent unpartitioned CI relation queries

What does this MR do and why?

This MR adds a new RuboCop cop Database/AvoidUnpartitionedCiRelations to prevent a critical performance issue where calling partitioned CI table relations on Project without .in_partition() causes expensive full cross-partition database scans.

Problem: Following a production incident, queries against partitioned CI tables without partition filtering were causing severe database performance issues, including lock contention and Sidekiq job processing delays.

Solution: This cop detects when developers call any of these 10 partitioned CI relations on a Project instance without scoping to a partition:

  • commit_statuses, all_pipelines, ci_pipelines, stages, pipeline_metadata
  • builds, bridges, processables, build_trace_chunks, job_artifacts

Example:

# Bad - causes expensive cross-partition scan
project.all_pipelines
project.builds.where(status: :failed)
# Good - scans only the specified partition
project.all_pipelines.in_partition(106)
project.builds.in_partition(106).where(status: :failed)

References

Related to #594579 (closed)

How to set up and validate locally

  1. Run the specs to verify the cop works correctly:

    bundle exec rspec spec/rubocop/cop/database/avoid_unpartitioned_ci_relations_spec.rb

  2. Test the cop on a sample file by creating test_cop.rb:

    class TestService
         def execute
           project.all_pipelines  # Should trigger offense
           project.builds.in_partition(106)  # Should NOT trigger offense
         end
       end
  3. Run RuboCop on the test file:

    bundle exec rubocop --only Database/AvoidUnpartitionedCiRelations test_cop.rb

    Expected: Offense on line 3 for all_pipelines

  4. Verify the cop is enabled:

    bundle exec rubocop --show-cops Database/AvoidUnpartitionedCiRelations

Cop Behaviour

Offense Detection

The cop registers an offense when:

  • A method call matches one of the 10 partitioned CI relations
  • The receiver is detected as a Project instance using these heuristics:
    • Variables: Local or instance variables containing "project" in the name (e.g., project, @project, @my_project)
    • Method calls: Methods known to return projects: project, find_project, target_project, source_project, forked_from_project
  • The method chain does NOT include .in_partition(...) anywhere

Non-Offenses

The cop does NOT register an offense when:

  • .in_partition(partition_id) is called anywhere in the method chain
  • The receiver is not a Project instance (e.g., user.all_pipelines, namespace.builds)
  • The relation is not one of the 10 partitioned CI relations (e.g., project.issues, project.merge_requests)

Examples

Offenses:

project.all_pipelines                    # Missing .in_partition
@project.builds.where(status: :failed)   # Missing .in_partition
find_project.job_artifacts               # Missing .in_partition
project&.ci_pipelines                    # Safe navigation, still missing .in_partition

Non-Offenses:

project.all_pipelines.in_partition(106)                           # Has .in_partition
project.builds.in_partition(106).where(status: :failed)           # Has .in_partition
project.all_pipelines.in_partition(106).recent.limit(10)          # Has .in_partition in chain
project&.ci_pipelines.in_partition(106)                           # Safe navigation with .in_partition
user.all_pipelines                                                # Not a project
project.issues                                                    # Not a partitioned CI relation

Implementation Details

Files Added/Modified

  1. rubocop/cop/database/avoid_unpartitioned_ci_relations.rb 
    • Main cop implementation
    • Detects all 10 partitioned CI relations
    • Supports both regular and safe navigation operators (&.)
  2. spec/rubocop/cop/database/avoid_unpartitioned_ci_relations_spec.rb
    • Comprehensive tests with 126 test cases
    • Tests each relation for both offense and non-offense scenarios
    • Includes edge cases for safe navigation and variable naming
  3. .rubocop_todo/database/avoid_unpartitioned_ci_relations.yml
    • TODO file excluding 41 existing violations
    • Allows cop to be enabled without blocking current code
  4. .rubocop.yml (2 lines added)
    • Enables the cop in RuboCop configuration

Related to #594579 (closed)

Edited by Madhusudan Vaishnao

Merge request reports

Loading
Loading