active_record_doctor 1.15.0 → 2.1.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 +74 -30
- data/lib/active_record_doctor/config/default.rb +10 -2
- data/lib/active_record_doctor/detectors/base.rb +9 -10
- data/lib/active_record_doctor/detectors/incorrect_dependent_option.rb +3 -7
- data/lib/active_record_doctor/detectors/incorrect_length_validation.rb +26 -7
- data/lib/active_record_doctor/detectors/missing_foreign_keys.rb +14 -29
- data/lib/active_record_doctor/detectors/missing_non_null_constraint.rb +16 -1
- data/lib/active_record_doctor/detectors/missing_presence_validation.rb +146 -55
- data/lib/active_record_doctor/detectors/missing_unique_indexes.rb +2 -3
- data/lib/active_record_doctor/detectors/short_primary_key_type.rb +2 -0
- data/lib/active_record_doctor/detectors/table_without_primary_key.rb +1 -1
- data/lib/active_record_doctor/detectors/table_without_timestamps.rb +38 -0
- data/lib/active_record_doctor/detectors/unindexed_foreign_keys.rb +1 -5
- data/lib/active_record_doctor/detectors/unused_tables.rb +31 -0
- data/lib/active_record_doctor/runner.rb +1 -1
- data/lib/active_record_doctor/utils.rb +2 -4
- data/lib/active_record_doctor/version.rb +1 -1
- data/lib/active_record_doctor.rb +3 -1
- data/lib/generators/active_record_doctor/add_indexes/add_indexes_generator.rb +1 -9
- metadata +66 -26
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7c93e45a93d9c11392978f37626379bd322e4827aac59ca6f51400e33a009813
|
|
4
|
+
data.tar.gz: e3e920647f6b68fdec23d7e23e7e4ff3653cb8823673e56b0916d1247cbb3864
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 54c002741991dc91c4afd46de5472d64be0ba02457d23df39bd16297a12f4192023679d281d9bc353a5de5a0c357ea960666967d820316fe5fdaceee36f99d6d
|
|
7
|
+
data.tar.gz: bb17cb57cc708b9d7e751bb79e341aac2df4de4adc5f59d2cf254ef564c0a481b8e8872f1eac34a37246eda9145a78abbb41511150bc064932504645e36453fd
|
data/README.md
CHANGED
|
@@ -16,6 +16,8 @@ can detect:
|
|
|
16
16
|
* primary keys having short integer types - [`active_record_doctor:short_primary_key_type`](#detecting-primary-keys-having-short-integer-types)
|
|
17
17
|
* mismatched foreign key types - [`active_record_doctor:mismatched_foreign_key_type`](#detecting-mismatched-foreign-key-types)
|
|
18
18
|
* tables without primary keys - [`active_record_doctor:table_without_primary_key`](#detecting-tables-without-primary-keys)
|
|
19
|
+
* tables without timestamps - [`active_record_doctor:table_without_timestamps`](#detecting-tables-without-timestamps)
|
|
20
|
+
* unused tables - [`active_record_doctor:unused_tables](#detecting-unused-tables)
|
|
19
21
|
|
|
20
22
|
It can also:
|
|
21
23
|
|
|
@@ -160,28 +162,26 @@ obtained via the help mechanism described in the previous section.
|
|
|
160
162
|
|
|
161
163
|
### Regexp-Based Ignores
|
|
162
164
|
|
|
163
|
-
Settings like `ignore_tables`, `ignore_indexes`, and so on
|
|
164
|
-
identifiers to ignore. These can be either:
|
|
165
|
+
Settings like `ignore_tables`, `ignore_indexes`, `ignore_models` and so on
|
|
166
|
+
accept list of identifiers to ignore. These can be either:
|
|
165
167
|
|
|
166
168
|
1. Strings - in which case an exact match is needed.
|
|
167
169
|
2. Regexps - which are matched against object names, and matching ones are
|
|
168
170
|
excluded from output.
|
|
169
171
|
|
|
170
|
-
For example, to ignore all tables starting with `legacy_`
|
|
172
|
+
For example, to ignore all tables starting with `legacy_` and all models under
|
|
173
|
+
the `Legacy::` namespace you can write:
|
|
171
174
|
|
|
172
175
|
```ruby
|
|
173
176
|
ActiveRecordDoctor.configure do
|
|
174
177
|
global :ignore_tables, [
|
|
175
|
-
# Ignore internal Rails-related tables.
|
|
176
|
-
"ar_internal_metadata",
|
|
177
|
-
"schema_migrations",
|
|
178
|
-
"active_storage_blobs",
|
|
179
|
-
"active_storage_attachments",
|
|
180
|
-
"action_text_rich_texts",
|
|
181
|
-
|
|
182
178
|
# Ignore all legacy tables.
|
|
183
179
|
/^legacy_/
|
|
184
180
|
]
|
|
181
|
+
global :ignore_models, [
|
|
182
|
+
# Ignore all legacy models.
|
|
183
|
+
/^Legacy::/
|
|
184
|
+
]
|
|
185
185
|
end
|
|
186
186
|
```
|
|
187
187
|
|
|
@@ -270,7 +270,7 @@ Supported configuration options:
|
|
|
270
270
|
|
|
271
271
|
- `enabled` - set to `false` to disable the detector altogether
|
|
272
272
|
- `ignore_tables` - tables whose indexes should never be reported as extraneous.
|
|
273
|
-
- `
|
|
273
|
+
- `ignore_indexes` - indexes that should never be reported as extraneous.
|
|
274
274
|
|
|
275
275
|
### Detecting Unindexed `deleted_at` Columns
|
|
276
276
|
|
|
@@ -303,16 +303,15 @@ Supported configuration options:
|
|
|
303
303
|
|
|
304
304
|
### Detecting Missing Foreign Key Constraints
|
|
305
305
|
|
|
306
|
-
If `
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
level.
|
|
306
|
+
If `User` defines a `belongs_to` association to `Profile` then the underlying
|
|
307
|
+
column (`users.profile_id` by convention) should be marked as a foreign key in
|
|
308
|
+
the database. If it's not then it's possible to end up in a situation where a
|
|
309
|
+
user is referencing a non-existent profile.
|
|
311
310
|
|
|
312
311
|
`active_record_doctor` can automatically detect foreign keys that could benefit
|
|
313
312
|
from a foreign key constraint (a future version will generate a migration that
|
|
314
|
-
add the constraint; for now, it's your job). You can obtain the list of
|
|
315
|
-
|
|
313
|
+
add the constraint; for now, it's your job). You can obtain the list of missing
|
|
314
|
+
foreign key constraints with the following command:
|
|
316
315
|
|
|
317
316
|
```bash
|
|
318
317
|
bundle exec rake active_record_doctor:missing_foreign_keys
|
|
@@ -332,9 +331,9 @@ end
|
|
|
332
331
|
Supported configuration options:
|
|
333
332
|
|
|
334
333
|
- `enabled` - set to `false` to disable the detector altogether
|
|
335
|
-
- `
|
|
336
|
-
- `
|
|
337
|
-
checked.
|
|
334
|
+
- `ignore_models` - models whose associations should not be checked.
|
|
335
|
+
- `ignore_associations` - associations, written as Model.association, that
|
|
336
|
+
should not be checked.
|
|
338
337
|
|
|
339
338
|
### Detecting Models Referencing Undefined Tables
|
|
340
339
|
|
|
@@ -403,7 +402,8 @@ Supported configuration options:
|
|
|
403
402
|
|
|
404
403
|
If there's an unconditional presence validation on a column then it should be
|
|
405
404
|
marked as non-`NULL`-able at the database level or should have a `IS NOT NULL`
|
|
406
|
-
constraint.
|
|
405
|
+
constraint. Timestamp columns are also expected to be made `NOT NULL` as they're
|
|
406
|
+
managed automatically by Active Record.
|
|
407
407
|
|
|
408
408
|
In order to detect columns whose presence is required but that are marked
|
|
409
409
|
`null: true` in the database run the following command:
|
|
@@ -433,7 +433,8 @@ Supported configuration options:
|
|
|
433
433
|
### Detecting Missing Presence Validations
|
|
434
434
|
|
|
435
435
|
If a column is marked as `null: false` then it's likely it should have the
|
|
436
|
-
corresponding presence validator
|
|
436
|
+
corresponding presence validator or an appropriately configured inclusion or
|
|
437
|
+
exclusion validation.
|
|
437
438
|
|
|
438
439
|
In order to detect models lacking these validations run:
|
|
439
440
|
|
|
@@ -640,17 +641,60 @@ add a primary key to companies
|
|
|
640
641
|
Supported configuration options:
|
|
641
642
|
|
|
642
643
|
- `enabled` - set to `false` to disable the detector altogether
|
|
643
|
-
- `ignore_tables` - tables whose primary key
|
|
644
|
+
- `ignore_tables` - tables whose primary key existence should not be checked
|
|
644
645
|
|
|
645
|
-
|
|
646
|
+
### Detecting Tables Without Timestamps
|
|
647
|
+
|
|
648
|
+
Tables should have timestamp columns (`created_at`/`updated_at`). Otherwise, it becomes problematic
|
|
649
|
+
to easily find when the record was created/updated, if the table is active or can be removed,
|
|
650
|
+
automatic Rails cache expiration after record updates is not possible.
|
|
651
|
+
|
|
652
|
+
Running the command below will list all tables without default timestamp columns:
|
|
653
|
+
|
|
654
|
+
```
|
|
655
|
+
bundle exec rake active_record_doctor:table_without_timestamps
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
The output of the command looks like this:
|
|
659
|
+
|
|
660
|
+
```
|
|
661
|
+
add a created_at column to companies
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
Supported configuration options:
|
|
665
|
+
|
|
666
|
+
- `enabled` - set to `false` to disable the detector altogether
|
|
667
|
+
- `ignore_tables` - tables whose timestamp columns existence should not be checked
|
|
668
|
+
|
|
669
|
+
### Detecting Unused Tables
|
|
670
|
+
|
|
671
|
+
**WARNING**: this detector is disabled by default and has to be turned on explicitly.
|
|
672
|
+
|
|
673
|
+
When a table is not used by the application it should usually be dropped from the database.
|
|
674
|
+
|
|
675
|
+
Running the command below will list all tables that are not referenced by any model:
|
|
676
|
+
|
|
677
|
+
```
|
|
678
|
+
bundle exec rake active_record_doctor:unused_tables
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
The output of the command looks like this:
|
|
646
682
|
|
|
647
|
-
|
|
648
|
-
|
|
683
|
+
```
|
|
684
|
+
The users table is not referenced by any Rails model and can be dropped
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
Supported configuration options:
|
|
688
|
+
|
|
689
|
+
- `enabled` - set to `true` to enable the detector
|
|
690
|
+
- `ignore_tables` - tables that are not represented by models but cannot yet be dropped from the database (i.e. because the deployed version of the code may still be reading it)
|
|
691
|
+
|
|
692
|
+
## Ruby and Rails Compatibility Policy
|
|
649
693
|
|
|
650
|
-
1.
|
|
694
|
+
1. `active_record_doctor` supports all Ruby on Rails versions that are supported
|
|
695
|
+
by the Rails core team.
|
|
696
|
+
2. If a Ruby version is compatible with a supported Rails version then it's also
|
|
651
697
|
supported by `active_record_doctor`.
|
|
652
|
-
2. If a Ruby version is compatible with a supported Rails version then it's
|
|
653
|
-
also supported by `active_record_doctor`.
|
|
654
698
|
3. Only the most recent teeny Ruby versions and patch Rails versions are supported.
|
|
655
699
|
|
|
656
700
|
## Author
|
|
@@ -36,8 +36,8 @@ ActiveRecordDoctor.configure do
|
|
|
36
36
|
|
|
37
37
|
detector :missing_foreign_keys,
|
|
38
38
|
enabled: true,
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
ignore_models: [],
|
|
40
|
+
ignore_associations: []
|
|
41
41
|
|
|
42
42
|
detector :missing_non_null_constraint,
|
|
43
43
|
enabled: true,
|
|
@@ -64,6 +64,10 @@ ActiveRecordDoctor.configure do
|
|
|
64
64
|
enabled: true,
|
|
65
65
|
ignore_tables: []
|
|
66
66
|
|
|
67
|
+
detector :table_without_timestamps,
|
|
68
|
+
enabled: true,
|
|
69
|
+
ignore_tables: []
|
|
70
|
+
|
|
67
71
|
detector :undefined_table_references,
|
|
68
72
|
enabled: true,
|
|
69
73
|
ignore_models: []
|
|
@@ -79,4 +83,8 @@ ActiveRecordDoctor.configure do
|
|
|
79
83
|
enabled: true,
|
|
80
84
|
ignore_tables: [],
|
|
81
85
|
ignore_columns: []
|
|
86
|
+
|
|
87
|
+
detector :unused_tables,
|
|
88
|
+
enabled: false,
|
|
89
|
+
ignore_tables: []
|
|
82
90
|
end
|
|
@@ -136,8 +136,7 @@ module ActiveRecordDoctor
|
|
|
136
136
|
end
|
|
137
137
|
|
|
138
138
|
def check_constraints(table_name)
|
|
139
|
-
|
|
140
|
-
if connection.respond_to?(:supports_check_constraints?) && connection.supports_check_constraints?
|
|
139
|
+
if connection.supports_check_constraints?
|
|
141
140
|
connection.check_constraints(table_name).select(&:validated?).map(&:expression)
|
|
142
141
|
elsif Utils.postgresql?(connection)
|
|
143
142
|
definitions =
|
|
@@ -245,6 +244,13 @@ module ActiveRecordDoctor
|
|
|
245
244
|
end
|
|
246
245
|
end
|
|
247
246
|
|
|
247
|
+
def looks_like_foreign_key?(column)
|
|
248
|
+
type = column.type.to_s
|
|
249
|
+
|
|
250
|
+
column.name.end_with?("_id") &&
|
|
251
|
+
(type == "integer" || type.include?("uuid"))
|
|
252
|
+
end
|
|
253
|
+
|
|
248
254
|
def each_foreign_key(table_name)
|
|
249
255
|
log("Iterating over foreign keys on #{table_name}") do
|
|
250
256
|
connection.foreign_keys(table_name).each do |foreign_key|
|
|
@@ -256,15 +262,8 @@ module ActiveRecordDoctor
|
|
|
256
262
|
end
|
|
257
263
|
|
|
258
264
|
def each_table(except: [])
|
|
259
|
-
tables =
|
|
260
|
-
if ActiveRecord::VERSION::STRING >= "5.1"
|
|
261
|
-
connection.tables
|
|
262
|
-
else
|
|
263
|
-
connection.data_sources
|
|
264
|
-
end
|
|
265
|
-
|
|
266
265
|
log("Iterating over tables") do
|
|
267
|
-
tables.each do |table|
|
|
266
|
+
connection.tables.each do |table|
|
|
268
267
|
case
|
|
269
268
|
when ignored?(table, except)
|
|
270
269
|
log("#{table} - ignored via the configuration; skipping")
|
|
@@ -58,7 +58,7 @@ module ActiveRecordDoctor
|
|
|
58
58
|
# model lacks the next leg in the :through relationship. For
|
|
59
59
|
# instance, if user has many comments through posts then a nil
|
|
60
60
|
# source_reflection means that Post doesn't define +has_many :comments+.
|
|
61
|
-
if
|
|
61
|
+
if association.through_reflection? && association.source_reflection.nil?
|
|
62
62
|
log("through association with nil source_reflection")
|
|
63
63
|
|
|
64
64
|
through_association = model.reflect_on_association(association.options.fetch(:through))
|
|
@@ -77,14 +77,14 @@ module ActiveRecordDoctor
|
|
|
77
77
|
associated_models: [through_association.klass.name],
|
|
78
78
|
associated_models_type: "join"
|
|
79
79
|
)
|
|
80
|
-
next
|
|
81
80
|
end
|
|
81
|
+
next
|
|
82
82
|
end
|
|
83
83
|
|
|
84
84
|
associated_models, associated_models_type =
|
|
85
85
|
if association.polymorphic?
|
|
86
86
|
[models_having_association_with_options(as: association.name), nil]
|
|
87
|
-
elsif
|
|
87
|
+
elsif association.through_reflection?
|
|
88
88
|
[[association.source_reflection.active_record], "join"]
|
|
89
89
|
else
|
|
90
90
|
[[association.klass], nil]
|
|
@@ -159,10 +159,6 @@ module ActiveRecordDoctor
|
|
|
159
159
|
end
|
|
160
160
|
end
|
|
161
161
|
|
|
162
|
-
def through?(reflection)
|
|
163
|
-
reflection.is_a?(ActiveRecord::Reflection::ThroughReflection)
|
|
164
|
-
end
|
|
165
|
-
|
|
166
162
|
def defines_destroy_callbacks?(model)
|
|
167
163
|
# Destroying an associated model involves loading it first hence
|
|
168
164
|
# initialize and find are present. If they are defined on the model
|
|
@@ -18,9 +18,12 @@ module ActiveRecordDoctor
|
|
|
18
18
|
|
|
19
19
|
private
|
|
20
20
|
|
|
21
|
-
def message(model:,
|
|
21
|
+
def message(model:, column:, table:, database_maximum:, model_maximum:)
|
|
22
|
+
attribute = column.name
|
|
22
23
|
# rubocop:disable Layout/LineLength
|
|
23
|
-
if database_maximum &&
|
|
24
|
+
if database_maximum && column.respond_to?(:array?) && column.array?
|
|
25
|
+
"the schema limits each #{table}.#{attribute} array element to #{database_maximum} characters - use a custom validation for this attribute or add it to the ignore list"
|
|
26
|
+
elsif database_maximum && model_maximum
|
|
24
27
|
"the schema limits #{table}.#{attribute} to #{database_maximum} characters but the length validator on #{model}.#{attribute} enforces a maximum of #{model_maximum} characters - set both limits to the same value or remove both"
|
|
25
28
|
elsif database_maximum && model_maximum.nil?
|
|
26
29
|
"the schema limits #{table}.#{attribute} to #{database_maximum} characters but there's no length validator on #{model}.#{attribute} - remove the database limit or add the validator"
|
|
@@ -33,21 +36,23 @@ module ActiveRecordDoctor
|
|
|
33
36
|
def detect
|
|
34
37
|
each_model(except: config(:ignore_models), existing_tables_only: true) do |model|
|
|
35
38
|
each_attribute(model, except: config(:ignore_attributes), type: [:string, :text]) do |column|
|
|
36
|
-
model_maximum =
|
|
37
|
-
|
|
39
|
+
model_maximum = maximum_allowed_by_length_validation(model, column.name.to_sym)
|
|
40
|
+
database_maximum = column.limit
|
|
41
|
+
next if model_maximum == database_maximum
|
|
42
|
+
next if database_maximum && covered_by_inclusion_validation?(model, column.name.to_sym, database_maximum)
|
|
38
43
|
|
|
39
44
|
problem!(
|
|
40
45
|
model: model.name,
|
|
41
|
-
|
|
46
|
+
column: column,
|
|
42
47
|
table: model.table_name,
|
|
43
|
-
database_maximum:
|
|
48
|
+
database_maximum: database_maximum,
|
|
44
49
|
model_maximum: model_maximum
|
|
45
50
|
)
|
|
46
51
|
end
|
|
47
52
|
end
|
|
48
53
|
end
|
|
49
54
|
|
|
50
|
-
def
|
|
55
|
+
def maximum_allowed_by_length_validation(model, column)
|
|
51
56
|
length_validator = model.validators.find do |validator|
|
|
52
57
|
validator.kind == :length &&
|
|
53
58
|
validator.options.include?(:maximum) &&
|
|
@@ -55,6 +60,20 @@ module ActiveRecordDoctor
|
|
|
55
60
|
end
|
|
56
61
|
length_validator ? length_validator.options[:maximum] : nil
|
|
57
62
|
end
|
|
63
|
+
|
|
64
|
+
def covered_by_inclusion_validation?(model, column, limit)
|
|
65
|
+
inclusion_validator = model.validators.find do |validator|
|
|
66
|
+
validator.kind == :inclusion &&
|
|
67
|
+
validator.attributes.include?(column) &&
|
|
68
|
+
[:if, :unless].none? { |option| validator.options.key?(option) } &&
|
|
69
|
+
[:allow_nil, :allow_blank].none? { |option| validator.options[option] == true }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
return false if !inclusion_validator
|
|
73
|
+
|
|
74
|
+
values = inclusion_validator.options[:in] || inclusion_validator.options[:within]
|
|
75
|
+
values.is_a?(Array) && values.all? { |value| value.is_a?(String) && value.size <= limit }
|
|
76
|
+
end
|
|
58
77
|
end
|
|
59
78
|
end
|
|
60
79
|
end
|
|
@@ -7,12 +7,12 @@ module ActiveRecordDoctor
|
|
|
7
7
|
class MissingForeignKeys < Base # :nodoc:
|
|
8
8
|
@description = "detect foreign-key-like columns lacking an actual foreign key constraint"
|
|
9
9
|
@config = {
|
|
10
|
-
|
|
11
|
-
description: "
|
|
10
|
+
ignore_models: {
|
|
11
|
+
description: "models whose columns should not be checked",
|
|
12
12
|
global: true
|
|
13
13
|
},
|
|
14
|
-
|
|
15
|
-
description: "
|
|
14
|
+
ignore_associations: {
|
|
15
|
+
description: "associations, written as Model.association, that should not be checked"
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
|
|
@@ -23,34 +23,19 @@ module ActiveRecordDoctor
|
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def detect
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
# multiple tables but a foreign key constraint can reference
|
|
30
|
-
# a single predefined table.
|
|
31
|
-
next unless named_like_foreign_key?(column)
|
|
32
|
-
next if foreign_key?(table, column)
|
|
33
|
-
next if polymorphic_foreign_key?(table, column)
|
|
34
|
-
|
|
35
|
-
problem!(table: table, column: column.name)
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
26
|
+
each_model(except: config(:ignore_models), existing_tables_only: true) do |model|
|
|
27
|
+
foreign_keys = connection.foreign_keys(model.table_name)
|
|
28
|
+
foreign_key_columns = foreign_keys.map { |key| key.options[:column] }
|
|
39
29
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
30
|
+
each_association(model, type: :belongs_to) do |association|
|
|
31
|
+
next if ignored?("#{model.name}.#{association.name}", config(:ignore_associations))
|
|
32
|
+
next if association.options[:polymorphic]
|
|
43
33
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
foreign_key.options[:column] == column.name
|
|
47
|
-
end
|
|
48
|
-
end
|
|
34
|
+
has_foreign_key = foreign_key_columns.include?(association.foreign_key)
|
|
35
|
+
next if has_foreign_key
|
|
49
36
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
connection.columns(table).any? do |another_column|
|
|
53
|
-
another_column.name == type_column_name
|
|
37
|
+
problem!(table: model.table_name, column: association.foreign_key)
|
|
38
|
+
end
|
|
54
39
|
end
|
|
55
40
|
end
|
|
56
41
|
end
|
|
@@ -18,15 +18,27 @@ module ActiveRecordDoctor
|
|
|
18
18
|
|
|
19
19
|
private
|
|
20
20
|
|
|
21
|
+
TIMESTAMPS = ["created_at", "created_on", "updated_at", "updated_on"].freeze
|
|
22
|
+
|
|
21
23
|
def message(column:, table:)
|
|
22
|
-
|
|
24
|
+
if TIMESTAMPS.include?(column)
|
|
25
|
+
<<~WARN.squish
|
|
26
|
+
add `NOT NULL` to #{table}.#{column} - timestamp columns are set
|
|
27
|
+
automatically by Active Record and allowing NULL may lead to
|
|
28
|
+
inconsistencies introduced by bulk operations
|
|
29
|
+
WARN
|
|
30
|
+
else
|
|
31
|
+
"add `NOT NULL` to #{table}.#{column} - models validates its presence but it's not non-NULL in the database"
|
|
32
|
+
end
|
|
23
33
|
end
|
|
24
34
|
|
|
25
35
|
def detect
|
|
26
36
|
table_models = models.select(&:table_exists?).group_by(&:table_name)
|
|
37
|
+
views = connection.views
|
|
27
38
|
|
|
28
39
|
table_models.each do |table, models|
|
|
29
40
|
next if ignored?(table, config(:ignore_tables))
|
|
41
|
+
next if views.include?(table)
|
|
30
42
|
|
|
31
43
|
concrete_models = models.reject do |model|
|
|
32
44
|
model.abstract_class? || sti_base_model?(model)
|
|
@@ -50,6 +62,8 @@ module ActiveRecordDoctor
|
|
|
50
62
|
end
|
|
51
63
|
|
|
52
64
|
def non_null_needed?(model, column)
|
|
65
|
+
return true if TIMESTAMPS.include?(column.name)
|
|
66
|
+
|
|
53
67
|
belongs_to = model.reflect_on_all_associations(:belongs_to).find do |reflection|
|
|
54
68
|
reflection.foreign_key == column.name ||
|
|
55
69
|
(reflection.polymorphic? && reflection.foreign_type == column.name)
|
|
@@ -67,6 +81,7 @@ module ActiveRecordDoctor
|
|
|
67
81
|
model.validators.select do |validator|
|
|
68
82
|
validator.is_a?(ActiveRecord::Validations::PresenceValidator) &&
|
|
69
83
|
!validator.options[:allow_nil] &&
|
|
84
|
+
validator.options[:on].blank? &&
|
|
70
85
|
(rails_belongs_to_presence_validator?(validator) || !conditional_validator?(validator))
|
|
71
86
|
end
|
|
72
87
|
end
|
|
@@ -21,78 +21,169 @@ module ActiveRecordDoctor
|
|
|
21
21
|
|
|
22
22
|
private
|
|
23
23
|
|
|
24
|
-
def message(column:, model:)
|
|
25
|
-
|
|
24
|
+
def message(type:, column:, reflection:, model:)
|
|
25
|
+
case type
|
|
26
|
+
when :missing_validator
|
|
27
|
+
"add a `presence` validator to #{model}.#{column} - it's NOT NULL but lacks a validator"
|
|
28
|
+
when :optional_association
|
|
29
|
+
"add `optional: false` to #{model}.#{reflection.name} - the foreign key #{reflection.foreign_key} is NOT NULL"
|
|
30
|
+
when :optional_polymorphic_association
|
|
31
|
+
"add `optional: false` to #{model}.#{reflection.name} - the foreign key #{reflection.foreign_key} or type #{reflection.foreign_type} are NOT NULL" # rubocop:disable Layout/LineLength
|
|
32
|
+
end
|
|
26
33
|
end
|
|
27
34
|
|
|
28
35
|
def detect
|
|
29
36
|
each_model(except: config(:ignore_models), existing_tables_only: true) do |model|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
37
|
+
# List all columns and then remove those that don't need or don't have
|
|
38
|
+
# a missing validator.
|
|
39
|
+
problematic_columns = connection.columns(model.table_name)
|
|
40
|
+
problematic_columns.reject! do |column|
|
|
41
|
+
# The primary key, timestamps, and counter caches are special
|
|
42
|
+
# columns that are automatically managed by Rails and don't need
|
|
43
|
+
# an explicit presence validator.
|
|
44
|
+
column.name == model.primary_key ||
|
|
45
|
+
["created_at", "updated_at", "created_on", "updated_on"].include?(column.name) ||
|
|
46
|
+
counter_cache_column?(model, column) ||
|
|
47
|
+
|
|
48
|
+
# NULL-able columns don't need a presence validator as they can be
|
|
49
|
+
# set to NULL after all. A check constraint (column IS NOT NULL)
|
|
50
|
+
# is an alternative approach and the absence of such constraint is
|
|
51
|
+
# tested below.
|
|
52
|
+
(column.null && !not_null_check_constraint_exists?(model.table_name, column)) ||
|
|
53
|
+
|
|
54
|
+
# If requested, columns with a default value don't need presence
|
|
55
|
+
# validation as they'd have the default value substituted automatically.
|
|
56
|
+
(config(:ignore_columns_with_default) && (column.default || column.default_function)) ||
|
|
57
|
+
|
|
58
|
+
# Explicitly ignored columns should be skipped.
|
|
59
|
+
ignored?("#{model.name}.#{column.name}", config(:ignore_attributes))
|
|
35
60
|
end
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
61
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
62
|
+
# At this point the only columns that are left are those that DO
|
|
63
|
+
# need presence validation in the model. Let's iterate over all
|
|
64
|
+
# validators to see which columns are actually validated, but before
|
|
65
|
+
# we do that let's define a map for quickly translating foreign key
|
|
66
|
+
# names to belongs_to association names.
|
|
67
|
+
column_name_to_association_name = {}
|
|
68
|
+
model.reflect_on_all_associations(:belongs_to).each do |reflection|
|
|
69
|
+
column_name_to_association_name[reflection.foreign_key] = reflection.name
|
|
70
|
+
if reflection.polymorphic?
|
|
71
|
+
column_name_to_association_name[reflection.foreign_type] = reflection.name
|
|
72
|
+
end
|
|
73
|
+
end
|
|
44
74
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
75
|
+
# We're now ready to iterate over the validators and remove columns
|
|
76
|
+
# that are validated directly or via an association name.
|
|
77
|
+
model.validators.each do |validator|
|
|
78
|
+
problematic_columns.reject! do |column|
|
|
79
|
+
attribute_names = [
|
|
80
|
+
column.name.to_sym,
|
|
81
|
+
column_name_to_association_name[column.name]
|
|
82
|
+
].compact
|
|
83
|
+
|
|
84
|
+
case validator
|
|
85
|
+
|
|
86
|
+
# A regular presence validator is enough if the column name is
|
|
87
|
+
# listed among the attributes it's validating.
|
|
88
|
+
when ActiveRecord::Validations::PresenceValidator
|
|
89
|
+
(validator.attributes & attribute_names).present?
|
|
90
|
+
|
|
91
|
+
# An inclusion validator ensures the column is not nil if it covers
|
|
92
|
+
# the column and nil is NOT one of the values it allows.
|
|
93
|
+
when ActiveModel::Validations::InclusionValidator
|
|
94
|
+
validator_items = inclusion_or_exclusion_validator_items(validator)
|
|
95
|
+
(validator.attributes & attribute_names).present? &&
|
|
96
|
+
(dynamic_validator_items?(validator_items) || validator_items.exclude?(nil))
|
|
97
|
+
|
|
98
|
+
# An exclusion validator ensures the column is not nil if it covers
|
|
99
|
+
# the column and excludes nil as an allowed value explicitly.
|
|
100
|
+
when ActiveModel::Validations::ExclusionValidator
|
|
101
|
+
validator_items = inclusion_or_exclusion_validator_items(validator)
|
|
102
|
+
(validator.attributes & attribute_names).present? &&
|
|
103
|
+
(dynamic_validator_items?(validator_items) || validator_items.include?(nil))
|
|
104
|
+
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
48
108
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
109
|
+
# Associations need to be checked whether they're marked optional
|
|
110
|
+
# while the underlying foreign key or type columns are marked NOT NULL.
|
|
111
|
+
problematic_associations = []
|
|
112
|
+
problematic_polymorphic_associations = []
|
|
113
|
+
|
|
114
|
+
model.reflect_on_all_associations(:belongs_to).each do |reflection|
|
|
115
|
+
foreign_key_column = problematic_columns.find { |column| column.name == reflection.foreign_key }
|
|
116
|
+
if reflection.polymorphic?
|
|
117
|
+
# If the foreign key and type are not one of the columns that lack
|
|
118
|
+
# a validator then it means the association added a validator and
|
|
119
|
+
# is configured correctly.
|
|
120
|
+
foreign_type_column = problematic_columns.find { |column| column.name == reflection.foreign_type }
|
|
121
|
+
next if foreign_key_column.nil? && foreign_type_column.nil?
|
|
122
|
+
|
|
123
|
+
# Otherwise, don't report errors about missing validators on the
|
|
124
|
+
# foreign key or type, but instead ...
|
|
125
|
+
problematic_columns.delete(foreign_key_column)
|
|
126
|
+
problematic_columns.delete(foreign_type_column)
|
|
127
|
+
|
|
128
|
+
# ... report an error about an incorrectly configured polymorphic
|
|
129
|
+
# association.
|
|
130
|
+
problematic_polymorphic_associations << reflection
|
|
131
|
+
else
|
|
132
|
+
# If the foreign key is not one of the columns that lack a
|
|
133
|
+
# validator then it means the association added a validator and is
|
|
134
|
+
# configured correctly.
|
|
135
|
+
next if foreign_key_column.nil?
|
|
136
|
+
|
|
137
|
+
# Otherwise, don't report an error about a missing validator on
|
|
138
|
+
# the foreign key, but instead ...
|
|
139
|
+
problematic_columns.delete(foreign_key_column)
|
|
140
|
+
|
|
141
|
+
# ... report an error about an incorrectly configured association.
|
|
142
|
+
problematic_associations << reflection
|
|
143
|
+
end
|
|
144
|
+
end
|
|
57
145
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
146
|
+
# Finally, regular and polymorphic associations that are explicitly
|
|
147
|
+
# ignored should be removed from the output. It's NOT enough to skip
|
|
148
|
+
# processing them in the loop above because their underlying foreign
|
|
149
|
+
# key and type columns must be removed from output, too.
|
|
150
|
+
problematic_associations.reject! do |reflection|
|
|
151
|
+
config(:ignore_attributes).include?("#{model.name}.#{reflection.name}")
|
|
152
|
+
end
|
|
153
|
+
problematic_polymorphic_associations.reject! do |reflection|
|
|
154
|
+
config(:ignore_attributes).include?("#{model.name}.#{reflection.name}")
|
|
155
|
+
end
|
|
62
156
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
!
|
|
157
|
+
# Job is done and all accumulated errors can be reported.
|
|
158
|
+
problematic_polymorphic_associations.each do |reflection|
|
|
159
|
+
problem!(type: :optional_polymorphic_association, column: nil, reflection: reflection, model: model.name)
|
|
160
|
+
end
|
|
161
|
+
problematic_associations.each do |reflection|
|
|
162
|
+
problem!(type: :optional_association, column: nil, reflection: reflection, model: model.name)
|
|
163
|
+
end
|
|
164
|
+
problematic_columns.each do |column|
|
|
165
|
+
problem!(type: :missing_validator, column: column.name, reflection: nil, model: model.name)
|
|
166
|
+
end
|
|
66
167
|
end
|
|
67
168
|
end
|
|
68
169
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
return true if validator_items.is_a?(Proc)
|
|
73
|
-
|
|
74
|
-
validator.is_a?(ActiveModel::Validations::ExclusionValidator) &&
|
|
75
|
-
validator.attributes.include?(column.name.to_sym) &&
|
|
76
|
-
validator_items.include?(nil)
|
|
77
|
-
end
|
|
170
|
+
# Normalizes the list of values passed to an inclusion or exclusion validator.
|
|
171
|
+
def inclusion_or_exclusion_validator_items(validator)
|
|
172
|
+
validator.options[:in] || validator.options[:within] || []
|
|
78
173
|
end
|
|
79
174
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
reflection.foreign_key == column.name
|
|
85
|
-
end
|
|
86
|
-
allowed_attributes << belongs_to.name.to_sym if belongs_to
|
|
87
|
-
|
|
88
|
-
model.validators.any? do |validator|
|
|
89
|
-
validator.is_a?(ActiveRecord::Validations::PresenceValidator) &&
|
|
90
|
-
(validator.attributes & allowed_attributes).present?
|
|
91
|
-
end
|
|
175
|
+
# The allowed values can be a Proc or a method name (Symbol) that's resolved
|
|
176
|
+
# at validation time, so the set of values isn't known statically.
|
|
177
|
+
def dynamic_validator_items?(validator_items)
|
|
178
|
+
validator_items.is_a?(Proc) || validator_items.is_a?(Symbol)
|
|
92
179
|
end
|
|
93
180
|
|
|
94
|
-
|
|
95
|
-
|
|
181
|
+
# Determines whether the given column is used as a counter cache column by
|
|
182
|
+
# a has_many association on the model.
|
|
183
|
+
def counter_cache_column?(model, column)
|
|
184
|
+
model.reflect_on_all_associations(:has_many).any? do |reflection|
|
|
185
|
+
reflection.has_cached_counter? && reflection.counter_cache_column == column.name
|
|
186
|
+
end
|
|
96
187
|
end
|
|
97
188
|
end
|
|
98
189
|
end
|
|
@@ -65,9 +65,8 @@ module ActiveRecordDoctor
|
|
|
65
65
|
# put true literally.
|
|
66
66
|
case_sensitive = validator.options.fetch(:case_sensitive, true)
|
|
67
67
|
|
|
68
|
-
#
|
|
69
|
-
|
|
70
|
-
next if !case_sensitive && Utils.expression_indexes_unsupported?
|
|
68
|
+
# Avoid a false positive if expression indexes are unsupported.
|
|
69
|
+
next if !case_sensitive && !connection.supports_expression_index?
|
|
71
70
|
|
|
72
71
|
validator.attributes.each do |attribute|
|
|
73
72
|
columns = resolve_attributes(model, scope + [attribute])
|
|
@@ -8,7 +8,7 @@ module ActiveRecordDoctor
|
|
|
8
8
|
@description = "detect tables without primary keys"
|
|
9
9
|
@config = {
|
|
10
10
|
ignore_tables: {
|
|
11
|
-
description: "tables whose primary key
|
|
11
|
+
description: "tables whose primary key existence should not be checked",
|
|
12
12
|
global: true
|
|
13
13
|
}
|
|
14
14
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_record_doctor/detectors/base"
|
|
4
|
+
|
|
5
|
+
module ActiveRecordDoctor
|
|
6
|
+
module Detectors
|
|
7
|
+
class TableWithoutTimestamps < Base # :nodoc:
|
|
8
|
+
@description = "detect tables without created_at/updated_at columns"
|
|
9
|
+
@config = {
|
|
10
|
+
ignore_tables: {
|
|
11
|
+
description: "tables whose timestamp columns existence should not be checked",
|
|
12
|
+
global: true
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
TIMESTAMPS = {
|
|
19
|
+
"created_at" => "created_on",
|
|
20
|
+
"updated_at" => "updated_on"
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
def message(table:, column:)
|
|
24
|
+
"add a #{column} column to #{table}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def detect
|
|
28
|
+
each_table(except: config(:ignore_tables)) do |table|
|
|
29
|
+
TIMESTAMPS.each do |column, alternative_column|
|
|
30
|
+
unless column(table, column) || column(table, alternative_column)
|
|
31
|
+
problem!(table: table, column: column)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -27,7 +27,7 @@ module ActiveRecordDoctor
|
|
|
27
27
|
def detect
|
|
28
28
|
each_table(except: config(:ignore_tables)) do |table|
|
|
29
29
|
each_column(table, except: config(:ignore_columns)) do |column|
|
|
30
|
-
next unless
|
|
30
|
+
next unless looks_like_foreign_key?(column) || foreign_key?(table, column)
|
|
31
31
|
next if indexed?(table, column)
|
|
32
32
|
next if indexed_as_polymorphic?(table, column)
|
|
33
33
|
next if connection.primary_key(table) == column.name
|
|
@@ -46,10 +46,6 @@ module ActiveRecordDoctor
|
|
|
46
46
|
end
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
-
def named_like_foreign_key?(column)
|
|
50
|
-
column.name.end_with?("_id")
|
|
51
|
-
end
|
|
52
|
-
|
|
53
49
|
def foreign_key?(table, column)
|
|
54
50
|
connection.foreign_keys(table).any? do |foreign_key|
|
|
55
51
|
foreign_key.column == column.name
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_record_doctor/detectors/base"
|
|
4
|
+
|
|
5
|
+
module ActiveRecordDoctor
|
|
6
|
+
module Detectors
|
|
7
|
+
class UnusedTables < Base # :nodoc:
|
|
8
|
+
@description = "detect tables not referenced by any model"
|
|
9
|
+
@config = {
|
|
10
|
+
ignore_tables: {
|
|
11
|
+
description: "tables whose corresponding models should not be checked for existence",
|
|
12
|
+
global: true
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def message(table:)
|
|
19
|
+
"The #{table} table is not referenced by any Rails model and can be dropped"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def detect
|
|
23
|
+
each_table(except: config(:ignore_tables)) do |table|
|
|
24
|
+
next if models.any? { |model| model.table_name == table }
|
|
25
|
+
|
|
26
|
+
problem!(table: table)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -27,7 +27,7 @@ module ActiveRecordDoctor # :nodoc:
|
|
|
27
27
|
# We can't use #all? because of its short-circuit behavior - it stops
|
|
28
28
|
# iteration and returns false upon the first falsey value. This
|
|
29
29
|
# prevents other detectors from running if there's a failure.
|
|
30
|
-
ActiveRecordDoctor.detectors.
|
|
30
|
+
ActiveRecordDoctor.detectors.each_key do |name|
|
|
31
31
|
success = false if !run_one(name)
|
|
32
32
|
end
|
|
33
33
|
|
|
@@ -11,10 +11,8 @@ module ActiveRecordDoctor
|
|
|
11
11
|
connection.adapter_name == "Mysql2"
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
def
|
|
15
|
-
|
|
16
|
-
# Active Record is unable to correctly parse expression indexes for MySQL.
|
|
17
|
-
(mysql?(connection) && ActiveRecord::VERSION::STRING < "7.1")
|
|
14
|
+
def sqlite?(connection = ActiveRecord::Base.connection)
|
|
15
|
+
connection.adapter_name == "SQLite"
|
|
18
16
|
end
|
|
19
17
|
end
|
|
20
18
|
end
|
data/lib/active_record_doctor.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "active_record_doctor/railtie" if defined?(Rails
|
|
3
|
+
require "active_record_doctor/railtie" if defined?(Rails::Railtie)
|
|
4
4
|
require "active_record_doctor/utils"
|
|
5
5
|
require "active_record_doctor/logger"
|
|
6
6
|
require "active_record_doctor/logger/dummy"
|
|
@@ -21,6 +21,8 @@ require "active_record_doctor/detectors/incorrect_dependent_option"
|
|
|
21
21
|
require "active_record_doctor/detectors/short_primary_key_type"
|
|
22
22
|
require "active_record_doctor/detectors/mismatched_foreign_key_type"
|
|
23
23
|
require "active_record_doctor/detectors/table_without_primary_key"
|
|
24
|
+
require "active_record_doctor/detectors/table_without_timestamps"
|
|
25
|
+
require "active_record_doctor/detectors/unused_tables"
|
|
24
26
|
require "active_record_doctor/errors"
|
|
25
27
|
require "active_record_doctor/help"
|
|
26
28
|
require "active_record_doctor/runner"
|
|
@@ -47,7 +47,7 @@ module ActiveRecordDoctor
|
|
|
47
47
|
# rubocop rule below.
|
|
48
48
|
|
|
49
49
|
<<MIGRATION
|
|
50
|
-
class IndexForeignKeysIn#{table.camelize} < ActiveRecord::Migration#{
|
|
50
|
+
class IndexForeignKeysIn#{table.camelize} < ActiveRecord::Migration[#{ActiveRecord::Migration.current_version}]
|
|
51
51
|
def change
|
|
52
52
|
#{add_indexes(table, indexes)}
|
|
53
53
|
end
|
|
@@ -71,13 +71,5 @@ MIGRATION
|
|
|
71
71
|
" add_index :#{table}, #{columns.inspect}"
|
|
72
72
|
end
|
|
73
73
|
end
|
|
74
|
-
|
|
75
|
-
def migration_version
|
|
76
|
-
if ActiveRecord::VERSION::STRING >= "5.1"
|
|
77
|
-
"[#{ActiveRecord::Migration.current_version}]"
|
|
78
|
-
else
|
|
79
|
-
""
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
74
|
end
|
|
83
75
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: active_record_doctor
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 2.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Greg Navis
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: activerecord
|
|
@@ -16,113 +15,154 @@ dependencies:
|
|
|
16
15
|
requirements:
|
|
17
16
|
- - ">="
|
|
18
17
|
- !ruby/object:Gem::Version
|
|
19
|
-
version:
|
|
18
|
+
version: 7.0.0
|
|
20
19
|
type: :runtime
|
|
21
20
|
prerelease: false
|
|
22
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
22
|
requirements:
|
|
24
23
|
- - ">="
|
|
25
24
|
- !ruby/object:Gem::Version
|
|
26
|
-
version:
|
|
25
|
+
version: 7.0.0
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: minitest
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '5.0'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '5.0'
|
|
27
40
|
- !ruby/object:Gem::Dependency
|
|
28
41
|
name: minitest-fork_executor
|
|
29
42
|
requirement: !ruby/object:Gem::Requirement
|
|
30
43
|
requirements:
|
|
31
44
|
- - "~>"
|
|
32
45
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: 1.
|
|
46
|
+
version: '1.1'
|
|
34
47
|
type: :development
|
|
35
48
|
prerelease: false
|
|
36
49
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
50
|
requirements:
|
|
38
51
|
- - "~>"
|
|
39
52
|
- !ruby/object:Gem::Version
|
|
40
|
-
version: 1.
|
|
53
|
+
version: '1.1'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: minitest-mock
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '5.27'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '5.27'
|
|
41
68
|
- !ruby/object:Gem::Dependency
|
|
42
69
|
name: mysql2
|
|
43
70
|
requirement: !ruby/object:Gem::Requirement
|
|
44
71
|
requirements:
|
|
45
72
|
- - "~>"
|
|
46
73
|
- !ruby/object:Gem::Version
|
|
47
|
-
version: 0.5.
|
|
74
|
+
version: 0.5.6
|
|
48
75
|
type: :development
|
|
49
76
|
prerelease: false
|
|
50
77
|
version_requirements: !ruby/object:Gem::Requirement
|
|
51
78
|
requirements:
|
|
52
79
|
- - "~>"
|
|
53
80
|
- !ruby/object:Gem::Version
|
|
54
|
-
version: 0.5.
|
|
81
|
+
version: 0.5.6
|
|
55
82
|
- !ruby/object:Gem::Dependency
|
|
56
83
|
name: pg
|
|
57
84
|
requirement: !ruby/object:Gem::Requirement
|
|
58
85
|
requirements:
|
|
59
86
|
- - "~>"
|
|
60
87
|
- !ruby/object:Gem::Version
|
|
61
|
-
version: 1.5.
|
|
88
|
+
version: 1.5.9
|
|
62
89
|
type: :development
|
|
63
90
|
prerelease: false
|
|
64
91
|
version_requirements: !ruby/object:Gem::Requirement
|
|
65
92
|
requirements:
|
|
66
93
|
- - "~>"
|
|
67
94
|
- !ruby/object:Gem::Version
|
|
68
|
-
version: 1.5.
|
|
95
|
+
version: 1.5.9
|
|
69
96
|
- !ruby/object:Gem::Dependency
|
|
70
97
|
name: railties
|
|
71
98
|
requirement: !ruby/object:Gem::Requirement
|
|
72
99
|
requirements:
|
|
73
100
|
- - ">="
|
|
74
101
|
- !ruby/object:Gem::Version
|
|
75
|
-
version:
|
|
102
|
+
version: 7.0.0
|
|
76
103
|
type: :development
|
|
77
104
|
prerelease: false
|
|
78
105
|
version_requirements: !ruby/object:Gem::Requirement
|
|
79
106
|
requirements:
|
|
80
107
|
- - ">="
|
|
81
108
|
- !ruby/object:Gem::Version
|
|
82
|
-
version:
|
|
109
|
+
version: 7.0.0
|
|
83
110
|
- !ruby/object:Gem::Dependency
|
|
84
111
|
name: rake
|
|
85
112
|
requirement: !ruby/object:Gem::Requirement
|
|
86
113
|
requirements:
|
|
87
114
|
- - "~>"
|
|
88
115
|
- !ruby/object:Gem::Version
|
|
89
|
-
version:
|
|
116
|
+
version: 13.2.1
|
|
90
117
|
type: :development
|
|
91
118
|
prerelease: false
|
|
92
119
|
version_requirements: !ruby/object:Gem::Requirement
|
|
93
120
|
requirements:
|
|
94
121
|
- - "~>"
|
|
95
122
|
- !ruby/object:Gem::Version
|
|
96
|
-
version:
|
|
123
|
+
version: 13.2.1
|
|
97
124
|
- !ruby/object:Gem::Dependency
|
|
98
|
-
name:
|
|
125
|
+
name: rubocop
|
|
99
126
|
requirement: !ruby/object:Gem::Requirement
|
|
100
127
|
requirements:
|
|
101
128
|
- - "~>"
|
|
102
129
|
- !ruby/object:Gem::Version
|
|
103
|
-
version:
|
|
130
|
+
version: 1.68.0
|
|
104
131
|
type: :development
|
|
105
132
|
prerelease: false
|
|
106
133
|
version_requirements: !ruby/object:Gem::Requirement
|
|
107
134
|
requirements:
|
|
108
135
|
- - "~>"
|
|
109
136
|
- !ruby/object:Gem::Version
|
|
110
|
-
version:
|
|
137
|
+
version: 1.68.0
|
|
111
138
|
- !ruby/object:Gem::Dependency
|
|
112
|
-
name:
|
|
139
|
+
name: sqlite3
|
|
140
|
+
requirement: !ruby/object:Gem::Requirement
|
|
141
|
+
requirements:
|
|
142
|
+
- - ">="
|
|
143
|
+
- !ruby/object:Gem::Version
|
|
144
|
+
version: '1.4'
|
|
145
|
+
type: :development
|
|
146
|
+
prerelease: false
|
|
147
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
148
|
+
requirements:
|
|
149
|
+
- - ">="
|
|
150
|
+
- !ruby/object:Gem::Version
|
|
151
|
+
version: '1.4'
|
|
152
|
+
- !ruby/object:Gem::Dependency
|
|
153
|
+
name: transient_record
|
|
113
154
|
requirement: !ruby/object:Gem::Requirement
|
|
114
155
|
requirements:
|
|
115
156
|
- - "~>"
|
|
116
157
|
- !ruby/object:Gem::Version
|
|
117
|
-
version:
|
|
158
|
+
version: 3.0.0
|
|
118
159
|
type: :development
|
|
119
160
|
prerelease: false
|
|
120
161
|
version_requirements: !ruby/object:Gem::Requirement
|
|
121
162
|
requirements:
|
|
122
163
|
- - "~>"
|
|
123
164
|
- !ruby/object:Gem::Version
|
|
124
|
-
version:
|
|
125
|
-
description:
|
|
165
|
+
version: 3.0.0
|
|
126
166
|
email:
|
|
127
167
|
- contact@gregnavis.com
|
|
128
168
|
executables: []
|
|
@@ -148,9 +188,11 @@ files:
|
|
|
148
188
|
- lib/active_record_doctor/detectors/missing_unique_indexes.rb
|
|
149
189
|
- lib/active_record_doctor/detectors/short_primary_key_type.rb
|
|
150
190
|
- lib/active_record_doctor/detectors/table_without_primary_key.rb
|
|
191
|
+
- lib/active_record_doctor/detectors/table_without_timestamps.rb
|
|
151
192
|
- lib/active_record_doctor/detectors/undefined_table_references.rb
|
|
152
193
|
- lib/active_record_doctor/detectors/unindexed_deleted_at.rb
|
|
153
194
|
- lib/active_record_doctor/detectors/unindexed_foreign_keys.rb
|
|
195
|
+
- lib/active_record_doctor/detectors/unused_tables.rb
|
|
154
196
|
- lib/active_record_doctor/errors.rb
|
|
155
197
|
- lib/active_record_doctor/help.rb
|
|
156
198
|
- lib/active_record_doctor/logger.rb
|
|
@@ -170,7 +212,6 @@ licenses:
|
|
|
170
212
|
- MIT
|
|
171
213
|
metadata:
|
|
172
214
|
rubygems_mfa_required: 'true'
|
|
173
|
-
post_install_message:
|
|
174
215
|
rdoc_options: []
|
|
175
216
|
require_paths:
|
|
176
217
|
- lib
|
|
@@ -178,15 +219,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
178
219
|
requirements:
|
|
179
220
|
- - ">="
|
|
180
221
|
- !ruby/object:Gem::Version
|
|
181
|
-
version: 2.
|
|
222
|
+
version: 2.7.0
|
|
182
223
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
183
224
|
requirements:
|
|
184
225
|
- - ">="
|
|
185
226
|
- !ruby/object:Gem::Version
|
|
186
227
|
version: '0'
|
|
187
228
|
requirements: []
|
|
188
|
-
rubygems_version:
|
|
189
|
-
signing_key:
|
|
229
|
+
rubygems_version: 4.0.20
|
|
190
230
|
specification_version: 4
|
|
191
231
|
summary: Identify database issues before they hit production.
|
|
192
232
|
test_files: []
|