diff --git a/doc/development/rake_tasks.md b/doc/development/rake_tasks.md index e9d6cfe00b2..a144bf70a86 100644 --- a/doc/development/rake_tasks.md +++ b/doc/development/rake_tasks.md @@ -216,3 +216,13 @@ bundle exec rake routes Since these take some time to create, it's often helpful to save the output to a file for quick reference. + +## Show obsolete `ignored_columns` + +To see a list of all obsolete `ignored_columns` run: + +``` +bundle exec rake db:obsolete_ignored_columns +``` + +Feel free to remove their definitions from their `ignored_columns` definitions. diff --git a/lib/gitlab/database/obsolete_ignored_columns.rb b/lib/gitlab/database/obsolete_ignored_columns.rb new file mode 100644 index 00000000000..6266b6a4b65 --- /dev/null +++ b/lib/gitlab/database/obsolete_ignored_columns.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Gitlab + module Database + # Checks which `ignored_columns` can be safely removed by scanning + # the current schema for all `ApplicationRecord` descendants. + class ObsoleteIgnoredColumns + def initialize(base = ApplicationRecord) + @base = base + end + + def execute + @base.descendants.map do |klass| + next if klass.abstract_class? + + safe_to_remove = ignored_columns_safe_to_remove_for(klass) + next if safe_to_remove.empty? + + [klass.name, safe_to_remove] + end.compact.sort_by(&:first) + end + + private + + def ignored_columns_safe_to_remove_for(klass) + ignored = klass.ignored_columns.map(&:to_s) + + return [] if ignored.empty? + + schema = klass.connection.schema_cache.columns_hash(klass.table_name) + existing = schema.values.map(&:name) + + used = ignored & existing + ignored - used + end + end + end +end diff --git a/lib/tasks/db_obsolete_ignored_columns.rake b/lib/tasks/db_obsolete_ignored_columns.rake new file mode 100644 index 00000000000..184e407f28c --- /dev/null +++ b/lib/tasks/db_obsolete_ignored_columns.rake @@ -0,0 +1,21 @@ +desc 'Show a list of obsolete `ignored_columns`' +task 'db:obsolete_ignored_columns' => :environment do + list = Gitlab::Database::ObsoleteIgnoredColumns.new.execute + + if list.empty? + puts 'No obsolete `ignored_columns` found.' + else + puts 'The following `ignored_columns` are obsolete and can be removed:' + + list.each do |name, ignored_columns| + puts "- #{name}: #{ignored_columns.join(', ')}" + end + + puts <<~TEXT + + WARNING: Removing columns is tricky because running GitLab processes may still be using the columns. + + See also https://docs.gitlab.com/ee/development/what_requires_downtime.html#dropping-columns + TEXT + end +end diff --git a/spec/lib/gitlab/database/obsolete_ignored_columns_spec.rb b/spec/lib/gitlab/database/obsolete_ignored_columns_spec.rb new file mode 100644 index 00000000000..6d38f7f1b95 --- /dev/null +++ b/spec/lib/gitlab/database/obsolete_ignored_columns_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Database::ObsoleteIgnoredColumns do + module Testing + class MyBase < ApplicationRecord + end + + class SomeAbstract < MyBase + self.abstract_class = true + + self.table_name = 'projects' + + self.ignored_columns += %i[unused] + end + + class B < MyBase + self.table_name = 'issues' + + self.ignored_columns += %i[id other] + end + + class A < SomeAbstract + self.ignored_columns += %i[id also_unused] + end + + class C < MyBase + self.table_name = 'users' + end + end + + subject { described_class.new(Testing::MyBase) } + + describe '#execute' do + it 'returns a list of class names and columns pairs' do + expect(subject.execute).to eq([ + ['Testing::A', %w(unused also_unused)], + ['Testing::B', %w(other)] + ]) + end + end +end