active_record_doctor 1.7.1 → 1.9.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 +287 -43
- 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 +155 -0
- data/lib/active_record_doctor/detectors/extraneous_indexes.rb +114 -0
- data/lib/active_record_doctor/detectors/incorrect_boolean_presence_validation.rb +49 -0
- data/lib/active_record_doctor/detectors/incorrect_dependent_option.rb +107 -0
- data/lib/active_record_doctor/detectors/mismatched_foreign_key_type.rb +45 -0
- data/lib/active_record_doctor/detectors/missing_foreign_keys.rb +60 -0
- data/lib/active_record_doctor/detectors/missing_non_null_constraint.rb +72 -0
- data/lib/active_record_doctor/{tasks → detectors}/missing_presence_validation.rb +35 -21
- data/lib/active_record_doctor/detectors/missing_unique_indexes.rb +71 -0
- data/lib/active_record_doctor/detectors/short_primary_key_type.rb +41 -0
- data/lib/active_record_doctor/detectors/undefined_table_references.rb +33 -0
- data/lib/active_record_doctor/detectors/unindexed_deleted_at.rb +55 -0
- data/lib/active_record_doctor/detectors/unindexed_foreign_keys.rb +59 -0
- data/lib/active_record_doctor/detectors.rb +21 -0
- data/lib/active_record_doctor/errors.rb +226 -0
- data/lib/active_record_doctor/help.rb +39 -0
- data/lib/active_record_doctor/printers.rb +3 -1
- data/lib/active_record_doctor/railtie.rb +2 -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 +3 -1
- data/lib/active_record_doctor.rb +24 -2
- data/lib/generators/active_record_doctor/add_indexes/add_indexes_generator.rb +46 -29
- data/lib/tasks/active_record_doctor.rake +21 -29
- 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 +190 -0
- data/test/active_record_doctor/detectors/incorrect_boolean_presence_validation_test.rb +79 -0
- data/test/active_record_doctor/detectors/incorrect_dependent_option_test.rb +295 -0
- 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 +70 -0
- data/test/active_record_doctor/detectors/missing_non_null_constraint_test.rb +216 -0
- data/test/active_record_doctor/detectors/missing_presence_validation_test.rb +168 -0
- data/test/active_record_doctor/detectors/missing_unique_indexes_test.rb +163 -0
- 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 +57 -0
- data/test/active_record_doctor/detectors/unindexed_deleted_at_test.rb +171 -0
- data/test/active_record_doctor/detectors/unindexed_foreign_keys_test.rb +78 -0
- 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 +128 -0
- data/test/setup.rb +116 -0
- metadata +103 -154
- data/Rakefile +0 -28
- data/lib/active_record_doctor/printers/io_printer.rb +0 -105
- data/lib/active_record_doctor/tasks/base.rb +0 -78
- data/lib/active_record_doctor/tasks/extraneous_indexes.rb +0 -82
- data/lib/active_record_doctor/tasks/incorrect_boolean_presence_validation.rb +0 -33
- data/lib/active_record_doctor/tasks/missing_foreign_keys.rb +0 -46
- data/lib/active_record_doctor/tasks/missing_non_null_constraint.rb +0 -52
- data/lib/active_record_doctor/tasks/missing_unique_indexes.rb +0 -56
- data/lib/active_record_doctor/tasks/undefined_table_references.rb +0 -33
- data/lib/active_record_doctor/tasks/unindexed_deleted_at.rb +0 -19
- data/lib/active_record_doctor/tasks/unindexed_foreign_keys.rb +0 -43
- data/lib/active_record_doctor/tasks.rb +0 -7
- data/test/active_record_doctor/printers/io_printer_test.rb +0 -20
- data/test/active_record_doctor/tasks/extraneous_indexes_test.rb +0 -81
- data/test/active_record_doctor/tasks/incorrect_boolean_presence_validation_test.rb +0 -33
- data/test/active_record_doctor/tasks/missing_foreign_keys_test.rb +0 -27
- data/test/active_record_doctor/tasks/missing_non_null_constraint_test.rb +0 -108
- data/test/active_record_doctor/tasks/missing_presence_validation_test.rb +0 -110
- data/test/active_record_doctor/tasks/missing_unique_indexes_test.rb +0 -95
- data/test/active_record_doctor/tasks/undefined_table_references_test.rb +0 -51
- data/test/active_record_doctor/tasks/unindexed_deleted_at_test.rb +0 -34
- data/test/active_record_doctor/tasks/unindexed_foreign_keys_test.rb +0 -27
- data/test/dummy/README.rdoc +0 -28
- data/test/dummy/Rakefile +0 -6
- data/test/dummy/app/assets/config/manifest.js +0 -1
- data/test/dummy/app/assets/javascripts/application.js +0 -13
- data/test/dummy/app/assets/stylesheets/application.css +0 -15
- data/test/dummy/app/controllers/application_controller.rb +0 -5
- data/test/dummy/app/helpers/application_helper.rb +0 -2
- data/test/dummy/app/models/application_record.rb +0 -3
- data/test/dummy/app/views/layouts/application.html.erb +0 -14
- data/test/dummy/bin/bundle +0 -3
- data/test/dummy/bin/rails +0 -4
- data/test/dummy/bin/rake +0 -4
- data/test/dummy/bin/setup +0 -29
- data/test/dummy/config/application.rb +0 -23
- data/test/dummy/config/boot.rb +0 -5
- data/test/dummy/config/database.yml +0 -19
- data/test/dummy/config/database.yml.travis +0 -5
- data/test/dummy/config/environment.rb +0 -5
- data/test/dummy/config/environments/development.rb +0 -41
- data/test/dummy/config/environments/production.rb +0 -79
- data/test/dummy/config/environments/test.rb +0 -47
- data/test/dummy/config/initializers/assets.rb +0 -11
- data/test/dummy/config/initializers/backtrace_silencers.rb +0 -7
- data/test/dummy/config/initializers/cookies_serializer.rb +0 -3
- data/test/dummy/config/initializers/filter_parameter_logging.rb +0 -4
- data/test/dummy/config/initializers/inflections.rb +0 -16
- data/test/dummy/config/initializers/mime_types.rb +0 -4
- data/test/dummy/config/initializers/session_store.rb +0 -3
- data/test/dummy/config/initializers/wrap_parameters.rb +0 -14
- data/test/dummy/config/locales/en.yml +0 -23
- data/test/dummy/config/routes.rb +0 -56
- data/test/dummy/config/secrets.yml +0 -22
- data/test/dummy/config.ru +0 -4
- data/test/dummy/db/migrate/base_migration.rb +0 -5
- data/test/dummy/db/schema.rb +0 -19
- data/test/dummy/log/development.log +0 -111
- data/test/dummy/log/test.log +0 -79424
- data/test/dummy/public/404.html +0 -67
- data/test/dummy/public/422.html +0 -67
- data/test/dummy/public/500.html +0 -66
- data/test/dummy/public/favicon.ico +0 -0
- data/test/support/assertions.rb +0 -11
- data/test/support/temping.rb +0 -25
- data/test/test_helper.rb +0 -17
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e69330f83b83c3adcd1397554bc916ee163acf3797b8be5b8f96f1960e2d8a3e
|
|
4
|
+
data.tar.gz: 8f6e09d3af78bb9aaf01374437653b25070c2c328a0ffeaedc800378a10074f6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4b9d75665c0524cd5ad18c5aa21aaf59b3e7a75b63c88fb332cd3f600de4f2a9fa073b985f5155b81d54f177b25da5fba5dde494fa6eed28e16086a955082012
|
|
7
|
+
data.tar.gz: 94c14b73d06c92803b196fde5ca0572c9545b2ef8b89bc91e88755c5aed9c4e51087ab59a53a94f1fbcacf3f7a58a25cac38dfb4f81d7096ee4d03c7796632b4
|
data/README.md
CHANGED
|
@@ -1,41 +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
5
|
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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)
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
It can also:
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
* index unindexed foreign keys - [`active_record_doctor:unindexed_foreign_keys`](#indexing-unindexed-foreign-keys)
|
|
19
21
|
|
|
20
|
-
[
|
|
22
|
+
[](https://github.com/gregnavis/active_record_doctor/actions/workflows/test.yml)
|
|
21
23
|
|
|
22
24
|
## Installation
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
`Gemfile`:
|
|
26
|
+
In order to use the latest production release, please add the following to
|
|
27
|
+
your `Gemfile`:
|
|
26
28
|
|
|
27
29
|
```ruby
|
|
28
30
|
gem 'active_record_doctor', group: :development
|
|
29
31
|
```
|
|
30
32
|
|
|
31
|
-
|
|
33
|
+
and run `bundle install`. If you'd like to use the most recent development
|
|
34
|
+
version then use this instead:
|
|
32
35
|
|
|
33
|
-
```
|
|
34
|
-
|
|
36
|
+
```ruby
|
|
37
|
+
gem 'active_record_doctor', github: 'gregnavis/active_record_doctor'
|
|
35
38
|
```
|
|
36
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
|
|
63
|
+
```
|
|
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
|
+
|
|
37
69
|
## Usage
|
|
38
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
|
+
|
|
39
145
|
### Indexing Unindexed Foreign Keys
|
|
40
146
|
|
|
41
147
|
Foreign keys should be indexed unless it's proven ineffective. However, Rails
|
|
@@ -65,6 +171,11 @@ three-step process:
|
|
|
65
171
|
bundle exec rake db:migrate
|
|
66
172
|
```
|
|
67
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
|
+
|
|
68
179
|
### Removing Extraneous Indexes
|
|
69
180
|
|
|
70
181
|
Let me illustrate with an example. Consider a `users` table with columns
|
|
@@ -106,6 +217,11 @@ example, if there's a unique index on `users.login` and a non-unique index on
|
|
|
106
217
|
`users.login, users.domain` then the tool will _not_ suggest dropping
|
|
107
218
|
`users.login` as it could violate the uniqueness assumption.
|
|
108
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
|
+
|
|
109
225
|
### Detecting Unindexed `deleted_at` Columns
|
|
110
226
|
|
|
111
227
|
If you soft-delete some models (e.g. with `paranoia`) then you need to modify
|
|
@@ -124,6 +240,13 @@ This will print a list of indexes that don't have the `deleted_at IS NULL`
|
|
|
124
240
|
clause. Currently, `active_record_doctor` cannot automatically generate
|
|
125
241
|
appropriate migrations. You need to do that manually.
|
|
126
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
|
+
|
|
127
250
|
### Detecting Missing Foreign Key Constraints
|
|
128
251
|
|
|
129
252
|
If `users.profile_id` references a row in `profiles` then this can be expressed
|
|
@@ -141,20 +264,8 @@ keys with the following command:
|
|
|
141
264
|
bundle exec rake active_record_doctor:missing_foreign_keys
|
|
142
265
|
```
|
|
143
266
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
```
|
|
147
|
-
users profile_id
|
|
148
|
-
comments user_id article_id
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
Tables are listed one per line. Each line starts with a table name followed by
|
|
152
|
-
column names that should have a foreign key constraint. In the example above,
|
|
153
|
-
`users.profile_id`, `comments.user_id`, and `comments.article_id` lack a foreign
|
|
154
|
-
key constraint.
|
|
155
|
-
|
|
156
|
-
In order to add a foreign key constraint to `users.profile_id` use the following
|
|
157
|
-
migration:
|
|
267
|
+
In order to add a foreign key constraint to `users.profile_id` use a migration
|
|
268
|
+
like:
|
|
158
269
|
|
|
159
270
|
```ruby
|
|
160
271
|
class AddForeignKeyConstraintToUsersProfileId < ActiveRecord::Migration
|
|
@@ -164,6 +275,11 @@ class AddForeignKeyConstraintToUsersProfileId < ActiveRecord::Migration
|
|
|
164
275
|
end
|
|
165
276
|
```
|
|
166
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
|
+
|
|
167
283
|
### Detecting Models Referencing Undefined Tables
|
|
168
284
|
|
|
169
285
|
Active Record guesses the table name based on the class name. There are a few
|
|
@@ -186,13 +302,16 @@ If there a model references an undefined table then you'll see a message like
|
|
|
186
302
|
this:
|
|
187
303
|
|
|
188
304
|
```
|
|
189
|
-
|
|
190
|
-
Contract (the table contract_records is undefined)
|
|
305
|
+
Contract references a non-existent table or view named contract_records
|
|
191
306
|
```
|
|
192
307
|
|
|
193
308
|
On top of that `rake` will exit with status code of 1. This allows you to use
|
|
194
309
|
this check as part of your Continuous Integration pipeline.
|
|
195
310
|
|
|
311
|
+
Supported configuration options:
|
|
312
|
+
|
|
313
|
+
- `ignore_models` - models whose underlying tables should not be checked for existence.
|
|
314
|
+
|
|
196
315
|
### Detecting Uniqueness Validations not Backed by an Index
|
|
197
316
|
|
|
198
317
|
A model-level uniqueness validations should be backed by a database index in
|
|
@@ -208,12 +327,16 @@ bundle exec rake active_record_doctor:missing_unique_indexes
|
|
|
208
327
|
If there are such indexes then the command will print:
|
|
209
328
|
|
|
210
329
|
```
|
|
211
|
-
|
|
212
|
-
users: email
|
|
330
|
+
add a unique index on users(email) - validating uniqueness in the model without an index can lead to duplicates
|
|
213
331
|
```
|
|
214
332
|
|
|
215
333
|
This means that you should create a unique index on `users.email`.
|
|
216
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
|
+
|
|
217
340
|
### Detecting Missing Non-`NULL` Constraints
|
|
218
341
|
|
|
219
342
|
If there's an unconditional presence validation on a column then it should be
|
|
@@ -229,14 +352,19 @@ bundle exec rake active_record_doctor:missing_non_null_constraint
|
|
|
229
352
|
The output of the command is similar to:
|
|
230
353
|
|
|
231
354
|
```
|
|
232
|
-
|
|
233
|
-
users: name
|
|
234
|
-
|
|
355
|
+
add `NOT NULL` to users.name - models validates its presence but it's not non-NULL in the database
|
|
235
356
|
```
|
|
236
357
|
|
|
237
358
|
You can mark the columns mentioned in the output as `null: false` by creating a
|
|
238
359
|
migration and calling `change_column_null`.
|
|
239
360
|
|
|
361
|
+
This validator skips models whose corresponding database tables don't exist.
|
|
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
|
+
|
|
240
368
|
### Detecting Missing Presence Validations
|
|
241
369
|
|
|
242
370
|
If a column is marked as `null: false` then it's likely it should have the
|
|
@@ -251,12 +379,19 @@ bundle exec rake active_record_doctor:missing_presence_validation
|
|
|
251
379
|
The output of the command looks like this:
|
|
252
380
|
|
|
253
381
|
```
|
|
254
|
-
|
|
255
|
-
|
|
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
|
|
256
384
|
```
|
|
257
385
|
|
|
258
386
|
This means `User` should have a presence validator on `email` and `name`.
|
|
259
387
|
|
|
388
|
+
This validator skips models whose corresponding database tables don't exist.
|
|
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
|
+
|
|
260
395
|
### Detecting Incorrect Presence Validations on Boolean Columns
|
|
261
396
|
|
|
262
397
|
A boolean column's presence should be validated using inclusion or exclusion
|
|
@@ -271,13 +406,122 @@ bundle exec rake active_record_doctor:incorrect_boolean_presence_validation
|
|
|
271
406
|
The output of the command looks like this:
|
|
272
407
|
|
|
273
408
|
```
|
|
274
|
-
|
|
275
|
-
User: active
|
|
409
|
+
replace the `presence` validator on User.active with `inclusion` - `presence` can't be used on booleans
|
|
276
410
|
```
|
|
277
411
|
|
|
278
412
|
This means `active` is validated with `presence: true` instead of
|
|
279
413
|
`inclusion: { in: [true, false] }` or `exclusion: { in: [nil] }`.
|
|
280
414
|
|
|
415
|
+
This validator skips models whose corresponding database tables don't exist.
|
|
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
|
+
|
|
422
|
+
### Detecting Incorrect `dependent` Option on Associations
|
|
423
|
+
|
|
424
|
+
Cascading model deletions can be sped up with `dependent: :delete_all` (to
|
|
425
|
+
delete all dependent models with one SQL query) but only if the deleted models
|
|
426
|
+
have no callbacks as they're skipped.
|
|
427
|
+
|
|
428
|
+
This can lead to two types of errors:
|
|
429
|
+
|
|
430
|
+
- Using `delete_all` when dependent models define callbacks - they will NOT be
|
|
431
|
+
invoked.
|
|
432
|
+
- Using `destroy` when dependent models define no callbacks - dependent models
|
|
433
|
+
will be loaded one-by-one with no reason
|
|
434
|
+
|
|
435
|
+
In order to detect associations affected by the two aforementioned problems run
|
|
436
|
+
the following command:
|
|
437
|
+
|
|
438
|
+
```
|
|
439
|
+
bundle exec rake active_record_doctor:incorrect_dependent_option
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
The output of the command looks like this:
|
|
443
|
+
|
|
444
|
+
```
|
|
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
|
|
447
|
+
```
|
|
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
|
+
|
|
514
|
+
## Ruby and Rails Compatibility Policy
|
|
515
|
+
|
|
516
|
+
The goal of the policy is to ensure proper functioning in reasonable
|
|
517
|
+
combinations of Ruby and Rails versions. Specifically:
|
|
518
|
+
|
|
519
|
+
1. If a Rails version is officially supported by the Rails Core Team then it's
|
|
520
|
+
supported by `active_record_doctor`.
|
|
521
|
+
2. If a Ruby version is compatible with a supported Rails version then it's
|
|
522
|
+
also supported by `active_record_doctor`.
|
|
523
|
+
3. Only most recent teeny Ruby versions and patch Rails versions are supported.
|
|
524
|
+
|
|
281
525
|
## Author
|
|
282
526
|
|
|
283
527
|
This gem is developed and maintained by [Greg Navis](http://www.gregnavis.com).
|
|
@@ -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
|