active_record_doctor 1.8.0 → 1.10.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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +316 -54
  3. data/lib/active_record_doctor/config/default.rb +76 -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 +142 -21
  7. data/lib/active_record_doctor/detectors/extraneous_indexes.rb +59 -48
  8. data/lib/active_record_doctor/detectors/incorrect_boolean_presence_validation.rb +31 -23
  9. data/lib/active_record_doctor/detectors/incorrect_dependent_option.rb +102 -35
  10. data/lib/active_record_doctor/detectors/incorrect_length_validation.rb +63 -0
  11. data/lib/active_record_doctor/detectors/mismatched_foreign_key_type.rb +45 -0
  12. data/lib/active_record_doctor/detectors/missing_foreign_keys.rb +32 -23
  13. data/lib/active_record_doctor/detectors/missing_non_null_constraint.rb +41 -28
  14. data/lib/active_record_doctor/detectors/missing_presence_validation.rb +29 -23
  15. data/lib/active_record_doctor/detectors/missing_unique_indexes.rb +92 -32
  16. data/lib/active_record_doctor/detectors/short_primary_key_type.rb +45 -0
  17. data/lib/active_record_doctor/detectors/undefined_table_references.rb +17 -20
  18. data/lib/active_record_doctor/detectors/unindexed_deleted_at.rb +43 -18
  19. data/lib/active_record_doctor/detectors/unindexed_foreign_keys.rb +31 -20
  20. data/lib/active_record_doctor/detectors.rb +12 -4
  21. data/lib/active_record_doctor/errors.rb +226 -0
  22. data/lib/active_record_doctor/help.rb +39 -0
  23. data/lib/active_record_doctor/rake/task.rb +78 -0
  24. data/lib/active_record_doctor/runner.rb +41 -0
  25. data/lib/active_record_doctor/version.rb +1 -1
  26. data/lib/active_record_doctor.rb +8 -3
  27. data/lib/generators/active_record_doctor/add_indexes/add_indexes_generator.rb +34 -21
  28. data/lib/tasks/active_record_doctor.rake +9 -18
  29. data/test/active_record_doctor/config/loader_test.rb +120 -0
  30. data/test/active_record_doctor/config_test.rb +116 -0
  31. data/test/active_record_doctor/detectors/disable_test.rb +30 -0
  32. data/test/active_record_doctor/detectors/extraneous_indexes_test.rb +165 -8
  33. data/test/active_record_doctor/detectors/incorrect_boolean_presence_validation_test.rb +48 -5
  34. data/test/active_record_doctor/detectors/incorrect_dependent_option_test.rb +288 -12
  35. data/test/active_record_doctor/detectors/incorrect_length_validation_test.rb +105 -0
  36. data/test/active_record_doctor/detectors/mismatched_foreign_key_type_test.rb +82 -0
  37. data/test/active_record_doctor/detectors/missing_foreign_keys_test.rb +50 -4
  38. data/test/active_record_doctor/detectors/missing_non_null_constraint_test.rb +172 -24
  39. data/test/active_record_doctor/detectors/missing_presence_validation_test.rb +111 -14
  40. data/test/active_record_doctor/detectors/missing_unique_indexes_test.rb +223 -10
  41. data/test/active_record_doctor/detectors/short_primary_key_type_test.rb +72 -0
  42. data/test/active_record_doctor/detectors/undefined_table_references_test.rb +34 -21
  43. data/test/active_record_doctor/detectors/unindexed_deleted_at_test.rb +118 -8
  44. data/test/active_record_doctor/detectors/unindexed_foreign_keys_test.rb +56 -4
  45. data/test/active_record_doctor/runner_test.rb +42 -0
  46. data/test/generators/active_record_doctor/add_indexes/add_indexes_generator_test.rb +131 -0
  47. data/test/model_factory.rb +73 -23
  48. data/test/setup.rb +65 -71
  49. metadata +43 -7
  50. data/lib/active_record_doctor/printers/io_printer.rb +0 -133
  51. data/lib/active_record_doctor/task.rb +0 -28
  52. data/test/active_record_doctor/printers/io_printer_test.rb +0 -33
