active_record_doctor 1.8.0 → 1.9.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +246 -48
  3. data/lib/active_record_doctor/config/default.rb +59 -0
  4. data/lib/active_record_doctor/config/loader.rb +137 -0
  5. data/lib/active_record_doctor/config.rb +14 -0
  6. data/lib/active_record_doctor/detectors/base.rb +110 -19
  7. data/lib/active_record_doctor/detectors/extraneous_indexes.rb +63 -37
  8. data/lib/active_record_doctor/detectors/incorrect_boolean_presence_validation.rb +32 -23
  9. data/lib/active_record_doctor/detectors/incorrect_dependent_option.rb +70 -34
  10. data/lib/active_record_doctor/detectors/mismatched_foreign_key_type.rb +45 -0
  11. data/lib/active_record_doctor/detectors/missing_foreign_keys.rb +32 -23
  12. data/lib/active_record_doctor/detectors/missing_non_null_constraint.rb +40 -28
  13. data/lib/active_record_doctor/detectors/missing_presence_validation.rb +28 -21
  14. data/lib/active_record_doctor/detectors/missing_unique_indexes.rb +40 -30
  15. data/lib/active_record_doctor/detectors/short_primary_key_type.rb +41 -0
  16. data/lib/active_record_doctor/detectors/undefined_table_references.rb +19 -20
  17. data/lib/active_record_doctor/detectors/unindexed_deleted_at.rb +44 -18
  18. data/lib/active_record_doctor/detectors/unindexed_foreign_keys.rb +31 -20
  19. data/lib/active_record_doctor/detectors.rb +12 -4
  20. data/lib/active_record_doctor/errors.rb +226 -0
  21. data/lib/active_record_doctor/help.rb +39 -0
  22. data/lib/active_record_doctor/rake/task.rb +78 -0
  23. data/lib/active_record_doctor/runner.rb +41 -0
  24. data/lib/active_record_doctor/version.rb +1 -1
  25. data/lib/active_record_doctor.rb +7 -3
  26. data/lib/generators/active_record_doctor/add_indexes/add_indexes_generator.rb +34 -21
  27. data/lib/tasks/active_record_doctor.rake +9 -18
  28. data/test/active_record_doctor/config/loader_test.rb +120 -0
  29. data/test/active_record_doctor/config_test.rb +116 -0
  30. data/test/active_record_doctor/detectors/extraneous_indexes_test.rb +131 -8
  31. data/test/active_record_doctor/detectors/incorrect_boolean_presence_validation_test.rb +48 -5
  32. data/test/active_record_doctor/detectors/incorrect_dependent_option_test.rb +190 -12
  33. data/test/active_record_doctor/detectors/mismatched_foreign_key_type_test.rb +82 -0
  34. data/test/active_record_doctor/detectors/missing_foreign_keys_test.rb +50 -4
  35. data/test/active_record_doctor/detectors/missing_non_null_constraint_test.rb +138 -24
  36. data/test/active_record_doctor/detectors/missing_presence_validation_test.rb +74 -13
  37. data/test/active_record_doctor/detectors/missing_unique_indexes_test.rb +57 -8
  38. data/test/active_record_doctor/detectors/short_primary_key_type_test.rb +64 -0
  39. data/test/active_record_doctor/detectors/undefined_table_references_test.rb +34 -21
  40. data/test/active_record_doctor/detectors/unindexed_deleted_at_test.rb +112 -8
  41. data/test/active_record_doctor/detectors/unindexed_foreign_keys_test.rb +56 -4
  42. data/test/active_record_doctor/runner_test.rb +42 -0
  43. data/test/generators/active_record_doctor/add_indexes/add_indexes_generator_test.rb +131 -0
  44. data/test/model_factory.rb +73 -23
  45. data/test/setup.rb +62 -72
  46. metadata +40 -9
  47. data/lib/active_record_doctor/printers/io_printer.rb +0 -133
  48. data/lib/active_record_doctor/task.rb +0 -28
  49. data/test/active_record_doctor/printers/io_printer_test.rb +0 -33
@@ -5,33 +5,126 @@ module ActiveRecordDoctor
5
5
  # Base class for all active_record_doctor detectors.
6
6
  class Base
7
7
  class << self
8
- attr_reader :description
8
+ attr_reader :description, :config
9
9
 
