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.
- 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
|