require 'foreign_key_checker/utils' module ForeignKeyChecker::Checkers::Relations class Result attr_reader :ok, :model, :association, :fk, :error, :table def initialize(**args) %i[model association ok error fk table].each do |key| instance_variable_set("@#{key}", args[key]) end end end class JoinResult < Result def message "expect { #{model.to_s}.joins(:#{association.name}).first }.to_not raise_exception" if !ok end end class ErrorResult < Result def message "`#{model.to_s}##{association.name}` is broken\n#{error.class.to_s}\n#{error.message}\n#{error.backtrace.join("\n")}" if !ok end end class HasOneOrManyResult < Result def message "expected has_many or has_one association for #{fk.inspect}" end end class HasOneOrManyDependentResult < Result def message "expected has_many or has_one association with dependent option for #{fk.inspect}" end end class NoModelResult < Result def message "expected find model for table #{table}" end end def self.check_by_join Rails.application.eager_load! models = ActiveRecord::Base.descendants models.each_with_object([]) do |model, results| next if model.to_s.include?('HABTM') model.reflect_on_all_associations.each do |association| begin next if association.options[:polymorphic] next if association.scope && association.scope.is_a?(Proc) && association.scope.arity > 0 next if model.connection_specification_name != association.klass.connection_specification_name model.joins(association.name).first result = JoinResult.new(model: model, association: association, ok: true) yield result if block_given? results << result rescue => e result = JoinResult.new(model: model, association: association, ok: false, error: e) yield result if block_given? results << result end end end end def self.check_by_fks Rails.application.eager_load! models = ActiveRecord::Base.descendants.group_by(&:table_name) check_relation = proc do |model, fk, association| begin pk = association.options[:primary_key] || model.primary_key association.klass.table_name.to_s == fk.from_table.to_s && association.foreign_key.to_s == fk.from_column.to_s && pk.to_s == fk.to_column.to_s rescue => e if block_given? yield ErrorResult.new(model: model, association: association, fk: fk, ok: false, error: e) else p model p fk p association raise e end end end ForeignKeyChecker::Utils.get_foreign_keys_hash.each do |to_table, fks| fks.each do |fk| unless models.key?(fk.to_table) result = NoModelResult.new(ok: false, table: fk.to_table) if block_given? yield result else raise result.message end next end models[fk.to_table].each do |model| next if model.connection_specification_name != 'primary' ok = false ok ||= !!model.reflect_on_all_associations(:has_many).find do |association| check_relation.call(model, fk, association) end ok ||= !!model.reflect_on_all_associations(:has_one).find do |association| check_relation.call(model, fk, association) end if block_given? yield HasOneOrManyResult.new(model: model, fk: fk, ok: ok) else raise "expected has_many or has_one association for #{fk.inspect}" if !ok end next if !ok dep = false dep ||= !!model.reflect_on_all_associations(:has_many).find do |association| check_relation.call(model, fk, association) && association.options[:dependent] end dep ||= !!model.reflect_on_all_associations(:has_one).find do |association| check_relation.call(model, fk, association) && association.options[:dependent] end if block_given? yield HasOneOrManyDependentResult.new(model: model, fk: fk, ok: ok) else raise "expected has_many or has_one association with dependent option for #{fk.inspect}" if !dep end end end end end end