10
- def run
11
- new.run
10
+ def run(config, io)
11
+ new(config, io).run
12
12
  end
13
+
14
+ def underscored_name
15
+ name.demodulize.underscore.to_sym
16
+ end
17
+
18
+ def locals_and_globals
19
+ locals = []
20
+ globals = []
21
+
22
+ config.each do |key, metadata|
23
+ locals << key
24
+ globals << key if metadata[:global]
25
+ end
26
+
27
+ [locals, globals]
28
+ end
29
+ end
30
+
31
+ def initialize(config, io)
32
+ @problems = []
33
+ @config = config
34
+ @io = io
35
+ end
36
+
37
+ def run
38
+ @problems = []
39
+
40
+ detect
41
+ @problems.each do |problem|
42
+ @io.puts(message(**problem))
43
+ end
44
+
45
+ success = @problems.empty?
46
+ @problems = nil
47
+ success
13
48
  end
14
49
 
15
50
  private
16
51
 
17
- def problems(problems, options = {})
18
- [problems, options]
52
+ def config(key)
53
+ local = @config.detectors.fetch(underscored_name).fetch(key)
54
+ return local if !self.class.config.fetch(key)[:global]
55
+
56
+ global = @config.globals[key]
57
+ return local if global.nil?
58
+
59
+ # Right now, all globals are arrays so we can merge them here. Once
60
+ # we add non-array globals we'll need to support per-global merging.
61
+ Array.new(local).concat(global)
62
+ end
63
+
64
+ def detect
65
+ raise("#detect should be implemented by a subclass")
66
+ end
67
+
68
+ def message(**_attrs)
69
+ raise("#message should be implemented by a subclass")
70
+ end
71
+
72
+ def problem!(**attrs)
73
+ @problems << attrs
74
+ end
75
+
76
+ def warning(message)
77
+ puts(message)
19
78
  end
20
79
 
21
80
  def connection
22
81
  @connection ||= ActiveRecord::Base.connection
23
82
  end
24
83
 
25
- def indexes(table_name)
26
- connection.indexes(table_name)
84
+ def indexes(table_name, except: [])
85
+ connection.indexes(table_name).reject do |index|
86
+ except.include?(index.name)
87
+ end
27
88
  end
28
89
 
29
- def tables
30
- connection.tables
90
+ def tables(except: [])
91
+ tables =
92
+ if ActiveRecord::VERSION::STRING >= "5.1"
93
+ connection.tables
94
+ else
95
+ connection.data_sources
96
+ end
97
+
98
+ tables.reject do |table|
99
+ except.include?(table)
100
+ end
31
101
  end
32
102
 
33
103
  def table_exists?(table_name)
34
- connection.table_exists?(table_name)
104
+ if ActiveRecord::VERSION::STRING >= "5.1"
105
+ connection.table_exists?(table_name)
106
+ else
107
+ connection.data_source_exists?(table_name)
108
+ end
109
+ end
110
+
111
+ def tables_and_views
112
+ if connection.respond_to?(:data_sources)
113
+ connection.data_sources
114
+ else
115
+ connection.tables
116
+ end
117
+ end
118
+
119
+ def primary_key(table_name)
120
+ primary_key_name = connection.primary_key(table_name)
121
+ return nil if primary_key_name.nil?
122
+
123
+ column(table_name, primary_key_name)
124
+ end
125
+
126
+ def column(table_name, column_name)
127
+ connection.columns(table_name).find { |column| column.name == column_name }
35
128
  end
36
129
 
37
130
  def views
@@ -42,22 +135,20 @@ module ActiveRecordDoctor
42
135
  ActiveRecord::Base.connection.execute(<<-SQL).map { |tuple| tuple.fetch("relname") }
43
136
  SELECT c.relname FROM pg_class c WHERE c.relkind IN ('m', 'v')
44
137
  SQL
45
- else # rubocop:disable Style/EmptyElse
138
+ else
46
139
  # We don't support this Rails/database combination yet.
47
140
  nil
48
141
  end
49
142
  end
50
143
 
51
- def hash_from_pairs(pairs)
52
- Hash[*pairs.flatten(1)]
53
- end
54
-
55
- def eager_load!
56
- Rails.application.eager_load!
144
+ def models(except: [])
145
+ ActiveRecord::Base.descendants.reject do |model|
146
+ model.name.start_with?("HABTM_") || except.include?(model.name)
147
+ end
57
148
  end