@@ -13,10 +13,9 @@ class ActiveRecordDoctor::Detectors::IncorrectDependentOptionTest < Minitest::Te
13
13
  belongs_to :company
14
14
  end
15
15
 
16
- assert_problems(<<OUTPUT)
17
- The following associations might be using invalid dependent settings:
18
- ModelFactory::Models::Company: users loads models one-by-one to invoke callbacks even though the related model defines none - consider using `dependent: :delete_all`
19
- OUTPUT
16
+ assert_problems(<<~OUTPUT)
17
+ use `dependent: :delete_all` or similar on ModelFactory::Models::Company.users - associated model ModelFactory::Models::User has no validations and can be deleted in bulk
18
+ OUTPUT
20
19
  end
21
20
 
22
21
  def test_invoking_callbacks_does_not_suggest_delete_all
@@ -56,10 +55,9 @@ OUTPUT
56
55
  end
57
56
  end
58
57
 
59
- assert_problems(<<OUTPUT)
60
- The following associations might be using invalid dependent settings:
61
- ModelFactory::Models::Company: users skips callbacks that are defined on the associated model - consider changing to `dependent: :destroy` or similar
62
- OUTPUT
58
+ assert_problems(<<~OUTPUT)
59
+ use `dependent: :destroy` or similar on ModelFactory::Models::Company.users - the associated model ModelFactory::Models::User has callbacks that are currently skipped
60
+ OUTPUT
63
61
  end
64
62
 
65
63
  def test_invoking_callbacks_does_not_suggest_destroy
@@ -94,10 +92,125 @@ OUTPUT
94
92
  belongs_to :company
95
93
  end
96
94
 
97
- assert_problems(<<OUTPUT)
98
- The following associations might be using invalid dependent settings:
99
- ModelFactory::Models::Company: owner loads the associated model before deleting it - consider using `dependent: :delete`
100
- OUTPUT
95
+ assert_problems(<<~OUTPUT)
96
+ use `dependent: :delete` or similar on ModelFactory::Models::Company.owner - the associated model ModelFactory::Models::User has no callbacks and can be deleted without loading
97
+ OUTPUT
98
+ end
99
+
100
+ def test_works_on_belongs_to
101
+ create_table(:companies) do
102
+ end.create_model do
103
+ has_many :users
104
+ end
105
+
106
+ create_table(:users) do |t|
107
+ t.references :company
108
+ end.create_model do
109
+ belongs_to :company, dependent: :destroy
110
+ end
111
+
112
+ assert_problems(<<~OUTPUT)
113
+ use `dependent: :delete` or similar on ModelFactory::Models::User.company - the associated model ModelFactory::Models::Company has no callbacks and can be deleted without loading
114
+ OUTPUT
115
+ end
116
+
117
+ def test_no_foreign_key_on_second_level_association
118
+ create_table(:companies) do
119
+ end.create_model do
120
+ has_many :users
121
+ has_many :projects
122
+ end
123
+
124
+ create_table(:users) do |t|
125
+ t.references :company
126
+ end.create_model do
127
+ belongs_to :company, dependent: :destroy
128
+ end
129
+
130
+ create_table(:projects) do |t|
131
+ t.references :company
132
+ end.create_model do
133
+ belongs_to :company
134
+ end
135
+
136
+ assert_problems(<<~OUTPUT)
137
+ use `dependent: :delete` or similar on ModelFactory::Models::User.company - the associated model ModelFactory::Models::Company has no callbacks and can be deleted without loading
138
+ OUTPUT
139
+ end
140
+
141
+ def test_nullify_foreign_key_on_second_level_association
142
+ create_table(:companies) do
143
+ end.create_model do
144
+ has_many :users
145
+ has_many :projects
146
+ end
147
+
148
+ create_table(:users) do |t|
149
+ t.references :company
150
+ end.create_model do
151
+ belongs_to :company, dependent: :destroy
152
+ end
153
+
154
+ create_table(:projects) do |t|
155
+ t.references :company, foreign_key: { on_delete: :nullify }
156
+ end.create_model do
157
+ belongs_to :company
158
+ end
159
+
160
+ assert_problems(<<~OUTPUT)
161
+ use `dependent: :delete` or similar on ModelFactory::Models::User.company - the associated model ModelFactory::Models::Company has no callbacks and can be deleted without loading
162
+ OUTPUT
163
+ end
164
+
165
+ def test_cascade_foreign_key_and_callbacks_on_second_level_association
166
+ create_table(:companies) do
167
+ end.create_model do
168
+ has_many :users
169
+ has_many :projects
170
+ end
171
+
172
+ create_table(:users) do |t|
173
+ t.references :company
174
+ end.create_model do
175
+ belongs_to :company, dependent: :delete
176
+ end
177
+
178
+ create_table(:projects) do |t|
179
+ t.references :company, foreign_key: { on_delete: :cascade }
180
+ end.create_model do
181
+ belongs_to :company
182
+
183
+ before_destroy :log
184
+
185
+ def log
186
+ end
187
+ end
188
+
189
+ assert_problems(<<~OUTPUT)
190
+ use `dependent: :destroy` or similar on ModelFactory::Models::User.company - the associated model ModelFactory::Models::Company has callbacks that are currently skipped
191
+ OUTPUT
192
+ end
193
+
194
+ def test_cascade_foreign_key_and_no_callbacks_on_second_level_association
195
+ create_table(:companies) do
196
+ end.create_model do
197
+ has_many :users
198
+ has_many :projects
199
+ end
200
+
201
+ create_table(:users) do |t|
202
+ t.references :company
203
+ end.create_model do
204
+ belongs_to :company, dependent: :delete
205
+ end
206
+
207
+ create_table(:projects) do |t|
208
+ t.references :company, foreign_key: { on_delete: :cascade }
209
+ end.create_model do
210
+ belongs_to :company
211
+ end
212
+
213
+ refute_problems
101
214
  end
