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