58
149
 
59
- def models
60
- ActiveRecord::Base.descendants
150
+ def underscored_name
151
+ self.class.underscored_name
61
152
  end
62
153
  end
63
154
  end
@@ -4,55 +4,60 @@ require "active_record_doctor/detectors/base"
4
4
 
5
5
  module ActiveRecordDoctor
6
6
  module Detectors
7
- # Detect indexes whose function can be overtaken by other indexes. For example, an index on columns A, B, and C
8
- # can also serve as an index on A and A, B.
9
- class ExtraneousIndexes < Base
10
- @description = "Detect extraneous indexes"
7
+ class ExtraneousIndexes < Base # :nodoc:
8
+ @description = "identify indexes that can be dropped without degrading performance"
9
+ @config = {
10
+ ignore_tables: {
11
+ description: "tables whose indexes should never be reported as extraneous",
12
+ global: true
13
+ },
14
+ ignore_indexes: {
15
+ description: "indexes that should never be reported as extraneous",
16
+ global: true
17
+ }
18
+ }
11
19
 
12
- def run
13
- problems(subindexes_of_multi_column_indexes + indexed_primary_keys)
20
+ private
21
+
22
+ def message(extraneous_index:, replacement_indexes:)
23
+ if replacement_indexes.nil?
24
+ "remove #{extraneous_index} - coincides with the primary key on the table"
25
+ else
26
+ "remove #{extraneous_index} - can be replaced by #{replacement_indexes.join(' or ')}"
27
+ end
14
28
  end
15
29
 
16
- private
30
+ def detect
31
+ subindexes_of_multi_column_indexes
32
+ indexed_primary_keys
33
+ end
17
34
 
18
35
  def subindexes_of_multi_column_indexes
19
- tables.reject do |table|
20
- table == "schema_migrations"
21
- end.flat_map do |table|
36
+ tables(except: config(:ignore_tables)).each do |table|
22
37
  indexes = indexes(table)
23
- maximum_indexes = indexes.select do |index|
24
- maximal?(indexes, index)
25
- end
38
+ maximal_indexes = indexes.select { |index| maximal?(indexes, index) }
39
+
40
+ indexes.each do |index|
41
+ next if maximal_indexes.include?(index)
42
+
43
+ replacement_indexes = maximal_indexes.select do |maximum_index|
44
+ cover?(maximum_index, index)
45
+ end.map(&:name).sort
46
+
47
+ next if config(:ignore_indexes).include?(index.name)
26
48
 
27
- indexes.reject do |index|
28
- maximum_indexes.include?(index)
29
- end.map do |extraneous_index|
30
- [
31
- extraneous_index.name,
32
- [
33
- :multi_column,
34
- maximum_indexes.select do |maximum_index|
35
- cover?(maximum_index, extraneous_index)
36
- end.map(&:name).sort
37
- ].flatten(1)
38
- ]
49
+ problem!(extraneous_index: index.name, replacement_indexes: replacement_indexes)
39
50
  end
40
51
  end
41
52
  end
42
53
 
43
54
  def indexed_primary_keys
44
- @indexed_primary_keys ||= tables.reject do |table|
45
- table == "schema_migrations"
46
- end.map do |table|
47
- [
48
- table,
49
- indexes(table).select do |index|
50
- index.columns == ["id"]
51
- end
52
- ]
53
- end.flat_map do |table, indexes|
54
- indexes.map do |index|
55
- [index.name, [:primary_key, table]]
55
+ tables(except: config(:ignore_tables)).each do |table|
56
+ indexes(table).each do |index|
57
+ next if config(:ignore_indexes).include?(index.name)
58
+ next if index.columns != ["id"]
59
+
60
+ problem!(extraneous_index: index.name, replacement_indexes: nil)
56
61
  end
57
62
  end
58
63
  end
@@ -65,6 +70,8 @@ module ActiveRecordDoctor
65
70
 
66
71
  # Does lhs cover rhs?
67
72
  def cover?(lhs, rhs)
73
+ return false unless compatible_options?(lhs, rhs)
74
+
68
75
  case [lhs.unique, rhs.unique]