102
215
 
103
216
  def test_no_dependent_suggests_nothing
@@ -114,4 +227,167 @@ OUTPUT
114
227
 
115
228
  refute_problems
116
229
  end
230
+
231
+ def test_polymorphic_destroy_reported_when_all_associations_deletable
232
+ create_table(:images) do |t|
233
+ t.bigint :imageable_id, null: false
234
+ t.string :imageable_type, null: true
235
+ end.create_model do
236
+ belongs_to :imageable, polymorphic: true, dependent: :destroy
237
+ end
238
+
239
+ create_table(:users) do
240
+ end.create_model do
241
+ has_one :image, as: :imageable
242
+ end
243
+
244
+ create_table(:companies) do
245
+ end.create_model do
246
+ has_one :image, as: :imageable
247
+ end
248
+
249
+ assert_problems(<<~OUTPUT)
250
+ use `dependent: :delete` or similar on ModelFactory::Models::Image.imageable - the associated models ModelFactory::Models::Company, ModelFactory::Models::User have no callbacks and can be deleted without loading
251
+ OUTPUT
252
+ end
253
+
254
+ def test_polymorphic_destroy_not_reported_when_some_associations_not_deletable
255
+ create_table(:images) do |t|
256
+ t.bigint :imageable_id, null: false
257
+ t.string :imageable_type, null: true
258
+ end.create_model do
259
+ belongs_to :imageable, polymorphic: true, dependent: :destroy
260
+ end
261
+
262
+ create_table(:users) do
263
+ end.create_model do
264
+ has_one :image, as: :imageable
265
+
266
+ before_destroy :log
267
+
268
+ def log
269
+ end
270
+ end
271
+
272
+ create_table(:companies) do
273
+ end.create_model do
274
+ has_one :image, as: :imageable
275
+ end
276
+
277
+ refute_problems
278
+ end
279
+
280
+ def test_polymorphic_delete_reported_when_some_associations_not_deletable
281
+ create_table(:images) do |t|
282
+ t.bigint :imageable_id, null: false
283
+ t.string :imageable_type, null: true
284
+ end.create_model do
285
+ belongs_to :imageable, polymorphic: true, dependent: :delete
286
+ end
287
+
288
+ create_table(:users) do
289
+ end.create_model do
290
+ has_one :image, as: :imageable
291
+
292
+ before_destroy :log
293
+
294
+ def log
295
+ end
296
+ end
297
+
298
+ create_table(:companies) do
299
+ end.create_model do
300
+ has_one :image, as: :imageable
301
+ end
302
+
303
+ assert_problems(<<~OUTPUT)
304
+ use `dependent: :destroy` or similar on ModelFactory::Models::Image.imageable - the associated model ModelFactory::Models::User has callbacks that are currently skipped
305
+ OUTPUT
306
+ end
307
+
308
+ def test_polymorphic_delete_not_reported_when_all_associations_deletable
309
+ create_table(:images) do |t|
310
+ t.bigint :imageable_id, null: false
311
+ t.string :imageable_type, null: true
312
+ end.create_model do
313
+ belongs_to :imageable, polymorphic: true, dependent: :delete
314
+ end
315
+
316
+ create_table(:users) do
317
+ end.create_model do
318
+ has_one :image, as: :imageable
319
+ end
320
+
321
+ create_table(:companies) do
322
+ end.create_model do
323
+ has_one :image, as: :imageable
324
+ end
325
+
326
+ refute_problems
327
+ end
328
+
329
+ def test_config_ignore_models
330
+ create_table(:companies) do
331
+ end.create_model do
332
+ has_many :users, dependent: :destroy
333
+ end
334
+
335
+ create_table(:users) do |t|
336
+ t.references :companies
337
+ end.create_model do
338
+ belongs_to :company
339
+ end
340
+
341
+ config_file(<<-CONFIG)
342
+ ActiveRecordDoctor.configure do |config|
343
+ config.detector :incorrect_dependent_option,
344
+ ignore_models: ["ModelFactory::Models::Company"]
345
+ end
346
+ CONFIG
347
+
348
+ refute_problems
349
+ end
350
+
351
+ def test_global_ignore_models
352
+ create_table(:companies) do
353
+ end.create_model do
354
+ has_many :users, dependent: :destroy
355
+ end
356
+
357
+ create_table(:users) do |t|
358
+ t.references :companies
359
+ end.create_model do
360
+ belongs_to :company
361
+ end
362
+
363
+ config_file(<<-CONFIG)
364
+ ActiveRecordDoctor.configure do |config|
365
+ config.global :ignore_models, ["ModelFactory::Models::Company"]
366
+ end
367
+ CONFIG
368
+
369
+ refute_problems
370
+ end
371
+
372
+ def test_config_ignore_associations
373
+ create_table(:companies) do
374
+ end.create_model do
375
+ has_many :users, dependent: :destroy
376
+ end
377
+
378
+ create_table(:users) do |t|
379
+ t.references :companies
380
+ end.create_model do
381
+ belongs_to :company
382
+ end
383
+
384
+ config_file(<<-CONFIG)
385
+ ActiveRecordDoctor.configure do |config|
386
+ config.detector :incorrect_dependent_option,
387
+ ignore_associations: ["ModelFactory::Models::Company.users"]
388
+ end
389
+ CONFIG
390
+
391
+ refute_problems
392
+ end
117
393
  end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ActiveRecordDoctor::Detectors::IncorrectLengthValidationTest < Minitest::Test
