active_record_doctor 1.7.1 → 1.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (114) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +287 -43
  3. data/lib/active_record_doctor/config/default.rb +59 -0
  4. data/lib/active_record_doctor/config/loader.rb +137 -0
  5. data/lib/active_record_doctor/config.rb +14 -0
  6. data/lib/active_record_doctor/detectors/base.rb +155 -0
  7. data/lib/active_record_doctor/detectors/extraneous_indexes.rb +114 -0
  8. data/lib/active_record_doctor/detectors/incorrect_boolean_presence_validation.rb +49 -0
  9. data/lib/active_record_doctor/detectors/incorrect_dependent_option.rb +107 -0
  10. data/lib/active_record_doctor/detectors/mismatched_foreign_key_type.rb +45 -0
  11. data/lib/active_record_doctor/detectors/missing_foreign_keys.rb +60 -0
  12. data/lib/active_record_doctor/detectors/missing_non_null_constraint.rb +72 -0
  13. data/lib/active_record_doctor/{tasks → detectors}/missing_presence_validation.rb +35 -21
  14. data/lib/active_record_doctor/detectors/missing_unique_indexes.rb +71 -0
  15. data/lib/active_record_doctor/detectors/short_primary_key_type.rb +41 -0
  16. data/lib/active_record_doctor/detectors/undefined_table_references.rb +33 -0
  17. data/lib/active_record_doctor/detectors/unindexed_deleted_at.rb +55 -0
  18. data/lib/active_record_doctor/detectors/unindexed_foreign_keys.rb +59 -0
  19. data/lib/active_record_doctor/detectors.rb +21 -0
  20. data/lib/active_record_doctor/errors.rb +226 -0
  21. data/lib/active_record_doctor/help.rb +39 -0
  22. data/lib/active_record_doctor/printers.rb +3 -1
  23. data/lib/active_record_doctor/railtie.rb +2 -0
  24. data/lib/active_record_doctor/rake/task.rb +78 -0
  25. data/lib/active_record_doctor/runner.rb +41 -0
  26. data/lib/active_record_doctor/version.rb +3 -1
  27. data/lib/active_record_doctor.rb +24 -2
  28. data/lib/generators/active_record_doctor/add_indexes/add_indexes_generator.rb +46 -29
  29. data/lib/tasks/active_record_doctor.rake +21 -29
  30. data/test/active_record_doctor/config/loader_test.rb +120 -0
  31. data/test/active_record_doctor/config_test.rb +116 -0
  32. data/test/active_record_doctor/detectors/extraneous_indexes_test.rb +190 -0
  33. data/test/active_record_doctor/detectors/incorrect_boolean_presence_validation_test.rb +79 -0
  34. data/test/active_record_doctor/detectors/incorrect_dependent_option_test.rb +295 -0
  35. data/test/active_record_doctor/detectors/mismatched_foreign_key_type_test.rb +82 -0
  36. data/test/active_record_doctor/detectors/missing_foreign_keys_test.rb +70 -0
  37. data/test/active_record_doctor/detectors/missing_non_null_constraint_test.rb +216 -0
  38. data/test/active_record_doctor/detectors/missing_presence_validation_test.rb +168 -0
  39. data/test/active_record_doctor/detectors/missing_unique_indexes_test.rb +163 -0
  40. data/test/active_record_doctor/detectors/short_primary_key_type_test.rb +64 -0
  41. data/test/active_record_doctor/detectors/undefined_table_references_test.rb +57 -0
  42. data/test/active_record_doctor/detectors/unindexed_deleted_at_test.rb +171 -0
  43. data/test/active_record_doctor/detectors/unindexed_foreign_keys_test.rb +78 -0
  44. data/test/active_record_doctor/runner_test.rb +42 -0
  45. data/test/generators/active_record_doctor/add_indexes/add_indexes_generator_test.rb +131 -0
  46. data/test/model_factory.rb +128 -0
  47. data/test/setup.rb +116 -0
  48. metadata +103 -154
  49. data/Rakefile +0 -28
  50. data/lib/active_record_doctor/printers/io_printer.rb +0 -105
  51. data/lib/active_record_doctor/tasks/base.rb +0 -78
  52. data/lib/active_record_doctor/tasks/extraneous_indexes.rb +0 -82
  53. data/lib/active_record_doctor/tasks/incorrect_boolean_presence_validation.rb +0 -33
  54. data/lib/active_record_doctor/tasks/missing_foreign_keys.rb +0 -46
  55. data/lib/active_record_doctor/tasks/missing_non_null_constraint.rb +0 -52
  56. data/lib/active_record_doctor/tasks/missing_unique_indexes.rb +0 -56
  57. data/lib/active_record_doctor/tasks/undefined_table_references.rb +0 -33
  58. data/lib/active_record_doctor/tasks/unindexed_deleted_at.rb +0 -19
  59. data/lib/active_record_doctor/tasks/unindexed_foreign_keys.rb +0 -43
  60. data/lib/active_record_doctor/tasks.rb +0 -7
  61. data/test/active_record_doctor/printers/io_printer_test.rb +0 -20
  62. data/test/active_record_doctor/tasks/extraneous_indexes_test.rb +0 -81
  63. data/test/active_record_doctor/tasks/incorrect_boolean_presence_validation_test.rb +0 -33
  64. data/test/active_record_doctor/tasks/missing_foreign_keys_test.rb +0 -27
  65. data/test/active_record_doctor/tasks/missing_non_null_constraint_test.rb +0 -108
  66. data/test/active_record_doctor/tasks/missing_presence_validation_test.rb +0 -110
  67. data/test/active_record_doctor/tasks/missing_unique_indexes_test.rb +0 -95
  68. data/test/active_record_doctor/tasks/undefined_table_references_test.rb +0 -51
  69. data/test/active_record_doctor/tasks/unindexed_deleted_at_test.rb +0 -34
  70. data/test/active_record_doctor/tasks/unindexed_foreign_keys_test.rb +0 -27
  71. data/test/dummy/README.rdoc +0 -28
  72. data/test/dummy/Rakefile +0 -6
  73. data/test/dummy/app/assets/config/manifest.js +0 -1
  74. data/test/dummy/app/assets/javascripts/application.js +0 -13
  75. data/test/dummy/app/assets/stylesheets/application.css +0 -15
  76. data/test/dummy/app/controllers/application_controller.rb +0 -5
  77. data/test/dummy/app/helpers/application_helper.rb +0 -2
  78. data/test/dummy/app/models/application_record.rb +0 -3
  79. data/test/dummy/app/views/layouts/application.html.erb +0 -14
  80. data/test/dummy/bin/bundle +0 -3
  81. data/test/dummy/bin/rails +0 -4
  82. data/test/dummy/bin/rake +0 -4
  83. data/test/dummy/bin/setup +0 -29
  84. data/test/dummy/config/application.rb +0 -23
  85. data/test/dummy/config/boot.rb +0 -5
  86. data/test/dummy/config/database.yml +0 -19
  87. data/test/dummy/config/database.yml.travis +0 -5
  88. data/test/dummy/config/environment.rb +0 -5
  89. data/test/dummy/config/environments/development.rb +0 -41
  90. data/test/dummy/config/environments/production.rb +0 -79
  91. data/test/dummy/config/environments/test.rb +0 -47
  92. data/test/dummy/config/initializers/assets.rb +0 -11
  93. data/test/dummy/config/initializers/backtrace_silencers.rb +0 -7
  94. data/test/dummy/config/initializers/cookies_serializer.rb +0 -3
  95. data/test/dummy/config/initializers/filter_parameter_logging.rb +0 -4
  96. data/test/dummy/config/initializers/inflections.rb +0 -16
  97. data/test/dummy/config/initializers/mime_types.rb +0 -4
  98. data/test/dummy/config/initializers/session_store.rb +0 -3
  99. data/test/dummy/config/initializers/wrap_parameters.rb +0 -14
  100. data/test/dummy/config/locales/en.yml +0 -23
  101. data/test/dummy/config/routes.rb +0 -56
  102. data/test/dummy/config/secrets.yml +0 -22
  103. data/test/dummy/config.ru +0 -4
  104. data/test/dummy/db/migrate/base_migration.rb +0 -5
  105. data/test/dummy/db/schema.rb +0 -19
  106. data/test/dummy/log/development.log +0 -111
  107. data/test/dummy/log/test.log +0 -79424
  108. data/test/dummy/public/404.html +0 -67
  109. data/test/dummy/public/422.html +0 -67
  110. data/test/dummy/public/500.html +0 -66
  111. data/test/dummy/public/favicon.ico +0 -0
  112. data/test/support/assertions.rb +0 -11
  113. data/test/support/temping.rb +0 -25
  114. data/test/test_helper.rb +0 -17
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8d5091f9668515416b73453756daa5f78598f6d35f0f60073a947719e535110f
4
- data.tar.gz: 428450c43db5adb8510debfb9e895efba1f21649355255fe3eb7a2ea16c82642
3
+ metadata.gz: e69330f83b83c3adcd1397554bc916ee163acf3797b8be5b8f96f1960e2d8a3e
4
+ data.tar.gz: 8f6e09d3af78bb9aaf01374437653b25070c2c328a0ffeaedc800378a10074f6
5
5
  SHA512:
6
- metadata.gz: fffc1ffd284a2448ac5c74dae34a3bbe45eda4836b0e22a0af072ab6353f16b1386385254b1cdb2b87e4776372869d8cee67bf0a066f180b85bd45ef7ed3cc33
7
- data.tar.gz: 0011f60f1e37542911a72cce23cc8c8430310f8263fe7ba73aba30230ec8ff3f6e7ee9df72f42d8db6ff54e732a86aa49f1e6489a5b44d51bf1bd6c68821ebcd
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
- * index unindexed foreign keys
7
- * detect extraneous indexes
8
- * detect unindexed `deleted_at` columns
9
- * detect missing foreign key constraints
10
- * detect models referencing undefined tables
11
- * detect uniqueness validations not backed by an unique index
12
- * detect missing non-`NULL` constraints
13
- * detect missing presence validations
14
- * detect incorrect presence validations on boolean columns
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
- More features coming soon!
18
+ It can also:
17
19
 
18
- Want to suggest a feature? Just shoot me [an email](mailto:contact@gregnavis.com).
20
+ * index unindexed foreign keys - [`active_record_doctor:unindexed_foreign_keys`](#indexing-unindexed-foreign-keys)
19
21
 
20
- [<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)
21
23
 
22
24
  ## Installation
23
25
 
24
- The preferred installation method is adding `active_record_doctor` to your
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
- Then run:
33
+ and run `bundle install`. If you'd like to use the most recent development
34
+ version then use this instead:
32
35
 
33
- ```bash
34
- bundle install
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
- The output will look like:
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
- The following models reference undefined tables:
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
- The following indexes should be created to back model-level uniqueness validations:
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
- The following columns should be marked as `null: false`:
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
- The following models and columns should have presence validations:
255
- User: email, name
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
- The presence of the following boolean columns is validated incorrectly:
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