active_record_doctor 1.8.0 → 1.9.0.rc1
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 +246 -48
- data/lib/active_record_doctor/config/default.rb +59 -0
- data/lib/active_record_doctor/config/loader.rb +137 -0
- data/lib/active_record_doctor/config.rb +14 -0
- data/lib/active_record_doctor/detectors/base.rb +110 -19
- data/lib/active_record_doctor/detectors/extraneous_indexes.rb +63 -37
- data/lib/active_record_doctor/detectors/incorrect_boolean_presence_validation.rb +32 -23
- data/lib/active_record_doctor/detectors/incorrect_dependent_option.rb +70 -34
- data/lib/active_record_doctor/detectors/mismatched_foreign_key_type.rb +45 -0
- data/lib/active_record_doctor/detectors/missing_foreign_keys.rb +32 -23
- data/lib/active_record_doctor/detectors/missing_non_null_constraint.rb +40 -28
- data/lib/active_record_doctor/detectors/missing_presence_validation.rb +28 -21
- data/lib/active_record_doctor/detectors/missing_unique_indexes.rb +40 -30
- data/lib/active_record_doctor/detectors/short_primary_key_type.rb +41 -0
- data/lib/active_record_doctor/detectors/undefined_table_references.rb +19 -20
- data/lib/active_record_doctor/detectors/unindexed_deleted_at.rb +44 -18
- data/lib/active_record_doctor/detectors/unindexed_foreign_keys.rb +31 -20
- data/lib/active_record_doctor/detectors.rb +12 -4
- data/lib/active_record_doctor/errors.rb +226 -0
- data/lib/active_record_doctor/help.rb +39 -0
- data/lib/active_record_doctor/rake/task.rb +78 -0
- data/lib/active_record_doctor/runner.rb +41 -0
- data/lib/active_record_doctor/version.rb +1 -1
- data/lib/active_record_doctor.rb +7 -3
- data/lib/generators/active_record_doctor/add_indexes/add_indexes_generator.rb +34 -21
- data/lib/tasks/active_record_doctor.rake +9 -18
- data/test/active_record_doctor/config/loader_test.rb +120 -0
- data/test/active_record_doctor/config_test.rb +116 -0
- data/test/active_record_doctor/detectors/extraneous_indexes_test.rb +131 -8
- data/test/active_record_doctor/detectors/incorrect_boolean_presence_validation_test.rb +48 -5
- data/test/active_record_doctor/detectors/incorrect_dependent_option_test.rb +190 -12
- data/test/active_record_doctor/detectors/mismatched_foreign_key_type_test.rb +82 -0
- data/test/active_record_doctor/detectors/missing_foreign_keys_test.rb +50 -4
- data/test/active_record_doctor/detectors/missing_non_null_constraint_test.rb +138 -24
- data/test/active_record_doctor/detectors/missing_presence_validation_test.rb +74 -13
- data/test/active_record_doctor/detectors/missing_unique_indexes_test.rb +57 -8
- data/test/active_record_doctor/detectors/short_primary_key_type_test.rb +64 -0
- data/test/active_record_doctor/detectors/undefined_table_references_test.rb +34 -21
- data/test/active_record_doctor/detectors/unindexed_deleted_at_test.rb +112 -8
- data/test/active_record_doctor/detectors/unindexed_foreign_keys_test.rb +56 -4
- data/test/active_record_doctor/runner_test.rb +42 -0
- data/test/generators/active_record_doctor/add_indexes/add_indexes_generator_test.rb +131 -0
- data/test/model_factory.rb +73 -23
- data/test/setup.rb +62 -72
- metadata +40 -9
- data/lib/active_record_doctor/printers/io_printer.rb +0 -133
- data/lib/active_record_doctor/task.rb +0 -28
- data/test/active_record_doctor/printers/io_printer_test.rb +0 -33
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4126519be8847ccb2b1584696ae1c13b26c61bcfa9eb39ad69f8907afb514efb
|
|
4
|
+
data.tar.gz: c6cbafc9d9ef3679bb79ac6c8bad03f19a4f2e5188dd8be2672166a4742b3fa2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 110732302b0603e41b54c2c0bcec8b313899fc6f43a985c955db7e2a8582980bb756cfa35be965e862b0f6b8904e5972a6ce999ebfb2b1a6871d7dcc71bc21f3
|
|
7
|
+
data.tar.gz: a899ae429e450cc2e1fe5caf8d37b8a2820a5d6121981e9f361106805ec93e6739cc190a5eb264fb74a41bc4738575e372e03fad5fe70a0870052a210ea632fa
|
data/README.md
CHANGED
|
@@ -1,42 +1,147 @@
|
|
|
1
1
|
# Active Record Doctor
|
|
2
2
|
|
|
3
3
|
Active Record Doctor helps to keep the database in a good shape. Currently, it
|
|
4
|
-
can:
|
|
4
|
+
can detect:
|
|
5
|
+
|
|
6
|
+
* extraneous indexes - [`active_record_doctor:extraneous_indexes`](#removing-extraneous-indexes)
|
|
7
|
+
* unindexed `deleted_at` columns - [`active_record_doctor:unindexed_deleted_at`](#detecting-unindexed-deleted_at-columns)
|
|
8
|
+
* missing foreign key constraints - [`active_record_doctor:missing_foreign_keys`](#detecting-missing-foreign-key-constraints)
|
|
9
|
+
* models referencing undefined tables - [`active_record_doctor:undefined_table_references`](#detecting-models-referencing-undefined-tables)
|
|
10
|
+
* uniqueness validations not backed by an unique index - [`active_record_doctor:missing_unique_indexes`](#detecting-uniqueness-validations-not-backed-by-an-index)
|
|
11
|
+
* missing non-`NULL` constraints - [`active_record_doctor:missing_non_null_constraint`](#detecting-missing-non-null-constraints)
|
|
12
|
+
* missing presence validations - [`active_record_doctor:missing_presence_validation`](#detecting-missing-presence-validations)
|
|
13
|
+
* incorrect presence validations on boolean columns - [`active_record_doctor:incorrect_boolean_presence_validation`](#detecting-incorrect-presence-validations-on-boolean-columns)
|
|
14
|
+
* incorrect values of `dependent` on associations - [`active_record_doctor:incorrect_dependent_option`](#detecting-incorrect-dependent-option-on-associations)
|
|
15
|
+
* primary keys having short integer types - [`active_record_doctor:short_primary_key_type`](#detecting-primary-keys-having-short-integer-types)
|
|
16
|
+
* mismatched foreign key types - [`active_record_doctor:mismatched_foreign_key_type`](#detecting-mismatched-foreign-key-types)
|
|
17
|
+
|
|
18
|
+
It can also:
|
|
5
19
|
|
|
6
20
|
* index unindexed foreign keys - [`active_record_doctor:unindexed_foreign_keys`](#indexing-unindexed-foreign-keys)
|
|
7
|
-
* detect extraneous indexes - [`active_record_doctor:extraneous_indexes`](#removing-extraneous-indexes)
|
|
8
|
-
* detect unindexed `deleted_at` columns - [`active_record_doctor:unindexed_deleted_at`](#detecting-unindexed-deleted_at-columns)
|
|
9
|
-
* detect missing foreign key constraints - [`active_record_doctor:missing_foreign_keys`](#detecting-missing-foreign-key-constraints)
|
|
10
|
-
* detect models referencing undefined tables - [`active_record_doctor:undefined_table_references`](#detecting-models-referencing-undefined-tables)
|
|
11
|
-
* detect uniqueness validations not backed by an unique index - [`active_record_doctor:missing_unique_indexes`](#detecting-uniqueness-validations-not-backed-by-an-index)
|
|
12
|
-
* detect missing non-`NULL` constraints - [`active_record_doctor:missing_non_null_constraint`](#detecting-missing-non-null-constraints)
|
|
13
|
-
* detect missing presence validations - [`active_record_doctor:missing_presence_validation`](#detecting-missing-presence-validations)
|
|
14
|
-
* detect incorrect presence validations on boolean columns - [`active_record_doctor:incorrect_boolean_presence_validation`](#detecting-incorrect-presence-validations-on-boolean-columns)
|
|
15
|
-
* detect incorrect values of `dependent` on associations - [`active_record_doctor:incorrect_dependent_option`](#detecting-incorrect-dependent-option-on-associations)
|
|
16
21
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
Want to suggest a feature? Just shoot me [an email](mailto:contact@gregnavis.com).
|
|
20
|
-
|
|
21
|
-
[<img src="https://travis-ci.org/gregnavis/active_record_doctor.svg?branch=master" alt="Build Status" />](https://travis-ci.org/gregnavis/active_record_doctor)
|
|
22
|
+
[](https://github.com/gregnavis/active_record_doctor/actions/workflows/test.yml)
|
|
22
23
|
|
|
23
24
|
## Installation
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
`Gemfile`:
|
|
26
|
+
In order to use the latest production release, please add the following to
|
|
27
|
+
your `Gemfile`:
|
|
27
28
|
|
|
28
29
|
```ruby
|
|
29
30
|
gem 'active_record_doctor', group: :development
|
|
30
31
|
```
|
|
31
32
|
|
|
32
|
-
|
|
33
|
+
and run `bundle install`. If you'd like to use the most recent development
|
|
34
|
+
version then use this instead:
|
|
33
35
|
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
+
```ruby
|
|
37
|
+
gem 'active_record_doctor', github: 'gregnavis/active_record_doctor'
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
That's it when it comes to Rails projects. If your project doesn't use Rails
|
|
41
|
+
then you can use `active_record_doctor` via `Rakefile`.
|
|
42
|
+
|
|
43
|
+
### Additional Installation Steps for non-Rails Projects
|
|
44
|
+
|
|
45
|
+
If your project uses Rake then you can add the following to `Rakefile` in order
|
|
46
|
+
to use `active_record_doctor`:
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
require "active_record_doctor"
|
|
50
|
+
|
|
51
|
+
ActiveRecordDoctor::Rake::Task.new do |task|
|
|
52
|
+
# Add project-specific Rake dependencies that should be run before running
|
|
53
|
+
# active_record_doctor.
|
|
54
|
+
task.deps = []
|
|
55
|
+
|
|
56
|
+
# A path to your active_record_doctor configuration file.
|
|
57
|
+
task.config_path = ::Rails.root.join(".active_record_doctor")
|
|
58
|
+
|
|
59
|
+
# A Proc called right before running detectors that should ensure your Active
|
|
60
|
+
# Record models are preloaded and a database connection is ready.
|
|
61
|
+
task.setup = -> { ::Rails.application.eager_load! }
|
|
62
|
+
end
|
|
36
63
|
```
|
|
37
64
|
|
|
65
|
+
**IMPORTANT**. `active_record_doctor` expects that after running `deps` and
|
|
66
|
+
calling `setup` your Active Record models are loaded and a database connection
|
|
67
|
+
is established.
|
|
68
|
+
|
|
38
69
|
## Usage
|
|
39
70
|
|
|
71
|
+
`active_record_doctor` can be used via `rake` or `rails`.
|
|
72
|
+
|
|
73
|
+
You can run all available detectors via:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
bundle exec rake active_record_doctor
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
You can run a specific detector via:
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
bundle exec rake active_record_doctor:extraneous_indexes
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Continuous Integration
|
|
86
|
+
|
|
87
|
+
If you want to use `active_record_doctor` in a Continuous Integration setting
|
|
88
|
+
then ensure the configuration file is committed and run the tool as one of your
|
|
89
|
+
build steps -- it returns a non-zero exit status if any errors were reported.
|
|
90
|
+
|
|
91
|
+
### Obtaining Help
|
|
92
|
+
|
|
93
|
+
If you'd like to obtain help on a specific detector then use the `help` sub-task:
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
bundle exec rake active_record_doctor:extraneous_indexes:help
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
This will show the detector help text in the terminal, along with supported
|
|
100
|
+
configuration options, their meaning, and whether they're global or local.
|
|
101
|
+
|
|
102
|
+
### Configuration
|
|
103
|
+
|
|
104
|
+
`active_record_doctor` can be configured to better suite your project's needs.
|
|
105
|
+
For example, if it complains about a model that you want ignored then you can
|
|
106
|
+
add that model to the configuration file.
|
|
107
|
+
|
|
108
|
+
If you want to use the default configuration then you don't have to do anything.
|
|
109
|
+
Just run `active_record_doctor` in your project directory.
|
|
110
|
+
|
|
111
|
+
If you want to customize the tool you should create a file named
|
|
112
|
+
`.active_record_doctor` in your project root directory with content like:
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
ActiveRecordDoctor.configure do
|
|
116
|
+
# Global settings affect all detectors.
|
|
117
|
+
global :ignore_tables, [
|
|
118
|
+
# Ignore internal Rails-related tables.
|
|
119
|
+
"ar_internal_metadata",
|
|
120
|
+
"schema_migrations",
|
|
121
|
+
"active_storage_blobs",
|
|
122
|
+
"active_storage_attachments",
|
|
123
|
+
"action_text_rich_texts",
|
|
124
|
+
|
|
125
|
+
# Add project-specific tables here.
|
|
126
|
+
"legacy_users"
|
|
127
|
+
]
|
|
128
|
+
|
|
129
|
+
# Detector-specific settings affect only one specific detector.
|
|
130
|
+
detector :extraneous_indexes,
|
|
131
|
+
ignore_tables: ["users"],
|
|
132
|
+
ignore_indexes: ["accounts_on_email_organization_id"]
|
|
133
|
+
end
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The configuration file above will make `active_record_doctor` ignore internal
|
|
137
|
+
Rails tables (which are ignored by default) and also the `legacy_users` table.
|
|
138
|
+
It'll also make the `extraneous_indexes` detector skip the `users` table
|
|
139
|
+
entirely and will not report the index named `accounts_on_email_organization_id`
|
|
140
|
+
as extraneous.
|
|
141
|
+
|
|
142
|
+
Configuration options for each detector are listed below. They can also be
|
|
143
|
+
obtained via the help mechanism described in the previous section.
|
|
144
|
+
|
|
40
145
|
### Indexing Unindexed Foreign Keys
|
|
41
146
|
|
|
42
147
|
Foreign keys should be indexed unless it's proven ineffective. However, Rails
|
|
@@ -66,6 +171,11 @@ three-step process:
|
|
|
66
171
|
bundle exec rake db:migrate
|
|
67
172
|
```
|
|
68
173
|
|
|
174
|
+
Supported configuration options:
|
|
175
|
+
|
|
176
|
+
- `ignore_tables` - tables whose foreign keys should not be checked
|
|
177
|
+
- `ignore_columns` - columns, written as table.column, that should not be checked.
|
|
178
|
+
|
|
69
179
|
### Removing Extraneous Indexes
|
|
70
180
|
|
|
71
181
|
Let me illustrate with an example. Consider a `users` table with columns
|
|
@@ -107,6 +217,11 @@ example, if there's a unique index on `users.login` and a non-unique index on
|
|
|
107
217
|
`users.login, users.domain` then the tool will _not_ suggest dropping
|
|
108
218
|
`users.login` as it could violate the uniqueness assumption.
|
|
109
219
|
|
|
220
|
+
Supported configuration options:
|
|
221
|
+
|
|
222
|
+
- `ignore_tables` - tables whose indexes should never be reported as extraneous.
|
|
223
|
+
- `ignore_columns` - indexes that should never be reported as extraneous.
|
|
224
|
+
|
|
110
225
|
### Detecting Unindexed `deleted_at` Columns
|
|
111
226
|
|
|
112
227
|
If you soft-delete some models (e.g. with `paranoia`) then you need to modify
|
|
@@ -125,6 +240,13 @@ This will print a list of indexes that don't have the `deleted_at IS NULL`
|
|
|
125
240
|
clause. Currently, `active_record_doctor` cannot automatically generate
|
|
126
241
|
appropriate migrations. You need to do that manually.
|
|
127
242
|
|
|
243
|
+
Supported configuration options:
|
|
244
|
+
|
|
245
|
+
- `ignore_tables` - tables whose indexes should not be checked.
|
|
246
|
+
- `ignore_columns` - specific columns, written as table.column, that should not be reported as unindexed.
|
|
247
|
+
- `ignore_indexes` - specific indexes that should not be reported as excluding a timestamp column.
|
|
248
|
+
- `column_names` - deletion timestamp column names.
|
|
249
|
+
|
|
128
250
|
### Detecting Missing Foreign Key Constraints
|
|
129
251
|
|
|
130
252
|
If `users.profile_id` references a row in `profiles` then this can be expressed
|
|
@@ -142,20 +264,8 @@ keys with the following command:
|
|
|
142
264
|
bundle exec rake active_record_doctor:missing_foreign_keys
|
|
143
265
|
```
|
|
144
266
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
```
|
|
148
|
-
users profile_id
|
|
149
|
-
comments user_id article_id
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
Tables are listed one per line. Each line starts with a table name followed by
|
|
153
|
-
column names that should have a foreign key constraint. In the example above,
|
|
154
|
-
`users.profile_id`, `comments.user_id`, and `comments.article_id` lack a foreign
|
|
155
|
-
key constraint.
|
|
156
|
-
|
|
157
|
-
In order to add a foreign key constraint to `users.profile_id` use the following
|
|
158
|
-
migration:
|
|
267
|
+
In order to add a foreign key constraint to `users.profile_id` use a migration
|
|
268
|
+
like:
|
|
159
269
|
|
|
160
270
|
```ruby
|
|
161
271
|
class AddForeignKeyConstraintToUsersProfileId < ActiveRecord::Migration
|
|
@@ -165,6 +275,11 @@ class AddForeignKeyConstraintToUsersProfileId < ActiveRecord::Migration
|
|
|
165
275
|
end
|
|
166
276
|
```
|
|
167
277
|
|
|
278
|
+
Supported configuration options:
|
|
279
|
+
|
|
280
|
+
- `ignore_tables` - tables whose columns should not be checked.
|
|
281
|
+
- `ignore_columns` - columns, written as table.column, that should not be checked.
|
|
282
|
+
|
|
168
283
|
### Detecting Models Referencing Undefined Tables
|
|
169
284
|
|
|
170
285
|
Active Record guesses the table name based on the class name. There are a few
|
|
@@ -187,13 +302,16 @@ If there a model references an undefined table then you'll see a message like
|
|
|
187
302
|
this:
|
|
188
303
|
|
|
189
304
|
```
|
|
190
|
-
|
|
191
|
-
Contract (the table contract_records is undefined)
|
|
305
|
+
Contract references a non-existent table or view named contract_records
|
|
192
306
|
```
|
|
193
307
|
|
|
194
308
|
On top of that `rake` will exit with status code of 1. This allows you to use
|
|
195
309
|
this check as part of your Continuous Integration pipeline.
|
|
196
310
|
|
|
311
|
+
Supported configuration options:
|
|
312
|
+
|
|
313
|
+
- `ignore_models` - models whose underlying tables should not be checked for existence.
|
|
314
|
+
|
|
197
315
|
### Detecting Uniqueness Validations not Backed by an Index
|
|
198
316
|
|
|
199
317
|
A model-level uniqueness validations should be backed by a database index in
|
|
@@ -209,12 +327,16 @@ bundle exec rake active_record_doctor:missing_unique_indexes
|
|
|
209
327
|
If there are such indexes then the command will print:
|
|
210
328
|
|
|
211
329
|
```
|
|
212
|
-
|
|
213
|
-
users: email
|
|
330
|
+
add a unique index on users(email) - validating uniqueness in the model without an index can lead to duplicates
|
|
214
331
|
```
|
|
215
332
|
|
|
216
333
|
This means that you should create a unique index on `users.email`.
|
|
217
334
|
|
|
335
|
+
Supported configuration options:
|
|
336
|
+
|
|
337
|
+
- `ignore_models` - models whose uniqueness validators should not be checked.
|
|
338
|
+
- `ignore_columns` - specific validators, written as Model(column1, column2, ...), that should not be checked.
|
|
339
|
+
|
|
218
340
|
### Detecting Missing Non-`NULL` Constraints
|
|
219
341
|
|
|
220
342
|
If there's an unconditional presence validation on a column then it should be
|
|
@@ -230,9 +352,7 @@ bundle exec rake active_record_doctor:missing_non_null_constraint
|
|
|
230
352
|
The output of the command is similar to:
|
|
231
353
|
|
|
232
354
|
```
|
|
233
|
-
|
|
234
|
-
users: name
|
|
235
|
-
|
|
355
|
+
add `NOT NULL` to users.name - models validates its presence but it's not non-NULL in the database
|
|
236
356
|
```
|
|
237
357
|
|
|
238
358
|
You can mark the columns mentioned in the output as `null: false` by creating a
|
|
@@ -240,6 +360,11 @@ migration and calling `change_column_null`.
|
|
|
240
360
|
|
|
241
361
|
This validator skips models whose corresponding database tables don't exist.
|
|
242
362
|
|
|
363
|
+
Supported configuration options:
|
|
364
|
+
|
|
365
|
+
- `ignore_tables` - tables whose columns should not be checked.
|
|
366
|
+
- `ignore_columns` - columns, written as table.column, that should not be checked.
|
|
367
|
+
|
|
243
368
|
### Detecting Missing Presence Validations
|
|
244
369
|
|
|
245
370
|
If a column is marked as `null: false` then it's likely it should have the
|
|
@@ -254,14 +379,19 @@ bundle exec rake active_record_doctor:missing_presence_validation
|
|
|
254
379
|
The output of the command looks like this:
|
|
255
380
|
|
|
256
381
|
```
|
|
257
|
-
|
|
258
|
-
|
|
382
|
+
add a `presence` validator to User.email - it's NOT NULL but lacks a validator
|
|
383
|
+
add a `presence` validator to User.name - it's NOT NULL but lacks a validator
|
|
259
384
|
```
|
|
260
385
|
|
|
261
386
|
This means `User` should have a presence validator on `email` and `name`.
|
|
262
387
|
|
|
263
388
|
This validator skips models whose corresponding database tables don't exist.
|
|
264
389
|
|
|
390
|
+
Supported configuration options:
|
|
391
|
+
|
|
392
|
+
- `ignore_models` - models whose underlying tables' columns should not be checked.
|
|
393
|
+
- `ignore_columns` - specific attributes, written as Model.attribute, that should not be checked.
|
|
394
|
+
|
|
265
395
|
### Detecting Incorrect Presence Validations on Boolean Columns
|
|
266
396
|
|
|
267
397
|
A boolean column's presence should be validated using inclusion or exclusion
|
|
@@ -276,8 +406,7 @@ bundle exec rake active_record_doctor:incorrect_boolean_presence_validation
|
|
|
276
406
|
The output of the command looks like this:
|
|
277
407
|
|
|
278
408
|
```
|
|
279
|
-
|
|
280
|
-
User: active
|
|
409
|
+
replace the `presence` validator on User.active with `inclusion` - `presence` can't be used on booleans
|
|
281
410
|
```
|
|
282
411
|
|
|
283
412
|
This means `active` is validated with `presence: true` instead of
|
|
@@ -285,6 +414,11 @@ This means `active` is validated with `presence: true` instead of
|
|
|
285
414
|
|
|
286
415
|
This validator skips models whose corresponding database tables don't exist.
|
|
287
416
|
|
|
417
|
+
Supported configuration options:
|
|
418
|
+
|
|
419
|
+
- `ignore_models` - models whose validators should not be checked.
|
|
420
|
+
- `ignore_columns` - attributes, written as Model.attribute, whose validators should not be checked.
|
|
421
|
+
|
|
288
422
|
### Detecting Incorrect `dependent` Option on Associations
|
|
289
423
|
|
|
290
424
|
Cascading model deletions can be sped up with `dependent: :delete_all` (to
|
|
@@ -308,11 +442,75 @@ bundle exec rake active_record_doctor:incorrect_dependent_option
|
|
|
308
442
|
The output of the command looks like this:
|
|
309
443
|
|
|
310
444
|
```
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
Post: comments skips callbacks that are defined on the associated model - consider changing to `dependent: :destroy` or similar
|
|
445
|
+
use `dependent: :delete_all` or similar on Company.users - associated models have no validations and can be deleted in bulk
|
|
446
|
+
use `dependent: :destroy` or similar on Post.comments - the associated model has callbacks that are currently skipped
|
|
314
447
|
```
|
|
315
448
|
|
|
449
|
+
Supported configuration options:
|
|
450
|
+
|
|
451
|
+
- `ignore_models` - models whose associations should not be checked.
|
|
452
|
+
- `ignore_columns` - associations, written as Model.association, that should not be checked.
|
|
453
|
+
|
|
454
|
+
### Detecting Primary Keys Having Short Integer Types
|
|
455
|
+
|
|
456
|
+
Active Record 5.1 changed the default primary and foreign key type from INTEGER
|
|
457
|
+
to BIGINT. The reason is to reduce the risk of running out of IDs on inserts.
|
|
458
|
+
|
|
459
|
+
In order to detect primary keys using shorter integer types, for example created
|
|
460
|
+
before migrating to 5.1, you can run the following command:
|
|
461
|
+
|
|
462
|
+
```
|
|
463
|
+
bundle exec rake active_record_doctor:short_primary_key_type
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
The output of the command looks like this:
|
|
467
|
+
|
|
468
|
+
```
|
|
469
|
+
change the type of companies.id to bigint
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
The above means `comanies.id` should be migrated to a wider integer type. An
|
|
473
|
+
example migration to accomplish this looks likes this:
|
|
474
|
+
|
|
475
|
+
```ruby
|
|
476
|
+
class ChangeCompaniesPrimaryKeyType < ActiveRecord::Migration[5.1]
|
|
477
|
+
def change
|
|
478
|
+
change_column :companies, :id, :bigint
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
**IMPORTANT**. Running the above migration on a large table can cause downtime
|
|
484
|
+
as all rows need to be rewritten.
|
|
485
|
+
|
|
486
|
+
Supported configuration options:
|
|
487
|
+
|
|
488
|
+
- `ignore_tables` - tables whose primary keys should not be checked.
|
|
489
|
+
|
|
490
|
+
### Detecting Mismatched Foreign Key Types
|
|
491
|
+
|
|
492
|
+
Foreign keys should be of the same type as the referenced primary key.
|
|
493
|
+
Otherwise, there's a risk of bugs caused by IDs representable by one type but
|
|
494
|
+
not the other.
|
|
495
|
+
|
|
496
|
+
Running the command below will list all foreign keys whose type is different
|
|
497
|
+
from the referenced primary key:
|
|
498
|
+
|
|
499
|
+
```
|
|
500
|
+
bundle exec rake active_record_doctor:mismatched_foreign_key_type
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
The output of the command looks like this:
|
|
504
|
+
|
|
505
|
+
```
|
|
506
|
+
companies.user_id references a column of different type - foreign keys should be of the same type as the referenced column
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
Supported configuration options:
|
|
510
|
+
|
|
511
|
+
- `ignore_tables` - tables whose foreign keys should not be checked.
|
|
512
|
+
- `ignore_columns` - foreign keys, written as table.column, that should not be checked.
|
|
513
|
+
|
|
316
514
|
## Ruby and Rails Compatibility Policy
|
|
317
515
|
|
|
318
516
|
The goal of the policy is to ensure proper functioning in reasonable
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
ActiveRecordDoctor.configure do
|
|
4
|
+
global :ignore_tables, [
|
|
5
|
+
"ar_internal_metadata",
|
|
6
|
+
"schema_migrations",
|
|
7
|
+
"active_storage_blobs",
|
|
8
|
+
"active_storage_attachments",
|
|
9
|
+
"action_text_rich_texts"
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
detector :extraneous_indexes,
|
|
13
|
+
ignore_tables: [],
|
|
14
|
+
ignore_indexes: []
|
|
15
|
+
|
|
16
|
+
detector :incorrect_boolean_presence_validation,
|
|
17
|
+
ignore_models: [],
|
|
18
|
+
ignore_attributes: []
|
|
19
|
+
|
|
20
|
+
detector :incorrect_dependent_option,
|
|
21
|
+
ignore_models: [],
|
|
22
|
+
ignore_associations: []
|
|
23
|
+
|
|
24
|
+
detector :mismatched_foreign_key_type,
|
|
25
|
+
ignore_tables: [],
|
|
26
|
+
ignore_columns: []
|
|
27
|
+
|
|
28
|
+
detector :missing_foreign_keys,
|
|
29
|
+
ignore_tables: [],
|
|
30
|
+
ignore_columns: []
|
|
31
|
+
|
|
32
|
+
detector :missing_non_null_constraint,
|
|
33
|
+
ignore_tables: [],
|
|
34
|
+
ignore_columns: []
|
|
35
|
+
|
|
36
|
+
detector :missing_presence_validation,
|
|
37
|
+
ignore_models: [],
|
|
38
|
+
ignore_attributes: []
|
|
39
|
+
|
|
40
|
+
detector :missing_unique_indexes,
|
|
41
|
+
ignore_models: [],
|
|
42
|
+
ignore_columns: []
|
|
43
|
+
|
|
44
|
+
detector :short_primary_key_type,
|
|
45
|
+
ignore_tables: []
|
|
46
|
+
|
|
47
|
+
detector :undefined_table_references,
|
|
48
|
+
ignore_models: []
|
|
49
|
+
|
|
50
|
+
detector :unindexed_deleted_at,
|
|
51
|
+
ignore_tables: [],
|
|
52
|
+
ignore_columns: [],
|
|
53
|
+
ignore_indexes: [],
|
|
54
|
+
column_names: ["deleted_at", "discarded_at"]
|
|
55
|
+
|
|
56
|
+
detector :unindexed_foreign_keys,
|
|
57
|
+
ignore_tables: [],
|
|
58
|
+
ignore_columns: []
|
|
59
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveRecordDoctor # :nodoc:
|
|
4
|
+
class << self
|
|
5
|
+
# The config file that's currently being processed by .load_config.
|
|
6
|
+
attr_reader :current_config
|
|
7
|
+
|
|
8
|
+
# This method is part of the public API that is intended for use by
|
|
9
|
+
# active_record_doctor users. The remaining methods are considered to be
|
|
10
|
+
# public-not-published.
|
|
11
|
+
def configure(&block)
|
|
12
|
+
# If current_config is set it means that .configure was already called
|
|
13
|
+
# so we must raise an error.
|
|
14
|
+
raise ActiveRecordDoctor::Error::ConfigureCalledTwice if current_config
|
|
15
|
+
|
|
16
|
+
# Determine the recognized global and detector settings based on detector
|
|
17
|
+
# metadata. recognizedd_detectors maps detector names to setting names.
|
|
18
|
+
# recognized_globals contains global setting names.
|
|
19
|
+
recognized_detectors = {}
|
|
20
|
+
recognized_globals = []
|
|
21
|
+
|
|
22
|
+
ActiveRecordDoctor.detectors.each do |name, detector|
|
|
23
|
+
locals, globals = detector.locals_and_globals
|
|
24
|
+
|
|
25
|
+
recognized_detectors[name] = locals
|
|
26
|
+
recognized_globals.concat(globals)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# The same global can be used by multiple detectors so we must remove
|
|
30
|
+
# duplicates to ensure they aren't reported mutliple times via the user
|
|
31
|
+
# interface (e.g. in error messages).
|
|
32
|
+
recognized_globals.uniq!
|
|
33
|
+
|
|
34
|
+
# Prepare an empty configuration and call the loader. After .new returns
|
|
35
|
+
# @current_config will contain the configuration provided by the block.
|
|
36
|
+
@current_config = Config.new({}, {})
|
|
37
|
+
Loader.new(current_config, recognized_globals, recognized_detectors, &block)
|
|
38
|
+
|
|
39
|
+
# This method is part of the public API expected to be called by users.
|
|
40
|
+
# In order to avoid leaking internal objects, we return an explicit nil.
|
|
41
|
+
nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def load_config(path)
|
|
45
|
+
begin
|
|
46
|
+
load(path)
|
|
47
|
+
rescue ActiveRecordDoctor::Error
|
|
48
|
+
raise
|
|
49
|
+
rescue LoadError
|
|
50
|
+
raise ActiveRecordDoctor::Error::ConfigurationFileMissing
|
|
51
|
+
rescue StandardError => e
|
|
52
|
+
raise ActiveRecordDoctor::Error::ConfigurationError[e]
|
|
53
|
+
end
|
|
54
|
+
raise ActiveRecordDoctor::Error::ConfigureNotCalled if current_config.nil?
|
|
55
|
+
|
|
56
|
+
# Store the configuration and reset @current_config. We cannot reset
|
|
57
|
+
# @current_config in .configure because that would prevent us from
|
|
58
|
+
# detecting multiple calls to that method.
|
|
59
|
+
config = @current_config
|
|
60
|
+
@current_config = nil
|
|
61
|
+
|
|
62
|
+
config
|
|
63
|
+
rescue ActiveRecordDoctor::Error => e
|
|
64
|
+
e.config_path = path
|
|
65
|
+
raise e
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
DEFAULT_CONFIG_PATH = File.join(__dir__, "default.rb").freeze
|
|
69
|
+
private_constant :DEFAULT_CONFIG_PATH
|
|
70
|
+
|
|
71
|
+
def load_config_with_defaults(path)
|
|
72
|
+
default_config = load_config(DEFAULT_CONFIG_PATH)
|
|
73
|
+
return default_config if path.nil?
|
|
74
|
+
|
|
75
|
+
config = load_config(path)
|
|
76
|
+
default_config.merge(config)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# A class used for loading user-provided configuration files.
|
|
81
|
+
class Loader
|
|
82
|
+
def initialize(config, recognized_globals, recognized_detectors, &block)
|
|
83
|
+
@config = config
|
|
84
|
+
@recognized_globals = recognized_globals
|
|
85
|
+
@recognized_detectors = recognized_detectors
|
|
86
|
+
instance_eval(&block)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def global(name, value)
|
|
90
|
+
name = name.to_sym
|
|
91
|
+
|
|
92
|
+
unless recognized_globals.include?(name)
|
|
93
|
+
raise ActiveRecordDoctor::Error::UnrecognizedGlobalSetting[
|
|
94
|
+
name,
|
|
95
|
+
recognized_globals
|
|
96
|
+
]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
if config.globals.include?(name)
|
|
100
|
+
raise ActiveRecordDoctor::Error::DuplicateGlobalSetting[name]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
config.globals[name] = value
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def detector(name, settings)
|
|
107
|
+
name = name.to_sym
|
|
108
|
+
|
|
109
|
+
recognized_settings = recognized_detectors[name]
|
|
110
|
+
if recognized_settings.nil?
|
|
111
|
+
raise ActiveRecordDoctor::Error::UnrecognizedDetectorName[
|
|
112
|
+
name,
|
|
113
|
+
recognized_detectors.keys
|
|
114
|
+
]
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
if config.detectors.include?(name)
|
|
118
|
+
raise ActiveRecordDoctor::Error::DetectorConfiguredTwice[name]
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
unrecognized_settings = settings.keys - recognized_settings
|
|
122
|
+
unless unrecognized_settings.empty?
|
|
123
|
+
raise ActiveRecordDoctor::Error::UnrecognizedDetectorSettings[
|
|
124
|
+
name,
|
|
125
|
+
unrecognized_settings,
|
|
126
|
+
recognized_settings
|
|
127
|
+
]
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
config.detectors[name] = settings
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
private
|
|
134
|
+
|
|
135
|
+
attr_reader :config, :recognized_globals, :recognized_detectors
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveRecordDoctor # :nodoc:
|
|
4
|
+
Config = Struct.new(:globals, :detectors) do
|
|
5
|
+
def merge(config)
|
|
6
|
+
globals = self.globals.merge(config.globals)
|
|
7
|
+
detectors = self.detectors.merge(config.detectors) do |_name, self_settings, config_settings|
|
|
8
|
+
self_settings.merge(config_settings)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
Config.new(globals, detectors)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|