active_record_doctor 1.9.0 → 1.11.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +83 -19
  3. data/lib/active_record_doctor/config/default.rb +17 -0
  4. data/lib/active_record_doctor/detectors/base.rb +216 -56
  5. data/lib/active_record_doctor/detectors/extraneous_indexes.rb +38 -56
  6. data/lib/active_record_doctor/detectors/incorrect_boolean_presence_validation.rb +2 -6
  7. data/lib/active_record_doctor/detectors/incorrect_dependent_option.rb +88 -15
  8. data/lib/active_record_doctor/detectors/incorrect_length_validation.rb +60 -0
  9. data/lib/active_record_doctor/detectors/mismatched_foreign_key_type.rb +16 -9
  10. data/lib/active_record_doctor/detectors/missing_foreign_keys.rb +2 -4
  11. data/lib/active_record_doctor/detectors/missing_non_null_constraint.rb +14 -11
  12. data/lib/active_record_doctor/detectors/missing_presence_validation.rb +16 -10
  13. data/lib/active_record_doctor/detectors/missing_unique_indexes.rb +61 -17
  14. data/lib/active_record_doctor/detectors/short_primary_key_type.rb +6 -2
  15. data/lib/active_record_doctor/detectors/undefined_table_references.rb +2 -4
  16. data/lib/active_record_doctor/detectors/unindexed_deleted_at.rb +6 -15
  17. data/lib/active_record_doctor/detectors/unindexed_foreign_keys.rb +2 -4
  18. data/lib/active_record_doctor/logger/dummy.rb +11 -0
  19. data/lib/active_record_doctor/logger/hierarchical.rb +22 -0
  20. data/lib/active_record_doctor/logger.rb +6 -0
  21. data/lib/active_record_doctor/rake/task.rb +10 -1
  22. data/lib/active_record_doctor/runner.rb +8 -3
  23. data/lib/active_record_doctor/version.rb +1 -1
  24. data/lib/active_record_doctor.rb +4 -0
  25. data/lib/generators/active_record_doctor/add_indexes/add_indexes_generator.rb +5 -5
  26. data/test/active_record_doctor/detectors/disable_test.rb +30 -0
  27. data/test/active_record_doctor/detectors/extraneous_indexes_test.rb +34 -0
  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 +220 -43
  30. data/test/active_record_doctor/detectors/incorrect_length_validation_test.rb +107 -0
  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 +78 -21
  33. data/test/active_record_doctor/detectors/missing_presence_validation_test.rb +89 -25
  34. data/test/active_record_doctor/detectors/missing_unique_indexes_test.rb +179 -15
  35. data/test/active_record_doctor/detectors/short_primary_key_type_test.rb +27 -19
  36. data/test/active_record_doctor/detectors/undefined_table_references_test.rb +11 -13
  37. data/test/active_record_doctor/detectors/unindexed_deleted_at_test.rb +9 -3
  38. data/test/active_record_doctor/runner_test.rb +18 -19
  39. data/test/setup.rb +15 -7
  40. metadata +25 -5
  41. data/test/model_factory.rb +0 -128
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e69330f83b83c3adcd1397554bc916ee163acf3797b8be5b8f96f1960e2d8a3e
4
- data.tar.gz: 8f6e09d3af78bb9aaf01374437653b25070c2c328a0ffeaedc800378a10074f6
3
+ metadata.gz: fef82b1493488683e11bc72273e142479cb47234651a8aa772a83121960f9309
4
+ data.tar.gz: c99afc25d8307ff0814e0459790f3d7fcf0b015d9f37f118870a9a1cfa2cf460
5
5
  SHA512:
