active_record_doctor 1.8.0 → 1.9.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://github.com/gregnavis/active_record_doctor/actions/workflows/test.yml/badge.svg?branch=master)](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
|