active_record_doctor 1.9.0 → 1.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (28) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +83 -19
  3. data/lib/active_record_doctor/config/default.rb +17 -0
  4. data/lib/active_record_doctor/detectors/base.rb +52 -22
  5. data/lib/active_record_doctor/detectors/extraneous_indexes.rb +25 -40
  6. data/lib/active_record_doctor/detectors/incorrect_boolean_presence_validation.rb +1 -2
  7. data/lib/active_record_doctor/detectors/incorrect_dependent_option.rb +40 -9
  8. data/lib/active_record_doctor/detectors/incorrect_length_validation.rb +63 -0
  9. data/lib/active_record_doctor/detectors/missing_non_null_constraint.rb +2 -1
  10. data/lib/active_record_doctor/detectors/missing_presence_validation.rb +3 -4
  11. data/lib/active_record_doctor/detectors/missing_unique_indexes.rb +65 -15
  12. data/lib/active_record_doctor/detectors/short_primary_key_type.rb +5 -1
  13. data/lib/active_record_doctor/detectors/undefined_table_references.rb +1 -3
  14. data/lib/active_record_doctor/detectors/unindexed_deleted_at.rb +2 -3
  15. data/lib/active_record_doctor/version.rb +1 -1
  16. data/lib/active_record_doctor.rb +1 -0
  17. data/lib/generators/active_record_doctor/add_indexes/add_indexes_generator.rb +5 -5
  18. data/test/active_record_doctor/detectors/disable_test.rb +30 -0
  19. data/test/active_record_doctor/detectors/extraneous_indexes_test.rb +34 -0
  20. data/test/active_record_doctor/detectors/incorrect_dependent_option_test.rb +105 -7
  21. data/test/active_record_doctor/detectors/incorrect_length_validation_test.rb +105 -0
  22. data/test/active_record_doctor/detectors/missing_non_null_constraint_test.rb +34 -0
  23. data/test/active_record_doctor/detectors/missing_presence_validation_test.rb +37 -1
  24. data/test/active_record_doctor/detectors/missing_unique_indexes_test.rb +167 -3
  25. data/test/active_record_doctor/detectors/short_primary_key_type_test.rb +27 -19
  26. data/test/active_record_doctor/detectors/unindexed_deleted_at_test.rb +9 -3
  27. data/test/setup.rb +6 -2
  28. metadata +8 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e69330f83b83c3adcd1397554bc916ee163acf3797b8be5b8f96f1960e2d8a3e
4
- data.tar.gz: 8f6e09d3af78bb9aaf01374437653b25070c2c328a0ffeaedc800378a10074f6
3
+ metadata.gz: 1e1bf7642b1c7b471fbf78956825067ff070d261d4c47a182d1aaf00f10b98b7
4
+ data.tar.gz: 6e6fc746327d6db30c282d7964ae778b254107281d9626a251f8593d6ce85db4
5
5
  SHA512:
6
- metadata.gz: 4b9d75665c0524cd5ad18c5aa21aaf59b3e7a75b63c88fb332cd3f600de4f2a9fa073b985f5155b81d54f177b25da5fba5dde494fa6eed28e16086a955082012
7
- data.tar.gz: 94c14b73d06c92803b196fde5ca0572c9545b2ef8b89bc91e88755c5aed9c4e51087ab59a53a94f1fbcacf3f7a58a25cac38dfb4f81d7096ee4d03c7796632b4
6
+ metadata.gz: a0a2d71947728b9d6a130901d94d1ed8c2ede9dde209e91897a8beb8b757da2fea854b89a3a3f6223d658c203e75e687edbee2af37b2203ddb24b28e831439e8
7
+ data.tar.gz: f0fe87dd6acf379ef946bc513504342c3977bf5c4d7f71eca79290c693b318f0a838c31b95111f68635579cdfa26249d5a95fd456817b6a3c32023c1b4fe0fd1
data/README.md CHANGED
@@ -11,6 +11,7 @@ can detect:
11
11
  * missing non-`NULL` constraints - [`active_record_doctor:missing_non_null_constraint`](#detecting-missing-non-null-constraints)
12
12
  * missing presence validations - [`active_record_doctor:missing_presence_validation`](#detecting-missing-presence-validations)
13
13
  * incorrect presence validations on boolean columns - [`active_record_doctor:incorrect_boolean_presence_validation`](#detecting-incorrect-presence-validations-on-boolean-columns)
14
+ * mismatches between model length validations and database validation constraints - [`active_record_doctor:incorrect_length_validation`](#detecting-incorrect-length-validation)
14
15
  * incorrect values of `dependent` on associations - [`active_record_doctor:incorrect_dependent_option`](#detecting-incorrect-dependent-option-on-associations)
15
16
  * primary keys having short integer types - [`active_record_doctor:short_primary_key_type`](#detecting-primary-keys-having-short-integer-types)
16
17
  * mismatched foreign key types - [`active_record_doctor:mismatched_foreign_key_type`](#detecting-mismatched-foreign-key-types)
@@ -90,7 +91,8 @@ build steps -- it returns a non-zero exit status if any errors were reported.
90
91
 
91
92
  ### Obtaining Help
92
93
 
93
- If you'd like to obtain help on a specific detector then use the `help` sub-task:
94
+ If you'd like to obtain help on a specific detector then use the `help`
95
+ sub-task:
94
96
 
95
97
  ```
96
98
  bundle exec rake active_record_doctor:extraneous_indexes:help
@@ -101,7 +103,7 @@ configuration options, their meaning, and whether they're global or local.
101
103
 
102
104
  ### Configuration
103
105
 
104
- `active_record_doctor` can be configured to better suite your project's needs.
106
+ `active_record_doctor` can be configured to better suit your project's needs.
105
107
  For example, if it complains about a model that you want ignored then you can
106
108
  add that model to the configuration file.
107
109
 
@@ -173,8 +175,10 @@ three-step process:
173
175
 
174
176
  Supported configuration options:
175
177
 
178
+ - `enabled` - set to `false` to disable the detector altogether
176
179
  - `ignore_tables` - tables whose foreign keys should not be checked
177
- - `ignore_columns` - columns, written as table.column, that should not be checked.
180
+ - `ignore_columns` - columns, written as table.column, that should not be
181
+ checked.
178
182
 
179
183
  ### Removing Extraneous Indexes
180
184
 
@@ -215,10 +219,15 @@ reported.
215
219
  Note that a unique index can _never be replaced by a non-unique one_. For
216
220
  example, if there's a unique index on `users.login` and a non-unique index on
217
221
  `users.login, users.domain` then the tool will _not_ suggest dropping
218
- `users.login` as it could violate the uniqueness assumption.
222
+ `users.login` as it could violate the uniqueness assumption. However, a unique
223
+ index on `users.login, user.domain` might be replaceable with `users.login` as
224
+ the uniqueness of the latter implies the uniqueness of the former (if a given
225
+ `login` can appear only once then it can be present in only one `login, domain`
226
+ pair).
219
227
 
220
228
  Supported configuration options:
221
229
 
230
+ - `enabled` - set to `false` to disable the detector altogether
222
231
  - `ignore_tables` - tables whose indexes should never be reported as extraneous.
223
232
  - `ignore_columns` - indexes that should never be reported as extraneous.
224
233
 
@@ -227,7 +236,8 @@ Supported configuration options:
227
236
  If you soft-delete some models (e.g. with `paranoia`) then you need to modify
228
237
  your indexes to include only non-deleted rows. Otherwise they will include
229
238
  logically non-existent rows. This will make them larger and slower to use. Most
230
- of the time they should only cover columns satisfying `deleted_at IS NULL`.
239
+ of the time they should only cover columns satisfying `deleted_at IS NULL` (to
240
+ cover existing records) or `deleted_at IS NOT NULL` (to cover deleted records).
231
241
 
232
242
  `active_record_doctor` can automatically detect indexes on tables with a
233
243
  `deleted_at` column. Just run:
@@ -242,9 +252,12 @@ appropriate migrations. You need to do that manually.
242
252
 
243
253
  Supported configuration options:
244
254
 
255
+ - `enabled` - set to `false` to disable the detector altogether
245
256
  - `ignore_tables` - tables whose indexes should not be checked.
246
- - `ignore_columns` - specific columns, written as table.column, that should not be reported as unindexed.
247
- - `ignore_indexes` - specific indexes that should not be reported as excluding a timestamp column.
257
+ - `ignore_columns` - specific columns, written as table.column, that should not
258
+ be reported as unindexed.
259
+ - `ignore_indexes` - specific indexes that should not be reported as excluding a
260
+ timestamp column.
248
261
  - `column_names` - deletion timestamp column names.
249
262
 
250
263
  ### Detecting Missing Foreign Key Constraints
@@ -277,8 +290,10 @@ end
277
290
 
278
291
  Supported configuration options:
279
292
 
293
+ - `enabled` - set to `false` to disable the detector altogether
280
294
  - `ignore_tables` - tables whose columns should not be checked.
281
- - `ignore_columns` - columns, written as table.column, that should not be checked.
295
+ - `ignore_columns` - columns, written as table.column, that should not be
296
+ checked.
282
297
 
283
298
  ### Detecting Models Referencing Undefined Tables
284
299
 
@@ -310,13 +325,15 @@ this check as part of your Continuous Integration pipeline.
310
325
 
311
326
  Supported configuration options:
312
327
 
313
- - `ignore_models` - models whose underlying tables should not be checked for existence.
328
+ - `enabled` - set to `false` to disable the detector altogether
329
+ - `ignore_models` - models whose underlying tables should not be checked for
330
+ existence.
314
331
 
315
332
  ### Detecting Uniqueness Validations not Backed by an Index
316
333
 
317
- A model-level uniqueness validations should be backed by a database index in
318
- order to be robust. Otherwise you risk inserting duplicate values under heavy
319
- load.
334
+ Model-level uniqueness validations and `has_one` associations should be backed
335
+ by a database index in order to be robust. Otherwise you risk inserting
336
+ duplicate values under heavy load.
320
337
 
321
338
  In order to detect such validations run:
322
339
 
@@ -334,13 +351,16 @@ This means that you should create a unique index on `users.email`.
334
351
 
335
352
  Supported configuration options:
336
353
 
354
+ - `enabled` - set to `false` to disable the detector altogether
337
355
  - `ignore_models` - models whose uniqueness validators should not be checked.
338
- - `ignore_columns` - specific validators, written as Model(column1, column2, ...), that should not be checked.
356
+ - `ignore_columns` - specific validators, written as Model(column1, ...), that
357
+ should not be checked.
339
358
 
340
359
  ### Detecting Missing Non-`NULL` Constraints
341
360
 
342
361
  If there's an unconditional presence validation on a column then it should be
343
- marked as non-`NULL`-able at the database level.
362
+ marked as non-`NULL`-able at the database level or should have a `IS NOT NULL`
363
+ constraint.
344
364
 
345
365
  In order to detect columns whose presence is required but that are marked
346
366
  `null: true` in the database run the following command:
@@ -362,8 +382,10 @@ This validator skips models whose corresponding database tables don't exist.
362
382
 
363
383
  Supported configuration options:
364
384
 
385
+ - `enabled` - set to `false` to disable the detector altogether
365
386
  - `ignore_tables` - tables whose columns should not be checked.
366
- - `ignore_columns` - columns, written as table.column, that should not be checked.
387
+ - `ignore_columns` - columns, written as table.column, that should not be
388
+ checked.
367
389
 
368
390
  ### Detecting Missing Presence Validations
369
391
 
@@ -389,8 +411,10 @@ This validator skips models whose corresponding database tables don't exist.
389
411
 
390
412
  Supported configuration options:
391
413
 
414
+ - `enabled` - set to `false` to disable the detector altogether
392
415
  - `ignore_models` - models whose underlying tables' columns should not be checked.
393
- - `ignore_columns` - specific attributes, written as Model.attribute, that should not be checked.
416
+ - `ignore_attributes` - specific attributes, written as Model.attribute, that
417
+ should not be checked.
394
418
 
395
419
  ### Detecting Incorrect Presence Validations on Boolean Columns
396
420
 
@@ -416,8 +440,43 @@ This validator skips models whose corresponding database tables don't exist.
416
440
 
417
441
  Supported configuration options:
418
442
 
443
+ - `enabled` - set to `false` to disable the detector altogether
419
444
  - `ignore_models` - models whose validators should not be checked.
420
- - `ignore_columns` - attributes, written as Model.attribute, whose validators should not be checked.
445
+ - `ignore_columns` - attributes, written as Model.attribute, whose validators
446
+ should not be checked.
447
+
448
+ ### Detecting Incorrect Length Validations
449
+
450
+ String length can be enforced by both the database and the application. If
451
+ there's a database limit then it's a good idea to add a model validation to
452
+ ensure user-friendly error messages. Similarly, if there's a model validator
453
+ without the corresponding database constraint then it's a good idea to add one
454
+ to avoid saving invalid models.
455
+
456
+ In order to detect columns whose length isn't validated properly run:
457
+
458
+ ```
459
+ bundle exec rake active_record_doctor:incorrect_length_validation
460
+ ```
461
+
462
+ The output of the command looks like this:
463
+
464
+ ```
465
+ set the maximum length in the validator of User.email (currently 32) and the database limit on users.email (currently 64) to the same value
466
+ add a length validator on User.address to enforce a maximum length of 64 defined on users.address
467
+ ```
468
+
469
+ The first message means the validator on `User.email` is checking for a
470
+ different maximum than the database limit on `users.email`. The second message
471
+ means there's a database limit on `users.address` without the corresponding
472
+ model validation.
473
+
474
+ Supported configuration options:
475
+
476
+ - `enabled` - set to `false` to disable the detector altogether
477
+ - `ignore_models` - models whose validators should not be checked.
478
+ - `ignore_columns` - attributes, written as Model.attribute, whose validators
479
+ should not be checked.
421
480
 
422
481
  ### Detecting Incorrect `dependent` Option on Associations
423
482
 
@@ -448,8 +507,10 @@ use `dependent: :destroy` or similar on Post.comments - the associated model has
448
507
 
449
508
  Supported configuration options:
450
509
 
510
+ - `enabled` - set to `false` to disable the detector altogether
451
511
  - `ignore_models` - models whose associations should not be checked.
452
- - `ignore_columns` - associations, written as Model.association, that should not be checked.
512
+ - `ignore_columns` - associations, written as Model.association, that should not
513
+ be checked.
453
514
 
454
515
  ### Detecting Primary Keys Having Short Integer Types
455
516
 
@@ -485,6 +546,7 @@ as all rows need to be rewritten.
485
546
 
486
547
  Supported configuration options:
487
548
 
549
+ - `enabled` - set to `false` to disable the detector altogether
488
550
  - `ignore_tables` - tables whose primary keys should not be checked.
489
551
 
490
552
  ### Detecting Mismatched Foreign Key Types
@@ -508,8 +570,10 @@ companies.user_id references a column of different type - foreign keys should be
508
570
 
509
571
  Supported configuration options:
510
572
 
573
+ - `enabled` - set to `false` to disable the detector altogether
511
574
  - `ignore_tables` - tables whose foreign keys should not be checked.
512
- - `ignore_columns` - foreign keys, written as table.column, that should not be checked.
575
+ - `ignore_columns` - foreign keys, written as table.column, that should not be
576
+ checked.
513
577
 
514
578
  ## Ruby and Rails Compatibility Policy
515
579
 
@@ -10,50 +10,67 @@ ActiveRecordDoctor.configure do
10
10
  ]
11
11
 
12
12
  detector :extraneous_indexes,
13
+ enabled: true,
13
14
  ignore_tables: [],
14
15
  ignore_indexes: []
15
16
 
16
17
  detector :incorrect_boolean_presence_validation,
18
+ enabled: true,
19
+ ignore_models: [],
20
+ ignore_attributes: []
21
+
22
+ detector :incorrect_length_validation,
23
+ enabled: true,
17
24
  ignore_models: [],
18
25
  ignore_attributes: []
19
26
 
20
27
  detector :incorrect_dependent_option,
28
+ enabled: true,
21
29
  ignore_models: [],
22
30
  ignore_associations: []
23
31
 
24
32
  detector :mismatched_foreign_key_type,
33
+ enabled: true,
25
34
  ignore_tables: [],
26
35
  ignore_columns: []
27
36
 
28
37
  detector :missing_foreign_keys,
38
+ enabled: true,
29
39
  ignore_tables: [],
30
40
  ignore_columns: []
31
41
 
32
42
  detector :missing_non_null_constraint,
43
+ enabled: true,
33
44
  ignore_tables: [],
34
45
  ignore_columns: []
35
46
 
36
47
  detector :missing_presence_validation,
48
+ enabled: true,
37
49
  ignore_models: [],
38
50
  ignore_attributes: []
39
51
 
40
52
  detector :missing_unique_indexes,
53
+ enabled: true,
41
54
  ignore_models: [],
42
55
  ignore_columns: []
43
56
 
44
57
  detector :short_primary_key_type,
58
+ enabled: true,
45
59
  ignore_tables: []
46
60
 
47
61
  detector :undefined_table_references,
62
+ enabled: true,
48
63
  ignore_models: []
49
64
 
50
65
  detector :unindexed_deleted_at,
66
+ enabled: true,
51
67
  ignore_tables: [],
52
68
  ignore_columns: [],
53
69
  ignore_indexes: [],
54
70
  column_names: ["deleted_at", "discarded_at"]
55
71
 
56
72
  detector :unindexed_foreign_keys,
73
+ enabled: true,
57
74
  ignore_tables: [],
58
75
  ignore_columns: []
59
76
  end
@@ -4,8 +4,14 @@ module ActiveRecordDoctor
4
4
  module Detectors
5
5
  # Base class for all active_record_doctor detectors.
6
6
  class Base
7
+ BASE_CONFIG = {
8
+ enabled: {
9
+ description: "set to false to disable the detector altogether"
10
+ }
11
+ }.freeze
12
+
7
13
  class << self
8
- attr_reader :description, :config
14
+ attr_reader :description
9
15
 
10
16
  def run(config, io)
11
17
  new(config, io).run
@@ -15,6 +21,10 @@ module ActiveRecordDoctor
15
21
  name.demodulize.underscore.to_sym
16
22
  end
17
23
 
24
+ def config
25
+ @config.merge(BASE_CONFIG)
26
+ end
27
+
18
28
  def locals_and_globals
19
29
  locals = []
20
30
  globals = []
@@ -37,7 +47,7 @@ module ActiveRecordDoctor
37
47
  def run
38
48
  @problems = []
39
49
 
40
- detect
50
+ detect if config(:enabled)
41
51
  @problems.each do |problem|
42
52
  @io.puts(message(**problem))
43
53
  end
@@ -100,22 +110,6 @@ module ActiveRecordDoctor
100
110
  end
101
111
  end
102
112
 
103
- def 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
113
  def primary_key(table_name)
120
114
  primary_key_name = connection.primary_key(table_name)
121
115
  return nil if primary_key_name.nil?
@@ -131,16 +125,48 @@ module ActiveRecordDoctor
131
125
  @views ||=
132
126
  if connection.respond_to?(:views)
133
127
  connection.views
134
- elsif connection.adapter_name == "PostgreSQL"
135
- ActiveRecord::Base.connection.execute(<<-SQL).map { |tuple| tuple.fetch("relname") }
136
- SELECT c.relname FROM pg_class c WHERE c.relkind IN ('m', 'v')
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'
137
135
  SQL
138
136
  else
139
137
  # We don't support this Rails/database combination yet.
140
- nil
138
+ []
141
139
  end
142
140
  end
143
141
 
142
+ def not_null_check_constraint_exists?(table, column)
143
+ check_constraints(table).any? do |definition|
144
+ definition =~ /\A#{column.name} IS NOT NULL\z/i ||
145
+ definition =~ /\A#{connection.quote_column_name(column.name)} IS NOT NULL\z/i
146
+ end
147
+ end
148
+
149
+ def check_constraints(table_name)
150
+ # ActiveRecord 6.1+
151
+ if connection.respond_to?(:supports_check_constraints?) && connection.supports_check_constraints?
152
+ connection.check_constraints(table_name).select(&:validated?).map(&:expression)
153
+ elsif postgresql?
154
+ definitions =
155
+ connection.select_values(<<-SQL)
156
+ SELECT pg_get_constraintdef(oid, true)
157
+ FROM pg_constraint
158
+ WHERE contype = 'c'
159
+ AND convalidated
160
+ AND conrelid = #{connection.quote(table_name)}::regclass
161
+ SQL
162
+
163
+ definitions.map { |definition| definition[/CHECK \((.+)\)/m, 1] }
164
+ else
165
+ # We don't support this Rails/database combination yet.
166
+ []
167
+ end
168
+ end
169
+
144
170
  def models(except: [])
145
171
  ActiveRecord::Base.descendants.reject do |model|
146
172
  model.name.start_with?("HABTM_") || except.include?(model.name)
@@ -150,6 +176,10 @@ module ActiveRecordDoctor
150
176
  def underscored_name
151
177
  self.class.underscored_name
152
178
  end
179
+
180
+ def postgresql?
181
+ ["PostgreSQL", "PostGIS"].include?(connection.adapter_name)
182
+ end
153
183
  end
154
184
  end
155
185
  end
@@ -35,18 +35,20 @@ module ActiveRecordDoctor
35
35
  def subindexes_of_multi_column_indexes
36
36
  tables(except: config(:ignore_tables)).each do |table|
37
37
  indexes = indexes(table)
38
- maximal_indexes = indexes.select { |index| maximal?(indexes, index) }
39
38
 
40
39
  indexes.each do |index|
41
- next if maximal_indexes.include?(index)
40
+ next if config(:ignore_indexes).include?(index.name)
42
41
 
43
- replacement_indexes = maximal_indexes.select do |maximum_index|
44
- cover?(maximum_index, index)
45
- end.map(&:name).sort
42
+ replacement_indexes = indexes.select do |other_index|
43
+ index != other_index && replaceable_with?(index, other_index)
44
+ end
46
45
 
47
- next if config(:ignore_indexes).include?(index.name)
46
+ next if replacement_indexes.empty?
48
47
 
49
- problem!(extraneous_index: index.name, replacement_indexes: replacement_indexes)
48
+ problem!(
49
+ extraneous_index: index.name,
50
+ replacement_indexes: replacement_indexes.map(&:name).sort
51
+ )
50
52
  end
51
53
  end
52
54
  end
@@ -55,33 +57,35 @@ module ActiveRecordDoctor
55
57
  tables(except: config(:ignore_tables)).each do |table|
56
58
  indexes(table).each do |index|
57
59
  next if config(:ignore_indexes).include?(index.name)
58
- next if index.columns != ["id"]
60
+
61
+ primary_key = connection.primary_key(table)
62
+ next if index.columns != [primary_key] || index.where
59
63
 
60
64
  problem!(extraneous_index: index.name, replacement_indexes: nil)
61
65
  end
62
66
  end
63
67
  end
64
68
 
65
- def maximal?(indexes, index)
66
- indexes.all? do |another_index|
67
- index == another_index || !cover?(another_index, index)
68
- end
69
- end
70
-
71
- # Does lhs cover rhs?
72
- def cover?(lhs, rhs)
73
- return false unless compatible_options?(lhs, rhs)
69
+ def replaceable_with?(index1, index2)
70
+ return false if index1.type != index2.type
71
+ return false if index1.using != index2.using
72
+ return false if index1.where != index2.where
73
+ return false if opclasses(index1) != opclasses(index2)
74
74
 
75
- case [lhs.unique, rhs.unique]
75
+ case [index1.unique, index2.unique]
76
76
  when [true, true]
77
- lhs.columns == rhs.columns
78
- when [false, true]
77
+ (index2.columns - index1.columns).empty?
78
+ when [true, false]
79
79
  false
80
80
  else
81
- prefix?(rhs, lhs)
81
+ prefix?(index1, index2)
82
82
  end
83
83
  end
84
84
 
85
+ def opclasses(index)
86
+ index.respond_to?(:opclasses) ? index.opclasses : nil
87
+ end
88
+
85
89
  def prefix?(lhs, rhs)
86
90
  lhs.columns.count <= rhs.columns.count &&
87
91
  rhs.columns[0...lhs.columns.count] == lhs.columns
@@ -90,25 +94,6 @@ module ActiveRecordDoctor
90
94
  def indexes(table_name)
91
95
  super.select { |index| index.columns.is_a?(Array) }
92
96
  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
112
97
  end
113
98
  end
114
99
  end
@@ -26,8 +26,7 @@ module ActiveRecordDoctor
26
26
 
27
27
  def detect
28
28
  models(except: config(:ignore_models)).each do |model|
29
- next if model.table_name.nil?
30
- next unless table_exists?(model.table_name)
29
+ next unless model.table_exists?
31
30
 
32
31
  connection.columns(model.table_name).each do |column|
33
32
  next if config(:ignore_attributes).include?("#{model.name}.#{column.name}")
@@ -18,22 +18,31 @@ module ActiveRecordDoctor
18
18
 
19
19
  private
20
20
 
21
- def message(model:, association:, problem:)
21
+ def message(model:, association:, problem:, associated_models:)
22
+ associated_models.sort!
23
+
24
+ models_part =
25
+ if associated_models.length == 1
26
+ "model #{associated_models[0]} has"
27
+ else
28
+ "models #{associated_models.join(', ')} have"
29
+ end
30
+
22
31
  # rubocop:disable Layout/LineLength
23
32
  case problem
24
33
  when :suggest_destroy
25
- "use `dependent: :destroy` or similar on #{model}.#{association} - the associated model has callbacks that are currently skipped"
34
+ "use `dependent: :destroy` or similar on #{model}.#{association} - the associated #{models_part} callbacks that are currently skipped"
26
35
  when :suggest_delete
27
- "use `dependent: :delete` or similar on #{model}.#{association} - the associated model has no callbacks and can be deleted without loading"
36
+ "use `dependent: :delete` or similar on #{model}.#{association} - the associated #{models_part} no callbacks and can be deleted without loading"
28
37
  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"
38
+ "use `dependent: :delete_all` or similar on #{model}.#{association} - associated #{models_part} no validations and can be deleted in bulk"
30
39
  end
31
40
  # rubocop:enable Layout/LineLength
32
41
  end
33
42
 
34
43
  def detect
35
44
  models(except: config(:ignore_models)).each do |model|
36
- next if model.table_name.nil?
45
+ next unless model.table_exists?
37
46
 
38
47
  associations = model.reflect_on_all_associations(:has_many) +
39
48
  model.reflect_on_all_associations(:has_one) +
@@ -42,7 +51,16 @@ module ActiveRecordDoctor
42
51
  associations.each do |association|
43
52
  next if config(:ignore_associations).include?("#{model.name}.#{association.name}")
44
53
 
45
- if callback_action(association) == :invoke && deletable?(association.klass)
54
+ associated_models =
55
+ if association.polymorphic?
56
+ models_having(as: association.name)
57
+ else
58
+ [association.klass]
59
+ end
60
+
61
+ deletable_models, destroyable_models = associated_models.partition { |klass| deletable?(klass) }
62
+
63
+ if callback_action(association) == :invoke && destroyable_models.empty? && deletable_models.present?
46
64
  suggestion =
47
65
  case association.macro
48
66
  when :has_many then :suggest_delete_all
@@ -50,14 +68,27 @@ module ActiveRecordDoctor
50
68
  else raise("unsupported association type #{association.macro}")
51
69
  end
52
70
 
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)
71
+ problem!(model: model.name, association: association.name, problem: suggestion,
72
+ associated_models: deletable_models.map(&:name))
73
+ elsif callback_action(association) == :skip && destroyable_models.present?
74
+ problem!(model: model.name, association: association.name, problem: :suggest_destroy,
75
+ associated_models: destroyable_models.map(&:name))
56
76
  end
57
77
  end
58
78
  end
59
79
  end
60
80
 
81
+ def models_having(as:)
82
+ models.select do |model|
83
+ associations = model.reflect_on_all_associations(:has_one) +
84
+ model.reflect_on_all_associations(:has_many)
85
+
86
+ associations.any? do |association|
87
+ association.options[:as] == as
88
+ end
89
+ end
90
+ end
91
+
61
92
  def callback_action(reflection)
62
93
  case reflection.options[:dependent]
63
94
  when :delete, :delete_all then :skip