69
76
  when [true, true]
70
77
  lhs.columns == rhs.columns
@@ -83,6 +90,25 @@ module ActiveRecordDoctor
83
90
  def indexes(table_name)
84
91
  super.select { |index| index.columns.is_a?(Array) }
85
92
  end
93
+
94
+ def compatible_options?(lhs, rhs)
95
+ lhs.type == rhs.type &&
96
+ lhs.using == rhs.using &&
97
+ lhs.where == rhs.where &&
98
+ same_opclasses?(lhs, rhs)
99
+ end
100
+
101
+ def same_opclasses?(lhs, rhs)
102
+ if ActiveRecord::VERSION::STRING >= "5.2"
103
+ rhs.columns.all? do |column|
104
+ lhs_opclass = lhs.opclasses.is_a?(Hash) ? lhs.opclasses[column] : lhs.opclasses
105
+ rhs_opclass = rhs.opclasses.is_a?(Hash) ? rhs.opclasses[column] : rhs.opclasses
106
+ lhs_opclass == rhs_opclass
107
+ end
108
+ else
109
+ true
110
+ end
111
+ end
86
112
  end
87
113
  end
88
114
  end
@@ -4,32 +4,41 @@ require "active_record_doctor/detectors/base"
4
4
 
5
5
  module ActiveRecordDoctor
6
6
  module Detectors
7
- # Find instances of boolean column presence validations that use presence/absence instead of includes/excludes.
8
- class IncorrectBooleanPresenceValidation < Base
9
- @description = "Detect boolean columns with presence/absence instead of includes/excludes validators"
10
-
11
- def run
12
- eager_load!
13
-
14
- problems(hash_from_pairs(models.reject do |model|
15
- model.table_name.nil? ||
16
- model.table_name == "schema_migrations" ||
17
- !table_exists?(model.table_name)
18
- end.map do |model|
19
- [
20
- model.name,
21
- connection.columns(model.table_name).select do |column|
22
- column.type == :boolean &&
23
- has_presence_validator?(model, column)
24
- end.map(&:name)
25
- ]
26
- end.reject do |_model_name, columns|
27
- columns.empty?
28
- end))
29
- end
7
+ class IncorrectBooleanPresenceValidation < Base # :nodoc:
8
+ @description = "detect persence (instead of inclusion) validators on boolean columns"
9
+ @config = {
10
+ ignore_models: {
11
+ description: "models whose validators should not be checked",
12
+ global: true
13
+ },
14
+ ignore_attributes: {
15
+ description: "attributes, written as Model.attribute, whose validators should not be checked"
16
+ }
17
+ }
30
18
 
31
19
  private
32
20
 
21
+ def message(model:, attribute:)
22
+ # rubocop:disable Layout/LineLength
23
+ "replace the `presence` validator on #{model}.#{attribute} with `inclusion` - `presence` can't be used on booleans"
24
+ # rubocop:enable Layout/LineLength
25
+ end
26
+
27
+ def detect
28
+ models(except: config(:ignore_models)).each do |model|
29
+ next if model.table_name.nil?
30
+ next unless table_exists?(model.table_name)
31
+
32
+ connection.columns(model.table_name).each do |column|
33
+ next if config(:ignore_attributes).include?("#{model.name}.#{column.name}")
34
+ next unless column.type == :boolean
35
+ next unless has_presence_validator?(model, column)
36
+
37
+ problem!(model: model.name, attribute: column.name)
38
+ end
39
+ end
40
+ end
41
+
33
42
  def has_presence_validator?(model, column)
34
43
  model.validators.any? do |validator|
35
44
  validator.kind == :presence && validator.attributes.include?(column.name.to_sym)
@@ -4,55 +4,79 @@ require "active_record_doctor/detectors/base"
4
4
 
5
5
  module ActiveRecordDoctor
6
6
  module Detectors
7
- # Find has_many/has_one associations with dependent options not taking the
8
- # related model's callbacks into account.
9
- class IncorrectDependentOption < Base
10
- # rubocop:disable Layout/LineLength
11
- @description = "Detect associations that should use a different dependent option based on callbacks on the related model"
12
- # rubocop:enable Layout/LineLength
7
+ class IncorrectDependentOption < Base # :nodoc:
8
+ @description = "detect associations with incorrect dependent options"
9
+ @config = {
10
+ ignore_models: {
11
+ description: "models whose associations should not be checked",
12
+ global: true
13
+ },
14
+ ignore_associations: {
15
+ description: "associations, written as Model.association, that should not be checked"
16
+ }
17
+ }
13
18
 
