class ActiveRecordDoctor::Detectors::Base

Base class for all active_record_doctor detectors.

Constants

BASE_CONFIG

Attributes

description[R]

Public Class Methods

config() click to toggle source
# File lib/active_record_doctor/detectors/base.rb, line 24
def config
  @config.merge(BASE_CONFIG)
end
locals_and_globals() click to toggle source
# File lib/active_record_doctor/detectors/base.rb, line 28
def locals_and_globals
  locals = []
  globals = []

  config.each do |key, metadata|
    locals << key
    globals << key if metadata[:global]
  end

  [locals, globals]
end
new(config:, logger:, io:) click to toggle source
# File lib/active_record_doctor/detectors/base.rb, line 41
def initialize(config:, logger:, io:)
  @problems = []
  @config = config
  @logger = logger
  @io = io
end
run(*args, **kwargs, &block) click to toggle source
# File lib/active_record_doctor/detectors/base.rb, line 16
def run(*args, **kwargs, &block)
  new(*args, **kwargs, &block).run
end
underscored_name() click to toggle source
# File lib/active_record_doctor/detectors/base.rb, line 20
def underscored_name
  name.demodulize.underscore.to_sym
end

Public Instance Methods

run() click to toggle source
# File lib/active_record_doctor/detectors/base.rb, line 48
def run
  log(underscored_name) do
    @problems = []

    if config(:enabled)
      detect
    else
      log("disabled; skipping")
    end

    @problems.each do |problem|
      @io.puts(message(**problem))
    end

    success = @problems.empty?
    if success
      log("No problems found")
    else
      log("Found #{@problems.count} problem(s)")
    end
    @problems = nil
    success
  end
end

Private Instance Methods

check_constraints(table_name) click to toggle source
# File lib/active_record_doctor/detectors/base.rb, line 138
      def check_constraints(table_name)
        # ActiveRecord 6.1+
        if connection.respond_to?(:supports_check_constraints?) && connection.supports_check_constraints?
          connection.check_constraints(table_name).select(&:validated?).map(&:expression)
        elsif Utils.postgresql?(connection)
          definitions =
            connection.select_values(<<-SQL)
              SELECT pg_get_constraintdef(oid, true)
              FROM pg_constraint
              WHERE contype = 'c'
                AND convalidated
                AND conrelid = #{connection.quote(table_name)}::regclass
            SQL

          definitions.map { |definition| definition[/CHECK \((.+)\)/m, 1] }
        else
          # We don't support this Rails/database combination yet.
          []
        end
      end
column(table_name, column_name) click to toggle source
# File lib/active_record_doctor/detectors/base.rb, line 127
def column(table_name, column_name)
  connection.columns(table_name).find { |column| column.name == column_name }
end
config(key) click to toggle source
# File lib/active_record_doctor/detectors/base.rb, line 75
def config(key)
  local = @config.detectors.fetch(underscored_name).fetch(key)
  return local if !self.class.config.fetch(key)[:global]

  global = @config.globals[key]
  return local if global.nil?

  # Right now, all globals are arrays so we can merge them here. Once
  # we add non-array globals we'll need to support per-global merging.
  Array.new(local).concat(global)
end
connection() click to toggle source
# File lib/active_record_doctor/detectors/base.rb, line 112
def connection
  @connection ||= ActiveRecord::Base.connection
end
detect() click to toggle source
# File lib/active_record_doctor/detectors/base.rb, line 87
def detect
  raise("#detect should be implemented by a subclass")
end
each_association(model, except: [], type: [:has_many, :has_one, :belongs_to], has_scope: nil, through: nil) { |association| ... } click to toggle source
# File lib/active_record_doctor/detectors/base.rb, line 294
def each_association(model, except: [], type: [:has_many, :has_one, :belongs_to], has_scope: nil, through: nil)
  type = Array(type)

  log("Iterating over associations on #{model.name}") do
    associations = type.map do |type1|
      # Skip inherited associations from STI to prevent them
      # from being reported multiple times on subclasses.
      model.reflect_on_all_associations(type1) - model.superclass.reflect_on_all_associations(type1)
    end.flatten

    associations.each do |association|
      case
      when except.include?("#{model.name}.#{association.name}")
        log("#{model.name}.#{association.name} - ignored via the configuration; skipping")
      when through && !association.is_a?(ActiveRecord::Reflection::ThroughReflection)
        log("#{model.name}.#{association.name} - is not a through association; skipping")
      when through == false && association.is_a?(ActiveRecord::Reflection::ThroughReflection)
        log("#{model.name}.#{association.name} - is a through association; skipping")
      when has_scope && association.scope.nil?
        log("#{model.name}.#{association.name} - doesn't have a scope; skipping")
      when has_scope == false && association.scope
        log("#{model.name}.#{association.name} - has a scope; skipping")
      else
        log("#{association.macro} :#{association.name}") do
          yield(association)
        end
      end
    end
  end
end
each_attribute(model, except: [], type: nil) { |column| ... } click to toggle source
# File lib/active_record_doctor/detectors/base.rb, line 216
def each_attribute(model, except: [], type: nil)
  log("Iterating over attributes of #{model.name}") do
    connection.columns(model.table_name).each do |column|
      case
      when except.include?("#{model.name}.#{column.name}")
        log("#{model.name}.#{column.name} - ignored via the configuration; skipping")
      when type && !Array(type).include?(column.type)
        log("#{model.name}.#{column.name} - ignored due to the #{column.type} type; skipping")
      else
        log("#{model.name}.#{column.name}") do
          yield(column)
        end
      end
    end
  end
