active_record_doctor 1.5.0 → 1.6.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 +73 -6
- data/lib/active_record_doctor/printers/io_printer.rb +35 -6
- data/lib/active_record_doctor/railtie.rb +1 -1
- data/lib/active_record_doctor/tasks.rb +3 -0
- data/lib/active_record_doctor/tasks/base.rb +64 -0
- data/lib/active_record_doctor/tasks/extraneous_indexes.rb +4 -27
- data/lib/active_record_doctor/tasks/missing_foreign_keys.rb +6 -29
- data/lib/active_record_doctor/tasks/missing_non_null_constraint.rb +42 -0
- data/lib/active_record_doctor/tasks/missing_presence_validation.rb +39 -0
- data/lib/active_record_doctor/tasks/missing_unique_indexes.rb +51 -0
- data/lib/active_record_doctor/tasks/undefined_table_references.rb +6 -22
- data/lib/active_record_doctor/tasks/unindexed_deleted_at.rb +4 -25
- data/lib/active_record_doctor/tasks/unindexed_foreign_keys.rb +6 -29
- data/lib/active_record_doctor/version.rb +1 -1
- data/lib/tasks/active_record_doctor.rake +31 -0
- data/test/active_record_doctor/printers/io_printer_test.rb +4 -4
- data/test/active_record_doctor/tasks/extraneous_indexes_test.rb +70 -16
- data/test/active_record_doctor/tasks/missing_foreign_keys_test.rb +16 -8
- data/test/active_record_doctor/tasks/missing_non_null_constraint_test.rb +89 -0
- data/test/active_record_doctor/tasks/missing_presence_validation_test.rb +49 -0
- data/test/active_record_doctor/tasks/missing_unique_indexes_test.rb +95 -0
- data/test/active_record_doctor/tasks/unindexed_deleted_at_test.rb +23 -8
- data/test/active_record_doctor/tasks/unindexed_foreign_keys_test.rb +16 -8
- data/test/dummy/app/models/application_record.rb +1 -1
- data/test/dummy/db/schema.rb +1 -50
- data/test/dummy/log/development.log +38 -498
- data/test/dummy/log/test.log +54108 -1571
- data/test/support/assertions.rb +11 -0
- data/test/support/forking_test.rb +28 -0
- data/test/support/temping.rb +25 -0
- data/test/test_helper.rb +0 -7
- metadata +34 -27
- data/lib/active_record_doctor/compatibility.rb +0 -11
- data/lib/tasks/active_record_doctor_tasks.rake +0 -27
- data/test/active_record_doctor/tasks/undefined_table_references_test.rb +0 -19
- data/test/dummy/app/models/comment.rb +0 -3
- data/test/dummy/app/models/contract.rb +0 -3
- data/test/dummy/app/models/employer.rb +0 -2
- data/test/dummy/app/models/profile.rb +0 -2
- data/test/dummy/app/models/user.rb +0 -3
- data/test/dummy/db/migrate/20160213101213_create_employers.rb +0 -15
- data/test/dummy/db/migrate/20160213101221_create_users.rb +0 -23
- data/test/dummy/db/migrate/20160213101232_create_profiles.rb +0 -15
- data/test/dummy/db/migrate/20160604081452_create_comments.rb +0 -11
- data/test/support/spy_printer.rb +0 -52
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 01e06f004eba174cadece575f77fe83381f53133
|
4
|
+
data.tar.gz: d835060741b338b07a3f87aef33eff707ef32f98
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e07f92a2fd4b200fd5d9e0ef50e1b5a516347b8216080071648bca31c6e72683ceb01d85e125002e4aa0cd5f48a8020ca43097488a67906c15be443e68783da4
|
7
|
+
data.tar.gz: 2faca055adba20d77aac44658adb35d40243f491b533a088e5ca44615d16f36e4cee7deca99b1a37607127c5f2bc276e5dac0aa56c34f0b5aa87b611667afd3e
|
data/README.md
CHANGED
@@ -8,6 +8,9 @@ can:
|
|
8
8
|
* detect unindexed `deleted_at` columns
|
9
9
|
* detect missing foreign key constraints
|
10
10
|
* detect models referencing undefined tables
|
11
|
+
* detect uniqueness validations not backed by an unique index
|
12
|
+
* detect missing non-`NULL` constraints
|
13
|
+
* detect missing presence validations
|
11
14
|
|
12
15
|
More features coming soon!
|
13
16
|
|
@@ -42,7 +45,7 @@ three-step process:
|
|
42
45
|
1. Generate a list of unindexed foreign keys by running
|
43
46
|
|
44
47
|
```bash
|
45
|
-
rake active_record_doctor:unindexed_foreign_keys > unindexed_foreign_keys.txt
|
48
|
+
bundle exec rake active_record_doctor:unindexed_foreign_keys > unindexed_foreign_keys.txt
|
46
49
|
```
|
47
50
|
|
48
51
|
2. Remove columns that should _not_ be indexed from `unindexed_foreign_keys.txt`
|
@@ -58,7 +61,7 @@ three-step process:
|
|
58
61
|
4. Run the migrations
|
59
62
|
|
60
63
|
```bash
|
61
|
-
rake db:migrate
|
64
|
+
bundle exec rake db:migrate
|
62
65
|
```
|
63
66
|
|
64
67
|
### Removing Extraneous Indexes
|
@@ -83,7 +86,7 @@ To discover such indexes automatically just follow these steps:
|
|
83
86
|
1. List extraneous indexes by running:
|
84
87
|
|
85
88
|
```bash
|
86
|
-
rake active_record_doctor:extraneous_indexes
|
89
|
+
bundle exec rake active_record_doctor:extraneous_indexes
|
87
90
|
```
|
88
91
|
|
89
92
|
2. Confirm that each of the indexes can be indeed dropped.
|
@@ -113,7 +116,7 @@ of the time they should only cover columns satisfying `deleted_at IS NULL`.
|
|
113
116
|
`deleted_at` column. Just run:
|
114
117
|
|
115
118
|
```
|
116
|
-
rake active_record_doctor:
|
119
|
+
bundle exec rake active_record_doctor:unindexed_deleted_at
|
117
120
|
```
|
118
121
|
|
119
122
|
This will print a list of indexes that don't have the `deleted_at IS NULL`
|
@@ -134,7 +137,7 @@ add the constraint; for now, it's your job). You can obtain the list of foreign
|
|
134
137
|
keys with the following command:
|
135
138
|
|
136
139
|
```bash
|
137
|
-
rake active_record_doctor:missing_foreign_keys
|
140
|
+
bundle exec rake active_record_doctor:missing_foreign_keys
|
138
141
|
```
|
139
142
|
|
140
143
|
The output will look like:
|
@@ -170,7 +173,7 @@ before they hit production.
|
|
170
173
|
The only think you need to do is run:
|
171
174
|
|
172
175
|
```
|
173
|
-
rake active_record_doctor:undefined_table_references
|
176
|
+
bundle exec rake active_record_doctor:undefined_table_references
|
174
177
|
```
|
175
178
|
|
176
179
|
If there a model references an undefined table then you'll see a message like
|
@@ -184,6 +187,70 @@ The following models reference undefined tables:
|
|
184
187
|
On top of that `rake` will exit with status code of 1. This allows you to use
|
185
188
|
this check as part of your Continuous Integration pipeline.
|
186
189
|
|
190
|
+
### Detecting Uniqueness Validations not Backed by an Index
|
191
|
+
|
192
|
+
A model-level uniqueness validations should be backed by a database index in
|
193
|
+
order to be robust. Otherwise you risk inserting duplicate values under heavy
|
194
|
+
load.
|
195
|
+
|
196
|
+
In order to detect such validations run:
|
197
|
+
|
198
|
+
```
|
199
|
+
bundle exec rake active_record_doctor:missing_unique_indexes
|
200
|
+
```
|
201
|
+
|
202
|
+
If there are such indexes then the command will print:
|
203
|
+
|
204
|
+
```
|
205
|
+
The following indexes should be created to back model-level uniqueness validations:
|
206
|
+
users: email
|
207
|
+
```
|
208
|
+
|
209
|
+
This means that you should create a unique index on `users.email`.
|
210
|
+
|
211
|
+
### Detecting Missing Non-`NULL` Constraints
|
212
|
+
|
213
|
+
If there's an unconditional presence validation on a column then it should be
|
214
|
+
marked as non-`NULL`-able at the database level.
|
215
|
+
|
216
|
+
In order to detect columns whose presence is required but that are marked
|
217
|
+
`null: true` in the database run the following command:
|
218
|
+
|
219
|
+
```
|
220
|
+
bundle exec rake active_record_doctor:missing_non_null_constraint
|
221
|
+
```
|
222
|
+
|
223
|
+
The output of the command is similar to:
|
224
|
+
|
225
|
+
```
|
226
|
+
The following columns should be marked as `null: false`:
|
227
|
+
users: name
|
228
|
+
|
229
|
+
```
|
230
|
+
|
231
|
+
You can mark the columns mentioned in the output as `null: false` by creating a
|
232
|
+
migration and calling `change_column_null`.
|
233
|
+
|
234
|
+
### Detecting Missing Presence Validations
|
235
|
+
|
236
|
+
If a column is marked as `null: false` then it's likely it should have the
|
237
|
+
corresponding presence validator.
|
238
|
+
|
239
|
+
In order to detect models lacking these validations run:
|
240
|
+
|
241
|
+
```
|
242
|
+
bundle exec rake active_record_doctor:missing_presence_validation
|
243
|
+
```
|
244
|
+
|
245
|
+
The output of the command looks like this:
|
246
|
+
|
247
|
+
```
|
248
|
+
The following models and columns should have presence validations:
|
249
|
+
User: email, name
|
250
|
+
```
|
251
|
+
|
252
|
+
This means `User` should have a presence validator on `email` and `name`.
|
253
|
+
|
187
254
|
## Author
|
188
255
|
|
189
256
|
This gem is developed and maintained by [Greg Navis](http://www.gregnavis.com).
|
@@ -1,17 +1,17 @@
|
|
1
1
|
module ActiveRecordDoctor
|
2
2
|
module Printers
|
3
3
|
class IOPrinter
|
4
|
-
def initialize(io
|
4
|
+
def initialize(io = STDOUT)
|
5
5
|
@io = io
|
6
6
|
end
|
7
7
|
|
8
|
-
def
|
8
|
+
def unindexed_foreign_keys(unindexed_foreign_keys)
|
9
9
|
@io.puts(unindexed_foreign_keys.sort.map do |table, columns|
|
10
10
|
"#{table} #{columns.sort.join(' ')}"
|
11
11
|
end.join("\n"))
|
12
12
|
end
|
13
13
|
|
14
|
-
def
|
14
|
+
def extraneous_indexes(extraneous_indexes)
|
15
15
|
if extraneous_indexes.empty?
|
16
16
|
@io.puts("No indexes are extraneous.")
|
17
17
|
else
|
@@ -30,13 +30,13 @@ module ActiveRecordDoctor
|
|
30
30
|
end
|
31
31
|
end
|
32
32
|
|
33
|
-
def
|
33
|
+
def missing_foreign_keys(missing_foreign_keys)
|
34
34
|
@io.puts(missing_foreign_keys.sort.map do |table, columns|
|
35
35
|
"#{table} #{columns.sort.join(' ')}"
|
36
36
|
end.join("\n"))
|
37
37
|
end
|
38
38
|
|
39
|
-
def
|
39
|
+
def undefined_table_references(models)
|
40
40
|
return if models.empty?
|
41
41
|
|
42
42
|
@io.puts('The following models reference undefined tables:')
|
@@ -45,7 +45,7 @@ module ActiveRecordDoctor
|
|
45
45
|
end
|
46
46
|
end
|
47
47
|
|
48
|
-
def
|
48
|
+
def unindexed_deleted_at(indexes)
|
49
49
|
return if indexes.empty?
|
50
50
|
|
51
51
|
@io.puts('The following indexes should include `deleted_at IS NULL`:')
|
@@ -53,6 +53,35 @@ module ActiveRecordDoctor
|
|
53
53
|
@io.puts(" #{index}")
|
54
54
|
end
|
55
55
|
end
|
56
|
+
|
57
|
+
def missing_unique_indexes(indexes)
|
58
|
+
return if indexes.empty?
|
59
|
+
|
60
|
+
@io.puts('The following indexes should be created to back model-level uniqueness validations:')
|
61
|
+
indexes.each do |table, arrays_of_columns|
|
62
|
+
arrays_of_columns.each do |columns|
|
63
|
+
@io.puts(" #{table}: #{columns.join(', ')}")
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def missing_presence_validation(missing_presence_validators)
|
69
|
+
return if missing_presence_validators.empty?
|
70
|
+
|
71
|
+
@io.puts('The following models and columns should have presence validations:')
|
72
|
+
missing_presence_validators.each do |model_name, array_of_columns|
|
73
|
+
@io.puts(" #{model_name}: #{array_of_columns.join(', ')}")
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def missing_non_null_constraint(missing_non_null_constraints)
|
78
|
+
return if missing_non_null_constraints.empty?
|
79
|
+
|
80
|
+
@io.puts('The following columns should be marked as `null: false`:')
|
81
|
+
missing_non_null_constraints.each do |table, columns|
|
82
|
+
@io.puts(" #{table}: #{columns.join(', ')}")
|
83
|
+
end
|
84
|
+
end
|
56
85
|
end
|
57
86
|
end
|
58
87
|
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require "active_record_doctor/printers/io_printer"
|
2
|
+
|
3
|
+
module ActiveRecordDoctor
|
4
|
+
module Tasks
|
5
|
+
class Base
|
6
|
+
def self.run
|
7
|
+
new.run
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(printer = ActiveRecordDoctor::Printers::IOPrinter.new)
|
11
|
+
@printer = printer
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def success(result)
|
17
|
+
[result, true]
|
18
|
+
end
|
19
|
+
|
20
|
+
def connection
|
21
|
+
@connection ||= ActiveRecord::Base.connection
|
22
|
+
end
|
23
|
+
|
24
|
+
def indexes(table_name)
|
25
|
+
connection.indexes(table_name)
|
26
|
+
end
|
27
|
+
|
28
|
+
def tables
|
29
|
+
@tables ||=
|
30
|
+
if Rails::VERSION::MAJOR == 5
|
31
|
+
connection.data_sources
|
32
|
+
else
|
33
|
+
connection.tables
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def hash_from_pairs(pairs)
|
38
|
+
Hash[*pairs.flatten(1)]
|
39
|
+
end
|
40
|
+
|
41
|
+
def eager_load!
|
42
|
+
# We call GC.start to make the test suite work. It's (probably) not
|
43
|
+
# needed for use during development. However, if we remove it then the
|
44
|
+
# test suite will start accumulating temporary model classes in the
|
45
|
+
# object space. Running the garbage collector gets rid of them.
|
46
|
+
GC.start
|
47
|
+
|
48
|
+
Rails.application.eager_load!
|
49
|
+
end
|
50
|
+
|
51
|
+
def models
|
52
|
+
descendants(ActiveRecord::Base)
|
53
|
+
end
|
54
|
+
|
55
|
+
def descendants(superclass)
|
56
|
+
superclass.descendants
|
57
|
+
end
|
58
|
+
|
59
|
+
def descendant?(klass, superclass)
|
60
|
+
!klass.nil? && (klass.superclass == superclass || descendant?(klass.superclass, superclass))
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -1,29 +1,14 @@
|
|
1
|
-
require "active_record_doctor/
|
2
|
-
require "active_record_doctor/printers/io_printer"
|
1
|
+
require "active_record_doctor/tasks/base"
|
3
2
|
|
4
3
|
module ActiveRecordDoctor
|
5
4
|
module Tasks
|
6
|
-
class ExtraneousIndexes
|
7
|
-
include Compatibility
|
8
|
-
|
9
|
-
def self.run
|
10
|
-
new.run
|
11
|
-
end
|
12
|
-
|
13
|
-
def initialize(printer: ActiveRecordDoctor::Printers::IOPrinter.new)
|
14
|
-
@printer = printer
|
15
|
-
end
|
16
|
-
|
5
|
+
class ExtraneousIndexes < Base
|
17
6
|
def run
|
18
|
-
|
7
|
+
success(subindexes_of_multi_column_indexes + indexed_primary_keys)
|
19
8
|
end
|
20
9
|
|
21
10
|
private
|
22
11
|
|
23
|
-
def extraneous_indexes
|
24
|
-
subindexes_of_multi_column_indexes + indexed_primary_keys
|
25
|
-
end
|
26
|
-
|
27
12
|
def subindexes_of_multi_column_indexes
|
28
13
|
tables.reject do |table|
|
29
14
|
"schema_migrations" == table
|
@@ -90,15 +75,7 @@ module ActiveRecordDoctor
|
|
90
75
|
end
|
91
76
|
|
92
77
|
def indexes(table_name)
|
93
|
-
|
94
|
-
end
|
95
|
-
|
96
|
-
def tables
|
97
|
-
@tables ||= connection_tables
|
98
|
-
end
|
99
|
-
|
100
|
-
def connection
|
101
|
-
@connection ||= ActiveRecord::Base.connection
|
78
|
+
super.select { |index| index.columns.kind_of?(Array) }
|
102
79
|
end
|
103
80
|
end
|
104
81
|
end
|
@@ -1,27 +1,10 @@
|
|
1
|
-
require "active_record_doctor/
|
2
|
-
require "active_record_doctor/printers/io_printer"
|
1
|
+
require "active_record_doctor/tasks/base"
|
3
2
|
|
4
3
|
module ActiveRecordDoctor
|
5
4
|
module Tasks
|
6
|
-
class MissingForeignKeys
|
7
|
-
include Compatibility
|
8
|
-
|
9
|
-
def self.run
|
10
|
-
new.run
|
11
|
-
end
|
12
|
-
|
13
|
-
def initialize(printer: ActiveRecordDoctor::Printers::IOPrinter.new)
|
14
|
-
@printer = printer
|
15
|
-
end
|
16
|
-
|
5
|
+
class MissingForeignKeys < Base
|
17
6
|
def run
|
18
|
-
|
19
|
-
end
|
20
|
-
|
21
|
-
private
|
22
|
-
|
23
|
-
def missing_foreign_keys
|
24
|
-
hash_from_pairs(connection_tables.select do |table|
|
7
|
+
success(hash_from_pairs(tables.select do |table|
|
25
8
|
"schema_migrations" != table
|
26
9
|
end.map do |table|
|
27
10
|
[
|
@@ -37,9 +20,11 @@ module ActiveRecordDoctor
|
|
37
20
|
]
|
38
21
|
end.select do |table, columns|
|
39
22
|
!columns.empty?
|
40
|
-
end)
|
23
|
+
end))
|
41
24
|
end
|
42
25
|
|
26
|
+
private
|
27
|
+
|
43
28
|
def id?(table, column)
|
44
29
|
column.name.end_with?("_id")
|
45
30
|
end
|
@@ -56,14 +41,6 @@ module ActiveRecordDoctor
|
|
56
41
|
another_column.name == type_column_name
|
57
42
|
end
|
58
43
|
end
|
59
|
-
|
60
|
-
def connection
|
61
|
-
@connection ||= ActiveRecord::Base.connection
|
62
|
-
end
|
63
|
-
|
64
|
-
def hash_from_pairs(pairs)
|
65
|
-
Hash[*pairs.flatten(1)]
|
66
|
-
end
|
67
44
|
end
|
68
45
|
end
|
69
46
|
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require "active_record_doctor/tasks/base"
|
2
|
+
|
3
|
+
module ActiveRecordDoctor
|
4
|
+
module Tasks
|
5
|
+
class MissingNonNullConstraint < Base
|
6
|
+
def run
|
7
|
+
eager_load!
|
8
|
+
|
9
|
+
success(hash_from_pairs(models.reject do |model|
|
10
|
+
model.table_name.nil? || model.table_name == 'schema_migrations'
|
11
|
+
end.map do |model|
|
12
|
+
[
|
13
|
+
model.table_name,
|
14
|
+
connection.columns(model.table_name).select do |column|
|
15
|
+
validator_needed?(model, column) &&
|
16
|
+
has_mandatory_presence_validator?(model, column) &&
|
17
|
+
column.null
|
18
|
+
end.map(&:name)
|
19
|
+
]
|
20
|
+
end.reject do |model_name, columns|
|
21
|
+
columns.empty?
|
22
|
+
end))
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def validator_needed?(model, column)
|
28
|
+
![model.primary_key, 'created_at', 'updated_at'].include?(column.name)
|
29
|
+
end
|
30
|
+
|
31
|
+
def has_mandatory_presence_validator?(model, column)
|
32
|
+
model.validators.any? do |validator|
|
33
|
+
validator.is_a?(ActiveRecord::Validations::PresenceValidator) &&
|
34
|
+
validator.attributes.include?(column.name.to_sym) &&
|
35
|
+
!validator.options[:allow_nil] &&
|
36
|
+
!validator.options[:if] &&
|
37
|
+
!validator.options[:unless]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|