6
- metadata.gz: 4b9d75665c0524cd5ad18c5aa21aaf59b3e7a75b63c88fb332cd3f600de4f2a9fa073b985f5155b81d54f177b25da5fba5dde494fa6eed28e16086a955082012
7
- data.tar.gz: 94c14b73d06c92803b196fde5ca0572c9545b2ef8b89bc91e88755c5aed9c4e51087ab59a53a94f1fbcacf3f7a58a25cac38dfb4f81d7096ee4d03c7796632b4
6
+ metadata.gz: ab75ff192c31cfc10cbd5c4106ca0033cc4d408c1e4977a417f76c53f2a2e73e37e5706162cd4807c700808e171756ddda022af6658b60f460d78109f9bea2ee
7
+ data.tar.gz: 075625a08ac5f192a68ed6c99409a4acf85cadb77ec117d6933186c2bf4d07e5aaf08928e9f30d5dd96b263e8e530a4a4da848ef5a4dfc82d5dae0df2475f7d2
data/README.md CHANGED
@@ -11,6 +11,7 @@ can detect:
11
11
  * missing non-`NULL` constraints - [`active_record_doctor:missing_non_null_constraint`](#detecting-missing-non-null-constraints)
12
12
  * missing presence validations - [`active_record_doctor:missing_presence_validation`](#detecting-missing-presence-validations)
13
13
  * incorrect presence validations on boolean columns - [`active_record_doctor:incorrect_boolean_presence_validation`](#detecting-incorrect-presence-validations-on-boolean-columns)
14
+ * mismatches between model length validations and database validation constraints - [`active_record_doctor:incorrect_length_validation`](#detecting-incorrect-length-validation)
14
15
  * incorrect values of `dependent` on associations - [`active_record_doctor:incorrect_dependent_option`](#detecting-incorrect-dependent-option-on-associations)
15
16
  * primary keys having short integer types - [`active_record_doctor:short_primary_key_type`](#detecting-primary-keys-having-short-integer-types)
16
17
  * mismatched foreign key types - [`active_record_doctor:mismatched_foreign_key_type`](#detecting-mismatched-foreign-key-types)
@@ -90,7 +91,8 @@ build steps -- it returns a non-zero exit status if any errors were reported.
90
91
 
91
92
  ### Obtaining Help
92
93
 
93
- If you'd like to obtain help on a specific detector then use the `help` sub-task:
94
+ If you'd like to obtain help on a specific detector then use the `help`
95
+ sub-task:
94
96
 
95
97
  ```
96
98
  bundle exec rake active_record_doctor:extraneous_indexes:help
@@ -101,7 +103,7 @@ configuration options, their meaning, and whether they're global or local.
101
103
 
102
104
  ### Configuration
103
105
 
104
- `active_record_doctor` can be configured to better suite your project's needs.
106
+ `active_record_doctor` can be configured to better suit your project's needs.
105
107
  For example, if it complains about a model that you want ignored then you can
106
108
  add that model to the configuration file.
107
109
 
@@ -173,8 +175,10 @@ three-step process:
173
175
 
174
176
  Supported configuration options:
175
177
 
178
+ - `enabled` - set to `false` to disable the detector altogether
176
179
  - `ignore_tables` - tables whose foreign keys should not be checked
177
- - `ignore_columns` - columns, written as table.column, that should not be checked.
180
+ - `ignore_columns` - columns, written as table.column, that should not be
181
+ checked.
178
182
 
179
183
  ### Removing Extraneous Indexes
180
184
 
@@ -215,10 +219,15 @@ reported.
215
219
  Note that a unique index can _never be replaced by a non-unique one_. For
216
220
  example, if there's a unique index on `users.login` and a non-unique index on
217
221
  `users.login, users.domain` then the tool will _not_ suggest dropping
218
- `users.login` as it could violate the uniqueness assumption.
222
+ `users.login` as it could violate the uniqueness assumption. However, a unique
223
+ index on `users.login, user.domain` might be replaceable with `users.login` as
224
+ the uniqueness of the latter implies the uniqueness of the former (if a given
225
+ `login` can appear only once then it can be present in only one `login, domain`
226
+ pair).
219
227
 
220
228
  Supported configuration options:
221
229
 
230
+ - `enabled` - set to `false` to disable the detector altogether
222
231
  - `ignore_tables` - tables whose indexes should never be reported as extraneous.
223
232
  - `ignore_columns` - indexes that should never be reported as extraneous.
224
233
 
@@ -227,7 +236,8 @@ Supported configuration options:
227
236
  If you soft-delete some models (e.g. with `paranoia`) then you need to modify
228
237
  your indexes to include only non-deleted rows. Otherwise they will include
229
238
  logically non-existent rows. This will make them larger and slower to use. Most
230
- of the time they should only cover columns satisfying `deleted_at IS NULL`.
239
+ of the time they should only cover columns satisfying `deleted_at IS NULL` (to
240
+ cover existing records) or `deleted_at IS NOT NULL` (to cover deleted records).
231
241
 
232
242
  `active_record_doctor` can automatically detect indexes on tables with a
233
243
  `deleted_at` column. Just run:
@@ -242,9 +252,12 @@ appropriate migrations. You need to do that manually.
242
252
 
243
253
  Supported configuration options:
244
254
 
255
+ - `enabled` - set to `false` to disable the detector altogether
245
256
  - `ignore_tables` - tables whose indexes should not be checked.
246
- - `ignore_columns` - specific columns, written as table.column, that should not be reported as unindexed.
247
- - `ignore_indexes` - specific indexes that should not be reported as excluding a timestamp column.
257
+ - `ignore_columns` - specific columns, written as table.column, that should not
258
+ be reported as unindexed.
259
+ - `ignore_indexes` - specific indexes that should not be reported as excluding a
260
+ timestamp column.
248
261
  - `column_names` - deletion timestamp column names.
249
262
 
250
263
  ### Detecting Missing Foreign Key Constraints
@@ -277,8 +290,10 @@ end
277
290
 
278
291
  Supported configuration options:
279
292
 
293
+ - `enabled` - set to `false` to disable the detector altogether
280
294
  - `ignore_tables` - tables whose columns should not be checked.
281
- - `ignore_columns` - columns, written as table.column, that should not be checked.
295
+ - `ignore_columns` - columns, written as table.column, that should not be
296
+ checked.
282
297
 
283
298
  ### Detecting Models Referencing Undefined Tables
284
299
 
@@ -310,13 +325,15 @@ this check as part of your Continuous Integration pipeline.
310
325
 
311
326
  Supported configuration options:
312
327
 
313
- - `ignore_models` - models whose underlying tables should not be checked for existence.
328
+ - `enabled` - set to `false` to disable the detector altogether
329
+ - `ignore_models` - models whose underlying tables should not be checked for
330
+ existence.
314
331
 
315
332
  ### Detecting Uniqueness Validations not Backed by an Index
316
333
 
317
- A model-level uniqueness validations should be backed by a database index in
318
- order to be robust. Otherwise you risk inserting duplicate values under heavy
319
- load.
334
+ Model-level uniqueness validations and `has_one` associations should be backed
335
+ by a database index in order to be robust. Otherwise you risk inserting
336
+ duplicate values under heavy load.
320
337
 
321
338
  In order to detect such validations run:
322
339
 
@@ -334,13 +351,16 @@ This means that you should create a unique index on `users.email`.
334
351
 
335
352
  Supported configuration options:
336
353
 
354
+ - `enabled` - set to `false` to disable the detector altogether
337
355
  - `ignore_models` - models whose uniqueness validators should not be checked.
338
- - `ignore_columns` - specific validators, written as Model(column1, column2, ...), that should not be checked.
356
+ - `ignore_columns` - specific validators, written as Model(column1, ...), that
357
+ should not be checked.
339
358
 
340
359
  ### Detecting Missing Non-`NULL` Constraints
341
360
 
342
361
  If there's an unconditional presence validation on a column then it should be
343
- marked as non-`NULL`-able at the database level.
362
+ marked as non-`NULL`-able at the database level or should have a `IS NOT NULL`
363
+ constraint.
344
364
 
345
365
  In order to detect columns whose presence is required but that are marked
346
366
  `null: true` in the database run the following command:
@@ -362,8 +382,10 @@ This validator skips models whose corresponding database tables don't exist.
362
382
 
363
383
  Supported configuration options:
364
384
 
385
+ - `enabled` - set to `false` to disable the detector altogether
365
386
  - `ignore_tables` - tables whose columns should not be checked.
366
- - `ignore_columns` - columns, written as table.column, that should not be checked.
387
+ - `ignore_columns` - columns, written as table.column, that should not be
388
+ checked.
367
389
 
368
390
  ### Detecting Missing Presence Validations
369
391
 
@@ -389,8 +411,10 @@ This validator skips models whose corresponding database tables don't exist.
389
411
 
390
412
  Supported configuration options:
391
413
 
414
+ - `enabled` - set to `false` to disable the detector altogether
392
415
  - `ignore_models` - models whose underlying tables' columns should not be checked.
393
- - `ignore_columns` - specific attributes, written as Model.attribute, that should not be checked.
416
+ - `ignore_attributes` - specific attributes, written as Model.attribute, that
417
+ should not be checked.
394
418
 
395
419
  ### Detecting Incorrect Presence Validations on Boolean Columns
396
420
 
@@ -416,8 +440,43 @@ This validator skips models whose corresponding database tables don't exist.
416
440
 
417
441
  Supported configuration options:
418
442
 
443
+ - `enabled` - set to `false` to disable the detector altogether
419
444
  - `ignore_models` - models whose validators should not be checked.
420
- - `ignore_columns` - attributes, written as Model.attribute, whose validators should not be checked.
445
+ - `ignore_columns` - attributes, written as Model.attribute, whose validators
446
+ should not be checked.
447
+
448
+ ### Detecting Incorrect Length Validations
449
+
450
+ String length can be enforced by both the database and the application. If
451
+ there's a database limit then it's a good idea to add a model validation to
452
+ ensure user-friendly error messages. Similarly, if there's a model validator
453
+ without the corresponding database constraint then it's a good idea to add one
454
+ to avoid saving invalid models.
455
+
456
+ In order to detect columns whose length isn't validated properly run:
457
+
458
+ ```
459
+ bundle exec rake active_record_doctor:incorrect_length_validation
460
+ ```
461
+
462
+ The output of the command looks like this:
463
+
464
+ ```
465
+ set the maximum length in the validator of User.email (currently 32) and the database limit on users.email (currently 64) to the same value
466
+ add a length validator on User.address to enforce a maximum length of 64 defined on users.address
467
+ ```
468
+
469
+ The first message means the validator on `User.email` is checking for a
470
+ different maximum than the database limit on `users.email`. The second message
471
+ means there's a database limit on `users.address` without the corresponding
472
+ model validation.
473
+
474
+ Supported configuration options:
475
+
476
+ - `enabled` - set to `false` to disable the detector altogether
477
+ - `ignore_models` - models whose validators should not be checked.
478
+ - `ignore_columns` - attributes, written as Model.attribute, whose validators
479
+ should not be checked.
421
480
 
422
481
  ### Detecting Incorrect `dependent` Option on Associations
423
482
 
@@ -448,8 +507,10 @@ use `dependent: :destroy` or similar on Post.comments - the associated model has
448
507
 
449
508
  Supported configuration options:
450
509
 
510
+ - `enabled` - set to `false` to disable the detector altogether
451
511
  - `ignore_models` - models whose associations should not be checked.
452
- - `ignore_columns` - associations, written as Model.association, that should not be checked.
512
+ - `ignore_associations` - associations, written as Model.association, that should not
513
+ be checked.
453
514
 
454
515
  ### Detecting Primary Keys Having Short Integer Types
455
516
 
@@ -485,6 +546,7 @@ as all rows need to be rewritten.
485
546
 
486
547
  Supported configuration options:
487
548
 
549
+ - `enabled` - set to `false` to disable the detector altogether
488
550
  - `ignore_tables` - tables whose primary keys should not be checked.
489
551
 
490
552
  ### Detecting Mismatched Foreign Key Types
@@ -508,8 +570,10 @@ companies.user_id references a column of different type - foreign keys should be
508
570
 
509
571
  Supported configuration options:
510
572
 
573
+ - `enabled` - set to `false` to disable the detector altogether
511
574
  - `ignore_tables` - tables whose foreign keys should not be checked.
512
- - `ignore_columns` - foreign keys, written as table.column, that should not be checked.
575
+ - `ignore_columns` - foreign keys, written as table.column, that should not be
576
+ checked.
513
577
 
514
578
  ## Ruby and Rails Compatibility Policy
515
579
 
@@ -10,50 +10,67 @@ ActiveRecordDoctor.configure do
10
10
  ]
11
11
 
12
12
  detector :extraneous_indexes,
13
+ enabled: true,
13
14
  ignore_tables: [],
14
15
  ignore_indexes: []
15
16
 
16
17
  detector :incorrect_boolean_presence_validation,
18
+ enabled: true,
19
+ ignore_models: [],
20
+ ignore_attributes: []
21
+
22
+ detector :incorrect_length_validation,
23
+ enabled: true,
17
24
  ignore_models: [],
18
25
  ignore_attributes: []
19
26
 
20
27
  detector :incorrect_dependent_option,
28
+ enabled: true,
21
29
  ignore_models: [],
22
30
  ignore_associations: []
23
31
 
24
32
  detector :mismatched_foreign_key_type,
33
+ enabled: true,
25
34
  ignore_tables: [],
26
35
  ignore_columns: []
27
36
 
28
37
  detector :missing_foreign_keys,
38
+ enabled: true,
29
39
  ignore_tables: [],
30
40
  ignore_columns: []
31
41
 
32
42
  detector :missing_non_null_constraint,
43
+ enabled: true,
33
44
  ignore_tables: [],
34
45
  ignore_columns: []
35
46
 
36
47
  detector :missing_presence_validation,
48
+ enabled: true,
37
49
  ignore_models: [],
38
50
  ignore_attributes: []
39
51
 
40
52
  detector :missing_unique_indexes,
53
+ enabled: true,
41
54
  ignore_models: [],
42
55
  ignore_columns: []
43
56
 
44
57
  detector :short_primary_key_type,
58
+ enabled: true,
45
59
  ignore_tables: []
46
60
 
47
61
  detector :undefined_table_references,
62
+ enabled: true,
48
63
  ignore_models: []
49
64
 
50
65
  detector :unindexed_deleted_at,
66
+ enabled: true,
51
67
  ignore_tables: [],
52
68
  ignore_columns: [],
53
69
  ignore_indexes: [],
54
70
  column_names: ["deleted_at", "discarded_at"]
55
71
 
56
72
  detector :unindexed_foreign_keys,
73
+ enabled: true,
57
74
  ignore_tables: [],
58
75
  ignore_columns: []
59
76
  end
@@ -4,17 +4,27 @@ module ActiveRecordDoctor
4
4
  module Detectors
5
5
  # Base class for all active_record_doctor detectors.
6
6
  class Base
7
+ BASE_CONFIG = {
8
+ enabled: {
9
+ description: "set to false to disable the detector altogether"
10
+ }
11
+ }.freeze
12
+
7
13
  class << self
8
- attr_reader :description, :config
14
+ attr_reader :description
9
15
 
10
- def run(config, io)
11
- new(config, io).run
16
+ def run(*args, **kwargs, &block)
17
+ new(*args, **kwargs, &block).run
12
18
  end
13
19
 
14
20
  def underscored_name
15
21
  name.demodulize.underscore.to_sym
16
22
  end
17
23
 
24
+ def config
25
+ @config.merge(BASE_CONFIG)
26
+ end
27
+
18
28
  def locals_and_globals
19
29
  locals = []
20
30
  globals = []
@@ -28,23 +38,36 @@ module ActiveRecordDoctor
28
38
  end
29
39
  end
30
40
 
31
- def initialize(config, io)
41
+ def initialize(config:, logger:, io:)
32
42
  @problems = []
33
43
  @config = config
44
+ @logger = logger
34
45
  @io = io
35
46
  end
36
47
 
37
48
  def run
38
- @problems = []
49
+ log(underscored_name) do
50
+ @problems = []
39
51
 
40
- detect
41
- @problems.each do |problem|
42
- @io.puts(message(**problem))
43
- end
52
+ if config(:enabled)
53
+ detect
54
+ else
55
+ log("disabled; skipping")
56
+ end
44
57
 
45
- success = @problems.empty?
46
- @problems = nil
47
- 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
48
71
  end
49
72
 
50
73
  private
@@ -69,7 +92,16 @@ module ActiveRecordDoctor
69
92
  raise("#message should be implemented by a subclass")
70
93
  end
71
94
 
95
+ def log(message, &block)
96
+ @logger.log(message, &block)
97
+ end
98
+
72
99
  def problem!(**attrs)
100
+ log("Problem found") do
101
+ attrs.each do |key, value|
102
+ log("#{key}: #{value.inspect}")
103
+ end
104
+ end
73
105
  @problems << attrs
74
106
  end
75
107
 
@@ -81,74 +113,202 @@ module ActiveRecordDoctor
81
113
  @connection ||= ActiveRecord::Base.connection
82
114
  end
83
115
 
84
- def indexes(table_name, except: [])
85
- connection.indexes(table_name).reject do |index|
86
- except.include?(index.name)
87
- end
116
+ def indexes(table_name)
117
+ connection.indexes(table_name)
88
118
  end
89
119
 
90
- def tables(except: [])
91
- tables =
92
- if ActiveRecord::VERSION::STRING >= "5.1"
93
- connection.tables
94
- else
95
- connection.data_sources
96
- end
120
+ def primary_key(table_name)
121
+ primary_key_name = connection.primary_key(table_name)
122
+ return nil if primary_key_name.nil?
97
123
 
98
- tables.reject do |table|
99
- except.include?(table)
100
- end
124
+ column(table_name, primary_key_name)
101
125
  end
102
126
 
103
- def table_exists?(table_name)
104
- if ActiveRecord::VERSION::STRING >= "5.1"
105
- connection.table_exists?(table_name)
106
- else
107
- connection.data_source_exists?(table_name)
127
+ def column(table_name, column_name)
128
+ connection.columns(table_name).find { |column| column.name == column_name }
129
+ end
130
+
131
+ def not_null_check_constraint_exists?(table, column)
132
+ check_constraints(table).any? do |definition|
133
+ definition =~ /\A#{column.name} IS NOT NULL\z/i ||
134
+ definition =~ /\A#{connection.quote_column_name(column.name)} IS NOT NULL\z/i
108
135
  end
109
136
  end
110
137
 
111
- def tables_and_views
112
- if connection.respond_to?(:data_sources)
113
- connection.data_sources
138
+ def check_constraints(table_name)
139
+ # ActiveRecord 6.1+
140
+ if connection.respond_to?(:supports_check_constraints?) && connection.supports_check_constraints?
141
+ connection.check_constraints(table_name).select(&:validated?).map(&:expression)
142
+ elsif postgresql?
143
+ definitions =
144
+ connection.select_values(<<-SQL)
145
+ SELECT pg_get_constraintdef(oid, true)
146
+ FROM pg_constraint
147
+ WHERE contype = 'c'
148
+ AND convalidated
149
+ AND conrelid = #{connection.quote(table_name)}::regclass
150
+ SQL
151
+
152
+ definitions.map { |definition| definition[/CHECK \((.+)\)/m, 1] }
114
153
  else
115
- connection.tables
154
+ # We don't support this Rails/database combination yet.
155
+ []
116
156
  end
117
157
  end
118
158
 
119
- def primary_key(table_name)
120
- primary_key_name = connection.primary_key(table_name)
121
- return nil if primary_key_name.nil?
159
+ def models
160
+ ActiveRecord::Base.descendants
161
+ end
122
162
 
123
- column(table_name, primary_key_name)
163
+ def underscored_name
164
+ self.class.underscored_name
124
165
  end
125
166
 
126
- def column(table_name, column_name)
127
- connection.columns(table_name).find { |column| column.name == column_name }
167
+ def postgresql?
168
+ ["PostgreSQL", "PostGIS"].include?(connection.adapter_name)
128
169
  end
129
170
 
130
- def views
131
- @views ||=
132
- if connection.respond_to?(:views)
133
- connection.views
134
- elsif connection.adapter_name == "PostgreSQL"
135
- ActiveRecord::Base.connection.execute(<<-SQL).map { |tuple| tuple.fetch("relname") }
136
- SELECT c.relname FROM pg_class c WHERE c.relkind IN ('m', 'v')
137
- SQL
171
+ def each_model(except: [], abstract: nil, existing_tables_only: false)
172
+ log("Iterating over Active Record models") do
173
+ models.each do |model|
174
+ case
175
+ when model.name.start_with?("HABTM_")
176
+ log("#{model.name} - has-belongs-to-many model; skipping")
177
+ when except.include?(model.name)
178
+ log("#{model.name} - ignored via the configuration; skipping")
179
+ when abstract && !model.abstract_class?
180
+ log("#{model.name} - non-abstract model; skipping")
181
+ when abstract == false && model.abstract_class?
182
+ log("#{model.name} - abstract model; skipping")
183
+ when existing_tables_only && (model.table_name.nil? || !model.table_exists?)
184
+ log("#{model.name} - backed by a non-existent table #{model.table_name}; skipping")
185
+ else
186
+ log(model.name) do
187
+ yield(model)
188
+ end
189
+ end
190
+ end
191
+ end
192
+ end
193
+
194
+ def each_index(table_name, except: [], multicolumn_only: false)
195
+ indexes = connection.indexes(table_name)
196
+
197
+ message =
198
+ if multicolumn_only
199
+ "Iterating over multi-column indexes on #{table_name}"
138
200
  else
139
- # We don't support this Rails/database combination yet.
140
- nil
201
+ "Iterating over indexes on #{table_name}"
141
202
  end
203
+
204
+ log(message) do
205
+ indexes.each do |index|
206
+ case
207
+ when except.include?(index.name)
208
+ log("#{index.name} - ignored via the configuration; skipping")
209
+ when multicolumn_only && !index.columns.is_a?(Array)
210
+ log("#{index.name} - single-column index; skipping")
211
+ else
212
+ log("Index #{index.name} on #{table_name}") do
213
+ yield(index, indexes)
214
+ end
215
+ end
216
+ end
217
+ end
142
218
  end
143
219
 
144
- def models(except: [])
145
- ActiveRecord::Base.descendants.reject do |model|
146
- model.name.start_with?("HABTM_") || except.include?(model.name)
220
+ def each_attribute(model, except: [], type: nil)
221
+ log("Iterating over attributes of #{model.name}") do
222
+ connection.columns(model.table_name).each do |column|
223
+ case
224
+ when except.include?("#{model.name}.#{column.name}")
225
+ log("#{model.name}.#{column.name} - ignored via the configuration; skipping")
226
+ when type && !Array(type).include?(column.type)
227
+ log("#{model.name}.#{column.name} - ignored due to the #{column.type} type; skipping")
228
+ else
229
+ log("#{model.name}.#{column.name}") do
230
+ yield(column)
231
+ end
232
+ end
233
+ end
147
234
  end
148
235
  end
149
236
 
150
- def underscored_name
151
- self.class.underscored_name
237
+ def each_column(table_name, only: nil, except: [])
238
+ log("Iterating over columns of #{table_name}") do
239
+ connection.columns(table_name).each do |column|
240
+ case
241
+ when except.include?("#{table_name}.#{column.name}")
242
+ log("#{column.name} - ignored via the configuration; skipping")
243
+ when only.nil? || only.include?(column.name)
244
+ log(column.name.to_s) do
245
+ yield(column)
246
+ end
247
+ end
248
+ end
249
+ end
250
+ end
251
+
252
+ def each_foreign_key(table_name)
253
+ log("Iterating over foreign keys on #{table_name}") do
254
+ connection.foreign_keys(table_name).each do |foreign_key|
255
+ 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
256
+ yield(foreign_key)
257
+ end
258
+ end
259
+ end
260
+ end
261
+
262
+ def each_table(except: [])
263
+ tables =
264
+ if ActiveRecord::VERSION::STRING >= "5.1"
265
+ connection.tables
266
+ else
267
+ connection.data_sources
268
+ end
269
+
270
+ log("Iterating over tables") do
271
+ tables.each do |table|
272
+ case
273
+ when except.include?(table)
274
+ log("#{table} - ignored via the configuration; skipping")
275
+ else
276
+ log(table) do
277
+ yield(table)
278
+ end
279
+ end
280
+ end
281
+ end
282
+ end
283
+
284
+ def each_association(model, except: [], type: [:has_many, :has_one, :belongs_to], has_scope: nil, through: nil)
285
+ type = Array(type)
286
+
287
+ log("Iterating over associations on #{model.name}") do
288
+ associations = []
289
+ type.each do |type1|
290
+ associations.concat(model.reflect_on_all_associations(type1))
291
+ end
292
+
293
+ associations.each do |association|
294
+ case
295
+ when except.include?("#{model.name}.#{association.name}")
296
+ log("#{model.name}.#{association.name} - ignored via the configuration; skipping")
297
+ when through && !association.is_a?(ActiveRecord::Reflection::ThroughReflection)
298
+ log("#{model.name}.#{association.name} - is not a through association; skipping")
299
+ when through == false && association.is_a?(ActiveRecord::Reflection::ThroughReflection)
300
+ log("#{model.name}.#{association.name} - is a through association; skipping")
301
+ when has_scope && association.scope.nil?
302
+ log("#{model.name}.#{association.name} - doesn't have a scope; skipping")
303
+ when has_scope == false && association.scope
304
+ log("#{model.name}.#{association.name} - has a scope; skipping")
305
+ else
306
+ log("#{association.macro} :#{association.name}") do
307
+ yield(association)
308
+ end
309
+ end
310
+ end
311
+ end
152
312
  end
153
313
  end
154
314
  end