active_record_doctor 1.9.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 (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