# frozen_string_literal: true require "active_record_doctor/detectors/base" module ActiveRecordDoctor module Detectors class UnindexedForeignKeys < Base # :nodoc: @description = "detect unindexed foreign keys" @config = { ignore_tables: { description: "tables whose foreign keys should not be checked", global: true }, ignore_columns: { description: "columns, written as table.column, that should not be checked" } } private def message(table:, columns:) # rubocop:disable Layout/LineLength "add an index on #{table}(#{columns.join(', ')}) - foreign keys are often used in database lookups and should be indexed for performance reasons" # rubocop:enable Layout/LineLength end def detect each_table(except: config(:ignore_tables)) do |table| each_column(table, except: config(:ignore_columns)) do |column| next unless named_like_foreign_key?(column) || foreign_key?(table, column) next if indexed?(table, column) next if indexed_as_polymorphic?(table, column) type_column_name = type_column_name(column) columns = if column_exists?(table, type_column_name) [type_column_name, column.name] else [column.name] end problem!(table: table, columns: columns) end end end def named_like_foreign_key?(column) column.name.end_with?("_id") end def foreign_key?(table, column) connection.foreign_keys(table).any? do |foreign_key| foreign_key.column == column.name end end def indexed?(table, column) connection.indexes(table).any? do |index| index.columns.first == column.name end end def indexed_as_polymorphic?(table, column) connection.indexes(table).any? do |index| index.columns[0, 2] == [type_column_name(column), column.name] end end def column_exists?(table, column_name) connection.columns(table).any? { |column| column.name == column_name } end def type_column_name(column) if column.name.end_with?("_id") column.name.sub(/_id\Z/, "_type") else "#{column.name}_type" end end end end end