14
- def run
15
- eager_load!
19
+ private
16
20
 
17
- problems(hash_from_pairs(models.reject do |model|
18
- model.table_name.nil?
19
- end.map do |model|
20
- [
21
- model.name,
22
- associations_with_incorrect_dependent_options(model)
23
- ]
24
- end.reject do |_model_name, associations|
25
- associations.empty?
26
- end))
21
+ def message(model:, association:, problem:)
22
+ # rubocop:disable Layout/LineLength
23
+ case problem
24
+ when :suggest_destroy
25
+ "use `dependent: :destroy` or similar on #{model}.#{association} - the associated model has callbacks that are currently skipped"
26
+ when :suggest_delete
27
+ "use `dependent: :delete` or similar on #{model}.#{association} - the associated model has no callbacks and can be deleted without loading"
28
+ when :suggest_delete_all
29
+ "use `dependent: :delete_all` or similar on #{model}.#{association} - associated models have no validations and can be deleted in bulk"
30
+ end
31
+ # rubocop:enable Layout/LineLength
27
32
  end
28
33
 
29
- private
34
+ def detect
35
+ models(except: config(:ignore_models)).each do |model|
36
+ next if model.table_name.nil?
37
+
38
+ associations = model.reflect_on_all_associations(:has_many) +
39
+ model.reflect_on_all_associations(:has_one) +
40
+ model.reflect_on_all_associations(:belongs_to)
30
41
 
31
- def associations_with_incorrect_dependent_options(model)
32
- reflections = model.reflect_on_all_associations(:has_many) + model.reflect_on_all_associations(:has_one)
33
- reflections.map do |reflection|
34
- if callback_action(reflection) == :invoke && !defines_destroy_callbacks?(reflection.klass)
35
- suggestion =
36
- case reflection.macro
37
- when :has_many then :suggest_delete_all
38
- when :has_one then :suggest_delete
39
- else raise("unsupported association type #{reflection.macro}")
40
- end
42
+ associations.each do |association|
43
+ next if config(:ignore_associations).include?("#{model.name}.#{association.name}")
41
44
 
42
- [reflection.name, suggestion]
43
- elsif callback_action(reflection) == :skip && defines_destroy_callbacks?(reflection.klass)
44
- [reflection.name, :suggest_destroy]
45
+ if callback_action(association) == :invoke && deletable?(association.klass)
46
+ suggestion =
47
+ case association.macro
48
+ when :has_many then :suggest_delete_all
49
+ when :has_one, :belongs_to then :suggest_delete
50
+ else raise("unsupported association type #{association.macro}")
51
+ end
52
+
53
+ problem!(model: model.name, association: association.name, problem: suggestion)
54
+ elsif callback_action(association) == :skip && !deletable?(association.klass)
55
+ problem!(model: model.name, association: association.name, problem: :suggest_destroy)
56
+ end
45
57
  end
46
- end.compact
58
+ end
47
59
  end
48
60
 
49
61
  def callback_action(reflection)
50
62
  case reflection.options[:dependent]
51
- when :delete_all then :skip
63
+ when :delete, :delete_all then :skip
52
64
  when :destroy then :invoke
53
65
  end
54
66
  end
55
67
 
68
+ def deletable?(model)
69
+ !defines_destroy_callbacks?(model) &&
70
+ dependent_models(model).all? do |dependent_model|
71
+ foreign_key = foreign_key(dependent_model.table_name, model.table_name)
72
+
73
+ foreign_key.nil? ||
74
+ foreign_key.on_delete == :nullify || (
75
+ foreign_key.on_delete == :cascade && deletable?(dependent_model)
76
+ )
77
+ end
78
+ end
79
+
56
80
  def defines_destroy_callbacks?(model)
57
81
  # Destroying an associated model involves loading it first hence
58
82
  # initialize and find are present. If they are defined on the model
@@ -66,6 +90,18 @@ module ActiveRecordDoctor
66
90
  model._commit_callbacks.present? ||
