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.
- checksums.yaml +4 -4
- data/README.md +83 -19
- data/lib/active_record_doctor/config/default.rb +17 -0
- data/lib/active_record_doctor/detectors/base.rb +216 -56
- data/lib/active_record_doctor/detectors/extraneous_indexes.rb +38 -56
- data/lib/active_record_doctor/detectors/incorrect_boolean_presence_validation.rb +2 -6
- data/lib/active_record_doctor/detectors/incorrect_dependent_option.rb +88 -15
- data/lib/active_record_doctor/detectors/incorrect_length_validation.rb +60 -0
- data/lib/active_record_doctor/detectors/mismatched_foreign_key_type.rb +16 -9
- data/lib/active_record_doctor/detectors/missing_foreign_keys.rb +2 -4
- data/lib/active_record_doctor/detectors/missing_non_null_constraint.rb +14 -11
- data/lib/active_record_doctor/detectors/missing_presence_validation.rb +16 -10
- data/lib/active_record_doctor/detectors/missing_unique_indexes.rb +61 -17
- data/lib/active_record_doctor/detectors/short_primary_key_type.rb +6 -2
- data/lib/active_record_doctor/detectors/undefined_table_references.rb +2 -4
- data/lib/active_record_doctor/detectors/unindexed_deleted_at.rb +6 -15
- data/lib/active_record_doctor/detectors/unindexed_foreign_keys.rb +2 -4
- data/lib/active_record_doctor/logger/dummy.rb +11 -0
- data/lib/active_record_doctor/logger/hierarchical.rb +22 -0
- data/lib/active_record_doctor/logger.rb +6 -0
- data/lib/active_record_doctor/rake/task.rb +10 -1
- data/lib/active_record_doctor/runner.rb +8 -3
- data/lib/active_record_doctor/version.rb +1 -1
- data/lib/active_record_doctor.rb +4 -0
- data/lib/generators/active_record_doctor/add_indexes/add_indexes_generator.rb +5 -5
- data/test/active_record_doctor/detectors/disable_test.rb +30 -0
- data/test/active_record_doctor/detectors/extraneous_indexes_test.rb +34 -0
- data/test/active_record_doctor/detectors/incorrect_boolean_presence_validation_test.rb +7 -7
- data/test/active_record_doctor/detectors/incorrect_dependent_option_test.rb +220 -43
- data/test/active_record_doctor/detectors/incorrect_length_validation_test.rb +107 -0
- data/test/active_record_doctor/detectors/mismatched_foreign_key_type_test.rb +35 -1
- data/test/active_record_doctor/detectors/missing_non_null_constraint_test.rb +78 -21
- data/test/active_record_doctor/detectors/missing_presence_validation_test.rb +89 -25
- data/test/active_record_doctor/detectors/missing_unique_indexes_test.rb +179 -15
- data/test/active_record_doctor/detectors/short_primary_key_type_test.rb +27 -19
- data/test/active_record_doctor/detectors/undefined_table_references_test.rb +11 -13
- data/test/active_record_doctor/detectors/unindexed_deleted_at_test.rb +9 -3
- data/test/active_record_doctor/runner_test.rb +18 -19
- data/test/setup.rb +15 -7
- metadata +25 -5
- data/test/model_factory.rb +0 -128
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fef82b1493488683e11bc72273e142479cb47234651a8aa772a83121960f9309
|
4
|
+
data.tar.gz: c99afc25d8307ff0814e0459790f3d7fcf0b015d9f37f118870a9a1cfa2cf460
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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`
|
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
|
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
|
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
|
247
|
-
|
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
|
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
|
-
- `
|
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
|
-
|
318
|
-
order to be robust. Otherwise you risk inserting
|
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,
|
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
|
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
|
-
- `
|
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
|
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
|
-
- `
|
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
|
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
|
14
|
+
attr_reader :description
|
9
15
|
|
10
|
-
def run(
|
11
|
-
new(
|
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
|
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
|
-
|
49
|
+
log(underscored_name) do
|
50
|
+
@problems = []
|
39
51
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
52
|
+
if config(:enabled)
|
53
|
+
detect
|
54
|
+
else
|
55
|
+
log("disabled; skipping")
|
56
|
+
end
|
44
57
|
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
85
|
-
connection.indexes(table_name)
|
86
|
-
except.include?(index.name)
|
87
|
-
end
|
116
|
+
def indexes(table_name)
|
117
|
+
connection.indexes(table_name)
|
88
118
|
end
|
89
119
|
|
90
|
-
def
|
91
|
-
|
92
|
-
|
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
|
-
|
99
|
-
except.include?(table)
|
100
|
-
end
|
124
|
+
column(table_name, primary_key_name)
|
101
125
|
end
|
102
126
|
|
103
|
-
def
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
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
|
112
|
-
|
113
|
-
|
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
|
-
|
154
|
+
# We don't support this Rails/database combination yet.
|
155
|
+
[]
|
116
156
|
end
|
117
157
|
end
|
118
158
|
|
119
|
-
def
|
120
|
-
|
121
|
-
|
159
|
+
def models
|
160
|
+
ActiveRecord::Base.descendants
|
161
|
+
end
|
122
162
|
|
123
|
-
|
163
|
+
def underscored_name
|
164
|
+
self.class.underscored_name
|
124
165
|
end
|
125
166
|
|
126
|
-
def
|
127
|
-
connection.
|
167
|
+
def postgresql?
|
168
|
+
["PostgreSQL", "PostGIS"].include?(connection.adapter_name)
|
128
169
|
end
|
129
170
|
|
130
|
-
def
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
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
|
-
|
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
|
145
|
-
|
146
|
-
model.
|
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
|
151
|
-
|
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
|