active_record_doctor 1.9.0 → 1.11.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 (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