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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +15 -15
  3. data/lib/active_record_doctor/detectors/base.rb +194 -53
  4. data/lib/active_record_doctor/detectors/extraneous_indexes.rb +36 -34
  5. data/lib/active_record_doctor/detectors/incorrect_boolean_presence_validation.rb +2 -5
  6. data/lib/active_record_doctor/detectors/incorrect_dependent_option.rb +87 -37
  7. data/lib/active_record_doctor/detectors/incorrect_length_validation.rb +7 -10
  8. data/lib/active_record_doctor/detectors/mismatched_foreign_key_type.rb +16 -9
  9. data/lib/active_record_doctor/detectors/missing_foreign_keys.rb +2 -4
  10. data/lib/active_record_doctor/detectors/missing_non_null_constraint.rb +13 -11
  11. data/lib/active_record_doctor/detectors/missing_presence_validation.rb +14 -7
  12. data/lib/active_record_doctor/detectors/missing_unique_indexes.rb +70 -35
  13. data/lib/active_record_doctor/detectors/short_primary_key_type.rb +4 -4
  14. data/lib/active_record_doctor/detectors/undefined_table_references.rb +2 -2
  15. data/lib/active_record_doctor/detectors/unindexed_deleted_at.rb +5 -13
  16. data/lib/active_record_doctor/detectors/unindexed_foreign_keys.rb +35 -11
  17. data/lib/active_record_doctor/logger/dummy.rb +11 -0
  18. data/lib/active_record_doctor/logger/hierarchical.rb +22 -0
  19. data/lib/active_record_doctor/logger.rb +6 -0
  20. data/lib/active_record_doctor/rake/task.rb +10 -1
  21. data/lib/active_record_doctor/runner.rb +8 -3
  22. data/lib/active_record_doctor/utils.rb +21 -0
  23. data/lib/active_record_doctor/version.rb +1 -1
  24. data/lib/active_record_doctor.rb +5 -0
  25. data/lib/generators/active_record_doctor/add_indexes/add_indexes_generator.rb +14 -14
  26. data/test/active_record_doctor/detectors/disable_test.rb +1 -1
  27. data/test/active_record_doctor/detectors/extraneous_indexes_test.rb +59 -6
  28. data/test/active_record_doctor/detectors/incorrect_boolean_presence_validation_test.rb +7 -7
  29. data/test/active_record_doctor/detectors/incorrect_dependent_option_test.rb +175 -57
  30. data/test/active_record_doctor/detectors/incorrect_length_validation_test.rb +16 -14
  31. data/test/active_record_doctor/detectors/mismatched_foreign_key_type_test.rb +35 -1
  32. data/test/active_record_doctor/detectors/missing_non_null_constraint_test.rb +46 -23
  33. data/test/active_record_doctor/detectors/missing_presence_validation_test.rb +55 -27
  34. data/test/active_record_doctor/detectors/missing_unique_indexes_test.rb +216 -47
  35. data/test/active_record_doctor/detectors/short_primary_key_type_test.rb +5 -0
  36. data/test/active_record_doctor/detectors/undefined_table_references_test.rb +11 -13
  37. data/test/active_record_doctor/detectors/unindexed_foreign_keys_test.rb +39 -1
  38. data/test/active_record_doctor/runner_test.rb +18 -19
  39. data/test/generators/active_record_doctor/add_indexes/add_indexes_generator_test.rb +16 -6
  40. data/test/setup.rb +10 -6
  41. metadata +23 -7
  42. data/test/model_factory.rb +0 -128
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1e1bf7642b1c7b471fbf78956825067ff070d261d4c47a182d1aaf00f10b98b7
4
- data.tar.gz: 6e6fc746327d6db30c282d7964ae778b254107281d9626a251f8593d6ce85db4
3
+ metadata.gz: d848e296c39f7994781bd645261eb45235450be36058fbdf7368ec6358ebeba5
4
+ data.tar.gz: a63cbbfe5b43fb3bf6daeaba24a6298073643d5b5c96696c27c084106fe54594
5
5
  SHA512:
6
- metadata.gz: a0a2d71947728b9d6a130901d94d1ed8c2ede9dde209e91897a8beb8b757da2fea854b89a3a3f6223d658c203e75e687edbee2af37b2203ddb24b28e831439e8
7
- data.tar.gz: f0fe87dd6acf379ef946bc513504342c3977bf5c4d7f71eca79290c693b318f0a838c31b95111f68635579cdfa26249d5a95fd456817b6a3c32023c1b4fe0fd1
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 an unique index - [`active_record_doctor:missing_unique_indexes`](#detecting-uniqueness-validations-not-backed-by-an-index)
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. end with `_id`) without being
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's usually just a few of
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 migrations that
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 think you need to do is run:
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
- - `ignore_columns` - attributes, written as Model.attribute, whose validators
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-by-one with no reason
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
- - `ignore_columns` - associations, written as Model.association, that should not
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 `comanies.id` should be migrated to a wider integer type. An
534
- example migration to accomplish this looks likes this:
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(config, io)
17
- new(config, io).run
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, io)
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
- @problems = []
49
+ log(underscored_name) do
50
+ @problems = []
49
51
 
50
- detect if config(:enabled)
51
- @problems.each do |problem|
52
- @io.puts(message(**problem))
53
- end
52
+ if config(:enabled)
53
+ detect
54
+ else
55
+ log("disabled; skipping")
56
+ end
54
57
 
55
- success = @problems.empty?
56
- @problems = nil
57
- success
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, except: [])
95
- connection.indexes(table_name).reject do |index|
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(except: [])
171
- ActiveRecord::Base.descendants.reject do |model|
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 postgresql?
181
- ["PostgreSQL", "PostGIS"].include?(connection.adapter_name)
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
- "remove #{extraneous_index} - can be replaced by #{replacement_indexes.join(' or ')}"
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
- tables(except: config(:ignore_tables)).each do |table|
37
- indexes = indexes(table)
38
-
39
- indexes.each do |index|
40
- next if config(:ignore_indexes).include?(index.name)
41
-
42
- replacement_indexes = indexes.select do |other_index|
43
- index != other_index && replaceable_with?(index, other_index)
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
- tables(except: config(:ignore_tables)).each do |table|
58
- indexes(table).each do |index|
59
- next if config(:ignore_indexes).include?(index.name)
60
-
61
- primary_key = connection.primary_key(table)
62
- next if index.columns != [primary_key] || index.where
63
-
64
- problem!(extraneous_index: index.name, replacement_indexes: nil)
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
- (index2.columns - index1.columns).empty?
84
+ (index2_columns - index1_columns).empty?
78
85
  when [true, false]
79
86
  false
80
87
  else
81
- prefix?(index1, index2)
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.columns.count <= rhs.columns.count &&
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
- models(except: config(:ignore_models)).each do |model|
29
- next unless model.table_exists?
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