67
91
  model._rollback_callbacks.present?
68
92
  end
93
+
94
+ def dependent_models(model)
95
+ reflections = model.reflect_on_all_associations(:has_many) +
96
+ model.reflect_on_all_associations(:has_one)
97
+ reflections.map(&:klass)
98
+ end
99
+
100
+ def foreign_key(from_table, to_table)
101
+ connection.foreign_keys(from_table).find do |foreign_key|
102
+ foreign_key.to_table == to_table
103
+ end
104
+ end
69
105
  end
70
106
  end
71
107
  end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record_doctor/detectors/base"
4
+
5
+ module ActiveRecordDoctor
6
+ module Detectors
7
+ class MismatchedForeignKeyType < Base # :nodoc:
8
+ @description = "detect foreign key type mismatches"
9
+ @config = {
10
+ ignore_tables: {
11
+ description: "tables whose foreign keys should not be checked",
12
+ global: true
13
+ },
14
+ ignore_columns: {
15
+ description: "foreign keys, written as table.column, that should not be checked"
16
+ }
17
+ }
18
+
19
+ private
20
+
21
+ def message(table:, column:)
22
+ # rubocop:disable Layout/LineLength
23
+ "#{table}.#{column} references a column of different type - foreign keys should be of the same type as the referenced column"
24
+ # rubocop:enable Layout/LineLength
25
+ end
26
+
27
+ def detect
28
+ tables(except: config(:ignore_tables)).each do |table|
29
+ connection.foreign_keys(table).each do |foreign_key|
30
+ from_column = column(table, foreign_key.column)
31
+
32
+ next if config(:ignore_columns).include?("#{table}.#{from_column.name}")
33
+
34
+ to_table = foreign_key.to_table
35
+ primary_key = primary_key(to_table)
36
+
37
+ next if from_column.sql_type == primary_key.sql_type
38
+
39
+ problem!(table: table, column: from_column.name)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -4,32 +4,41 @@ require "active_record_doctor/detectors/base"
4
4
 
5
5
  module ActiveRecordDoctor
6
6
  module Detectors
7
- # Find foreign-key like columns lacking an actual foreign key constraint.
8
- class MissingForeignKeys < Base
9
- @description = "Detect association columns without a foreign key constraint"
10
-
11
- def run
12
- problems(hash_from_pairs(tables.reject do |table|
13
- table == "schema_migrations"
14
- end.map do |table|
15
- [
16
- table,
17
- connection.columns(table).select do |column|
18
- # We need to skip polymorphic associations as they can reference
19
- # multiple tables but a foreign key constraint can reference
20
- # a single predefined table.
21
- named_like_foreign_key?(column) &&
22
- !foreign_key?(table, column) &&
23
- !polymorphic_foreign_key?(table, column)
24
- end.map(&:name)
25
- ]
26
- end.reject do |_table, columns|
27
- columns.empty?
28
- end))
29
- end
7
+ class MissingForeignKeys < Base # :nodoc:
8
+ @description = "detect foreign-key-like columns lacking an actual foreign key constraint"
9
+ @config = {
10
+ ignore_tables: {
11
+ description: "tables whose columns should not be checked",
12
+ global: true
13
+ },
14
+ ignore_columns: {
15
+ description: "columns, written as table.column, that should not be checked"
16
+ }
17
+ }
30
18
 
31
19
  private
32
20
 
21
+ def message(table:, column:)
22
+ "create a foreign key on #{table}.#{column} - looks like an association without a foreign key constraint"
23
+ end
24
+
25
+ def detect
26
+ tables(except: config(:ignore_tables)).each do |table|
27
+ connection.columns(table).each do |column|
28
+ next if config(:ignore_columns).include?("#{table}.#{column.name}")
29
+
30
+ # We need to skip polymorphic associations as they can reference
31
+ # multiple tables but a foreign key constraint can reference
32
+ # a single predefined table.
33
+ next unless named_like_foreign_key?(column)
34
+ next if foreign_key?(table, column)
35
+ next if polymorphic_foreign_key?(table, column)
36
+
37
+ problem!(table: table, column: column.name)
38
+ end
39
+ end
40
+ end
41
+
33
42
  def named_like_foreign_key?(column)
34
43
  column.name.end_with?("_id")
35
44
  end