4
+ def test_validation_and_limit_equal_is_ok
5
+ create_table(:users) do |t|
6
+ t.string :email, limit: 64
7
+ end.create_model do
8
+ validates :email, length: { maximum: 64 }
9
+ end
10
+
11
+ refute_problems
12
+ end
13
+
14
+ def test_validation_and_limit_different_is_error
15
+ create_table(:users) do |t|
16
+ t.string :email, limit: 64
17
+ end.create_model do
18
+ validates :email, length: { maximum: 32 }
19
+ end
20
+
21
+ assert_problems(<<~OUTPUT)
22
+ the schema limits users.email to 64 characters but the length validator on ModelFactory::Models::User.email enforces a maximum of 32 characters - set both limits to the same value or remove both
23
+ OUTPUT
24
+ end
25
+
26
+ def test_validation_and_no_limit_is_error
27
+ skip("MySQL always sets a limit on text columns") if mysql?
28
+
29
+ create_table(:users) do |t|
30
+ t.string :email
31
+ end.create_model do
32
+ validates :email, length: { maximum: 32 }
33
+ end
34
+
35
+ assert_problems(<<~OUTPUT)
36
+ the length validator on ModelFactory::Models::User.email enforces a maximum of 32 characters but there's no schema limit on users.email - remove the validator or the schema length limit
37
+ OUTPUT
38
+ end
39
+
40
+ def test_no_validation_and_limit_is_error
41
+ create_table(:users) do |t|
42
+ t.string :email, limit: 64
43
+ end.create_model do
44
+ end
45
+
46
+ assert_problems(<<~OUTPUT)
47
+ the schema limits users.email to 64 characters but there's no length validator on ModelFactory::Models::User.email - remove the database limit or add the validator
48
+ OUTPUT
49
+ end
50
+
51
+ def test_no_validation_and_no_limit_is_ok
52
+ skip("MySQL always sets a limit on text columns") if mysql?
53
+
54
+ create_table(:users) do |t|
55
+ t.string :email
56
+ end.create_model do
57
+ end
58
+
59
+ refute_problems
60
+ end
61
+
62
+ def test_config_ignore_models
63
+ create_table(:users) do |t|
64
+ t.string :email, limit: 64
65
+ end.create_model
66
+
67
+ config_file(<<-CONFIG)
68
+ ActiveRecordDoctor.configure do |config|
69
+ config.detector :incorrect_length_validation,
70
+ ignore_models: ["ModelFactory::Models::User"]
71
+ end
72
+ CONFIG
73
+
74
+ refute_problems
75
+ end
76
+
77
+ def test_global_ignore_models
78
+ create_table(:users) do |t|
79
+ t.string :email, limit: 64
80
+ end.create_model
81
+
82
+ config_file(<<-CONFIG)
83
+ ActiveRecordDoctor.configure do |config|
84
+ config.global :ignore_models, ["ModelFactory::Models::User"]
85
+ end
86
+ CONFIG
87
+
88
+ refute_problems
89
+ end
90
+
91
+ def test_config_ignore_attributes
92
+ create_table(:users) do |t|
93
+ t.string :email, limit: 64
94
+ end.create_model
95
+
96
+ config_file(<<-CONFIG)
97
+ ActiveRecordDoctor.configure do |config|
98
+ config.detector :incorrect_length_validation,
99
+ ignore_attributes: ["ModelFactory::Models::User.email"]
100
+ end
101
+ CONFIG
102
+
103
+ refute_problems
104
+ end
105
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ActiveRecordDoctor::Detectors::MismatchedForeignKeyTypeTest < Minitest::Test
4
+ def test_mismatched_foreign_key_type_is_reported
5
+ # MySQL does not allow foreign keys to have different type than paired primary keys
6
+ return if mysql?
7
+
8
+ create_table(:companies, id: :bigint)
9
+ create_table(:users) do |t|
10
+ t.references :company, foreign_key: true, type: :integer
11
+ end
12
+
13
+ assert_problems(<<~OUTPUT)
14
+ users.company_id references a column of different type - foreign keys should be of the same type as the referenced column
15
+ OUTPUT
16
+ end
17
+
18
+ def test_matched_foreign_key_type_is_not_reported
19
+ create_table(:companies)
20
+ create_table(:users) do |t|
21
+ t.references :company, foreign_key: true
22
+ end
23
+
24
+ refute_problems
25
+ end
26
+
27
+ def test_config_ignore_tables
28
+ # MySQL does not allow foreign keys to have different type than paired primary keys
29
+ return if mysql?
30
+
31
+ create_table(:companies, id: :bigint)
32
+ create_table(:users) do |t|
33
+ t.references :company, foreign_key: true, type: :integer
34
+ end
35
+
36
+ config_file(<<-CONFIG)
37
+ ActiveRecordDoctor.configure do |config|
38
+ config.detector :mismatched_foreign_key_type,
39
+ ignore_tables: ["users"]
40
+ end
41
+ CONFIG
42
+
43
+ refute_problems
44
+ end
45
+
46
+ def test_global_ignore_tables
47
+ # MySQL does not allow foreign keys to have different type than paired primary keys
48
+ return if mysql?
49
+
50
+ create_table(:companies, id: :bigint)
51
+ create_table(:users) do |t|
52
+ t.references :company, foreign_key: true, type: :integer
53
+ end
54
+
55
+ config_file(<<-CONFIG)
56
+ ActiveRecordDoctor.configure do |config|
57
+ config.global :ignore_tables, ["users"]
58
+ end
59
+ CONFIG
60
+
61
+ refute_problems
62
+ end
63
+
64
+ def test_config_ignore_columns
65
+ # MySQL does not allow foreign keys to have different type than paired primary keys
66
+ return if mysql?
67
+
68
+ create_table(:companies, id: :bigint)
69
+ create_table(:users) do |t|
70
+ t.references :company, foreign_key: true, type: :integer
71
+ end
72
+
73
+ config_file(<<-CONFIG)
74
+ ActiveRecordDoctor.configure do |config|
75
+ config.detector :mismatched_foreign_key_type,
76
+ ignore_columns: ["users.company_id"]
77
+ end
78
+ CONFIG
79
+
80
+ refute_problems
81
+ end
82
+ end
@@ -7,10 +7,9 @@ class ActiveRecordDoctor::Detectors::MissingForeignKeysTest < Minitest::Test
7
7
  t.references :company, foreign_key: false
