active_record_doctor 1.10.0 → 1.12.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 +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
|
|