end
each_column(table_name, only: nil, except: []) { |column| ... } click to toggle source
# File lib/active_record_doctor/detectors/base.rb, line 233
def each_column(table_name, only: nil, except: [])
  log("Iterating over columns of #{table_name}") do
    connection.columns(table_name).each do |column|
      case
      when except.include?("#{table_name}.#{column.name}")
        log("#{column.name} - ignored via the configuration; skipping")
      when only.nil? || only.include?(column.name)
        log(column.name.to_s) do
          yield(column)
        end
      end
    end
  end
end
each_data_source(except: []) { |data_source| ... } click to toggle source
# File lib/active_record_doctor/detectors/base.rb, line 280
def each_data_source(except: [])
  log("Iterating over data sources") do
    connection.data_sources.each do |data_source|
      if except.include?(data_source)
        log("#{data_source} - ignored via the configuration; skipping")
      else
        log(data_source) do
          yield(data_source)
        end
      end
    end
  end
end
each_foreign_key(table_name) { |foreign_key| ... } click to toggle source
# File lib/active_record_doctor/detectors/base.rb, line 248
def each_foreign_key(table_name)
  log("Iterating over foreign keys on #{table_name}") do
    connection.foreign_keys(table_name).each do |foreign_key|
      log("#{foreign_key.name} - #{foreign_key.from_table}(#{foreign_key.options[:column]}) to #{foreign_key.to_table}(#{foreign_key.options[:primary_key]})") do # rubocop:disable Layout/LineLength
        yield(foreign_key)
      end
    end
  end
end
each_index(table_name, except: [], multicolumn_only: false) { |index, indexes| ... } click to toggle source
# File lib/active_record_doctor/detectors/base.rb, line 190
def each_index(table_name, except: [], multicolumn_only: false)
  indexes = connection.indexes(table_name)

  message =
    if multicolumn_only
      "Iterating over multi-column indexes on #{table_name}"
    else
      "Iterating over indexes on #{table_name}"
    end

  log(message) do
    indexes.each do |index|
      case
      when except.include?(index.name)
        log("#{index.name} - ignored via the configuration; skipping")
      when multicolumn_only && !index.columns.is_a?(Array)
        log("#{index.name} - single-column index; skipping")
      else
        log("Index #{index.name} on #{table_name}") do
          yield(index, indexes)
        end
      end
    end
  end
end
each_model(except: [], abstract: nil, existing_tables_only: false) { |model| ... } click to toggle source
# File lib/active_record_doctor/detectors/base.rb, line 167
def each_model(except: [], abstract: nil, existing_tables_only: false)
  log("Iterating over Active Record models") do
    models.each do |model|
      case
      when model.name.start_with?("HABTM_")
        log("#{model.name} - has-belongs-to-many model; skipping")
      when except.include?(model.name)
        log("#{model.name} - ignored via the configuration; skipping")
      when abstract && !model.abstract_class?
        log("#{model.name} - non-abstract model; skipping")
      when abstract == false && model.abstract_class?
        log("#{model.name} - abstract model; skipping")
      when existing_tables_only && (model.table_name.nil? || !model.table_exists?)
        log("#{model.name} - backed by a non-existent table #{model.table_name}; skipping")
      else
        log(model.name) do
          yield(model)
        end
      end
    end
  end
end
each_table(except: []) { |table| ... } click to toggle source
# File lib/active_record_doctor/detectors/base.rb, line 258
def each_table(except: [])
  tables =
    if ActiveRecord::VERSION::STRING >= "5.1"
      connection.tables
    else
      connection.data_sources
    end

  log("Iterating over tables") do
    tables.each do |table|
      case
      when except.include?(table)
        log("#{table} - ignored via the configuration; skipping")
      else
        log(table) do
          yield(table)
        end
      end
    end
  end
end
indexes(table_name) click to toggle source
# File lib/active_record_doctor/detectors/base.rb, line 116
def indexes(table_name)
  connection.indexes(table_name)
end
log(message, &block) click to toggle source
# File lib/active_record_doctor/detectors/base.rb, line 95
def log(message, &block)
  @logger.log(message, &block)
end
message(**_attrs) click to toggle source
# File lib/active_record_doctor/detectors/base.rb, line 91
def message(**_attrs)
  raise("#message should be implemented by a subclass")
end
models() click to toggle source
# File lib/active_record_doctor/detectors/base.rb, line 159
def models
  ActiveRecord::Base.descendants
end
not_null_check_constraint_exists?(table, column) click to toggle source
# File lib/active_record_doctor/detectors/base.rb, line 131
def not_null_check_constraint_exists?(table, column)
  check_constraints(table).any? do |definition|
    definition =~ /\A#{column.name} IS NOT NULL\z/i ||
      definition =~ /\A#{connection.quote_column_name(column.name)} IS NOT NULL\z/i
  end
end
primary_key(table_name) click to toggle source
# File lib/active_record_doctor/detectors/base.rb, line 120
def primary_key(table_name)
  primary_key_name = connection.primary_key(table_name)
  return nil if primary_key_name.nil?

  column(table_name, primary_key_name)
end
problem!(**attrs) click to toggle source
# File lib/active_record_doctor/detectors/base.rb, line 99
def problem!(**attrs)
  log("Problem found") do
    attrs.each do |key, value|
      log("#{key}: #{value.inspect}")
    end
  end
  @problems << attrs
end
underscored_name() click to toggle source
# File lib/active_record_doctor/detectors/base.rb, line 163
def underscored_name
  self.class.underscored_name
end
warning(message) click to toggle source
# File lib/active_record_doctor/detectors/base.rb, line 108
def warning(message)
  puts(message)
end