active_record_doctor 1.10.0 → 1.12.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 +15 -15
- data/lib/active_record_doctor/detectors/base.rb +194 -53
- data/lib/active_record_doctor/detectors/extraneous_indexes.rb +36 -34
- data/lib/active_record_doctor/detectors/incorrect_boolean_presence_validation.rb +2 -5
- data/lib/active_record_doctor/detectors/incorrect_dependent_option.rb +87 -37
- data/lib/active_record_doctor/detectors/incorrect_length_validation.rb +7 -10
- data/lib/active_record_doctor/detectors/mismatched_foreign_key_type.rb +16 -9
- data/lib/active_record_doctor/detectors/missing_foreign_keys.rb +2 -4
- data/lib/active_record_doctor/detectors/missing_non_null_constraint.rb +13 -11
- data/lib/active_record_doctor/detectors/missing_presence_validation.rb +14 -7
- data/lib/active_record_doctor/detectors/missing_unique_indexes.rb +70 -35
- data/lib/active_record_doctor/detectors/short_primary_key_type.rb +4 -4
- data/lib/active_record_doctor/detectors/undefined_table_references.rb +2 -2
- data/lib/active_record_doctor/detectors/unindexed_deleted_at.rb +5 -13
- data/lib/active_record_doctor/detectors/unindexed_foreign_keys.rb +35 -11
- data/lib/active_record_doctor/logger/dummy.rb +11 -0
- data/lib/active_record_doctor/logger/hierarchical.rb +22 -0
- data/lib/active_record_doctor/logger.rb +6 -0
- data/lib/active_record_doctor/rake/task.rb +10 -1
- data/lib/active_record_doctor/runner.rb +8 -3
- data/lib/active_record_doctor/utils.rb +21 -0
- data/lib/active_record_doctor/version.rb +1 -1
- data/lib/active_record_doctor.rb +5 -0
- data/lib/generators/active_record_doctor/add_indexes/add_indexes_generator.rb +14 -14
- data/test/active_record_doctor/detectors/disable_test.rb +1 -1
- data/test/active_record_doctor/detectors/extraneous_indexes_test.rb +59 -6
- data/test/active_record_doctor/detectors/incorrect_boolean_presence_validation_test.rb +7 -7
- data/test/active_record_doctor/detectors/incorrect_dependent_option_test.rb +175 -57
- data/test/active_record_doctor/detectors/incorrect_length_validation_test.rb +16 -14
- data/test/active_record_doctor/detectors/mismatched_foreign_key_type_test.rb +35 -1
- data/test/active_record_doctor/detectors/missing_non_null_constraint_test.rb +46 -23
- data/test/active_record_doctor/detectors/missing_presence_validation_test.rb +55 -27
- data/test/active_record_doctor/detectors/missing_unique_indexes_test.rb +216 -47
- data/test/active_record_doctor/detectors/short_primary_key_type_test.rb +5 -0
- data/test/active_record_doctor/detectors/undefined_table_references_test.rb +11 -13
- data/test/active_record_doctor/detectors/unindexed_foreign_keys_test.rb +39 -1
- data/test/active_record_doctor/runner_test.rb +18 -19
- data/test/generators/active_record_doctor/add_indexes/add_indexes_generator_test.rb +16 -6
- data/test/setup.rb +10 -6
- metadata +23 -7
- data/test/model_factory.rb +0 -128
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d848e296c39f7994781bd645261eb45235450be36058fbdf7368ec6358ebeba5
|
4
|
+
data.tar.gz: a63cbbfe5b43fb3bf6daeaba24a6298073643d5b5c96696c27c084106fe54594
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 47411eea54d4c21329459dc7490090a535c53e9f3be912268ceed2f6e2588246cb07c9b90e758559d9b0c08de319f3dc6a4735c79511bc30c2ec7b058104c08e
|
7
|
+
data.tar.gz: 9596cb799a4346b3ec60562876750990b486efc7daebfda0834b028bf2db9075f04bf6b028185df6076ede3a7227cb9755a6ac9e8db609431d063261f3c0732d
|
data/README.md
CHANGED
@@ -7,7 +7,7 @@ can detect:
|
|
7
7
|
* unindexed `deleted_at` columns - [`active_record_doctor:unindexed_deleted_at`](#detecting-unindexed-deleted_at-columns)
|
8
8
|
* missing foreign key constraints - [`active_record_doctor:missing_foreign_keys`](#detecting-missing-foreign-key-constraints)
|
9
9
|
* models referencing undefined tables - [`active_record_doctor:undefined_table_references`](#detecting-models-referencing-undefined-tables)
|
10
|
-
* uniqueness validations not backed by
|
10
|
+
* uniqueness validations not backed by a unique index - [`active_record_doctor:missing_unique_indexes`](#detecting-uniqueness-validations-not-backed-by-an-index)
|
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)
|
@@ -158,7 +158,7 @@ three-step process:
|
|
158
158
|
```
|
159
159
|
|
160
160
|
2. Remove columns that should _not_ be indexed from `unindexed_foreign_keys.txt`
|
161
|
-
as a column can look like a foreign key (i.e.
|
161
|
+
as a column can look like a foreign key (i.e. ending with `_id`) without being
|
162
162
|
one.
|
163
163
|
|
164
164
|
3. Generate the migrations
|
@@ -209,7 +209,7 @@ To discover such indexes automatically just follow these steps:
|
|
209
209
|
|
210
210
|
3. Create a migration to drop the indexes.
|
211
211
|
|
212
|
-
The indexes aren't dropped automatically because there
|
212
|
+
The indexes aren't dropped automatically because there are usually just a few of
|
213
213
|
them and it's a good idea to double-check that you won't drop something
|
214
214
|
necessary.
|
215
215
|
|
@@ -265,11 +265,11 @@ Supported configuration options:
|
|
265
265
|
If `users.profile_id` references a row in `profiles` then this can be expressed
|
266
266
|
at the database level with a foreign key constraint. It _forces_
|
267
267
|
`users.profile_id` to point to an existing row in `profiles`. The problem is
|
268
|
-
that in many legacy Rails apps the constraint isn't enforced at the database
|
268
|
+
that in many legacy Rails apps, the constraint isn't enforced at the database
|
269
269
|
level.
|
270
270
|
|
271
271
|
`active_record_doctor` can automatically detect foreign keys that could benefit
|
272
|
-
from a foreign key constraint (a future version will generate a
|
272
|
+
from a foreign key constraint (a future version will generate a migration that
|
273
273
|
add the constraint; for now, it's your job). You can obtain the list of foreign
|
274
274
|
keys with the following command:
|
275
275
|
|
@@ -307,7 +307,7 @@ before they hit production.
|
|
307
307
|
* Rails 5+ and _any_ database or
|
308
308
|
* Rails 4.2 with PostgreSQL.
|
309
309
|
|
310
|
-
The only
|
310
|
+
The only thing you need to do is run:
|
311
311
|
|
312
312
|
```
|
313
313
|
bundle exec rake active_record_doctor:undefined_table_references
|
@@ -320,7 +320,7 @@ this:
|
|
320
320
|
Contract references a non-existent table or view named contract_records
|
321
321
|
```
|
322
322
|
|
323
|
-
On top of that `rake` will exit with status code of 1. This allows you to use
|
323
|
+
On top of that `rake` will exit with a status code of 1. This allows you to use
|
324
324
|
this check as part of your Continuous Integration pipeline.
|
325
325
|
|
326
326
|
Supported configuration options:
|
@@ -333,7 +333,7 @@ Supported configuration options:
|
|
333
333
|
|
334
334
|
Model-level uniqueness validations and `has_one` associations should be backed
|
335
335
|
by a database index in order to be robust. Otherwise you risk inserting
|
336
|
-
duplicate values under heavy load.
|
336
|
+
duplicate values under a heavy load.
|
337
337
|
|
338
338
|
In order to detect such validations run:
|
339
339
|
|
@@ -475,7 +475,7 @@ Supported configuration options:
|
|
475
475
|
|
476
476
|
- `enabled` - set to `false` to disable the detector altogether
|
477
477
|
- `ignore_models` - models whose validators should not be checked.
|
478
|
-
- `
|
478
|
+
- `ignore_attributes` - attributes, written as Model.attribute, whose validators
|
479
479
|
should not be checked.
|
480
480
|
|
481
481
|
### Detecting Incorrect `dependent` Option on Associations
|
@@ -489,7 +489,7 @@ This can lead to two types of errors:
|
|
489
489
|
- Using `delete_all` when dependent models define callbacks - they will NOT be
|
490
490
|
invoked.
|
491
491
|
- Using `destroy` when dependent models define no callbacks - dependent models
|
492
|
-
will be loaded one
|
492
|
+
will be loaded one by one with no reason
|
493
493
|
|
494
494
|
In order to detect associations affected by the two aforementioned problems run
|
495
495
|
the following command:
|
@@ -509,7 +509,7 @@ Supported configuration options:
|
|
509
509
|
|
510
510
|
- `enabled` - set to `false` to disable the detector altogether
|
511
511
|
- `ignore_models` - models whose associations should not be checked.
|
512
|
-
- `
|
512
|
+
- `ignore_associations` - associations, written as Model.association, that should not
|
513
513
|
be checked.
|
514
514
|
|
515
515
|
### Detecting Primary Keys Having Short Integer Types
|
@@ -530,8 +530,8 @@ The output of the command looks like this:
|
|
530
530
|
change the type of companies.id to bigint
|
531
531
|
```
|
532
532
|
|
533
|
-
The above means `
|
534
|
-
example migration to accomplish this looks
|
533
|
+
The above means `companies.id` should be migrated to a wider integer type. An
|
534
|
+
example migration to accomplish this looks like this:
|
535
535
|
|
536
536
|
```ruby
|
537
537
|
class ChangeCompaniesPrimaryKeyType < ActiveRecord::Migration[5.1]
|
@@ -565,7 +565,7 @@ bundle exec rake active_record_doctor:mismatched_foreign_key_type
|
|
565
565
|
The output of the command looks like this:
|
566
566
|
|
567
567
|
```
|
568
|
-
companies.user_id references a column of different type - foreign keys should be of the same type as the referenced column
|
568
|
+
companies.user_id references a column of a different type - foreign keys should be of the same type as the referenced column
|
569
569
|
```
|
570
570
|
|
571
571
|
Supported configuration options:
|
@@ -584,7 +584,7 @@ combinations of Ruby and Rails versions. Specifically:
|
|
584
584
|
supported by `active_record_doctor`.
|
585
585
|
2. If a Ruby version is compatible with a supported Rails version then it's
|
586
586
|
also supported by `active_record_doctor`.
|
587
|
-
3. Only most recent teeny Ruby versions and patch Rails versions are supported.
|
587
|
+
3. Only the most recent teeny Ruby versions and patch Rails versions are supported.
|
588
588
|
|
589
589
|
## Author
|
590
590
|
|
@@ -13,8 +13,8 @@ module ActiveRecordDoctor
|
|
13
13
|
class << self
|
14
14
|
attr_reader :description
|
15
15
|
|
16
|
-
def run(
|
17
|
-
new(
|
16
|
+
def run(*args, **kwargs, &block)
|
17
|
+
new(*args, **kwargs, &block).run
|
18
18
|
end
|
19
19
|
|
20
20
|
def underscored_name
|
@@ -38,23 +38,36 @@ module ActiveRecordDoctor
|
|
38
38
|
end
|
39
39
|
end
|
40
40
|
|
41
|
-
def initialize(config
|
41
|
+
def initialize(config:, logger:, io:)
|
42
42
|
@problems = []
|
43
43
|
@config = config
|
44
|
+
@logger = logger
|
44
45
|
@io = io
|
45
46
|
end
|
46
47
|
|
47
48
|
def run
|
48
|
-
|
49
|
+
log(underscored_name) do
|
50
|
+
@problems = []
|
49
51
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
52
|
+
if config(:enabled)
|
53
|
+
detect
|
54
|
+
else
|
55
|
+
log("disabled; skipping")
|
56
|
+
end
|
54
57
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
+
@problems.each do |problem|
|
59
|
+
@io.puts(message(**problem))
|
60
|
+
end
|
61
|
+
|
62
|
+
success = @problems.empty?
|
63
|
+
if success
|
64
|
+
log("No problems found")
|
65
|
+
else
|
66
|
+
log("Found #{@problems.count} problem(s)")
|
67
|
+
end
|
68
|
+
@problems = nil
|
69
|
+
success
|
70
|
+
end
|
58
71
|
end
|
59
72
|
|
60
73
|
private
|
@@ -79,7 +92,16 @@ module ActiveRecordDoctor
|
|
79
92
|
raise("#message should be implemented by a subclass")
|
80
93
|
end
|
81
94
|
|
95
|
+
def log(message, &block)
|
96
|
+
@logger.log(message, &block)
|
97
|
+
end
|
98
|
+
|
82
99
|
def problem!(**attrs)
|
100
|
+
log("Problem found") do
|
101
|
+
attrs.each do |key, value|
|
102
|
+
log("#{key}: #{value.inspect}")
|
103
|
+
end
|
104
|
+
end
|
83
105
|
@problems << attrs
|
84
106
|
end
|
85
107
|
|
@@ -91,23 +113,8 @@ module ActiveRecordDoctor
|
|
91
113
|
@connection ||= ActiveRecord::Base.connection
|
92
114
|
end
|
93
115
|
|
94
|
-
def indexes(table_name
|
95
|
-
connection.indexes(table_name)
|
96
|
-
except.include?(index.name)
|
97
|
-
end
|
98
|
-
end
|
99
|
-
|
100
|
-
def tables(except: [])
|
101
|
-
tables =
|
102
|
-
if ActiveRecord::VERSION::STRING >= "5.1"
|
103
|
-
connection.tables
|
104
|
-
else
|
105
|
-
connection.data_sources
|
106
|
-
end
|
107
|
-
|
108
|
-
tables.reject do |table|
|
109
|
-
except.include?(table)
|
110
|
-
end
|
116
|
+
def indexes(table_name)
|
117
|
+
connection.indexes(table_name)
|
111
118
|
end
|
112
119
|
|
113
120
|
def primary_key(table_name)
|
@@ -121,24 +128,6 @@ module ActiveRecordDoctor
|
|
121
128
|
connection.columns(table_name).find { |column| column.name == column_name }
|
122
129
|
end
|
123
130
|
|
124
|
-
def views
|
125
|
-
@views ||=
|
126
|
-
if connection.respond_to?(:views)
|
127
|
-
connection.views
|
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'
|
135
|
-
SQL
|
136
|
-
else
|
137
|
-
# We don't support this Rails/database combination yet.
|
138
|
-
[]
|
139
|
-
end
|
140
|
-
end
|
141
|
-
|
142
131
|
def not_null_check_constraint_exists?(table, column)
|
143
132
|
check_constraints(table).any? do |definition|
|
144
133
|
definition =~ /\A#{column.name} IS NOT NULL\z/i ||
|
@@ -150,7 +139,7 @@ module ActiveRecordDoctor
|
|
150
139
|
# ActiveRecord 6.1+
|
151
140
|
if connection.respond_to?(:supports_check_constraints?) && connection.supports_check_constraints?
|
152
141
|
connection.check_constraints(table_name).select(&:validated?).map(&:expression)
|
153
|
-
elsif postgresql?
|
142
|
+
elsif Utils.postgresql?(connection)
|
154
143
|
definitions =
|
155
144
|
connection.select_values(<<-SQL)
|
156
145
|
SELECT pg_get_constraintdef(oid, true)
|
@@ -167,18 +156,170 @@ module ActiveRecordDoctor
|
|
167
156
|
end
|
168
157
|
end
|
169
158
|
|
170
|
-
def models
|
171
|
-
ActiveRecord::Base.descendants
|
172
|
-
model.name.start_with?("HABTM_") || except.include?(model.name)
|
173
|
-
end
|
159
|
+
def models
|
160
|
+
ActiveRecord::Base.descendants
|
174
161
|
end
|
175
162
|
|
176
163
|
def underscored_name
|
177
164
|
self.class.underscored_name
|
178
165
|
end
|
179
166
|
|
180
|
-
def
|
181
|
-
|
167
|
+
def each_model(except: [], abstract: nil, existing_tables_only: false)
|
168
|
+
log("Iterating over Active Record models") do
|
169
|
+
models.each do |model|
|
170
|
+
case
|
171
|
+
when model.name.start_with?("HABTM_")
|
172
|
+
log("#{model.name} - has-belongs-to-many model; skipping")
|
173
|
+
when except.include?(model.name)
|
174
|
+
log("#{model.name} - ignored via the configuration; skipping")
|
175
|
+
when abstract && !model.abstract_class?
|
176
|
+
log("#{model.name} - non-abstract model; skipping")
|
177
|
+
when abstract == false && model.abstract_class?
|
178
|
+
log("#{model.name} - abstract model; skipping")
|
179
|
+
when existing_tables_only && (model.table_name.nil? || !model.table_exists?)
|
180
|
+
log("#{model.name} - backed by a non-existent table #{model.table_name}; skipping")
|
181
|
+
else
|
182
|
+
log(model.name) do
|
183
|
+
yield(model)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def each_index(table_name, except: [], multicolumn_only: false)
|
191
|
+
indexes = connection.indexes(table_name)
|
192
|
+
|
193
|
+
message =
|
194
|
+
if multicolumn_only
|
195
|
+
"Iterating over multi-column indexes on #{table_name}"
|
196
|
+
else
|
197
|
+
"Iterating over indexes on #{table_name}"
|
198
|
+
end
|
199
|
+
|
200
|
+
log(message) do
|
201
|
+
indexes.each do |index|
|
202
|
+
case
|
203
|
+
when except.include?(index.name)
|
204
|
+
log("#{index.name} - ignored via the configuration; skipping")
|
205
|
+
when multicolumn_only && !index.columns.is_a?(Array)
|
206
|
+
log("#{index.name} - single-column index; skipping")
|
207
|
+
else
|
208
|
+
log("Index #{index.name} on #{table_name}") do
|
209
|
+
yield(index, indexes)
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
def each_attribute(model, except: [], type: nil)
|
217
|
+
log("Iterating over attributes of #{model.name}") do
|
218
|
+
connection.columns(model.table_name).each do |column|
|
219
|
+
case
|
220
|
+
when except.include?("#{model.name}.#{column.name}")
|
221
|
+
log("#{model.name}.#{column.name} - ignored via the configuration; skipping")
|
222
|
+
when type && !Array(type).include?(column.type)
|
223
|
+
log("#{model.name}.#{column.name} - ignored due to the #{column.type} type; skipping")
|
224
|
+
else
|
225
|
+
log("#{model.name}.#{column.name}") do
|
226
|
+
yield(column)
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
def each_column(table_name, only: nil, except: [])
|
234
|
+
log("Iterating over columns of #{table_name}") do
|
235
|
+
connection.columns(table_name).each do |column|
|
236
|
+
case
|
237
|
+
when except.include?("#{table_name}.#{column.name}")
|
238
|
+
log("#{column.name} - ignored via the configuration; skipping")
|
239
|
+
when only.nil? || only.include?(column.name)
|
240
|
+
log(column.name.to_s) do
|
241
|
+
yield(column)
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
def each_foreign_key(table_name)
|
249
|
+
log("Iterating over foreign keys on #{table_name}") do
|
250
|
+
connection.foreign_keys(table_name).each do |foreign_key|
|
251
|
+
log("#{foreign_key.name} - #{foreign_key.from_table}(#{foreign_key.options[:column]}) to #{foreign_key.to_table}(#{foreign_key.options[:primary_key]})") do # rubocop:disable Layout/LineLength
|
252
|
+
yield(foreign_key)
|
253
|
+
end
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
def each_table(except: [])
|
259
|
+
tables =
|
260
|
+
if ActiveRecord::VERSION::STRING >= "5.1"
|
261
|
+
connection.tables
|
262
|
+
else
|
263
|
+
connection.data_sources
|
264
|
+
end
|
265
|
+
|
266
|
+
log("Iterating over tables") do
|
267
|
+
tables.each do |table|
|
268
|
+
case
|
269
|
+
when except.include?(table)
|
270
|
+
log("#{table} - ignored via the configuration; skipping")
|
271
|
+
else
|
272
|
+
log(table) do
|
273
|
+
yield(table)
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
def each_data_source(except: [])
|
281
|
+
log("Iterating over data sources") do
|
282
|
+
connection.data_sources.each do |data_source|
|
283
|
+
if except.include?(data_source)
|
284
|
+
log("#{data_source} - ignored via the configuration; skipping")
|
285
|
+
else
|
286
|
+
log(data_source) do
|
287
|
+
yield(data_source)
|
288
|
+
end
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
def each_association(model, except: [], type: [:has_many, :has_one, :belongs_to], has_scope: nil, through: nil)
|
295
|
+
type = Array(type)
|
296
|
+
|
297
|
+
log("Iterating over associations on #{model.name}") do
|
298
|
+
associations = type.map do |type1|
|
299
|
+
# Skip inherited associations from STI to prevent them
|
300
|
+
# from being reported multiple times on subclasses.
|
301
|
+
model.reflect_on_all_associations(type1) - model.superclass.reflect_on_all_associations(type1)
|
302
|
+
end.flatten
|
303
|
+
|
304
|
+
associations.each do |association|
|
305
|
+
case
|
306
|
+
when except.include?("#{model.name}.#{association.name}")
|
307
|
+
log("#{model.name}.#{association.name} - ignored via the configuration; skipping")
|
308
|
+
when through && !association.is_a?(ActiveRecord::Reflection::ThroughReflection)
|
309
|
+
log("#{model.name}.#{association.name} - is not a through association; skipping")
|
310
|
+
when through == false && association.is_a?(ActiveRecord::Reflection::ThroughReflection)
|
311
|
+
log("#{model.name}.#{association.name} - is a through association; skipping")
|
312
|
+
when has_scope && association.scope.nil?
|
313
|
+
log("#{model.name}.#{association.name} - doesn't have a scope; skipping")
|
314
|
+
when has_scope == false && association.scope
|
315
|
+
log("#{model.name}.#{association.name} - has a scope; skipping")
|
316
|
+
else
|
317
|
+
log("#{association.macro} :#{association.name}") do
|
318
|
+
yield(association)
|
319
|
+
end
|
320
|
+
end
|
321
|
+
end
|
322
|
+
end
|
182
323
|
end
|
183
324
|
end
|
184
325
|
end
|
@@ -19,11 +19,13 @@ module ActiveRecordDoctor
|
|
19
19
|
|
20
20
|
private
|
21
21
|
|
22
|
-
def message(extraneous_index:, replacement_indexes:)
|
22
|
+
def message(table:, extraneous_index:, replacement_indexes:)
|
23
23
|
if replacement_indexes.nil?
|
24
|
-
"remove #{extraneous_index} - coincides with the primary key on the table"
|
24
|
+
"remove #{extraneous_index} from #{table} - coincides with the primary key on the table"
|
25
25
|
else
|
26
|
-
|
26
|
+
# rubocop:disable Layout/LineLength
|
27
|
+
"remove the index #{extraneous_index} from the table #{table} - queries should be able to use the following #{'index'.pluralize(replacement_indexes.count)} instead: #{replacement_indexes.join(' or ')}"
|
28
|
+
# rubocop:enable Layout/LineLength
|
27
29
|
end
|
28
30
|
end
|
29
31
|
|
@@ -33,35 +35,37 @@ module ActiveRecordDoctor
|
|
33
35
|
end
|
34
36
|
|
35
37
|
def subindexes_of_multi_column_indexes
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
38
|
+
log(__method__) do
|
39
|
+
each_data_source(except: config(:ignore_tables)) do |table|
|
40
|
+
each_index(table, except: config(:ignore_indexes), multicolumn_only: true) do |index, indexes|
|
41
|
+
replacement_indexes = indexes.select do |other_index|
|
42
|
+
index != other_index && replaceable_with?(index, other_index)
|
43
|
+
end
|
44
|
+
|
45
|
+
if replacement_indexes.empty?
|
46
|
+
log("Found no replacement indexes; skipping")
|
47
|
+
next
|
48
|
+
end
|
49
|
+
|
50
|
+
problem!(
|
51
|
+
table: table,
|
52
|
+
extraneous_index: index.name,
|
53
|
+
replacement_indexes: replacement_indexes.map(&:name).sort
|
54
|
+
)
|
44
55
|
end
|
45
|
-
|
46
|
-
next if replacement_indexes.empty?
|
47
|
-
|
48
|
-
problem!(
|
49
|
-
extraneous_index: index.name,
|
50
|
-
replacement_indexes: replacement_indexes.map(&:name).sort
|
51
|
-
)
|
52
56
|
end
|
53
57
|
end
|
54
58
|
end
|
55
59
|
|
56
60
|
def indexed_primary_keys
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
61
|
+
log(__method__) do
|
62
|
+
each_table(except: config(:ignore_tables)) do |table|
|
63
|
+
each_index(table, except: config(:ignore_indexes), multicolumn_only: true) do |index|
|
64
|
+
primary_key = connection.primary_key(table)
|
65
|
+
if index.columns == [primary_key] && index.where.nil?
|
66
|
+
problem!(table: table, extraneous_index: index.name, replacement_indexes: nil)
|
67
|
+
end
|
68
|
+
end
|
65
69
|
end
|
66
70
|
end
|
67
71
|
end
|
@@ -72,13 +76,16 @@ module ActiveRecordDoctor
|
|
72
76
|
return false if index1.where != index2.where
|
73
77
|
return false if opclasses(index1) != opclasses(index2)
|
74
78
|
|
79
|
+
index1_columns = Array(index1.columns)
|
80
|
+
index2_columns = Array(index2.columns)
|
81
|
+
|
75
82
|
case [index1.unique, index2.unique]
|
76
83
|
when [true, true]
|
77
|
-
(
|
84
|
+
(index2_columns - index1_columns).empty?
|
78
85
|
when [true, false]
|
79
86
|
false
|
80
87
|
else
|
81
|
-
prefix?(
|
88
|
+
prefix?(index1_columns, index2_columns)
|
82
89
|
end
|
83
90
|
end
|
84
91
|
|
@@ -87,12 +94,7 @@ module ActiveRecordDoctor
|
|
87
94
|
end
|
88
95
|
|
89
96
|
def prefix?(lhs, rhs)
|
90
|
-
lhs.
|
91
|
-
rhs.columns[0...lhs.columns.count] == lhs.columns
|
92
|
-
end
|
93
|
-
|
94
|
-
def indexes(table_name)
|
95
|
-
super.select { |index| index.columns.is_a?(Array) }
|
97
|
+
lhs.count <= rhs.count && rhs[0...lhs.count] == lhs
|
96
98
|
end
|
97
99
|
end
|
98
100
|
end
|
@@ -25,11 +25,8 @@ module ActiveRecordDoctor
|
|
25
25
|
end
|
26
26
|
|
27
27
|
def detect
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
connection.columns(model.table_name).each do |column|
|
32
|
-
next if config(:ignore_attributes).include?("#{model.name}.#{column.name}")
|
28
|
+
each_model(except: config(:ignore_models), existing_tables_only: true) do |model|
|
29
|
+
each_attribute(model, except: config(:ignore_attributes)) do |column|
|
33
30
|
next unless column.type == :boolean
|
34
31
|
next unless has_presence_validator?(model, column)
|
35
32
|
|