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.
- checksums.yaml +4 -4
- data/README.md +83 -19
- data/lib/active_record_doctor/config/default.rb +17 -0
- data/lib/active_record_doctor/detectors/base.rb +52 -22
- data/lib/active_record_doctor/detectors/extraneous_indexes.rb +25 -40
- data/lib/active_record_doctor/detectors/incorrect_boolean_presence_validation.rb +1 -2
- data/lib/active_record_doctor/detectors/incorrect_dependent_option.rb +40 -9
- data/lib/active_record_doctor/detectors/incorrect_length_validation.rb +63 -0
- data/lib/active_record_doctor/detectors/missing_non_null_constraint.rb +2 -1
- data/lib/active_record_doctor/detectors/missing_presence_validation.rb +3 -4
- data/lib/active_record_doctor/detectors/missing_unique_indexes.rb +65 -15
- data/lib/active_record_doctor/detectors/short_primary_key_type.rb +5 -1
- data/lib/active_record_doctor/detectors/undefined_table_references.rb +1 -3
- data/lib/active_record_doctor/detectors/unindexed_deleted_at.rb +2 -3
- data/lib/active_record_doctor/version.rb +1 -1
- data/lib/active_record_doctor.rb +1 -0
- data/lib/generators/active_record_doctor/add_indexes/add_indexes_generator.rb +5 -5
- data/test/active_record_doctor/detectors/disable_test.rb +30 -0
- data/test/active_record_doctor/detectors/extraneous_indexes_test.rb +34 -0
- data/test/active_record_doctor/detectors/incorrect_dependent_option_test.rb +105 -7
- data/test/active_record_doctor/detectors/incorrect_length_validation_test.rb +105 -0
- data/test/active_record_doctor/detectors/missing_non_null_constraint_test.rb +34 -0
- data/test/active_record_doctor/detectors/missing_presence_validation_test.rb +37 -1
- data/test/active_record_doctor/detectors/missing_unique_indexes_test.rb +167 -3
- data/test/active_record_doctor/detectors/short_primary_key_type_test.rb +27 -19
- data/test/active_record_doctor/detectors/unindexed_deleted_at_test.rb +9 -3
- data/test/setup.rb +6 -2
- metadata +8 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1e1bf7642b1c7b471fbf78956825067ff070d261d4c47a182d1aaf00f10b98b7
|
4
|
+
data.tar.gz: 6e6fc746327d6db30c282d7964ae778b254107281d9626a251f8593d6ce85db4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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`
|
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
|
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
|
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
|
247
|
-
|
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
|
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
|
-
- `
|
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
|
-
|
318
|
-
order to be robust. Otherwise you risk inserting
|
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,
|
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
|
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
|
-
- `
|
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
|
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
|
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
|
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
|
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
|
135
|
-
ActiveRecord::Base.connection.
|
136
|
-
SELECT
|
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
|
-
|
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
|
40
|
+
next if config(:ignore_indexes).include?(index.name)
|
42
41
|
|
43
|
-
replacement_indexes =
|
44
|
-
|
45
|
-
end
|
42
|
+
replacement_indexes = indexes.select do |other_index|
|
43
|
+
index != other_index && replaceable_with?(index, other_index)
|
44
|
+
end
|
46
45
|
|
47
|
-
next if
|
46
|
+
next if replacement_indexes.empty?
|
48
47
|
|
49
|
-
problem!(
|
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
|
-
|
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
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
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 [
|
75
|
+
case [index1.unique, index2.unique]
|
76
76
|
when [true, true]
|
77
|
-
|
78
|
-
when [
|
77
|
+
(index2.columns - index1.columns).empty?
|
78
|
+
when [true, false]
|
79
79
|
false
|
80
80
|
else
|
81
|
-
prefix?(
|
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
|
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
|
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
|
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
|
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
|
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
|
-
|
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
|
-
|
55
|
-
|
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
|