8
8
  end
9
9
 
10
- assert_problems(<<OUTPUT)
11
- The following columns lack a foreign key constraint:
12
- users company_id
13
- OUTPUT
10
+ assert_problems(<<~OUTPUT)
11
+ create a foreign key on users.company_id - looks like an association without a foreign key constraint
12
+ OUTPUT
14
13
  end
15
14
 
16
15
  def test_present_foreign_key_is_not_reported
@@ -21,4 +20,51 @@ OUTPUT
21
20
 
22
21
  refute_problems
23
22
  end
23
+
24
+ def test_config_ignore_models
25
+ create_table(:companies)
26
+ create_table(:users) do |t|
27
+ t.references :company, foreign_key: false
28
+ end
29
+
30
+ config_file(<<-CONFIG)
31
+ ActiveRecordDoctor.configure do |config|
32
+ config.detector :missing_foreign_keys,
33
+ ignore_tables: ["users"]
34
+ end
35
+ CONFIG
36
+
37
+ refute_problems
38
+ end
39
+
40
+ def test_global_ignore_models
41
+ create_table(:companies)
42
+ create_table(:users) do |t|
43
+ t.references :company, foreign_key: false
44
+ end
45
+
46
+ config_file(<<-CONFIG)
47
+ ActiveRecordDoctor.configure do |config|
48
+ config.global :ignore_tables, ["users"]
49
+ end
50
+ CONFIG
51
+
52
+ refute_problems
53
+ end
54
+
55
+ def test_config_ignore_columns
56
+ create_table(:companies)
57
+ create_table(:users) do |t|
58
+ t.references :company, foreign_key: false
59
+ end
60
+
61
+ config_file(<<-CONFIG)
62
+ ActiveRecordDoctor.configure do |config|
63
+ config.detector :missing_foreign_keys,
64
+ ignore_columns: ["users.company_id"]
65
+ end
66
+ CONFIG
67
+
68
+ refute_problems
69
+ end
24
70
  end