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_metadatabuilds,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
-
Run the specs to verify the cop works correctly:
bundle exec rspec spec/rubocop/cop/database/avoid_unpartitioned_ci_relations_spec.rb -
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 -
Run RuboCop on the test file:
bundle exec rubocop --only Database/AvoidUnpartitionedCiRelations test_cop.rbExpected: Offense on line 3 for all_pipelines
-
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
- Variables: Local or instance variables containing "project" in the name (e.g.,
- 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_partitionNon-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 relationImplementation Details
Files Added/Modified
rubocop/cop/database/avoid_unpartitioned_ci_relations.rb- Main cop implementation
- Detects all 10 partitioned CI relations
- Supports both regular and safe navigation operators (
&.)
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
.rubocop_todo/database/avoid_unpartitioned_ci_relations.yml- TODO file excluding 41 existing violations
- Allows cop to be enabled without blocking current code
.rubocop.yml(2 lines added)- Enables the cop in RuboCop configuration
Related to #594579 (closed)