active_record_doctor 1.10.0 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/lib/active_record_doctor/detectors/base.rb +180 -50
- data/lib/active_record_doctor/detectors/extraneous_indexes.rb +24 -27
- data/lib/active_record_doctor/detectors/incorrect_boolean_presence_validation.rb +2 -5
- data/lib/active_record_doctor/detectors/incorrect_dependent_option.rb +63 -21
- data/lib/active_record_doctor/detectors/incorrect_length_validation.rb +7 -10
- data/lib/active_record_doctor/detectors/mismatched_foreign_key_type.rb +16 -9
- data/lib/active_record_doctor/detectors/missing_foreign_keys.rb +2 -4
- data/lib/active_record_doctor/detectors/missing_non_null_constraint.rb +13 -11
- data/lib/active_record_doctor/detectors/missing_presence_validation.rb +14 -7
- data/lib/active_record_doctor/detectors/missing_unique_indexes.rb +5 -11
- data/lib/active_record_doctor/detectors/short_primary_key_type.rb +1 -1
- data/lib/active_record_doctor/detectors/undefined_table_references.rb +2 -2
- data/lib/active_record_doctor/detectors/unindexed_deleted_at.rb +5 -13
- data/lib/active_record_doctor/detectors/unindexed_foreign_keys.rb +2 -4
- data/lib/active_record_doctor/logger/dummy.rb +11 -0
- data/lib/active_record_doctor/logger/hierarchical.rb +22 -0
- data/lib/active_record_doctor/logger.rb +6 -0
- data/lib/active_record_doctor/rake/task.rb +10 -1
- data/lib/active_record_doctor/runner.rb +8 -3
- data/lib/active_record_doctor/version.rb +1 -1
- data/lib/active_record_doctor.rb +3 -0
- data/test/active_record_doctor/detectors/disable_test.rb +1 -1
- data/test/active_record_doctor/detectors/incorrect_boolean_presence_validation_test.rb +7 -7
- data/test/active_record_doctor/detectors/incorrect_dependent_option_test.rb +136 -57
- data/test/active_record_doctor/detectors/incorrect_length_validation_test.rb +16 -14
- data/test/active_record_doctor/detectors/mismatched_foreign_key_type_test.rb +35 -1
- data/test/active_record_doctor/detectors/missing_non_null_constraint_test.rb +46 -23
- data/test/active_record_doctor/detectors/missing_presence_validation_test.rb +55 -27
- data/test/active_record_doctor/detectors/missing_unique_indexes_test.rb +36 -36
- data/test/active_record_doctor/detectors/undefined_table_references_test.rb +11 -13
- data/test/active_record_doctor/runner_test.rb +18 -19
- data/test/setup.rb +10 -6
- metadata +19 -4
- data/test/model_factory.rb +0 -128
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fef82b1493488683e11bc72273e142479cb47234651a8aa772a83121960f9309
|
4
|
+
data.tar.gz: c99afc25d8307ff0814e0459790f3d7fcf0b015d9f37f118870a9a1cfa2cf460
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ab75ff192c31cfc10cbd5c4106ca0033cc4d408c1e4977a417f76c53f2a2e73e37e5706162cd4807c700808e171756ddda022af6658b60f460d78109f9bea2ee
|
7
|
+
data.tar.gz: 075625a08ac5f192a68ed6c99409a4acf85cadb77ec117d6933186c2bf4d07e5aaf08928e9f30d5dd96b263e8e530a4a4da848ef5a4dfc82d5dae0df2475f7d2
|
data/README.md
CHANGED
@@ -509,7 +509,7 @@ Supported configuration options:
|
|
509
509
|
|
510
510
|
- `enabled` - set to `false` to disable the detector altogether
|
511
511
|
- `ignore_models` - models whose associations should not be checked.
|
512
|
-
- `
|
512
|
+
- `ignore_associations` - associations, written as Model.association, that should not
|
513
513
|
be checked.
|
514
514
|
|
515
515
|
### Detecting Primary Keys Having Short Integer Types
|
@@ -13,8 +13,8 @@ module ActiveRecordDoctor
|
|
13
13
|
class << self
|
14
14
|
attr_reader :description
|
15
15
|
|
16
|
-
def run(
|
17
|
-
new(
|
16
|
+
def run(*args, **kwargs, &block)
|
17
|
+
new(*args, **kwargs, &block).run
|
18
18
|
end
|
19
19
|
|
20
20
|
def underscored_name
|
@@ -38,23 +38,36 @@ module ActiveRecordDoctor
|
|
38
38
|
end
|
39
39
|
end
|
40
40
|
|
41
|
-
def initialize(config
|
41
|
+
def initialize(config:, logger:, io:)
|
42
42
|
@problems = []
|
43
43
|
@config = config
|
44
|
+
@logger = logger
|
44
45
|
@io = io
|
45
46
|
end
|
46
47
|
|
47
48
|
def run
|
48
|
-
|
49
|
+
log(underscored_name) do
|
50
|
+
@problems = []
|
49
51
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
52
|
+
if config(:enabled)
|
53
|
+
detect
|
54
|
+
else
|
55
|
+
log("disabled; skipping")
|
56
|
+
end
|
57
|
+
|
58
|
+
@problems.each do |problem|
|
59
|
+
@io.puts(message(**problem))
|
60
|
+
end
|
54
61
|
|
55
|
-
|
56
|
-
|
57
|
-
|
62
|
+
success = @problems.empty?
|
63
|
+
if success
|
64
|
+
log("No problems found")
|
65
|
+
else
|
66
|
+
log("Found #{@problems.count} problem(s)")
|
67
|
+
end
|
68
|
+
@problems = nil
|
69
|
+
success
|
70
|
+
end
|
58
71
|
end
|
59
72
|
|
60
73
|
private
|
@@ -79,7 +92,16 @@ module ActiveRecordDoctor
|
|
79
92
|
raise("#message should be implemented by a subclass")
|
80
93
|
end
|
81
94
|
|
95
|
+
def log(message, &block)
|
96
|
+
@logger.log(message, &block)
|
97
|
+
end
|
98
|
+
|
82
99
|
def problem!(**attrs)
|
100
|
+
log("Problem found") do
|
101
|
+
attrs.each do |key, value|
|
102
|
+
log("#{key}: #{value.inspect}")
|
103
|
+
end
|
104
|
+
end
|
83
105
|
@problems << attrs
|
84
106
|
end
|
85
107
|
|
@@ -91,23 +113,8 @@ module ActiveRecordDoctor
|
|
91
113
|
@connection ||= ActiveRecord::Base.connection
|
92
114
|
end
|
93
115
|
|
94
|
-
def indexes(table_name
|
95
|
-
connection.indexes(table_name)
|
96
|
-
except.include?(index.name)
|
97
|
-
end
|
98
|
-
end
|
99
|
-
|
100
|
-
def tables(except: [])
|
101
|
-
tables =
|
102
|
-
if ActiveRecord::VERSION::STRING >= "5.1"
|
103
|
-
connection.tables
|
104
|
-
else
|
105
|
-
connection.data_sources
|
106
|
-
end
|
107
|
-
|
108
|
-
tables.reject do |table|
|
109
|
-
except.include?(table)
|
110
|
-
end
|
116
|
+
def indexes(table_name)
|
117
|
+
connection.indexes(table_name)
|
111
118
|
end
|
112
119
|
|
113
120
|
def primary_key(table_name)
|
@@ -121,24 +128,6 @@ module ActiveRecordDoctor
|
|
121
128
|
connection.columns(table_name).find { |column| column.name == column_name }
|
122
129
|
end
|
123
130
|
|
124
|
-
def views
|
125
|
-
@views ||=
|
126
|
-
if connection.respond_to?(:views)
|
127
|
-
connection.views
|
128
|
-
elsif postgresql?
|
129
|
-
ActiveRecord::Base.connection.select_values(<<-SQL)
|
130
|
-
SELECT relname FROM pg_class WHERE relkind IN ('m', 'v')
|
131
|
-
SQL
|
132
|
-
elsif connection.adapter_name == "Mysql2"
|
133
|
-
ActiveRecord::Base.connection.select_values(<<-SQL)
|
134
|
-
SHOW FULL TABLES WHERE table_type = 'VIEW'
|
135
|
-
SQL
|
136
|
-
else
|
137
|
-
# We don't support this Rails/database combination yet.
|
138
|
-
[]
|
139
|
-
end
|
140
|
-
end
|
141
|
-
|
142
131
|
def not_null_check_constraint_exists?(table, column)
|
143
132
|
check_constraints(table).any? do |definition|
|
144
133
|
definition =~ /\A#{column.name} IS NOT NULL\z/i ||
|
@@ -167,10 +156,8 @@ module ActiveRecordDoctor
|
|
167
156
|
end
|
168
157
|
end
|
169
158
|
|
170
|
-
def models
|
171
|
-
ActiveRecord::Base.descendants
|
172
|
-
model.name.start_with?("HABTM_") || except.include?(model.name)
|
173
|
-
end
|
159
|
+
def models
|
160
|
+
ActiveRecord::Base.descendants
|
174
161
|
end
|
175
162
|
|
176
163
|
def underscored_name
|
@@ -180,6 +167,149 @@ module ActiveRecordDoctor
|
|
180
167
|
def postgresql?
|
181
168
|
["PostgreSQL", "PostGIS"].include?(connection.adapter_name)
|
182
169
|
end
|
170
|
+
|
171
|
+
def each_model(except: [], abstract: nil, existing_tables_only: false)
|
172
|
+
log("Iterating over Active Record models") do
|
173
|
+
models.each do |model|
|
174
|
+
case
|
175
|
+
when model.name.start_with?("HABTM_")
|
176
|
+
log("#{model.name} - has-belongs-to-many model; skipping")
|
177
|
+
when except.include?(model.name)
|
178
|
+
log("#{model.name} - ignored via the configuration; skipping")
|
179
|
+
when abstract && !model.abstract_class?
|
180
|
+
log("#{model.name} - non-abstract model; skipping")
|
181
|
+
when abstract == false && model.abstract_class?
|
182
|
+
log("#{model.name} - abstract model; skipping")
|
183
|
+
when existing_tables_only && (model.table_name.nil? || !model.table_exists?)
|
184
|
+
log("#{model.name} - backed by a non-existent table #{model.table_name}; skipping")
|
185
|
+
else
|
186
|
+
log(model.name) do
|
187
|
+
yield(model)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
def each_index(table_name, except: [], multicolumn_only: false)
|
195
|
+
indexes = connection.indexes(table_name)
|
196
|
+
|
197
|
+
message =
|
198
|
+
if multicolumn_only
|
199
|
+
"Iterating over multi-column indexes on #{table_name}"
|
200
|
+
else
|
201
|
+
"Iterating over indexes on #{table_name}"
|
202
|
+
end
|
203
|
+
|
204
|
+
log(message) do
|
205
|
+
indexes.each do |index|
|
206
|
+
case
|
207
|
+
when except.include?(index.name)
|
208
|
+
log("#{index.name} - ignored via the configuration; skipping")
|
209
|
+
when multicolumn_only && !index.columns.is_a?(Array)
|
210
|
+
log("#{index.name} - single-column index; skipping")
|
211
|
+
else
|
212
|
+
log("Index #{index.name} on #{table_name}") do
|
213
|
+
yield(index, indexes)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
def each_attribute(model, except: [], type: nil)
|
221
|
+
log("Iterating over attributes of #{model.name}") do
|
222
|
+
connection.columns(model.table_name).each do |column|
|
223
|
+
case
|
224
|
+
when except.include?("#{model.name}.#{column.name}")
|
225
|
+
log("#{model.name}.#{column.name} - ignored via the configuration; skipping")
|
226
|
+
when type && !Array(type).include?(column.type)
|
227
|
+
log("#{model.name}.#{column.name} - ignored due to the #{column.type} type; skipping")
|
228
|
+
else
|
229
|
+
log("#{model.name}.#{column.name}") do
|
230
|
+
yield(column)
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
def each_column(table_name, only: nil, except: [])
|
238
|
+
log("Iterating over columns of #{table_name}") do
|
239
|
+
connection.columns(table_name).each do |column|
|
240
|
+
case
|
241
|
+
when except.include?("#{table_name}.#{column.name}")
|
242
|
+
log("#{column.name} - ignored via the configuration; skipping")
|
243
|
+
when only.nil? || only.include?(column.name)
|
244
|
+
log(column.name.to_s) do
|
245
|
+
yield(column)
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
def each_foreign_key(table_name)
|
253
|
+
log("Iterating over foreign keys on #{table_name}") do
|
254
|
+
connection.foreign_keys(table_name).each do |foreign_key|
|
255
|
+
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
|
256
|
+
yield(foreign_key)
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
def each_table(except: [])
|
263
|
+
tables =
|
264
|
+
if ActiveRecord::VERSION::STRING >= "5.1"
|
265
|
+
connection.tables
|
266
|
+
else
|
267
|
+
connection.data_sources
|
268
|
+
end
|
269
|
+
|
270
|
+
log("Iterating over tables") do
|
271
|
+
tables.each do |table|
|
272
|
+
case
|
273
|
+
when except.include?(table)
|
274
|
+
log("#{table} - ignored via the configuration; skipping")
|
275
|
+
else
|
276
|
+
log(table) do
|
277
|
+
yield(table)
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
def each_association(model, except: [], type: [:has_many, :has_one, :belongs_to], has_scope: nil, through: nil)
|
285
|
+
type = Array(type)
|
286
|
+
|
287
|
+
log("Iterating over associations on #{model.name}") do
|
288
|
+
associations = []
|
289
|
+
type.each do |type1|
|
290
|
+
associations.concat(model.reflect_on_all_associations(type1))
|
291
|
+
end
|
292
|
+
|
293
|
+
associations.each do |association|
|
294
|
+
case
|
295
|
+
when except.include?("#{model.name}.#{association.name}")
|
296
|
+
log("#{model.name}.#{association.name} - ignored via the configuration; skipping")
|
297
|
+
when through && !association.is_a?(ActiveRecord::Reflection::ThroughReflection)
|
298
|
+
log("#{model.name}.#{association.name} - is not a through association; skipping")
|
299
|
+
when through == false && association.is_a?(ActiveRecord::Reflection::ThroughReflection)
|
300
|
+
log("#{model.name}.#{association.name} - is a through association; skipping")
|
301
|
+
when has_scope && association.scope.nil?
|
302
|
+
log("#{model.name}.#{association.name} - doesn't have a scope; skipping")
|
303
|
+
when has_scope == false && association.scope
|
304
|
+
log("#{model.name}.#{association.name} - has a scope; skipping")
|
305
|
+
else
|
306
|
+
log("#{association.macro} :#{association.name}") do
|
307
|
+
yield(association)
|
308
|
+
end
|
309
|
+
end
|
310
|
+
end
|
311
|
+
end
|
312
|
+
end
|
183
313
|
end
|
184
314
|
end
|
185
315
|
end
|
@@ -33,35 +33,36 @@ module ActiveRecordDoctor
|
|
33
33
|
end
|
34
34
|
|
35
35
|
def subindexes_of_multi_column_indexes
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
36
|
+
log(__method__) do
|
37
|
+
each_table(except: config(:ignore_tables)) do |table|
|
38
|
+
each_index(table, except: config(:ignore_indexes), multicolumn_only: true) do |index, indexes|
|
39
|
+
replacement_indexes = indexes.select do |other_index|
|
40
|
+
index != other_index && replaceable_with?(index, other_index)
|
41
|
+
end
|
42
|
+
|
43
|
+
if replacement_indexes.empty?
|
44
|
+
log("Found no replacement indexes; skipping")
|
45
|
+
next
|
46
|
+
end
|
47
|
+
|
48
|
+
problem!(
|
49
|
+
extraneous_index: index.name,
|
50
|
+
replacement_indexes: replacement_indexes.map(&:name).sort
|
51
|
+
)
|
44
52
|
end
|
45
|
-
|
46
|
-
next if replacement_indexes.empty?
|
47
|
-
|
48
|
-
problem!(
|
49
|
-
extraneous_index: index.name,
|
50
|
-
replacement_indexes: replacement_indexes.map(&:name).sort
|
51
|
-
)
|
52
53
|
end
|
53
54
|
end
|
54
55
|
end
|
55
56
|
|
56
57
|
def indexed_primary_keys
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
58
|
+
log(__method__) do
|
59
|
+
each_table(except: config(:ignore_tables)) do |table|
|
60
|
+
each_index(table, except: config(:ignore_indexes), multicolumn_only: true) do |index|
|
61
|
+
primary_key = connection.primary_key(table)
|
62
|
+
if index.columns == [primary_key] && index.where.nil?
|
63
|
+
problem!(extraneous_index: index.name, replacement_indexes: nil)
|
64
|
+
end
|
65
|
+
end
|
65
66
|
end
|
66
67
|
end
|
67
68
|
end
|
@@ -90,10 +91,6 @@ module ActiveRecordDoctor
|
|
90
91
|
lhs.columns.count <= rhs.columns.count &&
|
91
92
|
rhs.columns[0...lhs.columns.count] == lhs.columns
|
92
93
|
end
|
93
|
-
|
94
|
-
def indexes(table_name)
|
95
|
-
super.select { |index| index.columns.is_a?(Array) }
|
96
|
-
end
|
97
94
|
end
|
98
95
|
end
|
99
96
|
end
|
@@ -25,11 +25,8 @@ module ActiveRecordDoctor
|
|
25
25
|
end
|
26
26
|
|
27
27
|
def detect
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
connection.columns(model.table_name).each do |column|
|
32
|
-
next if config(:ignore_attributes).include?("#{model.name}.#{column.name}")
|
28
|
+
each_model(except: config(:ignore_models), existing_tables_only: true) do |model|
|
29
|
+
each_attribute(model, except: config(:ignore_attributes)) do |column|
|
33
30
|
next unless column.type == :boolean
|
34
31
|
next unless has_presence_validator?(model, column)
|
35
32
|
|
@@ -18,7 +18,7 @@ module ActiveRecordDoctor
|
|
18
18
|
|
19
19
|
private
|
20
20
|
|
21
|
-
def message(model:, association:, problem:, associated_models:)
|
21
|
+
def message(model:, association:, problem:, associated_models:, associated_models_type:)
|
22
22
|
associated_models.sort!
|
23
23
|
|
24
24
|
models_part =
|
@@ -28,34 +28,62 @@ module ActiveRecordDoctor
|
|
28
28
|
"models #{associated_models.join(', ')} have"
|
29
29
|
end
|
30
30
|
|
31
|
+
if associated_models_type
|
32
|
+
models_part = "#{associated_models_type} #{models_part}"
|
33
|
+
end
|
34
|
+
|
31
35
|
# rubocop:disable Layout/LineLength
|
32
36
|
case problem
|
37
|
+
when :invalid_through
|
38
|
+
"ensure #{model}.#{association} is configured correctly - #{associated_models[0]}.#{association} may be undefined"
|
33
39
|
when :suggest_destroy
|
34
|
-
"use `dependent: :destroy` or similar on #{model}.#{association} -
|
40
|
+
"use `dependent: :destroy` or similar on #{model}.#{association} - associated #{models_part} callbacks that are currently skipped"
|
35
41
|
when :suggest_delete
|
36
|
-
"use `dependent: :delete` or similar on #{model}.#{association} -
|
42
|
+
"use `dependent: :delete` or similar on #{model}.#{association} - associated #{models_part} no callbacks and can be deleted without loading"
|
37
43
|
when :suggest_delete_all
|
38
|
-
"use `dependent: :delete_all` or similar on #{model}.#{association} - associated #{models_part} no
|
44
|
+
"use `dependent: :delete_all` or similar on #{model}.#{association} - associated #{models_part} no callbacks and can be deleted in bulk"
|
39
45
|
end
|
40
46
|
# rubocop:enable Layout/LineLength
|
41
47
|
end
|
42
48
|
|
43
49
|
def detect
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
50
|
+
each_model(except: config(:ignore_models), existing_tables_only: true) do |model|
|
51
|
+
each_association(model, except: config(:ignore_associations)) do |association|
|
52
|
+
# A properly configured :through association will have a non-nil
|
53
|
+
# source_reflection. If it's nil then it indicates the :through
|
54
|
+
# model lacks the next leg in the :through relationship. For
|
55
|
+
# instance, if user has many comments through posts then a nil
|
56
|
+
# source_reflection means that Post doesn't define +has_many :comments+.
|
57
|
+
if through?(association) && association.source_reflection.nil?
|
58
|
+
log("through association with nil source_reflection")
|
59
|
+
|
60
|
+
through_association = model.reflect_on_association(association.options.fetch(:through))
|
61
|
+
association_on_join_model = through_association.klass.reflect_on_association(association.name)
|
62
|
+
|
63
|
+
# We report a problem only if the +has_many+ association mentioned
|
64
|
+
# above is actually missing. We let the detector continue in other
|
65
|
+
# cases, risking an exception, as the absence of source_reflection
|
66
|
+
# must be caused by something else in those cases. Each further
|
67
|
+
# exception will be handled on a case-by-case basis.
|
68
|
+
if association_on_join_model.nil?
|
69
|
+
problem!(
|
70
|
+
model: model.name,
|
71
|
+
association: association.name,
|
72
|
+
problem: :invalid_through,
|
73
|
+
associated_models: [through_association.klass.name],
|
74
|
+
associated_models_type: "join"
|
75
|
+
)
|
76
|
+
next
|
77
|
+
end
|
78
|
+
end
|
53
79
|
|
54
|
-
associated_models =
|
80
|
+
associated_models, associated_models_type =
|
55
81
|
if association.polymorphic?
|
56
|
-
|
82
|
+
[models_having_association_with_options(as: association.name), nil]
|
83
|
+
elsif through?(association)
|
84
|
+
[[association.source_reflection.active_record], "join"]
|
57
85
|
else
|
58
|
-
[association.klass]
|
86
|
+
[[association.klass], nil]
|
59
87
|
end
|
60
88
|
|
61
89
|
deletable_models, destroyable_models = associated_models.partition { |klass| deletable?(klass) }
|
@@ -68,17 +96,27 @@ module ActiveRecordDoctor
|
|
68
96
|
else raise("unsupported association type #{association.macro}")
|
69
97
|
end
|
70
98
|
|
71
|
-
problem!(
|
72
|
-
|
99
|
+
problem!(
|
100
|
+
model: model.name,
|
101
|
+
association: association.name,
|
102
|
+
problem: suggestion,
|
103
|
+
associated_models: deletable_models.map(&:name),
|
104
|
+
associated_models_type: associated_models_type
|
105
|
+
)
|
73
106
|
elsif callback_action(association) == :skip && destroyable_models.present?
|
74
|
-
problem!(
|
75
|
-
|
107
|
+
problem!(
|
108
|
+
model: model.name,
|
109
|
+
association: association.name,
|
110
|
+
problem: :suggest_destroy,
|
111
|
+
associated_models: destroyable_models.map(&:name),
|
112
|
+
associated_models_type: associated_models_type
|
113
|
+
)
|
76
114
|
end
|
77
115
|
end
|
78
116
|
end
|
79
117
|
end
|
80
118
|
|
81
|
-
def
|
119
|
+
def models_having_association_with_options(as:)
|
82
120
|
models.select do |model|
|
83
121
|
associations = model.reflect_on_all_associations(:has_one) +
|
84
122
|
model.reflect_on_all_associations(:has_many)
|
@@ -108,6 +146,10 @@ module ActiveRecordDoctor
|
|
108
146
|
end
|
109
147
|
end
|
110
148
|
|
149
|
+
def through?(reflection)
|
150
|
+
reflection.is_a?(ActiveRecord::Reflection::ThroughReflection)
|
151
|
+
end
|
152
|
+
|
111
153
|
def defines_destroy_callbacks?(model)
|
112
154
|
# Destroying an associated model involves loading it first hence
|
113
155
|
# initialize and find are present. If they are defined on the model
|
@@ -31,14 +31,9 @@ module ActiveRecordDoctor
|
|
31
31
|
end
|
32
32
|
|
33
33
|
def detect
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
connection.columns(model.table_name).each do |column|
|
38
|
-
next if config(:ignore_attributes).include?("#{model.name}.#{column.name}")
|
39
|
-
next if ![:string, :text].include?(column.type)
|
40
|
-
|
41
|
-
model_maximum = maximum_allowed_by_validations(model)
|
34
|
+
each_model(except: config(:ignore_models), existing_tables_only: true) do |model|
|
35
|
+
each_attribute(model, except: config(:ignore_attributes), type: [:string, :text]) do |column|
|
36
|
+
model_maximum = maximum_allowed_by_validations(model, column.name.to_sym)
|
42
37
|
next if model_maximum == column.limit
|
43
38
|
|
44
39
|
problem!(
|
@@ -52,9 +47,11 @@ module ActiveRecordDoctor
|
|
52
47
|
end
|
53
48
|
end
|
54
49
|
|
55
|
-
def maximum_allowed_by_validations(model)
|
50
|
+
def maximum_allowed_by_validations(model, column)
|
56
51
|
length_validator = model.validators.find do |validator|
|
57
|
-
validator.kind == :length &&
|
52
|
+
validator.kind == :length &&
|
53
|
+
validator.options.include?(:maximum) &&
|
54
|
+
validator.attributes.include?(column)
|
58
55
|
end
|
59
56
|
length_validator ? length_validator.options[:maximum] : nil
|
60
57
|
end
|
@@ -18,25 +18,32 @@ module ActiveRecordDoctor
|
|
18
18
|
|
19
19
|
private
|
20
20
|
|
21
|
-
def message(
|
21
|
+
def message(from_table:, from_column:, from_type:, to_table:, to_column:, to_type:)
|
22
22
|
# rubocop:disable Layout/LineLength
|
23
|
-
"#{
|
23
|
+
"#{from_table}.#{from_column} is a foreign key of type #{from_type} and references #{to_table}.#{to_column} of type #{to_type} - foreign keys should be of the same type as the referenced column"
|
24
24
|
# rubocop:enable Layout/LineLength
|
25
25
|
end
|
26
26
|
|
27
27
|
def detect
|
28
|
-
|
29
|
-
|
28
|
+
each_table(except: config(:ignore_tables)) do |table|
|
29
|
+
each_foreign_key(table) do |foreign_key|
|
30
30
|
from_column = column(table, foreign_key.column)
|
31
31
|
|
32
32
|
next if config(:ignore_columns).include?("#{table}.#{from_column.name}")
|
33
33
|
|
34
34
|
to_table = foreign_key.to_table
|
35
|
-
|
36
|
-
|
37
|
-
next if from_column.sql_type ==
|
38
|
-
|
39
|
-
problem!(
|
35
|
+
to_column = column(to_table, foreign_key.primary_key)
|
36
|
+
|
37
|
+
next if from_column.sql_type == to_column.sql_type
|
38
|
+
|
39
|
+
problem!(
|
40
|
+
from_table: table,
|
41
|
+
from_column: from_column.name,
|
42
|
+
from_type: from_column.sql_type,
|
43
|
+
to_table: to_table,
|
44
|
+
to_column: to_column.name,
|
45
|
+
to_type: to_column.sql_type
|
46
|
+
)
|
40
47
|
end
|
41
48
|
end
|
42
49
|
end
|
@@ -23,10 +23,8 @@ module ActiveRecordDoctor
|
|
23
23
|
end
|
24
24
|
|
25
25
|
def detect
|
26
|
-
|
27
|
-
|
28
|
-
next if config(:ignore_columns).include?("#{table}.#{column.name}")
|
29
|
-
|
26
|
+
each_table(except: config(:ignore_tables)) do |table|
|
27
|
+
each_column(table, except: config(:ignore_columns)) do |column|
|
30
28
|
# We need to skip polymorphic associations as they can reference
|
31
29
|
# multiple tables but a foreign key constraint can reference
|
32
30
|
# a single predefined table.
|
@@ -23,8 +23,7 @@ module ActiveRecordDoctor
|
|
23
23
|
end
|
24
24
|
|
25
25
|
def detect
|
26
|
-
table_models = models.group_by(&:table_name)
|
27
|
-
table_models.delete_if { |_table, models| !models.first.table_exists? }
|
26
|
+
table_models = models.select(&:table_exists?).group_by(&:table_name)
|
28
27
|
|
29
28
|
table_models.each do |table, models|
|
30
29
|
next if config(:ignore_tables).include?(table)
|
@@ -50,19 +49,22 @@ module ActiveRecordDoctor
|
|
50
49
|
end
|
51
50
|
|
52
51
|
def non_null_needed?(model, column)
|
53
|
-
# A foreign key can be validates via the column name (e.g. company_id)
|
54
|
-
# or the association name (e.g. company). We collect the allowed names
|
55
|
-
# in an array to check for their presence in the validator definition
|
56
|
-
# in one go.
|
57
|
-
attribute_name_forms = [column.name.to_sym]
|
58
52
|
belongs_to = model.reflect_on_all_associations(:belongs_to).find do |reflection|
|
59
|
-
reflection.foreign_key == column.name
|
53
|
+
reflection.foreign_key == column.name ||
|
54
|
+
(reflection.polymorphic? && reflection.foreign_type == column.name)
|
60
55
|
end
|
61
|
-
attribute_name_forms << belongs_to.name.to_sym if belongs_to
|
62
56
|
|
63
|
-
model.
|
57
|
+
required_presence_validators(model).any? do |validator|
|
58
|
+
attributes = validator.attributes
|
59
|
+
|
60
|
+
attributes.include?(column.name.to_sym) ||
|
61
|
+
(belongs_to && attributes.include?(belongs_to.name.to_sym))
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def required_presence_validators(model)
|
66
|
+
model.validators.select do |validator|
|
64
67
|
validator.is_a?(ActiveRecord::Validations::PresenceValidator) &&
|
65
|
-
(validator.attributes & attribute_name_forms).present? &&
|
66
68
|
!validator.options[:allow_nil] &&
|
67
69
|
!validator.options[:if] &&
|
68
70
|
!validator.options[:unless]
|