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